@alpic-ai/ui 1.170.0 → 1.172.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/components/area-chart.d.mts +3 -3
- package/dist/components/avatar.d.mts +1 -1
- package/dist/components/badge.d.mts +1 -1
- package/dist/components/bar-chart.d.mts +2 -2
- package/dist/components/bar-chart.mjs +1 -1
- package/dist/components/bar-list.d.mts +2 -2
- package/dist/components/button.d.mts +1 -1
- package/dist/components/chart-card.d.mts +2 -2
- package/dist/components/chart-legend.d.mts +3 -3
- package/dist/components/chart-primitives.d.mts +9 -10
- package/dist/components/chart-tooltip.d.mts +3 -3
- package/dist/components/donut-chart.d.mts +2 -2
- package/dist/components/editable.d.mts +101 -0
- package/dist/components/editable.mjs +509 -0
- package/dist/components/form.d.mts +10 -8
- package/dist/components/form.mjs +18 -8
- package/dist/components/heatmap-chart.d.mts +2 -2
- package/dist/components/input.d.mts +3 -1
- package/dist/components/input.mjs +5 -3
- package/dist/components/line-chart.d.mts +2 -2
- package/dist/components/select-trigger-variants.d.mts +2 -3
- package/dist/components/shimmer-text.d.mts +2 -3
- package/dist/components/skeleton.d.mts +1 -1
- package/dist/components/spinner.d.mts +2 -2
- package/dist/components/stat.d.mts +3 -3
- package/dist/components/typography.d.mts +5 -6
- package/dist/components/visually-hidden-input.d.mts +12 -0
- package/dist/components/visually-hidden-input.mjs +109 -0
- package/dist/hooks/use-as-ref.d.mts +5 -0
- package/dist/hooks/use-as-ref.mjs +12 -0
- package/dist/hooks/use-chart-theme.d.mts +3 -4
- package/dist/hooks/use-copy-to-clipboard.d.mts +2 -3
- package/dist/hooks/use-isomorphic-layout-effect.d.mts +5 -0
- package/dist/hooks/use-isomorphic-layout-effect.mjs +5 -0
- package/dist/hooks/use-lazy-ref.d.mts +6 -0
- package/dist/hooks/use-lazy-ref.mjs +9 -0
- package/dist/hooks/use-mobile.d.mts +2 -3
- package/dist/lib/chart-palette.d.mts +13 -14
- package/dist/lib/chart.d.mts +3 -4
- package/dist/lib/cn.d.mts +2 -3
- package/dist/lib/compose-refs.mjs +23 -0
- package/package.json +9 -13
- package/src/components/bar-chart.tsx +1 -1
- package/src/components/editable.tsx +871 -0
- package/src/components/form.tsx +16 -6
- package/src/components/input.tsx +8 -0
- package/src/components/visually-hidden-input.tsx +148 -0
- package/src/hooks/use-as-ref.ts +15 -0
- package/src/hooks/use-isomorphic-layout-effect.ts +5 -0
- package/src/hooks/use-lazy-ref.ts +13 -0
- package/src/lib/compose-refs.ts +38 -0
- package/src/stories/input.stories.tsx +3 -0
package/src/components/form.tsx
CHANGED
|
@@ -93,9 +93,10 @@ function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
93
93
|
interface FormLabelProps extends React.ComponentProps<typeof Label> {
|
|
94
94
|
required?: boolean;
|
|
95
95
|
tooltip?: string;
|
|
96
|
+
marker?: React.ReactNode;
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
function FormLabel({ className, required, tooltip, children, ...props }: FormLabelProps) {
|
|
99
|
+
function FormLabel({ className, required, tooltip, marker, children, ...props }: FormLabelProps) {
|
|
99
100
|
const { error, formItemId } = useFormField();
|
|
100
101
|
|
|
101
102
|
return (
|
|
@@ -122,6 +123,7 @@ function FormLabel({ className, required, tooltip, children, ...props }: FormLab
|
|
|
122
123
|
<TooltipContent>{tooltip}</TooltipContent>
|
|
123
124
|
</Tooltip>
|
|
124
125
|
)}
|
|
126
|
+
{marker && <span className="ml-1 flex items-center">{marker}</span>}
|
|
125
127
|
</div>
|
|
126
128
|
);
|
|
127
129
|
}
|
|
@@ -194,6 +196,7 @@ interface FormFieldBaseProps<TFieldValues extends FieldValues, TName extends Fie
|
|
|
194
196
|
label?: string;
|
|
195
197
|
description?: string;
|
|
196
198
|
tooltip?: string;
|
|
199
|
+
marker?: React.ReactNode;
|
|
197
200
|
}
|
|
198
201
|
|
|
199
202
|
interface InputFieldProps<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValues>>
|
|
@@ -208,6 +211,7 @@ function InputField<TFieldValues extends FieldValues, TName extends FieldPath<TF
|
|
|
208
211
|
label,
|
|
209
212
|
description,
|
|
210
213
|
tooltip,
|
|
214
|
+
marker,
|
|
211
215
|
...inputProps
|
|
212
216
|
}: InputFieldProps<TFieldValues, TName>) {
|
|
213
217
|
return (
|
|
@@ -218,7 +222,7 @@ function InputField<TFieldValues extends FieldValues, TName extends FieldPath<TF
|
|
|
218
222
|
render={({ field }) => (
|
|
219
223
|
<FormItem>
|
|
220
224
|
{label && (
|
|
221
|
-
<FormLabel required={required} tooltip={tooltip}>
|
|
225
|
+
<FormLabel required={required} tooltip={tooltip} marker={marker}>
|
|
222
226
|
{label}
|
|
223
227
|
</FormLabel>
|
|
224
228
|
)}
|
|
@@ -245,6 +249,7 @@ function TextareaField<TFieldValues extends FieldValues, TName extends FieldPath
|
|
|
245
249
|
label,
|
|
246
250
|
description,
|
|
247
251
|
tooltip,
|
|
252
|
+
marker,
|
|
248
253
|
...textareaProps
|
|
249
254
|
}: TextareaFieldProps<TFieldValues, TName>) {
|
|
250
255
|
return (
|
|
@@ -255,7 +260,7 @@ function TextareaField<TFieldValues extends FieldValues, TName extends FieldPath
|
|
|
255
260
|
render={({ field }) => (
|
|
256
261
|
<FormItem>
|
|
257
262
|
{label && (
|
|
258
|
-
<FormLabel required={required} tooltip={tooltip}>
|
|
263
|
+
<FormLabel required={required} tooltip={tooltip} marker={marker}>
|
|
259
264
|
{label}
|
|
260
265
|
</FormLabel>
|
|
261
266
|
)}
|
|
@@ -290,6 +295,7 @@ function SelectField<TFieldValues extends FieldValues, TName extends FieldPath<T
|
|
|
290
295
|
label,
|
|
291
296
|
description,
|
|
292
297
|
tooltip,
|
|
298
|
+
marker,
|
|
293
299
|
options,
|
|
294
300
|
placeholder,
|
|
295
301
|
}: SelectFieldProps<TFieldValues, TName>) {
|
|
@@ -301,7 +307,7 @@ function SelectField<TFieldValues extends FieldValues, TName extends FieldPath<T
|
|
|
301
307
|
render={({ field }) => (
|
|
302
308
|
<FormItem>
|
|
303
309
|
{label && (
|
|
304
|
-
<FormLabel required={required} tooltip={tooltip}>
|
|
310
|
+
<FormLabel required={required} tooltip={tooltip} marker={marker}>
|
|
305
311
|
{label}
|
|
306
312
|
</FormLabel>
|
|
307
313
|
)}
|
|
@@ -340,6 +346,7 @@ function RadioField<TFieldValues extends FieldValues, TName extends FieldPath<TF
|
|
|
340
346
|
label,
|
|
341
347
|
description,
|
|
342
348
|
tooltip,
|
|
349
|
+
marker,
|
|
343
350
|
options,
|
|
344
351
|
}: RadioFieldProps<TFieldValues, TName>) {
|
|
345
352
|
return (
|
|
@@ -350,7 +357,7 @@ function RadioField<TFieldValues extends FieldValues, TName extends FieldPath<TF
|
|
|
350
357
|
render={({ field }) => (
|
|
351
358
|
<FormItem>
|
|
352
359
|
{label && (
|
|
353
|
-
<FormLabel required={required} tooltip={tooltip}>
|
|
360
|
+
<FormLabel required={required} tooltip={tooltip} marker={marker}>
|
|
354
361
|
{label}
|
|
355
362
|
</FormLabel>
|
|
356
363
|
)}
|
|
@@ -393,6 +400,7 @@ function ChecklistField<TFieldValues extends FieldValues, TName extends FieldPat
|
|
|
393
400
|
label,
|
|
394
401
|
description,
|
|
395
402
|
tooltip,
|
|
403
|
+
marker,
|
|
396
404
|
options,
|
|
397
405
|
}: ChecklistFieldProps<TFieldValues, TName>) {
|
|
398
406
|
return (
|
|
@@ -412,7 +420,7 @@ function ChecklistField<TFieldValues extends FieldValues, TName extends FieldPat
|
|
|
412
420
|
return (
|
|
413
421
|
<FormItem>
|
|
414
422
|
{label && (
|
|
415
|
-
<FormLabel required={required} tooltip={tooltip}>
|
|
423
|
+
<FormLabel required={required} tooltip={tooltip} marker={marker}>
|
|
416
424
|
{label}
|
|
417
425
|
</FormLabel>
|
|
418
426
|
)}
|
|
@@ -459,6 +467,7 @@ function CheckboxField<TFieldValues extends FieldValues, TName extends FieldPath
|
|
|
459
467
|
label,
|
|
460
468
|
description,
|
|
461
469
|
tooltip,
|
|
470
|
+
marker,
|
|
462
471
|
}: CheckboxFieldProps<TFieldValues, TName>) {
|
|
463
472
|
return (
|
|
464
473
|
<FormField
|
|
@@ -478,6 +487,7 @@ function CheckboxField<TFieldValues extends FieldValues, TName extends FieldPath
|
|
|
478
487
|
{label && (
|
|
479
488
|
<FormLabel
|
|
480
489
|
tooltip={tooltip}
|
|
490
|
+
marker={marker}
|
|
481
491
|
className="font-normal max-md:-ml-2 max-md:min-h-11 max-md:flex-1 max-md:items-start max-md:pl-2"
|
|
482
492
|
>
|
|
483
493
|
{label}
|
package/src/components/input.tsx
CHANGED
|
@@ -33,6 +33,8 @@ interface InputProps extends Omit<React.ComponentProps<"input">, "size"> {
|
|
|
33
33
|
leadingText?: string;
|
|
34
34
|
leadingIcon?: React.ReactNode;
|
|
35
35
|
size?: "sm" | "md";
|
|
36
|
+
sensitive?: boolean;
|
|
37
|
+
show?: boolean;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
function Input({
|
|
@@ -46,6 +48,8 @@ function Input({
|
|
|
46
48
|
leadingText,
|
|
47
49
|
leadingIcon,
|
|
48
50
|
size = "md",
|
|
51
|
+
sensitive = false,
|
|
52
|
+
show = false,
|
|
49
53
|
...props
|
|
50
54
|
}: InputProps) {
|
|
51
55
|
const generatedId = React.useId();
|
|
@@ -74,12 +78,16 @@ function Input({
|
|
|
74
78
|
error && "border-border-error",
|
|
75
79
|
],
|
|
76
80
|
hasAddons && "flex-1 min-h-0",
|
|
81
|
+
// Obscuring the text keeps password managers away, where type="password" would make them offer to save the value.
|
|
82
|
+
sensitive && !show && "[-webkit-text-security:disc]",
|
|
77
83
|
className,
|
|
78
84
|
)}
|
|
79
85
|
required={required}
|
|
80
86
|
aria-invalid={error ? true : undefined}
|
|
81
87
|
aria-describedby={fieldId && (hint || error) ? `${fieldId}-description` : undefined}
|
|
82
88
|
{...props}
|
|
89
|
+
autoComplete={sensitive ? "off" : props.autoComplete}
|
|
90
|
+
spellCheck={sensitive ? false : props.spellCheck}
|
|
83
91
|
/>
|
|
84
92
|
);
|
|
85
93
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
type InputValue = string[] | string;
|
|
6
|
+
|
|
7
|
+
interface VisuallyHiddenInputProps<T = InputValue>
|
|
8
|
+
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "value" | "checked" | "onReset"> {
|
|
9
|
+
value?: T;
|
|
10
|
+
checked?: boolean;
|
|
11
|
+
control: HTMLElement | null;
|
|
12
|
+
bubbles?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function VisuallyHiddenInput<T = InputValue>(props: VisuallyHiddenInputProps<T>) {
|
|
16
|
+
const { control, value, checked, bubbles = true, type = "hidden", style, ...inputProps } = props;
|
|
17
|
+
|
|
18
|
+
const isCheckInput = React.useMemo(() => type === "checkbox" || type === "radio" || type === "switch", [type]);
|
|
19
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
20
|
+
|
|
21
|
+
const prevValueRef = React.useRef<{
|
|
22
|
+
value: T | boolean | undefined;
|
|
23
|
+
previous: T | boolean | undefined;
|
|
24
|
+
}>({
|
|
25
|
+
value: isCheckInput ? checked : value,
|
|
26
|
+
previous: isCheckInput ? checked : value,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const prevValue = React.useMemo(() => {
|
|
30
|
+
const currentValue = isCheckInput ? checked : value;
|
|
31
|
+
if (prevValueRef.current.value !== currentValue) {
|
|
32
|
+
prevValueRef.current.previous = prevValueRef.current.value;
|
|
33
|
+
prevValueRef.current.value = currentValue;
|
|
34
|
+
}
|
|
35
|
+
return prevValueRef.current.previous;
|
|
36
|
+
}, [isCheckInput, value, checked]);
|
|
37
|
+
|
|
38
|
+
const [controlSize, setControlSize] = React.useState<{
|
|
39
|
+
width?: number;
|
|
40
|
+
height?: number;
|
|
41
|
+
}>({});
|
|
42
|
+
|
|
43
|
+
React.useLayoutEffect(() => {
|
|
44
|
+
if (!control) {
|
|
45
|
+
setControlSize({});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setControlSize({
|
|
50
|
+
width: control.offsetWidth,
|
|
51
|
+
height: control.offsetHeight,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (typeof window === "undefined") {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
59
|
+
if (!Array.isArray(entries) || !entries.length) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const entry = entries[0];
|
|
64
|
+
if (!entry) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let width: number;
|
|
69
|
+
let height: number;
|
|
70
|
+
|
|
71
|
+
if ("borderBoxSize" in entry) {
|
|
72
|
+
const borderSizeEntry = entry.borderBoxSize;
|
|
73
|
+
const borderSize = Array.isArray(borderSizeEntry) ? borderSizeEntry[0] : borderSizeEntry;
|
|
74
|
+
width = borderSize.inlineSize;
|
|
75
|
+
height = borderSize.blockSize;
|
|
76
|
+
} else {
|
|
77
|
+
width = control.offsetWidth;
|
|
78
|
+
height = control.offsetHeight;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setControlSize({ width, height });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
resizeObserver.observe(control, { box: "border-box" });
|
|
85
|
+
return () => {
|
|
86
|
+
resizeObserver.disconnect();
|
|
87
|
+
};
|
|
88
|
+
}, [control]);
|
|
89
|
+
|
|
90
|
+
React.useEffect(() => {
|
|
91
|
+
const input = inputRef.current;
|
|
92
|
+
if (!input) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const inputProto = window.HTMLInputElement.prototype;
|
|
97
|
+
const propertyKey = isCheckInput ? "checked" : "value";
|
|
98
|
+
const eventType = isCheckInput ? "click" : "input";
|
|
99
|
+
const currentValue = isCheckInput ? checked : value;
|
|
100
|
+
|
|
101
|
+
const serializedCurrentValue = isCheckInput
|
|
102
|
+
? checked
|
|
103
|
+
: typeof value === "object" && value !== null
|
|
104
|
+
? JSON.stringify(value)
|
|
105
|
+
: value;
|
|
106
|
+
|
|
107
|
+
const descriptor = Object.getOwnPropertyDescriptor(inputProto, propertyKey);
|
|
108
|
+
|
|
109
|
+
const setter = descriptor?.set;
|
|
110
|
+
|
|
111
|
+
if (prevValue !== currentValue && setter) {
|
|
112
|
+
const event = new Event(eventType, { bubbles });
|
|
113
|
+
setter.call(input, serializedCurrentValue);
|
|
114
|
+
input.dispatchEvent(event);
|
|
115
|
+
}
|
|
116
|
+
}, [prevValue, value, checked, bubbles, isCheckInput]);
|
|
117
|
+
|
|
118
|
+
const composedStyle = React.useMemo<React.CSSProperties>(() => {
|
|
119
|
+
return {
|
|
120
|
+
...style,
|
|
121
|
+
...(controlSize.width !== undefined && controlSize.height !== undefined ? controlSize : {}),
|
|
122
|
+
border: 0,
|
|
123
|
+
clip: "rect(0 0 0 0)",
|
|
124
|
+
clipPath: "inset(50%)",
|
|
125
|
+
height: "1px",
|
|
126
|
+
margin: "-1px",
|
|
127
|
+
overflow: "hidden",
|
|
128
|
+
padding: 0,
|
|
129
|
+
position: "absolute",
|
|
130
|
+
whiteSpace: "nowrap",
|
|
131
|
+
width: "1px",
|
|
132
|
+
};
|
|
133
|
+
}, [style, controlSize]);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<input
|
|
137
|
+
type={type}
|
|
138
|
+
{...inputProps}
|
|
139
|
+
ref={inputRef}
|
|
140
|
+
aria-hidden={isCheckInput}
|
|
141
|
+
tabIndex={-1}
|
|
142
|
+
defaultChecked={isCheckInput ? checked : undefined}
|
|
143
|
+
style={composedStyle}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export { VisuallyHiddenInput };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { useIsomorphicLayoutEffect } from "./use-isomorphic-layout-effect";
|
|
4
|
+
|
|
5
|
+
function useAsRef<T>(props: T) {
|
|
6
|
+
const ref = React.useRef<T>(props);
|
|
7
|
+
|
|
8
|
+
useIsomorphicLayoutEffect(() => {
|
|
9
|
+
ref.current = props;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
return ref;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export { useAsRef };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
function useLazyRef<T>(fn: () => T) {
|
|
4
|
+
const ref = React.useRef<{ current: T } | null>(null);
|
|
5
|
+
|
|
6
|
+
if (ref.current === null) {
|
|
7
|
+
ref.current = { current: fn() };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return ref.current;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { useLazyRef };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
type PossibleRef<T> = React.Ref<T> | undefined;
|
|
4
|
+
|
|
5
|
+
function setRef<T>(ref: PossibleRef<T>, value: T) {
|
|
6
|
+
if (typeof ref === "function") {
|
|
7
|
+
return ref(value);
|
|
8
|
+
}
|
|
9
|
+
if (ref !== null && ref !== undefined) {
|
|
10
|
+
ref.current = value;
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
|
|
16
|
+
return (node) => {
|
|
17
|
+
const cleanups = refs.map((ref) => setRef(ref, node));
|
|
18
|
+
if (!cleanups.some((cleanup) => typeof cleanup === "function")) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return () => {
|
|
22
|
+
cleanups.forEach((cleanup, index) => {
|
|
23
|
+
if (typeof cleanup === "function") {
|
|
24
|
+
cleanup();
|
|
25
|
+
} else {
|
|
26
|
+
setRef(refs[index], null);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
|
|
34
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: the composed callback must be rebuilt whenever any ref identity changes.
|
|
35
|
+
return React.useCallback(composeRefs(...refs), refs);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { composeRefs, useComposedRefs };
|
|
@@ -46,6 +46,9 @@ export const AllVariants: Story = () => (
|
|
|
46
46
|
disabled
|
|
47
47
|
/>
|
|
48
48
|
|
|
49
|
+
<span className={SECTION_HEADER}>Sensitive</span>
|
|
50
|
+
<Input id="sensitive" label="Client secret" sensitive defaultValue="s3cr3t-value" />
|
|
51
|
+
|
|
49
52
|
<span className={SECTION_HEADER}>Leading icon (email)</span>
|
|
50
53
|
<Input id="leading-icon" label="Email" required placeholder="alice@example.com" leadingIcon={<Mail />} />
|
|
51
54
|
|