@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
@@ -1,8 +1,20 @@
1
1
  import type { AppLocale } from "./types.js";
2
2
  import type { AppDateFormat } from "./date-formats.js";
3
3
  export declare const APP_DATE_FORMAT_OPTIONS: {
4
- value: "dmy" | "iso" | "mdy";
4
+ value: "dmy" | "iso" | "mdy" | "ymd";
5
5
  }[];
6
6
  export declare function getDateFormatLabel(dateFormat: AppDateFormat, locale: AppLocale, fallbackLocale?: AppLocale): string;
7
- /** Suggested default per locale — vi → dmy, ja → iso, en → mdy. */
7
+ /**
8
+ * Suggested default per locale — vi → dmy, ja → ymd, en → mdy.
9
+ *
10
+ * `ja` used to be `iso` (`yyyy-MM-dd`). Nothing recorded a reason for it, and it is not the form a
11
+ * Japanese business document is written in: those use `YYYY/MM/DD` (or `YYYY年MM月DD日`). The cost
12
+ * of the old default was measured in a consumer — gino-cloud shipped its own date formatter as an
13
+ * explicit stopgap purely to get the slashes, which is the package's own rule 1 broken by the
14
+ * package's own default.
15
+ *
16
+ * ONLY THE DEFAULT MOVES. A stored preference still wins (see `resolveDateFormat` in
17
+ * app-provider.tsx), a service can pin any value through `defaultDateFormat`, and `iso` is still
18
+ * one keystroke away in `DateFormatPicker`.
19
+ */
8
20
  export declare function resolveDefaultDateFormat(locale: AppLocale): AppDateFormat;
@@ -6,7 +6,7 @@ function getDateFormatLabel(dateFormat, locale, fallbackLocale = "en") {
6
6
  }
7
7
  function resolveDefaultDateFormat(locale) {
8
8
  if (locale === "en") return "mdy";
9
- if (locale === "ja") return "iso";
9
+ if (locale === "ja") return "ymd";
10
10
  return "dmy";
11
11
  }
12
12
  export {
@@ -1,7 +1,19 @@
1
1
  /** Date-only display preset — sent as `x-date-format` to backend. */
2
2
  import { type AppTimeFormat } from "./time-formats.js";
3
- export type AppDateFormat = "iso" | "dmy" | "mdy";
4
- export declare const APP_DATE_FORMATS: readonly ["iso", "dmy", "mdy"];
3
+ export type AppDateFormat = "iso" | "ymd" | "dmy" | "mdy";
4
+ /**
5
+ * `ymd` (`yyyy/MM/dd`) IS THE JAPANESE BUSINESS FORM, AND ITS ABSENCE WAS A HOLE WITH NO WAY OUT.
6
+ *
7
+ * The axis shipped three values and none of them said `2026/05/01`: `iso` writes hyphens, and the
8
+ * two slash forms put the day or the month first. A Japanese-first consumer (gino-cloud) therefore
9
+ * wrote its own formatter and marked it an explicit stopgap — the tell that a named axis was
10
+ * missing a value rather than the consumer wanting something exotic, since the only alternative
11
+ * the package left was the thing docs/DATETIME.md rule 1 forbids ("never use date-fns/format,
12
+ * toLocaleString, or raw ISO slices for UI").
13
+ *
14
+ * It sits BEFORE the day/month-first pair because it is the other year-first form, next to `iso`.
15
+ */
16
+ export declare const APP_DATE_FORMATS: readonly ["iso", "ymd", "dmy", "mdy"];
5
17
  export declare const APP_REQUEST_HEADER_DATE_FORMAT: "x-date-format";
6
18
  /** date-fns pattern for date-only display. */
7
19
  export declare function getDatePattern(dateFormat: AppDateFormat): string;
@@ -1,8 +1,15 @@
1
1
  import { getTimePattern } from "./time-formats.js";
2
- const APP_DATE_FORMATS = ["iso", "dmy", "mdy"];
2
+ const APP_DATE_FORMATS = [
3
+ "iso",
4
+ "ymd",
5
+ "dmy",
6
+ "mdy"
7
+ ];
3
8
  const APP_REQUEST_HEADER_DATE_FORMAT = "x-date-format";
4
9
  function getDatePattern(dateFormat) {
5
10
  switch (dateFormat) {
11
+ case "ymd":
12
+ return "yyyy/MM/dd";
6
13
  case "dmy":
7
14
  return "dd/MM/yyyy";
8
15
  case "mdy":
@@ -16,7 +23,7 @@ function getDateTimePattern(timeFormat, dateFormat) {
16
23
  return `${getDatePattern(dateFormat)} ${getTimePattern(timeFormat)}`;
17
24
  }
18
25
  function isAppDateFormat(value) {
19
- return value === "iso" || value === "dmy" || value === "mdy";
26
+ return APP_DATE_FORMATS.includes(value);
20
27
  }
21
28
  export {
22
29
  APP_DATE_FORMATS,
@@ -13,8 +13,13 @@ type CardAccent = "primary" | "success" | "warning" | "info" | "attention" | "de
13
13
  * borrowing the brand colour.
14
14
  */
15
15
  type CardAccentPlacement = "edge" | "perimeter";
16
- /** Surface fill — plain card, muted band, borderless outline, or emphasized featured ring. */
17
- type CardVariant = "default" | "muted" | "outline" | "featured";
16
+ /**
17
+ * Surface fill and edge. `outline` is antd's `outlined` — it drops the FILL and keeps the
18
+ * hairline; `borderless` is antd's `variant="borderless"` (its deprecated `bordered={false}`) —
19
+ * it drops the HAIRLINE and keeps the fill. They are two different cards, which is why both
20
+ * values exist: neither one substitutes for the other.
21
+ */
22
+ type CardVariant = "default" | "muted" | "outline" | "borderless" | "featured";
18
23
  /** Padding density — base 16px · tight 12px · cozy 20px. */
19
24
  type CardDensity = "tight" | "cozy";
20
25
  export type CardProps = React.HTMLAttributes<HTMLDivElement> & {
@@ -26,6 +31,16 @@ export type CardProps = React.HTMLAttributes<HTMLDivElement> & {
26
31
  accentPlacement?: CardAccentPlacement;
27
32
  variant?: CardVariant;
28
33
  density?: CardDensity;
34
+ /**
35
+ * Lift the card on hover (antd `hoverable`) — the resting elevation steps up to
36
+ * `--card-hover-shadow` and the pointer becomes a `pointer`.
37
+ *
38
+ * This is a PRESENTATION flag, not an interaction: it announces nothing and binds no handler.
39
+ * A card that looks clickable has to BE clickable for everyone, so pair it with a real control
40
+ * — a `Link`/`Button` in the header or footer, or the whole card rendered as one — never with a
41
+ * bare `onClick` on this div, which a keyboard or screen-reader user cannot reach.
42
+ */
43
+ hoverable?: boolean;
29
44
  };
30
45
  export declare const Card: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & {
31
46
  accent?: CardAccent;
@@ -36,6 +51,16 @@ export declare const Card: React.ForwardRefExoticComponent<React.HTMLAttributes<
36
51
  accentPlacement?: CardAccentPlacement;
37
52
  variant?: CardVariant;
38
53
  density?: CardDensity;
54
+ /**
55
+ * Lift the card on hover (antd `hoverable`) — the resting elevation steps up to
56
+ * `--card-hover-shadow` and the pointer becomes a `pointer`.
57
+ *
58
+ * This is a PRESENTATION flag, not an interaction: it announces nothing and binds no handler.
59
+ * A card that looks clickable has to BE clickable for everyone, so pair it with a real control
60
+ * — a `Link`/`Button` in the header or footer, or the whole card rendered as one — never with a
61
+ * bare `onClick` on this div, which a keyboard or screen-reader user cannot reach.
62
+ */
63
+ hoverable?: boolean;
39
64
  } & React.RefAttributes<HTMLDivElement>>;
40
65
  /** Full-bleed cover media — first child; header below uses section top (φ⁰), not shell. */
41
66
  export type CardCoverProps = React.HTMLAttributes<HTMLDivElement>;
@@ -90,12 +115,30 @@ export type CardFooterProps = React.HTMLAttributes<HTMLDivElement> & {
90
115
  separated?: boolean;
91
116
  /** Full-bleed footer (the conventional `actions` bar). */
92
117
  flush?: boolean;
118
+ /**
119
+ * Split the footer into EQUAL-WIDTH cells divided by vertical rules — antd's `actions` row.
120
+ *
121
+ * A different band from `separated`, which packs its children at the inline end at their
122
+ * natural widths: that is the right shape for a Save/Cancel pair and the wrong one for antd's
123
+ * divided strip. Self-sufficient — it draws its own top rule and full-bleed edges, because
124
+ * upstream's actions row is never anything else.
125
+ */
126
+ actions?: boolean;
93
127
  };
94
128
  export declare const CardFooter: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & {
95
129
  /** Top border + symmetric action band — form Save/Cancel, table summary. */
96
130
  separated?: boolean;
97
131
  /** Full-bleed footer (the conventional `actions` bar). */
98
132
  flush?: boolean;
133
+ /**
134
+ * Split the footer into EQUAL-WIDTH cells divided by vertical rules — antd's `actions` row.
135
+ *
136
+ * A different band from `separated`, which packs its children at the inline end at their
137
+ * natural widths: that is the right shape for a Save/Cancel pair and the wrong one for antd's
138
+ * divided strip. Self-sufficient — it draws its own top rule and full-bleed edges, because
139
+ * upstream's actions row is never anything else.
140
+ */
141
+ actions?: boolean;
99
142
  } & React.RefAttributes<HTMLDivElement>>;
100
143
  export type CardBarProps = React.HTMLAttributes<HTMLDivElement> & {
101
144
  pad?: PadProp;
@@ -3,13 +3,14 @@ import { padStyle, padStepToken } from "../../lib/variants.js";
3
3
  import * as React from "react";
4
4
  import { cn } from "../../lib/utils.js";
5
5
  const Card = React.forwardRef(
6
- ({ className, accent, accentPlacement, variant, density, ...props }, ref) => /* @__PURE__ */ jsx(
6
+ ({ className, accent, accentPlacement, variant, density, hoverable, ...props }, ref) => /* @__PURE__ */ jsx(
7
7
  "div",
8
8
  {
9
9
  ref,
10
10
  className: cn("group/card", className),
11
11
  "data-slot": "card",
12
12
  "data-accent": accent,
13
+ "data-hoverable": hoverable ? "" : void 0,
13
14
  "data-accent-placement": accentPlacement === "perimeter" ? "perimeter" : void 0,
14
15
  "data-variant": variant && variant !== "default" ? variant : void 0,
15
16
  "data-density": density,
@@ -61,13 +62,14 @@ const CardContent = React.forwardRef(
61
62
  );
62
63
  CardContent.displayName = "CardContent";
63
64
  const CardFooter = React.forwardRef(
64
- ({ className, separated, flush, ...props }, ref) => /* @__PURE__ */ jsx(
65
+ ({ className, separated, flush, actions, ...props }, ref) => /* @__PURE__ */ jsx(
65
66
  "div",
66
67
  {
67
68
  ref,
68
69
  "data-slot": "card-footer",
69
70
  "data-separated": separated ? "" : void 0,
70
71
  "data-flush": flush ? "" : void 0,
72
+ "data-actions": actions ? "" : void 0,
71
73
  className,
72
74
  ...props
73
75
  }
@@ -13,6 +13,13 @@ interface DataTableProps<T> {
13
13
  columns: ColumnDef<T>[];
14
14
  /** Required when `selectable` is true. Default: assume row.id (typed as any). */
15
15
  getRowId?: (row: T) => string;
16
+ /**
17
+ * Human name of a row, announced by its selection checkbox or radio as "Select row {label}".
18
+ * Default: the text of the `priority: "primary"` column, else of the first column, when that
19
+ * value is a string or number; the row id only as a last resort — an id is a KEY, and announced
20
+ * it reads a UUID aloud. `rowSelection.getCheckboxProps` `aria-label` still overrides a row.
21
+ */
22
+ getRowLabel?: (row: T) => string;
16
23
  selectable?: boolean;
17
24
  selected?: Set<string>;
18
25
  onSelectChange?: (next: Set<string>) => void;
@@ -130,7 +137,7 @@ interface DataTableProps<T> {
130
137
  className?: string;
131
138
  children?: React.ReactNode;
132
139
  }
133
- export declare function DataTable<T>({ data, columns, getRowId, selectable, selected: controlledSelected, onSelectChange, onRowClick, density: controlledDensity, onDensityChange, sort, onSortChange, globalFilter: controlledGlobalFilter, onGlobalFilterChange, pagination: paginationProp, onPaginationChange, rowCount, columnVisibility: controlledVisibility, onColumnVisibilityChange, manualSorting, manualFiltering, manualPagination, loading, empty, error, denied, onRetry, striped, hoverable, stickyHeader, preset, collapseBelow, rowClassName, rowTone, rowSelection, expandable, summary, scroll, sticky, onRow, bordered, showSorterTooltip, sortDirections, onFilterChange, className, children, }: DataTableProps<T>): React.JSX.Element;
140
+ export declare function DataTable<T>({ data, columns, getRowId, getRowLabel, selectable, selected: controlledSelected, onSelectChange, onRowClick, density: controlledDensity, onDensityChange, sort, onSortChange, globalFilter: controlledGlobalFilter, onGlobalFilterChange, pagination: paginationProp, onPaginationChange, rowCount, columnVisibility: controlledVisibility, onColumnVisibilityChange, manualSorting, manualFiltering, manualPagination, loading, empty, error, denied, onRetry, striped, hoverable, stickyHeader, preset, collapseBelow, rowClassName, rowTone, rowSelection, expandable, summary, scroll, sticky, onRow, bordered, showSorterTooltip, sortDirections, onFilterChange, className, children, }: DataTableProps<T>): React.JSX.Element;
134
141
  export declare namespace DataTable {
135
142
  var Toolbar: ({ children, className, }: {
136
143
  children?: React.ReactNode;
@@ -190,6 +190,7 @@ function DataTable({
190
190
  data,
191
191
  columns,
192
192
  getRowId = noopGetRowId,
193
+ getRowLabel,
193
194
  selectable = false,
194
195
  selected: controlledSelected,
195
196
  onSelectChange,
@@ -443,6 +444,7 @@ function DataTable({
443
444
  toggleExpanded,
444
445
  rowSelection,
445
446
  getRowId,
447
+ getRowLabel,
446
448
  showSorterTooltip,
447
449
  sortDirections,
448
450
  sortPriorities,
@@ -764,6 +766,7 @@ DataTable.Content = function DataTableContent() {
764
766
  toggleExpanded,
765
767
  rowSelection,
766
768
  getRowId,
769
+ getRowLabel,
767
770
  showSorterTooltip,
768
771
  sortDirections,
769
772
  sortPriorities
@@ -791,6 +794,7 @@ DataTable.Content = function DataTableContent() {
791
794
  );
792
795
  }
793
796
  }, [missingHeaderNames]);
797
+ const selectLabelColumn = visibleColumns.find((col) => col.priority === "primary") ?? visibleColumns[0];
794
798
  const scrollRef = React.useRef(null);
795
799
  const [hasOverflowEnd, setHasOverflowEnd] = React.useState(false);
796
800
  React.useEffect(() => {
@@ -1096,6 +1100,9 @@ DataTable.Content = function DataTableContent() {
1096
1100
  const rowKey = getRowId(original);
1097
1101
  const isSelected = row.getIsSelected();
1098
1102
  const checkboxProps = rowSelection?.getCheckboxProps?.(original) ?? {};
1103
+ const labelValue = selectLabelColumn ? original[selectLabelColumn.key] : void 0;
1104
+ const rowLabel = getRowLabel?.(original) ?? (typeof labelValue === "string" && labelValue.trim() !== "" || typeof labelValue === "number" ? String(labelValue) : row.id);
1105
+ const selectRowLabel = checkboxProps["aria-label"] ?? t("dataTable.selectRow", { id: rowLabel });
1099
1106
  const canExpand = expandColumnShown && (expandable?.rowExpandable?.(original) ?? true);
1100
1107
  const isExpanded = canExpand && expandedKeys.includes(rowKey);
1101
1108
  const rowProps = onRow?.(original, rowIndex) ?? {};
@@ -1206,7 +1213,7 @@ DataTable.Content = function DataTableContent() {
1206
1213
  RadioItem,
1207
1214
  {
1208
1215
  value: rowKey,
1209
- "aria-label": checkboxProps["aria-label"] ?? t("dataTable.selectRow", { id: row.id }),
1216
+ "aria-label": selectRowLabel,
1210
1217
  onClick: (e) => {
1211
1218
  e.stopPropagation();
1212
1219
  }
@@ -1222,7 +1229,7 @@ DataTable.Content = function DataTableContent() {
1222
1229
  onCheckedChange: (v) => {
1223
1230
  row.toggleSelected(!!v);
1224
1231
  },
1225
- "aria-label": checkboxProps["aria-label"] ?? t("dataTable.selectRow", { id: row.id }),
1232
+ "aria-label": selectRowLabel,
1226
1233
  onClick: (e) => {
1227
1234
  e.stopPropagation();
1228
1235
  }
@@ -0,0 +1,26 @@
1
+ export type { FeatureItemProp, FeatureListProp, FeatureListProp as FeatureListProps, FeatureStateProp, } from "../../props/components/data-display.prop.js";
2
+ import type { FeatureListProp } from "../../props/components/data-display.prop.js";
3
+ /**
4
+ * FeatureList — a list of STATEMENTS, each carrying a leading state glyph: what a plan includes,
5
+ * what a tier supports, which requirements a submission met.
6
+ *
7
+ * ## Why it is not one of the three components it looks like
8
+ *
9
+ * - **`ListRow`** is a single-line ENTITY row — a session, a token, a passkey — with entity
10
+ * padding, a divider between every row and a trailing action slot. A feature line is none of
11
+ * those: it has no action, it is not a thing you can open, and reading a plan's contents
12
+ * through a stack of ruled rows makes six facts look like six records.
13
+ * - **`Timeline`** has the glyph rail, but it is an ORDERED event list: a connector line runs
14
+ * dot-to-dot and the statuses are `done`/`current`/`pending`, which is time, not inclusion.
15
+ * - **`Descriptions`** is a term/value grid. A feature line has no value column — the state IS
16
+ * the value, and it is drawn, not written.
17
+ *
18
+ * ## The alignment, which is the actual reason this exists
19
+ *
20
+ * The glyph column is one line box tall (`1lh`) and the glyph is centred in it, so it lands on
21
+ * the FIRST line of a label that wraps to three. That is why the component can be handed a
22
+ * one-word label and a paragraph and both come out aligned, at any font size and at any density,
23
+ * with no number in the call site. The shape it replaces reached for `mt-0.5` — 2px, below
24
+ * `--space-1`, off every scale, and wrong the moment the text beside it changes size.
25
+ */
26
+ export declare function FeatureList({ items, className, ...props }: FeatureListProp): import("react").JSX.Element;
@@ -0,0 +1,29 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { Check, Minus, X } from "lucide-react";
4
+ import { useTranslation } from "../../i18n/use-translation.js";
5
+ import { cn } from "../../lib/utils.js";
6
+ const STATE = {
7
+ included: { Icon: Check, key: "dataDisplay.featureList.included" },
8
+ excluded: { Icon: X, key: "dataDisplay.featureList.excluded" },
9
+ limited: { Icon: Minus, key: "dataDisplay.featureList.limited" }
10
+ };
11
+ function FeatureList({ items, className, ...props }) {
12
+ const { t } = useTranslation();
13
+ return /* @__PURE__ */ jsx("ul", { className: cn("ui-feature-list", className), ...props, children: items.map((item, index) => {
14
+ const { Icon, key } = STATE[item.state];
15
+ return /* @__PURE__ */ jsxs("li", { className: "ui-feature-list-item", "data-state": item.state, children: [
16
+ /* @__PURE__ */ jsx("span", { className: "ui-feature-list-mark", "aria-hidden": "true", children: /* @__PURE__ */ jsx(Icon, {}) }),
17
+ /* @__PURE__ */ jsxs("span", { className: "ui-feature-list-body", children: [
18
+ /* @__PURE__ */ jsxs("span", { className: "ui-feature-list-label", children: [
19
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: t(key) }),
20
+ item.label
21
+ ] }),
22
+ item.description != null ? /* @__PURE__ */ jsx("span", { className: "ui-feature-list-description", children: item.description }) : null
23
+ ] })
24
+ ] }, index);
25
+ }) });
26
+ }
27
+ export {
28
+ FeatureList
29
+ };
@@ -2,6 +2,12 @@ export { Badge, StatusBadge } from "./badge.js";
2
2
  export type { BadgeProps, BadgeTone } from "./badge.js";
3
3
  export { Legend } from "./legend.js";
4
4
  export type { LegendItemProp, LegendProp, LegendProps } from "./legend.js";
5
+ export { Swatch } from "./swatch.js";
6
+ export type { SwatchProp, SwatchProps } from "./swatch.js";
7
+ export { FeatureList } from "./feature-list.js";
8
+ export type { FeatureItemProp, FeatureListProp, FeatureListProps, FeatureStateProp, } from "./feature-list.js";
9
+ export { Thumbnail } from "./thumbnail.js";
10
+ export type { ThumbnailProp, ThumbnailProps, ThumbnailSizeProp } from "./thumbnail.js";
5
11
  export { ListRow } from "./list-row.js";
6
12
  export type { ListRowProps, ListRowDensity } from "./list-row.js";
7
13
  export { CredentialReveal } from "./credential-reveal.js";
@@ -1,5 +1,8 @@
1
1
  import { Badge, StatusBadge } from "./badge.js";
2
2
  import { Legend } from "./legend.js";
3
+ import { Swatch } from "./swatch.js";
4
+ import { FeatureList } from "./feature-list.js";
5
+ import { Thumbnail } from "./thumbnail.js";
3
6
  import { ListRow } from "./list-row.js";
4
7
  import { CredentialReveal } from "./credential-reveal.js";
5
8
  import { QrCode } from "./qr-code.js";
@@ -90,6 +93,7 @@ export {
90
93
  DataTable,
91
94
  Descriptions,
92
95
  EmptyState,
96
+ FeatureList,
93
97
  HoverCard,
94
98
  HoverCardContent,
95
99
  HoverCardTrigger,
@@ -114,12 +118,14 @@ export {
114
118
  ServiceLauncherCardSkeleton,
115
119
  StatCard,
116
120
  StatusBadge,
121
+ Swatch,
117
122
  Table,
118
123
  TableBody,
119
124
  TableCell,
120
125
  TableHead,
121
126
  TableHeader,
122
127
  TableRow,
128
+ Thumbnail,
123
129
  Timeline,
124
130
  TimelineGrid,
125
131
  Tree,
@@ -0,0 +1,24 @@
1
+ import type { SwatchProp } from "../../props/components/data-display.prop.js";
2
+ export type { SwatchProp, SwatchProp as SwatchProps, } from "../../props/components/data-display.prop.js";
3
+ /**
4
+ * Swatch — a READ-ONLY sample of one colour a person chose (gh#527).
5
+ *
6
+ * The colour arrives as a VALUE, never as a token: it is a brand's `primary_color`, a calendar
7
+ * category, a tag a user tinted. That is the axis `Legend` cannot express — its `tone` is a closed
8
+ * set of semantic roles — and the reason there was no legal way to draw one: a hand-rolled
9
+ * `<span className="w-[10px] h-[10px] rounded-[2px] bg-[#c0392f]" />` is blocked by ui-audit three
10
+ * times over, so the only remaining move was a suppression comment.
11
+ *
12
+ * It is the same square mark `Legend` draws (a sample reads as a SAMPLE; a pill reads as a chip
13
+ * you could click), one step larger, because it stands beside a name rather than inside an 11px
14
+ * key. The hairline is not decoration: without it a white or near-white value is invisible on a
15
+ * white card, and the sample stops showing anything at all.
16
+ *
17
+ * ## The accessible name
18
+ *
19
+ * Given an `aria-label` the mark is a `role="img"` that announces it, which is what lets the
20
+ * colour be shown with no visible label beside it. Given none it is `aria-hidden` — the Legend
21
+ * rule, for the same reason: a swatch that repeats a label already on screen would announce the
22
+ * same thing twice. Colour is never the sole carrier either way.
23
+ */
24
+ export declare function Swatch({ color, className, style, ...props }: SwatchProp): import("react").JSX.Element;
@@ -0,0 +1,19 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { cn } from "../../lib/utils.js";
3
+ function Swatch({ color, className, style, ...props }) {
4
+ const named = props["aria-label"] !== void 0 || props["aria-labelledby"] !== void 0;
5
+ return /* @__PURE__ */ jsx(
6
+ "span",
7
+ {
8
+ "data-slot": "swatch",
9
+ className: cn("ui-swatch", className),
10
+ style: { ...style, ["--swatch-color"]: color },
11
+ role: named ? "img" : void 0,
12
+ "aria-hidden": named ? void 0 : true,
13
+ ...props
14
+ }
15
+ );
16
+ }
17
+ export {
18
+ Swatch
19
+ };
@@ -0,0 +1,37 @@
1
+ import * as React from "react";
2
+ export type { ThumbnailProp, ThumbnailProp as ThumbnailProps, ThumbnailSizeProp, } from "../../props/components/data-display.prop.js";
3
+ /**
4
+ * Thumbnail — a framed picture at a FIXED HEIGHT and its OWN intrinsic width.
5
+ *
6
+ * That combination is the whole component. A wrapping row of screenshots holds portrait and
7
+ * landscape shots together; equal heights make the row read as one strip, and letting each
8
+ * width follow its own ratio is what keeps every shot uncropped.
9
+ *
10
+ * ## Why not the four components it is mistaken for
11
+ *
12
+ * - **`AspectRatio`** constrains a RATIO and spans `width: 100%`. Two different intrinsic ratios
13
+ * in one row can only be forced into one number by letterboxing or cropping them, which is the
14
+ * opposite of the requirement. It also draws no frame.
15
+ * - **`Avatar`** is an identity mark (with initials fallback), not a picture.
16
+ * - **`Card`** puts padding between its edge and the picture. The frame here is ON the image.
17
+ * - **`CardCover`** is the full-bleed media slot INSIDE a Card, not a standalone thumbnail.
18
+ *
19
+ * ## The frame is on the `<img>` itself
20
+ *
21
+ * No wrapper element. A wrapper would have to be told the image's width to hug it, which is
22
+ * exactly the number nobody has until the picture loads; a border on the replaced element is
23
+ * flush by construction at every ratio.
24
+ *
25
+ * ## Reserve the width before the picture arrives
26
+ *
27
+ * With an intrinsic width there is nothing to lay out until the bytes land, so a row of these
28
+ * reflows as they load. Pass the file's real pixel `width` and `height` attributes — the browser
29
+ * derives the ratio from them, and with the block size already fixed the frame takes its final
30
+ * width on the first paint (`<Thumbnail width={1280} height={800} … />`).
31
+ */
32
+ export declare const Thumbnail: React.ForwardRefExoticComponent<Omit<React.ImgHTMLAttributes<HTMLImageElement>, "alt"> & {
33
+ src: string;
34
+ alt: string;
35
+ size?: import("./thumbnail.js").ThumbnailSizeProp;
36
+ className?: import("../../props/index.js").ClassNameProp;
37
+ } & React.RefAttributes<HTMLImageElement>>;
@@ -0,0 +1,20 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { cn } from "../../lib/utils.js";
4
+ const Thumbnail = React.forwardRef(
5
+ ({ size = "md", className, alt, ...props }, ref) => /* @__PURE__ */ jsx(
6
+ "img",
7
+ {
8
+ ref,
9
+ "data-slot": "thumbnail",
10
+ "data-size": size,
11
+ alt,
12
+ className: cn("ui-thumbnail", className),
13
+ ...props
14
+ }
15
+ )
16
+ );
17
+ Thumbnail.displayName = "Thumbnail";
18
+ export {
19
+ Thumbnail
20
+ };
@@ -94,7 +94,7 @@ const CheckboxRoot = React.forwardRef((props, ref) => {
94
94
  // itself). react-aria's root is a `<label>`, which is `display:inline` — without this the
95
95
  // 16px box collapses to nothing. CheckboxVisual already carries the same three for the
96
96
  // same reason.
97
- "peer ui-checkbox data-[invalid]:border-destructive data-[state=checked]:border-primary data-[state=checked]:text-primary-foreground inline-flex shrink-0 items-center justify-center shadow-xs transition-shadow outline-none",
97
+ "peer ui-checkbox data-[invalid]:border-destructive data-[state=checked]:border-primary data-[state=checked]:text-primary-foreground inline-flex shrink-0 items-center justify-center shadow-xs transition-shadow",
98
98
  className
99
99
  ),
100
100
  render: (domProps) => /* @__PURE__ */ jsx("label", { ...domProps, children: withOwnHitTarget(domProps.children) }),
@@ -47,7 +47,7 @@ function FormField({
47
47
  const resolvedId = id ?? autoId;
48
48
  const labelId = `${resolvedId}-label`;
49
49
  const helperId = helper ? `${resolvedId}-helper` : void 0;
50
- const helperNode = helper ? /* @__PURE__ */ jsx("p", { id: helperId, className: "text-muted-foreground text-xs", children: helper }) : null;
50
+ const helperNode = helper ? /* @__PURE__ */ jsx("p", { id: helperId, className: "ui-form-field-note text-muted-foreground text-xs", children: helper }) : null;
51
51
  const validationStatus = error ? "error" : validateStatus;
52
52
  const feedbackId = hasFeedback && validationStatus ? `${resolvedId}-feedback` : void 0;
53
53
  const FeedbackIcon = validationStatus === "validating" ? LoaderCircle : validationStatus === "error" ? CircleAlert : validationStatus === "warning" ? TriangleAlert : CheckCircle;
@@ -175,7 +175,7 @@ function FormField({
175
175
  t(`dataEntry.form.${validationStatus}`)
176
176
  ] }) : null,
177
177
  helperPlacement === "after" ? helperNode : null,
178
- error ? /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "text-destructive text-xs", children: error }) : null
178
+ error ? /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "ui-form-field-note text-destructive text-xs", children: error }) : null
179
179
  ] })
180
180
  ]
181
181
  }
@@ -13,7 +13,7 @@ import {
13
13
  } from "./control-appearance.js";
14
14
  import { resolveAllowClear } from "./control-surface.js";
15
15
  const inputBaseClass = [
16
- "ui-control ui-input w-full rounded-[var(--control-radius)] transition-[color,box-shadow] outline-none",
16
+ "ui-control ui-input w-full rounded-[var(--control-radius)] transition-[color,box-shadow]",
17
17
  "selection:bg-primary selection:text-primary-foreground",
18
18
  "placeholder:text-muted-foreground",
19
19
  "aria-invalid:border-destructive",
@@ -57,6 +57,21 @@ const NumberInput = React.forwardRef(
57
57
  }, ref) => {
58
58
  const { t, locale } = useTranslation();
59
59
  const fieldA11y = pickFieldA11y(ariaProps);
60
+ const stepperVerbId = React.useId();
61
+ const fieldLabel = typeof fieldA11y["aria-label"] === "string" ? fieldA11y["aria-label"] : void 0;
62
+ const fieldLabelledBy = typeof fieldA11y["aria-labelledby"] === "string" ? fieldA11y["aria-labelledby"] : void 0;
63
+ const stepperName = (direction) => {
64
+ const verb = t(`ui.numberInput.${direction}`);
65
+ if (fieldLabelledBy) {
66
+ return { "aria-labelledby": `${fieldLabelledBy} ${stepperVerbId}-${direction}` };
67
+ }
68
+ if (fieldLabel) {
69
+ return {
70
+ "aria-label": t(`ui.numberInput.${direction}Field`, { label: fieldLabel }) || verb
71
+ };
72
+ }
73
+ return { "aria-label": verb };
74
+ };
60
75
  const isControlled = controlledValue !== void 0;
61
76
  const [internal, setInternal] = React.useState(defaultValue);
62
77
  const numericValue = isControlled ? controlledValue ?? null : internal;
@@ -228,7 +243,7 @@ const NumberInput = React.forwardRef(
228
243
  }
229
244
  ) : null,
230
245
  controls ? /* @__PURE__ */ jsxs("span", { "data-slot": "number-input-steppers", className: "ui-number-input-steppers", children: [
231
- /* @__PURE__ */ jsx(
246
+ /* @__PURE__ */ jsxs(
232
247
  Button,
233
248
  {
234
249
  type: "button",
@@ -237,12 +252,15 @@ const NumberInput = React.forwardRef(
237
252
  className: "ui-number-input-step ui-number-input-step-up",
238
253
  tabIndex: -1,
239
254
  disabled: !interactive || atMax,
240
- "aria-label": t("ui.numberInput.increment"),
255
+ ...stepperName("increment"),
241
256
  onClick: () => stepBy(1),
242
- children: /* @__PURE__ */ jsx(ChevronUp, { "aria-hidden": "true" })
257
+ children: [
258
+ /* @__PURE__ */ jsx(ChevronUp, { "aria-hidden": "true" }),
259
+ /* @__PURE__ */ jsx("span", { id: `${stepperVerbId}-increment`, className: "sr-only", children: t("ui.numberInput.increment") })
260
+ ]
243
261
  }
244
262
  ),
245
- /* @__PURE__ */ jsx(
263
+ /* @__PURE__ */ jsxs(
246
264
  Button,
247
265
  {
248
266
  type: "button",
@@ -251,9 +269,12 @@ const NumberInput = React.forwardRef(
251
269
  className: "ui-number-input-step ui-number-input-step-down",
252
270
  tabIndex: -1,
253
271
  disabled: !interactive || atMin,
254
- "aria-label": t("ui.numberInput.decrement"),
272
+ ...stepperName("decrement"),
255
273
  onClick: () => stepBy(-1),
256
- children: /* @__PURE__ */ jsx(ChevronDown, { "aria-hidden": "true" })
274
+ children: [
275
+ /* @__PURE__ */ jsx(ChevronDown, { "aria-hidden": "true" }),
276
+ /* @__PURE__ */ jsx("span", { id: `${stepperVerbId}-decrement`, className: "sr-only", children: t("ui.numberInput.decrement") })
277
+ ]
257
278
  }
258
279
  )
259
280
  ] }) : null
@@ -62,7 +62,7 @@ const RadioItem = React.forwardRef(
62
62
  // records: react-aria's root is a `<label>`, which is `display:inline`, so without it the
63
63
  // 16px dot collapses to nothing. `data-[invalid]` replaces `aria-invalid:` because
64
64
  // react-aria puts `aria-invalid` on the `<input>` and `data-invalid` on this box.
65
- "ui-radio data-[invalid]:border-destructive inline-flex shrink-0 items-center justify-center shadow-xs transition-shadow outline-none",
65
+ "ui-radio data-[invalid]:border-destructive inline-flex shrink-0 items-center justify-center shadow-xs transition-shadow",
66
66
  className
67
67
  ),
68
68
  ...props,
@@ -80,7 +80,7 @@ const Switch = React.forwardRef(
80
80
  // components, so it silently outranked that token. Byte-identical: the token
81
81
  // defaults to 0.5. (`shadow-xs` STAYS — .ui-switch declares --shadow-sm, so this
82
82
  // utility is the switch's real resting elevation, not a duplicate.)
83
- "peer ui-switch shadow-xs transition-all outline-none",
83
+ "peer ui-switch shadow-xs transition-all",
84
84
  className
85
85
  ),
86
86
  children: [
@@ -149,7 +149,7 @@ function DialogContent({
149
149
  showCloseButton ? /* @__PURE__ */ jsxs(
150
150
  DialogClose,
151
151
  {
152
- className: "ui-focus-ring transition-opacity focus:outline-hidden disabled:pointer-events-none",
152
+ className: "ui-focus-ring transition-opacity disabled:pointer-events-none",
153
153
  children: [
154
154
  /* @__PURE__ */ jsx(X, { className: "ui-dialog-close-icon", "aria-hidden": "true" }),
155
155
  /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("feedback.alert.dismiss") })
@@ -265,7 +265,7 @@ function AlertDialogContent({
265
265
  {
266
266
  type: "button",
267
267
  "data-slot": "dialog-close",
268
- className: "ui-focus-ring transition-opacity focus:outline-hidden disabled:pointer-events-none",
268
+ className: "ui-focus-ring transition-opacity disabled:pointer-events-none",
269
269
  "aria-label": t("feedback.alert.dismiss"),
270
270
  onClick: () => {
271
271
  state.setOpen(false);