sellmate-design-system-react 0.1.11 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/SActionModal/SActionModal.cjs +9 -3
- package/dist/components/SActionModal/SActionModal.cjs.map +1 -1
- package/dist/components/SActionModal/SActionModal.js +9 -3
- package/dist/components/SActionModal/SActionModal.js.map +1 -1
- package/dist/components/SBarcodeInput/SBarcodeInput.cjs +9 -3
- package/dist/components/SBarcodeInput/SBarcodeInput.cjs.map +1 -1
- package/dist/components/SBarcodeInput/SBarcodeInput.js +9 -3
- package/dist/components/SBarcodeInput/SBarcodeInput.js.map +1 -1
- package/dist/components/SCalendar/SCalendar.cjs +9 -3
- package/dist/components/SCalendar/SCalendar.cjs.map +1 -1
- package/dist/components/SCalendar/SCalendar.js +9 -3
- package/dist/components/SCalendar/SCalendar.js.map +1 -1
- package/dist/components/SChip/SChip.cjs +9 -3
- package/dist/components/SChip/SChip.cjs.map +1 -1
- package/dist/components/SChip/SChip.js +9 -3
- package/dist/components/SChip/SChip.js.map +1 -1
- package/dist/components/SChipInput/SChipInput.cjs +25 -10
- package/dist/components/SChipInput/SChipInput.cjs.map +1 -1
- package/dist/components/SChipInput/SChipInput.js +25 -10
- package/dist/components/SChipInput/SChipInput.js.map +1 -1
- package/dist/components/SCircleProgress/SCircleProgress.cjs +6 -2
- package/dist/components/SCircleProgress/SCircleProgress.cjs.map +1 -1
- package/dist/components/SCircleProgress/SCircleProgress.js +6 -2
- package/dist/components/SCircleProgress/SCircleProgress.js.map +1 -1
- package/dist/components/SConfirmModal/SConfirmModal.cjs +9 -3
- package/dist/components/SConfirmModal/SConfirmModal.cjs.map +1 -1
- package/dist/components/SConfirmModal/SConfirmModal.js +9 -3
- package/dist/components/SConfirmModal/SConfirmModal.js.map +1 -1
- package/dist/components/SDatePicker/SDatePicker.cjs +9 -3
- package/dist/components/SDatePicker/SDatePicker.cjs.map +1 -1
- package/dist/components/SDatePicker/SDatePicker.js +9 -3
- package/dist/components/SDatePicker/SDatePicker.js.map +1 -1
- package/dist/components/SDateRangePicker/SDateRangePicker.cjs +9 -3
- package/dist/components/SDateRangePicker/SDateRangePicker.cjs.map +1 -1
- package/dist/components/SDateRangePicker/SDateRangePicker.js +9 -3
- package/dist/components/SDateRangePicker/SDateRangePicker.js.map +1 -1
- package/dist/components/SDropdownButton/SDropdownButton.cjs +1 -1
- package/dist/components/SDropdownButton/SDropdownButton.cjs.map +1 -1
- package/dist/components/SDropdownButton/SDropdownButton.js +1 -1
- package/dist/components/SDropdownButton/SDropdownButton.js.map +1 -1
- package/dist/components/SField/SField.cjs +9 -3
- package/dist/components/SField/SField.cjs.map +1 -1
- package/dist/components/SField/SField.js +9 -3
- package/dist/components/SField/SField.js.map +1 -1
- package/dist/components/SFilePicker/SFilePicker.cjs +9 -3
- package/dist/components/SFilePicker/SFilePicker.cjs.map +1 -1
- package/dist/components/SFilePicker/SFilePicker.js +9 -3
- package/dist/components/SFilePicker/SFilePicker.js.map +1 -1
- package/dist/components/SGhostButton/SGhostButton.cjs +9 -3
- package/dist/components/SGhostButton/SGhostButton.cjs.map +1 -1
- package/dist/components/SGhostButton/SGhostButton.d.ts +7 -1
- package/dist/components/SGhostButton/SGhostButton.js +9 -3
- package/dist/components/SGhostButton/SGhostButton.js.map +1 -1
- package/dist/components/SGnb/SGnb.cjs +3478 -0
- package/dist/components/SGnb/SGnb.cjs.map +1 -0
- package/dist/components/SGnb/SGnb.d.ts +39 -0
- package/dist/components/SGnb/SGnb.js +3456 -0
- package/dist/components/SGnb/SGnb.js.map +1 -0
- package/dist/components/SGnb/gnb.config.d.ts +87 -0
- package/dist/components/SGnb/index.d.ts +2 -0
- package/dist/components/SGuide/SGuide.cjs +9 -3
- package/dist/components/SGuide/SGuide.cjs.map +1 -1
- package/dist/components/SGuide/SGuide.js +9 -3
- package/dist/components/SGuide/SGuide.js.map +1 -1
- package/dist/components/SInput/SInput.cjs +9 -3
- package/dist/components/SInput/SInput.cjs.map +1 -1
- package/dist/components/SInput/SInput.js +9 -3
- package/dist/components/SInput/SInput.js.map +1 -1
- package/dist/components/SKeyValueTable/SKeyValueTable.cjs +20 -6
- package/dist/components/SKeyValueTable/SKeyValueTable.cjs.map +1 -1
- package/dist/components/SKeyValueTable/SKeyValueTable.js +20 -6
- package/dist/components/SKeyValueTable/SKeyValueTable.js.map +1 -1
- package/dist/components/SLayout/SLayout.cjs +97 -0
- package/dist/components/SLayout/SLayout.cjs.map +1 -0
- package/dist/components/SLayout/SLayout.d.ts +47 -0
- package/dist/components/SLayout/SLayout.js +93 -0
- package/dist/components/SLayout/SLayout.js.map +1 -0
- package/dist/components/SLayout/index.d.ts +1 -0
- package/dist/components/SLoadingContainer/SLoadingContainer.cjs +6 -2
- package/dist/components/SLoadingContainer/SLoadingContainer.cjs.map +1 -1
- package/dist/components/SLoadingContainer/SLoadingContainer.js +6 -2
- package/dist/components/SLoadingContainer/SLoadingContainer.js.map +1 -1
- package/dist/components/SLoadingContainer/loading.cjs +6 -2
- package/dist/components/SLoadingContainer/loading.cjs.map +1 -1
- package/dist/components/SLoadingContainer/loading.js +6 -2
- package/dist/components/SLoadingContainer/loading.js.map +1 -1
- package/dist/components/SLoadingModal/SLoadingModal.cjs +15 -5
- package/dist/components/SLoadingModal/SLoadingModal.cjs.map +1 -1
- package/dist/components/SLoadingModal/SLoadingModal.js +15 -5
- package/dist/components/SLoadingModal/SLoadingModal.js.map +1 -1
- package/dist/components/SModalContainer/SModalContainer.cjs +9 -3
- package/dist/components/SModalContainer/SModalContainer.cjs.map +1 -1
- package/dist/components/SModalContainer/SModalContainer.d.ts +10 -0
- package/dist/components/SModalContainer/SModalContainer.js +9 -3
- package/dist/components/SModalContainer/SModalContainer.js.map +1 -1
- package/dist/components/SNumberInput/SNumberInput.cjs +9 -3
- package/dist/components/SNumberInput/SNumberInput.cjs.map +1 -1
- package/dist/components/SNumberInput/SNumberInput.js +9 -3
- package/dist/components/SNumberInput/SNumberInput.js.map +1 -1
- package/dist/components/SPage/SPage.cjs +104 -0
- package/dist/components/SPage/SPage.cjs.map +1 -0
- package/dist/components/SPage/SPage.d.ts +13 -0
- package/dist/components/SPage/SPage.js +102 -0
- package/dist/components/SPage/SPage.js.map +1 -0
- package/dist/components/SPage/index.d.ts +1 -0
- package/dist/components/SPopover/SPopover.cjs +9 -3
- package/dist/components/SPopover/SPopover.cjs.map +1 -1
- package/dist/components/SPopover/SPopover.js +9 -3
- package/dist/components/SPopover/SPopover.js.map +1 -1
- package/dist/components/SPortal/SPortal.cjs +1 -1
- package/dist/components/SPortal/SPortal.cjs.map +1 -1
- package/dist/components/SPortal/SPortal.js +1 -1
- package/dist/components/SPortal/SPortal.js.map +1 -1
- package/dist/components/SSelect/SSelect.cjs +20 -6
- package/dist/components/SSelect/SSelect.cjs.map +1 -1
- package/dist/components/SSelect/SSelect.js +20 -6
- package/dist/components/SSelect/SSelect.js.map +1 -1
- package/dist/components/STable/STable.cjs +28 -10
- package/dist/components/STable/STable.cjs.map +1 -1
- package/dist/components/STable/STable.js +28 -10
- package/dist/components/STable/STable.js.map +1 -1
- package/dist/components/STextarea/STextarea.cjs +9 -3
- package/dist/components/STextarea/STextarea.cjs.map +1 -1
- package/dist/components/STextarea/STextarea.js +9 -3
- package/dist/components/STextarea/STextarea.js.map +1 -1
- package/dist/components/SToast/SToast.cjs +9 -3
- package/dist/components/SToast/SToast.cjs.map +1 -1
- package/dist/components/SToast/SToast.js +9 -3
- package/dist/components/SToast/SToast.js.map +1 -1
- package/dist/components/SToast/SToastContainer.cjs +9 -3
- package/dist/components/SToast/SToastContainer.cjs.map +1 -1
- package/dist/components/SToast/SToastContainer.js +9 -3
- package/dist/components/SToast/SToastContainer.js.map +1 -1
- package/dist/components/STooltip/STooltip.cjs +9 -3
- package/dist/components/STooltip/STooltip.cjs.map +1 -1
- package/dist/components/STooltip/STooltip.js +9 -3
- package/dist/components/STooltip/STooltip.js.map +1 -1
- package/dist/index.cjs +453 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +444 -19
- package/dist/index.js.map +1 -1
- package/dist/styles.css +530 -26
- package/dist/theme.css +70 -14
- package/package.json +1 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var clsx = require('clsx');
|
|
6
|
+
var tailwindMerge = require('tailwind-merge');
|
|
7
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
8
|
+
|
|
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
|
+
|
|
20
|
+
// src/components/SGnb/gnb.config.ts
|
|
21
|
+
var belt = (p) => `var(--cmp-gnbBelt-${p})`;
|
|
22
|
+
var box = (p) => `var(--cmp-gnbBox-${p})`;
|
|
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
|
+
};
|
|
38
|
+
var GNB_MENU_WIDTH = 240;
|
|
39
|
+
var SLayoutContext = react.createContext(null);
|
|
40
|
+
function useSLayout() {
|
|
41
|
+
const ctx = react.useContext(SLayoutContext);
|
|
42
|
+
if (!ctx) throw new Error("useSLayout \uC740 <SLayout> \uC548\uC5D0\uC11C\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
|
|
43
|
+
return ctx;
|
|
44
|
+
}
|
|
45
|
+
var SLayout = react.forwardRef(function SLayout2({
|
|
46
|
+
type = "box",
|
|
47
|
+
header = "fix",
|
|
48
|
+
folded,
|
|
49
|
+
defaultFolded = false,
|
|
50
|
+
onFoldedChange,
|
|
51
|
+
className,
|
|
52
|
+
style,
|
|
53
|
+
children,
|
|
54
|
+
...rest
|
|
55
|
+
}, ref) {
|
|
56
|
+
const [uncontrolledFolded, setUncontrolledFolded] = react.useState(defaultFolded);
|
|
57
|
+
const isControlled = folded !== void 0;
|
|
58
|
+
const currentFolded = isControlled ? folded : uncontrolledFolded;
|
|
59
|
+
const setFolded = react.useCallback(
|
|
60
|
+
(next) => {
|
|
61
|
+
if (!isControlled) setUncontrolledFolded(next);
|
|
62
|
+
onFoldedChange?.(next);
|
|
63
|
+
},
|
|
64
|
+
[isControlled, onFoldedChange]
|
|
65
|
+
);
|
|
66
|
+
const ctx = react.useMemo(
|
|
67
|
+
() => ({ type, header, folded: currentFolded, setFolded }),
|
|
68
|
+
[type, header, currentFolded, setFolded]
|
|
69
|
+
);
|
|
70
|
+
const isFullHeader = header === "full";
|
|
71
|
+
const frameStyle = isFullHeader ? {
|
|
72
|
+
gridTemplateColumns: `${currentFolded ? 0 : GNB_MENU_WIDTH}px 1fr`,
|
|
73
|
+
gridTemplateRows: `${GNB_HEADER[header].topHeight} 1fr`
|
|
74
|
+
} : void 0;
|
|
75
|
+
return /* @__PURE__ */ jsxRuntime.jsx(SLayoutContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
76
|
+
"div",
|
|
77
|
+
{
|
|
78
|
+
ref,
|
|
79
|
+
"data-type": type,
|
|
80
|
+
"data-header": header,
|
|
81
|
+
className: cn(
|
|
82
|
+
"box-border h-full overflow-hidden",
|
|
83
|
+
isFullHeader ? "grid" : "flex flex-row",
|
|
84
|
+
className
|
|
85
|
+
),
|
|
86
|
+
style: { ...frameStyle, ...style },
|
|
87
|
+
...rest,
|
|
88
|
+
children
|
|
89
|
+
}
|
|
90
|
+
) });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
exports.SLayout = SLayout;
|
|
94
|
+
exports.SLayoutContext = SLayoutContext;
|
|
95
|
+
exports.useSLayout = useSLayout;
|
|
96
|
+
//# sourceMappingURL=SLayout.cjs.map
|
|
97
|
+
//# sourceMappingURL=SLayout.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/cn.ts","../../../src/components/SGnb/gnb.config.ts","../../../src/components/SLayout/SLayout.tsx"],"names":["extendTailwindMerge","clsx","createContext","useContext","forwardRef","SLayout","useState","useCallback","useMemo","jsx"],"mappings":"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","file":"SLayout.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"]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
import { type SGnbHeader, type SGnbType } from '../SGnb/gnb.config';
|
|
3
|
+
/** 메뉴 스타일: box(라운드) / belt(풀폭 행) — 자식 SGnb 의 메뉴 모양 */
|
|
4
|
+
export type SLayoutType = SGnbType;
|
|
5
|
+
/** 레이아웃 구조: fix(좌측 GNB + 페이지 가로 분할) / full(풀폭 상단바 + 아래에 메뉴|페이지) */
|
|
6
|
+
export type SLayoutHeader = SGnbHeader;
|
|
7
|
+
export interface SLayoutContextValue {
|
|
8
|
+
/** 메뉴 스타일 — 자식 SGnb 가 스타일을 맞추는 기준 */
|
|
9
|
+
type: SLayoutType;
|
|
10
|
+
/** 상단바 구조 — 자식 SGnb·SPage 가 배치를 맞추는 기준 */
|
|
11
|
+
header: SLayoutHeader;
|
|
12
|
+
/** GNB 접힘 상태 */
|
|
13
|
+
folded: boolean;
|
|
14
|
+
/** 접힘 상태 변경 */
|
|
15
|
+
setFolded: (folded: boolean) => void;
|
|
16
|
+
}
|
|
17
|
+
export declare const SLayoutContext: import("react").Context<SLayoutContextValue | null>;
|
|
18
|
+
/**
|
|
19
|
+
* SLayout 의 상태를 읽고 제어한다. `<SLayout>` 밖에서 호출하면 throw.
|
|
20
|
+
* 외부 버튼으로 GNB 를 접거나, 현재 구조(header)·메뉴 스타일(type)에 따라 분기할 때 사용한다.
|
|
21
|
+
*/
|
|
22
|
+
export declare function useSLayout(): SLayoutContextValue;
|
|
23
|
+
export interface SLayoutProps extends HTMLAttributes<HTMLDivElement> {
|
|
24
|
+
/** 메뉴 스타일: box(라운드) / belt(풀폭 행). 자식 SGnb 가 이 값을 따른다. */
|
|
25
|
+
type?: SLayoutType;
|
|
26
|
+
/** 레이아웃 구조: fix(가로 분할) / full(풀폭 상단바). 자식 SGnb·SPage 가 이 값을 따른다. */
|
|
27
|
+
header?: SLayoutHeader;
|
|
28
|
+
/** GNB 접힘 상태 (controlled) */
|
|
29
|
+
folded?: boolean;
|
|
30
|
+
/** GNB 초기 접힘 상태 (uncontrolled) */
|
|
31
|
+
defaultFolded?: boolean;
|
|
32
|
+
/** 접힘 상태 변경 */
|
|
33
|
+
onFoldedChange?: (folded: boolean) => void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* SLayout — 앱 셸 프레임 겸 context provider.
|
|
37
|
+
* header 가 배치를 결정하고 overflow:hidden 으로 스크롤 경계를 만든다.
|
|
38
|
+
* 자식 SGnb·SPage 는 context 에서 type·header·folded 를 읽으므로 앱은 이 값들을 여기 한 곳에만 쓴다.
|
|
39
|
+
* 높이는 기본 100%(부모를 채움) — 앱 루트로 쓸 땐 부모를 뷰포트 높이로 잡는다.
|
|
40
|
+
*
|
|
41
|
+
* - header="fix": [GNB(자기 폭 소유) | 페이지] 가로 flex.
|
|
42
|
+
* - header="full": 상단바가 풀폭이고 그 아래에 [메뉴 | 페이지] 가 나란히 서야 하는데, SGnb 가 상단바와
|
|
43
|
+
* 메뉴를 한 엘리먼트로 갖고 있어 사이에 끼워넣을 수 없다. 그래서 grid 로 깔고
|
|
44
|
+
* SGnb 를 전 셀에 겹친 뒤(root 는 투명) SPage 가 (2행,2열) = "메뉴 오른쪽 투명 영역" 을 차지한다.
|
|
45
|
+
* 접으면 메뉴가 사라지므로 1열을 0 으로 접어 페이지가 전폭을 쓴다.
|
|
46
|
+
*/
|
|
47
|
+
export declare const SLayout: import("react").ForwardRefExoticComponent<SLayoutProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, forwardRef, useState, useCallback, useMemo, useContext } from 'react';
|
|
3
|
+
import { clsx } from 'clsx';
|
|
4
|
+
import { extendTailwindMerge } from 'tailwind-merge';
|
|
5
|
+
import { jsx } from 'react/jsx-runtime';
|
|
6
|
+
|
|
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
|
+
|
|
18
|
+
// src/components/SGnb/gnb.config.ts
|
|
19
|
+
var belt = (p) => `var(--cmp-gnbBelt-${p})`;
|
|
20
|
+
var box = (p) => `var(--cmp-gnbBox-${p})`;
|
|
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
|
+
};
|
|
36
|
+
var GNB_MENU_WIDTH = 240;
|
|
37
|
+
var SLayoutContext = createContext(null);
|
|
38
|
+
function useSLayout() {
|
|
39
|
+
const ctx = useContext(SLayoutContext);
|
|
40
|
+
if (!ctx) throw new Error("useSLayout \uC740 <SLayout> \uC548\uC5D0\uC11C\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
|
|
41
|
+
return ctx;
|
|
42
|
+
}
|
|
43
|
+
var SLayout = forwardRef(function SLayout2({
|
|
44
|
+
type = "box",
|
|
45
|
+
header = "fix",
|
|
46
|
+
folded,
|
|
47
|
+
defaultFolded = false,
|
|
48
|
+
onFoldedChange,
|
|
49
|
+
className,
|
|
50
|
+
style,
|
|
51
|
+
children,
|
|
52
|
+
...rest
|
|
53
|
+
}, ref) {
|
|
54
|
+
const [uncontrolledFolded, setUncontrolledFolded] = useState(defaultFolded);
|
|
55
|
+
const isControlled = folded !== void 0;
|
|
56
|
+
const currentFolded = isControlled ? folded : uncontrolledFolded;
|
|
57
|
+
const setFolded = useCallback(
|
|
58
|
+
(next) => {
|
|
59
|
+
if (!isControlled) setUncontrolledFolded(next);
|
|
60
|
+
onFoldedChange?.(next);
|
|
61
|
+
},
|
|
62
|
+
[isControlled, onFoldedChange]
|
|
63
|
+
);
|
|
64
|
+
const ctx = useMemo(
|
|
65
|
+
() => ({ type, header, folded: currentFolded, setFolded }),
|
|
66
|
+
[type, header, currentFolded, setFolded]
|
|
67
|
+
);
|
|
68
|
+
const isFullHeader = header === "full";
|
|
69
|
+
const frameStyle = isFullHeader ? {
|
|
70
|
+
gridTemplateColumns: `${currentFolded ? 0 : GNB_MENU_WIDTH}px 1fr`,
|
|
71
|
+
gridTemplateRows: `${GNB_HEADER[header].topHeight} 1fr`
|
|
72
|
+
} : void 0;
|
|
73
|
+
return /* @__PURE__ */ jsx(SLayoutContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx(
|
|
74
|
+
"div",
|
|
75
|
+
{
|
|
76
|
+
ref,
|
|
77
|
+
"data-type": type,
|
|
78
|
+
"data-header": header,
|
|
79
|
+
className: cn(
|
|
80
|
+
"box-border h-full overflow-hidden",
|
|
81
|
+
isFullHeader ? "grid" : "flex flex-row",
|
|
82
|
+
className
|
|
83
|
+
),
|
|
84
|
+
style: { ...frameStyle, ...style },
|
|
85
|
+
...rest,
|
|
86
|
+
children
|
|
87
|
+
}
|
|
88
|
+
) });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export { SLayout, SLayoutContext, useSLayout };
|
|
92
|
+
//# sourceMappingURL=SLayout.js.map
|
|
93
|
+
//# sourceMappingURL=SLayout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/cn.ts","../../../src/components/SGnb/gnb.config.ts","../../../src/components/SLayout/SLayout.tsx"],"names":["SLayout"],"mappings":"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","file":"SLayout.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"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SLayout, SLayoutContext, useSLayout, type SLayoutProps, type SLayoutType, type SLayoutHeader, type SLayoutContextValue, } from './SLayout';
|
|
@@ -49,8 +49,12 @@ var SCircleProgress = react.forwardRef(
|
|
|
49
49
|
"span",
|
|
50
50
|
{
|
|
51
51
|
className: cn(
|
|
52
|
-
"text-center text-[16px] font-bold
|
|
53
|
-
innerValue
|
|
52
|
+
"text-center text-[16px] font-bold",
|
|
53
|
+
innerValue ? (
|
|
54
|
+
// leading-[normal]: 26px leading 은 폰트 content(19px) 위아래로 홀수(7px)만큼 남아
|
|
55
|
+
// 3px/4px 로 쪼개지고 글자가 링 중심보다 위로 뜬다. (leading-normal 은 1.5 라 동일 문제)
|
|
56
|
+
"absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 leading-[normal]"
|
|
57
|
+
) : "leading-[26px]"
|
|
54
58
|
),
|
|
55
59
|
style: { color: percentColor },
|
|
56
60
|
children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/cn.ts","../../../src/components/SCircleProgress/SCircleProgress.tsx","../../../src/components/SLoadingContainer/SLoadingContainer.tsx"],"names":["extendTailwindMerge","clsx","forwardRef","SCircleProgress","jsxs","jsx","SLoadingContainer","useState","useEffect","useRef","useCallback","createPortal"],"mappings":"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","file":"SLoadingContainer.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 { forwardRef, type CSSProperties } from 'react';\nimport { cn } from '../../lib/cn';\n\nexport type SCircleProgressType = 'primary' | 'inverse' | 'error' | 'complete';\n\nconst VIEWBOX = 80;\nconst RADIUS = 34;\nconst STROKE = 12;\nconst CIRCUMFERENCE = 2 * Math.PI * RADIUS;\n\nconst COLOR_MAP: Record<SCircleProgressType, { arc: string; track: string }> = {\n primary: { arc: 'var(--cmp-progress-active-color)', track: 'var(--cmp-progress-color-track)' },\n // inverse: 어두운 배경용 — arc 흰색, track 은 흰색 40%(회색 링)\n inverse: { arc: 'var(--color-white)', track: 'rgba(255,255,255,0.4)' },\n error: { arc: 'var(--color-red-75)', track: 'var(--cmp-progress-color-track)' },\n complete: { arc: '#01BB4B', track: 'var(--cmp-progress-color-track)' },\n};\n\nexport interface SCircleProgressProps {\n /** 진행률 (0–100) */\n value?: number;\n /** 색상 테마 */\n type?: SCircleProgressType;\n /** 불확정(스피너) 모드 — value 무시 */\n indeterminate?: boolean;\n /** 하단 레이블 */\n label?: string;\n /** true면 퍼센트를 원 아래가 아닌 원 가운데에 표시 */\n innerValue?: boolean;\n className?: string;\n style?: CSSProperties;\n}\n\nconst clamp = (v: number) => (Number.isFinite(v) ? Math.max(0, Math.min(100, v)) : 0);\n\n/** SCircleProgress — sd-circle-progress 포팅. determinate=진행률 arc, indeterminate=스피너. */\nexport const SCircleProgress = forwardRef<HTMLDivElement, SCircleProgressProps>(\n function SCircleProgress(\n {\n value = 0,\n type = 'primary',\n indeterminate = false,\n label,\n innerValue = false,\n className,\n style,\n },\n ref,\n ) {\n const v = clamp(value);\n const colors = COLOR_MAP[type];\n const c = VIEWBOX / 2;\n const percentColor = v === 0 ? 'var(--sys-color-fg-tertiary, #888888)' : colors.arc;\n const filled = (v / 100) * CIRCUMFERENCE;\n const showPercent = !indeterminate;\n const percentNode = (\n <span\n className={cn(\n 'text-center text-[16px] font-bold leading-[26px]',\n innerValue && 'absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2',\n )}\n style={{ color: percentColor }}\n >\n {Math.round(v)}%\n </span>\n );\n\n return (\n <div\n ref={ref}\n className={cn(\n 'inline-flex flex-col items-center gap-[var(--cmp-progress-circular-gap)]',\n className,\n )}\n style={style}\n >\n <div className={cn(innerValue && 'relative')}>\n <svg\n viewBox={`0 0 ${VIEWBOX} ${VIEWBOX}`}\n xmlns=\"http://www.w3.org/2000/svg\"\n className={cn(\n 'h-[80px] w-[80px]',\n indeterminate && 'origin-center animate-circle-spin',\n )}\n >\n <circle\n cx={c}\n cy={c}\n r={RADIUS}\n fill=\"none\"\n strokeWidth={STROKE}\n style={{ stroke: colors.track }}\n />\n <circle\n cx={c}\n cy={c}\n r={RADIUS}\n fill=\"none\"\n strokeWidth={STROKE}\n strokeLinecap=\"round\"\n className={cn(\n 'origin-center',\n indeterminate\n ? 'animate-circle-arc'\n : '-rotate-90 transition-[stroke-dasharray] duration-[400ms] ease-linear',\n )}\n style={{\n stroke: colors.arc,\n ...(indeterminate\n ? {}\n : { strokeDasharray: `${filled} ${CIRCUMFERENCE}`, strokeDashoffset: 0 }),\n }}\n />\n </svg>\n {/* innerValue: 원 가운데에 퍼센트 오버레이 */}\n {showPercent && innerValue && percentNode}\n </div>\n {/* 기본: 퍼센트를 원(ring) 아래에 표시 */}\n {showPercent && !innerValue && percentNode}\n {label && (\n <span className=\"text-center text-[12px] font-normal leading-[20px] text-[color:var(--cmp-progress-color-label)]\">\n {label}\n </span>\n )}\n </div>\n );\n },\n);\n","'use client';\n\nimport { forwardRef, useCallback, useEffect, useRef, useState, type CSSProperties } from 'react';\nimport { createPortal } from 'react-dom';\nimport { cn } from '../../lib/cn';\nimport { SCircleProgress } from '../SCircleProgress';\n\nexport interface SLoadingContainerProps {\n /** 오버레이 표시 여부 (Stencil의 show()/hide() 명령형 API → 제어 prop으로 대체) */\n open?: boolean;\n /** 스피너 하단 메시지 */\n message?: string;\n /** z-index. 기본값(1200)은 토스트(1100)·모달(1000/1001) 위 — 전체 화면을 막는 최상단 블로킹 오버레이 */\n zIndex?: number;\n className?: string;\n style?: CSSProperties;\n}\n\n/**\n * SLoadingContainer — sd-loading-container 포팅.\n * 전체 화면 딤 오버레이 + inverse 스피너. React에서는 `open` 제어 prop 사용.\n * body 로 portal 해 조상의 스태킹 컨텍스트에 갇히지 않고 항상 최상단에 뜨게 한다.\n */\nexport const SLoadingContainer = forwardRef<HTMLDivElement, SLoadingContainerProps>(\n function SLoadingContainer({ open = false, message, zIndex = 1200, className, style }, ref) {\n // portal 은 클라이언트에서만 (SSR 시 document 없음) — 마운트 후 body 로 렌더\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n\n // 오버레이 표시 중 배경 스크롤 차단\n useEffect(() => {\n if (!open) return;\n const prev = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n return () => {\n document.body.style.overflow = prev;\n };\n }, [open]);\n\n // body 로 portal 된 블로킹 오버레이라, 클릭이 document 까지 전파되면 Radix Dialog 의\n // \"바깥 클릭\" 감지에 걸려 열려있는 모달이 닫힌다. 오버레이 요소에 네이티브 pointerdown\n // 리스너를 직접 붙여(요소 자신에서 가장 먼저 발화) 전파를 끊어 document 리스너를 차단한다.\n const innerRef = useRef<HTMLDivElement | null>(null);\n const setRef = useCallback(\n (node: HTMLDivElement | null) => {\n innerRef.current = node;\n if (typeof ref === 'function') ref(node);\n else if (ref) ref.current = node;\n },\n [ref],\n );\n useEffect(() => {\n const el = innerRef.current;\n if (!el) return;\n const stop = (e: Event) => e.stopPropagation();\n el.addEventListener('pointerdown', stop);\n el.addEventListener('mousedown', stop);\n return () => {\n el.removeEventListener('pointerdown', stop);\n el.removeEventListener('mousedown', stop);\n };\n }, [mounted]);\n\n const overlay = (\n <div\n ref={setRef}\n aria-hidden={!open}\n className={cn(\n 'fixed inset-0 flex items-center justify-center transition-opacity duration-200',\n open\n ? 'pointer-events-auto visible opacity-100'\n : 'pointer-events-none invisible opacity-0',\n className,\n )}\n style={{ zIndex, ...style }}\n >\n <div className=\"absolute inset-0 bg-black/40\" />\n <div className=\"relative flex flex-col items-center gap-[16px]\">\n <SCircleProgress indeterminate type=\"inverse\" />\n {message && (\n <p className=\"m-0 max-w-[400px] break-keep text-center text-[14px] font-medium leading-[22px] text-[color:var(--color-white)]\">\n {message}\n </p>\n )}\n </div>\n </div>\n );\n\n if (!mounted) return null;\n return createPortal(overlay, document.body);\n },\n);\n"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/cn.ts","../../../src/components/SCircleProgress/SCircleProgress.tsx","../../../src/components/SLoadingContainer/SLoadingContainer.tsx"],"names":["extendTailwindMerge","clsx","forwardRef","SCircleProgress","jsxs","jsx","SLoadingContainer","useState","useEffect","useRef","useCallback","createPortal"],"mappings":";;;;;;;;AAQA,IAAM,UAAUA,iCAAA,CAAoB;AAAA,EAClC,MAAA,EAAQ;AAAA,IACN,WAAA,EAAa;AAAA,MACX,aAAa,CAAC,EAAE,IAAA,EAAM,CAAC,MAAM,IAAA,EAAM,IAAA,EAAM,IAAA,EAAM,IAAA,EAAM,MAAM,IAAA,EAAM,IAAA,EAAM,MAAM,IAAA,EAAM,IAAI,GAAG;AAAA;AAC5F;AAEJ,CAAC,CAAA;AAMM,SAAS,MAAM,MAAA,EAA8B;AAClD,EAAA,OAAO,OAAA,CAAQC,SAAA,CAAK,MAAM,CAAC,CAAA;AAC7B;ACjBA,IAAM,OAAA,GAAU,EAAA;AAChB,IAAM,MAAA,GAAS,EAAA;AACf,IAAM,MAAA,GAAS,EAAA;AACf,IAAM,aAAA,GAAgB,CAAA,GAAI,IAAA,CAAK,EAAA,GAAK,MAAA;AAEpC,IAAM,SAAA,GAAyE;AAAA,EAC7E,OAAA,EAAS,EAAE,GAAA,EAAK,kCAAA,EAAoC,OAAO,iCAAA,EAAkC;AAAA;AAAA,EAE7F,OAAA,EAAS,EAAE,GAAA,EAAK,oBAAA,EAAsB,OAAO,uBAAA,EAAwB;AAAA,EACrE,KAAA,EAAO,EAAE,GAAA,EAAK,qBAAA,EAAuB,OAAO,iCAAA,EAAkC;AAAA,EAC9E,QAAA,EAAU,EAAE,GAAA,EAAK,SAAA,EAAW,OAAO,iCAAA;AACrC,CAAA;AAiBA,IAAM,KAAA,GAAQ,CAAC,CAAA,KAAe,MAAA,CAAO,SAAS,CAAC,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,GAAG,IAAA,CAAK,GAAA,CAAI,GAAA,EAAK,CAAC,CAAC,CAAA,GAAI,CAAA;AAG5E,IAAM,eAAA,GAAkBC,gBAAA;AAAA,EAC7B,SAASC,gBAAAA,CACP;AAAA,IACE,KAAA,GAAQ,CAAA;AAAA,IACR,IAAA,GAAO,SAAA;AAAA,IACP,aAAA,GAAgB,KAAA;AAAA,IAChB,KAAA;AAAA,IACA,UAAA,GAAa,KAAA;AAAA,IACb,SAAA;AAAA,IACA;AAAA,KAEF,GAAA,EACA;AACA,IAAA,MAAM,CAAA,GAAI,MAAM,KAAK,CAAA;AACrB,IAAA,MAAM,MAAA,GAAS,UAAU,IAAI,CAAA;AAC7B,IAAA,MAAM,IAAI,OAAA,GAAU,CAAA;AACpB,IAAA,MAAM,YAAA,GAAe,CAAA,KAAM,CAAA,GAAI,uCAAA,GAA0C,MAAA,CAAO,GAAA;AAChF,IAAA,MAAM,MAAA,GAAU,IAAI,GAAA,GAAO,aAAA;AAC3B,IAAA,MAAM,cAAc,CAAC,aAAA;AACrB,IAAA,MAAM,WAAA,mBACJC,eAAA;AAAA,MAAC,MAAA;AAAA,MAAA;AAAA,QACC,SAAA,EAAW,EAAA;AAAA,UACT,mCAAA;AAAA,UACA,UAAA;AAAA;AAAA;AAAA,YAGI;AAAA,cACA;AAAA,SACN;AAAA,QACA,KAAA,EAAO,EAAE,KAAA,EAAO,YAAA,EAAa;AAAA,QAE5B,QAAA,EAAA;AAAA,UAAA,IAAA,CAAK,MAAM,CAAC,CAAA;AAAA,UAAE;AAAA;AAAA;AAAA,KACjB;AAGF,IAAA,uBACEA,eAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,GAAA;AAAA,QACA,SAAA,EAAW,EAAA;AAAA,UACT,0EAAA;AAAA,UACA;AAAA,SACF;AAAA,QACA,KAAA;AAAA,QAEA,QAAA,EAAA;AAAA,0BAAAA,eAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,EAAA,CAAG,UAAA,IAAc,UAAU,CAAA,EACzC,QAAA,EAAA;AAAA,4BAAAA,eAAA;AAAA,cAAC,KAAA;AAAA,cAAA;AAAA,gBACC,OAAA,EAAS,CAAA,IAAA,EAAO,OAAO,CAAA,CAAA,EAAI,OAAO,CAAA,CAAA;AAAA,gBAClC,KAAA,EAAM,4BAAA;AAAA,gBACN,SAAA,EAAW,EAAA;AAAA,kBACT,mBAAA;AAAA,kBACA,aAAA,IAAiB;AAAA,iBACnB;AAAA,gBAEA,QAAA,EAAA;AAAA,kCAAAC,cAAA;AAAA,oBAAC,QAAA;AAAA,oBAAA;AAAA,sBACC,EAAA,EAAI,CAAA;AAAA,sBACJ,EAAA,EAAI,CAAA;AAAA,sBACJ,CAAA,EAAG,MAAA;AAAA,sBACH,IAAA,EAAK,MAAA;AAAA,sBACL,WAAA,EAAa,MAAA;AAAA,sBACb,KAAA,EAAO,EAAE,MAAA,EAAQ,MAAA,CAAO,KAAA;AAAM;AAAA,mBAChC;AAAA,kCACAA,cAAA;AAAA,oBAAC,QAAA;AAAA,oBAAA;AAAA,sBACC,EAAA,EAAI,CAAA;AAAA,sBACJ,EAAA,EAAI,CAAA;AAAA,sBACJ,CAAA,EAAG,MAAA;AAAA,sBACH,IAAA,EAAK,MAAA;AAAA,sBACL,WAAA,EAAa,MAAA;AAAA,sBACb,aAAA,EAAc,OAAA;AAAA,sBACd,SAAA,EAAW,EAAA;AAAA,wBACT,eAAA;AAAA,wBACA,gBACI,oBAAA,GACA;AAAA,uBACN;AAAA,sBACA,KAAA,EAAO;AAAA,wBACL,QAAQ,MAAA,CAAO,GAAA;AAAA,wBACf,GAAI,aAAA,GACA,EAAC,GACD,EAAE,eAAA,EAAiB,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,aAAa,CAAA,CAAA,EAAI,gBAAA,EAAkB,CAAA;AAAE;AAC3E;AAAA;AACF;AAAA;AAAA,aACF;AAAA,YAEC,eAAe,UAAA,IAAc;AAAA,WAAA,EAChC,CAAA;AAAA,UAEC,WAAA,IAAe,CAAC,UAAA,IAAc,WAAA;AAAA,UAC9B,KAAA,oBACCA,cAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,mGACb,QAAA,EAAA,KAAA,EACH;AAAA;AAAA;AAAA,KAEJ;AAAA,EAEJ;AACF,CAAA;AC5GO,IAAM,iBAAA,GAAoBH,gBAAAA;AAAA,EAC/B,SAASI,kBAAAA,CAAkB,EAAE,IAAA,GAAO,KAAA,EAAO,OAAA,EAAS,MAAA,GAAS,IAAA,EAAM,SAAA,EAAW,KAAA,EAAM,EAAG,GAAA,EAAK;AAE1F,IAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAIC,eAAS,KAAK,CAAA;AAC5C,IAAAC,eAAA,CAAU,MAAM,UAAA,CAAW,IAAI,CAAA,EAAG,EAAE,CAAA;AAGpC,IAAAA,eAAA,CAAU,MAAM;AACd,MAAA,IAAI,CAAC,IAAA,EAAM;AACX,MAAA,MAAM,IAAA,GAAO,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,QAAA;AACjC,MAAA,QAAA,CAAS,IAAA,CAAK,MAAM,QAAA,GAAW,QAAA;AAC/B,MAAA,OAAO,MAAM;AACX,QAAA,QAAA,CAAS,IAAA,CAAK,MAAM,QAAA,GAAW,IAAA;AAAA,MACjC,CAAA;AAAA,IACF,CAAA,EAAG,CAAC,IAAI,CAAC,CAAA;AAKT,IAAA,MAAM,QAAA,GAAWC,aAA8B,IAAI,CAAA;AACnD,IAAA,MAAM,MAAA,GAASC,iBAAA;AAAA,MACb,CAAC,IAAA,KAAgC;AAC/B,QAAA,QAAA,CAAS,OAAA,GAAU,IAAA;AACnB,QAAA,IAAI,OAAO,GAAA,KAAQ,UAAA,EAAY,GAAA,CAAI,IAAI,CAAA;AAAA,aAAA,IAC9B,GAAA,MAAS,OAAA,GAAU,IAAA;AAAA,MAC9B,CAAA;AAAA,MACA,CAAC,GAAG;AAAA,KACN;AACA,IAAAF,eAAA,CAAU,MAAM;AACd,MAAA,MAAM,KAAK,QAAA,CAAS,OAAA;AACpB,MAAA,IAAI,CAAC,EAAA,EAAI;AACT,MAAA,MAAM,IAAA,GAAO,CAAC,CAAA,KAAa,CAAA,CAAE,eAAA,EAAgB;AAC7C,MAAA,EAAA,CAAG,gBAAA,CAAiB,eAAe,IAAI,CAAA;AACvC,MAAA,EAAA,CAAG,gBAAA,CAAiB,aAAa,IAAI,CAAA;AACrC,MAAA,OAAO,MAAM;AACX,QAAA,EAAA,CAAG,mBAAA,CAAoB,eAAe,IAAI,CAAA;AAC1C,QAAA,EAAA,CAAG,mBAAA,CAAoB,aAAa,IAAI,CAAA;AAAA,MAC1C,CAAA;AAAA,IACF,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AAEZ,IAAA,MAAM,0BACJJ,eAAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,GAAA,EAAK,MAAA;AAAA,QACL,eAAa,CAAC,IAAA;AAAA,QACd,SAAA,EAAW,EAAA;AAAA,UACT,gFAAA;AAAA,UACA,OACI,yCAAA,GACA,yCAAA;AAAA,UACJ;AAAA,SACF;AAAA,QACA,KAAA,EAAO,EAAE,MAAA,EAAQ,GAAG,KAAA,EAAM;AAAA,QAE1B,QAAA,EAAA;AAAA,0BAAAC,cAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,8BAAA,EAA+B,CAAA;AAAA,0BAC9CD,eAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,gDAAA,EACb,QAAA,EAAA;AAAA,4BAAAC,cAAAA,CAAC,eAAA,EAAA,EAAgB,aAAA,EAAa,IAAA,EAAC,MAAK,SAAA,EAAU,CAAA;AAAA,YAC7C,2BACCA,cAAAA,CAAC,GAAA,EAAA,EAAE,SAAA,EAAU,mHACV,QAAA,EAAA,OAAA,EACH;AAAA,WAAA,EAEJ;AAAA;AAAA;AAAA,KACF;AAGF,IAAA,IAAI,CAAC,SAAS,OAAO,IAAA;AACrB,IAAA,OAAOM,qBAAA,CAAa,OAAA,EAAS,QAAA,CAAS,IAAI,CAAA;AAAA,EAC5C;AACF","file":"SLoadingContainer.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 { forwardRef, type CSSProperties } from 'react';\nimport { cn } from '../../lib/cn';\n\nexport type SCircleProgressType = 'primary' | 'inverse' | 'error' | 'complete';\n\nconst VIEWBOX = 80;\nconst RADIUS = 34;\nconst STROKE = 12;\nconst CIRCUMFERENCE = 2 * Math.PI * RADIUS;\n\nconst COLOR_MAP: Record<SCircleProgressType, { arc: string; track: string }> = {\n primary: { arc: 'var(--cmp-progress-active-color)', track: 'var(--cmp-progress-color-track)' },\n // inverse: 어두운 배경용 — arc 흰색, track 은 흰색 40%(회색 링)\n inverse: { arc: 'var(--color-white)', track: 'rgba(255,255,255,0.4)' },\n error: { arc: 'var(--color-red-75)', track: 'var(--cmp-progress-color-track)' },\n complete: { arc: '#01BB4B', track: 'var(--cmp-progress-color-track)' },\n};\n\nexport interface SCircleProgressProps {\n /** 진행률 (0–100) */\n value?: number;\n /** 색상 테마 */\n type?: SCircleProgressType;\n /** 불확정(스피너) 모드 — value 무시 */\n indeterminate?: boolean;\n /** 하단 레이블 */\n label?: string;\n /** true면 퍼센트를 원 아래가 아닌 원 가운데에 표시 */\n innerValue?: boolean;\n className?: string;\n style?: CSSProperties;\n}\n\nconst clamp = (v: number) => (Number.isFinite(v) ? Math.max(0, Math.min(100, v)) : 0);\n\n/** SCircleProgress — sd-circle-progress 포팅. determinate=진행률 arc, indeterminate=스피너. */\nexport const SCircleProgress = forwardRef<HTMLDivElement, SCircleProgressProps>(\n function SCircleProgress(\n {\n value = 0,\n type = 'primary',\n indeterminate = false,\n label,\n innerValue = false,\n className,\n style,\n },\n ref,\n ) {\n const v = clamp(value);\n const colors = COLOR_MAP[type];\n const c = VIEWBOX / 2;\n const percentColor = v === 0 ? 'var(--sys-color-fg-tertiary, #888888)' : colors.arc;\n const filled = (v / 100) * CIRCUMFERENCE;\n const showPercent = !indeterminate;\n const percentNode = (\n <span\n className={cn(\n 'text-center text-[16px] font-bold',\n innerValue\n ? // leading-[normal]: 26px leading 은 폰트 content(19px) 위아래로 홀수(7px)만큼 남아\n // 3px/4px 로 쪼개지고 글자가 링 중심보다 위로 뜬다. (leading-normal 은 1.5 라 동일 문제)\n 'absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 leading-[normal]'\n : 'leading-[26px]',\n )}\n style={{ color: percentColor }}\n >\n {Math.round(v)}%\n </span>\n );\n\n return (\n <div\n ref={ref}\n className={cn(\n 'inline-flex flex-col items-center gap-[var(--cmp-progress-circular-gap)]',\n className,\n )}\n style={style}\n >\n <div className={cn(innerValue && 'relative')}>\n <svg\n viewBox={`0 0 ${VIEWBOX} ${VIEWBOX}`}\n xmlns=\"http://www.w3.org/2000/svg\"\n className={cn(\n 'h-[80px] w-[80px]',\n indeterminate && 'origin-center animate-circle-spin',\n )}\n >\n <circle\n cx={c}\n cy={c}\n r={RADIUS}\n fill=\"none\"\n strokeWidth={STROKE}\n style={{ stroke: colors.track }}\n />\n <circle\n cx={c}\n cy={c}\n r={RADIUS}\n fill=\"none\"\n strokeWidth={STROKE}\n strokeLinecap=\"round\"\n className={cn(\n 'origin-center',\n indeterminate\n ? 'animate-circle-arc'\n : '-rotate-90 transition-[stroke-dasharray] duration-[400ms] ease-linear',\n )}\n style={{\n stroke: colors.arc,\n ...(indeterminate\n ? {}\n : { strokeDasharray: `${filled} ${CIRCUMFERENCE}`, strokeDashoffset: 0 }),\n }}\n />\n </svg>\n {/* innerValue: 원 가운데에 퍼센트 오버레이 */}\n {showPercent && innerValue && percentNode}\n </div>\n {/* 기본: 퍼센트를 원(ring) 아래에 표시 */}\n {showPercent && !innerValue && percentNode}\n {label && (\n <span className=\"text-center text-[12px] font-normal leading-[20px] text-[color:var(--cmp-progress-color-label)]\">\n {label}\n </span>\n )}\n </div>\n );\n },\n);\n","'use client';\n\nimport { forwardRef, useCallback, useEffect, useRef, useState, type CSSProperties } from 'react';\nimport { createPortal } from 'react-dom';\nimport { cn } from '../../lib/cn';\nimport { SCircleProgress } from '../SCircleProgress';\n\nexport interface SLoadingContainerProps {\n /** 오버레이 표시 여부 (Stencil의 show()/hide() 명령형 API → 제어 prop으로 대체) */\n open?: boolean;\n /** 스피너 하단 메시지 */\n message?: string;\n /** z-index. 기본값(1200)은 토스트(1100)·모달(1000/1001) 위 — 전체 화면을 막는 최상단 블로킹 오버레이 */\n zIndex?: number;\n className?: string;\n style?: CSSProperties;\n}\n\n/**\n * SLoadingContainer — sd-loading-container 포팅.\n * 전체 화면 딤 오버레이 + inverse 스피너. React에서는 `open` 제어 prop 사용.\n * body 로 portal 해 조상의 스태킹 컨텍스트에 갇히지 않고 항상 최상단에 뜨게 한다.\n */\nexport const SLoadingContainer = forwardRef<HTMLDivElement, SLoadingContainerProps>(\n function SLoadingContainer({ open = false, message, zIndex = 1200, className, style }, ref) {\n // portal 은 클라이언트에서만 (SSR 시 document 없음) — 마운트 후 body 로 렌더\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n\n // 오버레이 표시 중 배경 스크롤 차단\n useEffect(() => {\n if (!open) return;\n const prev = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n return () => {\n document.body.style.overflow = prev;\n };\n }, [open]);\n\n // body 로 portal 된 블로킹 오버레이라, 클릭이 document 까지 전파되면 Radix Dialog 의\n // \"바깥 클릭\" 감지에 걸려 열려있는 모달이 닫힌다. 오버레이 요소에 네이티브 pointerdown\n // 리스너를 직접 붙여(요소 자신에서 가장 먼저 발화) 전파를 끊어 document 리스너를 차단한다.\n const innerRef = useRef<HTMLDivElement | null>(null);\n const setRef = useCallback(\n (node: HTMLDivElement | null) => {\n innerRef.current = node;\n if (typeof ref === 'function') ref(node);\n else if (ref) ref.current = node;\n },\n [ref],\n );\n useEffect(() => {\n const el = innerRef.current;\n if (!el) return;\n const stop = (e: Event) => e.stopPropagation();\n el.addEventListener('pointerdown', stop);\n el.addEventListener('mousedown', stop);\n return () => {\n el.removeEventListener('pointerdown', stop);\n el.removeEventListener('mousedown', stop);\n };\n }, [mounted]);\n\n const overlay = (\n <div\n ref={setRef}\n aria-hidden={!open}\n className={cn(\n 'fixed inset-0 flex items-center justify-center transition-opacity duration-200',\n open\n ? 'pointer-events-auto visible opacity-100'\n : 'pointer-events-none invisible opacity-0',\n className,\n )}\n style={{ zIndex, ...style }}\n >\n <div className=\"absolute inset-0 bg-black/40\" />\n <div className=\"relative flex flex-col items-center gap-[16px]\">\n <SCircleProgress indeterminate type=\"inverse\" />\n {message && (\n <p className=\"m-0 max-w-[400px] break-keep text-center text-[14px] font-medium leading-[22px] text-[color:var(--color-white)]\">\n {message}\n </p>\n )}\n </div>\n </div>\n );\n\n if (!mounted) return null;\n return createPortal(overlay, document.body);\n },\n);\n"]}
|
|
@@ -47,8 +47,12 @@ var SCircleProgress = forwardRef(
|
|
|
47
47
|
"span",
|
|
48
48
|
{
|
|
49
49
|
className: cn(
|
|
50
|
-
"text-center text-[16px] font-bold
|
|
51
|
-
innerValue
|
|
50
|
+
"text-center text-[16px] font-bold",
|
|
51
|
+
innerValue ? (
|
|
52
|
+
// leading-[normal]: 26px leading 은 폰트 content(19px) 위아래로 홀수(7px)만큼 남아
|
|
53
|
+
// 3px/4px 로 쪼개지고 글자가 링 중심보다 위로 뜬다. (leading-normal 은 1.5 라 동일 문제)
|
|
54
|
+
"absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 leading-[normal]"
|
|
55
|
+
) : "leading-[26px]"
|
|
52
56
|
),
|
|
53
57
|
style: { color: percentColor },
|
|
54
58
|
children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/cn.ts","../../../src/components/SCircleProgress/SCircleProgress.tsx","../../../src/components/SLoadingContainer/SLoadingContainer.tsx"],"names":["SCircleProgress","forwardRef","SLoadingContainer","jsxs","jsx"],"mappings":"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","file":"SLoadingContainer.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 { forwardRef, type CSSProperties } from 'react';\nimport { cn } from '../../lib/cn';\n\nexport type SCircleProgressType = 'primary' | 'inverse' | 'error' | 'complete';\n\nconst VIEWBOX = 80;\nconst RADIUS = 34;\nconst STROKE = 12;\nconst CIRCUMFERENCE = 2 * Math.PI * RADIUS;\n\nconst COLOR_MAP: Record<SCircleProgressType, { arc: string; track: string }> = {\n primary: { arc: 'var(--cmp-progress-active-color)', track: 'var(--cmp-progress-color-track)' },\n // inverse: 어두운 배경용 — arc 흰색, track 은 흰색 40%(회색 링)\n inverse: { arc: 'var(--color-white)', track: 'rgba(255,255,255,0.4)' },\n error: { arc: 'var(--color-red-75)', track: 'var(--cmp-progress-color-track)' },\n complete: { arc: '#01BB4B', track: 'var(--cmp-progress-color-track)' },\n};\n\nexport interface SCircleProgressProps {\n /** 진행률 (0–100) */\n value?: number;\n /** 색상 테마 */\n type?: SCircleProgressType;\n /** 불확정(스피너) 모드 — value 무시 */\n indeterminate?: boolean;\n /** 하단 레이블 */\n label?: string;\n /** true면 퍼센트를 원 아래가 아닌 원 가운데에 표시 */\n innerValue?: boolean;\n className?: string;\n style?: CSSProperties;\n}\n\nconst clamp = (v: number) => (Number.isFinite(v) ? Math.max(0, Math.min(100, v)) : 0);\n\n/** SCircleProgress — sd-circle-progress 포팅. determinate=진행률 arc, indeterminate=스피너. */\nexport const SCircleProgress = forwardRef<HTMLDivElement, SCircleProgressProps>(\n function SCircleProgress(\n {\n value = 0,\n type = 'primary',\n indeterminate = false,\n label,\n innerValue = false,\n className,\n style,\n },\n ref,\n ) {\n const v = clamp(value);\n const colors = COLOR_MAP[type];\n const c = VIEWBOX / 2;\n const percentColor = v === 0 ? 'var(--sys-color-fg-tertiary, #888888)' : colors.arc;\n const filled = (v / 100) * CIRCUMFERENCE;\n const showPercent = !indeterminate;\n const percentNode = (\n <span\n className={cn(\n 'text-center text-[16px] font-bold leading-[26px]',\n innerValue && 'absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2',\n )}\n style={{ color: percentColor }}\n >\n {Math.round(v)}%\n </span>\n );\n\n return (\n <div\n ref={ref}\n className={cn(\n 'inline-flex flex-col items-center gap-[var(--cmp-progress-circular-gap)]',\n className,\n )}\n style={style}\n >\n <div className={cn(innerValue && 'relative')}>\n <svg\n viewBox={`0 0 ${VIEWBOX} ${VIEWBOX}`}\n xmlns=\"http://www.w3.org/2000/svg\"\n className={cn(\n 'h-[80px] w-[80px]',\n indeterminate && 'origin-center animate-circle-spin',\n )}\n >\n <circle\n cx={c}\n cy={c}\n r={RADIUS}\n fill=\"none\"\n strokeWidth={STROKE}\n style={{ stroke: colors.track }}\n />\n <circle\n cx={c}\n cy={c}\n r={RADIUS}\n fill=\"none\"\n strokeWidth={STROKE}\n strokeLinecap=\"round\"\n className={cn(\n 'origin-center',\n indeterminate\n ? 'animate-circle-arc'\n : '-rotate-90 transition-[stroke-dasharray] duration-[400ms] ease-linear',\n )}\n style={{\n stroke: colors.arc,\n ...(indeterminate\n ? {}\n : { strokeDasharray: `${filled} ${CIRCUMFERENCE}`, strokeDashoffset: 0 }),\n }}\n />\n </svg>\n {/* innerValue: 원 가운데에 퍼센트 오버레이 */}\n {showPercent && innerValue && percentNode}\n </div>\n {/* 기본: 퍼센트를 원(ring) 아래에 표시 */}\n {showPercent && !innerValue && percentNode}\n {label && (\n <span className=\"text-center text-[12px] font-normal leading-[20px] text-[color:var(--cmp-progress-color-label)]\">\n {label}\n </span>\n )}\n </div>\n );\n },\n);\n","'use client';\n\nimport { forwardRef, useCallback, useEffect, useRef, useState, type CSSProperties } from 'react';\nimport { createPortal } from 'react-dom';\nimport { cn } from '../../lib/cn';\nimport { SCircleProgress } from '../SCircleProgress';\n\nexport interface SLoadingContainerProps {\n /** 오버레이 표시 여부 (Stencil의 show()/hide() 명령형 API → 제어 prop으로 대체) */\n open?: boolean;\n /** 스피너 하단 메시지 */\n message?: string;\n /** z-index. 기본값(1200)은 토스트(1100)·모달(1000/1001) 위 — 전체 화면을 막는 최상단 블로킹 오버레이 */\n zIndex?: number;\n className?: string;\n style?: CSSProperties;\n}\n\n/**\n * SLoadingContainer — sd-loading-container 포팅.\n * 전체 화면 딤 오버레이 + inverse 스피너. React에서는 `open` 제어 prop 사용.\n * body 로 portal 해 조상의 스태킹 컨텍스트에 갇히지 않고 항상 최상단에 뜨게 한다.\n */\nexport const SLoadingContainer = forwardRef<HTMLDivElement, SLoadingContainerProps>(\n function SLoadingContainer({ open = false, message, zIndex = 1200, className, style }, ref) {\n // portal 은 클라이언트에서만 (SSR 시 document 없음) — 마운트 후 body 로 렌더\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n\n // 오버레이 표시 중 배경 스크롤 차단\n useEffect(() => {\n if (!open) return;\n const prev = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n return () => {\n document.body.style.overflow = prev;\n };\n }, [open]);\n\n // body 로 portal 된 블로킹 오버레이라, 클릭이 document 까지 전파되면 Radix Dialog 의\n // \"바깥 클릭\" 감지에 걸려 열려있는 모달이 닫힌다. 오버레이 요소에 네이티브 pointerdown\n // 리스너를 직접 붙여(요소 자신에서 가장 먼저 발화) 전파를 끊어 document 리스너를 차단한다.\n const innerRef = useRef<HTMLDivElement | null>(null);\n const setRef = useCallback(\n (node: HTMLDivElement | null) => {\n innerRef.current = node;\n if (typeof ref === 'function') ref(node);\n else if (ref) ref.current = node;\n },\n [ref],\n );\n useEffect(() => {\n const el = innerRef.current;\n if (!el) return;\n const stop = (e: Event) => e.stopPropagation();\n el.addEventListener('pointerdown', stop);\n el.addEventListener('mousedown', stop);\n return () => {\n el.removeEventListener('pointerdown', stop);\n el.removeEventListener('mousedown', stop);\n };\n }, [mounted]);\n\n const overlay = (\n <div\n ref={setRef}\n aria-hidden={!open}\n className={cn(\n 'fixed inset-0 flex items-center justify-center transition-opacity duration-200',\n open\n ? 'pointer-events-auto visible opacity-100'\n : 'pointer-events-none invisible opacity-0',\n className,\n )}\n style={{ zIndex, ...style }}\n >\n <div className=\"absolute inset-0 bg-black/40\" />\n <div className=\"relative flex flex-col items-center gap-[16px]\">\n <SCircleProgress indeterminate type=\"inverse\" />\n {message && (\n <p className=\"m-0 max-w-[400px] break-keep text-center text-[14px] font-medium leading-[22px] text-[color:var(--color-white)]\">\n {message}\n </p>\n )}\n </div>\n </div>\n );\n\n if (!mounted) return null;\n return createPortal(overlay, document.body);\n },\n);\n"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/cn.ts","../../../src/components/SCircleProgress/SCircleProgress.tsx","../../../src/components/SLoadingContainer/SLoadingContainer.tsx"],"names":["SCircleProgress","forwardRef","SLoadingContainer","jsxs","jsx"],"mappings":"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","file":"SLoadingContainer.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 { forwardRef, type CSSProperties } from 'react';\nimport { cn } from '../../lib/cn';\n\nexport type SCircleProgressType = 'primary' | 'inverse' | 'error' | 'complete';\n\nconst VIEWBOX = 80;\nconst RADIUS = 34;\nconst STROKE = 12;\nconst CIRCUMFERENCE = 2 * Math.PI * RADIUS;\n\nconst COLOR_MAP: Record<SCircleProgressType, { arc: string; track: string }> = {\n primary: { arc: 'var(--cmp-progress-active-color)', track: 'var(--cmp-progress-color-track)' },\n // inverse: 어두운 배경용 — arc 흰색, track 은 흰색 40%(회색 링)\n inverse: { arc: 'var(--color-white)', track: 'rgba(255,255,255,0.4)' },\n error: { arc: 'var(--color-red-75)', track: 'var(--cmp-progress-color-track)' },\n complete: { arc: '#01BB4B', track: 'var(--cmp-progress-color-track)' },\n};\n\nexport interface SCircleProgressProps {\n /** 진행률 (0–100) */\n value?: number;\n /** 색상 테마 */\n type?: SCircleProgressType;\n /** 불확정(스피너) 모드 — value 무시 */\n indeterminate?: boolean;\n /** 하단 레이블 */\n label?: string;\n /** true면 퍼센트를 원 아래가 아닌 원 가운데에 표시 */\n innerValue?: boolean;\n className?: string;\n style?: CSSProperties;\n}\n\nconst clamp = (v: number) => (Number.isFinite(v) ? Math.max(0, Math.min(100, v)) : 0);\n\n/** SCircleProgress — sd-circle-progress 포팅. determinate=진행률 arc, indeterminate=스피너. */\nexport const SCircleProgress = forwardRef<HTMLDivElement, SCircleProgressProps>(\n function SCircleProgress(\n {\n value = 0,\n type = 'primary',\n indeterminate = false,\n label,\n innerValue = false,\n className,\n style,\n },\n ref,\n ) {\n const v = clamp(value);\n const colors = COLOR_MAP[type];\n const c = VIEWBOX / 2;\n const percentColor = v === 0 ? 'var(--sys-color-fg-tertiary, #888888)' : colors.arc;\n const filled = (v / 100) * CIRCUMFERENCE;\n const showPercent = !indeterminate;\n const percentNode = (\n <span\n className={cn(\n 'text-center text-[16px] font-bold',\n innerValue\n ? // leading-[normal]: 26px leading 은 폰트 content(19px) 위아래로 홀수(7px)만큼 남아\n // 3px/4px 로 쪼개지고 글자가 링 중심보다 위로 뜬다. (leading-normal 은 1.5 라 동일 문제)\n 'absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 leading-[normal]'\n : 'leading-[26px]',\n )}\n style={{ color: percentColor }}\n >\n {Math.round(v)}%\n </span>\n );\n\n return (\n <div\n ref={ref}\n className={cn(\n 'inline-flex flex-col items-center gap-[var(--cmp-progress-circular-gap)]',\n className,\n )}\n style={style}\n >\n <div className={cn(innerValue && 'relative')}>\n <svg\n viewBox={`0 0 ${VIEWBOX} ${VIEWBOX}`}\n xmlns=\"http://www.w3.org/2000/svg\"\n className={cn(\n 'h-[80px] w-[80px]',\n indeterminate && 'origin-center animate-circle-spin',\n )}\n >\n <circle\n cx={c}\n cy={c}\n r={RADIUS}\n fill=\"none\"\n strokeWidth={STROKE}\n style={{ stroke: colors.track }}\n />\n <circle\n cx={c}\n cy={c}\n r={RADIUS}\n fill=\"none\"\n strokeWidth={STROKE}\n strokeLinecap=\"round\"\n className={cn(\n 'origin-center',\n indeterminate\n ? 'animate-circle-arc'\n : '-rotate-90 transition-[stroke-dasharray] duration-[400ms] ease-linear',\n )}\n style={{\n stroke: colors.arc,\n ...(indeterminate\n ? {}\n : { strokeDasharray: `${filled} ${CIRCUMFERENCE}`, strokeDashoffset: 0 }),\n }}\n />\n </svg>\n {/* innerValue: 원 가운데에 퍼센트 오버레이 */}\n {showPercent && innerValue && percentNode}\n </div>\n {/* 기본: 퍼센트를 원(ring) 아래에 표시 */}\n {showPercent && !innerValue && percentNode}\n {label && (\n <span className=\"text-center text-[12px] font-normal leading-[20px] text-[color:var(--cmp-progress-color-label)]\">\n {label}\n </span>\n )}\n </div>\n );\n },\n);\n","'use client';\n\nimport { forwardRef, useCallback, useEffect, useRef, useState, type CSSProperties } from 'react';\nimport { createPortal } from 'react-dom';\nimport { cn } from '../../lib/cn';\nimport { SCircleProgress } from '../SCircleProgress';\n\nexport interface SLoadingContainerProps {\n /** 오버레이 표시 여부 (Stencil의 show()/hide() 명령형 API → 제어 prop으로 대체) */\n open?: boolean;\n /** 스피너 하단 메시지 */\n message?: string;\n /** z-index. 기본값(1200)은 토스트(1100)·모달(1000/1001) 위 — 전체 화면을 막는 최상단 블로킹 오버레이 */\n zIndex?: number;\n className?: string;\n style?: CSSProperties;\n}\n\n/**\n * SLoadingContainer — sd-loading-container 포팅.\n * 전체 화면 딤 오버레이 + inverse 스피너. React에서는 `open` 제어 prop 사용.\n * body 로 portal 해 조상의 스태킹 컨텍스트에 갇히지 않고 항상 최상단에 뜨게 한다.\n */\nexport const SLoadingContainer = forwardRef<HTMLDivElement, SLoadingContainerProps>(\n function SLoadingContainer({ open = false, message, zIndex = 1200, className, style }, ref) {\n // portal 은 클라이언트에서만 (SSR 시 document 없음) — 마운트 후 body 로 렌더\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n\n // 오버레이 표시 중 배경 스크롤 차단\n useEffect(() => {\n if (!open) return;\n const prev = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n return () => {\n document.body.style.overflow = prev;\n };\n }, [open]);\n\n // body 로 portal 된 블로킹 오버레이라, 클릭이 document 까지 전파되면 Radix Dialog 의\n // \"바깥 클릭\" 감지에 걸려 열려있는 모달이 닫힌다. 오버레이 요소에 네이티브 pointerdown\n // 리스너를 직접 붙여(요소 자신에서 가장 먼저 발화) 전파를 끊어 document 리스너를 차단한다.\n const innerRef = useRef<HTMLDivElement | null>(null);\n const setRef = useCallback(\n (node: HTMLDivElement | null) => {\n innerRef.current = node;\n if (typeof ref === 'function') ref(node);\n else if (ref) ref.current = node;\n },\n [ref],\n );\n useEffect(() => {\n const el = innerRef.current;\n if (!el) return;\n const stop = (e: Event) => e.stopPropagation();\n el.addEventListener('pointerdown', stop);\n el.addEventListener('mousedown', stop);\n return () => {\n el.removeEventListener('pointerdown', stop);\n el.removeEventListener('mousedown', stop);\n };\n }, [mounted]);\n\n const overlay = (\n <div\n ref={setRef}\n aria-hidden={!open}\n className={cn(\n 'fixed inset-0 flex items-center justify-center transition-opacity duration-200',\n open\n ? 'pointer-events-auto visible opacity-100'\n : 'pointer-events-none invisible opacity-0',\n className,\n )}\n style={{ zIndex, ...style }}\n >\n <div className=\"absolute inset-0 bg-black/40\" />\n <div className=\"relative flex flex-col items-center gap-[16px]\">\n <SCircleProgress indeterminate type=\"inverse\" />\n {message && (\n <p className=\"m-0 max-w-[400px] break-keep text-center text-[14px] font-medium leading-[22px] text-[color:var(--color-white)]\">\n {message}\n </p>\n )}\n </div>\n </div>\n );\n\n if (!mounted) return null;\n return createPortal(overlay, document.body);\n },\n);\n"]}
|
|
@@ -50,8 +50,12 @@ var SCircleProgress = react.forwardRef(
|
|
|
50
50
|
"span",
|
|
51
51
|
{
|
|
52
52
|
className: cn(
|
|
53
|
-
"text-center text-[16px] font-bold
|
|
54
|
-
innerValue
|
|
53
|
+
"text-center text-[16px] font-bold",
|
|
54
|
+
innerValue ? (
|
|
55
|
+
// leading-[normal]: 26px leading 은 폰트 content(19px) 위아래로 홀수(7px)만큼 남아
|
|
56
|
+
// 3px/4px 로 쪼개지고 글자가 링 중심보다 위로 뜬다. (leading-normal 은 1.5 라 동일 문제)
|
|
57
|
+
"absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 leading-[normal]"
|
|
58
|
+
) : "leading-[26px]"
|
|
55
59
|
),
|
|
56
60
|
style: { color: percentColor },
|
|
57
61
|
children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/cn.ts","../../../src/components/SCircleProgress/SCircleProgress.tsx","../../../src/components/SLoadingContainer/SLoadingContainer.tsx","../../../src/components/SLoadingContainer/loading.tsx"],"names":["extendTailwindMerge","clsx","forwardRef","SCircleProgress","jsxs","jsx","SLoadingContainer","mounted","useState","useEffect","useRef","useCallback","createPortal","useSyncExternalStore","createRoot"],"mappings":"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","file":"loading.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 { forwardRef, type CSSProperties } from 'react';\nimport { cn } from '../../lib/cn';\n\nexport type SCircleProgressType = 'primary' | 'inverse' | 'error' | 'complete';\n\nconst VIEWBOX = 80;\nconst RADIUS = 34;\nconst STROKE = 12;\nconst CIRCUMFERENCE = 2 * Math.PI * RADIUS;\n\nconst COLOR_MAP: Record<SCircleProgressType, { arc: string; track: string }> = {\n primary: { arc: 'var(--cmp-progress-active-color)', track: 'var(--cmp-progress-color-track)' },\n // inverse: 어두운 배경용 — arc 흰색, track 은 흰색 40%(회색 링)\n inverse: { arc: 'var(--color-white)', track: 'rgba(255,255,255,0.4)' },\n error: { arc: 'var(--color-red-75)', track: 'var(--cmp-progress-color-track)' },\n complete: { arc: '#01BB4B', track: 'var(--cmp-progress-color-track)' },\n};\n\nexport interface SCircleProgressProps {\n /** 진행률 (0–100) */\n value?: number;\n /** 색상 테마 */\n type?: SCircleProgressType;\n /** 불확정(스피너) 모드 — value 무시 */\n indeterminate?: boolean;\n /** 하단 레이블 */\n label?: string;\n /** true면 퍼센트를 원 아래가 아닌 원 가운데에 표시 */\n innerValue?: boolean;\n className?: string;\n style?: CSSProperties;\n}\n\nconst clamp = (v: number) => (Number.isFinite(v) ? Math.max(0, Math.min(100, v)) : 0);\n\n/** SCircleProgress — sd-circle-progress 포팅. determinate=진행률 arc, indeterminate=스피너. */\nexport const SCircleProgress = forwardRef<HTMLDivElement, SCircleProgressProps>(\n function SCircleProgress(\n {\n value = 0,\n type = 'primary',\n indeterminate = false,\n label,\n innerValue = false,\n className,\n style,\n },\n ref,\n ) {\n const v = clamp(value);\n const colors = COLOR_MAP[type];\n const c = VIEWBOX / 2;\n const percentColor = v === 0 ? 'var(--sys-color-fg-tertiary, #888888)' : colors.arc;\n const filled = (v / 100) * CIRCUMFERENCE;\n const showPercent = !indeterminate;\n const percentNode = (\n <span\n className={cn(\n 'text-center text-[16px] font-bold leading-[26px]',\n innerValue && 'absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2',\n )}\n style={{ color: percentColor }}\n >\n {Math.round(v)}%\n </span>\n );\n\n return (\n <div\n ref={ref}\n className={cn(\n 'inline-flex flex-col items-center gap-[var(--cmp-progress-circular-gap)]',\n className,\n )}\n style={style}\n >\n <div className={cn(innerValue && 'relative')}>\n <svg\n viewBox={`0 0 ${VIEWBOX} ${VIEWBOX}`}\n xmlns=\"http://www.w3.org/2000/svg\"\n className={cn(\n 'h-[80px] w-[80px]',\n indeterminate && 'origin-center animate-circle-spin',\n )}\n >\n <circle\n cx={c}\n cy={c}\n r={RADIUS}\n fill=\"none\"\n strokeWidth={STROKE}\n style={{ stroke: colors.track }}\n />\n <circle\n cx={c}\n cy={c}\n r={RADIUS}\n fill=\"none\"\n strokeWidth={STROKE}\n strokeLinecap=\"round\"\n className={cn(\n 'origin-center',\n indeterminate\n ? 'animate-circle-arc'\n : '-rotate-90 transition-[stroke-dasharray] duration-[400ms] ease-linear',\n )}\n style={{\n stroke: colors.arc,\n ...(indeterminate\n ? {}\n : { strokeDasharray: `${filled} ${CIRCUMFERENCE}`, strokeDashoffset: 0 }),\n }}\n />\n </svg>\n {/* innerValue: 원 가운데에 퍼센트 오버레이 */}\n {showPercent && innerValue && percentNode}\n </div>\n {/* 기본: 퍼센트를 원(ring) 아래에 표시 */}\n {showPercent && !innerValue && percentNode}\n {label && (\n <span className=\"text-center text-[12px] font-normal leading-[20px] text-[color:var(--cmp-progress-color-label)]\">\n {label}\n </span>\n )}\n </div>\n );\n },\n);\n","'use client';\n\nimport { forwardRef, useCallback, useEffect, useRef, useState, type CSSProperties } from 'react';\nimport { createPortal } from 'react-dom';\nimport { cn } from '../../lib/cn';\nimport { SCircleProgress } from '../SCircleProgress';\n\nexport interface SLoadingContainerProps {\n /** 오버레이 표시 여부 (Stencil의 show()/hide() 명령형 API → 제어 prop으로 대체) */\n open?: boolean;\n /** 스피너 하단 메시지 */\n message?: string;\n /** z-index. 기본값(1200)은 토스트(1100)·모달(1000/1001) 위 — 전체 화면을 막는 최상단 블로킹 오버레이 */\n zIndex?: number;\n className?: string;\n style?: CSSProperties;\n}\n\n/**\n * SLoadingContainer — sd-loading-container 포팅.\n * 전체 화면 딤 오버레이 + inverse 스피너. React에서는 `open` 제어 prop 사용.\n * body 로 portal 해 조상의 스태킹 컨텍스트에 갇히지 않고 항상 최상단에 뜨게 한다.\n */\nexport const SLoadingContainer = forwardRef<HTMLDivElement, SLoadingContainerProps>(\n function SLoadingContainer({ open = false, message, zIndex = 1200, className, style }, ref) {\n // portal 은 클라이언트에서만 (SSR 시 document 없음) — 마운트 후 body 로 렌더\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n\n // 오버레이 표시 중 배경 스크롤 차단\n useEffect(() => {\n if (!open) return;\n const prev = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n return () => {\n document.body.style.overflow = prev;\n };\n }, [open]);\n\n // body 로 portal 된 블로킹 오버레이라, 클릭이 document 까지 전파되면 Radix Dialog 의\n // \"바깥 클릭\" 감지에 걸려 열려있는 모달이 닫힌다. 오버레이 요소에 네이티브 pointerdown\n // 리스너를 직접 붙여(요소 자신에서 가장 먼저 발화) 전파를 끊어 document 리스너를 차단한다.\n const innerRef = useRef<HTMLDivElement | null>(null);\n const setRef = useCallback(\n (node: HTMLDivElement | null) => {\n innerRef.current = node;\n if (typeof ref === 'function') ref(node);\n else if (ref) ref.current = node;\n },\n [ref],\n );\n useEffect(() => {\n const el = innerRef.current;\n if (!el) return;\n const stop = (e: Event) => e.stopPropagation();\n el.addEventListener('pointerdown', stop);\n el.addEventListener('mousedown', stop);\n return () => {\n el.removeEventListener('pointerdown', stop);\n el.removeEventListener('mousedown', stop);\n };\n }, [mounted]);\n\n const overlay = (\n <div\n ref={setRef}\n aria-hidden={!open}\n className={cn(\n 'fixed inset-0 flex items-center justify-center transition-opacity duration-200',\n open\n ? 'pointer-events-auto visible opacity-100'\n : 'pointer-events-none invisible opacity-0',\n className,\n )}\n style={{ zIndex, ...style }}\n >\n <div className=\"absolute inset-0 bg-black/40\" />\n <div className=\"relative flex flex-col items-center gap-[16px]\">\n <SCircleProgress indeterminate type=\"inverse\" />\n {message && (\n <p className=\"m-0 max-w-[400px] break-keep text-center text-[14px] font-medium leading-[22px] text-[color:var(--color-white)]\">\n {message}\n </p>\n )}\n </div>\n </div>\n );\n\n if (!mounted) return null;\n return createPortal(overlay, document.body);\n },\n);\n","'use client';\n\nimport { useSyncExternalStore } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { SLoadingContainer } from './SLoadingContainer';\n\n/**\n * 전역 로딩 오버레이 싱글톤 — sd-loading-container 의 `sdLoading.show()/hide()` 포팅.\n * 앱 어디서든 `loading.show({ message })` / `loading.hide()` 로 호출한다.\n * 최초 show 시 document.body 에 오버레이 루트를 자동 마운트한다.\n */\ninterface LoadingState {\n open: boolean;\n message?: string;\n}\n\nlet state: LoadingState = { open: false, message: undefined };\nconst listeners = new Set<() => void>();\n\nconst emit = () => listeners.forEach(l => l());\nconst setState = (patch: Partial<LoadingState>) => {\n state = { ...state, ...patch };\n emit();\n};\nconst subscribe = (cb: () => void) => {\n listeners.add(cb);\n return () => listeners.delete(cb);\n};\nconst getSnapshot = () => state;\n\n/** 오버레이 뷰포트 — 스토어를 구독해 SLoadingContainer 를 렌더한다. 직접 배치도 가능. */\nexport function SLoadingViewport() {\n const s = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n return <SLoadingContainer open={s.open} message={s.message} />;\n}\n\nlet mounted = false;\nfunction ensureMounted() {\n if (mounted || typeof document === 'undefined') return;\n mounted = true;\n const host = document.createElement('div');\n host.setAttribute('data-sd-loading-root', '');\n document.body.appendChild(host);\n createRoot(host).render(<SLoadingViewport />);\n}\n\nexport const loading = {\n /** 로딩 오버레이 표시 (메시지 갱신도 동일 호출) */\n show(options: { message?: string } = {}) {\n ensureMounted();\n setState({ open: true, message: options.message });\n },\n /** 로딩 오버레이 숨김 */\n hide() {\n setState({ open: false });\n },\n};\n"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/cn.ts","../../../src/components/SCircleProgress/SCircleProgress.tsx","../../../src/components/SLoadingContainer/SLoadingContainer.tsx","../../../src/components/SLoadingContainer/loading.tsx"],"names":["extendTailwindMerge","clsx","forwardRef","SCircleProgress","jsxs","jsx","SLoadingContainer","mounted","useState","useEffect","useRef","useCallback","createPortal","useSyncExternalStore","createRoot"],"mappings":"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","file":"loading.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 { forwardRef, type CSSProperties } from 'react';\nimport { cn } from '../../lib/cn';\n\nexport type SCircleProgressType = 'primary' | 'inverse' | 'error' | 'complete';\n\nconst VIEWBOX = 80;\nconst RADIUS = 34;\nconst STROKE = 12;\nconst CIRCUMFERENCE = 2 * Math.PI * RADIUS;\n\nconst COLOR_MAP: Record<SCircleProgressType, { arc: string; track: string }> = {\n primary: { arc: 'var(--cmp-progress-active-color)', track: 'var(--cmp-progress-color-track)' },\n // inverse: 어두운 배경용 — arc 흰색, track 은 흰색 40%(회색 링)\n inverse: { arc: 'var(--color-white)', track: 'rgba(255,255,255,0.4)' },\n error: { arc: 'var(--color-red-75)', track: 'var(--cmp-progress-color-track)' },\n complete: { arc: '#01BB4B', track: 'var(--cmp-progress-color-track)' },\n};\n\nexport interface SCircleProgressProps {\n /** 진행률 (0–100) */\n value?: number;\n /** 색상 테마 */\n type?: SCircleProgressType;\n /** 불확정(스피너) 모드 — value 무시 */\n indeterminate?: boolean;\n /** 하단 레이블 */\n label?: string;\n /** true면 퍼센트를 원 아래가 아닌 원 가운데에 표시 */\n innerValue?: boolean;\n className?: string;\n style?: CSSProperties;\n}\n\nconst clamp = (v: number) => (Number.isFinite(v) ? Math.max(0, Math.min(100, v)) : 0);\n\n/** SCircleProgress — sd-circle-progress 포팅. determinate=진행률 arc, indeterminate=스피너. */\nexport const SCircleProgress = forwardRef<HTMLDivElement, SCircleProgressProps>(\n function SCircleProgress(\n {\n value = 0,\n type = 'primary',\n indeterminate = false,\n label,\n innerValue = false,\n className,\n style,\n },\n ref,\n ) {\n const v = clamp(value);\n const colors = COLOR_MAP[type];\n const c = VIEWBOX / 2;\n const percentColor = v === 0 ? 'var(--sys-color-fg-tertiary, #888888)' : colors.arc;\n const filled = (v / 100) * CIRCUMFERENCE;\n const showPercent = !indeterminate;\n const percentNode = (\n <span\n className={cn(\n 'text-center text-[16px] font-bold',\n innerValue\n ? // leading-[normal]: 26px leading 은 폰트 content(19px) 위아래로 홀수(7px)만큼 남아\n // 3px/4px 로 쪼개지고 글자가 링 중심보다 위로 뜬다. (leading-normal 은 1.5 라 동일 문제)\n 'absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 leading-[normal]'\n : 'leading-[26px]',\n )}\n style={{ color: percentColor }}\n >\n {Math.round(v)}%\n </span>\n );\n\n return (\n <div\n ref={ref}\n className={cn(\n 'inline-flex flex-col items-center gap-[var(--cmp-progress-circular-gap)]',\n className,\n )}\n style={style}\n >\n <div className={cn(innerValue && 'relative')}>\n <svg\n viewBox={`0 0 ${VIEWBOX} ${VIEWBOX}`}\n xmlns=\"http://www.w3.org/2000/svg\"\n className={cn(\n 'h-[80px] w-[80px]',\n indeterminate && 'origin-center animate-circle-spin',\n )}\n >\n <circle\n cx={c}\n cy={c}\n r={RADIUS}\n fill=\"none\"\n strokeWidth={STROKE}\n style={{ stroke: colors.track }}\n />\n <circle\n cx={c}\n cy={c}\n r={RADIUS}\n fill=\"none\"\n strokeWidth={STROKE}\n strokeLinecap=\"round\"\n className={cn(\n 'origin-center',\n indeterminate\n ? 'animate-circle-arc'\n : '-rotate-90 transition-[stroke-dasharray] duration-[400ms] ease-linear',\n )}\n style={{\n stroke: colors.arc,\n ...(indeterminate\n ? {}\n : { strokeDasharray: `${filled} ${CIRCUMFERENCE}`, strokeDashoffset: 0 }),\n }}\n />\n </svg>\n {/* innerValue: 원 가운데에 퍼센트 오버레이 */}\n {showPercent && innerValue && percentNode}\n </div>\n {/* 기본: 퍼센트를 원(ring) 아래에 표시 */}\n {showPercent && !innerValue && percentNode}\n {label && (\n <span className=\"text-center text-[12px] font-normal leading-[20px] text-[color:var(--cmp-progress-color-label)]\">\n {label}\n </span>\n )}\n </div>\n );\n },\n);\n","'use client';\n\nimport { forwardRef, useCallback, useEffect, useRef, useState, type CSSProperties } from 'react';\nimport { createPortal } from 'react-dom';\nimport { cn } from '../../lib/cn';\nimport { SCircleProgress } from '../SCircleProgress';\n\nexport interface SLoadingContainerProps {\n /** 오버레이 표시 여부 (Stencil의 show()/hide() 명령형 API → 제어 prop으로 대체) */\n open?: boolean;\n /** 스피너 하단 메시지 */\n message?: string;\n /** z-index. 기본값(1200)은 토스트(1100)·모달(1000/1001) 위 — 전체 화면을 막는 최상단 블로킹 오버레이 */\n zIndex?: number;\n className?: string;\n style?: CSSProperties;\n}\n\n/**\n * SLoadingContainer — sd-loading-container 포팅.\n * 전체 화면 딤 오버레이 + inverse 스피너. React에서는 `open` 제어 prop 사용.\n * body 로 portal 해 조상의 스태킹 컨텍스트에 갇히지 않고 항상 최상단에 뜨게 한다.\n */\nexport const SLoadingContainer = forwardRef<HTMLDivElement, SLoadingContainerProps>(\n function SLoadingContainer({ open = false, message, zIndex = 1200, className, style }, ref) {\n // portal 은 클라이언트에서만 (SSR 시 document 없음) — 마운트 후 body 로 렌더\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n\n // 오버레이 표시 중 배경 스크롤 차단\n useEffect(() => {\n if (!open) return;\n const prev = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n return () => {\n document.body.style.overflow = prev;\n };\n }, [open]);\n\n // body 로 portal 된 블로킹 오버레이라, 클릭이 document 까지 전파되면 Radix Dialog 의\n // \"바깥 클릭\" 감지에 걸려 열려있는 모달이 닫힌다. 오버레이 요소에 네이티브 pointerdown\n // 리스너를 직접 붙여(요소 자신에서 가장 먼저 발화) 전파를 끊어 document 리스너를 차단한다.\n const innerRef = useRef<HTMLDivElement | null>(null);\n const setRef = useCallback(\n (node: HTMLDivElement | null) => {\n innerRef.current = node;\n if (typeof ref === 'function') ref(node);\n else if (ref) ref.current = node;\n },\n [ref],\n );\n useEffect(() => {\n const el = innerRef.current;\n if (!el) return;\n const stop = (e: Event) => e.stopPropagation();\n el.addEventListener('pointerdown', stop);\n el.addEventListener('mousedown', stop);\n return () => {\n el.removeEventListener('pointerdown', stop);\n el.removeEventListener('mousedown', stop);\n };\n }, [mounted]);\n\n const overlay = (\n <div\n ref={setRef}\n aria-hidden={!open}\n className={cn(\n 'fixed inset-0 flex items-center justify-center transition-opacity duration-200',\n open\n ? 'pointer-events-auto visible opacity-100'\n : 'pointer-events-none invisible opacity-0',\n className,\n )}\n style={{ zIndex, ...style }}\n >\n <div className=\"absolute inset-0 bg-black/40\" />\n <div className=\"relative flex flex-col items-center gap-[16px]\">\n <SCircleProgress indeterminate type=\"inverse\" />\n {message && (\n <p className=\"m-0 max-w-[400px] break-keep text-center text-[14px] font-medium leading-[22px] text-[color:var(--color-white)]\">\n {message}\n </p>\n )}\n </div>\n </div>\n );\n\n if (!mounted) return null;\n return createPortal(overlay, document.body);\n },\n);\n","'use client';\n\nimport { useSyncExternalStore } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { SLoadingContainer } from './SLoadingContainer';\n\n/**\n * 전역 로딩 오버레이 싱글톤 — sd-loading-container 의 `sdLoading.show()/hide()` 포팅.\n * 앱 어디서든 `loading.show({ message })` / `loading.hide()` 로 호출한다.\n * 최초 show 시 document.body 에 오버레이 루트를 자동 마운트한다.\n */\ninterface LoadingState {\n open: boolean;\n message?: string;\n}\n\nlet state: LoadingState = { open: false, message: undefined };\nconst listeners = new Set<() => void>();\n\nconst emit = () => listeners.forEach(l => l());\nconst setState = (patch: Partial<LoadingState>) => {\n state = { ...state, ...patch };\n emit();\n};\nconst subscribe = (cb: () => void) => {\n listeners.add(cb);\n return () => listeners.delete(cb);\n};\nconst getSnapshot = () => state;\n\n/** 오버레이 뷰포트 — 스토어를 구독해 SLoadingContainer 를 렌더한다. 직접 배치도 가능. */\nexport function SLoadingViewport() {\n const s = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n return <SLoadingContainer open={s.open} message={s.message} />;\n}\n\nlet mounted = false;\nfunction ensureMounted() {\n if (mounted || typeof document === 'undefined') return;\n mounted = true;\n const host = document.createElement('div');\n host.setAttribute('data-sd-loading-root', '');\n document.body.appendChild(host);\n createRoot(host).render(<SLoadingViewport />);\n}\n\nexport const loading = {\n /** 로딩 오버레이 표시 (메시지 갱신도 동일 호출) */\n show(options: { message?: string } = {}) {\n ensureMounted();\n setState({ open: true, message: options.message });\n },\n /** 로딩 오버레이 숨김 */\n hide() {\n setState({ open: false });\n },\n};\n"]}
|