diff --git a/.storybook/manager.css b/.storybook/manager.css index bde02b5..3f1704c 100644 --- a/.storybook/manager.css +++ b/.storybook/manager.css @@ -61,13 +61,13 @@ background: var(--hm-fg-on-green-secondary); } #storybook-sidebar-region .sidebar-header { - margin: 0 20px 24px 0; + margin: 0 20px 0px 0; padding: 0; color: var(--color-fg-on-green-primary); } #storybook-sidebar-region .sidebar-header > div a { color: var(--color-fg-on-green-primary); - font: 700 20px/30px var(--hm-manager-font); + font: 700 16px/30px var(--hm-manager-font); white-space: normal; /* §6 Text Border: brand title keeps the original 1px (no shadow). */ -webkit-text-stroke: 1px var(--effect-text-border-color); @@ -78,7 +78,6 @@ } #storybook-sidebar-region [role='search'] { margin-right: 20px; - margin-bottom: 24px; } #storybook-sidebar-region .search-field { min-height: 44px; @@ -109,16 +108,16 @@ position: relative; margin: 0; padding: 0 20px 0 0; - min-height: 56px; + min-height: 48px; color: var(--hm-fg-on-green-secondary); } #storybook-explorer-tree .sidebar-subheading[data-hm-section]:not([data-hm-section='0']) { - margin-top: 40px; + margin-top: 24px; } #storybook-explorer-tree .sidebar-subheading[data-hm-section]:not([data-hm-section='0'])::before { content: ''; position: absolute; - top: -20px; + top: -12px; left: 0; right: 20px; height: 2px; @@ -126,9 +125,9 @@ mask-image: linear-gradient(90deg, transparent, #000 20%, #000 80%, transparent); } #storybook-explorer-tree .sidebar-subheading > button { - min-height: 56px; + min-height: 48px; color: inherit; - font: 700 16px/28px var(--hm-manager-font); + font: 700 14px/28px var(--hm-manager-font); letter-spacing: 0; text-transform: none; } @@ -136,7 +135,7 @@ position: relative; margin: 0; padding: 0; - min-height: 56px; + min-height: 48px; overflow: visible; border: 0; border-radius: 0; @@ -163,22 +162,30 @@ display: flex; align-items: center; gap: 12px; - min-height: 56px; + min-height: 48px; height: auto; width: 100%; margin: 0; - padding: 14px 32px 14px 18px; + padding: 10px 32px 10px 18px; border: 0; border-radius: 4px 0 0 4px; background: transparent; color: inherit; - font: 700 18px/28px var(--hm-manager-font); + font: 700 14px/28px var(--hm-manager-font); text-align: left; text-decoration: none; white-space: normal; overflow: visible; text-overflow: clip; } +/* Sub rows (depth 2+): 42px (7px + 28px line + 7px), shorter than the 48px top-level rows. */ +#storybook-explorer-tree .sidebar-item:is([data-hm-depth='2'], [data-hm-depth='3'], [data-hm-depth='4']) { + min-height: 42px; +} +#storybook-explorer-tree .sidebar-item:is([data-hm-depth='2'], [data-hm-depth='3'], [data-hm-depth='4']) > :is(a[id], button[id]:not([data-testid])) { + min-height: 42px; + padding-block: 7px; +} #storybook-explorer-tree .sidebar-item[data-hm-depth='2'] > :is(a[id], button[id]:not([data-testid])) { margin-left: 32px; width: calc(100% - 32px); @@ -188,6 +195,10 @@ margin-left: 44px; width: calc(100% - 44px); } +/* Menu item labels and section heading labels (.sidebar-subheading) are both 14px / 700. */ +#storybook-explorer-tree .sidebar-item .hm-manager-label { + font-size: 14px; +} #storybook-explorer-tree .hm-manager-label { min-width: 0; white-space: normal; @@ -198,8 +209,10 @@ } /* D-093 D-014 (§6 Text Border): non-selected labels, including hover and the yellow current parent, use the specimen's 1.5px border plus lower shadow. - Thickness and offset are REVIEW VALUEs. Selected labels carry no effect. */ -#storybook-explorer-tree .sidebar-item[data-selected='false'] .hm-manager-label { + Thickness and offset are REVIEW VALUEs. Selected labels carry no effect. + Section heading labels (Foundation, Components, ...) share this exact rule. */ +#storybook-explorer-tree .sidebar-item[data-selected='false'] .hm-manager-label, +#storybook-explorer-tree .sidebar-subheading .hm-manager-label { -webkit-text-stroke: 1.5px var(--effect-text-border-color); paint-order: stroke fill; text-shadow: 1px 2px 1px var(--effect-text-border-color); diff --git a/.storybook/manager.js b/.storybook/manager.js index fef8d37..e9ff6df 100644 --- a/.storybook/manager.js +++ b/.storybook/manager.js @@ -31,6 +31,9 @@ document.head.appendChild(toolbarStyle) addons.setConfig({ layout: { showPanel: false, + // LNB width 246px. recentVisibleSizes keeps the same width when the sidebar is collapsed and expanded again. + navSize: 246, + recentVisibleSizes: { navSize: 246 }, }, theme: create({ base: 'light', diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 7c4abd9..e988b90 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -25,7 +25,7 @@ const preview: Preview = { includeNames: true, order: [ "Foundation", - ["Color", "Company Colors", "Typography", "Layout"], + ["HM Color", "Company Colors", "Typography", "Layout"], "Components", [ "Form", diff --git a/.storybook/qa/STORY-INVENTORY.md b/.storybook/qa/STORY-INVENTORY.md index c3ff92f..2194292 100644 --- a/.storybook/qa/STORY-INVENTORY.md +++ b/.storybook/qa/STORY-INVENTORY.md @@ -499,11 +499,11 @@ Source: `src/components/tooltip.stories.tsx`; meta.tags: [] - All — Tooltip; tags: []; PRIMARY; KEEP. Representative story or distinct reference page. - Open — Open; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar. -### Foundation/Color +### Foundation/HM Color Source: `src/foundation/color.stories.tsx`; meta.tags: [] -- All — Color; tags: []; PRIMARY; KEEP. Representative story or distinct reference page. +- All — HM Color; tags: []; PRIMARY; KEEP. Representative story or distinct reference page. ### Foundation/Company Colors diff --git a/.storybook/qa/story-inventory.json b/.storybook/qa/story-inventory.json index 7fe872f..3fe65fa 100644 --- a/.storybook/qa/story-inventory.json +++ b/.storybook/qa/story-inventory.json @@ -3637,13 +3637,13 @@ }, { "file": "src/foundation/color.stories.tsx", - "title": "Foundation/Color", + "title": "Foundation/HM Color", "metaTags": [], "stories": [ { "id": "foundation-color--all", "exportName": "All", - "name": "Color", + "name": "HM Color", "storyTags": [], "effectiveTags": [], "category": "PRIMARY", diff --git a/docs/guidelines/brand-color-steps.md b/docs/guidelines/brand-color-steps.md index 73ead27..07ef203 100644 --- a/docs/guidelines/brand-color-steps.md +++ b/docs/guidelines/brand-color-steps.md @@ -33,8 +33,8 @@ Orange Dark 1·2의 원본 Needs Review 상태와 [이전 검수 보고서](../r ## 호환성과 Storybook -페이지 소개는 `한맥 브랜드 컬러`로 표시한다. 그룹 설명의 단계 순서 나열 문구는 제공하지 않으며, 개별 컬러칩의 단계명과 HEX는 유지한다. +페이지 제목은 `HM Color`(메뉴 `Foundation > HM Color`)이며 별도 소개 문구는 표시하지 않는다. 기존 Story ID `foundation-color--all`은 `meta.id`로 유지한다. 과거 `한맥 브랜드 컬러` 소개 문구는 당시 이력이다. 그룹 설명의 단계 순서 나열 문구는 제공하지 않으며, 개별 컬러칩의 단계명과 HEX는 유지한다. -[Color 대표 Story](../../src/foundation/color.stories.tsx)는 한맥그린·한맥브라운·한맥오렌지 3개 그룹에 각 8개, 총 24개 컬러칩만 표시한다. Green 8/8·Brown 8/8·Orange 8/8 확정이며 단계명·현재 HEX와 Default 기본색 강조를 유지한다. 2026-10-11 최종 표시 요청에 따라 Brown Deep, Orange Light·Mid·Dark·Deep의 별도 표시, Accent·Semantic·Gradients 섹션과 개발용 Token·CSS Variable·Runtime Key 설명은 제거한다. Legacy Button 색상과 Accent·Semantic·Gradient의 실제 Key·값·소비처는 보존한다. 기존 Component mapping을 새 값에 연결하지 않으며 public Key·Property·type·Default·API·schemaVersion 1과 서비스 partial JSON 호환성을 유지한다. [Runtime Theme](runtime-theme.md)의 geometry/상태 권한과 Designer의 기존 17개 상위 역할 입력 UI는 확대하지 않는다. 한맥오렌지의 미확정 브랜드 단계는 없다. [표시 정리 검증 기록](../reports/foundation-color-display-cleanup-2026-10-11.md). +[HM Color 대표 Story](../../src/foundation/color.stories.tsx)는 한맥그린·한맥브라운·한맥오렌지 3개 그룹에 각 8개, 총 24개 컬러칩만 표시한다. Green 8/8·Brown 8/8·Orange 8/8 확정이며 단계명·현재 HEX와 Default 기본색 강조를 유지한다. 2026-10-11 최종 표시 요청에 따라 Brown Deep, Orange Light·Mid·Dark·Deep의 별도 표시, Accent·Semantic·Gradients 섹션과 개발용 Token·CSS Variable·Runtime Key 설명은 제거한다. Legacy Button 색상과 Accent·Semantic·Gradient의 실제 Key·값·소비처는 보존한다. 기존 Component mapping을 새 값에 연결하지 않으며 public Key·Property·type·Default·API·schemaVersion 1과 서비스 partial JSON 호환성을 유지한다. [Runtime Theme](runtime-theme.md)의 geometry/상태 권한과 Designer의 기존 17개 상위 역할 입력 UI는 확대하지 않는다. 한맥오렌지의 미확정 브랜드 단계는 없다. [표시 정리 검증 기록](../reports/foundation-color-display-cleanup-2026-10-11.md). -[Company Colors Story](../../src/foundation/company-colors.stories.tsx)는 Foundation > Color와 별도 메뉴(Color 바로 아래)이며 7개 계열사·14개 컬러를 회사별로 표시한다. 데이터는 OrgChart의 기존 `companyColorPresets`를 그대로 읽고(메인 → 서브 순서, 메인은 Default 강조 재사용) HEX를 중복 정의하지 않는다. Theme 토큰·`themes/*.json`·공개 계약은 변경하지 않으며 Foundation > Color의 3그룹 24칩은 그대로다. +[Company Colors Story](../../src/foundation/company-colors.stories.tsx)는 Foundation > HM Color와 별도 메뉴(HM Color 바로 아래)이며 7개 계열사·14개 컬러를 회사별로 표시한다(별도 소개 문구 없음). 데이터는 OrgChart의 기존 `companyColorPresets`를 그대로 읽고(메인 → 서브 순서, 메인은 Default 강조 재사용) HEX를 중복 정의하지 않는다. Theme 토큰·`themes/*.json`·공개 계약은 변경하지 않으며 Foundation > HM Color의 3그룹 24칩은 그대로다. diff --git a/docs/patterns/lnb.md b/docs/patterns/lnb.md index d21351a..fc63e16 100644 --- a/docs/patterns/lnb.md +++ b/docs/patterns/lnb.md @@ -230,6 +230,8 @@ 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` 범위 안에서만 적용하며 소비 프로젝트에는 영향이 없다). + ## 10. 남은 차이 | 항목 | 분류 | @@ -251,4 +253,5 @@ packages/ui/src/patterns/lnb/{lnb.tsx,lnb-mobile.tsx,index.ts} packages/ui/src/theme/lnb.css packages/ui/src/theme/primitives.css (--hm-yellow-accent) src/patterns/lnb.stories.tsx +src/patterns/lnb.stories.css (Storybook 전용 오버라이드) ``` diff --git a/src/components/story-page.css b/src/components/story-page.css index 4793608..4088575 100644 --- a/src/components/story-page.css +++ b/src/components/story-page.css @@ -1,5 +1,5 @@ /* Storybook presentation only. Common page frame for representative Stories; all selectors stay inside .sb-page. - Inner (fluid; 16px each side up to 480px, growing to 60px at 768px; vertical gutter 16–40) and title (kicker 12 / title 28·36·700 / description, underlined) reuse Foundation / Color. + Inner (fluid; 16px each side up to 480px, growing to 60px at 768px; vertical gutter 16–40) and title (kicker 12 / title 28·36·700 / description, no divider below) reuse Foundation / Color. Section reuses the Center Graphic / 교량 split: heading + description, a divider, then the specimen. */ .sb-page { --sb-page-gutter: clamp(16px, 3vw, 40px); @@ -22,7 +22,6 @@ .sb-page .sb-page-header { padding-bottom: 24px; - border-bottom: 1px solid var(--hm-sem-stroke-input-default); font-size: var(--hm-label-font-size); line-height: var(--hm-label-line-height); overflow-wrap: anywhere; diff --git a/src/foundation/color.stories.tsx b/src/foundation/color.stories.tsx index 0fb22f6..db80aac 100644 --- a/src/foundation/color.stories.tsx +++ b/src/foundation/color.stories.tsx @@ -35,12 +35,13 @@ function ColorChip({ prefix, stage, values }: { prefix: PalettePrefix; stage: Co function ColorReferencePage() { const values = useResolvedProperties(colorProperties) - return + return {palettes.map(palette =>
{stages.map(stage => )}
)}
} -const meta = { title: "Foundation/Color", parameters: { layout: "fullscreen" } } satisfies Meta +// The sidebar name is "HM Color"; `id` keeps the existing Story ID (foundation-color--all) stable. +const meta = { title: "Foundation/HM Color", id: "foundation-color", parameters: { layout: "fullscreen" } } satisfies Meta export default meta -export const All: StoryObj = { name: "Color", render: () => } +export const All: StoryObj = { name: "HM Color", render: () => } diff --git a/src/foundation/company-colors.stories.tsx b/src/foundation/company-colors.stories.tsx index b094b8f..6e36708 100644 --- a/src/foundation/company-colors.stories.tsx +++ b/src/foundation/company-colors.stories.tsx @@ -14,7 +14,7 @@ function CompanyColorChip({ label, hex, main }: { label: string; hex: string; ma } function CompanyColorsPage() { - return + return {companies.map(company =>
diff --git a/src/patterns/lnb.stories.css b/src/patterns/lnb.stories.css new file mode 100644 index 0000000..a991feb --- /dev/null +++ b/src/patterns/lnb.stories.css @@ -0,0 +1,113 @@ +/* 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. */ +.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). + The sizes use the existing profile variables; the container gets the name size. */ +.hm-lnb-story { + --hm-lnb-profile-name-size: 14px; + --hm-lnb-profile-description-size: 12px; +} +.hm-lnb-story [data-slot="lnb"] [data-slot="lnb-profile-text"] { + font-size: 14px; +} +.hm-lnb-story [data-slot="lnb"] [data-slot="lnb-profile-text"] :is(strong, p) { + font-weight: 700; +} +.hm-lnb-story [data-slot="lnb"] [data-slot="lnb-profile-text"] p { + margin-top: 0; +} +/* Divider lines, same design as the Storybook Manager section divider (manager.css, .sidebar-subheading ::before): a 2px double + line (black 30% over white 40%) faded at both ends by a mask. Shared by the profile divider and the group divider below. */ +.hm-lnb-story [data-slot="lnb"] > [data-slot="lnb-profile"]::before, +.hm-lnb-story .hm-lnb-story-group-divider { + height: 2px; + background: linear-gradient(var(--hm-lnb-text-stroke-color) 0 50%, color-mix(in srgb, var(--hm-sem-palette-white) 40%, transparent) 50% 100%); + mask-image: linear-gradient(90deg, transparent, #000 20%, #000 80%, transparent); + pointer-events: none; +} +/* Above the PC profile, 12px above the block. Like the Manager, it starts after the left inset (--hm-lnb-inset-start, 20px: where + the menu rows start) and stops 20px short of the right edge. The profile button is only as wide as its content, so the line is + sized from the column width (--hm-lnb-width) and offset back by the button's left margin. It sits in the 22px gap between the + menu and the profile, never over the avatar or text. The mobile Sheet profile is not under [data-slot="lnb"]. */ +.hm-lnb-story [data-slot="lnb"] > [data-slot="lnb-profile"] { + position: relative; +} +.hm-lnb-story [data-slot="lnb"] > [data-slot="lnb-profile"]::before { + content: ""; + position: absolute; + top: -12px; + left: calc(var(--hm-lnb-inset-start) + 8px - var(--hm-lnb-profile-padding-x)); + width: calc(var(--hm-lnb-width) - var(--hm-lnb-inset-start) - var(--hm-lnb-inset-end)); +} +/* Between large menu groups (a role="separator" element, direct child of LNBNav). The package already puts a direct child at the + menu rows' x start (margin-left), so only the other margins are set: the line ends 20px short of the column's right edge (the + nav is 1px wider than the column), and 12px + 2px + 10px = 24px in total, the Manager's section gap. */ +.hm-lnb-story .hm-lnb-story-group-divider { + flex: none; /* keep the 2px in a height-limited (scrolling) menu, where an empty item would otherwise shrink to 0 */ + margin-top: 12px; + margin-right: calc(var(--hm-lnb-inset-end) + 1px); + margin-bottom: 10px; +} + +/* Profile in the mobile Sheet (<=1023px): 14px, 700, no text stroke. The Sheet is portaled to body, outside .hm-lnb-story, so the + Story marks its LNBProfile with a class instead. The doubled attribute raises specificity above the package's + [data-slot="lnb-profile"] strong/p rules. Menu links and the mobile header already have no text stroke. */ +.hm-lnb-story-mobile-profile[data-slot="lnb-profile"] [data-slot="lnb-profile-text"], +.hm-lnb-story-mobile-profile[data-slot="lnb-profile"] strong { + font-size: 14px; +} +/* Description: 12px with no top margin, like the PC profile (the line height is unchanged, so the Sheet profile is 4px lower + without the old 4px margin: 8 + 20 + 20 + 8 = 56px). */ +.hm-lnb-story-mobile-profile[data-slot="lnb-profile"] p { + font-size: 12px; + margin-top: 0; +} +.hm-lnb-story-mobile-profile[data-slot="lnb-profile"] :is(strong, p) { + font-weight: 700; + -webkit-text-stroke: 0; + 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 078ee88..43ef77f 100644 --- a/src/patterns/lnb.stories.tsx +++ b/src/patterns/lnb.stories.tsx @@ -32,9 +32,11 @@ import { } from "@hm-design/ui" 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 (272px green column, 18/700 items, Beige current item joined -// to the content, Black 10% hover, Black 8% open group). 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: 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 // (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). @@ -106,6 +108,12 @@ const contentStyle: CSSProperties = { type Child = { id: string; label: string } type LocalNode = { id: string; label: string; icon?: LucideIcon; children?: LocalNode[] } +// Line between large menu groups (a top-level group and its neighbours). A direct child of LNBNav, so it follows the menu rows' +// x start; the look is in lnb.stories.css (same line as the profile divider). Desktop column only: the mobile Sheet is unchanged. +function GroupDivider() { + return
+} + function Menu({ initial = "overview", groupOpen = true, @@ -116,6 +124,7 @@ function Menu({ many, height, profile = true, + dividers = true, }: { initial?: string | null groupOpen?: boolean @@ -128,6 +137,8 @@ function Menu({ height?: number /** Story composition only: GNB + LNB gives profile ownership to the GNB. */ profile?: boolean + /** Group divider lines around the menu group. `false` hides them (the profile divider above the profile stays). */ + dividers?: boolean }) { const [current, setCurrent] = useState(initial) const [open, setOpen] = useState(groupOpen) @@ -162,6 +173,7 @@ function Menu({