feat(sw): bold 9-slice footer tabs and viewer panel bottom edge
User-approved 2026-10-22 SW changes (CSS only, public API unchanged). - Footer Model / Layout tab labels 700 in every state, letter-spacing -5%; the name-edit input keeps 400. - Slanted Layout tab and Model tab draw their SVG assets as 9-slice border-images: 97px / 73px stay the minimum, the slant and corners keep their size and only the flat middle grows with a longer label (max 12em, then ellipsis, like the rounded tabs). Hover surfaces use a union mask of the same asset (no mask-border), so Chromium and Firefox match. The default width renders pixel-identical to the previous drawing; the 8px slant / Model joint is unchanged. - Viewer panel body: the 7px bottom padding (inside the scrollport) becomes a 2px transparent bottom border, so scrolled rows stop 2px above the frame border instead of running into it. SWPopup keeps its 7px. - AI docs: DESIGN_RULES, AI_USAGE, FOOTER, VIEWER updated. - Tests: text-weight and footer-design-review expect the 700 labels and read the Model surface from the 9-slice ::before.
This commit is contained in:
1 parent
449398e3de
commit
fc65b2e836
8 files changed
+88
-36
No files matched your search
@@ -104,7 +104,7 @@ import "@hm-design/sw/styles.css" // 앱 진입점에서 한 번
|
||||
- SVG·PNG 자산은 JS에 data URI로 포함되어 별도 설정이 없다.
|
||||
- SW 기본 글꼴은 Noto Sans KR이다(`--hm-sw-header-font-family: "Noto Sans KR", "Malgun Gothic", "Apple SD Gothic Neo", sans-serif`, 2026-10-20 확정 — 같은 날의 Dotum 기본값은 철회, Figma의 Dotum은 의도적으로 대체). 패키지는 글꼴 파일을 포함하지 않으며 앱이 Noto Sans KR(400/500/700)을 로드한다. 없으면 Malgun Gothic → Apple SD Gothic Neo로 대체된다. Dotum을 기본값으로 다시 넣지 않는다.
|
||||
- SW 화면은 `SWTheme`(또는 SW 컴포넌트 루트)와 SW 토큰의 글꼴을 그대로 쓴다. Web UI도 Noto Sans KR이지만 Web 토큰(`--hm-font-family-base`)이나 Web `theme.css`의 글꼴 설정을 SW 화면에 복사하지 않고, SW 컴포넌트에 `font-family`를 개별 지정하지 않는다. 프로그램(사용자)이 다른 글꼴을 정한 경우에만 §3.1 방법으로 `--hm-sw-header-font-family`를 바꾸고 결과 보고에 "글꼴 변경"으로 적는다.
|
||||
- Weight 범위(2026-10-20 사용자 확정): SWHeader 안의 필드 값은 400, Composed Controls(SWHeader 밖에 단독 배치한 `SWHeaderLayerMenu` 등)의 필드 값은 700. 파일명·Footer 보조기능/명령어 제목·Footer 분류 탭·Viewer Header 파일명/버튼 라벨/Model·Layout·최근파일 이름/경로/미리보기 제목은 700, Footer `전체보기`·Model/Layout 탭(Footer)·명령·보조기능 라벨은 400. 임의로 일괄 변경하지 않는다. Figma 고정 폭 칸(예: 가중치·스케일 38px)에 그보다 긴 값(예: `ByLayer`)이 들어가면 말줄임 + `title`이 된다. Figma 레이어 이름의 `Dotum/bold/14`를 보고 700으로 바꾸거나 겹침·외곽선을 따로 만들지 않는다.
|
||||
- Weight 범위(2026-10-20 사용자 확정): SWHeader 안의 필드 값은 400, Composed Controls(SWHeader 밖에 단독 배치한 `SWHeaderLayerMenu` 등)의 필드 값은 700. 파일명·Footer 보조기능/명령어 제목·Footer 분류 탭·Viewer Header 파일명/버튼 라벨/Model·Layout·최근파일 이름/경로/미리보기 제목, Footer Model/Layout 탭 라벨(모든 상태, 2026-10-22 사용자 확정, 이름 편집 입력은 400)은 700, Footer `전체보기`·명령·보조기능 라벨은 400. 임의로 일괄 변경하지 않는다. Figma 고정 폭 칸(예: 가중치·스케일 38px)에 그보다 긴 값(예: `ByLayer`)이 들어가면 말줄임 + `title`이 된다. Figma 레이어 이름의 `Dotum/bold/14`를 보고 700으로 바꾸거나 겹침·외곽선을 따로 만들지 않는다.
|
||||
- CSS는 전역(`html`, `body`, `*`)을 건드리지 않는다. `body` margin 등 페이지 기본값은 소비 앱이 정한다.
|
||||
- 스크롤바: SW 전용 스크롤바 디자인은 없다. 기존 SW 컴포넌트는 브라우저 기본 스크롤바를 쓰며(속성Bar만 `scrollbar-width: thin`), Popup 본문 스크롤은 `SWPopup`이 처리한다. 새 스크롤 영역에 임의 스크롤바 색·두께를 만들지 않는다.
|
||||
- TypeScript에서 CSS side-effect import 오류가 나면 Vite의 `vite/client` 타입을 포함한다.
|
||||
|
||||
@@ -200,9 +200,9 @@
|
||||
| 빈 칸 | 68 × 46(위 1), + `#BEB5A6`. Hover: 주황 + 와 주황 그라데이션 점선 링(자산) | — |
|
||||
| 가이드 | 208 × 149, 빈 칸 위 4px 가운데, 1px `#DA6813` radius 2, 면 `#0E3C2E`, 미리보기 194 × 111(5, 5, 검정 1px), 제목 14px 외곽선, 캡션 12px `#B6D0C9` 1px 외곽선 | `--hm-sw-footer-guide-*` |
|
||||
| 분류 행 | 높이 24, 면 `#282216` → 위 14% `#574B30`, 안쪽 1px 테두리 그라데이션, 현재 탭 Pocket(좌우 7px 곡선, 아래 radius 2, 가장자리 `#675A3B`). 탭 14px `#D4CBBD`, 자간 10%, 위 4, 좌우 padding 8, 최소 43. 구분선 1 × 10 검정(위 7). Hover·현재 `#FF5C00` | `--hm-sw-footer-menu-*` |
|
||||
| Layout 탭 | 높이 22, 12px 자간 2%. Default 면 `#C3B79E` 글자 `#161410`. On: `linear-gradient(0deg, #7C6C4C, #7C6C4C 85%, #A6946F)` 흰 글자. 이름 편집: `#0F0D0A` 빨강 `#E60403` 주황 테두리. Hover·On·편집 글자 아래 70% 검정 1.3px 외곽선. 둥근 탭: 1px `rgba(0,0,0,.05)`(Hover·편집 `#DA6813`), 위 radius 4, 안쪽 그림자 `inset 0 -3px 4px rgba(56,48,31,.25)`, ×(16) Hover 노랑. 사선 탭(마지막): 97 × 22 자산, On hover = 베이지 면 + 주황 글자, ×(28, 빨강 사선). Hover(선택 아님, 두 모양 모두, 2026-10-20) = Model과 같은 `gra-border-orange` 면(사선 탭은 자산 모양 안)만 바뀌고 글자는 Default 그대로(`#161410` 12px 400, 외곽선 없음, 위치·크기 불변) | `--hm-sw-footer-tab-*`, `--hm-sw-footer-gra-border-orange` |
|
||||
| Layout 탭 | 높이 22, 12px 자간 -5%(Model 포함, 2026-10-22; 이전 2%), 라벨 700(모든 상태, 2026-10-22; 이름 편집 입력은 400). Default 면 `#C3B79E` 글자 `#161410`. On: `linear-gradient(0deg, #7C6C4C, #7C6C4C 85%, #A6946F)` 흰 글자. 이름 편집: `#0F0D0A` 빨강 `#E60403` 주황 테두리. Hover·On·편집 글자 아래 70% 검정 1.3px 외곽선. 둥근 탭: 1px `rgba(0,0,0,.05)`(Hover·편집 `#DA6813`), 위 radius 4, 안쪽 그림자 `inset 0 -3px 4px rgba(56,48,31,.25)`, ×(16) Hover 노랑. 사선 탭(마지막): 97 × 22 자산(9-slice, 2026-10-22: 97px·라벨 57px가 최소, 라벨이 길면 사선 21px·모서리 4px는 고정하고 가운데만 늘어남, 라벨 최대 12em), On hover = 베이지 면 + 주황 글자, ×(28, 빨강 사선). Hover(선택 아님, 두 모양 모두, 2026-10-20) = Model과 같은 `gra-border-orange` 면(사선 탭은 자산 모양 안)만 바뀌고 글자는 Default 그대로(`#161410` 12px 700, 외곽선 없음, 위치·크기 불변) | `--hm-sw-footer-tab-*`, `--hm-sw-footer-gra-border-orange` |
|
||||
| `+` 버튼 | Layout 옆 `+`(24 × 22) Hover = `gra-border-orange` 면 + `+`(분류 행 `+` 자산). 분류 행 `+`(28 × 12) Hover = `+` 채움 `#FF5C00`(검정 외곽선 그대로) | `--hm-sw-footer-gra-border-orange`, `--hm-sw-footer-accent` |
|
||||
| Model 탭 | 73 × 22 자산(왼쪽 사선), 글자 왼쪽 22. Hover·On = 자산 모양 안 `gra-border-orange` 면 + 검정 `#161410` 글자(12px 400, 외곽선 없음)(2026-10-20 사용자 확정, 이전 Figma On 갈색·흰 글자 대체). 앞 사선 탭과 8px 겹침 | `--hm-sw-footer-gra-border-orange` |
|
||||
| Model 탭 | 73 × 22 자산(왼쪽 사선, 9-slice: 73px 최소, 긴 라벨이면 사선 21px 고정·가운데만 늘어남, 라벨 최대 12em, 2026-10-22), 글자 왼쪽 22. Hover·On = 자산 모양 안 `gra-border-orange` 면 + 검정 `#161410` 글자(12px, 외곽선 없음)(2026-10-20 사용자 확정, 이전 Figma On 갈색·흰 글자 대체). 라벨은 모든 상태 700(2026-10-22). 앞 사선 탭과 8px 겹침 | `--hm-sw-footer-gra-border-orange` |
|
||||
| 명령창 | 566 × 71 / 270, 위 radius 4, 위 71px `linear-gradient(180deg, #635537, #262218 5%, #281F16 10%, #282216)` + 아래 `#282216`. 12px 자간 10% 줄 14, 1px 외곽선. 입력 칸 23(좌우 4) `#16130D` + `0 -1px 2px rgba(255,255,255,.25)`, `inset 0 1px 4px #000`. 화살표 18 × 18 검정 radius 2(4, 4 오른쪽 위) | `--hm-sw-footer-command-*` |
|
||||
| 바 안 명령창(실제 화면) | 보조기능 뒤 4px, 바 아래에 붙어 펼치면 위로 270. 남는 폭(바 − 보조기능 영역 − 명령어 전체보기)을 칸 영역 0.57 : 명령창 나머지(최소 280)로 나눔 — 칸 수는 바 폭으로만 계산, 분류의 명령 수와 무관(1920 → 12칸) | `--hm-sw-footer-command-min-width`, `--hm-sw-footer-slot-share` |
|
||||
| 좌표(실제 화면) | 바 위 왼쪽, 12px `#F6F4F2` 1px 외곽선, 아래 3px | — |
|
||||
|
||||
@@ -71,6 +71,7 @@ import "@hm-design/sw/styles.css"
|
||||
- `SWFooter`·`SWFooterCommandLine`은 루트에 `hm-sw-theme`이 있어 `<SWTheme>`가 필요 없습니다. 나머지 부품은 `<SWTheme>` 안에 둡니다.
|
||||
- `SWFooterMenu`의 Pocket은 부모 배경(명령 칸 면)을 비춰 보입니다. 단독으로 둘 때 부모에 `background: var(--hm-sw-footer-group-background)`를 줍니다.
|
||||
- `SWFooterLayoutTabs`의 마지막 탭은 사선 모양(Figma layout01, × = btn_close2), 나머지는 둥근 모양(layout02/03, × = btn_delete)입니다. `shape`로 바꿀 수 있습니다. 탭 순서는 배열 순서 그대로(Figma 예시: `+`, Layout 03, 02, 01, Model).
|
||||
- 탭 글자(2026-10-22): Model·Layout 라벨은 모든 상태 12px **700**, 자간 **-5%**. 이름 편집 입력은 400. 탭 폭은 라벨에 맞춰 늘어납니다(패키지 CSS가 처리): 사선 탭 97px·Model 73px·둥근 탭은 기본 폭이 최소이고, 사선·모서리는 그대로 가운데만 늘어나며(9-slice), 라벨은 최대 12em 이후 말줄임. 폭·자간을 소비 CSS로 덮지 않습니다.
|
||||
- `SWFooterCommandLine`은 `SWFooter`의 `command`에 넣는 것이 기본 구성입니다(실제 화면). 바 밖에 두면 위치(보통 도면 영역 왼쪽 아래)는 소비 레이아웃이 정합니다. 접힘 = 마지막 3줄, 펼침 = 전체 기록 스크롤.
|
||||
|
||||
## 4. 상태 (Figma Variant ↔ Props)
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
- Escape는 Focus가 패널 안에 있을 때 그 패널만 닫습니다. 비모달 패널은 바깥 클릭으로 닫히지 않습니다(참고 뷰어와 동일, 도구 창). 마지막으로 누른 패널이 맨 앞으로 옵니다.
|
||||
- **콘텐츠 우선순위:** `children`·`footer`는 타입상 `ReactNode`이고 소스 주석도 자유 콘텐츠(“any component”)라고 적습니다. 그래도 기본은 `SWViewerSection`·`SWViewerField`·`SWViewerSwitch`·`SWViewerSlider`·`SWViewerInput`·`SWViewerButton`·`SWViewerBadge`·`SWViewerHint`와 선택 목록 `SWSelect`(축척 지정 단위, 뷰어 패널 토큰으로 표시) 우선입니다. 이것으로 안 되는 컨트롤(예: Select·Checkbox·표)만 `@hm-design/ui` 컴포넌트나 소비 프로젝트 요소를 넣을 수 있습니다. `@hm-design/ui`는 SW 패키지 dependency가 아니어서 별도 설치(사용 유형 혼합)가 필요하고, `SWPopup`과 달리 뷰어 패널 안 `@hm-design/ui` 외형 정합성은 문서화·검증 기록이 없으므로 결과 보고에 “디자인 확인 필요”로 적습니다. Header 안에는 `@hm-design/ui` DropdownMenu 등을 직접 두지 않습니다(§1 표).
|
||||
- **콘텐츠 교체:** 같은 프레임에 다른 `children`을 넣습니다(레이어 목록 / 옵션 / 속성 / 입력 폼 — Storybook `SW/Viewer › Settings Popup`). 프레임을 복사하거나 `hm-sw-viewer-*` class로 새 팝업을 만들지 않습니다.
|
||||
- **본문 하단 경계(2026-10-22):** 뷰어 패널 본문은 아래 7px 여백 대신 스크롤 영역 밖의 2px 투명 경계를 둡니다(패키지 CSS). 스크롤된 목록 행은 테두리 2px 위에서 잘리고, 맨 아래로 스크롤하면 마지막 행이 다 보입니다. 소비 코드에서 하단 여백·`padding`을 덧대지 않습니다. 일반 `SWPopup`은 7px 그대로입니다.
|
||||
|
||||
### 4.1 레이어 Popup — 개별 표시·숨김은 전구 아이콘 버튼, 전체는 Switch (필수, 2026-10-22)
|
||||
|
||||
|
||||
@@ -424,10 +424,13 @@
|
||||
height: 22px;
|
||||
min-width: 69px;
|
||||
padding: 0 4px 0 8px;
|
||||
/* -5% (2026-10-22 user-set, was the row's 2%): the Bold labels fit the fixed 57px slanted label box. */
|
||||
letter-spacing: -0.05em;
|
||||
white-space: nowrap;
|
||||
line-height: 14px;
|
||||
}
|
||||
.hm-sw-footer-tab__label { display: block; }
|
||||
/* Model / Layout labels are Bold in every state (2026-10-22 user request); the name-edit input keeps 400. */
|
||||
.hm-sw-footer-tab__label { display: block; font-weight: 700; }
|
||||
.hm-sw-footer-tab__input {
|
||||
width: 57px;
|
||||
height: 16px;
|
||||
@@ -469,33 +472,55 @@
|
||||
/* Hover, not selected (2026-10-20, user request): the Model orange surface; text as on Model (below). */
|
||||
.hm-sw-footer-tab[data-shape="rounded"][data-state="off"]:hover { background: var(--hm-sw-footer-gra-border-orange); }
|
||||
|
||||
/* Slanted tab (layout01, next to Model): 97 × 22 mirrored bg asset + the 28px red close piece. */
|
||||
.hm-sw-footer-tab[data-shape="slant"] { width: 97px; }
|
||||
.hm-sw-footer-tab[data-shape="slant"] .hm-sw-footer-tab__label { width: 57px; overflow: hidden; text-overflow: ellipsis; }
|
||||
/* Slanted tab (layout01, next to Model): 97 × 22 mirrored bg asset + the 28px red close piece.
|
||||
9-slice (2026-10-22): 97px / 57px label stay the minimum and the tab grows with a longer label. Each state asset is
|
||||
drawn as a border-image: the 21px slant and the 4px corner keep their size, only the flat middle stretches (the
|
||||
asset gradients are vertical, so stretching changes nothing else). Border-image works in every target browser;
|
||||
mask-border (Firefox: none) is not used. */
|
||||
.hm-sw-footer-tab[data-shape="slant"] { min-width: 97px; }
|
||||
.hm-sw-footer-tab[data-shape="slant"] .hm-sw-footer-tab__label { min-width: 57px; max-width: 12em; overflow: hidden; text-overflow: ellipsis; }
|
||||
.hm-sw-footer-tab[data-shape="slant"]::before,
|
||||
.hm-sw-footer-tab[data-shape="slant"]::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
box-sizing: border-box;
|
||||
transform: scaleX(-1);
|
||||
background: url("../components/sw-footer/assets/tab-slant.svg") 0 0 / 97px 22px no-repeat;
|
||||
border: solid transparent;
|
||||
border-width: 0 4px 0 21px;
|
||||
border-image: url("../components/sw-footer/assets/tab-slant.svg") 0 4 0 21 fill / 0 4px 0 21px stretch;
|
||||
pointer-events: none;
|
||||
}
|
||||
.hm-sw-footer-tab[data-shape="slant"]::after { z-index: 2; background-image: none; }
|
||||
.hm-sw-footer-tab[data-shape="slant"]::after { z-index: 2; border-image-source: none; }
|
||||
.hm-sw-footer-tab[data-shape="slant"] .hm-sw-footer-tab__remove { width: 28px; background: url("../components/sw-footer/assets/tab-close.svg") 0 0 / 28px 22px no-repeat; }
|
||||
.hm-sw-footer-tab[data-shape="slant"] .hm-sw-footer-tab__remove:hover { background-image: url("../components/sw-footer/assets/tab-close-hover.svg"); }
|
||||
.hm-sw-footer-tab[data-shape="slant"][data-state="on"]::before { background-image: url("../components/sw-footer/assets/tab-slant-on.svg"); }
|
||||
.hm-sw-footer-tab[data-shape="slant"][data-state="editing"]::before { background-image: url("../components/sw-footer/assets/tab-slant-editing.svg"); }
|
||||
/* Hover, not selected (2026-10-20, user request): the Model orange surface in the slanted shape. */
|
||||
.hm-sw-footer-tab[data-shape="slant"][data-state="on"]::before { border-image-source: url("../components/sw-footer/assets/tab-slant-on.svg"); }
|
||||
.hm-sw-footer-tab[data-shape="slant"][data-state="editing"]::before { border-image-source: url("../components/sw-footer/assets/tab-slant-editing.svg"); }
|
||||
/* Hover, not selected (2026-10-20, user request): the Model orange surface in the slanted shape. The shape mask is the
|
||||
union of the asset at both ends (slant end, corner end) and a plain middle, so it follows any width without
|
||||
mask-border. */
|
||||
.hm-sw-footer-tab[data-shape="slant"][data-state="off"]:hover::before {
|
||||
background: var(--hm-sw-footer-gra-border-orange);
|
||||
-webkit-mask: url("../components/sw-footer/assets/tab-slant.svg") 0 0 / 97px 22px no-repeat;
|
||||
mask: url("../components/sw-footer/assets/tab-slant.svg") 0 0 / 97px 22px no-repeat;
|
||||
border-image-source: none;
|
||||
/* Longhands: a var() shorthand followed by background-origin does not survive CSSOM re-serialization. */
|
||||
background-image: var(--hm-sw-footer-gra-border-orange);
|
||||
background-origin: border-box;
|
||||
-webkit-mask:
|
||||
url("../components/sw-footer/assets/tab-slant.svg") 0 0 / 97px 22px no-repeat,
|
||||
url("../components/sw-footer/assets/tab-slant.svg") 100% 0 / 97px 22px no-repeat,
|
||||
linear-gradient(#000 0 0) 21px 0 / calc(100% - 25px) 22px no-repeat;
|
||||
mask:
|
||||
url("../components/sw-footer/assets/tab-slant.svg") 0 0 / 97px 22px no-repeat,
|
||||
url("../components/sw-footer/assets/tab-slant.svg") 100% 0 / 97px 22px no-repeat,
|
||||
linear-gradient(#000 0 0) 21px 0 / calc(100% - 25px) 22px no-repeat;
|
||||
-webkit-mask-origin: border-box;
|
||||
mask-origin: border-box;
|
||||
-webkit-mask-clip: border-box;
|
||||
mask-clip: border-box;
|
||||
}
|
||||
.hm-sw-footer-tab[data-shape="slant"][data-state="off"]:hover::after { background-image: url("../components/sw-footer/assets/tab-slant-ring.svg"); }
|
||||
.hm-sw-footer-tab[data-shape="slant"][data-state="off"]:hover::after { border-image-source: url("../components/sw-footer/assets/tab-slant-ring.svg"); }
|
||||
/* layout01 Variant6 (editing + hover): the fill without its own ring and the orange ring on top. */
|
||||
.hm-sw-footer-tab[data-shape="slant"][data-state="editing"]:hover::before { background-image: url("../components/sw-footer/assets/tab-slant-editing-fill.svg"); }
|
||||
.hm-sw-footer-tab[data-shape="slant"][data-state="editing"]:hover::after { background-image: url("../components/sw-footer/assets/tab-slant-ring.svg"); }
|
||||
.hm-sw-footer-tab[data-shape="slant"][data-state="editing"]:hover::before { border-image-source: url("../components/sw-footer/assets/tab-slant-editing-fill.svg"); }
|
||||
.hm-sw-footer-tab[data-shape="slant"][data-state="editing"]:hover::after { border-image-source: url("../components/sw-footer/assets/tab-slant-ring.svg"); }
|
||||
.hm-sw-footer-tab[data-shape="slant"] + .hm-sw-footer-tab--model { margin-left: -9px; }
|
||||
|
||||
/* Text per state (Figma layout01 / layout02 variants). */
|
||||
@@ -518,26 +543,43 @@
|
||||
.hm-sw-footer-tab:is([data-shape="rounded"], [data-shape="slant"])[data-state="off"]:hover,
|
||||
.hm-sw-footer-tab--model:is([data-state="on"], :hover) { color: var(--hm-sw-footer-tab-text); }
|
||||
/* layout01 "on hover" draws the default beige surface under the orange text. */
|
||||
.hm-sw-footer-tab[data-shape="slant"][data-state="on"]:hover::before { background-image: url("../components/sw-footer/assets/tab-slant.svg"); }
|
||||
.hm-sw-footer-tab[data-shape="slant"][data-state="on"]:hover::before { border-image-source: url("../components/sw-footer/assets/tab-slant.svg"); }
|
||||
/* The × appears on hover (all shapes) and in no other state (Figma opacity 0). */
|
||||
.hm-sw-footer-tab:hover .hm-sw-footer-tab__remove { opacity: 1; }
|
||||
.hm-sw-footer-tab .hm-sw-footer-tab__remove:disabled { opacity: 0; cursor: default; }
|
||||
|
||||
/* Model (26:9914): 73 × 22 asset, label 22px from the left. */
|
||||
.hm-sw-footer-tab--model { width: 73px; background: url("../components/sw-footer/assets/tab-model.svg") 0 0 / 73px 22px no-repeat; }
|
||||
/* Selected and hovered (2026-10-20, user request; was the brown model On 26:9925): the gra-border-orange surface in the
|
||||
Model shape. */
|
||||
.hm-sw-footer-tab--model:is([data-state="on"], :hover) { background-image: none; }
|
||||
.hm-sw-footer-tab--model:is([data-state="on"], :hover)::before {
|
||||
/* Model (26:9914): 73 × 22 asset, label 22px from the left. 9-slice (2026-10-22, see the slanted tab): 73px stays the
|
||||
minimum; the 21px slant and the 1px right edge keep their size, the flat middle stretches with a longer label. */
|
||||
.hm-sw-footer-tab--model { min-width: 73px; }
|
||||
.hm-sw-footer-tab--model::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--hm-sw-footer-gra-border-orange);
|
||||
-webkit-mask: url("../components/sw-footer/assets/tab-model.svg") 0 0 / 73px 22px no-repeat;
|
||||
mask: url("../components/sw-footer/assets/tab-model.svg") 0 0 / 73px 22px no-repeat;
|
||||
box-sizing: border-box;
|
||||
border: solid transparent;
|
||||
border-width: 0 1px 0 21px;
|
||||
border-image: url("../components/sw-footer/assets/tab-model.svg") 0 1 0 21 fill / 0 1px 0 21px stretch;
|
||||
pointer-events: none;
|
||||
}
|
||||
.hm-sw-footer-tab--model .hm-sw-footer-tab__select { min-width: 0; width: 100%; padding: 0 16px 0 22px; }
|
||||
/* Selected and hovered (2026-10-20, user request; was the brown model On 26:9925): the gra-border-orange surface in the
|
||||
Model shape (union mask: the asset at the slant end + a plain rest, square right end). */
|
||||
.hm-sw-footer-tab--model:is([data-state="on"], :hover)::before {
|
||||
border-image-source: none;
|
||||
background-image: var(--hm-sw-footer-gra-border-orange);
|
||||
background-origin: border-box;
|
||||
-webkit-mask:
|
||||
url("../components/sw-footer/assets/tab-model.svg") 0 0 / 73px 22px no-repeat,
|
||||
linear-gradient(#000 0 0) 21px 0 / calc(100% - 21px) 22px no-repeat;
|
||||
mask:
|
||||
url("../components/sw-footer/assets/tab-model.svg") 0 0 / 73px 22px no-repeat,
|
||||
linear-gradient(#000 0 0) 21px 0 / calc(100% - 21px) 22px no-repeat;
|
||||
-webkit-mask-origin: border-box;
|
||||
mask-origin: border-box;
|
||||
-webkit-mask-clip: border-box;
|
||||
mask-clip: border-box;
|
||||
}
|
||||
.hm-sw-footer-tab--model .hm-sw-footer-tab__select { flex: 1 0 auto; min-width: 0; padding: 0 16px 0 22px; }
|
||||
.hm-sw-footer-tab--model .hm-sw-footer-tab__label { max-width: 12em; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
/* ---------- Command line ---------- */
|
||||
.hm-sw-footer-command {
|
||||
|
||||
@@ -552,6 +552,10 @@ button.hm-sw-viewer-header__brand { text-align: left; }
|
||||
/* ---------- Panel (SWPopup frame + viewer window behavior) ---------- */
|
||||
.hm-sw-viewer-panel-backdrop { inset: 0; background: var(--hm-sw-viewer-panel-overlay); }
|
||||
.hm-sw-viewer-panel[data-collapsed] .hm-sw-property__body { display: none; }
|
||||
/* Bottom gutter outside the scrollport (2026-10-22, user-set 2px): the SWPopup body's 7px bottom padding is inside the
|
||||
scroll area, so scrolled rows ran into it up to the frame border. A transparent border instead clips the list
|
||||
2px above the frame border. */
|
||||
.hm-sw-viewer-panel .hm-sw-property__body { padding-bottom: 0; border-bottom: 2px solid transparent; }
|
||||
.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
|
||||
|
||||
@@ -78,7 +78,8 @@ facts.addCallsBefore = addCalls
|
||||
|
||||
// 3. Model tab: selected vs not selected, Layout text rule
|
||||
const tabs = footer.locator('.hm-sw-footer-tab')
|
||||
const tabInfo = async loc => ({ tab: await style(loc), before: await style(loc, '::before'), text: await style(loc.locator('.hm-sw-footer-tab__label, .hm-sw-footer-tab__select').first()) })
|
||||
// 2026-10-22: the label (Bold 700) is the text; the 9-slice tab surface is the ::before border-image.
|
||||
const tabInfo = async loc => ({ tab: await style(loc), before: await style(loc, '::before'), surface: await loc.evaluate(e => getComputedStyle(e, '::before').borderImageSource), text: await style(loc.locator('.hm-sw-footer-tab__label').first()) })
|
||||
const model = footer.locator('.hm-sw-footer-tab--model').first()
|
||||
const modelState = await model.getAttribute('data-state')
|
||||
facts.model = { initialState: modelState, selected: null, unselected: null }
|
||||
@@ -94,8 +95,8 @@ await model.locator('.hm-sw-footer-tab__select').click(); await away(); await pa
|
||||
facts.model.selected = await tabInfo(model); await shot(model, 'model-selected', 4)
|
||||
const mu = facts.model.unselected, ms = facts.model.selected
|
||||
check('Model selected: gra-border-orange surface (::before) masked to the model shape', ms.before.bg === GRADIENT && ms.before.mask.startsWith('url('), ms.before.bg.slice(0, 60))
|
||||
check('Model not selected: its own tab image surface (tab-model.svg, beige), no orange ::before', mu.tab.bg.startsWith('url(') && !mu.before.bg.includes('rgb(218, 104, 19)'), { tab: mu.tab.bg, before: mu.before.bg })
|
||||
check('Model text #161410 12px 400 no outline — selected, unselected and hover identical', [ms, mu, facts.model.unselectedHover].every(s => s.text.color === 'rgb(22, 20, 16)' && s.text.size === '12px' && s.text.weight === '400' && s.text.shadow === 'none') && ['x', 'y', 'w', 'h'].every(k => ms.text[k] === mu.text[k] && mu.text[k] === facts.model.unselectedHover.text[k]), [ms.text, mu.text].map(t => `${t.color} ${t.size} ${t.weight} ${t.font}`))
|
||||
check('Model not selected: its own tab image surface (tab-model.svg 9-slice on ::before, beige), no orange ::before', mu.surface.startsWith('url(') && !mu.before.bg.includes('rgb(218, 104, 19)'), { surface: mu.surface.slice(0, 40), before: mu.before.bg })
|
||||
check('Model text #161410 12px 700 no outline — selected, unselected and hover identical', [ms, mu, facts.model.unselectedHover].every(s => s.text.color === 'rgb(22, 20, 16)' && s.text.size === '12px' && s.text.weight === '700' && s.text.shadow === 'none') && ['x', 'y', 'w', 'h'].every(k => ms.text[k] === mu.text[k] && mu.text[k] === facts.model.unselectedHover.text[k]), [ms.text, mu.text].map(t => `${t.color} ${t.size} ${t.weight} ${t.font}`))
|
||||
// Layout default vs hover text (non-selected layouts)
|
||||
facts.layoutHover = []
|
||||
for (let i = 1; i < layoutCount; i++) {
|
||||
@@ -105,7 +106,7 @@ for (let i = 1; i < layoutCount; i++) {
|
||||
await t.hover(); await page.waitForTimeout(150)
|
||||
const h = await tabInfo(t); await shot(t, `layout-${i}-hover`, 4); await away()
|
||||
facts.layoutHover.push({ i, d, h })
|
||||
check(`Layout ${i} default/hover text: #161410 12px 400, same box (Noto Sans KR)`, [d, h].every(s => s.text.color === 'rgb(22, 20, 16)' && s.text.size === '12px' && s.text.weight === '400' && s.text.font === '"Noto Sans KR"') && ['x', 'y', 'w', 'h'].every(k => d.text[k] === h.text[k]), `${d.text.color} → ${h.text.color}, ${d.text.w}×${d.text.h}`)
|
||||
check(`Layout ${i} default/hover text: #161410 12px 700, same box (Noto Sans KR)`, [d, h].every(s => s.text.color === 'rgb(22, 20, 16)' && s.text.size === '12px' && s.text.weight === '700' && s.text.font === '"Noto Sans KR"') && ['x', 'y', 'w', 'h'].every(k => d.text[k] === h.text[k]), `${d.text.color} → ${h.text.color}, ${d.text.w}×${d.text.h}`)
|
||||
}
|
||||
// restore the initial selection
|
||||
if (modelState !== 'on') await layoutTabs.first().locator('.hm-sw-footer-tab__select').click()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// 2026-10-20 SW text weight request: Bold only on the named texts, with the black text outline where asked.
|
||||
// Header file name (700) · Composed Controls field values (700) vs SWHeader field values (400) · Footer 보조기능 / 명령어
|
||||
// titles and category tabs (700, 전체보기 / Model · Layout / commands unchanged) · Viewer header file name, labels and
|
||||
// titles and category tabs (700, 전체보기 / commands unchanged; Model · Layout tabs 700 since 2026-10-22) · Viewer header file name, labels and
|
||||
// Model / Layout tabs (700, More kept; 2026-10-21 logo menu and sample list follow the SWHeader `...` menu / file-name
|
||||
// list rules) · recent files name / path / preview title (700 + outline). Hover keeps the
|
||||
// text in place. Usage: build Storybook first, then node packages/sw/tests/text-weight.browser.mjs [evidence-dir]
|
||||
@@ -99,8 +99,11 @@ const pocket = await work.locator('.hm-sw-footer-menu').evaluate(m => { const t
|
||||
return [under(t.x + 3), under(t.x + t.width / 2), under(t.right - 3)] })
|
||||
check('Footer: 수정 pocket surface stays continuous under the Bold tab', pocket.every(p => p === 'hm-sw-footer__group'), pocket)
|
||||
await tabLoc.nth(0).click(); await away()
|
||||
const keep = await styles('.sw-footer-story-workspace :is(.hm-sw-footer-tab__select, .hm-sw-footer-tool__label, .hm-sw-footer-aux__label)')
|
||||
check('Footer: Model / Layout, command and auxiliary labels keep 400', keep.length > 0 && keep.every(k => k.weight === '400'), keep.filter(k => k.weight !== '400'))
|
||||
// 2026-10-22 user request: the Model / Layout tab labels are 700; command and auxiliary labels keep 400.
|
||||
const layoutTabs = await styles('.sw-footer-story-workspace .hm-sw-footer-tab__label')
|
||||
check(`Footer: Model / Layout tab labels (${layoutTabs.length}) 12px 700`, layoutTabs.length >= 3 && layoutTabs.every(k => k.weight === '700' && k.size === '12px'), layoutTabs.filter(k => k.weight !== '700' || k.size !== '12px'))
|
||||
const keep = await styles('.sw-footer-story-workspace :is(.hm-sw-footer-tool__label, .hm-sw-footer-aux__label)')
|
||||
check('Footer: command and auxiliary labels keep 400', keep.length > 0 && keep.every(k => k.weight === '400'), keep.filter(k => k.weight !== '400'))
|
||||
const area = await work.locator('.hm-sw-footer-menu__viewport').evaluate(e => ({ w: Math.round(e.getBoundingClientRect().width * 10) / 10, h: Math.round(e.getBoundingClientRect().height), content: e.scrollWidth, steps: [...e.parentElement.querySelectorAll('.hm-sw-footer-menu__step')].filter(s => s.getClientRects().length).length }))
|
||||
report.footerCategoryArea = area
|
||||
console.log('INFO Footer category area at 1920:', JSON.stringify(area))
|
||||
|
||||
Reference in new issue
Block a user