@vitavision/ui 0.7.0 → 0.8.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 +16 -1
- package/dist/index.d.ts +129 -9
- package/dist/index.js +280 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ui/Input.tsx +67 -10
- package/src/components/ui/PoseInput.tsx +181 -0
- package/src/components/ui/VectorInput.tsx +183 -0
- package/src/components/ui/index.ts +10 -0
- package/src/components/ui/numberText.ts +36 -0
package/README.md
CHANGED
|
@@ -124,11 +124,26 @@ keep independent preferences.
|
|
|
124
124
|
|
|
125
125
|
**Primitives** — `Badge`, `CountRun`, `StatusDot` · `Button`, `ButtonLink` (a navigation styled as a button — never nest a `Button` in a link), `buttonClasses` · `Dialog`, `ConfirmDialog`,
|
|
126
126
|
`DialogClose` · `Disclosure` · `Callout`, `Empty`, `ErrorBox`, `ProgressBar`, `Skeleton`,
|
|
127
|
-
`SkeletonRows` · `Field` · `Input`, `NumberInput
|
|
127
|
+
`SkeletonRows` · `Field` · `Input`, `NumberInput` (with an optional `unit` written in the
|
|
128
|
+
field), `Textarea` · `VectorInput`, `PoseInput` · `PageHeader`, `Panel`,
|
|
128
129
|
`ReadoutStrip`, `Section` · `SegmentedControl` · `Select` · `Slider` · `Table` ·
|
|
129
130
|
`Checkbox`, `Switch` · `ToggleChip` · `InfoHint`, `Tooltip`, `TooltipProvider` ·
|
|
130
131
|
`ThemeToggle` · plus `cn`, `focusRing`, `focusRingInset`.
|
|
131
132
|
|
|
133
|
+
`PoseInput` edits an SE(3) pose in the wire form `{ rotation: [qx, qy, qz, qw], translation:
|
|
134
|
+
[tx, ty, tz] }` as a translation and roll/pitch/yaw in degrees, but does no rotation
|
|
135
|
+
mathematics: the quaternion ↔ angles conversion — and so what "roll, pitch, yaw" means — is
|
|
136
|
+
passed in as a `RotationView` by the package that owns the frame conventions:
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
<PoseInput
|
|
140
|
+
value={camera.world_se3_self}
|
|
141
|
+
onValueChange={setPose}
|
|
142
|
+
rotationView={{ toEuler: quatToRpy, fromEuler: rpyToQuat }}
|
|
143
|
+
translationUnit="mm"
|
|
144
|
+
/>
|
|
145
|
+
```
|
|
146
|
+
|
|
132
147
|
### Density
|
|
133
148
|
|
|
134
149
|
Every primitive with two spacings reads `useDensity()`. The default, `comfortable`, is what
|
package/dist/index.d.ts
CHANGED
|
@@ -460,18 +460,33 @@ export declare function useControlHeight(): string;
|
|
|
460
460
|
* is described by the field's description and error.
|
|
461
461
|
*/
|
|
462
462
|
export declare function Input({ className, "aria-describedby": describedBy, ...rest }: ComponentProps<"input">): import("react").JSX.Element;
|
|
463
|
-
/**
|
|
464
|
-
|
|
465
|
-
*
|
|
466
|
-
* `font-mono` because these are read as quantities and compared down a column. Takes every
|
|
467
|
-
* `<input>` prop, `ref` included (`type` is always `number`).
|
|
468
|
-
*/
|
|
469
|
-
export declare function NumberInput({ className, "aria-describedby": describedBy, ...rest }: Omit<ComponentProps<"input">, "min" | "max"> & {
|
|
463
|
+
/** Props of `NumberInput`: every `<input>` prop, `ref` included, with numeric bounds and a `unit`. */
|
|
464
|
+
type NumberInputProps = Omit<ComponentProps<"input">, "min" | "max"> & {
|
|
470
465
|
/** The smallest accepted value. */
|
|
471
466
|
min?: number | undefined;
|
|
472
467
|
/** The largest accepted value. */
|
|
473
468
|
max?: number | undefined;
|
|
474
|
-
|
|
469
|
+
/**
|
|
470
|
+
* The quantity's unit (`mm`, `m`, `°`, `px`), written inside the field after the number, in
|
|
471
|
+
* mono and muted. It is not part of the value. It is announced as the field's description
|
|
472
|
+
* (`aria-describedby`): after the caller's own `aria-describedby`, before a surrounding
|
|
473
|
+
* `Field`'s description and error.
|
|
474
|
+
*/
|
|
475
|
+
unit?: string | undefined;
|
|
476
|
+
};
|
|
477
|
+
/**
|
|
478
|
+
* A number, with the schema's own bounds attached and, optionally, its unit written in the
|
|
479
|
+
* field.
|
|
480
|
+
*
|
|
481
|
+
* `font-mono` because these are read as quantities and compared down a column. Takes every
|
|
482
|
+
* `<input>` prop, `ref` included (`type` is always `number`); `className`, `style` and `ref`
|
|
483
|
+
* always go to the `<input>`.
|
|
484
|
+
*
|
|
485
|
+
* With a `unit`, the input sits in a `relative` full-width wrapper carrying `data-unit`, keeps
|
|
486
|
+
* room on its right for the unit, and is described by it; size such a field through its
|
|
487
|
+
* container. Without one — or with `""` — the markup is exactly the plain input.
|
|
488
|
+
*/
|
|
489
|
+
export declare function NumberInput({ unit, ...props }: NumberInputProps): import("react").JSX.Element;
|
|
475
490
|
/**
|
|
476
491
|
* Multi-line text — set in mono, for JSON and lists. Takes every `<textarea>` prop, `ref`
|
|
477
492
|
* included.
|
|
@@ -572,6 +587,71 @@ export declare function ReadoutStrip({ items, className }: {
|
|
|
572
587
|
className?: string | undefined;
|
|
573
588
|
}): import("react").JSX.Element | null;
|
|
574
589
|
//#endregion
|
|
590
|
+
//#region src/components/ui/PoseInput.d.ts
|
|
591
|
+
/** Three numbers: a translation `[tx, ty, tz]`, or Euler angles. */
|
|
592
|
+
type Vec3 = readonly [number, number, number];
|
|
593
|
+
/** A unit quaternion in the wire order `[qx, qy, qz, qw]` (scalar last). */
|
|
594
|
+
type Quaternion = readonly [number, number, number, number];
|
|
595
|
+
/**
|
|
596
|
+
* An SE(3) pose in the wire form (`a_se3_b` as nalgebra's `Isometry3` serialises it):
|
|
597
|
+
* translation in metres, rotation as a unit quaternion `[qx, qy, qz, qw]`.
|
|
598
|
+
*/
|
|
599
|
+
interface PoseValue {
|
|
600
|
+
/** The rotation, `[qx, qy, qz, qw]`. */
|
|
601
|
+
rotation: Quaternion;
|
|
602
|
+
/** The translation `[tx, ty, tz]`, in metres. */
|
|
603
|
+
translation: Vec3;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* How a rotation is shown as three angles: the conversion both ways, and the angles' names.
|
|
607
|
+
* Supplied by the package that owns the frame conventions; `PoseInput` only calls it.
|
|
608
|
+
*/
|
|
609
|
+
interface RotationView {
|
|
610
|
+
/** The quaternion as three angles, in radians, in the order of `labels`. */
|
|
611
|
+
toEuler: (rotation: Quaternion) => Vec3;
|
|
612
|
+
/** Three angles in radians, in the order of `labels`, as a unit quaternion `[qx, qy, qz, qw]`. */
|
|
613
|
+
fromEuler: (angles: Vec3) => Quaternion;
|
|
614
|
+
/** The angles' names, shown before each field. Defaults to roll, pitch, yaw. */
|
|
615
|
+
labels?: readonly [string, string, string] | undefined;
|
|
616
|
+
}
|
|
617
|
+
/** Props of `PoseInput`. */
|
|
618
|
+
interface PoseInputProps {
|
|
619
|
+
/** The pose, in the wire form. */
|
|
620
|
+
value: PoseValue;
|
|
621
|
+
/** Called with a new pose (new arrays) whenever a component changes. */
|
|
622
|
+
onValueChange?: ((value: PoseValue) => void) | undefined;
|
|
623
|
+
/** The quaternion ↔ angles conversion and the angles' names. Required: see `RotationView`. */
|
|
624
|
+
rotationView: RotationView;
|
|
625
|
+
/** Show and edit the translation in metres (the default) or millimetres. The value is always metres. */
|
|
626
|
+
translationUnit?: "m" | "mm" | undefined;
|
|
627
|
+
/** Decimals of the translation. Defaults to 4 for m, 1 for mm. */
|
|
628
|
+
translationPrecision?: number | undefined;
|
|
629
|
+
/** Decimals of the angles, in degrees. Defaults to 2. */
|
|
630
|
+
rotationPrecision?: number | undefined;
|
|
631
|
+
/** Show a compact mono readout instead of fields. */
|
|
632
|
+
readOnly?: boolean | undefined;
|
|
633
|
+
/** Blocks every field. */
|
|
634
|
+
disabled?: boolean | undefined;
|
|
635
|
+
/** Names the pose (`role="group"`). Defaults to "Pose". */
|
|
636
|
+
"aria-label"?: string | undefined;
|
|
637
|
+
/** Merged with the component's own classes through `cn`. */
|
|
638
|
+
className?: string | undefined;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* An SE(3) pose shown and edited as a translation (m or mm) and three angles in degrees,
|
|
642
|
+
* reading and writing the wire form `{ rotation: [qx, qy, qz, qw], translation: [tx, ty, tz] }`.
|
|
643
|
+
*
|
|
644
|
+
* The angles come from `rotationView.toEuler` and go back through `rotationView.fromEuler`.
|
|
645
|
+
* The angles as typed are kept while the pose they produced is the one shown, so a yaw of
|
|
646
|
+
* 190° is not redisplayed as −170° and a gimbal-locked pitch does not scramble the other two
|
|
647
|
+
* angles mid-edit.
|
|
648
|
+
*
|
|
649
|
+
* A `role="group"` named by `aria-label`, holding two `VectorInput` groups ("Translation",
|
|
650
|
+
* "Rotation"). With `readOnly` it renders a `ReadoutStrip` instead
|
|
651
|
+
* (`t 0.1000 0.2000 0.3000 m · rpy 0.00 90.00 0.00 °`). The root carries `data-readonly`.
|
|
652
|
+
*/
|
|
653
|
+
export declare function PoseInput({ value, onValueChange, rotationView, translationUnit, translationPrecision, rotationPrecision, readOnly, disabled, "aria-label": ariaLabel, className }: PoseInputProps): import("react").JSX.Element;
|
|
654
|
+
//#endregion
|
|
575
655
|
//#region src/components/ui/SegmentedControl.d.ts
|
|
576
656
|
/**
|
|
577
657
|
* A choice among two or three, shown as a strip of segments (native radios in one group, so
|
|
@@ -840,6 +920,46 @@ export declare function InfoHint({ children, label, icon: Icon }: {
|
|
|
840
920
|
icon?: LucideIcon | undefined;
|
|
841
921
|
}): import("react").JSX.Element;
|
|
842
922
|
//#endregion
|
|
923
|
+
//#region src/components/ui/VectorInput.d.ts
|
|
924
|
+
/** Props of `VectorInput`. */
|
|
925
|
+
interface VectorInputProps {
|
|
926
|
+
/** The components. Its length is the number of fields. */
|
|
927
|
+
value: readonly number[];
|
|
928
|
+
/** Called with the whole vector (a new array) whenever one component changes to a valid number. */
|
|
929
|
+
onValueChange?: ((value: number[]) => void) | undefined;
|
|
930
|
+
/** Each component's label, shown before its field and naming it. Defaults to x, y, z, w. */
|
|
931
|
+
labels?: readonly string[] | undefined;
|
|
932
|
+
/** The unit of every component, shown once after the row (`m`, `mm`, `°`). */
|
|
933
|
+
unit?: string | undefined;
|
|
934
|
+
/** The arrow-key step. Defaults to `any` (the browser then steps by 1). */
|
|
935
|
+
step?: number | undefined;
|
|
936
|
+
/** Decimals shown when a field is not being edited. Defaults to 3. */
|
|
937
|
+
precision?: number | undefined;
|
|
938
|
+
/** The smallest accepted component. */
|
|
939
|
+
min?: number | undefined;
|
|
940
|
+
/** The largest accepted component. */
|
|
941
|
+
max?: number | undefined;
|
|
942
|
+
/** Show the values as a compact mono readout instead of fields. */
|
|
943
|
+
readOnly?: boolean | undefined;
|
|
944
|
+
/** Blocks every field. */
|
|
945
|
+
disabled?: boolean | undefined;
|
|
946
|
+
/** Names the group (`role="group"`). */
|
|
947
|
+
"aria-label": string;
|
|
948
|
+
/** Merged with the row's own classes through `cn`. */
|
|
949
|
+
className?: string | undefined;
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* A row of number fields editing one small vector, with the axis labels before the fields
|
|
953
|
+
* and the unit once after them.
|
|
954
|
+
*
|
|
955
|
+
* Controlled. A `role="group"` named by `aria-label`, each field named by its label and
|
|
956
|
+
* described by the unit. While a field has focus it shows what is typed; on blur or Enter it
|
|
957
|
+
* shows the value at `precision`; Escape restores the value it had on focus. With `readOnly`
|
|
958
|
+
* it renders a `ReadoutStrip` (`x 0.100 m · y …`) instead. The row carries `data-readonly`
|
|
959
|
+
* and `data-disabled`.
|
|
960
|
+
*/
|
|
961
|
+
export declare function VectorInput({ value, onValueChange, labels, unit, step, precision, min, max, readOnly, disabled, "aria-label": ariaLabel, className }: VectorInputProps): import("react").JSX.Element;
|
|
962
|
+
//#endregion
|
|
843
963
|
//#region src/components/Tabs.d.ts
|
|
844
964
|
/** One tab of a `Tabs` strip. */
|
|
845
965
|
interface TabItem<Id extends string> {
|
|
@@ -879,5 +999,5 @@ export declare function Tabs<Id extends string>({ items, active, onSelect, label
|
|
|
879
999
|
className?: string | undefined;
|
|
880
1000
|
}): import("react").JSX.Element;
|
|
881
1001
|
//#endregion
|
|
882
|
-
export type { BackLink, ButtonSize, ButtonVariant, CalloutTone, Column, Density, MeasureTone, ReadoutItem, SelectOption, TabItem, ThemeChoice, Tone };
|
|
1002
|
+
export type { BackLink, ButtonSize, ButtonVariant, CalloutTone, Column, Density, MeasureTone, NumberInputProps, PoseInputProps, PoseValue, Quaternion, ReadoutItem, RotationView, SelectOption, TabItem, ThemeChoice, Tone, Vec3, VectorInputProps };
|
|
883
1003
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -813,12 +813,24 @@ function Input({ className, "aria-describedby": describedBy, ...rest }) {
|
|
|
813
813
|
});
|
|
814
814
|
}
|
|
815
815
|
/**
|
|
816
|
-
* A number, with the schema's own bounds attached
|
|
816
|
+
* A number, with the schema's own bounds attached and, optionally, its unit written in the
|
|
817
|
+
* field.
|
|
817
818
|
*
|
|
818
819
|
* `font-mono` because these are read as quantities and compared down a column. Takes every
|
|
819
|
-
* `<input>` prop, `ref` included (`type` is always `number`)
|
|
820
|
+
* `<input>` prop, `ref` included (`type` is always `number`); `className`, `style` and `ref`
|
|
821
|
+
* always go to the `<input>`.
|
|
822
|
+
*
|
|
823
|
+
* With a `unit`, the input sits in a `relative` full-width wrapper carrying `data-unit`, keeps
|
|
824
|
+
* room on its right for the unit, and is described by it; size such a field through its
|
|
825
|
+
* container. Without one — or with `""` — the markup is exactly the plain input.
|
|
820
826
|
*/
|
|
821
|
-
function NumberInput({
|
|
827
|
+
function NumberInput({ unit, ...props }) {
|
|
828
|
+
return unit === void 0 || unit === "" ? /* @__PURE__ */ jsx(PlainNumberInput, { ...props }) : /* @__PURE__ */ jsx(UnitNumberInput, {
|
|
829
|
+
unit,
|
|
830
|
+
...props
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
function PlainNumberInput({ className, "aria-describedby": describedBy, ...rest }) {
|
|
822
834
|
return /* @__PURE__ */ jsx("input", {
|
|
823
835
|
...useFieldDescription(describedBy),
|
|
824
836
|
...rest,
|
|
@@ -827,6 +839,26 @@ function NumberInput({ className, "aria-describedby": describedBy, ...rest }) {
|
|
|
827
839
|
className: cn(controlClasses, useControlHeight(), "font-mono tabular-nums", className)
|
|
828
840
|
});
|
|
829
841
|
}
|
|
842
|
+
function UnitNumberInput({ unit, style, "aria-describedby": describedBy, ...rest }) {
|
|
843
|
+
const unitId = useId();
|
|
844
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
845
|
+
"data-unit": unit,
|
|
846
|
+
className: "relative flex w-full min-w-0 items-center",
|
|
847
|
+
children: [/* @__PURE__ */ jsx(PlainNumberInput, {
|
|
848
|
+
...rest,
|
|
849
|
+
"aria-describedby": describedBy ? `${describedBy} ${unitId}` : unitId,
|
|
850
|
+
style: {
|
|
851
|
+
paddingInlineEnd: `calc(${unit.length}ch + 1rem)`,
|
|
852
|
+
...style
|
|
853
|
+
}
|
|
854
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
855
|
+
id: unitId,
|
|
856
|
+
"aria-hidden": true,
|
|
857
|
+
className: "pointer-events-none absolute inset-y-0 right-2.5 flex items-center font-mono text-xs text-fg-subtle select-none",
|
|
858
|
+
children: unit
|
|
859
|
+
})]
|
|
860
|
+
});
|
|
861
|
+
}
|
|
830
862
|
/**
|
|
831
863
|
* Multi-line text — set in mono, for JSON and lists. Takes every `<textarea>` prop, `ref`
|
|
832
864
|
* included.
|
|
@@ -974,6 +1006,250 @@ function ReadoutStrip({ items, className }) {
|
|
|
974
1006
|
});
|
|
975
1007
|
}
|
|
976
1008
|
//#endregion
|
|
1009
|
+
//#region src/components/ui/numberText.ts
|
|
1010
|
+
/**
|
|
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).
|
|
1013
|
+
*
|
|
1014
|
+
* @param value - The number. Non-finite values print as `NaN`, `Infinity`, `-Infinity`.
|
|
1015
|
+
* @param precision - Decimals, 0–20.
|
|
1016
|
+
* @returns The text.
|
|
1017
|
+
*/
|
|
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;
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
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.
|
|
1028
|
+
*/
|
|
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;
|
|
1034
|
+
}
|
|
1035
|
+
//#endregion
|
|
1036
|
+
//#region src/components/ui/VectorInput.tsx
|
|
1037
|
+
const DEFAULT_LABELS = [
|
|
1038
|
+
"x",
|
|
1039
|
+
"y",
|
|
1040
|
+
"z",
|
|
1041
|
+
"w"
|
|
1042
|
+
];
|
|
1043
|
+
/**
|
|
1044
|
+
* A row of number fields editing one small vector, with the axis labels before the fields
|
|
1045
|
+
* and the unit once after them.
|
|
1046
|
+
*
|
|
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
|
|
1049
|
+
* shows the value at `precision`; Escape restores the value it had on focus. With `readOnly`
|
|
1050
|
+
* it renders a `ReadoutStrip` (`x 0.100 m · y …`) instead. The row carries `data-readonly`
|
|
1051
|
+
* and `data-disabled`.
|
|
1052
|
+
*/
|
|
1053
|
+
function VectorInput({ value, onValueChange, labels = DEFAULT_LABELS, unit, step, precision = 3, min, max, readOnly = false, disabled = false, "aria-label": ariaLabel, className }) {
|
|
1054
|
+
const unitId = useId();
|
|
1055
|
+
const labelOf = (index) => labels[index] ?? String(index);
|
|
1056
|
+
if (readOnly) return /* @__PURE__ */ jsx("div", {
|
|
1057
|
+
role: "group",
|
|
1058
|
+
"aria-label": ariaLabel,
|
|
1059
|
+
"data-readonly": "",
|
|
1060
|
+
className: cn("min-w-0", className),
|
|
1061
|
+
children: /* @__PURE__ */ jsx(ReadoutStrip, { items: value.map((component, index) => ({
|
|
1062
|
+
label: labelOf(index),
|
|
1063
|
+
value: unit ? `${formatNumber(component, precision)} ${unit}` : formatNumber(component, precision)
|
|
1064
|
+
})) })
|
|
1065
|
+
});
|
|
1066
|
+
const change = (index, component) => {
|
|
1067
|
+
if (component === value[index]) return;
|
|
1068
|
+
onValueChange?.(value.map((current, i) => i === index ? component : current));
|
|
1069
|
+
};
|
|
1070
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1071
|
+
role: "group",
|
|
1072
|
+
"aria-label": ariaLabel,
|
|
1073
|
+
"data-disabled": disabled ? "" : void 0,
|
|
1074
|
+
className: cn("flex min-w-0 items-center gap-1.5", className),
|
|
1075
|
+
children: [value.map((component, index) => /* @__PURE__ */ jsxs("label", {
|
|
1076
|
+
className: "flex min-w-0 flex-1 items-center gap-1",
|
|
1077
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1078
|
+
className: "shrink-0 font-mono text-[11px] text-fg-subtle",
|
|
1079
|
+
children: labelOf(index)
|
|
1080
|
+
}), /* @__PURE__ */ jsx(ComponentField, {
|
|
1081
|
+
value: component,
|
|
1082
|
+
precision,
|
|
1083
|
+
step,
|
|
1084
|
+
min,
|
|
1085
|
+
max,
|
|
1086
|
+
disabled,
|
|
1087
|
+
describedBy: unit ? unitId : void 0,
|
|
1088
|
+
onCommit: (next) => change(index, next)
|
|
1089
|
+
})]
|
|
1090
|
+
}, index)), unit && /* @__PURE__ */ jsx("span", {
|
|
1091
|
+
id: unitId,
|
|
1092
|
+
className: "shrink-0 font-mono text-xs text-fg-subtle",
|
|
1093
|
+
children: unit
|
|
1094
|
+
})]
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
/** One component: formatted at rest, the typed text while focused. */
|
|
1098
|
+
function ComponentField({ value, precision, step, min, max, disabled, describedBy, onCommit }) {
|
|
1099
|
+
const [draft, setDraft] = useState(null);
|
|
1100
|
+
const formatted = formatNumber(value, precision);
|
|
1101
|
+
const onKeyDown = (event) => {
|
|
1102
|
+
if (event.key === "Enter") setDraft((current) => current && {
|
|
1103
|
+
...current,
|
|
1104
|
+
text: formatted
|
|
1105
|
+
});
|
|
1106
|
+
else if (event.key === "Escape" && draft) {
|
|
1107
|
+
event.preventDefault();
|
|
1108
|
+
onCommit(draft.initial);
|
|
1109
|
+
setDraft({
|
|
1110
|
+
...draft,
|
|
1111
|
+
text: formatNumber(draft.initial, precision)
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
return /* @__PURE__ */ jsx(NumberInput, {
|
|
1116
|
+
value: draft?.text ?? formatted,
|
|
1117
|
+
step: step ?? "any",
|
|
1118
|
+
min,
|
|
1119
|
+
max,
|
|
1120
|
+
disabled,
|
|
1121
|
+
"aria-describedby": describedBy,
|
|
1122
|
+
onFocus: () => setDraft({
|
|
1123
|
+
text: formatted,
|
|
1124
|
+
initial: value
|
|
1125
|
+
}),
|
|
1126
|
+
onBlur: () => setDraft(null),
|
|
1127
|
+
onKeyDown,
|
|
1128
|
+
onChange: (event) => {
|
|
1129
|
+
const text = event.currentTarget.value;
|
|
1130
|
+
setDraft((current) => ({
|
|
1131
|
+
text,
|
|
1132
|
+
initial: current?.initial ?? value
|
|
1133
|
+
}));
|
|
1134
|
+
const parsed = parseNumber(text);
|
|
1135
|
+
if (parsed !== null) onCommit(parsed);
|
|
1136
|
+
},
|
|
1137
|
+
className: "min-w-0 px-1.5"
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
//#endregion
|
|
1141
|
+
//#region src/components/ui/PoseInput.tsx
|
|
1142
|
+
const DEGREES = 180 / Math.PI;
|
|
1143
|
+
const DEFAULT_ANGLE_LABELS = [
|
|
1144
|
+
"roll",
|
|
1145
|
+
"pitch",
|
|
1146
|
+
"yaw"
|
|
1147
|
+
];
|
|
1148
|
+
const sameQuaternion = (a, b) => a.every((component, index) => component === b[index]);
|
|
1149
|
+
/**
|
|
1150
|
+
* An SE(3) pose shown and edited as a translation (m or mm) and three angles in degrees,
|
|
1151
|
+
* reading and writing the wire form `{ rotation: [qx, qy, qz, qw], translation: [tx, ty, tz] }`.
|
|
1152
|
+
*
|
|
1153
|
+
* The angles come from `rotationView.toEuler` and go back through `rotationView.fromEuler`.
|
|
1154
|
+
* The angles as typed are kept while the pose they produced is the one shown, so a yaw of
|
|
1155
|
+
* 190° is not redisplayed as −170° and a gimbal-locked pitch does not scramble the other two
|
|
1156
|
+
* angles mid-edit.
|
|
1157
|
+
*
|
|
1158
|
+
* A `role="group"` named by `aria-label`, holding two `VectorInput` groups ("Translation",
|
|
1159
|
+
* "Rotation"). With `readOnly` it renders a `ReadoutStrip` instead
|
|
1160
|
+
* (`t 0.1000 0.2000 0.3000 m · rpy 0.00 90.00 0.00 °`). The root carries `data-readonly`.
|
|
1161
|
+
*/
|
|
1162
|
+
function PoseInput({ value, onValueChange, rotationView, translationUnit = "m", translationPrecision, rotationPrecision = 2, readOnly = false, disabled = false, "aria-label": ariaLabel = "Pose", className }) {
|
|
1163
|
+
const density = useDensity();
|
|
1164
|
+
const [typed, setTyped] = useState(null);
|
|
1165
|
+
const scale = translationUnit === "mm" ? 1e3 : 1;
|
|
1166
|
+
const tPrecision = translationPrecision ?? (translationUnit === "mm" ? 1 : 4);
|
|
1167
|
+
const translation = value.translation.map((component) => component * scale);
|
|
1168
|
+
const degrees = (typed !== null && sameQuaternion(typed.rotation, value.rotation) ? typed.angles : rotationView.toEuler(value.rotation)).map((angle) => angle * DEGREES);
|
|
1169
|
+
const angleLabels = rotationView.labels ?? DEFAULT_ANGLE_LABELS;
|
|
1170
|
+
if (readOnly) {
|
|
1171
|
+
const join = (values, precision) => values.map((component) => formatNumber(component, precision)).join(" ");
|
|
1172
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1173
|
+
role: "group",
|
|
1174
|
+
"aria-label": ariaLabel,
|
|
1175
|
+
"data-readonly": "",
|
|
1176
|
+
className: cn("min-w-0", className),
|
|
1177
|
+
children: /* @__PURE__ */ jsx(ReadoutStrip, { items: [{
|
|
1178
|
+
label: "t",
|
|
1179
|
+
value: `${join(translation, tPrecision)} ${translationUnit}`
|
|
1180
|
+
}, {
|
|
1181
|
+
label: "rpy",
|
|
1182
|
+
value: `${join(degrees, rotationPrecision)} °`
|
|
1183
|
+
}] })
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
const onTranslation = (next) => {
|
|
1187
|
+
const [x = 0, y = 0, z = 0] = next.map((component) => component / scale);
|
|
1188
|
+
onValueChange?.({
|
|
1189
|
+
rotation: value.rotation,
|
|
1190
|
+
translation: [
|
|
1191
|
+
x,
|
|
1192
|
+
y,
|
|
1193
|
+
z
|
|
1194
|
+
]
|
|
1195
|
+
});
|
|
1196
|
+
};
|
|
1197
|
+
const onRotation = (next) => {
|
|
1198
|
+
const [a = 0, b = 0, c = 0] = next.map((angle) => angle / DEGREES);
|
|
1199
|
+
const angles = [
|
|
1200
|
+
a,
|
|
1201
|
+
b,
|
|
1202
|
+
c
|
|
1203
|
+
];
|
|
1204
|
+
const rotation = rotationView.fromEuler(angles);
|
|
1205
|
+
setTyped({
|
|
1206
|
+
rotation,
|
|
1207
|
+
angles
|
|
1208
|
+
});
|
|
1209
|
+
onValueChange?.({
|
|
1210
|
+
rotation,
|
|
1211
|
+
translation: value.translation
|
|
1212
|
+
});
|
|
1213
|
+
};
|
|
1214
|
+
const rowLabel = cn("text-fg-muted", byDensity(density, "text-xs", "text-[11px]"));
|
|
1215
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1216
|
+
role: "group",
|
|
1217
|
+
"aria-label": ariaLabel,
|
|
1218
|
+
className: cn("grid min-w-0 grid-cols-[auto_minmax(0,1fr)] items-center gap-x-2", byDensity(density, "gap-y-2", "gap-y-1"), className),
|
|
1219
|
+
children: [
|
|
1220
|
+
/* @__PURE__ */ jsx("span", {
|
|
1221
|
+
"aria-hidden": true,
|
|
1222
|
+
className: rowLabel,
|
|
1223
|
+
children: "Translation"
|
|
1224
|
+
}),
|
|
1225
|
+
/* @__PURE__ */ jsx(VectorInput, {
|
|
1226
|
+
"aria-label": "Translation",
|
|
1227
|
+
value: translation,
|
|
1228
|
+
onValueChange: onTranslation,
|
|
1229
|
+
unit: translationUnit,
|
|
1230
|
+
precision: tPrecision,
|
|
1231
|
+
step: translationUnit === "mm" ? 1 : .001,
|
|
1232
|
+
disabled
|
|
1233
|
+
}),
|
|
1234
|
+
/* @__PURE__ */ jsx("span", {
|
|
1235
|
+
"aria-hidden": true,
|
|
1236
|
+
className: rowLabel,
|
|
1237
|
+
children: "Rotation"
|
|
1238
|
+
}),
|
|
1239
|
+
/* @__PURE__ */ jsx(VectorInput, {
|
|
1240
|
+
"aria-label": "Rotation",
|
|
1241
|
+
value: degrees,
|
|
1242
|
+
onValueChange: onRotation,
|
|
1243
|
+
labels: angleLabels,
|
|
1244
|
+
unit: "°",
|
|
1245
|
+
precision: rotationPrecision,
|
|
1246
|
+
step: 1,
|
|
1247
|
+
disabled
|
|
1248
|
+
})
|
|
1249
|
+
]
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
//#endregion
|
|
977
1253
|
//#region src/components/ui/SegmentedControl.tsx
|
|
978
1254
|
/**
|
|
979
1255
|
* A choice among two or three, shown as a strip of segments (native radios in one group, so
|
|
@@ -1344,6 +1620,6 @@ function Tabs({ items, active, onSelect, label, idPrefix, className }) {
|
|
|
1344
1620
|
});
|
|
1345
1621
|
}
|
|
1346
1622
|
//#endregion
|
|
1347
|
-
export { Badge, Button, ButtonLink, Callout, Checkbox, ConfirmDialog, CountRun, DEFAULT_THEME_STORAGE_KEY, DensityProvider, Dialog, DialogClose, Disclosure, Empty, ErrorBox, Field, InfoHint, Input, NumberInput, PageHeader, Panel, ProgressBar, ReadoutStrip, Section, SegmentedControl, Select, Skeleton, SkeletonRows, Slider, StatusDot, Switch, Table, Tabs, Textarea, ThemeToggle, ToggleChip, Tooltip, TooltipProvider, buttonClasses, byDensity, cn, controlClasses, focusRing, focusRingInset, initTheme, inputClasses, readThemeChoice, resolveTheme, setThemeChoice, toneColor, useControlHeight, useDensity };
|
|
1623
|
+
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, ToggleChip, Tooltip, TooltipProvider, VectorInput, buttonClasses, byDensity, cn, controlClasses, focusRing, focusRingInset, initTheme, inputClasses, readThemeChoice, resolveTheme, setThemeChoice, toneColor, useControlHeight, useDensity };
|
|
1348
1624
|
|
|
1349
1625
|
//# sourceMappingURL=index.js.map
|