@antadesign/anta 0.3.2 → 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.
Files changed (96) hide show
  1. package/dist/anta_helpers.d.ts +97 -0
  2. package/dist/anta_helpers.js +70 -0
  3. package/dist/calendar-core.d.ts +126 -0
  4. package/dist/calendar-core.js +289 -0
  5. package/dist/components/Button.d.ts +9 -1
  6. package/dist/components/Button.js +5 -2
  7. package/dist/components/Calendar.d.ts +85 -0
  8. package/dist/components/Calendar.js +226 -0
  9. package/dist/components/Checkbox.d.ts +16 -11
  10. package/dist/components/Checkbox.js +13 -7
  11. package/dist/components/Expander.d.ts +7 -1
  12. package/dist/components/Expander.js +4 -2
  13. package/dist/components/Input.d.ts +13 -5
  14. package/dist/components/Input.js +14 -6
  15. package/dist/components/InputDate.d.ts +89 -0
  16. package/dist/components/InputDate.js +273 -0
  17. package/dist/components/InputDate.module.css +1 -0
  18. package/dist/components/Menu.d.ts +14 -1
  19. package/dist/components/Menu.js +6 -2
  20. package/dist/components/MenuItem.d.ts +48 -7
  21. package/dist/components/MenuItem.js +72 -14
  22. package/dist/components/MenuSeparator.d.ts +13 -2
  23. package/dist/components/MenuSeparator.js +12 -2
  24. package/dist/components/Progress.d.ts +4 -1
  25. package/dist/components/Progress.js +4 -3
  26. package/dist/components/RadioGroup.d.ts +12 -10
  27. package/dist/components/RadioGroup.js +5 -5
  28. package/dist/components/Select.d.ts +319 -0
  29. package/dist/components/Select.js +341 -0
  30. package/dist/components/Select.module.css +1 -0
  31. package/dist/components/Tab.d.ts +3 -0
  32. package/dist/components/Tab.js +2 -1
  33. package/dist/components/TabPanel.d.ts +21 -13
  34. package/dist/components/TabPanel.js +13 -1
  35. package/dist/components/Tabs.d.ts +75 -40
  36. package/dist/components/Tabs.js +20 -88
  37. package/dist/components/Tag.d.ts +10 -9
  38. package/dist/components/Tag.js +2 -2
  39. package/dist/components/Text.d.ts +6 -5
  40. package/dist/components/Title.d.ts +4 -2
  41. package/dist/components/Tooltip.d.ts +4 -1
  42. package/dist/components/Tooltip.js +5 -0
  43. package/dist/elements/a-button.css +1 -1
  44. package/dist/elements/a-calendar.css +1 -0
  45. package/dist/elements/a-calendar.d.ts +76 -0
  46. package/dist/elements/a-calendar.js +190 -0
  47. package/dist/elements/a-checkbox.css +1 -1
  48. package/dist/elements/a-checkbox.d.ts +1 -2
  49. package/dist/elements/a-checkbox.js +5 -5
  50. package/dist/elements/a-expander.css +1 -1
  51. package/dist/elements/a-expander.d.ts +16 -5
  52. package/dist/elements/a-expander.js +59 -8
  53. package/dist/elements/a-icon.shapes.css +1 -1
  54. package/dist/elements/a-icon.shapes.d.ts +6 -1
  55. package/dist/elements/a-icon.shapes.js +9 -0
  56. package/dist/elements/a-input.css +1 -1
  57. package/dist/elements/a-input.d.ts +6 -0
  58. package/dist/elements/a-input.js +28 -8
  59. package/dist/elements/a-menu-group.css +1 -1
  60. package/dist/elements/a-menu-item.css +1 -1
  61. package/dist/elements/a-menu-item.d.ts +22 -10
  62. package/dist/elements/a-menu-item.js +17 -0
  63. package/dist/elements/a-menu-separator.css +1 -1
  64. package/dist/elements/a-menu.css +1 -1
  65. package/dist/elements/a-menu.d.ts +61 -20
  66. package/dist/elements/a-menu.js +337 -58
  67. package/dist/elements/a-progress.css +1 -1
  68. package/dist/elements/a-radio-group.d.ts +1 -3
  69. package/dist/elements/a-radio-group.js +13 -13
  70. package/dist/elements/a-radio.css +1 -1
  71. package/dist/elements/a-radio.d.ts +3 -11
  72. package/dist/elements/a-radio.js +3 -34
  73. package/dist/elements/a-tab.css +1 -1
  74. package/dist/elements/a-tab.d.ts +3 -11
  75. package/dist/elements/a-tab.js +3 -34
  76. package/dist/elements/a-tabpanel.css +1 -1
  77. package/dist/elements/a-tabpanel.d.ts +17 -0
  78. package/dist/elements/a-tabpanel.js +66 -0
  79. package/dist/elements/a-tabs.css +1 -1
  80. package/dist/elements/a-tabs.d.ts +1 -4
  81. package/dist/elements/a-tabs.js +14 -14
  82. package/dist/elements/a-tag.css +1 -1
  83. package/dist/elements/a-text.d.ts +14 -13
  84. package/dist/elements/a-text.js +53 -23
  85. package/dist/elements/a-tooltip.css +1 -1
  86. package/dist/elements/a-tooltip.d.ts +1 -11
  87. package/dist/elements/a-tooltip.js +20 -19
  88. package/dist/elements/index.d.ts +2 -1
  89. package/dist/elements/index.js +6 -1
  90. package/dist/general_types.d.ts +158 -42
  91. package/dist/index.d.ts +10 -4
  92. package/dist/index.js +38 -3
  93. package/dist/jsx-runtime.d.ts +16 -7
  94. package/dist/jsx-runtime.js +6 -0
  95. package/dist/reset.css +1 -1
  96. package/package.json +4 -2
@@ -0,0 +1,89 @@
1
+ import type { BaseProps } from '../general_types';
2
+ import type { IconShape } from '../elements/a-icon.shapes';
3
+ /** Snapshot passed as the 2nd argument to `onValueChange` — the new ISO value
4
+ * (`''` when cleared) plus the field name (mirrors `Input` / `Calendar`). */
5
+ export interface InputDateChangeAttrs {
6
+ value: string;
7
+ name?: string;
8
+ }
9
+ /** `<InputDate>` props — `Calendar`'s date surface plus `Input`'s field surface. */
10
+ export interface InputDateProps extends Omit<BaseProps, 'children'> {
11
+ /** Controlled value — ISO `YYYY-MM-DD` (or `YYYY-MM-DDTHH:mm` with `time`), `''`
12
+ * for empty. Pair with `onValueChange`; the field and calendar follow it and a
13
+ * pick only *requests* a change. */
14
+ value?: string;
15
+ /** Initial value for the uncontrolled case. */
16
+ defaultValue?: string;
17
+ /** Earliest selectable date (ISO). Earlier days disable, and a typed date before
18
+ * it stays uncommitted. */
19
+ min?: string;
20
+ /** Latest selectable date (ISO). */
21
+ max?: string;
22
+ /** BCP-47 locale driving the display format, the placeholder mask, parsing order,
23
+ * and the calendar.
24
+ * @defaultValue navigator.language */
25
+ locale?: string;
26
+ /** Form field name — the ISO value submits under this key. */
27
+ name?: string;
28
+ /** Field size. Also sizes the calendar and the time row.
29
+ * @defaultValue medium */
30
+ size?: 'small' | 'medium' | 'large';
31
+ /** Disable the field and the calendar. */
32
+ disabled?: boolean;
33
+ /** Field label, above the control. */
34
+ label?: React.ReactNode;
35
+ /** Helper text under the field. Replaced by a format hint while the entry is
36
+ * unrecognized. */
37
+ hint?: React.ReactNode;
38
+ /** Placeholder shown when empty.
39
+ * @defaultValue the locale's format mask (e.g. `MM/DD/YYYY`, `MM/DD/YYYY HH:MM` with `time`) */
40
+ placeholder?: string;
41
+ /** Validation/feedback tone. An unrecognized entry forces `critical` until fixed.
42
+ * @defaultValue neutral */
43
+ status?: 'neutral' | 'brand' | 'info' | 'success' | 'warning' | 'critical';
44
+ /** Round the field corners — `true` for fully round, or a number / CSS length. */
45
+ round?: boolean | number | string;
46
+ /** Show a clear button once the field has a value. */
47
+ clearable?: boolean;
48
+ /** Leading icon at the start of the field — the calendar affordance.
49
+ * @defaultValue calendar-days */
50
+ icon?: IconShape;
51
+ /** Include a time. The value becomes ISO `YYYY-MM-DDTHH:mm`, the field parses a
52
+ * trailing time after a space (`06/15/2026 14:30`, `… 2:30pm`), and the menu
53
+ * shows a time row (hours : minutes, an AM/PM toggle in 12-hour locales, then a
54
+ * Done button) under the calendar. Picking a day keeps the menu open so you can
55
+ * set the time. */
56
+ time?: boolean;
57
+ /** Force the time cycle when `time` is on: `true` for 12-hour (AM/PM), `false`
58
+ * for 24-hour. Omit to follow the locale (`en-US` → 12-hour, most others → 24). */
59
+ hour12?: boolean;
60
+ /** Fired after the value resolves (a recognized entry, a calendar pick, a time
61
+ * change, or a clear), with the new ISO value (`''` when cleared) and a
62
+ * `{ value, name }` snapshot. An unrecognized entry does not fire it. */
63
+ onValueChange?: (value: string, attrs: InputDateChangeAttrs) => void;
64
+ }
65
+ /**
66
+ * `<InputDate>` — a date (or date-time) field that replaces the native
67
+ * `type="date"` / `type="datetime-local"` input, **composed** from `Input` +
68
+ * `Menu` + `Calendar` (no `a-inputdate` element; the wrapper is the coordinator).
69
+ * The field accepts free text and resolves it on commit (blur / Enter) with a
70
+ * lenient, locale-aware parser, then rewrites the entry to the canonical format;
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`.
76
+ *
77
+ * Controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`).
78
+ * Requires `@antadesign/anta/elements` (client-side only).
79
+ *
80
+ * @remarks Not SSR-safe (it embeds `Calendar`): it reads the current date
81
+ * (`Temporal.Now`) and locale (`navigator.language`) at render, so server and
82
+ * client output diverge and hydration mismatches. Render it client-side only.
83
+ *
84
+ * @example
85
+ * ```tsx
86
+ * <InputDate label="Starts" time defaultValue="2026-06-15T09:00" onValueChange={(v) => save(v)} />
87
+ * ```
88
+ */
89
+ export declare const InputDate: ({ value, defaultValue, min, max, locale, name, size, disabled, label, hint, placeholder, status, round, clearable, icon, time, hour12, onValueChange, className, style, ...rest }: InputDateProps) => any;
@@ -0,0 +1,273 @@
1
+ import { Fragment, jsx, jsxs } from "@antadesign/anta/jsx-runtime";
2
+ import cn from "clsx";
3
+ import { useMemo, useState } from "../jsx-runtime";
4
+ import { Temporal } from "temporal-polyfill";
5
+ import {
6
+ parseISODate,
7
+ parseDateInput,
8
+ formatDateInput,
9
+ dateFormatPattern,
10
+ parseDateTimeInput,
11
+ formatDateTimeInput,
12
+ dateTimeFormatPattern,
13
+ usesHour12
14
+ } from "../calendar-core";
15
+ import { Input } from "./Input";
16
+ import { Menu } from "./Menu";
17
+ import { Calendar } from "./Calendar";
18
+ import { Button } from "./Button";
19
+ import { Icon } from "./Icon";
20
+ import { Tabs } from "./Tabs";
21
+ import styles from "./InputDate.module.css";
22
+ const InputDate = ({
23
+ value,
24
+ defaultValue,
25
+ min,
26
+ max,
27
+ locale,
28
+ name,
29
+ size,
30
+ disabled,
31
+ label,
32
+ hint,
33
+ placeholder,
34
+ status,
35
+ round,
36
+ clearable,
37
+ icon,
38
+ time,
39
+ hour12,
40
+ onValueChange,
41
+ className,
42
+ style,
43
+ ...rest
44
+ }) => {
45
+ const resolvedLocale = locale || (typeof navigator !== "undefined" ? navigator.language : "en-US");
46
+ const minD = parseISODate(min);
47
+ const maxD = parseISODate(max);
48
+ const twelveHour = time ? hour12 ?? usesHour12(resolvedLocale) : false;
49
+ const pattern = useMemo(
50
+ () => placeholder ?? (time ? dateTimeFormatPattern(resolvedLocale, twelveHour) : dateFormatPattern(resolvedLocale)),
51
+ [placeholder, time, resolvedLocale, twelveHour]
52
+ );
53
+ const fmt = (iso) => {
54
+ if (!iso) return "";
55
+ const d = parseISODate(iso.slice(0, 10));
56
+ if (!d) return "";
57
+ if (!time) return formatDateInput(d, resolvedLocale);
58
+ const hasT = iso.length >= 16 && iso[10] === "T";
59
+ const dt = d.toPlainDateTime(Temporal.PlainTime.from(hasT ? iso.slice(11, 16) : "00:00"));
60
+ return formatDateTimeInput(dt, resolvedLocale, twelveHour);
61
+ };
62
+ const normalize = (iso) => !iso || !time ? iso : iso.length <= 10 ? `${iso}T00:00` : iso.slice(0, 16);
63
+ const controlled = value !== void 0;
64
+ const [internal, setInternal] = useState(() => normalize(defaultValue));
65
+ const current = controlled ? value : internal;
66
+ const dateISO = current ? current.slice(0, 10) : "";
67
+ const hasTime = !!current && current.length >= 16 && current[10] === "T";
68
+ const h24 = hasTime ? parseInt(current.slice(11, 13), 10) || 0 : 0;
69
+ const curM = hasTime ? current.slice(14, 16) : "00";
70
+ const curMer = h24 < 12 ? "AM" : "PM";
71
+ const curHourShown = twelveHour ? String((h24 + 11) % 12 + 1).padStart(2, "0") : String(h24).padStart(2, "0");
72
+ const [text, setText] = useState(() => fmt(current));
73
+ const [open, setOpen] = useState(false);
74
+ const [invalid, setInvalid] = useState(false);
75
+ const [focusNonce, setFocusNonce] = useState(0);
76
+ const [hourText, setHourText] = useState(curHourShown);
77
+ const [minuteText, setMinuteText] = useState(curM);
78
+ const meridiem = curMer;
79
+ const [lastValue, setLastValue] = useState(current);
80
+ if (current !== lastValue) {
81
+ setLastValue(current);
82
+ setText(fmt(current));
83
+ setInvalid(false);
84
+ }
85
+ const [lastTime, setLastTime] = useState(`${h24}:${curM}`);
86
+ if (time && `${h24}:${curM}` !== lastTime) {
87
+ setLastTime(`${h24}:${curM}`);
88
+ setHourText(curHourShown);
89
+ setMinuteText(curM);
90
+ }
91
+ const commit = (iso) => {
92
+ if (!controlled) setInternal(iso || void 0);
93
+ onValueChange?.(iso, { value: iso, name });
94
+ };
95
+ const clampPad = (v, hi) => String(Math.min(Math.max(parseInt(String(v || "0"), 10) || 0, 0), hi)).padStart(2, "0");
96
+ const resolveHour = (raw, mer) => {
97
+ let n = parseInt(String(raw || "0"), 10) || 0;
98
+ if (!twelveHour) {
99
+ const h = Math.min(Math.max(n, 0), 23);
100
+ return { h24: h, shown: String(h).padStart(2, "0"), mer };
101
+ }
102
+ if (n === 0) return { h24: 0, shown: "12", mer: "AM" };
103
+ if (n > 23) n = 23;
104
+ if (n > 12) return { h24: n, shown: String(n - 12).padStart(2, "0"), mer: "PM" };
105
+ return { h24: mer === "AM" ? n % 12 : n % 12 + 12, shown: String(n).padStart(2, "0"), mer };
106
+ };
107
+ const withTime = (d) => {
108
+ const { h24: h } = resolveHour(hourText, meridiem);
109
+ return `${d}T${String(h).padStart(2, "0")}:${clampPad(minuteText, 59)}`;
110
+ };
111
+ const apply = (iso) => {
112
+ setInvalid(false);
113
+ if (iso === (current ?? "")) setText(fmt(iso));
114
+ else commit(iso);
115
+ };
116
+ const resolve = (raw) => {
117
+ const t = raw.trim();
118
+ if (!t) {
119
+ setInvalid(false);
120
+ if (current) commit("");
121
+ else setText("");
122
+ return;
123
+ }
124
+ const parsed = time ? parseDateTimeInput(t, resolvedLocale, { min: minD, max: maxD }) : parseDateInput(t, resolvedLocale, { min: minD, max: maxD });
125
+ if (!parsed) {
126
+ setInvalid(true);
127
+ return;
128
+ }
129
+ const iso = time ? parsed.toString({ smallestUnit: "minute" }) : parsed.toString();
130
+ apply(iso);
131
+ };
132
+ const commitTime = (hRaw, mRaw, mer) => {
133
+ const { h24: h, shown } = resolveHour(hRaw, mer);
134
+ const mm = clampPad(mRaw, 59);
135
+ setHourText(shown);
136
+ setMinuteText(mm);
137
+ const base = dateISO || Temporal.Now.plainDateISO().toString();
138
+ commit(`${base}T${String(h).padStart(2, "0")}:${mm}`);
139
+ };
140
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
141
+ /* @__PURE__ */ jsx(
142
+ Input,
143
+ {
144
+ label,
145
+ hint: invalid ? `Unrecognized date. Try ${pattern}.` : hint,
146
+ status: invalid ? "critical" : status,
147
+ size,
148
+ round,
149
+ disabled,
150
+ clearable,
151
+ leading: /* @__PURE__ */ jsx(Icon, { shape: icon ?? "calendar-days" }),
152
+ value: text,
153
+ placeholder: pattern,
154
+ inputMode: time ? "text" : "numeric",
155
+ autoComplete: "off",
156
+ "aria-haspopup": "dialog",
157
+ "aria-expanded": open ? "true" : "false",
158
+ onInput: (e) => {
159
+ setText(e.currentTarget.value);
160
+ if (invalid) setInvalid(false);
161
+ },
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
+ }
169
+ },
170
+ className: cn(styles.dateField, className),
171
+ style,
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: [
183
+ /* @__PURE__ */ jsx(
184
+ Calendar,
185
+ {
186
+ value: dateISO,
187
+ min,
188
+ max,
189
+ locale,
190
+ size,
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
+ }
199
+ }
200
+ ),
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
+ ] })
266
+ }
267
+ ),
268
+ name ? /* @__PURE__ */ jsx("input", { type: "hidden", name, value: current ?? "" }) : null
269
+ ] });
270
+ };
271
+ export {
272
+ InputDate
273
+ };
@@ -0,0 +1 @@
1
+ .calendarMenu{--menu-padding: 8px}.timeRow{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:6px;padding-top:8px;border-top:1px solid var(--border-3)}.time{display:flex;align-items:center;gap:4px}.clock{flex:none;color:var(--text-5);margin-inline-end:2px}.sep{color:var(--text-3);user-select:none}.timeField{width:2.25rem}.timeField[size=small]{width:2rem}.timeField[size=large]{width:2.5rem}.timeField::part(input){text-align:center;font-variant-numeric:tabular-nums;font-weight:500}.meridiem{margin-inline-start:4px;--tab-padding-x: 6px;--tabs-gap: 0px;--tab-padding-y: max(0px, calc((10px - var(--_pb)) / 2 - 2px))}.meridiem a-tab{font-weight:400}.dateField::part(input){text-overflow:ellipsis}
@@ -29,6 +29,10 @@ export interface MenuProps extends BaseProps {
29
29
  * in response. Submenus are always uncontrolled regardless of this. See
30
30
  * STATEFUL-COMPONENTS.md. */
31
31
  open?: boolean;
32
+ /** Round the menu: the container softens to a 20px radius and its items go
33
+ * fully round. A `number` (px) or CSS length string tunes the container radius
34
+ * only — items stay full pills. */
35
+ round?: boolean | number | string;
32
36
  /** Fired before the open state changes — on open, and on every dismiss (Esc,
33
37
  * outside-click, scroll, selecting an item). `event` is the cancelable
34
38
  * `statechange`; `detail.next`/`detail.prev` are the requested/previous open
@@ -40,6 +44,15 @@ export interface MenuProps extends BaseProps {
40
44
  next: boolean;
41
45
  prev: boolean;
42
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;
43
56
  /** The menu's contents: `MenuItem`, `MenuSeparator`, `MenuGroup`, or any
44
57
  * custom element. */
45
58
  children?: React.ReactNode;
@@ -70,4 +83,4 @@ export interface MenuProps extends BaseProps {
70
83
  * </Menu>
71
84
  * ```
72
85
  */
73
- export declare const Menu: ({ placement, context, coord, nohover, offset, open, onStateChange, className, children, ...rest }: MenuProps) => any;
86
+ export declare const Menu: ({ placement, context, coord, nohover, offset, open, onStateChange, round, className, style, children, ...rest }: MenuProps) => any;
@@ -1,5 +1,5 @@
1
1
  import { jsx } from "@antadesign/anta/jsx-runtime";
2
- import { nativeStateChange } from "../anta_helpers";
2
+ import { nativeStateChange, roundStyle } from "../anta_helpers";
3
3
  const Menu = ({
4
4
  placement,
5
5
  context,
@@ -8,7 +8,9 @@ const Menu = ({
8
8
  offset,
9
9
  open,
10
10
  onStateChange,
11
+ round,
11
12
  className,
13
+ style,
12
14
  children,
13
15
  ...rest
14
16
  }) => {
@@ -23,15 +25,17 @@ const Menu = ({
23
25
  state: open === void 0 ? void 0 : open ? "open" : "closed",
24
26
  onstatechange: onStateChange ? (e) => {
25
27
  const { event, detail } = nativeStateChange(e);
26
- if (detail)
28
+ if (detail && (detail.next === "open" || detail.next === "closed"))
27
29
  onStateChange(event, {
28
30
  next: detail.next === "open",
29
31
  prev: detail.prev === "open"
30
32
  });
31
33
  } : void 0,
34
+ round: round ? "" : void 0,
32
35
  role: "menu",
33
36
  "aria-orientation": "vertical",
34
37
  class: className,
38
+ style: roundStyle(round, "--menu-round", style),
35
39
  ...rest,
36
40
  children
37
41
  }
@@ -3,8 +3,14 @@ import type { IconShape } from '../elements/a-icon.shapes';
3
3
  export interface MenuItemProps extends BaseProps {
4
4
  /** Leading icon shape. */
5
5
  icon?: IconShape;
6
- /** The item's text. Omit and pass `children` for richer content. */
7
- label?: string;
6
+ /** The item's text. Usually a string, but any node is accepted — e.g. a filtered
7
+ * `Select` bolds the matched substring. Omit and pass `children` for richer
8
+ * content. */
9
+ label?: React.ReactNode;
10
+ /** Secondary text under the label — explanatory copy, like `RadioGroup`'s
11
+ * option `hint`. Requires `label` (it stacks in a column beneath it). Muted
12
+ * (`--text-3`) and tracks the row's `tone`. A string, or any node. */
13
+ hint?: React.ReactNode;
8
14
  /** A trailing keyboard-shortcut hint, e.g. `"⌘E"`. */
9
15
  kbd?: string;
10
16
  /** A trailing icon. On a `submenu` item this **overrides** the default
@@ -13,10 +19,45 @@ export interface MenuItemProps extends BaseProps {
13
19
  iconTrailing?: IconShape;
14
20
  /** Disable the item: greyed out, not focusable for activation, no close. */
15
21
  disabled?: boolean;
16
- /** Semantic tone colors the label, icon, and hover tint. `critical` is the
17
- * destructive action; `neutral` (the default) is the standard gray.
22
+ /** Mark the item as selected. On a plain row (no `selectionIndicator`) this is
23
+ * a persistent background tint, the same resting fill a pressed row shows. On a
24
+ * checkable row (`selectionIndicator` set) it instead drives the leading
25
+ * `checkbox` / `radio` indicator and the row's `aria-checked`. */
26
+ selected?: boolean;
27
+ /** Turn the row into a checkable item, driven by `selected` (the row stays the
28
+ * control and carries `aria-checked`):
29
+ * - `'checkbox'` → `role="menuitemcheckbox"`, a leading passive `<a-checkbox>`
30
+ * (before `icon`); the tint is dropped (the box carries state).
31
+ * - `'radio'` → `role="menuitemradio"`, a leading passive `<a-radio>`; tint dropped.
32
+ * - `'check'` → `role="menuitemradio"`, a trailing check glyph on the selected
33
+ * row *and* the background tint (the canonical single-select look).
34
+ * Omit for a plain row (the default). */
35
+ selectionIndicator?: 'checkbox' | 'radio' | 'check';
36
+ /** Only meaningful with `selectionIndicator="checkbox"`: render the box in the
37
+ * mixed state (`aria-checked="mixed"`) — e.g. a "Select all" row when some but
38
+ * not all of its options are selected. */
39
+ indeterminate?: boolean;
40
+ /** Replace the built-in selection-indicator *visual* with your own node,
41
+ * rendered at the **leading** edge (where the checkbox / radio sit). Pair with
42
+ * `selectionIndicator` to keep the semantics — the row stays the control and
43
+ * carries `role` + `aria-checked`; only the drawn mark changes. Suppresses the
44
+ * built-in checkbox / radio and the trailing `check` glyph. The node is made
45
+ * passive (aria-hidden, no pointer events) so the row owns the click. */
46
+ indicator?: React.ReactNode;
47
+ /** Semantic tone — colors the label, icon, and hover/selected tint (and the
48
+ * `checkbox`/`radio` indicator, which adopts it). A named tone, or any literal
49
+ * CSS color (`'#ff1493'`, `'rebeccapurple'`) for a one-off custom tone whose
50
+ * hue + chroma are kept while the lightness is pinned to match the brand text.
51
+ * `critical` is the destructive action; `neutral` (the default) is the standard
52
+ * gray.
18
53
  * @defaultValue neutral */
19
- tone?: 'neutral' | 'brand' | 'info' | 'success' | 'warning' | 'critical';
54
+ tone?: 'neutral' | 'brand' | 'info' | 'success' | 'warning' | 'critical' | (string & {});
55
+ /** Like `tone`, but applied only while the row is `selected` — an unselected row
56
+ * stays neutral. The whole selected row (label, icon, tint, and the `checkbox` /
57
+ * `radio` indicator) takes the tone. Same value set as `tone`; on a selected row
58
+ * `toneSelected` wins over `tone` when both are set.
59
+ * @defaultValue neutral */
60
+ toneSelected?: 'neutral' | 'brand' | 'info' | 'success' | 'warning' | 'critical' | (string & {});
20
61
  /** Marks this item as a submenu parent: adds the trailing chevron,
21
62
  * `aria-haspopup="menu"`, and an `aria-expanded` baseline (kept in sync by
22
63
  * the nested menu). Nest the flyout as a `<Menu>` child. */
@@ -32,7 +73,7 @@ export interface MenuItemProps extends BaseProps {
32
73
  * `{ value, label }` detail. */
33
74
  onSelect?: (event: any, detail: {
34
75
  value?: string | number;
35
- label?: string;
76
+ label?: React.ReactNode;
36
77
  }) => void;
37
78
  /** Item content. With `label` set, children are extra content — most
38
79
  * notably the nested `<Menu>` for a submenu parent. */
@@ -58,4 +99,4 @@ export interface MenuItemProps extends BaseProps {
58
99
  * </MenuItem>
59
100
  * ```
60
101
  */
61
- export declare const MenuItem: ({ icon, label, kbd, iconTrailing, disabled, tone, submenu, value, onSelect, className, children, ...rest }: MenuItemProps) => any;
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;
@@ -1,46 +1,104 @@
1
1
  import { jsx, jsxs } from "@antadesign/anta/jsx-runtime";
2
+ import { toneStyle } from "../anta_helpers";
2
3
  const MenuItem = ({
3
4
  icon,
4
5
  label,
6
+ hint,
5
7
  kbd,
6
8
  iconTrailing,
7
9
  disabled,
10
+ selected,
11
+ selectionIndicator,
12
+ indeterminate,
13
+ indicator,
8
14
  tone,
15
+ toneSelected,
9
16
  submenu,
10
17
  value,
11
18
  onSelect,
12
19
  className,
20
+ style,
13
21
  children,
14
22
  ...rest
15
23
  }) => {
24
+ const checkable = selectionIndicator === "checkbox" || selectionIndicator === "radio" || selectionIndicator === "check";
25
+ const role = selectionIndicator === "checkbox" ? "menuitemcheckbox" : selectionIndicator === "radio" || selectionIndicator === "check" ? "menuitemradio" : "menuitem";
26
+ const ariaChecked = !checkable ? void 0 : selectionIndicator === "checkbox" && indeterminate ? "mixed" : selected ? "true" : "false";
27
+ const keepTint = selected && (selectionIndicator === void 0 || selectionIndicator === "check");
28
+ const effectiveTone = selected && toneSelected || tone;
29
+ const toneAttr = effectiveTone && effectiveTone !== "neutral" ? effectiveTone : void 0;
16
30
  return /* @__PURE__ */ jsxs(
17
31
  "a-menu-item",
18
32
  {
19
- role: "menuitem",
33
+ role,
20
34
  tabIndex: 0,
21
35
  disabled: disabled ? "" : void 0,
22
- tone: tone && tone !== "neutral" ? tone : void 0,
36
+ selected: keepTint ? "" : void 0,
37
+ "aria-checked": ariaChecked,
38
+ tone: toneAttr,
39
+ style: toneStyle(effectiveTone, "--menu-item-tone-source", style),
23
40
  submenu: submenu ? "" : void 0,
24
41
  "aria-haspopup": submenu ? "menu" : void 0,
25
- "aria-expanded": submenu ? "false" : void 0,
26
42
  "aria-disabled": disabled ? "true" : void 0,
27
- onClick: disabled || !onSelect ? void 0 : (e) => {
28
- if (submenu) return;
29
- const t = e.target;
30
- if (t?.closest?.("a-menu-item") !== e.currentTarget) return;
31
- onSelect(e, { value, label });
32
- },
43
+ onmenuselect: onSelect ? (e) => onSelect(e, { value, label }) : void 0,
33
44
  class: className,
34
45
  ...rest,
35
46
  children: [
47
+ indicator != null ? (
48
+ // `min-height: 1lh` makes the wrapper one text line tall and centres the node
49
+ // in it, so a small indicator aligns with the label's first line in a hinted
50
+ // (top-aligned) row instead of riding at the top.
51
+ /* @__PURE__ */ jsx(
52
+ "span",
53
+ {
54
+ "aria-hidden": "true",
55
+ "data-indicator": "",
56
+ style: { flex: "none", display: "inline-flex", alignItems: "center", minHeight: "1lh", pointerEvents: "none" },
57
+ children: indicator
58
+ }
59
+ )
60
+ ) : selectionIndicator === "checkbox" ? /* @__PURE__ */ jsx(
61
+ "a-checkbox",
62
+ {
63
+ "aria-hidden": "true",
64
+ state: indeterminate ? "indeterminate" : selected ? "checked" : "unchecked",
65
+ tone: toneAttr,
66
+ style: toneStyle(effectiveTone, "--checkbox-tone-source")
67
+ }
68
+ ) : selectionIndicator === "radio" ? (
69
+ // Real boolean, not the ''/undefined presence form: `a-radio` has a
70
+ // `selected` *property* setter (`applyState(!!on)`), so Preact routes the
71
+ // prop through it — `''` would read as false and never select. A boolean
72
+ // drives the property correctly and clears stale state on deselect. (The
73
+ // checkbox above is safe: its `state` is attribute-only, no property.)
74
+ /* @__PURE__ */ jsx(
75
+ "a-radio",
76
+ {
77
+ "aria-hidden": "true",
78
+ selected: !!selected,
79
+ tone: toneAttr,
80
+ style: toneStyle(effectiveTone, "--radio-tone-source")
81
+ }
82
+ )
83
+ ) : null,
36
84
  icon && /* @__PURE__ */ jsx("a-icon", { shape: icon, "aria-hidden": "true" }),
37
- label != null && /* @__PURE__ */ jsx("a-menu-item-label", { children: label }),
85
+ label != null && (hint != null ? (
86
+ // A hint stacks under the label in a column; the icon / kbd / trailing
87
+ // icon stay in the row (the item's `align-items: center` centers them
88
+ // against the two-line block).
89
+ /* @__PURE__ */ jsxs("a-menu-item-text", { children: [
90
+ /* @__PURE__ */ jsx("a-menu-item-label", { children: label }),
91
+ /* @__PURE__ */ jsx("a-menu-item-hint", { children: hint })
92
+ ] })
93
+ ) : /* @__PURE__ */ jsx("a-menu-item-label", { children: label })),
94
+ children,
38
95
  kbd && /* @__PURE__ */ jsx("kbd", { children: kbd }),
39
- /* @__PURE__ */ (() => {
40
- const trailing = submenu ? iconTrailing ?? "chevron-right" : iconTrailing;
96
+ (() => {
97
+ let trailing = submenu ? iconTrailing ?? "chevron-right" : iconTrailing;
98
+ if (!submenu && !iconTrailing && selectionIndicator === "check" && indicator == null)
99
+ trailing = selected ? "check" : "blank";
41
100
  return trailing ? /* @__PURE__ */ jsx("a-icon", { shape: trailing, "aria-hidden": "true" }) : null;
42
- })(),
43
- children
101
+ })()
44
102
  ]
45
103
  }
46
104
  );
@@ -1,14 +1,25 @@
1
1
  import type { BaseProps } from '../general_types';
2
2
  export interface MenuSeparatorProps extends BaseProps {
3
+ /** Optional caption. With content, the separator renders as a small muted text
4
+ * row — a group / subsection name, or a utility message like `Loading…` — instead
5
+ * of a line, and the wrapper flips it to an `aria-live="polite"` status region
6
+ * (`role="status"`) so a changing message is announced. Empty, it's a plain divider. */
7
+ children?: React.ReactNode;
3
8
  }
4
9
  /**
5
- * MenuSeparator — a thin divider between groups of `MenuItem`s.
10
+ * MenuSeparator — a divider between groups of `MenuItem`s, or a small caption row.
11
+ *
12
+ * Empty, it's a thin line. Given text, it becomes a muted caption (a group name or
13
+ * a status message) and turns into an `aria-live="polite"` region — this is what
14
+ * `Select`'s `renderEmpty` uses for its "no results" / "loading" messages.
6
15
  *
7
16
  * @example
8
17
  * ```tsx
9
18
  * <MenuItem label="Edit" />
10
19
  * <MenuSeparator />
11
20
  * <MenuItem tone="critical" label="Delete" />
21
+ *
22
+ * <MenuSeparator>No results</MenuSeparator>
12
23
  * ```
13
24
  */
14
- export declare const MenuSeparator: ({ className, ...rest }: MenuSeparatorProps) => any;
25
+ export declare const MenuSeparator: ({ className, children, ...rest }: MenuSeparatorProps) => any;