@antadesign/anta 0.3.3 → 0.3.5

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 (50) hide show
  1. package/dist/anta_helpers.d.ts +0 -4
  2. package/dist/anta_helpers.js +0 -7
  3. package/dist/components/Calendar.d.ts +9 -3
  4. package/dist/components/Calendar.js +24 -7
  5. package/dist/components/InputDate.d.ts +9 -6
  6. package/dist/components/InputDate.js +83 -143
  7. package/dist/components/InputDate.module.css +1 -1
  8. package/dist/components/InputTime.d.ts +105 -0
  9. package/dist/components/InputTime.js +110 -0
  10. package/dist/components/Menu.d.ts +15 -1
  11. package/dist/components/Menu.js +2 -0
  12. package/dist/components/MenuItem.js +1 -7
  13. package/dist/components/Select.d.ts +96 -14
  14. package/dist/components/Select.js +69 -8
  15. package/dist/components/Select.module.css +1 -1
  16. package/dist/components/SelectFaceted.d.ts +184 -0
  17. package/dist/components/SelectFaceted.js +331 -0
  18. package/dist/components/SelectFaceted.module.css +1 -0
  19. package/dist/components/TabPanel.d.ts +21 -13
  20. package/dist/components/TabPanel.js +13 -2
  21. package/dist/components/Tabs.d.ts +76 -52
  22. package/dist/components/Tabs.js +18 -86
  23. package/dist/elements/a-button.css +1 -1
  24. package/dist/elements/a-calendar.css +1 -1
  25. package/dist/elements/a-checkbox.css +1 -1
  26. package/dist/elements/a-input-time.css +1 -0
  27. package/dist/elements/a-input-time.d.ts +27 -0
  28. package/dist/elements/a-input-time.js +856 -0
  29. package/dist/elements/a-input.css +1 -1
  30. package/dist/elements/a-input.js +8 -1
  31. package/dist/elements/a-menu-item.css +1 -1
  32. package/dist/elements/a-menu-item.d.ts +8 -6
  33. package/dist/elements/a-menu.d.ts +13 -15
  34. package/dist/elements/a-menu.js +71 -25
  35. package/dist/elements/a-radio.css +1 -1
  36. package/dist/elements/a-tab.css +1 -1
  37. package/dist/elements/a-tabpanel.css +1 -1
  38. package/dist/elements/a-tabpanel.d.ts +17 -0
  39. package/dist/elements/a-tabpanel.js +66 -0
  40. package/dist/elements/a-tabs.css +1 -1
  41. package/dist/elements/a-tooltip.js +1 -1
  42. package/dist/elements/index.d.ts +2 -1
  43. package/dist/elements/index.js +6 -1
  44. package/dist/general_types.d.ts +83 -11
  45. package/dist/index.d.ts +7 -5
  46. package/dist/index.js +6 -3
  47. package/dist/jsx-runtime.d.ts +2 -1
  48. package/dist/reset.css +1 -1
  49. package/dist/tokens.css +1 -1
  50. package/package.json +1 -1
@@ -0,0 +1,110 @@
1
+ import { jsx, jsxs } from "@antadesign/anta/jsx-runtime";
2
+ import { nativeStateChange, toneStyle, roundStyle } from "../anta_helpers";
3
+ import { Button } from "./Button";
4
+ import { Icon } from "./Icon";
5
+ const presence = (on) => on ? "" : void 0;
6
+ const isStringish = (n) => typeof n === "string" || typeof n === "number";
7
+ const attrsOf = (e) => {
8
+ const el = e?.target ?? {};
9
+ const value = el.value ?? "";
10
+ return {
11
+ value,
12
+ name: el.getAttribute?.("name") ?? void 0,
13
+ empty: !value,
14
+ valid: el.validity ? el.validity.valid : void 0,
15
+ validationMessage: el.validationMessage ?? ""
16
+ };
17
+ };
18
+ const STATUS_ICON = {
19
+ critical: "warning-diamond",
20
+ warning: "warning-triangle",
21
+ success: "circle-check",
22
+ info: "info",
23
+ brand: "circle-small-solid"
24
+ };
25
+ const InputTime = ({
26
+ label,
27
+ hint,
28
+ status,
29
+ statusIcon,
30
+ tone,
31
+ size,
32
+ round,
33
+ value,
34
+ defaultValue,
35
+ locale,
36
+ hour12,
37
+ min,
38
+ max,
39
+ name,
40
+ disabled,
41
+ required,
42
+ icon,
43
+ clearable,
44
+ dimActions,
45
+ trailing,
46
+ onValueChange,
47
+ onClearInput,
48
+ children,
49
+ className,
50
+ style,
51
+ ...rest
52
+ }) => {
53
+ const statusTone = status && status !== "neutral" ? status : void 0;
54
+ const glyph = statusIcon === void 0 ? statusTone ? STATUS_ICON[statusTone] : void 0 : statusIcon;
55
+ return /* @__PURE__ */ jsxs(
56
+ "a-input-time",
57
+ {
58
+ size: size && size !== "medium" ? size : void 0,
59
+ round: round ? "" : void 0,
60
+ value,
61
+ defaultvalue: defaultValue,
62
+ locale,
63
+ hour12: hour12 === void 0 ? void 0 : hour12 ? "true" : "false",
64
+ min,
65
+ max,
66
+ status: statusTone,
67
+ tone: tone || void 0,
68
+ name,
69
+ disabled: presence(disabled),
70
+ required: presence(required),
71
+ "dim-actions": presence(dimActions),
72
+ "aria-invalid": status === "critical" ? "true" : void 0,
73
+ oninput: onValueChange ? (e) => onValueChange(e, attrsOf(e)) : void 0,
74
+ onchange: onValueChange ? (e) => onValueChange(e, attrsOf(e)) : void 0,
75
+ onclearinput: onClearInput ? (e) => onClearInput(nativeStateChange(e).event) : void 0,
76
+ class: className,
77
+ style: roundStyle(round, "--input-time-round", toneStyle(tone, "--input-time-tone-source", style)),
78
+ ...rest,
79
+ children: [
80
+ label != null && (isStringish(label) ? /* @__PURE__ */ jsx("span", { slot: "label", children: label }) : /* @__PURE__ */ jsx("span", { slot: "label", style: { display: "contents" }, children: label })),
81
+ icon !== false && /* @__PURE__ */ jsx("span", { slot: "leading", style: { display: "contents" }, children: /* @__PURE__ */ jsx(Icon, { shape: icon ?? "clock" }) }),
82
+ clearable && // Real <a-button> in the element's `clear` slot; the element owns its
83
+ // visibility (shown only when filled). It fires `clearrequest`, which the
84
+ // element turns into clear(). CONTRACT: `data-custom-event` MUST match
85
+ // CLEAR_TRIGGER in src/elements/a-input-time.ts (string duplicated, not
86
+ // shared, to keep the wrapper/element decoupled).
87
+ /* @__PURE__ */ jsx("span", { slot: "clear", style: { display: "contents" }, children: /* @__PURE__ */ jsx(
88
+ Button,
89
+ {
90
+ priority: "tertiary",
91
+ size,
92
+ round: !!round,
93
+ icon: "x",
94
+ "aria-label": "Clear",
95
+ "data-custom-event": "clearrequest"
96
+ }
97
+ ) }),
98
+ trailing != null && /* @__PURE__ */ jsx("span", { slot: "trailing", style: { display: "contents" }, children: trailing }),
99
+ hint != null && /* @__PURE__ */ jsxs("span", { slot: "hint", style: { display: "contents" }, children: [
100
+ glyph && /* @__PURE__ */ jsx(Icon, { shape: glyph, "aria-hidden": "true" }),
101
+ /* @__PURE__ */ jsx("span", { children: hint })
102
+ ] }),
103
+ children
104
+ ]
105
+ }
106
+ );
107
+ };
108
+ export {
109
+ InputTime
110
+ };
@@ -22,6 +22,11 @@ export interface MenuProps extends BaseProps {
22
22
  /** Gap in pixels between the trigger and the menu.
23
23
  * @defaultValue 4 */
24
24
  offset?: number;
25
+ /** Size the menu to its content instead of flooring its width to the trigger.
26
+ * A root menu is never narrower than its trigger by default; set this for a
27
+ * content menu under a wide trigger (e.g. a calendar below a full-width field)
28
+ * so it wraps its content and left-aligns under the trigger. */
29
+ autoWidth?: boolean;
25
30
  /** Controlled open state. Omit for the default **uncontrolled** menu (it
26
31
  * opens/closes itself via its triggers). Pass a boolean to **control** it:
27
32
  * the menu's visibility follows `open`, and user dismiss (Esc, outside-click,
@@ -44,6 +49,15 @@ export interface MenuProps extends BaseProps {
44
49
  next: boolean;
45
50
  prev: boolean;
46
51
  }) => void;
52
+ /** Combobox-mode cursor report (raw element event). Fires when the active
53
+ * option changes as arrow keys move the cursor while focus stays in a
54
+ * `[data-menu-search]` filter field; `detail.id` is the active option's `id`
55
+ * (`null` when none). The reactive layer that owns the field reflects this
56
+ * onto its `aria-activedescendant` — the element can't write that light-DOM
57
+ * attribute itself. Passed straight through to the element. */
58
+ onactivedescendant?: (e: CustomEvent<{
59
+ id: string | null;
60
+ }>) => void;
47
61
  /** The menu's contents: `MenuItem`, `MenuSeparator`, `MenuGroup`, or any
48
62
  * custom element. */
49
63
  children?: React.ReactNode;
@@ -74,4 +88,4 @@ export interface MenuProps extends BaseProps {
74
88
  * </Menu>
75
89
  * ```
76
90
  */
77
- export declare const Menu: ({ placement, context, coord, nohover, offset, open, onStateChange, round, className, style, children, ...rest }: MenuProps) => any;
91
+ export declare const Menu: ({ placement, context, coord, nohover, offset, autoWidth, open, onStateChange, round, className, style, children, ...rest }: MenuProps) => any;
@@ -6,6 +6,7 @@ const Menu = ({
6
6
  coord,
7
7
  nohover,
8
8
  offset,
9
+ autoWidth,
9
10
  open,
10
11
  onStateChange,
11
12
  round,
@@ -22,6 +23,7 @@ const Menu = ({
22
23
  coord: coord ? "" : void 0,
23
24
  nohover: nohover ? "" : void 0,
24
25
  offset: offset != null ? String(offset) : void 0,
26
+ autowidth: autoWidth ? "" : void 0,
25
27
  state: open === void 0 ? void 0 : open ? "open" : "closed",
26
28
  onstatechange: onStateChange ? (e) => {
27
29
  const { event, detail } = nativeStateChange(e);
@@ -39,14 +39,8 @@ const MenuItem = ({
39
39
  style: toneStyle(effectiveTone, "--menu-item-tone-source", style),
40
40
  submenu: submenu ? "" : void 0,
41
41
  "aria-haspopup": submenu ? "menu" : void 0,
42
- "aria-expanded": submenu ? "false" : void 0,
43
42
  "aria-disabled": disabled ? "true" : void 0,
44
- onClick: disabled || !onSelect ? void 0 : (e) => {
45
- if (submenu) return;
46
- const t = e.target;
47
- if (t?.closest?.("a-menu-item") !== e.currentTarget) return;
48
- onSelect(e, { value, label });
49
- },
43
+ onmenuselect: onSelect ? (e) => onSelect(e, { value, label }) : void 0,
50
44
  class: className,
51
45
  ...rest,
52
46
  children: [
@@ -5,7 +5,9 @@ import type { IconShape } from '../elements/a-icon.shapes';
5
5
  * arbitrary fields (a `ranAt` date, a `status`, …) and read them back in
6
6
  * `renderOption`; the built-in filter still matches on `value`/`label`/`hint`. */
7
7
  export interface SelectOption {
8
- /** The option's value — its identity and what `onValueChange` reports. */
8
+ /** The option's value — its identity, what `value` / `defaultValue` name, and what
9
+ * `onValueChange` reports. Unique across the whole `options` tree (selection is
10
+ * value-keyed and global across groups / submenus). */
9
11
  value: string;
10
12
  /** Visible label. Defaults to `value`. */
11
13
  label?: string;
@@ -18,6 +20,11 @@ export interface SelectOption {
18
20
  /** Tone for this option's row (label, icon, hint, selected tint, and the
19
21
  * checkbox/radio indicator). A named tone or a custom CSS color. */
20
22
  tone?: 'neutral' | 'brand' | 'info' | 'success' | 'warning' | 'critical' | (string & {});
23
+ /** Tooltip for this option's row — a string or any node. In a `multiple`
24
+ * select with `selectAll`, a row with no `tooltip` falls back to a default
25
+ * hint for the Alt/Option-click "select only this" accelerator; set `tooltip`
26
+ * to override that, or `''` to suppress it. */
27
+ tooltip?: React.ReactNode;
21
28
  /** Your own data — attach anything and read it in `renderOption`. */
22
29
  [key: string]: unknown;
23
30
  }
@@ -90,8 +97,10 @@ export interface TriggerState {
90
97
  }
91
98
  /** Snapshot passed as the 2nd argument to `onValueChange` — describes *what*
92
99
  * changed, alongside the new full value in the 1st argument. A discriminated
93
- * union: a row toggle carries `value` + `option`; the "Select all" row carries
94
- * `all: true` instead. Narrow on `'all' in attrs` before reading `option`. */
100
+ * union: a row toggle carries `value` + `option`; a bulk change (the "Select all"
101
+ * row, or the `clearable` "Clear" footer) carries `all: true` instead. **Always
102
+ * narrow on `'all' in attrs` before reading `option`** — the `all` variant fires
103
+ * for single-select too (via `clearable`), not just multiple. */
95
104
  export type SelectChangeAttrs = {
96
105
  /** The option value that changed — the chosen value (single) or the toggled
97
106
  * row (multiple). */
@@ -101,9 +110,11 @@ export type SelectChangeAttrs = {
101
110
  /** Multiple only: whether the change turned selection **on** (true) or off. */
102
111
  selected?: boolean;
103
112
  } | {
104
- /** Marks the change as coming from the "Select all" row (multiple only). */
113
+ /** Marks the change as a bulk one with no single `option`: the "Select all"
114
+ * row (multiple), or the `clearable` "Clear" footer (single or multiple). */
105
115
  all: true;
106
- /** Whether Select-all turned everything **on** (true) or cleared it. */
116
+ /** Whether the bulk change turned everything **on** (true) or cleared it
117
+ * (false — always false for a "Clear"). */
107
118
  selected: boolean;
108
119
  };
109
120
  /** Props shared by both selection modes, intersected into `SelectProps`. Exported
@@ -113,7 +124,12 @@ export interface SelectCommonProps extends Omit<BaseProps, 'children'> {
113
124
  /** The options to choose from — bare strings, `SelectOption` objects, `SelectGroup`s
114
125
  * (inline titled sections), or `SelectSubmenu`s (flyout branches). Groups and
115
126
  * submenus nest and mix with plain options. Selection stays global (one `value`,
116
- * leaf options only); a filter query flattens the tree into grouped results. */
127
+ * leaf options only); a filter query flattens the tree into grouped results.
128
+ *
129
+ * Each leaf `value` is the option's identity and must be **unique across the whole
130
+ * tree** — selection is value-keyed, so a value repeated in two sections is one
131
+ * logical pick (both rows toggle together; the trigger resolves to the last). Dev
132
+ * builds `console.warn` on a duplicate. */
117
133
  options: SelectItem[];
118
134
  /** The per-row mark for **single**-select: `'none'` (a tint-only highlight),
119
135
  * `'check'` (a trailing checkmark on the selected row, keeping the tint — the
@@ -127,6 +143,11 @@ export interface SelectCommonProps extends Omit<BaseProps, 'children'> {
127
143
  * `leading` slot). With a custom `renderTrigger`, it's passed through as
128
144
  * `state.icon` instead — the consumer places it. */
129
145
  icon?: IconShape;
146
+ /** Arbitrary content for the default trigger's leading slot (piped straight to
147
+ * `Input`'s `leading`) — e.g. a key prefix before the value. Overrides the
148
+ * `icon`-derived glyph when both are set; include your own `<Icon>` if you want
149
+ * one alongside. Ignored with a custom `renderTrigger`. */
150
+ leading?: React.ReactNode;
130
151
  /** Field label, above the trigger (Input's `label`). */
131
152
  label?: string;
132
153
  /** Helper text under the field (Input's `hint`). */
@@ -162,6 +183,13 @@ export interface SelectCommonProps extends Omit<BaseProps, 'children'> {
162
183
  /** Label for the `selectAll` row.
163
184
  * @defaultValue Select all */
164
185
  selectAllLabel?: string;
186
+ /** Add a "Clear" row pinned in the menu **footer** that empties the selection
187
+ * (single → none, multiple → `[]`). Shown only while something is selected, so
188
+ * it never scrolls away in a long or filtered list. */
189
+ clearable?: boolean;
190
+ /** Label for the `clearable` footer row.
191
+ * @defaultValue Clear */
192
+ clearLabel?: string;
165
193
  /** Render the **content** of each option row yourself, replacing the built-in
166
194
  * `label`/`hint`/`icon` layout. Select still supplies the row box, click, ARIA,
167
195
  * and the selection indicator — you return only what goes *inside*. Read extra
@@ -182,8 +210,10 @@ export interface SelectCommonProps extends Omit<BaseProps, 'children'> {
182
210
  * the menu anchors to it (its own previous DOM sibling) and opens it on click,
183
211
  * so a fragment, multiple siblings, or a non-focusable wrapper will misanchor
184
212
  * (the menu warns in the console if the trigger has no focusable element). Give
185
- * the element `aria-haspopup="menu"` plus `aria-expanded={state.open}`. The field
186
- * props (`label`, `hint`, `size`, `status`, `placeholder`, `round`) don't apply. */
213
+ * the element `aria-haspopup="menu"` plus `aria-expanded={state.open}`. The custom
214
+ * trigger owns its own styling: the field props (`label`, `hint`, `size`, `status`,
215
+ * `placeholder`, `round`) and `className` / `style` apply to the *default* trigger
216
+ * only — put your own on the element you return. */
187
217
  renderTrigger?: (state: TriggerState) => React.ReactNode;
188
218
  /** Render content in the menu body when the (filtered) option list is empty —
189
219
  * a "no results" message, a loading indicator (gated on your own external
@@ -204,11 +234,13 @@ export type SelectProps = SelectCommonProps & ({
204
234
  * array value.
205
235
  * @defaultValue single */
206
236
  selection?: 'single';
207
- /** Controlled value. When provided, the consumer owns selection: the field
208
- * follows this prop and a pick only *requests* a change via `onValueChange`
209
- * (reject by not updating). Leave undefined for uncontrolled. */
237
+ /** Controlled value the `value` string of the selected option. When
238
+ * provided, the consumer owns selection: the field follows this prop and a
239
+ * pick only *requests* a change via `onValueChange` (reject by not updating).
240
+ * Leave undefined for uncontrolled. */
210
241
  value?: string;
211
- /** Initial value for the uncontrolled case (the wrapper then owns it). */
242
+ /** Initial value (an option's `value` string) for the uncontrolled case the
243
+ * wrapper then owns it. */
212
244
  defaultValue?: string;
213
245
  /** Fires after the selection changes, with the new value and a
214
246
  * `{ value, option }` snapshot. Select has no discrete element state, so
@@ -218,15 +250,65 @@ export type SelectProps = SelectCommonProps & ({
218
250
  /** Multi-select: checkboxes on every row, the menu stays open while
219
251
  * toggling, the field shows an "N selected" count, and `value` is an array. */
220
252
  selection: 'multiple';
221
- /** Controlled values (see the single-select `value` note). */
253
+ /** Controlled values the `value` strings of the selected options (see the
254
+ * single-select `value` note). */
222
255
  value?: string[];
223
- /** Initial values for the uncontrolled case. */
256
+ /** Initial values (option `value` strings) for the uncontrolled case. */
224
257
  defaultValue?: string[];
225
258
  /** Fires after any toggle, with the new value array and a `{ value, option,
226
259
  * selected }` snapshot of the row that changed (or `{ all: true }` for the
227
260
  * Select-all row). */
228
261
  onValueChange?: (value: string[], attrs: SelectChangeAttrs) => void;
229
262
  });
263
+ /** Rolled-up selection of a group / submenu subtree: `'none'` / `'all'` of the
264
+ * descendant leaves selected, or `'some'` in between. On `SelectedGroup` /
265
+ * `SelectedSubmenu`, for a section indicator or custom styling. */
266
+ export type SelectionState = 'none' | 'some' | 'all';
267
+ /** A leaf option annotated with its current selection — a `SelectOption` plus
268
+ * `selected`. Returned by {@link optionsWithSelection}. */
269
+ export type SelectedOption = SelectOption & {
270
+ selected: boolean;
271
+ };
272
+ /** A group with its descendants annotated and a rolled-up `selectionState`.
273
+ * Returned by {@link optionsWithSelection}. */
274
+ export interface SelectedGroup extends Omit<SelectGroup, 'options'> {
275
+ options: SelectedItem[];
276
+ selectionState: SelectionState;
277
+ }
278
+ /** A submenu with its descendants annotated and a rolled-up `selectionState`.
279
+ * Returned by {@link optionsWithSelection}. */
280
+ export interface SelectedSubmenu extends Omit<SelectSubmenu, 'submenu'> {
281
+ submenu: SelectedItem[];
282
+ selectionState: SelectionState;
283
+ }
284
+ /** One node of the tree from {@link optionsWithSelection}: a leaf `SelectedOption`
285
+ * (with `selected`), or a `SelectedGroup` / `SelectedSubmenu` (annotated children
286
+ * + rolled-up `selectionState`). Mirrors `SelectItem` minus the bare-string
287
+ * shorthand — strings are normalized to `SelectedOption`. */
288
+ export type SelectedItem = SelectedOption | SelectedGroup | SelectedSubmenu;
289
+ /**
290
+ * Project a `Select` `options` tree onto a set of selected values: returns a mirror
291
+ * of the tree with every leaf marked `selected` and every group / submenu carrying a
292
+ * rolled-up `selectionState` (`'none'` / `'some'` / `'all'` of its descendant leaves).
293
+ * Bare-string options are normalized to `{ value, label }`; structure and order are
294
+ * preserved.
295
+ *
296
+ * A pure function of `(options, values)` — it needs no `Select` instance and reads
297
+ * nothing off the change event, so it behaves identically in controlled and
298
+ * uncontrolled code. Pass the current `value` (a string, an array, or `undefined`)
299
+ * and render a grouped summary, drive section indicators, or diff picks.
300
+ *
301
+ * A value that appears under more than one section marks the leaf in *every* place it
302
+ * occurs (selection is value-keyed), mirroring how the menu itself renders it.
303
+ *
304
+ * @example
305
+ * ```tsx
306
+ * const tree = optionsWithSelection(options, values)
307
+ * // tree[1] === { label: 'Engineering', selectionState: 'some', options: [
308
+ * // { value: 'eng-fe', label: 'Frontend', selected: false }, … ] }
309
+ * ```
310
+ */
311
+ export declare function optionsWithSelection(options: SelectItem[], values: string | string[] | undefined): SelectedItem[];
230
312
  /**
231
313
  * `<Select>` — a single- or multi-select dropdown, composed from `<Input>` (a
232
314
  * read-only trigger) and `<Menu>` (the options). `selection` sets behaviour:
@@ -6,8 +6,34 @@ import { Menu } from "./Menu";
6
6
  import { MenuItem } from "./MenuItem";
7
7
  import { MenuGroup } from "./MenuGroup";
8
8
  import { MenuSeparator } from "./MenuSeparator";
9
+ import { Tooltip } from "./Tooltip";
9
10
  import styles from "./Select.module.css";
11
+ const IS_MAC = typeof navigator !== "undefined" && /Mac|iPhone|iPad/i.test(navigator.userAgent || "");
10
12
  const normalize = (o) => typeof o === "string" ? { value: o, label: o } : o;
13
+ function optionsWithSelection(options, values) {
14
+ const set = new Set(values == null ? [] : Array.isArray(values) ? values : [values]);
15
+ const state = (on, total) => on === 0 ? "none" : on === total ? "all" : "some";
16
+ const walk = (items) => items.map((raw) => {
17
+ if (typeof raw !== "string" && Array.isArray(raw.submenu)) {
18
+ const sm = raw;
19
+ const children = walk(sm.submenu);
20
+ const on = children.reduce((n, c) => n + c.on, 0);
21
+ const total = children.reduce((n, c) => n + c.total, 0);
22
+ return { node: { ...sm, submenu: children.map((c) => c.node), selectionState: state(on, total) }, on, total };
23
+ }
24
+ if (typeof raw !== "string" && Array.isArray(raw.options)) {
25
+ const g = raw;
26
+ const children = walk(g.options);
27
+ const on = children.reduce((n, c) => n + c.on, 0);
28
+ const total = children.reduce((n, c) => n + c.total, 0);
29
+ return { node: { ...g, options: children.map((c) => c.node), selectionState: state(on, total) }, on, total };
30
+ }
31
+ const o = normalize(raw);
32
+ const selected = set.has(o.value);
33
+ return { node: { ...o, selected }, on: selected ? 1 : 0, total: 1 };
34
+ });
35
+ return walk(options).map((c) => c.node);
36
+ }
11
37
  const Select = (props) => {
12
38
  const {
13
39
  options,
@@ -18,6 +44,7 @@ const Select = (props) => {
18
44
  onValueChange,
19
45
  placeholder,
20
46
  icon,
47
+ leading,
21
48
  label,
22
49
  hint,
23
50
  size,
@@ -29,6 +56,8 @@ const Select = (props) => {
29
56
  filter,
30
57
  selectAll,
31
58
  selectAllLabel = "Select all",
59
+ clearable,
60
+ clearLabel = "Clear",
32
61
  renderOption,
33
62
  renderIndicator,
34
63
  renderTrigger,
@@ -47,6 +76,7 @@ const Select = (props) => {
47
76
  const currentRaw = controlled ? value : internal;
48
77
  const [open, setOpen] = useState(false);
49
78
  const [query, setQuery] = useState("");
79
+ const [activeId, setActiveId] = useState(null);
50
80
  const uid = useId();
51
81
  const isSubmenu = (it) => typeof it === "object" && Array.isArray(it.submenu);
52
82
  const isGroup = (it) => typeof it === "object" && Array.isArray(it.options);
@@ -62,6 +92,12 @@ const Select = (props) => {
62
92
  };
63
93
  const allLeaves = [];
64
94
  collectLeaves(options, void 0, false, allLeaves);
95
+ const seenValues = /* @__PURE__ */ new Set();
96
+ for (const l of allLeaves) {
97
+ if (seenValues.has(l.opt.value))
98
+ console.warn(`[anta] <Select> duplicate option value=${JSON.stringify(l.opt.value)} \u2014 values must be unique.`);
99
+ seenValues.add(l.opt.value);
100
+ }
65
101
  const byValue = new Map(allLeaves.map((l) => [l.opt.value, l.opt]));
66
102
  const filtering = filter !== void 0 && filter !== false;
67
103
  const q = query.trim();
@@ -98,8 +134,14 @@ const Select = (props) => {
98
134
  const o = byValue.get(currentRaw);
99
135
  display = o ? o.label ?? o.value : "";
100
136
  }
101
- const choose = (o) => {
137
+ const choose = (o, e) => {
102
138
  if (multiple) {
139
+ if (selectAll && e?.altKey) {
140
+ const next2 = [o.value];
141
+ if (!controlled) setInternal(next2);
142
+ emit?.(next2, { value: o.value, option: o, selected: true });
143
+ return;
144
+ }
103
145
  const has = selectedValues.includes(o.value);
104
146
  const next = has ? selectedValues.filter((v) => v !== o.value) : [...selectedValues, o.value];
105
147
  if (!controlled) setInternal(next);
@@ -119,12 +161,20 @@ const Select = (props) => {
119
161
  if (!controlled) setInternal(next);
120
162
  emit?.(next, { all: true, selected: !allSelected });
121
163
  };
164
+ const clear = () => {
165
+ const next = multiple ? [] : void 0;
166
+ if (!controlled) setInternal(next);
167
+ emit?.(next, { all: true, selected: false });
168
+ };
122
169
  const renderOptionRow = (o, disabled2) => {
123
170
  const optState = { value: o.value, selected: isSelected(o.value), disabled: disabled2 };
124
171
  const custom = renderOption?.(o, optState);
125
172
  const customMark = renderIndicator?.(optState);
126
173
  const ariaSelectable = !multiple && mark === "none" ? { role: "menuitemradio", "aria-checked": isSelected(o.value) ? "true" : "false" } : void 0;
127
- return /* @__PURE__ */ jsx(
174
+ const isolateHint = multiple && selectAll && !disabled2 ? IS_MAC ? "\u2325+Click to select only this" : "Alt+Click to select only this" : void 0;
175
+ const tip = o.tooltip ?? isolateHint;
176
+ const hintOnly = o.tooltip == null && isolateHint != null;
177
+ return /* @__PURE__ */ jsxs(
128
178
  MenuItem,
129
179
  {
130
180
  id: `${uid}-opt-${o.value}`,
@@ -139,8 +189,11 @@ const Select = (props) => {
139
189
  selected: isSelected(o.value),
140
190
  disabled: disabled2 || void 0,
141
191
  "data-menu-open": multiple ? "" : void 0,
142
- onSelect: () => choose(o),
143
- children: custom
192
+ onSelect: (e) => choose(o, e),
193
+ children: [
194
+ custom,
195
+ tip && /* @__PURE__ */ jsx(Tooltip, { ...hintOnly ? { follow: true, delay: 700 } : {}, children: tip })
196
+ ]
144
197
  },
145
198
  o.value
146
199
  );
@@ -211,7 +264,7 @@ const Select = (props) => {
211
264
  readOnly: true,
212
265
  dimActions: true,
213
266
  disabled,
214
- leading: icon ? /* @__PURE__ */ jsx(Icon, { shape: icon }) : void 0,
267
+ leading: leading ?? (icon ? /* @__PURE__ */ jsx(Icon, { shape: icon }) : void 0),
215
268
  size,
216
269
  status,
217
270
  statusIcon,
@@ -246,8 +299,12 @@ const Select = (props) => {
246
299
  {
247
300
  onStateChange: (_e, { next }) => {
248
301
  setOpen(next);
249
- if (!next) setQuery("");
302
+ if (!next) {
303
+ setQuery("");
304
+ setActiveId(null);
305
+ }
250
306
  },
307
+ onactivedescendant: (e) => setActiveId((e.nativeEvent ?? e).detail?.id ?? null),
251
308
  children: [
252
309
  filtering && // `slot="header"` pins the field in the Menu's fixed header region (above
253
310
  // the scrolling options); `data-menu-search` puts the menu in combobox
@@ -261,6 +318,7 @@ const Select = (props) => {
261
318
  placeholder: "Filter\u2026",
262
319
  "aria-label": "Filter options",
263
320
  "aria-autocomplete": "list",
321
+ "aria-activedescendant": open && activeId ? activeId : void 0,
264
322
  onInput: (e) => setQuery(e.currentTarget.value)
265
323
  }
266
324
  ) }),
@@ -279,12 +337,15 @@ const Select = (props) => {
279
337
  ),
280
338
  /* @__PURE__ */ jsx(MenuSeparator, {})
281
339
  ] }),
282
- visibleLeaves.length === 0 ? renderEmpty?.({ query: q }) : flattening ? renderFlat() : renderTree(options, false)
340
+ visibleLeaves.length === 0 ? renderEmpty?.({ query: q }) : flattening ? renderFlat() : renderTree(options, false),
341
+ clearable && selectedValues.length > 0 && // Pinned in the footer so it never scrolls away in a long / filtered list.
342
+ /* @__PURE__ */ jsx("div", { slot: "footer", className: styles.footer, children: /* @__PURE__ */ jsx(MenuItem, { icon: "x", label: clearLabel, "data-menu-open": "", onSelect: clear }) })
283
343
  ]
284
344
  }
285
345
  )
286
346
  ] });
287
347
  };
288
348
  export {
289
- Select
349
+ Select,
350
+ optionsWithSelection
290
351
  };
@@ -1 +1 @@
1
- .chevron{margin-inline-end:7px;transition:transform .15s ease}.chevron.open{transform:rotate(180deg)}.filter{padding:4px 4px 0}
1
+ .chevron{margin-inline-end:7px;transition:transform .15s ease}.chevron.open{transform:rotate(180deg)}.filter{padding:4px 4px 0}.footer{padding:var(--menu-padding, 4px);border-top:1px solid var(--border-4)}