@parallelworks/ui 0.42.0 → 0.44.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.
@@ -1,22 +1,6 @@
1
1
  import { type ReactElement, type ReactNode } from 'react';
2
- export interface ComboboxItem<Value = string> {
3
- value: Value;
4
- label: string;
5
- description?: string | undefined;
6
- icon?: ReactNode;
7
- disabled?: boolean | undefined;
8
- }
9
- export interface ComboboxGroup<Value = string> {
10
- label: string;
11
- /** Leads the heading, such as a provider's logo. */
12
- icon?: ReactNode;
13
- /** Follows the label, such as who shared the group or a status like "Not responding". */
14
- description?: string | undefined;
15
- /** Empty for a group that only reports a status; a search finds it by its heading. */
16
- items: readonly (ComboboxItem<Value> | (Value & string))[];
17
- }
18
- /** A flat list, or groups under headings. A bare string is an item labelled by its value. */
19
- export type ComboboxItems<Value = string> = readonly (ComboboxItem<Value> | (Value & string))[] | readonly ComboboxGroup<Value>[];
2
+ import { type ComboboxItems } from './comboboxItems.js';
3
+ export type { ComboboxGroup, ComboboxItem, ComboboxItems, ComboboxLeaf, } from './comboboxItems.js';
20
4
  interface FieldProps {
21
5
  id?: string | undefined;
22
6
  placeholder?: string | undefined;
@@ -63,4 +47,3 @@ export interface AutocompleteProps extends FieldProps {
63
47
  export declare function Combobox<Value = string>(props: ComboboxProps<Value>): import("react").JSX.Element;
64
48
  /** A free-text field that suggests values as you type. */
65
49
  export declare function Autocomplete({ items, value, defaultValue, onValueChange, className, placeholder, disabled, required, id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-invalid': ariaInvalid, }: AutocompleteProps): import("react").JSX.Element;
66
- export {};
@@ -5,6 +5,7 @@ import { panelClassName, panelItemClassName, portalContainer, positionerClassNam
5
5
  import { Tooltip } from "./Tooltip.js";
6
6
  import { useStrings } from "./Provider.js";
7
7
  import { useQueryMatch } from "./search.js";
8
+ import { isComboboxGroup } from "./comboboxItems.js";
8
9
  import { clsx } from "clsx";
9
10
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
11
  import { useCallback, useImperativeHandle, useMemo, useRef, useState } from "react";
@@ -13,9 +14,6 @@ import { Autocomplete } from "@base-ui/react/autocomplete";
13
14
  import { useVirtualizer } from "@tanstack/react-virtual";
14
15
  import { dequal } from "dequal";
15
16
  //#region src/components/Combobox.tsx
16
- function isGroup(item) {
17
- return typeof item === "object" && "items" in item;
18
- }
19
17
  function toEntry(item, group) {
20
18
  const entry = typeof item === "string" ? {
21
19
  value: item,
@@ -26,29 +24,43 @@ function toEntry(item, group) {
26
24
  group
27
25
  };
28
26
  }
29
- /** Base UI takes either flat items or groups, and filters and navigates by the leaf items. */
27
+ /** Base UI takes either flat items or groups, and filters and navigates by the leaf items, so
28
+ * items listed between groups gather into headless groups where they sit. */
30
29
  function useEntries(items) {
31
30
  return useMemo(() => {
32
- const [first] = items;
33
- if (first !== void 0 && isGroup(first)) {
34
- const groups = items.map((g, order) => ({
35
- label: g.label,
36
- icon: g.icon,
37
- description: g.description,
38
- items: g.items.map((item) => toEntry(item, g.label)),
39
- order
40
- }));
31
+ if (!items.some(isComboboxGroup)) {
32
+ const flat = items.map((item) => toEntry(item));
41
33
  return {
42
- items: groups,
43
- flat: groups.flatMap((g) => g.items),
44
- statusGroups: groups.filter((g) => g.items.length === 0)
34
+ items: flat,
35
+ flat,
36
+ statusGroups: []
45
37
  };
46
38
  }
47
- const flat = items.map((item) => toEntry(item));
39
+ const groups = [];
40
+ for (const item of items) {
41
+ if (isComboboxGroup(item)) {
42
+ groups.push({
43
+ label: item.label,
44
+ icon: item.icon,
45
+ description: item.description,
46
+ items: item.items.map((leaf) => toEntry(leaf, item.label)),
47
+ order: groups.length
48
+ });
49
+ continue;
50
+ }
51
+ const last = groups.at(-1);
52
+ if (last?.headless) last.items.push(toEntry(item));
53
+ else groups.push({
54
+ label: "",
55
+ items: [toEntry(item)],
56
+ order: groups.length,
57
+ headless: true
58
+ });
59
+ }
48
60
  return {
49
- items: flat,
50
- flat,
51
- statusGroups: []
61
+ items: groups,
62
+ flat: groups.flatMap((g) => g.items),
63
+ statusGroups: groups.filter((g) => g.items.length === 0)
52
64
  };
53
65
  }, [items]);
54
66
  }
@@ -294,7 +306,7 @@ function toRows(filtered, statusGroups) {
294
306
  const rowOfItem = [];
295
307
  for (const node of nodes) {
296
308
  const leaves = "items" in node ? node.items : [node];
297
- if ("items" in node) rows.push({
309
+ if ("items" in node && !node.headless) rows.push({
298
310
  kind: "group",
299
311
  group: node
300
312
  });
@@ -1,5 +1,5 @@
1
1
  import type { InputHTMLAttributes, ReactNode, TextareaHTMLAttributes } from 'react';
2
- import { type ComboboxItems } from './Combobox.js';
2
+ import { type ComboboxItems } from './comboboxItems.js';
3
3
  import type { MenuEntry } from './MenuEntries.js';
4
4
  interface CreateModalProps {
5
5
  open: boolean;
@@ -38,13 +38,13 @@ interface CreateModalProps {
38
38
  /** Submits on Enter in the name field or ⌘/Ctrl+Enter from anywhere in the panel. */
39
39
  export declare function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, namePlaceholder, hideName, initialName, showDescription, descriptionPlaceholder, submitLabel, submittingLabel, validateName, sanitizeName, nameHint, children, properties, advanced, submitDisabled, hideSubmit, nameOptional, onNameChange, dirty, showFavorite, onSubmit, }: CreateModalProps): import("react").JSX.Element;
40
40
  /** Labeled single-line input row for the dialog's extra fields. */
41
- export declare function CreateModalField({ label, tooltip, ...props }: InputHTMLAttributes<HTMLInputElement> & {
41
+ export declare function CreateModalField({ label, tooltip, id, ...props }: InputHTMLAttributes<HTMLInputElement> & {
42
42
  label: string;
43
43
  /** Help text surfaced via an info icon next to the label. */
44
44
  tooltip?: string | undefined;
45
45
  }): import("react").JSX.Element;
46
46
  /** Labeled multi-line field for credential blobs (JSON keys, PEM keys). */
47
- export declare function CreateModalTextarea({ label, tooltip, rows, ...props }: TextareaHTMLAttributes<HTMLTextAreaElement> & {
47
+ export declare function CreateModalTextarea({ label, tooltip, rows, id, ...props }: TextareaHTMLAttributes<HTMLTextAreaElement> & {
48
48
  label: string;
49
49
  /** Help text surfaced via an info icon next to the label. */
50
50
  tooltip?: string | undefined;
@@ -5,6 +5,7 @@ import { LuChevronRight } from "../icons/lu/LuChevronRight.js";
5
5
  import { Tooltip } from "./Tooltip.js";
6
6
  import { useStrings } from "./Provider.js";
7
7
  import { ActionMenu } from "./ActionMenu.js";
8
+ import { comboboxLeaves } from "./comboboxItems.js";
8
9
  import { Autocomplete, Combobox } from "./Combobox.js";
9
10
  import { ghostButtonClasses } from "./ghostButton.js";
10
11
  import { Modal } from "./Modal.js";
@@ -199,34 +200,53 @@ function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, namePlaceho
199
200
  }
200
201
  var fieldInputClasses = "w-full h-8 px-2.5 rounded-md border border-(--theme-border) bg-(--theme-input-bg) shadow-none text-sm text-(--theme-app) placeholder:text-(--theme-muted-text-color) transition-colors focus:outline-none focus:ring-0 focus:border-(--theme-element)";
201
202
  var fieldLabelClasses = "block mb-1 text-xs font-medium text-(--theme-muted-text-color)";
203
+ /** A field's label, with its help beside it rather than inside the label, so
204
+ * the label names the field and not the help button. */
205
+ function FieldLabel({ id, label, tooltip }) {
206
+ return /* @__PURE__ */ jsxs("div", {
207
+ className: clsx(fieldLabelClasses, "flex items-center gap-1"),
208
+ children: [/* @__PURE__ */ jsx("label", {
209
+ htmlFor: id,
210
+ children: label
211
+ }), tooltip && /* @__PURE__ */ jsx(InfoTip, { text: tooltip })]
212
+ });
213
+ }
202
214
  /** Labeled single-line input row for the dialog's extra fields. */
203
- function CreateModalField({ label, tooltip, ...props }) {
204
- return /* @__PURE__ */ jsxs("label", {
205
- className: "block mb-3",
206
- children: [/* @__PURE__ */ jsxs("span", {
207
- className: clsx(fieldLabelClasses, "flex items-center gap-1"),
208
- children: [label, /* @__PURE__ */ jsx(InfoTip, { text: tooltip ?? "" })]
215
+ function CreateModalField({ label, tooltip, id, ...props }) {
216
+ const fallbackId = useId();
217
+ const fieldId = id ?? fallbackId;
218
+ return /* @__PURE__ */ jsxs("div", {
219
+ className: "mb-3",
220
+ children: [/* @__PURE__ */ jsx(FieldLabel, {
221
+ id: fieldId,
222
+ label,
223
+ tooltip
209
224
  }), /* @__PURE__ */ jsx("input", {
210
225
  type: "text",
211
226
  autoComplete: "off",
212
227
  "data-1p-ignore": true,
213
228
  ...props,
229
+ id: fieldId,
214
230
  className: fieldInputClasses
215
231
  })]
216
232
  });
217
233
  }
218
234
  /** Labeled multi-line field for credential blobs (JSON keys, PEM keys). */
219
- function CreateModalTextarea({ label, tooltip, rows = 4, ...props }) {
220
- return /* @__PURE__ */ jsxs("label", {
221
- className: "block mb-3",
222
- children: [/* @__PURE__ */ jsxs("span", {
223
- className: clsx(fieldLabelClasses, "flex items-center gap-1"),
224
- children: [label, /* @__PURE__ */ jsx(InfoTip, { text: tooltip ?? "" })]
235
+ function CreateModalTextarea({ label, tooltip, rows = 4, id, ...props }) {
236
+ const fallbackId = useId();
237
+ const fieldId = id ?? fallbackId;
238
+ return /* @__PURE__ */ jsxs("div", {
239
+ className: "mb-3",
240
+ children: [/* @__PURE__ */ jsx(FieldLabel, {
241
+ id: fieldId,
242
+ label,
243
+ tooltip
225
244
  }), /* @__PURE__ */ jsx("textarea", {
226
245
  autoComplete: "off",
227
246
  "data-1p-ignore": true,
228
247
  rows,
229
248
  ...props,
249
+ id: fieldId,
230
250
  className: clsx(fieldInputClasses, "h-auto py-2 font-mono text-xs leading-relaxed resize-y")
231
251
  })]
232
252
  });
@@ -236,12 +256,10 @@ function CreateModalAutocompleteField({ label, tooltip, value, onValueChange, it
236
256
  const id = useId();
237
257
  return /* @__PURE__ */ jsxs("div", {
238
258
  className: "mb-3",
239
- children: [/* @__PURE__ */ jsxs("div", {
240
- className: clsx(fieldLabelClasses, "flex items-center gap-1"),
241
- children: [/* @__PURE__ */ jsx("label", {
242
- htmlFor: id,
243
- children: label
244
- }), tooltip && /* @__PURE__ */ jsx(InfoTip, { text: tooltip })]
259
+ children: [/* @__PURE__ */ jsx(FieldLabel, {
260
+ id,
261
+ label,
262
+ tooltip
245
263
  }), /* @__PURE__ */ jsx(Autocomplete, {
246
264
  id,
247
265
  items,
@@ -270,7 +288,10 @@ function CreateModalSwitch({ label, checked, onCheckedChange, tooltip }) {
270
288
  * icons, descriptions and group headings (e.g. Clusters vs Kubernetes). */
271
289
  function CreateModalSelect({ label, icon, value, items, onValueChange, disabled = false, disabledReason, tooltip }) {
272
290
  const t = useStrings().modal;
273
- const selected = leafItems(items).find((item) => item.value === value);
291
+ const selected = comboboxLeaves(items).map((item) => typeof item === "string" ? {
292
+ value: item,
293
+ label: item
294
+ } : item).find((item) => item.value === value);
274
295
  const selectedLabel = selected?.label ?? (value || void 0);
275
296
  const pillContent = /* @__PURE__ */ jsxs(Fragment, { children: [
276
297
  selected?.icon ?? icon,
@@ -307,15 +328,5 @@ function CreateModalSelect({ label, icon, value, items, onValueChange, disabled
307
328
  })
308
329
  });
309
330
  }
310
- function leafItems(items) {
311
- return items.flatMap((node) => {
312
- if (typeof node === "string") return [{
313
- value: node,
314
- label: node
315
- }];
316
- if ("items" in node) return leafItems(node.items);
317
- return [node];
318
- });
319
- }
320
331
  //#endregion
321
332
  export { CreateModal, CreateModalAutocompleteField, CreateModalField, CreateModalSelect, CreateModalSwitch, CreateModalTextarea };
@@ -21,7 +21,10 @@ export type SelectProps<Value, Multiple extends boolean | undefined = false> = O
21
21
  trigger?: ReactElement | undefined;
22
22
  /** A hint on the trigger button. */
23
23
  triggerTooltip?: ReactNode;
24
+ /** The item a person is on as they move through the open list, then undefined once it
25
+ * closes, such as to preview a theme before it is chosen. */
26
+ onItemHighlighted?: ((value: Value | undefined) => void) | undefined;
24
27
  };
25
28
  /** Picks one value, or several with `multiple`, from a short list. Use
26
29
  * Combobox when the list is long enough to need filtering. */
27
- export declare function Select<Value, Multiple extends boolean | undefined = false>({ items, placeholder, className, 'aria-label': ariaLabel, 'aria-describedby': describedBy, 'aria-invalid': invalid, trigger, triggerTooltip, ...rootProps }: SelectProps<Value, Multiple>): import("react").JSX.Element;
30
+ export declare function Select<Value, Multiple extends boolean | undefined = false>({ items, placeholder, className, 'aria-label': ariaLabel, 'aria-describedby': describedBy, 'aria-invalid': invalid, trigger, triggerTooltip, onItemHighlighted, ...rootProps }: SelectProps<Value, Multiple>): import("react").JSX.Element;
@@ -9,7 +9,7 @@ import { Select } from "@base-ui/react/select";
9
9
  var triggerClassName = clsx("flex w-full items-center justify-between gap-2 rounded-md border border-(--theme-border) bg-(--theme-input-bg) py-2 pl-3 pr-2 text-left text-sm text-(--theme-app) cursor-pointer outline-none", "focus-visible:border-(--theme-element) focus-visible:ring-1 focus-visible:ring-(--theme-element)", "aria-invalid:border-(--theme-error) data-disabled:cursor-not-allowed data-disabled:bg-(--theme-input-disabled-bg) data-disabled:text-(--theme-input-disabled-text)");
10
10
  /** Picks one value, or several with `multiple`, from a short list. Use
11
11
  * Combobox when the list is long enough to need filtering. */
12
- function Select$1({ items, placeholder, className, "aria-label": ariaLabel, "aria-describedby": describedBy, "aria-invalid": invalid, trigger, triggerTooltip, ...rootProps }) {
12
+ function Select$1({ items, placeholder, className, "aria-label": ariaLabel, "aria-describedby": describedBy, "aria-invalid": invalid, trigger, triggerTooltip, onItemHighlighted, ...rootProps }) {
13
13
  const labelOf = (value) => items.find((item) => Object.is(item.value, value))?.label;
14
14
  const triggerProps = {
15
15
  "aria-label": ariaLabel,
@@ -19,6 +19,10 @@ function Select$1({ items, placeholder, className, "aria-label": ariaLabel, "ari
19
19
  return /* @__PURE__ */ jsxs(Select.Root, {
20
20
  items,
21
21
  ...rootProps,
22
+ onOpenChange: (open, details) => {
23
+ rootProps.onOpenChange?.(open, details);
24
+ if (!open) onItemHighlighted?.(void 0);
25
+ },
22
26
  children: [trigger ? /* @__PURE__ */ jsx(Tooltip, {
23
27
  content: triggerTooltip,
24
28
  children: /* @__PURE__ */ jsx(Select.Trigger, {
@@ -50,6 +54,7 @@ function Select$1({ items, placeholder, className, "aria-label": ariaLabel, "ari
50
54
  value: item.value,
51
55
  disabled: item.disabled,
52
56
  className: clsx("grid grid-cols-[1rem_1fr]", panelItemClassName),
57
+ onFocus: onItemHighlighted && (() => onItemHighlighted(item.value)),
53
58
  children: [
54
59
  /* @__PURE__ */ jsx(Select.ItemIndicator, {
55
60
  className: "col-start-1",
@@ -0,0 +1,24 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface ComboboxItem<Value = string> {
3
+ value: Value;
4
+ label: string;
5
+ description?: string | undefined;
6
+ icon?: ReactNode;
7
+ disabled?: boolean | undefined;
8
+ }
9
+ export interface ComboboxGroup<Value = string> {
10
+ label: string;
11
+ /** Leads the heading, such as a provider's logo. */
12
+ icon?: ReactNode;
13
+ /** Follows the label, such as who shared the group or a status like "Not responding". */
14
+ description?: string | undefined;
15
+ /** Empty for a group that only reports a status; a search finds it by its heading. */
16
+ items: readonly ComboboxLeaf<Value>[];
17
+ }
18
+ /** One item: an option, or a bare string labelled by its value. */
19
+ export type ComboboxLeaf<Value = string> = ComboboxItem<Value> | (Value & string);
20
+ /** Items, groups under headings, or both: items between groups list where they sit, without a heading. */
21
+ export type ComboboxItems<Value = string> = readonly (ComboboxLeaf<Value> | ComboboxGroup<Value>)[];
22
+ export declare function isComboboxGroup<Value>(item: ComboboxLeaf<Value> | ComboboxGroup<Value>): item is ComboboxGroup<Value>;
23
+ /** Every item, out of their groups. */
24
+ export declare function comboboxLeaves<Value>(items: ComboboxItems<Value>): ComboboxLeaf<Value>[];
@@ -0,0 +1,10 @@
1
+ //#region src/components/comboboxItems.ts
2
+ function isComboboxGroup(item) {
3
+ return typeof item === "object" && "items" in item;
4
+ }
5
+ /** Every item, out of their groups. */
6
+ function comboboxLeaves(items) {
7
+ return items.flatMap((item) => isComboboxGroup(item) ? item.items : [item]);
8
+ }
9
+ //#endregion
10
+ export { comboboxLeaves, isComboboxGroup };
@@ -1,8 +1,8 @@
1
1
  import { Autocomplete, Combobox } from "../../components/Combobox.js";
2
+ import { toFieldItems } from "../utils/fieldItems.js";
2
3
  import { FieldWrapper } from "../FieldWrapper.js";
3
4
  import { useChoiceField } from "../useChoiceField.js";
4
5
  import { useDependentOptions } from "../utils/dependentOptions.js";
5
- import { toFieldItems } from "../utils/fieldItems.js";
6
6
  import { getPlaceholder } from "../utils/getPlaceholder.js";
7
7
  import { jsx } from "react/jsx-runtime";
8
8
  import { useMemo } from "react";
@@ -1,8 +1,8 @@
1
1
  import { Combobox } from "../../components/Combobox.js";
2
+ import { toFieldItems } from "../utils/fieldItems.js";
2
3
  import { FieldWrapper } from "../FieldWrapper.js";
3
4
  import { useFieldValue } from "../useFieldValue.js";
4
5
  import { useDependentOptions } from "../utils/dependentOptions.js";
5
- import { toFieldItems } from "../utils/fieldItems.js";
6
6
  import { usePrevious } from "../usePrevious.js";
7
7
  import { jsx } from "react/jsx-runtime";
8
8
  import { useEffect, useMemo } from "react";
package/dist/form/lib.js CHANGED
@@ -1,4 +1,5 @@
1
- import { applySecondaryField, findSecondaryOption } from "./utils/secondaryField.js";
1
+ import { findChoice, toFieldItems } from "./utils/fieldItems.js";
2
+ import { applySecondaryField } from "./utils/secondaryField.js";
2
3
  import { copyBounds } from "./Wizard/utils.js";
3
4
  //#region src/form/lib.ts
4
5
  var EMPTY_ENTRY = {};
@@ -107,7 +108,7 @@ function initializeValues(options, srcData = {}, blankDefault) {
107
108
  if (data[field] !== void 0) {
108
109
  acc[field] = data[field];
109
110
  if ((fieldSchema.type === "dropdown" || fieldSchema.type === "storage") && fieldSchema.secondaryField && Array.isArray(fieldSchema.options)) {
110
- const option = findSecondaryOption(fieldSchema.options, data[field]);
111
+ const option = findChoice(toFieldItems(fieldSchema.options), data[field]);
111
112
  applySecondaryField(fieldSchema.secondaryField, option?.secondaryValue, (secondaryField, secondaryValue) => {
112
113
  if (!(fieldSchema.type === "storage" && Object.hasOwn(options, secondaryField) && source[secondaryField] !== void 0)) impureSetValueFromPath(acc, secondaryField, secondaryValue);
113
114
  });
@@ -1,8 +1,6 @@
1
- import type { ComboboxItem, ComboboxItems } from '../components/Combobox.js';
2
- /** An item that also sets other fields when chosen, through `secondaryField`. */
3
- export type ChoiceItem = ComboboxItem<unknown> & {
4
- secondaryValue?: unknown;
5
- };
1
+ import type { ComboboxItems } from '../components/Combobox.js';
2
+ import { type ChoiceItem } from './utils/fieldItems.js';
3
+ export type { ChoiceItem };
6
4
  /**
7
5
  * A field that picks one of `items`: its value, and the choice the person makes, which also sets the
8
6
  * fields its `secondaryField` names from the item's `secondaryValue`.
@@ -1,13 +1,10 @@
1
+ import { itemValue, leaves } from "./utils/fieldItems.js";
1
2
  import { applySecondaryField } from "./utils/secondaryField.js";
2
3
  import { useFieldValue } from "./useFieldValue.js";
3
4
  import { useEffect, useEffectEvent, useRef } from "react";
4
5
  import { dequal } from "dequal";
5
6
  import { useFormContext } from "react-hook-form";
6
7
  //#region src/form/useChoiceField.ts
7
- function leaves(items) {
8
- return items.flatMap((item) => typeof item === "object" && "items" in item ? item.items : [item]);
9
- }
10
- var itemValue = (item) => typeof item === "string" ? item : item.value;
11
8
  /**
12
9
  * A field that picks one of `items`: its value, and the choice the person makes, which also sets the
13
10
  * fields its `secondaryField` names from the item's `secondaryValue`.
@@ -1,10 +1,32 @@
1
- import type { ComboboxItem } from '../../components/Combobox.js';
2
- /** A dropdown option as workflow YAML writes it. */
3
- export type FieldOption<Value = string> = string | {
1
+ import type { ReactNode } from 'react';
2
+ import { type ComboboxItem, type ComboboxItems } from '../../components/comboboxItems.js';
3
+ /** A choice as workflow YAML writes it, or as a host's option hook builds one. */
4
+ export interface FieldOptionItem<Value = string> {
4
5
  label: string;
5
6
  value: Value;
6
- description?: string;
7
- secondaryLabel?: string;
7
+ description?: string | undefined;
8
+ /** The older key for description. */
9
+ secondaryLabel?: string | undefined;
10
+ /** What choosing this sets the field's `secondaryField` to. */
11
+ secondaryValue?: unknown;
12
+ disabled?: boolean | undefined;
13
+ icon?: ReactNode;
14
+ }
15
+ /** Choices under a heading. */
16
+ export interface FieldOptionGroup<Value = string> {
17
+ category?: string | undefined;
18
+ options: readonly (FieldOptionItem<Value> | string)[];
19
+ }
20
+ /** A dropdown option: a bare string is a choice labelled by its value. */
21
+ export type FieldOption<Value = string> = string | FieldOptionItem<Value> | FieldOptionGroup<Value>;
22
+ /** A picker item that also sets other fields when chosen, through `secondaryField`. */
23
+ export type ChoiceItem<Value = unknown> = ComboboxItem<Value> & {
24
+ secondaryValue?: unknown;
8
25
  };
9
- /** YAML options as picker items. secondaryLabel is the older key for description. */
10
- export declare function toFieldItems<Value>(options: readonly FieldOption<Value>[]): (ComboboxItem<Value> | string)[];
26
+ /** Options as picker items, groups kept where they sit. */
27
+ export declare function toFieldItems<Value>(options: readonly FieldOption<Value>[]): ComboboxItems<Value>;
28
+ /** Every item, groups flattened. */
29
+ export declare const leaves: (items: ComboboxItems<unknown>) => (ChoiceItem | string)[];
30
+ export declare const itemValue: (item: ChoiceItem | string) => unknown;
31
+ /** The item among `items` holding `value`, with what it sets through `secondaryField`. */
32
+ export declare function findChoice(items: ComboboxItems<unknown>, value: unknown): ChoiceItem | undefined;
@@ -1,11 +1,32 @@
1
+ import { comboboxLeaves } from "../../components/comboboxItems.js";
2
+ import { dequal } from "dequal";
1
3
  //#region src/form/utils/fieldItems.ts
2
- /** YAML options as picker items. secondaryLabel is the older key for description. */
4
+ var isGroup = (option) => typeof option === "object" && "options" in option;
5
+ function toItem(option) {
6
+ if (typeof option === "string") return option;
7
+ const { label, value, description, secondaryLabel, secondaryValue, disabled, icon } = option;
8
+ return {
9
+ label,
10
+ value,
11
+ description: description ?? secondaryLabel,
12
+ disabled,
13
+ icon,
14
+ ...secondaryValue === void 0 ? {} : { secondaryValue }
15
+ };
16
+ }
17
+ /** Options as picker items, groups kept where they sit. */
3
18
  function toFieldItems(options) {
4
- return options.map((option) => typeof option === "string" ? option : {
5
- label: option.label,
6
- value: option.value,
7
- description: option.description ?? option.secondaryLabel
8
- });
19
+ return options.map((option) => isGroup(option) ? {
20
+ label: option.category ?? "",
21
+ items: option.options.map(toItem)
22
+ } : toItem(option));
23
+ }
24
+ /** Every item, groups flattened. */
25
+ var leaves = (items) => comboboxLeaves(items);
26
+ var itemValue = (item) => typeof item === "string" ? item : item.value;
27
+ /** The item among `items` holding `value`, with what it sets through `secondaryField`. */
28
+ function findChoice(items, value) {
29
+ return leaves(items).find((item) => typeof item === "object" && dequal(item.value, value));
9
30
  }
10
31
  //#endregion
11
- export { toFieldItems };
32
+ export { findChoice, itemValue, leaves, toFieldItems };
@@ -53,11 +53,16 @@ export type CopySubmenu = (fields: CopyFields) => MenuSubmenu;
53
53
  /** Returns a builder for the Copy submenu, labeled from the provider's strings. */
54
54
  export declare function useCopySubmenu(): CopySubmenu;
55
55
  /** Opens a row's menu under itself. Shows on the row's hover or focus, and
56
- * stays while its menu is `open`, though the pointer has left the row. */
57
- export declare function MoreButton({ onOpen, open, visible, className, }: {
56
+ * stays while its menu is `open`, though the pointer has left the row. A touch
57
+ * screen has no hover, so there it always shows: the row's menu is otherwise
58
+ * out of reach. */
59
+ export declare function MoreButton({ onOpen, open, visible, label, className, }: {
58
60
  onOpen: (button: HTMLElement) => void;
59
61
  open?: boolean;
60
62
  /** Shows at rest too. */
61
63
  visible?: boolean;
64
+ /** Names the row the menu acts on, as "More actions for Core", for a list
65
+ * of rows that would otherwise each read "More actions". */
66
+ label?: string | undefined;
62
67
  className?: string | undefined;
63
68
  }): import("react").JSX.Element;
@@ -114,19 +114,21 @@ function useCopySubmenu() {
114
114
  }, [t]);
115
115
  }
116
116
  /** Opens a row's menu under itself. Shows on the row's hover or focus, and
117
- * stays while its menu is `open`, though the pointer has left the row. */
118
- function MoreButton({ onOpen, open = false, visible = open, className }) {
117
+ * stays while its menu is `open`, though the pointer has left the row. A touch
118
+ * screen has no hover, so there it always shows: the row's menu is otherwise
119
+ * out of reach. */
120
+ function MoreButton({ onOpen, open = false, visible = open, label, className }) {
119
121
  const { list: t } = useStrings();
120
122
  return /* @__PURE__ */ jsx("button", {
121
123
  type: "button",
122
- "aria-label": t.moreActions,
124
+ "aria-label": label ?? t.moreActions,
123
125
  "aria-haspopup": "menu",
124
126
  "aria-expanded": open,
125
127
  onClick: (e) => {
126
128
  e.stopPropagation();
127
129
  onOpen(e.currentTarget);
128
130
  },
129
- className: clsx("rounded p-1 cursor-pointer text-(--theme-muted-text-color) transition-colors hover:bg-(--theme-muted-panel-bg) hover:text-(--theme-app) focus-visible:opacity-100", visible ? "opacity-100" : "opacity-0 group-hover:opacity-100", className),
131
+ className: clsx("rounded p-1 cursor-pointer text-(--theme-muted-text-color) transition-colors hover:bg-(--theme-muted-panel-bg) hover:text-(--theme-app) focus-visible:opacity-100", visible ? "opacity-100" : "opacity-0 group-hover:opacity-100 pointer-coarse:opacity-100", className),
130
132
  children: /* @__PURE__ */ jsx(BsThreeDots, { className: "h-4 w-4" })
131
133
  });
132
134
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parallelworks/ui",
3
- "version": "0.42.0",
3
+ "version": "0.44.0",
4
4
  "description": "React UI components: a theme contract, icons, primitives, lists, forms, a job graph, a code editor, a log viewer, a file explorer and an AI chat, each on its own subpath",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {