@godxjp/ui 27.1.0 → 27.4.1

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 (74) hide show
  1. package/dist/components/data-display/badge.d.ts +10 -3
  2. package/dist/components/data-display/badge.js +14 -3
  3. package/dist/components/data-display/data-table.d.ts +20 -12
  4. package/dist/components/data-display/data-table.js +53 -5
  5. package/dist/components/data-display/range-timeline.d.ts +16 -0
  6. package/dist/components/data-display/range-timeline.js +115 -80
  7. package/dist/components/data-display/table.d.ts +16 -0
  8. package/dist/components/data-display/table.js +2 -0
  9. package/dist/components/data-entry/checkbox.js +23 -5
  10. package/dist/components/data-entry/command.d.ts +3 -1
  11. package/dist/components/data-entry/command.js +10 -2
  12. package/dist/components/data-entry/form-field.js +9 -8
  13. package/dist/components/data-entry/index.d.ts +1 -0
  14. package/dist/components/data-entry/search-select.js +145 -136
  15. package/dist/components/data-entry/select.js +38 -3
  16. package/dist/components/general/icon.d.ts +31 -0
  17. package/dist/components/general/icon.js +23 -0
  18. package/dist/components/general/index.d.ts +2 -0
  19. package/dist/components/general/index.js +2 -0
  20. package/dist/components/layout/flex.d.ts +2 -2
  21. package/dist/components/layout/flex.js +7 -1
  22. package/dist/components/layout/org-switcher.js +1 -1
  23. package/dist/components/layout/responsive-grid.d.ts +8 -2
  24. package/dist/components/layout/responsive-grid.js +2 -0
  25. package/dist/components/layout/topbar-item.d.ts +1 -0
  26. package/dist/components/layout/topbar-item.js +3 -1
  27. package/dist/contracts/measurement.json +1 -1
  28. package/dist/form/form-field-control.d.ts +1 -1
  29. package/dist/form/form-field-control.js +32 -23
  30. package/dist/i18n/messages/en.json +2 -1
  31. package/dist/i18n/messages/ja.json +2 -1
  32. package/dist/i18n/messages/vi.json +2 -1
  33. package/dist/props/components/data-display.prop.d.ts +12 -0
  34. package/dist/props/components/data-entry.prop.d.ts +10 -0
  35. package/dist/props/components/form.prop.d.ts +34 -2
  36. package/dist/props/components/general.prop.d.ts +42 -1
  37. package/dist/props/components/index.d.ts +1 -1
  38. package/dist/props/components/layout.prop.d.ts +49 -0
  39. package/dist/props/registry.d.ts +57 -3
  40. package/dist/props/registry.js +62 -2
  41. package/dist/props/vocabulary/data.prop.d.ts +27 -1
  42. package/dist/props/vocabulary/index.d.ts +2 -2
  43. package/dist/props/vocabulary/interaction.prop.d.ts +11 -0
  44. package/dist/styles/control.css +44 -1
  45. package/dist/styles/core.css +2 -0
  46. package/dist/styles/data-display-layout.css +58 -1
  47. package/dist/styles/form-layout.css +21 -0
  48. package/dist/styles/icon-layout.css +37 -0
  49. package/dist/styles/index.css +2 -0
  50. package/dist/styles/layout.css +7 -0
  51. package/dist/styles/navigation-layout.css +5 -1
  52. package/dist/styles/shell-layout.css +11 -14
  53. package/dist/styles/table-layout.css +80 -8
  54. package/dist/styles/text-layout.css +16 -8
  55. package/dist/tokens/components/control.css +5 -0
  56. package/dist/tokens/components/data-display.css +6 -0
  57. package/dist/tokens/components/navigation.css +1 -1
  58. package/dist/tokens/components/table.css +2 -0
  59. package/docs/CONSUMER-RULES.md +2 -2
  60. package/docs/DESIGN-AUTHORITY.md +32 -1
  61. package/docs/FRAME-COVERAGE-REPORT.md +3 -2
  62. package/docs/data-display/data-table/examples/server-paged.tsx +156 -0
  63. package/docs/data-display/data-table/examples/striped-list.tsx +173 -0
  64. package/docs/data-display/popover.tsx +43 -1
  65. package/docs/data-display/timeline.tsx +73 -9
  66. package/docs/data-entry/command.tsx +80 -1
  67. package/docs/data-entry/form-root.tsx +12 -2
  68. package/docs/general/icon.tsx +215 -0
  69. package/docs/layout/flex.tsx +59 -0
  70. package/docs/layout/responsive-grid.tsx +100 -0
  71. package/docs/layout/topbar-item.tsx +29 -1
  72. package/package.json +3 -2
  73. package/scripts/consumer-rule.md +18 -17
  74. package/scripts/ui-audit.mjs +128 -0
@@ -80,14 +80,21 @@ export interface BadgeProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "
80
80
  * `closable={false}`: no × is rendered.
81
81
  *
82
82
  * The × accessible name is built from the visible label (`children`, or the resolved `status`
83
- * label) via `navigation.filterBar.removeFilter` — pass a string `children` (or plain text
84
- * label) so the name quotes the chip, e.g. `getByRole('button', { name: /期限: 今週/ })`.
83
+ * label) via `navigation.filterBar.removeFilter`, e.g. `getByRole('button', { name: /期限: 今週/ })`.
84
+ * When `children` is not a string (a link, a formatted node) the name is read from the label's
85
+ * rendered text instead, so several removable chips never share one bare "Delete" (gh#706).
85
86
  */
86
87
  onRemove?: () => void;
88
+ /**
89
+ * The × button's full accessible name, verbatim — antd 5.15+ `closable={{ 'aria-label' }}`.
90
+ * Wins over the name derived from the label. Use it when the chip's text alone does not say what
91
+ * removing it does.
92
+ */
93
+ removeLabel?: string;
87
94
  /** Disables only the × when `onRemove` is set (FilterBar bar/chip disabled). Label stays visible. */
88
95
  removeDisabled?: boolean;
89
96
  }
90
- export declare function Badge({ as: Element, className, variant, shape, tone, color, icon, status, tabular, style, children, onRemove, removeDisabled, ...props }: BadgeProps): React.JSX.Element;
97
+ export declare function Badge({ as: Element, className, variant, shape, tone, color, icon, status, tabular, style, children, onRemove, removeLabel: removeLabelProp, removeDisabled, ...props }: BadgeProps): React.JSX.Element;
91
98
  /**
92
99
  * Status-aware badge with the shared domain-status-to-tone mapping. `Badge` remains the
93
100
  * general-purpose primitive.
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
3
4
  import { cva } from "class-variance-authority";
4
5
  import {
5
6
  AlertCircle,
@@ -102,6 +103,7 @@ function Badge({
102
103
  style,
103
104
  children,
104
105
  onRemove,
106
+ removeLabel: removeLabelProp,
105
107
  removeDisabled,
106
108
  ...props
107
109
  }) {
@@ -111,7 +113,16 @@ function Badge({
111
113
  const ResolvedIcon = icon === void 0 ? statusDef?.icon : icon;
112
114
  const resolvedChildren = children ?? (status ? status in STATUS_MAP ? t(`status.${status}`) : status : void 0);
113
115
  const tinted = color != null && color !== "";
114
- const removeLabel = typeof children === "string" ? children : typeof resolvedChildren === "string" ? resolvedChildren : void 0;
116
+ const stringLabel = typeof children === "string" ? children : typeof resolvedChildren === "string" ? resolvedChildren : void 0;
117
+ const labelRef = React.useRef(null);
118
+ const [renderedLabel, setRenderedLabel] = React.useState(void 0);
119
+ const deriveFromDom = Boolean(onRemove) && removeLabelProp == null && stringLabel == null;
120
+ React.useEffect(() => {
121
+ if (!deriveFromDom) return;
122
+ const text = labelRef.current?.textContent?.replace(/\s+/g, " ").trim();
123
+ setRenderedLabel(text ? text : void 0);
124
+ }, [deriveFromDom, resolvedChildren]);
125
+ const removeLabel = stringLabel ?? (deriveFromDom ? renderedLabel : void 0);
115
126
  const named = props["aria-label"] != null || props["aria-labelledby"] != null;
116
127
  const nameRole = named && props.role === void 0 && !onRemove ? "img" : void 0;
117
128
  return /* @__PURE__ */ jsxs(
@@ -136,14 +147,14 @@ function Badge({
136
147
  ...props,
137
148
  children: [
138
149
  ResolvedIcon ? /* @__PURE__ */ jsx(ResolvedIcon, { "data-slot": "badge-icon", "aria-hidden": "true" }) : null,
139
- resolvedChildren != null ? /* @__PURE__ */ jsx("span", { "data-slot": "badge-label", children: resolvedChildren }) : null,
150
+ resolvedChildren != null ? /* @__PURE__ */ jsx("span", { ref: labelRef, "data-slot": "badge-label", children: resolvedChildren }) : null,
140
151
  onRemove ? /* @__PURE__ */ jsx(
141
152
  "button",
142
153
  {
143
154
  type: "button",
144
155
  "data-slot": "badge-remove",
145
156
  className: "ui-control-inline-affix-action ui-badge-remove",
146
- "aria-label": removeLabel != null ? t("navigation.filterBar.removeFilter", { label: removeLabel }) : t("common.delete"),
157
+ "aria-label": removeLabelProp ?? (removeLabel != null ? t("navigation.filterBar.removeFilter", { label: removeLabel }) : t("common.delete")),
147
158
  onClick: (event) => {
148
159
  event.stopPropagation();
149
160
  onRemove();
@@ -38,8 +38,11 @@ interface DataTableProps<T> {
38
38
  * Numbered-pagination state, surfaced by DataTable.Pagination. THREE shapes, all accepted:
39
39
  * the TanStack `{ pageIndex, pageSize }` this prop has always taken; antd's
40
40
  * `TablePaginationConfig` object (`{ current, pageSize, total, pageSizeOptions,
41
- * showSizeChanger, onChange }`, 1-based like antd); and `false`, which hides the pager
42
- * entirely (antd `pagination={false}`).
41
+ * showSizeChanger, showTotal, position, onChange }`, 1-based like antd); and `false`, which
42
+ * hides the pager entirely (antd `pagination={false}`). The config object with NO composed
43
+ * `<DataTable.Pagination>` renders the table's own antd footer — the real `Pagination` with
44
+ * total + page numbers at `position` (default `bottomEnd`), `size="sm"` on a compact table.
45
+ * Server paging: `pagination={{ total, current, pageSize, onChange }}` with `data` = that page.
43
46
  */
44
47
  pagination?: PaginationState | TablePaginationProp | false;
45
48
  onPaginationChange?: OnChangeFn<PaginationState>;
@@ -72,7 +75,11 @@ interface DataTableProps<T> {
72
75
  denied?: React.ReactNode;
73
76
  /** Retry handler for the built-in `error` state; omit to hide the retry action. */
74
77
  onRetry?: () => void;
75
- /** Zebra-stripe the body rows (even rows get a subtle fill). */
78
+ /**
79
+ * Zebra-stripe the body rows by LOGICAL record (an expanded detail row takes its record's stripe;
80
+ * frozen columns wear it too). Omit to inherit the theme default `--table-row-striped-alpha`;
81
+ * `true` / `false` override it for this table.
82
+ */
76
83
  striped?: boolean;
77
84
  /** Highlight a row on hover even when it is not clickable (no `onRowClick`). */
78
85
  hoverable?: boolean;
@@ -141,15 +148,15 @@ interface DataTableProps<T> {
141
148
  }
142
149
  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;
143
150
  export declare namespace DataTable {
144
- var Toolbar: ({ children, className, }: {
151
+ export var Toolbar: ({ children, className, }: {
145
152
  children?: React.ReactNode;
146
153
  className?: string;
147
154
  }) => React.JSX.Element;
148
- var Search: ({ placeholder, className, }: {
155
+ export var Search: ({ placeholder, className, }: {
149
156
  placeholder?: string;
150
157
  className?: string;
151
158
  }) => React.JSX.Element;
152
- var ViewOptions: ({ className, label, }: {
159
+ export var ViewOptions: ({ className, label, }: {
153
160
  className?: string;
154
161
  /**
155
162
  * Overrides the button's text. The default ("View") is the shortest thing
@@ -159,12 +166,13 @@ export declare namespace DataTable {
159
166
  */
160
167
  label?: React.ReactNode;
161
168
  }) => React.JSX.Element | null;
162
- var SelectAll: () => React.JSX.Element | null;
163
- var BulkActions: ({ count, children, className, }: BulkActionsProps) => React.JSX.Element | null;
164
- var DensityToggle: () => React.JSX.Element;
165
- var Content: () => React.JSX.Element;
166
- var Pagination: (props: PaginationProps) => React.JSX.Element;
167
- var RowActions: ({ ariaLabel, children }: RowActionsProps) => React.JSX.Element;
169
+ export var SelectAll: () => React.JSX.Element | null;
170
+ export var BulkActions: ({ count, children, className, }: BulkActionsProps) => React.JSX.Element | null;
171
+ export var DensityToggle: () => React.JSX.Element;
172
+ export var Content: () => React.JSX.Element;
173
+ var _a: (props: PaginationProps) => React.JSX.Element;
174
+ export { _a as Pagination };
175
+ export var RowActions: ({ ariaLabel, children }: RowActionsProps) => React.JSX.Element;
168
176
  }
169
177
  interface BulkActionsProps {
170
178
  count?: number;
@@ -56,6 +56,7 @@ import {
56
56
  DropdownMenuSeparator,
57
57
  DropdownMenuTrigger
58
58
  } from "../navigation/dropdown-menu.js";
59
+ import { Pagination } from "../navigation/pagination.js";
59
60
  import { RadioGroupRoot, RadioItem } from "../data-entry/radio.js";
60
61
  import { Tooltip, TooltipContent, TooltipTrigger } from "../feedback/tooltip.js";
61
62
  import {
@@ -224,7 +225,7 @@ function DataTable({
224
225
  error,
225
226
  denied,
226
227
  onRetry,
227
- striped = false,
228
+ striped,
228
229
  hoverable = false,
229
230
  stickyHeader = true,
230
231
  preset = "default",
@@ -389,6 +390,13 @@ function DataTable({
389
390
  (c) => React.isValidElement(c) && c.type.displayName === "DataTable.Pagination" && typeof c.props.onChange !== "function"
390
391
  );
391
392
  const paginationEngaged = !pagerHidden && (manualPagination || controlledPagination !== void 0 || paginationConfig !== void 0 || onPaginationChange !== void 0 || hasNumberedPager);
393
+ const serverPaged = paginationConfig?.total !== void 0 && data.length < paginationConfig.total && data.length <= pageSize;
394
+ const hasComposedPager = React.Children.toArray(children).some(
395
+ (c) => React.isValidElement(c) && c.type.displayName === "DataTable.Pagination"
396
+ );
397
+ const footerPositions = paginationConfig && !hasComposedPager && !pagerHidden ? (paginationConfig.position ?? ["bottomEnd"]).filter((p) => p !== "none") : [];
398
+ const topFooter = footerPositions.find((p) => p.startsWith("top"));
399
+ const bottomFooter = footerPositions.find((p) => p.startsWith("bottom"));
392
400
  const features = React.useMemo(() => dataTableFeatures(), []);
393
401
  const selectionEnabled = selectable || rowSelection !== void 0;
394
402
  const table = useTable({
@@ -400,7 +408,7 @@ function DataTable({
400
408
  manualFiltering,
401
409
  // "no pager on this table" is expressed the same way as "the server paginates" — leave the
402
410
  // rows unsliced. Page count still derives from `rowCount`/the pre-paginated model either way.
403
- manualPagination: manualPagination || !paginationEngaged,
411
+ manualPagination: manualPagination || serverPaged || !paginationEngaged,
404
412
  rowCount: paginationConfig?.total ?? rowCount,
405
413
  enableRowSelection: selectionEnabled,
406
414
  state: {
@@ -467,8 +475,10 @@ function DataTable({
467
475
  (c) => React.isValidElement(c) && c.type.displayName === "DataTable.Content"
468
476
  );
469
477
  return /* @__PURE__ */ jsx(DataTableContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxs("div", { className: cn("ui-data-table-root", densityClass[density], className), children: [
478
+ topFooter ? /* @__PURE__ */ jsx(PaginationFooter, { position: topFooter }) : null,
470
479
  children,
471
- !hasContent && /* @__PURE__ */ jsx(DataTable.Content, {})
480
+ !hasContent && /* @__PURE__ */ jsx(DataTable.Content, {}),
481
+ bottomFooter ? /* @__PURE__ */ jsx(PaginationFooter, { position: bottomFooter }) : null
472
482
  ] }) });
473
483
  }
474
484
  DataTable.Toolbar = function DataTableToolbar({
@@ -929,6 +939,7 @@ DataTable.Content = function DataTableContent() {
929
939
  preset,
930
940
  collapseBelow,
931
941
  bordered,
942
+ striped,
932
943
  children: [
933
944
  /* @__PURE__ */ jsx(
934
945
  TableHeader,
@@ -1145,8 +1156,10 @@ DataTable.Content = function DataTableContent() {
1145
1156
  } : rowProps.onKeyDown,
1146
1157
  className: cn(
1147
1158
  rowPadding,
1148
- // Hover highlight when rows are clickable OR explicitly hoverable…
1149
- (onRowClick || hoverable) && "hover:bg-muted/50",
1159
+ // Hover highlight when rows are clickable OR explicitly hoverable… The
1160
+ // --accent step TableRow itself uses, not --muted/50: that sat BELOW the
1161
+ // zebra stripe (--muted/0.8, gh#700), so hovering a striped row lightened it.
1162
+ (onRowClick || hoverable) && "hover:bg-accent/70",
1150
1163
  // …but the affordance (cursor + focus mark) only when clickable.
1151
1164
  //
1152
1165
  // `ui-focus-ring` = the single focus source (styles/focus-ring.css). It
@@ -1403,6 +1416,41 @@ function NumberedPagination({ pageSizeOptions, className }) {
1403
1416
  }
1404
1417
  );
1405
1418
  }
1419
+ const footerAlign = {
1420
+ Start: "start",
1421
+ Center: "center",
1422
+ End: "end"
1423
+ };
1424
+ function PaginationFooter({
1425
+ position
1426
+ }) {
1427
+ const { table, paginationConfig, density } = useDataTableContext();
1428
+ if (!paginationConfig) return null;
1429
+ const { pageIndex, pageSize } = table.state.pagination;
1430
+ const edge = position.replace(/^(top|bottom)/, "");
1431
+ return /* @__PURE__ */ jsx(
1432
+ "div",
1433
+ {
1434
+ className: "ui-data-table-pagination ui-data-table-pagination--footer",
1435
+ "data-position": position,
1436
+ children: /* @__PURE__ */ jsx(
1437
+ Pagination,
1438
+ {
1439
+ value: pageIndex + 1,
1440
+ total: table.getRowCount(),
1441
+ pageSize,
1442
+ pageSizeOptions: paginationConfig.pageSizeOptions,
1443
+ showSizeChanger: paginationConfig.showSizeChanger ?? true,
1444
+ showTotal: paginationConfig.showTotal,
1445
+ hideOnSinglePage: paginationConfig.hideOnSinglePage ?? false,
1446
+ size: density === "compact" ? "sm" : "md",
1447
+ align: footerAlign[edge],
1448
+ onValueChange: (page, size) => table.setPagination({ pageIndex: page - 1, pageSize: size })
1449
+ }
1450
+ )
1451
+ }
1452
+ );
1453
+ }
1406
1454
  DataTable.RowActions = function DataTableRowActions({ ariaLabel, children }) {
1407
1455
  const { t } = useTranslation();
1408
1456
  return /* @__PURE__ */ jsxs(
@@ -9,9 +9,11 @@ export type RangeTimelineRow = {
9
9
  };
10
10
  export type RangeTimelineProps = React.HTMLAttributes<HTMLElement> & {
11
11
  label: string;
12
+ /** `muted` tints that column down the whole body — a non-working day, a closed period. */
12
13
  columns: {
13
14
  label: string;
14
15
  units: number;
16
+ muted?: boolean;
15
17
  }[];
16
18
  bands?: {
17
19
  label: string;
@@ -20,13 +22,21 @@ export type RangeTimelineProps = React.HTMLAttributes<HTMLElement> & {
20
22
  rows: RangeTimelineRow[];
21
23
  today?: number | null;
22
24
  onRangeChange?: (id: string, edge: "start" | "end", delta: number) => void;
25
+ /**
26
+ * Rule the body as a grid: a line between rows, a line under the header, and a vertical line for
27
+ * every column down the whole body. Default `true`, like `Calendar bordered`; `false` restores
28
+ * the header-only ruling. Colour `--range-timeline-grid-color`, weight `--range-timeline-grid-width`.
29
+ */
30
+ bordered?: boolean;
23
31
  };
24
32
  /** A horizontal range axis. Units and labels are data; the design system owns all geometry. */
25
33
  export declare const RangeTimeline: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLElement> & {
26
34
  label: string;
35
+ /** `muted` tints that column down the whole body — a non-working day, a closed period. */
27
36
  columns: {
28
37
  label: string;
29
38
  units: number;
39
+ muted?: boolean;
30
40
  }[];
31
41
  bands?: {
32
42
  label: string;
@@ -35,4 +45,10 @@ export declare const RangeTimeline: React.ForwardRefExoticComponent<React.HTMLAt
35
45
  rows: RangeTimelineRow[];
36
46
  today?: number | null;
37
47
  onRangeChange?: (id: string, edge: "start" | "end", delta: number) => void;
48
+ /**
49
+ * Rule the body as a grid: a line between rows, a line under the header, and a vertical line for
50
+ * every column down the whole body. Default `true`, like `Calendar bordered`; `false` restores
51
+ * the header-only ruling. Colour `--range-timeline-grid-color`, weight `--range-timeline-grid-width`.
52
+ */
53
+ bordered?: boolean;
38
54
  } & React.RefAttributes<HTMLElement>>;
@@ -2,12 +2,12 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { useTranslation } from "../../i18n/use-translation.js";
5
- import { GripVertical } from "lucide-react";
5
+ import { ChevronLeft, ChevronRight, GripVertical } from "lucide-react";
6
6
  import { Button } from "../general/button.js";
7
7
  import { Text } from "../general/typography.js";
8
8
  import { cn } from "../../lib/utils.js";
9
9
  const RangeTimeline = React.forwardRef(
10
- function RangeTimeline2({ label, columns, bands, rows, today, onRangeChange, className, ...props }, ref) {
10
+ function RangeTimeline2({ label, columns, bands, rows, today, onRangeChange, bordered = true, className, ...props }, ref) {
11
11
  const { t } = useTranslation();
12
12
  const units = Math.max(
13
13
  1,
@@ -37,6 +37,7 @@ const RangeTimeline = React.forwardRef(
37
37
  ...props,
38
38
  ref,
39
39
  className: cn("ui-range-timeline", className),
40
+ "data-bordered": bordered ? "true" : void 0,
40
41
  "aria-label": label,
41
42
  tabIndex: 0,
42
43
  children: /* @__PURE__ */ jsxs(
@@ -69,86 +70,120 @@ const RangeTimeline = React.forwardRef(
69
70
  index
70
71
  )) })
71
72
  ] }),
72
- rows.map((row) => {
73
- const delta = preview?.id === row.id ? preview.delta : 0;
74
- const start = Math.min(row.end, row.start + (preview?.edge === "start" ? delta : 0));
75
- const end = Math.max(row.start, row.end + (preview?.edge === "end" ? delta : 0));
76
- const left = Math.max(0, start), right = Math.min(units, end + 1);
77
- return /* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-row", children: [
78
- /* @__PURE__ */ jsx("div", { className: "ui-range-timeline-label", children: row.label }),
79
- /* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-track", children: [
80
- today != null && today >= 0 && today < units && /* @__PURE__ */ jsx(
81
- "span",
82
- {
83
- "aria-hidden": "true",
84
- className: "ui-range-timeline-today",
85
- style: { insetInlineStart: `${(today + 0.5) / units * 100}%` }
86
- }
87
- ),
88
- right <= left && /* @__PURE__ */ jsx(Text, { tone: "muted", children: t("rangeTimeline.outsideRange") }),
89
- right > left && /* @__PURE__ */ jsx(
90
- "div",
91
- {
92
- className: "ui-range-timeline-bar",
93
- style: {
94
- insetInlineStart: `${left / units * 100}%`,
95
- inlineSize: `${(right - left) / units * 100}%`
96
- },
97
- children: onRangeChange && (right - left) * Math.max(1, columns.length) >= units && ["start", "end"].filter(
98
- (edge) => edge === "start" ? start >= 0 && start < units : end >= 0 && end < units
99
- ).map((edge) => /* @__PURE__ */ jsx(
100
- Button,
73
+ /* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-body", children: [
74
+ (bordered || columns.some((column) => column.muted)) && // Decorative: the same flex/units layout as the header columns, laid once behind every
75
+ // row so each vertical rule runs the full body height exactly under its header column.
76
+ /* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-grid", "aria-hidden": "true", children: [
77
+ /* @__PURE__ */ jsx("div", {}),
78
+ /* @__PURE__ */ jsx("div", { className: "ui-range-timeline-columns", children: columns.map((column, index) => /* @__PURE__ */ jsx(
79
+ "div",
80
+ {
81
+ className: "ui-range-timeline-grid-column",
82
+ "data-muted": column.muted ? "true" : void 0,
83
+ style: { flex: column.units }
84
+ },
85
+ index
86
+ )) })
87
+ ] }),
88
+ rows.map((row) => {
89
+ const delta = preview?.id === row.id ? preview.delta : 0;
90
+ const start = Math.min(row.end, row.start + (preview?.edge === "start" ? delta : 0));
91
+ const end = Math.max(row.start, row.end + (preview?.edge === "end" ? delta : 0));
92
+ const left = Math.max(0, start), right = Math.min(units, end + 1);
93
+ return /* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-row", children: [
94
+ /* @__PURE__ */ jsx("div", { className: "ui-range-timeline-label", children: row.label }),
95
+ /* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-track", children: [
96
+ today != null && today >= 0 && today < units && /* @__PURE__ */ jsx(
97
+ "span",
98
+ {
99
+ "aria-hidden": "true",
100
+ className: "ui-range-timeline-today",
101
+ style: { insetInlineStart: `${(today + 0.5) / units * 100}%` }
102
+ }
103
+ ),
104
+ right <= left && (end < 0 ? /* @__PURE__ */ jsxs("span", { className: "ui-range-timeline-outside", "data-direction": "before", children: [
105
+ /* @__PURE__ */ jsx(
106
+ ChevronLeft,
101
107
  {
102
- variant: "ghost",
103
- size: "icon-xs",
104
- className: "ui-range-timeline-handle",
105
- "data-edge": edge,
106
- "aria-label": edge === "start" ? row.startLabel : row.endLabel,
107
- onPointerDown: (event) => {
108
- const width = track.current?.getBoundingClientRect().width ?? 0;
109
- if (!width || event.button !== 0) return;
110
- event.currentTarget.setPointerCapture(event.pointerId);
111
- drag.current = {
112
- id: row.id,
113
- edge,
114
- x: event.clientX,
115
- width,
116
- direction: getComputedStyle(event.currentTarget).direction === "rtl" ? -1 : 1
117
- };
118
- },
119
- onPointerMove: (event) => {
120
- if (drag.current)
121
- setPreview({
122
- id: drag.current.id,
123
- edge: drag.current.edge,
124
- delta: deltaAt(event.clientX)
125
- });
126
- },
127
- onPointerUp: complete,
128
- onPointerCancel: cancel,
129
- onLostPointerCapture: cancel,
130
- onKeyDown: (event) => {
131
- if (event.key === "Escape") {
132
- cancel();
133
- return;
134
- }
135
- const step = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0;
136
- if (!step) return;
137
- event.preventDefault();
138
- const direction = getComputedStyle(event.currentTarget).direction === "rtl" ? -1 : 1;
139
- const delta2 = step * direction;
140
- if (edge === "start" ? row.start + delta2 <= row.end : row.end + delta2 >= row.start)
141
- onRangeChange(row.id, edge, delta2);
142
- },
143
- children: /* @__PURE__ */ jsx(GripVertical, { "aria-hidden": "true" })
108
+ "aria-hidden": "true",
109
+ className: "ui-range-timeline-outside-icon"
110
+ }
111
+ ),
112
+ /* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", children: t("rangeTimeline.outsideBefore") })
113
+ ] }) : /* @__PURE__ */ jsxs("span", { className: "ui-range-timeline-outside", "data-direction": "after", children: [
114
+ /* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", children: t("rangeTimeline.outsideAfter") }),
115
+ /* @__PURE__ */ jsx(
116
+ ChevronRight,
117
+ {
118
+ "aria-hidden": "true",
119
+ className: "ui-range-timeline-outside-icon"
120
+ }
121
+ )
122
+ ] })),
123
+ right > left && /* @__PURE__ */ jsx(
124
+ "div",
125
+ {
126
+ className: "ui-range-timeline-bar",
127
+ style: {
128
+ insetInlineStart: `${left / units * 100}%`,
129
+ inlineSize: `${(right - left) / units * 100}%`
144
130
  },
145
- edge
146
- ))
147
- }
148
- )
149
- ] })
150
- ] }, row.id);
151
- })
131
+ children: onRangeChange && (right - left) * Math.max(1, columns.length) >= units && ["start", "end"].filter(
132
+ (edge) => edge === "start" ? start >= 0 && start < units : end >= 0 && end < units
133
+ ).map((edge) => /* @__PURE__ */ jsx(
134
+ Button,
135
+ {
136
+ variant: "ghost",
137
+ size: "icon-xs",
138
+ className: "ui-range-timeline-handle",
139
+ "data-edge": edge,
140
+ "aria-label": edge === "start" ? row.startLabel : row.endLabel,
141
+ onPointerDown: (event) => {
142
+ const width = track.current?.getBoundingClientRect().width ?? 0;
143
+ if (!width || event.button !== 0) return;
144
+ event.currentTarget.setPointerCapture(event.pointerId);
145
+ drag.current = {
146
+ id: row.id,
147
+ edge,
148
+ x: event.clientX,
149
+ width,
150
+ direction: getComputedStyle(event.currentTarget).direction === "rtl" ? -1 : 1
151
+ };
152
+ },
153
+ onPointerMove: (event) => {
154
+ if (drag.current)
155
+ setPreview({
156
+ id: drag.current.id,
157
+ edge: drag.current.edge,
158
+ delta: deltaAt(event.clientX)
159
+ });
160
+ },
161
+ onPointerUp: complete,
162
+ onPointerCancel: cancel,
163
+ onLostPointerCapture: cancel,
164
+ onKeyDown: (event) => {
165
+ if (event.key === "Escape") {
166
+ cancel();
167
+ return;
168
+ }
169
+ const step = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0;
170
+ if (!step) return;
171
+ event.preventDefault();
172
+ const direction = getComputedStyle(event.currentTarget).direction === "rtl" ? -1 : 1;
173
+ const delta2 = step * direction;
174
+ if (edge === "start" ? row.start + delta2 <= row.end : row.end + delta2 >= row.start)
175
+ onRangeChange(row.id, edge, delta2);
176
+ },
177
+ children: /* @__PURE__ */ jsx(GripVertical, { "aria-hidden": "true" })
178
+ },
179
+ edge
180
+ ))
181
+ }
182
+ )
183
+ ] })
184
+ ] }, row.id);
185
+ })
186
+ ] })
152
187
  ]
153
188
  }
154
189
  )
@@ -13,6 +13,14 @@ export type TableProps = React.HTMLAttributes<HTMLTableElement> & {
13
13
  * (default `--border`).
14
14
  */
15
15
  bordered?: boolean;
16
+ /**
17
+ * Zebra rows — every even LOGICAL body row wears `--table-row-striped-background`, so a wide list
18
+ * row is easy to follow across its columns. An expanded detail row (a `<tr data-expanded-row>`
19
+ * under its record) is skipped when counting and takes its record's stripe. Leave it out to
20
+ * inherit the theme default (`--table-row-striped-alpha`, `0%` unless a service turns striping on
21
+ * for every table); `true` / `false` override that default for this table only.
22
+ */
23
+ striped?: boolean;
16
24
  /**
17
25
  * Named collection contract. `"default"` (the default) emits no attribute and keeps the plain
18
26
  * table exactly as it is.
@@ -64,6 +72,14 @@ export declare const Table: React.ForwardRefExoticComponent<React.HTMLAttributes
64
72
  * (default `--border`).
65
73
  */
66
74
  bordered?: boolean;
75
+ /**
76
+ * Zebra rows — every even LOGICAL body row wears `--table-row-striped-background`, so a wide list
77
+ * row is easy to follow across its columns. An expanded detail row (a `<tr data-expanded-row>`
78
+ * under its record) is skipped when counting and takes its record's stripe. Leave it out to
79
+ * inherit the theme default (`--table-row-striped-alpha`, `0%` unless a service turns striping on
80
+ * for every table); `true` / `false` override that default for this table only.
81
+ */
82
+ striped?: boolean;
67
83
  /**
68
84
  * Named collection contract. `"default"` (the default) emits no attribute and keeps the plain
69
85
  * table exactly as it is.
@@ -7,6 +7,7 @@ const Table = React.forwardRef(
7
7
  className,
8
8
  scrollable = true,
9
9
  bordered = false,
10
+ striped,
10
11
  preset = "default",
11
12
  collapseBelow = "sm",
12
13
  columnWidths,
@@ -41,6 +42,7 @@ const Table = React.forwardRef(
41
42
  {
42
43
  ref,
43
44
  "data-slot": "table",
45
+ "data-striped": striped === void 0 ? void 0 : striped ? "" : "false",
44
46
  className: cn("w-full caption-bottom", bordered && "ui-table-bordered", className),
45
47
  ...props
46
48
  }