@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/README.md +17 -5
- package/dist/index.d.ts +235 -6
- package/dist/index.js +395 -26
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/src/components/ui/DropdownMenu.tsx +179 -0
- package/src/components/ui/Input.tsx +133 -8
- package/src/components/ui/Kbd.tsx +27 -0
- package/src/components/ui/Listbox.tsx +160 -0
- package/src/components/ui/Popover.tsx +96 -0
- package/src/components/ui/VectorInput.tsx +6 -2
- package/src/components/ui/index.ts +19 -0
- package/src/components/ui/listboxModel.ts +63 -0
- package/src/components/ui/numberText.ts +2 -2
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({
|
|
828
|
-
|
|
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
|
-
...
|
|
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/
|
|
1364
|
+
//#region src/components/ui/Popover.tsx
|
|
1010
1365
|
/**
|
|
1011
|
-
*
|
|
1012
|
-
*
|
|
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
|
-
*
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
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
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
-
*
|
|
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
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
|
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
|