Initial import
This commit is contained in:
847
src/views/play/LayoutPlaygroundView.css
Executable file
847
src/views/play/LayoutPlaygroundView.css
Executable file
@@ -0,0 +1,847 @@
|
||||
.layout-playground__hero {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 24px;
|
||||
border: 1px solid rgba(8, 145, 178, 0.14);
|
||||
border-radius: 24px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(8, 145, 178, 0.12), rgba(59, 130, 246, 0.08)),
|
||||
rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.layout-playground__title.ant-typography {
|
||||
margin-top: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.layout-playground__meta {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.layout-playground__meta span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
color: #155e75;
|
||||
}
|
||||
|
||||
.layout-playground__controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.layout-playground__control-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
.layout-playground__control-card .ant-input-number,
|
||||
.layout-playground__control-card .ant-radio-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.layout-playground__control-card .ant-radio-button-wrapper {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.layout-playground__preview-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.layout-playground__actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 18px 20px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
border-radius: 22px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.layout-playground__storage-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
padding: 20px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
border-radius: 22px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.96)),
|
||||
rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.layout-playground__storage-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.layout-playground__storage-copy.ant-typography {
|
||||
margin-top: 6px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.layout-playground__storage-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.layout-playground__bottom-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
padding: 20px 22px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
border-radius: 24px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(241, 245, 249, 0.92)),
|
||||
rgba(255, 255, 255, 0.94);
|
||||
}
|
||||
|
||||
.layout-playground__bottom-menu-copy {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.layout-playground__bottom-menu-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.layout-playground__saved-item {
|
||||
padding-inline: 14px !important;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 18px;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
background-color 0.18s ease,
|
||||
box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.layout-playground__saved-item:hover {
|
||||
background: rgba(248, 250, 252, 0.86);
|
||||
border-color: rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
|
||||
.layout-playground__saved-item--active {
|
||||
border-color: rgba(59, 130, 246, 0.28);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(239, 246, 255, 0.92), rgba(255, 255, 255, 0.98)),
|
||||
#fff;
|
||||
box-shadow: 0 12px 24px rgba(37, 99, 235, 0.08);
|
||||
}
|
||||
|
||||
.layout-playground__saved-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.layout-playground__preview-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.layout-playground__fullscreen-shell {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: calc(100dvh - 92px);
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layout-playground__fullscreen-shell--embedded {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.layout-playground__fullscreen-shell--saved-fit {
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layout-playground__preview-frame {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 460px;
|
||||
width: 100%;
|
||||
padding: 18px;
|
||||
border: 1px solid rgba(14, 116, 144, 0.14);
|
||||
border-radius: 28px;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(14, 165, 233, 0.12), transparent 28%),
|
||||
linear-gradient(180deg, rgba(240, 249, 255, 0.96), rgba(255, 255, 255, 0.98));
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.layout-playground__preview-frame--fullscreen {
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.layout-playground__preview-frame--saved-detail {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layout-playground__preview-frame--gallery {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 10px;
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
.layout-playground__saved-browser {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layout-playground__saved-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.layout-playground__saved-fit-viewport {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layout-playground__saved-fit-stage {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
flex: 0 0 auto;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layout-playground__saved-detail--fit {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
transform-origin: top left;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.layout-playground__saved-detail--fit,
|
||||
.layout-playground__saved-detail--fit > * {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.layout-playground__saved-detail-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
.layout-playground__saved-browser-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.layout-playground__saved-browser-layout {
|
||||
min-height: calc(100vh - 220px);
|
||||
}
|
||||
|
||||
.layout-playground__saved-browser-preview {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.layout-playground__saved-gallery {
|
||||
--layout-gallery-columns: 1;
|
||||
--layout-gallery-rows: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--layout-gallery-columns), minmax(0, 1fr));
|
||||
grid-template-rows: repeat(var(--layout-gallery-rows), minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.layout-playground__saved-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
flex: 1;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
border-radius: 20px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(239, 246, 255, 0.94)),
|
||||
rgba(255, 255, 255, 0.94);
|
||||
overflow: hidden;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.layout-playground__saved-card:hover {
|
||||
border-color: rgba(59, 130, 246, 0.22);
|
||||
box-shadow: 0 14px 28px rgba(37, 99, 235, 0.08);
|
||||
}
|
||||
|
||||
.layout-playground__saved-card--active {
|
||||
border-color: rgba(59, 130, 246, 0.36);
|
||||
box-shadow: 0 16px 30px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.layout-playground__saved-card-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.layout-playground__empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
min-height: 420px;
|
||||
padding: 28px;
|
||||
border: 1px dashed rgba(14, 116, 144, 0.28);
|
||||
border-radius: 24px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(125, 211, 252, 0.28), transparent 30%),
|
||||
rgba(255, 255, 255, 0.76);
|
||||
}
|
||||
|
||||
.layout-playground__empty-state--fullscreen {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.layout-playground__saved-browser-loading {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.layout-playground__saved-browser-loading .ant-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.layout-playground__empty-state .ant-typography {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.layout-playground__editor-card.ant-card,
|
||||
.layout-playground__editor-card.ant-card .ant-card-body,
|
||||
.layout-playground__editor-stack.ant-space,
|
||||
.layout-playground__editor-stack.ant-space .ant-space-item {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.layout-playground__editor-card.ant-card {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.layout-playground__editor-card.ant-card .ant-card-body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.layout-playground__editor-stack.ant-space {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.layout-playground__splitter {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 420px;
|
||||
min-width: 0;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
background: rgba(148, 163, 184, 0.08);
|
||||
}
|
||||
|
||||
.layout-playground__splitter--preview {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.layout-playground__splitter-frame {
|
||||
height: 100%;
|
||||
min-height: inherit;
|
||||
}
|
||||
|
||||
.layout-playground__splitter-frame .ant-splitter-panel {
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layout-playground__splitter--collapsed {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-height: inherit;
|
||||
}
|
||||
|
||||
.layout-playground__splitter--collapsed.layout-playground__splitter--preview {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.layout-playground__splitter--collapsed-horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.layout-playground__splitter--collapsed-vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.layout-playground__splitter--collapsed > :first-child {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.layout-playground__splitter-toggle-dock {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.layout-playground__splitter-toggle-dock .ant-btn {
|
||||
pointer-events: auto;
|
||||
box-shadow: 0 12px 26px rgba(15, 23, 42, 0.16);
|
||||
}
|
||||
|
||||
.layout-playground__splitter-toggle-dock--horizontal {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
flex-direction: column;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.layout-playground__splitter-toggle-dock--vertical {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.layout-playground__splitter--depth-2 {
|
||||
background: rgba(186, 230, 253, 0.2);
|
||||
}
|
||||
|
||||
.layout-playground__splitter--depth-3 {
|
||||
background: rgba(224, 231, 255, 0.24);
|
||||
}
|
||||
|
||||
.layout-playground__splitter--depth-4 {
|
||||
background: rgba(254, 240, 138, 0.18);
|
||||
}
|
||||
|
||||
.layout-playground__pane {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
background-color 0.18s ease;
|
||||
}
|
||||
|
||||
.layout-playground__pane:hover {
|
||||
background: rgba(255, 255, 255, 0.48);
|
||||
}
|
||||
|
||||
.layout-playground__pane--readonly {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.layout-playground__pane--readonly:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.layout-playground__pane--preview {
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.layout-playground__pane--surface {
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.layout-playground__pane:focus-visible {
|
||||
outline: 3px solid rgba(37, 99, 235, 0.24);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.layout-playground__pane-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-height: 32px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.layout-playground__pane-toolbar--overlay {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
left: 12px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.layout-playground__pane-toolbar--compact {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.layout-playground__pane--selected .layout-playground__pane-toolbar {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
left: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.layout-playground__pane--primary {
|
||||
background: rgba(207, 250, 254, 0.42);
|
||||
}
|
||||
|
||||
.layout-playground__pane--secondary {
|
||||
background: rgba(224, 231, 255, 0.4);
|
||||
}
|
||||
|
||||
.layout-playground__pane--accent {
|
||||
background: rgba(254, 249, 195, 0.42);
|
||||
}
|
||||
|
||||
.layout-playground__pane--neutral {
|
||||
background: rgba(226, 232, 240, 0.48);
|
||||
}
|
||||
|
||||
.layout-playground__pane--surface.layout-playground__pane--primary,
|
||||
.layout-playground__pane--surface.layout-playground__pane--secondary,
|
||||
.layout-playground__pane--surface.layout-playground__pane--accent,
|
||||
.layout-playground__pane--surface.layout-playground__pane--neutral,
|
||||
.layout-playground__pane--hidden {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.layout-playground__pane-label {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.layout-playground__pane-placeholder {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 40px 0 4px;
|
||||
}
|
||||
|
||||
.layout-playground__pane--preview .layout-playground__pane-placeholder {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.layout-playground__pane--surface .layout-playground__pane-placeholder {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.layout-playground__pane-component-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||
background: rgba(255, 255, 255, 0.84);
|
||||
}
|
||||
|
||||
.layout-playground__pane-component-body > * {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Saved-layout thumbnails must clamp nested sample scroll areas to the scaled preview box. */
|
||||
.layout-playground__saved-detail--fit .layout-playground__preview-frame,
|
||||
.layout-playground__saved-detail--fit .layout-playground__splitter,
|
||||
.layout-playground__saved-detail--fit .layout-playground__splitter-frame,
|
||||
.layout-playground__saved-detail--fit .layout-playground__splitter-frame .ant-splitter-panel,
|
||||
.layout-playground__saved-detail--fit .layout-playground__pane,
|
||||
.layout-playground__saved-detail--fit .layout-playground__pane-component-body,
|
||||
.layout-playground__saved-detail--fit .layout-playground__pane-component-body > *,
|
||||
.layout-playground__saved-detail--fit .ant-card,
|
||||
.layout-playground__saved-detail--fit .ant-card-body,
|
||||
.layout-playground__saved-detail--fit .ant-list,
|
||||
.layout-playground__saved-detail--fit .ant-list-items,
|
||||
.layout-playground__saved-detail--fit .ant-space,
|
||||
.layout-playground__saved-detail--fit .ant-space-item {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layout-playground__saved-detail--fit .layout-playground__saved-detail-bar {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.layout-playground__saved-detail--fit .layout-playground__preview-frame--saved-detail {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.layout-playground__saved-detail--fit .layout-playground__splitter,
|
||||
.layout-playground__saved-detail--fit .layout-playground__splitter-frame,
|
||||
.layout-playground__saved-detail--fit .layout-playground__splitter-frame .ant-splitter-panel,
|
||||
.layout-playground__saved-detail--fit .layout-playground__pane,
|
||||
.layout-playground__saved-detail--fit .layout-playground__pane-component-body,
|
||||
.layout-playground__saved-detail--fit .layout-playground__pane-component-body > *,
|
||||
.layout-playground__saved-detail--fit .ant-card,
|
||||
.layout-playground__saved-detail--fit .ant-card-body,
|
||||
.layout-playground__saved-detail--fit .ant-space,
|
||||
.layout-playground__saved-detail--fit .ant-space-item {
|
||||
min-height: 0 !important;
|
||||
min-width: 0;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.layout-playground__saved-detail--fit .layout-playground__pane-component-body > * {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.layout-playground__saved-detail--fit * {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.layout-playground__saved-detail--fit *::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.layout-playground__saved-detail--fit .previewer-ui__scroll,
|
||||
.layout-playground__saved-detail--fit .previewer-ui__pre,
|
||||
.layout-playground__saved-detail--fit .previewer-ui__markdown pre,
|
||||
.layout-playground__saved-detail--fit .codex-diff-previewer__diff-body,
|
||||
.layout-playground__saved-detail--fit .codex-diff-previewer__diff-section--expanded .codex-diff-previewer__diff-body {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.layout-playground__saved-detail--fit .previewer-ui,
|
||||
.layout-playground__saved-detail--fit .previewer-ui__body,
|
||||
.layout-playground__saved-detail--fit .previewer-ui__editor,
|
||||
.layout-playground__saved-detail--fit .previewer-ui__editor-body,
|
||||
.layout-playground__saved-detail--fit .codex-diff-previewer,
|
||||
.layout-playground__saved-detail--fit .codex-diff-previewer__diff-list,
|
||||
.layout-playground__saved-detail--fit .codex-diff-previewer__diff-section,
|
||||
.layout-playground__saved-detail--fit .embedded-map-ui,
|
||||
.layout-playground__saved-detail--fit .embedded-map-ui__frame,
|
||||
.layout-playground__saved-detail--fit .embedded-map-ui__slot {
|
||||
min-height: 0 !important;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.layout-playground__saved-detail--fit .embedded-map-ui__canvas {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.layout-playground__pane--selected {
|
||||
border-color: rgba(37, 99, 235, 0.75);
|
||||
background: rgba(219, 234, 254, 0.5);
|
||||
}
|
||||
|
||||
.layout-playground__pane--surface.layout-playground__pane--selected {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.layout-playground__pane--hidden {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.layout-playground__divider.ant-divider {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.layout-playground__code {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.layout-playground__code-block {
|
||||
margin: 0;
|
||||
padding: 18px 20px;
|
||||
overflow: auto;
|
||||
border-radius: 20px;
|
||||
background: #0f172a;
|
||||
color: #dbeafe;
|
||||
font-family: 'JetBrains Mono', 'SFMono-Regular', Consolas, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.layout-playground__hero {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.layout-playground__meta {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.layout-playground__preview-head {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.layout-playground__bottom-menu {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.layout-playground__bottom-menu-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.layout-playground__saved-browser-head {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.layout-playground__saved-detail-bar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.layout-playground__saved-fit-viewport {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.layout-playground__saved-browser-layout {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.layout-playground__saved-gallery {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: repeat(var(--layout-gallery-rows), minmax(0, 1fr));
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.layout-playground__saved-card-head {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.layout-playground__storage-head {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.layout-playground__pane-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.layout-playground__preview-frame {
|
||||
min-height: 520px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.layout-playground__preview-frame--fullscreen {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.layout-playground__preview-frame--gallery {
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
.layout-playground__splitter {
|
||||
min-height: 480px;
|
||||
}
|
||||
|
||||
.layout-playground__splitter-toggle-dock {
|
||||
width: calc(100% - 24px);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.layout-playground__splitter-toggle-dock--horizontal,
|
||||
.layout-playground__splitter-toggle-dock--vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.layout-playground__splitter-toggle-dock .ant-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
1340
src/views/play/LayoutPlaygroundView.tsx
Executable file
1340
src/views/play/LayoutPlaygroundView.tsx
Executable file
File diff suppressed because it is too large
Load Diff
313
src/views/play/layoutStorage.ts
Executable file
313
src/views/play/layoutStorage.ts
Executable file
@@ -0,0 +1,313 @@
|
||||
import { appendClientIdHeader } from '../../app/main/clientIdentity';
|
||||
import { getRegisteredAccessToken } from '../../app/main/tokenAccess';
|
||||
|
||||
export type LayoutAxis = 'horizontal' | 'vertical';
|
||||
export type SizeUnit = 'px' | '%';
|
||||
|
||||
export type SavedLayoutRecord = {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
axis: LayoutAxis;
|
||||
sizeUnit: SizeUnit;
|
||||
primarySize: number;
|
||||
primaryMin: number;
|
||||
secondaryMin: number;
|
||||
resizable: boolean;
|
||||
selectedLeafId: string | null;
|
||||
totalPanes: number;
|
||||
summary: string;
|
||||
tree: unknown;
|
||||
};
|
||||
|
||||
type SavedLayoutRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
axis: LayoutAxis;
|
||||
size_unit: SizeUnit;
|
||||
primary_size: number;
|
||||
primary_min: number;
|
||||
secondary_min: number;
|
||||
resizable: boolean;
|
||||
selected_leaf_id: string | null;
|
||||
total_panes: number;
|
||||
summary: string;
|
||||
tree: unknown;
|
||||
};
|
||||
|
||||
const WORK_SERVER_TIMEOUT_MS = 8000;
|
||||
const PLAY_LAYOUTS_TABLE = 'play_layouts';
|
||||
|
||||
let setupPromise: Promise<void> | null = null;
|
||||
|
||||
function resolveWorkServerBaseUrl() {
|
||||
if (import.meta.env.VITE_WORK_SERVER_URL) {
|
||||
return import.meta.env.VITE_WORK_SERVER_URL;
|
||||
}
|
||||
|
||||
return '/api';
|
||||
}
|
||||
|
||||
function resolveWorkServerFallbackBaseUrl() {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hostname = window.location.hostname;
|
||||
const isLocalWorkServerHost =
|
||||
hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '0.0.0.0';
|
||||
|
||||
if (!isLocalWorkServerHost) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fallbackUrl = new URL(window.location.origin);
|
||||
fallbackUrl.port = '3100';
|
||||
fallbackUrl.pathname = '/api';
|
||||
fallbackUrl.search = '';
|
||||
fallbackUrl.hash = '';
|
||||
|
||||
return fallbackUrl.toString().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
const WORK_SERVER_BASE_URL = resolveWorkServerBaseUrl();
|
||||
const WORK_SERVER_FALLBACK_BASE_URL =
|
||||
!import.meta.env.VITE_WORK_SERVER_URL && WORK_SERVER_BASE_URL === '/api'
|
||||
? resolveWorkServerFallbackBaseUrl()
|
||||
: null;
|
||||
|
||||
class LayoutStorageError extends Error {
|
||||
status: number;
|
||||
|
||||
constructor(message: string, status: number) {
|
||||
super(message);
|
||||
this.name = 'LayoutStorageError';
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
async function requestOnce<T>(baseUrl: string, path: string, init?: RequestInit): Promise<T> {
|
||||
const headers = appendClientIdHeader(init?.headers);
|
||||
const hasBody = init?.body !== undefined && init.body !== null;
|
||||
const method = init?.method?.toUpperCase() ?? 'GET';
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => {
|
||||
controller.abort();
|
||||
}, WORK_SERVER_TIMEOUT_MS);
|
||||
|
||||
if (hasBody && !headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
const token = getRegisteredAccessToken();
|
||||
if (token && !headers.has('X-Access-Token')) {
|
||||
headers.set('X-Access-Token', token);
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
response = await fetch(`${baseUrl}${path}`, {
|
||||
...init,
|
||||
headers,
|
||||
signal: controller.signal,
|
||||
cache: init?.cache ?? (method === 'GET' ? 'no-store' : undefined),
|
||||
});
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (error instanceof DOMException && error.name === 'AbortError') {
|
||||
throw new LayoutStorageError('work-db 응답이 지연됩니다. 잠시 후 다시 시도해 주세요.', 408);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
let message = text || '레이아웃 저장소 요청에 실패했습니다.';
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(text) as { message?: string };
|
||||
message = payload.message || message;
|
||||
} catch {
|
||||
// Keep the raw response text when JSON parsing is not available.
|
||||
}
|
||||
|
||||
throw new LayoutStorageError(message, response.status);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type') ?? '';
|
||||
|
||||
if (!contentType.toLowerCase().includes('application/json')) {
|
||||
const text = await response.text();
|
||||
throw new LayoutStorageError(text ? 'work-db 응답이 JSON이 아닙니다.' : 'work-db 응답을 확인할 수 없습니다.', 502);
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
try {
|
||||
return await requestOnce<T>(WORK_SERVER_BASE_URL, path, init);
|
||||
} catch (error) {
|
||||
const shouldRetryWithFallback =
|
||||
WORK_SERVER_FALLBACK_BASE_URL &&
|
||||
WORK_SERVER_FALLBACK_BASE_URL !== WORK_SERVER_BASE_URL &&
|
||||
(error instanceof LayoutStorageError
|
||||
? error.status === 404 || error.status === 408 || error.status === 502
|
||||
: error instanceof Error && (/not found/i.test(error.message) || /404/.test(error.message)));
|
||||
|
||||
if (!shouldRetryWithFallback) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return requestOnce<T>(WORK_SERVER_FALLBACK_BASE_URL, path, init);
|
||||
}
|
||||
}
|
||||
|
||||
function toRecord(row: SavedLayoutRow): SavedLayoutRecord {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
axis: row.axis,
|
||||
sizeUnit: row.size_unit,
|
||||
primarySize: Number(row.primary_size),
|
||||
primaryMin: Number(row.primary_min),
|
||||
secondaryMin: Number(row.secondary_min),
|
||||
resizable: Boolean(row.resizable),
|
||||
selectedLeafId: row.selected_leaf_id,
|
||||
totalPanes: Number(row.total_panes),
|
||||
summary: row.summary,
|
||||
tree: row.tree,
|
||||
};
|
||||
}
|
||||
|
||||
function toRow(record: SavedLayoutRecord): SavedLayoutRow {
|
||||
return {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
created_at: record.createdAt,
|
||||
updated_at: record.updatedAt,
|
||||
axis: record.axis,
|
||||
size_unit: record.sizeUnit,
|
||||
primary_size: record.primarySize,
|
||||
primary_min: record.primaryMin,
|
||||
secondary_min: record.secondaryMin,
|
||||
resizable: record.resizable,
|
||||
selected_leaf_id: record.selectedLeafId,
|
||||
total_panes: record.totalPanes,
|
||||
summary: record.summary,
|
||||
tree: record.tree,
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureLayoutStorageTable() {
|
||||
if (!setupPromise) {
|
||||
setupPromise = (async () => {
|
||||
const schemaResponse = await request<{ items: Array<{ table_name: string }> }>('/schema/tables');
|
||||
const tableExists = schemaResponse.items.some((item) => item.table_name === PLAY_LAYOUTS_TABLE);
|
||||
|
||||
if (tableExists) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await request<{ ok: boolean; tableName: string }>('/ddl/create-table', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
tableName: PLAY_LAYOUTS_TABLE,
|
||||
columns: [
|
||||
{ name: 'id', type: 'text', nullable: false, primary: true },
|
||||
{ name: 'name', type: 'text', nullable: false },
|
||||
{ name: 'created_at', type: 'timestamp with time zone', nullable: false },
|
||||
{ name: 'updated_at', type: 'timestamp with time zone', nullable: false },
|
||||
{ name: 'axis', type: 'text', nullable: false },
|
||||
{ name: 'size_unit', type: 'text', nullable: false },
|
||||
{ name: 'primary_size', type: 'integer', nullable: false },
|
||||
{ name: 'primary_min', type: 'integer', nullable: false },
|
||||
{ name: 'secondary_min', type: 'integer', nullable: false },
|
||||
{ name: 'resizable', type: 'boolean', nullable: false, defaultTo: true },
|
||||
{ name: 'selected_leaf_id', type: 'text', nullable: true },
|
||||
{ name: 'total_panes', type: 'integer', nullable: false },
|
||||
{ name: 'summary', type: 'text', nullable: false },
|
||||
{ name: 'tree', type: 'jsonb', nullable: false },
|
||||
],
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
if (!(error instanceof LayoutStorageError) || !/already exists/i.test(error.message)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
})().catch((error) => {
|
||||
setupPromise = null;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
return setupPromise;
|
||||
}
|
||||
|
||||
export async function listSavedLayouts() {
|
||||
await ensureLayoutStorageTable();
|
||||
|
||||
const response = await request<{ rows: SavedLayoutRow[] }>(`/crud/${PLAY_LAYOUTS_TABLE}/select`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
orderBy: [{ field: 'updated_at', direction: 'desc' }],
|
||||
limit: 200,
|
||||
}),
|
||||
});
|
||||
|
||||
return response.rows.map(toRecord);
|
||||
}
|
||||
|
||||
export async function saveLayout(record: SavedLayoutRecord) {
|
||||
await ensureLayoutStorageTable();
|
||||
|
||||
const row = toRow(record);
|
||||
const existing = await request<{ rows: SavedLayoutRow[] }>(`/crud/${PLAY_LAYOUTS_TABLE}/select`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
where: [{ field: 'id', operator: 'eq', value: record.id }],
|
||||
limit: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
if (existing.rows.length > 0) {
|
||||
await request<{ ok: boolean }>(`/crud/${PLAY_LAYOUTS_TABLE}/update`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
data: row,
|
||||
where: [{ field: 'id', operator: 'eq', value: record.id }],
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await request<{ ok: boolean }>(`/crud/${PLAY_LAYOUTS_TABLE}/insert`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
data: row,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteLayout(id: string) {
|
||||
await ensureLayoutStorageTable();
|
||||
|
||||
await request<{ ok: boolean }>(`/crud/${PLAY_LAYOUTS_TABLE}/delete`, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({
|
||||
where: [{ field: 'id', operator: 'eq', value: id }],
|
||||
}),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user