From e42b67c53a8cde4d6cb5aef3013106bd9ef9fd34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=88=98=ED=98=84?= Date: Mon, 9 Nov 2026 21:34:28 +0900 Subject: [PATCH] feat(ui): standardize LNB sizing and active depth effect - Adopt the official LNB desktop spec as package defaults: 240px column, 14px menu text, 48px 1-depth rows and 42px 2/3-depth rows, with new item/subitem padding-block and profile min-height tokens. - Match --hm-lnb-current-depth to the Storybook Manager selected item: sampled soft inset falloff (left/top/bottom) and a fade over the last quarter, keeping the concave corners and content join. - Drop the size overrides from the LNB Story CSS and update the LNB, GNB and Dashboard docs, AI guides and the theme verification baseline. --- docs/patterns/dashboard-layout.md | 6 +-- docs/patterns/gnb.md | 4 +- docs/patterns/lnb.md | 28 ++++++----- packages/ui/ai/COMPOSITION_PATTERNS.md | 1 + packages/ui/ai/DESIGN_RULES.md | 1 + packages/ui/src/theme/lnb.css | 66 +++++++++++++++++++++----- src/patterns/lnb.stories.css | 41 ++-------------- src/patterns/lnb.stories.tsx | 9 ++-- src/theme/verification-baseline.json | 14 ++++-- 9 files changed, 95 insertions(+), 75 deletions(-) diff --git a/docs/patterns/dashboard-layout.md b/docs/patterns/dashboard-layout.md index 2473a46..6321d2a 100644 --- a/docs/patterns/dashboard-layout.md +++ b/docs/patterns/dashboard-layout.md @@ -162,9 +162,9 @@ ui-viz에서 확인한 구조(실제 파일: `compositions/dashboard-variant-spe | 영역 | ui-viz 구조 | 현재 HM 대응 | 이번 구현 | |---|---|---|---| -| 조립 | Variant A(LNB 272 + 콘텐츠), Variant B(GNB 70 + 콘텐츠), Variant C(Compact 48). **GNB+LNB 동시 사용 예 없음** | `DashboardLayout` | slot 방식. 둘 다 / 하나만 | +| 조립 | Variant A(LNB 240 + 콘텐츠. 2026-11-09 이전 272), Variant B(GNB 70 + 콘텐츠), Variant C(Compact 48). **GNB+LNB 동시 사용 예 없음** | `DashboardLayout` | slot 방식. 둘 다 / 하나만 | | GNB | 70px green, 콘텐츠와 12px 접점 | `GNB` | slot `header` | -| LNB | 272px green, 콘텐츠와 18px 접점 | `LNB` | slot `sidebar` | +| LNB | 240px green(2026-11-09 이전 272), 콘텐츠와 18px 접점 | `LNB` | slot `sidebar` | | Content surface | `#F5F0E5`, padding 34, 접점 쪽 radius(LNB 18 / GNB 12), 그림자 `-8px 0 18px`(LNB) · `0 -8px 18px`(GNB) rgba(7,30,24,.18), 별도 border 없음 | 신규 | `DashboardMain` | | 스크롤 | "메뉴/헤더는 고정, 콘텐츠 영역에서만 스크롤"(Confirmed). `overflow-y:auto`, `overscroll-behavior: contain` | 신규 | `DashboardMain`이 스크롤 소유 | | Page heading | breadcrumb 12px(정적 `p`, 컴포넌트 아님) → 제목 28 → 설명, space-between·align end·gap 24, **하단 20px** | Breadcrumb + 신규 Pattern | `PageHeader` | @@ -260,7 +260,7 @@ Compact / Sidebar mobile hide: NONE | 항목 | ui-viz | 현재 HM | 분류 | |---|---|---|---| -| GNB 70 / LNB 272 | 같음 | `--hm-gnb-height` / `--hm-lnb-width` | MATCHED | +| GNB 70 / LNB 240 | 같음(LNB 폭은 2026-11-09 공식 규격 채택, ui-viz 272와 다름) | `--hm-gnb-height` / `--hm-lnb-width` | MATCHED | | 콘텐츠 면 색·padding 34 | `#F5F0E5`, 34 | 같음 | MATCHED | | 접점 radius (LNB 18 / GNB 12), shadow | 같음 | 같음. 소유를 Dashboard로 이동 | MATCHED | | 헤더·메뉴 고정, 콘텐츠만 스크롤 | 같음 | `DashboardMain` 스크롤 | MATCHED | diff --git a/docs/patterns/gnb.md b/docs/patterns/gnb.md index e075d17..5eb4f4c 100644 --- a/docs/patterns/gnb.md +++ b/docs/patterns/gnb.md @@ -183,7 +183,7 @@ src/patterns/gnb.stories.tsx 공용 `@hm-design/ui/theme.css`의 opt-in HTML attribute 계약: - 프레임: `data-hm-gnb-layout="sidebar"`. 소비자가 높이를 지정하며 Grid가 GNB의 자연 높이와 남은 본문 높이를 배분한다. -- 본문: 프레임의 직계 `data-hm-gnb-body`. Desktop은 기존 `--hm-lnb-width` 열 + 가용 콘텐츠 열, Mobile은 콘텐츠 한 열이다. +- 본문: 프레임의 직계 `data-hm-gnb-body`. Desktop은 기존 `--hm-lnb-width` 열 + 가용 콘텐츠 열, Mobile은 콘텐츠 한 열이다. 열 폭과 항목 크기는 LNB 공식 규격(2026-11-09: 240px, 14px, 1뎁스 48px·2/3뎁스 42px)을 그대로 따르며 GNB 전용 override는 두지 않는다. - Sidebar: 본문의 직계 ``. 기존 `data-slot="lnb"`는 유지한다. 이 Reference 범위에서 본문과 같은 배경, 세로 divider 한 개를 사용한다. Green은 Primary, `data-hm-color="brown"`은 기존 Brown 팔레트로 선택·Hover를 표현한다. 펼침 화살표는 유지하며 아이콘은 선택적으로 전달한다. 신규 AI GNB_ONLY에는 Sidebar를 추가하지 않는다. - Main: 본문의 직계 `
`. 공용 CSS가 면·접점·스크롤을 제공하며 콘텐츠 padding은 소비자가 정한다. GNB의 상단 접점과 LNB의 측면 접점은 기존 값을 재사용한다. @@ -284,4 +284,4 @@ export function Example() { 사용자 메뉴와 콘텐츠 padding은 기존 GNB 사용 예제에 따라 소비자가 추가한다. Theme Source, LNB 원본, Runtime 계약, 공개 API는 변경하지 않는다. 공통 요약은 [AGENTS.md](../../AGENTS.md), 탐색 경로는 [Reference Index](../guidelines/component-pattern-reference.md)를 따른다. -검증(2026-10-08, 기존 Storybook 6024): 1440px Sidebar 272px / Green Light 2, 2·3Depth 선택·그룹 Enter/Space·Hover·선택 경로·메뉴 스크롤 확인. 768px·375px에서 GNB 56px / Sidebar 숨김 / Main 전체 폭 / 흰 Sheet 1개 / 링크 44px·14px / 선택 시 닫기·Escape·포커스 복귀 확인. 열린 Sheet에서 Desktop 복귀 시 닫힘과 선택 경로 보존 확인. 기존 GNB Overview·LNB 대표 메뉴·Dashboard Header computed style 비교 동일, 검사한 화면의 Console error 없음. 앱 TypeScript 검사 통과. 전체 프로젝트 회귀 테스트와 실기기 터치 검증은 수행하지 않았다. +검증(2026-10-08, 기존 Storybook 6024): 1440px Sidebar 272px(당시 값. 2026-11-09부터 LNB 규격 240px) / Green Light 2, 2·3Depth 선택·그룹 Enter/Space·Hover·선택 경로·메뉴 스크롤 확인. 768px·375px에서 GNB 56px / Sidebar 숨김 / Main 전체 폭 / 흰 Sheet 1개 / 링크 44px·14px / 선택 시 닫기·Escape·포커스 복귀 확인. 열린 Sheet에서 Desktop 복귀 시 닫힘과 선택 경로 보존 확인. 기존 GNB Overview·LNB 대표 메뉴·Dashboard Header computed style 비교 동일, 검사한 화면의 Console error 없음. 앱 TypeScript 검사 통과. 전체 프로젝트 회귀 테스트와 실기기 터치 검증은 수행하지 않았다. diff --git a/docs/patterns/lnb.md b/docs/patterns/lnb.md index 60ea0e8..54cef51 100644 --- a/docs/patterns/lnb.md +++ b/docs/patterns/lnb.md @@ -81,10 +81,10 @@ Collapsible의 trigger/content는 `asChild`로 자체 `button`/`div`를 써서 C | 항목 | 명세 | 구현 | 분류 | |---|---|---|---| -| 글자 | 1·2뎁스 18/28/700, 3뎁스 18/24/500, 선택 700 | 같음 | MATCHED | -| 3뎁스 행 | 최소 48, 상하 12, 링크 padding-left 88(2뎁스 68 + 20) | 같음 (1·2뎁스 56 유지) | MATCHED | +| 글자 | 1·2뎁스 18/28/700, 3뎁스 18/24/500, 선택 700 | 크기만 14로 변경: 1·2뎁스 14/28/700, 3뎁스 14/24/500, 선택 700 유지 | INTENTIONAL_CHANGE (2026-11-09) | +| 3뎁스 행 | 최소 48, 상하 12, 링크 padding-left 88(2뎁스 68 + 20) | 최소 42, 상하 9, padding-left 88 유지 (1뎁스 48, 2뎁스 42) | INTENTIONAL_CHANGE (2026-11-09) | | 3뎁스 글자 장식 | 외곽선·그림자 없음 | 같음 | MATCHED | -| 긴 이름 | 줄바꿈, 행 높이 증가 | 48 / 72 / 96 (1·2·3줄), ellipsis 없음, 가로 overflow 0 | MATCHED | +| 긴 이름 | 줄바꿈, 행 높이 증가 | 3뎁스 기준 42 / 66 / 90 (1·2·3줄, 24px 줄 + 상하 9), ellipsis 없음, 가로 overflow 0. 2026-11-09 이전 값은 48 / 72 / 96 | INTENTIONAL_CHANGE (2026-11-09) | | 선택 | Beige + `--hm-sem-action-primary`, 오른쪽 곡선 | 기존 current 규칙 그대로(폭 +1, 위·아래 오목 곡선) | MATCHED | | 상위 경로 | 1·2뎁스 노랑, 접혀도 유지 | `current` → `--hm-yellow-accent` | MATCHED | | 연결선 | 3뎁스만, 1px, 세로선 x 68, 가로 12, 글자까지 8, 행 세로 중앙, 마지막 행 중앙에서 끝 | 행마다 background 2겹(세로 `1px × 100%`, 마지막 행 `1px × 50%`, 가로 `12 × 1` at `calc(50% - 0.5px)`). DOM·tab stop·aria 없음 | MATCHED / IMPLEMENTATION_CHANGE(명세의 "배경 레이어" 방식) | @@ -153,11 +153,11 @@ LNB Mode가 Desktop Brand/Profile과 Mobile Header/Sheet Profile을 담당한다 | 항목 | ui-viz | 최종 HM | 분류 | |---|---|---|---| -| 폭 | track 272, visual 290(오른쪽 18 bleed) | 열 272(`--hm-lnb-width`), 뒤로 18(`--hm-lnb-join`) 같은 색 연장 | MATCHED (연장은 `::after`) | +| 폭 | track 272, visual 290(오른쪽 18 bleed) | 열 240(`--hm-lnb-width`, 2026-11-09 공식 규격 채택), 뒤로 18(`--hm-lnb-join`) 같은 색 연장 | INTENTIONAL_CHANGE (폭만 ui-viz 272와 다름, 연장은 `::after`) | | 배경 | 90° 5-stop `#123328/#205241/#266651/#249473/#266651` 0/30/50/90/100%, 트랙 폭, 트랙 밖 `#266651` | 같음 (`--hm-green-*`) | MATCHED | | 상단 | 위 28, 브랜드 padding 0 10 22, 메뉴 위 20 | 위 28, 브랜드 inset 30(=20+10), 아래 22, 메뉴 위 20 | MATCHED | | 브랜드 | 로고 조건부, 위 로고·아래 서비스명, eyebrow 11/700, 이름 17 | 같음 | MATCHED | -| 항목 | 18/28/700, 최소 56, padding 14 12 14 18, White, 아이콘 20 opacity .5, 글자 테두리 1.5px + 그림자 `1px 2px 1px` | 같음 | MATCHED | +| 항목 | 18/28/700, 최소 56, padding 14 12 14 18, White, 아이콘 20 opacity .5, 글자 테두리 1.5px + 그림자 `1px 2px 1px` | 14/28/700, 1뎁스 최소 48(padding 상하 10), 2뎁스 최소 42(상하 7). 색·아이콘·글자 테두리는 같음 | INTENTIONAL_CHANGE (2026-11-09) | | hover | Black 10%, 콘텐츠 경계까지, 시작 쪽 radius 4 | 같음 | MATCHED | | current | Beige + `#123328`, 왼쪽 radius 8, 오른쪽 20+1 연장, 위·아래 18 오목 곡선, 안쪽 깊이 그림자(마지막 25% fade), 글자 효과 없음 | 같음. 깊이는 span 대신 background gradient 3겹으로 근사 | MATCHED / IMPLEMENTATION_CHANGE | | nested(2뎁스) | 상위 = disclosure 버튼, 화살표 접힘 → 펼침 90°, 펼친 그룹 Black 8% + 1px `#266651` 선 + 왼쪽 radius 8, 하위 padding-left 68, 하단 8, 하위 선택 시 상위 노랑 `#FFC600` | 같음. HM Collapsible(Radix) 사용 | MATCHED / IMPLEMENTATION_CHANGE | @@ -175,13 +175,15 @@ LNB Mode가 Desktop Brand/Profile과 Mobile Header/Sheet Profile을 담당한다 | Token | 값 | |---|---| -| `--hm-lnb-width` | 272px (1023px 이하에서 0px) | +| `--hm-lnb-width` | 240px (1023px 이하에서 0px) | | `--hm-lnb-join` | 18px. 콘텐츠 둥근 모서리·bleed | | `--hm-lnb-background` / `-background-end` | 5-stop gradient / `#266651` | -| item | min-height 56, 18/28/700, gap 12, 아이콘 20, 글자 테두리 1.5px | +| item | 1뎁스 min-height 48(`--hm-lnb-item-min-height`) + padding-block 10(`--hm-lnb-item-padding-block`), 14/28/700(`--hm-lnb-item-font-size`), gap 12, 아이콘 20, 글자 테두리 1.5px | +| `--hm-lnb-subitem-*` | 2뎁스(그룹 안 직속 링크·중첩 그룹 trigger) min-height 42(`-min-height`), padding-block 7(`-padding-block`), padding-start 68 | +| `--hm-lnb-profile-min-height` | 48px. Profile 높이는 3뎁스 행과 독립이다 | | current | `--hm-sem-bg-content` + `--hm-sem-action-primary`, radius 8 0 0 8 | | group | open face Black 8%, 선 1px `--hm-sem-palette-green-light-2`, 하위 padding-start 68, current 글자 `--hm-sem-palette-yellow-accent` | -| `--hm-lnb-third-*` | min-height 48, padding-block 12, text padding-start 88(메뉴 행 기준), line-height 24, weight 500 / current 700, connector x `= --hm-lnb-subitem-padding-start`(68)·length 12·width 1, 모든 상태에서 `--hm-sem-palette-green-tint` 40%. 실제 link는 connector 끝부터 시작 | +| `--hm-lnb-third-*` | min-height 42, padding-block 9, text padding-start 88(메뉴 행 기준), line-height 24, weight 500 / current 700, connector x `= --hm-lnb-subitem-padding-start`(68)·length 12·width 1, 모든 상태에서 `--hm-sem-palette-green-tint` 40%. 실제 link는 connector 끝부터 시작 | 새 Primitive: `--hm-yellow-accent: #ffc600`(`primitives.css`). ui-viz `color.action.accent-on-green`이며 HM 팔레트에 없어 추가했다. 그 외 새 Semantic은 없다. @@ -212,11 +214,11 @@ viewport 높이 shell에서 모바일 Header와 main을 세로 flex로 배분할 > HISTORICAL_RECORD: 아래 날짜별 geometry·상태 측정은 당시 기록이다. 현재 링크 interaction 범위·connector 색은 §3a/§5, Profile interaction은 §7을 따른다. -- 1280: current 항목 x 20→273, 높이 56, 18/28/700, Beige `#F5F0E5`/`#123328`, radius `8 0 0 8`, 그룹·하위 x 20→272. 하위 선택 시 상위 `rgb(255,198,0)`, 접힌 채여도 유지. 긴 이름은 줄바꿈(112px), 스크롤 영역은 메뉴만 스크롤하고 profile은 아래 22px 고정. 그룹 trigger Enter로 펼침. +- 1280(당시 값. 2026-11-09부터 공식 규격은 열 240, 1뎁스 48·2/3뎁스 42, 14px): current 항목 x 20→273, 높이 56, 18/28/700, Beige `#F5F0E5`/`#123328`, radius `8 0 0 8`, 그룹·하위 x 20→272. 하위 선택 시 상위 `rgb(255,198,0)`, 접힌 채여도 유지. 긴 이름은 줄바꿈(112px), 스크롤 영역은 메뉴만 스크롤하고 profile은 아래 22px 고정. 그룹 trigger Enter로 펼침. - 1000px: 열 `display:none`, 가로 overflow 없음. 390px은 GNB 모바일 메뉴로 대체. - `npm run typecheck`, `npm run build` 통과. 기존 primitive Story 변화 없음. -- 3뎁스(2026-10-06, Storybook): 위 §3a 수치를 computed style로 확인(1280 열 272, 3뎁스 x 20 → 272/선택 273, 그룹 2개 모두 x 20·폭 252, 안쪽 그룹 배경 투명·`::before` 없음). 키보드: Enter로 바깥 그룹 접힘(노랑 유지, 3뎁스·선 사라짐), Space로 다시 펼침(안쪽 그룹 열림 유지), Tab → 안쪽 그룹 Enter로 접힘. trigger는 `button` + `aria-expanded`/`aria-controls`, 선택 링크 `aria-current="page"`, focus-visible 흰 outline. 기존 Selected Child 수치 변화 없음(2뎁스 68/56/700, 그룹 하단 8, 선 1px). +- 3뎁스(2026-10-06, Storybook): 위 §3a 수치를 computed style로 확인(당시 1280 열 272, 3뎁스 x 20 → 272/선택 273, 그룹 2개 모두 x 20·폭 252, 안쪽 그룹 배경 투명·`::before` 없음). 키보드: Enter로 바깥 그룹 접힘(노랑 유지, 3뎁스·선 사라짐), Space로 다시 펼침(안쪽 그룹 열림 유지), Tab → 안쪽 그룹 Enter로 접힘. trigger는 `button` + `aria-expanded`/`aria-controls`, 선택 링크 `aria-current="page"`, focus-visible 흰 outline. 기존 Selected Child 수치 변화 없음(2뎁스 68/56/700, 그룹 하단 8, 선 1px). - 스크롤 소유(2026-10-06, Storybook): 열 = flex column, brand·profile `flex: none`, `LNBNav`만 `flex: 1 1 auto; min-height: 0; overflow-y: auto`(열 overflow 없음, 높이는 Story shell / Dashboard Layout이 결정). 메뉴 위 20을 nav margin 2 + padding 18에서 nav padding 20(`--hm-lnb-nav-padding-top`)으로 옮겨 스크롤 영역이 brand 바로 아래에서 시작(첫 항목 y 그대로). 측정: Scroll(460) nav 93.7→370 = brand bottom → profile top, client 276 / scroll 830, 최하단 마지막 항목 bottom 351.7(= nav bottom − 18); Third Level Scroll(460) client 276 / scroll 990, 3뎁스 선택 행·곡선·연결선 함께 스크롤; Dashboard Third Level 1280·1024×600 nav 163.7→510, client 346 / scroll 526, 최하단 491.7. 1023px 열 `display:none`, 가로 overflow 없음. @@ -230,7 +232,9 @@ viewport 높이 shell에서 모바일 Header와 main을 세로 flex로 배분할 Collapsible, Navigation Menu 등 기존 primitive 코드·theme CSS는 수정하지 않았다. `primitives.css`에 `--hm-yellow-accent` 한 줄만 추가했다. -**공용 패키지 기본값 vs Storybook 전용 오버라이드:** 이 문서의 공용 패키지 기본 규격(열 272px, 메뉴 18px, 행 56px 등)과 `packages/ui`·Theme Token은 변경하지 않았다. `Patterns/Layout/LNB` 대표 Story(`src/patterns/lnb.stories.tsx`)만 `src/patterns/lnb.stories.css`로 PC 열 너비 240px(1024px 이상, 최소 너비 212px), 메뉴 폰트 14px, 행 높이 depth1 48px·depth2·3 42px를 Storybook 표시용으로 덮어쓴다(`.hm-lnb-story` 범위 안에서만 적용하며 소비 프로젝트에는 영향이 없다). +**공식 기본 규격 (2026-11-09):** PC 열 240px, 메뉴 14px, 행 높이 1뎁스 48px·2/3뎁스 42px를 `packages/ui/src/theme/lnb.css`의 공식 기본값으로 채택했다. 이전 값(272px, 18px, 56px / 3뎁스 48px)은 위 날짜별 검증 기록에만 남는다. 모바일 규격(`--hm-lnb-mobile-*`)과 `DashboardSidebar`(자체 `--hm-dashboard-sidebar-width` 272px)는 변경하지 않았다. GNB ONLY 내부 Sidebar는 LNB 부품과 `--hm-lnb-width`를 재사용하므로 같은 규격(240px / 14px / 48px / 42px)을 따른다. 소비 프로젝트는 크기를 Storybook 화면이 아니라 `--hm-lnb-*` 토큰에서 읽어야 하며, 새 CSS 변수는 `--hm-lnb-item-padding-block`, `--hm-lnb-subitem-min-height`, `--hm-lnb-subitem-padding-block`, `--hm-lnb-profile-min-height` 4개다. + +**Storybook 전용 표시(`src/patterns/lnb.stories.css`):** `Patterns/Layout/LNB` 대표 Story만 `.hm-lnb-story` 범위 안에서 최소 너비 212px, 브랜드 아래 padding 8px, Profile 글자(14/12/700), 구분선을 덮어쓴다. 크기 규격은 더 이상 덮어쓰지 않는다. ## 10. 남은 차이 @@ -243,7 +247,7 @@ Collapsible, Navigation Menu 등 기존 primitive 코드·theme CSS는 수정하 | 실기기, 1024~1280 사이 시각 | 미확인 | | 4뎁스 이상 | 명세 없음. 3뎁스 규칙만 정의(더 깊이 중첩하면 3뎁스 모양이 반복된다) | | 선택 경로 전체 연결선을 선택색으로 칠할지 | 현재 모든 connector는 Green Tint 40%를 유지한다(§3a/§5). 선택은 Beige 면 + Primary 글자 + 700 weight로 표현하며, 경로 전체 선의 선택색은 별도 결정 전까지 적용하지 않는다 | -| LNB 모바일 Sheet 그룹 접기·상위 current 표시 | 현재 링크 목록 방식. Desktop 18px / Mobile 14px 유지 | +| LNB 모바일 Sheet 그룹 접기·상위 current 표시 | 현재 링크 목록 방식. Desktop 14px(2026-11-09, 이전 18px) / Mobile 14px 유지 | | LNB Mobile Header 공개 API | 구현 완료. GNB/Dashboard 의존성 없음 | ## 11. 관련 파일 diff --git a/packages/ui/ai/COMPOSITION_PATTERNS.md b/packages/ui/ai/COMPOSITION_PATTERNS.md index c709eee..c3c95f3 100644 --- a/packages/ui/ai/COMPOSITION_PATTERNS.md +++ b/packages/ui/ai/COMPOSITION_PATTERNS.md @@ -31,6 +31,7 @@ - 요청이 모드를 말하지 않으면 화면 목적(서비스 상단 메뉴 = GNB, 계층 업무 메뉴 = LNB, 운영 현황 = DASHBOARD)으로 고르고 **선택 근거를 보고**합니다. 임의로 Sidebar를 추가하지 않습니다. - 혼합 요청(GNB + LNB 등)은 정책 충돌로 안내하고 하나를 고르게 합니다. Drawer·Hamburger 등 대체 메뉴를 추측해 만들지 않습니다. - Breadcrumb·FilterBar·사용자 Dropdown은 Navigation 모드가 아닙니다. +- **LNB 크기의 기준은 패키지 토큰입니다.** `@hm-design/ui`의 `--hm-lnb-*`가 정의한 값(Desktop 열 240px, 메뉴 14/28/700, 1뎁스 행 48px, 2·3뎁스 행 42px, Profile 48px, 모바일 Header 56px·링크 44px)을 그대로 씁니다. Storybook 화면에서 눈으로 잰 값이나 과거 예제의 272px·18px·56px를 새로 정하지 않고, `--hm-lnb-width` 등을 소비 CSS에서 다시 정의하지 않습니다. 레이아웃 열은 `var(--hm-lnb-width)`로 읽습니다. ```tsx // LNB ONLY 골격 diff --git a/packages/ui/ai/DESIGN_RULES.md b/packages/ui/ai/DESIGN_RULES.md index ad3f326..b404720 100644 --- a/packages/ui/ai/DESIGN_RULES.md +++ b/packages/ui/ai/DESIGN_RULES.md @@ -68,6 +68,7 @@ Brown·Orange 계열 팔레트(`--hm-sem-palette-brown-*`, `--hm-sem-palette-ora | Select Trigger 높이 | medium 34 · large 44(기본) | — | | Control radius | 4px | `--hm-radius-control-md` → `--hm-radius-button`, `--hm-radius-input`, `--hm-radius-select`, `--hm-radius-container` | | Card | border 1px, padding 24px, radius 4px, 그림자 없음 | `--hm-card-*` | +| LNB(Desktop) | 열 240px, 메뉴 14/28/700, 1뎁스 행 48 · 2·3뎁스 행 42, Profile 48, 콘텐츠 접점 18. 모바일 Header 56 · 링크 44 | `--hm-lnb-width`, `--hm-lnb-item-*`, `--hm-lnb-subitem-*`, `--hm-lnb-third-*`, `--hm-lnb-profile-min-height`, `--hm-lnb-join` | | Dialog | radius 8px, padding 24px, Header 아래 16px, Footer 위 16px, 버튼 간격 8px, 그림자 `0 0 12px` 검정 20% | `--hm-dialog-*` | ## 5. 상태 diff --git a/packages/ui/src/theme/lnb.css b/packages/ui/src/theme/lnb.css index 2b92808..a2ef265 100644 --- a/packages/ui/src/theme/lnb.css +++ b/packages/ui/src/theme/lnb.css @@ -4,7 +4,7 @@ Black 10% to the same edge; an open group is a Black 8% face with a 1px green line. Spacing values are pattern-local. */ :root, [data-hm-theme] { /* Exposed for the layout: grid column width and the surface that continues behind the content's rounded corners. */ - --hm-lnb-width: 272px; + --hm-lnb-width: 240px; --hm-lnb-join: 18px; --hm-lnb-background: linear-gradient(90deg, var(--hm-sem-action-primary) 0%, var(--hm-sem-palette-green-light-1) 30%, var(--hm-sem-palette-green-light-2) 50%, var(--hm-sem-palette-green-light-3) 90%, var(--hm-sem-palette-green-light-2) 100%); --hm-lnb-background-end: var(--hm-sem-palette-green-light-2); @@ -29,10 +29,12 @@ --hm-lnb-brand-name-size: 17px; --hm-lnb-brand-text-gap: 5px; - --hm-lnb-item-min-height: 56px; + --hm-lnb-item-min-height: 48px; + /* 1-depth row: 28px line + 2 x 10px = 48px. */ + --hm-lnb-item-padding-block: 10px; --hm-lnb-item-padding-start: 18px; --hm-lnb-item-gap: 12px; - --hm-lnb-item-font-size: 18px; + --hm-lnb-item-font-size: 14px; --hm-lnb-item-line-height: 28px; --hm-lnb-item-font-weight: 700; --hm-lnb-item-radius: 4px 0 0 4px; @@ -46,11 +48,38 @@ --hm-lnb-current-background: var(--hm-sem-bg-content); --hm-lnb-current-text: var(--hm-sem-action-primary); --hm-lnb-current-radius: 8px 0 0 8px; - /* Inset depth of the current face: left edge, top, bottom; the last quarter fades so the content side stays open. */ + /* Inset depth of the current face (left, top, bottom), matched to the Storybook Manager's selected item: inset box-shadows + 6px 0 8px -5px Black 30%, 0 3px 5px -3px Black 20% and 0 -2px 4px -3px Black 20%, faded out over the last quarter. A background + cannot hold a box-shadow and ::before / ::after already draw the concave corners, so the blurred falloff is sampled as gradient + stops (left 10px, top 5px, bottom 4px), and the first layer repaints the face color from 75% to 100% so the depth fades toward + the content side. */ --hm-lnb-current-depth: - linear-gradient(to right, color-mix(in srgb, var(--hm-sem-palette-black) 30%, transparent), transparent 6px) 0 0 / 75% 100% no-repeat, - linear-gradient(to bottom, color-mix(in srgb, var(--hm-sem-palette-black) 20%, transparent), transparent 3px) 0 0 / 75% 100% no-repeat, - linear-gradient(to top, color-mix(in srgb, var(--hm-sem-palette-black) 20%, transparent), transparent 3px) 0 0 / 75% 100% no-repeat; + linear-gradient(to right, transparent, var(--hm-lnb-current-background)) 100% 0 / 25% 100% no-repeat, + linear-gradient(to right, + color-mix(in srgb, var(--hm-sem-palette-black) 20%, transparent) 0, + color-mix(in srgb, var(--hm-sem-palette-black) 15.7%, transparent) 1px, + color-mix(in srgb, var(--hm-sem-palette-black) 12.2%, transparent) 2px, + color-mix(in srgb, var(--hm-sem-palette-black) 9.5%, transparent) 3px, + color-mix(in srgb, var(--hm-sem-palette-black) 7.1%, transparent) 4px, + color-mix(in srgb, var(--hm-sem-palette-black) 5%, transparent) 5px, + color-mix(in srgb, var(--hm-sem-palette-black) 3.3%, transparent) 6px, + color-mix(in srgb, var(--hm-sem-palette-black) 2.2%, transparent) 7px, + color-mix(in srgb, var(--hm-sem-palette-black) 1.4%, transparent) 8px, + color-mix(in srgb, var(--hm-sem-palette-black) 0.8%, transparent) 9px, + transparent 10px), + linear-gradient(to bottom, + color-mix(in srgb, var(--hm-sem-palette-black) 12%, transparent) 0, + color-mix(in srgb, var(--hm-sem-palette-black) 8.5%, transparent) 1px, + color-mix(in srgb, var(--hm-sem-palette-black) 5%, transparent) 2px, + color-mix(in srgb, var(--hm-sem-palette-black) 2.3%, transparent) 3px, + color-mix(in srgb, var(--hm-sem-palette-black) 0.8%, transparent) 4px, + transparent 5px), + linear-gradient(to top, + color-mix(in srgb, var(--hm-sem-palette-black) 9%, transparent) 0, + color-mix(in srgb, var(--hm-sem-palette-black) 5.1%, transparent) 1px, + color-mix(in srgb, var(--hm-sem-palette-black) 2.4%, transparent) 2px, + color-mix(in srgb, var(--hm-sem-palette-black) 0.7%, transparent) 3px, + transparent 4px); --hm-lnb-group-open-background: color-mix(in srgb, var(--hm-sem-palette-black) 8%, transparent); --hm-lnb-group-open-line: 1px solid var(--hm-sem-palette-green-light-2); @@ -58,14 +87,17 @@ --hm-lnb-group-current-text: var(--hm-sem-palette-yellow-accent); --hm-lnb-group-arrow-size: 18px; --hm-lnb-subitem-padding-start: 68px; + /* 2-depth row (links directly in a group, nested group triggers): 28px line + 2 x 7px = 42px. */ + --hm-lnb-subitem-min-height: 42px; + --hm-lnb-subitem-padding-block: 7px; /* Align the 2-depth link surface with the 1-depth group text; retain its existing text inset. */ --hm-lnb-subitem-surface-inset: calc(var(--hm-lnb-item-padding-start) + var(--hm-lnb-group-arrow-size) + var(--hm-lnb-item-gap)); --hm-lnb-group-padding-bottom: 8px; - /* 3-depth: the items of an `LNBGroup` nested in a group. Same 18px; the hierarchy is the extra 20px indent and the connector - (a vertical line at the 2-depth text start, a 12px branch to each row's middle, 8px to the text; it ends at the last row). */ - --hm-lnb-third-min-height: 48px; - --hm-lnb-third-padding-block: 12px; + /* 3-depth: the items of an `LNBGroup` nested in a group. Same font size as the other depths; the hierarchy is the extra 20px indent and the connector + (a vertical line at the 2-depth text start, a 12px branch to each row's middle, 8px to the text; it ends at the last row). 42px = 24px line + 9px padding x2. */ + --hm-lnb-third-min-height: 42px; + --hm-lnb-third-padding-block: 9px; --hm-lnb-third-padding-start: 88px; --hm-lnb-third-line-height: 24px; --hm-lnb-third-font-weight: 500; @@ -79,6 +111,7 @@ --hm-lnb-profile-padding-x: 30px; --hm-lnb-profile-padding-top: 22px; --hm-lnb-profile-gap: 12px; + --hm-lnb-profile-min-height: 48px; --hm-lnb-profile-name-size: 16px; --hm-lnb-profile-description-size: 12px; } @@ -150,7 +183,7 @@ max-width: calc(100% - 2 * (var(--hm-lnb-profile-padding-x) - 8px)); margin-top: auto; margin-inline: calc(var(--hm-lnb-profile-padding-x) - 8px); - min-height: var(--hm-lnb-third-min-height); + min-height: var(--hm-lnb-profile-min-height); padding: 0 8px; border: 0; background: transparent; @@ -211,7 +244,7 @@ gap: var(--hm-lnb-item-gap); width: calc(100% - 1px); min-height: var(--hm-lnb-item-min-height); - padding: 14px calc(var(--hm-lnb-inset-end) + 12px) 14px var(--hm-lnb-item-padding-start); + padding: var(--hm-lnb-item-padding-block) calc(var(--hm-lnb-inset-end) + 12px) var(--hm-lnb-item-padding-block) var(--hm-lnb-item-padding-start); border: 0; border-radius: var(--hm-lnb-item-radius); background: transparent; @@ -329,6 +362,13 @@ [data-slot="lnb-group-content"] [data-slot="lnb-item"]:not([aria-current="page"]):hover { border-radius: var(--hm-lnb-item-radius); } +/* 2-depth rows (links directly in a group, nested group triggers): 42px = 28px line + 2 x 7px. */ +[data-slot="lnb-group-content"] > [data-slot="lnb-item"], +[data-slot="lnb-group-content"] [data-slot="lnb-group-trigger"] { + min-height: var(--hm-lnb-subitem-min-height); + padding-top: var(--hm-lnb-subitem-padding-block); + padding-bottom: var(--hm-lnb-subitem-padding-block); +} [data-slot="lnb-group-content"] [data-slot="lnb-item"][aria-current="page"] { width: calc(100% + 1px); margin-right: -1px; diff --git a/src/patterns/lnb.stories.css b/src/patterns/lnb.stories.css index a991feb..e18f502 100644 --- a/src/patterns/lnb.stories.css +++ b/src/patterns/lnb.stories.css @@ -1,27 +1,16 @@ -/* Storybook presentation only: Patterns/Layout/LNB Stories. Scoped by the decorator wrapper in lnb.stories.tsx, so the shared - @hm-design/ui LNB Pattern, its tokens and every other Story keep their values. - - min-width 212px: a floor, not a fixed width. The column follows --hm-lnb-width (240px from 1024px, see below) and may be wider. - - menu font 14px: --hm-lnb-item-font-size is redefined for this subtree; weight (700) and line height (28px) stay. - - row height: depth1 48px, depth2 and deeper 42px. Heights come from min-height + vertical padding (not a fixed height), so a - label that wraps grows the row instead of being clipped. Current and hover only change color/background, not the height. */ +/* Storybook presentation only: Patterns/Layout/LNB Stories. Scoped by the decorator wrapper in lnb.stories.tsx. + Column width (240px), menu font (14px) and row heights (depth1 48px, depth2/3 42px) are now the @hm-design/ui package defaults + (packages/ui/src/theme/lnb.css); only the Story-specific presentation below stays here. + - min-width 212px: a floor, not a fixed width. The column follows --hm-lnb-width and may be wider. + - brand bottom padding 8px, profile text, and divider lines. */ .hm-lnb-story { display: contents; - --hm-lnb-item-font-size: 14px; - /* Existing variable: min-height of every item and group trigger (used by nothing else). */ - --hm-lnb-item-min-height: 48px; /* Existing variable: bottom padding of [data-slot="lnb-brand"] (22px by default; top 0 and the 30px sides are unchanged). */ --hm-lnb-brand-padding-bottom: 8px; } .hm-lnb-story [data-slot="lnb"] { min-width: 212px; } -/* PC column width 240px through the existing --hm-lnb-width (the column, the Story grid track and the menu row offset all read it). - Only from 1024px: the package sets --hm-lnb-width to 0px below that and collapses the column, which must keep working. */ -@media (min-width: 1024px) { - .hm-lnb-story { - --hm-lnb-width: 240px; - } -} /* Profile text (PC column): name (strong) 14px, description (p) 12px with no top margin, both 700. The package's existing 1px text stroke on strong/p stays as is; the color and the 1.5 line-height ratio are unchanged (21px + 18px lines in the 48px profile). @@ -91,23 +80,3 @@ paint-order: normal; } -/* depth1: nav items and top-level group triggers. The package hard-codes 14px vertical padding (28px line + 28px = 56px). */ -.hm-lnb-story [data-slot="lnb-item"], -.hm-lnb-story [data-slot="lnb-group-trigger"] { - padding-top: 10px; - padding-bottom: 10px; -} -/* depth2: links directly inside a group and nested group triggers (28px line + 14px = 42px). */ -.hm-lnb-story [data-slot="lnb-group-content"] > [data-slot="lnb-item"], -.hm-lnb-story [data-slot="lnb-group-content"] [data-slot="lnb-group-trigger"] { - min-height: 42px; - padding-top: 7px; - padding-bottom: 7px; -} -/* depth3: links in a nested group (24px line + 18px = 42px). --hm-lnb-third-min-height is also the Profile's height, so it is - overridden on the row's link instead of through the variable. */ -.hm-lnb-story [data-slot="lnb-group-content"] [data-slot="lnb-third-row"] > [data-slot="lnb-item"] { - min-height: 42px; - padding-top: 9px; - padding-bottom: 9px; -} diff --git a/src/patterns/lnb.stories.tsx b/src/patterns/lnb.stories.tsx index 7f4eb56..9db389d 100644 --- a/src/patterns/lnb.stories.tsx +++ b/src/patterns/lnb.stories.tsx @@ -34,9 +34,10 @@ import { StateBoard } from "@/components/story-board" import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page" import "./lnb.stories.css" -// LNB is a composition pattern. Layout visual reference is the ui-viz LNB (the shared package default: 272px green column, 18/700 items, -// Beige current item joined to the content, Black 10% hover, Black 8% open group). This Story overrides only the display values (240px column, -// 212px min width, 14px items, 48/42px rows) in lnb.stories.css; the package defaults are unchanged. The menu data and the current location are local sample data for this Story +// LNB is a composition pattern. Layout visual reference is the ui-viz LNB (the shared package default: 240px green column, 14/700 items, +// 48px 1-depth and 42px 2/3-depth rows, Beige current item joined to the content, Black 10% hover, Black 8% open group). The sizes come from the package +// (`--hm-lnb-*` in packages/ui/src/theme/lnb.css); lnb.stories.css only keeps Story presentation (212px min width, brand bottom padding, profile text, +// divider lines). The menu data and the current location are local sample data for this Story // (no router). The content next to the column is a positioned surface with `--hm-lnb-join` rounded left corners: the LNB Mode page // owns it, here it only shows the connection. The column is desktop only (hidden below 1024px). @@ -619,7 +620,7 @@ const mobileViewport = { globals: { viewport: { value: "hmMobile", isRotated: false } }, } -// `hm-lnb-story` (display: contents, see lnb.stories.css) scopes the 212px min width and 14px menu font to this Story only. +// `hm-lnb-story` (display: contents, see lnb.stories.css) scopes the 212px min width, brand bottom padding, profile text and divider lines to this Story only. const meta = { title: "Patterns/Layout/LNB", component: LNB, decorators: [(Story) =>
], parameters: { layout: "fullscreen", controls: { disable: true } } } satisfies Meta export default meta type Story = StoryObj diff --git a/src/theme/verification-baseline.json b/src/theme/verification-baseline.json index 600eb16..2bcc9cd 100644 --- a/src/theme/verification-baseline.json +++ b/src/theme/verification-baseline.json @@ -588,7 +588,7 @@ "--hm-label-line-height": "20px", "--hm-label-font-weight": "500", "--hm-label-text": "var(--hm-sem-fg-secondary)", - "--hm-lnb-width": "272px", + "--hm-lnb-width": "240px", "--hm-lnb-join": "18px", "--hm-lnb-background": "linear-gradient(90deg, var(--hm-green-default) 0%, var(--hm-green-light-1) 30%, var(--hm-green-light-2) 50%, var(--hm-green-light-3) 90%, var(--hm-green-light-2) 100%)", "--hm-lnb-background-end": "var(--hm-green-light-2)", @@ -608,10 +608,11 @@ "--hm-lnb-brand-eyebrow-spacing": "0.12em", "--hm-lnb-brand-name-size": "17px", "--hm-lnb-brand-text-gap": "5px", - "--hm-lnb-item-min-height": "56px", + "--hm-lnb-item-min-height": "48px", + "--hm-lnb-item-padding-block": "10px", "--hm-lnb-item-padding-start": "18px", "--hm-lnb-item-gap": "12px", - "--hm-lnb-item-font-size": "18px", + "--hm-lnb-item-font-size": "14px", "--hm-lnb-item-line-height": "28px", "--hm-lnb-item-font-weight": "700", "--hm-lnb-item-radius": "4px 0 0 4px", @@ -631,9 +632,11 @@ "--hm-lnb-group-current-text": "var(--hm-yellow-accent)", "--hm-lnb-group-arrow-size": "18px", "--hm-lnb-subitem-padding-start": "68px", + "--hm-lnb-subitem-min-height": "42px", + "--hm-lnb-subitem-padding-block": "7px", "--hm-lnb-group-padding-bottom": "8px", - "--hm-lnb-third-min-height": "48px", - "--hm-lnb-third-padding-block": "12px", + "--hm-lnb-third-min-height": "42px", + "--hm-lnb-third-padding-block": "9px", "--hm-lnb-third-padding-start": "88px", "--hm-lnb-third-line-height": "24px", "--hm-lnb-third-font-weight": "500", @@ -646,6 +649,7 @@ "--hm-lnb-profile-padding-x": "30px", "--hm-lnb-profile-padding-top": "22px", "--hm-lnb-profile-gap": "12px", + "--hm-lnb-profile-min-height": "48px", "--hm-lnb-profile-name-size": "16px", "--hm-lnb-profile-description-size": "12px", "--hm-loading-screen-background": "var(--hm-green-default)",