@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.
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkZ2SHCDPP_cjs = require('../chunk-Z2SHCDPP.cjs');
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: chunkZ2SHCDPP_cjs.TwoLineCellRenderer,
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: chunkZ2SHCDPP_cjs.cn("flex flex-col gap-[12px]", className),
50
+ className: chunkIYDQLTRF_cjs.cn("flex flex-col gap-[12px]", className),
51
51
  children: [
52
- /* @__PURE__ */ jsxRuntime.jsx(chunkZ2SHCDPP_cjs.Skeleton, { className: "h-[24px] w-[240px]" }),
53
- Array.from({ length: lines }, (_, i) => /* @__PURE__ */ jsxRuntime.jsx(chunkZ2SHCDPP_cjs.Skeleton, { className: chunkZ2SHCDPP_cjs.cn("h-[16px]", i % 2 ? "w-3/5" : "w-full") }, i))
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: chunkZ2SHCDPP_cjs.cn(
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: chunkZ2SHCDPP_cjs.cn("mb-[4px] flex text-sys-text-subtler", iconClassName), children: icon }),
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(chunkZ2SHCDPP_cjs.EmptyBoxIllustration, {}),
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(chunkZ2SHCDPP_cjs.LucideIcon, { iconComponentName: "CircleAlert", size: 36 }),
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(chunkZ2SHCDPP_cjs.Button, { type: "button", variant: "primary", onClick: onRetry, children: retryLabel }) : null
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
- chunkZ2SHCDPP_cjs.Button,
177
+ chunkIYDQLTRF_cjs.Button,
178
178
  {
179
179
  type: "button",
180
180
  variant: "secondaryOutline",
181
181
  onClick: onResetFilters,
182
- className: chunkZ2SHCDPP_cjs.cn(filtersActive === false && "invisible"),
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
- chunkZ2SHCDPP_cjs.ToggleGroup,
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(chunkZ2SHCDPP_cjs.ToggleGroupItem, { value: QUICK_ALL, count: quickAllCount, countVariant: "primary", children: "\uC804\uCCB4" }),
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
- chunkZ2SHCDPP_cjs.ToggleGroupItem,
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(chunkZ2SHCDPP_cjs.Button, { type: "button", variant: "secondaryLink", onClick: () => resetRef.current?.(), children: "\uD544\uD130 \uCD08\uAE30\uD654" }) : null
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
- chunkZ2SHCDPP_cjs.PageHeader,
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(chunkZ2SHCDPP_cjs.Alert, { variant: "danger", children: /* @__PURE__ */ jsxRuntime.jsx(chunkZ2SHCDPP_cjs.AlertDescription, { children: error }) }) : (
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
- chunkZ2SHCDPP_cjs.AgGridTable,
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(chunkZ2SHCDPP_cjs.getSortedColumnState(e).value) : void 0,
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(chunkZ2SHCDPP_cjs.TablePagination, { children: /* @__PURE__ */ jsxRuntime.jsx(
307
- chunkZ2SHCDPP_cjs.Pagination,
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
- chunkZ2SHCDPP_cjs.Panel,
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
- chunkZ2SHCDPP_cjs.IconButton,
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: chunkZ2SHCDPP_cjs.cn("sticky top-[24px] shrink-0", DETAIL_WIDTH[detail.width ?? "medium"]),
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(chunkZ2SHCDPP_cjs.DropdownMenu, { children: [
357
- /* @__PURE__ */ jsxRuntime.jsx(chunkZ2SHCDPP_cjs.DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(chunkZ2SHCDPP_cjs.IconButton, { ref: triggerRef, variant: "secondary", "aria-label": "\uB354\uBCF4\uAE30", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Ellipsis, { size: 24 }) }) }),
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
- chunkZ2SHCDPP_cjs.DropdownMenuContent,
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(chunkZ2SHCDPP_cjs.DropdownMenuItem, { disabled, onSelect: select(onClick), children }, i)),
371
- deleteAction && items.length ? /* @__PURE__ */ jsxRuntime.jsx(chunkZ2SHCDPP_cjs.DropdownMenuSeparator, {}) : null,
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
- chunkZ2SHCDPP_cjs.DropdownMenuItem,
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(chunkZ2SHCDPP_cjs.Panel, { title: asideTitle, children: aside })
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(chunkZ2SHCDPP_cjs.Button, { type: "button", variant: "primaryBold", ...primaryAction }) : null;
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(chunkZ2SHCDPP_cjs.Button, { type: "button", variant: "secondary", ...action }, i)),
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(chunkZ2SHCDPP_cjs.PageHeader, { ...header, actions: actionRow }),
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: chunkZ2SHCDPP_cjs.cn("w-full", width === "panel" && "mx-auto max-w-[900px]"), children: [
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(chunkZ2SHCDPP_cjs.Alert, { variant: "danger", className, children: /* @__PURE__ */ jsxRuntime.jsx(chunkZ2SHCDPP_cjs.AlertDescription, { children: error }) });
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
- chunkZ2SHCDPP_cjs.Button,
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
- chunkZ2SHCDPP_cjs.Button,
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(chunkZ2SHCDPP_cjs.Form, { ...form, children: /* @__PURE__ */ jsxRuntime.jsxs("form", { id: formId, noValidate: true, onSubmit: handleSubmit, children: [
560
- /* @__PURE__ */ jsxRuntime.jsx(chunkZ2SHCDPP_cjs.FormSectionLevelContext.Provider, { value: 2, children }),
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
- chunkZ2SHCDPP_cjs.FormFooter,
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
- chunkZ2SHCDPP_cjs.Card,
575
+ chunkIYDQLTRF_cjs.Card,
576
576
  {
577
- className: chunkZ2SHCDPP_cjs.cn(
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: chunkZ2SHCDPP_cjs.cn(
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: chunkZ2SHCDPP_cjs.cn("w-full", canvas === "subtle" && "bg-sys-background-hover"), children: [
596
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chunkIYDQLTRF_cjs.cn("w-full", canvas === "subtle" && "bg-sys-background-hover"), children: [
597
597
  /* @__PURE__ */ jsxRuntime.jsx(
598
- chunkZ2SHCDPP_cjs.PageHeader,
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(chunkZ2SHCDPP_cjs.Panel, { title: asideTitle, className: "xl:sticky xl:top-[24px]", children: [
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(chunkZ2SHCDPP_cjs.Drawer, { open, onOpenChange: guardedOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(
650
- chunkZ2SHCDPP_cjs.DrawerContent,
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(chunkZ2SHCDPP_cjs.DrawerHeader, { children: [
657
- /* @__PURE__ */ jsxRuntime.jsx(chunkZ2SHCDPP_cjs.DrawerTitle, { children: title }),
658
- description ? /* @__PURE__ */ jsxRuntime.jsx(chunkZ2SHCDPP_cjs.DrawerDescription, { children: description }) : null
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(chunkZ2SHCDPP_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(chunkZ2SHCDPP_cjs.DrawerBody, { className: "px-[24px]", children: [
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(chunkZ2SHCDPP_cjs.DrawerFooter, { children: /* @__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(chunkZ2SHCDPP_cjs.Dialog, { open, onOpenChange: guardedOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(
701
- chunkZ2SHCDPP_cjs.DialogContent,
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(chunkZ2SHCDPP_cjs.DialogHeader, { children: [
708
- /* @__PURE__ */ jsxRuntime.jsx(chunkZ2SHCDPP_cjs.DialogTitle, { children: title }),
709
- description ? /* @__PURE__ */ jsxRuntime.jsx(chunkZ2SHCDPP_cjs.DialogDescription, { children: description }) : null
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(chunkZ2SHCDPP_cjs.Form, { ...form, children: /* @__PURE__ */ jsxRuntime.jsxs("form", { noValidate: true, onSubmit: handleSubmit, className: "flex flex-col gap-[16px]", children: [
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(chunkZ2SHCDPP_cjs.DialogFooter, { children: /* @__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(chunkZ2SHCDPP_cjs.Card, { className: "relative gap-[4px] p-[16px]", children: [
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(chunkZ2SHCDPP_cjs.Skeleton, { role: "status", "aria-label": "\uBD88\uB7EC\uC624\uB294 \uC911", className: "h-[32px] w-[80px]" }) : /* @__PURE__ */ jsxRuntime.jsxs("span", { className: chunkZ2SHCDPP_cjs.cn("flex items-center gap-[6px] text-h2", TONE_TEXT[tone]), children: [
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: chunkZ2SHCDPP_cjs.cn("h-full rounded-full", TONE_BAR[tone]),
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(chunkZ2SHCDPP_cjs.PageHeader, { ...header, actions }),
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
- chunkZ2SHCDPP_cjs.Card,
825
+ chunkIYDQLTRF_cjs.Card,
826
826
  {
827
- className: chunkZ2SHCDPP_cjs.cn(
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-ZIfOVixA.cjs';
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';
@@ -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-ZIfOVixA.js';
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';
@@ -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-LEHEPAQO.js';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t3q-ui/design-system",
3
- "version": "1.57.0",
3
+ "version": "1.58.0",
4
4
  "description": "T3Q Design System - React Components",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -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
- - **라운드는 한 계열** — 뱃지·태그 4px, 칩·버튼·입력 6px, 표·카드 8px. 컴포넌트 기본값이 이미 그렇다. `rounded-full`·`shape='rounded'`·`rounded='full'` 을 덧씌워 pill 을 만들지 않는다(각진 것 옆의 pill 은 세 라운드 연속 감점, 2026-10-18 10차).
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'`(기본 각진 4px) — 상태 뱃지·칩·입력과 같은 라운드 계열. pill(`shape='rounded'`)로 그리지 않는다(2026-10-18 10차).
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`), 날짜는 생성일 ≤ 수정일, 건수는 목록 길이와 일치. **목록은 20행 이상**(페이지 크기 20 이면 두 페이지 분량) — 10~12행이면 표 아래가 비어 심사자가 "빈 공간" 으로 깎는다(한 줄 판 12차 6쌍, 2026-10-18). 심사자는 데이터가 말이 되는지도 본다.
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").