@antadesign/anta 0.3.3 → 0.3.4

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,10 +22,6 @@ export declare function nativeStateChange<D>(e: CustomEvent<D> | {
22
22
  event: CustomEvent<D>;
23
23
  detail?: D;
24
24
  };
25
- export declare const TABS_KIND: unique symbol;
26
- export declare function markTabsKind<F>(component: F & {
27
- [TABS_KIND]?: "tab" | "panel";
28
- }, kind: "tab" | "panel"): F;
29
25
  /** The six named tones every toned component shares. Anything else is a literal
30
26
  * CSS colour the element resolves through its `--{component}-tone-source` var. */
31
27
  export declare const NAMED_TONES: Set<string>;
@@ -16,11 +16,6 @@ function nativeStateChange(e) {
16
16
  const event = "nativeEvent" in e ? e.nativeEvent : e;
17
17
  return { event, detail: event?.detail };
18
18
  }
19
- const TABS_KIND = Symbol.for("@antadesign/anta/tabs.kind");
20
- function markTabsKind(component, kind) {
21
- component[TABS_KIND] = kind;
22
- return component;
23
- }
24
19
  const NAMED_TONES = /* @__PURE__ */ new Set([
25
20
  "brand",
26
21
  "neutral",
@@ -112,12 +107,10 @@ export {
112
107
  HTMLElementBase,
113
108
  NAMED_TONES,
114
109
  SelectableChildElement,
115
- TABS_KIND,
116
110
  anchorRect,
117
111
  hasChildren,
118
112
  isInsideOpenMenu,
119
113
  isMenuOpen,
120
- markTabsKind,
121
114
  nativeStateChange,
122
115
  roundStyle,
123
116
  setMenuPresence,
@@ -36,6 +36,11 @@ export interface CalendarProps extends Omit<BaseProps, "children" | "onChange">
36
36
  size?: "small" | "medium" | "large";
37
37
  /** Disable the whole calendar (not focusable or selectable). */
38
38
  disabled?: boolean;
39
+ /** Move keyboard focus onto the active day. Change this to a new value (e.g.
40
+ * increment a counter) to focus the cursor cell — `InputDate` bumps it when
41
+ * the calendar is opened from the field by keyboard (ArrowDown), so focus
42
+ * lands in the grid. The initial value never focuses; only a change does. */
43
+ focusSignal?: number;
39
44
  /** Accessible name for the grid (defaults to the visible month heading). */
40
45
  "aria-label"?: string;
41
46
  /** Fired whenever the selection changes — event-first. `detail` is
@@ -76,5 +81,5 @@ export interface CalendarProps extends Omit<BaseProps, "children" | "onChange">
76
81
  * hydration mismatch. Render it client-side only (Astro `client:only`, or a
77
82
  * dynamic import inside `useEffect`).
78
83
  */
79
- export declare const Calendar: ({ value, defaultValue, min, max, locale, name, size, disabled, onStateChange, onChange, onValueChange, className, style, "aria-label": ariaLabel, ...rest }: CalendarProps) => any;
84
+ export declare const Calendar: ({ value, defaultValue, min, max, locale, name, size, disabled, focusSignal, onStateChange, onChange, onValueChange, className, style, "aria-label": ariaLabel, ...rest }: CalendarProps) => any;
80
85
  export {};
@@ -17,6 +17,7 @@ const Calendar = ({
17
17
  name,
18
18
  size,
19
19
  disabled,
20
+ focusSignal,
20
21
  onStateChange,
21
22
  onChange,
22
23
  onValueChange,
@@ -42,6 +43,11 @@ const Calendar = ({
42
43
  const headingId = useId();
43
44
  const month = buildMonth({ anchor: cursor, locale: resolvedLocale, min: minD, max: maxD, selected, today });
44
45
  const cursorIso = cursor.toString();
46
+ const [lastFocusSignal, setLastFocusSignal] = useState(focusSignal);
47
+ if (focusSignal !== lastFocusSignal) {
48
+ setLastFocusSignal(focusSignal);
49
+ if (focusSignal !== void 0) setFocusReq({ iso: cursorIso, n: focusSignal });
50
+ }
45
51
  const minYear = minD ? minD.year : today.year - 3;
46
52
  const maxYear = maxD ? maxD.year : today.year + 3;
47
53
  const years = [];
@@ -45,7 +45,8 @@ export interface InputDateProps extends Omit<BaseProps, 'children'> {
45
45
  round?: boolean | number | string;
46
46
  /** Show a clear button once the field has a value. */
47
47
  clearable?: boolean;
48
- /** Leading icon at the start of the field. */
48
+ /** Leading icon at the start of the field — the calendar affordance.
49
+ * @defaultValue calendar-days */
49
50
  icon?: IconShape;
50
51
  /** Include a time. The value becomes ISO `YYYY-MM-DDTHH:mm`, the field parses a
51
52
  * trailing time after a space (`06/15/2026 14:30`, `… 2:30pm`), and the menu
@@ -67,10 +68,11 @@ export interface InputDateProps extends Omit<BaseProps, 'children'> {
67
68
  * `Menu` + `Calendar` (no `a-inputdate` element; the wrapper is the coordinator).
68
69
  * The field accepts free text and resolves it on commit (blur / Enter) with a
69
70
  * lenient, locale-aware parser, then rewrites the entry to the canonical format;
70
- * an unrecognized entry marks the field `critical` and keeps the text. The trailing
71
- * calendar button opens a `Calendar` in a menu (a field click just types); with
72
- * `time`, a time row sits under the grid. The value is an ISO `YYYY-MM-DD` string,
73
- * or `YYYY-MM-DDTHH:mm` with `time`.
71
+ * an unrecognized entry marks the field `critical` and keeps the text. Clicking the
72
+ * field (or pressing ArrowDown) opens a `Calendar` in a menu; a leading calendar
73
+ * icon marks the affordance. Mouse-open keeps focus in the field so you can keep
74
+ * typing; ArrowDown moves focus into the grid. With `time`, a time row sits under
75
+ * the grid. The value is an ISO `YYYY-MM-DD` string, or `YYYY-MM-DDTHH:mm` with `time`.
74
76
  *
75
77
  * Controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`).
76
78
  * Requires `@antadesign/anta/elements` (client-side only).
@@ -72,6 +72,7 @@ const InputDate = ({
72
72
  const [text, setText] = useState(() => fmt(current));
73
73
  const [open, setOpen] = useState(false);
74
74
  const [invalid, setInvalid] = useState(false);
75
+ const [focusNonce, setFocusNonce] = useState(0);
75
76
  const [hourText, setHourText] = useState(curHourShown);
76
77
  const [minuteText, setMinuteText] = useState(curM);
77
78
  const meridiem = curMer;
@@ -147,127 +148,121 @@ const InputDate = ({
147
148
  round,
148
149
  disabled,
149
150
  clearable,
150
- leading: icon ? /* @__PURE__ */ jsx(Icon, { shape: icon }) : void 0,
151
+ leading: /* @__PURE__ */ jsx(Icon, { shape: icon ?? "calendar-days" }),
151
152
  value: text,
152
153
  placeholder: pattern,
153
154
  inputMode: time ? "text" : "numeric",
154
155
  autoComplete: "off",
156
+ "aria-haspopup": "dialog",
157
+ "aria-expanded": open ? "true" : "false",
155
158
  onInput: (e) => {
156
159
  setText(e.currentTarget.value);
157
160
  if (invalid) setInvalid(false);
158
161
  },
159
- onChange: (e) => {
160
- if (e.target !== e.currentTarget) return;
161
- resolve(e.currentTarget.value);
162
+ onChange: (e) => resolve(e.currentTarget.value),
163
+ onKeyDown: (e) => {
164
+ if (e.key === "ArrowDown" && !disabled) {
165
+ e.preventDefault();
166
+ if (!open) e.currentTarget.click();
167
+ setFocusNonce((n) => n + 1);
168
+ }
162
169
  },
163
170
  className: cn(styles.dateField, className),
164
171
  style,
165
- trailing: /* @__PURE__ */ jsxs(Fragment, { children: [
172
+ ...rest
173
+ }
174
+ ),
175
+ /* @__PURE__ */ jsx(
176
+ Menu,
177
+ {
178
+ open,
179
+ placement: "bottom-start",
180
+ className: styles.calendarMenu,
181
+ onStateChange: (_e, { next }) => setOpen(next),
182
+ children: /* @__PURE__ */ jsxs("div", { "data-menu-open": "", children: [
166
183
  /* @__PURE__ */ jsx(
167
- Button,
184
+ Calendar,
168
185
  {
169
- priority: "tertiary",
186
+ value: dateISO,
187
+ min,
188
+ max,
189
+ locale,
170
190
  size,
171
- icon: "calendar-days",
172
- "aria-label": "Open calendar",
173
- "aria-haspopup": "dialog",
174
- disabled
191
+ disabled,
192
+ focusSignal: focusNonce,
193
+ onStateChange: (_e, { next, reason }) => {
194
+ if (reason !== "user") return;
195
+ const d = next ?? "";
196
+ apply(time && d ? withTime(d) : d);
197
+ if (!time) setOpen(false);
198
+ }
175
199
  }
176
200
  ),
177
- /* @__PURE__ */ jsx(
178
- Menu,
179
- {
180
- open,
181
- placement: "bottom-end",
182
- className: styles.calendarMenu,
183
- onStateChange: (_e, { next }) => setOpen(next),
184
- children: /* @__PURE__ */ jsxs("div", { "data-menu-open": "", children: [
185
- /* @__PURE__ */ jsx(
186
- Calendar,
187
- {
188
- value: dateISO,
189
- min,
190
- max,
191
- locale,
192
- size,
193
- disabled,
194
- onStateChange: (_e, { next, reason }) => {
195
- if (reason !== "user") return;
196
- const d = next ?? "";
197
- apply(time && d ? withTime(d) : d);
198
- if (!time) setOpen(false);
199
- }
200
- }
201
- ),
202
- time && /* @__PURE__ */ jsxs("div", { className: styles.timeRow, children: [
203
- /* @__PURE__ */ jsxs("div", { className: styles.time, role: "group", "aria-label": "Time", children: [
204
- /* @__PURE__ */ jsx(Icon, { shape: "clock", "aria-hidden": "true", className: styles.clock }),
205
- /* @__PURE__ */ jsx(
206
- Input,
207
- {
208
- size,
209
- className: styles.timeField,
210
- value: hourText,
211
- placeholder: "HH",
212
- type: "number",
213
- inputMode: "numeric",
214
- "aria-label": "Hours",
215
- disabled,
216
- onInput: (e) => setHourText(e.currentTarget.value.replace(/\D/g, "").slice(0, 2)),
217
- onChange: (e) => commitTime(e.currentTarget.value, minuteText, meridiem)
218
- }
219
- ),
220
- /* @__PURE__ */ jsx("span", { className: styles.sep, "aria-hidden": "true", children: ":" }),
221
- /* @__PURE__ */ jsx(
222
- Input,
223
- {
224
- size,
225
- className: styles.timeField,
226
- value: minuteText,
227
- placeholder: "MM",
228
- type: "number",
229
- inputMode: "numeric",
230
- "aria-label": "Minutes",
231
- disabled,
232
- onInput: (e) => setMinuteText(e.currentTarget.value.replace(/\D/g, "").slice(0, 2)),
233
- onChange: (e) => commitTime(hourText, e.currentTarget.value, meridiem)
234
- }
235
- ),
236
- twelveHour && /* @__PURE__ */ jsx(
237
- Tabs,
238
- {
239
- className: styles.meridiem,
240
- size,
241
- priority: "primary",
242
- "aria-label": "AM or PM",
243
- options: [
244
- { value: "AM", label: "AM" },
245
- { value: "PM", label: "PM" }
246
- ],
247
- value: meridiem,
248
- onStateChange: (_e, { next }) => {
249
- if (next === "AM" || next === "PM") commitTime(hourText, minuteText, next);
250
- },
251
- disabled
252
- }
253
- )
254
- ] }),
255
- /* @__PURE__ */ jsx(
256
- Button,
257
- {
258
- priority: "tertiary",
259
- size,
260
- icon: "check",
261
- "aria-label": "Done",
262
- onClick: () => setOpen(false)
263
- }
264
- )
265
- ] })
266
- ] })
267
- }
268
- )
269
- ] }),
270
- ...rest
201
+ time && /* @__PURE__ */ jsxs("div", { className: styles.timeRow, children: [
202
+ /* @__PURE__ */ jsxs("div", { className: styles.time, role: "group", "aria-label": "Time", children: [
203
+ /* @__PURE__ */ jsx(Icon, { shape: "clock", "aria-hidden": "true", className: styles.clock }),
204
+ /* @__PURE__ */ jsx(
205
+ Input,
206
+ {
207
+ size,
208
+ className: styles.timeField,
209
+ value: hourText,
210
+ placeholder: "HH",
211
+ type: "number",
212
+ inputMode: "numeric",
213
+ "aria-label": "Hours",
214
+ disabled,
215
+ onInput: (e) => setHourText(e.currentTarget.value.replace(/\D/g, "").slice(0, 2)),
216
+ onChange: (e) => commitTime(e.currentTarget.value, minuteText, meridiem)
217
+ }
218
+ ),
219
+ /* @__PURE__ */ jsx("span", { className: styles.sep, "aria-hidden": "true", children: ":" }),
220
+ /* @__PURE__ */ jsx(
221
+ Input,
222
+ {
223
+ size,
224
+ className: styles.timeField,
225
+ value: minuteText,
226
+ placeholder: "MM",
227
+ type: "number",
228
+ inputMode: "numeric",
229
+ "aria-label": "Minutes",
230
+ disabled,
231
+ onInput: (e) => setMinuteText(e.currentTarget.value.replace(/\D/g, "").slice(0, 2)),
232
+ onChange: (e) => commitTime(hourText, e.currentTarget.value, meridiem)
233
+ }
234
+ ),
235
+ twelveHour && /* @__PURE__ */ jsx(
236
+ Tabs,
237
+ {
238
+ className: styles.meridiem,
239
+ size,
240
+ priority: "primary",
241
+ "aria-label": "AM or PM",
242
+ options: [
243
+ { value: "AM", label: "AM" },
244
+ { value: "PM", label: "PM" }
245
+ ],
246
+ value: meridiem,
247
+ onStateChange: (_e, { next }) => {
248
+ if (next === "AM" || next === "PM") commitTime(hourText, minuteText, next);
249
+ },
250
+ disabled
251
+ }
252
+ )
253
+ ] }),
254
+ /* @__PURE__ */ jsx(
255
+ Button,
256
+ {
257
+ priority: "tertiary",
258
+ size,
259
+ icon: "check",
260
+ "aria-label": "Done",
261
+ onClick: () => setOpen(false)
262
+ }
263
+ )
264
+ ] })
265
+ ] })
271
266
  }
272
267
  ),
273
268
  name ? /* @__PURE__ */ jsx("input", { type: "hidden", name, value: current ?? "" }) : null
@@ -44,6 +44,15 @@ export interface MenuProps extends BaseProps {
44
44
  next: boolean;
45
45
  prev: boolean;
46
46
  }) => void;
47
+ /** Combobox-mode cursor report (raw element event). Fires when the active
48
+ * option changes as arrow keys move the cursor while focus stays in a
49
+ * `[data-menu-search]` filter field; `detail.id` is the active option's `id`
50
+ * (`null` when none). The reactive layer that owns the field reflects this
51
+ * onto its `aria-activedescendant` — the element can't write that light-DOM
52
+ * attribute itself. Passed straight through to the element. */
53
+ onactivedescendant?: (e: CustomEvent<{
54
+ id: string | null;
55
+ }>) => void;
47
56
  /** The menu's contents: `MenuItem`, `MenuSeparator`, `MenuGroup`, or any
48
57
  * custom element. */
49
58
  children?: React.ReactNode;
@@ -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
  }
@@ -113,7 +120,12 @@ export interface SelectCommonProps extends Omit<BaseProps, 'children'> {
113
120
  /** The options to choose from — bare strings, `SelectOption` objects, `SelectGroup`s
114
121
  * (inline titled sections), or `SelectSubmenu`s (flyout branches). Groups and
115
122
  * 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. */
123
+ * leaf options only); a filter query flattens the tree into grouped results.
124
+ *
125
+ * Each leaf `value` is the option's identity and must be **unique across the whole
126
+ * tree** — selection is value-keyed, so a value repeated in two sections is one
127
+ * logical pick (both rows toggle together; the trigger resolves to the last). Dev
128
+ * builds `console.warn` on a duplicate. */
117
129
  options: SelectItem[];
118
130
  /** The per-row mark for **single**-select: `'none'` (a tint-only highlight),
119
131
  * `'check'` (a trailing checkmark on the selected row, keeping the tint — the
@@ -204,11 +216,13 @@ export type SelectProps = SelectCommonProps & ({
204
216
  * array value.
205
217
  * @defaultValue single */
206
218
  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. */
219
+ /** Controlled value the `value` string of the selected option. When
220
+ * provided, the consumer owns selection: the field follows this prop and a
221
+ * pick only *requests* a change via `onValueChange` (reject by not updating).
222
+ * Leave undefined for uncontrolled. */
210
223
  value?: string;
211
- /** Initial value for the uncontrolled case (the wrapper then owns it). */
224
+ /** Initial value (an option's `value` string) for the uncontrolled case the
225
+ * wrapper then owns it. */
212
226
  defaultValue?: string;
213
227
  /** Fires after the selection changes, with the new value and a
214
228
  * `{ value, option }` snapshot. Select has no discrete element state, so
@@ -218,15 +232,65 @@ export type SelectProps = SelectCommonProps & ({
218
232
  /** Multi-select: checkboxes on every row, the menu stays open while
219
233
  * toggling, the field shows an "N selected" count, and `value` is an array. */
220
234
  selection: 'multiple';
221
- /** Controlled values (see the single-select `value` note). */
235
+ /** Controlled values the `value` strings of the selected options (see the
236
+ * single-select `value` note). */
222
237
  value?: string[];
223
- /** Initial values for the uncontrolled case. */
238
+ /** Initial values (option `value` strings) for the uncontrolled case. */
224
239
  defaultValue?: string[];
225
240
  /** Fires after any toggle, with the new value array and a `{ value, option,
226
241
  * selected }` snapshot of the row that changed (or `{ all: true }` for the
227
242
  * Select-all row). */
228
243
  onValueChange?: (value: string[], attrs: SelectChangeAttrs) => void;
229
244
  });
245
+ /** Rolled-up selection of a group / submenu subtree: `'none'` / `'all'` of the
246
+ * descendant leaves selected, or `'some'` in between. On `SelectedGroup` /
247
+ * `SelectedSubmenu`, for a section indicator or custom styling. */
248
+ export type SelectionState = 'none' | 'some' | 'all';
249
+ /** A leaf option annotated with its current selection — a `SelectOption` plus
250
+ * `selected`. Returned by {@link optionsWithSelection}. */
251
+ export type SelectedOption = SelectOption & {
252
+ selected: boolean;
253
+ };
254
+ /** A group with its descendants annotated and a rolled-up `selectionState`.
255
+ * Returned by {@link optionsWithSelection}. */
256
+ export interface SelectedGroup extends Omit<SelectGroup, 'options'> {
257
+ options: SelectedItem[];
258
+ selectionState: SelectionState;
259
+ }
260
+ /** A submenu with its descendants annotated and a rolled-up `selectionState`.
261
+ * Returned by {@link optionsWithSelection}. */
262
+ export interface SelectedSubmenu extends Omit<SelectSubmenu, 'submenu'> {
263
+ submenu: SelectedItem[];
264
+ selectionState: SelectionState;
265
+ }
266
+ /** One node of the tree from {@link optionsWithSelection}: a leaf `SelectedOption`
267
+ * (with `selected`), or a `SelectedGroup` / `SelectedSubmenu` (annotated children
268
+ * + rolled-up `selectionState`). Mirrors `SelectItem` minus the bare-string
269
+ * shorthand — strings are normalized to `SelectedOption`. */
270
+ export type SelectedItem = SelectedOption | SelectedGroup | SelectedSubmenu;
271
+ /**
272
+ * Project a `Select` `options` tree onto a set of selected values: returns a mirror
273
+ * of the tree with every leaf marked `selected` and every group / submenu carrying a
274
+ * rolled-up `selectionState` (`'none'` / `'some'` / `'all'` of its descendant leaves).
275
+ * Bare-string options are normalized to `{ value, label }`; structure and order are
276
+ * preserved.
277
+ *
278
+ * A pure function of `(options, values)` — it needs no `Select` instance and reads
279
+ * nothing off the change event, so it behaves identically in controlled and
280
+ * uncontrolled code. Pass the current `value` (a string, an array, or `undefined`)
281
+ * and render a grouped summary, drive section indicators, or diff picks.
282
+ *
283
+ * A value that appears under more than one section marks the leaf in *every* place it
284
+ * occurs (selection is value-keyed), mirroring how the menu itself renders it.
285
+ *
286
+ * @example
287
+ * ```tsx
288
+ * const tree = optionsWithSelection(options, values)
289
+ * // tree[1] === { label: 'Engineering', selectionState: 'some', options: [
290
+ * // { value: 'eng-fe', label: 'Frontend', selected: false }, … ] }
291
+ * ```
292
+ */
293
+ export declare function optionsWithSelection(options: SelectItem[], values: string | string[] | undefined): SelectedItem[];
230
294
  /**
231
295
  * `<Select>` — a single- or multi-select dropdown, composed from `<Input>` (a
232
296
  * 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,
@@ -47,6 +73,7 @@ const Select = (props) => {
47
73
  const currentRaw = controlled ? value : internal;
48
74
  const [open, setOpen] = useState(false);
49
75
  const [query, setQuery] = useState("");
76
+ const [activeId, setActiveId] = useState(null);
50
77
  const uid = useId();
51
78
  const isSubmenu = (it) => typeof it === "object" && Array.isArray(it.submenu);
52
79
  const isGroup = (it) => typeof it === "object" && Array.isArray(it.options);
@@ -62,6 +89,12 @@ const Select = (props) => {
62
89
  };
63
90
  const allLeaves = [];
64
91
  collectLeaves(options, void 0, false, allLeaves);
92
+ const seenValues = /* @__PURE__ */ new Set();
93
+ for (const l of allLeaves) {
94
+ if (seenValues.has(l.opt.value))
95
+ console.warn(`[anta] <Select> duplicate option value=${JSON.stringify(l.opt.value)} \u2014 values must be unique.`);
96
+ seenValues.add(l.opt.value);
97
+ }
65
98
  const byValue = new Map(allLeaves.map((l) => [l.opt.value, l.opt]));
66
99
  const filtering = filter !== void 0 && filter !== false;
67
100
  const q = query.trim();
@@ -98,8 +131,14 @@ const Select = (props) => {
98
131
  const o = byValue.get(currentRaw);
99
132
  display = o ? o.label ?? o.value : "";
100
133
  }
101
- const choose = (o) => {
134
+ const choose = (o, e) => {
102
135
  if (multiple) {
136
+ if (selectAll && e?.altKey) {
137
+ const next2 = [o.value];
138
+ if (!controlled) setInternal(next2);
139
+ emit?.(next2, { value: o.value, option: o, selected: true });
140
+ return;
141
+ }
103
142
  const has = selectedValues.includes(o.value);
104
143
  const next = has ? selectedValues.filter((v) => v !== o.value) : [...selectedValues, o.value];
105
144
  if (!controlled) setInternal(next);
@@ -124,7 +163,10 @@ const Select = (props) => {
124
163
  const custom = renderOption?.(o, optState);
125
164
  const customMark = renderIndicator?.(optState);
126
165
  const ariaSelectable = !multiple && mark === "none" ? { role: "menuitemradio", "aria-checked": isSelected(o.value) ? "true" : "false" } : void 0;
127
- return /* @__PURE__ */ jsx(
166
+ const isolateHint = multiple && selectAll && !disabled2 ? IS_MAC ? "\u2325+Click to select only this" : "Alt+Click to select only this" : void 0;
167
+ const tip = o.tooltip ?? isolateHint;
168
+ const hintOnly = o.tooltip == null && isolateHint != null;
169
+ return /* @__PURE__ */ jsxs(
128
170
  MenuItem,
129
171
  {
130
172
  id: `${uid}-opt-${o.value}`,
@@ -139,8 +181,11 @@ const Select = (props) => {
139
181
  selected: isSelected(o.value),
140
182
  disabled: disabled2 || void 0,
141
183
  "data-menu-open": multiple ? "" : void 0,
142
- onSelect: () => choose(o),
143
- children: custom
184
+ onSelect: (e) => choose(o, e),
185
+ children: [
186
+ custom,
187
+ tip && /* @__PURE__ */ jsx(Tooltip, { ...hintOnly ? { follow: true, delay: 700 } : {}, children: tip })
188
+ ]
144
189
  },
145
190
  o.value
146
191
  );
@@ -246,8 +291,12 @@ const Select = (props) => {
246
291
  {
247
292
  onStateChange: (_e, { next }) => {
248
293
  setOpen(next);
249
- if (!next) setQuery("");
294
+ if (!next) {
295
+ setQuery("");
296
+ setActiveId(null);
297
+ }
250
298
  },
299
+ onactivedescendant: (e) => setActiveId((e.nativeEvent ?? e).detail?.id ?? null),
251
300
  children: [
252
301
  filtering && // `slot="header"` pins the field in the Menu's fixed header region (above
253
302
  // the scrolling options); `data-menu-search` puts the menu in combobox
@@ -261,6 +310,7 @@ const Select = (props) => {
261
310
  placeholder: "Filter\u2026",
262
311
  "aria-label": "Filter options",
263
312
  "aria-autocomplete": "list",
313
+ "aria-activedescendant": open && activeId ? activeId : void 0,
264
314
  onInput: (e) => setQuery(e.currentTarget.value)
265
315
  }
266
316
  ) }),
@@ -286,5 +336,6 @@ const Select = (props) => {
286
336
  ] });
287
337
  };
288
338
  export {
289
- Select
339
+ Select,
340
+ optionsWithSelection
290
341
  };