@t3q-ui/design-system 1.57.0 → 1.58.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/CHANGELOG.md +8 -0
- package/dist/{chunk-Z2SHCDPP.cjs → chunk-IYDQLTRF.cjs} +6 -5
- package/dist/chunk-IYDQLTRF.cjs.map +1 -0
- package/dist/{chunk-LEHEPAQO.js → chunk-UNNNNYKG.js} +6 -5
- package/dist/chunk-UNNNNYKG.js.map +1 -0
- package/dist/{index-ZIfOVixA.d.cts → index-3laRfJM7.d.cts} +2 -1
- package/dist/{index-ZIfOVixA.d.ts → index-3laRfJM7.d.ts} +2 -1
- package/dist/index.cjs +457 -457
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/patterns/index.cjs +69 -69
- package/dist/patterns/index.d.cts +1 -1
- package/dist/patterns/index.d.ts +1 -1
- package/dist/patterns/index.js +1 -1
- package/package.json +1 -1
- package/skills/tds/SKILL.md +6 -5
- package/skills/tds/examples/detail-page.tsx +13 -13
- package/skills/tds/examples/list-page.tsx +3 -2
- package/skills/tds/reference/components.md +2 -1
- package/dist/chunk-LEHEPAQO.js.map +0 -1
- package/dist/chunk-Z2SHCDPP.cjs.map +0 -1
package/dist/patterns/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkIYDQLTRF_cjs = require('../chunk-IYDQLTRF.cjs');
|
|
4
4
|
var react = require('react');
|
|
5
5
|
var lucideReact = require('lucide-react');
|
|
6
6
|
var jsxRuntime = require('react/jsx-runtime');
|
|
@@ -21,7 +21,7 @@ function withSecondaryLine(columns, primaryField, secondaryField) {
|
|
|
21
21
|
return columns.map(
|
|
22
22
|
(c) => c.field === primaryField && c.cellRenderer == null ? {
|
|
23
23
|
...c,
|
|
24
|
-
cellRenderer:
|
|
24
|
+
cellRenderer: chunkIYDQLTRF_cjs.TwoLineCellRenderer,
|
|
25
25
|
cellRendererParams: { ...c.cellRendererParams, secondaryField }
|
|
26
26
|
} : c
|
|
27
27
|
);
|
|
@@ -47,10 +47,10 @@ function LoadingState({ className, lines = 3 }) {
|
|
|
47
47
|
{
|
|
48
48
|
role: "status",
|
|
49
49
|
"aria-label": "\uBD88\uB7EC\uC624\uB294 \uC911",
|
|
50
|
-
className:
|
|
50
|
+
className: chunkIYDQLTRF_cjs.cn("flex flex-col gap-[12px]", className),
|
|
51
51
|
children: [
|
|
52
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
53
|
-
Array.from({ length: lines }, (_, i) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
52
|
+
/* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.Skeleton, { className: "h-[24px] w-[240px]" }),
|
|
53
|
+
Array.from({ length: lines }, (_, i) => /* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.Skeleton, { className: chunkIYDQLTRF_cjs.cn("h-[16px]", i % 2 ? "w-3/5" : "w-full") }, i))
|
|
54
54
|
]
|
|
55
55
|
}
|
|
56
56
|
);
|
|
@@ -68,12 +68,12 @@ function StateBox({
|
|
|
68
68
|
"div",
|
|
69
69
|
{
|
|
70
70
|
role,
|
|
71
|
-
className:
|
|
71
|
+
className: chunkIYDQLTRF_cjs.cn(
|
|
72
72
|
"flex flex-col items-center justify-center gap-[4px] text-center text-sys-text-subtler",
|
|
73
73
|
className
|
|
74
74
|
),
|
|
75
75
|
children: [
|
|
76
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, className:
|
|
76
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, className: chunkIYDQLTRF_cjs.cn("mb-[4px] flex text-sys-text-subtler", iconClassName), children: icon }),
|
|
77
77
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-h5 text-sys-text-main", children: title }),
|
|
78
78
|
description ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "break-keep text-base-regular text-sys-text-subtle wrap-break-word", children: description }) : null,
|
|
79
79
|
action ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pointer-events-auto mt-[12px] flex gap-[8px]", children: action }) : null
|
|
@@ -94,7 +94,7 @@ function EmptyState({
|
|
|
94
94
|
{
|
|
95
95
|
role: "status",
|
|
96
96
|
className,
|
|
97
|
-
icon: icon ?? /* @__PURE__ */ jsxRuntime.jsx(
|
|
97
|
+
icon: icon ?? /* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.EmptyBoxIllustration, {}),
|
|
98
98
|
title: title ?? children,
|
|
99
99
|
description,
|
|
100
100
|
action
|
|
@@ -113,11 +113,11 @@ function ErrorState({
|
|
|
113
113
|
{
|
|
114
114
|
role: "alert",
|
|
115
115
|
className,
|
|
116
|
-
icon: /* @__PURE__ */ jsxRuntime.jsx(
|
|
116
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.LucideIcon, { iconComponentName: "CircleAlert", size: 36 }),
|
|
117
117
|
iconClassName: "text-danger30",
|
|
118
118
|
title,
|
|
119
119
|
description,
|
|
120
|
-
action: onRetry ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
120
|
+
action: onRetry ? /* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.Button, { type: "button", variant: "primary", onClick: onRetry, children: retryLabel }) : null
|
|
121
121
|
}
|
|
122
122
|
);
|
|
123
123
|
}
|
|
@@ -174,12 +174,12 @@ function ListPage({
|
|
|
174
174
|
return () => document.removeEventListener("keydown", onKey);
|
|
175
175
|
}, [detailOpen]);
|
|
176
176
|
const resetButton = onResetFilters ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
177
|
-
|
|
177
|
+
chunkIYDQLTRF_cjs.Button,
|
|
178
178
|
{
|
|
179
179
|
type: "button",
|
|
180
180
|
variant: "secondaryOutline",
|
|
181
181
|
onClick: onResetFilters,
|
|
182
|
-
className:
|
|
182
|
+
className: chunkIYDQLTRF_cjs.cn(filtersActive === false && "invisible"),
|
|
183
183
|
"aria-hidden": filtersActive === false || void 0,
|
|
184
184
|
children: [
|
|
185
185
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, { size: 16, "aria-hidden": true }),
|
|
@@ -205,7 +205,7 @@ function ListPage({
|
|
|
205
205
|
const quick = quickFilter ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-[8px]", children: [
|
|
206
206
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-small-regular font-medium text-sys-text-subtle", children: quickFilter.label }),
|
|
207
207
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
208
|
-
|
|
208
|
+
chunkIYDQLTRF_cjs.ToggleGroup,
|
|
209
209
|
{
|
|
210
210
|
type: "multiple",
|
|
211
211
|
variant: "outline",
|
|
@@ -217,9 +217,9 @@ function ListPage({
|
|
|
217
217
|
quickFilter.onChange(pressedAll ? [] : next.filter((v) => v !== QUICK_ALL));
|
|
218
218
|
},
|
|
219
219
|
children: [
|
|
220
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
220
|
+
/* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.ToggleGroupItem, { value: QUICK_ALL, count: quickAllCount, countVariant: "primary", children: "\uC804\uCCB4" }),
|
|
221
221
|
quickFilter.options.map((o) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
222
|
-
|
|
222
|
+
chunkIYDQLTRF_cjs.ToggleGroupItem,
|
|
223
223
|
{
|
|
224
224
|
value: o.value,
|
|
225
225
|
count: o.count,
|
|
@@ -260,7 +260,7 @@ function ListPage({
|
|
|
260
260
|
className: "pt-[32px] pb-[16px]",
|
|
261
261
|
title: content.title,
|
|
262
262
|
description: content.description ?? content.children,
|
|
263
|
-
action: withReset && content.children == null ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
263
|
+
action: withReset && content.children == null ? /* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.Button, { type: "button", variant: "secondaryLink", onClick: () => resetRef.current?.(), children: "\uD544\uD130 \uCD08\uAE30\uD654" }) : null
|
|
264
264
|
}
|
|
265
265
|
);
|
|
266
266
|
},
|
|
@@ -272,7 +272,7 @@ function ListPage({
|
|
|
272
272
|
);
|
|
273
273
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full", children: [
|
|
274
274
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
275
|
-
|
|
275
|
+
chunkIYDQLTRF_cjs.PageHeader,
|
|
276
276
|
{
|
|
277
277
|
title,
|
|
278
278
|
description,
|
|
@@ -281,19 +281,19 @@ function ListPage({
|
|
|
281
281
|
bottomRight: filterRow
|
|
282
282
|
}
|
|
283
283
|
),
|
|
284
|
-
/* @__PURE__ */ jsxRuntime.jsx("section", { "aria-label": title, className: "px-[24px] py-[32px]", children: error ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
284
|
+
/* @__PURE__ */ jsxRuntime.jsx("section", { "aria-label": title, className: "px-[24px] py-[32px]", children: error ? /* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.Alert, { variant: "danger", children: /* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.AlertDescription, { children: error }) }) : (
|
|
285
285
|
// 상세 패널(detail)이 있으면 표 쪽이 줄고 패널이 오른쪽에 나란히 선다 — 목록 맥락을 유지한 채 상세를 본다
|
|
286
286
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-[24px]", children: [
|
|
287
287
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
288
288
|
caption,
|
|
289
289
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
290
|
-
|
|
290
|
+
chunkIYDQLTRF_cjs.AgGridTable,
|
|
291
291
|
{
|
|
292
292
|
rowData: rows,
|
|
293
293
|
columnDefs,
|
|
294
294
|
loading,
|
|
295
295
|
noRowsOverlayComponent: EmptyOverlay,
|
|
296
|
-
onSortChanged: onSortChange ? (e) => onSortChange(
|
|
296
|
+
onSortChanged: onSortChange ? (e) => onSortChange(chunkIYDQLTRF_cjs.getSortedColumnState(e).value) : void 0,
|
|
297
297
|
onRowClicked: onRowClick ? (e) => {
|
|
298
298
|
if (e.data) onRowClick(e.data);
|
|
299
299
|
} : void 0,
|
|
@@ -303,8 +303,8 @@ function ListPage({
|
|
|
303
303
|
rowClassRules
|
|
304
304
|
}
|
|
305
305
|
),
|
|
306
|
-
pagination ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
307
|
-
|
|
306
|
+
pagination ? /* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.TablePagination, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
307
|
+
chunkIYDQLTRF_cjs.Pagination,
|
|
308
308
|
{
|
|
309
309
|
total: pagination.total,
|
|
310
310
|
rowsPerPage: pagination.pageSize,
|
|
@@ -318,13 +318,13 @@ function ListPage({
|
|
|
318
318
|
// 나란한 상세 패널: 제목·설명·닫기 → 본문 → (안내) → 버튼 줄. 스크롤해도 따라오게 sticky. Card 와 같은 테두리·라운드(그림자 없음).
|
|
319
319
|
// <aside>(complementary) 는 다른 landmark 안에 둘 수 없으므로(axe landmark-complementary-is-top-level) 이름 있는 section — DetailPage 의 aside 와 같다
|
|
320
320
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
321
|
-
|
|
321
|
+
chunkIYDQLTRF_cjs.Panel,
|
|
322
322
|
{
|
|
323
323
|
title: detail.title,
|
|
324
324
|
description: detail.description,
|
|
325
325
|
padding: "large",
|
|
326
326
|
titleRight: /* @__PURE__ */ jsxRuntime.jsx(
|
|
327
|
-
|
|
327
|
+
chunkIYDQLTRF_cjs.IconButton,
|
|
328
328
|
{
|
|
329
329
|
type: "button",
|
|
330
330
|
variant: "secondary",
|
|
@@ -335,7 +335,7 @@ function ListPage({
|
|
|
335
335
|
),
|
|
336
336
|
note: detail.note,
|
|
337
337
|
footer: detail.footer,
|
|
338
|
-
className:
|
|
338
|
+
className: chunkIYDQLTRF_cjs.cn("sticky top-[24px] shrink-0", DETAIL_WIDTH[detail.width ?? "medium"]),
|
|
339
339
|
children: detail.children
|
|
340
340
|
}
|
|
341
341
|
)
|
|
@@ -353,10 +353,10 @@ function MoreMenu({
|
|
|
353
353
|
const select = (fn) => () => {
|
|
354
354
|
pendingRef.current = fn;
|
|
355
355
|
};
|
|
356
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
357
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
356
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(chunkIYDQLTRF_cjs.DropdownMenu, { children: [
|
|
357
|
+
/* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.IconButton, { ref: triggerRef, variant: "secondary", "aria-label": "\uB354\uBCF4\uAE30", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Ellipsis, { size: 24 }) }) }),
|
|
358
358
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
359
|
-
|
|
359
|
+
chunkIYDQLTRF_cjs.DropdownMenuContent,
|
|
360
360
|
{
|
|
361
361
|
align: "end",
|
|
362
362
|
onCloseAutoFocus: (e) => {
|
|
@@ -367,10 +367,10 @@ function MoreMenu({
|
|
|
367
367
|
fn?.();
|
|
368
368
|
},
|
|
369
369
|
children: [
|
|
370
|
-
items.map(({ children, onClick, disabled }, i) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
371
|
-
deleteAction && items.length ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
370
|
+
items.map(({ children, onClick, disabled }, i) => /* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.DropdownMenuItem, { disabled, onSelect: select(onClick), children }, i)),
|
|
371
|
+
deleteAction && items.length ? /* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.DropdownMenuSeparator, {}) : null,
|
|
372
372
|
deleteAction ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
373
|
-
|
|
373
|
+
chunkIYDQLTRF_cjs.DropdownMenuItem,
|
|
374
374
|
{
|
|
375
375
|
variant: "danger",
|
|
376
376
|
disabled: deleteAction.disabled,
|
|
@@ -418,7 +418,7 @@ function DetailPage({
|
|
|
418
418
|
body = // 넓은 화면: 본문 2 : 패널 1 + 세로선. 좁은 화면: 세로로 쌓이고 패널은 제목 + 가로선으로 시작한다
|
|
419
419
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-[24px] xl:grid-cols-[2fr_1fr] xl:items-start", children: [
|
|
420
420
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0", children }),
|
|
421
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
421
|
+
/* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.Panel, { title: asideTitle, children: aside })
|
|
422
422
|
] });
|
|
423
423
|
} else {
|
|
424
424
|
body = children;
|
|
@@ -426,11 +426,11 @@ function DetailPage({
|
|
|
426
426
|
const VISIBLE_SECONDARY = 2;
|
|
427
427
|
const visibleSecondary = secondaryActions?.slice(0, VISIBLE_SECONDARY) ?? [];
|
|
428
428
|
const overflowSecondary = secondaryActions?.slice(VISIBLE_SECONDARY) ?? [];
|
|
429
|
-
const primaryButton = primaryAction ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
429
|
+
const primaryButton = primaryAction ? /* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.Button, { type: "button", variant: "primaryBold", ...primaryAction }) : null;
|
|
430
430
|
const moreMenu = (items) => items.length || deleteAction ? /* @__PURE__ */ jsxRuntime.jsx(MoreMenu, { items, deleteAction }) : null;
|
|
431
431
|
const fullRow = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
432
432
|
moreMenu(overflowSecondary),
|
|
433
|
-
visibleSecondary.map((action, i) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
433
|
+
visibleSecondary.map((action, i) => /* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.Button, { type: "button", variant: "secondary", ...action }, i)),
|
|
434
434
|
primaryButton
|
|
435
435
|
] });
|
|
436
436
|
const collapsedRow = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
@@ -451,7 +451,7 @@ function DetailPage({
|
|
|
451
451
|
/* @__PURE__ */ jsxRuntime.jsx("div", { "data-slot": "detail-actions-collapsed", className: "flex items-center gap-[8px] @3xl:hidden", children: collapsedRow })
|
|
452
452
|
] }) : primaryButton ? primaryButton : void 0;
|
|
453
453
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full", children: [
|
|
454
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
454
|
+
/* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.PageHeader, { ...header, actions: actionRow }),
|
|
455
455
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
456
456
|
"section",
|
|
457
457
|
{
|
|
@@ -459,7 +459,7 @@ function DetailPage({
|
|
|
459
459
|
className: "flex flex-col gap-[24px] px-[24px] py-[24px]",
|
|
460
460
|
children: [
|
|
461
461
|
banner,
|
|
462
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className:
|
|
462
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: chunkIYDQLTRF_cjs.cn("w-full", width === "panel" && "mx-auto max-w-[900px]"), children: [
|
|
463
463
|
note,
|
|
464
464
|
body
|
|
465
465
|
] })
|
|
@@ -481,7 +481,7 @@ function useFormShell({
|
|
|
481
481
|
}
|
|
482
482
|
function FormError({ error, className }) {
|
|
483
483
|
if (!error) return null;
|
|
484
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
484
|
+
return /* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.Alert, { variant: "danger", className, children: /* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.AlertDescription, { children: error }) });
|
|
485
485
|
}
|
|
486
486
|
function FormActions({
|
|
487
487
|
busy,
|
|
@@ -493,7 +493,7 @@ function FormActions({
|
|
|
493
493
|
}) {
|
|
494
494
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
495
495
|
onCancel ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
496
|
-
|
|
496
|
+
chunkIYDQLTRF_cjs.Button,
|
|
497
497
|
{
|
|
498
498
|
type: "button",
|
|
499
499
|
variant: "secondaryOutline",
|
|
@@ -504,7 +504,7 @@ function FormActions({
|
|
|
504
504
|
}
|
|
505
505
|
) : null,
|
|
506
506
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
507
|
-
|
|
507
|
+
chunkIYDQLTRF_cjs.Button,
|
|
508
508
|
{
|
|
509
509
|
type: "submit",
|
|
510
510
|
variant: "primaryBold",
|
|
@@ -556,11 +556,11 @@ function FormPage({
|
|
|
556
556
|
" ",
|
|
557
557
|
"\uD45C\uC2DC\uB294 \uD544\uC218 \uC785\uB825 \uD56D\uBAA9\uC785\uB2C8\uB2E4"
|
|
558
558
|
] }) : null,
|
|
559
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
560
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
559
|
+
/* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.Form, { ...form, children: /* @__PURE__ */ jsxRuntime.jsxs("form", { id: formId, noValidate: true, onSubmit: handleSubmit, children: [
|
|
560
|
+
/* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.FormSectionLevelContext.Provider, { value: 2, children }),
|
|
561
561
|
/* @__PURE__ */ jsxRuntime.jsx(FormError, { error, className: "mt-[24px]" }),
|
|
562
562
|
aside ? null : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
563
|
-
|
|
563
|
+
chunkIYDQLTRF_cjs.FormFooter,
|
|
564
564
|
{
|
|
565
565
|
align: "end",
|
|
566
566
|
className: "mt-[24px] border-t border-sys-divider-subtle pt-[16px]",
|
|
@@ -572,9 +572,9 @@ function FormPage({
|
|
|
572
572
|
const column = canvas === "subtle" ? (
|
|
573
573
|
// 폼 전체를 흰 Card 하나로(섹션 사이 구분선은 그대로). 위 8px 는 첫 FormSection 의 pt-[16px] 과 더해 24
|
|
574
574
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
575
|
-
|
|
575
|
+
chunkIYDQLTRF_cjs.Card,
|
|
576
576
|
{
|
|
577
|
-
className:
|
|
577
|
+
className: chunkIYDQLTRF_cjs.cn(
|
|
578
578
|
"w-full max-w-[var(--max-w-form-control)] px-[24px] pb-[24px] pt-[8px]",
|
|
579
579
|
align === "center" && !aside && "mx-auto"
|
|
580
580
|
),
|
|
@@ -584,7 +584,7 @@ function FormPage({
|
|
|
584
584
|
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
585
585
|
"div",
|
|
586
586
|
{
|
|
587
|
-
className:
|
|
587
|
+
className: chunkIYDQLTRF_cjs.cn(
|
|
588
588
|
"w-full max-w-[var(--max-w-form-control)]",
|
|
589
589
|
align === "center" && !aside && "mx-auto"
|
|
590
590
|
),
|
|
@@ -593,9 +593,9 @@ function FormPage({
|
|
|
593
593
|
);
|
|
594
594
|
return (
|
|
595
595
|
// subtle 의 캔버스 회색은 기존 토큰(grayscale20, 진단 18-A 의 f7 변형)을 쓴다 — 채택되면 전용 sys 토큰으로 올린다
|
|
596
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className:
|
|
596
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: chunkIYDQLTRF_cjs.cn("w-full", canvas === "subtle" && "bg-sys-background-hover"), children: [
|
|
597
597
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
598
|
-
|
|
598
|
+
chunkIYDQLTRF_cjs.PageHeader,
|
|
599
599
|
{
|
|
600
600
|
title,
|
|
601
601
|
description,
|
|
@@ -609,7 +609,7 @@ function FormPage({
|
|
|
609
609
|
// <aside>(complementary) 는 다른 landmark 안에 둘 수 없으므로(axe) 이름 있는 section (DetailPage aside 와 같은 꼴)
|
|
610
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
611
|
column,
|
|
612
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
612
|
+
/* @__PURE__ */ jsxRuntime.jsxs(chunkIYDQLTRF_cjs.Panel, { title: asideTitle, className: "xl:sticky xl:top-[24px]", children: [
|
|
613
613
|
aside,
|
|
614
614
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
615
615
|
"div",
|
|
@@ -646,23 +646,23 @@ function FormDrawer({
|
|
|
646
646
|
if (!next && busy) return;
|
|
647
647
|
onOpenChange(next);
|
|
648
648
|
};
|
|
649
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
650
|
-
|
|
649
|
+
return /* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.Drawer, { open, onOpenChange: guardedOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
650
|
+
chunkIYDQLTRF_cjs.DrawerContent,
|
|
651
651
|
{
|
|
652
652
|
side,
|
|
653
653
|
size,
|
|
654
654
|
...description ? {} : { "aria-describedby": void 0 },
|
|
655
655
|
children: [
|
|
656
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
657
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
658
|
-
description ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
656
|
+
/* @__PURE__ */ jsxRuntime.jsxs(chunkIYDQLTRF_cjs.DrawerHeader, { children: [
|
|
657
|
+
/* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.DrawerTitle, { children: title }),
|
|
658
|
+
description ? /* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.DrawerDescription, { children: description }) : null
|
|
659
659
|
] }),
|
|
660
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
661
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
660
|
+
/* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_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(chunkIYDQLTRF_cjs.DrawerBody, { className: "px-[24px]", children: [
|
|
662
662
|
children,
|
|
663
663
|
/* @__PURE__ */ jsxRuntime.jsx(FormError, { error, className: "mt-[24px]" })
|
|
664
664
|
] }),
|
|
665
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
665
|
+
/* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.DrawerFooter, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
666
666
|
FormActions,
|
|
667
667
|
{
|
|
668
668
|
busy,
|
|
@@ -697,21 +697,21 @@ function FormDialog({
|
|
|
697
697
|
if (!next && busy) return;
|
|
698
698
|
onOpenChange(next);
|
|
699
699
|
};
|
|
700
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
701
|
-
|
|
700
|
+
return /* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.Dialog, { open, onOpenChange: guardedOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
701
|
+
chunkIYDQLTRF_cjs.DialogContent,
|
|
702
702
|
{
|
|
703
703
|
width,
|
|
704
704
|
hideCloseButton: busy,
|
|
705
705
|
...description ? {} : { "aria-describedby": void 0 },
|
|
706
706
|
children: [
|
|
707
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
708
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
709
|
-
description ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
707
|
+
/* @__PURE__ */ jsxRuntime.jsxs(chunkIYDQLTRF_cjs.DialogHeader, { children: [
|
|
708
|
+
/* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.DialogTitle, { children: title }),
|
|
709
|
+
description ? /* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.DialogDescription, { children: description }) : null
|
|
710
710
|
] }),
|
|
711
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
711
|
+
/* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.Form, { ...form, children: /* @__PURE__ */ jsxRuntime.jsxs("form", { noValidate: true, onSubmit: handleSubmit, className: "flex flex-col gap-[16px]", children: [
|
|
712
712
|
children,
|
|
713
713
|
/* @__PURE__ */ jsxRuntime.jsx(FormError, { error }),
|
|
714
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
714
|
+
/* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.DialogFooter, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
715
715
|
FormActions,
|
|
716
716
|
{
|
|
717
717
|
busy,
|
|
@@ -743,7 +743,7 @@ function StatTile({
|
|
|
743
743
|
}) {
|
|
744
744
|
const labelId = react.useId();
|
|
745
745
|
const percent = ratio == null ? null : Math.round(Math.min(Math.max(ratio, 0), 1) * 100);
|
|
746
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
746
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(chunkIYDQLTRF_cjs.Card, { className: "relative gap-[4px] p-[16px]", children: [
|
|
747
747
|
/* @__PURE__ */ jsxRuntime.jsx("span", { id: labelId, className: "text-h6 text-sys-text-subtle", children: href ? (
|
|
748
748
|
// 타일 전체를 클릭 영역으로: 링크의 ::after 를 Card 크기로 늘린다
|
|
749
749
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -755,7 +755,7 @@ function StatTile({
|
|
|
755
755
|
}
|
|
756
756
|
)
|
|
757
757
|
) : label }),
|
|
758
|
-
loading ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
758
|
+
loading ? /* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.Skeleton, { role: "status", "aria-label": "\uBD88\uB7EC\uC624\uB294 \uC911", className: "h-[32px] w-[80px]" }) : /* @__PURE__ */ jsxRuntime.jsxs("span", { className: chunkIYDQLTRF_cjs.cn("flex items-center gap-[6px] text-h2", TONE_TEXT[tone]), children: [
|
|
759
759
|
tone !== "default" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.TriangleAlert, { "aria-hidden": true, size: 20, className: "shrink-0" }) : null,
|
|
760
760
|
value
|
|
761
761
|
] }),
|
|
@@ -771,7 +771,7 @@ function StatTile({
|
|
|
771
771
|
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
772
772
|
"div",
|
|
773
773
|
{
|
|
774
|
-
className:
|
|
774
|
+
className: chunkIYDQLTRF_cjs.cn("h-full rounded-full", TONE_BAR[tone]),
|
|
775
775
|
style: { width: `${percent}%` }
|
|
776
776
|
}
|
|
777
777
|
)
|
|
@@ -800,7 +800,7 @@ function DashboardLayout({
|
|
|
800
800
|
children
|
|
801
801
|
] });
|
|
802
802
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full", children: [
|
|
803
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
803
|
+
/* @__PURE__ */ jsxRuntime.jsx(chunkIYDQLTRF_cjs.PageHeader, { ...header, actions }),
|
|
804
804
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
805
805
|
"section",
|
|
806
806
|
{
|
|
@@ -822,9 +822,9 @@ function DashboardSection({ title, action, columns, children }) {
|
|
|
822
822
|
// 1px 간격 + 구분선 색 배경 = 칸 사이 가로·세로 구분선. 칸(DashboardPanel)은 자기 배경을 가진다
|
|
823
823
|
// 2열에서 칸 수가 홀수면 마지막 칸이 두 열을 차지해 빈 칸이 생기지 않게 한다
|
|
824
824
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
825
|
-
|
|
825
|
+
chunkIYDQLTRF_cjs.Card,
|
|
826
826
|
{
|
|
827
|
-
className:
|
|
827
|
+
className: chunkIYDQLTRF_cjs.cn(
|
|
828
828
|
"grid grid-cols-1 gap-px overflow-hidden bg-sys-divider-subtler",
|
|
829
829
|
columns === 2 && "xl:grid-cols-2 xl:[&>:last-child:nth-child(odd)]:col-span-2"
|
|
830
830
|
),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { ReactNode, ComponentProps } from 'react';
|
|
3
3
|
import { ColDef } from 'ag-grid-community';
|
|
4
|
-
import { B as BadgeProps, K as AgGridTableType, g as ButtonProps, G as PageHeaderProps, v as DrawerContent, l as DialogContent } from '../index-
|
|
4
|
+
import { B as BadgeProps, K as AgGridTableType, g as ButtonProps, G as PageHeaderProps, v as DrawerContent, l as DialogContent } from '../index-3laRfJM7.cjs';
|
|
5
5
|
import { FieldValues, UseFormReturn } from 'react-hook-form';
|
|
6
6
|
import 'class-variance-authority';
|
|
7
7
|
import '@radix-ui/react-dialog';
|
package/dist/patterns/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { ReactNode, ComponentProps } from 'react';
|
|
3
3
|
import { ColDef } from 'ag-grid-community';
|
|
4
|
-
import { B as BadgeProps, K as AgGridTableType, g as ButtonProps, G as PageHeaderProps, v as DrawerContent, l as DialogContent } from '../index-
|
|
4
|
+
import { B as BadgeProps, K as AgGridTableType, g as ButtonProps, G as PageHeaderProps, v as DrawerContent, l as DialogContent } from '../index-3laRfJM7.js';
|
|
5
5
|
import { FieldValues, UseFormReturn } from 'react-hook-form';
|
|
6
6
|
import 'class-variance-authority';
|
|
7
7
|
import '@radix-ui/react-dialog';
|
package/dist/patterns/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cn, Skeleton, EmptyBoxIllustration, Button, LucideIcon, ToggleGroup, ToggleGroupItem, PageHeader, Alert, AlertDescription, AgGridTable, getSortedColumnState, TablePagination, Pagination, Panel, IconButton, Form, FormSectionLevelContext, FormFooter, Card, Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription, DrawerBody, DrawerFooter, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, TwoLineCellRenderer, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from '../chunk-
|
|
1
|
+
import { cn, Skeleton, EmptyBoxIllustration, Button, LucideIcon, ToggleGroup, ToggleGroupItem, PageHeader, Alert, AlertDescription, AgGridTable, getSortedColumnState, TablePagination, Pagination, Panel, IconButton, Form, FormSectionLevelContext, FormFooter, Card, Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription, DrawerBody, DrawerFooter, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, TwoLineCellRenderer, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from '../chunk-UNNNNYKG.js';
|
|
2
2
|
import { useRef, useEffect, useMemo, useId, isValidElement } from 'react';
|
|
3
3
|
import { RotateCcw, X, TriangleAlert, Ellipsis } from 'lucide-react';
|
|
4
4
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
package/package.json
CHANGED
package/skills/tds/SKILL.md
CHANGED
|
@@ -44,14 +44,14 @@ license: Proprietary. See LICENSE.md in the @t3q-ui/design-system package
|
|
|
44
44
|
- Pattern 본문은 이미 `<section>` landmark 다. `<main>` 으로 또 감싸지 않는다. 앱의 `<main>` 에는 `p-8`·`max-w-*`·배경을 주지 않는다 — Pattern 이 헤더 띠(화면 끝까지)와 본문 여백(24px)을 가지므로, 감싸면 헤더가 가장자리에서 32px 떠 보인다(2026-10-18 한 줄 판 run). **사이드바·상단바 같은 앱 셸을 새로 그리지 않는다** — 과제는 페이지 하나이고 Pattern 이 헤더(제목·행동)를 그린다. 셸이 꼭 필요하면 `DashboardLayout` 을 쓰고, `fixed` 사이드바로 Pattern 을 덮지 않는다(2026-10-20 17차: Codex 가 그린 사이드바가 ListPage 제목을 덮어 27점).
|
|
45
45
|
- `ListPage` 의 `onResetFilters` 는 **항상** 넘기고, 필터가 없을 때는 `filtersActive={false}` 로 보이지 않게 한다(자리는 유지). 조건부로 넘기면 빈 결과 화면에서 버튼이 튀어나와 배치가 바뀐다. 버튼은 패턴이 빠른 필터 줄 오른쪽에 그린다 — 검색창 옆에 직접 만들지 않는다.
|
|
46
46
|
- `ListPage` 에 `tableTitle`("실행환경 목록" 같은 표 제목)을 넘기지 않는다 — 헤더 제목과 중복이고 빠른 필터 옆에서 빽빽해 보인다(2026-10-18 10차). 빠른 필터 줄에는 칩과 "필터 초기화"만.
|
|
47
|
-
- **라운드는 한 계열** —
|
|
47
|
+
- **라운드는 한 계열** — 뱃지(상태·건수)와 빠른 필터 칩은 pill, 태그 4px, 버튼·입력 6px, 표·카드 8px. 컴포넌트 기본값이 이미 그렇다(`Badge` 기본 pill — 2026-11-06 사용자 결정, 홀드아웃 20 "각진 상태 뱃지 vs 둥근 필터 칩"). 뱃지에 `rounded='square'` 를, 태그·버튼에 `rounded-full`·`shape='rounded'` 를 덧씌워 계열을 섞지 않는다.
|
|
48
48
|
- 행을 눌러 보는 상세는 `ListPage detail={{ title, description, onClose, children, note, footer }}` 로 **목록 오른쪽에 나란히** 연다(표가 좁아지고 목록은 계속 보인다, 닫기 버튼·Esc). `activeRowId={selected?.id ?? null}` 로 열린 행을 표시하고, 상세 필드는 `MetaList` 안에 `MetaRow` 로 세로로 쌓는다. `Stack` 은 기본 방향이 가로라 MetaRow 를 감싸면 옆으로 늘어서 잘린다. 표를 좁히면 안 될 때만 `<Drawer modal={false}>` 옆 패널(덮는 형태)을 쓴다.
|
|
49
49
|
- **`DetailPage asideTitle` 을 주면 패널 안에 같은 제목을 또 쓰지 않는다** — 패턴이 패널 제목을 그리므로 안쪽 `MetaList`·카드에 "작업 정보" 같은 머리글을 반복하면 제목이 연달아 두 번 보인다(2026-10-27 홀드아웃 12, 심판 2건). 패널 안은 항목(`MetaRow`)부터 시작한다.
|
|
50
50
|
- **"예시 데이터로 구성된 페이지입니다" 같은 개발자 면책문·'샘플 데이터' 뱃지를 화면에 넣지 않는다** — 목 데이터임은 코드 주석에만 적는다. 심판은 이를 제품 문구의 결함으로 읽는다(2026-10-27 홀드아웃 12, 문구 차원 감점).
|
|
51
|
-
- **목록의 검색 입력은 `ColumnFilter` + `ColumnFilterInput`**(스토리와 같은 조합). 필터 칩(`TableFilter*`, 채움형) 옆에 `TextField`(외곽선)를 따로 두면 두 모양이 어긋나 감점(2026-10-26 홀드아웃 11, 심판 4건). 표 셀 정렬은 `TableCell align='right'`(`className='text-right'` 도 같은 효과), 숫자 열은 `type: 'number'`(cellClass 를 더해도 정렬이 유지된다), 금액 아래 사용량 같은 두 줄 셀은 `TwoLineCellRenderer`(직접 flex 로 두 줄을 그리면 위·아래로 벌어진다).
|
|
51
|
+
- **목록의 검색 입력은 `ColumnFilter` + `ColumnFilterInput`**(스토리와 같은 조합). 필터 칩(`TableFilter*`, 채움형) 옆에 `TextField`(외곽선)를 따로 두면 두 모양이 어긋나 감점(2026-10-26 홀드아웃 11, 심판 4건). 표 셀 정렬은 `TableCell align='right'`(`className='text-right'` 도 같은 효과), 숫자 열은 `type: 'number'`(cellClass 를 더해도 정렬이 유지된다). **검색창 placeholder 는 무엇으로 찾는지만** — "모델 이름·요청 ID 검색". "…로 검색하거나 조건을 추가하세요" 처럼 조작을 설명하지 않는다(2026-11-06 홀드아웃 20, Codex copy 3/3), 금액 아래 사용량 같은 두 줄 셀은 `TwoLineCellRenderer`(직접 flex 로 두 줄을 그리면 위·아래로 벌어진다).
|
|
52
52
|
- 정렬할 수 있는 열은 `sortable: true` 를 **명시**한다 → 정렬 전에도 아이콘이 보이고, 누르면 방향이 바뀐다. 요청문에 "정렬 가능" 이 있으면 반드시.
|
|
53
53
|
- **검토 패널(`aside`)이 있는 폼의 취소·제출 버튼은 패널 바닥** — FormPage 가 그린다(폼 끝에 따로 두지 않는다). 긴 폼 옆에서 패널이 짧게 끝나고 제출이 멀어 두 심판 모두 감점했다(2026-10-20 16차). 요약 패널에는 입력값 요약과 예상 비용·시간만.
|
|
54
|
-
- **표의 진행률 열은 고정 폭 막대 + % 글자 한 줄** — `<Progress value={p} className='w-[120px]' aria-label={`GPU 사용률 ${p}%`} />` 오른쪽에 `tabular-nums` 로 `{p}%`. **막대마다 `aria-label` 로 '무엇 몇 %' 이름을 준다** — 이름 없는 progressbar 는 행마다 axe serious(17차 파이프라인 목록 21건; DS 가 기본 이름 '46%' 를 붙이지만 뜻이 약하다). 분수(816 / 1,200)는 상세나 툴팁으로. 글자 아래 막대를 쌓으면 행마다 막대 폭이 달라져 감점(2026-10-20 홀드아웃 8 Codex 목록).
|
|
54
|
+
- **표의 진행률 열은 고정 폭 막대 + % 글자 한 줄** — `<Progress value={p} className='w-[120px]' aria-label={`GPU 사용률 ${p}%`} />` 오른쪽에 `tabular-nums` 로 `{p}%`. **막대마다 `aria-label` 로 '무엇 몇 %' 이름을 준다** — 이름 없는 progressbar 는 행마다 axe serious(17차 파이프라인 목록 21건; DS 가 기본 이름 '46%' 를 붙이지만 뜻이 약하다). 분수(816 / 1,200)는 상세나 툴팁으로. 글자 아래 막대를 쌓으면 행마다 막대 폭이 달라져 감점(2026-10-20 홀드아웃 8 Codex 목록). **막대 색은 값의 뜻대로** — 높을수록 좋은 비율(정상 행·통과율·완료율)은 기본(primary)이나 `success`, 낮을수록 좋은 값의 경고 구간만 `warning`·`danger`. 페이지·항목이 실패 상태여도 "정상 99%" 막대를 빨갛게 칠하지 않는다(2026-11-06 홀드아웃 20, 3 run 이 `danger`).
|
|
55
55
|
- 제출 중 상태는 앱이 `const [submitting, setSubmitting] = useState(false)` 로 갖고 `FormPage submitting={submitting}` 에 넘긴다(`onSubmit` 안에서 try/finally). FormPage 가 버튼을 잠그고 스피너를 보이지만, 상태가 코드에 드러나야 중복 제출 방지가 확인된다.
|
|
56
56
|
- 빈 결과는 패턴이 아이콘·제목·설명·"필터 초기화" 버튼 상자로 그린다. `emptyState` 에는 `{ title: '조건에 맞는 모델이 없습니다', description: '검색어나 필터를 바꿔 보세요.' }` 또는 두 문장 문자열("…없습니다. …해 보세요.")만 넘긴다. 버튼·아이콘·`<div>` 를 직접 만들어 넣지 않는다.
|
|
57
57
|
- 비활성 버튼은 `disabled` 로만 둔다. DetailPage 에는 사유 안내 한 줄(`actionsNote`)을 **쓰지 않는다**(2026-10-18 11차) — 상태 자체의 안내가 필요하면 `banner`(Alert). ListPage 상세 패널의 `detail.note` 는 그대로. `disabled` 를 빼먹고 배너만 두지 않는다.
|
|
@@ -69,7 +69,7 @@ license: Proprietary. See LICENSE.md in the @t3q-ui/design-system package
|
|
|
69
69
|
- **숫자 열(개수·크기·비율·시간)은 `type: 'number'`** — AgGridTable 이 셀과 제목을 오른쪽 정렬하고 숫자 폭을 고정한다(셀 렌더러가 있어도 적용). 폭을 안 주면 140 으로 잡는다(ag-grid 기본 200 은 숫자 열이 비어 보인다 — 2026-10-27 홀드아웃 13 layout 지적 "wide empty area"). 숫자를 왼쪽 정렬로 두지 않는다.
|
|
70
70
|
- **상세 패널·오른쪽 정보 열의 필드는 `MetaRow align='start'`** — 값이 라벨 바로 옆에 붙는다. 기본(`end`)은 값을 행 오른쪽 끝에 두어 넓은 패널에서 라벨과 값 사이가 빈다.
|
|
71
71
|
- **진행 막대의 색은 상태와 맞춘다** — 실패한 작업은 `Progress variant='danger'`, 경고는 `warning`, 완료는 `success`. 실패한 작업에 파란 막대를 두지 않는다.
|
|
72
|
-
- **헤더 `meta` 에 쓴 값(ID·버전·소유자·수정일)은 본문 Panel 에 다시 쓰지 않는다** — 같은 값이 두 번 보여도 심사자가 중복으로 본다. 키워드·분류 태그는 `Tag variant='secondary'`(기본
|
|
72
|
+
- **헤더 `meta` 에 쓴 값(ID·버전·소유자·수정일)은 본문 Panel 에 다시 쓰지 않는다** — 같은 값이 두 번 보여도 심사자가 중복으로 본다. 키워드·분류 태그는 `Tag variant='secondary'`(기본 4px). 태그는 pill(`shape='rounded'`)로 그리지 않는다 — pill 은 뱃지와 필터 칩의 모양이다.
|
|
73
73
|
- **설명 문장은 PageHeader `description` 한 곳에만.** 같은 문장을 본문 위·카드 안에 다시 쓰지 않고, 본문에 "설명" Panel 을 따로 만들지 않는다(예제 detail-page 와 같이 — 같은 문장이 두 번 보이면 심사자가 중복으로 본다). 본문의 `<p>` 안내문은 사용자가 지금 해야 할 일이 있을 때만. 본문 문단에는 `break-keep wrap-break-word` 를 붙인다(한국어가 단어 중간에서 꺾이지 않게).
|
|
74
74
|
- **상세 개요 탭의 블록(정보·표·목록)은 전부 `Panel` 안에** — 표 블록도 `Panel title='최근 실행' titleRight={<Button variant='primaryLink'>전체 보기</Button>}` 안에 넣고, 제목(`h2`) + 표를 맨몸으로 두지 않는다(이웃이 카드인데 하나만 카드 밖이면 "sits outside a card unlike its neighbors", 2026-10-21 18차 상세 4/6 run). **카드 안의 표는 예제처럼 `Table`(가벼운 표)로** — `AgGridTable`(회색 머리줄·둥근 테두리 상자)은 목록 화면과 전용 탭에만 쓰고 카드 안에 넣지 않는다("bordered table header inside the jobs card while the other cards use plain rows", 2026-10-21 19차). **개요 탭은 요약 패널 + 작은 표** — `Panel '주요 정보'`(MetaList) 아래 `Panel '최근 배포'`(또는 최근 실행·최근 버전) Table **4열·5행까지**(이름·상태·핵심 지표 1·시각) + `titleRight` 에 '전체 보기' 링크(`Button variant='primaryLink'`)로 전용 탭. 열이 더 필요한 표는 전용 탭에서 — 패턴이 `aside` 없는 탭을 **전체 폭**으로 그린다(GitHub PR 식: Conversation 은 2단, Files changed 는 전체 폭). 열 5~7개 표를 2/3 열에 넣으면 이름·날짜가 줄바꿈해 두 심판 모두 감점(2026-10-19 15차). 패널 하나만 두면 오른쪽 열 아래가 비어 "large empty area" 감점(11차).
|
|
75
75
|
- **상세 주 행동은 생산적 행동 하나** — `primaryAction` 은 배포·실행·저장처럼 되돌릴 수 있는 것. 중지·삭제·초기화는 `secondaryActions`(회색) 또는 ⋯ 메뉴(`deleteAction`) — primary 파랑도 빨간 채움도 주지 않는다(5차 빨간 버튼·15차 파란 "실험 중지" 감점).
|
|
@@ -78,7 +78,8 @@ license: Proprietary. See LICENSE.md in the @t3q-ui/design-system package
|
|
|
78
78
|
- **상세 본문 구역은 `Panel title='…'` 안에 `MetaList`** — 구역마다 상자 하나, 상자 사이 16px. `Separator`·`Card`·맨 `<h2>` 로 나누지 않는다. 옆 패널(`aside`)·목록의 상세 패널(`detail`)·폼의 검토 패널(`aside`)은 패턴이 같은 Panel 로 그리므로 안에 상자를 또 두르지 않는다. 표(`Table`·`AgGridTable`)는 자기 상자가 있으니 Panel 로 감싸지 않는다.
|
|
79
79
|
- **폼 옆 검토·요약 패널은 `FormPage aside={…} asideTitle='검토'`** 로 — 패턴이 폼 열 옆에 세로선으로 구분해 sticky 로 따라오게 그린다(좁으면 폼 아래). FormPage 를 `<div className='flex'>` 로 감싸 옆에 `<aside>` 를 두지 않는다: 헤더가 화면 폭을 다 못 써 구분선이 패널에서 끊기고, 패널이 중간에서 잘린다. Card(그림자)로 만들지도 않는다. FormPage 는 헤더 내용과 본문을 같은 가운데 폭(패널 있으면 1152, 없으면 720)에 가둔다(2026-10-26 22차) — 바깥에서 `max-w`·`mx-auto` 로 또 감싸지 않는다.
|
|
80
80
|
- **날짜·시각 입력은 `DatePicker`·`TimePicker` 로, 폼 칸을 채우게** — 기본 폭이 250·160px 라 두 칸 줄에서 절반이 빈다(2026-10-28 홀드아웃 14, Opus·Gemini "date/time fields leave a lot of unused space"). `DatePicker classNames={{ button: 'w-full' }}`·`TimePicker classNames={{ trigger: 'w-full' }}`(두 부품의 통로 이름이 다르다). `<input type='date'>` 를 직접 쓰지 않는다.
|
|
81
|
-
- **목 데이터는 의미가 맞게, 양도 화면을 채울 만큼** — 태스크·프레임워크·이름·상태가 서로 어울리는 조합(예: `yolo-v8-detector` 는 `object-detection` + `PyTorch`), 날짜는 생성일 ≤ 수정일, 건수는 목록 길이와 일치.
|
|
81
|
+
- **목 데이터는 의미가 맞게, 양도 화면을 채울 만큼** — 태스크·프레임워크·이름·상태가 서로 어울리는 조합(예: `yolo-v8-detector` 는 `object-detection` + `PyTorch`), 날짜는 생성일 ≤ 수정일, 건수는 목록 길이와 일치. **목록 데이터는 20행 이상, 한 페이지는 10행** — 데이터가 10~12행이면 표 아래가 비어 "빈 공간" 으로 깎이고(한 줄 판 12차 6쌍, 2026-10-18), 한 페이지가 20행이면 첫 화면(1440×1000)에서 표 아래가 잘려 페이지네이션이 안 보인다(2026-11-06 홀드아웃 20 "bottom row clipped"). 10행이면 첫 화면에 표와 페이지네이션이 함께 들어간다. 심사자는 데이터가 말이 되는지도 본다.
|
|
82
|
+
- **표 열 순서는 이름(주 식별자) → 상태·결과 → 숫자 → 날짜 → 동작** — 같은 화면에 표가 둘 이상이면 같은 순서로 둔다. 표마다 결과 열 자리가 다르면 "result column in different places" 로 깎인다(2026-11-06 홀드아웃 20 상세, Opus).
|
|
82
83
|
- **표 열 폭** — 이름 열 `flex: 2, minWidth: 200, maxWidth: 480`, **글자가 들어가는 열은 모두** `flex: 1, minWidth: 120`(소유자·팀·프레임워크·설명 — 하나만 고르지 않는다. 남는 폭이 여러 열로 흩어져야 이름 열만 넓어지지 않는다; 글자 열이 하나뿐이면 2:1 로 나눠도 이름 열이 500px 라 "name column leaves substantial whitespace", 2026-10-20 17차·18차), 숫자·날짜·상태·진행률 열은 `width` 로 — **숫자 열은 `type: 'number'` 만 주고 `flex` 를 주지 않는다**(폭 없으면 140). DS `Table` 의 숫자 열은 `TableHead align='right'` 로 — 폭 140 으로 고정돼 글자 열이 남는 폭을 가진다(표가 `table-fixed` 라 머리글의 `align` 이 열 폭을 정한다). **DS `Table` 의 상태 열은 `TableHead className='w-32'`, 날짜·시각 열은 `className='w-44'` 로 폭을 준다** — 폭 없는 열은 남는 폭을 똑같이 나눠 가져 뱃지 하나뿐인 상태 열이 400px 가 된다(2026-10-30 홀드아웃 17 "large empty gap between the status and GPU columns"). 남는 폭은 이름·글자 열이 나눈다. 글자 열이 하나도 없으면 `maxWidth` 를 뺀다(표 오른쪽이 빈다 — 상한 없는 flex 열이 없으면 AgGridTable 이 flex 열의 `maxWidth` 를 떼어 표를 채운다, 2026-10-28 홀드아웃 14 "row dividers stop around 1330px")(제목 글자가 들어가는 최소 폭은 AgGridTable 이 보정하고 넘치는 셀 글자는 말줄임표로 끝난다). 열은 6~7개 이내로 — **보조 정보(id·이미지·경로·설명)는 열을 늘리지 말고 `ListPage secondaryField='image'` 로 이름 아래 12px 줄에**(dl-ui-v3 목록의 이름 / 설명 셀; 패턴이 행을 56 으로 그린다). 셀마다 `cellStyle`·`width: 110` 같은 좁은 폭을 손으로 맞추지 않는다(2026-10-18 13차). **주 식별자가 짧은 ID·코드(요청 ID·커밋 해시·키)면 이름 열 공식(`flex: 2`) 대신 `width: 200` 으로 고정**하고, 남는 폭은 엔드포인트·모델·설명 같은 글자 열(`flex: 1`)이 나눈다 — ID 열이 남는 폭을 다 받으면 "request ID column is too wide"(2026-10-28 홀드아웃 14, 목록 3/3).
|
|
83
84
|
- **도움말 문구(`FormDescription`)는 규칙·전제·추천값** — "실행 경로에 쓰여 만든 뒤 바꿀 수 없습니다. 영문 소문자·숫자·하이픈 3~40자", "1~32 코어. 전처리는 2~4, 학습은 8 이상을 권합니다". placeholder 와 같은 말("1~64" / "1~64")을 되풀이하지 않고, placeholder 는 "예: 4" 같은 보기 하나. **여러 필드에 같은 안내는 필드마다 되풀이하지 않는다** — 섹션 설명(또는 `Alert`)에 한 번 쓰고, 필드 도움말은 그 필드만의 값(현재값·범위)만. **placeholder 보기는 안내와 같은 방식으로** — 안내가 "추가분이 아니라 변경 후 전체 한도" 면 보기도 전체 값("예: 8"), "4개에서 8개로 증설" 처럼 안내와 반대로 쓰지 않는다(2026-11-06 홀드아웃 19 폼, 세 심판 copy 감점). **도움말·선택지 설명·`Alert`·패널 설명까지 한 화면의 짧은 문장은 끝 마침표 없이 통일**한다(두 문장을 이을 때만 가운데 마침표 — "1~32 코어. 전처리는 2~4") — 일부만 마침표가 붙으면 심사자가 copy 불일치로 본다(2026-10-18 13차 폼). 검토·요약 패널의 **빈 값은 "—" 하나**(단위는 값이 있을 때만 — "CPU —코어 · 메모리 —GB" 처럼 이어 붙이지 않고 항목마다 한 행). 예제 form-page.tsx 의 리소스 섹션·검토 패널이 본보기(2026-10-18 13차, 기준선 copy 12쌍 벤치마킹).
|
|
84
85
|
- **글자 수("0 / 500자") 같은 보조 표시는 `FormDescription icon={false}`** — `FormDescription` 은 앞에 정보 아이콘을 그리므로 안내 문장에만 쓴다(2026-10-28 홀드아웃 15, Opus·Gemini "info icon next to a simple character counter").
|