@parallelworks/ui 0.50.0 → 0.51.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 (46) hide show
  1. package/README.md +50 -5
  2. package/dist/ai/components/ModelSelector.js +1 -1
  3. package/dist/ai/components/agent/AgentMessageParts.js +3 -3
  4. package/dist/components/Callout.js +3 -7
  5. package/dist/components/CreateModal.js +1 -1
  6. package/dist/components/DescriptionList.d.ts +31 -17
  7. package/dist/components/DescriptionList.js +83 -31
  8. package/dist/components/Field.js +1 -1
  9. package/dist/components/MenuEntries.d.ts +39 -12
  10. package/dist/components/MenuEntries.js +24 -8
  11. package/dist/components/Meter.js +3 -3
  12. package/dist/components/PageHeader.js +1 -1
  13. package/dist/components/Provider.d.ts +2 -0
  14. package/dist/components/Provider.js +1 -0
  15. package/dist/components/RequiredMark.js +1 -1
  16. package/dist/components/RunStatusIcon.js +2 -2
  17. package/dist/components/StatusIcons.js +2 -2
  18. package/dist/components/ThemeEditor.js +6 -6
  19. package/dist/components/Toast.js +2 -2
  20. package/dist/components/buttonClasses.js +1 -1
  21. package/dist/components/tone.d.ts +5 -2
  22. package/dist/components/tone.js +15 -6
  23. package/dist/form/Wizard/WizardNavigation.js +1 -1
  24. package/dist/form/Wizard/WizardStepIndicator.js +1 -1
  25. package/dist/graph/DependencyGraph.js +1 -1
  26. package/dist/graph/GraphEditor.js +1 -1
  27. package/dist/graph/GraphEditorDialogs.js +1 -1
  28. package/dist/graph/ProblemsButton.js +2 -2
  29. package/dist/graph/ShortcutsButton.js +1 -1
  30. package/dist/graph/editorChrome.js +1 -1
  31. package/dist/graph/fieldProblems.js +1 -1
  32. package/dist/graph/stepWith.js +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.js +2 -2
  35. package/dist/list/ActionToolbar.d.ts +2 -2
  36. package/dist/list/ActionToolbar.js +6 -3
  37. package/dist/list/ListPage.js +4 -2
  38. package/dist/list/ListViewControls.js +1 -1
  39. package/dist/list/rowMenu.d.ts +2 -2
  40. package/dist/logviewer/LogViewer.js +1 -1
  41. package/dist/styles/theme.css +24 -10
  42. package/dist/styles.css +1 -1
  43. package/dist/theme/derive.d.ts +1 -1
  44. package/dist/theme/derive.js +46 -21
  45. package/dist/theme/fit.js +10 -4
  46. package/package.json +1 -1
package/README.md CHANGED
@@ -34,7 +34,8 @@ Each need has one component. Reach for it before styling markup by hand.
34
34
  | A text field | `Input`, `Textarea`, or a control framed by `Field`, whose `actions` hold buttons that act on it |
35
35
  | A number, a date, a color | `NumberInput`, `DatePicker`, `ColorInput` |
36
36
  | A one-time code, such as a sign-in check | `OneTimeCodeInput` |
37
- | A labeled value nobody edits, among fields | `FieldValue` in a `Field`; `DescriptionList` for a set of them alone |
37
+ | A labeled value nobody edits, among fields | `FieldValue` in a `Field` |
38
+ | A record's details, such as in a drawer or a card: names and values on their own | `DescriptionList` of `DescriptionListItem`s; `copy` on an ID, `mono` on a path, `variant="card"` to frame them |
38
39
  | An error beside a control outside a `Field` | `ErrorText` |
39
40
  | A dense field in a toolbar or a table cell | any field control with `size="sm"`; a width in `className` wins, and `cx` merges classes the same way |
40
41
  | On or off | `Switch`; `Checkbox` in a list of things to tick, several of which may be on |
@@ -101,6 +102,30 @@ Some pairs look alike and are split on purpose:
101
102
  A `Combobox` filters as you type, for a list long enough to search, and is the
102
103
  one that takes several values, new ones, or free text (`Autocomplete`).
103
104
 
105
+ ## Menu and row actions
106
+
107
+ A `MenuAction`, and the `RowAction` and `ToolbarAction` built on it, does
108
+ exactly one thing: runs `onClick`, navigates to `href`, or copies `copy`. An
109
+ action with none of them, or with two, doesn't compile, so a row that silently
110
+ does nothing when chosen can't ship. `checked` makes an `onClick` row an on/off
111
+ choice, and `reloadDocument` belongs to an `href`. A disabled action keeps its
112
+ behavior. In development, a menu row, pinned row button or toolbar button warns
113
+ in the console when it gets an action with no behavior anyway, from JavaScript
114
+ or a cast.
115
+
116
+ Moving to it:
117
+
118
+ - Rename a handler kept under another name, such as `onSelect`, to `onClick`.
119
+ An action built apart from its array, typed loosely or by another interface,
120
+ slipped past the old type; it now fails where it joins the array.
121
+ - Leave out an action that can't run, rather than passing
122
+ `onClick: cond ? run : undefined` or `href: url ?? undefined`:
123
+ `...(cond ? [{ key, label, onClick: run }] : [])`.
124
+ - Drop `onClick` from a `copy` or `href` action, and `checked` from an `href`
125
+ or `copy` one. To copy and also do something, call `useCopy` in an `onClick`.
126
+ - Write `RowAction` and `ToolbarAction` as types, not interfaces to extend:
127
+ `type MyAction = RowAction & { … }`.
128
+
104
129
  ## Themes
105
130
 
106
131
  Every color a component may use is one of the closed set of `--theme-*` CSS
@@ -150,14 +175,29 @@ until it reaches its target against every surface it sits on.
150
175
  | Pair | WCAG at 0 / 30 / 100 | APCA Lc |
151
176
  | --- | --- | --- |
152
177
  | Body text | 4.5 / 7 / 12 | 75 / 90 / 105 |
153
- | Secondary text, links, status, syntax and terminal colors | 4.5 / 4.5 / 7 | 60 / 70 / 85 |
178
+ | Secondary text, links, status text, syntax and terminal colors | 4.5 / 4.5 / 7 | 60 / 70 / 85 |
154
179
  | Placeholders | 3 / 3 / 4.5 | 45 / 50 / 65 |
155
- | Focus ring | 3 / 3 / 4.5 | |
180
+ | Focus ring, status icons, dots, borders and bars | 3 / 3 / 4.5 | |
156
181
  | Control borders | 1.6 / 2.2 / 3 | |
157
- | Text on the accent | 4.5 | |
182
+ | Text on the accent, white text on a solid status fill | 4.5 | |
158
183
 
159
184
  Status colors (success, error, warning, info, favorite) and terminal colors keep
160
- fixed hues, so red means the same thing in every theme. Syntax colors turn away
185
+ fixed hues, so red means the same thing in every theme.
186
+
187
+ Each status comes in four tokens, so a red icon stays bright while red words stay
188
+ readable:
189
+
190
+ | Token | For | Solved to |
191
+ | --- | --- | --- |
192
+ | `--theme-error` | Icons, dots, borders, rings, bars: anything but words | 3:1 on every surface and on the muted tint (WCAG 1.4.11) |
193
+ | `--theme-error-text` | Words in the status: error messages, a destructive menu item or text button, a badge's label | Secondary text's targets, on the surfaces and on the muted tint |
194
+ | `--theme-error-solid` | A fill under a white label, such as a destructive button | White on it at 4.5:1 |
195
+ | `--theme-error-muted` | A tinted background, such as a callout or badge | The vivid shade at 10% (15% in dark mode) |
196
+
197
+ The same four exist for `success`, `warning`, `info` and `favorite`. Words never
198
+ take the bare token: `--theme-error` meets only the 3:1 that graphics need. An
199
+ icon beside an error message may keep the message's text shade through
200
+ `currentColor`. Syntax colors turn away
161
201
  from the accent's hue when they come within 30° of it, so a keyword never looks
162
202
  like a link.
163
203
 
@@ -259,6 +299,11 @@ one is meant to differ.
259
299
  | `--theme-bg` | `--theme-sidebar-bg` |
260
300
  | `theme-element` (utility) | `theme-accent` |
261
301
 
302
+ `--theme-<status>` (`success`, `error`, `warning`, `info`, `favorite`) was once
303
+ solved as text. It is now the vivid graphic shade, so words that read it move to
304
+ `--theme-<status>-text`, and a fill under a white label to
305
+ `--theme-<status>-solid`.
306
+
262
307
  ## Buttons and spinners
263
308
 
264
309
  Every pressable thing has one look, from `buttonClasses({ variant, size, iconOnly })`:
@@ -109,7 +109,7 @@ function ModelSelector({ targetSession }) {
109
109
  children: t.loading
110
110
  });
111
111
  if (modelsError && models.length === 0) return /* @__PURE__ */ jsxs("span", {
112
- className: cx(chipClassName, "cursor-default text-(--theme-error)"),
112
+ className: cx(chipClassName, "cursor-default text-(--theme-error-text)"),
113
113
  children: [t.failed, /* @__PURE__ */ jsxs("button", {
114
114
  type: "button",
115
115
  onClick: () => refreshModels(),
@@ -220,7 +220,7 @@ function ToolCallBlock({ part }) {
220
220
  }), /* @__PURE__ */ jsx(ResultBody, { part })]
221
221
  }),
222
222
  !isDiff && !expanded && /* @__PURE__ */ jsx("div", {
223
- className: cx("ml-5 mt-0.5 text-[13px]", part.status === "error" ? "text-(--theme-error)" : "theme-muted-text"),
223
+ className: cx("ml-5 mt-0.5 text-[13px]", part.status === "error" ? "text-(--theme-error-text)" : "theme-muted-text"),
224
224
  children: summary
225
225
  }),
226
226
  isDiff && /* @__PURE__ */ jsx("div", {
@@ -277,7 +277,7 @@ function SubagentCard({ part }) {
277
277
  ]
278
278
  })]
279
279
  })] }),
280
- panelClassName: cx("ml-5 mt-0.5 space-y-1 text-[13px]", part.status === "failed" ? "text-(--theme-error)" : "theme-muted-text"),
280
+ panelClassName: cx("ml-5 mt-0.5 space-y-1 text-[13px]", part.status === "failed" ? "text-(--theme-error-text)" : "theme-muted-text"),
281
281
  children: [actions.length > 0 && /* @__PURE__ */ jsxs("ul", {
282
282
  className: "space-y-0.5",
283
283
  children: [actions.map((a, i) => /* @__PURE__ */ jsx("li", { children: a }, i)), part.status !== "running" && /* @__PURE__ */ jsx("li", { children: outcome })]
@@ -286,7 +286,7 @@ function SubagentCard({ part }) {
286
286
  children: /* @__PURE__ */ jsx(Markdown, { children: part.report })
287
287
  })]
288
288
  }), !expanded && /* @__PURE__ */ jsx("div", {
289
- className: cx("ml-5 mt-0.5 text-[13px]", part.status === "failed" ? "text-(--theme-error)" : "theme-muted-text"),
289
+ className: cx("ml-5 mt-0.5 text-[13px]", part.status === "failed" ? "text-(--theme-error-text)" : "theme-muted-text"),
290
290
  children: outcome
291
291
  })]
292
292
  });
@@ -1,16 +1,12 @@
1
1
  import { cx } from "../cx.js";
2
2
  import { InfoIcon, WarningIcon } from "../icons.js";
3
- import { toneSurface } from "./tone.js";
3
+ import { toneIcon, toneSurface } from "./tone.js";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/components/Callout.tsx
6
6
  function CalloutIcon({ type }) {
7
- if (type === "warning" || type === "error") return /* @__PURE__ */ jsx(WarningIcon, {
7
+ return /* @__PURE__ */ jsx(type === "info" ? InfoIcon : WarningIcon, {
8
8
  "aria-hidden": "true",
9
- className: "mr-3 h-5 w-5 shrink-0"
10
- });
11
- else return /* @__PURE__ */ jsx(InfoIcon, {
12
- "aria-hidden": "true",
13
- className: "mr-3 h-5 w-5 shrink-0"
9
+ className: cx("mr-3 h-5 w-5 shrink-0", toneIcon[type])
14
10
  });
15
11
  }
16
12
  function Callout({ type, className = "", children }) {
@@ -127,7 +127,7 @@ function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, namePlaceho
127
127
  className: cx("mt-2 -mb-1 -mx-1.5 w-[calc(100%+0.75rem)] rounded-md border-0 px-1.5 py-1 bg-transparent shadow-none text-lg font-medium text-(--theme-app) placeholder:text-(--theme-placeholder) focus:outline-none", nameFlash && "animate-field-flash", nameMissing ? "ring-1 ring-(--theme-error)" : "focus:ring-0")
128
128
  }),
129
129
  nameError && /* @__PURE__ */ jsx("p", {
130
- className: "mt-1 text-xs text-(--theme-error)",
130
+ className: "mt-1 text-xs text-(--theme-error-text)",
131
131
  children: nameError
132
132
  }),
133
133
  hint && /* @__PURE__ */ jsx("p", {
@@ -1,18 +1,32 @@
1
- import React from 'react';
2
- interface IDescriptionListItemProps {
3
- label: string;
4
- children: React.ReactNode;
5
- tooltip?: string;
6
- className?: string;
1
+ import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
2
+ /** `plain` sets rows apart by space alone; `divided` rules a line between them; `card`
3
+ * also frames them in a panel. */
4
+ export type DescriptionListVariant = 'plain' | 'divided' | 'card';
5
+ export interface DescriptionListProps extends Omit<ComponentPropsWithoutRef<'dl'>, 'className'> {
6
+ variant?: DescriptionListVariant | undefined;
7
+ /** Puts each label over its value at any width. Without it they stack only when the
8
+ * list is too narrow for both side by side. */
9
+ stacked?: boolean | undefined;
10
+ /** Styles the box around the list, such as its margins or width. */
11
+ className?: string | undefined;
7
12
  }
8
- export declare function DescriptionListItem({ label, children, tooltip, className, ...props }: IDescriptionListItemProps & React.ComponentPropsWithoutRef<'dd'>): React.JSX.Element;
9
- export default function DescriptionList({ children, title, separators, subtitle, className, wrapperClassName, vertical, }: {
10
- children: React.ReactNode;
11
- className?: string;
12
- wrapperClassName?: string;
13
- title?: string | React.ReactNode;
14
- separators?: boolean;
15
- subtitle?: string | React.ReactNode;
16
- vertical?: boolean;
17
- }): React.JSX.Element;
18
- export {};
13
+ /** Names and their values, such as the details of a record in a drawer or a card. The
14
+ * label column is as wide as its longest label, up to two fifths of the list, so a
15
+ * long label wraps rather than squeezing the values; each label sits over its value
16
+ * when the list is narrow. */
17
+ export default function DescriptionList({ variant, stacked, className, children, ...props }: DescriptionListProps): import("react").JSX.Element;
18
+ export interface DescriptionListItemProps {
19
+ label: ReactNode;
20
+ /** The value. Nothing, an empty string or `false` shows the library's empty mark. */
21
+ children?: ReactNode;
22
+ /** Help behind an info button beside the label. */
23
+ tooltip?: string | undefined;
24
+ /** Sets the value in a monospace face, for an ID, a path or a hash. */
25
+ mono?: boolean | undefined;
26
+ /** Adds a button that copies the value: `true` copies its text as shown; a string
27
+ * copies that instead, such as a full ID behind a shortened one. */
28
+ copy?: boolean | string | undefined;
29
+ className?: string | undefined;
30
+ }
31
+ /** A label and its value in a DescriptionList. */
32
+ export declare function DescriptionListItem({ label, children, tooltip, mono, copy, className, }: DescriptionListItemProps): import("react").JSX.Element;
@@ -1,41 +1,93 @@
1
1
  import { cx } from "../cx.js";
2
+ import { CheckIcon, CopyIcon } from "../icons.js";
3
+ import { useStrings } from "./Provider.js";
4
+ import { useCopy } from "./useCopy.js";
5
+ import { IconButton } from "./IconButton.js";
2
6
  import { InfoTip } from "./InfoTip.js";
3
7
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
- import React from "react";
8
+ import { useId, useRef } from "react";
5
9
  //#region src/components/DescriptionList.tsx
6
- function DescriptionListItem({ label, children, tooltip, className, ...props }) {
7
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("dt", {
8
- className: cx("text-[13px] leading-6 col-span-1 font-normal flex gap-1", className),
10
+ var variantClasses = {
11
+ plain: "*:py-1",
12
+ divided: "divide-y divide-(--theme-border) *:py-2",
13
+ card: "panel divide-y divide-(--theme-border) px-4 *:py-2"
14
+ };
15
+ /** Names and their values, such as the details of a record in a drawer or a card. The
16
+ * label column is as wide as its longest label, up to two fifths of the list, so a
17
+ * long label wraps rather than squeezing the values; each label sits over its value
18
+ * when the list is narrow. */
19
+ function DescriptionList({ variant = "plain", stacked = false, className, children, ...props }) {
20
+ return /* @__PURE__ */ jsx("div", {
21
+ className: cx("@container/dl min-w-0", className),
22
+ children: /* @__PURE__ */ jsx("dl", {
23
+ ...props,
24
+ className: cx("m-0 grid grid-cols-[minmax(0,1fr)] text-sm", !stacked && "@sm/dl:grid-cols-[fit-content(40%)_minmax(0,1fr)] @sm/dl:gap-x-6", variantClasses[variant]),
25
+ children
26
+ })
27
+ });
28
+ }
29
+ var isEmpty = (value) => value === null || value === void 0 || value === false || value === "";
30
+ /** A label and its value in a DescriptionList. */
31
+ function DescriptionListItem({ label, children, tooltip, mono = false, copy = false, className }) {
32
+ const labelId = useId();
33
+ return /* @__PURE__ */ jsxs("div", {
34
+ className: cx("col-span-full grid grid-cols-subgrid gap-y-0.5", className),
35
+ children: [/* @__PURE__ */ jsxs("dt", {
36
+ className: "min-w-0 leading-5 wrap-break-word text-(--theme-muted-text-color)",
37
+ children: [/* @__PURE__ */ jsx("span", {
38
+ id: labelId,
39
+ children: label
40
+ }), tooltip && /* @__PURE__ */ jsx(InfoTip, {
41
+ className: "ml-1 align-[-2px]",
42
+ text: tooltip
43
+ })]
44
+ }), /* @__PURE__ */ jsx("dd", {
45
+ className: "m-0 flex min-w-0 items-start gap-1 text-(--theme-app)",
46
+ children: isEmpty(children) ? /* @__PURE__ */ jsx(EmptyValue, {}) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
47
+ "data-value": true,
48
+ className: cx("min-w-0 leading-5 wrap-anywhere", mono && "font-mono text-[0.8125rem]"),
49
+ children
50
+ }), copy !== false && /* @__PURE__ */ jsx(CopyValue, {
51
+ text: copy === true ? void 0 : copy,
52
+ label,
53
+ labelId
54
+ })] })
55
+ })]
56
+ });
57
+ }
58
+ var EMPTY_MARK = "—";
59
+ function EmptyValue() {
60
+ const { common: t } = useStrings();
61
+ return /* @__PURE__ */ jsxs("span", {
62
+ className: "leading-5 text-(--theme-muted-text-color)",
9
63
  children: [/* @__PURE__ */ jsx("span", {
10
- className: "min-w-0 truncate",
11
- children: label
12
- }), tooltip && /* @__PURE__ */ jsx(InfoTip, {
13
- className: "pr-2",
14
- text: tooltip
64
+ "aria-hidden": "true",
65
+ children: EMPTY_MARK
66
+ }), /* @__PURE__ */ jsx("span", {
67
+ className: "sr-only",
68
+ children: t.noValue
15
69
  })]
16
- }), /* @__PURE__ */ jsx("dd", {
17
- ...props,
18
- className: "flex text-[13px] leading-6 col-span-4 font-light items-center overflow-auto",
19
- children
20
- })] });
70
+ });
21
71
  }
22
- function DescriptionList({ children, title, separators = true, subtitle, className, wrapperClassName, vertical = false }) {
23
- return /* @__PURE__ */ jsxs("div", {
24
- className: cx("w-full", wrapperClassName),
25
- children: [
26
- title && /* @__PURE__ */ jsx("h2", {
27
- className: "text-sm font-semibold w-full mb-1",
28
- children: title
29
- }),
30
- subtitle && /* @__PURE__ */ jsx("div", {
31
- className: "max-w-2xl text-sm/6 w-full",
32
- children: subtitle
33
- }),
34
- /* @__PURE__ */ jsx("dl", {
35
- className: cx("grid", separators && "*:not-last-of-type:border-b-1", vertical ? "grid-cols-1" : "grid-cols-5 gap-x-4 gap-y-0.5 *:pb-0.5", className),
36
- children: Array.isArray(children) ? React.Children.toArray(children) : children
37
- })
38
- ]
72
+ function CopyValue({ text, label, labelId }) {
73
+ const { common: t } = useStrings();
74
+ const { copy, copied } = useCopy();
75
+ const buttonId = useId();
76
+ const button = useRef(null);
77
+ const onClick = () => {
78
+ const value = button.current?.closest("dd")?.querySelector("[data-value]");
79
+ const shown = value?.innerText ?? value?.textContent;
80
+ copy(text ?? shown ?? "", typeof label === "string" ? { label } : void 0);
81
+ };
82
+ return /* @__PURE__ */ jsx(IconButton, {
83
+ ref: button,
84
+ id: buttonId,
85
+ size: "xs",
86
+ label: copied ? t.copied : t.copy,
87
+ "aria-labelledby": `${buttonId} ${labelId}`,
88
+ icon: copied ? /* @__PURE__ */ jsx(CheckIcon, {}) : /* @__PURE__ */ jsx(CopyIcon, {}),
89
+ onClick,
90
+ className: "-my-0.5"
39
91
  });
40
92
  }
41
93
  //#endregion
@@ -31,7 +31,7 @@ var labelSizes = {
31
31
  };
32
32
  var labelClassName = "flex items-center gap-1 font-medium";
33
33
  var actionsClassName = "ml-auto flex shrink-0 items-center gap-1";
34
- var errorTextClassName = "flex items-center gap-1.5 text-xs text-(--theme-error)";
34
+ var errorTextClassName = "flex items-center gap-1.5 text-xs text-(--theme-error-text)";
35
35
  /** An error in the look a Field gives its own, for a control outside a Field, or for a part of
36
36
  * one rather than the whole of it, such as a row of a list editor. Give it an `id` and name it
37
37
  * in the control's `aria-describedby` so the error is read with the control. */
@@ -1,27 +1,47 @@
1
1
  import { type ReactNode } from 'react';
2
- /** Something a menu row does: navigates with `href`, or runs `onClick`. */
3
- export interface MenuAction {
2
+ interface MenuActionBase {
4
3
  label: string;
5
4
  icon?: ReactNode;
6
- /** Navigates instead of calling onClick. */
7
- href?: string | undefined;
8
- /** Loads the page instead of routing, for paths the host's router doesn't serve. */
9
- reloadDocument?: boolean | undefined;
10
- onClick?: (() => void) | undefined;
11
5
  disabled?: boolean | undefined;
12
6
  /** Why the action is disabled, shown on hover and read with its name. */
13
7
  disabledReason?: string | undefined;
14
8
  destructive?: boolean | undefined;
9
+ /** More text a menu's search matches, beside the label. */
10
+ keywords?: string | undefined;
11
+ }
12
+ /** Runs `onClick`. */
13
+ interface MenuClickAction extends MenuActionBase {
14
+ onClick: () => void;
15
15
  /** Makes the row an on/off choice, checked while true. */
16
16
  checked?: boolean | undefined;
17
- /** Text the row copies to the clipboard, confirmed with a toast naming `label`. */
18
- copy?: {
17
+ href?: never;
18
+ reloadDocument?: never;
19
+ copy?: never;
20
+ }
21
+ /** Navigates to `href`. */
22
+ interface MenuLinkAction extends MenuActionBase {
23
+ href: string;
24
+ /** Loads the page instead of routing, for paths the host's router doesn't serve. */
25
+ reloadDocument?: boolean | undefined;
26
+ onClick?: never;
27
+ checked?: never;
28
+ copy?: never;
29
+ }
30
+ /** Copies `copy.text` to the clipboard, confirmed with a toast naming `copy.label`. */
31
+ interface MenuCopyAction extends MenuActionBase {
32
+ copy: {
19
33
  text: string;
20
34
  label: string;
21
- } | undefined;
22
- /** More text a menu's search matches, beside the label. */
23
- keywords?: string | undefined;
35
+ };
36
+ onClick?: never;
37
+ checked?: never;
38
+ href?: never;
39
+ reloadDocument?: never;
24
40
  }
41
+ /** Something a menu row does: runs `onClick`, navigates to `href`, or copies
42
+ * `copy`. Exactly one, so a row that does nothing when chosen doesn't compile.
43
+ * A disabled action keeps its behavior; one that has none belongs out of the menu. */
44
+ export type MenuAction = MenuClickAction | MenuLinkAction | MenuCopyAction;
25
45
  /** A row that opens a menu of its own. */
26
46
  export interface MenuSubmenu {
27
47
  label: string;
@@ -54,3 +74,10 @@ export declare function MenuEntries({ entries, search, query, }: {
54
74
  /** The search's text, from the MenuFilter around the menu's root. */
55
75
  query?: string;
56
76
  }): import("react").JSX.Element;
77
+ /** Warns in development when an action has no behavior. The types rule that
78
+ * out, but a JavaScript caller or a cast still can, and its row renders fine
79
+ * and silently does nothing. */
80
+ export declare function warnIfInert(action: MenuAction, where: string): void;
81
+ /** Runs an action that isn't a link: copies its text or calls its onClick. */
82
+ export declare function useRunAction(): (action: MenuAction) => void;
83
+ export {};
@@ -9,7 +9,7 @@ import { useQueryMatch } from "./search.js";
9
9
  import { useCopy } from "./useCopy.js";
10
10
  import { Menu } from "@base-ui/react/menu";
11
11
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
- import { useState } from "react";
12
+ import { useCallback, useState } from "react";
13
13
  //#region src/components/MenuEntries.tsx
14
14
  var menuDivider = { divider: true };
15
15
  var isMenuDivider = (entry) => "divider" in entry;
@@ -73,10 +73,29 @@ function SubmenuRow({ submenu }) {
73
73
  })] })
74
74
  });
75
75
  }
76
- var destructiveClassName = "text-(--theme-error) data-highlighted:text-(--theme-error)";
76
+ var destructiveClassName = "text-(--theme-error-text) data-highlighted:text-(--theme-error-text)";
77
+ var warnedInert = /* @__PURE__ */ new Set();
78
+ /** Warns in development when an action has no behavior. The types rule that
79
+ * out, but a JavaScript caller or a cast still can, and its row renders fine
80
+ * and silently does nothing. */
81
+ function warnIfInert(action, where) {
82
+ if (process.env.NODE_ENV === "production") return;
83
+ if (action.onClick || action.href || action.copy || warnedInert.has(action.label)) return;
84
+ warnedInert.add(action.label);
85
+ console.warn(`${where}: the action "${action.label}" has no onClick, href or copy, so choosing it does nothing.`);
86
+ }
87
+ /** Runs an action that isn't a link: copies its text or calls its onClick. */
88
+ function useRunAction() {
89
+ const { copy } = useCopy();
90
+ return useCallback((action) => {
91
+ if (action.copy) copy(action.copy.text, { label: action.copy.label });
92
+ else action.onClick?.();
93
+ }, [copy]);
94
+ }
77
95
  function ActionRow({ action }) {
96
+ warnIfInert(action, "MenuEntries");
78
97
  const Link = useLink();
79
- const { copy } = useCopy();
98
+ const runAction = useRunAction();
80
99
  const className = cx(action.destructive && destructiveClassName);
81
100
  const content = /* @__PURE__ */ jsxs(Fragment, { children: [action.icon, action.label] });
82
101
  if (action.href && !action.disabled) return action.reloadDocument ? /* @__PURE__ */ jsx(MenuLinkItem, {
@@ -88,10 +107,7 @@ function ActionRow({ action }) {
88
107
  className,
89
108
  children: content
90
109
  });
91
- const run = () => {
92
- if (action.copy) copy(action.copy.text, { label: action.copy.label });
93
- action.onClick?.();
94
- };
110
+ const run = () => runAction(action);
95
111
  const reason = action.disabled ? action.disabledReason : void 0;
96
112
  const label = /* @__PURE__ */ jsxs(Fragment, { children: [content, reason && /* @__PURE__ */ jsxs("span", {
97
113
  className: "sr-only",
@@ -115,4 +131,4 @@ function ActionRow({ action }) {
115
131
  });
116
132
  }
117
133
  //#endregion
118
- export { MenuEntries, MenuFilter, isMenuDivider, isMenuSubmenu, menuDivider };
134
+ export { MenuEntries, MenuFilter, isMenuDivider, isMenuSubmenu, menuDivider, useRunAction, warnIfInert };
@@ -6,9 +6,9 @@ import { Progress } from "@base-ui/react/progress";
6
6
  //#region src/components/Meter.tsx
7
7
  var fillClassName = {
8
8
  accent: "bg-(--theme-accent)",
9
- success: "bg-green-600",
10
- warning: "bg-amber-500",
11
- error: "bg-red-600",
9
+ success: "bg-(--theme-success)",
10
+ warning: "bg-(--theme-warning)",
11
+ error: "bg-(--theme-error)",
12
12
  muted: "bg-(--theme-muted-text-color)"
13
13
  };
14
14
  var trackClassName = {
@@ -100,7 +100,7 @@ function BreadcrumbsRightAction({ children }) {
100
100
  var breadcrumbsActionClasses = "flex cursor-pointer items-center gap-1 text-xs whitespace-nowrap hover:not-disabled:underline disabled:cursor-not-allowed disabled:opacity-50";
101
101
  function BreadcrumbsActionButton({ icon: Icon, label, ariaLabel, href, onClick, disabled, tooltip, destructive = false }) {
102
102
  const Link = useLink();
103
- const className = cx(breadcrumbsActionClasses, destructive ? "text-(--theme-error)" : "text-(--theme-link)");
103
+ const className = cx(breadcrumbsActionClasses, destructive ? "text-(--theme-error-text)" : "text-(--theme-link)");
104
104
  const content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Icon, { className: "w-3 h-3" }), label] });
105
105
  return /* @__PURE__ */ jsx(Tooltip, {
106
106
  content: tooltip,
@@ -56,6 +56,8 @@ export interface UIStrings {
56
56
  copyThing: (label: string) => string;
57
57
  copiedThing: (label: string) => string;
58
58
  copyThingFailed: (label: string) => string;
59
+ /** Read out where a value is missing, which shows as a dash. */
60
+ noValue: string;
59
61
  /** A name field's rule: the characters a key or identifier allows. */
60
62
  invalidName: string;
61
63
  invalidNumber: string;
@@ -68,6 +68,7 @@ var DEFAULTS = {
68
68
  copyThing: (label) => `Copy ${label}`,
69
69
  copiedThing: (label) => `Copied ${label}`,
70
70
  copyThingFailed: (label) => `Couldn't copy ${label}`,
71
+ noValue: "None",
71
72
  invalidName: "Use letters, numbers, dashes, and underscores.",
72
73
  invalidNumber: "Use a number.",
73
74
  copy: "Copy",
@@ -3,7 +3,7 @@ import { jsx } from "react/jsx-runtime";
3
3
  function RequiredMark() {
4
4
  return /* @__PURE__ */ jsx("span", {
5
5
  "aria-hidden": "true",
6
- className: "text-red-500 ml-0.5",
6
+ className: "ml-0.5 text-(--theme-error)",
7
7
  children: "*"
8
8
  });
9
9
  }
@@ -1,6 +1,6 @@
1
1
  import { cx } from "../cx.js";
2
2
  import { CancelIcon, FailIcon, NotRunningIcon, RunningIcon, SkipIcon, SuccessIcon } from "../icons.js";
3
- import { toneText } from "./tone.js";
3
+ import { toneIcon } from "./tone.js";
4
4
  import { useStrings } from "./Provider.js";
5
5
  import { jsx } from "react/jsx-runtime";
6
6
  //#region src/components/RunStatusIcon.tsx
@@ -73,7 +73,7 @@ function RunStatusIcon({ status, className }) {
73
73
  "aria-label": label
74
74
  } : { "aria-hidden": true },
75
75
  "data-status": status || "not-run",
76
- className: cx("flex h-2 items-center", toneText[look.tone], className),
76
+ className: cx("flex h-2 items-center", toneIcon[look.tone], className),
77
77
  children: /* @__PURE__ */ jsx(Icon, {
78
78
  "aria-hidden": "true",
79
79
  className: cx(look.spin && "animate-spin rounded-full")
@@ -3,10 +3,10 @@ import { ErrorIcon, SuccessIcon } from "../icons.js";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
  //#region src/components/StatusIcons.tsx
5
5
  function SuccessCheckmark({ className, colored = true }) {
6
- return /* @__PURE__ */ jsx(SuccessIcon, { className: cx(colored && "text-green-500", className) });
6
+ return /* @__PURE__ */ jsx(SuccessIcon, { className: cx(colored && "text-(--theme-success)", className) });
7
7
  }
8
8
  function ErrorCheckmark({ className, colored = true }) {
9
- return /* @__PURE__ */ jsx(ErrorIcon, { className: cx(colored && "text-red-400", className) });
9
+ return /* @__PURE__ */ jsx(ErrorIcon, { className: cx(colored && "text-(--theme-error)", className) });
10
10
  }
11
11
  //#endregion
12
12
  export { ErrorCheckmark, SuccessCheckmark };
@@ -318,13 +318,13 @@ function ThemePreview({ theme, mode }) {
318
318
  /* @__PURE__ */ jsxs("div", {
319
319
  className: "flex flex-wrap items-center gap-2",
320
320
  children: [
321
- /* @__PURE__ */ jsx("span", {
322
- className: "rounded-full bg-(--theme-success-muted) px-2 py-0.5 text-xs text-(--theme-success)",
323
- children: t.previewPassed
321
+ /* @__PURE__ */ jsxs("span", {
322
+ className: "inline-flex items-center gap-1.5 rounded-full bg-(--theme-success-muted) px-2 py-0.5 text-xs text-(--theme-success-text)",
323
+ children: [/* @__PURE__ */ jsx("span", { className: "size-1.5 rounded-full bg-(--theme-success)" }), t.previewPassed]
324
324
  }),
325
- /* @__PURE__ */ jsx("span", {
326
- className: "rounded-full bg-(--theme-error-muted) px-2 py-0.5 text-xs text-(--theme-error)",
327
- children: t.previewFailed
325
+ /* @__PURE__ */ jsxs("span", {
326
+ className: "inline-flex items-center gap-1.5 rounded-full bg-(--theme-error-muted) px-2 py-0.5 text-xs text-(--theme-error-text)",
327
+ children: [/* @__PURE__ */ jsx("span", { className: "size-1.5 rounded-full bg-(--theme-error)" }), t.previewFailed]
328
328
  }),
329
329
  /* @__PURE__ */ jsx("span", {
330
330
  className: "text-(--theme-link) underline underline-offset-2",
@@ -2,7 +2,7 @@ import { cx } from "../cx.js";
2
2
  import { CloseIcon, ErrorIcon, InfoIcon, SuccessIcon, WarningIcon } from "../icons.js";
3
3
  import { buttonClasses } from "./buttonClasses.js";
4
4
  import { Loader } from "./Loader.js";
5
- import { toneText } from "./tone.js";
5
+ import { toneIcon } from "./tone.js";
6
6
  import { useStrings } from "./Provider.js";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
8
8
  import { useEffect, useId, useSyncExternalStore } from "react";
@@ -149,7 +149,7 @@ function ToneIcon({ tone }) {
149
149
  const Icon = ICONS[tone];
150
150
  return /* @__PURE__ */ jsx(Icon, {
151
151
  "aria-hidden": "true",
152
- className: cx("mt-0.5 size-4 shrink-0", toneText[tone])
152
+ className: cx("mt-0.5 size-4 shrink-0", toneIcon[tone])
153
153
  });
154
154
  }
155
155
  function ToastList() {
@@ -6,7 +6,7 @@ var variants = {
6
6
  secondary: "border border-(--theme-border) bg-(--theme-panel-bg) text-(--theme-app) hover:not-disabled:not-aria-disabled:bg-(--theme-hover)",
7
7
  outline: "border border-(--theme-border) text-(--theme-muted-text-color) hover:not-disabled:not-aria-disabled:bg-(--theme-muted-panel-bg) hover:not-disabled:not-aria-disabled:text-(--theme-app) aria-pressed:border-(--theme-link) aria-pressed:text-(--theme-app) data-selected:border-(--theme-link) data-selected:text-(--theme-app) data-popup-open:bg-(--theme-muted-panel-bg) data-popup-open:text-(--theme-app)",
8
8
  ghost: "text-(--theme-muted-text-color) hover:not-disabled:not-aria-disabled:bg-(--theme-muted-panel-bg) hover:not-disabled:not-aria-disabled:text-(--theme-app) aria-pressed:bg-(--theme-muted-panel-bg) aria-pressed:text-(--theme-link) data-selected:bg-(--theme-muted-panel-bg) data-selected:text-(--theme-link) data-popup-open:bg-(--theme-muted-panel-bg) data-popup-open:text-(--theme-app)",
9
- danger: "bg-(--theme-error) text-white hover:not-disabled:not-aria-disabled:bg-(--theme-error)/90"
9
+ danger: "bg-(--theme-error-solid) text-white hover:not-disabled:not-aria-disabled:bg-(--theme-error-solid)/90"
10
10
  };
11
11
  var sizes = {
12
12
  xs: "h-6 px-2 text-xs [&_svg]:size-3",
@@ -1,9 +1,12 @@
1
1
  /** How a status reads: its colors come from the theme's tone tokens, so every
2
- * theme, dark ones included, sets them once. */
2
+ * theme, dark ones included, sets them once. Words take a status's text shade,
3
+ * and icons and dots its vivid one, which only graphics' 3:1 holds. */
3
4
  export type Tone = 'default' | 'success' | 'warning' | 'error' | 'info' | 'muted';
4
5
  /** Text in the tone, for a status line beside a dot or icon. */
5
6
  export declare const toneText: Record<Tone, string>;
7
+ /** An icon in the tone, through `currentColor`. */
8
+ export declare const toneIcon: Record<Tone, string>;
6
9
  /** A tinted surface with a border and text in the tone, for a badge or a callout. */
7
10
  export declare const toneSurface: Record<Tone, string>;
8
- /** A solid fill in the tone, for a dot. */
11
+ /** A solid fill in the tone, for a dot or a bar. */
9
12
  export declare const toneFill: Record<Tone, string>;