@godxjp/ui 23.0.0 → 23.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/app/date-format-labels.d.ts +14 -2
  2. package/dist/app/date-format-labels.js +1 -1
  3. package/dist/app/date-formats.d.ts +14 -2
  4. package/dist/app/date-formats.js +9 -2
  5. package/dist/components/data-display/card.d.ts +45 -2
  6. package/dist/components/data-display/card.js +4 -2
  7. package/dist/components/data-display/data-table.d.ts +8 -1
  8. package/dist/components/data-display/data-table.js +9 -2
  9. package/dist/components/data-display/feature-list.d.ts +26 -0
  10. package/dist/components/data-display/feature-list.js +29 -0
  11. package/dist/components/data-display/index.d.ts +6 -0
  12. package/dist/components/data-display/index.js +6 -0
  13. package/dist/components/data-display/swatch.d.ts +24 -0
  14. package/dist/components/data-display/swatch.js +19 -0
  15. package/dist/components/data-display/thumbnail.d.ts +37 -0
  16. package/dist/components/data-display/thumbnail.js +20 -0
  17. package/dist/components/data-entry/checkbox.js +1 -1
  18. package/dist/components/data-entry/form-field.js +2 -2
  19. package/dist/components/data-entry/input.js +1 -1
  20. package/dist/components/data-entry/number-input.js +27 -6
  21. package/dist/components/data-entry/radio.js +1 -1
  22. package/dist/components/data-entry/switch.js +1 -1
  23. package/dist/components/feedback/dialog.js +2 -2
  24. package/dist/components/feedback/index.d.ts +2 -2
  25. package/dist/components/feedback/index.js +19 -1
  26. package/dist/components/feedback/sheet.js +2 -22
  27. package/dist/components/feedback/skeleton.d.ts +33 -3
  28. package/dist/components/feedback/skeleton.js +154 -3
  29. package/dist/components/layout/flex.d.ts +1 -1
  30. package/dist/components/layout/flex.js +17 -5
  31. package/dist/components/layout/sidebar.js +9 -2
  32. package/dist/components/navigation/filter-bar.js +2 -1
  33. package/dist/components/navigation/tabs-scroll.d.ts +27 -0
  34. package/dist/components/navigation/tabs-scroll.js +36 -1
  35. package/dist/components/navigation/tabs.d.ts +2 -2
  36. package/dist/components/navigation/tabs.js +60 -16
  37. package/dist/i18n/messages/en.json +10 -1
  38. package/dist/i18n/messages/ja.json +10 -1
  39. package/dist/i18n/messages/vi.json +10 -1
  40. package/dist/lib/breakpoint-token.d.ts +8 -0
  41. package/dist/lib/breakpoint-token.js +29 -0
  42. package/dist/props/components/data-display.prop.d.ts +101 -1
  43. package/dist/props/components/feedback.prop.d.ts +85 -1
  44. package/dist/props/components/layout.prop.d.ts +72 -1
  45. package/dist/props/components/navigation.prop.d.ts +80 -2
  46. package/dist/props/registry.d.ts +124 -1
  47. package/dist/props/registry.js +140 -0
  48. package/dist/props/vocabulary/data.prop.d.ts +5 -0
  49. package/dist/props/vocabulary/index.d.ts +1 -1
  50. package/dist/styles/alert-layout.css +139 -0
  51. package/dist/styles/card-layout.css +48 -0
  52. package/dist/styles/control.css +41 -5
  53. package/dist/styles/data-display-layout.css +88 -0
  54. package/dist/styles/focus-ring.css +28 -0
  55. package/dist/styles/form-layout.css +4 -0
  56. package/dist/styles/layout.css +2 -1
  57. package/dist/styles/navigation-layout.css +89 -5
  58. package/dist/styles/shell-layout.css +22 -0
  59. package/dist/styles/table-layout.css +5 -2
  60. package/dist/tokens/components/card.css +2 -0
  61. package/dist/tokens/components/control.css +8 -3
  62. package/dist/tokens/components/data-display.css +23 -0
  63. package/dist/tokens/components/feedback.css +18 -0
  64. package/dist/tokens/components/navigation.css +4 -0
  65. package/dist/tokens/foundation.css +18 -16
  66. package/docs/CONSUMER-RULES.md +8 -3
  67. package/docs/CUSTOMER-THEMING.md +18 -8
  68. package/docs/DATETIME.md +11 -2
  69. package/docs/DESIGN-AUTHORITY.md +2 -1
  70. package/docs/FRAME-COVERAGE-REPORT.md +29 -3
  71. package/docs/SPACING.md +35 -5
  72. package/docs/TOKENS.md +1 -1
  73. package/docs/assets/shot-landscape.svg +12 -0
  74. package/docs/assets/shot-portrait.svg +10 -0
  75. package/docs/data-display/card/index.tsx +40 -0
  76. package/docs/data-display/data-table/index.tsx +17 -2
  77. package/docs/data-display/feature-list.tsx +159 -0
  78. package/docs/data-display/swatch.tsx +107 -0
  79. package/docs/data-display/table.tsx +1 -1
  80. package/docs/data-display/thumbnail.tsx +128 -0
  81. package/docs/data-entry/segmented-in-filter-row.tsx +77 -0
  82. package/docs/feedback/skeleton.tsx +57 -0
  83. package/docs/layout/flex.tsx +29 -0
  84. package/docs/layout/sidebar.tsx +66 -10
  85. package/docs/navigation/dropdown-menu.tsx +4 -4
  86. package/docs/navigation/tabs.tsx +131 -0
  87. package/docs/roadmap/ai-chat-components.md +1 -2
  88. package/docs/roadmap/tree-components.md +0 -1
  89. package/package.json +12 -21
  90. package/scripts/_agent-setup.mjs +18 -0
  91. package/scripts/consumer-rule.md +16 -0
  92. package/scripts/guinea-pig-skill.md +12 -6
  93. package/scripts/ui-audit.mjs +184 -7
@@ -9,6 +9,7 @@ import {
9
9
  } from "react-aria-components";
10
10
  import { MoreHorizontal, Plus, X } from "lucide-react";
11
11
  import { useTranslation } from "../../i18n/use-translation.js";
12
+ import { useMaxWidthBreakpoint } from "../../lib/breakpoint-token.js";
12
13
  import { cn } from "../../lib/utils.js";
13
14
  import {
14
15
  DropdownMenu,
@@ -16,10 +17,22 @@ import {
16
17
  DropdownMenuItem,
17
18
  DropdownMenuTrigger
18
19
  } from "./dropdown-menu.js";
19
- import { useKeepActiveTabVisible, useTabsOverflowValues } from "./tabs-scroll.js";
20
+ import {
21
+ useKeepActiveTabVisible,
22
+ useTabsOverflowValues,
23
+ useTabsScrollReporter
24
+ } from "./tabs-scroll.js";
25
+ function triggerSizeClassName(size) {
26
+ return cn(
27
+ size === "sm" && "min-h-[var(--tabs-trigger-height-sm)] px-[var(--tabs-trigger-padding-x-sm)] text-[length:var(--tabs-trigger-font-size-sm)]",
28
+ size === "md" && "min-h-[var(--tabs-trigger-height-md)] px-[var(--tabs-trigger-padding-x-md)] text-[length:var(--tabs-trigger-font-size-md)]",
29
+ size === "lg" && "min-h-[var(--tabs-trigger-height-lg)] px-[var(--tabs-trigger-padding-x-lg)] text-[length:var(--tabs-trigger-font-size-lg)]"
30
+ );
31
+ }
20
32
  const TabsFrameContext = React.createContext({
21
33
  orientation: "horizontal",
22
- selectionSuppressed: false
34
+ selectionSuppressed: false,
35
+ size: "md"
23
36
  });
24
37
  function resolveFallbackTabValue(items, requested) {
25
38
  if (!items || items.length === 0) return requested;
@@ -45,6 +58,12 @@ function resolveTabsExtra(extra) {
45
58
  if (isSlotMap) return extra;
46
59
  return { end: extra };
47
60
  }
61
+ function resolveTabsAnimated(animated) {
62
+ if (animated === false) return { inkBar: false, tabPane: false };
63
+ if (animated === true) return { inkBar: true, tabPane: true };
64
+ if (animated === void 0) return { inkBar: true, tabPane: false };
65
+ return { inkBar: animated.inkBar ?? true, tabPane: animated.tabPane ?? false };
66
+ }
48
67
  function resolveTabsAxis(tabPlacement, orientation) {
49
68
  if (tabPlacement) {
50
69
  return {
@@ -57,6 +76,13 @@ function resolveTabsAxis(tabPlacement, orientation) {
57
76
  orientation: orientation ?? "horizontal"
58
77
  };
59
78
  }
79
+ const TABS_PLACEMENT_BREAKPOINT_TOKEN = "--tabs-placement-responsive-breakpoint-width";
80
+ const TABS_PLACEMENT_BREAKPOINT_FALLBACK_QUERY = "(max-width: 768px)";
81
+ function foldVerticalPlacement(axis, narrow) {
82
+ if (!narrow || axis.orientation !== "vertical") return axis;
83
+ const trailing = axis.placement === "end" || axis.placement === "bottom";
84
+ return { placement: trailing ? "bottom" : "top", orientation: "horizontal" };
85
+ }
60
86
  function Tabs({
61
87
  className,
62
88
  orientation,
@@ -78,21 +104,32 @@ function Tabs({
78
104
  closeIcon,
79
105
  onTabClick,
80
106
  overflow = "scroll",
107
+ animated,
108
+ indicator,
109
+ moreIcon,
110
+ onTabScroll,
81
111
  listClassName,
82
112
  contentClassName,
83
113
  children,
84
114
  ...props
85
115
  }) {
86
116
  const { t } = useTranslation();
117
+ const motion = resolveTabsAnimated(animated);
118
+ const indicatorSize = indicator?.size ?? "full";
119
+ const indicatorAlign = indicator?.align ?? "center";
87
120
  const resolvedDefault = resolveFallbackTabValue(items, defaultValue);
88
- const { placement, orientation: resolvedOrientation } = resolveTabsAxis(
89
- tabPlacement,
90
- orientation
121
+ const narrow = useMaxWidthBreakpoint(
122
+ TABS_PLACEMENT_BREAKPOINT_TOKEN,
123
+ TABS_PLACEMENT_BREAKPOINT_FALLBACK_QUERY
124
+ );
125
+ const { placement, orientation: resolvedOrientation } = foldVerticalPlacement(
126
+ resolveTabsAxis(tabPlacement, orientation),
127
+ narrow
91
128
  );
92
129
  const selectionSuppressed = value === void 0 && items != null && items.length > 0 && resolvedDefault === void 0;
93
130
  const frame = React.useMemo(
94
- () => ({ orientation: resolvedOrientation, selectionSuppressed }),
95
- [resolvedOrientation, selectionSuppressed]
131
+ () => ({ orientation: resolvedOrientation, selectionSuppressed, size }),
132
+ [resolvedOrientation, selectionSuppressed, size]
96
133
  );
97
134
  const editable = variant === "editable-card";
98
135
  const collapsible = items != null && overflow === "menu";
@@ -108,6 +145,7 @@ function Tabs({
108
145
  );
109
146
  }, []);
110
147
  useTabsOverflowValues(listRef, itemValues, handleHiddenChange);
148
+ useTabsScrollReporter(listRef, resolvedOrientation === "vertical", onTabScroll);
111
149
  const [mirroredValue, setMirroredValue] = React.useState(
112
150
  value ?? resolvedDefault
113
151
  );
@@ -127,11 +165,7 @@ function Tabs({
127
165
  const showAdd = editable && !hideAdd && Boolean(onEdit);
128
166
  const needsBar = Boolean(extraStart || extraEnd || showAdd || collapsible);
129
167
  const card = variant === "card" || variant === "editable-card";
130
- const sizeTriggerClassName = cn(
131
- size === "sm" && "min-h-[var(--tabs-trigger-height-sm)] px-[var(--tabs-trigger-padding-x-sm)] text-[length:var(--tabs-trigger-font-size-sm)]",
132
- size === "md" && "min-h-[var(--tabs-trigger-height-md)] px-[var(--tabs-trigger-padding-x-md)] text-[length:var(--tabs-trigger-font-size-md)]",
133
- size === "lg" && "min-h-[var(--tabs-trigger-height-lg)] px-[var(--tabs-trigger-padding-x-lg)] text-[length:var(--tabs-trigger-font-size-lg)]"
134
- );
168
+ const sizeTriggerClassName = triggerSizeClassName(size);
135
169
  const list = items ? /* @__PURE__ */ jsx(
136
170
  TabsList,
137
171
  {
@@ -271,6 +305,10 @@ function Tabs({
271
305
  "data-placement": placement,
272
306
  "data-size": size,
273
307
  "data-centered": centered ? "true" : void 0,
308
+ "data-animated-ink-bar": motion.inkBar ? "true" : "false",
309
+ "data-animated-tab-pane": motion.tabPane ? "true" : "false",
310
+ "data-indicator-size": indicatorSize,
311
+ "data-indicator-align": indicatorAlign,
274
312
  orientation: resolvedOrientation,
275
313
  keyboardActivation: activationMode,
276
314
  selectedKey: value ?? (collapsible ? mirroredValue : void 0),
@@ -295,7 +333,11 @@ function Tabs({
295
333
  "data-variant": variant,
296
334
  "data-placement": placement,
297
335
  "data-size": size,
298
- "data-centered": centered ? "true" : void 0
336
+ "data-centered": centered ? "true" : void 0,
337
+ "data-animated-ink-bar": motion.inkBar ? "true" : "false",
338
+ "data-animated-tab-pane": motion.tabPane ? "true" : "false",
339
+ "data-indicator-size": indicatorSize,
340
+ "data-indicator-align": indicatorAlign
299
341
  }),
300
342
  children: items ? /* @__PURE__ */ jsxs(Fragment, { children: [
301
343
  needsBar ? /* @__PURE__ */ jsxs("div", { "data-slot": "tabs-bar", className: "ui-tabs-bar", children: [
@@ -318,7 +360,7 @@ function Tabs({
318
360
  {
319
361
  className: "ui-tabs-overflow",
320
362
  "aria-label": t("navigation.tabs.moreTabs"),
321
- children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "ui-tabs-overflow-icon", "aria-hidden": "true" })
363
+ children: moreIcon ?? /* @__PURE__ */ jsx(MoreHorizontal, { className: "ui-tabs-overflow-icon", "aria-hidden": "true" })
322
364
  }
323
365
  ),
324
366
  /* @__PURE__ */ jsx(DropdownMenuContent, { placement: "bottomEnd", children: overflowItems.map((item) => /* @__PURE__ */ jsx(
@@ -395,7 +437,7 @@ const TabsList = React.forwardRef(
395
437
  TabsList.displayName = "TabsList";
396
438
  const TabsTrigger = React.forwardRef(
397
439
  ({ className, value, disabled, children, onKeyDown, onClick, ...props }, ref) => {
398
- const { orientation, selectionSuppressed } = React.useContext(TabsFrameContext);
440
+ const { orientation, selectionSuppressed, size } = React.useContext(TabsFrameContext);
399
441
  return /* @__PURE__ */ jsx(
400
442
  AriaTab,
401
443
  {
@@ -422,6 +464,8 @@ const TabsTrigger = React.forwardRef(
422
464
  // Selected and focused stay visually distinct (WCAG 2.4.7): selected is a 1px hairline in the
423
465
  // border, focused is the 2px ring plus its halo outside it.
424
466
  "text-muted-foreground ring-offset-background hover:text-foreground ui-focus-ring data-[state=active]:bg-background data-[state=active]:text-foreground group-data-[variant=default]/tabs-list:data-[state=active]:border-primary/25 relative inline-flex flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:flex-none group-data-[orientation=vertical]/tabs:justify-start group-data-[variant=line]/tabs-list:border-e-0 group-data-[variant=line]/tabs-list:border-b-0 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none",
467
+ // The tier the root was given. Without this the compound form ignored `size` outright.
468
+ triggerSizeClassName(size),
425
469
  className
426
470
  ),
427
471
  render: (domProps, renderProps) => withDomProps("button", "tabs-trigger", { type: "button", ...props }, domProps, {
@@ -470,7 +514,7 @@ const TabsContent = React.forwardRef(
470
514
  ref,
471
515
  id: value,
472
516
  shouldForceMount: forceMount,
473
- className: cn("ui-focus-ring flex-1 outline-none", className),
517
+ className: cn("ui-focus-ring flex-1", className),
474
518
  render: (domProps, renderProps) => withDomProps("div", "tabs-content", props, domProps, {
475
519
  // A force-mounted panel whose tab is not selected: Radix marked it `data-state="inactive"`
476
520
  // and `hidden`; RAC marks it inert. The state hook is re-emitted so CSS keys the same way.
@@ -283,6 +283,11 @@
283
283
  "dotsLabel": "Choose slide",
284
284
  "goToSlide": "Go to slide {index}"
285
285
  },
286
+ "featureList": {
287
+ "included": "Included",
288
+ "excluded": "Not included",
289
+ "limited": "Limited"
290
+ },
286
291
  "listRow": {
287
292
  "unread": "Unread",
288
293
  "read": "Read"
@@ -341,7 +346,9 @@
341
346
  },
342
347
  "numberInput": {
343
348
  "increment": "Increase",
344
- "decrement": "Decrease"
349
+ "decrement": "Decrease",
350
+ "incrementField": "Increase {label}",
351
+ "decrementField": "Decrease {label}"
345
352
  }
346
353
  },
347
354
  "navigation": {
@@ -368,6 +375,7 @@
368
375
  },
369
376
  "filterBar": {
370
377
  "appliedFilters": "Applied filters",
378
+ "clearFilter": "Clear {label} selection",
371
379
  "removeFilter": "Remove filter: {label}",
372
380
  "resultCount": {
373
381
  "one": "{count} result",
@@ -442,6 +450,7 @@
442
450
  },
443
451
  "dateFormat": {
444
452
  "iso": "ISO (yyyy-MM-dd)",
453
+ "ymd": "Year / Month / Day (yyyy/MM/dd)",
445
454
  "dmy": "Day / Month / Year",
446
455
  "mdy": "Month / Day / Year"
447
456
  },
@@ -276,6 +276,11 @@
276
276
  "dotsLabel": "スライドを選択",
277
277
  "goToSlide": "{index} 番目のスライドへ"
278
278
  },
279
+ "featureList": {
280
+ "included": "含む",
281
+ "excluded": "含まない",
282
+ "limited": "制限あり"
283
+ },
279
284
  "listRow": {
280
285
  "unread": "未読",
281
286
  "read": "既読"
@@ -334,7 +339,9 @@
334
339
  },
335
340
  "numberInput": {
336
341
  "increment": "増やす",
337
- "decrement": "減らす"
342
+ "decrement": "減らす",
343
+ "incrementField": "{label}を増やす",
344
+ "decrementField": "{label}を減らす"
338
345
  }
339
346
  },
340
347
  "navigation": {
@@ -361,6 +368,7 @@
361
368
  },
362
369
  "filterBar": {
363
370
  "appliedFilters": "適用中のフィルター",
371
+ "clearFilter": "{label}の選択をクリア",
364
372
  "removeFilter": "フィルターを解除: {label}",
365
373
  "resultCount": "{count} 件の結果"
366
374
  },
@@ -429,6 +437,7 @@
429
437
  },
430
438
  "dateFormat": {
431
439
  "iso": "YYYY-MM-DD(年-月-日)",
440
+ "ymd": "YYYY/MM/DD(年/月/日)",
432
441
  "dmy": "日/月/年",
433
442
  "mdy": "月/日/年"
434
443
  },
@@ -277,6 +277,11 @@
277
277
  "dotsLabel": "Chọn slide",
278
278
  "goToSlide": "Đến slide {index}"
279
279
  },
280
+ "featureList": {
281
+ "included": "Có",
282
+ "excluded": "Không có",
283
+ "limited": "Giới hạn"
284
+ },
280
285
  "listRow": {
281
286
  "unread": "Chưa đọc",
282
287
  "read": "Đã đọc"
@@ -335,7 +340,9 @@
335
340
  },
336
341
  "numberInput": {
337
342
  "increment": "Tăng",
338
- "decrement": "Giảm"
343
+ "decrement": "Giảm",
344
+ "incrementField": "Tăng {label}",
345
+ "decrementField": "Giảm {label}"
339
346
  }
340
347
  },
341
348
  "navigation": {
@@ -362,6 +369,7 @@
362
369
  },
363
370
  "filterBar": {
364
371
  "appliedFilters": "Bộ lọc đang áp dụng",
372
+ "clearFilter": "Xóa lựa chọn {label}",
365
373
  "removeFilter": "Bỏ bộ lọc: {label}",
366
374
  "resultCount": "{count} kết quả"
367
375
  },
@@ -430,6 +438,7 @@
430
438
  },
431
439
  "dateFormat": {
432
440
  "iso": "ISO (yyyy-MM-dd)",
441
+ "ymd": "Năm / Tháng / Ngày (yyyy/MM/dd)",
433
442
  "dmy": "Ngày / Tháng / Năm",
434
443
  "mdy": "Tháng / Ngày / Năm"
435
444
  },
@@ -0,0 +1,8 @@
1
+ /**
2
+ * True while the viewport is at or below the width the theme token `token` names.
3
+ *
4
+ * The token is read once per mount rather than at module scope: the fallback is what SSR and the
5
+ * first client render agree on, and the themed value arrives in the effect. Same-value updates
6
+ * bail out inside React, so a theme that left the default alone re-renders nothing.
7
+ */
8
+ export declare function useMaxWidthBreakpoint(token: string, fallbackQuery: string): boolean;
@@ -0,0 +1,29 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { useMediaQuery } from "./hooks.js";
4
+ function cssLengthToPx(value, rootFontSize) {
5
+ const match = /^(-?\d*\.?\d+)(px|rem|em)?$/.exec(value.trim());
6
+ if (match == null) return void 0;
7
+ const amount = Number(match[1]);
8
+ if (!Number.isFinite(amount)) return void 0;
9
+ return match[2] === "rem" || match[2] === "em" ? amount * rootFontSize : amount;
10
+ }
11
+ function readMaxWidthQuery(token, fallbackQuery) {
12
+ if (typeof document === "undefined" || typeof window.getComputedStyle !== "function") {
13
+ return fallbackQuery;
14
+ }
15
+ const rootStyle = window.getComputedStyle(document.documentElement);
16
+ const rootFontSize = cssLengthToPx(rootStyle.fontSize || "16px", 16) ?? 16;
17
+ const px = cssLengthToPx(rootStyle.getPropertyValue(token), rootFontSize);
18
+ return px == null ? fallbackQuery : `(max-width: ${String(px)}px)`;
19
+ }
20
+ function useMaxWidthBreakpoint(token, fallbackQuery) {
21
+ const [query, setQuery] = React.useState(fallbackQuery);
22
+ React.useEffect(() => {
23
+ setQuery(readMaxWidthQuery(token, fallbackQuery));
24
+ }, [token, fallbackQuery]);
25
+ return useMediaQuery(query);
26
+ }
27
+ export {
28
+ useMaxWidthBreakpoint
29
+ };
@@ -28,7 +28,7 @@ export type ProseProp = {
28
28
  className?: ClassNameProp;
29
29
  children?: ChildrenProp;
30
30
  };
31
- import type { ActionProp, ClassNameProp, DescriptionProp, IconProp, TitleProp, ColumnDefProp, GetRowIdProp, OnRowClickProp, OnSelectChangeProp, OnSortChangeProp, OnTableDensityChangeProp, SelectedIdsProp, SortStateProp, TableDensityProp, TablePresetProp, BreakpointProp, DensityProp, ChildrenProp, PendingProp, ToneProp, AvatarShapeProp, HeadingLevelProp, HandlerProp, SizeProp, LabelProp, IdProp, DescriptionsLayoutProp, DescriptionsColumnProp, DescriptionsSpanProp, DescriptionsItemsProp, SortDirectionProp, OnColumnFilterChangeProp, OnRowProp, TableExpandableProp, TableRowSelectionProp, TableScrollProp, TableStickyProp, TableSummaryProp, DisabledProp, ValueProp, DefaultValueProp, OnValueChangeProp } from "../vocabulary/index.js";
31
+ import type { ActionProp, ClassNameProp, DescriptionProp, IconProp, TitleProp, ColumnDefProp, GetRowIdProp, GetRowLabelProp, OnRowClickProp, OnSelectChangeProp, OnSortChangeProp, OnTableDensityChangeProp, SelectedIdsProp, SortStateProp, TableDensityProp, TablePresetProp, BreakpointProp, DensityProp, ChildrenProp, PendingProp, ToneProp, AvatarShapeProp, HeadingLevelProp, HandlerProp, SizeProp, LabelProp, IdProp, DescriptionsLayoutProp, DescriptionsColumnProp, DescriptionsSpanProp, DescriptionsItemsProp, SortDirectionProp, OnColumnFilterChangeProp, OnRowProp, TableExpandableProp, TableRowSelectionProp, TableScrollProp, TableStickyProp, TableSummaryProp, DisabledProp, ValueProp, DefaultValueProp, OnValueChangeProp } from "../vocabulary/index.js";
32
32
  import type { TreeFieldNamesProp, TreeOptionProp } from "./data-entry.prop.js";
33
33
  /**
34
34
  * One key in a `Legend`: a tone, and the words that tone stands for.
@@ -47,6 +47,99 @@ export type LegendProp = Omit<React.HTMLAttributes<HTMLUListElement>, "children"
47
47
  items: LegendItemProp[];
48
48
  className?: ClassNameProp;
49
49
  };
50
+ /**
51
+ * @see Swatch — a READ-ONLY sample of ONE colour a person chose.
52
+ *
53
+ * ## Why it is not a Legend, a ColorPicker or a Badge
54
+ *
55
+ * `Legend` is a KEY: a closed set of semantic TONES, each with required words beside it, because
56
+ * there the colour stands FOR something. Here the colour IS the content — a brand's
57
+ * `primary_color`, a calendar category, a label a user tinted — so there is no tone it maps to and
58
+ * no second thing for a word to name. `ColorPicker` is the INPUT for the same value, and rendering
59
+ * a disabled input to display one reads as a control that broke. `Badge` is a chip: tinted fill,
60
+ * border, clickable affordance — not a sample of the exact colour.
61
+ *
62
+ * Its mark is the same square Legend draws, one type step larger, because it stands beside a name
63
+ * rather than inside an 11px key.
64
+ */
65
+ export type SwatchProp = Omit<React.HTMLAttributes<HTMLSpanElement>, "children" | "color"> & {
66
+ /**
67
+ * The colour to show, as a CSS colour VALUE — `#7C3AED`, `rgb(…)`, `oklch(…)`. It is DATA the
68
+ * same way `Badge`'s `color` is: a value a person picked in a settings screen, so it arrives as
69
+ * a prop and is never written into a stylesheet. Passing a semantic token's own colour here is
70
+ * the one thing to avoid — a tone that MEANS something belongs on a component that names the
71
+ * meaning (`Badge tone`, `Legend`), not on a sample.
72
+ */
73
+ color: string;
74
+ /**
75
+ * The accessible NAME of the sample, and the reason this component can exist without a visible
76
+ * label. Say what the colour is FOR and what it is: `aria-label={`${t.primaryColor}: ${hex}`}`.
77
+ *
78
+ * With it, the swatch is a `role="img"` that announces that sentence. Without it the swatch is
79
+ * `aria-hidden` — correct, and the only correct option, when a visible line beside it already
80
+ * states the colour. Either way colour is never the sole carrier of the meaning (WCAG 1.4.1);
81
+ * what is NOT offered is a third path where it is.
82
+ */
83
+ "aria-label"?: string;
84
+ };
85
+ /**
86
+ * Whether a `FeatureList` line is in, out, or in with a limit.
87
+ *
88
+ * Three members and no fourth: the axis is INCLUSION, and a list that also carried "coming soon",
89
+ * "beta" or "deprecated" would be encoding a roadmap on the same glyph column as a fact about
90
+ * today. Those are a `Badge` beside the label, which is content the screen already owns.
91
+ */
92
+ export type FeatureStateProp = "included" | "excluded" | "limited";
93
+ /**
94
+ * One line of a `FeatureList`: what state it is in, what it is called, and — optionally — a
95
+ * sentence about it that WRAPS.
96
+ *
97
+ * There is deliberately no field for a quantity ("10,000 req/mo"). Composing it into `label` as
98
+ * `<>API calls <Text tone="muted" tabular>10,000 req/mo</Text></>` is already legal and already
99
+ * audit-clean, so a prop for it would fail question 1 of docs/WHAT-BELONGS-HERE.md — the consumer
100
+ * has a move. What the consumer did NOT have a move for is the glyph column and its alignment,
101
+ * which is what this component owns.
102
+ */
103
+ export type FeatureItemProp = {
104
+ /** In, out, or in with a limit. Drives the glyph, its mark colour and the `sr-only` prefix. */
105
+ state: FeatureStateProp;
106
+ /** What the line is about. Wraps. */
107
+ label: LabelProp;
108
+ /** An optional muted sentence under the label. Wraps; long unbroken tokens break. */
109
+ description?: DescriptionProp;
110
+ };
111
+ /**
112
+ * @see FeatureList — a list of statements, each with a leading state glyph: what a plan includes,
113
+ * what a tier supports, which requirements a submission met.
114
+ */
115
+ export type FeatureListProp = Omit<React.HTMLAttributes<HTMLUListElement>, "children"> & {
116
+ items: FeatureItemProp[];
117
+ className?: ClassNameProp;
118
+ };
119
+ /**
120
+ * Fixed BLOCK size of a `Thumbnail`; the inline size stays intrinsic.
121
+ *
122
+ * A subset of the shared `SizeProp` ladder — `xs` is off the bottom because a 48px frame with a
123
+ * 1px hairline is a favicon, not a thumbnail, and nothing in the reported cases wanted one.
124
+ */
125
+ export type ThumbnailSizeProp = Extract<SizeProp, "sm" | "md" | "lg">;
126
+ /**
127
+ * @see Thumbnail — a framed image at a FIXED HEIGHT and its own intrinsic width, for a wrapping
128
+ * row of pictures whose aspect ratios differ.
129
+ */
130
+ export type ThumbnailProp = Omit<React.ImgHTMLAttributes<HTMLImageElement>, "alt"> & {
131
+ /** Image URL. */
132
+ src: string;
133
+ /**
134
+ * Required, with no way to omit it. Pass `""` for a picture that carries no information the
135
+ * page does not already say — the empty string is a DECISION the author has to make, where a
136
+ * missing attribute is an omission nobody notices (WCAG 1.1.1).
137
+ */
138
+ alt: string;
139
+ /** Frame height. Default `md`. The width follows the picture's own ratio. */
140
+ size?: ThumbnailSizeProp;
141
+ className?: ClassNameProp;
142
+ };
50
143
  /** @see EmptyState */
51
144
  /**
52
145
  * Semantic intent of the EmptyState icon medallion — a subset of the shared `ToneProp` vocabulary
@@ -255,6 +348,13 @@ export type DataTableProp<T> = {
255
348
  data: T[];
256
349
  columns: ColumnDefProp<T>[];
257
350
  getRowId?: GetRowIdProp<T>;
351
+ /**
352
+ * Human name of a row, announced by its selection checkbox or radio as "Select row {label}".
353
+ * Default: the text of the `priority: "primary"` column, else of the first column, when that
354
+ * value is a string or number; the row id only as a last resort. `rowSelection.getCheckboxProps`
355
+ * `aria-label` still overrides a single row.
356
+ */
357
+ getRowLabel?: GetRowLabelProp<T>;
258
358
  selectable?: boolean;
259
359
  selected?: SelectedIdsProp;
260
360
  onSelectChange?: OnSelectChangeProp;
@@ -1,7 +1,7 @@
1
1
  /** Feedback component prop types — @see docs/COMPONENTS.md#feedback */
2
2
  import type * as React from "react";
3
3
  import type { QueryErrorCategory } from "../../lib/query-error.js";
4
- import type { AlertVariantProp, CancelLabelProp, ChildrenProp, ClassNameProp, ConfirmLabelProp, ConfirmVariantProp, DescriptionProp, HandlerProp, IconProp, OpenProp, OnOpenChangeProp, PendingProp, ToneProp, TitleProp } from "../vocabulary/index.js";
4
+ import type { AlertVariantProp, AvatarShapeProp, CancelLabelProp, ChildrenProp, ClassNameProp, ConfirmLabelProp, ConfirmVariantProp, DescriptionProp, HandlerProp, IconProp, OpenProp, OnOpenChangeProp, PendingProp, ShapeProp, SizeProp, ToneProp, TitleProp } from "../vocabulary/index.js";
5
5
  /** @see AlertDialog */
6
6
  export type AlertDialogProp = {
7
7
  open: OpenProp;
@@ -83,3 +83,87 @@ export type SkeletonRowsProp = {
83
83
  rows?: number;
84
84
  columns?: number;
85
85
  };
86
+ /**
87
+ * A skeleton line's MEASURE. `number` is read as pixels, matching antd's
88
+ * `SkeletonParagraphProps["width"]` (components/skeleton/Paragraph.tsx); a string is any CSS length
89
+ * or percentage. It reaches the DOM as the `--skeleton-line-width` custom property rather than a
90
+ * raw `width`, so the line still resolves its own block size and radius from the token tier.
91
+ */
92
+ export type SkeletonWidth = number | string;
93
+ /**
94
+ * @see Skeleton — the placeholder BLOCK. Both fields are antd's, ported onto the block this
95
+ * library already shipped rather than onto a second component beside it.
96
+ */
97
+ export type SkeletonProp = React.HTMLAttributes<HTMLDivElement> & {
98
+ /**
99
+ * Swap the resting pulse for the travelling SHEEN (antd's `active`). antd's non-active skeleton
100
+ * is fully static; this library's block has always pulsed, so the default is left alone and
101
+ * `active` selects the louder of the two motions instead of turning motion on.
102
+ */
103
+ active?: boolean;
104
+ /**
105
+ * `false` renders `children` in place of the placeholder. An OMITTED `loading` still renders the
106
+ * placeholder — antd's `loading || !("loading" in props)`.
107
+ */
108
+ loading?: boolean;
109
+ };
110
+ /** @see SkeletonAvatar */
111
+ export type SkeletonAvatarProp = {
112
+ /** Box, from the `--control-height` tier — the same tier the real `Avatar` sizes from. */
113
+ size?: SizeProp;
114
+ shape?: AvatarShapeProp;
115
+ active?: boolean;
116
+ className?: ClassNameProp;
117
+ };
118
+ /** @see SkeletonButton */
119
+ export type SkeletonButtonProp = {
120
+ size?: SizeProp;
121
+ /** Corner, in `Button`'s own vocabulary: `pill` is antd's `shape="round"`. */
122
+ shape?: ShapeProp;
123
+ /** Fill the inline axis, for a button that spans its column (antd's `block`). */
124
+ block?: boolean;
125
+ active?: boolean;
126
+ className?: ClassNameProp;
127
+ };
128
+ /** @see SkeletonInput */
129
+ export type SkeletonInputProp = {
130
+ size?: SizeProp;
131
+ block?: boolean;
132
+ active?: boolean;
133
+ className?: ClassNameProp;
134
+ };
135
+ /** @see SkeletonNode — a square standing in for a media/custom slot; `children` centres in it. */
136
+ export type SkeletonNodeProp = {
137
+ active?: boolean;
138
+ children?: ChildrenProp;
139
+ className?: ClassNameProp;
140
+ };
141
+ /** @see SkeletonImage */
142
+ export type SkeletonImageProp = {
143
+ active?: boolean;
144
+ className?: ClassNameProp;
145
+ };
146
+ /**
147
+ * @see SkeletonArticle — antd's own `<Skeleton>` shape (avatar + title + paragraph). It is a
148
+ * SIBLING of `Skeleton`, not a replacement: `Skeleton` here is the block antd spells
149
+ * `Skeleton.Node`, and re-pointing it at the article would change what every existing call site
150
+ * renders.
151
+ */
152
+ export type SkeletonArticleProp = {
153
+ avatar?: boolean | Pick<SkeletonAvatarProp, "size" | "shape">;
154
+ /** The heading line. NOT a string title — `false` drops the line, `{ width }` re-measures it. */
155
+ title?: boolean | {
156
+ width?: SkeletonWidth;
157
+ };
158
+ /** `width` as an array measures each row; as a single value it measures the LAST row. */
159
+ paragraph?: boolean | {
160
+ rows?: number;
161
+ width?: SkeletonWidth | SkeletonWidth[];
162
+ };
163
+ /** Pill corners on every line (antd's `round`, whose capsule radius is `--radius-pill` here). */
164
+ round?: boolean;
165
+ active?: boolean;
166
+ loading?: boolean;
167
+ children?: ChildrenProp;
168
+ className?: ClassNameProp;
169
+ };
@@ -185,6 +185,47 @@ export type FlexProp = React.HTMLAttributes<HTMLElement> & {
185
185
  * only on the narrow side (a compact-only affordance). Omit for no attribute and no rule.
186
186
  */
187
187
  hideFrom?: BreakpointProp;
188
+ /**
189
+ * ESCAPE HATCH: the same drop, at a width in PIXELS that is off every step of the scale — the
190
+ * `gapRaw`/`padRaw` contract applied to the breakpoint axis (gh#528).
191
+ *
192
+ * ## Why the token form stays the default
193
+ *
194
+ * An off-scale breakpoint is a LOCAL EXCEPTION, not a new tier. `sm`/`md`/`lg`/`xl` are the
195
+ * package's canonical steps, shared with `--master-detail-collapse-below` and with every
196
+ * `collapseBelow`, so two regions asked to fold "at the same place" actually fold together.
197
+ * A raw width folds one region and nothing else; spend the four named steps FIRST, and reach
198
+ * here only when the canonical design specifies a width the scale does not have (a nav that
199
+ * becomes a hamburger at 900px).
200
+ *
201
+ * ## The price, and why it is the right one
202
+ *
203
+ * It leaves `data-hide-below-raw` on the DOM, exactly like `data-gap-raw`/`data-pad-raw`, so
204
+ * every escape is COUNTABLE — grep the source or scan the DOM and a repo drifting off the scale
205
+ * shows up as a number instead of a feeling. Before this prop the only move left was
206
+ * `className="hidden min-[901px]:flex"`, which `ui-audit` blocks and which counts as nothing.
207
+ *
208
+ * It also prints ONE media rule per distinct width (a media query cannot read a `var()`, so an
209
+ * off-scale width can only reach CSS as a literal).
210
+ *
211
+ * ## The seam
212
+ *
213
+ * `hideBelowRaw` hides while `width < N`, `hideFromRaw` hides while `width >= N` — the SAME pair
214
+ * of comparisons as the token steps, so the two are exact complements and at exactly N the
215
+ * `hideBelowRaw` region is the visible one. Do NOT reach for an inclusive `<= N` here: pairing
216
+ * `<= N` with `>= N` leaves BOTH regions hidden at exactly N, which is the one-pixel hole a
217
+ * consumer measured on a hand-rolled `max-[900px]:` pair (gh#528).
218
+ *
219
+ * When present it WINS over `hideBelow`, which then emits no attribute, so the two cannot both
220
+ * match at one width.
221
+ */
222
+ hideBelowRaw?: number;
223
+ /**
224
+ * The inverse of `hideBelowRaw` — drop the region FROM a raw pixel width upwards. Same contract,
225
+ * same price, same seam (`width >= N`); it wins over `hideFrom` and leaves `data-hide-from-raw`
226
+ * on the DOM.
227
+ */
228
+ hideFromRaw?: number;
188
229
  /**
189
230
  * Take the space the siblings leave — the Flex becomes the row's ELASTIC column.
190
231
  *
@@ -802,6 +843,21 @@ export type SidebarItemProp = {
802
843
  * merely unread: an @mention, a direct message, a failure waiting on them.
803
844
  */
804
845
  badgeTone?: SidebarBadgeToneProp;
846
+ /**
847
+ * TRAILING 16px glyph — the disclosure mark of a row that opens something (the `⌃⌄` of a
848
+ * workspace switcher, a `→` on a row that leaves the app). Pinned to the same box as `icon`,
849
+ * and hidden on the collapsed rail exactly like `badge`.
850
+ *
851
+ * It is NOT `badge`, and the difference is the pill: `badge` wraps whatever it is given in
852
+ * `.sb-badge` — a 9999px-radius `hsl(var(--secondary))` capsule sized for a COUNT — so a
853
+ * chevron passed there renders as a grey lozenge with a 24px SVG inside it (measured 36×24 with
854
+ * a 24×24 glyph, beside a 16×16 leading icon in the same 32px row). A glyph draws no surface,
855
+ * so it gets its own slot rather than a size tier on the count pill.
856
+ *
857
+ * Takes the COMPONENT, like `icon` — not an element. That is what lets the rail pin the size;
858
+ * a `ReactNode` hole is how `badge` ended up carrying an unsized 24px SVG.
859
+ */
860
+ trailingIcon?: ComponentType<SVGProps<SVGSVGElement>>;
805
861
  disabled?: boolean;
806
862
  /**
807
863
  * Destination of the row. It is the SOLE interactive element (no nested `<button>`), so
@@ -1164,7 +1220,22 @@ export type SidebarProp = {
1164
1220
  * canonical row.
1165
1221
  */
1166
1222
  renderItem?: (item: SidebarItemData, rowProps: SidebarRenderItemProp) => ReactNode;
1167
- footer?: ReactNode;
1223
+ /**
1224
+ * Footer slot pinned BELOW the scroll area — identity, status, a mode switch.
1225
+ *
1226
+ * Takes a FUNCTION for the same reason `brand` does, and it is the same reason twice because
1227
+ * both slots live INSIDE the collapsible rail: a plain node is built outside this component and
1228
+ * cannot see the EFFECTIVE collapsed state, so the consumer has no correct move. Reading their
1229
+ * own `collapsed` boolean renders a glyph-only footer inside the full-width drawer (`AppShell`
1230
+ * hands the same Sidebar to both surfaces and the drawer un-collapses); building a second
1231
+ * Sidebar for `AppShell.mobileNav` is the override that switches `railInDrawer` off; doing
1232
+ * neither leaves the expanded footer to reflow inside a 64px rail (measured: a two-line identity
1233
+ * block went 255×66 docked → 63×111 collapsed, wrapping the name across three lines).
1234
+ *
1235
+ * The function is called with the surface-effective value. A plain `ReactNode` still works
1236
+ * unchanged.
1237
+ */
1238
+ footer?: ReactNode | ((collapsed: boolean) => ReactNode);
1168
1239
  /** Override the nav landmark's accessible name. Defaults to a localized "Main navigation". */
1169
1240
  "aria-label"?: string;
1170
1241
  };