@antadesign/anta 0.3.5 → 0.3.6

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.
@@ -22,6 +22,16 @@ export declare function nativeStateChange<D>(e: CustomEvent<D> | {
22
22
  event: CustomEvent<D>;
23
23
  detail?: D;
24
24
  };
25
+ export declare const IS_MAC: boolean;
26
+ /** Default row hint teaching the Alt/⌥-click "select only this" accelerator on
27
+ * bulk-select rows (`Select` multiple, `SelectFaceted` multiple facets). Shared
28
+ * so the wording stays identical across both. */
29
+ export declare const ISOLATE_HINT: string;
30
+ /** Parse an open/closed `state` / `default-state` attribute — the shared
31
+ * open-state vocabulary (`a-dialog`, `a-expander`, …). Anything but the literal
32
+ * `'open'` is `'closed'`, so an absent / malformed attribute reads closed. Kept
33
+ * here so the two elements can't drift on how the token is parsed. */
34
+ export declare function parseOpenState(v: string | null): 'open' | 'closed';
25
35
  /** The six named tones every toned component shares. Anything else is a literal
26
36
  * CSS colour the element resolves through its `--{component}-tone-source` var. */
27
37
  export declare const NAMED_TONES: Set<string>;
@@ -16,6 +16,11 @@ function nativeStateChange(e) {
16
16
  const event = "nativeEvent" in e ? e.nativeEvent : e;
17
17
  return { event, detail: event?.detail };
18
18
  }
19
+ const IS_MAC = typeof navigator !== "undefined" && /Mac|iPhone|iPad/i.test(navigator.userAgent || "");
20
+ const ISOLATE_HINT = IS_MAC ? "\u2325+Click to select only this" : "Alt+Click to select only this";
21
+ function parseOpenState(v) {
22
+ return v === "open" ? "open" : "closed";
23
+ }
19
24
  const NAMED_TONES = /* @__PURE__ */ new Set([
20
25
  "brand",
21
26
  "neutral",
@@ -105,6 +110,8 @@ class SelectableChildElement extends HTMLElementBase {
105
110
  }
106
111
  export {
107
112
  HTMLElementBase,
113
+ ISOLATE_HINT,
114
+ IS_MAC,
108
115
  NAMED_TONES,
109
116
  SelectableChildElement,
110
117
  anchorRect,
@@ -112,6 +119,7 @@ export {
112
119
  isInsideOpenMenu,
113
120
  isMenuOpen,
114
121
  nativeStateChange,
122
+ parseOpenState,
115
123
  roundStyle,
116
124
  setMenuPresence,
117
125
  toneStyle,
@@ -0,0 +1,92 @@
1
+ import type { BaseProps } from "../general_types";
2
+ /** Public props for the `<Dialog>` modal / drawer. `header` and `footer` are the
3
+ * chrome zones; `children` is the (scrollable) body. */
4
+ export interface DialogProps extends Omit<BaseProps, "title"> {
5
+ /** Header content: a title, or any node. Rendered in the top zone. Omit for a
6
+ * chromeless dialog with only a body. */
7
+ header?: React.ReactNode;
8
+ /** Footer content, usually the action buttons. Rendered in the bottom zone as
9
+ * a right-aligned row (wraps under pressure). Omit for none. */
10
+ footer?: React.ReactNode;
11
+ /** The dialog body. Scrolls when it overflows the available height. */
12
+ children?: React.ReactNode;
13
+ /** Placement. `center` (the default) is a centered modal; `left` / `right` /
14
+ * `top` / `bottom` turn it into an edge drawer (full height for left/right,
15
+ * full width for top/bottom); `fullscreen` fills the whole viewport with no
16
+ * edge gap or corner radius.
17
+ * @defaultValue 'center' */
18
+ position?: "center" | "left" | "right" | "top" | "bottom" | "fullscreen";
19
+ /** Whether the top-right ✕ button is present. It's one way to close the dialog,
20
+ * alongside Esc, the backdrop, a `data-dialog-close` / footer action, and your
21
+ * own code; `false` just removes the ✕, it doesn't make the dialog un-closable.
22
+ * @defaultValue true */
23
+ closable?: boolean;
24
+ /** Turn off light-dismiss: a backdrop click and Esc no longer close the dialog.
25
+ * It stays closable through explicit controls (the ✕ when `closable`, a footer
26
+ * action, your own code); `persistent` isn't "un-closable", it stops an
27
+ * accidental click or stray Esc from dismissing. For an alert / confirm that
28
+ * should be answered deliberately. Omit for the default dismissible behavior. */
29
+ persistent?: boolean;
30
+ /** Corner radius. A side keyword — `'top'` / `'right'` / `'bottom'` / `'left'` —
31
+ * rounds only that edge's two corners (at `--dialog-radius`), which pairs with
32
+ * a drawer's exposed edge (a bottom sheet → `'top'`, a right drawer → `'left'`).
33
+ * A `number` (px) or CSS string rounds all corners at that value; the string
34
+ * may be a full `border-radius` shorthand (`'12px 12px 0 0'`). `true` rounds all
35
+ * corners at `--dialog-radius`. Omit for the position default: `center` is
36
+ * rounded, drawers and `fullscreen` are square. */
37
+ round?: boolean | number | "top" | "right" | "bottom" | "left" | (string & {});
38
+ /** Controlled open state. When provided, the consumer owns open/close: the
39
+ * dialog only follows this prop, and every user dismiss (Esc, backdrop, close
40
+ * button) *requests* a change via `onStateChange` (reject by not updating).
41
+ * Leave undefined for uncontrolled. */
42
+ open?: boolean;
43
+ /** Initial open state for the uncontrolled case (read once on mount). */
44
+ defaultOpen?: boolean;
45
+ /** Uncontrolled trigger name. Any element with `data-dialog-open="{name}"`
46
+ * opens this dialog, `data-dialog-close="{name}"` closes it. A convenience for
47
+ * triggers rendered elsewhere; ignored in controlled mode. */
48
+ name?: string;
49
+ /** Fired before the open state changes, on every user open or dismiss.
50
+ * `event` is the cancelable `statechange`: call `event.preventDefault()` to
51
+ * veto an *uncontrolled* transition. `detail.next` is the requested open
52
+ * state, `detail.prev` the current one (booleans). In controlled mode, apply
53
+ * `detail.next` to `open` to accept, or do nothing to reject. */
54
+ onStateChange?: (event: CustomEvent, detail: {
55
+ next: boolean;
56
+ prev: boolean;
57
+ }) => void;
58
+ }
59
+ /**
60
+ * `<Dialog>` — a modal dialog (or edge drawer, via `position`), built on a
61
+ * native `<dialog>` inside `<a-dialog>` — so the top layer, focus trap, focus
62
+ * return, backdrop, and Esc all come from the platform.
63
+ *
64
+ * A pure, stateless pass-through: the element owns all interaction (open/close,
65
+ * dismiss, focus), so the wrapper holds no state and grabs no ref — it only maps
66
+ * props to attributes and drops content into the `header` / body / `footer` /
67
+ * `close` slots.
68
+ *
69
+ * Uncontrolled (`defaultOpen`, or the `name` trigger convenience): the element
70
+ * owns open/close. Controlled (`open` + `onStateChange`): the wrapper emits
71
+ * `state="open"|"closed"`; the element treats the attribute as the source of
72
+ * truth, and every user dismiss only *requests* a change (a consumer can reject
73
+ * it). See STATEFUL-COMPONENTS.md.
74
+ *
75
+ * Requires `@antadesign/anta/elements` to be imported (client-side only) to
76
+ * register the underlying custom element.
77
+ *
78
+ * @example Controlled
79
+ * ```tsx
80
+ * const [open, setOpen] = useState(false)
81
+ * <Button onClick={() => setOpen(true)}>Open</Button>
82
+ * <Dialog
83
+ * open={open}
84
+ * onStateChange={(_e, { next }) => setOpen(next)}
85
+ * header="Delete project?"
86
+ * footer={<><Button onClick={() => setOpen(false)}>Cancel</Button><Button priority="primary" tone="critical">Delete</Button></>}
87
+ * >
88
+ * This action cannot be undone.
89
+ * </Dialog>
90
+ * ```
91
+ */
92
+ export declare const Dialog: ({ header, footer, position, closable, persistent, round, open, defaultOpen, name, onStateChange, className, style, children, ...rest }: DialogProps) => any;
@@ -0,0 +1,71 @@
1
+ import { jsx, jsxs } from "@antadesign/anta/jsx-runtime";
2
+ import { nativeStateChange, roundStyle } from "../anta_helpers";
3
+ import { Button } from "./Button";
4
+ const ROUND_SIDES = /* @__PURE__ */ new Set(["top", "right", "bottom", "left"]);
5
+ const Dialog = ({
6
+ header,
7
+ footer,
8
+ position,
9
+ closable,
10
+ persistent,
11
+ round,
12
+ open,
13
+ defaultOpen,
14
+ name,
15
+ onStateChange,
16
+ className,
17
+ style,
18
+ children,
19
+ ...rest
20
+ }) => {
21
+ const controlled = open !== void 0;
22
+ const roundSide = typeof round === "string" && ROUND_SIDES.has(round) ? round : void 0;
23
+ return /* @__PURE__ */ jsxs(
24
+ "a-dialog",
25
+ {
26
+ state: controlled ? open ? "open" : "closed" : void 0,
27
+ "default-state": !controlled && defaultOpen ? "open" : void 0,
28
+ position: position && position !== "center" ? position : void 0,
29
+ persistent: persistent ? "" : void 0,
30
+ round: roundSide ?? (round ? "" : void 0),
31
+ name: !controlled ? name : void 0,
32
+ onstatechange: onStateChange ? (e) => {
33
+ const { event, detail } = nativeStateChange(e);
34
+ if (detail)
35
+ onStateChange(event, {
36
+ next: detail.next === "open",
37
+ prev: detail.prev === "open"
38
+ });
39
+ } : void 0,
40
+ class: className,
41
+ style: roundStyle(roundSide ? void 0 : round, "--dialog-round", style),
42
+ ...rest,
43
+ children: [
44
+ closable !== false && // A real <a-button> (light DOM → fully styled, keyboard-focusable) in the
45
+ // element's `close` slot. It fires the bubbling `closerequest` event via
46
+ // a-button's global listener, so closing works even without framework
47
+ // hydration; the element turns that into a close request.
48
+ // CONTRACT: `data-custom-event` below MUST match `CLOSE_TRIGGER` in the
49
+ // element (src/elements/a-dialog.ts). Duplicated, not shared — importing
50
+ // the element module here would self-register it and break the
51
+ // wrapper/element decoupling. Rename in both places.
52
+ /* @__PURE__ */ jsx("span", { slot: "close", style: { display: "contents" }, children: /* @__PURE__ */ jsx(
53
+ Button,
54
+ {
55
+ priority: "tertiary",
56
+ size: "large",
57
+ icon: "x",
58
+ "aria-label": "Close",
59
+ "data-custom-event": "closerequest"
60
+ }
61
+ ) }),
62
+ header != null && /* @__PURE__ */ jsx("div", { slot: "header", children: header }),
63
+ children,
64
+ footer != null && /* @__PURE__ */ jsx("span", { slot: "footer", style: { display: "contents" }, children: footer })
65
+ ]
66
+ }
67
+ );
68
+ };
69
+ export {
70
+ Dialog
71
+ };
@@ -146,6 +146,9 @@ export interface InputProps extends BaseProps, DOMEventHandlers {
146
146
  onFocus?: (e: any) => void;
147
147
  /** Fires when the field loses focus. */
148
148
  onBlur?: (e: any) => void;
149
+ /** ARIA `role` for the field — e.g. `combobox` when the input drives a
150
+ * suggestion `listbox` (see `InputAutocomplete`). Left unset by default. */
151
+ role?: string;
149
152
  }
150
153
  /**
151
154
  * `<Input>` — a text field. Renders an `<a-input>` web component whose real
@@ -0,0 +1,69 @@
1
+ import type { BaseProps } from '../general_types';
2
+ import type { IconShape } from '../elements/a-icon.shapes';
3
+ import type { SelectOption } from './Select';
4
+ export interface InputAutocompleteProps extends Omit<BaseProps, 'children'> {
5
+ /** The suggestions: bare strings or `SelectOption`s (`value` / `label` / `hint` /
6
+ * `icon` / `tone` / `disabled`). A flat list; picking one fills the field. */
7
+ suggestions: (string | SelectOption)[];
8
+ /** Controlled value: the field's text, a **free** string not constrained to a
9
+ * suggestion. Leave undefined for uncontrolled. */
10
+ value?: string;
11
+ /** Initial value for the uncontrolled case (the wrapper then owns it). */
12
+ defaultValue?: string;
13
+ /** Fires as the text changes (typing, picking a suggestion, or clearing) with
14
+ * the new field text. */
15
+ onValueChange?: (value: string) => void;
16
+ /** Fires only when a suggestion is chosen, with that option. */
17
+ onSelect?: (option: SelectOption) => void;
18
+ /** How suggestions match the text. `true` uses the built-in case-insensitive
19
+ * substring match on `value` / `label` / `hint`; a function `(option, query) =>
20
+ * boolean` does custom matching; `false` shows `suggestions` verbatim so you
21
+ * can filter them yourself (async / remote).
22
+ * @defaultValue true */
23
+ filter?: boolean | ((option: SelectOption, query: string) => boolean);
24
+ /** Placeholder text. */
25
+ placeholder?: string;
26
+ /** Field size.
27
+ * @defaultValue medium */
28
+ size?: 'small' | 'medium' | 'large';
29
+ /** Validation status. */
30
+ status?: 'neutral' | 'brand' | 'info' | 'success' | 'warning' | 'critical';
31
+ /** Status icon override, or `false` to drop it. */
32
+ statusIcon?: IconShape | (string & {}) | false;
33
+ /** Custom accent colour — a named tone or any CSS colour. */
34
+ tone?: string;
35
+ /** Show a clear button while the field has text. */
36
+ clearable?: boolean;
37
+ /** Form field name. */
38
+ name?: string;
39
+ /** Disable the field. */
40
+ disabled?: boolean;
41
+ /** Field label. */
42
+ label?: React.ReactNode;
43
+ /** Hint under the field. */
44
+ hint?: React.ReactNode;
45
+ /** Leading adornment inside the field. */
46
+ leading?: React.ReactNode;
47
+ /** Trailing adornment inside the field. */
48
+ trailing?: React.ReactNode;
49
+ }
50
+ /**
51
+ * `<InputAutocomplete>` — a text field whose value is a free string, with a
52
+ * suggestion dropdown that assists without constraining. Typing something not in
53
+ * the list is valid and stays the value; picking a suggestion just fills the
54
+ * field. For a value that MUST be one of the options, use `Select` with `filter`.
55
+ *
56
+ * Controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`).
57
+ * Requires `@antadesign/anta/elements` (client-side only).
58
+ *
59
+ * @example
60
+ * ```tsx
61
+ * <InputAutocomplete
62
+ * label="City"
63
+ * suggestions={['Berlin', 'Bogotá', 'Boston']}
64
+ * value={city}
65
+ * onValueChange={setCity}
66
+ * />
67
+ * ```
68
+ */
69
+ export declare const InputAutocomplete: (props: InputAutocompleteProps) => any;
@@ -0,0 +1,95 @@
1
+ import { Fragment, jsx, jsxs } from "@antadesign/anta/jsx-runtime";
2
+ import { useState, useMemo, useId } from "../jsx-runtime";
3
+ import { nativeStateChange } from "../anta_helpers";
4
+ import { normalizeOpt, matchQueryRegex, matchesQuery, highlight } from "./select-options";
5
+ import { Input } from "./Input";
6
+ import { Menu } from "./Menu";
7
+ import { MenuItem } from "./MenuItem";
8
+ const InputAutocomplete = (props) => {
9
+ const {
10
+ suggestions,
11
+ value,
12
+ defaultValue,
13
+ onValueChange,
14
+ onSelect,
15
+ filter = true,
16
+ ...rest
17
+ } = props;
18
+ const rid = useId();
19
+ const controlled = value !== void 0;
20
+ const [internal, setInternal] = useState(defaultValue ?? "");
21
+ const text = controlled ? value : internal;
22
+ const [open, setOpen] = useState(false);
23
+ const [activeId, setActiveId] = useState(null);
24
+ const options = useMemo(() => suggestions.map(normalizeOpt), [suggestions]);
25
+ const custom = typeof filter === "function";
26
+ const queryRe = filter !== false && !custom ? matchQueryRegex(text) : null;
27
+ const visible = custom ? options.filter((o) => filter(o, text)) : options.filter((o) => matchesQuery(o, queryRe));
28
+ const menuOpen = open && visible.length > 0;
29
+ const setText = (next) => {
30
+ if (!controlled) setInternal(next);
31
+ onValueChange?.(next);
32
+ };
33
+ const pick = (opt) => {
34
+ setText(opt.value);
35
+ onSelect?.(opt);
36
+ setOpen(false);
37
+ };
38
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
39
+ /* @__PURE__ */ jsx(
40
+ Input,
41
+ {
42
+ ...rest,
43
+ value: text,
44
+ "data-menu-search": "",
45
+ role: "combobox",
46
+ "aria-haspopup": "listbox",
47
+ "aria-controls": `${rid}-list`,
48
+ "aria-expanded": menuOpen ? "true" : "false",
49
+ "aria-autocomplete": "list",
50
+ "aria-activedescendant": menuOpen && activeId ? activeId : void 0,
51
+ onFocus: () => setOpen(true),
52
+ onInput: (e) => {
53
+ setText(e.currentTarget.value);
54
+ setOpen(true);
55
+ },
56
+ onKeyDown: (e) => {
57
+ if (e.key === "Enter") setOpen(false);
58
+ }
59
+ }
60
+ ),
61
+ /* @__PURE__ */ jsx(
62
+ Menu,
63
+ {
64
+ id: `${rid}-list`,
65
+ role: "listbox",
66
+ open: menuOpen,
67
+ onStateChange: (_e, { next }) => {
68
+ setOpen(next);
69
+ if (!next) setActiveId(null);
70
+ },
71
+ onactivedescendant: (e) => setActiveId(nativeStateChange(e).detail?.id ?? null),
72
+ children: visible.map((opt, i) => /* @__PURE__ */ jsx(
73
+ MenuItem,
74
+ {
75
+ id: `${rid}-o${i}`,
76
+ role: "option",
77
+ "aria-selected": activeId === `${rid}-o${i}` ? "true" : "false",
78
+ icon: opt.icon,
79
+ label: highlight(opt.label ?? opt.value, queryRe),
80
+ hint: opt.hint ? highlight(opt.hint, queryRe) : opt.hint,
81
+ tone: opt.tone,
82
+ disabled: opt.disabled,
83
+ value: opt.value,
84
+ onMouseDown: (e) => e.preventDefault(),
85
+ onSelect: () => pick(opt)
86
+ },
87
+ `${rid}-${opt.value}-${i}`
88
+ ))
89
+ }
90
+ )
91
+ ] });
92
+ };
93
+ export {
94
+ InputAutocomplete
95
+ };
@@ -58,6 +58,9 @@ export interface MenuProps extends BaseProps {
58
58
  onactivedescendant?: (e: CustomEvent<{
59
59
  id: string | null;
60
60
  }>) => void;
61
+ /** ARIA role of the menu surface.
62
+ * @defaultValue menu */
63
+ role?: string;
61
64
  /** The menu's contents: `MenuItem`, `MenuSeparator`, `MenuGroup`, or any
62
65
  * custom element. */
63
66
  children?: React.ReactNode;
@@ -88,4 +91,4 @@ export interface MenuProps extends BaseProps {
88
91
  * </Menu>
89
92
  * ```
90
93
  */
91
- export declare const Menu: ({ placement, context, coord, nohover, offset, autoWidth, open, onStateChange, round, className, style, children, ...rest }: MenuProps) => any;
94
+ export declare const Menu: ({ placement, context, coord, nohover, offset, autoWidth, open, onStateChange, round, role, className, style, children, ...rest }: MenuProps) => any;
@@ -10,6 +10,7 @@ const Menu = ({
10
10
  open,
11
11
  onStateChange,
12
12
  round,
13
+ role = "menu",
13
14
  className,
14
15
  style,
15
16
  children,
@@ -34,7 +35,7 @@ const Menu = ({
34
35
  });
35
36
  } : void 0,
36
37
  round: round ? "" : void 0,
37
- role: "menu",
38
+ role,
38
39
  "aria-orientation": "vertical",
39
40
  class: className,
40
41
  style: roundStyle(round, "--menu-round", style),
@@ -75,6 +75,14 @@ export interface MenuItemProps extends BaseProps {
75
75
  value?: string | number;
76
76
  label?: React.ReactNode;
77
77
  }) => void;
78
+ /** Raw `mousedown` on the row. Mainly to `preventDefault()` so the row doesn't
79
+ * take focus on a mouse press — e.g. a combobox option keeping focus in its
80
+ * input field while the click still selects. */
81
+ onMouseDown?: (event: any) => void;
82
+ /** ARIA role override. Defaults to the role implied by `selectionIndicator`
83
+ * (`menuitem` / `menuitemcheckbox` / `menuitemradio`); set it to reparent the
84
+ * row under a different container role — e.g. `option` inside a `listbox`. */
85
+ role?: string;
78
86
  /** Item content. With `label` set, children are extra content — most
79
87
  * notably the nested `<Menu>` for a submenu parent. */
80
88
  children?: React.ReactNode;
@@ -99,4 +107,4 @@ export interface MenuItemProps extends BaseProps {
99
107
  * </MenuItem>
100
108
  * ```
101
109
  */
102
- export declare const MenuItem: ({ icon, label, hint, kbd, iconTrailing, disabled, selected, selectionIndicator, indeterminate, indicator, tone, toneSelected, submenu, value, onSelect, className, style, children, ...rest }: MenuItemProps) => any;
110
+ export declare const MenuItem: ({ icon, label, hint, kbd, iconTrailing, disabled, selected, selectionIndicator, indeterminate, indicator, tone, toneSelected, submenu, value, onSelect, role: roleOverride, className, style, children, ...rest }: MenuItemProps) => any;
@@ -16,6 +16,7 @@ const MenuItem = ({
16
16
  submenu,
17
17
  value,
18
18
  onSelect,
19
+ role: roleOverride,
19
20
  className,
20
21
  style,
21
22
  children,
@@ -30,7 +31,7 @@ const MenuItem = ({
30
31
  return /* @__PURE__ */ jsxs(
31
32
  "a-menu-item",
32
33
  {
33
- role,
34
+ role: roleOverride ?? role,
34
35
  tabIndex: 0,
35
36
  disabled: disabled ? "" : void 0,
36
37
  selected: keepTint ? "" : void 0,
@@ -176,9 +176,12 @@ export interface SelectCommonProps extends Omit<BaseProps, 'children'> {
176
176
  * option's **value / label / hint**. Pass a **function** `(option, query) =>
177
177
  * boolean` for custom matching (called per option; return `true` to keep it). */
178
178
  filter?: boolean | ((option: SelectOption, query: string) => boolean);
179
- /** `multiple` only: add a "Select all" row at the top that toggles every enabled
179
+ /** `multiple` only: a "Select all" row at the top that toggles every enabled
180
180
  * option (the currently-visible ones when a `filter` query is active); its box
181
- * shows the mixed state when only some are selected. */
181
+ * shows the mixed state when only some are selected. On by default in
182
+ * `multiple` mode — set `false` to drop the row (and with it the Alt/⌥-click
183
+ * isolate accelerator it gates).
184
+ * @defaultValue true */
182
185
  selectAll?: boolean;
183
186
  /** Label for the `selectAll` row.
184
187
  * @defaultValue Select all */
@@ -1,5 +1,7 @@
1
1
  import { Fragment, jsx, jsxs } from "@antadesign/anta/jsx-runtime";
2
2
  import { useState, useId } from "../jsx-runtime";
3
+ import { ISOLATE_HINT } from "../anta_helpers";
4
+ import { normalizeOpt, matchQueryRegex, matchesQuery, highlight } from "./select-options";
3
5
  import { Input } from "./Input";
4
6
  import { Icon } from "./Icon";
5
7
  import { Menu } from "./Menu";
@@ -8,8 +10,6 @@ import { MenuGroup } from "./MenuGroup";
8
10
  import { MenuSeparator } from "./MenuSeparator";
9
11
  import { Tooltip } from "./Tooltip";
10
12
  import styles from "./Select.module.css";
11
- const IS_MAC = typeof navigator !== "undefined" && /Mac|iPhone|iPad/i.test(navigator.userAgent || "");
12
- const normalize = (o) => typeof o === "string" ? { value: o, label: o } : o;
13
13
  function optionsWithSelection(options, values) {
14
14
  const set = new Set(values == null ? [] : Array.isArray(values) ? values : [values]);
15
15
  const state = (on, total) => on === 0 ? "none" : on === total ? "all" : "some";
@@ -28,7 +28,7 @@ function optionsWithSelection(options, values) {
28
28
  const total = children.reduce((n, c) => n + c.total, 0);
29
29
  return { node: { ...g, options: children.map((c) => c.node), selectionState: state(on, total) }, on, total };
30
30
  }
31
- const o = normalize(raw);
31
+ const o = normalizeOpt(raw);
32
32
  const selected = set.has(o.value);
33
33
  return { node: { ...o, selected }, on: selected ? 1 : 0, total: 1 };
34
34
  });
@@ -54,7 +54,7 @@ const Select = (props) => {
54
54
  disabled,
55
55
  toneSelected,
56
56
  filter,
57
- selectAll,
57
+ selectAll = true,
58
58
  selectAllLabel = "Select all",
59
59
  clearable,
60
60
  clearLabel = "Clear",
@@ -85,7 +85,7 @@ const Select = (props) => {
85
85
  if (typeof raw !== "string" && isSubmenu(raw)) collectLeaves(raw.submenu, raw.label, disabled2 || !!raw.disabled, out);
86
86
  else if (typeof raw !== "string" && isGroup(raw)) collectLeaves(raw.options, raw.label, disabled2 || !!raw.disabled, out);
87
87
  else {
88
- const o = normalize(raw);
88
+ const o = normalizeOpt(raw);
89
89
  out.push({ opt: o, disabled: disabled2 || !!o.disabled, group });
90
90
  }
91
91
  }
@@ -101,28 +101,11 @@ const Select = (props) => {
101
101
  const byValue = new Map(allLeaves.map((l) => [l.opt.value, l.opt]));
102
102
  const filtering = filter !== void 0 && filter !== false;
103
103
  const q = query.trim();
104
- const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
105
- const queryRe = filtering && typeof filter !== "function" && q ? new RegExp(q.split(/\s+/).map(escapeRe).join("\\s+"), "i") : null;
106
- const matches = (o) => typeof filter === "function" ? filter(o, q) : !queryRe || [o.value, o.label ?? "", o.hint ?? ""].some((s) => queryRe.test(s));
104
+ const queryRe = filtering && typeof filter !== "function" ? matchQueryRegex(q) : null;
105
+ const matches = (o) => typeof filter === "function" ? filter(o, q) : matchesQuery(o, queryRe);
107
106
  const prune = typeof filter === "function" || filtering && !!q;
108
107
  const visibleLeaves = prune ? allLeaves.filter((l) => matches(l.opt)) : allLeaves;
109
108
  const flattening = filtering && !!q;
110
- const highlight = (text) => {
111
- if (!queryRe) return text;
112
- const re = new RegExp(queryRe.source, "gi");
113
- const out = [];
114
- let last = 0;
115
- let m;
116
- while ((m = re.exec(text)) !== null) {
117
- if (m.index > last) out.push(text.slice(last, m.index));
118
- out.push(/* @__PURE__ */ jsx("b", { children: m[0] }, out.length));
119
- last = m.index + m[0].length;
120
- if (m[0].length === 0) re.lastIndex++;
121
- }
122
- if (out.length === 0) return text;
123
- if (last < text.length) out.push(text.slice(last));
124
- return out;
125
- };
126
109
  const selectedValues = Array.isArray(currentRaw) ? currentRaw : currentRaw != null ? [currentRaw] : [];
127
110
  const isSelected = (v) => selectedValues.includes(v);
128
111
  const selectedOptions = selectedValues.map((v) => byValue.get(v)).filter(Boolean);
@@ -171,7 +154,7 @@ const Select = (props) => {
171
154
  const custom = renderOption?.(o, optState);
172
155
  const customMark = renderIndicator?.(optState);
173
156
  const ariaSelectable = !multiple && mark === "none" ? { role: "menuitemradio", "aria-checked": isSelected(o.value) ? "true" : "false" } : void 0;
174
- const isolateHint = multiple && selectAll && !disabled2 ? IS_MAC ? "\u2325+Click to select only this" : "Alt+Click to select only this" : void 0;
157
+ const isolateHint = multiple && selectAll && !disabled2 ? ISOLATE_HINT : void 0;
175
158
  const tip = o.tooltip ?? isolateHint;
176
159
  const hintOnly = o.tooltip == null && isolateHint != null;
177
160
  return /* @__PURE__ */ jsxs(
@@ -181,8 +164,8 @@ const Select = (props) => {
181
164
  selectionIndicator: menuItemIndicator,
182
165
  ...ariaSelectable,
183
166
  indicator: customMark ?? void 0,
184
- label: custom ? void 0 : queryRe ? highlight(o.label ?? o.value) : o.label ?? o.value,
185
- hint: custom ? void 0 : queryRe && o.hint ? highlight(o.hint) : o.hint,
167
+ label: custom ? void 0 : highlight(o.label ?? o.value, queryRe),
168
+ hint: custom ? void 0 : o.hint ? highlight(o.hint, queryRe) : o.hint,
186
169
  icon: custom ? void 0 : o.icon,
187
170
  tone: o.tone,
188
171
  toneSelected,
@@ -227,7 +210,7 @@ const Select = (props) => {
227
210
  if (inner.length) out.push(/* @__PURE__ */ jsx(MenuGroup, { label: raw.label, children: inner }, `grp-${i}-${raw.label}`));
228
211
  return;
229
212
  }
230
- const o = normalize(raw);
213
+ const o = normalizeOpt(raw);
231
214
  if (typeof filter === "function" && !matches(o)) return;
232
215
  out.push(renderOptionRow(o, disabled2 || !!o.disabled));
233
216
  });
@@ -36,7 +36,9 @@ export interface SelectFacetMultiple extends FacetBase {
36
36
  options: SelectItem[];
37
37
  /** Add a search field atop this facet's flyout that filters its options. */
38
38
  filter?: FacetFilter;
39
- /** Add a "Select all" row that toggles every option. */
39
+ /** A "Select all" row that toggles every option. On by default — set `false`
40
+ * to drop it.
41
+ * @defaultValue true */
40
42
  selectAll?: boolean;
41
43
  /** Label for the `selectAll` row.
42
44
  * @defaultValue Select all */
@@ -129,6 +131,10 @@ export interface SelectFacetedProps extends Omit<BaseProps, 'children'> {
129
131
  priority?: 'primary' | 'secondary';
130
132
  /** Disable the whole control. */
131
133
  disabled?: boolean;
134
+ /** Tone applied to a selected option row in the facet flyouts (label, selected
135
+ * tint, and the check / checkbox indicator). A named tone or a custom CSS
136
+ * colour, matching `Select`'s `toneSelected`. Defaults to a neutral selection. */
137
+ toneSelected?: 'neutral' | 'brand' | 'info' | 'success' | 'warning' | 'critical' | (string & {});
132
138
  /** Add a single search field to the top of the root menu that flattens every
133
139
  * `single` / `multiple` facet's options into one list and searches across them
134
140
  * — typing "alice" surfaces it under both an Assignee and an Owner facet, each