sellmate-design-system-react 0.2.0 → 0.4.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/README.md +84 -21
- package/dist/components/SActionModal/SActionModal.cjs +2 -0
- package/dist/components/SActionModal/SActionModal.cjs.map +1 -1
- package/dist/components/SActionModal/SActionModal.js +2 -0
- package/dist/components/SActionModal/SActionModal.js.map +1 -1
- package/dist/components/SBarcodeInput/SBarcodeInput.cjs +2 -0
- package/dist/components/SBarcodeInput/SBarcodeInput.cjs.map +1 -1
- package/dist/components/SBarcodeInput/SBarcodeInput.js +2 -0
- package/dist/components/SBarcodeInput/SBarcodeInput.js.map +1 -1
- package/dist/components/SCalendar/SCalendar.cjs +2 -0
- package/dist/components/SCalendar/SCalendar.cjs.map +1 -1
- package/dist/components/SCalendar/SCalendar.js +2 -0
- package/dist/components/SCalendar/SCalendar.js.map +1 -1
- package/dist/components/SChip/SChip.cjs +2 -0
- package/dist/components/SChip/SChip.cjs.map +1 -1
- package/dist/components/SChip/SChip.js +2 -0
- package/dist/components/SChip/SChip.js.map +1 -1
- package/dist/components/SChipInput/SChipInput.cjs +2 -0
- package/dist/components/SChipInput/SChipInput.cjs.map +1 -1
- package/dist/components/SChipInput/SChipInput.js +2 -0
- package/dist/components/SChipInput/SChipInput.js.map +1 -1
- package/dist/components/SConfirmModal/SConfirmModal.cjs +2 -0
- package/dist/components/SConfirmModal/SConfirmModal.cjs.map +1 -1
- package/dist/components/SConfirmModal/SConfirmModal.js +2 -0
- package/dist/components/SConfirmModal/SConfirmModal.js.map +1 -1
- package/dist/components/SDatePicker/SDatePicker.cjs +2 -0
- package/dist/components/SDatePicker/SDatePicker.cjs.map +1 -1
- package/dist/components/SDatePicker/SDatePicker.js +2 -0
- package/dist/components/SDatePicker/SDatePicker.js.map +1 -1
- package/dist/components/SDateRangePicker/SDateRangePicker.cjs +2 -0
- package/dist/components/SDateRangePicker/SDateRangePicker.cjs.map +1 -1
- package/dist/components/SDateRangePicker/SDateRangePicker.js +2 -0
- package/dist/components/SDateRangePicker/SDateRangePicker.js.map +1 -1
- package/dist/components/SField/SField.cjs +2 -0
- package/dist/components/SField/SField.cjs.map +1 -1
- package/dist/components/SField/SField.js +2 -0
- package/dist/components/SField/SField.js.map +1 -1
- package/dist/components/SFilePicker/SFilePicker.cjs +2 -0
- package/dist/components/SFilePicker/SFilePicker.cjs.map +1 -1
- package/dist/components/SFilePicker/SFilePicker.js +2 -0
- package/dist/components/SFilePicker/SFilePicker.js.map +1 -1
- package/dist/components/SGhostButton/SGhostButton.cjs +2 -0
- package/dist/components/SGhostButton/SGhostButton.cjs.map +1 -1
- package/dist/components/SGhostButton/SGhostButton.d.ts +3 -1
- package/dist/components/SGhostButton/SGhostButton.js +2 -0
- package/dist/components/SGhostButton/SGhostButton.js.map +1 -1
- package/dist/components/SGnb/SGnb.cjs +394 -213
- package/dist/components/SGnb/SGnb.cjs.map +1 -1
- package/dist/components/SGnb/SGnb.d.ts +16 -12
- package/dist/components/SGnb/SGnb.js +375 -214
- package/dist/components/SGnb/SGnb.js.map +1 -1
- package/dist/components/SGnb/gnb.config.d.ts +35 -11
- package/dist/components/SGnb/index.d.ts +1 -1
- package/dist/components/SGuide/SGuide.cjs +2 -0
- package/dist/components/SGuide/SGuide.cjs.map +1 -1
- package/dist/components/SGuide/SGuide.js +2 -0
- package/dist/components/SGuide/SGuide.js.map +1 -1
- package/dist/components/SInput/SInput.cjs +2 -0
- package/dist/components/SInput/SInput.cjs.map +1 -1
- package/dist/components/SInput/SInput.js +2 -0
- package/dist/components/SInput/SInput.js.map +1 -1
- package/dist/components/SKeyValueTable/SKeyValueTable.cjs +2 -0
- package/dist/components/SKeyValueTable/SKeyValueTable.cjs.map +1 -1
- package/dist/components/SKeyValueTable/SKeyValueTable.js +2 -0
- package/dist/components/SKeyValueTable/SKeyValueTable.js.map +1 -1
- package/dist/components/SLayout/SLayout.cjs +26 -8
- package/dist/components/SLayout/SLayout.cjs.map +1 -1
- package/dist/components/SLayout/SLayout.d.ts +15 -9
- package/dist/components/SLayout/SLayout.js +26 -8
- package/dist/components/SLayout/SLayout.js.map +1 -1
- package/dist/components/SLayout/index.d.ts +1 -1
- package/dist/components/SLoadingModal/SLoadingModal.cjs +2 -0
- package/dist/components/SLoadingModal/SLoadingModal.cjs.map +1 -1
- package/dist/components/SLoadingModal/SLoadingModal.js +2 -0
- package/dist/components/SLoadingModal/SLoadingModal.js.map +1 -1
- package/dist/components/SModalContainer/SModalContainer.cjs +2 -0
- package/dist/components/SModalContainer/SModalContainer.cjs.map +1 -1
- package/dist/components/SModalContainer/SModalContainer.js +2 -0
- package/dist/components/SModalContainer/SModalContainer.js.map +1 -1
- package/dist/components/SNumberInput/SNumberInput.cjs +2 -0
- package/dist/components/SNumberInput/SNumberInput.cjs.map +1 -1
- package/dist/components/SNumberInput/SNumberInput.js +2 -0
- package/dist/components/SNumberInput/SNumberInput.js.map +1 -1
- package/dist/components/SPage/SPage.cjs +27 -9
- package/dist/components/SPage/SPage.cjs.map +1 -1
- package/dist/components/SPage/SPage.d.ts +2 -2
- package/dist/components/SPage/SPage.js +27 -9
- package/dist/components/SPage/SPage.js.map +1 -1
- package/dist/components/SPopover/SPopover.cjs +2 -0
- package/dist/components/SPopover/SPopover.cjs.map +1 -1
- package/dist/components/SPopover/SPopover.js +2 -0
- package/dist/components/SPopover/SPopover.js.map +1 -1
- package/dist/components/SSelect/SSelect.cjs +2 -0
- package/dist/components/SSelect/SSelect.cjs.map +1 -1
- package/dist/components/SSelect/SSelect.js +2 -0
- package/dist/components/SSelect/SSelect.js.map +1 -1
- package/dist/components/STable/STable.cjs +3 -1
- package/dist/components/STable/STable.cjs.map +1 -1
- package/dist/components/STable/STable.js +3 -1
- package/dist/components/STable/STable.js.map +1 -1
- package/dist/components/STextarea/STextarea.cjs +2 -0
- package/dist/components/STextarea/STextarea.cjs.map +1 -1
- package/dist/components/STextarea/STextarea.js +2 -0
- package/dist/components/STextarea/STextarea.js.map +1 -1
- package/dist/components/STimePicker/STimePicker.cjs +3834 -0
- package/dist/components/STimePicker/STimePicker.cjs.map +1 -0
- package/dist/components/STimePicker/STimePicker.d.ts +50 -0
- package/dist/components/STimePicker/STimePicker.js +3811 -0
- package/dist/components/STimePicker/STimePicker.js.map +1 -0
- package/dist/components/STimePicker/index.d.ts +2 -0
- package/dist/components/STimePicker/timepicker.config.d.ts +23 -0
- package/dist/components/STimeRangePicker/STimeRangePicker.cjs +3908 -0
- package/dist/components/STimeRangePicker/STimeRangePicker.cjs.map +1 -0
- package/dist/components/STimeRangePicker/STimeRangePicker.d.ts +53 -0
- package/dist/components/STimeRangePicker/STimeRangePicker.js +3885 -0
- package/dist/components/STimeRangePicker/STimeRangePicker.js.map +1 -0
- package/dist/components/STimeRangePicker/index.d.ts +1 -0
- package/dist/components/STimeRangePicker/time-range-picker.config.d.ts +9 -0
- package/dist/components/SToast/SToast.cjs +2 -0
- package/dist/components/SToast/SToast.cjs.map +1 -1
- package/dist/components/SToast/SToast.js +2 -0
- package/dist/components/SToast/SToast.js.map +1 -1
- package/dist/components/SToast/SToastContainer.cjs +2 -0
- package/dist/components/SToast/SToastContainer.cjs.map +1 -1
- package/dist/components/SToast/SToastContainer.js +2 -0
- package/dist/components/SToast/SToastContainer.js.map +1 -1
- package/dist/components/STooltip/STooltip.cjs +2 -0
- package/dist/components/STooltip/STooltip.cjs.map +1 -1
- package/dist/components/STooltip/STooltip.js +2 -0
- package/dist/components/STooltip/STooltip.js.map +1 -1
- package/dist/index.cjs +1011 -136
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1006 -136
- package/dist/index.js.map +1 -1
- package/dist/styles.css +475 -74
- package/dist/theme.css +85 -70
- package/package.json +4 -1
|
@@ -16,13 +16,30 @@ var twMerge = tailwindMerge.extendTailwindMerge({
|
|
|
16
16
|
function cn(...inputs) {
|
|
17
17
|
return twMerge(clsx.clsx(inputs));
|
|
18
18
|
}
|
|
19
|
+
|
|
20
|
+
// src/components/SGnb/gnb.config.ts
|
|
21
|
+
var belt = (p) => `var(--cmp-gnbBelt-${p})`;
|
|
19
22
|
var box = (p) => `var(--cmp-gnbBox-${p})`;
|
|
20
|
-
var
|
|
21
|
-
|
|
23
|
+
var GNB_HEADER = {
|
|
24
|
+
fix: {
|
|
25
|
+
topHeight: belt("top-height"),
|
|
26
|
+
topPaddingX: belt("top-paddingX"),
|
|
27
|
+
topGap: belt("top-gap"),
|
|
28
|
+
logoHeight: belt("logo-height")
|
|
29
|
+
},
|
|
30
|
+
full: {
|
|
31
|
+
topHeight: box("top-height"),
|
|
32
|
+
topPaddingX: box("paddingX"),
|
|
33
|
+
topGap: box("gap"),
|
|
34
|
+
// 상단바 gap (버튼 간 세부 gap 은 --cmp-gnbBox-button-gap)
|
|
35
|
+
logoHeight: box("logo-height")
|
|
36
|
+
}
|
|
37
|
+
};
|
|
22
38
|
var GNB_MENU_WIDTH = 240;
|
|
23
39
|
var SLayoutContext = react.createContext(null);
|
|
24
40
|
react.forwardRef(function SLayout2({
|
|
25
41
|
type = "box",
|
|
42
|
+
header = "fix",
|
|
26
43
|
folded,
|
|
27
44
|
defaultFolded = false,
|
|
28
45
|
onFoldedChange,
|
|
@@ -42,22 +59,23 @@ react.forwardRef(function SLayout2({
|
|
|
42
59
|
[isControlled, onFoldedChange]
|
|
43
60
|
);
|
|
44
61
|
const ctx = react.useMemo(
|
|
45
|
-
() => ({ type, folded: currentFolded, setFolded }),
|
|
46
|
-
[type, currentFolded, setFolded]
|
|
62
|
+
() => ({ type, header, folded: currentFolded, setFolded }),
|
|
63
|
+
[type, header, currentFolded, setFolded]
|
|
47
64
|
);
|
|
48
|
-
const
|
|
49
|
-
const frameStyle =
|
|
65
|
+
const isFullHeader = header === "full";
|
|
66
|
+
const frameStyle = isFullHeader ? {
|
|
50
67
|
gridTemplateColumns: `${currentFolded ? 0 : GNB_MENU_WIDTH}px 1fr`,
|
|
51
|
-
gridTemplateRows: `${
|
|
68
|
+
gridTemplateRows: `${GNB_HEADER[header].topHeight} 1fr`
|
|
52
69
|
} : void 0;
|
|
53
70
|
return /* @__PURE__ */ jsxRuntime.jsx(SLayoutContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
54
71
|
"div",
|
|
55
72
|
{
|
|
56
73
|
ref,
|
|
57
74
|
"data-type": type,
|
|
75
|
+
"data-header": header,
|
|
58
76
|
className: cn(
|
|
59
77
|
"box-border h-full overflow-hidden",
|
|
60
|
-
|
|
78
|
+
isFullHeader ? "grid" : "flex flex-row",
|
|
61
79
|
className
|
|
62
80
|
),
|
|
63
81
|
style: { ...frameStyle, ...style },
|
|
@@ -68,7 +86,7 @@ react.forwardRef(function SLayout2({
|
|
|
68
86
|
});
|
|
69
87
|
var SPage = react.forwardRef(function SPage2({ className, style, children, ...rest }, ref) {
|
|
70
88
|
const layout = react.useContext(SLayoutContext);
|
|
71
|
-
const gridStyle = layout?.
|
|
89
|
+
const gridStyle = layout?.header === "full" ? { gridColumn: 2, gridRow: 2 } : void 0;
|
|
72
90
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
73
91
|
"main",
|
|
74
92
|
{
|
|
@@ -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 { SIconName } from '../SIcon';\n\n/** 구조(레이아웃): box = 라운드/좁은 들여쓰기, belt = 풀폭/넓은 들여쓰기 */\nexport type SGnbType = 'box' | 'belt';\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 /** 비활성 여부 */\n disabled?: boolean;\n /** 하위 아이템 (있으면 펼침/접힘 대상) */\n children?: SGnbMenuItem[];\n}\n\n// theme.css 의 컴포넌트 토큰 참조 (디자인팀 소유, 직접 수정 금지)\n// 치수(type 축)는 gnbBox/gnbBelt 토큰, 색(color 축)은 gnb light/dark 토큰(--cmp-light-*/--cmp-dark-*).\nconst belt = (p: string) => `var(--cmp-gnbBelt-${p})`;\nconst box = (p: string) => `var(--cmp-gnbBox-${p})`;\nconst light = (p: string) => `var(--cmp-light-${p})`;\nconst dark = (p: string) => `var(--cmp-dark-${p})`;\n\n/** 레이아웃(type 축). box → --cmp-gnbBox-*, belt → --cmp-gnbBelt-* */\nexport const GNB_LAYOUT: Record<\n SGnbType,\n {\n topPaddingX: string;\n topGap: string;\n bodyPadding: 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 // 사용자 명명: type=\"box\" → gnbBox 토큰(둥금), type=\"belt\" → gnbBelt 토큰(각짐)\n box: {\n topPaddingX: box('paddingX'),\n topGap: box('gap'), // 상단바 gap (버튼 간 세부 gap 은 --cmp-gnbBox-button-gap)\n bodyPadding: box('menu-body-padding'),\n itemPaddingRight: box('item-paddingRight'),\n itemGap: box('item-gap'),\n itemRadius: box('item-radius'),\n itemArrow: box('item-arrow'),\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: gnbBox.item.typography.bold.fontSize\n depth1LineHeight: 24, // token: gnbBox.item.typography.bold.lineHeight\n depth1FontWeight: 700,\n itemFontSize: 14, // token: gnbBox.item.typography.default.fontSize\n itemLineHeight: 24, // token: gnbBox.item.typography.default.lineHeight\n },\n belt: {\n topPaddingX: belt('top-paddingX'),\n topGap: belt('top-gap'),\n bodyPadding: belt('body-paddingY'),\n itemPaddingRight: belt('item-paddingRight'),\n itemGap: belt('item-gap'),\n itemRadius: '0px', // belt 토큰에 radius 없음 → 풀폭 행\n itemArrow: belt('item-icon'),\n depth1PaddingLeft: belt('item-depth1-paddingLeft'),\n depth2PaddingLeft: belt('item-depth2-paddingLeft'),\n depth3PaddingLeft: belt('item-depth3-paddingLeft'),\n depth1Icon: box('item-depth1-icon'), // belt 토큰에 depth1-icon 없음 → box 값(20) 공유\n depth1FontSize: 13, // token: gnbBelt.item.depth1.typography.fontSize\n depth1LineHeight: 22, // token: gnbBelt.item.depth1.typography.lineHeight\n depth1FontWeight: 700,\n itemFontSize: 12, // token: gnbBelt.item.typography.default.fontSize\n itemLineHeight: 20, // token: gnbBelt.item.typography.default.lineHeight\n },\n};\n\n/** 색상(color 축). light → --cmp-light-* 색, dark → --cmp-dark-* 색 (gnb colortype 토큰) */\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 itemColorActive: 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 itemColorActive: light('item-fg-selected'),\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 itemColorActive: dark('item-fg-selected'),\n },\n};\n\n/** 구조와 무관한 공통 레이아웃 */\nexport const GNB_COMMON = {\n topHeight: box('top-height'), // 56 (box=belt 동일)\n logoHeight: box('logo-height'), // 32\n itemPaddingY: box('item-paddingY'), // 6\n foldWidth: belt('fold-width'), // 48 (belt 토큰 공유)\n};\n\n/**\n * 메뉴 고정 폭. belt/box 모두 메뉴는 이 폭으로 좌측에 붙는다(토큰 없음 → 상수).\n * box 에서 SLayout 이 \"메뉴 오른쪽 = 페이지\" 영역을 잡을 때도 이 값을 쓴다.\n */\nexport const GNB_MENU_WIDTH = 240;\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 { GNB_COMMON, GNB_MENU_WIDTH, type SGnbType } from '../SGnb/gnb.config';\n\n/** 레이아웃 구조: box(풀폭 상단바 + 아래에 메뉴|페이지) / belt(좌측 레일 + 페이지 가로 분할) */\nexport type SLayoutType = SGnbType;\n\nexport interface SLayoutContextValue {\n /** 레이아웃 구조 — 자식 SGnb 가 배치·스타일을 맞추는 기준 */\n type: SLayoutType;\n /** GNB 접힘 상태 */\n folded: boolean;\n /** 접힘 상태 변경 */\n setFolded: (folded: boolean) => void;\n}\n\nexport const SLayoutContext = createContext<SLayoutContextValue | null>(null);\n\n/**\n * SLayout 의 상태를 읽고 제어한다. `<SLayout>` 밖에서 호출하면 throw.\n * 외부 버튼으로 GNB 를 접거나, 현재 구조(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 /** 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 * type 하나로 배치를 결정하고 overflow:hidden 으로 스크롤 경계를 만든다.\n * 자식 SGnb·SPage 는 context 에서 type·folded 를 읽으므로 앱은 type 을 여기 한 곳에만 쓴다.\n * 높이는 기본 100%(부모를 채움) — 앱 루트로 쓸 땐 부모를 뷰포트 높이로 잡는다.\n *\n * - belt: [GNB(자기 폭 소유) | 페이지] 가로 flex.\n * - box: 상단바가 풀폭이고 그 아래에 [메뉴 | 페이지] 가 나란히 서야 하는데, 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 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 const ctx = useMemo<SLayoutContextValue>(\n () => ({ type, folded: currentFolded, setFolded }),\n [type, currentFolded, setFolded],\n );\n\n const isBox = type === 'box';\n const frameStyle: CSSProperties | undefined = isBox\n ? {\n gridTemplateColumns: `${currentFolded ? 0 : GNB_MENU_WIDTH}px 1fr`,\n gridTemplateRows: `${GNB_COMMON.topHeight} 1fr`,\n }\n : undefined;\n\n return (\n <SLayoutContext.Provider value={ctx}>\n <div\n ref={ref}\n data-type={type}\n className={cn(\n 'box-border h-full overflow-hidden',\n isBox ? '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 * - belt: flex 자식으로 남은 가로 공간을 채운다.\n * - box: 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?.type === 'box' ? { 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-24', className)}\n style={{ ...gridStyle, ...style }}\n {...rest}\n >\n {children}\n </main>\n );\n});\n"]}
|
|
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 { 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// 토큰은 gnbBox/gnbBelt 두 벌뿐이라 축 이름과 1:1 로 맞지 않는다:\n// 상단바(header 축) fix → gnbBelt-top-*, full → gnbBox-*\n// 메뉴(type 축) box → gnbBox-*, belt → gnbBelt-*\n// 색(color 축)은 --cmp-light-* / --cmp-dark-*.\nconst belt = (p: string) => `var(--cmp-gnbBelt-${p})`;\nconst box = (p: string) => `var(--cmp-gnbBox-${p})`;\nconst light = (p: string) => `var(--cmp-light-${p})`;\nconst dark = (p: string) => `var(--cmp-dark-${p})`;\n\n/**\n * 상단바(header 축). fix → gnbBelt-top-* 토큰, full → gnbBox-* 토큰.\n * 배치까지 이 축이 정한다 — fix 는 GNB 컬럼 안, full 은 레이아웃 전폭.\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: {\n topHeight: belt('top-height'),\n topPaddingX: belt('top-paddingX'),\n topGap: belt('top-gap'),\n logoHeight: belt('logo-height'),\n },\n full: {\n topHeight: box('top-height'),\n topPaddingX: box('paddingX'),\n topGap: box('gap'), // 상단바 gap (버튼 간 세부 gap 은 --cmp-gnbBox-button-gap)\n logoHeight: box('logo-height'),\n },\n};\n\n/** 메뉴(type 축). box → --cmp-gnbBox-*, belt → --cmp-gnbBelt-* */\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 // menu.body.padding 은 전방향 토큰 → 세로 패딩과 아이템 좌우 여백 양쪽에 쓴다\n bodyPaddingY: box('menu-body-padding'),\n itemMarginX: box('menu-body-padding'),\n itemPaddingY: box('item-paddingY'),\n itemPaddingRight: box('item-paddingRight'),\n itemGap: box('item-gap'),\n itemRadius: box('item-radius'),\n itemArrow: box('item-arrow'),\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: gnbBox.item.typography.bold.fontSize\n depth1LineHeight: 24, // token: gnbBox.item.typography.bold.lineHeight\n depth1FontWeight: 700,\n itemFontSize: 14, // token: gnbBox.item.typography.default.fontSize\n itemLineHeight: 24, // token: gnbBox.item.typography.default.lineHeight\n },\n belt: {\n bodyPaddingY: belt('body-paddingY'),\n itemMarginX: '0px', // belt 토큰에 가로 여백 없음 → 풀폭 행 (들여쓰기는 depth*-paddingLeft 전담)\n itemPaddingY: belt('item-paddingY'),\n itemPaddingRight: belt('item-paddingRight'),\n itemGap: belt('item-gap'),\n itemRadius: '0px', // belt 토큰에 radius 없음 → 풀폭 행\n itemArrow: belt('item-icon'),\n depth1PaddingLeft: belt('item-depth1-paddingLeft'),\n depth2PaddingLeft: belt('item-depth2-paddingLeft'),\n depth3PaddingLeft: belt('item-depth3-paddingLeft'),\n depth1Icon: box('item-depth1-icon'), // belt 토큰에 depth1-icon 없음 → box 값(20) 공유\n depth1FontSize: 13, // token: gnbBelt.item.depth1.typography.fontSize\n depth1LineHeight: 22, // token: gnbBelt.item.depth1.typography.lineHeight\n depth1FontWeight: 700,\n itemFontSize: 12, // token: gnbBelt.item.typography.default.fontSize\n itemLineHeight: 20, // token: gnbBelt.item.typography.default.lineHeight\n },\n};\n\n/**\n * 색상(color 축). light → --cmp-light-* 색, dark → --cmp-dark-* 색 (gnb colortype 토큰)\n * 상단바 아이콘 색(--cmp-*-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 }\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 },\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 },\n};\n\n/** 축과 무관한 공통 레이아웃. */\nexport const GNB_COMMON = {\n /** 접힘 레일 폭. header=\"full\" 은 접으면 메뉴만 사라져 레일이 없으므로 fix 전용. */\n foldWidth: belt('fold-width'),\n /**\n * 상단바 아이콘 버튼 사이 간격. fix 레일(세로 스택)의 gap·상하 여백에 쓴다.\n * 레일 전용 여백 토큰이 따로 없어 같은 값(8px)인 이 토큰으로 재해석했다.\n */\n buttonGap: box('button-gap'),\n};\n\n/**\n * 메뉴 고정 폭. 두 header 모두 메뉴는 이 폭으로 좌측에 붙는다(토큰 없음 → 상수).\n * header=\"full\" 에서 SLayout 이 \"메뉴 오른쪽 = 페이지\" 영역을 잡을 때도 이 값을 쓴다.\n */\nexport const GNB_MENU_WIDTH = 240;\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 { GNB_HEADER, GNB_MENU_WIDTH, type SGnbHeader, type SGnbType } 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 /** 상단바 구조 — 자식 SGnb·SPage 가 배치를 맞추는 기준 */\n header: SLayoutHeader;\n /** GNB 접힘 상태 */\n folded: boolean;\n /** 접힘 상태 변경 */\n setFolded: (folded: 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 /** 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 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 const ctx = useMemo<SLayoutContextValue>(\n () => ({ type, header, folded: currentFolded, setFolded }),\n [type, header, currentFolded, setFolded],\n );\n\n const isFullHeader = header === 'full';\n const frameStyle: CSSProperties | undefined = isFullHeader\n ? {\n gridTemplateColumns: `${currentFolded ? 0 : GNB_MENU_WIDTH}px 1fr`,\n gridTemplateRows: `${GNB_HEADER[header].topHeight} 1fr`,\n }\n : undefined;\n\n return (\n <SLayoutContext.Provider value={ctx}>\n <div\n ref={ref}\n data-type={type}\n data-header={header}\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-24', className)}\n style={{ ...gridStyle, ...style }}\n {...rest}\n >\n {children}\n </main>\n );\n});\n"]}
|
|
@@ -6,8 +6,8 @@ export interface SPageProps extends HTMLAttributes<HTMLElement> {
|
|
|
6
6
|
* 스크롤은 SLayout(overflow:hidden)이 잡아둔 경계 안에서 이 영역에만 생긴다.
|
|
7
7
|
* 패딩·배경은 className 으로 덮어쓸 수 있다.
|
|
8
8
|
*
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
9
|
+
* - header="fix": flex 자식으로 남은 가로 공간을 채운다.
|
|
10
|
+
* - header="full": SLayout 이 깐 grid 의 (2행,2열) = 상단바 아래·메뉴 오른쪽 자리를 차지한다.
|
|
11
11
|
* (SGnb 가 전 셀에 겹쳐 있지만 그 영역은 투명해서 SPage 가 보인다)
|
|
12
12
|
*/
|
|
13
13
|
export declare const SPage: import("react").ForwardRefExoticComponent<SPageProps & import("react").RefAttributes<HTMLElement>>;
|
|
@@ -14,13 +14,30 @@ var twMerge = extendTailwindMerge({
|
|
|
14
14
|
function cn(...inputs) {
|
|
15
15
|
return twMerge(clsx(inputs));
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
// src/components/SGnb/gnb.config.ts
|
|
19
|
+
var belt = (p) => `var(--cmp-gnbBelt-${p})`;
|
|
17
20
|
var box = (p) => `var(--cmp-gnbBox-${p})`;
|
|
18
|
-
var
|
|
19
|
-
|
|
21
|
+
var GNB_HEADER = {
|
|
22
|
+
fix: {
|
|
23
|
+
topHeight: belt("top-height"),
|
|
24
|
+
topPaddingX: belt("top-paddingX"),
|
|
25
|
+
topGap: belt("top-gap"),
|
|
26
|
+
logoHeight: belt("logo-height")
|
|
27
|
+
},
|
|
28
|
+
full: {
|
|
29
|
+
topHeight: box("top-height"),
|
|
30
|
+
topPaddingX: box("paddingX"),
|
|
31
|
+
topGap: box("gap"),
|
|
32
|
+
// 상단바 gap (버튼 간 세부 gap 은 --cmp-gnbBox-button-gap)
|
|
33
|
+
logoHeight: box("logo-height")
|
|
34
|
+
}
|
|
35
|
+
};
|
|
20
36
|
var GNB_MENU_WIDTH = 240;
|
|
21
37
|
var SLayoutContext = createContext(null);
|
|
22
38
|
forwardRef(function SLayout2({
|
|
23
39
|
type = "box",
|
|
40
|
+
header = "fix",
|
|
24
41
|
folded,
|
|
25
42
|
defaultFolded = false,
|
|
26
43
|
onFoldedChange,
|
|
@@ -40,22 +57,23 @@ forwardRef(function SLayout2({
|
|
|
40
57
|
[isControlled, onFoldedChange]
|
|
41
58
|
);
|
|
42
59
|
const ctx = useMemo(
|
|
43
|
-
() => ({ type, folded: currentFolded, setFolded }),
|
|
44
|
-
[type, currentFolded, setFolded]
|
|
60
|
+
() => ({ type, header, folded: currentFolded, setFolded }),
|
|
61
|
+
[type, header, currentFolded, setFolded]
|
|
45
62
|
);
|
|
46
|
-
const
|
|
47
|
-
const frameStyle =
|
|
63
|
+
const isFullHeader = header === "full";
|
|
64
|
+
const frameStyle = isFullHeader ? {
|
|
48
65
|
gridTemplateColumns: `${currentFolded ? 0 : GNB_MENU_WIDTH}px 1fr`,
|
|
49
|
-
gridTemplateRows: `${
|
|
66
|
+
gridTemplateRows: `${GNB_HEADER[header].topHeight} 1fr`
|
|
50
67
|
} : void 0;
|
|
51
68
|
return /* @__PURE__ */ jsx(SLayoutContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx(
|
|
52
69
|
"div",
|
|
53
70
|
{
|
|
54
71
|
ref,
|
|
55
72
|
"data-type": type,
|
|
73
|
+
"data-header": header,
|
|
56
74
|
className: cn(
|
|
57
75
|
"box-border h-full overflow-hidden",
|
|
58
|
-
|
|
76
|
+
isFullHeader ? "grid" : "flex flex-row",
|
|
59
77
|
className
|
|
60
78
|
),
|
|
61
79
|
style: { ...frameStyle, ...style },
|
|
@@ -66,7 +84,7 @@ forwardRef(function SLayout2({
|
|
|
66
84
|
});
|
|
67
85
|
var SPage = forwardRef(function SPage2({ className, style, children, ...rest }, ref) {
|
|
68
86
|
const layout = useContext(SLayoutContext);
|
|
69
|
-
const gridStyle = layout?.
|
|
87
|
+
const gridStyle = layout?.header === "full" ? { gridColumn: 2, gridRow: 2 } : void 0;
|
|
70
88
|
return /* @__PURE__ */ jsx(
|
|
71
89
|
"main",
|
|
72
90
|
{
|
|
@@ -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 { SIconName } from '../SIcon';\n\n/** 구조(레이아웃): box = 라운드/좁은 들여쓰기, belt = 풀폭/넓은 들여쓰기 */\nexport type SGnbType = 'box' | 'belt';\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 /** 비활성 여부 */\n disabled?: boolean;\n /** 하위 아이템 (있으면 펼침/접힘 대상) */\n children?: SGnbMenuItem[];\n}\n\n// theme.css 의 컴포넌트 토큰 참조 (디자인팀 소유, 직접 수정 금지)\n// 치수(type 축)는 gnbBox/gnbBelt 토큰, 색(color 축)은 gnb light/dark 토큰(--cmp-light-*/--cmp-dark-*).\nconst belt = (p: string) => `var(--cmp-gnbBelt-${p})`;\nconst box = (p: string) => `var(--cmp-gnbBox-${p})`;\nconst light = (p: string) => `var(--cmp-light-${p})`;\nconst dark = (p: string) => `var(--cmp-dark-${p})`;\n\n/** 레이아웃(type 축). box → --cmp-gnbBox-*, belt → --cmp-gnbBelt-* */\nexport const GNB_LAYOUT: Record<\n SGnbType,\n {\n topPaddingX: string;\n topGap: string;\n bodyPadding: 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 // 사용자 명명: type=\"box\" → gnbBox 토큰(둥금), type=\"belt\" → gnbBelt 토큰(각짐)\n box: {\n topPaddingX: box('paddingX'),\n topGap: box('gap'), // 상단바 gap (버튼 간 세부 gap 은 --cmp-gnbBox-button-gap)\n bodyPadding: box('menu-body-padding'),\n itemPaddingRight: box('item-paddingRight'),\n itemGap: box('item-gap'),\n itemRadius: box('item-radius'),\n itemArrow: box('item-arrow'),\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: gnbBox.item.typography.bold.fontSize\n depth1LineHeight: 24, // token: gnbBox.item.typography.bold.lineHeight\n depth1FontWeight: 700,\n itemFontSize: 14, // token: gnbBox.item.typography.default.fontSize\n itemLineHeight: 24, // token: gnbBox.item.typography.default.lineHeight\n },\n belt: {\n topPaddingX: belt('top-paddingX'),\n topGap: belt('top-gap'),\n bodyPadding: belt('body-paddingY'),\n itemPaddingRight: belt('item-paddingRight'),\n itemGap: belt('item-gap'),\n itemRadius: '0px', // belt 토큰에 radius 없음 → 풀폭 행\n itemArrow: belt('item-icon'),\n depth1PaddingLeft: belt('item-depth1-paddingLeft'),\n depth2PaddingLeft: belt('item-depth2-paddingLeft'),\n depth3PaddingLeft: belt('item-depth3-paddingLeft'),\n depth1Icon: box('item-depth1-icon'), // belt 토큰에 depth1-icon 없음 → box 값(20) 공유\n depth1FontSize: 13, // token: gnbBelt.item.depth1.typography.fontSize\n depth1LineHeight: 22, // token: gnbBelt.item.depth1.typography.lineHeight\n depth1FontWeight: 700,\n itemFontSize: 12, // token: gnbBelt.item.typography.default.fontSize\n itemLineHeight: 20, // token: gnbBelt.item.typography.default.lineHeight\n },\n};\n\n/** 색상(color 축). light → --cmp-light-* 색, dark → --cmp-dark-* 색 (gnb colortype 토큰) */\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 itemColorActive: 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 itemColorActive: light('item-fg-selected'),\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 itemColorActive: dark('item-fg-selected'),\n },\n};\n\n/** 구조와 무관한 공통 레이아웃 */\nexport const GNB_COMMON = {\n topHeight: box('top-height'), // 56 (box=belt 동일)\n logoHeight: box('logo-height'), // 32\n itemPaddingY: box('item-paddingY'), // 6\n foldWidth: belt('fold-width'), // 48 (belt 토큰 공유)\n};\n\n/**\n * 메뉴 고정 폭. belt/box 모두 메뉴는 이 폭으로 좌측에 붙는다(토큰 없음 → 상수).\n * box 에서 SLayout 이 \"메뉴 오른쪽 = 페이지\" 영역을 잡을 때도 이 값을 쓴다.\n */\nexport const GNB_MENU_WIDTH = 240;\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 { GNB_COMMON, GNB_MENU_WIDTH, type SGnbType } from '../SGnb/gnb.config';\n\n/** 레이아웃 구조: box(풀폭 상단바 + 아래에 메뉴|페이지) / belt(좌측 레일 + 페이지 가로 분할) */\nexport type SLayoutType = SGnbType;\n\nexport interface SLayoutContextValue {\n /** 레이아웃 구조 — 자식 SGnb 가 배치·스타일을 맞추는 기준 */\n type: SLayoutType;\n /** GNB 접힘 상태 */\n folded: boolean;\n /** 접힘 상태 변경 */\n setFolded: (folded: boolean) => void;\n}\n\nexport const SLayoutContext = createContext<SLayoutContextValue | null>(null);\n\n/**\n * SLayout 의 상태를 읽고 제어한다. `<SLayout>` 밖에서 호출하면 throw.\n * 외부 버튼으로 GNB 를 접거나, 현재 구조(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 /** 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 * type 하나로 배치를 결정하고 overflow:hidden 으로 스크롤 경계를 만든다.\n * 자식 SGnb·SPage 는 context 에서 type·folded 를 읽으므로 앱은 type 을 여기 한 곳에만 쓴다.\n * 높이는 기본 100%(부모를 채움) — 앱 루트로 쓸 땐 부모를 뷰포트 높이로 잡는다.\n *\n * - belt: [GNB(자기 폭 소유) | 페이지] 가로 flex.\n * - box: 상단바가 풀폭이고 그 아래에 [메뉴 | 페이지] 가 나란히 서야 하는데, 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 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 const ctx = useMemo<SLayoutContextValue>(\n () => ({ type, folded: currentFolded, setFolded }),\n [type, currentFolded, setFolded],\n );\n\n const isBox = type === 'box';\n const frameStyle: CSSProperties | undefined = isBox\n ? {\n gridTemplateColumns: `${currentFolded ? 0 : GNB_MENU_WIDTH}px 1fr`,\n gridTemplateRows: `${GNB_COMMON.topHeight} 1fr`,\n }\n : undefined;\n\n return (\n <SLayoutContext.Provider value={ctx}>\n <div\n ref={ref}\n data-type={type}\n className={cn(\n 'box-border h-full overflow-hidden',\n isBox ? '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 * - belt: flex 자식으로 남은 가로 공간을 채운다.\n * - box: 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?.type === 'box' ? { 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-24', className)}\n style={{ ...gridStyle, ...style }}\n {...rest}\n >\n {children}\n </main>\n );\n});\n"]}
|
|
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 { 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// 토큰은 gnbBox/gnbBelt 두 벌뿐이라 축 이름과 1:1 로 맞지 않는다:\n// 상단바(header 축) fix → gnbBelt-top-*, full → gnbBox-*\n// 메뉴(type 축) box → gnbBox-*, belt → gnbBelt-*\n// 색(color 축)은 --cmp-light-* / --cmp-dark-*.\nconst belt = (p: string) => `var(--cmp-gnbBelt-${p})`;\nconst box = (p: string) => `var(--cmp-gnbBox-${p})`;\nconst light = (p: string) => `var(--cmp-light-${p})`;\nconst dark = (p: string) => `var(--cmp-dark-${p})`;\n\n/**\n * 상단바(header 축). fix → gnbBelt-top-* 토큰, full → gnbBox-* 토큰.\n * 배치까지 이 축이 정한다 — fix 는 GNB 컬럼 안, full 은 레이아웃 전폭.\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: {\n topHeight: belt('top-height'),\n topPaddingX: belt('top-paddingX'),\n topGap: belt('top-gap'),\n logoHeight: belt('logo-height'),\n },\n full: {\n topHeight: box('top-height'),\n topPaddingX: box('paddingX'),\n topGap: box('gap'), // 상단바 gap (버튼 간 세부 gap 은 --cmp-gnbBox-button-gap)\n logoHeight: box('logo-height'),\n },\n};\n\n/** 메뉴(type 축). box → --cmp-gnbBox-*, belt → --cmp-gnbBelt-* */\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 // menu.body.padding 은 전방향 토큰 → 세로 패딩과 아이템 좌우 여백 양쪽에 쓴다\n bodyPaddingY: box('menu-body-padding'),\n itemMarginX: box('menu-body-padding'),\n itemPaddingY: box('item-paddingY'),\n itemPaddingRight: box('item-paddingRight'),\n itemGap: box('item-gap'),\n itemRadius: box('item-radius'),\n itemArrow: box('item-arrow'),\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: gnbBox.item.typography.bold.fontSize\n depth1LineHeight: 24, // token: gnbBox.item.typography.bold.lineHeight\n depth1FontWeight: 700,\n itemFontSize: 14, // token: gnbBox.item.typography.default.fontSize\n itemLineHeight: 24, // token: gnbBox.item.typography.default.lineHeight\n },\n belt: {\n bodyPaddingY: belt('body-paddingY'),\n itemMarginX: '0px', // belt 토큰에 가로 여백 없음 → 풀폭 행 (들여쓰기는 depth*-paddingLeft 전담)\n itemPaddingY: belt('item-paddingY'),\n itemPaddingRight: belt('item-paddingRight'),\n itemGap: belt('item-gap'),\n itemRadius: '0px', // belt 토큰에 radius 없음 → 풀폭 행\n itemArrow: belt('item-icon'),\n depth1PaddingLeft: belt('item-depth1-paddingLeft'),\n depth2PaddingLeft: belt('item-depth2-paddingLeft'),\n depth3PaddingLeft: belt('item-depth3-paddingLeft'),\n depth1Icon: box('item-depth1-icon'), // belt 토큰에 depth1-icon 없음 → box 값(20) 공유\n depth1FontSize: 13, // token: gnbBelt.item.depth1.typography.fontSize\n depth1LineHeight: 22, // token: gnbBelt.item.depth1.typography.lineHeight\n depth1FontWeight: 700,\n itemFontSize: 12, // token: gnbBelt.item.typography.default.fontSize\n itemLineHeight: 20, // token: gnbBelt.item.typography.default.lineHeight\n },\n};\n\n/**\n * 색상(color 축). light → --cmp-light-* 색, dark → --cmp-dark-* 색 (gnb colortype 토큰)\n * 상단바 아이콘 색(--cmp-*-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 }\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 },\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 },\n};\n\n/** 축과 무관한 공통 레이아웃. */\nexport const GNB_COMMON = {\n /** 접힘 레일 폭. header=\"full\" 은 접으면 메뉴만 사라져 레일이 없으므로 fix 전용. */\n foldWidth: belt('fold-width'),\n /**\n * 상단바 아이콘 버튼 사이 간격. fix 레일(세로 스택)의 gap·상하 여백에 쓴다.\n * 레일 전용 여백 토큰이 따로 없어 같은 값(8px)인 이 토큰으로 재해석했다.\n */\n buttonGap: box('button-gap'),\n};\n\n/**\n * 메뉴 고정 폭. 두 header 모두 메뉴는 이 폭으로 좌측에 붙는다(토큰 없음 → 상수).\n * header=\"full\" 에서 SLayout 이 \"메뉴 오른쪽 = 페이지\" 영역을 잡을 때도 이 값을 쓴다.\n */\nexport const GNB_MENU_WIDTH = 240;\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 { GNB_HEADER, GNB_MENU_WIDTH, type SGnbHeader, type SGnbType } 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 /** 상단바 구조 — 자식 SGnb·SPage 가 배치를 맞추는 기준 */\n header: SLayoutHeader;\n /** GNB 접힘 상태 */\n folded: boolean;\n /** 접힘 상태 변경 */\n setFolded: (folded: 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 /** 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 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 const ctx = useMemo<SLayoutContextValue>(\n () => ({ type, header, folded: currentFolded, setFolded }),\n [type, header, currentFolded, setFolded],\n );\n\n const isFullHeader = header === 'full';\n const frameStyle: CSSProperties | undefined = isFullHeader\n ? {\n gridTemplateColumns: `${currentFolded ? 0 : GNB_MENU_WIDTH}px 1fr`,\n gridTemplateRows: `${GNB_HEADER[header].topHeight} 1fr`,\n }\n : undefined;\n\n return (\n <SLayoutContext.Provider value={ctx}>\n <div\n ref={ref}\n data-type={type}\n data-header={header}\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-24', className)}\n style={{ ...gridStyle, ...style }}\n {...rest}\n >\n {children}\n </main>\n );\n});\n"]}
|
|
@@ -2995,6 +2995,7 @@ function SGhostButton({
|
|
|
2995
2995
|
size = "sm",
|
|
2996
2996
|
intent = "default",
|
|
2997
2997
|
ariaLabel = "",
|
|
2998
|
+
ariaPressed,
|
|
2998
2999
|
tooltipText,
|
|
2999
3000
|
disabled = false,
|
|
3000
3001
|
onClick,
|
|
@@ -3031,6 +3032,7 @@ function SGhostButton({
|
|
|
3031
3032
|
disabled,
|
|
3032
3033
|
"aria-disabled": disabled ? "true" : void 0,
|
|
3033
3034
|
"aria-label": ariaLabel.trim() || void 0,
|
|
3035
|
+
"aria-pressed": ariaPressed,
|
|
3034
3036
|
tabIndex: disabled ? -1 : void 0,
|
|
3035
3037
|
onClick: (e) => {
|
|
3036
3038
|
if (disabled) {
|