@t3q-ui/design-system 0.0.0-stage → 1.56.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.
@@ -0,0 +1,885 @@
1
+ 'use strict';
2
+
3
+ var chunkMHAO45AL_cjs = require('../chunk-MHAO45AL.cjs');
4
+ var react = require('react');
5
+ var lucideReact = require('lucide-react');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+
8
+ // src/patterns/listPageRules.ts
9
+ function resolvePrimaryField(primaryField, hasRowClick) {
10
+ if (primaryField !== void 0) return primaryField;
11
+ return hasRowClick ? "name" : void 0;
12
+ }
13
+ function withPrimaryColumn(columns, primaryField) {
14
+ if (!primaryField) return columns;
15
+ return columns.map(
16
+ (c) => c.field === primaryField && c.type == null ? { ...c, type: "primary" } : c
17
+ );
18
+ }
19
+ function withSecondaryLine(columns, primaryField, secondaryField) {
20
+ if (!primaryField || !secondaryField) return columns;
21
+ return columns.map(
22
+ (c) => c.field === primaryField && c.cellRenderer == null ? {
23
+ ...c,
24
+ cellRenderer: chunkMHAO45AL_cjs.TwoLineCellRenderer,
25
+ cellRendererParams: { ...c.cellRendererParams, secondaryField }
26
+ } : c
27
+ );
28
+ }
29
+ function activeRowClassRules(activeRowId, userRules) {
30
+ if (activeRowId == null) return userRules;
31
+ return {
32
+ ...userRules,
33
+ "ag-row-active": (p) => p.data?.id === activeRowId
34
+ };
35
+ }
36
+ var showEmptyReset = (onResetFilters, filtersActive) => Boolean(onResetFilters) && filtersActive !== false;
37
+ function splitEmptyText(text) {
38
+ const t = text.trim();
39
+ const m = /^(.*?[.!?。])\s+(\S[\s\S]*)$/.exec(t);
40
+ const strip = (s) => s.replace(/[.。]$/, "");
41
+ if (!m) return { title: strip(t) };
42
+ return { title: strip(m[1]), description: m[2] };
43
+ }
44
+ function LoadingState({ className, lines = 3 }) {
45
+ return /* @__PURE__ */ jsxRuntime.jsxs(
46
+ "div",
47
+ {
48
+ role: "status",
49
+ "aria-label": "\uBD88\uB7EC\uC624\uB294 \uC911",
50
+ className: chunkMHAO45AL_cjs.cn("flex flex-col gap-[12px]", className),
51
+ children: [
52
+ /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.Skeleton, { className: "h-[24px] w-[240px]" }),
53
+ Array.from({ length: lines }, (_, i) => /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.Skeleton, { className: chunkMHAO45AL_cjs.cn("h-[16px]", i % 2 ? "w-3/5" : "w-full") }, i))
54
+ ]
55
+ }
56
+ );
57
+ }
58
+ function StateBox({
59
+ role,
60
+ className,
61
+ icon,
62
+ iconClassName,
63
+ title,
64
+ description,
65
+ action
66
+ }) {
67
+ return /* @__PURE__ */ jsxRuntime.jsxs(
68
+ "div",
69
+ {
70
+ role,
71
+ className: chunkMHAO45AL_cjs.cn(
72
+ "flex flex-col items-center justify-center gap-[4px] text-center text-sys-text-subtler",
73
+ className
74
+ ),
75
+ children: [
76
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, className: chunkMHAO45AL_cjs.cn("mb-[4px] flex text-sys-text-subtler", iconClassName), children: icon }),
77
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-h5 text-sys-text-main", children: title }),
78
+ description ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "break-keep text-base-regular text-sys-text-subtle wrap-break-word", children: description }) : null,
79
+ action ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pointer-events-auto mt-[12px] flex gap-[8px]", children: action }) : null
80
+ ]
81
+ }
82
+ );
83
+ }
84
+ function EmptyState({
85
+ className,
86
+ children,
87
+ icon,
88
+ title,
89
+ description,
90
+ action
91
+ }) {
92
+ return /* @__PURE__ */ jsxRuntime.jsx(
93
+ StateBox,
94
+ {
95
+ role: "status",
96
+ className,
97
+ icon: icon ?? /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.EmptyBoxIllustration, {}),
98
+ title: title ?? children,
99
+ description,
100
+ action
101
+ }
102
+ );
103
+ }
104
+ function ErrorState({
105
+ className,
106
+ title = "\uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4",
107
+ description,
108
+ onRetry,
109
+ retryLabel = "\uB2E4\uC2DC \uC2DC\uB3C4"
110
+ }) {
111
+ return /* @__PURE__ */ jsxRuntime.jsx(
112
+ StateBox,
113
+ {
114
+ role: "alert",
115
+ className,
116
+ icon: /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.LucideIcon, { iconComponentName: "CircleAlert", size: 36 }),
117
+ iconClassName: "text-danger30",
118
+ title,
119
+ description,
120
+ action: onRetry ? /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.Button, { type: "button", variant: "primary", onClick: onRetry, children: retryLabel }) : null
121
+ }
122
+ );
123
+ }
124
+ var DETAIL_WIDTH = {
125
+ small: "w-[360px]",
126
+ medium: "w-[480px]"
127
+ };
128
+ var QUICK_ALL = "__all__";
129
+ var isEmptyContent = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && !react.isValidElement(v) && "title" in v;
130
+ function resolveEmptyContent(emptyState) {
131
+ if (emptyState == null) return splitEmptyText("\uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
132
+ if (typeof emptyState === "string") return splitEmptyText(emptyState);
133
+ if (isEmptyContent(emptyState)) return emptyState;
134
+ return { children: emptyState };
135
+ }
136
+ function ListPage({
137
+ title,
138
+ description,
139
+ titleRight,
140
+ actions,
141
+ filters,
142
+ onResetFilters,
143
+ columns,
144
+ rows,
145
+ loading,
146
+ error,
147
+ emptyState,
148
+ pagination,
149
+ filtersActive,
150
+ primaryField,
151
+ secondaryField,
152
+ tableTitle,
153
+ quickFilter,
154
+ activeRowId,
155
+ detail,
156
+ onSortChange,
157
+ onRowClick,
158
+ tableProps
159
+ }) {
160
+ const onCloseRef = react.useRef(detail?.onClose);
161
+ react.useEffect(() => {
162
+ onCloseRef.current = detail?.onClose;
163
+ }, [detail?.onClose]);
164
+ const detailOpen = detail != null;
165
+ react.useEffect(() => {
166
+ if (!detailOpen) return;
167
+ const onKey = (e) => {
168
+ if (e.key !== "Escape" || e.defaultPrevented) return;
169
+ const target = e.target;
170
+ if (target?.closest?.('[role="dialog"],[role="alertdialog"]')) return;
171
+ onCloseRef.current?.();
172
+ };
173
+ document.addEventListener("keydown", onKey);
174
+ return () => document.removeEventListener("keydown", onKey);
175
+ }, [detailOpen]);
176
+ const resetButton = onResetFilters ? /* @__PURE__ */ jsxRuntime.jsxs(
177
+ chunkMHAO45AL_cjs.Button,
178
+ {
179
+ type: "button",
180
+ variant: "secondaryOutline",
181
+ onClick: onResetFilters,
182
+ className: chunkMHAO45AL_cjs.cn(filtersActive === false && "invisible"),
183
+ "aria-hidden": filtersActive === false || void 0,
184
+ children: [
185
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, { size: 16, "aria-hidden": true }),
186
+ "\uD544\uD130 \uCD08\uAE30\uD654"
187
+ ]
188
+ }
189
+ ) : null;
190
+ const hasCaption = tableTitle != null || quickFilter != null;
191
+ const filterRow = filters || resetButton && !hasCaption ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex w-full items-start gap-[8px]", children: [
192
+ filters,
193
+ !hasCaption ? resetButton : null
194
+ ] }) : void 0;
195
+ const columnDefs = react.useMemo(() => {
196
+ const primary = resolvePrimaryField(
197
+ primaryField,
198
+ Boolean(onRowClick) || Boolean(secondaryField)
199
+ );
200
+ return withSecondaryLine(withPrimaryColumn(columns, primary), primary, secondaryField);
201
+ }, [columns, primaryField, onRowClick, secondaryField]);
202
+ const total = pagination?.total;
203
+ const quickAllCount = quickFilter?.options.every((o) => o.count !== void 0) ? quickFilter.options.reduce((sum, o) => sum + (o.count ?? 0), 0) : void 0;
204
+ const quickIsAll = (quickFilter?.value.length ?? 0) === 0;
205
+ const quick = quickFilter ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-[8px]", children: [
206
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-small-regular font-medium text-sys-text-subtle", children: quickFilter.label }),
207
+ /* @__PURE__ */ jsxRuntime.jsxs(
208
+ chunkMHAO45AL_cjs.ToggleGroup,
209
+ {
210
+ type: "multiple",
211
+ variant: "outline",
212
+ spacing: true,
213
+ "aria-label": quickFilter.label,
214
+ value: quickIsAll ? [QUICK_ALL] : quickFilter.value,
215
+ onValueChange: (next) => {
216
+ const pressedAll = next.includes(QUICK_ALL) && !quickIsAll;
217
+ quickFilter.onChange(pressedAll ? [] : next.filter((v) => v !== QUICK_ALL));
218
+ },
219
+ children: [
220
+ /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.ToggleGroupItem, { value: QUICK_ALL, count: quickAllCount, countVariant: "primary", children: "\uC804\uCCB4" }),
221
+ quickFilter.options.map((o) => /* @__PURE__ */ jsxRuntime.jsx(
222
+ chunkMHAO45AL_cjs.ToggleGroupItem,
223
+ {
224
+ value: o.value,
225
+ count: o.count,
226
+ countVariant: o.countVariant,
227
+ children: o.label
228
+ },
229
+ o.value
230
+ ))
231
+ ]
232
+ }
233
+ )
234
+ ] }) : null;
235
+ const caption = hasCaption ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-[8px] flex items-center justify-between gap-[8px]", children: [
236
+ tableTitle != null || quick ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-[16px]", children: [
237
+ tableTitle != null ? /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-h6 text-sys-text-main", children: tableTitle }) : null,
238
+ quick
239
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("span", {}),
240
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-[16px]", children: [
241
+ total != null && quickAllCount === void 0 ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-small-regular text-sys-text-subtle", children: [
242
+ "\uCD1D ",
243
+ total.toLocaleString(),
244
+ "\uAC1C"
245
+ ] }) : null,
246
+ resetButton
247
+ ] })
248
+ ] }) : null;
249
+ const resetRef = react.useRef(onResetFilters);
250
+ react.useEffect(() => {
251
+ resetRef.current = onResetFilters;
252
+ }, [onResetFilters]);
253
+ const withReset = showEmptyReset(onResetFilters, filtersActive);
254
+ const EmptyOverlay = react.useMemo(
255
+ () => function ListPageEmptyOverlay() {
256
+ const content = resolveEmptyContent(emptyState);
257
+ return /* @__PURE__ */ jsxRuntime.jsx(
258
+ EmptyState,
259
+ {
260
+ className: "pt-[32px] pb-[16px]",
261
+ title: content.title,
262
+ description: content.description ?? content.children,
263
+ action: withReset && content.children == null ? /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.Button, { type: "button", variant: "secondaryLink", onClick: () => resetRef.current?.(), children: "\uD544\uD130 \uCD08\uAE30\uD654" }) : null
264
+ }
265
+ );
266
+ },
267
+ [emptyState, withReset]
268
+ );
269
+ const rowClassRules = react.useMemo(
270
+ () => activeRowClassRules(activeRowId, tableProps?.rowClassRules),
271
+ [activeRowId, tableProps?.rowClassRules]
272
+ );
273
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full", children: [
274
+ /* @__PURE__ */ jsxRuntime.jsx(
275
+ chunkMHAO45AL_cjs.PageHeader,
276
+ {
277
+ title,
278
+ description,
279
+ titleRight,
280
+ actions,
281
+ bottomRight: filterRow
282
+ }
283
+ ),
284
+ /* @__PURE__ */ jsxRuntime.jsx("section", { "aria-label": title, className: "px-[24px] py-[32px]", children: error ? /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.Alert, { variant: "danger", children: /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.AlertDescription, { children: error }) }) : (
285
+ // 상세 패널(detail)이 있으면 표 쪽이 줄고 패널이 오른쪽에 나란히 선다 — 목록 맥락을 유지한 채 상세를 본다
286
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-[24px]", children: [
287
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
288
+ caption,
289
+ /* @__PURE__ */ jsxRuntime.jsx(
290
+ chunkMHAO45AL_cjs.AgGridTable,
291
+ {
292
+ rowData: rows,
293
+ columnDefs,
294
+ loading,
295
+ noRowsOverlayComponent: EmptyOverlay,
296
+ onSortChanged: onSortChange ? (e) => onSortChange(chunkMHAO45AL_cjs.getSortedColumnState(e).value) : void 0,
297
+ onRowClicked: onRowClick ? (e) => {
298
+ if (e.data) onRowClick(e.data);
299
+ } : void 0,
300
+ domLayout: "autoHeight",
301
+ ...secondaryField ? { rowHeight: 56 } : {},
302
+ ...tableProps,
303
+ rowClassRules
304
+ }
305
+ ),
306
+ pagination ? /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.TablePagination, { children: /* @__PURE__ */ jsxRuntime.jsx(
307
+ chunkMHAO45AL_cjs.Pagination,
308
+ {
309
+ total: pagination.total,
310
+ rowsPerPage: pagination.pageSize,
311
+ page: pagination.page,
312
+ onChange: pagination.onChange,
313
+ showLabel: true
314
+ }
315
+ ) }) : null
316
+ ] }),
317
+ detail ? (
318
+ // 나란한 상세 패널: 제목·설명·닫기 → 본문 → (안내) → 버튼 줄. 스크롤해도 따라오게 sticky. Card 와 같은 테두리·라운드(그림자 없음).
319
+ // <aside>(complementary) 는 다른 landmark 안에 둘 수 없으므로(axe landmark-complementary-is-top-level) 이름 있는 section — DetailPage 의 aside 와 같다
320
+ /* @__PURE__ */ jsxRuntime.jsx(
321
+ chunkMHAO45AL_cjs.Panel,
322
+ {
323
+ title: detail.title,
324
+ description: detail.description,
325
+ padding: "large",
326
+ titleRight: /* @__PURE__ */ jsxRuntime.jsx(
327
+ chunkMHAO45AL_cjs.IconButton,
328
+ {
329
+ type: "button",
330
+ variant: "secondary",
331
+ "aria-label": "\uB2EB\uAE30",
332
+ onClick: detail.onClose,
333
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 20 })
334
+ }
335
+ ),
336
+ note: detail.note,
337
+ footer: detail.footer,
338
+ className: chunkMHAO45AL_cjs.cn("sticky top-[24px] shrink-0", DETAIL_WIDTH[detail.width ?? "medium"]),
339
+ children: detail.children
340
+ }
341
+ )
342
+ ) : null
343
+ ] })
344
+ ) })
345
+ ] });
346
+ }
347
+ function MoreMenu({
348
+ items,
349
+ deleteAction
350
+ }) {
351
+ const triggerRef = react.useRef(null);
352
+ const pendingRef = react.useRef(void 0);
353
+ const select = (fn) => () => {
354
+ pendingRef.current = fn;
355
+ };
356
+ return /* @__PURE__ */ jsxRuntime.jsxs(chunkMHAO45AL_cjs.DropdownMenu, { children: [
357
+ /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.IconButton, { ref: triggerRef, variant: "secondary", "aria-label": "\uB354\uBCF4\uAE30", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Ellipsis, { size: 24 }) }) }),
358
+ /* @__PURE__ */ jsxRuntime.jsxs(
359
+ chunkMHAO45AL_cjs.DropdownMenuContent,
360
+ {
361
+ align: "end",
362
+ onCloseAutoFocus: (e) => {
363
+ e.preventDefault();
364
+ triggerRef.current?.focus();
365
+ const fn = pendingRef.current;
366
+ pendingRef.current = void 0;
367
+ fn?.();
368
+ },
369
+ children: [
370
+ items.map(({ children, onClick, disabled }, i) => /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.DropdownMenuItem, { disabled, onSelect: select(onClick), children }, i)),
371
+ deleteAction && items.length ? /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.DropdownMenuSeparator, {}) : null,
372
+ deleteAction ? /* @__PURE__ */ jsxRuntime.jsx(
373
+ chunkMHAO45AL_cjs.DropdownMenuItem,
374
+ {
375
+ variant: "danger",
376
+ disabled: deleteAction.disabled,
377
+ onSelect: select(deleteAction.onClick),
378
+ children: deleteAction.children
379
+ }
380
+ ) : null
381
+ ]
382
+ }
383
+ )
384
+ ] });
385
+ }
386
+ function DetailPage({
387
+ primaryAction,
388
+ secondaryActions,
389
+ deleteAction,
390
+ actions,
391
+ actionsNote,
392
+ banner,
393
+ loading,
394
+ error,
395
+ onRetry,
396
+ notFound,
397
+ aside,
398
+ asideTitle = "\uAE30\uBCF8 \uC815\uBCF4",
399
+ width = "full",
400
+ children,
401
+ ...header
402
+ }) {
403
+ let body;
404
+ if (loading) {
405
+ body = /* @__PURE__ */ jsxRuntime.jsx(LoadingState, {});
406
+ } else if (error) {
407
+ body = /* @__PURE__ */ jsxRuntime.jsx(ErrorState, { className: "py-[32px]", description: error, onRetry });
408
+ } else if (notFound) {
409
+ body = /* @__PURE__ */ jsxRuntime.jsx(
410
+ EmptyState,
411
+ {
412
+ className: "py-[32px]",
413
+ title: notFound === true ? "\uD56D\uBAA9\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4." : notFound,
414
+ description: notFound === true ? "\uC0AD\uC81C\uB418\uC5C8\uAC70\uB098 \uC8FC\uC18C\uAC00 \uC798\uBABB\uB418\uC5C8\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4." : void 0
415
+ }
416
+ );
417
+ } else if (aside) {
418
+ body = // 넓은 화면: 본문 2 : 패널 1 + 세로선. 좁은 화면: 세로로 쌓이고 패널은 제목 + 가로선으로 시작한다
419
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-[24px] xl:grid-cols-[2fr_1fr] xl:items-start", children: [
420
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0", children }),
421
+ /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.Panel, { title: asideTitle, children: aside })
422
+ ] });
423
+ } else {
424
+ body = children;
425
+ }
426
+ const VISIBLE_SECONDARY = 2;
427
+ const visibleSecondary = secondaryActions?.slice(0, VISIBLE_SECONDARY) ?? [];
428
+ const overflowSecondary = secondaryActions?.slice(VISIBLE_SECONDARY) ?? [];
429
+ const primaryButton = primaryAction ? /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.Button, { type: "button", variant: "primaryBold", ...primaryAction }) : null;
430
+ const moreMenu = (items) => items.length || deleteAction ? /* @__PURE__ */ jsxRuntime.jsx(MoreMenu, { items, deleteAction }) : null;
431
+ const fullRow = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
432
+ moreMenu(overflowSecondary),
433
+ visibleSecondary.map((action, i) => /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.Button, { type: "button", variant: "secondary", ...action }, i)),
434
+ primaryButton
435
+ ] });
436
+ const collapsedRow = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
437
+ moreMenu(secondaryActions ?? []),
438
+ primaryButton
439
+ ] });
440
+ const collapsible = Boolean(deleteAction || secondaryActions?.length);
441
+ const note = actionsNote != null ? /* @__PURE__ */ jsxRuntime.jsx(
442
+ "p",
443
+ {
444
+ "data-slot": "detail-actions-note",
445
+ className: "mb-[8px] break-keep text-right text-small-regular text-sys-text-subtle wrap-break-word",
446
+ children: actionsNote
447
+ }
448
+ ) : null;
449
+ const actionRow = actions ? actions : collapsible ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
450
+ /* @__PURE__ */ jsxRuntime.jsx("div", { "data-slot": "detail-actions-full", className: "hidden items-center gap-[8px] @3xl:flex", children: fullRow }),
451
+ /* @__PURE__ */ jsxRuntime.jsx("div", { "data-slot": "detail-actions-collapsed", className: "flex items-center gap-[8px] @3xl:hidden", children: collapsedRow })
452
+ ] }) : primaryButton ? primaryButton : void 0;
453
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full", children: [
454
+ /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.PageHeader, { ...header, actions: actionRow }),
455
+ /* @__PURE__ */ jsxRuntime.jsxs(
456
+ "section",
457
+ {
458
+ "aria-label": header.title || "\uBCF8\uBB38",
459
+ className: "flex flex-col gap-[24px] px-[24px] py-[24px]",
460
+ children: [
461
+ banner,
462
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chunkMHAO45AL_cjs.cn("w-full", width === "panel" && "mx-auto max-w-[900px]"), children: [
463
+ note,
464
+ body
465
+ ] })
466
+ ]
467
+ }
468
+ )
469
+ ] });
470
+ }
471
+ function useFormShell({
472
+ form,
473
+ onSubmit,
474
+ submitting,
475
+ submitMode = "always"
476
+ }) {
477
+ const { isValid, isSubmitting } = form.formState;
478
+ const busy = Boolean(submitting) || isSubmitting;
479
+ const handleSubmit = form.handleSubmit((values) => onSubmit(values));
480
+ return { busy, canSubmit: !busy && (submitMode === "always" || isValid), handleSubmit };
481
+ }
482
+ function FormError({ error, className }) {
483
+ if (!error) return null;
484
+ return /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.Alert, { variant: "danger", className, children: /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.AlertDescription, { children: error }) });
485
+ }
486
+ function FormActions({
487
+ busy,
488
+ canSubmit,
489
+ onCancel,
490
+ submitLabel = "\uC800\uC7A5",
491
+ cancelLabel = "\uCDE8\uC18C",
492
+ formId
493
+ }) {
494
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
495
+ onCancel ? /* @__PURE__ */ jsxRuntime.jsx(
496
+ chunkMHAO45AL_cjs.Button,
497
+ {
498
+ type: "button",
499
+ variant: "secondaryOutline",
500
+ onClick: onCancel,
501
+ disabled: busy,
502
+ form: formId,
503
+ children: cancelLabel
504
+ }
505
+ ) : null,
506
+ /* @__PURE__ */ jsxRuntime.jsx(
507
+ chunkMHAO45AL_cjs.Button,
508
+ {
509
+ type: "submit",
510
+ variant: "primaryBold",
511
+ loading: busy,
512
+ disabled: !canSubmit,
513
+ form: formId,
514
+ children: submitLabel
515
+ }
516
+ )
517
+ ] });
518
+ }
519
+ function FormPage({
520
+ title,
521
+ description,
522
+ breadcrumb,
523
+ titleRight,
524
+ form,
525
+ onSubmit,
526
+ onCancel,
527
+ submitLabel,
528
+ cancelLabel,
529
+ submitting,
530
+ submitMode,
531
+ error,
532
+ requiredLegend,
533
+ canvas = "plain",
534
+ align = "start",
535
+ aside,
536
+ asideTitle = "\uAC80\uD1A0",
537
+ children
538
+ }) {
539
+ const { busy, canSubmit, handleSubmit } = useFormShell({ form, onSubmit, submitting, submitMode });
540
+ const formId = react.useId();
541
+ const contentMaxWidth = aside ? "var(--max-w-page-content)" : "var(--max-w-form-control)";
542
+ const actions = /* @__PURE__ */ jsxRuntime.jsx(
543
+ FormActions,
544
+ {
545
+ busy,
546
+ canSubmit,
547
+ onCancel,
548
+ submitLabel,
549
+ cancelLabel,
550
+ formId
551
+ }
552
+ );
553
+ const body = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
554
+ requiredLegend ? /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-small-regular text-sys-text-subtle", children: [
555
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, className: "text-danger30", children: "*" }),
556
+ " ",
557
+ "\uD45C\uC2DC\uB294 \uD544\uC218 \uC785\uB825 \uD56D\uBAA9\uC785\uB2C8\uB2E4"
558
+ ] }) : null,
559
+ /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.Form, { ...form, children: /* @__PURE__ */ jsxRuntime.jsxs("form", { id: formId, noValidate: true, onSubmit: handleSubmit, children: [
560
+ /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.FormSectionLevelContext.Provider, { value: 2, children }),
561
+ /* @__PURE__ */ jsxRuntime.jsx(FormError, { error, className: "mt-[24px]" }),
562
+ aside ? null : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsx(
563
+ chunkMHAO45AL_cjs.FormFooter,
564
+ {
565
+ align: "end",
566
+ className: "mt-[24px] border-t border-sys-divider-subtle pt-[16px]",
567
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-[8px]", children: actions })
568
+ }
569
+ ) })
570
+ ] }) })
571
+ ] });
572
+ const column = canvas === "subtle" ? (
573
+ // 폼 전체를 흰 Card 하나로(섹션 사이 구분선은 그대로). 위 8px 는 첫 FormSection 의 pt-[16px] 과 더해 24
574
+ /* @__PURE__ */ jsxRuntime.jsx(
575
+ chunkMHAO45AL_cjs.Card,
576
+ {
577
+ className: chunkMHAO45AL_cjs.cn(
578
+ "w-full max-w-[var(--max-w-form-control)] px-[24px] pb-[24px] pt-[8px]",
579
+ align === "center" && !aside && "mx-auto"
580
+ ),
581
+ children: body
582
+ }
583
+ )
584
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
585
+ "div",
586
+ {
587
+ className: chunkMHAO45AL_cjs.cn(
588
+ "w-full max-w-[var(--max-w-form-control)]",
589
+ align === "center" && !aside && "mx-auto"
590
+ ),
591
+ children: body
592
+ }
593
+ );
594
+ return (
595
+ // subtle 의 캔버스 회색은 기존 토큰(grayscale20, 진단 18-A 의 f7 변형)을 쓴다 — 채택되면 전용 sys 토큰으로 올린다
596
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chunkMHAO45AL_cjs.cn("w-full", canvas === "subtle" && "bg-sys-background-hover"), children: [
597
+ /* @__PURE__ */ jsxRuntime.jsx(
598
+ chunkMHAO45AL_cjs.PageHeader,
599
+ {
600
+ title,
601
+ description,
602
+ breadcrumb,
603
+ titleRight,
604
+ contentMaxWidth
605
+ }
606
+ ),
607
+ /* @__PURE__ */ jsxRuntime.jsx("section", { "aria-label": title || "\uBCF8\uBB38", className: "px-[24px] py-[32px]", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-auto w-full", style: { maxWidth: contentMaxWidth }, children: aside ? (
608
+ // 폼 열(최대 720px) + 검토 패널. 넓은 화면: 옆에 세로선, sticky. 좁은 화면: 폼 아래에 가로선으로 시작.
609
+ // <aside>(complementary) 는 다른 landmark 안에 둘 수 없으므로(axe) 이름 있는 section (DetailPage aside 와 같은 꼴)
610
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-[24px] xl:grid-cols-[minmax(0,var(--max-w-form-control))_minmax(280px,1fr)] xl:items-start", children: [
611
+ column,
612
+ /* @__PURE__ */ jsxRuntime.jsxs(chunkMHAO45AL_cjs.Panel, { title: asideTitle, className: "xl:sticky xl:top-[24px]", children: [
613
+ aside,
614
+ /* @__PURE__ */ jsxRuntime.jsx(
615
+ "div",
616
+ {
617
+ "data-slot": "form-page-aside-actions",
618
+ className: "mt-[16px] flex justify-end gap-[8px] border-t border-sys-divider-subtle pt-[16px]",
619
+ children: actions
620
+ }
621
+ )
622
+ ] })
623
+ ] })
624
+ ) : column }) })
625
+ ] })
626
+ );
627
+ }
628
+ function FormDrawer({
629
+ open,
630
+ onOpenChange,
631
+ side,
632
+ size,
633
+ title,
634
+ description,
635
+ form,
636
+ onSubmit,
637
+ submitLabel,
638
+ cancelLabel,
639
+ submitting,
640
+ error,
641
+ submitMode,
642
+ children
643
+ }) {
644
+ const { busy, canSubmit, handleSubmit } = useFormShell({ form, onSubmit, submitting, submitMode });
645
+ const guardedOpenChange = (next) => {
646
+ if (!next && busy) return;
647
+ onOpenChange(next);
648
+ };
649
+ return /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.Drawer, { open, onOpenChange: guardedOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(
650
+ chunkMHAO45AL_cjs.DrawerContent,
651
+ {
652
+ side,
653
+ size,
654
+ ...description ? {} : { "aria-describedby": void 0 },
655
+ children: [
656
+ /* @__PURE__ */ jsxRuntime.jsxs(chunkMHAO45AL_cjs.DrawerHeader, { children: [
657
+ /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.DrawerTitle, { children: title }),
658
+ description ? /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.DrawerDescription, { children: description }) : null
659
+ ] }),
660
+ /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.Form, { ...form, children: /* @__PURE__ */ jsxRuntime.jsxs("form", { noValidate: true, onSubmit: handleSubmit, className: "flex min-h-0 flex-1 flex-col", children: [
661
+ /* @__PURE__ */ jsxRuntime.jsxs(chunkMHAO45AL_cjs.DrawerBody, { className: "px-[24px]", children: [
662
+ children,
663
+ /* @__PURE__ */ jsxRuntime.jsx(FormError, { error, className: "mt-[24px]" })
664
+ ] }),
665
+ /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.DrawerFooter, { children: /* @__PURE__ */ jsxRuntime.jsx(
666
+ FormActions,
667
+ {
668
+ busy,
669
+ canSubmit,
670
+ onCancel: () => guardedOpenChange(false),
671
+ submitLabel,
672
+ cancelLabel
673
+ }
674
+ ) })
675
+ ] }) })
676
+ ]
677
+ }
678
+ ) });
679
+ }
680
+ function FormDialog({
681
+ open,
682
+ onOpenChange,
683
+ width,
684
+ title,
685
+ description,
686
+ form,
687
+ onSubmit,
688
+ submitLabel,
689
+ cancelLabel,
690
+ submitting,
691
+ error,
692
+ submitMode,
693
+ children
694
+ }) {
695
+ const { busy, canSubmit, handleSubmit } = useFormShell({ form, onSubmit, submitting, submitMode });
696
+ const guardedOpenChange = (next) => {
697
+ if (!next && busy) return;
698
+ onOpenChange(next);
699
+ };
700
+ return /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.Dialog, { open, onOpenChange: guardedOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(
701
+ chunkMHAO45AL_cjs.DialogContent,
702
+ {
703
+ width,
704
+ hideCloseButton: busy,
705
+ ...description ? {} : { "aria-describedby": void 0 },
706
+ children: [
707
+ /* @__PURE__ */ jsxRuntime.jsxs(chunkMHAO45AL_cjs.DialogHeader, { children: [
708
+ /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.DialogTitle, { children: title }),
709
+ description ? /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.DialogDescription, { children: description }) : null
710
+ ] }),
711
+ /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.Form, { ...form, children: /* @__PURE__ */ jsxRuntime.jsxs("form", { noValidate: true, onSubmit: handleSubmit, className: "flex flex-col gap-[16px]", children: [
712
+ children,
713
+ /* @__PURE__ */ jsxRuntime.jsx(FormError, { error }),
714
+ /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.DialogFooter, { children: /* @__PURE__ */ jsxRuntime.jsx(
715
+ FormActions,
716
+ {
717
+ busy,
718
+ canSubmit,
719
+ onCancel: () => guardedOpenChange(false),
720
+ submitLabel,
721
+ cancelLabel
722
+ }
723
+ ) })
724
+ ] }) })
725
+ ]
726
+ }
727
+ ) });
728
+ }
729
+ var TONE_TEXT = {
730
+ default: "text-sys-text-main",
731
+ warning: "text-warning40",
732
+ danger: "text-danger30"
733
+ };
734
+ var TONE_BAR = { default: "bg-primary30", warning: "bg-warning30", danger: "bg-danger30" };
735
+ function StatTile({
736
+ label,
737
+ value,
738
+ caption,
739
+ href,
740
+ ratio,
741
+ tone = "default",
742
+ loading
743
+ }) {
744
+ const labelId = react.useId();
745
+ const percent = ratio == null ? null : Math.round(Math.min(Math.max(ratio, 0), 1) * 100);
746
+ return /* @__PURE__ */ jsxRuntime.jsxs(chunkMHAO45AL_cjs.Card, { className: "relative gap-[4px] p-[16px]", children: [
747
+ /* @__PURE__ */ jsxRuntime.jsx("span", { id: labelId, className: "text-h6 text-sys-text-subtle", children: href ? (
748
+ // 타일 전체를 클릭 영역으로: 링크의 ::after 를 Card 크기로 늘린다
749
+ /* @__PURE__ */ jsxRuntime.jsx(
750
+ "a",
751
+ {
752
+ href,
753
+ className: "after:absolute after:inset-0 after:rounded-medium focus-visible:outline-[2px] focus-visible:outline-sys-focus-ring-main",
754
+ children: label
755
+ }
756
+ )
757
+ ) : label }),
758
+ loading ? /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.Skeleton, { role: "status", "aria-label": "\uBD88\uB7EC\uC624\uB294 \uC911", className: "h-[32px] w-[80px]" }) : /* @__PURE__ */ jsxRuntime.jsxs("span", { className: chunkMHAO45AL_cjs.cn("flex items-center gap-[6px] text-h2", TONE_TEXT[tone]), children: [
759
+ tone !== "default" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.TriangleAlert, { "aria-hidden": true, size: 20, className: "shrink-0" }) : null,
760
+ value
761
+ ] }),
762
+ percent != null && !loading ? /* @__PURE__ */ jsxRuntime.jsx(
763
+ "div",
764
+ {
765
+ role: "progressbar",
766
+ "aria-labelledby": labelId,
767
+ "aria-valuemin": 0,
768
+ "aria-valuemax": 100,
769
+ "aria-valuenow": percent,
770
+ className: "mt-[4px] h-[4px] w-full overflow-hidden rounded-full bg-secondary-o10",
771
+ children: /* @__PURE__ */ jsxRuntime.jsx(
772
+ "div",
773
+ {
774
+ className: chunkMHAO45AL_cjs.cn("h-full rounded-full", TONE_BAR[tone]),
775
+ style: { width: `${percent}%` }
776
+ }
777
+ )
778
+ }
779
+ ) : null,
780
+ caption ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-small-regular text-sys-text-subtler", children: caption }) : null
781
+ ] });
782
+ }
783
+ function DashboardLayout({
784
+ actions,
785
+ stats,
786
+ loading,
787
+ error,
788
+ onRetry,
789
+ empty,
790
+ children,
791
+ ...header
792
+ }) {
793
+ let body;
794
+ if (error) body = /* @__PURE__ */ jsxRuntime.jsx(ErrorState, { className: "py-[32px]", description: error, onRetry });
795
+ else if (empty)
796
+ body = /* @__PURE__ */ jsxRuntime.jsx(EmptyState, { className: "py-[32px]", children: empty === true ? "\uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." : empty });
797
+ else
798
+ body = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
799
+ stats?.length ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-[12px] xl:grid-cols-4", children: stats.map((stat, i) => /* @__PURE__ */ jsxRuntime.jsx(StatTile, { ...stat, loading }, i)) }) : null,
800
+ children
801
+ ] });
802
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full", children: [
803
+ /* @__PURE__ */ jsxRuntime.jsx(chunkMHAO45AL_cjs.PageHeader, { ...header, actions }),
804
+ /* @__PURE__ */ jsxRuntime.jsx(
805
+ "section",
806
+ {
807
+ "aria-label": header.title || "\uBCF8\uBB38",
808
+ className: "flex flex-col gap-[32px] px-[24px] py-[24px]",
809
+ children: body
810
+ }
811
+ )
812
+ ] });
813
+ }
814
+ function DashboardSection({ title, action, columns, children }) {
815
+ const id = react.useId();
816
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-labelledby": id, className: "flex flex-col gap-[16px]", children: [
817
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-[12px]", children: [
818
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { id, className: "text-h4 text-sys-text-main", children: title }),
819
+ action
820
+ ] }),
821
+ columns ? (
822
+ // 1px 간격 + 구분선 색 배경 = 칸 사이 가로·세로 구분선. 칸(DashboardPanel)은 자기 배경을 가진다
823
+ // 2열에서 칸 수가 홀수면 마지막 칸이 두 열을 차지해 빈 칸이 생기지 않게 한다
824
+ /* @__PURE__ */ jsxRuntime.jsx(
825
+ chunkMHAO45AL_cjs.Card,
826
+ {
827
+ className: chunkMHAO45AL_cjs.cn(
828
+ "grid grid-cols-1 gap-px overflow-hidden bg-sys-divider-subtler",
829
+ columns === 2 && "xl:grid-cols-2 xl:[&>:last-child:nth-child(odd)]:col-span-2"
830
+ ),
831
+ children
832
+ }
833
+ )
834
+ ) : children
835
+ ] });
836
+ }
837
+ function DashboardPanel({
838
+ title,
839
+ value,
840
+ unit,
841
+ stats,
842
+ loading,
843
+ empty,
844
+ children
845
+ }) {
846
+ let body = children;
847
+ if (loading) body = /* @__PURE__ */ jsxRuntime.jsx(LoadingState, { lines: 2 });
848
+ else if (empty)
849
+ body = /* @__PURE__ */ jsxRuntime.jsx(EmptyState, { className: "py-[32px]", children: empty === true ? "\uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." : empty });
850
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-[16px] bg-sys-background p-[24px]", children: [
851
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-end gap-[12px]", children: [
852
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-h6 text-sys-text-subtle", children: title }),
853
+ value !== void 0 && value !== null ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-end gap-[2px]", children: [
854
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-h3 text-sys-text-main", children: value }),
855
+ unit ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-base-regular text-sys-text-subtle", children: unit }) : null
856
+ ] }) : null,
857
+ stats?.map((stat, i) => /* @__PURE__ */ jsxRuntime.jsxs(
858
+ "span",
859
+ {
860
+ className: "flex items-end gap-[4px] border-l border-sys-divider-subtler pl-[12px]",
861
+ children: [
862
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-base-regular text-sys-text-subtle", children: stat.label }),
863
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-h5 text-sys-text-main", children: stat.value })
864
+ ]
865
+ },
866
+ i
867
+ ))
868
+ ] }),
869
+ body
870
+ ] });
871
+ }
872
+
873
+ exports.DashboardLayout = DashboardLayout;
874
+ exports.DashboardPanel = DashboardPanel;
875
+ exports.DashboardSection = DashboardSection;
876
+ exports.DetailPage = DetailPage;
877
+ exports.EmptyState = EmptyState;
878
+ exports.ErrorState = ErrorState;
879
+ exports.FormDialog = FormDialog;
880
+ exports.FormDrawer = FormDrawer;
881
+ exports.FormPage = FormPage;
882
+ exports.ListPage = ListPage;
883
+ exports.LoadingState = LoadingState;
884
+ //# sourceMappingURL=index.cjs.map
885
+ //# sourceMappingURL=index.cjs.map