gridsmith-ui 0.17.3 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # gridsmith-ui
2
2
 
3
- 98 React components with a CSS custom property theming engine, 14 chart components, and 5 icon packs.
3
+ 103 React components with a CSS custom property theming engine, 13 chart components, and 5 icon packs.
4
4
 
5
5
  ## Installation
6
6
 
7
- gridsmith-ui needs React 19. On a React 18 project, upgrade `react`, `react-dom`, `@types/react` and `@types/react-dom` to 19 first; installing with `--force` or `--legacy-peer-deps` gives an app that cannot run.
7
+ gridsmith-ui needs React 19. On a React 18 project, upgrade `react`, `react-dom`, `@types/react` and `@types/react-dom` to 19 first; installing with `--force` or `--legacy-peer-deps` gives an app that cannot run. On React 18, `AppShell` and `PageContainer` stop with an error that says so.
8
8
 
9
9
  ```bash
10
10
  npm install gridsmith-ui
@@ -18,7 +18,7 @@ npm install recharts
18
18
 
19
19
  ### Heavy components (optional)
20
20
 
21
- Five components depend on large third-party libraries. They live in their own entry points so the main bundle stays small — install the library only if you use the component:
21
+ Seven components depend on large third-party libraries. They live in six entry points of their own so the main bundle stays small — install the library only if you use the component:
22
22
 
23
23
  ```bash
24
24
  npm install @tiptap/react @tiptap/pm @tiptap/starter-kit lowlight \
@@ -28,18 +28,19 @@ npm install @tiptap/react @tiptap/pm @tiptap/starter-kit lowlight \
28
28
  @tiptap/extension-table @tiptap/extension-table-cell @tiptap/extension-table-header \
29
29
  @tiptap/extension-table-row @tiptap/extension-task-item @tiptap/extension-task-list \
30
30
  @tiptap/extension-text-align @tiptap/extension-text-style @tiptap/extension-underline # gridsmith-ui/editor
31
+ npm install react-markdown remark-gfm # gridsmith-ui/chat
31
32
  npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities # gridsmith-ui/kanban
32
33
  npm install @tanstack/react-table # gridsmith-ui/data-grid
33
34
  npm install @tanstack/react-virtual # gridsmith-ui/virtualized
34
35
  npm install react-resizable-panels # gridsmith-ui/resizable
35
36
  ```
36
37
 
37
- ### Icon packs (optional)
38
+ ### Icon packs
38
39
 
39
- The `Icon` component uses `lucide-react` by default. Other packs are loaded on demand — install only what you need:
40
+ The `Icon` component uses `lucide-react` by default, and every other pack falls back to it for a missing name, so always install it. Other packs are loaded on demand — install only what you need:
40
41
 
41
42
  ```bash
42
- # Default (recommended)
43
+ # Default and fallback (required)
43
44
  npm install lucide-react
44
45
 
45
46
  # Alternative packs
@@ -58,11 +59,13 @@ Import the design tokens and Tailwind bindings in your CSS entry point:
58
59
  @import "gridsmith-ui/roots.css";
59
60
  @import "gridsmith-ui/tokens.css";
60
61
  @import "gridsmith-ui/tailwind.css";
62
+ @source "../node_modules/gridsmith-ui/dist/*.js";
61
63
  ```
62
64
 
63
65
  - **roots.css** — the CSS custom property engine (L1 roots, L2 derived scales, L3 semantics)
64
- - **tokens.css** — default Precision Minimal preset values (optional: use `generate-tokens.ts` to customise)
66
+ - **tokens.css** — default Precision Minimal preset values (a Gridsmith project imports the `tokens_css` from `get_tokens` called with `css: true`, saved as `gridsmith-tokens.css`, instead)
65
67
  - **tailwind.css** — maps `--ds-*` tokens to Tailwind utilities (`p-ds-4`, `text-2xs`, `opacity-disabled`)
68
+ - **@source** — tells Tailwind to scan the compiled components; without it they render unstyled. It must come after every `@import`.
66
69
 
67
70
  For the fastest icon rendering on cold loads, call `preloadPack` in your entry point before `createRoot`:
68
71
 
@@ -75,13 +78,14 @@ preloadPack("lucide");
75
78
 
76
79
  | Import path | What it provides |
77
80
  |---|---|
78
- | `gridsmith-ui` | 97 components (actions, forms, data display, navigation, overlays, feedback, layout) |
79
- | `gridsmith-ui/charts` | 14 chart components (Bar, Line, Area, Pie, Composite, Scatter, Radar, RadialBar, Funnel, Treemap, Sankey, Sparkline, ChartContainer) + `useChartColors` hook |
81
+ | `gridsmith-ui` | 96 components (actions, forms, data display, navigation, overlays, feedback, layout) |
82
+ | `gridsmith-ui/charts` | 13 chart components (Bar, Line, Area, Pie, Composite, Scatter, Radar, RadialBar, Funnel, Treemap, Sankey, Sparkline, ChartContainer) + `useChartColors` hook + `categoryKind()` |
80
83
  | `gridsmith-ui/editor` | `RichTextEditor` (requires the tiptap packages + `lowlight`) |
84
+ | `gridsmith-ui/chat` | `MarkdownContent` for chat replies; pass it to `ChatMessageList`'s `renderContent` (requires `react-markdown` + `remark-gfm`) |
81
85
  | `gridsmith-ui/kanban` | `KanbanBoard` (requires `@dnd-kit/*`) |
82
86
  | `gridsmith-ui/data-grid` | `DataGrid` (requires `@tanstack/react-table`) |
83
87
  | `gridsmith-ui/virtualized` | `VirtualizedList` (requires `@tanstack/react-virtual`) |
84
- | `gridsmith-ui/resizable` | `ResizablePanelGroup`, `ResizablePanel`, `ResizableHandle` (requires `react-resizable-panels`) |
88
+ | `gridsmith-ui/resizable` | `ResizablePanelGroup`, `ResizablePanel`, `ResizableHandle`, `SplitView` (requires `react-resizable-panels`) |
85
89
  | `gridsmith-ui/icons` | `Icon`, `IconProvider`, `preloadPack` — 5 icon packs with two-phase loading |
86
90
  | `gridsmith-ui/utils` | `cn()` — className merge utility (clsx + tailwind-merge) |
87
91
  | `gridsmith-ui/roots.css` | CSS custom property engine |
@@ -94,10 +98,11 @@ preloadPack("lucide");
94
98
  import { Button, Card, Input, Table } from "gridsmith-ui";
95
99
  import { BarChart, LineChart } from "gridsmith-ui/charts";
96
100
  import { RichTextEditor } from "gridsmith-ui/editor";
101
+ import { MarkdownContent } from "gridsmith-ui/chat";
97
102
  import { KanbanBoard } from "gridsmith-ui/kanban";
98
103
  import { DataGrid } from "gridsmith-ui/data-grid";
99
104
  import { VirtualizedList } from "gridsmith-ui/virtualized";
100
- import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "gridsmith-ui/resizable";
105
+ import { ResizablePanelGroup, ResizablePanel, ResizableHandle, SplitView } from "gridsmith-ui/resizable";
101
106
  import { Icon, IconProvider } from "gridsmith-ui/icons";
102
107
  import { cn } from "gridsmith-ui/utils";
103
108
  ```
@@ -134,10 +139,11 @@ Wrap your app in `<IconProvider>` to select a pack:
134
139
 
135
140
  | Package | Required |
136
141
  |---------|----------|
137
- | `react` / `react-dom` | Yes (^18 or ^19) |
138
- | `lucide-react` | Optional (default icon pack) |
142
+ | `react` / `react-dom` | Yes (^19) |
143
+ | `lucide-react` | Yes (the default icon pack and every pack's fallback) |
139
144
  | `recharts` | Optional (required for `gridsmith-ui/charts`) |
140
145
  | `@tiptap/*`, `lowlight` | Optional (required for `gridsmith-ui/editor`) |
146
+ | `react-markdown`, `remark-gfm` | Optional (required for `gridsmith-ui/chat`) |
141
147
  | `@dnd-kit/*` | Optional (required for `gridsmith-ui/kanban`) |
142
148
  | `@tanstack/react-table` | Optional (required for `gridsmith-ui/data-grid`) |
143
149
  | `@tanstack/react-virtual` | Optional (required for `gridsmith-ui/virtualized`) |
package/dist/index.d.ts CHANGED
@@ -682,16 +682,23 @@ interface TableProps {
682
682
  * The table's title inside its frame (Sprint 25.9, A1): a heading in CardTitle's style at CardHeader's inset, in a band
683
683
  * above the scroll area that never scrolls sideways, and the `<table>`'s name. Title a table this way or with a
684
684
  * SectionHeading above it — the same way as the blocks beside it (page_composition#45). In card mode, and while it
685
- * loads or is empty there, the same heading sits above the cards. Without a title the table renders as before.
685
+ * loads or is empty there, the same heading sits above the cards. Without a title the frame holds the scroll area
686
+ * alone.
686
687
  */
687
688
  title?: string;
688
689
  /** A line under the title, which also describes the `<table>` (aria-describedby). Needs `title`. */
689
690
  description?: string;
690
691
  /** The table's buttons, at the end of the title band as in a CardHeader. Needs `title`. */
691
692
  actions?: ReactNode;
693
+ /**
694
+ * The table's name for assistive tech when it has no title (a title names a titled one): the `<table>`'s label, and
695
+ * its scroll region's while it scrolls, which is otherwise "Table". Two untitled tables on one page need a name each,
696
+ * or their regions share one (Sprint 25.10 review).
697
+ */
698
+ "aria-label"?: string;
692
699
  ref?: Ref<HTMLDivElement>;
693
700
  }
694
- declare function Table({ columns, data, className, sortable, sortKey: controlledSortKey, sortDir: controlledSortDir, onSortChange, selectable, selectedRows, onSelectionChange, expandable, renderExpanded, density, stickyHeader, striped, loading, emptyState, onRowClick, mobileBreakpoint: mobileBreakpointProp, title, description, actions, ref, }: TableProps): react_jsx_runtime.JSX.Element;
701
+ declare function Table({ columns, data, className, sortable, sortKey: controlledSortKey, sortDir: controlledSortDir, onSortChange, selectable, selectedRows, onSelectionChange, expandable, renderExpanded, density, stickyHeader, striped, loading, emptyState, onRowClick, mobileBreakpoint: mobileBreakpointProp, title, description, actions, "aria-label": ariaLabel, ref, }: TableProps): react_jsx_runtime.JSX.Element;
695
702
 
696
703
  interface CalendarEvent {
697
704
  id: string;
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import { mergeRefs, Slot } from './chunk-7Q46JXSK.js';
19
19
  import { useLatest } from './chunk-WH2FUFAC.js';
20
20
  import { Icon } from './chunk-4LDYR7QP.js';
21
21
  import { cn, GLASS_SURFACE, ACTIVE_RING, GLASS_BACKDROP, GLASS_DROPDOWN, lockBodyScroll, unlockBodyScroll, GLASS_OVERLAY, ERROR_ACTIVE_RING, ERROR_FOCUS_RING, FOCUS_RING, ERROR_HAS_FOCUS_RING, HAS_FOCUS_RING, ERROR_FOCUS_WITHIN_RING, FOCUS_WITHIN_RING } from './chunk-4KVFH5KC.js';
22
- import { createContext, forwardRef, useState, useRef, useCallback, useLayoutEffect, useMemo, useContext, useId, memo, useEffect, useImperativeHandle, Children, isValidElement, cloneElement, Fragment as Fragment$1, useSyncExternalStore } from 'react';
22
+ import { createContext, forwardRef, useState, useRef, useCallback, useLayoutEffect, useMemo, useContext, Children, isValidElement, useId, memo, useEffect, useImperativeHandle, Fragment as Fragment$1, cloneElement, useSyncExternalStore } from 'react';
23
23
  import { createPortal, flushSync } from 'react-dom';
24
24
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
25
25
 
@@ -4907,6 +4907,17 @@ function MentionInput({
4907
4907
  )
4908
4908
  ] });
4909
4909
  }
4910
+ var HEADER_LAYOUT = {
4911
+ row: "flex flex-wrap items-start justify-between gap-ds-4",
4912
+ block: "flex flex-col space-y-ds-1.5 grow basis-1/2 [min-width:min-content] break-words",
4913
+ titleLeads: "grow basis-1/2 max-w-max [min-width:min-content] break-words",
4914
+ actions: "flex items-center gap-ds-2 shrink-0 ml-auto",
4915
+ below: "mt-ds-1.5"
4916
+ };
4917
+ function actionsAreBadges(actions) {
4918
+ const items = Children.toArray(actions);
4919
+ return items.length > 0 && items.every((item) => isValidElement(item) && (item.type === Fragment$1 ? actionsAreBadges(item.props.children) : item.type === Badge));
4920
+ }
4910
4921
  var CardCtx = createContext(false);
4911
4922
  var CardActionCtx = createContext(null);
4912
4923
  var CARD_ACTION_ATTR = "data-card-action";
@@ -5149,7 +5160,7 @@ var CardHeader = forwardRef(function CardHeader2({ className, children, actions,
5149
5160
  const isCheckable = useContext(CardCtx);
5150
5161
  const hasActions = actions || menuItems;
5151
5162
  const pl = isCheckable ? "pl-ds-10" : "pl-ds-5";
5152
- const actionsBlock = hasActions ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-2 shrink-0 ml-auto", children: [
5163
+ const actionsBlock = hasActions ? /* @__PURE__ */ jsxs("div", { className: HEADER_LAYOUT.actions, children: [
5153
5164
  actions,
5154
5165
  menuItems && /* @__PURE__ */ jsx(
5155
5166
  DropdownMenu,
@@ -5174,8 +5185,20 @@ var CardHeader = forwardRef(function CardHeader2({ className, children, actions,
5174
5185
  ] }) : null;
5175
5186
  const iconBlock = icon ? /* @__PURE__ */ jsx("span", { className: "shrink-0 h-10 w-10 rounded-[var(--ds-radius-md)] bg-[var(--ds-bg-muted)] flex items-center justify-center mb-ds-1", children: /* @__PURE__ */ jsx(Icon, { name: icon, size: 20, className: "text-[var(--ds-text-secondary)]" }) }) : null;
5176
5187
  if (hasActions) {
5177
- return /* @__PURE__ */ jsx("div", { ref, className: cn(pl, "pr-ds-5 pt-ds-4 last:pb-ds-4", className), ...props, children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-start justify-between gap-ds-4", children: [
5178
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col space-y-ds-1.5 grow basis-1/2 [min-width:min-content] break-words", children: [
5188
+ const parts = Children.toArray(children);
5189
+ const titleLeads = !iconBlock && !menuItems && actionsAreBadges(actions) && parts.length > 0 && isValidElement(parts[0]) && parts[0].type === CardTitle;
5190
+ if (titleLeads) {
5191
+ const below = parts.slice(1);
5192
+ return /* @__PURE__ */ jsxs("div", { ref, className: cn(pl, "pr-ds-5 pt-ds-4 last:pb-ds-4", className), ...props, children: [
5193
+ /* @__PURE__ */ jsxs("div", { className: HEADER_LAYOUT.row, children: [
5194
+ /* @__PURE__ */ jsx("div", { "data-card-title-row": "", className: HEADER_LAYOUT.titleLeads, children: parts[0] }),
5195
+ actionsBlock
5196
+ ] }),
5197
+ below.length > 0 && /* @__PURE__ */ jsx("div", { className: cn("flex flex-col space-y-ds-1.5 break-words", HEADER_LAYOUT.below), children: below })
5198
+ ] });
5199
+ }
5200
+ return /* @__PURE__ */ jsx("div", { ref, className: cn(pl, "pr-ds-5 pt-ds-4 last:pb-ds-4", className), ...props, children: /* @__PURE__ */ jsxs("div", { className: HEADER_LAYOUT.row, children: [
5201
+ /* @__PURE__ */ jsxs("div", { className: HEADER_LAYOUT.block, children: [
5179
5202
  iconBlock,
5180
5203
  children
5181
5204
  ] }),
@@ -5599,6 +5622,69 @@ function Accordion({ items, type = "single", defaultOpen = [], open: controlledO
5599
5622
  ] }, i);
5600
5623
  }) });
5601
5624
  }
5625
+ var EDGE_FADE = 24;
5626
+ var NONE = { left: false, right: false, vertical: false, gutterRight: 0, gutterBottom: 0 };
5627
+ var same = (a, b) => a.left === b.left && a.right === b.right && a.vertical === b.vertical && a.gutterRight === b.gutterRight && a.gutterBottom === b.gutterBottom;
5628
+ function useScrollEdges(options = {}) {
5629
+ const { enabled = true, fade = EDGE_FADE } = options;
5630
+ const elRef = useRef(null);
5631
+ const observer = useRef(null);
5632
+ const borders = useRef({ x: 0, y: 0 });
5633
+ const [state, setState] = useState(NONE);
5634
+ const measure = useCallback(() => {
5635
+ const el = elRef.current;
5636
+ if (!el) return;
5637
+ const next = {
5638
+ left: el.scrollLeft > 1,
5639
+ right: el.scrollLeft + el.clientWidth < el.scrollWidth - 1,
5640
+ vertical: el.scrollHeight > el.clientHeight + 1,
5641
+ gutterRight: Math.max(0, el.offsetWidth - el.clientWidth - borders.current.x),
5642
+ gutterBottom: Math.max(0, el.offsetHeight - el.clientHeight - borders.current.y)
5643
+ };
5644
+ setState((prev) => same(prev, next) ? prev : next);
5645
+ }, []);
5646
+ const ref = useCallback((el) => {
5647
+ observer.current?.disconnect();
5648
+ observer.current = null;
5649
+ elRef.current = el;
5650
+ if (!el || !enabled) return;
5651
+ const style = getComputedStyle(el);
5652
+ const px = (v) => parseFloat(v) || 0;
5653
+ borders.current = { x: px(style.borderLeftWidth) + px(style.borderRightWidth), y: px(style.borderTopWidth) + px(style.borderBottomWidth) };
5654
+ measure();
5655
+ if (typeof ResizeObserver === "undefined") return;
5656
+ const o = new ResizeObserver(measure);
5657
+ o.observe(el);
5658
+ for (const child of Array.from(el.children)) o.observe(child);
5659
+ observer.current = o;
5660
+ }, [enabled, measure]);
5661
+ const shown = enabled && (state.left || state.right);
5662
+ const { gutterRight: gr, gutterBottom: gb } = state;
5663
+ const fadeLayer = `linear-gradient(to right, ${state.left ? `transparent 0, black ${fade}px` : "black 0"}, ${state.right ? `black calc(100% - ${gr + fade}px), transparent calc(100% - ${gr}px)` : "black 100%"})`;
5664
+ const gutters = [gb > 0 && { size: `100% ${gb}px`, position: "0 100%" }, gr > 0 && { size: `${gr}px 100%`, position: "100% 0" }].filter(Boolean);
5665
+ const mask = shown ? {
5666
+ image: [fadeLayer, ...gutters.map(() => "linear-gradient(black, black)")].join(", "),
5667
+ size: ["100% 100%", ...gutters.map((g) => g.size)].join(", "),
5668
+ position: ["0 0", ...gutters.map((g) => g.position)].join(", ")
5669
+ } : void 0;
5670
+ return {
5671
+ /** The scroller's ref (a callback). */
5672
+ ref,
5673
+ /** The scroller itself, for code that moves it. */
5674
+ elRef,
5675
+ measure,
5676
+ /** Content hides past a side edge: the scroller moves sideways, and its edges fade. */
5677
+ overflowing: shown,
5678
+ /** It scrolls either way, sideways or up and down: a keyboard needs to reach it. */
5679
+ scrollable: enabled && (state.left || state.right || state.vertical),
5680
+ edges: { left: state.left, right: state.right },
5681
+ /** For the scroller: `data-fade` names the faded edges ("left", "right" or "left right"), the style masks them. */
5682
+ fadeProps: {
5683
+ "data-fade": shown ? [state.left && "left", state.right && "right"].filter(Boolean).join(" ") : void 0,
5684
+ style: mask ? { maskImage: mask.image, WebkitMaskImage: mask.image, maskSize: mask.size, WebkitMaskSize: mask.size, maskPosition: mask.position, WebkitMaskPosition: mask.position, maskRepeat: "no-repeat", WebkitMaskRepeat: "no-repeat" } : void 0
5685
+ }
5686
+ };
5687
+ }
5602
5688
  function TruncCell({ children, lines }) {
5603
5689
  const { ref, isTruncated } = useTextOverflow({ lines });
5604
5690
  const clampClass = lines === 1 ? "truncate" : `line-clamp-${lines}`;
@@ -5614,13 +5700,22 @@ var FRAME_CLASSES = cn(
5614
5700
  "bg-[var(--ds-bg-surface)]"
5615
5701
  );
5616
5702
  function TableHeading({ title, description, actions, titleId, descriptionId, inset }) {
5617
- return /* @__PURE__ */ jsxs("div", { "data-table-heading": "", className: cn("flex flex-wrap items-start justify-between gap-ds-4", inset ? "px-ds-5 py-ds-4" : "pb-ds-4"), children: [
5618
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col space-y-ds-1.5 grow basis-1/2 [min-width:min-content] break-words", children: [
5619
- /* @__PURE__ */ jsx("h3", { id: titleId, className: TITLE_CLASSES, children: title }),
5620
- description && /* @__PURE__ */ jsx("p", { id: descriptionId, className: "text-sm text-[var(--ds-text-muted)]", children: description })
5703
+ const heading = /* @__PURE__ */ jsx("h3", { id: titleId, className: TITLE_CLASSES, children: title });
5704
+ const actionsBlock = actions ? /* @__PURE__ */ jsx("div", { className: HEADER_LAYOUT.actions, children: actions }) : null;
5705
+ const descriptionText = (className) => description ? /* @__PURE__ */ jsx("p", { id: descriptionId, className: cn(className, "text-sm text-[var(--ds-text-muted)] break-words"), children: description }) : null;
5706
+ return /* @__PURE__ */ jsx("div", { "data-table-heading": "", className: cn(inset ? "px-ds-5 py-ds-4" : "pb-ds-4"), children: actions && !actionsAreBadges(actions) ? /* @__PURE__ */ jsxs("div", { className: HEADER_LAYOUT.row, children: [
5707
+ /* @__PURE__ */ jsxs("div", { className: HEADER_LAYOUT.block, children: [
5708
+ heading,
5709
+ descriptionText()
5621
5710
  ] }),
5622
- actions && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-ds-2 shrink-0 ml-auto", children: actions })
5623
- ] });
5711
+ actionsBlock
5712
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
5713
+ /* @__PURE__ */ jsxs("div", { className: HEADER_LAYOUT.row, children: [
5714
+ /* @__PURE__ */ jsx("div", { className: HEADER_LAYOUT.titleLeads, children: heading }),
5715
+ actionsBlock
5716
+ ] }),
5717
+ descriptionText(HEADER_LAYOUT.below)
5718
+ ] }) });
5624
5719
  }
5625
5720
  function Table({
5626
5721
  columns,
@@ -5645,6 +5740,7 @@ function Table({
5645
5740
  title,
5646
5741
  description,
5647
5742
  actions,
5743
+ "aria-label": ariaLabel,
5648
5744
  ref
5649
5745
  }) {
5650
5746
  const headingId = useId();
@@ -5716,6 +5812,8 @@ function Table({
5716
5812
  const preferences = useShellPreferences();
5717
5813
  const mobileBreakpoint = mobileBreakpointProp ?? preferences.tableToCardsBreakpoint ?? "md";
5718
5814
  const isMobile = useBreakpoint(mobileBreakpoint);
5815
+ const { ref: scrollRef, measure: measureScroll, scrollable, fadeProps } = useScrollEdges({ enabled: !isMobile });
5816
+ const [regionFocused, setRegionFocused] = useState(false);
5719
5817
  const cardHeading = title ? /* @__PURE__ */ jsx(TableHeading, { title, description, actions, titleId, descriptionId, inset: false }) : null;
5720
5818
  const titledCards = (body) => /* @__PURE__ */ jsxs("div", { ref, "data-ds-list": "", "data-gs": "table", className: cn("flex flex-col", className), children: [
5721
5819
  cardHeading,
@@ -5766,7 +5864,7 @@ function Table({
5766
5864
  }) }) });
5767
5865
  return title ? titledCards(cards) : cards;
5768
5866
  }
5769
- const table = /* @__PURE__ */ jsxs("table", { className: "w-full text-sm", ...title ? { "aria-labelledby": titleId, "aria-describedby": description ? descriptionId : void 0 } : {}, children: [
5867
+ const table = /* @__PURE__ */ jsxs("table", { className: "w-full text-sm", ...title ? { "aria-labelledby": titleId, "aria-describedby": description ? descriptionId : void 0 } : ariaLabel ? { "aria-label": ariaLabel } : {}, children: [
5770
5868
  /* @__PURE__ */ jsx("thead", { className: cn("bg-[var(--ds-bg-muted)]", stickyHeader && "sticky top-0 z-10"), children: /* @__PURE__ */ jsxs("tr", { className: "border-b-[length:var(--ds-divider-width)] border-[var(--ds-border-subtle)]", children: [
5771
5869
  selectable && /* @__PURE__ */ jsx("th", { className: cn("w-10", density === "compact" ? "px-ds-2 py-ds-2" : "px-ds-3 py-ds-3"), children: /* @__PURE__ */ jsx(
5772
5870
  Checkbox,
@@ -5917,26 +6015,37 @@ function Table({
5917
6015
  ] }, i);
5918
6016
  }) })
5919
6017
  ] });
5920
- if (!title) {
5921
- return /* @__PURE__ */ jsx(
5922
- "div",
5923
- {
5924
- ref,
5925
- "data-ds-list": "",
5926
- "data-gs": "table",
5927
- className: cn(
5928
- "w-full overflow-x-auto",
5929
- FRAME_CLASSES,
5930
- className
5931
- ),
5932
- children: table
5933
- }
5934
- );
5935
- }
5936
- return /* @__PURE__ */ jsxs("div", { ref, "data-ds-list": "", "data-gs": "table", className: cn("w-full flex flex-col", FRAME_CLASSES, className), children: [
5937
- /* @__PURE__ */ jsx(TableHeading, { title, description, actions, titleId, descriptionId, inset: true }),
5938
- /* @__PURE__ */ jsx("div", { "data-table-scroll": "", className: "min-h-0 overflow-x-auto rounded-b-[inherit]", children: table })
5939
- ] });
6018
+ const region = scrollable || regionFocused ? { tabIndex: 0, role: "region", ...title ? { "aria-labelledby": titleId } : { "aria-label": ariaLabel ?? "Table" } } : {};
6019
+ return /* @__PURE__ */ jsxs(
6020
+ "div",
6021
+ {
6022
+ ref,
6023
+ "data-ds-list": "",
6024
+ "data-gs": "table",
6025
+ className: cn("w-full min-w-0 flex flex-col", FRAME_CLASSES, "has-[>[data-table-scroll]:focus-visible]:shadow-[var(--ds-shadow-focus)]", className),
6026
+ children: [
6027
+ title && /* @__PURE__ */ jsx(TableHeading, { title, description, actions, titleId, descriptionId, inset: true }),
6028
+ /* @__PURE__ */ jsx(
6029
+ "div",
6030
+ {
6031
+ ref: scrollRef,
6032
+ "data-table-scroll": "",
6033
+ onScroll: measureScroll,
6034
+ onFocus: (e) => {
6035
+ if (e.target === e.currentTarget) setRegionFocused(true);
6036
+ },
6037
+ onBlur: (e) => {
6038
+ if (e.target === e.currentTarget) setRegionFocused(false);
6039
+ },
6040
+ ...fadeProps,
6041
+ ...region,
6042
+ className: cn("min-h-0 overflow-x-auto focus-visible:outline-none", title ? "rounded-b-[inherit]" : "rounded-[inherit]"),
6043
+ children: table
6044
+ }
6045
+ )
6046
+ ]
6047
+ }
6048
+ );
5940
6049
  }
5941
6050
  function optionsKeyForFit(options) {
5942
6051
  return options.map((o) => `${o.value}:${o.label}`).join("|");
@@ -11121,40 +11230,22 @@ function trackClasses(orientation) {
11121
11230
  "border-b-[length:var(--ds-nav-h-track-border)] border-[var(--ds-border-default)]"
11122
11231
  );
11123
11232
  }
11124
- var FADE = 24;
11125
11233
  function useScrollTrack(activeTab, enabled) {
11126
- const ref = useRef(null);
11127
- const [edges, setEdges] = useState({ left: false, right: false });
11128
- const measure = useCallback(() => {
11129
- const el = ref.current;
11130
- if (!el) return;
11131
- const left = el.scrollLeft > 1;
11132
- const right = el.scrollLeft + el.clientWidth < el.scrollWidth - 1;
11133
- setEdges((prev) => prev.left === left && prev.right === right ? prev : { left, right });
11134
- }, []);
11135
- useLayoutEffect(() => {
11136
- if (!enabled) return;
11137
- measure();
11138
- const el = ref.current;
11139
- if (!el || typeof ResizeObserver === "undefined") return;
11140
- const observer = new ResizeObserver(measure);
11141
- observer.observe(el);
11142
- return () => observer.disconnect();
11143
- }, [enabled, measure]);
11234
+ const { ref, elRef, measure, fadeProps } = useScrollEdges({ enabled });
11144
11235
  useLayoutEffect(() => {
11145
11236
  if (!enabled) return;
11146
- const el = ref.current;
11237
+ const el = elRef.current;
11147
11238
  const tab = el?.querySelector('[role="tab"][aria-selected="true"]');
11148
11239
  if (!el || !tab) return;
11149
11240
  const start = tab.offsetLeft;
11150
11241
  const end = start + tab.offsetWidth;
11151
- if (start < el.scrollLeft + FADE) el.scrollLeft = Math.max(0, start - FADE);
11152
- else if (end > el.scrollLeft + el.clientWidth - FADE) el.scrollLeft = end - el.clientWidth + FADE;
11242
+ if (start < el.scrollLeft + EDGE_FADE) el.scrollLeft = Math.max(0, start - EDGE_FADE);
11243
+ else if (end > el.scrollLeft + el.clientWidth - EDGE_FADE) el.scrollLeft = end - el.clientWidth + EDGE_FADE;
11153
11244
  measure();
11154
- }, [activeTab, enabled, measure]);
11245
+ }, [activeTab, enabled, measure, elRef]);
11155
11246
  useEffect(() => {
11156
11247
  if (!enabled) return;
11157
- const el = ref.current;
11248
+ const el = elRef.current;
11158
11249
  if (!el) return;
11159
11250
  const onWheel = (e) => {
11160
11251
  if (el.scrollWidth <= el.clientWidth || Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;
@@ -11204,14 +11295,11 @@ function useScrollTrack(activeTab, enabled) {
11204
11295
  el.removeEventListener("pointercancel", onUp);
11205
11296
  el.removeEventListener("click", onClick, true);
11206
11297
  };
11207
- }, [enabled]);
11208
- const fade = enabled ? [edges.left && "left", edges.right && "right"].filter(Boolean).join(" ") : "";
11209
- const mask = fade ? `linear-gradient(to right, ${edges.left ? `transparent 0, black ${FADE}px` : "black 0"}, ${edges.right ? `black calc(100% - ${FADE}px), transparent 100%` : "black 100%"})` : void 0;
11298
+ }, [enabled, elRef]);
11210
11299
  return {
11211
11300
  ref,
11212
11301
  onScroll: measure,
11213
- "data-fade": fade || void 0,
11214
- style: mask ? { maskImage: mask, WebkitMaskImage: mask } : void 0
11302
+ ...fadeProps
11215
11303
  };
11216
11304
  }
11217
11305
  var SCROLL_TRACK = "overflow-x-auto overflow-y-hidden [scrollbar-width:none] [&::-webkit-scrollbar]:hidden max-w-full";