@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.
Files changed (52) hide show
  1. package/dist/components/area-chart.d.mts +3 -3
  2. package/dist/components/avatar.d.mts +1 -1
  3. package/dist/components/badge.d.mts +1 -1
  4. package/dist/components/bar-chart.d.mts +2 -2
  5. package/dist/components/bar-chart.mjs +1 -1
  6. package/dist/components/bar-list.d.mts +2 -2
  7. package/dist/components/button.d.mts +1 -1
  8. package/dist/components/chart-card.d.mts +2 -2
  9. package/dist/components/chart-legend.d.mts +3 -3
  10. package/dist/components/chart-primitives.d.mts +9 -10
  11. package/dist/components/chart-tooltip.d.mts +3 -3
  12. package/dist/components/donut-chart.d.mts +2 -2
  13. package/dist/components/editable.d.mts +101 -0
  14. package/dist/components/editable.mjs +509 -0
  15. package/dist/components/form.d.mts +10 -8
  16. package/dist/components/form.mjs +18 -8
  17. package/dist/components/heatmap-chart.d.mts +2 -2
  18. package/dist/components/input.d.mts +3 -1
  19. package/dist/components/input.mjs +5 -3
  20. package/dist/components/line-chart.d.mts +2 -2
  21. package/dist/components/select-trigger-variants.d.mts +2 -3
  22. package/dist/components/shimmer-text.d.mts +2 -3
  23. package/dist/components/skeleton.d.mts +1 -1
  24. package/dist/components/spinner.d.mts +2 -2
  25. package/dist/components/stat.d.mts +3 -3
  26. package/dist/components/typography.d.mts +5 -6
  27. package/dist/components/visually-hidden-input.d.mts +12 -0
  28. package/dist/components/visually-hidden-input.mjs +109 -0
  29. package/dist/hooks/use-as-ref.d.mts +5 -0
  30. package/dist/hooks/use-as-ref.mjs +12 -0
  31. package/dist/hooks/use-chart-theme.d.mts +3 -4
  32. package/dist/hooks/use-copy-to-clipboard.d.mts +2 -3
  33. package/dist/hooks/use-isomorphic-layout-effect.d.mts +5 -0
  34. package/dist/hooks/use-isomorphic-layout-effect.mjs +5 -0
  35. package/dist/hooks/use-lazy-ref.d.mts +6 -0
  36. package/dist/hooks/use-lazy-ref.mjs +9 -0
  37. package/dist/hooks/use-mobile.d.mts +2 -3
  38. package/dist/lib/chart-palette.d.mts +13 -14
  39. package/dist/lib/chart.d.mts +3 -4
  40. package/dist/lib/cn.d.mts +2 -3
  41. package/dist/lib/compose-refs.mjs +23 -0
  42. package/package.json +9 -13
  43. package/src/components/bar-chart.tsx +1 -1
  44. package/src/components/editable.tsx +871 -0
  45. package/src/components/form.tsx +16 -6
  46. package/src/components/input.tsx +8 -0
  47. package/src/components/visually-hidden-input.tsx +148 -0
  48. package/src/hooks/use-as-ref.ts +15 -0
  49. package/src/hooks/use-isomorphic-layout-effect.ts +5 -0
  50. package/src/hooks/use-lazy-ref.ts +13 -0
  51. package/src/lib/compose-refs.ts +38 -0
  52. package/src/stories/input.stories.tsx +3 -0
@@ -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}
@@ -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,5 @@
1
+ import * as React from "react";
2
+
3
+ const useIsomorphicLayoutEffect = typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect;
4
+
5
+ export { useIsomorphicLayoutEffect };
@@ -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