@ciridae/ui 1.0.4 → 1.0.9

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 (147) hide show
  1. package/README.md +3 -2
  2. package/dist/chat/conversation-source.d.ts +1 -3
  3. package/dist/chat/message-text.js +6 -6
  4. package/dist/chat/use-chat-conversation.d.ts +7 -2
  5. package/dist/chat/use-chat-conversation.js +2 -1
  6. package/dist/components/activity-feed.d.ts +0 -1
  7. package/dist/components/activity-feed.js +6 -7
  8. package/dist/components/agent-work.d.ts +5 -1
  9. package/dist/components/agent-work.js +27 -13
  10. package/dist/components/app-frame.d.ts +3 -17
  11. package/dist/components/app-frame.js +11 -30
  12. package/dist/components/app-navigation.d.ts +5 -6
  13. package/dist/components/app-navigation.js +70 -40
  14. package/dist/components/badge.d.ts +0 -4
  15. package/dist/components/badge.js +17 -27
  16. package/dist/components/bulk-action-bar.js +2 -2
  17. package/dist/components/button.js +41 -50
  18. package/dist/components/calendar.js +5 -8
  19. package/dist/components/centered-page.js +4 -4
  20. package/dist/components/chart.js +3 -4
  21. package/dist/components/chat-client-source.js +55 -34
  22. package/dist/components/chat-page.js +1 -1
  23. package/dist/components/chat-sheet.d.ts +30 -20
  24. package/dist/components/chat-sheet.js +112 -54
  25. package/dist/components/chat.d.ts +27 -56
  26. package/dist/components/chat.js +153 -132
  27. package/dist/components/checkbox-item.js +4 -4
  28. package/dist/components/checkbox-list.js +6 -6
  29. package/dist/components/chip.js +2 -2
  30. package/dist/components/collapsible.js +3 -3
  31. package/dist/components/color-theme.d.ts +3 -6
  32. package/dist/components/color-theme.js +10 -11
  33. package/dist/components/column-settings.js +3 -3
  34. package/dist/components/command-menu.d.ts +1 -2
  35. package/dist/components/command-menu.js +56 -77
  36. package/dist/components/command-palette.js +5 -2
  37. package/dist/components/copyable-id.js +5 -5
  38. package/dist/components/data-display.d.ts +1 -7
  39. package/dist/components/data-display.js +32 -55
  40. package/dist/components/data-grid.d.ts +4 -7
  41. package/dist/components/data-grid.js +106 -90
  42. package/dist/components/date-range-picker.js +4 -5
  43. package/dist/components/document-viewer.js +23 -23
  44. package/dist/components/drawer.d.ts +2 -1
  45. package/dist/components/drawer.js +45 -20
  46. package/dist/components/dropzone.js +13 -13
  47. package/dist/components/editable-field.d.ts +2 -7
  48. package/dist/components/editable-field.js +14 -27
  49. package/dist/components/email-input.js +2 -3
  50. package/dist/components/environment-frame.js +7 -7
  51. package/dist/components/expandable-text.js +2 -2
  52. package/dist/components/feedback-screenshot.js +7 -7
  53. package/dist/components/feedback-sheet.js +8 -8
  54. package/dist/components/filter-bar.js +41 -27
  55. package/dist/components/form-controls.d.ts +3 -6
  56. package/dist/components/form-controls.js +15 -22
  57. package/dist/components/form-layout.d.ts +6 -6
  58. package/dist/components/form-layout.js +10 -16
  59. package/dist/components/glass-menu.d.ts +2 -0
  60. package/dist/components/glass-menu.js +14 -18
  61. package/dist/components/gradient-edge.d.ts +0 -1
  62. package/dist/components/gradient-edge.js +8 -9
  63. package/dist/components/icon.js +1 -1
  64. package/dist/components/input.js +18 -17
  65. package/dist/components/keycap.d.ts +2 -6
  66. package/dist/components/keycap.js +4 -12
  67. package/dist/components/layout.js +11 -10
  68. package/dist/components/lightbox.js +8 -8
  69. package/dist/components/link.js +1 -1
  70. package/dist/components/locale.d.ts +23 -7
  71. package/dist/components/locale.js +19 -7
  72. package/dist/components/log-entry.d.ts +2 -14
  73. package/dist/components/log-entry.js +11 -22
  74. package/dist/components/log-list.js +1 -1
  75. package/dist/components/mention-input.js +13 -14
  76. package/dist/components/menu.d.ts +7 -4
  77. package/dist/components/menu.js +23 -10
  78. package/dist/components/meter.d.ts +2 -3
  79. package/dist/components/meter.js +12 -16
  80. package/dist/components/navigation.d.ts +0 -7
  81. package/dist/components/navigation.js +3 -12
  82. package/dist/components/notification-tray.js +7 -7
  83. package/dist/components/overlays.d.ts +5 -2
  84. package/dist/components/overlays.js +24 -24
  85. package/dist/components/panel.js +3 -3
  86. package/dist/components/progress.js +2 -2
  87. package/dist/components/radio-card.d.ts +1 -1
  88. package/dist/components/radio-card.js +9 -9
  89. package/dist/components/record.d.ts +2 -4
  90. package/dist/components/record.js +19 -32
  91. package/dist/components/scroll-area.js +10 -10
  92. package/dist/components/segmented-bar.js +9 -9
  93. package/dist/components/select.d.ts +3 -5
  94. package/dist/components/select.js +26 -44
  95. package/dist/components/settings.js +1 -1
  96. package/dist/components/shell-skeleton.js +1 -1
  97. package/dist/components/shell.d.ts +1 -1
  98. package/dist/components/shell.js +69 -24
  99. package/dist/components/side-panel.js +3 -3
  100. package/dist/components/sidebar.d.ts +0 -5
  101. package/dist/components/sidebar.js +25 -33
  102. package/dist/components/single-line-text.js +3 -4
  103. package/dist/components/site-banner.js +4 -4
  104. package/dist/components/skeleton.js +19 -22
  105. package/dist/components/skeletons.d.ts +0 -3
  106. package/dist/components/skeletons.js +5 -10
  107. package/dist/components/stages.d.ts +6 -3
  108. package/dist/components/stages.js +77 -105
  109. package/dist/components/stat-card.d.ts +2 -5
  110. package/dist/components/stat-card.js +23 -31
  111. package/dist/components/status-banner.js +6 -6
  112. package/dist/components/status.js +3 -3
  113. package/dist/components/steps.js +2 -2
  114. package/dist/components/switch.js +1 -1
  115. package/dist/components/tabs.js +15 -18
  116. package/dist/components/theme-customizer.d.ts +5 -8
  117. package/dist/components/theme-customizer.js +9 -41
  118. package/dist/components/theme-toggle.d.ts +0 -1
  119. package/dist/components/theme-toggle.js +6 -11
  120. package/dist/components/toast.d.ts +1 -7
  121. package/dist/components/toast.js +19 -22
  122. package/dist/components/user-chip.js +8 -8
  123. package/dist/components/wizard.js +6 -4
  124. package/dist/preset.d.ts +1 -2
  125. package/dist/preset.js +41 -89
  126. package/dist/styled-system/css/conditions.mjs +1 -1
  127. package/dist/styled-system/css/css.mjs +1 -1
  128. package/dist/styled-system/styles.css +820 -1297
  129. package/dist/styled-system/tokens/index.mjs +78 -1926
  130. package/dist/styled-system/tokens/tokens.d.ts +18 -33
  131. package/dist/styled-system/types/conditions.d.ts +0 -74
  132. package/dist/styled-system/types/prop-type.d.ts +15 -40
  133. package/dist/styled-system/types/style-props.d.ts +705 -705
  134. package/dist/theme/color.d.ts +0 -1
  135. package/dist/theme/color.js +0 -16
  136. package/dist/theme/derive.d.ts +3 -3
  137. package/dist/theme/derive.js +9 -32
  138. package/dist/theme/index.d.ts +1 -2
  139. package/dist/theme/index.js +3 -5
  140. package/dist/theme/options.d.ts +2 -4
  141. package/dist/theme/options.js +0 -1
  142. package/dist/theme/{palettes.d.ts → palette.d.ts} +12 -23
  143. package/dist/theme/palette.js +127 -0
  144. package/dist/theme/tokens.d.ts +8 -13
  145. package/dist/theme/tokens.js +39 -92
  146. package/package.json +1 -1
  147. package/dist/theme/palettes.js +0 -254
@@ -25,7 +25,7 @@ const row = css({
25
25
  transitionDuration: "fast",
26
26
  // The selected tint in dark and the row wash in light, as a menu row hovers; focus rings the row
27
27
  // inside its own edge.
28
- _hover: { background: "token(gradients.selected)", _light: { background: "bg.rowHover" } },
28
+ _hover: { background: "[token(gradients.selected)]", _light: { background: "bg.rowHover" } },
29
29
  "&:has(:focus-visible)": { focusRing: "inset" },
30
30
  "&:has([data-disabled])": { opacity: "disabled", cursor: "not-allowed" },
31
31
  });
@@ -35,10 +35,10 @@ const grip = css({
35
35
  flexShrink: "0",
36
36
  color: "fg.muted",
37
37
  cursor: "grab",
38
- opacity: "0",
39
- transitionProperty: "opacity",
38
+ opacity: "[0]",
39
+ transitionProperty: "[opacity]",
40
40
  transitionDuration: "fast",
41
- "label:hover > &, label:focus-within > &": { opacity: "1" },
41
+ "label:hover > &, label:focus-within > &": { opacity: "[1]" },
42
42
  // A locked row keeps the grip's room, so its label lines up with the rows that move.
43
43
  "&[data-locked]": { visibility: "hidden" },
44
44
  });
@@ -17,7 +17,7 @@ const list = css({
17
17
  gridTemplateColumns: "minmax(0, 1fr)",
18
18
  gap: "layout.tightGap",
19
19
  paddingBottom: "layout.controlGap",
20
- paddingInlineEnd: "calc(token(sizes.scroll.bar) + token(spacing.layout.tightGap))",
20
+ paddingInlineEnd: "[calc(token(sizes.scroll.bar) + token(spacing.layout.tightGap))]",
21
21
  });
22
22
  const itemStack = css({
23
23
  display: "grid",
@@ -31,7 +31,7 @@ const itemRowEnd = css({ paddingInlineEnd: "panel.compact" });
31
31
  const flushRow = css({ paddingInlineStart: "panel.compact" });
32
32
  // Grouped rows sit under their group's label, so their boxes line up with its text.
33
33
  const groupedRow = css({
34
- paddingInlineStart: "calc(token(spacing.panel.compact) + token(sizes.control.checkbox) + token(spacing.layout.controlGap))",
34
+ paddingInlineStart: "[calc(token(spacing.panel.compact) + token(sizes.control.checkbox) + token(spacing.layout.controlGap))]",
35
35
  });
36
36
  const groupBlock = css({
37
37
  display: "grid",
@@ -55,7 +55,7 @@ const groupLabel = css({ flex: "1", minWidth: "0", fontSize: "caption", fontWeig
55
55
  const label = css({ flex: "1", minWidth: "0", fontSize: "caption" });
56
56
  // A description lines up with its label's text, past the checkbox.
57
57
  const description = css({
58
- paddingInlineStart: "calc(token(sizes.control.checkbox) + token(spacing.layout.controlGap))",
58
+ paddingInlineStart: "[calc(token(sizes.control.checkbox) + token(spacing.layout.controlGap))]",
59
59
  minWidth: "0",
60
60
  });
61
61
  const box = css({ flexShrink: "0" });
@@ -129,7 +129,7 @@ function SearchableCheckboxes({ items, emptyMessage, selectedKeys, onChange, dis
129
129
  }
130
130
  onChange(next);
131
131
  };
132
- const listed = shown.length === 0 ? (_jsx("div", { className: cx(list, muted), children: searching ? labels.noMatches(search.trim()) : emptyMessage })) : (_jsx(ListedCheckboxes, { items: shown, selectedKeys: selectedKeys, onChange: onChange, disabled: disabled }));
132
+ const listed = shown.length === 0 ? (_jsx("div", { className: cx(list, muted), children: searching ? labels.noMatches(search.trim()) : emptyMessage })) : (_jsx(ListedCheckboxes, { items: shown, selectedKeys: selectedKeys, onChange: onChange, disabled: disabled, labels: labels }));
133
133
  if (items.length === 0 || (!offerSearch && !offerBulkActions))
134
134
  return listed;
135
135
  return (_jsxs("div", { className: withToolbar, "data-ciridae-ui": "", children: [_jsxs(Toolbar, { children: [offerSearch && (_jsx(SearchInput, { "aria-label": labels.search, placeholder: labels.search, value: search, onChange: (event) => setSearch(event.target.value),
@@ -139,7 +139,7 @@ function SearchableCheckboxes({ items, emptyMessage, selectedKeys, onChange, dis
139
139
  event.preventDefault();
140
140
  } })), offerBulkActions && (_jsxs("div", { className: toolbarActions, children: [_jsxs("div", { className: bulkActions, role: "group", "aria-label": labels.bulkActions, children: [_jsx(Button, { variant: "secondary", size: "small", focusableWhenDisabled: true, disabled: disabled || shownKeys.every((key) => selectedKeys.has(key)), onClick: () => setShown(true), children: searching ? labels.selectMatches : labels.selectAll }), _jsx(Button, { variant: "secondary", size: "small", focusableWhenDisabled: true, disabled: disabled || !shownKeys.some((key) => selectedKeys.has(key)), onClick: () => setShown(false), children: searching ? labels.clearMatches : labels.clear })] }), _jsx("span", { className: selectedCount, "aria-live": "polite", children: labels.selectedCount(items.filter((item) => selectedKeys.has(item.key)).length, items.length) })] }))] }), listed] }));
141
141
  }
142
- function ListedCheckboxes({ items, selectedKeys, onChange, disabled, }) {
142
+ function ListedCheckboxes({ items, selectedKeys, onChange, disabled, labels, }) {
143
143
  const toggle = (keys, checked) => {
144
144
  const next = new Set(selectedKeys);
145
145
  for (const key of keys) {
@@ -162,6 +162,6 @@ function ListedCheckboxes({ items, selectedKeys, onChange, disabled, }) {
162
162
  return (_jsx(ScrollArea, { viewportClassName: listViewport, children: _jsxs("div", { className: list, "data-ciridae-ui": "", children: [items.filter((item) => item.group === undefined).map((item) => itemRow(item, "flush")), [...groups].map(([groupKey, { label: group, members }]) => {
163
163
  const keys = members.map((item) => item.key);
164
164
  const selectedCount = keys.filter((key) => selectedKeys.has(key)).length;
165
- return (_jsxs("div", { role: "group", "aria-label": group, className: groupBlock, children: [_jsxs("div", { className: groupHeading, children: [_jsx(Checkbox, { disabled: disabled, wrapperClassName: groupLabel, className: box, checked: selectedCount === keys.length, indeterminate: selectedCount > 0 && selectedCount < keys.length, onCheckedChange: (checked) => toggle(keys, checked), children: group }), _jsx("span", { className: muted, children: `${selectedCount}/${keys.length}` })] }), members.map((item) => itemRow(item, "under-group"))] }, groupKey));
165
+ return (_jsxs("div", { role: "group", "aria-label": group, className: groupBlock, children: [_jsxs("div", { className: groupHeading, children: [_jsx(Checkbox, { disabled: disabled, wrapperClassName: groupLabel, className: box, checked: selectedCount === keys.length, indeterminate: selectedCount > 0 && selectedCount < keys.length, onCheckedChange: (checked) => toggle(keys, checked), children: group }), _jsx("span", { className: muted, children: labels.groupSelectedCount(selectedCount, keys.length) })] }), members.map((item) => itemRow(item, "under-group"))] }, groupKey));
166
166
  })] }) }));
167
167
  }
@@ -7,7 +7,7 @@ import { icon as iconStyle } from "./icon.js";
7
7
  import { useCiridaeLocale } from "./locale.js";
8
8
  const chip = css({
9
9
  display: "inline-flex",
10
- maxWidth: "100%",
10
+ maxWidth: "full",
11
11
  overflowWrap: "anywhere",
12
12
  alignItems: "center",
13
13
  gap: "control.iconGap",
@@ -47,7 +47,7 @@ const tag = css({
47
47
  paddingInlineEnd: "layout.controlGap",
48
48
  border: "1px solid transparent",
49
49
  borderRadius: "round",
50
- background: "token(gradients.raised)",
50
+ background: "[token(gradients.raised)]",
51
51
  color: "fg.label",
52
52
  fontFamily: "sans",
53
53
  fontSize: "code",
@@ -10,7 +10,7 @@ const trigger = css({
10
10
  gap: "control.iconGap",
11
11
  padding: "none",
12
12
  border: "none",
13
- background: "none",
13
+ background: "[none]",
14
14
  color: "fg.primary",
15
15
  font: "inherit",
16
16
  textAlign: "start",
@@ -21,7 +21,7 @@ const trigger = css({
21
21
  const caret = css({
22
22
  flexShrink: "0",
23
23
  color: "fg.muted",
24
- transitionProperty: "transform",
24
+ transitionProperty: "[transform]",
25
25
  transitionDuration: "fast",
26
26
  "[data-panel-open] > &": { transform: "rotate(180deg)" },
27
27
  });
@@ -32,7 +32,7 @@ const panel = css({
32
32
  overflow: "hidden",
33
33
  // Clips for the height animation, with room for the focus rings of the fields inside.
34
34
  ringRoom: true,
35
- transitionProperty: "height",
35
+ transitionProperty: "[height]",
36
36
  transitionDuration: "fast",
37
37
  "&[data-starting-style], &[data-ending-style]": { height: "0" },
38
38
  });
@@ -1,6 +1,5 @@
1
1
  import { type ReactNode } from "react";
2
2
  import type { Brand, CiridaeTheme, ColorTheme } from "../theme/options.js";
3
- import type { PaletteName } from "../theme/palettes.js";
4
3
  export type { Brand, ColorTheme } from "../theme/options.js";
5
4
  /** What the reader chose: a theme, or to follow the operating system's. */
6
5
  export type ColorThemePreference = ColorTheme | "system";
@@ -20,11 +19,9 @@ export type ThemeProviderProps = {
20
19
  /** What the product calls itself, for `useBrand`. */
21
20
  brand?: Brand;
22
21
  /**
23
- * A palette to show over the one the stylesheet was built with, switchable at runtime, such as
24
- * a customer's choice or a preview. Leave it and `fonts` out to keep the built tokens.
22
+ * Faces to show over the ones the stylesheet was built with, switchable at runtime, such as a
23
+ * preview. Leave it out to keep the built faces.
25
24
  */
26
- palette?: PaletteName;
27
- /** Faces to show over the built ones, at runtime, as `palette` does for colors. */
28
25
  fonts?: CiridaeTheme["fonts"];
29
26
  };
30
27
  /**
@@ -33,7 +30,7 @@ export type ThemeProviderProps = {
33
30
  * `_light` values resolve through, so every component follows without
34
31
  * threading a prop.
35
32
  */
36
- export declare function ThemeProvider({ children, defaultColorTheme, storageKey, brand, palette, fonts, }: ThemeProviderProps): import("react").JSX.Element;
33
+ export declare function ThemeProvider({ children, defaultColorTheme, storageKey, brand, fonts, }: ThemeProviderProps): import("react").JSX.Element;
37
34
  /** Throws outside a `ThemeProvider`, where there is no theme to report or change. */
38
35
  export declare function useColorTheme(): ColorThemeContextValue;
39
36
  /** Throws outside a `ThemeProvider` given a `brand`, where there is no name to show. */
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react";
3
- import { themeStylesheet } from "../theme/tokens.js";
3
+ import { fontStylesheet } from "../theme/tokens.js";
4
4
  /** The attribute the `light` Panda condition and `ciridae.css` both select on. */
5
5
  const THEME_ATTRIBUTE = "data-ciridae-theme";
6
6
  const DEFAULT_STORAGE_KEY = "ciridae-ui-color-theme";
@@ -33,23 +33,22 @@ function readSystemTheme() {
33
33
  }
34
34
  const ColorThemeContext = createContext(null);
35
35
  const BrandContext = createContext(null);
36
- const RUNTIME_THEME_ELEMENT_ID = "ciridae-runtime-theme";
37
- /** Writes the palette's and fonts' token variables onto the document while either is set. */
38
- function useRuntimeTheme(palette, fonts) {
36
+ const RUNTIME_FONTS_ELEMENT_ID = "ciridae-runtime-fonts";
37
+ function useRuntimeFonts(fonts) {
39
38
  const display = fonts?.display;
40
39
  const sans = fonts?.sans;
41
40
  const mono = fonts?.mono;
42
41
  useEffect(() => {
43
42
  const document = globalThis.document;
44
- if (!document || (palette === undefined && !display && !sans && !mono))
43
+ if (!document || (!display && !sans && !mono))
45
44
  return;
46
- const existing = document.getElementById(RUNTIME_THEME_ELEMENT_ID);
45
+ const existing = document.getElementById(RUNTIME_FONTS_ELEMENT_ID);
47
46
  const style = existing instanceof HTMLStyleElement ? existing : document.createElement("style");
48
- style.id = RUNTIME_THEME_ELEMENT_ID;
49
- style.textContent = themeStylesheet({ palette, fonts: { display, sans, mono } });
47
+ style.id = RUNTIME_FONTS_ELEMENT_ID;
48
+ style.textContent = fontStylesheet({ display, sans, mono });
50
49
  document.head.append(style);
51
50
  return () => style.remove();
52
- }, [palette, display, sans, mono]);
51
+ }, [display, sans, mono]);
53
52
  }
54
53
  /**
55
54
  * Owns the document's color theme. The chosen theme is written to
@@ -57,8 +56,8 @@ function useRuntimeTheme(palette, fonts) {
57
56
  * `_light` values resolve through, so every component follows without
58
57
  * threading a prop.
59
58
  */
60
- export function ThemeProvider({ children, defaultColorTheme = "dark", storageKey = DEFAULT_STORAGE_KEY, brand, palette, fonts, }) {
61
- useRuntimeTheme(palette, fonts);
59
+ export function ThemeProvider({ children, defaultColorTheme = "dark", storageKey = DEFAULT_STORAGE_KEY, brand, fonts, }) {
60
+ useRuntimeFonts(fonts);
62
61
  const [preference, setPreferenceState] = useState(() => readStoredPreference(storageKey) ?? defaultColorTheme);
63
62
  const [systemTheme, setSystemTheme] = useState(readSystemTheme);
64
63
  // Only a `system` preference follows the operating system as it changes.
@@ -16,7 +16,7 @@ import { SegmentedControl, SegmentedOption } from "./form-layout.js";
16
16
  import { icon } from "./icon.js";
17
17
  import { useCiridaeLocale } from "./locale.js";
18
18
  import { Popover } from "./overlays.js";
19
- const panel = css({ display: "grid", width: "260px", maxWidth: "100%" });
19
+ const panel = css({ display: "grid", width: "[260px]", maxWidth: "full" });
20
20
  const heading = css({
21
21
  display: "flex",
22
22
  alignItems: "center",
@@ -39,7 +39,7 @@ const dropSlot = css({
39
39
  paddingInline: "layout.tightGap",
40
40
  "& > span": {
41
41
  display: "block",
42
- height: "100%",
42
+ height: "full",
43
43
  border: "1px solid token(colors.border.focus)",
44
44
  borderRadius: "keycap",
45
45
  },
@@ -52,7 +52,7 @@ const hint = css({
52
52
  fontSize: "code",
53
53
  letterSpacing: "compact",
54
54
  });
55
- const densityControl = css({ display: "flex!", width: "100%", "& > *": { flex: "1" } });
55
+ const densityControl = css({ display: "flex!", width: "full", "& > *": { flex: "1" } });
56
56
  export function ColumnSettings({ columns, columnVisibility, onColumnVisibilityChange, columnOrder, onColumnOrderChange, onReset, density, onDensityChange, trigger, open, onOpenChange, labels: ownLabels, }) {
57
57
  const { labels: localeLabels } = useCiridaeLocale();
58
58
  const labels = { ...localeLabels.table, ...ownLabels };
@@ -6,7 +6,6 @@ export type CommandMenuItem = {
6
6
  icon?: ReactNode;
7
7
  /** Muted text after the label, such as a record's title. */
8
8
  detail?: string;
9
- /** What the row is, at its end, such as "Invoice" or "Job". */
10
9
  kind?: string;
11
10
  /** The hotkey that runs it while the menu is open, such as "Mod+J", shown at the row's end. */
12
11
  shortcut?: HotkeyChord;
@@ -41,7 +40,7 @@ export type CommandMenuProps = {
41
40
  placeholder?: string;
42
41
  /** Shown when no row matches, above any `unfiltered` row; the locale's "No results" by default. */
43
42
  empty?: ReactNode;
44
- /** What the menu searches, at the end of its footer, such as "Records, people and actions". */
43
+ /** Drawn under the footer's key hints, such as what the search covers. */
45
44
  searchScope?: ReactNode;
46
45
  };
47
46
  export declare function CommandMenu({ open, onOpenChange, query, onQueryChange, groups, title: ownTitle, placeholder: ownPlaceholder, empty: ownEmpty, searchScope, }: CommandMenuProps): import("react").JSX.Element;
@@ -2,7 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  /*
3
3
  * A keyboard-first command palette: a search field over grouped rows in a dialog near the top of
4
4
  * the window. Arrows move, Enter runs the highlighted row, Escape closes. `cmdk` owns the list's
5
- * keyboard and selection; the menu matches rows itself, so their order stays the caller's.
5
+ * keyboard; the menu matches rows itself, so their order stays the caller's, and holds which row
6
+ * is highlighted, so the field names it to a screen reader from the first result on.
6
7
  */
7
8
  import { Dialog as BaseDialog } from "@base-ui/react/dialog";
8
9
  import { useRender } from "@base-ui/react/use-render";
@@ -21,34 +22,34 @@ const backdrop = css({
21
22
  inset: "none",
22
23
  zIndex: "dialog",
23
24
  background: "bg.backdrop",
24
- transitionProperty: "opacity",
25
+ transitionProperty: "[opacity]",
25
26
  transitionDuration: "fast",
26
- "&[data-starting-style], &[data-ending-style]": { opacity: "0" },
27
- _motionReduce: { transition: "none" },
27
+ "&[data-starting-style], &[data-ending-style]": { opacity: "[0]" },
28
+ _motionReduce: { transition: "[none]" },
28
29
  });
29
30
  // The palette is a sheet in a glass shell, as a modal is: the shell blurs the page in dark, the
30
31
  // sheet blurs it in light.
31
32
  const popup = css({
32
33
  zIndex: "dialog",
33
34
  position: "fixed",
34
- top: "18dvh",
35
- left: "50%",
35
+ top: "[18dvh]",
36
+ left: "[50%]",
36
37
  display: "flex",
37
- width: "calc(100vw - token(spacing.layout.pageGap) * 2)",
38
- maxWidth: "calc(token(sizes.overlay.palette) + token(spacing.menu.inset) * 2 + 2px)",
38
+ width: "[calc(100vw - token(spacing.layout.pageGap) * 2)]",
39
+ maxWidth: "[calc(token(sizes.overlay.palette) + token(spacing.menu.inset) * 2 + 2px)]",
39
40
  padding: "menu.inset",
40
41
  border: "1px solid token(colors.border.divider)",
41
42
  borderRadius: "shell",
42
43
  background: "bg.modalShell",
43
- backdropFilter: "blur(token(blurs.modalShell))",
44
+ backdropFilter: "[blur(token(blurs.modalShell))]",
44
45
  color: "fg.primary",
45
46
  textStyle: "body",
46
- outline: "none",
47
+ outline: "0",
47
48
  transform: "translateX(-50%)",
48
- transitionProperty: "opacity",
49
+ transitionProperty: "[opacity]",
49
50
  transitionDuration: "fast",
50
- "&[data-starting-style], &[data-ending-style]": { opacity: "0" },
51
- _motionReduce: { transition: "none" },
51
+ "&[data-starting-style], &[data-ending-style]": { opacity: "[0]" },
52
+ _motionReduce: { transition: "[none]" },
52
53
  _phone: { top: "layout.pageGap" },
53
54
  "& > [cmdk-root]": {
54
55
  display: "flex",
@@ -59,21 +60,11 @@ const popup = css({
59
60
  border: "1px solid token(colors.border.divider)",
60
61
  borderRadius: "inset",
61
62
  background: "bg.modalSheet",
62
- backdropFilter: "blur(token(blurs.modalSheet))",
63
+ backdropFilter: "[blur(token(blurs.modalSheet))]",
63
64
  boxShadow: "soft",
64
65
  },
65
66
  });
66
- const visuallyHidden = css({
67
- position: "absolute",
68
- width: "1px",
69
- height: "1px",
70
- padding: "none",
71
- margin: "-1px",
72
- overflow: "hidden",
73
- clip: "rect(0, 0, 0, 0)",
74
- whiteSpace: "nowrap",
75
- border: "0",
76
- });
67
+ const visuallyHidden = css({ srOnly: true });
77
68
  const field = css({
78
69
  display: "flex",
79
70
  alignItems: "center",
@@ -85,7 +76,7 @@ const field = css({
85
76
  "& > input": {
86
77
  flex: "1",
87
78
  minWidth: "0",
88
- height: "100%",
79
+ height: "full",
89
80
  border: "0",
90
81
  outline: "0",
91
82
  background: "transparent",
@@ -104,7 +95,7 @@ const divider = css({
104
95
  background: "border.divider",
105
96
  });
106
97
  const list = css({
107
- maxHeight: "min(60dvh, 420px)",
98
+ maxHeight: "[min(60dvh, 420px)]",
108
99
  overflowY: "auto",
109
100
  overscrollBehavior: "contain",
110
101
  padding: "panel.compact",
@@ -134,18 +125,19 @@ const row = css({
134
125
  lineHeight: "tight",
135
126
  cursor: "pointer",
136
127
  "&[data-selected=true]": {
137
- background: "token(gradients.selected)",
138
- boxShadow: "inset 0 0 0 1px token(colors.border.divider)",
128
+ background: "[token(gradients.selected)]",
129
+ boxShadow: "[inset 0 0 0 1px token(colors.border.divider)]",
139
130
  },
140
- "& > svg": { flexShrink: "0", color: "fg.muted" },
141
- "& > [data-label]": {
131
+ // A link row's content sits in its anchor and lays out as a plain row's does.
132
+ "& > svg, & > [data-row-link] > svg": { flexShrink: "0", color: "fg.muted" },
133
+ "& > [data-label], & > [data-row-link] > [data-label]": {
142
134
  flexShrink: "0",
143
- maxWidth: "60%",
135
+ maxWidth: "[60%]",
144
136
  overflow: "hidden",
145
137
  textOverflow: "ellipsis",
146
138
  whiteSpace: "nowrap",
147
139
  },
148
- "& > [data-detail]": {
140
+ "& > [data-detail], & > [data-row-link] > [data-detail]": {
149
141
  flex: "1",
150
142
  minWidth: "0",
151
143
  overflow: "hidden",
@@ -154,48 +146,25 @@ const row = css({
154
146
  color: "fg.subtle",
155
147
  fontSize: "code",
156
148
  },
157
- "& > [data-kind]": {
149
+ "& > [data-kind], & > [data-row-link] > [data-kind]": {
158
150
  flexShrink: "0",
159
151
  marginInlineStart: "auto",
160
152
  color: "fg.subtle",
161
153
  fontSize: "code",
162
154
  },
163
155
  "& > [data-shortcut]": { marginInlineStart: "auto" },
164
- // A link row's anchor fills the row, so its content lays out as a plain row's does. Choosing
165
- // the row clicks it, so it takes no pointer of its own and never navigates twice.
156
+ // The anchor fills the row. Choosing the row clicks it, so it takes no pointer of its own and
157
+ // never navigates twice.
166
158
  "& > [data-row-link]": {
167
159
  pointerEvents: "none",
168
160
  display: "flex",
169
161
  alignItems: "center",
170
- gap: "inherit",
162
+ gap: "[inherit]",
171
163
  flex: "1",
172
164
  minWidth: "0",
173
165
  alignSelf: "stretch",
174
- color: "inherit",
166
+ color: "[inherit]",
175
167
  textDecoration: "none",
176
- "& > svg": { flexShrink: "0", color: "fg.muted" },
177
- "& > [data-label]": {
178
- flexShrink: "0",
179
- maxWidth: "60%",
180
- overflow: "hidden",
181
- textOverflow: "ellipsis",
182
- whiteSpace: "nowrap",
183
- },
184
- "& > [data-detail]": {
185
- flex: "1",
186
- minWidth: "0",
187
- overflow: "hidden",
188
- textOverflow: "ellipsis",
189
- whiteSpace: "nowrap",
190
- color: "fg.subtle",
191
- fontSize: "code",
192
- },
193
- "& > [data-kind]": {
194
- flexShrink: "0",
195
- marginInlineStart: "auto",
196
- color: "fg.subtle",
197
- fontSize: "code",
198
- },
199
168
  },
200
169
  });
201
170
  const emptyRow = css({
@@ -204,7 +173,6 @@ const emptyRow = css({
204
173
  color: "fg.muted",
205
174
  fontSize: "caption",
206
175
  });
207
- // How to drive the palette from the keyboard, and what it searches.
208
176
  const footer = css({
209
177
  display: "flex",
210
178
  alignItems: "center",
@@ -238,7 +206,6 @@ function CommandMenuShortcuts({ items, run, }) {
238
206
  : []));
239
207
  return null;
240
208
  }
241
- /** The keys the palette's footer names, as its keycaps write them. */
242
209
  const escapeKey = formatForDisplay("Escape");
243
210
  const moveKeys = [formatForDisplay("ArrowUp"), formatForDisplay("ArrowDown")];
244
211
  const openKey = formatForDisplay("Enter");
@@ -249,7 +216,7 @@ const menuRowHeight = 40;
249
216
  // The rows lie over a box as tall as all of them, each moved down to its place.
250
217
  const virtualLines = css({
251
218
  position: "relative",
252
- "& > *": { position: "absolute", top: "0", insetInline: "0" },
219
+ "& > *": { position: "absolute", top: "none", insetInline: "none" },
253
220
  });
254
221
  /** Every word of the query matches the row's label, detail or keywords (`scoreCommandMatch`). */
255
222
  function matchesQuery(item, query) {
@@ -289,15 +256,10 @@ export function CommandMenu({ open, onOpenChange, query, onQueryChange, groups,
289
256
  const before = focusedBeforeOpen.current;
290
257
  return before?.isConnected ? before : true;
291
258
  };
292
- const setOpen = (next) => {
293
- if (!next)
294
- onQueryChange("");
295
- onOpenChange(next);
296
- };
297
259
  const links = useRef(new Map());
298
260
  const run = (item) => {
299
261
  ranItem.current = true;
300
- setOpen(false);
262
+ onOpenChange(false);
301
263
  // A link row navigates through its anchor, so a router's link handles it as a click.
302
264
  const link = links.current.get(item.id);
303
265
  if (link)
@@ -330,12 +292,24 @@ export function CommandMenu({ open, onOpenChange, query, onQueryChange, groups,
330
292
  overscan: 8,
331
293
  enabled: virtualized,
332
294
  });
333
- const [chosenId, setChosenId] = useState("");
334
- const selectedId = shownItems.some((item) => item.id === chosenId)
335
- ? chosenId
295
+ // The row the reader moved to for this query; the first row until they do, and again once they
296
+ // type.
297
+ const [chosen, setChosen] = useState({ id: "", query: "" });
298
+ const chooseRow = (id) => setChosen({ id, query });
299
+ const selectedId = chosen.query === query && shownItems.some((item) => item.id === chosen.id)
300
+ ? chosen.id
336
301
  : (shownItems[0]?.id ?? "");
302
+ // cmdk names the highlighted row on the field only once an arrow has moved it, so the field
303
+ // takes the row's id from the drawn list instead, again whenever the rows are drawn anew.
304
+ const [highlightedRowElementId, setHighlightedRowElementId] = useState();
305
+ const drawnVirtualRows = virtualizer.getVirtualItems();
306
+ useLayoutEffect(() => {
307
+ const row = listElement?.querySelector(`[data-command-item-id="${CSS.escape(selectedId)}"]`);
308
+ setHighlightedRowElementId(row?.id || undefined);
309
+ }, [listElement, selectedId, query, groups, drawnVirtualRows]);
337
310
  const moveSelection = (event) => {
338
- if (!virtualized)
311
+ // While an input method composes, its keys pick a candidate, not a row.
312
+ if (!virtualized || event.nativeEvent.isComposing)
339
313
  return;
340
314
  const index = shownItems.findIndex((item) => item.id === selectedId);
341
315
  const last = shownItems.length - 1;
@@ -364,16 +338,21 @@ export function CommandMenu({ open, onOpenChange, query, onQueryChange, groups,
364
338
  if (!item)
365
339
  return;
366
340
  event.preventDefault();
367
- setChosenId(item.id);
341
+ chooseRow(item.id);
368
342
  virtualizer.scrollToIndex(lines.findIndex((line) => line.key === item.id), { align: "auto" });
369
343
  };
370
- const drawItem = (item, measured) => (_jsxs(Command.Item, { ref: measured?.ref, "data-index": measured?.index, style: measured?.style, value: item.id, className: row, onSelect: () => run(item), children: [item.href !== undefined || item.render !== undefined ? (_jsx(CommandMenuLink, { item: item, register: (element) => {
344
+ const drawItem = (item, measured) => (_jsxs(Command.Item, { ref: measured?.ref, "data-index": measured?.index, style: measured?.style, value: item.id, className: row, "data-command-item-id": item.id, onSelect: () => run(item), children: [item.href !== undefined || item.render !== undefined ? (_jsx(CommandMenuLink, { item: item, register: (element) => {
371
345
  if (element)
372
346
  links.current.set(item.id, element);
373
347
  else
374
348
  links.current.delete(item.id);
375
349
  } })) : (_jsx(CommandMenuRowContent, { item: item })), item.shortcut && _jsx(ShortcutHint, { boxed: true, "data-shortcut": "", hotkey: item.shortcut })] }, item.id));
376
- return (_jsx(BaseDialog.Root, { open: open, onOpenChange: setOpen, children: _jsx(BaseDialog.Portal, { children: _jsxs(HotkeyLayer, { children: [_jsx(CommandMenuShortcuts, { items: shownGroups.flatMap((group) => group.items), run: run }), _jsx(BaseDialog.Backdrop, { className: backdrop, "data-ciridae-ui": "" }), _jsxs(BaseDialog.Popup, { className: popup, "data-ciridae-ui": "", ref: popupRef, finalFocus: restoreFocus, children: [_jsx(BaseDialog.Title, { className: visuallyHidden, children: title }), _jsxs(Command, { label: title, shouldFilter: false, loop: true, ...(virtualized ? { value: selectedId, onValueChange: setChosenId } : {}), onKeyDown: moveSelection, children: [_jsxs("div", { className: field, children: [_jsx(MagnifyingGlassIcon, { className: icon({ size: "small" }) }), _jsx(Command.Input, { autoFocus: true, placeholder: placeholder, value: query, onValueChange: onQueryChange }), _jsx(Keycap, { "aria-hidden": "true", children: escapeKey })] }), _jsx("div", { className: divider }), _jsxs(Command.List, { ref: setListElement, className: list, children: [!matched && (_jsx("div", { className: emptyRow, role: "presentation", children: empty })), virtualized ? (_jsx("div", { className: virtualLines, style: { height: virtualizer.getTotalSize() }, children: virtualizer.getVirtualItems().map((virtualItem) => {
350
+ return (_jsx(BaseDialog.Root, { open: open, onOpenChange: onOpenChange,
351
+ // However it closes, by its own keys or by the host's toggle, the next open starts empty.
352
+ onOpenChangeComplete: (isOpen) => {
353
+ if (!isOpen)
354
+ onQueryChange("");
355
+ }, children: _jsx(BaseDialog.Portal, { children: _jsxs(HotkeyLayer, { children: [_jsx(CommandMenuShortcuts, { items: shownGroups.flatMap((group) => group.items), run: run }), _jsx(BaseDialog.Backdrop, { className: backdrop, "data-ciridae-ui": "" }), _jsxs(BaseDialog.Popup, { className: popup, "data-ciridae-ui": "", ref: popupRef, finalFocus: restoreFocus, children: [_jsx(BaseDialog.Title, { className: visuallyHidden, children: title }), _jsxs(Command, { label: title, shouldFilter: false, loop: true, value: selectedId, onValueChange: chooseRow, onKeyDown: moveSelection, children: [_jsxs("div", { className: field, children: [_jsx(MagnifyingGlassIcon, { className: icon({ size: "small" }) }), _jsx(Command.Input, { asChild: true, autoFocus: true, placeholder: placeholder, value: query, onValueChange: onQueryChange, children: _jsx("input", { "aria-activedescendant": highlightedRowElementId }) }), _jsx(Keycap, { "aria-hidden": "true", children: escapeKey })] }), _jsx("div", { className: divider }), _jsxs(Command.List, { ref: setListElement, className: list, children: [!matched && (_jsx("div", { className: emptyRow, role: "presentation", children: empty })), virtualized ? (_jsx("div", { className: virtualLines, style: { height: virtualizer.getTotalSize() }, children: drawnVirtualRows.map((virtualItem) => {
377
356
  const line = lines[virtualItem.index];
378
357
  if (!line)
379
358
  return null;
@@ -15,6 +15,7 @@ import { useHotkey } from "./hotkeys.js";
15
15
  import { icon } from "./icon.js";
16
16
  import { ShortcutHint } from "./keycap.js";
17
17
  import { useCiridaeLocale } from "./locale.js";
18
+ import { Tooltip } from "./overlays.js";
18
19
  /** Whether the palette is open; `hotkey` (Cmd/Ctrl+K by default) toggles it from anywhere. */
19
20
  export function useCommandPalette({ hotkey = "Mod+K" } = {}) {
20
21
  const { labels } = useCiridaeLocale();
@@ -54,7 +55,7 @@ const trigger = css({
54
55
  display: "flex",
55
56
  alignItems: "center",
56
57
  gap: "control.iconGap",
57
- width: "100%",
58
+ width: "full",
58
59
  height: "control.sidebarSearch",
59
60
  paddingInlineStart: "control.compactInline",
60
61
  paddingInlineEnd: "control.block",
@@ -82,5 +83,7 @@ const trigger = css({
82
83
  /** A search field's face, as a button that opens the palette; reads "Search" unless given text. */
83
84
  export function CommandPaletteTrigger({ hotkey = "Mod+K", compact = false, className, children, ...props }) {
84
85
  const { labels } = useCiridaeLocale();
85
- return (_jsxs("button", { type: "button", "aria-haspopup": "dialog", "aria-label": compact ? labels.common.search : undefined, ...props, className: cx(trigger, className), "data-ciridae-ui": "", "data-compact": compact || undefined, children: [_jsx(MagnifyingGlassIcon, { className: icon({ size: "small" }) }), !compact && (_jsxs(_Fragment, { children: [children ?? labels.common.search, _jsx(ShortcutHint, { boxed: true, "data-shortcut": "", hotkey: hotkey })] }))] }));
86
+ const button = (_jsxs("button", { type: "button", "aria-haspopup": "dialog", "aria-label": compact ? labels.common.search : undefined, ...props, className: cx(trigger, className), "data-ciridae-ui": "", "data-compact": compact || undefined, children: [_jsx(MagnifyingGlassIcon, { className: icon({ size: "small" }) }), !compact && (_jsxs(_Fragment, { children: [children ?? labels.common.search, _jsx(ShortcutHint, { boxed: true, "data-shortcut": "", hotkey: hotkey })] }))] }));
87
+ // The rail's icon-only search names itself on hover and focus, as the rail's other icons do.
88
+ return compact ? (_jsx(Tooltip, { content: labels.common.search, side: "inline-end", children: button })) : (button);
86
89
  }
@@ -69,7 +69,7 @@ const inlineCopy = css({
69
69
  padding: "none",
70
70
  border: "0",
71
71
  background: "transparent",
72
- color: "inherit",
72
+ color: "[inherit]",
73
73
  cursor: "pointer",
74
74
  _hover: { color: "fg.primary" },
75
75
  _focusVisible: { focusRing: "outer", borderRadius: "control" },
@@ -108,7 +108,7 @@ const copyableText = css({
108
108
  "&:hover > button, &:focus-within > button, & > button[data-copied]": {
109
109
  width: "auto",
110
110
  marginInlineStart: "control.iconGap",
111
- opacity: "1",
111
+ opacity: "[1]",
112
112
  },
113
113
  "&:hover > span > span": {
114
114
  transform: "translateX(calc(var(--marquee-shift, 0px) * -1))",
@@ -121,11 +121,11 @@ const marqueeBox = css({
121
121
  whiteSpace: "nowrap",
122
122
  "& > span": {
123
123
  display: "inline-block",
124
- transitionProperty: "transform",
124
+ transitionProperty: "[transform]",
125
125
  transitionTimingFunction: "linear",
126
126
  transitionDuration: "var(--marquee-duration, 0s)",
127
127
  },
128
- _motionReduce: { "& > span": { transitionDuration: "0s" } },
128
+ _motionReduce: { "& > span": { transitionDuration: "[0s]" } },
129
129
  });
130
130
  const copyTextButton = css({
131
131
  display: "inline-flex",
@@ -137,7 +137,7 @@ const copyTextButton = css({
137
137
  cursor: "pointer",
138
138
  width: "0",
139
139
  overflow: "hidden",
140
- opacity: "0",
140
+ opacity: "[0]",
141
141
  _hover: { color: "fg.primary" },
142
142
  _focusVisible: { focusRing: "outer" },
143
143
  });
@@ -23,7 +23,6 @@ export declare function Avatar({ label, src, size, kind, icon, badge, color, cla
23
23
  badge?: ReactNode;
24
24
  color?: AvatarColor;
25
25
  }): import("react").JSX.Element;
26
- /** The slot where an owner's avatar goes, before anyone owns the record. */
27
26
  export declare function UnassignedAvatar({ size, className, ...props }: ComponentProps<"span"> & {
28
27
  size?: "small" | "owner" | "row";
29
28
  }): import("react").JSX.Element;
@@ -54,7 +53,6 @@ export declare function CodeBlock({ className, copy, copyLabel, ...props }: Comp
54
53
  copy?: string;
55
54
  copyLabel?: string;
56
55
  }): import("react").JSX.Element;
57
- /** Where a field's value came from, or who vouches for it: a 14px icon and a line. */
58
56
  export type DetailFieldNote = {
59
57
  icon?: ReactNode;
60
58
  label: ReactNode;
@@ -62,17 +60,13 @@ export type DetailFieldNote = {
62
60
  export type DetailFieldProps = Omit<ComponentProps<"div">, "children"> & {
63
61
  label: ReactNode;
64
62
  value: ReactNode;
65
- /** Where the value came from, such as "Net 30 from Aug 22". */
66
63
  source?: DetailFieldNote;
67
- /** Who or what vouches for it, such as "Checked by Dana R.". */
68
64
  assurance?: DetailFieldNote;
69
- /** Beside the value, such as a Contested badge; its line under the value is `description`. */
70
65
  badge?: ReactNode;
71
66
  description?: ReactNode;
72
- /** A trailing button, such as Resolve. */
73
67
  action?: ReactNode;
74
68
  state?: "default" | "selected" | "inset" | "highlighted";
75
- /** A hairline under the row; the last row of a list leaves it out. Defaults to true. */
69
+ /** A hairline under the row; the last row of a list leaves it out. */
76
70
  divider?: boolean;
77
71
  };
78
72
  export declare function DetailField({ label, value, source, assurance, badge, description, action, state, divider, className, ...props }: DetailFieldProps): import("react").JSX.Element;