sellmate-design-system-react 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/SBarcodeInput/SBarcodeInput.cjs +61 -49
- package/dist/components/SBarcodeInput/SBarcodeInput.cjs.map +1 -1
- package/dist/components/SBarcodeInput/SBarcodeInput.js +61 -49
- package/dist/components/SBarcodeInput/SBarcodeInput.js.map +1 -1
- package/dist/components/SChipInput/SChipInput.cjs +61 -49
- package/dist/components/SChipInput/SChipInput.cjs.map +1 -1
- package/dist/components/SChipInput/SChipInput.js +61 -49
- package/dist/components/SChipInput/SChipInput.js.map +1 -1
- package/dist/components/SDatePicker/SDatePicker.cjs +61 -49
- package/dist/components/SDatePicker/SDatePicker.cjs.map +1 -1
- package/dist/components/SDatePicker/SDatePicker.js +61 -49
- package/dist/components/SDatePicker/SDatePicker.js.map +1 -1
- package/dist/components/SDateRangePicker/SDateRangePicker.cjs +61 -49
- package/dist/components/SDateRangePicker/SDateRangePicker.cjs.map +1 -1
- package/dist/components/SDateRangePicker/SDateRangePicker.js +61 -49
- package/dist/components/SDateRangePicker/SDateRangePicker.js.map +1 -1
- package/dist/components/SField/SField.cjs +61 -49
- package/dist/components/SField/SField.cjs.map +1 -1
- package/dist/components/SField/SField.d.ts +5 -2
- package/dist/components/SField/SField.js +61 -49
- package/dist/components/SField/SField.js.map +1 -1
- package/dist/components/SFilePicker/SFilePicker.cjs +61 -49
- package/dist/components/SFilePicker/SFilePicker.cjs.map +1 -1
- package/dist/components/SFilePicker/SFilePicker.js +61 -49
- package/dist/components/SFilePicker/SFilePicker.js.map +1 -1
- package/dist/components/SInput/SInput.cjs +61 -49
- package/dist/components/SInput/SInput.cjs.map +1 -1
- package/dist/components/SInput/SInput.js +61 -49
- package/dist/components/SInput/SInput.js.map +1 -1
- package/dist/components/SKeyValueTable/SKeyValueTable.cjs +61 -49
- package/dist/components/SKeyValueTable/SKeyValueTable.cjs.map +1 -1
- package/dist/components/SKeyValueTable/SKeyValueTable.js +61 -49
- package/dist/components/SKeyValueTable/SKeyValueTable.js.map +1 -1
- package/dist/components/SNumberInput/SNumberInput.cjs +61 -49
- package/dist/components/SNumberInput/SNumberInput.cjs.map +1 -1
- package/dist/components/SNumberInput/SNumberInput.js +61 -49
- package/dist/components/SNumberInput/SNumberInput.js.map +1 -1
- package/dist/components/SPage/SPage.cjs +19 -2
- package/dist/components/SPage/SPage.cjs.map +1 -1
- package/dist/components/SPage/SPage.d.ts +7 -1
- package/dist/components/SPage/SPage.js +19 -2
- package/dist/components/SPage/SPage.js.map +1 -1
- package/dist/components/SPage/index.d.ts +1 -0
- package/dist/components/SPage/page.config.d.ts +8 -0
- package/dist/components/SPopup/SPopup.cjs.map +1 -1
- package/dist/components/SPopup/SPopup.js.map +1 -1
- package/dist/components/SScrollArea/SScrollArea.cjs.map +1 -1
- package/dist/components/SScrollArea/SScrollArea.js.map +1 -1
- package/dist/components/SSelect/SSelect.cjs +61 -49
- package/dist/components/SSelect/SSelect.cjs.map +1 -1
- package/dist/components/SSelect/SSelect.js +61 -49
- package/dist/components/SSelect/SSelect.js.map +1 -1
- package/dist/components/STable/STable.cjs +67 -55
- package/dist/components/STable/STable.cjs.map +1 -1
- package/dist/components/STable/STable.d.ts +2 -0
- package/dist/components/STable/STable.js +67 -55
- package/dist/components/STable/STable.js.map +1 -1
- package/dist/components/STextarea/STextarea.cjs +61 -49
- package/dist/components/STextarea/STextarea.cjs.map +1 -1
- package/dist/components/STextarea/STextarea.js +61 -49
- package/dist/components/STextarea/STextarea.js.map +1 -1
- package/dist/components/STimePicker/STimePicker.cjs +61 -49
- package/dist/components/STimePicker/STimePicker.cjs.map +1 -1
- package/dist/components/STimePicker/STimePicker.js +61 -49
- package/dist/components/STimePicker/STimePicker.js.map +1 -1
- package/dist/components/STimeRangePicker/STimeRangePicker.cjs +61 -49
- package/dist/components/STimeRangePicker/STimeRangePicker.cjs.map +1 -1
- package/dist/components/STimeRangePicker/STimeRangePicker.js +61 -49
- package/dist/components/STimeRangePicker/STimeRangePicker.js.map +1 -1
- package/dist/index.cjs +84 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +84 -58
- package/dist/index.js.map +1 -1
- package/dist/lib/scrollbar.cjs +2 -0
- package/dist/lib/scrollbar.cjs.map +1 -1
- package/dist/lib/scrollbar.d.ts +13 -1
- package/dist/lib/scrollbar.js +2 -1
- package/dist/lib/scrollbar.js.map +1 -1
- package/dist/styles.css +53 -3
- package/package.json +1 -1
|
@@ -17,6 +17,13 @@ function cn(...inputs) {
|
|
|
17
17
|
return twMerge(clsx.clsx(inputs));
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// src/lib/scrollbar.ts
|
|
21
|
+
var FIREFOX_SCROLLBAR_CLASS = "supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-width:thin] supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-color:var(--color-grey-45)_transparent]";
|
|
22
|
+
var SCROLLBAR_CLASS = `${FIREFOX_SCROLLBAR_CLASS} [&::-webkit-scrollbar]:h-[12px] [&::-webkit-scrollbar]:w-[12px] [&::-webkit-scrollbar]:bg-transparent [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:border-2 [&::-webkit-scrollbar-thumb]:border-solid [&::-webkit-scrollbar-thumb]:border-transparent [&::-webkit-scrollbar-thumb]:bg-[var(--color-grey-45)] [&::-webkit-scrollbar-thumb]:bg-clip-padding`;
|
|
23
|
+
var SCROLLBAR_TRACK_BG_CLASS = "supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-color:var(--color-grey-45)_var(--color-grey-10)] [&::-webkit-scrollbar]:bg-[var(--color-grey-10)] [&::-webkit-scrollbar-track]:bg-[var(--color-grey-10)]";
|
|
24
|
+
var SCROLLBAR_TRACK_BG_WHITE_CLASS = "supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-color:var(--color-grey-45)_var(--color-white)] [&::-webkit-scrollbar]:bg-[var(--color-white)] [&::-webkit-scrollbar-track]:bg-[var(--color-white)]";
|
|
25
|
+
var SCROLLBAR_TRACK_BORDER_CLASS = "[&::-webkit-scrollbar-track:vertical]:[border-left:1px_solid_var(--sys-color-border-default)] [&::-webkit-scrollbar-track:horizontal]:[border-top:1px_solid_var(--sys-color-border-default)] [&::-webkit-scrollbar-thumb:vertical]:[border-left-width:3px] [&::-webkit-scrollbar-thumb:horizontal]:[border-top-width:3px]";
|
|
26
|
+
|
|
20
27
|
// src/components/SGnb/gnb.config.ts
|
|
21
28
|
var gnb = (p) => `var(--cmp-gnb-${p})`;
|
|
22
29
|
var HEADER_TOP = {
|
|
@@ -100,14 +107,24 @@ react.forwardRef(function SLayout2({
|
|
|
100
107
|
}
|
|
101
108
|
) });
|
|
102
109
|
});
|
|
103
|
-
var
|
|
110
|
+
var BACKGROUND_CLASS = {
|
|
111
|
+
frame: `bg-[var(--sys-color-bg-frame)] ${SCROLLBAR_TRACK_BORDER_CLASS} ${SCROLLBAR_TRACK_BG_CLASS}`,
|
|
112
|
+
neutral: `bg-[var(--sys-color-bg-neutralLight)] ${SCROLLBAR_TRACK_BORDER_CLASS} ${SCROLLBAR_TRACK_BG_WHITE_CLASS}`,
|
|
113
|
+
screen: `bg-[var(--sys-color-bg-screen)] ${SCROLLBAR_TRACK_BORDER_CLASS} ${SCROLLBAR_TRACK_BG_CLASS}`
|
|
114
|
+
};
|
|
115
|
+
var SPage = react.forwardRef(function SPage2({ background = "frame", className, style, children, ...rest }, ref) {
|
|
104
116
|
const layout = react.useContext(SLayoutContext);
|
|
105
117
|
const gridStyle = layout?.header === "full" ? { gridColumn: 2, gridRow: 2 } : void 0;
|
|
106
118
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
107
119
|
"main",
|
|
108
120
|
{
|
|
109
121
|
ref,
|
|
110
|
-
className: cn(
|
|
122
|
+
className: cn(
|
|
123
|
+
"box-border min-h-0 min-w-0 flex-1 overflow-auto p-20",
|
|
124
|
+
SCROLLBAR_CLASS,
|
|
125
|
+
BACKGROUND_CLASS[background],
|
|
126
|
+
className
|
|
127
|
+
),
|
|
111
128
|
style: { ...gridStyle, ...style },
|
|
112
129
|
...rest,
|
|
113
130
|
children
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/cn.ts","../../../src/components/SGnb/gnb.config.ts","../../../src/components/SLayout/SLayout.tsx","../../../src/components/SPage/SPage.tsx"],"names":["extendTailwindMerge","clsx","createContext","forwardRef","SLayout","useState","useCallback","useMemo","jsx","SPage","useContext"],"mappings":"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","file":"SPage.cjs","sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { extendTailwindMerge } from 'tailwind-merge';\n\n/**\n * theme.css 의 --text-* 는 기본 Tailwind 폰트사이즈 스케일(xs/sm/base...)이 아닌\n * 숫자 스케일(10~32)이라, 확장 없이는 tailwind-merge 가 이를 텍스트 컬러 그룹으로\n * 오인해 text-12 같은 클래스를 text-(--color-var) 뒤에서 충돌 제거해버린다.\n */\nconst twMerge = extendTailwindMerge({\n extend: {\n classGroups: {\n 'font-size': [{ text: ['10', '11', '12', '13', '14', '16', '18', '20', '24', '28', '32'] }],\n },\n },\n});\n\n/**\n * className 병합 유틸.\n * clsx로 조건부 클래스를 합치고, tailwind-merge로 Tailwind 클래스 충돌을 해소한다.\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","import type { SColor } from '../../lib/color';\nimport type { SIconName } from '../SIcon';\nimport type { STagColor } from '../STag';\n\n/** 메뉴 스타일: box = 라운드/좁은 들여쓰기, belt = 풀폭 행/넓은 들여쓰기 */\nexport type SGnbType = 'box' | 'belt';\n/** 상단바 구조: fix = GNB 폭에 고정, full = 레이아웃 전폭 */\nexport type SGnbHeader = 'fix' | 'full';\n/** 색상(테마): light / dark */\nexport type SGnbColor = 'light' | 'dark';\n\n/** GNB 메뉴 아이템 (재귀 트리). depth1 → depth2 → depth3. */\nexport interface SGnbMenuItem {\n /** 표시 텍스트 */\n label: string;\n /** 고유 식별값 (선택 상태 비교 기준) */\n value: string;\n /** depth1 전용 아이콘명 */\n icon?: SIconName;\n /** 우측 Tag 뱃지 텍스트 */\n tag?: string;\n /** 뱃지 색. 미지정 시 blue. */\n tagColor?: STagColor;\n /** 비활성 여부 */\n disabled?: boolean;\n /** 하위 아이템 (있으면 펼침/접힘 대상) */\n children?: SGnbMenuItem[];\n}\n\n// theme.css 의 컴포넌트 토큰 참조 (디자인팀 소유, 직접 수정 금지)\n// 토큰은 gnb 한 벌로 통합됐다:\n// 공통(상단바/로고/폴드/바디/아이템) → --cmp-gnb-*\n// 메뉴 변형(type 축) box → --cmp-gnb-box-*, belt → --cmp-gnb-belt-*\n// 접힘 레일 → --cmp-gnb-rail-*\n// 색(color 축) light → --cmp-gnb-light-*, dark → --cmp-gnb-dark-*\nconst gnb = (p: string) => `var(--cmp-gnb-${p})`;\nconst box = (p: string) => `var(--cmp-gnb-box-${p})`;\nconst belt = (p: string) => `var(--cmp-gnb-belt-${p})`;\nconst light = (p: string) => `var(--cmp-gnb-light-${p})`;\nconst dark = (p: string) => `var(--cmp-gnb-dark-${p})`;\n\n/**\n * 상단바(header 축). 상단바 치수 토큰은 gnb 로 통합돼(--cmp-gnb-top-*) fix·full 이 공유한다.\n * 배치는 여전히 이 축이 정한다 — fix 는 GNB 컬럼 안, full 은 레이아웃 전폭(SGnb 에서 분기).\n */\nconst HEADER_TOP = {\n topHeight: gnb('top-height'),\n topPaddingX: gnb('top-paddingX'),\n topGap: gnb('top-gap'),\n logoHeight: gnb('logo-height'),\n};\n\nexport const GNB_HEADER: Record<\n SGnbHeader,\n {\n topHeight: string;\n topPaddingX: string;\n topGap: string;\n logoHeight: string;\n }\n> = {\n fix: HEADER_TOP,\n full: HEADER_TOP,\n};\n\n/** 메뉴(type 축). box → --cmp-gnb-box-*, belt → --cmp-gnb-belt-* (공통 치수는 --cmp-gnb-*) */\nexport const GNB_MENU: Record<\n SGnbType,\n {\n bodyPaddingY: string;\n itemMarginX: string;\n itemPaddingY: string;\n itemPaddingRight: string;\n itemGap: string;\n itemRadius: string;\n itemArrow: string;\n depth1PaddingLeft: string;\n depth2PaddingLeft: string;\n depth3PaddingLeft: string;\n depth1Icon: string;\n // 타이포 (theme.css 에 gnb 타이포 변수 없음 → 토큰 값 직접 명시)\n depth1FontSize: number;\n depth1LineHeight: number;\n depth1FontWeight: number;\n itemFontSize: number;\n itemLineHeight: number;\n }\n> = {\n box: {\n // 바디 패딩·세로 패딩·chevron 은 gnb 로 통합된 공통 토큰\n bodyPaddingY: gnb('body-padding'),\n itemMarginX: gnb('body-padding'), // 전방향 토큰 → 아이템 좌우 여백에도 사용\n itemPaddingY: gnb('item-paddingY'),\n itemPaddingRight: box('item-paddingRight'),\n itemGap: box('item-gap'),\n itemRadius: box('item-radius'),\n itemArrow: gnb('item-chevron-size'),\n depth1PaddingLeft: box('item-depth1-paddingLeft'),\n depth2PaddingLeft: box('item-depth2-paddingLeft'),\n depth3PaddingLeft: box('item-depth3-paddingLeft'),\n depth1Icon: box('item-depth1-icon'),\n depth1FontSize: 14, // token: gnb.box.item.typography.selected.fontSize\n depth1LineHeight: 24, // token: gnb.box.item.typography.selected.lineHeight\n depth1FontWeight: 700,\n itemFontSize: 14, // token: gnb.box.item.typography.default.fontSize\n itemLineHeight: 24, // token: gnb.box.item.typography.default.lineHeight\n },\n belt: {\n bodyPaddingY: gnb('body-padding'),\n itemMarginX: '0px', // belt 토큰에 가로 여백 없음 → 풀폭 행 (들여쓰기는 depth*-paddingLeft 전담)\n itemPaddingY: gnb('item-paddingY'),\n itemPaddingRight: belt('item-paddingRight'),\n itemGap: belt('item-gap'),\n itemRadius: '0px', // belt 토큰에 radius 없음 → 풀폭 행\n itemArrow: gnb('item-chevron-size'),\n depth1PaddingLeft: belt('item-depth1-paddingLeft'),\n depth2PaddingLeft: belt('item-depth2-paddingLeft'),\n depth3PaddingLeft: belt('item-depth3-paddingLeft'),\n depth1Icon: belt('item-depth1-icon'), // belt 전용 depth1 아이콘 토큰(16)\n depth1FontSize: 13, // token: gnb.belt.item.depth1.typography.fontSize\n depth1LineHeight: 22, // token: gnb.belt.item.depth1.typography.lineHeight\n depth1FontWeight: 700,\n itemFontSize: 12, // token: gnb.belt.item.typography.default.fontSize\n itemLineHeight: 20, // token: gnb.belt.item.typography.default.lineHeight\n },\n};\n\n/**\n * 색상(color 축). light → --cmp-gnb-light-* 색, dark → --cmp-gnb-dark-* 색\n * 상단바 아이콘 색(--cmp-gnb-*-top-icon)은 SGhostButton 이 자체 intent 토큰으로 칠한다:\n * light → --cmp-ghostButton-default-content, dark → --cmp-ghostButton-inverse-content (값 동일).\n */\nexport const GNB_COLORS: Record<\n SGnbColor,\n {\n panelBg: string;\n topBorder: string;\n itemBgDefault: string;\n itemBgHover: string;\n itemBgSelected: string;\n itemColorDefault: string;\n itemColorHover: string;\n itemColorActive: string;\n itemChevron: string;\n /** depth1 아이콘 색. 대응 --cmp-gnb-*-item-icon 토큰이 없어 팔레트 키로 직접 지정한다. */\n itemIcon: SColor;\n /** 레일 아이템 기본 색(아이콘+라벨). 선택 시엔 itemColorActive 를 쓴다. */\n railFgDefault: string;\n }\n> = {\n light: {\n panelBg: light('bg'),\n topBorder: light('divider'),\n itemBgDefault: light('item-bg-default'),\n itemBgHover: light('item-bg-hover'),\n itemBgSelected: light('item-bg-selected'),\n itemColorDefault: light('item-fg-default'),\n itemColorHover: light('item-fg-hover'),\n itemColorActive: light('item-fg-selected'),\n itemChevron: light('item-chevron'),\n itemIcon: 'grey_65',\n railFgDefault: light('rail-fg-default'),\n },\n dark: {\n panelBg: dark('bg'),\n topBorder: dark('divider'),\n itemBgDefault: dark('item-bg-default'),\n itemBgHover: dark('item-bg-hover'),\n itemBgSelected: dark('item-bg-selected'),\n itemColorDefault: dark('item-fg-default'),\n itemColorHover: dark('item-fg-hover'),\n itemColorActive: dark('item-fg-selected'),\n itemChevron: dark('item-chevron'),\n itemIcon: 'grey_55',\n railFgDefault: dark('rail-fg-default'),\n },\n};\n\n/** 축과 무관한 공통 레이아웃. */\nexport const GNB_COMMON = {\n /** 접힘 레일 폭. header=\"full\" 은 접으면 메뉴만 사라져 레일이 없으므로 fix 전용. */\n foldWidth: gnb('fold-width'),\n};\n\n/**\n * 좌측 레일(useRail) — items 의 첫 depth 를 아이콘+라벨 버튼으로 세우는 1차 내비게이션.\n * 선택 색은 레일 전용 토큰이 없어 메뉴 아이템의 선택 색(item-bg-selected / item-fg-selected)을 함께 쓴다.\n */\nexport const GNB_RAIL = {\n padding: gnb('rail-padding'),\n gap: gnb('rail-gap'),\n itemWidth: gnb('rail-item-width'),\n itemPaddingTop: gnb('rail-item-paddingTop'),\n itemPaddingBottom: gnb('rail-item-paddingBottom'),\n itemRadius: gnb('rail-item-radius'),\n itemIcon: gnb('rail-item-icon'),\n // 타이포 (theme.css 에 gnb 타이포 변수 없음 → 토큰 값 직접 명시)\n itemFontSize: 11, // token: gnb.rail.item.typography.default.fontSize\n itemLineHeight: 18, // token: gnb.rail.item.typography.default.lineHeight\n itemFontWeight: 500, // token: gnb.rail.item.typography.default.fontWeight\n itemFontWeightSelected: 700, // token: gnb.rail.item.typography.selected.fontWeight\n};\n\n/**\n * 레일 컬럼 폭 = rail-item-width(44) + rail-padding(12)*2.\n * 토큰이 CSS 변수라 JS 에서 더할 수 없어 합을 상수로 둔다 (GNB_MENU_WIDTH 와 같은 이유).\n */\nexport const GNB_RAIL_WIDTH = 68;\n\n/**\n * 접힘/펼침 전환 시간(ms). GNB 폭·메뉴 슬라이드·상단바 페이드와 SLayout 의 메뉴 열 폭이\n * 이 값을 공유해야 한 덩어리로 미끄러진다.\n */\nexport const GNB_FOLD_MS = 200;\n\n/**\n * 하위 메뉴 펼침/접힘 전환 시간(ms). 아이템을 눌러 children 이 열리고 닫힐 때\n * 높이(grid-template-rows 0fr↔1fr)가 이 길이만큼 부드럽게 늘고 줄어든다.\n * 폴드와 같은 값을 써서 두 모션이 한 언어로 읽히게 한다.\n */\nexport const GNB_EXPAND_MS = 200;\n\n/**\n * 메뉴 고정 폭. 두 header 모두 메뉴는 이 폭으로 좌측에 붙는다(토큰 없음 → 상수).\n * header=\"full\" 에서 SLayout 이 \"메뉴 오른쪽 = 페이지\" 영역을 잡을 때도 이 값을 쓴다.\n */\nexport const GNB_MENU_WIDTH = 240;\n\n/**\n * useRail 에서 value 를 품은 첫 depth(레일) 아이템의 value.\n * value 자체가 레일 아이템이면 그 값을, 어디에도 없으면 undefined 를 반환한다.\n */\nexport function findGnbRailValue(items: SGnbMenuItem[], value: string): string | undefined {\n if (items.some(item => item.value === value)) return value;\n return findGnbAncestors(items, value)[0];\n}\n\n/** value 로 선택된 아이템의 조상 value 목록(자기 자신 제외)을 반환 → 자동 펼침용 */\nexport function findGnbAncestors(items: SGnbMenuItem[], value: string): string[] {\n const walk = (list: SGnbMenuItem[], trail: string[]): string[] | null => {\n for (const item of list) {\n if (item.value === value) return trail;\n if (item.children) {\n const found = walk(item.children, [...trail, item.value]);\n if (found) return found;\n }\n }\n return null;\n };\n return walk(items, []) ?? [];\n}\n","'use client';\n\nimport {\n createContext,\n forwardRef,\n useCallback,\n useContext,\n useMemo,\n useState,\n type CSSProperties,\n type HTMLAttributes,\n} from 'react';\nimport { cn } from '../../lib/cn';\nimport {\n GNB_FOLD_MS,\n GNB_HEADER,\n GNB_MENU_WIDTH,\n GNB_RAIL_WIDTH,\n type SGnbHeader,\n type SGnbType,\n} from '../SGnb/gnb.config';\n\n/** 메뉴 스타일: box(라운드) / belt(풀폭 행) — 자식 SGnb 의 메뉴 모양 */\nexport type SLayoutType = SGnbType;\n/** 레이아웃 구조: fix(좌측 GNB + 페이지 가로 분할) / full(풀폭 상단바 + 아래에 메뉴|페이지) */\nexport type SLayoutHeader = SGnbHeader;\n\nexport interface SLayoutContextValue {\n /** 메뉴 스타일 — 자식 SGnb 가 스타일을 맞추는 기준 */\n type: SLayoutType;\n /**\n * 상단바 구조 — 자식 SGnb·SPage 가 배치를 맞추는 기준(유효 header).\n * 레일에 children 없는 아이템이 있어 SGnb 가 full 을 강제하면 여기에도 full 로 반영된다.\n */\n header: SLayoutHeader;\n /** GNB 좌측 레일 사용 여부 — 자식 SGnb 가 따르고, header=\"full\" 의 메뉴 열 폭도 이 값을 반영한다 */\n useRail: boolean;\n /** GNB 접힘 상태 */\n folded: boolean;\n /** 접힘 상태 변경 */\n setFolded: (folded: boolean) => void;\n /**\n * 자식 SGnb 가 런타임 배치 상태를 알린다(SLayout 은 items·activeRail 을 모른다).\n * - forceFull: 레일에 리프(children 없는) 아이템이 있어 header 를 full 로 강제했는지 → 프레임을 grid 로 맞춘다.\n * - menuVisible: 활성 레일에 메뉴 패널이 깔렸는지 → false 면 메뉴 열을 빼 레일 폭만 예약한다(빈틈 방지).\n */\n reportGnbState: (state: { forceFull: boolean; menuVisible: boolean }) => void;\n}\n\nexport const SLayoutContext = createContext<SLayoutContextValue | null>(null);\n\n/**\n * SLayout 의 상태를 읽고 제어한다. `<SLayout>` 밖에서 호출하면 throw.\n * 외부 버튼으로 GNB 를 접거나, 현재 구조(header)·메뉴 스타일(type)에 따라 분기할 때 사용한다.\n */\nexport function useSLayout(): SLayoutContextValue {\n const ctx = useContext(SLayoutContext);\n if (!ctx) throw new Error('useSLayout 은 <SLayout> 안에서만 사용할 수 있습니다.');\n return ctx;\n}\n\nexport interface SLayoutProps extends HTMLAttributes<HTMLDivElement> {\n /** 메뉴 스타일: box(라운드) / belt(풀폭 행). 자식 SGnb 가 이 값을 따른다. */\n type?: SLayoutType;\n /** 레이아웃 구조: fix(가로 분할) / full(풀폭 상단바). 자식 SGnb·SPage 가 이 값을 따른다. */\n header?: SLayoutHeader;\n /**\n * GNB 좌측 레일 사용 여부. 자식 SGnb 가 이 값을 따르고, full 의 메뉴 열 폭도 레일만큼 넓어진다.\n * 레일에 children 없는 아이템이 있으면 SGnb 가 header 를 full 로 강제하고, 이 레이아웃도 그에 맞춰 full 그리드로 선다.\n */\n useRail?: boolean;\n /** GNB 접힘 상태 (controlled) */\n folded?: boolean;\n /** GNB 초기 접힘 상태 (uncontrolled) */\n defaultFolded?: boolean;\n /** 접힘 상태 변경 */\n onFoldedChange?: (folded: boolean) => void;\n}\n\n/**\n * SLayout — 앱 셸 프레임 겸 context provider.\n * header 가 배치를 결정하고 overflow:hidden 으로 스크롤 경계를 만든다.\n * 자식 SGnb·SPage 는 context 에서 type·header·folded 를 읽으므로 앱은 이 값들을 여기 한 곳에만 쓴다.\n * 높이는 기본 100%(부모를 채움) — 앱 루트로 쓸 땐 부모를 뷰포트 높이로 잡는다.\n *\n * - header=\"fix\": [GNB(자기 폭 소유) | 페이지] 가로 flex.\n * - header=\"full\": 상단바가 풀폭이고 그 아래에 [메뉴 | 페이지] 가 나란히 서야 하는데, SGnb 가 상단바와\n * 메뉴를 한 엘리먼트로 갖고 있어 사이에 끼워넣을 수 없다. 그래서 grid 로 깔고\n * SGnb 를 전 셀에 겹친 뒤(root 는 투명) SPage 가 (2행,2열) = \"메뉴 오른쪽 투명 영역\" 을 차지한다.\n * 접으면 메뉴가 사라지므로 1열을 0 으로 접어 페이지가 전폭을 쓴다.\n */\nexport const SLayout = forwardRef<HTMLDivElement, SLayoutProps>(function SLayout(\n {\n type = 'box',\n header = 'fix',\n useRail = false,\n folded,\n defaultFolded = false,\n onFoldedChange,\n className,\n style,\n children,\n ...rest\n },\n ref,\n) {\n const [uncontrolledFolded, setUncontrolledFolded] = useState(defaultFolded);\n const isControlled = folded !== undefined;\n const currentFolded = isControlled ? folded : uncontrolledFolded;\n\n const setFolded = useCallback(\n (next: boolean) => {\n if (!isControlled) setUncontrolledFolded(next);\n onFoldedChange?.(next);\n },\n [isControlled, onFoldedChange],\n );\n\n // 자식 SGnb 가 알려주는 런타임 배치 상태. SLayout 은 items·activeRail 을 모르므로 이 신호로\n // full 강제 여부와 메뉴 열 폭을 맞춘다. prev 동일값이면 같은 참조를 유지해 불필요한 리렌더를 막는다.\n const [gnbState, setGnbState] = useState({ forceFull: false, menuVisible: true });\n const reportGnbState = useCallback(\n (next: { forceFull: boolean; menuVisible: boolean }) =>\n setGnbState(prev =>\n prev.forceFull === next.forceFull && prev.menuVisible === next.menuVisible ? prev : next,\n ),\n [],\n );\n\n // 레일에 리프 아이템이 있으면 SGnb 가 header 를 full 로 강제한다 → 프레임(grid)도 그에 맞춘다.\n const effectiveHeader: SLayoutHeader = header === 'full' || gnbState.forceFull ? 'full' : 'fix';\n\n const ctx = useMemo<SLayoutContextValue>(\n () => ({\n type,\n header: effectiveHeader,\n useRail,\n folded: currentFolded,\n setFolded,\n reportGnbState,\n }),\n [type, effectiveHeader, useRail, currentFolded, setFolded, reportGnbState],\n );\n\n const isFullHeader = effectiveHeader === 'full';\n // 레일이 있으면 GNB 가 [레일 | 메뉴] 두 칸이므로 페이지는 그만큼 오른쪽에서 시작한다.\n // 리프 레일이 활성이라 메뉴가 숨겨지면(menuVisible=false) 메뉴 칸을 빼 레일 폭만 예약한다(빈틈 방지).\n const gnbColumnWidth =\n (useRail ? GNB_RAIL_WIDTH : 0) + (gnbState.menuVisible ? GNB_MENU_WIDTH : 0);\n const frameStyle: CSSProperties | undefined = isFullHeader\n ? {\n gridTemplateColumns: `${currentFolded ? 0 : gnbColumnWidth}px 1fr`,\n gridTemplateRows: `${GNB_HEADER[effectiveHeader].topHeight} 1fr`,\n // 메뉴가 미끄러지는 동안 페이지 열도 같은 길이로 따라와야 한 덩어리로 읽힌다.\n // (fix 는 SGnb 가 제 폭을 직접 전환하므로 프레임이 손댈 게 없다)\n transition: `grid-template-columns ${GNB_FOLD_MS}ms ease-out`,\n }\n : undefined;\n\n return (\n <SLayoutContext.Provider value={ctx}>\n <div\n ref={ref}\n data-type={type}\n data-header={effectiveHeader}\n className={cn(\n 'box-border h-full overflow-hidden',\n isFullHeader ? 'grid' : 'flex flex-row',\n className,\n )}\n style={{ ...frameStyle, ...style }}\n {...rest}\n >\n {children}\n </div>\n </SLayoutContext.Provider>\n );\n});\n","import { forwardRef, useContext, type CSSProperties, type HTMLAttributes } from 'react';\nimport { cn } from '../../lib/cn';\nimport { SLayoutContext } from '../SLayout/SLayout';\n\n// 자체 prop 없이 표준 HTML 속성만 받는다.\n// docs:gen 은 export interface 만 수집하므로(gen-docs.mjs) 타입 별칭 대신 interface 로 둔다.\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface SPageProps extends HTMLAttributes<HTMLElement> {}\n\n/**\n * SPage — SLayout 안에서 남은 공간을 채우는 스크롤 콘텐츠 영역.\n * 스크롤은 SLayout(overflow:hidden)이 잡아둔 경계 안에서 이 영역에만 생긴다.\n * 패딩·배경은 className 으로 덮어쓸 수 있다.\n *\n * - header=\"fix\": flex 자식으로 남은 가로 공간을 채운다.\n * - header=\"full\": SLayout 이 깐 grid 의 (2행,2열) = 상단바 아래·메뉴 오른쪽 자리를 차지한다.\n * (SGnb 가 전 셀에 겹쳐 있지만 그 영역은 투명해서 SPage 가 보인다)\n */\nexport const SPage = forwardRef<HTMLElement, SPageProps>(function SPage(\n { className, style, children, ...rest },\n ref,\n) {\n const layout = useContext(SLayoutContext);\n const gridStyle: CSSProperties | undefined =\n layout?.header === 'full' ? { gridColumn: 2, gridRow: 2 } : undefined;\n\n return (\n <main\n ref={ref}\n className={cn('box-border min-h-0 min-w-0 flex-1 overflow-auto p-20', className)}\n style={{ ...gridStyle, ...style }}\n {...rest}\n >\n {children}\n </main>\n );\n});\n"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/cn.ts","../../../src/lib/scrollbar.ts","../../../src/components/SGnb/gnb.config.ts","../../../src/components/SLayout/SLayout.tsx","../../../src/components/SPage/SPage.tsx"],"names":["extendTailwindMerge","clsx","createContext","forwardRef","SLayout","useState","useCallback","useMemo","jsx","SPage","useContext"],"mappings":"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","file":"SPage.cjs","sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { extendTailwindMerge } from 'tailwind-merge';\n\n/**\n * theme.css 의 --text-* 는 기본 Tailwind 폰트사이즈 스케일(xs/sm/base...)이 아닌\n * 숫자 스케일(10~32)이라, 확장 없이는 tailwind-merge 가 이를 텍스트 컬러 그룹으로\n * 오인해 text-12 같은 클래스를 text-(--color-var) 뒤에서 충돌 제거해버린다.\n */\nconst twMerge = extendTailwindMerge({\n extend: {\n classGroups: {\n 'font-size': [{ text: ['10', '11', '12', '13', '14', '16', '18', '20', '24', '28', '32'] }],\n },\n },\n});\n\n/**\n * className 병합 유틸.\n * clsx로 조건부 클래스를 합치고, tailwind-merge로 Tailwind 클래스 충돌을 해소한다.\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","/**\n * 공용 커스텀 스크롤바 클래스.\n * Stencil의 styles/scrollbar.scss mixin과 짝을 이루는 단일 기준이며,\n * 스크롤 영역을 가진 컴포넌트는 DOM을 감싸지 않고 이 클래스를 직접 붙여 쓴다.\n *\n * SVG 스펙: 트랙 12px / 썸 8px(grey-45, 2px 인셋, radius 4).\n * 인셋은 투명 테두리 + padding-box 클립으로 만들어, 트랙을 칠하든 안 칠하든 썸이 8px로 보인다.\n */\n\n// Chrome 121+/WebKit는 scrollbar-width·scrollbar-color의 계산값이 초기값(auto)이 아니면\n// ::-webkit-scrollbar-* 를 통째로 무시한다(CSSWG 결정). 표준 속성을 무조건 켜면 아래 트랙/썸이\n// 전부 죽으므로, webkit 유사요소가 없는 브라우저(Firefox)에서만 적용한다.\nconst FIREFOX_SCROLLBAR_CLASS =\n 'supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-width:thin] ' +\n 'supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-color:var(--color-grey-45)_transparent]';\n\n/** 스크롤 영역 기본 스크롤바. overflow 클래스는 각 컴포넌트가 따로 지정한다. */\nexport const SCROLLBAR_CLASS =\n `${FIREFOX_SCROLLBAR_CLASS} ` +\n '[&::-webkit-scrollbar]:h-[12px] [&::-webkit-scrollbar]:w-[12px] [&::-webkit-scrollbar]:bg-transparent ' +\n '[&::-webkit-scrollbar-track]:bg-transparent ' +\n '[&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:border-2 [&::-webkit-scrollbar-thumb]:border-solid [&::-webkit-scrollbar-thumb]:border-transparent [&::-webkit-scrollbar-thumb]:bg-[var(--color-grey-45)] [&::-webkit-scrollbar-thumb]:bg-clip-padding';\n\n/**\n * background: 트랙 배경을 채운다. 콘텐츠 영역 배경은 바뀌지 않는다.\n * Stencil 의 scrollbar-track-bg($color) mixin 과 짝이며, 이쪽은 기본값 grey-10 에 해당한다.\n * Tailwind 는 소스에 문자열로 박힌 클래스만 수집하므로 색을 인자로 받는 함수로 만들 수 없다.\n * 색이 필요하면 아래 WHITE 처럼 리터럴 상수를 하나 더 만든다.\n *\n * SCROLLBAR_CLASS 뒤에 와야 Firefox 폴백(scrollbar-color)을 덮어쓸 수 있다.\n */\nexport const SCROLLBAR_TRACK_BG_CLASS =\n 'supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-color:var(--color-grey-45)_var(--color-grey-10)] ' +\n '[&::-webkit-scrollbar]:bg-[var(--color-grey-10)] [&::-webkit-scrollbar-track]:bg-[var(--color-grey-10)]';\n\n/**\n * background(white): 트랙을 흰색으로 채운다.\n * 페이지 표면이 grey-10(#F6F6F6)과 같은 색이라 기본 트랙 배경이 묻히는 자리에 쓴다 — SPage 의 neutral 이 그 경우다.\n */\nexport const SCROLLBAR_TRACK_BG_WHITE_CLASS =\n 'supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-color:var(--color-grey-45)_var(--color-white)] ' +\n '[&::-webkit-scrollbar]:bg-[var(--color-white)] [&::-webkit-scrollbar-track]:bg-[var(--color-white)]';\n\n/**\n * bordered: 스크롤바와 콘텐츠 사이 1px 구분선.\n * 단축 속성으로 한 변만 지정한다 — border-solid는 네 변에 걸려 트랙에 기본 3px 테두리를 만든다.\n * 썸과 트랙은 형제 박스라 트랙의 구분선이 썸을 밀어주지 않는다. 구분선이 인셋 2px 중 1px을\n * 차지해 좌우 여백이 1px/2px로 어긋나므로, 썸 인셋을 같은 만큼 키워 2px/2px로 되돌린다(썸 8px → 7px).\n * Firefox는 thin 스크롤바를 이 수준으로 커스터마이즈할 수 없어 적용되지 않는다.\n */\nexport const SCROLLBAR_TRACK_BORDER_CLASS =\n '[&::-webkit-scrollbar-track:vertical]:[border-left:1px_solid_var(--sys-color-border-default)] ' +\n '[&::-webkit-scrollbar-track:horizontal]:[border-top:1px_solid_var(--sys-color-border-default)] ' +\n '[&::-webkit-scrollbar-thumb:vertical]:[border-left-width:3px] ' +\n '[&::-webkit-scrollbar-thumb:horizontal]:[border-top-width:3px]';\n","import type { SColor } from '../../lib/color';\nimport type { SIconName } from '../SIcon';\nimport type { STagColor } from '../STag';\n\n/** 메뉴 스타일: box = 라운드/좁은 들여쓰기, belt = 풀폭 행/넓은 들여쓰기 */\nexport type SGnbType = 'box' | 'belt';\n/** 상단바 구조: fix = GNB 폭에 고정, full = 레이아웃 전폭 */\nexport type SGnbHeader = 'fix' | 'full';\n/** 색상(테마): light / dark */\nexport type SGnbColor = 'light' | 'dark';\n\n/** GNB 메뉴 아이템 (재귀 트리). depth1 → depth2 → depth3. */\nexport interface SGnbMenuItem {\n /** 표시 텍스트 */\n label: string;\n /** 고유 식별값 (선택 상태 비교 기준) */\n value: string;\n /** depth1 전용 아이콘명 */\n icon?: SIconName;\n /** 우측 Tag 뱃지 텍스트 */\n tag?: string;\n /** 뱃지 색. 미지정 시 blue. */\n tagColor?: STagColor;\n /** 비활성 여부 */\n disabled?: boolean;\n /** 하위 아이템 (있으면 펼침/접힘 대상) */\n children?: SGnbMenuItem[];\n}\n\n// theme.css 의 컴포넌트 토큰 참조 (디자인팀 소유, 직접 수정 금지)\n// 토큰은 gnb 한 벌로 통합됐다:\n// 공통(상단바/로고/폴드/바디/아이템) → --cmp-gnb-*\n// 메뉴 변형(type 축) box → --cmp-gnb-box-*, belt → --cmp-gnb-belt-*\n// 접힘 레일 → --cmp-gnb-rail-*\n// 색(color 축) light → --cmp-gnb-light-*, dark → --cmp-gnb-dark-*\nconst gnb = (p: string) => `var(--cmp-gnb-${p})`;\nconst box = (p: string) => `var(--cmp-gnb-box-${p})`;\nconst belt = (p: string) => `var(--cmp-gnb-belt-${p})`;\nconst light = (p: string) => `var(--cmp-gnb-light-${p})`;\nconst dark = (p: string) => `var(--cmp-gnb-dark-${p})`;\n\n/**\n * 상단바(header 축). 상단바 치수 토큰은 gnb 로 통합돼(--cmp-gnb-top-*) fix·full 이 공유한다.\n * 배치는 여전히 이 축이 정한다 — fix 는 GNB 컬럼 안, full 은 레이아웃 전폭(SGnb 에서 분기).\n */\nconst HEADER_TOP = {\n topHeight: gnb('top-height'),\n topPaddingX: gnb('top-paddingX'),\n topGap: gnb('top-gap'),\n logoHeight: gnb('logo-height'),\n};\n\nexport const GNB_HEADER: Record<\n SGnbHeader,\n {\n topHeight: string;\n topPaddingX: string;\n topGap: string;\n logoHeight: string;\n }\n> = {\n fix: HEADER_TOP,\n full: HEADER_TOP,\n};\n\n/** 메뉴(type 축). box → --cmp-gnb-box-*, belt → --cmp-gnb-belt-* (공통 치수는 --cmp-gnb-*) */\nexport const GNB_MENU: Record<\n SGnbType,\n {\n bodyPaddingY: string;\n itemMarginX: string;\n itemPaddingY: string;\n itemPaddingRight: string;\n itemGap: string;\n itemRadius: string;\n itemArrow: string;\n depth1PaddingLeft: string;\n depth2PaddingLeft: string;\n depth3PaddingLeft: string;\n depth1Icon: string;\n // 타이포 (theme.css 에 gnb 타이포 변수 없음 → 토큰 값 직접 명시)\n depth1FontSize: number;\n depth1LineHeight: number;\n depth1FontWeight: number;\n itemFontSize: number;\n itemLineHeight: number;\n }\n> = {\n box: {\n // 바디 패딩·세로 패딩·chevron 은 gnb 로 통합된 공통 토큰\n bodyPaddingY: gnb('body-padding'),\n itemMarginX: gnb('body-padding'), // 전방향 토큰 → 아이템 좌우 여백에도 사용\n itemPaddingY: gnb('item-paddingY'),\n itemPaddingRight: box('item-paddingRight'),\n itemGap: box('item-gap'),\n itemRadius: box('item-radius'),\n itemArrow: gnb('item-chevron-size'),\n depth1PaddingLeft: box('item-depth1-paddingLeft'),\n depth2PaddingLeft: box('item-depth2-paddingLeft'),\n depth3PaddingLeft: box('item-depth3-paddingLeft'),\n depth1Icon: box('item-depth1-icon'),\n depth1FontSize: 14, // token: gnb.box.item.typography.selected.fontSize\n depth1LineHeight: 24, // token: gnb.box.item.typography.selected.lineHeight\n depth1FontWeight: 700,\n itemFontSize: 14, // token: gnb.box.item.typography.default.fontSize\n itemLineHeight: 24, // token: gnb.box.item.typography.default.lineHeight\n },\n belt: {\n bodyPaddingY: gnb('body-padding'),\n itemMarginX: '0px', // belt 토큰에 가로 여백 없음 → 풀폭 행 (들여쓰기는 depth*-paddingLeft 전담)\n itemPaddingY: gnb('item-paddingY'),\n itemPaddingRight: belt('item-paddingRight'),\n itemGap: belt('item-gap'),\n itemRadius: '0px', // belt 토큰에 radius 없음 → 풀폭 행\n itemArrow: gnb('item-chevron-size'),\n depth1PaddingLeft: belt('item-depth1-paddingLeft'),\n depth2PaddingLeft: belt('item-depth2-paddingLeft'),\n depth3PaddingLeft: belt('item-depth3-paddingLeft'),\n depth1Icon: belt('item-depth1-icon'), // belt 전용 depth1 아이콘 토큰(16)\n depth1FontSize: 13, // token: gnb.belt.item.depth1.typography.fontSize\n depth1LineHeight: 22, // token: gnb.belt.item.depth1.typography.lineHeight\n depth1FontWeight: 700,\n itemFontSize: 12, // token: gnb.belt.item.typography.default.fontSize\n itemLineHeight: 20, // token: gnb.belt.item.typography.default.lineHeight\n },\n};\n\n/**\n * 색상(color 축). light → --cmp-gnb-light-* 색, dark → --cmp-gnb-dark-* 색\n * 상단바 아이콘 색(--cmp-gnb-*-top-icon)은 SGhostButton 이 자체 intent 토큰으로 칠한다:\n * light → --cmp-ghostButton-default-content, dark → --cmp-ghostButton-inverse-content (값 동일).\n */\nexport const GNB_COLORS: Record<\n SGnbColor,\n {\n panelBg: string;\n topBorder: string;\n itemBgDefault: string;\n itemBgHover: string;\n itemBgSelected: string;\n itemColorDefault: string;\n itemColorHover: string;\n itemColorActive: string;\n itemChevron: string;\n /** depth1 아이콘 색. 대응 --cmp-gnb-*-item-icon 토큰이 없어 팔레트 키로 직접 지정한다. */\n itemIcon: SColor;\n /** 레일 아이템 기본 색(아이콘+라벨). 선택 시엔 itemColorActive 를 쓴다. */\n railFgDefault: string;\n }\n> = {\n light: {\n panelBg: light('bg'),\n topBorder: light('divider'),\n itemBgDefault: light('item-bg-default'),\n itemBgHover: light('item-bg-hover'),\n itemBgSelected: light('item-bg-selected'),\n itemColorDefault: light('item-fg-default'),\n itemColorHover: light('item-fg-hover'),\n itemColorActive: light('item-fg-selected'),\n itemChevron: light('item-chevron'),\n itemIcon: 'grey_65',\n railFgDefault: light('rail-fg-default'),\n },\n dark: {\n panelBg: dark('bg'),\n topBorder: dark('divider'),\n itemBgDefault: dark('item-bg-default'),\n itemBgHover: dark('item-bg-hover'),\n itemBgSelected: dark('item-bg-selected'),\n itemColorDefault: dark('item-fg-default'),\n itemColorHover: dark('item-fg-hover'),\n itemColorActive: dark('item-fg-selected'),\n itemChevron: dark('item-chevron'),\n itemIcon: 'grey_55',\n railFgDefault: dark('rail-fg-default'),\n },\n};\n\n/** 축과 무관한 공통 레이아웃. */\nexport const GNB_COMMON = {\n /** 접힘 레일 폭. header=\"full\" 은 접으면 메뉴만 사라져 레일이 없으므로 fix 전용. */\n foldWidth: gnb('fold-width'),\n};\n\n/**\n * 좌측 레일(useRail) — items 의 첫 depth 를 아이콘+라벨 버튼으로 세우는 1차 내비게이션.\n * 선택 색은 레일 전용 토큰이 없어 메뉴 아이템의 선택 색(item-bg-selected / item-fg-selected)을 함께 쓴다.\n */\nexport const GNB_RAIL = {\n padding: gnb('rail-padding'),\n gap: gnb('rail-gap'),\n itemWidth: gnb('rail-item-width'),\n itemPaddingTop: gnb('rail-item-paddingTop'),\n itemPaddingBottom: gnb('rail-item-paddingBottom'),\n itemRadius: gnb('rail-item-radius'),\n itemIcon: gnb('rail-item-icon'),\n // 타이포 (theme.css 에 gnb 타이포 변수 없음 → 토큰 값 직접 명시)\n itemFontSize: 11, // token: gnb.rail.item.typography.default.fontSize\n itemLineHeight: 18, // token: gnb.rail.item.typography.default.lineHeight\n itemFontWeight: 500, // token: gnb.rail.item.typography.default.fontWeight\n itemFontWeightSelected: 700, // token: gnb.rail.item.typography.selected.fontWeight\n};\n\n/**\n * 레일 컬럼 폭 = rail-item-width(44) + rail-padding(12)*2.\n * 토큰이 CSS 변수라 JS 에서 더할 수 없어 합을 상수로 둔다 (GNB_MENU_WIDTH 와 같은 이유).\n */\nexport const GNB_RAIL_WIDTH = 68;\n\n/**\n * 접힘/펼침 전환 시간(ms). GNB 폭·메뉴 슬라이드·상단바 페이드와 SLayout 의 메뉴 열 폭이\n * 이 값을 공유해야 한 덩어리로 미끄러진다.\n */\nexport const GNB_FOLD_MS = 200;\n\n/**\n * 하위 메뉴 펼침/접힘 전환 시간(ms). 아이템을 눌러 children 이 열리고 닫힐 때\n * 높이(grid-template-rows 0fr↔1fr)가 이 길이만큼 부드럽게 늘고 줄어든다.\n * 폴드와 같은 값을 써서 두 모션이 한 언어로 읽히게 한다.\n */\nexport const GNB_EXPAND_MS = 200;\n\n/**\n * 메뉴 고정 폭. 두 header 모두 메뉴는 이 폭으로 좌측에 붙는다(토큰 없음 → 상수).\n * header=\"full\" 에서 SLayout 이 \"메뉴 오른쪽 = 페이지\" 영역을 잡을 때도 이 값을 쓴다.\n */\nexport const GNB_MENU_WIDTH = 240;\n\n/**\n * useRail 에서 value 를 품은 첫 depth(레일) 아이템의 value.\n * value 자체가 레일 아이템이면 그 값을, 어디에도 없으면 undefined 를 반환한다.\n */\nexport function findGnbRailValue(items: SGnbMenuItem[], value: string): string | undefined {\n if (items.some(item => item.value === value)) return value;\n return findGnbAncestors(items, value)[0];\n}\n\n/** value 로 선택된 아이템의 조상 value 목록(자기 자신 제외)을 반환 → 자동 펼침용 */\nexport function findGnbAncestors(items: SGnbMenuItem[], value: string): string[] {\n const walk = (list: SGnbMenuItem[], trail: string[]): string[] | null => {\n for (const item of list) {\n if (item.value === value) return trail;\n if (item.children) {\n const found = walk(item.children, [...trail, item.value]);\n if (found) return found;\n }\n }\n return null;\n };\n return walk(items, []) ?? [];\n}\n","'use client';\n\nimport {\n createContext,\n forwardRef,\n useCallback,\n useContext,\n useMemo,\n useState,\n type CSSProperties,\n type HTMLAttributes,\n} from 'react';\nimport { cn } from '../../lib/cn';\nimport {\n GNB_FOLD_MS,\n GNB_HEADER,\n GNB_MENU_WIDTH,\n GNB_RAIL_WIDTH,\n type SGnbHeader,\n type SGnbType,\n} from '../SGnb/gnb.config';\n\n/** 메뉴 스타일: box(라운드) / belt(풀폭 행) — 자식 SGnb 의 메뉴 모양 */\nexport type SLayoutType = SGnbType;\n/** 레이아웃 구조: fix(좌측 GNB + 페이지 가로 분할) / full(풀폭 상단바 + 아래에 메뉴|페이지) */\nexport type SLayoutHeader = SGnbHeader;\n\nexport interface SLayoutContextValue {\n /** 메뉴 스타일 — 자식 SGnb 가 스타일을 맞추는 기준 */\n type: SLayoutType;\n /**\n * 상단바 구조 — 자식 SGnb·SPage 가 배치를 맞추는 기준(유효 header).\n * 레일에 children 없는 아이템이 있어 SGnb 가 full 을 강제하면 여기에도 full 로 반영된다.\n */\n header: SLayoutHeader;\n /** GNB 좌측 레일 사용 여부 — 자식 SGnb 가 따르고, header=\"full\" 의 메뉴 열 폭도 이 값을 반영한다 */\n useRail: boolean;\n /** GNB 접힘 상태 */\n folded: boolean;\n /** 접힘 상태 변경 */\n setFolded: (folded: boolean) => void;\n /**\n * 자식 SGnb 가 런타임 배치 상태를 알린다(SLayout 은 items·activeRail 을 모른다).\n * - forceFull: 레일에 리프(children 없는) 아이템이 있어 header 를 full 로 강제했는지 → 프레임을 grid 로 맞춘다.\n * - menuVisible: 활성 레일에 메뉴 패널이 깔렸는지 → false 면 메뉴 열을 빼 레일 폭만 예약한다(빈틈 방지).\n */\n reportGnbState: (state: { forceFull: boolean; menuVisible: boolean }) => void;\n}\n\nexport const SLayoutContext = createContext<SLayoutContextValue | null>(null);\n\n/**\n * SLayout 의 상태를 읽고 제어한다. `<SLayout>` 밖에서 호출하면 throw.\n * 외부 버튼으로 GNB 를 접거나, 현재 구조(header)·메뉴 스타일(type)에 따라 분기할 때 사용한다.\n */\nexport function useSLayout(): SLayoutContextValue {\n const ctx = useContext(SLayoutContext);\n if (!ctx) throw new Error('useSLayout 은 <SLayout> 안에서만 사용할 수 있습니다.');\n return ctx;\n}\n\nexport interface SLayoutProps extends HTMLAttributes<HTMLDivElement> {\n /** 메뉴 스타일: box(라운드) / belt(풀폭 행). 자식 SGnb 가 이 값을 따른다. */\n type?: SLayoutType;\n /** 레이아웃 구조: fix(가로 분할) / full(풀폭 상단바). 자식 SGnb·SPage 가 이 값을 따른다. */\n header?: SLayoutHeader;\n /**\n * GNB 좌측 레일 사용 여부. 자식 SGnb 가 이 값을 따르고, full 의 메뉴 열 폭도 레일만큼 넓어진다.\n * 레일에 children 없는 아이템이 있으면 SGnb 가 header 를 full 로 강제하고, 이 레이아웃도 그에 맞춰 full 그리드로 선다.\n */\n useRail?: boolean;\n /** GNB 접힘 상태 (controlled) */\n folded?: boolean;\n /** GNB 초기 접힘 상태 (uncontrolled) */\n defaultFolded?: boolean;\n /** 접힘 상태 변경 */\n onFoldedChange?: (folded: boolean) => void;\n}\n\n/**\n * SLayout — 앱 셸 프레임 겸 context provider.\n * header 가 배치를 결정하고 overflow:hidden 으로 스크롤 경계를 만든다.\n * 자식 SGnb·SPage 는 context 에서 type·header·folded 를 읽으므로 앱은 이 값들을 여기 한 곳에만 쓴다.\n * 높이는 기본 100%(부모를 채움) — 앱 루트로 쓸 땐 부모를 뷰포트 높이로 잡는다.\n *\n * - header=\"fix\": [GNB(자기 폭 소유) | 페이지] 가로 flex.\n * - header=\"full\": 상단바가 풀폭이고 그 아래에 [메뉴 | 페이지] 가 나란히 서야 하는데, SGnb 가 상단바와\n * 메뉴를 한 엘리먼트로 갖고 있어 사이에 끼워넣을 수 없다. 그래서 grid 로 깔고\n * SGnb 를 전 셀에 겹친 뒤(root 는 투명) SPage 가 (2행,2열) = \"메뉴 오른쪽 투명 영역\" 을 차지한다.\n * 접으면 메뉴가 사라지므로 1열을 0 으로 접어 페이지가 전폭을 쓴다.\n */\nexport const SLayout = forwardRef<HTMLDivElement, SLayoutProps>(function SLayout(\n {\n type = 'box',\n header = 'fix',\n useRail = false,\n folded,\n defaultFolded = false,\n onFoldedChange,\n className,\n style,\n children,\n ...rest\n },\n ref,\n) {\n const [uncontrolledFolded, setUncontrolledFolded] = useState(defaultFolded);\n const isControlled = folded !== undefined;\n const currentFolded = isControlled ? folded : uncontrolledFolded;\n\n const setFolded = useCallback(\n (next: boolean) => {\n if (!isControlled) setUncontrolledFolded(next);\n onFoldedChange?.(next);\n },\n [isControlled, onFoldedChange],\n );\n\n // 자식 SGnb 가 알려주는 런타임 배치 상태. SLayout 은 items·activeRail 을 모르므로 이 신호로\n // full 강제 여부와 메뉴 열 폭을 맞춘다. prev 동일값이면 같은 참조를 유지해 불필요한 리렌더를 막는다.\n const [gnbState, setGnbState] = useState({ forceFull: false, menuVisible: true });\n const reportGnbState = useCallback(\n (next: { forceFull: boolean; menuVisible: boolean }) =>\n setGnbState(prev =>\n prev.forceFull === next.forceFull && prev.menuVisible === next.menuVisible ? prev : next,\n ),\n [],\n );\n\n // 레일에 리프 아이템이 있으면 SGnb 가 header 를 full 로 강제한다 → 프레임(grid)도 그에 맞춘다.\n const effectiveHeader: SLayoutHeader = header === 'full' || gnbState.forceFull ? 'full' : 'fix';\n\n const ctx = useMemo<SLayoutContextValue>(\n () => ({\n type,\n header: effectiveHeader,\n useRail,\n folded: currentFolded,\n setFolded,\n reportGnbState,\n }),\n [type, effectiveHeader, useRail, currentFolded, setFolded, reportGnbState],\n );\n\n const isFullHeader = effectiveHeader === 'full';\n // 레일이 있으면 GNB 가 [레일 | 메뉴] 두 칸이므로 페이지는 그만큼 오른쪽에서 시작한다.\n // 리프 레일이 활성이라 메뉴가 숨겨지면(menuVisible=false) 메뉴 칸을 빼 레일 폭만 예약한다(빈틈 방지).\n const gnbColumnWidth =\n (useRail ? GNB_RAIL_WIDTH : 0) + (gnbState.menuVisible ? GNB_MENU_WIDTH : 0);\n const frameStyle: CSSProperties | undefined = isFullHeader\n ? {\n gridTemplateColumns: `${currentFolded ? 0 : gnbColumnWidth}px 1fr`,\n gridTemplateRows: `${GNB_HEADER[effectiveHeader].topHeight} 1fr`,\n // 메뉴가 미끄러지는 동안 페이지 열도 같은 길이로 따라와야 한 덩어리로 읽힌다.\n // (fix 는 SGnb 가 제 폭을 직접 전환하므로 프레임이 손댈 게 없다)\n transition: `grid-template-columns ${GNB_FOLD_MS}ms ease-out`,\n }\n : undefined;\n\n return (\n <SLayoutContext.Provider value={ctx}>\n <div\n ref={ref}\n data-type={type}\n data-header={effectiveHeader}\n className={cn(\n 'box-border h-full overflow-hidden',\n isFullHeader ? 'grid' : 'flex flex-row',\n className,\n )}\n style={{ ...frameStyle, ...style }}\n {...rest}\n >\n {children}\n </div>\n </SLayoutContext.Provider>\n );\n});\n","import { forwardRef, useContext, type CSSProperties, type HTMLAttributes } from 'react';\nimport { cn } from '../../lib/cn';\nimport {\n SCROLLBAR_CLASS,\n SCROLLBAR_TRACK_BG_CLASS,\n SCROLLBAR_TRACK_BG_WHITE_CLASS,\n SCROLLBAR_TRACK_BORDER_CLASS,\n} from '../../lib/scrollbar';\nimport { SLayoutContext } from '../SLayout/SLayout';\nimport type { SPageBackground } from './page.config';\n\n/**\n * 배경별 표면색 + 스크롤바 처리. 구분선(bordered)과 트랙 배경은 셋 다 깔되, 트랙 색만 갈린다.\n *\n * 기본 트랙 색은 grey-10(#F6F6F6)인데 neutral 표면(bg.neutralLight)이 정확히 같은 색이라\n * 그대로 쓰면 트랙이 페이지에 묻힌다. 그래서 neutral 만 흰 트랙을 써서 홈을 드러낸다.\n * (트랙 배경을 아예 빼면 흰색이 아니라 transparent 라 페이지색이 비친다 — 묻히는 건 마찬가지다)\n */\nconst BACKGROUND_CLASS: Record<SPageBackground, string> = {\n frame: `bg-[var(--sys-color-bg-frame)] ${SCROLLBAR_TRACK_BORDER_CLASS} ${SCROLLBAR_TRACK_BG_CLASS}`,\n neutral: `bg-[var(--sys-color-bg-neutralLight)] ${SCROLLBAR_TRACK_BORDER_CLASS} ${SCROLLBAR_TRACK_BG_WHITE_CLASS}`,\n screen: `bg-[var(--sys-color-bg-screen)] ${SCROLLBAR_TRACK_BORDER_CLASS} ${SCROLLBAR_TRACK_BG_CLASS}`,\n};\n\n// docs:gen 은 export interface 만 수집하므로(gen-docs.mjs) 타입 별칭 대신 interface 로 둔다.\nexport interface SPageProps extends HTMLAttributes<HTMLElement> {\n /**\n * 페이지 배경 표면. frame=흰 콘텐츠 면, neutral=옅은 회색 면, screen=앱 바탕.\n * 스크롤바 처리도 여기 묶여 있다 — 셋 다 구분선+트랙 배경이고, 트랙 색만 neutral 에서 흰색이 된다.\n */\n background?: SPageBackground;\n}\n\n/**\n * SPage — SLayout 안에서 남은 공간을 채우는 스크롤 콘텐츠 영역.\n * 스크롤은 SLayout(overflow:hidden)이 잡아둔 경계 안에서 이 영역에만 생긴다.\n * 패딩은 className 으로, 배경은 background 로 정한다(className 으로도 덮어쓸 수 있다).\n *\n * - header=\"fix\": flex 자식으로 남은 가로 공간을 채운다.\n * - header=\"full\": SLayout 이 깐 grid 의 (2행,2열) = 상단바 아래·메뉴 오른쪽 자리를 차지한다.\n * (SGnb 가 전 셀에 겹쳐 있지만 그 영역은 투명해서 SPage 가 보인다)\n */\nexport const SPage = forwardRef<HTMLElement, SPageProps>(function SPage(\n { background = 'frame', className, style, children, ...rest },\n ref,\n) {\n const layout = useContext(SLayoutContext);\n const gridStyle: CSSProperties | undefined =\n layout?.header === 'full' ? { gridColumn: 2, gridRow: 2 } : undefined;\n\n return (\n <main\n ref={ref}\n className={cn(\n 'box-border min-h-0 min-w-0 flex-1 overflow-auto p-20',\n SCROLLBAR_CLASS,\n BACKGROUND_CLASS[background],\n className,\n )}\n style={{ ...gridStyle, ...style }}\n {...rest}\n >\n {children}\n </main>\n );\n});\n"]}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { type HTMLAttributes } from 'react';
|
|
2
|
+
import type { SPageBackground } from './page.config';
|
|
2
3
|
export interface SPageProps extends HTMLAttributes<HTMLElement> {
|
|
4
|
+
/**
|
|
5
|
+
* 페이지 배경 표면. frame=흰 콘텐츠 면, neutral=옅은 회색 면, screen=앱 바탕.
|
|
6
|
+
* 스크롤바 처리도 여기 묶여 있다 — 셋 다 구분선+트랙 배경이고, 트랙 색만 neutral 에서 흰색이 된다.
|
|
7
|
+
*/
|
|
8
|
+
background?: SPageBackground;
|
|
3
9
|
}
|
|
4
10
|
/**
|
|
5
11
|
* SPage — SLayout 안에서 남은 공간을 채우는 스크롤 콘텐츠 영역.
|
|
6
12
|
* 스크롤은 SLayout(overflow:hidden)이 잡아둔 경계 안에서 이 영역에만 생긴다.
|
|
7
|
-
*
|
|
13
|
+
* 패딩은 className 으로, 배경은 background 로 정한다(className 으로도 덮어쓸 수 있다).
|
|
8
14
|
*
|
|
9
15
|
* - header="fix": flex 자식으로 남은 가로 공간을 채운다.
|
|
10
16
|
* - header="full": SLayout 이 깐 grid 의 (2행,2열) = 상단바 아래·메뉴 오른쪽 자리를 차지한다.
|
|
@@ -15,6 +15,13 @@ function cn(...inputs) {
|
|
|
15
15
|
return twMerge(clsx(inputs));
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// src/lib/scrollbar.ts
|
|
19
|
+
var FIREFOX_SCROLLBAR_CLASS = "supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-width:thin] supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-color:var(--color-grey-45)_transparent]";
|
|
20
|
+
var SCROLLBAR_CLASS = `${FIREFOX_SCROLLBAR_CLASS} [&::-webkit-scrollbar]:h-[12px] [&::-webkit-scrollbar]:w-[12px] [&::-webkit-scrollbar]:bg-transparent [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:border-2 [&::-webkit-scrollbar-thumb]:border-solid [&::-webkit-scrollbar-thumb]:border-transparent [&::-webkit-scrollbar-thumb]:bg-[var(--color-grey-45)] [&::-webkit-scrollbar-thumb]:bg-clip-padding`;
|
|
21
|
+
var SCROLLBAR_TRACK_BG_CLASS = "supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-color:var(--color-grey-45)_var(--color-grey-10)] [&::-webkit-scrollbar]:bg-[var(--color-grey-10)] [&::-webkit-scrollbar-track]:bg-[var(--color-grey-10)]";
|
|
22
|
+
var SCROLLBAR_TRACK_BG_WHITE_CLASS = "supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-color:var(--color-grey-45)_var(--color-white)] [&::-webkit-scrollbar]:bg-[var(--color-white)] [&::-webkit-scrollbar-track]:bg-[var(--color-white)]";
|
|
23
|
+
var SCROLLBAR_TRACK_BORDER_CLASS = "[&::-webkit-scrollbar-track:vertical]:[border-left:1px_solid_var(--sys-color-border-default)] [&::-webkit-scrollbar-track:horizontal]:[border-top:1px_solid_var(--sys-color-border-default)] [&::-webkit-scrollbar-thumb:vertical]:[border-left-width:3px] [&::-webkit-scrollbar-thumb:horizontal]:[border-top-width:3px]";
|
|
24
|
+
|
|
18
25
|
// src/components/SGnb/gnb.config.ts
|
|
19
26
|
var gnb = (p) => `var(--cmp-gnb-${p})`;
|
|
20
27
|
var HEADER_TOP = {
|
|
@@ -98,14 +105,24 @@ forwardRef(function SLayout2({
|
|
|
98
105
|
}
|
|
99
106
|
) });
|
|
100
107
|
});
|
|
101
|
-
var
|
|
108
|
+
var BACKGROUND_CLASS = {
|
|
109
|
+
frame: `bg-[var(--sys-color-bg-frame)] ${SCROLLBAR_TRACK_BORDER_CLASS} ${SCROLLBAR_TRACK_BG_CLASS}`,
|
|
110
|
+
neutral: `bg-[var(--sys-color-bg-neutralLight)] ${SCROLLBAR_TRACK_BORDER_CLASS} ${SCROLLBAR_TRACK_BG_WHITE_CLASS}`,
|
|
111
|
+
screen: `bg-[var(--sys-color-bg-screen)] ${SCROLLBAR_TRACK_BORDER_CLASS} ${SCROLLBAR_TRACK_BG_CLASS}`
|
|
112
|
+
};
|
|
113
|
+
var SPage = forwardRef(function SPage2({ background = "frame", className, style, children, ...rest }, ref) {
|
|
102
114
|
const layout = useContext(SLayoutContext);
|
|
103
115
|
const gridStyle = layout?.header === "full" ? { gridColumn: 2, gridRow: 2 } : void 0;
|
|
104
116
|
return /* @__PURE__ */ jsx(
|
|
105
117
|
"main",
|
|
106
118
|
{
|
|
107
119
|
ref,
|
|
108
|
-
className: cn(
|
|
120
|
+
className: cn(
|
|
121
|
+
"box-border min-h-0 min-w-0 flex-1 overflow-auto p-20",
|
|
122
|
+
SCROLLBAR_CLASS,
|
|
123
|
+
BACKGROUND_CLASS[background],
|
|
124
|
+
className
|
|
125
|
+
),
|
|
109
126
|
style: { ...gridStyle, ...style },
|
|
110
127
|
...rest,
|
|
111
128
|
children
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/cn.ts","../../../src/components/SGnb/gnb.config.ts","../../../src/components/SLayout/SLayout.tsx","../../../src/components/SPage/SPage.tsx"],"names":["SLayout","forwardRef","SPage","useContext","jsx"],"mappings":"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","file":"SPage.js","sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { extendTailwindMerge } from 'tailwind-merge';\n\n/**\n * theme.css 의 --text-* 는 기본 Tailwind 폰트사이즈 스케일(xs/sm/base...)이 아닌\n * 숫자 스케일(10~32)이라, 확장 없이는 tailwind-merge 가 이를 텍스트 컬러 그룹으로\n * 오인해 text-12 같은 클래스를 text-(--color-var) 뒤에서 충돌 제거해버린다.\n */\nconst twMerge = extendTailwindMerge({\n extend: {\n classGroups: {\n 'font-size': [{ text: ['10', '11', '12', '13', '14', '16', '18', '20', '24', '28', '32'] }],\n },\n },\n});\n\n/**\n * className 병합 유틸.\n * clsx로 조건부 클래스를 합치고, tailwind-merge로 Tailwind 클래스 충돌을 해소한다.\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","import type { SColor } from '../../lib/color';\nimport type { SIconName } from '../SIcon';\nimport type { STagColor } from '../STag';\n\n/** 메뉴 스타일: box = 라운드/좁은 들여쓰기, belt = 풀폭 행/넓은 들여쓰기 */\nexport type SGnbType = 'box' | 'belt';\n/** 상단바 구조: fix = GNB 폭에 고정, full = 레이아웃 전폭 */\nexport type SGnbHeader = 'fix' | 'full';\n/** 색상(테마): light / dark */\nexport type SGnbColor = 'light' | 'dark';\n\n/** GNB 메뉴 아이템 (재귀 트리). depth1 → depth2 → depth3. */\nexport interface SGnbMenuItem {\n /** 표시 텍스트 */\n label: string;\n /** 고유 식별값 (선택 상태 비교 기준) */\n value: string;\n /** depth1 전용 아이콘명 */\n icon?: SIconName;\n /** 우측 Tag 뱃지 텍스트 */\n tag?: string;\n /** 뱃지 색. 미지정 시 blue. */\n tagColor?: STagColor;\n /** 비활성 여부 */\n disabled?: boolean;\n /** 하위 아이템 (있으면 펼침/접힘 대상) */\n children?: SGnbMenuItem[];\n}\n\n// theme.css 의 컴포넌트 토큰 참조 (디자인팀 소유, 직접 수정 금지)\n// 토큰은 gnb 한 벌로 통합됐다:\n// 공통(상단바/로고/폴드/바디/아이템) → --cmp-gnb-*\n// 메뉴 변형(type 축) box → --cmp-gnb-box-*, belt → --cmp-gnb-belt-*\n// 접힘 레일 → --cmp-gnb-rail-*\n// 색(color 축) light → --cmp-gnb-light-*, dark → --cmp-gnb-dark-*\nconst gnb = (p: string) => `var(--cmp-gnb-${p})`;\nconst box = (p: string) => `var(--cmp-gnb-box-${p})`;\nconst belt = (p: string) => `var(--cmp-gnb-belt-${p})`;\nconst light = (p: string) => `var(--cmp-gnb-light-${p})`;\nconst dark = (p: string) => `var(--cmp-gnb-dark-${p})`;\n\n/**\n * 상단바(header 축). 상단바 치수 토큰은 gnb 로 통합돼(--cmp-gnb-top-*) fix·full 이 공유한다.\n * 배치는 여전히 이 축이 정한다 — fix 는 GNB 컬럼 안, full 은 레이아웃 전폭(SGnb 에서 분기).\n */\nconst HEADER_TOP = {\n topHeight: gnb('top-height'),\n topPaddingX: gnb('top-paddingX'),\n topGap: gnb('top-gap'),\n logoHeight: gnb('logo-height'),\n};\n\nexport const GNB_HEADER: Record<\n SGnbHeader,\n {\n topHeight: string;\n topPaddingX: string;\n topGap: string;\n logoHeight: string;\n }\n> = {\n fix: HEADER_TOP,\n full: HEADER_TOP,\n};\n\n/** 메뉴(type 축). box → --cmp-gnb-box-*, belt → --cmp-gnb-belt-* (공통 치수는 --cmp-gnb-*) */\nexport const GNB_MENU: Record<\n SGnbType,\n {\n bodyPaddingY: string;\n itemMarginX: string;\n itemPaddingY: string;\n itemPaddingRight: string;\n itemGap: string;\n itemRadius: string;\n itemArrow: string;\n depth1PaddingLeft: string;\n depth2PaddingLeft: string;\n depth3PaddingLeft: string;\n depth1Icon: string;\n // 타이포 (theme.css 에 gnb 타이포 변수 없음 → 토큰 값 직접 명시)\n depth1FontSize: number;\n depth1LineHeight: number;\n depth1FontWeight: number;\n itemFontSize: number;\n itemLineHeight: number;\n }\n> = {\n box: {\n // 바디 패딩·세로 패딩·chevron 은 gnb 로 통합된 공통 토큰\n bodyPaddingY: gnb('body-padding'),\n itemMarginX: gnb('body-padding'), // 전방향 토큰 → 아이템 좌우 여백에도 사용\n itemPaddingY: gnb('item-paddingY'),\n itemPaddingRight: box('item-paddingRight'),\n itemGap: box('item-gap'),\n itemRadius: box('item-radius'),\n itemArrow: gnb('item-chevron-size'),\n depth1PaddingLeft: box('item-depth1-paddingLeft'),\n depth2PaddingLeft: box('item-depth2-paddingLeft'),\n depth3PaddingLeft: box('item-depth3-paddingLeft'),\n depth1Icon: box('item-depth1-icon'),\n depth1FontSize: 14, // token: gnb.box.item.typography.selected.fontSize\n depth1LineHeight: 24, // token: gnb.box.item.typography.selected.lineHeight\n depth1FontWeight: 700,\n itemFontSize: 14, // token: gnb.box.item.typography.default.fontSize\n itemLineHeight: 24, // token: gnb.box.item.typography.default.lineHeight\n },\n belt: {\n bodyPaddingY: gnb('body-padding'),\n itemMarginX: '0px', // belt 토큰에 가로 여백 없음 → 풀폭 행 (들여쓰기는 depth*-paddingLeft 전담)\n itemPaddingY: gnb('item-paddingY'),\n itemPaddingRight: belt('item-paddingRight'),\n itemGap: belt('item-gap'),\n itemRadius: '0px', // belt 토큰에 radius 없음 → 풀폭 행\n itemArrow: gnb('item-chevron-size'),\n depth1PaddingLeft: belt('item-depth1-paddingLeft'),\n depth2PaddingLeft: belt('item-depth2-paddingLeft'),\n depth3PaddingLeft: belt('item-depth3-paddingLeft'),\n depth1Icon: belt('item-depth1-icon'), // belt 전용 depth1 아이콘 토큰(16)\n depth1FontSize: 13, // token: gnb.belt.item.depth1.typography.fontSize\n depth1LineHeight: 22, // token: gnb.belt.item.depth1.typography.lineHeight\n depth1FontWeight: 700,\n itemFontSize: 12, // token: gnb.belt.item.typography.default.fontSize\n itemLineHeight: 20, // token: gnb.belt.item.typography.default.lineHeight\n },\n};\n\n/**\n * 색상(color 축). light → --cmp-gnb-light-* 색, dark → --cmp-gnb-dark-* 색\n * 상단바 아이콘 색(--cmp-gnb-*-top-icon)은 SGhostButton 이 자체 intent 토큰으로 칠한다:\n * light → --cmp-ghostButton-default-content, dark → --cmp-ghostButton-inverse-content (값 동일).\n */\nexport const GNB_COLORS: Record<\n SGnbColor,\n {\n panelBg: string;\n topBorder: string;\n itemBgDefault: string;\n itemBgHover: string;\n itemBgSelected: string;\n itemColorDefault: string;\n itemColorHover: string;\n itemColorActive: string;\n itemChevron: string;\n /** depth1 아이콘 색. 대응 --cmp-gnb-*-item-icon 토큰이 없어 팔레트 키로 직접 지정한다. */\n itemIcon: SColor;\n /** 레일 아이템 기본 색(아이콘+라벨). 선택 시엔 itemColorActive 를 쓴다. */\n railFgDefault: string;\n }\n> = {\n light: {\n panelBg: light('bg'),\n topBorder: light('divider'),\n itemBgDefault: light('item-bg-default'),\n itemBgHover: light('item-bg-hover'),\n itemBgSelected: light('item-bg-selected'),\n itemColorDefault: light('item-fg-default'),\n itemColorHover: light('item-fg-hover'),\n itemColorActive: light('item-fg-selected'),\n itemChevron: light('item-chevron'),\n itemIcon: 'grey_65',\n railFgDefault: light('rail-fg-default'),\n },\n dark: {\n panelBg: dark('bg'),\n topBorder: dark('divider'),\n itemBgDefault: dark('item-bg-default'),\n itemBgHover: dark('item-bg-hover'),\n itemBgSelected: dark('item-bg-selected'),\n itemColorDefault: dark('item-fg-default'),\n itemColorHover: dark('item-fg-hover'),\n itemColorActive: dark('item-fg-selected'),\n itemChevron: dark('item-chevron'),\n itemIcon: 'grey_55',\n railFgDefault: dark('rail-fg-default'),\n },\n};\n\n/** 축과 무관한 공통 레이아웃. */\nexport const GNB_COMMON = {\n /** 접힘 레일 폭. header=\"full\" 은 접으면 메뉴만 사라져 레일이 없으므로 fix 전용. */\n foldWidth: gnb('fold-width'),\n};\n\n/**\n * 좌측 레일(useRail) — items 의 첫 depth 를 아이콘+라벨 버튼으로 세우는 1차 내비게이션.\n * 선택 색은 레일 전용 토큰이 없어 메뉴 아이템의 선택 색(item-bg-selected / item-fg-selected)을 함께 쓴다.\n */\nexport const GNB_RAIL = {\n padding: gnb('rail-padding'),\n gap: gnb('rail-gap'),\n itemWidth: gnb('rail-item-width'),\n itemPaddingTop: gnb('rail-item-paddingTop'),\n itemPaddingBottom: gnb('rail-item-paddingBottom'),\n itemRadius: gnb('rail-item-radius'),\n itemIcon: gnb('rail-item-icon'),\n // 타이포 (theme.css 에 gnb 타이포 변수 없음 → 토큰 값 직접 명시)\n itemFontSize: 11, // token: gnb.rail.item.typography.default.fontSize\n itemLineHeight: 18, // token: gnb.rail.item.typography.default.lineHeight\n itemFontWeight: 500, // token: gnb.rail.item.typography.default.fontWeight\n itemFontWeightSelected: 700, // token: gnb.rail.item.typography.selected.fontWeight\n};\n\n/**\n * 레일 컬럼 폭 = rail-item-width(44) + rail-padding(12)*2.\n * 토큰이 CSS 변수라 JS 에서 더할 수 없어 합을 상수로 둔다 (GNB_MENU_WIDTH 와 같은 이유).\n */\nexport const GNB_RAIL_WIDTH = 68;\n\n/**\n * 접힘/펼침 전환 시간(ms). GNB 폭·메뉴 슬라이드·상단바 페이드와 SLayout 의 메뉴 열 폭이\n * 이 값을 공유해야 한 덩어리로 미끄러진다.\n */\nexport const GNB_FOLD_MS = 200;\n\n/**\n * 하위 메뉴 펼침/접힘 전환 시간(ms). 아이템을 눌러 children 이 열리고 닫힐 때\n * 높이(grid-template-rows 0fr↔1fr)가 이 길이만큼 부드럽게 늘고 줄어든다.\n * 폴드와 같은 값을 써서 두 모션이 한 언어로 읽히게 한다.\n */\nexport const GNB_EXPAND_MS = 200;\n\n/**\n * 메뉴 고정 폭. 두 header 모두 메뉴는 이 폭으로 좌측에 붙는다(토큰 없음 → 상수).\n * header=\"full\" 에서 SLayout 이 \"메뉴 오른쪽 = 페이지\" 영역을 잡을 때도 이 값을 쓴다.\n */\nexport const GNB_MENU_WIDTH = 240;\n\n/**\n * useRail 에서 value 를 품은 첫 depth(레일) 아이템의 value.\n * value 자체가 레일 아이템이면 그 값을, 어디에도 없으면 undefined 를 반환한다.\n */\nexport function findGnbRailValue(items: SGnbMenuItem[], value: string): string | undefined {\n if (items.some(item => item.value === value)) return value;\n return findGnbAncestors(items, value)[0];\n}\n\n/** value 로 선택된 아이템의 조상 value 목록(자기 자신 제외)을 반환 → 자동 펼침용 */\nexport function findGnbAncestors(items: SGnbMenuItem[], value: string): string[] {\n const walk = (list: SGnbMenuItem[], trail: string[]): string[] | null => {\n for (const item of list) {\n if (item.value === value) return trail;\n if (item.children) {\n const found = walk(item.children, [...trail, item.value]);\n if (found) return found;\n }\n }\n return null;\n };\n return walk(items, []) ?? [];\n}\n","'use client';\n\nimport {\n createContext,\n forwardRef,\n useCallback,\n useContext,\n useMemo,\n useState,\n type CSSProperties,\n type HTMLAttributes,\n} from 'react';\nimport { cn } from '../../lib/cn';\nimport {\n GNB_FOLD_MS,\n GNB_HEADER,\n GNB_MENU_WIDTH,\n GNB_RAIL_WIDTH,\n type SGnbHeader,\n type SGnbType,\n} from '../SGnb/gnb.config';\n\n/** 메뉴 스타일: box(라운드) / belt(풀폭 행) — 자식 SGnb 의 메뉴 모양 */\nexport type SLayoutType = SGnbType;\n/** 레이아웃 구조: fix(좌측 GNB + 페이지 가로 분할) / full(풀폭 상단바 + 아래에 메뉴|페이지) */\nexport type SLayoutHeader = SGnbHeader;\n\nexport interface SLayoutContextValue {\n /** 메뉴 스타일 — 자식 SGnb 가 스타일을 맞추는 기준 */\n type: SLayoutType;\n /**\n * 상단바 구조 — 자식 SGnb·SPage 가 배치를 맞추는 기준(유효 header).\n * 레일에 children 없는 아이템이 있어 SGnb 가 full 을 강제하면 여기에도 full 로 반영된다.\n */\n header: SLayoutHeader;\n /** GNB 좌측 레일 사용 여부 — 자식 SGnb 가 따르고, header=\"full\" 의 메뉴 열 폭도 이 값을 반영한다 */\n useRail: boolean;\n /** GNB 접힘 상태 */\n folded: boolean;\n /** 접힘 상태 변경 */\n setFolded: (folded: boolean) => void;\n /**\n * 자식 SGnb 가 런타임 배치 상태를 알린다(SLayout 은 items·activeRail 을 모른다).\n * - forceFull: 레일에 리프(children 없는) 아이템이 있어 header 를 full 로 강제했는지 → 프레임을 grid 로 맞춘다.\n * - menuVisible: 활성 레일에 메뉴 패널이 깔렸는지 → false 면 메뉴 열을 빼 레일 폭만 예약한다(빈틈 방지).\n */\n reportGnbState: (state: { forceFull: boolean; menuVisible: boolean }) => void;\n}\n\nexport const SLayoutContext = createContext<SLayoutContextValue | null>(null);\n\n/**\n * SLayout 의 상태를 읽고 제어한다. `<SLayout>` 밖에서 호출하면 throw.\n * 외부 버튼으로 GNB 를 접거나, 현재 구조(header)·메뉴 스타일(type)에 따라 분기할 때 사용한다.\n */\nexport function useSLayout(): SLayoutContextValue {\n const ctx = useContext(SLayoutContext);\n if (!ctx) throw new Error('useSLayout 은 <SLayout> 안에서만 사용할 수 있습니다.');\n return ctx;\n}\n\nexport interface SLayoutProps extends HTMLAttributes<HTMLDivElement> {\n /** 메뉴 스타일: box(라운드) / belt(풀폭 행). 자식 SGnb 가 이 값을 따른다. */\n type?: SLayoutType;\n /** 레이아웃 구조: fix(가로 분할) / full(풀폭 상단바). 자식 SGnb·SPage 가 이 값을 따른다. */\n header?: SLayoutHeader;\n /**\n * GNB 좌측 레일 사용 여부. 자식 SGnb 가 이 값을 따르고, full 의 메뉴 열 폭도 레일만큼 넓어진다.\n * 레일에 children 없는 아이템이 있으면 SGnb 가 header 를 full 로 강제하고, 이 레이아웃도 그에 맞춰 full 그리드로 선다.\n */\n useRail?: boolean;\n /** GNB 접힘 상태 (controlled) */\n folded?: boolean;\n /** GNB 초기 접힘 상태 (uncontrolled) */\n defaultFolded?: boolean;\n /** 접힘 상태 변경 */\n onFoldedChange?: (folded: boolean) => void;\n}\n\n/**\n * SLayout — 앱 셸 프레임 겸 context provider.\n * header 가 배치를 결정하고 overflow:hidden 으로 스크롤 경계를 만든다.\n * 자식 SGnb·SPage 는 context 에서 type·header·folded 를 읽으므로 앱은 이 값들을 여기 한 곳에만 쓴다.\n * 높이는 기본 100%(부모를 채움) — 앱 루트로 쓸 땐 부모를 뷰포트 높이로 잡는다.\n *\n * - header=\"fix\": [GNB(자기 폭 소유) | 페이지] 가로 flex.\n * - header=\"full\": 상단바가 풀폭이고 그 아래에 [메뉴 | 페이지] 가 나란히 서야 하는데, SGnb 가 상단바와\n * 메뉴를 한 엘리먼트로 갖고 있어 사이에 끼워넣을 수 없다. 그래서 grid 로 깔고\n * SGnb 를 전 셀에 겹친 뒤(root 는 투명) SPage 가 (2행,2열) = \"메뉴 오른쪽 투명 영역\" 을 차지한다.\n * 접으면 메뉴가 사라지므로 1열을 0 으로 접어 페이지가 전폭을 쓴다.\n */\nexport const SLayout = forwardRef<HTMLDivElement, SLayoutProps>(function SLayout(\n {\n type = 'box',\n header = 'fix',\n useRail = false,\n folded,\n defaultFolded = false,\n onFoldedChange,\n className,\n style,\n children,\n ...rest\n },\n ref,\n) {\n const [uncontrolledFolded, setUncontrolledFolded] = useState(defaultFolded);\n const isControlled = folded !== undefined;\n const currentFolded = isControlled ? folded : uncontrolledFolded;\n\n const setFolded = useCallback(\n (next: boolean) => {\n if (!isControlled) setUncontrolledFolded(next);\n onFoldedChange?.(next);\n },\n [isControlled, onFoldedChange],\n );\n\n // 자식 SGnb 가 알려주는 런타임 배치 상태. SLayout 은 items·activeRail 을 모르므로 이 신호로\n // full 강제 여부와 메뉴 열 폭을 맞춘다. prev 동일값이면 같은 참조를 유지해 불필요한 리렌더를 막는다.\n const [gnbState, setGnbState] = useState({ forceFull: false, menuVisible: true });\n const reportGnbState = useCallback(\n (next: { forceFull: boolean; menuVisible: boolean }) =>\n setGnbState(prev =>\n prev.forceFull === next.forceFull && prev.menuVisible === next.menuVisible ? prev : next,\n ),\n [],\n );\n\n // 레일에 리프 아이템이 있으면 SGnb 가 header 를 full 로 강제한다 → 프레임(grid)도 그에 맞춘다.\n const effectiveHeader: SLayoutHeader = header === 'full' || gnbState.forceFull ? 'full' : 'fix';\n\n const ctx = useMemo<SLayoutContextValue>(\n () => ({\n type,\n header: effectiveHeader,\n useRail,\n folded: currentFolded,\n setFolded,\n reportGnbState,\n }),\n [type, effectiveHeader, useRail, currentFolded, setFolded, reportGnbState],\n );\n\n const isFullHeader = effectiveHeader === 'full';\n // 레일이 있으면 GNB 가 [레일 | 메뉴] 두 칸이므로 페이지는 그만큼 오른쪽에서 시작한다.\n // 리프 레일이 활성이라 메뉴가 숨겨지면(menuVisible=false) 메뉴 칸을 빼 레일 폭만 예약한다(빈틈 방지).\n const gnbColumnWidth =\n (useRail ? GNB_RAIL_WIDTH : 0) + (gnbState.menuVisible ? GNB_MENU_WIDTH : 0);\n const frameStyle: CSSProperties | undefined = isFullHeader\n ? {\n gridTemplateColumns: `${currentFolded ? 0 : gnbColumnWidth}px 1fr`,\n gridTemplateRows: `${GNB_HEADER[effectiveHeader].topHeight} 1fr`,\n // 메뉴가 미끄러지는 동안 페이지 열도 같은 길이로 따라와야 한 덩어리로 읽힌다.\n // (fix 는 SGnb 가 제 폭을 직접 전환하므로 프레임이 손댈 게 없다)\n transition: `grid-template-columns ${GNB_FOLD_MS}ms ease-out`,\n }\n : undefined;\n\n return (\n <SLayoutContext.Provider value={ctx}>\n <div\n ref={ref}\n data-type={type}\n data-header={effectiveHeader}\n className={cn(\n 'box-border h-full overflow-hidden',\n isFullHeader ? 'grid' : 'flex flex-row',\n className,\n )}\n style={{ ...frameStyle, ...style }}\n {...rest}\n >\n {children}\n </div>\n </SLayoutContext.Provider>\n );\n});\n","import { forwardRef, useContext, type CSSProperties, type HTMLAttributes } from 'react';\nimport { cn } from '../../lib/cn';\nimport { SLayoutContext } from '../SLayout/SLayout';\n\n// 자체 prop 없이 표준 HTML 속성만 받는다.\n// docs:gen 은 export interface 만 수집하므로(gen-docs.mjs) 타입 별칭 대신 interface 로 둔다.\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface SPageProps extends HTMLAttributes<HTMLElement> {}\n\n/**\n * SPage — SLayout 안에서 남은 공간을 채우는 스크롤 콘텐츠 영역.\n * 스크롤은 SLayout(overflow:hidden)이 잡아둔 경계 안에서 이 영역에만 생긴다.\n * 패딩·배경은 className 으로 덮어쓸 수 있다.\n *\n * - header=\"fix\": flex 자식으로 남은 가로 공간을 채운다.\n * - header=\"full\": SLayout 이 깐 grid 의 (2행,2열) = 상단바 아래·메뉴 오른쪽 자리를 차지한다.\n * (SGnb 가 전 셀에 겹쳐 있지만 그 영역은 투명해서 SPage 가 보인다)\n */\nexport const SPage = forwardRef<HTMLElement, SPageProps>(function SPage(\n { className, style, children, ...rest },\n ref,\n) {\n const layout = useContext(SLayoutContext);\n const gridStyle: CSSProperties | undefined =\n layout?.header === 'full' ? { gridColumn: 2, gridRow: 2 } : undefined;\n\n return (\n <main\n ref={ref}\n className={cn('box-border min-h-0 min-w-0 flex-1 overflow-auto p-20', className)}\n style={{ ...gridStyle, ...style }}\n {...rest}\n >\n {children}\n </main>\n );\n});\n"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/cn.ts","../../../src/lib/scrollbar.ts","../../../src/components/SGnb/gnb.config.ts","../../../src/components/SLayout/SLayout.tsx","../../../src/components/SPage/SPage.tsx"],"names":["SLayout","forwardRef","SPage","useContext","jsx"],"mappings":"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","file":"SPage.js","sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { extendTailwindMerge } from 'tailwind-merge';\n\n/**\n * theme.css 의 --text-* 는 기본 Tailwind 폰트사이즈 스케일(xs/sm/base...)이 아닌\n * 숫자 스케일(10~32)이라, 확장 없이는 tailwind-merge 가 이를 텍스트 컬러 그룹으로\n * 오인해 text-12 같은 클래스를 text-(--color-var) 뒤에서 충돌 제거해버린다.\n */\nconst twMerge = extendTailwindMerge({\n extend: {\n classGroups: {\n 'font-size': [{ text: ['10', '11', '12', '13', '14', '16', '18', '20', '24', '28', '32'] }],\n },\n },\n});\n\n/**\n * className 병합 유틸.\n * clsx로 조건부 클래스를 합치고, tailwind-merge로 Tailwind 클래스 충돌을 해소한다.\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","/**\n * 공용 커스텀 스크롤바 클래스.\n * Stencil의 styles/scrollbar.scss mixin과 짝을 이루는 단일 기준이며,\n * 스크롤 영역을 가진 컴포넌트는 DOM을 감싸지 않고 이 클래스를 직접 붙여 쓴다.\n *\n * SVG 스펙: 트랙 12px / 썸 8px(grey-45, 2px 인셋, radius 4).\n * 인셋은 투명 테두리 + padding-box 클립으로 만들어, 트랙을 칠하든 안 칠하든 썸이 8px로 보인다.\n */\n\n// Chrome 121+/WebKit는 scrollbar-width·scrollbar-color의 계산값이 초기값(auto)이 아니면\n// ::-webkit-scrollbar-* 를 통째로 무시한다(CSSWG 결정). 표준 속성을 무조건 켜면 아래 트랙/썸이\n// 전부 죽으므로, webkit 유사요소가 없는 브라우저(Firefox)에서만 적용한다.\nconst FIREFOX_SCROLLBAR_CLASS =\n 'supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-width:thin] ' +\n 'supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-color:var(--color-grey-45)_transparent]';\n\n/** 스크롤 영역 기본 스크롤바. overflow 클래스는 각 컴포넌트가 따로 지정한다. */\nexport const SCROLLBAR_CLASS =\n `${FIREFOX_SCROLLBAR_CLASS} ` +\n '[&::-webkit-scrollbar]:h-[12px] [&::-webkit-scrollbar]:w-[12px] [&::-webkit-scrollbar]:bg-transparent ' +\n '[&::-webkit-scrollbar-track]:bg-transparent ' +\n '[&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:border-2 [&::-webkit-scrollbar-thumb]:border-solid [&::-webkit-scrollbar-thumb]:border-transparent [&::-webkit-scrollbar-thumb]:bg-[var(--color-grey-45)] [&::-webkit-scrollbar-thumb]:bg-clip-padding';\n\n/**\n * background: 트랙 배경을 채운다. 콘텐츠 영역 배경은 바뀌지 않는다.\n * Stencil 의 scrollbar-track-bg($color) mixin 과 짝이며, 이쪽은 기본값 grey-10 에 해당한다.\n * Tailwind 는 소스에 문자열로 박힌 클래스만 수집하므로 색을 인자로 받는 함수로 만들 수 없다.\n * 색이 필요하면 아래 WHITE 처럼 리터럴 상수를 하나 더 만든다.\n *\n * SCROLLBAR_CLASS 뒤에 와야 Firefox 폴백(scrollbar-color)을 덮어쓸 수 있다.\n */\nexport const SCROLLBAR_TRACK_BG_CLASS =\n 'supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-color:var(--color-grey-45)_var(--color-grey-10)] ' +\n '[&::-webkit-scrollbar]:bg-[var(--color-grey-10)] [&::-webkit-scrollbar-track]:bg-[var(--color-grey-10)]';\n\n/**\n * background(white): 트랙을 흰색으로 채운다.\n * 페이지 표면이 grey-10(#F6F6F6)과 같은 색이라 기본 트랙 배경이 묻히는 자리에 쓴다 — SPage 의 neutral 이 그 경우다.\n */\nexport const SCROLLBAR_TRACK_BG_WHITE_CLASS =\n 'supports-[not_selector(::-webkit-scrollbar)]:[scrollbar-color:var(--color-grey-45)_var(--color-white)] ' +\n '[&::-webkit-scrollbar]:bg-[var(--color-white)] [&::-webkit-scrollbar-track]:bg-[var(--color-white)]';\n\n/**\n * bordered: 스크롤바와 콘텐츠 사이 1px 구분선.\n * 단축 속성으로 한 변만 지정한다 — border-solid는 네 변에 걸려 트랙에 기본 3px 테두리를 만든다.\n * 썸과 트랙은 형제 박스라 트랙의 구분선이 썸을 밀어주지 않는다. 구분선이 인셋 2px 중 1px을\n * 차지해 좌우 여백이 1px/2px로 어긋나므로, 썸 인셋을 같은 만큼 키워 2px/2px로 되돌린다(썸 8px → 7px).\n * Firefox는 thin 스크롤바를 이 수준으로 커스터마이즈할 수 없어 적용되지 않는다.\n */\nexport const SCROLLBAR_TRACK_BORDER_CLASS =\n '[&::-webkit-scrollbar-track:vertical]:[border-left:1px_solid_var(--sys-color-border-default)] ' +\n '[&::-webkit-scrollbar-track:horizontal]:[border-top:1px_solid_var(--sys-color-border-default)] ' +\n '[&::-webkit-scrollbar-thumb:vertical]:[border-left-width:3px] ' +\n '[&::-webkit-scrollbar-thumb:horizontal]:[border-top-width:3px]';\n","import type { SColor } from '../../lib/color';\nimport type { SIconName } from '../SIcon';\nimport type { STagColor } from '../STag';\n\n/** 메뉴 스타일: box = 라운드/좁은 들여쓰기, belt = 풀폭 행/넓은 들여쓰기 */\nexport type SGnbType = 'box' | 'belt';\n/** 상단바 구조: fix = GNB 폭에 고정, full = 레이아웃 전폭 */\nexport type SGnbHeader = 'fix' | 'full';\n/** 색상(테마): light / dark */\nexport type SGnbColor = 'light' | 'dark';\n\n/** GNB 메뉴 아이템 (재귀 트리). depth1 → depth2 → depth3. */\nexport interface SGnbMenuItem {\n /** 표시 텍스트 */\n label: string;\n /** 고유 식별값 (선택 상태 비교 기준) */\n value: string;\n /** depth1 전용 아이콘명 */\n icon?: SIconName;\n /** 우측 Tag 뱃지 텍스트 */\n tag?: string;\n /** 뱃지 색. 미지정 시 blue. */\n tagColor?: STagColor;\n /** 비활성 여부 */\n disabled?: boolean;\n /** 하위 아이템 (있으면 펼침/접힘 대상) */\n children?: SGnbMenuItem[];\n}\n\n// theme.css 의 컴포넌트 토큰 참조 (디자인팀 소유, 직접 수정 금지)\n// 토큰은 gnb 한 벌로 통합됐다:\n// 공통(상단바/로고/폴드/바디/아이템) → --cmp-gnb-*\n// 메뉴 변형(type 축) box → --cmp-gnb-box-*, belt → --cmp-gnb-belt-*\n// 접힘 레일 → --cmp-gnb-rail-*\n// 색(color 축) light → --cmp-gnb-light-*, dark → --cmp-gnb-dark-*\nconst gnb = (p: string) => `var(--cmp-gnb-${p})`;\nconst box = (p: string) => `var(--cmp-gnb-box-${p})`;\nconst belt = (p: string) => `var(--cmp-gnb-belt-${p})`;\nconst light = (p: string) => `var(--cmp-gnb-light-${p})`;\nconst dark = (p: string) => `var(--cmp-gnb-dark-${p})`;\n\n/**\n * 상단바(header 축). 상단바 치수 토큰은 gnb 로 통합돼(--cmp-gnb-top-*) fix·full 이 공유한다.\n * 배치는 여전히 이 축이 정한다 — fix 는 GNB 컬럼 안, full 은 레이아웃 전폭(SGnb 에서 분기).\n */\nconst HEADER_TOP = {\n topHeight: gnb('top-height'),\n topPaddingX: gnb('top-paddingX'),\n topGap: gnb('top-gap'),\n logoHeight: gnb('logo-height'),\n};\n\nexport const GNB_HEADER: Record<\n SGnbHeader,\n {\n topHeight: string;\n topPaddingX: string;\n topGap: string;\n logoHeight: string;\n }\n> = {\n fix: HEADER_TOP,\n full: HEADER_TOP,\n};\n\n/** 메뉴(type 축). box → --cmp-gnb-box-*, belt → --cmp-gnb-belt-* (공통 치수는 --cmp-gnb-*) */\nexport const GNB_MENU: Record<\n SGnbType,\n {\n bodyPaddingY: string;\n itemMarginX: string;\n itemPaddingY: string;\n itemPaddingRight: string;\n itemGap: string;\n itemRadius: string;\n itemArrow: string;\n depth1PaddingLeft: string;\n depth2PaddingLeft: string;\n depth3PaddingLeft: string;\n depth1Icon: string;\n // 타이포 (theme.css 에 gnb 타이포 변수 없음 → 토큰 값 직접 명시)\n depth1FontSize: number;\n depth1LineHeight: number;\n depth1FontWeight: number;\n itemFontSize: number;\n itemLineHeight: number;\n }\n> = {\n box: {\n // 바디 패딩·세로 패딩·chevron 은 gnb 로 통합된 공통 토큰\n bodyPaddingY: gnb('body-padding'),\n itemMarginX: gnb('body-padding'), // 전방향 토큰 → 아이템 좌우 여백에도 사용\n itemPaddingY: gnb('item-paddingY'),\n itemPaddingRight: box('item-paddingRight'),\n itemGap: box('item-gap'),\n itemRadius: box('item-radius'),\n itemArrow: gnb('item-chevron-size'),\n depth1PaddingLeft: box('item-depth1-paddingLeft'),\n depth2PaddingLeft: box('item-depth2-paddingLeft'),\n depth3PaddingLeft: box('item-depth3-paddingLeft'),\n depth1Icon: box('item-depth1-icon'),\n depth1FontSize: 14, // token: gnb.box.item.typography.selected.fontSize\n depth1LineHeight: 24, // token: gnb.box.item.typography.selected.lineHeight\n depth1FontWeight: 700,\n itemFontSize: 14, // token: gnb.box.item.typography.default.fontSize\n itemLineHeight: 24, // token: gnb.box.item.typography.default.lineHeight\n },\n belt: {\n bodyPaddingY: gnb('body-padding'),\n itemMarginX: '0px', // belt 토큰에 가로 여백 없음 → 풀폭 행 (들여쓰기는 depth*-paddingLeft 전담)\n itemPaddingY: gnb('item-paddingY'),\n itemPaddingRight: belt('item-paddingRight'),\n itemGap: belt('item-gap'),\n itemRadius: '0px', // belt 토큰에 radius 없음 → 풀폭 행\n itemArrow: gnb('item-chevron-size'),\n depth1PaddingLeft: belt('item-depth1-paddingLeft'),\n depth2PaddingLeft: belt('item-depth2-paddingLeft'),\n depth3PaddingLeft: belt('item-depth3-paddingLeft'),\n depth1Icon: belt('item-depth1-icon'), // belt 전용 depth1 아이콘 토큰(16)\n depth1FontSize: 13, // token: gnb.belt.item.depth1.typography.fontSize\n depth1LineHeight: 22, // token: gnb.belt.item.depth1.typography.lineHeight\n depth1FontWeight: 700,\n itemFontSize: 12, // token: gnb.belt.item.typography.default.fontSize\n itemLineHeight: 20, // token: gnb.belt.item.typography.default.lineHeight\n },\n};\n\n/**\n * 색상(color 축). light → --cmp-gnb-light-* 색, dark → --cmp-gnb-dark-* 색\n * 상단바 아이콘 색(--cmp-gnb-*-top-icon)은 SGhostButton 이 자체 intent 토큰으로 칠한다:\n * light → --cmp-ghostButton-default-content, dark → --cmp-ghostButton-inverse-content (값 동일).\n */\nexport const GNB_COLORS: Record<\n SGnbColor,\n {\n panelBg: string;\n topBorder: string;\n itemBgDefault: string;\n itemBgHover: string;\n itemBgSelected: string;\n itemColorDefault: string;\n itemColorHover: string;\n itemColorActive: string;\n itemChevron: string;\n /** depth1 아이콘 색. 대응 --cmp-gnb-*-item-icon 토큰이 없어 팔레트 키로 직접 지정한다. */\n itemIcon: SColor;\n /** 레일 아이템 기본 색(아이콘+라벨). 선택 시엔 itemColorActive 를 쓴다. */\n railFgDefault: string;\n }\n> = {\n light: {\n panelBg: light('bg'),\n topBorder: light('divider'),\n itemBgDefault: light('item-bg-default'),\n itemBgHover: light('item-bg-hover'),\n itemBgSelected: light('item-bg-selected'),\n itemColorDefault: light('item-fg-default'),\n itemColorHover: light('item-fg-hover'),\n itemColorActive: light('item-fg-selected'),\n itemChevron: light('item-chevron'),\n itemIcon: 'grey_65',\n railFgDefault: light('rail-fg-default'),\n },\n dark: {\n panelBg: dark('bg'),\n topBorder: dark('divider'),\n itemBgDefault: dark('item-bg-default'),\n itemBgHover: dark('item-bg-hover'),\n itemBgSelected: dark('item-bg-selected'),\n itemColorDefault: dark('item-fg-default'),\n itemColorHover: dark('item-fg-hover'),\n itemColorActive: dark('item-fg-selected'),\n itemChevron: dark('item-chevron'),\n itemIcon: 'grey_55',\n railFgDefault: dark('rail-fg-default'),\n },\n};\n\n/** 축과 무관한 공통 레이아웃. */\nexport const GNB_COMMON = {\n /** 접힘 레일 폭. header=\"full\" 은 접으면 메뉴만 사라져 레일이 없으므로 fix 전용. */\n foldWidth: gnb('fold-width'),\n};\n\n/**\n * 좌측 레일(useRail) — items 의 첫 depth 를 아이콘+라벨 버튼으로 세우는 1차 내비게이션.\n * 선택 색은 레일 전용 토큰이 없어 메뉴 아이템의 선택 색(item-bg-selected / item-fg-selected)을 함께 쓴다.\n */\nexport const GNB_RAIL = {\n padding: gnb('rail-padding'),\n gap: gnb('rail-gap'),\n itemWidth: gnb('rail-item-width'),\n itemPaddingTop: gnb('rail-item-paddingTop'),\n itemPaddingBottom: gnb('rail-item-paddingBottom'),\n itemRadius: gnb('rail-item-radius'),\n itemIcon: gnb('rail-item-icon'),\n // 타이포 (theme.css 에 gnb 타이포 변수 없음 → 토큰 값 직접 명시)\n itemFontSize: 11, // token: gnb.rail.item.typography.default.fontSize\n itemLineHeight: 18, // token: gnb.rail.item.typography.default.lineHeight\n itemFontWeight: 500, // token: gnb.rail.item.typography.default.fontWeight\n itemFontWeightSelected: 700, // token: gnb.rail.item.typography.selected.fontWeight\n};\n\n/**\n * 레일 컬럼 폭 = rail-item-width(44) + rail-padding(12)*2.\n * 토큰이 CSS 변수라 JS 에서 더할 수 없어 합을 상수로 둔다 (GNB_MENU_WIDTH 와 같은 이유).\n */\nexport const GNB_RAIL_WIDTH = 68;\n\n/**\n * 접힘/펼침 전환 시간(ms). GNB 폭·메뉴 슬라이드·상단바 페이드와 SLayout 의 메뉴 열 폭이\n * 이 값을 공유해야 한 덩어리로 미끄러진다.\n */\nexport const GNB_FOLD_MS = 200;\n\n/**\n * 하위 메뉴 펼침/접힘 전환 시간(ms). 아이템을 눌러 children 이 열리고 닫힐 때\n * 높이(grid-template-rows 0fr↔1fr)가 이 길이만큼 부드럽게 늘고 줄어든다.\n * 폴드와 같은 값을 써서 두 모션이 한 언어로 읽히게 한다.\n */\nexport const GNB_EXPAND_MS = 200;\n\n/**\n * 메뉴 고정 폭. 두 header 모두 메뉴는 이 폭으로 좌측에 붙는다(토큰 없음 → 상수).\n * header=\"full\" 에서 SLayout 이 \"메뉴 오른쪽 = 페이지\" 영역을 잡을 때도 이 값을 쓴다.\n */\nexport const GNB_MENU_WIDTH = 240;\n\n/**\n * useRail 에서 value 를 품은 첫 depth(레일) 아이템의 value.\n * value 자체가 레일 아이템이면 그 값을, 어디에도 없으면 undefined 를 반환한다.\n */\nexport function findGnbRailValue(items: SGnbMenuItem[], value: string): string | undefined {\n if (items.some(item => item.value === value)) return value;\n return findGnbAncestors(items, value)[0];\n}\n\n/** value 로 선택된 아이템의 조상 value 목록(자기 자신 제외)을 반환 → 자동 펼침용 */\nexport function findGnbAncestors(items: SGnbMenuItem[], value: string): string[] {\n const walk = (list: SGnbMenuItem[], trail: string[]): string[] | null => {\n for (const item of list) {\n if (item.value === value) return trail;\n if (item.children) {\n const found = walk(item.children, [...trail, item.value]);\n if (found) return found;\n }\n }\n return null;\n };\n return walk(items, []) ?? [];\n}\n","'use client';\n\nimport {\n createContext,\n forwardRef,\n useCallback,\n useContext,\n useMemo,\n useState,\n type CSSProperties,\n type HTMLAttributes,\n} from 'react';\nimport { cn } from '../../lib/cn';\nimport {\n GNB_FOLD_MS,\n GNB_HEADER,\n GNB_MENU_WIDTH,\n GNB_RAIL_WIDTH,\n type SGnbHeader,\n type SGnbType,\n} from '../SGnb/gnb.config';\n\n/** 메뉴 스타일: box(라운드) / belt(풀폭 행) — 자식 SGnb 의 메뉴 모양 */\nexport type SLayoutType = SGnbType;\n/** 레이아웃 구조: fix(좌측 GNB + 페이지 가로 분할) / full(풀폭 상단바 + 아래에 메뉴|페이지) */\nexport type SLayoutHeader = SGnbHeader;\n\nexport interface SLayoutContextValue {\n /** 메뉴 스타일 — 자식 SGnb 가 스타일을 맞추는 기준 */\n type: SLayoutType;\n /**\n * 상단바 구조 — 자식 SGnb·SPage 가 배치를 맞추는 기준(유효 header).\n * 레일에 children 없는 아이템이 있어 SGnb 가 full 을 강제하면 여기에도 full 로 반영된다.\n */\n header: SLayoutHeader;\n /** GNB 좌측 레일 사용 여부 — 자식 SGnb 가 따르고, header=\"full\" 의 메뉴 열 폭도 이 값을 반영한다 */\n useRail: boolean;\n /** GNB 접힘 상태 */\n folded: boolean;\n /** 접힘 상태 변경 */\n setFolded: (folded: boolean) => void;\n /**\n * 자식 SGnb 가 런타임 배치 상태를 알린다(SLayout 은 items·activeRail 을 모른다).\n * - forceFull: 레일에 리프(children 없는) 아이템이 있어 header 를 full 로 강제했는지 → 프레임을 grid 로 맞춘다.\n * - menuVisible: 활성 레일에 메뉴 패널이 깔렸는지 → false 면 메뉴 열을 빼 레일 폭만 예약한다(빈틈 방지).\n */\n reportGnbState: (state: { forceFull: boolean; menuVisible: boolean }) => void;\n}\n\nexport const SLayoutContext = createContext<SLayoutContextValue | null>(null);\n\n/**\n * SLayout 의 상태를 읽고 제어한다. `<SLayout>` 밖에서 호출하면 throw.\n * 외부 버튼으로 GNB 를 접거나, 현재 구조(header)·메뉴 스타일(type)에 따라 분기할 때 사용한다.\n */\nexport function useSLayout(): SLayoutContextValue {\n const ctx = useContext(SLayoutContext);\n if (!ctx) throw new Error('useSLayout 은 <SLayout> 안에서만 사용할 수 있습니다.');\n return ctx;\n}\n\nexport interface SLayoutProps extends HTMLAttributes<HTMLDivElement> {\n /** 메뉴 스타일: box(라운드) / belt(풀폭 행). 자식 SGnb 가 이 값을 따른다. */\n type?: SLayoutType;\n /** 레이아웃 구조: fix(가로 분할) / full(풀폭 상단바). 자식 SGnb·SPage 가 이 값을 따른다. */\n header?: SLayoutHeader;\n /**\n * GNB 좌측 레일 사용 여부. 자식 SGnb 가 이 값을 따르고, full 의 메뉴 열 폭도 레일만큼 넓어진다.\n * 레일에 children 없는 아이템이 있으면 SGnb 가 header 를 full 로 강제하고, 이 레이아웃도 그에 맞춰 full 그리드로 선다.\n */\n useRail?: boolean;\n /** GNB 접힘 상태 (controlled) */\n folded?: boolean;\n /** GNB 초기 접힘 상태 (uncontrolled) */\n defaultFolded?: boolean;\n /** 접힘 상태 변경 */\n onFoldedChange?: (folded: boolean) => void;\n}\n\n/**\n * SLayout — 앱 셸 프레임 겸 context provider.\n * header 가 배치를 결정하고 overflow:hidden 으로 스크롤 경계를 만든다.\n * 자식 SGnb·SPage 는 context 에서 type·header·folded 를 읽으므로 앱은 이 값들을 여기 한 곳에만 쓴다.\n * 높이는 기본 100%(부모를 채움) — 앱 루트로 쓸 땐 부모를 뷰포트 높이로 잡는다.\n *\n * - header=\"fix\": [GNB(자기 폭 소유) | 페이지] 가로 flex.\n * - header=\"full\": 상단바가 풀폭이고 그 아래에 [메뉴 | 페이지] 가 나란히 서야 하는데, SGnb 가 상단바와\n * 메뉴를 한 엘리먼트로 갖고 있어 사이에 끼워넣을 수 없다. 그래서 grid 로 깔고\n * SGnb 를 전 셀에 겹친 뒤(root 는 투명) SPage 가 (2행,2열) = \"메뉴 오른쪽 투명 영역\" 을 차지한다.\n * 접으면 메뉴가 사라지므로 1열을 0 으로 접어 페이지가 전폭을 쓴다.\n */\nexport const SLayout = forwardRef<HTMLDivElement, SLayoutProps>(function SLayout(\n {\n type = 'box',\n header = 'fix',\n useRail = false,\n folded,\n defaultFolded = false,\n onFoldedChange,\n className,\n style,\n children,\n ...rest\n },\n ref,\n) {\n const [uncontrolledFolded, setUncontrolledFolded] = useState(defaultFolded);\n const isControlled = folded !== undefined;\n const currentFolded = isControlled ? folded : uncontrolledFolded;\n\n const setFolded = useCallback(\n (next: boolean) => {\n if (!isControlled) setUncontrolledFolded(next);\n onFoldedChange?.(next);\n },\n [isControlled, onFoldedChange],\n );\n\n // 자식 SGnb 가 알려주는 런타임 배치 상태. SLayout 은 items·activeRail 을 모르므로 이 신호로\n // full 강제 여부와 메뉴 열 폭을 맞춘다. prev 동일값이면 같은 참조를 유지해 불필요한 리렌더를 막는다.\n const [gnbState, setGnbState] = useState({ forceFull: false, menuVisible: true });\n const reportGnbState = useCallback(\n (next: { forceFull: boolean; menuVisible: boolean }) =>\n setGnbState(prev =>\n prev.forceFull === next.forceFull && prev.menuVisible === next.menuVisible ? prev : next,\n ),\n [],\n );\n\n // 레일에 리프 아이템이 있으면 SGnb 가 header 를 full 로 강제한다 → 프레임(grid)도 그에 맞춘다.\n const effectiveHeader: SLayoutHeader = header === 'full' || gnbState.forceFull ? 'full' : 'fix';\n\n const ctx = useMemo<SLayoutContextValue>(\n () => ({\n type,\n header: effectiveHeader,\n useRail,\n folded: currentFolded,\n setFolded,\n reportGnbState,\n }),\n [type, effectiveHeader, useRail, currentFolded, setFolded, reportGnbState],\n );\n\n const isFullHeader = effectiveHeader === 'full';\n // 레일이 있으면 GNB 가 [레일 | 메뉴] 두 칸이므로 페이지는 그만큼 오른쪽에서 시작한다.\n // 리프 레일이 활성이라 메뉴가 숨겨지면(menuVisible=false) 메뉴 칸을 빼 레일 폭만 예약한다(빈틈 방지).\n const gnbColumnWidth =\n (useRail ? GNB_RAIL_WIDTH : 0) + (gnbState.menuVisible ? GNB_MENU_WIDTH : 0);\n const frameStyle: CSSProperties | undefined = isFullHeader\n ? {\n gridTemplateColumns: `${currentFolded ? 0 : gnbColumnWidth}px 1fr`,\n gridTemplateRows: `${GNB_HEADER[effectiveHeader].topHeight} 1fr`,\n // 메뉴가 미끄러지는 동안 페이지 열도 같은 길이로 따라와야 한 덩어리로 읽힌다.\n // (fix 는 SGnb 가 제 폭을 직접 전환하므로 프레임이 손댈 게 없다)\n transition: `grid-template-columns ${GNB_FOLD_MS}ms ease-out`,\n }\n : undefined;\n\n return (\n <SLayoutContext.Provider value={ctx}>\n <div\n ref={ref}\n data-type={type}\n data-header={effectiveHeader}\n className={cn(\n 'box-border h-full overflow-hidden',\n isFullHeader ? 'grid' : 'flex flex-row',\n className,\n )}\n style={{ ...frameStyle, ...style }}\n {...rest}\n >\n {children}\n </div>\n </SLayoutContext.Provider>\n );\n});\n","import { forwardRef, useContext, type CSSProperties, type HTMLAttributes } from 'react';\nimport { cn } from '../../lib/cn';\nimport {\n SCROLLBAR_CLASS,\n SCROLLBAR_TRACK_BG_CLASS,\n SCROLLBAR_TRACK_BG_WHITE_CLASS,\n SCROLLBAR_TRACK_BORDER_CLASS,\n} from '../../lib/scrollbar';\nimport { SLayoutContext } from '../SLayout/SLayout';\nimport type { SPageBackground } from './page.config';\n\n/**\n * 배경별 표면색 + 스크롤바 처리. 구분선(bordered)과 트랙 배경은 셋 다 깔되, 트랙 색만 갈린다.\n *\n * 기본 트랙 색은 grey-10(#F6F6F6)인데 neutral 표면(bg.neutralLight)이 정확히 같은 색이라\n * 그대로 쓰면 트랙이 페이지에 묻힌다. 그래서 neutral 만 흰 트랙을 써서 홈을 드러낸다.\n * (트랙 배경을 아예 빼면 흰색이 아니라 transparent 라 페이지색이 비친다 — 묻히는 건 마찬가지다)\n */\nconst BACKGROUND_CLASS: Record<SPageBackground, string> = {\n frame: `bg-[var(--sys-color-bg-frame)] ${SCROLLBAR_TRACK_BORDER_CLASS} ${SCROLLBAR_TRACK_BG_CLASS}`,\n neutral: `bg-[var(--sys-color-bg-neutralLight)] ${SCROLLBAR_TRACK_BORDER_CLASS} ${SCROLLBAR_TRACK_BG_WHITE_CLASS}`,\n screen: `bg-[var(--sys-color-bg-screen)] ${SCROLLBAR_TRACK_BORDER_CLASS} ${SCROLLBAR_TRACK_BG_CLASS}`,\n};\n\n// docs:gen 은 export interface 만 수집하므로(gen-docs.mjs) 타입 별칭 대신 interface 로 둔다.\nexport interface SPageProps extends HTMLAttributes<HTMLElement> {\n /**\n * 페이지 배경 표면. frame=흰 콘텐츠 면, neutral=옅은 회색 면, screen=앱 바탕.\n * 스크롤바 처리도 여기 묶여 있다 — 셋 다 구분선+트랙 배경이고, 트랙 색만 neutral 에서 흰색이 된다.\n */\n background?: SPageBackground;\n}\n\n/**\n * SPage — SLayout 안에서 남은 공간을 채우는 스크롤 콘텐츠 영역.\n * 스크롤은 SLayout(overflow:hidden)이 잡아둔 경계 안에서 이 영역에만 생긴다.\n * 패딩은 className 으로, 배경은 background 로 정한다(className 으로도 덮어쓸 수 있다).\n *\n * - header=\"fix\": flex 자식으로 남은 가로 공간을 채운다.\n * - header=\"full\": SLayout 이 깐 grid 의 (2행,2열) = 상단바 아래·메뉴 오른쪽 자리를 차지한다.\n * (SGnb 가 전 셀에 겹쳐 있지만 그 영역은 투명해서 SPage 가 보인다)\n */\nexport const SPage = forwardRef<HTMLElement, SPageProps>(function SPage(\n { background = 'frame', className, style, children, ...rest },\n ref,\n) {\n const layout = useContext(SLayoutContext);\n const gridStyle: CSSProperties | undefined =\n layout?.header === 'full' ? { gridColumn: 2, gridRow: 2 } : undefined;\n\n return (\n <main\n ref={ref}\n className={cn(\n 'box-border min-h-0 min-w-0 flex-1 overflow-auto p-20',\n SCROLLBAR_CLASS,\n BACKGROUND_CLASS[background],\n className,\n )}\n style={{ ...gridStyle, ...style }}\n {...rest}\n >\n {children}\n </main>\n );\n});\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 페이지 배경 — 표면의 역할로 이름을 붙인다(리터럴 색이 아니다).
|
|
3
|
+
* - frame: 콘텐츠를 얹는 흰 표면 (sys.color.bg.frame)
|
|
4
|
+
* - neutral: 한 단계 눌러앉은 회색 표면 (sys.color.bg.neutralLight)
|
|
5
|
+
* - screen: 카드·패널이 떠 있는 앱 바탕 (sys.color.bg.screen)
|
|
6
|
+
*/
|
|
7
|
+
export declare const PAGE_BACKGROUNDS: readonly ["frame", "neutral", "screen"];
|
|
8
|
+
export type SPageBackground = (typeof PAGE_BACKGROUNDS)[number];
|