From 3671657ee010d22774a9d4f20da76a1f835febad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=88=98=ED=98=84?= Date: Wed, 21 Oct 2026 19:02:11 +0900 Subject: [PATCH] fix(sw): rounded menu surface end before the viewer strip; solid viewer background User requests (2026-10-21), CSS and docs only; no Props, public .d.ts unchanged. - The viewer menu surface ends before the trailing strip like the SWHeader object area: 6px right radius (--hm-sw-header-object-radius), the same face and border, a 2px header gap. Painted by the first strip segment over the empty group padding + gap (4px more room below 1180px); heights, edges and button positions unchanged. - --hm-sw-viewer-background: the Storybook viewer canvas solid #0a0a0b instead of the gradient (viewer header only; SWHeader, Footer, Popup unchanged). --- packages/sw/ai/DESIGN_RULES.md | 2 +- packages/sw/ai/VIEWER.md | 2 +- packages/sw/src/theme/sw-viewer.css | 31 ++++++++++++++++++++++++++++- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/sw/ai/DESIGN_RULES.md b/packages/sw/ai/DESIGN_RULES.md index a26c2b5..c67c5c3 100644 --- a/packages/sw/ai/DESIGN_RULES.md +++ b/packages/sw/ai/DESIGN_RULES.md @@ -141,7 +141,7 @@ | 영역 | 토큰 = 기본값 | |---|---| -| Header | `--hm-sw-viewer-height` 52px, `--hm-sw-viewer-background` `linear-gradient(180deg, #266651 0%, #123328 45%, #0a221d 100%)`, `--hm-sw-viewer-border-color` #071e18, `--hm-sw-viewer-text` #fff, `--hm-sw-viewer-muted` #bddfd4, `--hm-sw-viewer-accent` #ffc600, `--hm-sw-viewer-hover` #266651 | +| Header | `--hm-sw-viewer-height` 52px, `--hm-sw-viewer-background` #0a0a0b(단색, 2026-10-21 — Storybook 뷰어 캔버스 바탕색), `--hm-sw-viewer-border-color` #071e18, `--hm-sw-viewer-text` #fff, `--hm-sw-viewer-muted` #bddfd4, `--hm-sw-viewer-accent` #ffc600, `--hm-sw-viewer-hover` #266651 | | 로고 면 | `--hm-sw-viewer-brand-width` 72px, `--hm-sw-viewer-brand-empty-width` 24px(로고 없음), `--hm-sw-viewer-brand-top` #123328, `--hm-sw-viewer-brand-middle` #205241, `--hm-sw-viewer-brand-bottom` #071e18 | | 크기·글자 | `--hm-sw-viewer-gap` 4px, `--hm-sw-viewer-radius` 4px, `--hm-sw-viewer-control-height` 40px, `--hm-sw-viewer-button-height` 34px, `--hm-sw-viewer-font-size` 14px, `--hm-sw-viewer-label-size` 11px, `--hm-sw-viewer-icon-size` 16px, `--hm-sw-viewer-disabled-opacity` 0.6 | | Header 스위치·아이콘 | `--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)`, `--hm-sw-viewer-icon-outline-color` = `var(--hm-sw-header-text-outline-color)`, `--hm-sw-viewer-icon-outline-width` 1px | diff --git a/packages/sw/ai/VIEWER.md b/packages/sw/ai/VIEWER.md index 21c7c2c..09b6ebe 100644 --- a/packages/sw/ai/VIEWER.md +++ b/packages/sw/ai/VIEWER.md @@ -40,7 +40,7 @@ | `onDocumentOpen?()`, `documentOpenLabel?` | 로고 오른쪽 파일 열기 버튼(`SWHeader` `documentOpenPlacement="header"`와 같은 파일명 면 `SWToolButton appearance="document"` + 폴더 아이콘(다른 Header 아이콘과 같은 검정 외곽선 `SWViewerIcon`, 테두리·면 없음) + `documentOpenLabel`, 기본 `Open`). 공간이 부족하면 아이콘만(라벨은 Tooltip·접근성 이름) 표시하고 More로 옮기지 않습니다. 실행 후 `onCommand({ id: "open", itemId: "document" })` | | `logoMenu?: { title?, items }` | (2026-10-21 이후 뷰어 화면에서 사용하지 않음, 호환용) 로고 클릭 메뉴. 파일 열기는 `onDocumentOpen` 버튼을 씁니다. 항목 `{ id, label, icon?, value?, checked?, disabled?, hidden?, onSelect? }` | | `logo?`, `logoSrc?`, `logoAlt?`, `onHomeClick?` | `SWHeader`와 같은 로고 Props. **주지 않으면 아무것도 그리지 않습니다**(“HM” 문자·기본 아이콘 없음, 로고 면은 사선 끝 24px만). 주면 72px 면(로고가 더 넓으면 그만큼), 높이 최대 24px | -| `trailing?: ReactNode` | 오른쪽 끝 자유 영역: `SWViewerStatus`, `SWViewerHeaderButton`, 창 최소화·닫기 버전이면 맨 끝에 `SWHeaderWindowControls`(§2.1). 이 세 부품은 `SWHeader` 창 영역처럼 전체 높이(48px)의 칸이 이어진 띠로 그려집니다: 같은 면, 칸 앞 1px 구분선(라이선스 버튼 앞뒤에는 없음), 위·아래 경계는 객체 면 테두리 하나라 모든 칸의 위·아래가 같음, 칸 사이 여백 없음, 객체 면 오른쪽 끝까지(2026-10-21). 상태 칸은 표시 전용, 아이콘 버튼(정보·라이선스) 칸은 테두리·선 없음 | +| `trailing?: ReactNode` | 오른쪽 끝 자유 영역: `SWViewerStatus`, `SWViewerHeaderButton`, 창 최소화·닫기 버전이면 맨 끝에 `SWHeaderWindowControls`(§2.1). 이 세 부품은 `SWHeader` 창 영역처럼 전체 높이(48px)의 칸이 이어진 띠로 그려집니다: 같은 면, 칸 앞 1px 구분선(라이선스 버튼 앞뒤에는 없음), 위·아래 경계는 객체 면 테두리 하나라 모든 칸의 위·아래가 같음, 칸 사이 여백 없음, 객체 면 오른쪽 끝까지(2026-10-21). 메뉴 면은 띠 앞에서 `SWHeader` 객체 영역처럼 오른쪽 라운드(`--hm-sw-header-object-radius` 6px, 같은 면·테두리)로 끝나고 2px 간격 뒤에 띠가 옵니다. 상태 칸은 표시 전용, 아이콘 버튼(정보·라이선스) 칸은 테두리·선 없음 | | `moreLabel?`, `toolbarLabel?`, `
` 속성, `ref` | | - **반응형(2026-10-21, 실제 가용 폭 기준 3단계):** 고정 Breakpoint를 쓰지 않고 Header 행의 실제 남는 폭(로고·파일명·메뉴·Gap·Padding·More 버튼 포함)을 측정합니다. 파일명 면은 글자 수가 아니라 Header 폭으로 정한 기본 폭으로 계산하므로, 같은 폭에서는 짧은 파일명과 긴 파일명의 단계·파일 열기 버튼·파일명 면 너비가 같고 긴 이름은 `…`입니다(2026-10-21). ① 모두 들어가면 기본 형태(아이콘+텍스트), ② 부족하면 `icon`과 `iconOnlyBelow`가 있는 항목을 모두 아이콘만(라벨은 Tooltip·접근성 이름), ③ 그래도 부족하면 `collapseBelow` 항목을 하나씩 More(⋯, 베이지 260px Dropdown)로 옮깁니다(값이 큰 것부터, 같으면 오른쪽 먼저). 넓어지면 More → 아이콘 → 기본 순서로 복귀하고, 상단과 More에 같은 항목이 동시에 나오지 않습니다. `collapseBelow`·`iconOnlyBelow`의 숫자는 px Breakpoint가 아니며 순서만 정합니다(`iconOnlyBelow`는 지정 여부만, `collapseBelow`는 값이 큰 것부터; Props 형식 동일). 로고 옆 파일 열기 버튼(`onDocumentOpen`)은 ② 단계에서 아이콘만으로 줄고 More로 옮기지 않으며, 샘플은 `documents`. 레이어·설정·속성은 `collapseBelow` 없이 아이콘까지만. 1180px 미만 간격 2px, 560px 미만 상태 글자 숨김(CSS container)은 그대로입니다. 가로 스크롤은 만들지 않습니다. diff --git a/packages/sw/src/theme/sw-viewer.css b/packages/sw/src/theme/sw-viewer.css index b5fdc17..0812e69 100644 --- a/packages/sw/src/theme/sw-viewer.css +++ b/packages/sw/src/theme/sw-viewer.css @@ -6,7 +6,8 @@ :is(.hm-sw-header, .hm-sw-theme):not(:where(.hm-sw-header, .hm-sw-theme) *) { /* Header */ --hm-sw-viewer-height: 52px; - --hm-sw-viewer-background: linear-gradient(180deg, #266651 0%, #123328 45%, #0a221d 100%); + /* 2026-10-21 user request: the solid color of the Storybook viewer canvas (--sw-story-canvas base #0a0a0b), no gradient. */ + --hm-sw-viewer-background: #0a0a0b; --hm-sw-viewer-border-color: #071e18; --hm-sw-viewer-brand-width: 72px; /* No logo: no text or icon is drawn; the cap keeps only its slanted edge (no wide empty space). */ @@ -418,6 +419,33 @@ button.hm-sw-viewer-header__brand { text-align: left; } /* No line at all on the button (2026-10-21 final): same 48px box and edges as minimize / close, the strip face only. */ box-shadow: none; } +/* Rounded end of the menu surface before the strip (2026-10-21 user request, as SWHeader: object area radius 6px, + 2px header gap, then the window area). The first strip segment paints, just left of itself and over the object + border band, the header background (gap + the corners outside the radius) and on top the surface end: the same + object face (a vertical gradient, so the copy lines up) and border with the right radius. The 8px it covers are + the empty group padding + gap; below 1180px (2px each) the segment keeps 4px more room. Heights are unchanged. */ +.hm-sw-viewer-header .hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons):not(:is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons) + *) { position: relative; } +.hm-sw-viewer-header .hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons):not(:is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons) + *)::before, +.hm-sw-viewer-header .hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons):not(:is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons) + *)::after { + content: ""; + position: absolute; + top: calc(-1 * var(--hm-sw-header-object-border-width, 1px)); + bottom: calc(-1 * var(--hm-sw-header-object-border-width, 1px)); + left: calc(-1 * (var(--hm-sw-header-object-radius) + 2px)); + pointer-events: none; +} +.hm-sw-viewer-header .hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons):not(:is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons) + *)::before { + width: calc(var(--hm-sw-header-object-radius) + 2px); + background: var(--hm-sw-viewer-background); +} +.hm-sw-viewer-header .hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons):not(:is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons) + *)::after { + box-sizing: border-box; + width: var(--hm-sw-header-object-radius); + border: 1px solid var(--hm-sw-header-object-border-color); + border-left: 0; + border-radius: 0 var(--hm-sw-header-object-radius) var(--hm-sw-header-object-radius) 0; + background: var(--hm-sw-header-object-background); +} /* No line around the license / info button: the window buttons right after it drop their line before it. */ .hm-sw-viewer-header .hm-sw-viewer-header__actions > .hm-sw-viewer-header__button + .hm-sw-header__window-buttons { box-shadow: none; } @@ -428,6 +456,7 @@ button.hm-sw-viewer-header__brand { text-align: left; } .hm-sw-viewer-header__actions { padding-left: 2px; gap: 2px; } .hm-sw-viewer-status { padding-inline: 2px; } .hm-sw-viewer-header__actions > .hm-sw-viewer-status { padding-inline: 8px; } + .hm-sw-viewer-header .hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons):not(:is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons) + *) { margin-left: 4px; } .hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons) + :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons) { margin-left: -2px; } } @container hm-sw-viewer (max-width: 559.98px) {