feat(sw): viewer follow-up — SVG icon outline, shared SW popup frame, story grid, green layer buttons

- A: SWViewerIcon draws the SWHeader black outline as SVG stroke on a copy behind the icon (no CSS filter)
- B: SWViewerPanel renders the common SWPopup frame (same DOM / CSS) and adds drag, corner resize,
  collapse, bounds and modal; SWPopup gains optional titleActions / footer; title art spans any width
- C: shared Story canvas grid (--sw-story-canvas) behind the SW Header work area and dropdown room
- D: SWViewerButton tone="green" for 전체 켜기 / 전체 끄기: default kept, hover = primary (확인) hover
  token, pressed = primary face
- AI guide, verification scripts (76 checks, element regression) and evidence updated
This commit is contained in:
shkim4 committed 2026-10-19 16:34:50 +09:00
1 parent 7f42923ccf
commit 68fbcf6e15
45 files changed
+1463 -377

No files matched your search

+52 -114
View File
@@ -29,15 +29,9 @@
--hm-sw-viewer-switch-off: #0a221d;
--hm-sw-viewer-switch-bevel: inset 0 1px 0 rgba(255, 255, 255, 0.1), inset 0 -1px 1px rgba(0, 0, 0, 0.25);
/* Icon stroke: the SWHeader icon assets (print / K guide / layer icons) are a fill + a 1px black OUTSIDE stroke
(Figma mask "outside" path) + a 0-offset glow (stdDeviation 1.615, #061A14). Program icons are free SVGs, so the
same stroke is a drop-shadow chain that follows the icon's own contour: the four 1px shadows dilate the shape
by 1px (each one also shadows the previous ones, so corners close), the last one is the glow. */
(Figma mask "outside" path). SWViewerIcon draws it as SVG stroke on a copy behind the icon (see SWViewerIcon). */
--hm-sw-viewer-icon-outline-color: var(--hm-sw-header-text-outline-color);
--hm-sw-viewer-icon-glow: #061a14;
--hm-sw-viewer-icon-outline:
drop-shadow(1px 0 0 var(--hm-sw-viewer-icon-outline-color)) drop-shadow(-1px 0 0 var(--hm-sw-viewer-icon-outline-color))
drop-shadow(0 1px 0 var(--hm-sw-viewer-icon-outline-color)) drop-shadow(0 -1px 0 var(--hm-sw-viewer-icon-outline-color))
drop-shadow(0 0 1.615px var(--hm-sw-viewer-icon-glow));
--hm-sw-viewer-icon-outline-width: 1px;
/* Dropdown (light) and More (beige) */
--hm-sw-viewer-menu-background: #fff;
@@ -51,31 +45,20 @@
--hm-sw-viewer-more-background: #f5f0e5;
--hm-sw-viewer-more-border-color: #897445;
/* Panel */
--hm-sw-viewer-panel-background: #e0dace;
--hm-sw-viewer-panel-field-background: #f5f0e5;
--hm-sw-viewer-panel-border: #897445;
--hm-sw-viewer-panel-inner-border: #9a9284;
--hm-sw-viewer-panel-title-background: #221d13;
--hm-sw-viewer-panel-title-highlight: #a98d53;
--hm-sw-viewer-panel-title-color: #ffc600;
--hm-sw-viewer-panel-title-button-hover: #604f32;
--hm-sw-viewer-panel-text: #1d1810;
/* Panel: the frame is SWPopup (title bar, title, close, border, radius, shadow). Content tokens map to the
SW property window tokens so viewer content sits on the same surface as every SW popup. */
--hm-sw-viewer-panel-background: var(--hm-sw-property-surface);
--hm-sw-viewer-panel-field-background: var(--hm-sw-property-content);
--hm-sw-viewer-panel-inner-border: var(--hm-sw-property-inner-border);
--hm-sw-viewer-panel-text: var(--hm-sw-property-label);
--hm-sw-viewer-panel-heading: #3e3523;
--hm-sw-viewer-panel-label: #6e6759;
--hm-sw-viewer-panel-spacing: 12px;
--hm-sw-viewer-panel-title-height: 34px;
--hm-sw-viewer-panel-shadow: 0 0 12px rgba(0, 0, 0, 0.2);
--hm-sw-viewer-panel-primary: #123328;
--hm-sw-viewer-panel-switch-off: #9a9284;
--hm-sw-viewer-panel-switch-off: var(--hm-sw-property-inner-border);
--hm-sw-viewer-panel-switch-on: #266651;
--hm-sw-viewer-panel-badge-background: #f4f1ea;
--hm-sw-viewer-panel-badge-background: var(--hm-sw-property-table);
--hm-sw-viewer-panel-overlay: rgba(0, 0, 0, 0.1);
/* Close: green curve, #E40000 on hover (user-supplied reference value). */
--hm-sw-viewer-close-glow: #123328;
--hm-sw-viewer-close-edge: #020a07;
--hm-sw-viewer-close-hover: #e40000;
--hm-sw-viewer-close-icon: #999;
--hm-sw-viewer-panel-spacing: 12px;
}
.hm-sw-viewer-sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
@@ -194,6 +177,13 @@ button.hm-sw-viewer-header__brand { text-align: left; }
.hm-sw-viewer-header__icon, .hm-sw-viewer-header__chevron { display: inline-flex; width: var(--hm-sw-viewer-icon-size); height: var(--hm-sw-viewer-icon-size); }
.hm-sw-viewer-header__icon > svg, .hm-sw-viewer-header__chevron > svg { width: 100%; height: 100%; }
/* SWViewerIcon: the outline copy (black SVG stroke) right behind the untouched icon. */
.hm-sw-viewer-icon { position: relative; display: inline-flex; width: 100%; height: 100%; }
.hm-sw-viewer-icon__outline, .hm-sw-viewer-icon__glyph { display: inline-flex; width: 100%; height: 100%; }
.hm-sw-viewer-icon__outline { position: absolute; inset: 0; overflow: visible; }
.hm-sw-viewer-icon__glyph { position: relative; }
.hm-sw-viewer-icon svg { width: 100%; height: 100%; overflow: visible; }
/* Tabs (Model / Layout): 40px triggers with an 11px count; the active one turns #FFC600. */
.hm-sw-viewer-header__tabs { display: flex; align-items: center; gap: var(--hm-sw-viewer-gap); }
.hm-sw-viewer-header .hm-sw-viewer-header__tab {
@@ -233,7 +223,7 @@ button.hm-sw-viewer-header__brand { text-align: left; }
/* Text and icon stroke (2026-10-19 request): the SWHeader black outline on the viewer header, same tokens and
method — an outside outline of the glyph / icon shape, never a box border.
Text: 14px = --hm-sw-header-text-outline (1.3px black), 11px captions / counts / status = --hm-sw-header-label-outline
(1px black), as SWHeader's 12px labels. Icons: --hm-sw-viewer-icon-outline (see the token). Fill color, size,
(1px black), as SWHeader's 12px labels. Icons: SWViewerIcon (SVG stroke, see the token). Fill color, size,
weight and spacing are unchanged; hover / active / open change only the fill, disabled fades the whole button
(opacity) like SWHeader. Dropdown faces (light / beige) and panels get no outline, like SWHeader's More menu. */
.hm-sw-viewer-header :is(
@@ -244,9 +234,6 @@ button.hm-sw-viewer-header__brand { text-align: left; }
.hm-sw-viewer-header :is(.hm-sw-viewer-header__count, .hm-sw-viewer-header__switch-label, .hm-sw-viewer-status__label) {
text-shadow: var(--hm-sw-header-label-outline);
}
.hm-sw-viewer-header :is(.hm-sw-viewer-header__icon, .hm-sw-viewer-header__chevron, .hm-sw-viewer-status > svg) {
filter: var(--hm-sw-viewer-icon-outline);
}
/* Clipped (ellipsis) file name: the outline may paint 2px outside the box instead of being cut. */
.hm-sw-viewer-header .hm-sw-viewer-header__document-name {
overflow: clip;
@@ -264,7 +251,7 @@ button.hm-sw-viewer-header__brand { text-align: left; }
font-weight: 400;
white-space: nowrap;
}
.hm-sw-viewer-status > svg { width: var(--hm-sw-viewer-icon-size); height: var(--hm-sw-viewer-icon-size); }
.hm-sw-viewer-status__icon { display: inline-flex; width: var(--hm-sw-viewer-icon-size); height: var(--hm-sw-viewer-icon-size); }
.hm-sw-viewer-status[data-state="error"] { color: var(--hm-sw-viewer-accent); }
.hm-sw-viewer-status__spin { animation: hm-sw-viewer-spin 1s linear infinite; }
@keyframes hm-sw-viewer-spin { to { transform: rotate(360deg); } }
@@ -356,105 +343,52 @@ button.hm-sw-viewer-header__brand { text-align: left; }
.hm-sw-viewer-menu__separator { flex: 0 0 1px; height: 1px; margin: 4px -4px; background: var(--hm-sw-viewer-menu-border-color); }
.hm-sw-viewer-menu[data-surface="beige"] .hm-sw-viewer-menu__separator { background: var(--hm-sw-viewer-panel-inner-border); }
/* ---------- Panel ---------- */
.hm-sw-viewer-panel {
display: flex;
flex-direction: column;
max-width: 90vw;
max-height: 90vh;
overflow: hidden;
border: 2px solid var(--hm-sw-viewer-panel-border);
border-radius: var(--hm-sw-viewer-radius);
background: var(--hm-sw-viewer-panel-background);
box-shadow: var(--hm-sw-viewer-panel-shadow), inset 0 0 0 1px var(--hm-sw-viewer-panel-field-background);
color: var(--hm-sw-viewer-panel-text);
font-size: 14px;
font-weight: 400;
line-height: 20px;
outline: none;
}
/* ---------- Panel (SWPopup frame + viewer window behavior) ---------- */
.hm-sw-viewer-panel-backdrop { inset: 0; background: var(--hm-sw-viewer-panel-overlay); }
.hm-sw-viewer-panel__titlebar {
display: flex;
flex-shrink: 0;
align-items: center;
gap: 4px;
min-height: var(--hm-sw-viewer-panel-title-height);
padding-left: var(--hm-sw-viewer-panel-spacing);
border-bottom: 1px solid var(--hm-sw-viewer-panel-border);
background: var(--hm-sw-viewer-panel-title-background);
box-shadow: inset 0 1px 0 var(--hm-sw-viewer-panel-title-highlight);
user-select: none;
.hm-sw-viewer-panel[data-collapsed] .hm-sw-property__body { display: none; }
.hm-sw-viewer-panel[data-draggable] .hm-sw-property__titlebar { cursor: grab; touch-action: none; user-select: none; }
.hm-sw-viewer-panel[data-dragging] .hm-sw-property__titlebar { cursor: grabbing; }
/* Any width: the Figma title art keeps its 308px curve at the close button (right-aligned; at 314px this is exactly
the SWPopup position), and a stretched copy of the same art fills the flat part on the left of wider panels.
The right one hides its own left 20px (left edge + corner) and the left copy ends exactly there: no seam, no
doubled shadow. */
.hm-sw-viewer-panel .hm-sw-property__title-art { left: auto; right: 4px; clip-path: inset(-4px -4px -4px 20px); }
.hm-sw-viewer-panel .hm-sw-property__titlebar::before {
content: "";
position: absolute;
z-index: 1;
left: 0;
top: -2px;
width: max(0px, calc(100% - 292px));
height: 38px;
background: url("../assets/property-title-single.svg") left top / calc(100% + 288px) 38px no-repeat;
transform: scaleY(-1);
pointer-events: none;
}
.hm-sw-viewer-panel__titlebar[data-draggable] { cursor: grab; touch-action: none; }
.hm-sw-viewer-panel__titlebar[data-dragging] { cursor: grabbing; }
.hm-sw-viewer-panel__title {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
margin: 0;
color: var(--hm-sw-viewer-panel-title-color);
font-size: 14px;
font-weight: 700;
line-height: 1.4;
}
.hm-sw-viewer-panel__title-icon { display: inline-flex; flex: 0 0 16px; width: 16px; height: 16px; }
.hm-sw-viewer-panel__title-icon > svg { width: 100%; height: 100%; }
.hm-sw-viewer-panel__title-text { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.hm-sw-viewer-panel__actions { display: flex; flex-shrink: 0; align-items: center; }
.hm-sw-viewer-panel__actions { position: relative; z-index: 2; display: flex; flex-shrink: 0; align-items: center; height: 30px; }
/* Title-bar buttons: SW text-only look (no face), #FFC600 on hover like the SW title-bar controls. */
.hm-sw-viewer-panel :is(.hm-sw-viewer-panel__icon-button, .hm-sw-viewer-button[data-variant="ghost"]) {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--hm-sw-viewer-panel-title-height);
height: var(--hm-sw-viewer-panel-title-height);
width: 26px;
height: 26px;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
color: var(--hm-sw-viewer-muted);
}
.hm-sw-viewer-panel :is(.hm-sw-viewer-panel__icon-button, .hm-sw-viewer-button[data-variant="ghost"]):hover:not(:disabled) { background: var(--hm-sw-viewer-panel-title-button-hover); color: var(--hm-sw-viewer-panel-title-color); }
.hm-sw-viewer-panel :is(.hm-sw-viewer-panel__icon-button, .hm-sw-viewer-button[data-variant="ghost"], .hm-sw-viewer-panel__close):focus-visible { outline: 2px solid var(--hm-sw-viewer-panel-title-color); outline-offset: -3px; }
.hm-sw-viewer-panel .hm-sw-viewer-panel__close {
position: relative;
isolation: isolate;
display: inline-flex;
align-items: center;
flex-shrink: 0;
width: calc(var(--hm-sw-viewer-panel-title-height) * 1.53);
height: var(--hm-sw-viewer-panel-title-height);
padding: 0 0 0 calc(var(--hm-sw-viewer-panel-title-height) * 0.7);
overflow: hidden;
color: var(--hm-sw-viewer-close-icon);
}
.hm-sw-viewer-panel__close-shape { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.hm-sw-viewer-panel__close-curve { stroke: var(--hm-sw-viewer-panel-border); stroke-width: 1; }
.hm-sw-viewer-panel__close-icon { position: relative; width: 16px; height: 16px; pointer-events: none; }
.hm-sw-viewer-panel .hm-sw-viewer-panel__close:focus-visible { --hm-sw-viewer-close-glow: #266651; color: #fff; }
.hm-sw-viewer-panel .hm-sw-viewer-panel__close:is(:hover, :active) { --hm-sw-viewer-close-glow: var(--hm-sw-viewer-close-hover); --hm-sw-viewer-close-edge: var(--hm-sw-viewer-close-hover); color: #fff; }
.hm-sw-viewer-panel__body {
display: flex;
flex-direction: column;
gap: 20px;
flex: 1;
min-height: 0;
overflow: auto;
overscroll-behavior: contain;
padding: var(--hm-sw-viewer-panel-spacing);
scrollbar-width: thin;
scrollbar-color: var(--hm-sw-viewer-panel-inner-border) var(--hm-sw-viewer-panel-background);
}
.hm-sw-viewer-panel :is(.hm-sw-viewer-panel__icon-button, .hm-sw-viewer-button[data-variant="ghost"]):hover:not(:disabled) { background: transparent; color: var(--hm-sw-header-selected-color); }
.hm-sw-viewer-panel :is(.hm-sw-viewer-panel__icon-button, .hm-sw-viewer-button[data-variant="ghost"]):focus-visible { outline: 2px solid var(--hm-sw-header-selected-color); outline-offset: -2px; }
.hm-sw-viewer-panel :is(.hm-sw-viewer-panel__icon-button, .hm-sw-viewer-button[data-variant="ghost"]) svg { width: 16px; height: 16px; }
.hm-sw-viewer-panel__footer {
display: flex;
flex-shrink: 0;
justify-content: flex-end;
gap: 8px;
padding: 8px var(--hm-sw-viewer-panel-spacing);
padding: 7px;
border-top: 1px solid var(--hm-sw-viewer-panel-inner-border);
background: var(--hm-sw-viewer-panel-background);
}
/* Panel content */
@@ -525,6 +459,10 @@ button.hm-sw-viewer-header__brand { text-align: left; }
}
.hm-sw-theme .hm-sw-viewer-button[data-variant="outline"]:hover:not(:disabled) { background: var(--hm-sw-viewer-panel-field-background); }
.hm-sw-theme .hm-sw-viewer-button[data-variant="primary"] { background: var(--hm-sw-viewer-panel-primary); color: #fff; }
/* tone="green" (e.g. 전체 켜기 / 전체 끄기): default unchanged. Hover = the primary (확인) button hover, the same token;
pressed = the primary button face. White text as on the primary button. Back to default / hover after the press. */
.hm-sw-theme .hm-sw-viewer-button[data-tone="green"]:hover:not(:disabled) { border-color: var(--hm-sw-viewer-hover); background: var(--hm-sw-viewer-hover); color: #fff; }
.hm-sw-theme .hm-sw-viewer-button[data-tone="green"]:active:not(:disabled) { border-color: var(--hm-sw-viewer-panel-primary); background: var(--hm-sw-viewer-panel-primary); color: #fff; }
.hm-sw-theme .hm-sw-viewer-button[data-variant="primary"]:hover:not(:disabled) { background: var(--hm-sw-viewer-hover); border-color: var(--hm-sw-viewer-hover); }
.hm-sw-theme .hm-sw-viewer-button:focus-visible { outline: 2px solid var(--hm-sw-viewer-panel-switch-on); outline-offset: 1px; }
.hm-sw-theme .hm-sw-viewer-button:disabled { opacity: 0.5; cursor: not-allowed; }