feat(storybook): refine Manager LNB, HM Color and Patterns/Layout/LNB
- Manager LNB: 246px width, 16px brand title, 14px/700 menu and section titles, depth1 48px / depth2+ 42px rows, 24px section gap - Foundation: rename Color to HM Color (Story ID foundation-color--all kept), remove Company Colors subtitle and the page-title bottom border - Patterns/Layout/LNB: Storybook-only overrides in lnb.stories.css (240px column, 212px min width, 14px menu, 48/42px rows, profile text, group dividers); representative board dividers per section - Sync story inventory and docs (brand-color-steps, lnb); shared package defaults and tokens are unchanged
This commit is contained in:
1 parent
228944a06b
commit
fc0a3e7725
12 files changed
+246
-86
No files matched your search
+27
-14
@@ -61,13 +61,13 @@
|
|||||||
background: var(--hm-fg-on-green-secondary);
|
background: var(--hm-fg-on-green-secondary);
|
||||||
}
|
}
|
||||||
#storybook-sidebar-region .sidebar-header {
|
#storybook-sidebar-region .sidebar-header {
|
||||||
margin: 0 20px 24px 0;
|
margin: 0 20px 0px 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: var(--color-fg-on-green-primary);
|
color: var(--color-fg-on-green-primary);
|
||||||
}
|
}
|
||||||
#storybook-sidebar-region .sidebar-header > div a {
|
#storybook-sidebar-region .sidebar-header > div a {
|
||||||
color: var(--color-fg-on-green-primary);
|
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;
|
white-space: normal;
|
||||||
/* §6 Text Border: brand title keeps the original 1px (no shadow). */
|
/* §6 Text Border: brand title keeps the original 1px (no shadow). */
|
||||||
-webkit-text-stroke: 1px var(--effect-text-border-color);
|
-webkit-text-stroke: 1px var(--effect-text-border-color);
|
||||||
@@ -78,7 +78,6 @@
|
|||||||
}
|
}
|
||||||
#storybook-sidebar-region [role='search'] {
|
#storybook-sidebar-region [role='search'] {
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
}
|
||||||
#storybook-sidebar-region .search-field {
|
#storybook-sidebar-region .search-field {
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
@@ -109,16 +108,16 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 20px 0 0;
|
padding: 0 20px 0 0;
|
||||||
min-height: 56px;
|
min-height: 48px;
|
||||||
color: var(--hm-fg-on-green-secondary);
|
color: var(--hm-fg-on-green-secondary);
|
||||||
}
|
}
|
||||||
#storybook-explorer-tree .sidebar-subheading[data-hm-section]:not([data-hm-section='0']) {
|
#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 {
|
#storybook-explorer-tree .sidebar-subheading[data-hm-section]:not([data-hm-section='0'])::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -20px;
|
top: -12px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
@@ -126,9 +125,9 @@
|
|||||||
mask-image: linear-gradient(90deg, transparent, #000 20%, #000 80%, transparent);
|
mask-image: linear-gradient(90deg, transparent, #000 20%, #000 80%, transparent);
|
||||||
}
|
}
|
||||||
#storybook-explorer-tree .sidebar-subheading > button {
|
#storybook-explorer-tree .sidebar-subheading > button {
|
||||||
min-height: 56px;
|
min-height: 48px;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font: 700 16px/28px var(--hm-manager-font);
|
font: 700 14px/28px var(--hm-manager-font);
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
}
|
}
|
||||||
@@ -136,7 +135,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
min-height: 56px;
|
min-height: 48px;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
@@ -163,22 +162,30 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
min-height: 56px;
|
min-height: 48px;
|
||||||
height: auto;
|
height: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 14px 32px 14px 18px;
|
padding: 10px 32px 10px 18px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 4px 0 0 4px;
|
border-radius: 4px 0 0 4px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font: 700 18px/28px var(--hm-manager-font);
|
font: 700 14px/28px var(--hm-manager-font);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
text-overflow: clip;
|
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])) {
|
#storybook-explorer-tree .sidebar-item[data-hm-depth='2'] > :is(a[id], button[id]:not([data-testid])) {
|
||||||
margin-left: 32px;
|
margin-left: 32px;
|
||||||
width: calc(100% - 32px);
|
width: calc(100% - 32px);
|
||||||
@@ -188,6 +195,10 @@
|
|||||||
margin-left: 44px;
|
margin-left: 44px;
|
||||||
width: calc(100% - 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 {
|
#storybook-explorer-tree .hm-manager-label {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
@@ -198,8 +209,10 @@
|
|||||||
}
|
}
|
||||||
/* D-093 D-014 (§6 Text Border): non-selected labels, including hover and the
|
/* 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.
|
yellow current parent, use the specimen's 1.5px border plus lower shadow.
|
||||||
Thickness and offset are REVIEW VALUEs. Selected labels carry no effect. */
|
Thickness and offset are REVIEW VALUEs. Selected labels carry no effect.
|
||||||
#storybook-explorer-tree .sidebar-item[data-selected='false'] .hm-manager-label {
|
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);
|
-webkit-text-stroke: 1.5px var(--effect-text-border-color);
|
||||||
paint-order: stroke fill;
|
paint-order: stroke fill;
|
||||||
text-shadow: 1px 2px 1px var(--effect-text-border-color);
|
text-shadow: 1px 2px 1px var(--effect-text-border-color);
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ document.head.appendChild(toolbarStyle)
|
|||||||
addons.setConfig({
|
addons.setConfig({
|
||||||
layout: {
|
layout: {
|
||||||
showPanel: false,
|
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({
|
theme: create({
|
||||||
base: 'light',
|
base: 'light',
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const preview: Preview = {
|
|||||||
includeNames: true,
|
includeNames: true,
|
||||||
order: [
|
order: [
|
||||||
"Foundation",
|
"Foundation",
|
||||||
["Color", "Company Colors", "Typography", "Layout"],
|
["HM Color", "Company Colors", "Typography", "Layout"],
|
||||||
"Components",
|
"Components",
|
||||||
[
|
[
|
||||||
"Form",
|
"Form",
|
||||||
|
|||||||
@@ -499,11 +499,11 @@ Source: `src/components/tooltip.stories.tsx`; meta.tags: []
|
|||||||
- All — Tooltip; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
- 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.
|
- 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: []
|
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
|
### Foundation/Company Colors
|
||||||
|
|
||||||
|
|||||||
@@ -3637,13 +3637,13 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "src/foundation/color.stories.tsx",
|
"file": "src/foundation/color.stories.tsx",
|
||||||
"title": "Foundation/Color",
|
"title": "Foundation/HM Color",
|
||||||
"metaTags": [],
|
"metaTags": [],
|
||||||
"stories": [
|
"stories": [
|
||||||
{
|
{
|
||||||
"id": "foundation-color--all",
|
"id": "foundation-color--all",
|
||||||
"exportName": "All",
|
"exportName": "All",
|
||||||
"name": "Color",
|
"name": "HM Color",
|
||||||
"storyTags": [],
|
"storyTags": [],
|
||||||
"effectiveTags": [],
|
"effectiveTags": [],
|
||||||
"category": "PRIMARY",
|
"category": "PRIMARY",
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ Orange Dark 1·2의 원본 Needs Review 상태와 [이전 검수 보고서](../r
|
|||||||
|
|
||||||
## 호환성과 Storybook
|
## 호환성과 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칩은 그대로다.
|
||||||
@@ -230,6 +230,8 @@ viewport 높이 shell에서 모바일 Header와 main을 세로 flex로 배분할
|
|||||||
|
|
||||||
Collapsible, Navigation Menu 등 기존 primitive 코드·theme CSS는 수정하지 않았다. `primitives.css`에 `--hm-yellow-accent` 한 줄만 추가했다.
|
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. 남은 차이
|
## 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/lnb.css
|
||||||
packages/ui/src/theme/primitives.css (--hm-yellow-accent)
|
packages/ui/src/theme/primitives.css (--hm-yellow-accent)
|
||||||
src/patterns/lnb.stories.tsx
|
src/patterns/lnb.stories.tsx
|
||||||
|
src/patterns/lnb.stories.css (Storybook 전용 오버라이드)
|
||||||
```
|
```
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/* Storybook presentation only. Common page frame for representative Stories; all selectors stay inside .sb-page.
|
/* 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. */
|
Section reuses the Center Graphic / 교량 split: heading + description, a divider, then the specimen. */
|
||||||
.sb-page {
|
.sb-page {
|
||||||
--sb-page-gutter: clamp(16px, 3vw, 40px);
|
--sb-page-gutter: clamp(16px, 3vw, 40px);
|
||||||
@@ -22,7 +22,6 @@
|
|||||||
|
|
||||||
.sb-page .sb-page-header {
|
.sb-page .sb-page-header {
|
||||||
padding-bottom: 24px;
|
padding-bottom: 24px;
|
||||||
border-bottom: 1px solid var(--hm-sem-stroke-input-default);
|
|
||||||
font-size: var(--hm-label-font-size);
|
font-size: var(--hm-label-font-size);
|
||||||
line-height: var(--hm-label-line-height);
|
line-height: var(--hm-label-line-height);
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
|
|||||||
@@ -35,12 +35,13 @@ function ColorChip({ prefix, stage, values }: { prefix: PalettePrefix; stage: Co
|
|||||||
|
|
||||||
function ColorReferencePage() {
|
function ColorReferencePage() {
|
||||||
const values = useResolvedProperties(colorProperties)
|
const values = useResolvedProperties(colorProperties)
|
||||||
return <ReferencePage title="Color" intro="한맥 브랜드 컬러">
|
return <ReferencePage title="HM Color">
|
||||||
{palettes.map(palette => <ReferenceSection key={palette.name} title={palette.name} note="">
|
{palettes.map(palette => <ReferenceSection key={palette.name} title={palette.name} note="">
|
||||||
<div className="hm-brand-color-row">{stages.map(stage => <ColorChip key={stage} prefix={palette.prefix} stage={stage} values={values} />)}</div>
|
<div className="hm-brand-color-row">{stages.map(stage => <ColorChip key={stage} prefix={palette.prefix} stage={stage} values={values} />)}</div>
|
||||||
</ReferenceSection>)}
|
</ReferenceSection>)}
|
||||||
</ReferencePage>
|
</ReferencePage>
|
||||||
}
|
}
|
||||||
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 default meta
|
||||||
export const All: StoryObj<typeof meta> = { name: "Color", render: () => <ColorReferencePage /> }
|
export const All: StoryObj<typeof meta> = { name: "HM Color", render: () => <ColorReferencePage /> }
|
||||||
@@ -14,7 +14,7 @@ function CompanyColorChip({ label, hex, main }: { label: string; hex: string; ma
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CompanyColorsPage() {
|
function CompanyColorsPage() {
|
||||||
return <ReferencePage title="Company Colors" intro="계열사 브랜드 컬러">
|
return <ReferencePage title="Company Colors">
|
||||||
{companies.map(company => <ReferenceSection key={company.companyId} title={company.companyName} note="">
|
{companies.map(company => <ReferenceSection key={company.companyId} title={company.companyName} note="">
|
||||||
<div className="hm-brand-color-row" data-company-id={company.companyId}>
|
<div className="hm-brand-color-row" data-company-id={company.companyId}>
|
||||||
<CompanyColorChip label="메인" hex={company.primary} main />
|
<CompanyColorChip label="메인" hex={company.primary} main />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -32,9 +32,11 @@ import {
|
|||||||
} from "@hm-design/ui"
|
} from "@hm-design/ui"
|
||||||
import { StateBoard } from "@/components/story-board"
|
import { StateBoard } from "@/components/story-board"
|
||||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
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
|
// LNB is a composition pattern. Layout visual reference is the ui-viz LNB (the shared package default: 272px green column, 18/700 items,
|
||||||
// to the content, Black 10% hover, Black 8% open group). The menu data and the current location are local sample data for this Story
|
// 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
|
// (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).
|
// 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 Child = { id: string; label: string }
|
||||||
type LocalNode = { id: string; label: string; icon?: LucideIcon; children?: LocalNode[] }
|
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 <div role="separator" className="hm-lnb-story-group-divider" />
|
||||||
|
}
|
||||||
|
|
||||||
function Menu({
|
function Menu({
|
||||||
initial = "overview",
|
initial = "overview",
|
||||||
groupOpen = true,
|
groupOpen = true,
|
||||||
@@ -116,6 +124,7 @@ function Menu({
|
|||||||
many,
|
many,
|
||||||
height,
|
height,
|
||||||
profile = true,
|
profile = true,
|
||||||
|
dividers = true,
|
||||||
}: {
|
}: {
|
||||||
initial?: string | null
|
initial?: string | null
|
||||||
groupOpen?: boolean
|
groupOpen?: boolean
|
||||||
@@ -128,6 +137,8 @@ function Menu({
|
|||||||
height?: number
|
height?: number
|
||||||
/** Story composition only: GNB + LNB gives profile ownership to the GNB. */
|
/** Story composition only: GNB + LNB gives profile ownership to the GNB. */
|
||||||
profile?: boolean
|
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<string | null>(initial)
|
const [current, setCurrent] = useState<string | null>(initial)
|
||||||
const [open, setOpen] = useState(groupOpen)
|
const [open, setOpen] = useState(groupOpen)
|
||||||
@@ -162,6 +173,7 @@ function Menu({
|
|||||||
<LayoutGridIcon aria-hidden="true" />
|
<LayoutGridIcon aria-hidden="true" />
|
||||||
{longLabels ? "프로젝트 현황과 전체 일정을 한눈에 보는 개요" : "메뉴 1"}
|
{longLabels ? "프로젝트 현황과 전체 일정을 한눈에 보는 개요" : "메뉴 1"}
|
||||||
</LNBItem>
|
</LNBItem>
|
||||||
|
{dividers ? <GroupDivider /> : null}
|
||||||
<LNBGroup label={longLabels ? "업무 관리와 진행 상황 점검" : "메뉴 2"} current={Boolean(current?.startsWith("tasks/"))} open={open} onOpenChange={setOpen}>
|
<LNBGroup label={longLabels ? "업무 관리와 진행 상황 점검" : "메뉴 2"} current={Boolean(current?.startsWith("tasks/"))} open={open} onOpenChange={setOpen}>
|
||||||
{children.map((child) => (
|
{children.map((child) => (
|
||||||
<LNBItem key={child.id} href={`#${child.id}`} active={current === child.id} onClick={go(child.id)}>
|
<LNBItem key={child.id} href={`#${child.id}`} active={current === child.id} onClick={go(child.id)}>
|
||||||
@@ -169,6 +181,7 @@ function Menu({
|
|||||||
</LNBItem>
|
</LNBItem>
|
||||||
))}
|
))}
|
||||||
</LNBGroup>
|
</LNBGroup>
|
||||||
|
{dividers ? <GroupDivider /> : null}
|
||||||
<LNBItem href="#reports" active={current === "reports"} onClick={go("reports")}>
|
<LNBItem href="#reports" active={current === "reports"} onClick={go("reports")}>
|
||||||
<ChartColumnIcon aria-hidden="true" />
|
<ChartColumnIcon aria-hidden="true" />
|
||||||
메뉴 3
|
메뉴 3
|
||||||
@@ -262,6 +275,7 @@ function ThirdLevelMenu({
|
|||||||
<LayoutGridIcon aria-hidden="true" />
|
<LayoutGridIcon aria-hidden="true" />
|
||||||
대시보드
|
대시보드
|
||||||
</LNBItem>
|
</LNBItem>
|
||||||
|
<GroupDivider />
|
||||||
<LNBGroup label="프로젝트 관리" current={onPath("projects")} open={open.projects} onOpenChange={(value) => setOpen((state) => ({ ...state, projects: value }))}>
|
<LNBGroup label="프로젝트 관리" current={onPath("projects")} open={open.projects} onOpenChange={(value) => setOpen((state) => ({ ...state, projects: value }))}>
|
||||||
<LNBGroup label="디자인 관리" current={onPath("projects/design")} open={open.design} onOpenChange={(value) => setOpen((state) => ({ ...state, design: value }))}>
|
<LNBGroup label="디자인 관리" current={onPath("projects/design")} open={open.design} onOpenChange={(value) => setOpen((state) => ({ ...state, design: value }))}>
|
||||||
{thirdLevelChildren(longLabels).map((child) => (
|
{thirdLevelChildren(longLabels).map((child) => (
|
||||||
@@ -277,6 +291,7 @@ function ThirdLevelMenu({
|
|||||||
일정 관리
|
일정 관리
|
||||||
</LNBItem>
|
</LNBItem>
|
||||||
</LNBGroup>
|
</LNBGroup>
|
||||||
|
<GroupDivider />
|
||||||
<LNBItem href="#reports" active={current === "reports"} onClick={go("reports")}>
|
<LNBItem href="#reports" active={current === "reports"} onClick={go("reports")}>
|
||||||
<ChartColumnIcon aria-hidden="true" />
|
<ChartColumnIcon aria-hidden="true" />
|
||||||
현황 보고
|
현황 보고
|
||||||
@@ -361,6 +376,7 @@ function ResponsiveSpecimen({
|
|||||||
brand = "프로젝트 대시보드",
|
brand = "프로젝트 대시보드",
|
||||||
longLabels,
|
longLabels,
|
||||||
height,
|
height,
|
||||||
|
dividers = true,
|
||||||
}: {
|
}: {
|
||||||
nodes: LocalNode[]
|
nodes: LocalNode[]
|
||||||
initial: string | null
|
initial: string | null
|
||||||
@@ -370,6 +386,8 @@ function ResponsiveSpecimen({
|
|||||||
brand?: string
|
brand?: string
|
||||||
longLabels?: boolean
|
longLabels?: boolean
|
||||||
height?: number
|
height?: number
|
||||||
|
/** Group divider lines in the desktop menu, like `Menu`'s `dividers`. `false` hides them (the profile divider stays). */
|
||||||
|
dividers?: boolean
|
||||||
}) {
|
}) {
|
||||||
const [current, setCurrent] = useState<string | null>(initial)
|
const [current, setCurrent] = useState<string | null>(initial)
|
||||||
const [open, setOpen] = useState<Record<string, boolean>>(initialOpen)
|
const [open, setOpen] = useState<Record<string, boolean>>(initialOpen)
|
||||||
@@ -380,25 +398,27 @@ function ResponsiveSpecimen({
|
|||||||
}
|
}
|
||||||
const onPath = (id: string) => Boolean(current?.startsWith(`${id}/`))
|
const onPath = (id: string) => Boolean(current?.startsWith(`${id}/`))
|
||||||
const isOpen = (id: string) => open[id] ?? true
|
const isOpen = (id: string) => open[id] ?? true
|
||||||
const lnbMenu = (items: LocalNode[]): ReactNode =>
|
const lnbMenu = (items: LocalNode[], top = true): ReactNode =>
|
||||||
items.map((node) =>
|
items.map((node, index) => (
|
||||||
node.children ? (
|
<Fragment key={node.id}>
|
||||||
<LNBGroup
|
{dividers && top && index > 0 && (node.children || items[index - 1].children) ? <GroupDivider /> : null}
|
||||||
key={node.id}
|
{node.children ? (
|
||||||
label={node.label}
|
<LNBGroup
|
||||||
current={onPath(node.id)}
|
label={node.label}
|
||||||
open={isOpen(node.id)}
|
current={onPath(node.id)}
|
||||||
onOpenChange={(value) => setOpen((state) => ({ ...state, [node.id]: value }))}
|
open={isOpen(node.id)}
|
||||||
>
|
onOpenChange={(value) => setOpen((state) => ({ ...state, [node.id]: value }))}
|
||||||
{lnbMenu(node.children)}
|
>
|
||||||
</LNBGroup>
|
{lnbMenu(node.children, false)}
|
||||||
) : (
|
</LNBGroup>
|
||||||
<LNBItem key={node.id} href={`#${node.id}`} active={current === node.id} onClick={go(node.id)}>
|
) : (
|
||||||
{node.icon ? <node.icon aria-hidden="true" /> : null}
|
<LNBItem href={`#${node.id}`} active={current === node.id} onClick={go(node.id)}>
|
||||||
{node.label}
|
{node.icon ? <node.icon aria-hidden="true" /> : null}
|
||||||
</LNBItem>
|
{node.label}
|
||||||
)
|
</LNBItem>
|
||||||
)
|
)}
|
||||||
|
</Fragment>
|
||||||
|
))
|
||||||
const sheetMenu = (items: LocalNode[]): ReactNode =>
|
const sheetMenu = (items: LocalNode[]): ReactNode =>
|
||||||
items.map((node) =>
|
items.map((node) =>
|
||||||
node.children ? (
|
node.children ? (
|
||||||
@@ -449,6 +469,7 @@ function ResponsiveSpecimen({
|
|||||||
modal={false}
|
modal={false}
|
||||||
trigger={
|
trigger={
|
||||||
<LNBProfile
|
<LNBProfile
|
||||||
|
className="hm-lnb-story-mobile-profile"
|
||||||
name={profile.name}
|
name={profile.name}
|
||||||
description={profile.description}
|
description={profile.description}
|
||||||
>
|
>
|
||||||
@@ -500,7 +521,7 @@ const standaloneNav: LocalNode[] = [
|
|||||||
{ id: "settings", label: "설정", icon: SettingsIcon },
|
{ id: "settings", label: "설정", icon: SettingsIcon },
|
||||||
]
|
]
|
||||||
|
|
||||||
function Standalone({ mobileOpen = false, profileOpen = false }: { mobileOpen?: boolean; profileOpen?: boolean }) {
|
function Standalone({ mobileOpen = false, profileOpen = false, dividers = true }: { mobileOpen?: boolean; profileOpen?: boolean; dividers?: boolean }) {
|
||||||
const [current, setCurrent] = useState("projects/design/tokens")
|
const [current, setCurrent] = useState("projects/design/tokens")
|
||||||
const [open, setOpen] = useState<Record<string, boolean>>({})
|
const [open, setOpen] = useState<Record<string, boolean>>({})
|
||||||
const go = (id: string) => (event: MouseEvent) => {
|
const go = (id: string) => (event: MouseEvent) => {
|
||||||
@@ -508,25 +529,27 @@ function Standalone({ mobileOpen = false, profileOpen = false }: { mobileOpen?:
|
|||||||
setCurrent(id)
|
setCurrent(id)
|
||||||
}
|
}
|
||||||
const onPath = (id: string) => current.startsWith(`${id}/`)
|
const onPath = (id: string) => current.startsWith(`${id}/`)
|
||||||
const lnbMenu = (nodes: LocalNode[]): ReactNode =>
|
const lnbMenu = (nodes: LocalNode[], top = true): ReactNode =>
|
||||||
nodes.map((node) =>
|
nodes.map((node, index) => (
|
||||||
node.children ? (
|
<Fragment key={node.id}>
|
||||||
<LNBGroup
|
{dividers && top && index > 0 && (node.children || nodes[index - 1].children) ? <GroupDivider /> : null}
|
||||||
key={node.id}
|
{node.children ? (
|
||||||
label={node.label}
|
<LNBGroup
|
||||||
current={onPath(node.id)}
|
label={node.label}
|
||||||
open={open[node.id] ?? true}
|
current={onPath(node.id)}
|
||||||
onOpenChange={(value) => setOpen((state) => ({ ...state, [node.id]: value }))}
|
open={open[node.id] ?? true}
|
||||||
>
|
onOpenChange={(value) => setOpen((state) => ({ ...state, [node.id]: value }))}
|
||||||
{lnbMenu(node.children)}
|
>
|
||||||
</LNBGroup>
|
{lnbMenu(node.children, false)}
|
||||||
) : (
|
</LNBGroup>
|
||||||
<LNBItem key={node.id} href={`#${node.id}`} active={current === node.id} onClick={go(node.id)}>
|
) : (
|
||||||
{node.icon ? <node.icon aria-hidden="true" /> : null}
|
<LNBItem href={`#${node.id}`} active={current === node.id} onClick={go(node.id)}>
|
||||||
{node.label}
|
{node.icon ? <node.icon aria-hidden="true" /> : null}
|
||||||
</LNBItem>
|
{node.label}
|
||||||
)
|
</LNBItem>
|
||||||
)
|
)}
|
||||||
|
</Fragment>
|
||||||
|
))
|
||||||
const sheetMenu = (nodes: LocalNode[]): ReactNode =>
|
const sheetMenu = (nodes: LocalNode[]): ReactNode =>
|
||||||
nodes.map((node) =>
|
nodes.map((node) =>
|
||||||
node.children ? (
|
node.children ? (
|
||||||
@@ -567,6 +590,7 @@ function Standalone({ mobileOpen = false, profileOpen = false }: { mobileOpen?:
|
|||||||
modal={false}
|
modal={false}
|
||||||
trigger={
|
trigger={
|
||||||
<LNBProfile
|
<LNBProfile
|
||||||
|
className="hm-lnb-story-mobile-profile"
|
||||||
name={profileData.name}
|
name={profileData.name}
|
||||||
description={profileData.description}
|
description={profileData.description}
|
||||||
>
|
>
|
||||||
@@ -598,7 +622,7 @@ const mobileViewport = {
|
|||||||
// Representative-board preview of a Story at a fixed viewport. The LNB/bar switch is a viewport media query, so the board embeds the
|
// Representative-board preview of a Story at a fixed viewport. The LNB/bar switch is a viewport media query, so the board embeds the
|
||||||
// verification Story (the same `Standalone` composition) in an iframe of that width instead of rebuilding it in a narrow container.
|
// verification Story (the same `Standalone` composition) in an iframe of that width instead of rebuilding it in a narrow container.
|
||||||
// Each iframe is its own document, so the closed Sheets do not share a portal or focus with the board.
|
// Each iframe is its own document, so the closed Sheets do not share a portal or focus with the board.
|
||||||
function ViewportPreview({ story, label, width, height }: { story: string; label: string; width: number; height: number }) {
|
function ViewportPreview({ story, label, width, height, args }: { story: string; label: string; width: number; height: number; args?: string }) {
|
||||||
return (
|
return (
|
||||||
<figure className="grid min-w-0 content-start gap-2">
|
<figure className="grid min-w-0 content-start gap-2">
|
||||||
<figcaption className="text-sm text-muted-foreground">
|
<figcaption className="text-sm text-muted-foreground">
|
||||||
@@ -607,7 +631,7 @@ function ViewportPreview({ story, label, width, height }: { story: string; label
|
|||||||
<div className="max-w-full overflow-x-auto">
|
<div className="max-w-full overflow-x-auto">
|
||||||
<iframe
|
<iframe
|
||||||
title={`${label} ${width}px`}
|
title={`${label} ${width}px`}
|
||||||
src={`iframe.html?id=patterns-layout-lnb--${story}&viewMode=story`}
|
src={`iframe.html?id=patterns-layout-lnb--${story}&viewMode=story${args ? `&args=${args}` : ""}`}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
className="box-content block border border-black/10"
|
className="box-content block border border-black/10"
|
||||||
@@ -617,7 +641,8 @@ function ViewportPreview({ story, label, width, height }: { story: string; label
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const meta = { title: "Patterns/Layout/LNB", component: LNB, parameters: { layout: "fullscreen", controls: { disable: true } } } satisfies Meta<typeof LNB>
|
// `hm-lnb-story` (display: contents, see lnb.stories.css) scopes the 212px min width and 14px menu font to this Story only.
|
||||||
|
const meta = { title: "Patterns/Layout/LNB", component: LNB, decorators: [(Story) => <div className="hm-lnb-story"><Story /></div>], parameters: { layout: "fullscreen", controls: { disable: true } } } satisfies Meta<typeof LNB>
|
||||||
export default meta
|
export default meta
|
||||||
type Story = StoryObj<typeof meta>
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
@@ -632,8 +657,8 @@ export const All: Story = {
|
|||||||
<StateBoard
|
<StateBoard
|
||||||
min={560}
|
min={560}
|
||||||
states={[
|
states={[
|
||||||
{ id: "selected", label: "Selected", children: <ResponsiveSpecimen nodes={simpleNav()} initial="overview" initialOpen={{ tasks: true }} /> },
|
{ id: "selected", label: "Selected", children: <ResponsiveSpecimen nodes={simpleNav()} initial="overview" initialOpen={{ tasks: true }} dividers={false} /> },
|
||||||
{ id: "third-selected", label: "3뎁스 · Selected", children: <ResponsiveSpecimen nodes={thirdLevelNav()} initial="projects/design/tokens" initialOpen={{ projects: true, "projects/design": true }} /> },
|
{ id: "third-selected", label: "3뎁스 · Selected", children: <ResponsiveSpecimen nodes={thirdLevelNav()} initial="projects/design/tokens" initialOpen={{ projects: true, "projects/design": true }} dividers={false} /> },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</StorySection>
|
</StorySection>
|
||||||
@@ -641,12 +666,8 @@ export const All: Story = {
|
|||||||
<StateBoard
|
<StateBoard
|
||||||
min={560}
|
min={560}
|
||||||
states={[
|
states={[
|
||||||
{ id: "default", label: "Default", children: <ResponsiveSpecimen nodes={simpleNav()} initial={null} initialOpen={{ tasks: false }} /> },
|
|
||||||
{ id: "expanded", label: "Expanded", children: <ResponsiveSpecimen nodes={simpleNav()} initial={null} initialOpen={{ tasks: true }} /> },
|
{ id: "expanded", label: "Expanded", children: <ResponsiveSpecimen nodes={simpleNav()} initial={null} initialOpen={{ tasks: true }} /> },
|
||||||
{ id: "selected-child", label: "Selected Child", children: <ResponsiveSpecimen nodes={simpleNav()} initial="tasks/list" initialOpen={{ tasks: true }} /> },
|
{ id: "states-third-selected", label: "3뎁스 · Selected", children: <ResponsiveSpecimen nodes={thirdLevelNav()} initial="projects/design/tokens" initialOpen={{ projects: true, "projects/design": true }} /> },
|
||||||
{ id: "collapsed-with-selection", label: "Collapsed With Selection", children: <ResponsiveSpecimen nodes={simpleNav()} initial="tasks/list" initialOpen={{ tasks: false }} /> },
|
|
||||||
{ id: "third-second-collapsed", label: "3뎁스 · 2뎁스 접힘", children: <ResponsiveSpecimen nodes={thirdLevelNav()} initial="projects/design/tokens" initialOpen={{ projects: true, "projects/design": false }} /> },
|
|
||||||
{ id: "third-first-collapsed", label: "3뎁스 · 1뎁스 접힘", children: <ResponsiveSpecimen nodes={thirdLevelNav()} initial="projects/design/tokens" initialOpen={{ projects: false, "projects/design": true }} /> },
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</StorySection>
|
</StorySection>
|
||||||
@@ -654,8 +675,8 @@ export const All: Story = {
|
|||||||
<StateBoard
|
<StateBoard
|
||||||
min={560}
|
min={560}
|
||||||
states={[
|
states={[
|
||||||
{ id: "with-logo", label: "With Logo", children: <ResponsiveSpecimen nodes={simpleNav()} initial="overview" initialOpen={{ tasks: true }} logo={testLogo} /> },
|
{ id: "with-logo", label: "With Logo", children: <ResponsiveSpecimen nodes={simpleNav()} initial="overview" initialOpen={{ tasks: true }} logo={testLogo} dividers={false} /> },
|
||||||
{ id: "long", label: "Long Labels", children: <ResponsiveSpecimen nodes={simpleNav(true)} initial="tasks/list" initialOpen={{ tasks: true }} longLabels /> },
|
{ id: "long", label: "Long Labels", children: <ResponsiveSpecimen nodes={simpleNav(true)} initial="tasks/list" initialOpen={{ tasks: true }} longLabels dividers={false} /> },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</StorySection>
|
</StorySection>
|
||||||
@@ -663,14 +684,14 @@ export const All: Story = {
|
|||||||
<StateBoard
|
<StateBoard
|
||||||
min={560}
|
min={560}
|
||||||
states={[
|
states={[
|
||||||
{ id: "scroll", label: "Scroll", children: <ResponsiveSpecimen nodes={simpleNav(false, true)} initial="overview" initialOpen={{ tasks: true }} height={460} /> },
|
{ id: "scroll", label: "Scroll", children: <ResponsiveSpecimen nodes={simpleNav(false, true)} initial="overview" initialOpen={{ tasks: true }} height={460} dividers={false} /> },
|
||||||
{ id: "third-scroll", label: "3뎁스 · Scroll", children: <ResponsiveSpecimen nodes={thirdLevelNav(true, true)} initial="projects/design/layout" initialOpen={{ projects: true, "projects/design": true }} longLabels height={460} /> },
|
{ id: "third-scroll", label: "3뎁스 · Scroll", children: <ResponsiveSpecimen nodes={thirdLevelNav(true, true)} initial="projects/design/layout" initialOpen={{ projects: true, "projects/design": true }} longLabels height={460} dividers={false} /> },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</StorySection>
|
</StorySection>
|
||||||
<StorySection title="Usage · Standalone 반응형">
|
<StorySection title="Usage · Standalone 반응형">
|
||||||
<div className="flex flex-wrap items-start gap-6">
|
<div className="flex flex-wrap items-start gap-6">
|
||||||
<ViewportPreview story="standalone-desktop" label="Desktop" width={1024} height={640} />
|
<ViewportPreview story="standalone-desktop" label="Desktop" width={1024} height={640} args="dividers:!false" />
|
||||||
<ViewportPreview story="standalone-mobile" label="Mobile" width={390} height={640} />
|
<ViewportPreview story="standalone-mobile" label="Mobile" width={390} height={640} />
|
||||||
</div>
|
</div>
|
||||||
</StorySection>
|
</StorySection>
|
||||||
@@ -679,7 +700,7 @@ export const All: Story = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Default: Story = { tags: ["verification"], render: () => <Menu initial={null} groupOpen={false} /> }
|
export const Default: Story = { tags: ["verification"], render: () => <Menu initial={null} groupOpen={false} /> }
|
||||||
export const Selected: Story = { tags: ["verification"], render: () => <Menu /> }
|
export const Selected: Story = { tags: ["verification"], render: () => <Menu dividers={false} /> }
|
||||||
export const Expanded: Story = { tags: ["verification"], render: () => <Menu initial={null} /> }
|
export const Expanded: Story = { tags: ["verification"], render: () => <Menu initial={null} /> }
|
||||||
export const SelectedChild: Story = { tags: ["verification"], name: "Selected Child", render: () => <Menu initial="tasks/list" /> }
|
export const SelectedChild: Story = { tags: ["verification"], name: "Selected Child", render: () => <Menu initial="tasks/list" /> }
|
||||||
export const CollapsedWithSelection: Story = { tags: ["verification"], name: "Collapsed With Selection", render: () => <Menu initial="tasks/list" groupOpen={false} /> }
|
export const CollapsedWithSelection: Story = { tags: ["verification"], name: "Collapsed With Selection", render: () => <Menu initial="tasks/list" groupOpen={false} /> }
|
||||||
@@ -733,7 +754,14 @@ export const WithGNB: Story = {
|
|||||||
|
|
||||||
// LNB without a GNB, responsive. Desktop (>= 1024px): the column. Mobile (<= 1023px): the column is hidden and the connected-content
|
// LNB without a GNB, responsive. Desktop (>= 1024px): the column. Mobile (<= 1023px): the column is hidden and the connected-content
|
||||||
// column becomes a full-width stack whose first row opens a Sheet with the same tree, selected item and every depth.
|
// column becomes a full-width stack whose first row opens a Sheet with the same tree, selected item and every depth.
|
||||||
export const StandaloneDesktop: Story = { tags: ["verification"], name: "LNB Standalone Desktop", render: () => <Standalone /> }
|
// `dividers` is a Story arg (default true: the Story itself keeps its group dividers). The representative board's Desktop preview
|
||||||
|
// opens this Story with `&args=dividers:!false` (see ViewportPreview) to show it without them.
|
||||||
|
export const StandaloneDesktop: StoryObj<{ dividers: boolean }> = {
|
||||||
|
tags: ["verification"],
|
||||||
|
name: "LNB Standalone Desktop",
|
||||||
|
args: { dividers: true },
|
||||||
|
render: (args) => <Standalone dividers={args.dividers} />,
|
||||||
|
}
|
||||||
export const LNBProfileMenuOpen: Story = { tags: ["verification"], name: "LNB Profile Menu Open", render: () => <Standalone profileOpen /> }
|
export const LNBProfileMenuOpen: Story = { tags: ["verification"], name: "LNB Profile Menu Open", render: () => <Standalone profileOpen /> }
|
||||||
export const StandaloneMobile: Story = { tags: ["verification"], name: "LNB Standalone Mobile", ...mobileViewport, render: () => <Standalone /> }
|
export const StandaloneMobile: Story = { tags: ["verification"], name: "LNB Standalone Mobile", ...mobileViewport, render: () => <Standalone /> }
|
||||||
export const StandaloneMobileMenuOpen: Story = {
|
export const StandaloneMobileMenuOpen: Story = {
|
||||||
|
|||||||
Reference in new issue
Block a user