@parallelworks/ui 0.49.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 (49) 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.d.ts +5 -1
  9. package/dist/components/Field.js +18 -5
  10. package/dist/components/MenuEntries.d.ts +39 -12
  11. package/dist/components/MenuEntries.js +24 -8
  12. package/dist/components/Meter.js +3 -3
  13. package/dist/components/PageHeader.d.ts +4 -1
  14. package/dist/components/PageHeader.js +5 -4
  15. package/dist/components/Provider.d.ts +2 -0
  16. package/dist/components/Provider.js +1 -0
  17. package/dist/components/RequiredMark.js +1 -1
  18. package/dist/components/RunStatusIcon.js +2 -2
  19. package/dist/components/StatusIcons.js +2 -2
  20. package/dist/components/ThemeEditor.js +6 -6
  21. package/dist/components/Toast.js +2 -2
  22. package/dist/components/buttonClasses.js +1 -1
  23. package/dist/components/splitter.js +9 -3
  24. package/dist/components/tone.d.ts +5 -2
  25. package/dist/components/tone.js +15 -6
  26. package/dist/form/Wizard/WizardNavigation.js +1 -1
  27. package/dist/form/Wizard/WizardStepIndicator.js +1 -1
  28. package/dist/graph/DependencyGraph.js +1 -1
  29. package/dist/graph/GraphEditor.js +1 -1
  30. package/dist/graph/GraphEditorDialogs.js +1 -1
  31. package/dist/graph/ProblemsButton.js +2 -2
  32. package/dist/graph/ShortcutsButton.js +1 -1
  33. package/dist/graph/editorChrome.js +1 -1
  34. package/dist/graph/fieldProblems.js +1 -1
  35. package/dist/graph/stepWith.js +1 -1
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.js +2 -2
  38. package/dist/list/ActionToolbar.d.ts +2 -2
  39. package/dist/list/ActionToolbar.js +6 -3
  40. package/dist/list/ListPage.js +4 -2
  41. package/dist/list/ListViewControls.js +1 -1
  42. package/dist/list/rowMenu.d.ts +2 -2
  43. package/dist/logviewer/LogViewer.js +1 -1
  44. package/dist/styles/theme.css +24 -10
  45. package/dist/styles.css +1 -1
  46. package/dist/theme/derive.d.ts +1 -1
  47. package/dist/theme/derive.js +46 -21
  48. package/dist/theme/fit.js +10 -4
  49. 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
@@ -38,6 +38,10 @@ export interface FieldProps {
38
38
  layout?: FieldLayout | undefined;
39
39
  /** With `inline`, the label takes only its own width rather than the column's. */
40
40
  compact?: boolean | undefined;
41
+ /** Matches the label to a control of the same `size`. At `sm` the label is a small, muted
42
+ * caption for a compact control, such as a picker in a toolbar, and an inline label's row
43
+ * is the `sm` control's height. Set the control's own `size` to match. */
44
+ size?: FieldBoxSize | undefined;
41
45
  /** A set of controls, such as radios or checkboxes, that a legend names instead of a label. */
42
46
  group?: boolean | undefined;
43
47
  /** The control's id; one is made up when left out. */
@@ -57,7 +61,7 @@ export declare function ErrorText({ id, children }: {
57
61
  }): import("react").JSX.Element | null;
58
62
  /** A control with its label, help and error, tied to it for assistive technology. One frame
59
63
  * for every field, in a form or standing alone. */
60
- export declare function Field({ label, hint, actions, description, error, invalid, required, disabled, layout, compact, group, id: ownId, className, children, }: FieldProps): import("react").JSX.Element;
64
+ export declare function Field({ label, hint, actions, description, error, invalid, required, disabled, layout, compact, size, group, id: ownId, className, children, }: FieldProps): import("react").JSX.Element;
61
65
  /** One checkbox or radio and its label inside a Field, such as an option of a group. A bare
62
66
  * `<label>` there would take the Field's label as its own, so every option read as the field. */
63
67
  export declare function FieldOption({ disabled, className, children, }: {
@@ -17,9 +17,21 @@ var FieldRowContext = createContext(false);
17
17
  function useFieldControl() {
18
18
  return use(ControlContext);
19
19
  }
20
- var labelClassName = "flex items-center gap-1 text-sm font-medium text-(--theme-app)";
20
+ var labelSizes = {
21
+ sm: {
22
+ text: "text-xs text-(--theme-muted-text-color)",
23
+ inlineHeight: "min-h-7",
24
+ stackGap: "gap-1"
25
+ },
26
+ md: {
27
+ text: "text-sm text-(--theme-app)",
28
+ inlineHeight: "min-h-9",
29
+ stackGap: "gap-1.5"
30
+ }
31
+ };
32
+ var labelClassName = "flex items-center gap-1 font-medium";
21
33
  var actionsClassName = "ml-auto flex shrink-0 items-center gap-1";
22
- 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)";
23
35
  /** An error in the look a Field gives its own, for a control outside a Field, or for a part of
24
36
  * one rather than the whole of it, such as a row of a list editor. Give it an `id` and name it
25
37
  * in the control's `aria-describedby` so the error is read with the control. */
@@ -36,7 +48,7 @@ function ErrorText({ id, children }) {
36
48
  }
37
49
  /** A control with its label, help and error, tied to it for assistive technology. One frame
38
50
  * for every field, in a form or standing alone. */
39
- function Field$1({ label, hint, actions, description, error, invalid, required = false, disabled = false, layout = "stacked", compact = false, group = false, id: ownId, className, children }) {
51
+ function Field$1({ label, hint, actions, description, error, invalid, required = false, disabled = false, layout = "stacked", compact = false, size = "md", group = false, id: ownId, className, children }) {
40
52
  const uid = useId();
41
53
  const id = ownId ?? `${uid}control`;
42
54
  const labelId = `${uid}label`;
@@ -53,7 +65,8 @@ function Field$1({ label, hint, actions, description, error, invalid, required =
53
65
  };
54
66
  const inline = layout === "inline";
55
67
  const inRow = use(FieldRowContext) && !inline && !group;
56
- const labelBox = cx(labelClassName, inline && cx("min-h-9 shrink-0", compact ? "mr-2 w-fit" : "w-52 pr-3"), inRow && "row-start-1 self-end");
68
+ const labelSize = labelSizes[size];
69
+ const labelBox = cx(labelClassName, labelSize.text, inline && cx(labelSize.inlineHeight, "shrink-0", compact ? "mr-2 w-fit" : "w-52 pr-3"), inRow && "row-start-1 self-end");
57
70
  const labelContent = /* @__PURE__ */ jsxs(Fragment, { children: [
58
71
  /* @__PURE__ */ jsxs("span", { children: [label, required && /* @__PURE__ */ jsx(RequiredMark, {})] }),
59
72
  hint,
@@ -89,7 +102,7 @@ function Field$1({ label, hint, actions, description, error, invalid, required =
89
102
  })
90
103
  ]
91
104
  });
92
- const frame = inRow ? cx("contents", className) : cx("flex w-full", inline ? "items-start" : "flex-col gap-1.5", className);
105
+ const frame = inRow ? cx("contents", className) : cx("flex w-full", inline ? "items-start" : cx("flex-col", labelSize.stackGap), className);
93
106
  if (group) return /* @__PURE__ */ jsx(Field.Root, {
94
107
  invalid: isInvalid,
95
108
  disabled,
@@ -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 = {
@@ -41,7 +41,7 @@ export declare function BreadcrumbsTitleAction({ children }: {
41
41
  export declare function BreadcrumbsRightAction({ children }: {
42
42
  children: ReactNode;
43
43
  }): import("react").ReactPortal | null;
44
- export declare function BreadcrumbsActionButton({ icon: Icon, label, ariaLabel, href, onClick, disabled, tooltip, }: {
44
+ export declare function BreadcrumbsActionButton({ icon: Icon, label, ariaLabel, href, onClick, disabled, tooltip, destructive, }: {
45
45
  icon: ComponentType<{
46
46
  className?: string;
47
47
  }>;
@@ -51,6 +51,9 @@ export declare function BreadcrumbsActionButton({ icon: Icon, label, ariaLabel,
51
51
  onClick?: (() => void) | undefined;
52
52
  disabled?: boolean;
53
53
  tooltip?: string | undefined;
54
+ /** In the error color, as a destructive menu entry is, for an action that removes or breaks
55
+ * something. */
56
+ destructive?: boolean | undefined;
54
57
  }): import("react").JSX.Element;
55
58
  interface PageHeaderProps {
56
59
  breadcrumbs?: PageHeaderBreadcrumb[] | undefined;
@@ -97,22 +97,23 @@ function BreadcrumbsRightAction({ children }) {
97
97
  if (!rightActionSlot) return null;
98
98
  return createPortal(children, rightActionSlot);
99
99
  }
100
- var breadcrumbsActionClasses = "flex items-center gap-1 link text-xs whitespace-nowrap disabled:opacity-50 disabled:cursor-not-allowed";
101
- function BreadcrumbsActionButton({ icon: Icon, label, ariaLabel, href, onClick, disabled, tooltip }) {
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
+ 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)" : "text-(--theme-link)");
103
104
  const content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Icon, { className: "w-3 h-3" }), label] });
104
105
  return /* @__PURE__ */ jsx(Tooltip, {
105
106
  content: tooltip,
106
107
  children: href && !disabled ? /* @__PURE__ */ jsx(Link, {
107
108
  href,
108
- className: breadcrumbsActionClasses,
109
+ className,
109
110
  "aria-label": ariaLabel,
110
111
  children: content
111
112
  }) : /* @__PURE__ */ jsx("button", {
112
113
  type: "button",
113
114
  onClick,
114
115
  disabled,
115
- className: breadcrumbsActionClasses,
116
+ className,
116
117
  "aria-label": ariaLabel,
117
118
  children: content
118
119
  })
@@ -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
  }