sellmate-design-system-react 0.1.10 → 0.2.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.
Files changed (163) hide show
  1. package/dist/components/SActionModal/SActionModal.cjs +10 -4
  2. package/dist/components/SActionModal/SActionModal.cjs.map +1 -1
  3. package/dist/components/SActionModal/SActionModal.js +10 -4
  4. package/dist/components/SActionModal/SActionModal.js.map +1 -1
  5. package/dist/components/SBarcodeInput/SBarcodeInput.cjs +22 -5
  6. package/dist/components/SBarcodeInput/SBarcodeInput.cjs.map +1 -1
  7. package/dist/components/SBarcodeInput/SBarcodeInput.d.ts +3 -1
  8. package/dist/components/SBarcodeInput/SBarcodeInput.js +22 -5
  9. package/dist/components/SBarcodeInput/SBarcodeInput.js.map +1 -1
  10. package/dist/components/SCalendar/SCalendar.cjs +9 -3
  11. package/dist/components/SCalendar/SCalendar.cjs.map +1 -1
  12. package/dist/components/SCalendar/SCalendar.js +9 -3
  13. package/dist/components/SCalendar/SCalendar.js.map +1 -1
  14. package/dist/components/SChip/SChip.cjs +9 -3
  15. package/dist/components/SChip/SChip.cjs.map +1 -1
  16. package/dist/components/SChip/SChip.js +9 -3
  17. package/dist/components/SChip/SChip.js.map +1 -1
  18. package/dist/components/SChipInput/SChipInput.cjs +44 -13
  19. package/dist/components/SChipInput/SChipInput.cjs.map +1 -1
  20. package/dist/components/SChipInput/SChipInput.d.ts +5 -0
  21. package/dist/components/SChipInput/SChipInput.js +44 -13
  22. package/dist/components/SChipInput/SChipInput.js.map +1 -1
  23. package/dist/components/SChipInput/index.d.ts +1 -1
  24. package/dist/components/SCircleProgress/SCircleProgress.cjs +6 -2
  25. package/dist/components/SCircleProgress/SCircleProgress.cjs.map +1 -1
  26. package/dist/components/SCircleProgress/SCircleProgress.js +6 -2
  27. package/dist/components/SCircleProgress/SCircleProgress.js.map +1 -1
  28. package/dist/components/SConfirmModal/SConfirmModal.cjs +20 -13
  29. package/dist/components/SConfirmModal/SConfirmModal.cjs.map +1 -1
  30. package/dist/components/SConfirmModal/SConfirmModal.d.ts +7 -4
  31. package/dist/components/SConfirmModal/SConfirmModal.js +20 -13
  32. package/dist/components/SConfirmModal/SConfirmModal.js.map +1 -1
  33. package/dist/components/SDatePicker/SDatePicker.cjs +22 -5
  34. package/dist/components/SDatePicker/SDatePicker.cjs.map +1 -1
  35. package/dist/components/SDatePicker/SDatePicker.d.ts +3 -1
  36. package/dist/components/SDatePicker/SDatePicker.js +22 -5
  37. package/dist/components/SDatePicker/SDatePicker.js.map +1 -1
  38. package/dist/components/SDateRangePicker/SDateRangePicker.cjs +22 -5
  39. package/dist/components/SDateRangePicker/SDateRangePicker.cjs.map +1 -1
  40. package/dist/components/SDateRangePicker/SDateRangePicker.d.ts +3 -1
  41. package/dist/components/SDateRangePicker/SDateRangePicker.js +22 -5
  42. package/dist/components/SDateRangePicker/SDateRangePicker.js.map +1 -1
  43. package/dist/components/SDropdownButton/SDropdownButton.cjs +3 -1
  44. package/dist/components/SDropdownButton/SDropdownButton.cjs.map +1 -1
  45. package/dist/components/SDropdownButton/SDropdownButton.js +3 -1
  46. package/dist/components/SDropdownButton/SDropdownButton.js.map +1 -1
  47. package/dist/components/SField/SField.cjs +20 -5
  48. package/dist/components/SField/SField.cjs.map +1 -1
  49. package/dist/components/SField/SField.d.ts +4 -1
  50. package/dist/components/SField/SField.js +20 -5
  51. package/dist/components/SField/SField.js.map +1 -1
  52. package/dist/components/SFilePicker/SFilePicker.cjs +23 -6
  53. package/dist/components/SFilePicker/SFilePicker.cjs.map +1 -1
  54. package/dist/components/SFilePicker/SFilePicker.d.ts +3 -1
  55. package/dist/components/SFilePicker/SFilePicker.js +23 -6
  56. package/dist/components/SFilePicker/SFilePicker.js.map +1 -1
  57. package/dist/components/SGhostButton/SGhostButton.cjs +9 -3
  58. package/dist/components/SGhostButton/SGhostButton.cjs.map +1 -1
  59. package/dist/components/SGhostButton/SGhostButton.d.ts +5 -1
  60. package/dist/components/SGhostButton/SGhostButton.js +9 -3
  61. package/dist/components/SGhostButton/SGhostButton.js.map +1 -1
  62. package/dist/components/SGnb/SGnb.cjs +3297 -0
  63. package/dist/components/SGnb/SGnb.cjs.map +1 -0
  64. package/dist/components/SGnb/SGnb.d.ts +35 -0
  65. package/dist/components/SGnb/SGnb.js +3295 -0
  66. package/dist/components/SGnb/SGnb.js.map +1 -0
  67. package/dist/components/SGnb/gnb.config.d.ts +63 -0
  68. package/dist/components/SGnb/index.d.ts +2 -0
  69. package/dist/components/SGuide/SGuide.cjs +9 -3
  70. package/dist/components/SGuide/SGuide.cjs.map +1 -1
  71. package/dist/components/SGuide/SGuide.js +9 -3
  72. package/dist/components/SGuide/SGuide.js.map +1 -1
  73. package/dist/components/SInput/SInput.cjs +22 -5
  74. package/dist/components/SInput/SInput.cjs.map +1 -1
  75. package/dist/components/SInput/SInput.d.ts +4 -1
  76. package/dist/components/SInput/SInput.js +22 -5
  77. package/dist/components/SInput/SInput.js.map +1 -1
  78. package/dist/components/SKeyValueTable/SKeyValueTable.cjs +511 -209
  79. package/dist/components/SKeyValueTable/SKeyValueTable.cjs.map +1 -1
  80. package/dist/components/SKeyValueTable/SKeyValueTable.d.ts +27 -6
  81. package/dist/components/SKeyValueTable/SKeyValueTable.js +510 -208
  82. package/dist/components/SKeyValueTable/SKeyValueTable.js.map +1 -1
  83. package/dist/components/SLayout/SLayout.cjs +79 -0
  84. package/dist/components/SLayout/SLayout.cjs.map +1 -0
  85. package/dist/components/SLayout/SLayout.d.ts +41 -0
  86. package/dist/components/SLayout/SLayout.js +75 -0
  87. package/dist/components/SLayout/SLayout.js.map +1 -0
  88. package/dist/components/SLayout/index.d.ts +1 -0
  89. package/dist/components/SLoadingContainer/SLoadingContainer.cjs +6 -2
  90. package/dist/components/SLoadingContainer/SLoadingContainer.cjs.map +1 -1
  91. package/dist/components/SLoadingContainer/SLoadingContainer.js +6 -2
  92. package/dist/components/SLoadingContainer/SLoadingContainer.js.map +1 -1
  93. package/dist/components/SLoadingContainer/loading.cjs +6 -2
  94. package/dist/components/SLoadingContainer/loading.cjs.map +1 -1
  95. package/dist/components/SLoadingContainer/loading.js +6 -2
  96. package/dist/components/SLoadingContainer/loading.js.map +1 -1
  97. package/dist/components/SLoadingModal/SLoadingModal.cjs +16 -6
  98. package/dist/components/SLoadingModal/SLoadingModal.cjs.map +1 -1
  99. package/dist/components/SLoadingModal/SLoadingModal.js +16 -6
  100. package/dist/components/SLoadingModal/SLoadingModal.js.map +1 -1
  101. package/dist/components/SModalContainer/SModalContainer.cjs +10 -4
  102. package/dist/components/SModalContainer/SModalContainer.cjs.map +1 -1
  103. package/dist/components/SModalContainer/SModalContainer.d.ts +10 -0
  104. package/dist/components/SModalContainer/SModalContainer.js +10 -4
  105. package/dist/components/SModalContainer/SModalContainer.js.map +1 -1
  106. package/dist/components/SNumberInput/SNumberInput.cjs +22 -5
  107. package/dist/components/SNumberInput/SNumberInput.cjs.map +1 -1
  108. package/dist/components/SNumberInput/SNumberInput.d.ts +2 -0
  109. package/dist/components/SNumberInput/SNumberInput.js +22 -5
  110. package/dist/components/SNumberInput/SNumberInput.js.map +1 -1
  111. package/dist/components/SPage/SPage.cjs +86 -0
  112. package/dist/components/SPage/SPage.cjs.map +1 -0
  113. package/dist/components/SPage/SPage.d.ts +13 -0
  114. package/dist/components/SPage/SPage.js +84 -0
  115. package/dist/components/SPage/SPage.js.map +1 -0
  116. package/dist/components/SPage/index.d.ts +1 -0
  117. package/dist/components/SPopover/SPopover.cjs +9 -3
  118. package/dist/components/SPopover/SPopover.cjs.map +1 -1
  119. package/dist/components/SPopover/SPopover.js +9 -3
  120. package/dist/components/SPopover/SPopover.js.map +1 -1
  121. package/dist/components/SPortal/SPortal.cjs +3 -1
  122. package/dist/components/SPortal/SPortal.cjs.map +1 -1
  123. package/dist/components/SPortal/SPortal.js +3 -1
  124. package/dist/components/SPortal/SPortal.js.map +1 -1
  125. package/dist/components/SSelect/SSelect.cjs +545 -265
  126. package/dist/components/SSelect/SSelect.cjs.map +1 -1
  127. package/dist/components/SSelect/SSelect.d.ts +4 -2
  128. package/dist/components/SSelect/SSelect.js +546 -265
  129. package/dist/components/SSelect/SSelect.js.map +1 -1
  130. package/dist/components/STable/STable.cjs +552 -268
  131. package/dist/components/STable/STable.cjs.map +1 -1
  132. package/dist/components/STable/STable.js +553 -268
  133. package/dist/components/STable/STable.js.map +1 -1
  134. package/dist/components/STextLink/STextLink.cjs +2 -0
  135. package/dist/components/STextLink/STextLink.cjs.map +1 -1
  136. package/dist/components/STextLink/STextLink.js +2 -0
  137. package/dist/components/STextLink/STextLink.js.map +1 -1
  138. package/dist/components/STextarea/STextarea.cjs +22 -5
  139. package/dist/components/STextarea/STextarea.cjs.map +1 -1
  140. package/dist/components/STextarea/STextarea.d.ts +2 -0
  141. package/dist/components/STextarea/STextarea.js +22 -5
  142. package/dist/components/STextarea/STextarea.js.map +1 -1
  143. package/dist/components/SToast/SToast.cjs +9 -3
  144. package/dist/components/SToast/SToast.cjs.map +1 -1
  145. package/dist/components/SToast/SToast.js +9 -3
  146. package/dist/components/SToast/SToast.js.map +1 -1
  147. package/dist/components/SToast/SToastContainer.cjs +9 -3
  148. package/dist/components/SToast/SToastContainer.cjs.map +1 -1
  149. package/dist/components/SToast/SToastContainer.js +9 -3
  150. package/dist/components/SToast/SToastContainer.js.map +1 -1
  151. package/dist/components/STooltip/STooltip.cjs +9 -3
  152. package/dist/components/STooltip/STooltip.cjs.map +1 -1
  153. package/dist/components/STooltip/STooltip.js +9 -3
  154. package/dist/components/STooltip/STooltip.js.map +1 -1
  155. package/dist/index.cjs +840 -199
  156. package/dist/index.cjs.map +1 -1
  157. package/dist/index.d.ts +3 -1
  158. package/dist/index.js +832 -200
  159. package/dist/index.js.map +1 -1
  160. package/dist/lib/portal-container.d.ts +7 -0
  161. package/dist/styles.css +253 -63
  162. package/dist/theme.css +66 -13
  163. package/package.json +1 -1
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var clsx = require('clsx');
5
+ var tailwindMerge = require('tailwind-merge');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+
8
+ // src/components/SPage/SPage.tsx
9
+ var twMerge = tailwindMerge.extendTailwindMerge({
10
+ extend: {
11
+ classGroups: {
12
+ "font-size": [{ text: ["10", "11", "12", "13", "14", "16", "18", "20", "24", "28", "32"] }]
13
+ }
14
+ }
15
+ });
16
+ function cn(...inputs) {
17
+ return twMerge(clsx.clsx(inputs));
18
+ }
19
+ var box = (p) => `var(--cmp-gnbBox-${p})`;
20
+ var GNB_COMMON = {
21
+ topHeight: box("top-height")};
22
+ var GNB_MENU_WIDTH = 240;
23
+ var SLayoutContext = react.createContext(null);
24
+ react.forwardRef(function SLayout2({
25
+ type = "box",
26
+ folded,
27
+ defaultFolded = false,
28
+ onFoldedChange,
29
+ className,
30
+ style,
31
+ children,
32
+ ...rest
33
+ }, ref) {
34
+ const [uncontrolledFolded, setUncontrolledFolded] = react.useState(defaultFolded);
35
+ const isControlled = folded !== void 0;
36
+ const currentFolded = isControlled ? folded : uncontrolledFolded;
37
+ const setFolded = react.useCallback(
38
+ (next) => {
39
+ if (!isControlled) setUncontrolledFolded(next);
40
+ onFoldedChange?.(next);
41
+ },
42
+ [isControlled, onFoldedChange]
43
+ );
44
+ const ctx = react.useMemo(
45
+ () => ({ type, folded: currentFolded, setFolded }),
46
+ [type, currentFolded, setFolded]
47
+ );
48
+ const isBox = type === "box";
49
+ const frameStyle = isBox ? {
50
+ gridTemplateColumns: `${currentFolded ? 0 : GNB_MENU_WIDTH}px 1fr`,
51
+ gridTemplateRows: `${GNB_COMMON.topHeight} 1fr`
52
+ } : void 0;
53
+ return /* @__PURE__ */ jsxRuntime.jsx(SLayoutContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxRuntime.jsx(
54
+ "div",
55
+ {
56
+ ref,
57
+ "data-type": type,
58
+ className: cn(
59
+ "box-border h-full overflow-hidden",
60
+ isBox ? "grid" : "flex flex-row",
61
+ className
62
+ ),
63
+ style: { ...frameStyle, ...style },
64
+ ...rest,
65
+ children
66
+ }
67
+ ) });
68
+ });
69
+ var SPage = react.forwardRef(function SPage2({ className, style, children, ...rest }, ref) {
70
+ const layout = react.useContext(SLayoutContext);
71
+ const gridStyle = layout?.type === "box" ? { gridColumn: 2, gridRow: 2 } : void 0;
72
+ return /* @__PURE__ */ jsxRuntime.jsx(
73
+ "main",
74
+ {
75
+ ref,
76
+ className: cn("box-border min-h-0 min-w-0 flex-1 overflow-auto p-24", className),
77
+ style: { ...gridStyle, ...style },
78
+ ...rest,
79
+ children
80
+ }
81
+ );
82
+ });
83
+
84
+ exports.SPage = SPage;
85
+ //# sourceMappingURL=SPage.cjs.map
86
+ //# sourceMappingURL=SPage.cjs.map
@@ -0,0 +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"]}
@@ -0,0 +1,13 @@
1
+ import { type HTMLAttributes } from 'react';
2
+ export interface SPageProps extends HTMLAttributes<HTMLElement> {
3
+ }
4
+ /**
5
+ * SPage — SLayout 안에서 남은 공간을 채우는 스크롤 콘텐츠 영역.
6
+ * 스크롤은 SLayout(overflow:hidden)이 잡아둔 경계 안에서 이 영역에만 생긴다.
7
+ * 패딩·배경은 className 으로 덮어쓸 수 있다.
8
+ *
9
+ * - belt: flex 자식으로 남은 가로 공간을 채운다.
10
+ * - box: SLayout 이 깐 grid 의 (2행,2열) = 상단바 아래·메뉴 오른쪽 자리를 차지한다.
11
+ * (SGnb 가 전 셀에 겹쳐 있지만 그 영역은 투명해서 SPage 가 보인다)
12
+ */
13
+ export declare const SPage: import("react").ForwardRefExoticComponent<SPageProps & import("react").RefAttributes<HTMLElement>>;
@@ -0,0 +1,84 @@
1
+ import { createContext, forwardRef, useState, useCallback, useMemo, useContext } from 'react';
2
+ import { clsx } from 'clsx';
3
+ import { extendTailwindMerge } from 'tailwind-merge';
4
+ import { jsx } from 'react/jsx-runtime';
5
+
6
+ // src/components/SPage/SPage.tsx
7
+ var twMerge = extendTailwindMerge({
8
+ extend: {
9
+ classGroups: {
10
+ "font-size": [{ text: ["10", "11", "12", "13", "14", "16", "18", "20", "24", "28", "32"] }]
11
+ }
12
+ }
13
+ });
14
+ function cn(...inputs) {
15
+ return twMerge(clsx(inputs));
16
+ }
17
+ var box = (p) => `var(--cmp-gnbBox-${p})`;
18
+ var GNB_COMMON = {
19
+ topHeight: box("top-height")};
20
+ var GNB_MENU_WIDTH = 240;
21
+ var SLayoutContext = createContext(null);
22
+ forwardRef(function SLayout2({
23
+ type = "box",
24
+ folded,
25
+ defaultFolded = false,
26
+ onFoldedChange,
27
+ className,
28
+ style,
29
+ children,
30
+ ...rest
31
+ }, ref) {
32
+ const [uncontrolledFolded, setUncontrolledFolded] = useState(defaultFolded);
33
+ const isControlled = folded !== void 0;
34
+ const currentFolded = isControlled ? folded : uncontrolledFolded;
35
+ const setFolded = useCallback(
36
+ (next) => {
37
+ if (!isControlled) setUncontrolledFolded(next);
38
+ onFoldedChange?.(next);
39
+ },
40
+ [isControlled, onFoldedChange]
41
+ );
42
+ const ctx = useMemo(
43
+ () => ({ type, folded: currentFolded, setFolded }),
44
+ [type, currentFolded, setFolded]
45
+ );
46
+ const isBox = type === "box";
47
+ const frameStyle = isBox ? {
48
+ gridTemplateColumns: `${currentFolded ? 0 : GNB_MENU_WIDTH}px 1fr`,
49
+ gridTemplateRows: `${GNB_COMMON.topHeight} 1fr`
50
+ } : void 0;
51
+ return /* @__PURE__ */ jsx(SLayoutContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx(
52
+ "div",
53
+ {
54
+ ref,
55
+ "data-type": type,
56
+ className: cn(
57
+ "box-border h-full overflow-hidden",
58
+ isBox ? "grid" : "flex flex-row",
59
+ className
60
+ ),
61
+ style: { ...frameStyle, ...style },
62
+ ...rest,
63
+ children
64
+ }
65
+ ) });
66
+ });
67
+ var SPage = forwardRef(function SPage2({ className, style, children, ...rest }, ref) {
68
+ const layout = useContext(SLayoutContext);
69
+ const gridStyle = layout?.type === "box" ? { gridColumn: 2, gridRow: 2 } : void 0;
70
+ return /* @__PURE__ */ jsx(
71
+ "main",
72
+ {
73
+ ref,
74
+ className: cn("box-border min-h-0 min-w-0 flex-1 overflow-auto p-24", className),
75
+ style: { ...gridStyle, ...style },
76
+ ...rest,
77
+ children
78
+ }
79
+ );
80
+ });
81
+
82
+ export { SPage };
83
+ //# sourceMappingURL=SPage.js.map
84
+ //# sourceMappingURL=SPage.js.map
@@ -0,0 +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"]}
@@ -0,0 +1 @@
1
+ export { SPage, type SPageProps } from './SPage';
@@ -188,7 +188,9 @@ var FLOATING_SLIDE_ANIM = [
188
188
  "data-[state=closed]:data-[side=right]:animate-floating-out-right"
189
189
  ].join(" ");
190
190
  var PortalContainerContext = react.createContext(null);
191
+ var InsideModalContext = react.createContext(false);
191
192
  PortalContainerContext.Provider;
193
+ InsideModalContext.Provider;
192
194
  function usePortalContainer() {
193
195
  return react.useContext(PortalContainerContext) ?? void 0;
194
196
  }
@@ -2996,6 +2998,8 @@ function SGhostButton({
2996
2998
  tooltipText,
2997
2999
  disabled = false,
2998
3000
  onClick,
3001
+ anchorClassName,
3002
+ anchorStyle,
2999
3003
  className,
3000
3004
  style
3001
3005
  }) {
@@ -3018,6 +3022,8 @@ function SGhostButton({
3018
3022
  };
3019
3023
  const s = SIZE[size];
3020
3024
  const contentColor = disabled ? intent === "inverse" ? "var(--cmp-ghostButton-inverse-disabled)" : "var(--cmp-ghostButton-icon-disabled)" : `var(--cmp-ghostButton-${intent}-content)`;
3025
+ const text = (tooltipText ?? "").trim();
3026
+ const hasTooltip = text !== "";
3021
3027
  const btn = /* @__PURE__ */ jsxRuntime.jsxs(
3022
3028
  "button",
3023
3029
  {
@@ -3058,13 +3064,13 @@ function SGhostButton({
3058
3064
  ]
3059
3065
  }
3060
3066
  );
3061
- const text = (tooltipText ?? "").trim();
3062
- if (text === "") return btn;
3067
+ if (!hasTooltip) return btn;
3063
3068
  return /* @__PURE__ */ jsxRuntime.jsxs(RPopover2__namespace.Root, { open, onOpenChange: setOpen, children: [
3064
3069
  /* @__PURE__ */ jsxRuntime.jsx(RPopover2__namespace.Anchor, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
3065
3070
  "span",
3066
3071
  {
3067
- className: "inline-flex",
3072
+ className: cn("inline-flex", anchorClassName),
3073
+ style: anchorStyle,
3068
3074
  onMouseEnter: openNow,
3069
3075
  onMouseLeave: startHide,
3070
3076
  onFocus: openNow,