feat(sw): refine headers, status label, popups and AI guide rules
Deploy Storybook / deploy (push) Successful in 1m10s

- SW Header: the five field previews hide and return together in one
  measured step; from 760 to 1920px the groups from 레이어 to 치수스타일
  share the room left before 속성보기, above 1920px they keep the 1920
  widths (controls bars and < 760px unchanged)
- SW Viewer Header Model / Layout: long names ellipsize in equal buttons
  (minmax(0, 1fr), name counted at --hm-sw-viewer-tab-name-basis) so they
  never change the box or the measured steps; green outer outline and its
  tokens removed; 378px / 42px, text-shadow and state unchanged
- SWViewerStatus: the label keeps the width of its widest real state text
  (idle keeps its icon slot), so a state change never moves the header;
  with too little room it ellipsizes after the file name gives way
- Popups: body inset shadow removed, title SVG drop shadow removed, viewer
  panel title icons use --hm-sw-viewer-panel-icon-color (#c7bda9)
- Storybook: SW/Viewer › Model / Layout story removed
- AI guide (docs/ai + package copies): design-only changes for every
  component, content limits for Submodule consumers (Header excluded),
  Submodule usage rules; VIEWER / DESIGN_RULES synced
- viewer.browser.mjs: explicit 1920 / 1440 labels and approved 1280
  icon-only expectations
This commit is contained in:
shkim4 committed 2026-10-23 18:18:17 +09:00
1 parent 325ec25e37
commit db03e155dd
19 files changed
+190 -119

No files matched your search

+57 -27
View File
@@ -34,10 +34,7 @@
--hm-sw-viewer-tabs-height: calc(var(--hm-sw-viewer-control-height) + 2px);
--hm-sw-viewer-tabs-radius: 6px;
--hm-sw-viewer-tabs-background: rgba(0, 0, 0, 0.8);
/* Outline: a 1px line just outside the frame, blurred (Figma Rectangle 9333: stroke outside + layer blur 2). */
--hm-sw-viewer-tabs-outline-color: #349f7f;
--hm-sw-viewer-tabs-outline-blur: 1px;
/* Extra glow around the frame: none in Figma (the blurred outline is the glow); set a color to add one. */
/* Extra glow around the frame: none by default (the green outline was removed on 2026-10-23); set a color to add one. */
--hm-sw-viewer-tabs-glow-color: transparent;
--hm-sw-viewer-tabs-glow: 0 0 4px var(--hm-sw-viewer-tabs-glow-color);
--hm-sw-viewer-tabs-shadow: inset 0 2.857px 2.857px rgba(0, 0, 0, 0.25);
@@ -45,6 +42,9 @@
--hm-sw-viewer-tab-radius: 5px;
--hm-sw-viewer-tab-border-width: 1.428px;
--hm-sw-viewer-tab-gap: 14px;
/* Width a tab name counts for in the space calculation (2026-10-23): a longer name measures as this (about the
default "Model"), so it never claims more room, takes free room when there is some and ellipsizes otherwise. */
--hm-sw-viewer-tab-name-basis: 3em;
--hm-sw-viewer-tab-background: #0e3d2f;
--hm-sw-viewer-tab-gradient: linear-gradient(180deg, #081813 0%, #09271e 8.6884%, #0b3227 25.428%, #0e3d2f 51.252%, #0b3227 80.027%, #0a2b21 89.759%, #081813 100%);
/* Selected (Model in Figma) / not selected (Layout in Figma). Borders are images (Figma gradient strokes, top to
@@ -105,6 +105,9 @@
--hm-sw-viewer-panel-badge-background: var(--hm-sw-property-table);
--hm-sw-viewer-panel-overlay: rgba(0, 0, 0, 0.1);
--hm-sw-viewer-panel-spacing: 12px;
/* Title-bar icon buttons of viewer panels (collapse, `ghost` header actions; 2026-10-23). Popup-only: the header
status text and switch captions keep --hm-sw-viewer-muted. SWPopup's own title icons are images. */
--hm-sw-viewer-panel-icon-color: #c7bda9;
}
.hm-sw-viewer-sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
@@ -160,7 +163,9 @@ button.hm-sw-viewer-header__brand { text-align: left; }
.hm-sw-viewer-header__document {
display: flex;
align-items: center;
flex: 0 1 clamp(140px, 25cqw, 360px);
/* Very large shrink weight (2026-10-23): the file name gives way first, as before; the object (shrink 1) only once
the file name is at its minimum (see .hm-sw-viewer-header__object). */
flex: 0 1000000 clamp(140px, 25cqw, 360px);
width: clamp(220px, 25cqw, 360px);
min-width: 56px;
height: calc(var(--hm-sw-viewer-height) - 2px);
@@ -318,7 +323,10 @@ button.hm-sw-viewer-header__brand { text-align: left; }
.hm-sw-viewer-header__object {
display: flex;
align-items: center;
flex: 1 0 auto;
/* Shrinkable (2026-10-23): the file name still gives way first (its huge shrink weight, down to its minimum); only
when nothing is left after the measured steps does the object give way, so the status label ellipsizes instead of
the header overflowing. The measurement (data-measuring) keeps flex: 0 0 auto. */
flex: 1 1 auto;
min-width: 0;
height: calc(var(--hm-sw-viewer-height) - 2px);
gap: var(--hm-sw-viewer-gap);
@@ -399,34 +407,25 @@ button.hm-sw-viewer-header__brand { text-align: left; }
position: relative;
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
/* minmax(0, 1fr) (2026-10-23): equal columns that may shrink below their content, so a long name never pushes the
box past its group (max 378px) or makes the buttons unequal; the name ellipsizes instead (below). */
grid-auto-columns: minmax(0, 1fr);
min-width: 0;
align-items: stretch;
gap: 1px;
box-sizing: border-box;
height: var(--hm-sw-viewer-tabs-height);
/* The transparent 1px edge is the room of the blurred outline (it stays inside the box height); the padding is the
black margin around the buttons, which fill the rest. 1.5px at the bottom (2026-10-23 user value): the outline
blur covers part of a 1px bottom margin, while the inner shadow darkens the top, so 1px read as no room below. */
/* The transparent 1px edge was the room of the outline (removed 2026-10-23, user request); it stays so the box and
button sizes do not change. The padding is the black margin around the buttons, which fill the rest; 1.5px at the
bottom (2026-10-23 user value). */
padding: 1px 1px 1.5px 1px;
border: 1px solid transparent;
border-radius: calc(var(--hm-sw-viewer-tabs-radius) + 1px);
background: var(--hm-sw-viewer-tabs-background) padding-box;
box-shadow: var(--hm-sw-viewer-tabs-glow);
}
/* The outline (blurred, on the frame edge) and the inner shadow lie over the buttons, as the last Figma layers do. */
.hm-sw-viewer-header__tabs::before {
content: "";
position: absolute;
z-index: 1;
/* The outer edge of the box (its border box, over the transparent 1px edge row): same size as the tabs box. */
inset: -1px;
border: 1px solid var(--hm-sw-viewer-tabs-outline-color);
border-radius: inherit;
filter: blur(var(--hm-sw-viewer-tabs-outline-blur));
/* The blur may soften 1px past that edge, no further (2026-10-23 user decision). */
clip-path: inset(-1px round calc(var(--hm-sw-viewer-tabs-radius) + 1px));
pointer-events: none;
}
/* The inner shadow lies over the buttons, as the last Figma layer does. (The blurred green outline, ::before, was
removed on 2026-10-23 at the user's request, with its outline color / blur tokens.) */
.hm-sw-viewer-header__tabs::after { content: ""; position: absolute; z-index: 1; inset: 0; border-radius: var(--hm-sw-viewer-tabs-radius); box-shadow: var(--hm-sw-viewer-tabs-shadow); pointer-events: none; }
/* A group holding only the tabs: the container is its face (no field face around it, as in Figma: the tabs sit on
the header object surface). */
@@ -474,7 +473,23 @@ button.hm-sw-viewer-header__brand { text-align: left; }
.hm-sw-viewer-header .hm-sw-viewer-header__tab[data-state="active"] { --hm-sw-viewer-tab-border-face: var(--hm-sw-viewer-tab-active-border); color: var(--hm-sw-viewer-tab-active-text); }
.hm-sw-viewer-header__count { font-size: var(--hm-sw-viewer-label-size); font-weight: 400; font-variant-numeric: tabular-nums; }
/* In a tab the count has the name's size (Figma: 14px Regular) and color. */
.hm-sw-viewer-header__tab .hm-sw-viewer-header__count { font-size: inherit; }
.hm-sw-viewer-header__tab .hm-sw-viewer-header__count { flex: 0 0 auto; font-size: inherit; }
/* Long names (2026-10-23 user decision): the name gives way first with an ellipsis, the count keeps its width; the
outline may paint 2px outside the clipped name (as the file name). The name counts at most
--hm-sw-viewer-tab-name-basis in the tabs' natural width (width + max-width: max-content: shorter names keep their own
width), so a long name changes neither the tabs' width nor the measured steps of the other items; it grows (flex-grow,
up to its text) only into room the box has anyway. */
.hm-sw-viewer-header .hm-sw-viewer-header__tab { min-width: 0; }
.hm-sw-viewer-header__tab-name {
flex: 1 1 auto;
width: var(--hm-sw-viewer-tab-name-basis);
max-width: max-content;
min-width: 0;
overflow: clip;
overflow-clip-margin: var(--hm-sw-header-text-outline-room);
text-overflow: ellipsis;
white-space: nowrap;
}
/* Switch fields (2.5D, 어두운 배경): 11px muted caption above the switch. */
.hm-sw-viewer-header__switch-field {
@@ -533,7 +548,22 @@ button.hm-sw-viewer-header__brand { text-align: left; }
font-weight: 400;
white-space: nowrap;
}
.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__icon { display: inline-flex; flex: 0 0 auto; width: var(--hm-sw-viewer-icon-size); height: var(--hm-sw-viewer-icon-size); }
/* Stable status width (2026-10-23): the empty idle icon slot keeps its place; the label is one grid cell holding the
shown text and, hidden, every other text it can show (SWViewerStatus), so its width is the widest real candidate in
the real font. With too little room (after the measured steps) it shrinks and the text ellipsizes; with room again
the full text returns. */
.hm-sw-viewer-status__icon[data-empty] { visibility: hidden; }
.hm-sw-viewer-status__label { display: inline-grid; grid-template-columns: minmax(0, auto); flex: 0 1 auto; min-width: 0; }
.hm-sw-viewer-status__label > * {
grid-area: 1 / 1;
min-width: 0;
overflow: clip;
overflow-clip-margin: var(--hm-sw-header-text-outline-room);
text-overflow: ellipsis;
white-space: nowrap;
}
.hm-sw-viewer-status__sizer { visibility: hidden; }
.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); } }
@@ -753,7 +783,7 @@ button.hm-sw-viewer-header__brand { text-align: left; }
border: 0;
border-radius: 0;
background: transparent;
color: var(--hm-sw-viewer-muted);
color: var(--hm-sw-viewer-panel-icon-color);
}
.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; }
+17 -9
View File
@@ -1637,7 +1637,7 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l
.hm-sw-property__close-hover { visibility: hidden; }
.hm-sw-property__close:hover > .hm-sw-property__close-default { visibility: hidden; }
.hm-sw-property__close:hover > .hm-sw-property__close-hover { visibility: visible; }
.hm-sw-property__body { display: flex; flex-direction: column; gap: 8px; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 7px 7px 7px; box-shadow: inset 0 2px 2px rgba(255,255,255,.6); }
.hm-sw-property__body { display: flex; flex-direction: column; gap: 8px; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 7px 7px 7px; }
/* Popup scrollbar: the body and any scrolling area inside it (zero-specificity so content can still override). */
.hm-sw-property__body, .hm-sw-property__body :where(*) { scrollbar-width: thin; scrollbar-color: var(--hm-sw-popup-scrollbar-thumb) var(--hm-sw-popup-scrollbar-track); }
/* Popup toggle switch (Figma toggle_btn_new 633:75188 / 633:75902): one face for every switch inside an SW popup —
@@ -1948,7 +1948,7 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l
< 1800 the 그리기모드 text hides; fields take their content width and shrink with an ellipsis, each by its own width.
< 1080 window move area 88 -> 24 (window 240 -> 176), field insets 6px.
Measured (2026-10-22 user request, SWHeader data-compact): from 760px to 1800px, while the object area lacks
room the field pictures hide one at a time — 치수 아이콘, 선 그림, 글꼴명, 색상 칩, 레이어 상태 아이콘 — then the file
room the field pictures hide together — 레이어 상태 아이콘, 색상 칩, 선 그림, 글꼴명, 치수 아이콘 — then the file
name gives way (310 -> 96px), then the values ellipsize. With room to spare nothing hides.
<= 900 the 선스타일 field shows 선스타일 only (가중치 / 스케일 hide).
Below 760px (phones, e.g. 390) the former tiers are unchanged: parts move into More (선/문자/치수 스타일, 그리기모드,
@@ -1994,6 +1994,16 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l
/* 1610 - 1301 + the tighter object area with titles, without 그리기모드 (891, measured) = 1200. */
.hm-sw-header__document { flex-basis: clamp(96px, calc(100cqw - 1200px - var(--hm-sw-header-leading-width, 0px)), 310px); }
}
/* Free room (2026-10-23 user request): from 760px (속성보기 shown) up to a 1920px header the groups from 레이어 to
치수스타일 share the room left before 속성보기 (each grows by the same amount from its own width, its field box with
it), so no gap stays there. Wider headers keep the 1920 widths (234 / 192 / 284 / 164 / 164) and the extra room stays
before 속성보기 (its auto margin). The standard bar only: a `controls` bar ([data-configured]) keeps its own layout.
The measured steps read the natural widths (data-measuring sets flex-grow 0), so hiding is unchanged. */
@container hm-sw-header (min-width: 760px) and (max-width: 1920px) {
.hm-sw-header__fields:not([data-configured]) { flex-grow: 1; }
.hm-sw-header__fields:not([data-configured]) > :is(.hm-sw-header__layer, .hm-sw-header__menu, .hm-sw-header__field),
.hm-sw-header__fields:not([data-configured]) :is(.hm-sw-header__layer, .hm-sw-header__menu) > .hm-sw-header__field { flex-grow: 1; }
}
@container hm-sw-header (min-width: 760px) and (max-width: 1079.98px) {
/* Values need the room (1024px, 2026-10-21 user request): insets 6px, every value part may shrink (ellipsis); the
pictures hide by measurement (data-compact). */
@@ -2151,13 +2161,11 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l
/* ...and the file name gives way only down to the default design's smallest width (96px, see the clamps above). */
.hm-sw-header[data-leading="open"] .hm-sw-header__fields[data-configured] > .hm-sw-header__control[data-control="document"] { min-width: 96px; }
/* Measured pictures (SWHeader data-compact, 2026-10-22 user request): from 760px to 1800px the field pictures hide one at a
time while the object area lacks room — 1 치수 아이콘, 2 선 그림, 3 글꼴명, 4 색상 칩, 5 레이어 상태 아이콘. */
.hm-sw-header__fields:is([data-compact="1"], [data-compact="2"], [data-compact="3"], [data-compact="4"], [data-compact="5"]) .hm-sw-header__dimension-icon,
.hm-sw-header__fields:is([data-compact="2"], [data-compact="3"], [data-compact="4"], [data-compact="5"]) .hm-sw-header__linetype,
.hm-sw-header__fields:is([data-compact="3"], [data-compact="4"], [data-compact="5"]) .hm-sw-header__font-name,
.hm-sw-header__fields:is([data-compact="4"], [data-compact="5"]) .hm-sw-header__chip,
.hm-sw-header__fields[data-compact="5"] .hm-sw-header__field--layer .hm-sw-header__layer-icons { display: none; }
/* Measured pictures (SWHeader data-compact): from 760px to 1800px, while the object area lacks room, the field pictures
hide together in one step (2026-10-23 user request; formerly one at a time): 레이어 상태 아이콘, 색상 칩, 선 그림,
글꼴명, 치수 아이콘. They come back together once all of them fit. */
.hm-sw-header__fields[data-compact="1"] :is(.hm-sw-header__dimension-icon, .hm-sw-header__linetype, .hm-sw-header__font-name, .hm-sw-header__chip),
.hm-sw-header__fields[data-compact="1"] .hm-sw-header__field--layer .hm-sw-header__layer-icons { display: none; }
/* Measured More (SWHeader, 2026-10-19): on top of the breakpoints, parts move into More while the object area has no
room for them and return when it has. data-measuring is set for one synchronous layout read and removed before