@vitavision/ui 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6,6 +6,8 @@ import { twMerge } from "tailwind-merge";
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
  import { Slot, Slottable } from "@radix-ui/react-slot";
8
8
  import * as RadixDialog from "@radix-ui/react-dialog";
9
+ import * as RadixMenu from "@radix-ui/react-dropdown-menu";
10
+ import * as RadixPopover from "@radix-ui/react-popover";
9
11
  import * as RadixSelect from "@radix-ui/react-select";
10
12
  import * as RadixSlider from "@radix-ui/react-slider";
11
13
  import * as RadixCheckbox from "@radix-ui/react-checkbox";
@@ -563,6 +565,109 @@ function Disclosure({ summary, count, defaultOpen = false, className, children }
563
565
  });
564
566
  }
565
567
  //#endregion
568
+ //#region src/components/ui/Kbd.tsx
569
+ /**
570
+ * A keyboard key or chord (`F`, `⌘K`, `[`), set in mono on a raised key cap.
571
+ *
572
+ * Renders `<kbd>`, which screen readers announce as text. Write the key as the user's keyboard
573
+ * labels it; for a chord, one `Kbd` with the whole chord reads better than several. Takes
574
+ * every `<kbd>` prop, `ref` included.
575
+ */
576
+ function Kbd({ className, ...rest }) {
577
+ return /* @__PURE__ */ jsx("kbd", {
578
+ ...rest,
579
+ className: cn("inline-flex min-w-[1.5em] items-center justify-center rounded-control border border-line-strong", "bg-raised px-1 font-mono text-[10px] leading-4 text-fg-muted", className)
580
+ });
581
+ }
582
+ //#endregion
583
+ //#region src/components/ui/DropdownMenu.tsx
584
+ /**
585
+ * A menu (`role="menu"`) that opens from `trigger`, with arrow-key navigation, typeahead,
586
+ * Escape or a press outside to close, and focus returned to the trigger. It is non-modal: the
587
+ * page behind stays readable and operable. Toggling a `MenuCheckboxItem` keeps the menu open,
588
+ * so several layers can be switched in one visit.
589
+ */
590
+ function DropdownMenu({ trigger, children, open, onOpenChange, align = "start", side = "bottom", className }) {
591
+ return /* @__PURE__ */ jsxs(RadixMenu.Root, {
592
+ modal: false,
593
+ ...open === void 0 ? {} : { open },
594
+ ...onOpenChange === void 0 ? {} : { onOpenChange },
595
+ children: [/* @__PURE__ */ jsx(RadixMenu.Trigger, {
596
+ asChild: true,
597
+ children: trigger
598
+ }), /* @__PURE__ */ jsx(RadixMenu.Portal, { children: /* @__PURE__ */ jsx(RadixMenu.Content, {
599
+ side,
600
+ align,
601
+ sideOffset: 6,
602
+ collisionPadding: 8,
603
+ className: cn("z-50 min-w-44 rounded-panel border border-line bg-overlay p-1 text-sm text-fg shadow-lg shadow-black/25", className),
604
+ children
605
+ }) })]
606
+ });
607
+ }
608
+ const itemClasses = cn("relative flex h-7 cursor-default items-center gap-2 rounded-control pr-2 pl-7 text-sm outline-none select-none", "data-[highlighted]:bg-raised data-[disabled]:pointer-events-none data-[disabled]:opacity-50");
609
+ /**
610
+ * A command. `onSelect` runs when it is clicked or chosen with Enter, and the menu then
611
+ * closes. `tone="defect"` marks a destructive command.
612
+ */
613
+ function MenuItem({ children, onSelect, shortcut, disabled, icon, tone }) {
614
+ return /* @__PURE__ */ jsxs(RadixMenu.Item, {
615
+ ...disabled === void 0 ? {} : { disabled },
616
+ onSelect: () => onSelect?.(),
617
+ className: cn(itemClasses, tone === "defect" && "text-defect"),
618
+ children: [
619
+ icon && /* @__PURE__ */ jsx("span", {
620
+ "aria-hidden": true,
621
+ className: "absolute left-2 flex size-3.5 items-center [&>svg]:size-3.5",
622
+ children: icon
623
+ }),
624
+ /* @__PURE__ */ jsx("span", {
625
+ className: "min-w-0 flex-1 truncate",
626
+ children
627
+ }),
628
+ shortcut && /* @__PURE__ */ jsx(Kbd, { children: shortcut })
629
+ ]
630
+ });
631
+ }
632
+ /**
633
+ * A toggle (`role="menuitemcheckbox"`): a layer on or off. A check marks it on. Choosing it
634
+ * leaves the menu open.
635
+ */
636
+ function MenuCheckboxItem({ children, checked, onCheckedChange, shortcut, disabled }) {
637
+ return /* @__PURE__ */ jsxs(RadixMenu.CheckboxItem, {
638
+ checked,
639
+ onCheckedChange,
640
+ ...disabled === void 0 ? {} : { disabled },
641
+ onSelect: (event) => event.preventDefault(),
642
+ className: itemClasses,
643
+ children: [
644
+ /* @__PURE__ */ jsx(RadixMenu.ItemIndicator, {
645
+ className: "absolute left-2 flex items-center text-signal",
646
+ children: /* @__PURE__ */ jsx(Check, {
647
+ className: "size-3.5",
648
+ "aria-hidden": true
649
+ })
650
+ }),
651
+ /* @__PURE__ */ jsx("span", {
652
+ className: "min-w-0 flex-1 truncate",
653
+ children
654
+ }),
655
+ shortcut && /* @__PURE__ */ jsx(Kbd, { children: shortcut })
656
+ ]
657
+ });
658
+ }
659
+ /** A heading over a group of items, in the eyebrow role. Not focusable. */
660
+ function MenuLabel({ children }) {
661
+ return /* @__PURE__ */ jsx(RadixMenu.Label, {
662
+ className: "px-2 pt-1.5 pb-1 text-[11px] font-semibold tracking-wider text-fg-muted uppercase",
663
+ children
664
+ });
665
+ }
666
+ /** A divider between groups of items. */
667
+ function MenuSeparator() {
668
+ return /* @__PURE__ */ jsx(RadixMenu.Separator, { className: "my-1 h-px bg-line" });
669
+ }
670
+ //#endregion
566
671
  //#region src/components/ui/Feedback.tsx
567
672
  const CALLOUT = {
568
673
  info: {
@@ -780,6 +885,33 @@ function Field({ label, description, error, required = false, annotation, as = "
780
885
  });
781
886
  }
782
887
  //#endregion
888
+ //#region src/components/ui/numberText.ts
889
+ /**
890
+ * A number at a fixed number of decimals, without a negative zero (`-0.000` reads as a sign
891
+ * error in a coordinate readout).
892
+ *
893
+ * @param value - The number. Non-finite values print as `NaN`, `Infinity`, `-Infinity`.
894
+ * @param precision - Decimals, 0–20.
895
+ * @returns The text.
896
+ */
897
+ function formatNumber(value, precision) {
898
+ if (!Number.isFinite(value)) return String(value);
899
+ const text = value.toFixed(precision);
900
+ return /^-0(?:\.0*)?$/.test(text) ? text.slice(1) : text;
901
+ }
902
+ /**
903
+ * The number a field's text means, if it means one.
904
+ *
905
+ * @param text - What is in the field.
906
+ * @returns The number, or `null` for empty, partial (`-`, `1e`) or non-numeric text.
907
+ */
908
+ function parseNumber(text) {
909
+ const trimmed = text.trim();
910
+ if (trimmed === "") return null;
911
+ const value = Number(trimmed);
912
+ return Number.isFinite(value) ? value : null;
913
+ }
914
+ //#endregion
783
915
  //#region src/components/ui/Input.tsx
784
916
  /**
785
917
  * The shared look of a text-like control — border, palette, placeholder, hover, disabled and
@@ -823,11 +955,94 @@ function Input({ className, "aria-describedby": describedBy, ...rest }) {
823
955
  * With a `unit`, the input sits in a `relative` full-width wrapper carrying `data-unit`, keeps
824
956
  * room on its right for the unit, and is described by it; size such a field through its
825
957
  * container. Without one — or with `""` — the markup is exactly the plain input.
958
+ *
959
+ * With `onValueChange` the field is number-valued: `value` is a number (`null` shows empty), the
960
+ * typed text survives while the field has focus, and only finite numbers inside `[min, max]`
961
+ * are reported (see `onValueChange`; `onClear` reports an emptied field). Without it the field
962
+ * is the native text-valued input.
826
963
  */
827
- function NumberInput({ unit, ...props }) {
828
- return unit === void 0 || unit === "" ? /* @__PURE__ */ jsx(PlainNumberInput, { ...props }) : /* @__PURE__ */ jsx(UnitNumberInput, {
964
+ function NumberInput({ onValueChange, onClear, precision, ...props }) {
965
+ if (onValueChange) return /* @__PURE__ */ jsx(ValueNumberInput, {
966
+ ...props,
967
+ onValueChange,
968
+ onClear,
969
+ precision
970
+ });
971
+ const { unit, ...rest } = props;
972
+ const text = rest.value === null ? {
973
+ ...rest,
974
+ value: ""
975
+ } : rest;
976
+ return unit === void 0 || unit === "" ? /* @__PURE__ */ jsx(PlainNumberInput, { ...text }) : /* @__PURE__ */ jsx(UnitNumberInput, {
829
977
  unit,
830
- ...props
978
+ ...text
979
+ });
980
+ }
981
+ /** The number-valued field: formatted at rest, the typed text while focused. */
982
+ function ValueNumberInput({ value, onValueChange, onClear, precision, step, min, max, onFocus, onBlur, onChange, onKeyDown, ...rest }) {
983
+ const numeric = typeof value === "number" ? value : null;
984
+ const show = (v) => v === null ? "" : precision === void 0 ? String(v) : formatNumber(v, precision);
985
+ const [draft, setDraft] = useState(null);
986
+ /** The number `text` commits, or `null` while it is empty, partial or out of range. */
987
+ const parse = (text) => {
988
+ const parsed = parseNumber(text);
989
+ if (parsed === null) return null;
990
+ if (min !== void 0 && parsed < min || max !== void 0 && parsed > max) return null;
991
+ return parsed;
992
+ };
993
+ /** An emptied field, committed: cleared if the caller allows it and it was not already empty. */
994
+ const commitEmpty = (text) => {
995
+ if (text.trim() === "" && numeric !== null) onClear?.();
996
+ };
997
+ const keyDown = (event) => {
998
+ if (draft && event.key === "Enter") {
999
+ commitEmpty(draft.text);
1000
+ setDraft({
1001
+ ...draft,
1002
+ text: draft.text.trim() === "" && onClear ? "" : show(numeric)
1003
+ });
1004
+ } else if (draft && event.key === "Escape") {
1005
+ event.preventDefault();
1006
+ if (draft.initial !== numeric) {
1007
+ if (draft.initial === null) onClear?.();
1008
+ else onValueChange(draft.initial);
1009
+ }
1010
+ setDraft({
1011
+ ...draft,
1012
+ text: show(draft.initial)
1013
+ });
1014
+ }
1015
+ onKeyDown?.(event);
1016
+ };
1017
+ return /* @__PURE__ */ jsx(NumberInput, {
1018
+ ...rest,
1019
+ value: draft?.text ?? show(numeric),
1020
+ step: step ?? "any",
1021
+ min,
1022
+ max,
1023
+ onFocus: (event) => {
1024
+ setDraft({
1025
+ text: show(numeric),
1026
+ initial: numeric
1027
+ });
1028
+ onFocus?.(event);
1029
+ },
1030
+ onBlur: (event) => {
1031
+ if (draft) commitEmpty(draft.text);
1032
+ setDraft(null);
1033
+ onBlur?.(event);
1034
+ },
1035
+ onKeyDown: keyDown,
1036
+ onChange: (event) => {
1037
+ const text = event.currentTarget.value;
1038
+ setDraft((current) => ({
1039
+ text,
1040
+ initial: current?.initial ?? numeric
1041
+ }));
1042
+ const parsed = parse(text);
1043
+ if (parsed !== null && parsed !== numeric) onValueChange(parsed);
1044
+ onChange?.(event);
1045
+ }
831
1046
  });
832
1047
  }
833
1048
  function PlainNumberInput({ className, "aria-describedby": describedBy, ...rest }) {
@@ -871,6 +1086,146 @@ function Textarea({ className, "aria-describedby": describedBy, ...rest }) {
871
1086
  });
872
1087
  }
873
1088
  //#endregion
1089
+ //#region src/components/ui/listboxModel.ts
1090
+ /**
1091
+ * The option a key moves the active one to, skipping disabled options.
1092
+ *
1093
+ * Arrows move by one and stop at the ends (no wrap: a long list that wraps loses its place).
1094
+ * Home and End go to the first and last enabled option. Page keys move by `page` options.
1095
+ * From no active option (`-1`), Down and Page Down go to the first enabled option, and Up and
1096
+ * Page Up to the last.
1097
+ *
1098
+ * @param disabled - Per option, whether it is disabled.
1099
+ * @param from - The active index, or -1.
1100
+ * @param key - The key pressed.
1101
+ * @param page - Options per page, for Page Up/Down. Defaults to 10.
1102
+ * @returns The new active index; `from` when there is nowhere to go; -1 when every option is
1103
+ * disabled.
1104
+ */
1105
+ function moveActive(disabled, from, key, page = 10) {
1106
+ const enabled = disabled.flatMap((off, index) => off ? [] : [index]);
1107
+ if (enabled.length === 0) return -1;
1108
+ const first = enabled[0];
1109
+ const last = enabled[enabled.length - 1];
1110
+ if (key === "Home") return first;
1111
+ if (key === "End") return last;
1112
+ if (from < 0) return key === "ArrowDown" || key === "PageDown" ? first : last;
1113
+ const forward = key === "ArrowDown" || key === "PageDown";
1114
+ const steps = key === "PageDown" || key === "PageUp" ? Math.max(1, page) : 1;
1115
+ let target = from;
1116
+ let moved = 0;
1117
+ for (let index = from + (forward ? 1 : -1); index >= 0 && index < disabled.length; index += forward ? 1 : -1) {
1118
+ if (disabled[index]) continue;
1119
+ target = index;
1120
+ moved += 1;
1121
+ if (moved === steps) break;
1122
+ }
1123
+ return target;
1124
+ }
1125
+ /**
1126
+ * The first enabled option at or after `index`, or before it when none is after. Used to place
1127
+ * the active option when the list opens or changes.
1128
+ *
1129
+ * @param disabled - Per option, whether it is disabled.
1130
+ * @param index - The preferred index.
1131
+ * @returns An enabled index, or -1 when every option is disabled.
1132
+ */
1133
+ function nearestEnabled(disabled, index) {
1134
+ for (let i = Math.max(0, index); i < disabled.length; i += 1) if (!disabled[i]) return i;
1135
+ for (let i = Math.min(index, disabled.length) - 1; i >= 0; i -= 1) if (!disabled[i]) return i;
1136
+ return -1;
1137
+ }
1138
+ //#endregion
1139
+ //#region src/components/ui/Listbox.tsx
1140
+ const KEYS = /* @__PURE__ */ new Set([
1141
+ "ArrowDown",
1142
+ "ArrowUp",
1143
+ "Home",
1144
+ "End",
1145
+ "PageDown",
1146
+ "PageUp"
1147
+ ]);
1148
+ /**
1149
+ * A single-select list (`role="listbox"`) of options that each render their own content.
1150
+ *
1151
+ * The list is one tab stop. The arrow keys, Home, End and Page keys move the active option
1152
+ * (`aria-activedescendant`), skipping disabled ones, and Enter or Space choose it. A click
1153
+ * chooses the option under the pointer. Controlled: the caller keeps `value`. Each option
1154
+ * carries `aria-selected`, `data-selected` and `data-active`; the list scrolls the active
1155
+ * option into view.
1156
+ */
1157
+ function Listbox({ options, value, onValueChange, renderOption = defaultRender, "aria-label": ariaLabel, autoFocus, className }) {
1158
+ const id = useId();
1159
+ const listRef = useRef(null);
1160
+ const disabled = options.map((option) => option.disabled === true);
1161
+ const selectedIndex = options.findIndex((option) => option.value === value);
1162
+ const [active, setActive] = useState(() => nearestEnabled(disabled, Math.max(0, selectedIndex)));
1163
+ const current = active >= 0 && active < options.length && !disabled[active] ? active : nearestEnabled(disabled, active);
1164
+ const optionId = (index) => `${id}-option-${index}`;
1165
+ const activate = (index) => {
1166
+ setActive(index);
1167
+ listRef.current?.querySelector(`#${CSS.escape(optionId(index))}`)?.scrollIntoView({ block: "nearest" });
1168
+ };
1169
+ const choose = (index) => {
1170
+ const option = options[index];
1171
+ if (!option || option.disabled) return;
1172
+ onValueChange?.(option.value);
1173
+ };
1174
+ const onKeyDown = (event) => {
1175
+ if (KEYS.has(event.key)) {
1176
+ event.preventDefault();
1177
+ const next = moveActive(disabled, current, event.key);
1178
+ if (next >= 0) activate(next);
1179
+ } else if (event.key === "Enter" || event.key === " ") {
1180
+ event.preventDefault();
1181
+ choose(current);
1182
+ }
1183
+ };
1184
+ return /* @__PURE__ */ jsx("div", {
1185
+ ref: listRef,
1186
+ role: "listbox",
1187
+ "aria-label": ariaLabel,
1188
+ tabIndex: 0,
1189
+ autoFocus,
1190
+ "aria-activedescendant": current >= 0 ? optionId(current) : void 0,
1191
+ onKeyDown,
1192
+ className: cn("flex flex-col gap-px rounded-control p-0.5 outline-none", focusRing, className),
1193
+ children: options.map((option, index) => {
1194
+ const selected = index === selectedIndex;
1195
+ const isActive = index === current;
1196
+ return /* @__PURE__ */ jsx("div", {
1197
+ id: optionId(index),
1198
+ role: "option",
1199
+ "aria-selected": selected,
1200
+ "aria-disabled": option.disabled ? true : void 0,
1201
+ "data-selected": selected ? "" : void 0,
1202
+ "data-active": isActive ? "" : void 0,
1203
+ onPointerMove: () => {
1204
+ if (!option.disabled && !isActive) setActive(index);
1205
+ },
1206
+ onClick: () => choose(index),
1207
+ className: cn("flex min-w-0 cursor-default items-center gap-2 rounded-control px-2 py-1 text-sm text-fg", "data-[selected]:text-signal data-[selected]:shadow-[inset_2px_0_0_var(--color-signal)]", "data-[active]:bg-raised", option.disabled && "opacity-50"),
1208
+ children: renderOption(option, {
1209
+ selected,
1210
+ active: isActive
1211
+ })
1212
+ }, option.value);
1213
+ })
1214
+ });
1215
+ }
1216
+ function defaultRender(option) {
1217
+ return /* @__PURE__ */ jsxs("span", {
1218
+ className: "flex min-w-0 flex-col",
1219
+ children: [/* @__PURE__ */ jsx("span", {
1220
+ className: "truncate",
1221
+ children: option.label
1222
+ }), option.description && /* @__PURE__ */ jsx("span", {
1223
+ className: "truncate text-xs text-fg-muted",
1224
+ children: option.description
1225
+ })]
1226
+ });
1227
+ }
1228
+ //#endregion
874
1229
  //#region src/components/ui/Panel.tsx
875
1230
  /**
876
1231
  * A bordered surface with an optional header (a title and actions) above a padded body.
@@ -1006,31 +1361,43 @@ function ReadoutStrip({ items, className }) {
1006
1361
  });
1007
1362
  }
1008
1363
  //#endregion
1009
- //#region src/components/ui/numberText.ts
1364
+ //#region src/components/ui/Popover.tsx
1010
1365
  /**
1011
- * A number at a fixed number of decimals, without a negative zero (`-0.000` reads as a sign
1012
- * error in a coordinate readout).
1366
+ * Floating content anchored to `trigger`, on the `overlay` surface with a shadow (floating
1367
+ * layers are the only ones that cast one).
1013
1368
  *
1014
- * @param value - The number. Non-finite values print as `NaN`, `Infinity`, `-Infinity`.
1015
- * @param precision - Decimals, 0–20.
1016
- * @returns The text.
1369
+ * Controlled (`open` / `onOpenChange`) or not (`defaultOpen`). Focus moves into the content
1370
+ * when it opens and back to the trigger when it closes. Escape and a press outside close it.
1371
+ * The content carries `data-state` and `data-side`.
1017
1372
  */
1018
- function formatNumber(value, precision) {
1019
- if (!Number.isFinite(value)) return String(value);
1020
- const text = value.toFixed(precision);
1021
- return /^-0(?:\.0*)?$/.test(text) ? text.slice(1) : text;
1373
+ function Popover({ trigger, children, open, defaultOpen, onOpenChange, side = "bottom", align = "start", "aria-label": ariaLabel, className }) {
1374
+ return /* @__PURE__ */ jsxs(RadixPopover.Root, {
1375
+ ...open === void 0 ? {} : { open },
1376
+ ...defaultOpen === void 0 ? {} : { defaultOpen },
1377
+ ...onOpenChange === void 0 ? {} : { onOpenChange },
1378
+ children: [/* @__PURE__ */ jsx(RadixPopover.Trigger, {
1379
+ asChild: true,
1380
+ children: trigger
1381
+ }), /* @__PURE__ */ jsx(RadixPopover.Portal, { children: /* @__PURE__ */ jsx(RadixPopover.Content, {
1382
+ side,
1383
+ align,
1384
+ sideOffset: 6,
1385
+ collisionPadding: 8,
1386
+ "aria-label": ariaLabel,
1387
+ className: cn("z-50 max-h-[var(--radix-popover-content-available-height)] overflow-y-auto", "rounded-panel border border-line bg-overlay p-1 text-sm text-fg shadow-lg shadow-black/25", className),
1388
+ children
1389
+ }) })]
1390
+ });
1022
1391
  }
1023
1392
  /**
1024
- * The number a field's text means, if it means one.
1025
- *
1026
- * @param text - What is in the field.
1027
- * @returns The number, or `null` for empty, partial (`-`, `1e`) or non-numeric text.
1393
+ * Closes the surrounding `Popover` when its child is activated. Wraps one element (Radix
1394
+ * `asChild`).
1028
1395
  */
1029
- function parseNumber(text) {
1030
- const trimmed = text.trim();
1031
- if (trimmed === "") return null;
1032
- const value = Number(trimmed);
1033
- return Number.isFinite(value) ? value : null;
1396
+ function PopoverClose({ children }) {
1397
+ return /* @__PURE__ */ jsx(RadixPopover.Close, {
1398
+ asChild: true,
1399
+ children
1400
+ });
1034
1401
  }
1035
1402
  //#endregion
1036
1403
  //#region src/components/ui/VectorInput.tsx
@@ -1044,8 +1411,8 @@ const DEFAULT_LABELS = [
1044
1411
  * A row of number fields editing one small vector, with the axis labels before the fields
1045
1412
  * and the unit once after them.
1046
1413
  *
1047
- * Controlled. A `role="group"` named by `aria-label`, each field named by its label and
1048
- * described by the unit. While a field has focus it shows what is typed; on blur or Enter it
1414
+ * Controlled. A `role="group"` named by `aria-label`. Each field is named by the group and its
1415
+ * label ("Translation x"), so two vectors on one screen stay apart, and is described by the unit. While a field has focus it shows what is typed; on blur or Enter it
1049
1416
  * shows the value at `precision`; Escape restores the value it had on focus. With `readOnly`
1050
1417
  * it renders a `ReadoutStrip` (`x 0.100 m · y …`) instead. The row carries `data-readonly`
1051
1418
  * and `data-disabled`.
@@ -1078,6 +1445,7 @@ function VectorInput({ value, onValueChange, labels = DEFAULT_LABELS, unit, step
1078
1445
  className: "shrink-0 font-mono text-[11px] text-fg-subtle",
1079
1446
  children: labelOf(index)
1080
1447
  }), /* @__PURE__ */ jsx(ComponentField, {
1448
+ name: `${ariaLabel} ${labelOf(index)}`,
1081
1449
  value: component,
1082
1450
  precision,
1083
1451
  step,
@@ -1095,7 +1463,7 @@ function VectorInput({ value, onValueChange, labels = DEFAULT_LABELS, unit, step
1095
1463
  });
1096
1464
  }
1097
1465
  /** One component: formatted at rest, the typed text while focused. */
1098
- function ComponentField({ value, precision, step, min, max, disabled, describedBy, onCommit }) {
1466
+ function ComponentField({ name, value, precision, step, min, max, disabled, describedBy, onCommit }) {
1099
1467
  const [draft, setDraft] = useState(null);
1100
1468
  const formatted = formatNumber(value, precision);
1101
1469
  const onKeyDown = (event) => {
@@ -1118,6 +1486,7 @@ function ComponentField({ value, precision, step, min, max, disabled, describedB
1118
1486
  min,
1119
1487
  max,
1120
1488
  disabled,
1489
+ "aria-label": name,
1121
1490
  "aria-describedby": describedBy,
1122
1491
  onFocus: () => setDraft({
1123
1492
  text: formatted,
@@ -1801,6 +2170,6 @@ function Tabs({ items, active, onSelect, label, idPrefix, className }) {
1801
2170
  });
1802
2171
  }
1803
2172
  //#endregion
1804
- export { Badge, Button, ButtonLink, Callout, Checkbox, ConfirmDialog, CountRun, DEFAULT_THEME_STORAGE_KEY, DensityProvider, Dialog, DialogClose, Disclosure, Empty, ErrorBox, Field, InfoHint, Input, NumberInput, PageHeader, Panel, PoseInput, ProgressBar, ReadoutStrip, Section, SegmentedControl, Select, Skeleton, SkeletonRows, Slider, StatusDot, Switch, Table, Tabs, Textarea, ThemeToggle, Toaster, ToggleChip, Tooltip, TooltipProvider, VectorInput, buttonClasses, byDensity, cn, controlClasses, createToastStore, defaultToastStore, focusRing, focusRingInset, initTheme, inputClasses, readThemeChoice, resolveTheme, setThemeChoice, toast, toneColor, useControlHeight, useDensity };
2173
+ export { Badge, Button, ButtonLink, Callout, Checkbox, ConfirmDialog, CountRun, DEFAULT_THEME_STORAGE_KEY, DensityProvider, Dialog, DialogClose, Disclosure, DropdownMenu, Empty, ErrorBox, Field, InfoHint, Input, Kbd, Listbox, MenuCheckboxItem, MenuItem, MenuLabel, MenuSeparator, NumberInput, PageHeader, Panel, Popover, PopoverClose, PoseInput, ProgressBar, ReadoutStrip, Section, SegmentedControl, Select, Skeleton, SkeletonRows, Slider, StatusDot, Switch, Table, Tabs, Textarea, ThemeToggle, Toaster, ToggleChip, Tooltip, TooltipProvider, VectorInput, buttonClasses, byDensity, cn, controlClasses, createToastStore, defaultToastStore, focusRing, focusRingInset, formatNumber, initTheme, inputClasses, parseNumber, readThemeChoice, resolveTheme, setThemeChoice, toast, toneColor, useControlHeight, useDensity };
1805
2174
 
1806
2175
  //# sourceMappingURL=index.js.map