rift-ds 1.16.0 → 1.17.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 (32) hide show
  1. package/bin/templates/blocks/components/blocks/AppShell.tsx +1 -1
  2. package/chat/ChatLauncher.d.ts +11 -1
  3. package/chat/ChatLauncher.js +7 -1
  4. package/chat/ChatWidget.css +21 -0
  5. package/components/AppLayout/AppLayout.css +90 -2
  6. package/components/AppLayout/AppLayout.d.ts +10 -2
  7. package/components/AppLayout/AppLayout.js +20 -2
  8. package/components/AppSidebar/AppSidebar.css +26 -0
  9. package/components/AppSidebar/AppSidebar.d.ts +15 -4
  10. package/components/AppSidebar/AppSidebar.js +7 -2
  11. package/components/DataTable/DataTable.js +29 -9
  12. package/components/DataTable/FilterEditor.js +1 -2
  13. package/components/DataTable/filterModel.js +4 -3
  14. package/components/DataTable/views.d.ts +9 -0
  15. package/components/DataTable/views.js +10 -1
  16. package/components/SegmentedControl/SegmentedControl.css +69 -0
  17. package/components/SegmentedControl/SegmentedControl.d.ts +4 -3
  18. package/components/SegmentedControl/SegmentedControl.js +90 -48
  19. package/components/TableToolbar/TableToolbar.css +6 -0
  20. package/components/TableToolbar/TableToolbar.js +52 -16
  21. package/components/ThemeToggle/ThemeToggle.css +2 -72
  22. package/components/ThemeToggle/ThemeToggle.d.ts +1 -1
  23. package/components/ThemeToggle/ThemeToggle.js +4 -40
  24. package/components/registry.json +1 -1
  25. package/components/registry.json.d.ts +1 -1
  26. package/components/registry.json.js +1 -1
  27. package/package.json +1 -1
  28. package/tokens/registry.json +2 -0
  29. package/tokens/registry.json.d.ts +2 -0
  30. package/tokens/registry.json.js +1 -1
  31. package/tokens/tokens-dark.css +7 -0
  32. package/tokens/tokens-light.css +7 -0
@@ -78,7 +78,7 @@ export function AppShell({ activeKey, breadcrumb, title, actions, children }: Ap
78
78
  expanded={expanded}
79
79
  onExpandedChange={setExpanded}
80
80
  logoText={PRODUCT_NAME}
81
- floating
81
+ layout="floating"
82
82
  />
83
83
 
84
84
  <div className={`${styles.main} ${expanded ? styles.mainExpanded : ''}`}>
@@ -44,6 +44,16 @@ export interface ChatLauncherProps {
44
44
  widget?: Omit<ChatWidgetProps, 'fullscreenEnabled' | 'placementEnabled' | 'compact' | 'phone'>;
45
45
  /** Rendered inside the panel after the widget: for a host's extra chrome. */
46
46
  children?: ReactNode;
47
+ /**
48
+ * How the docked panel sits on the page. `raised` (the default) is a glass
49
+ * card with the floating shadow, inset by `--ds-chat-inset`. `inset` is
50
+ * flat: the app surface colour, no shadow, border or blur, and the
51
+ * `--ds-shell-inset` gap, so it matches AppLayout's `inset` content
52
+ * surface beside it. The overlay below the dock threshold takes the same
53
+ * gap but keeps its shadow, since it still covers the page; the floating
54
+ * card and the takeover keep their own look.
55
+ */
56
+ surface?: 'raised' | 'inset';
47
57
  }
48
58
  /**
49
59
  * The site-wide panel. Mount it once, after the page content, so it sits
@@ -68,4 +78,4 @@ export interface ChatLauncherProps {
68
78
  * touch the host's body). The panel's geometry reads `--ds-chat-width`,
69
79
  * `--ds-chat-inset` and `--ds-chat-float-height`, defaulted on `:root`.
70
80
  */
71
- export declare function ChatLauncher({ label, icon, summary, hidden, panelId, hostAttribute, panelLabel, aiButtonLabels, widget, children, }: ChatLauncherProps): import("react").JSX.Element | null;
81
+ export declare function ChatLauncher({ label, icon, summary, hidden, panelId, hostAttribute, panelLabel, aiButtonLabels, widget, children, surface, }: ChatLauncherProps): import("react").JSX.Element | null;
@@ -44,7 +44,8 @@ function ChatLauncher({
44
44
  panelLabel = "Chat",
45
45
  aiButtonLabels,
46
46
  widget,
47
- children
47
+ children,
48
+ surface = "raised"
48
49
  }) {
49
50
  const { open, setOpen, panelPhase, view, side, floating, returnFocusRef, send } = useChatWidget();
50
51
  const launchSuggestion = (id) => {
@@ -270,6 +271,10 @@ function ChatLauncher({
270
271
  side === "left" ? "ds-chat-launcher__panel--left" : "",
271
272
  isFloating ? "ds-chat-launcher__panel--floating" : "",
272
273
  isFull ? "ds-chat-launcher__panel--full" : "",
274
+ // Inset: the dock and the overlay both sit at the shell's gap; only the
275
+ // dock goes flat, since the overlay still covers the page.
276
+ surface === "inset" && !isFloating && !isFull ? "ds-chat-launcher__panel--inset" : "",
277
+ surface === "inset" && docked && !isFloating && !isFull ? "ds-chat-launcher__panel--flat" : "",
273
278
  panelPhase === "closing" ? "ds-chat-launcher__panel--closing" : ""
274
279
  ].filter(Boolean).join(" ");
275
280
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -332,6 +337,7 @@ function ChatLauncher({
332
337
  className: [
333
338
  "ds-chat-launcher__dock-handle",
334
339
  side === "left" ? "ds-chat-launcher__dock-handle--left" : "",
340
+ surface === "inset" ? "ds-chat-launcher__dock-handle--inset" : "",
335
341
  resizing ? "ds-chat-launcher__dock-handle--resizing" : ""
336
342
  ].filter(Boolean).join(" "),
337
343
  "aria-hidden": "true",
@@ -995,6 +995,22 @@ html[data-chat-resizing] .ds-chat-launcher__panel {
995
995
  left: var(--ds-chat-inset); /* ds-allow(physical): the left dock is the screen's left edge */
996
996
  }
997
997
 
998
+ /* The inset surface (ChatLauncher's surface="inset"). Docked and as an
999
+ overlay, the panel is held off the edges by the shell's gap rather than
1000
+ the chat's own, so it lines up with AppLayout's inset content surface.
1001
+ Docked, it is also flat: a plane on the app surface colour with no
1002
+ shadow, border or blur. The overlay keeps its shadow over the page. */
1003
+ .ds-chat-launcher__panel--inset {
1004
+ --ds-chat-inset: var(--ds-shell-inset, 10px); /* AppSidebar.css declares the gap; the fallback mirrors it */
1005
+ }
1006
+
1007
+ .ds-chat-launcher__panel--flat {
1008
+ background: var(--color-bg-app-surface);
1009
+ border-color: transparent;
1010
+ box-shadow: none;
1011
+ backdrop-filter: none;
1012
+ }
1013
+
998
1014
  /* Fullscreen view: a true viewport takeover. The stronger blur keeps the
999
1015
  transcript readable while the page's colours still shine through. The
1000
1016
  border fades rather than vanishing, so the geometry never snaps mid-glide. */
@@ -1047,6 +1063,11 @@ html[data-chat-resizing] .ds-chat-launcher__panel {
1047
1063
  left: calc(var(--ds-chat-inset) + var(--ds-chat-width) - 10px); /* ds-allow(physical): the dock grip tracks the docked card's screen edge */
1048
1064
  }
1049
1065
 
1066
+ /* The inset surface's dock sits at the shell's gap, so its grip does too. */
1067
+ .ds-chat-launcher__dock-handle--inset {
1068
+ --ds-chat-inset: var(--ds-shell-inset, 10px); /* AppSidebar.css declares the gap; the fallback mirrors it */
1069
+ }
1070
+
1050
1071
  .ds-chat-launcher__dock-handle::after {
1051
1072
  content: "";
1052
1073
  width: 4px;
@@ -19,7 +19,7 @@
19
19
  /* The rail's width plus, while it shows, the side panel's. */
20
20
  margin-inline-start: calc(var(--ds-app-layout-rail, 64px) + var(--ds-app-layout-panel, 0px));
21
21
  padding: var(--padding-1000) var(--padding-500);
22
- transition: margin-left var(--motion-duration-slow) var(--motion-ease-emphasized);
22
+ transition: margin-inline-start var(--motion-duration-slow) var(--motion-ease-emphasized);
23
23
  min-width: 0;
24
24
  }
25
25
 
@@ -47,6 +47,72 @@
47
47
  gap: var(--gap-1000);
48
48
  }
49
49
 
50
+ /* ============================================
51
+ FLOATING LAYOUT — the rail floats as a glass
52
+ card, so the main column clears its inset on
53
+ both sides of it
54
+ ============================================ */
55
+ .ds-app-layout--floating .ds-app-layout__main {
56
+ margin-inline-start: calc(
57
+ var(--ds-app-layout-rail, 64px) + var(--ds-app-layout-panel, 0px) +
58
+ var(--ds-sidebar-float-inset, 20px) * 2
59
+ );
60
+ }
61
+
62
+ /* ============================================
63
+ INSET LAYOUT — the rail flush on the shell
64
+ colour, the content full width in a surface
65
+ inset from the other three edges. The page
66
+ holds the viewport and the surface scrolls.
67
+ The rail's own item inset is the gap on its
68
+ side, so the selected item sits evenly
69
+ between the two edges.
70
+ ============================================ */
71
+ .ds-app-layout--inset {
72
+ height: 100dvh;
73
+ min-height: 0;
74
+ background-color: var(--color-bg-app-shell);
75
+ }
76
+
77
+ .ds-app-layout--inset .ds-app-layout__main {
78
+ padding: var(--ds-shell-inset);
79
+ padding-inline-start: 0;
80
+ min-height: 0;
81
+ }
82
+
83
+ .ds-app-layout__surface {
84
+ flex: 1;
85
+ min-width: 0;
86
+ display: flex;
87
+ overflow: hidden;
88
+ background-color: var(--color-bg-app-surface);
89
+ border-radius: var(--radius-600);
90
+ }
91
+
92
+ .ds-app-layout__scroller {
93
+ flex: 1;
94
+ min-width: 0;
95
+ overflow-y: auto;
96
+ padding: var(--padding-1000) var(--padding-1000);
97
+ /* The scrollbar stays invisible until the surface scrolls (data-scrolling
98
+ is set by the scroll handler and cleared once it settles). */
99
+ scrollbar-width: thin;
100
+ scrollbar-color: transparent transparent;
101
+ }
102
+
103
+ .ds-app-layout__scroller[data-scrolling] {
104
+ scrollbar-color: var(--color-bg-container-border) transparent;
105
+ }
106
+
107
+ /* Full width in the surface, and a size container: content inside can
108
+ restack by the room the surface gives it (an open chat panel or an
109
+ expanded rail narrows it without the viewport changing) with
110
+ @container ds-app-layout-content queries. */
111
+ .ds-app-layout--inset .ds-app-layout__content {
112
+ max-width: none;
113
+ container: ds-app-layout-content / inline-size;
114
+ }
115
+
50
116
  /* ============================================
51
117
  RESPONSIVE
52
118
  ============================================ */
@@ -56,7 +122,29 @@
56
122
  padding: var(--padding-500) var(--padding-400);
57
123
  }
58
124
 
59
- .ds-app-layout--expanded .ds-app-layout__main {
125
+ .ds-app-layout--expanded .ds-app-layout__main,
126
+ .ds-app-layout--floating .ds-app-layout__main {
60
127
  margin-inline-start: 0;
61
128
  }
129
+
130
+ /* Phones: the rail is a drawer, so the inset surface runs edge to edge
131
+ and the page scrolls normally. */
132
+ .ds-app-layout--inset {
133
+ height: auto;
134
+ min-height: 100vh;
135
+ }
136
+
137
+ .ds-app-layout--inset .ds-app-layout__main {
138
+ padding: 0;
139
+ }
140
+
141
+ .ds-app-layout__surface {
142
+ overflow: visible;
143
+ border-radius: 0;
144
+ }
145
+
146
+ .ds-app-layout__scroller {
147
+ overflow-y: visible;
148
+ padding: var(--padding-500) var(--padding-400);
149
+ }
62
150
  }
@@ -1,5 +1,5 @@
1
1
  import { default as React } from 'react';
2
- import { AppSidebarSection, AppSidebarProfile } from '../AppSidebar/AppSidebar';
2
+ import { AppSidebarSection, AppSidebarProfile, AppSidebarLayout } from '../AppSidebar/AppSidebar';
3
3
  import { LinkComponent } from '../../behaviors/LinkProvider';
4
4
  /** Props owned by AppLayout itself — everything else falls through to the outer wrapper `<div>`. */
5
5
  type AppLayoutOwnProps = {
@@ -23,6 +23,14 @@ type AppLayoutOwnProps = {
23
23
  logo?: React.ReactNode;
24
24
  /** Page content — centred in the main area */
25
25
  children: React.ReactNode;
26
+ /**
27
+ * How the rail and the content meet the page. `flush` (the default) runs
28
+ * the rail edge to edge with the content centred beside it. `floating`
29
+ * floats the rail as a glass card, the content still centred. `inset` runs
30
+ * the rail flush on the app shell colour and sets the content full width
31
+ * in a surface inset from the other edges, which scrolls inside itself.
32
+ */
33
+ layout?: AppSidebarLayout;
26
34
  /**
27
35
  * Colour scheme: 'dark' pins the layout to the dark theme (the historical
28
36
  * behaviour and the default); 'inherit' drops the pin so the layout
@@ -42,7 +50,7 @@ export interface AppLayoutProps extends AppLayoutOwnProps, Omit<React.ComponentP
42
50
  }
43
51
  /**
44
52
  * AppLayout — full-page template with a collapsible sidebar
45
- * and a centred content area.
53
+ * and a content area, centred or set in an inset surface (see `layout`).
46
54
  *
47
55
  * The forwarded ref and any unrecognised props target the outer wrapper
48
56
  * `<div>`. With `theme="inherit"` a `data-theme` passed by the consumer
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  import { jsxs, jsx } from "react/jsx-runtime";
3
- import React, { useState, useCallback } from "react";
3
+ import React, { useState, useCallback, useRef, useEffect } from "react";
4
4
  import { AppSidebar } from "../AppSidebar/AppSidebar.js";
5
+ import { MOTION_SCROLL_SETTLE_MS } from "../../tokens/motion.js";
5
6
  import "./AppLayout.css";
6
7
  const AppLayout = React.forwardRef(
7
8
  ({
@@ -15,6 +16,7 @@ const AppLayout = React.forwardRef(
15
16
  logoText,
16
17
  logo,
17
18
  children,
19
+ layout = "flush",
18
20
  theme = "dark",
19
21
  linkComponent,
20
22
  className = "",
@@ -25,9 +27,20 @@ const AppLayout = React.forwardRef(
25
27
  const handleExpandedChange = useCallback((next) => {
26
28
  setExpanded(next);
27
29
  }, []);
30
+ const scrollTimer = useRef(void 0);
31
+ useEffect(() => () => clearTimeout(scrollTimer.current), []);
32
+ const handleSurfaceScroll = useCallback((event) => {
33
+ const scroller = event.currentTarget;
34
+ scroller.dataset.scrolling = "";
35
+ clearTimeout(scrollTimer.current);
36
+ scrollTimer.current = setTimeout(() => {
37
+ delete scroller.dataset.scrolling;
38
+ }, MOTION_SCROLL_SETTLE_MS);
39
+ }, []);
28
40
  const classes = [
29
41
  baseClass,
30
42
  expanded ? `${baseClass}--expanded` : "",
43
+ layout !== "flush" ? `${baseClass}--${layout}` : "",
31
44
  className
32
45
  ].filter(Boolean).join(" ");
33
46
  return /* @__PURE__ */ jsxs(
@@ -51,10 +64,15 @@ const AppLayout = React.forwardRef(
51
64
  onExpandedChange: handleExpandedChange,
52
65
  logoText,
53
66
  logo,
67
+ layout,
54
68
  linkComponent
55
69
  }
56
70
  ),
57
- /* @__PURE__ */ jsx("main", { className: `${baseClass}__main`, children: /* @__PURE__ */ jsx("div", { className: `${baseClass}__content`, children }) })
71
+ /* @__PURE__ */ jsx("main", { className: `${baseClass}__main`, children: layout === "inset" ? (
72
+ // The surface clips to its rounded corners and the scroller
73
+ // inside it scrolls, so the scrollbar never squares a corner.
74
+ /* @__PURE__ */ jsx("div", { className: `${baseClass}__surface`, children: /* @__PURE__ */ jsx("div", { className: `${baseClass}__scroller`, onScroll: handleSurfaceScroll, children: /* @__PURE__ */ jsx("div", { className: `${baseClass}__content`, children }) }) })
75
+ ) : /* @__PURE__ */ jsx("div", { className: `${baseClass}__content`, children }) })
58
76
  ]
59
77
  }
60
78
  );
@@ -51,6 +51,22 @@
51
51
  backdrop-filter: blur(24px);
52
52
  }
53
53
 
54
+ /* The inset layout's gap: between the content surface and the viewport
55
+ edges, and around a docked chat panel's inset surface (ChatLauncher's
56
+ surface="inset" falls back to the same value when this sheet is absent). */
57
+ :root {
58
+ --ds-shell-inset: 10px;
59
+ }
60
+
61
+ /* ============================================
62
+ INSET VARIANT — the rail flush on the app
63
+ shell colour, beside a content surface inset
64
+ from the edges (AppLayout's inset layout)
65
+ ============================================ */
66
+ .ds-app-sidebar--inset {
67
+ background-color: var(--color-bg-app-shell);
68
+ }
69
+
54
70
  /* ============================================
55
71
  TOP SECTION — logo + nav
56
72
  ============================================ */
@@ -605,6 +621,16 @@
605
621
  inset-inline-start: calc(var(--ds-sidebar-float-inset, 20px) + 280px + var(--gap-300));
606
622
  }
607
623
 
624
+ /* Inset: the side panel joins the rail on the shell plane, with no borders
625
+ between them, so rail and panel read as one column of navigation and the
626
+ inset content surface starts beyond both (AppLayout reserves the panel's
627
+ width while it shows). */
628
+ .ds-app-sidebar__panel--inset {
629
+ background-color: var(--color-bg-app-shell);
630
+ border-inline-start-color: transparent;
631
+ border-inline-end-color: transparent;
632
+ }
633
+
608
634
  /* ============================================
609
635
  PANEL MODE RAIL — labels under icons
610
636
  With the second level in the panel, the
@@ -61,6 +61,8 @@ export interface AppSidebarProfile {
61
61
  /** Avatar image URL */
62
62
  avatarUrl?: string;
63
63
  }
64
+ /** How the rail meets the page: edge to edge, floating as a card, or flush beside an inset content surface. */
65
+ export type AppSidebarLayout = 'flush' | 'floating' | 'inset';
64
66
  /** Props owned by AppSidebar itself — everything else falls through to the `<nav>`. */
65
67
  type AppSidebarOwnProps = {
66
68
  /** Navigation sections */
@@ -97,10 +99,19 @@ type AppSidebarOwnProps = {
97
99
  /** Callback when profile more button clicked */
98
100
  onProfileMore?: () => void;
99
101
  /**
100
- * Floats the rail off the viewport edges as a glass card: inset with
101
- * rounded corners, the translucent glass fill over a backdrop blur, and
102
- * the floating shadow. The inset defaults to 20px and is overridable via
103
- * the --ds-sidebar-float-inset custom property.
102
+ * How the rail meets the page. `flush` runs it edge to edge on the page
103
+ * colour. `floating` floats it off the viewport edges as a glass card:
104
+ * inset with rounded corners, the translucent glass fill over a backdrop
105
+ * blur, and the floating shadow (the inset defaults to 20px and is
106
+ * overridable via the --ds-sidebar-float-inset custom property). `inset`
107
+ * runs it flush on the app shell colour, beside a content surface inset
108
+ * from the edges, as AppLayout's `inset` layout draws it. Defaults to
109
+ * `flush` (or `floating` while the deprecated `floating` is set).
110
+ */
111
+ layout?: AppSidebarLayout;
112
+ /**
113
+ * Floats the rail as a glass card, the same as `layout="floating"`.
114
+ * @deprecated Use `layout="floating"`.
104
115
  */
105
116
  floating?: boolean;
106
117
  /** Rendered under the logo row, above the nav; fades out while collapsed */
@@ -46,7 +46,8 @@ const AppSidebar = React.forwardRef(function AppSidebar2({
46
46
  expanded: controlledExpanded,
47
47
  onExpandedChange,
48
48
  onProfileMore,
49
- floating = false,
49
+ layout: layoutProp,
50
+ floating: floatingProp = false,
50
51
  topSlot,
51
52
  footerSlot,
52
53
  className = "",
@@ -63,6 +64,8 @@ const AppSidebar = React.forwardRef(function AppSidebar2({
63
64
  ...rest
64
65
  }, ref) {
65
66
  const baseClass = "ds-app-sidebar";
67
+ const layout = layoutProp ?? (floatingProp ? "floating" : "flush");
68
+ const floating = layout === "floating";
66
69
  const Link = useLinkComponent(linkComponent);
67
70
  const avatarSeed = (item) => typeof item.pixelAvatar === "string" ? item.pixelAvatar : item.label;
68
71
  const avatarItems = sections.flatMap(
@@ -228,6 +231,7 @@ const AppSidebar = React.forwardRef(function AppSidebar2({
228
231
  // a modal overlay would be all cost and no labels.
229
232
  isExpanded || mobileOpen ? `${baseClass}--expanded` : "",
230
233
  floating ? `${baseClass}--floating` : "",
234
+ layout === "inset" ? `${baseClass}--inset` : "",
231
235
  mobileOpen ? `${baseClass}--mobile-open` : "",
232
236
  isPanelMode ? `${baseClass}--panel-mode` : "",
233
237
  className
@@ -480,7 +484,8 @@ const AppSidebar = React.forwardRef(function AppSidebar2({
480
484
  panelVisible ? `${baseClass}__panel--visible` : "",
481
485
  isExpanded ? `${baseClass}__panel--beside-expanded` : "",
482
486
  isPanelMode ? `${baseClass}__panel--panel-mode` : "",
483
- floating ? `${baseClass}__panel--floating` : ""
487
+ floating ? `${baseClass}__panel--floating` : "",
488
+ layout === "inset" ? `${baseClass}__panel--inset` : ""
484
489
  ].filter(Boolean).join(" "),
485
490
  items: panelSource.items,
486
491
  title: panelSource.label,
@@ -22,7 +22,7 @@ import { TableContext } from "./tableContext.js";
22
22
  import { Popover } from "../Popover/Popover.js";
23
23
  import { typedComparator, TYPE_DEFAULTS, typedText, formatNumber } from "./columnTypes.js";
24
24
  import { TypedCell } from "./TypedCell.js";
25
- import { reconcileView } from "./views.js";
25
+ import { slotStartingView, reconcileView } from "./views.js";
26
26
  import { calculate, TOTALS, NUMERIC_TOTALS } from "./aggregates.js";
27
27
  import { useLinkComponent } from "../../behaviors/LinkProvider.js";
28
28
  import { TYPED_KINDS, LIVE_KINDS, EDITOR_MIN_WIDTH } from "./editorKinds.js";
@@ -272,11 +272,21 @@ function DataTable({
272
272
  const locale = useLocale(localeProp);
273
273
  const collator = useMemo(() => textCollator(locale), [locale]);
274
274
  const compareValues = useMemo(() => valueComparator(collator), [collator]);
275
- const [size, setSize] = useControllable(sizeProp, defaultSize ?? "default", onSizeChange);
276
- const [search, setSearchState] = useControllable(searchProp, defaultSearch ?? "", onSearchChange);
275
+ const [startingView] = useState(() => slotStartingView(toolbar));
276
+ const startsWith = (key, controlled) => controlled === void 0 && startingView?.[key] !== void 0;
277
+ const [size, setSize] = useControllable(
278
+ sizeProp,
279
+ (startsWith("size", sizeProp) ? startingView.size : void 0) ?? defaultSize ?? "default",
280
+ onSizeChange
281
+ );
282
+ const [search, setSearchState] = useControllable(
283
+ searchProp,
284
+ (startsWith("search", searchProp) ? startingView.search : void 0) ?? defaultSearch ?? "",
285
+ onSearchChange
286
+ );
277
287
  const [filters, setFiltersState] = useControllable(
278
288
  filtersProp,
279
- defaultFilters ?? [],
289
+ (startsWith("filters", filtersProp) ? startingView.filters : void 0) ?? defaultFilters ?? [],
280
290
  onFiltersChange
281
291
  );
282
292
  const filterLabels = {
@@ -286,7 +296,7 @@ function DataTable({
286
296
  };
287
297
  const [pageSize, setPageSize] = useControllable(
288
298
  pageSizeProp,
289
- defaultPageSize,
299
+ (startsWith("pageSize", pageSizeProp) ? startingView.pageSize : void 0) ?? defaultPageSize,
290
300
  onPageSizeChange
291
301
  );
292
302
  const [page, setPageState] = useControllable(pageProp, defaultPage ?? 1, onPageChange);
@@ -321,14 +331,15 @@ function DataTable({
321
331
  return () => clearTimeout(timer);
322
332
  }, [announceRequest]);
323
333
  const isSortControlled = sortsProp !== void 0 || sort !== void 0;
334
+ const openingSorts = defaultSorts ?? (defaultSort ? [defaultSort] : []);
324
335
  const [uncontrolledSorts, setUncontrolledSorts] = useState(
325
- defaultSorts ?? (defaultSort ? [defaultSort] : [])
336
+ (startsWith("sorts", sortsProp ?? sort) ? startingView.sorts : void 0) ?? openingSorts
326
337
  );
327
338
  const currentSorts = sortsProp ?? (sort !== void 0 ? sort ? [sort] : [] : uncontrolledSorts);
328
339
  const sortOf = (key) => currentSorts.find((entry) => entry.key === key);
329
340
  const [groupingState, setGroupingState] = useControllable(
330
341
  groupingProp,
331
- defaultGrouping ?? [],
342
+ (startsWith("grouping", groupingProp) ? startingView.grouping : void 0) ?? defaultGrouping ?? [],
332
343
  onGroupingChange
333
344
  );
334
345
  const [expandedState, setExpanded] = useControllable(
@@ -391,7 +402,7 @@ function DataTable({
391
402
  const canHide = (col) => !col.locked && (col.hideable ?? true);
392
403
  const [widthsState, setWidths] = useControllable(
393
404
  columnWidths,
394
- defaultColumnWidths ?? {},
405
+ (startsWith("columnWidths", columnWidths) ? startingView.columnWidths : void 0) ?? defaultColumnWidths ?? {},
395
406
  onColumnWidthsChange
396
407
  );
397
408
  const [orderState, setOrder] = useControllable(columnOrder, defaultColumnOrder ?? [], onColumnOrderChange);
@@ -1022,7 +1033,16 @@ function DataTable({
1022
1033
  size,
1023
1034
  ...pageSize !== void 0 ? { pageSize } : {}
1024
1035
  };
1025
- const [baseView] = useState(currentView);
1036
+ const [baseView] = useState(() => ({
1037
+ ...currentView,
1038
+ ...startsWith("sorts", sortsProp ?? sort) ? { sorts: openingSorts } : {},
1039
+ ...startsWith("filters", filtersProp) ? { filters: defaultFilters ?? [] } : {},
1040
+ ...startsWith("search", searchProp) ? { search: defaultSearch ?? "" } : {},
1041
+ ...startsWith("columnWidths", columnWidths) ? { columnWidths: defaultColumnWidths ?? {} } : {},
1042
+ ...startsWith("grouping", groupingProp) ? { grouping: defaultGrouping ?? [] } : {},
1043
+ ...startsWith("size", sizeProp) ? { size: defaultSize ?? "default" } : {},
1044
+ ...startsWith("pageSize", pageSizeProp) ? { pageSize: defaultPageSize } : {}
1045
+ }));
1026
1046
  const viewColumns = columns.map((col) => ({
1027
1047
  key: col.key,
1028
1048
  filterKind: col.type ? TYPE_DEFAULTS[col.type].filter : void 0,
@@ -120,9 +120,8 @@ function FilterEditor({
120
120
  }
121
121
  );
122
122
  })();
123
- const isList = column.kind === "enum" || column.kind === "tags";
124
123
  return /* @__PURE__ */ jsxs("div", { className: "ds-data-table__filter-editor", role: "group", "aria-label": labels.editor(column.label), children: [
125
- !isList && /* @__PURE__ */ jsx(
124
+ operators.length > 1 && /* @__PURE__ */ jsx(
126
125
  Dropdown,
127
126
  {
128
127
  size,
@@ -2,9 +2,10 @@ const OPERATORS = {
2
2
  text: ["contains", "notContains", "is", "isNot", "startsWith", "endsWith", "isEmpty", "isNotEmpty"],
3
3
  number: ["eq", "neq", "lt", "lte", "gt", "gte", "between", "isEmpty", "isNotEmpty"],
4
4
  date: ["is", "before", "after", "between", "isEmpty", "isNotEmpty"],
5
- // A list filter is its checkboxes, so it has the one condition they mean.
6
- enum: ["isAnyOf"],
7
- tags: ["hasAnyOf"],
5
+ // A list filter is its checkboxes; the condition says what a tick means,
6
+ // and the first, "any of", is the one a new filter starts with.
7
+ enum: ["isAnyOf", "isNoneOf"],
8
+ tags: ["hasAnyOf", "hasAllOf", "hasNoneOf"],
8
9
  boolean: ["isChecked", "isNotChecked"]
9
10
  };
10
11
  const VALUELESS = /* @__PURE__ */ new Set(["isEmpty", "isNotEmpty", "isChecked", "isNotChecked"]);
@@ -30,6 +30,15 @@ export interface TableView {
30
30
  /** Rows per page. */
31
31
  pageSize?: number;
32
32
  }
33
+ /**
34
+ * The view a table starts on when a TableToolbar with saved views sits
35
+ * directly in its `toolbar` slot: the active one (`activeView`, else
36
+ * `defaultActiveView`, else the first). Read from the element's props, so
37
+ * the table's first render, the server's HTML included, is already that
38
+ * view; the toolbar applies it on mount too, for a bar the table cannot
39
+ * see into (one wrapped in another component).
40
+ */
41
+ export declare function slotStartingView(toolbar: unknown): TableView | undefined;
33
42
  /** What reconciling needs to know about a column. */
34
43
  export interface ViewColumn {
35
44
  key: string;
@@ -1,4 +1,12 @@
1
1
  import { OPERATORS, VALUELESS } from "./filterModel.js";
2
+ function slotStartingView(toolbar) {
3
+ if (!toolbar || typeof toolbar !== "object" || !("props" in toolbar)) return void 0;
4
+ const props = toolbar.props;
5
+ const views = props?.views ?? props?.defaultViews;
6
+ if (!Array.isArray(views) || views.length === 0) return void 0;
7
+ const id = props?.activeView ?? props?.defaultActiveView;
8
+ return (views.find((v) => v.id === id) ?? views[0]).view;
9
+ }
2
10
  function reconcileView(view, columns) {
3
11
  const byKey = new Map(columns.map((col) => [col.key, col]));
4
12
  const known = (key) => byKey.has(key);
@@ -35,5 +43,6 @@ function reconcileView(view, columns) {
35
43
  return next;
36
44
  }
37
45
  export {
38
- reconcileView
46
+ reconcileView,
47
+ slotStartingView
39
48
  };
@@ -228,3 +228,72 @@
228
228
  .ds-segmented-control--compact .ds-segmented-control__icon {
229
229
  --icon-size: var(--icon-size-500);
230
230
  }
231
+
232
+ /* ============================================
233
+ ICON STRIP (showLabels={false})
234
+ The glyphs alone, with each label turned
235
+ into the segment's tooltip: it appears below
236
+ the segment on hover and keyboard focus, on
237
+ Tooltip's surface, so an unfamiliar glyph is
238
+ never left to be guessed. The component
239
+ places the label with the shared
240
+ anchored-position behaviour, so a seat that
241
+ clips cannot cut it off, and it flips above
242
+ the strip when there is no room below. The
243
+ label stays in the accessibility tree as the
244
+ segment's name. A strip the collapse ladder
245
+ squeezed to icons keeps its labels clipped
246
+ instead.
247
+ ============================================ */
248
+
249
+ .ds-segmented-control__label--tip {
250
+ /* Out of flow at rest; the behaviour makes it fixed once it shows. */
251
+ position: absolute;
252
+ z-index: var(--layer-floating);
253
+ padding: var(--padding-150) var(--padding-200);
254
+ border-radius: var(--radius-100);
255
+ border: var(--border-025) solid var(--color-bg-container-border);
256
+ background-color: var(--color-bg-page-primary);
257
+ color: var(--color-text-primary);
258
+ box-shadow: var(--shadow-floating);
259
+ font-family: var(--font-paragraph-sm-family);
260
+ font-size: var(--font-paragraph-sm-size);
261
+ font-weight: var(--font-paragraph-sm-weight);
262
+ line-height: var(--font-paragraph-sm-line-height);
263
+ letter-spacing: var(--font-paragraph-sm-letter-spacing);
264
+ white-space: nowrap;
265
+ pointer-events: none;
266
+ opacity: 0;
267
+ transform: translateY(calc(-1 * var(--gap-100)));
268
+ transition:
269
+ opacity var(--motion-duration-fast) var(--motion-ease-standard),
270
+ transform var(--motion-duration-fast) var(--motion-ease-standard);
271
+ }
272
+
273
+ /* --motion-duration-slow matches MOTION_HOVER_SHOW_DELAY_MS, the hover-intent
274
+ delay Tooltip uses in JS, so the label does not flash while the pointer
275
+ passes through. Hiding takes no delay, like a tooltip leaving. */
276
+ .ds-segmented-control__segment:hover .ds-segmented-control__label--tip,
277
+ .ds-segmented-control__segment:focus-visible .ds-segmented-control__label--tip {
278
+ opacity: 1;
279
+ transform: none;
280
+ transition-delay: var(--motion-duration-slow);
281
+ }
282
+
283
+ /* Flipped above the strip, it slides down into place instead. */
284
+ .ds-segmented-control__label--tip[data-side='top'] {
285
+ transform: translateY(var(--gap-100));
286
+ }
287
+
288
+ /* Without an inline label a segment's asymmetric padding reads lopsided:
289
+ square each one into an even pill around its glyph. */
290
+ .ds-segmented-control--icons .ds-segmented-control__segment {
291
+ padding-inline: var(--padding-200);
292
+ }
293
+
294
+ /* ds-allow(motion): drops the label tooltip's slide outright rather than playing it at 0.01ms. */
295
+ @media (prefers-reduced-motion: reduce) {
296
+ .ds-segmented-control__label--tip {
297
+ transition: none;
298
+ }
299
+ }
@@ -65,9 +65,10 @@ type SegmentedControlOwnProps = {
65
65
  collapse?: boolean;
66
66
  /**
67
67
  * Show each segment's label beside its icon. Set it to `false` for an
68
- * icon-only strip: the labels stay in the accessibility tree as the
69
- * segments' names, and a segment without an icon keeps its label on
70
- * screen. Defaults to `true`.
68
+ * icon-only strip: each label becomes its segment's tooltip on hover and
69
+ * keyboard focus and stays the segment's name in the accessibility tree,
70
+ * and a segment without an icon keeps its label on screen. Defaults to
71
+ * `true`.
71
72
  */
72
73
  showLabels?: boolean;
73
74
  /**