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
@@ -5,6 +5,7 @@ import "./SegmentedControl.css";
5
5
  import "../../behaviors/visually-hidden.css";
6
6
  import "../../fonts/material-symbols.css";
7
7
  import { mirrorClass, logicalArrowKey } from "../../behaviors/direction.js";
8
+ import { useAnchoredPosition } from "../../behaviors/useAnchoredPosition.js";
8
9
  const SegmentedControl = React.forwardRef(
9
10
  ({
10
11
  segments,
@@ -70,6 +71,7 @@ const SegmentedControl = React.forwardRef(
70
71
  variantClass,
71
72
  fullWidthClass,
72
73
  pillReady ? `${baseClass}--pill-ready` : "",
74
+ !showLabels ? `${baseClass}--icons` : "",
73
75
  className
74
76
  ].filter(Boolean).join(" ");
75
77
  const positionPill = useCallback(
@@ -182,6 +184,28 @@ const SegmentedControl = React.forwardRef(
182
184
  }
183
185
  };
184
186
  }, []);
187
+ const tipSegmentRef = useRef(null);
188
+ const tipLabelRef = useRef(null);
189
+ const [tip, setTip] = useState(null);
190
+ const showTip = (target) => {
191
+ if (showLabels) return;
192
+ const segment = target?.closest(`.${baseClass}__segment`);
193
+ const strip = containerRef.current;
194
+ const label = segment?.querySelector(`.${baseClass}__label--tip`);
195
+ if (!segment || !strip || !label) return;
196
+ tipSegmentRef.current = segment;
197
+ tipLabelRef.current = label;
198
+ setTip([...strip.querySelectorAll(`.${baseClass}__segment`)].indexOf(segment));
199
+ };
200
+ useAnchoredPosition({
201
+ open: tip !== null,
202
+ target: tip,
203
+ anchorRef: tipSegmentRef,
204
+ panelRef: tipLabelRef,
205
+ side: "bottom",
206
+ align: "center",
207
+ gapProperty: "--gap-100"
208
+ });
185
209
  const handleKeyDown = (e, idx) => {
186
210
  const enabledSegments = segments.filter((s) => !s.disabled);
187
211
  const currentEnabledIdx = enabledSegments.findIndex((s) => s.value === segments[idx].value);
@@ -213,54 +237,72 @@ const SegmentedControl = React.forwardRef(
213
237
  });
214
238
  }
215
239
  };
216
- return /* @__PURE__ */ jsxs("div", { "aria-label": ariaLabel, ...rest, ref: setRef, className: classes, role: "tablist", children: [
217
- /* @__PURE__ */ jsx("span", { className: `${baseClass}__pill`, "aria-hidden": "true", ref: pillRef }),
218
- segments.map((segment, idx) => {
219
- const isActive = segment.value === activeSegment;
220
- const btnClasses = [
221
- `${baseClass}__segment`,
222
- isActive ? `${baseClass}__segment--active` : "",
223
- segment.disabled ? `${baseClass}__segment--disabled` : ""
224
- ].filter(Boolean).join(" ");
225
- return /* @__PURE__ */ jsxs(
226
- "button",
227
- {
228
- type: "button",
229
- role: "tab",
230
- className: btnClasses,
231
- "aria-selected": isActive,
232
- "aria-disabled": segment.disabled || void 0,
233
- disabled: segment.disabled,
234
- tabIndex: isActive ? 0 : -1,
235
- "data-segment-value": segment.value,
236
- onClick: () => {
237
- if (!segment.disabled) {
238
- select(segment.value);
239
- }
240
- },
241
- onKeyDown: (e) => handleKeyDown(e, idx),
242
- children: [
243
- segment.icon && collapseMode !== "labels" && /* @__PURE__ */ jsx(
244
- "span",
245
- {
246
- className: `${baseClass}__icon${typeof segment.icon === "string" ? ` material-symbols-rounded ${mirrorClass(segment.icon)}` : ""}`,
247
- "aria-hidden": "true",
248
- children: segment.icon
249
- }
250
- ),
251
- /* @__PURE__ */ jsx(
252
- "span",
253
- {
254
- className: `${baseClass}__label${collapseMode === "icons" && segment.icon ? ` ${baseClass}__label--hidden ds-visually-hidden` : ""}`,
255
- children: segment.label
256
- }
257
- )
258
- ]
259
- },
260
- segment.value
261
- );
262
- })
263
- ] });
240
+ return /* @__PURE__ */ jsxs(
241
+ "div",
242
+ {
243
+ "aria-label": ariaLabel,
244
+ ...rest,
245
+ ref: setRef,
246
+ className: classes,
247
+ role: "tablist",
248
+ onPointerOver: (event) => {
249
+ rest.onPointerOver?.(event);
250
+ showTip(event.target);
251
+ },
252
+ onFocus: (event) => {
253
+ rest.onFocus?.(event);
254
+ showTip(event.target);
255
+ },
256
+ children: [
257
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__pill`, "aria-hidden": "true", ref: pillRef }),
258
+ segments.map((segment, idx) => {
259
+ const isActive = segment.value === activeSegment;
260
+ const btnClasses = [
261
+ `${baseClass}__segment`,
262
+ isActive ? `${baseClass}__segment--active` : "",
263
+ segment.disabled ? `${baseClass}__segment--disabled` : ""
264
+ ].filter(Boolean).join(" ");
265
+ return /* @__PURE__ */ jsxs(
266
+ "button",
267
+ {
268
+ type: "button",
269
+ role: "tab",
270
+ className: btnClasses,
271
+ "aria-selected": isActive,
272
+ "aria-disabled": segment.disabled || void 0,
273
+ disabled: segment.disabled,
274
+ tabIndex: isActive ? 0 : -1,
275
+ "data-segment-value": segment.value,
276
+ onClick: () => {
277
+ if (!segment.disabled) {
278
+ select(segment.value);
279
+ }
280
+ },
281
+ onKeyDown: (e) => handleKeyDown(e, idx),
282
+ children: [
283
+ segment.icon && collapseMode !== "labels" && /* @__PURE__ */ jsx(
284
+ "span",
285
+ {
286
+ className: `${baseClass}__icon${typeof segment.icon === "string" ? ` material-symbols-rounded ${mirrorClass(segment.icon)}` : ""}`,
287
+ "aria-hidden": "true",
288
+ children: segment.icon
289
+ }
290
+ ),
291
+ /* @__PURE__ */ jsx(
292
+ "span",
293
+ {
294
+ className: `${baseClass}__label${!segment.icon ? "" : !showLabels ? ` ${baseClass}__label--tip` : collapseMode === "icons" ? ` ${baseClass}__label--hidden ds-visually-hidden` : ""}`,
295
+ children: segment.label
296
+ }
297
+ )
298
+ ]
299
+ },
300
+ segment.value
301
+ );
302
+ })
303
+ ]
304
+ }
305
+ );
264
306
  }
265
307
  );
266
308
  SegmentedControl.displayName = "SegmentedControl";
@@ -393,6 +393,12 @@
393
393
  min-width: 0;
394
394
  }
395
395
 
396
+ /* The count and Select all N never give up width: the actions' fit is
397
+ measured against what they need, and what does not fit folds into More. */
398
+ .ds-table-toolbar__bulk-start {
399
+ flex-shrink: 0;
400
+ }
401
+
396
402
  /* Actions that do not fit stay laid out out of sight, so they can be measured. */
397
403
  .ds-table-toolbar__bulk-end {
398
404
  position: relative;
@@ -408,12 +408,15 @@ const TableToolbar = React.forwardRef(
408
408
  }, [narrow, filterIds, size]);
409
409
  const bulkRef = useRef(null);
410
410
  const bulkId = useId();
411
- const [actionFit, setActionFit] = useState(Number.MAX_SAFE_INTEGER);
411
+ const [actionFit, setActionFit] = useState({
412
+ icons: false,
413
+ shown: Number.MAX_SAFE_INTEGER
414
+ });
412
415
  const bulkOn = Boolean(table?.selectable && table.selectedIds.length > 0);
413
416
  const actionIds = bulkActions.map((action) => action.id + action.label).join("|");
414
417
  useLayoutEffect(() => {
415
418
  const bar = bulkRef.current;
416
- if (!bar || narrow || !bulkOn) return;
419
+ if (!bar || !bulkOn) return;
417
420
  const measure = () => {
418
421
  const gap = parseFloat(getComputedStyle(bar).columnGap) || 0;
419
422
  const start = bar.querySelector(".ds-table-toolbar__bulk-start");
@@ -422,17 +425,25 @@ const TableToolbar = React.forwardRef(
422
425
  const endGap = parseFloat(getComputedStyle(end).columnGap) || 0;
423
426
  const room = bar.clientWidth - start.getBoundingClientRect().width - gap;
424
427
  const buttons = [...end.querySelectorAll(":scope > [data-action]")];
425
- const moreWidth = bar.getBoundingClientRect().height + endGap;
426
- let used = 0;
427
- let n = 0;
428
- for (let i = 0; i < buttons.length; i += 1) {
429
- const width = buttons[i].getBoundingClientRect().width + (i > 0 ? endGap : 0);
430
- const reserve = i < buttons.length - 1 ? moreWidth : 0;
431
- if (used + width + reserve > room) break;
432
- used += width;
433
- n += 1;
428
+ const square = bar.getBoundingClientRect().height;
429
+ const full = buttons.reduce((sum, button, i) => sum + button.getBoundingClientRect().width + (i > 0 ? endGap : 0), 0);
430
+ let next = { icons: false, shown: buttons.length };
431
+ if (narrow || full > room) {
432
+ const iconable = buttons.filter((button) => button.hasAttribute("data-iconable")).length;
433
+ let used = 0;
434
+ let n = 0;
435
+ for (let i = 0; i < iconable; i += 1) {
436
+ const width = square + (i > 0 ? endGap : 0);
437
+ const reserve = i < buttons.length - 1 ? square + endGap : 0;
438
+ if (used + width + reserve > room) break;
439
+ used += width;
440
+ n += 1;
441
+ }
442
+ next = { icons: true, shown: n };
434
443
  }
435
- setActionFit((previous) => previous === n ? previous : n);
444
+ setActionFit(
445
+ (previous) => previous.icons === next.icons && previous.shown === next.shown ? previous : next
446
+ );
436
447
  };
437
448
  measure();
438
449
  const observer = new ResizeObserver(measure);
@@ -460,14 +471,19 @@ const TableToolbar = React.forwardRef(
460
471
  ...bulkActions.filter((action) => !action.destructive),
461
472
  ...bulkActions.filter((action) => action.destructive)
462
473
  ];
463
- const shownActions = narrow ? 0 : Math.min(actionFit, orderedActions.length);
464
- const folded = orderedActions.slice(shownActions);
474
+ const iconFirst = [
475
+ ...orderedActions.filter((action) => action.icon != null),
476
+ ...orderedActions.filter((action) => action.icon == null)
477
+ ];
478
+ const shownActions = Math.min(actionFit.shown, orderedActions.length);
479
+ const folded = (actionFit.icons ? iconFirst : orderedActions).slice(shownActions);
465
480
  const bulkButton = (action, index) => {
466
- const out = index >= shownActions;
481
+ const out = actionFit.icons || index >= shownActions;
467
482
  return /* @__PURE__ */ jsx(
468
483
  Button,
469
484
  {
470
485
  "data-action": "",
486
+ "data-iconable": action.icon != null || void 0,
471
487
  className: out ? "ds-table-toolbar__chip--overflow" : void 0,
472
488
  "aria-hidden": out || void 0,
473
489
  inert: out || void 0,
@@ -523,7 +539,21 @@ const TableToolbar = React.forwardRef(
523
539
  )
524
540
  ] }),
525
541
  /* @__PURE__ */ jsxs("div", { className: "ds-table-toolbar__bulk-end", children: [
526
- !narrow && orderedActions.map(bulkButton),
542
+ orderedActions.map(bulkButton),
543
+ actionFit.icons && iconFirst.slice(0, shownActions).map((action) => /* @__PURE__ */ jsx(
544
+ CircularButton,
545
+ {
546
+ icon: action.icon,
547
+ variant: "secondary",
548
+ tone: action.destructive ? "destructive" : "neutral",
549
+ size,
550
+ ariaLabel: action.label,
551
+ tooltip: action.label,
552
+ disabled: action.disabled,
553
+ onClick: () => action.onSelect(selection)
554
+ },
555
+ action.id
556
+ )),
527
557
  folded.length > 0 && /* @__PURE__ */ jsx(
528
558
  DropdownMenu,
529
559
  {
@@ -902,6 +932,12 @@ const TableToolbar = React.forwardRef(
902
932
  );
903
933
  const active = views.find((v) => v.id === activeViewId) ?? views[0];
904
934
  const dirty = Boolean(table && active && !table.viewMatches(active.view));
935
+ const appliedStart = useRef(false);
936
+ useEffect(() => {
937
+ if (appliedStart.current || !table) return;
938
+ appliedStart.current = true;
939
+ if (active && !table.viewMatches(active.view)) table.applyView(active.view);
940
+ }, [table]);
905
941
  const [renaming, setRenaming] = useState(null);
906
942
  const commitRename = () => {
907
943
  const name = renaming?.trim();
@@ -3,80 +3,10 @@
3
3
  The colour-mode control. It is a
4
4
  SegmentedControl, which draws every pixel;
5
5
  this root keeps the strip at its own width
6
- in a squeezed row, and dresses the icon-only
7
- form.
6
+ in a squeezed row. The icon-only form, its
7
+ tooltips included, is SegmentedControl's.
8
8
  ============================================ */
9
9
 
10
10
  .ds-theme-toggle {
11
11
  flex-shrink: 0;
12
12
  }
13
-
14
- /* ============================================
15
- ICON STRIP (showLabels={false})
16
- The glyphs alone, with each label turned
17
- into the segment's tooltip: it appears below
18
- the segment on hover and keyboard focus, on
19
- Tooltip's surface, so "routine" is never
20
- left to be guessed as System. The component
21
- places the label with the shared
22
- anchored-position behaviour, so a seat that
23
- clips cannot cut it off, and it flips above
24
- the strip when there is no room below. The label
25
- stays in the accessibility tree as the
26
- segment's name. This is the documentation
27
- site's header toggle, moved into the
28
- package so a consumer's strip behaves the
29
- same.
30
- ============================================ */
31
-
32
- .ds-theme-toggle--icons .ds-segmented-control__label {
33
- /* Out of flow at rest; the behaviour makes it fixed once it shows. */
34
- position: absolute;
35
- z-index: var(--layer-floating);
36
- padding: var(--padding-150) var(--padding-200);
37
- border-radius: var(--radius-100);
38
- border: var(--border-025) solid var(--color-bg-container-border);
39
- background-color: var(--color-bg-page-primary);
40
- color: var(--color-text-primary);
41
- box-shadow: var(--shadow-floating);
42
- font-family: var(--font-paragraph-sm-family);
43
- font-size: var(--font-paragraph-sm-size);
44
- font-weight: var(--font-paragraph-sm-weight);
45
- line-height: var(--font-paragraph-sm-line-height);
46
- letter-spacing: var(--font-paragraph-sm-letter-spacing);
47
- white-space: nowrap;
48
- pointer-events: none;
49
- opacity: 0;
50
- transform: translateY(calc(-1 * var(--gap-100)));
51
- transition:
52
- opacity var(--motion-duration-fast) var(--motion-ease-standard),
53
- transform var(--motion-duration-fast) var(--motion-ease-standard);
54
- }
55
-
56
- /* --motion-duration-slow matches MOTION_HOVER_SHOW_DELAY_MS, the hover-intent
57
- delay Tooltip uses in JS, so the label does not flash while the pointer
58
- passes through. Hiding takes no delay, like a tooltip leaving. */
59
- .ds-theme-toggle--icons .ds-segmented-control__segment:hover .ds-segmented-control__label,
60
- .ds-theme-toggle--icons .ds-segmented-control__segment:focus-visible .ds-segmented-control__label {
61
- opacity: 1;
62
- transform: none;
63
- transition-delay: var(--motion-duration-slow);
64
- }
65
-
66
- /* Flipped above the strip, it slides down into place instead. */
67
- .ds-theme-toggle--icons .ds-segmented-control__label[data-side='top'] {
68
- transform: translateY(var(--gap-100));
69
- }
70
-
71
- /* Without an inline label a segment's asymmetric padding reads lopsided:
72
- square each one into an even pill around its glyph. */
73
- .ds-theme-toggle--icons .ds-segmented-control__segment {
74
- padding-inline: var(--padding-200);
75
- }
76
-
77
- /* ds-allow(motion): drops the label tooltip's slide outright rather than playing it at 0.01ms. */
78
- @media (prefers-reduced-motion: reduce) {
79
- .ds-theme-toggle--icons .ds-segmented-control__label {
80
- transition: none;
81
- }
82
- }
@@ -13,7 +13,7 @@ type ThemeToggleOwnProps = {
13
13
  defaultValue?: ColorModeSetting;
14
14
  /** Fires with the setting after every pick, controlled or not. */
15
15
  onValueChange?: (value: ColorModeSetting) => void;
16
- /** Control size, on SegmentedControl's scale. */
16
+ /** Control size, on SegmentedControl's scale: `default` (the default), the height of the CommandPaletteTrigger and AiButton beside it in a top bar, or `compact`. */
17
17
  size?: 'default' | 'compact';
18
18
  /** Show each segment's label beside its icon. Set `false` for an icon-only strip: each label becomes the segment's tooltip on hover and keyboard focus, and stays its accessible name. */
19
19
  showLabels?: boolean;
@@ -1,8 +1,7 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
- import React, { useEffect, useRef, useState } from "react";
3
+ import React, { useEffect } from "react";
4
4
  import { SegmentedControl } from "../SegmentedControl/SegmentedControl.js";
5
- import { useAnchoredPosition } from "../../behaviors/useAnchoredPosition.js";
6
5
  import { DEFAULT_COLOR_MODE_STORAGE_KEY } from "./colorMode.js";
7
6
  import { useColorMode } from "./useColorMode.js";
8
7
  import "./ThemeToggle.css";
@@ -11,7 +10,7 @@ const ThemeToggle = React.forwardRef(
11
10
  value,
12
11
  defaultValue,
13
12
  onValueChange,
14
- size = "compact",
13
+ size = "default",
15
14
  showLabels = true,
16
15
  storageKey,
17
16
  systemLabel = "System",
@@ -32,33 +31,6 @@ const ThemeToggle = React.forwardRef(
32
31
  }
33
32
  if (stored === null) setSetting(defaultValue);
34
33
  }, []);
35
- const stripRef = useRef(null);
36
- const segmentRef = useRef(null);
37
- const labelRef = useRef(null);
38
- const [tip, setTip] = useState(null);
39
- const setStripRef = (node) => {
40
- stripRef.current = node;
41
- if (typeof ref === "function") ref(node);
42
- else if (ref) ref.current = node;
43
- };
44
- const showTip = (target) => {
45
- if (showLabels) return;
46
- const segment = target?.closest(".ds-segmented-control__segment");
47
- const strip = stripRef.current;
48
- if (!segment || !strip) return;
49
- segmentRef.current = segment;
50
- labelRef.current = segment.querySelector(".ds-segmented-control__label");
51
- setTip([...strip.querySelectorAll(".ds-segmented-control__segment")].indexOf(segment));
52
- };
53
- useAnchoredPosition({
54
- open: tip !== null,
55
- target: tip,
56
- anchorRef: segmentRef,
57
- panelRef: labelRef,
58
- side: "bottom",
59
- align: "center",
60
- gapProperty: "--gap-100"
61
- });
62
34
  const segments = [
63
35
  { value: "system", label: systemLabel, icon: "routine" },
64
36
  { value: "light", label: lightLabel, icon: "light_mode" },
@@ -69,15 +41,7 @@ const ThemeToggle = React.forwardRef(
69
41
  {
70
42
  "aria-label": ariaLabel,
71
43
  ...rest,
72
- ref: setStripRef,
73
- onPointerOver: (event) => {
74
- rest.onPointerOver?.(event);
75
- showTip(event.target);
76
- },
77
- onFocus: (event) => {
78
- rest.onFocus?.(event);
79
- showTip(event.target);
80
- },
44
+ ref,
81
45
  className: ["ds-theme-toggle", !showLabels && "ds-theme-toggle--icons", className].filter(Boolean).join(" "),
82
46
  segments,
83
47
  value: setting,
@@ -87,7 +51,7 @@ const ThemeToggle = React.forwardRef(
87
51
  onValueChange?.(next);
88
52
  },
89
53
  size,
90
- showLabels: true
54
+ showLabels
91
55
  }
92
56
  );
93
57
  }
@@ -139,7 +139,7 @@
139
139
  "name": "AppLayout",
140
140
  "label": "App layout",
141
141
  "slug": "app-layout",
142
- "description": "Full-page template pairing the collapsible App sidebar with a centred content area.",
142
+ "description": "Full-page template pairing the collapsible App sidebar with a content area, centred or set in an inset surface.",
143
143
  "category": "layout",
144
144
  "client": true
145
145
  },
@@ -139,7 +139,7 @@ declare const _default: {
139
139
  "name": "AppLayout",
140
140
  "label": "App layout",
141
141
  "slug": "app-layout",
142
- "description": "Full-page template pairing the collapsible App sidebar with a centred content area.",
142
+ "description": "Full-page template pairing the collapsible App sidebar with a content area, centred or set in an inset surface.",
143
143
  "category": "layout",
144
144
  "client": true
145
145
  },