@zentauri-ui/zentauri-components 1.7.2 → 1.7.4

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 (69) hide show
  1. package/README.md +10 -6
  2. package/cli/registry.json +2 -0
  3. package/dist/chunk-KEKMMNL5.mjs +600 -0
  4. package/dist/chunk-KEKMMNL5.mjs.map +1 -0
  5. package/dist/chunk-NZDHSIIC.js +616 -0
  6. package/dist/chunk-NZDHSIIC.js.map +1 -0
  7. package/dist/design-system/command.d.ts +41 -0
  8. package/dist/design-system/command.d.ts.map +1 -0
  9. package/dist/design-system/index.d.ts +2 -0
  10. package/dist/design-system/index.d.ts.map +1 -1
  11. package/dist/design-system/otp-input.d.ts +27 -0
  12. package/dist/design-system/otp-input.d.ts.map +1 -0
  13. package/dist/ui/command/animated/animations.d.ts +3 -0
  14. package/dist/ui/command/animated/animations.d.ts.map +1 -0
  15. package/dist/ui/command/animated/command-content-animated.d.ts +6 -0
  16. package/dist/ui/command/animated/command-content-animated.d.ts.map +1 -0
  17. package/dist/ui/command/animated/index.d.ts +4 -0
  18. package/dist/ui/command/animated/index.d.ts.map +1 -0
  19. package/dist/ui/command/animated/types.d.ts +9 -0
  20. package/dist/ui/command/animated/types.d.ts.map +1 -0
  21. package/dist/ui/command/animated.js +92 -0
  22. package/dist/ui/command/animated.js.map +1 -0
  23. package/dist/ui/command/animated.mjs +89 -0
  24. package/dist/ui/command/animated.mjs.map +1 -0
  25. package/dist/ui/command/command-base.d.ts +53 -0
  26. package/dist/ui/command/command-base.d.ts.map +1 -0
  27. package/dist/ui/command/command.d.ts +6 -0
  28. package/dist/ui/command/command.d.ts.map +1 -0
  29. package/dist/ui/command/index.d.ts +5 -0
  30. package/dist/ui/command/index.d.ts.map +1 -0
  31. package/dist/ui/command/types.d.ts +111 -0
  32. package/dist/ui/command/types.d.ts.map +1 -0
  33. package/dist/ui/command/variants.d.ts +15 -0
  34. package/dist/ui/command/variants.d.ts.map +1 -0
  35. package/dist/ui/command.js +69 -0
  36. package/dist/ui/command.js.map +1 -0
  37. package/dist/ui/command.mjs +16 -0
  38. package/dist/ui/command.mjs.map +1 -0
  39. package/dist/ui/otp-input/index.d.ts +4 -0
  40. package/dist/ui/otp-input/index.d.ts.map +1 -0
  41. package/dist/ui/otp-input/otp-input.d.ts +6 -0
  42. package/dist/ui/otp-input/otp-input.d.ts.map +1 -0
  43. package/dist/ui/otp-input/types.d.ts +23 -0
  44. package/dist/ui/otp-input/types.d.ts.map +1 -0
  45. package/dist/ui/otp-input/variants.d.ts +5 -0
  46. package/dist/ui/otp-input/variants.d.ts.map +1 -0
  47. package/dist/ui/otp-input.js +302 -0
  48. package/dist/ui/otp-input.js.map +1 -0
  49. package/dist/ui/otp-input.mjs +299 -0
  50. package/dist/ui/otp-input.mjs.map +1 -0
  51. package/package.json +1 -1
  52. package/src/design-system/command.ts +80 -0
  53. package/src/design-system/index.ts +2 -0
  54. package/src/design-system/otp-input.ts +50 -0
  55. package/src/ui/command/animated/animations.ts +29 -0
  56. package/src/ui/command/animated/command-content-animated.tsx +58 -0
  57. package/src/ui/command/animated/index.ts +10 -0
  58. package/src/ui/command/animated/types.ts +23 -0
  59. package/src/ui/command/command-base.tsx +660 -0
  60. package/src/ui/command/command.test.tsx +130 -0
  61. package/src/ui/command/command.tsx +8 -0
  62. package/src/ui/command/index.ts +34 -0
  63. package/src/ui/command/types.ts +129 -0
  64. package/src/ui/command/variants.ts +41 -0
  65. package/src/ui/otp-input/index.ts +9 -0
  66. package/src/ui/otp-input/otp-input.test.tsx +99 -0
  67. package/src/ui/otp-input/otp-input.tsx +327 -0
  68. package/src/ui/otp-input/types.ts +32 -0
  69. package/src/ui/otp-input/variants.ts +18 -0
@@ -0,0 +1,327 @@
1
+ "use client";
2
+
3
+ import {
4
+ type ClipboardEvent,
5
+ type KeyboardEvent,
6
+ useCallback,
7
+ useId,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from "react";
12
+
13
+ import {
14
+ zuiOtpCellsBase,
15
+ zuiOtpErrorBase,
16
+ zuiOtpHintBase,
17
+ zuiOtpLabelBase,
18
+ zuiOtpRootBase,
19
+ zuiOtpSeparatorBase,
20
+ } from "../../design-system/otp-input";
21
+ import { cn } from "../../lib/utils";
22
+
23
+ import type { OTPInputAllowedCharacters, OTPInputProps } from "./types";
24
+ import { otpInputCellVariants } from "./variants";
25
+
26
+ function clampLength(length: number): number {
27
+ return Number.isFinite(length) ? Math.max(1, Math.min(12, length)) : 6;
28
+ }
29
+
30
+ function sanitizeValue(
31
+ value: string,
32
+ allowedCharacters: OTPInputAllowedCharacters,
33
+ maxLength: number,
34
+ ): string {
35
+ const pattern = allowedCharacters === "numeric" ? /[0-9]/g : /[a-zA-Z0-9]/g;
36
+ return (value.match(pattern) ?? []).join("").slice(0, maxLength);
37
+ }
38
+
39
+ function valueToCells(value: string, length: number): string[] {
40
+ return Array.from({ length }, (_, index) => {
41
+ const c = value[index] ?? "\x00";
42
+ return c === "\x00" ? "" : c;
43
+ });
44
+ }
45
+
46
+ function cellsToInternal(cells: string[], length: number): string {
47
+ return Array.from({ length }, (_, i) => cells[i] || "\x00").join("");
48
+ }
49
+
50
+ export function OTPInput(props: OTPInputProps) {
51
+ const {
52
+ allowedCharacters = "numeric",
53
+ appearance,
54
+ autoFocus,
55
+ cellClassName,
56
+ className,
57
+ defaultValue = "",
58
+ disabled,
59
+ errorMessage,
60
+ hint,
61
+ id,
62
+ label,
63
+ length = 6,
64
+ mask,
65
+ name,
66
+ onComplete,
67
+ onValueChange,
68
+ ref,
69
+ separatorEvery,
70
+ size,
71
+ value,
72
+ ...rest
73
+ } = props;
74
+ const generatedId = useId();
75
+ const rootId = id ?? generatedId;
76
+ const resolvedLength = clampLength(length);
77
+ const isControlled = value !== undefined;
78
+ const [uncontrolledValue, setUncontrolledValue] = useState(() => {
79
+ const clean = sanitizeValue(
80
+ defaultValue,
81
+ allowedCharacters,
82
+ resolvedLength,
83
+ );
84
+ return clean.padEnd(resolvedLength, "\x00");
85
+ });
86
+ const inputRefs = useRef<Array<HTMLInputElement | null>>([]);
87
+ const cells = useMemo(
88
+ () =>
89
+ isControlled
90
+ ? valueToCells(
91
+ sanitizeValue(
92
+ value ?? "",
93
+ allowedCharacters,
94
+ resolvedLength,
95
+ ).padEnd(resolvedLength, "\x00"),
96
+ resolvedLength,
97
+ )
98
+ : Array.from({ length: resolvedLength }, (_, index) => {
99
+ const c = uncontrolledValue[index] ?? "\x00";
100
+ return c === "\x00" ? "" : sanitizeValue(c, allowedCharacters, 1);
101
+ }),
102
+ [allowedCharacters, isControlled, resolvedLength, uncontrolledValue, value],
103
+ );
104
+ const sanitizedValue = cells.filter(Boolean).join("");
105
+ const labelId = `${rootId}-label`;
106
+ const hintId = `${rootId}-hint`;
107
+ const errorId = `${rootId}-error`;
108
+ const describedBy = [
109
+ hint !== undefined ? hintId : undefined,
110
+ errorMessage !== undefined ? errorId : undefined,
111
+ ]
112
+ .filter(Boolean)
113
+ .join(" ");
114
+
115
+ const commitValue = useCallback(
116
+ (nextCells: string[]) => {
117
+ if (!isControlled) {
118
+ setUncontrolledValue(cellsToInternal(nextCells, resolvedLength));
119
+ }
120
+ const next = nextCells.filter(Boolean).join("").slice(0, resolvedLength);
121
+ onValueChange?.(next);
122
+ if (next.length === resolvedLength) {
123
+ onComplete?.(next);
124
+ }
125
+ },
126
+ [isControlled, onComplete, onValueChange, resolvedLength],
127
+ );
128
+
129
+ const focusCell = useCallback(
130
+ (index: number) => {
131
+ const target =
132
+ inputRefs.current[Math.max(0, Math.min(index, resolvedLength - 1))];
133
+ target?.focus();
134
+ target?.select();
135
+ },
136
+ [resolvedLength],
137
+ );
138
+
139
+ const updateAtIndex = useCallback(
140
+ (index: number, nextChars: string, isPaste = false) => {
141
+ let chars: string | undefined = sanitizeValue(
142
+ nextChars,
143
+ allowedCharacters,
144
+ resolvedLength,
145
+ );
146
+
147
+ // Detect single-char overwrite: browser gives "existingChar + typedChar"
148
+ if (
149
+ !isPaste &&
150
+ chars &&
151
+ chars.length === 2 &&
152
+ chars[0] === (cells[index] ?? "")
153
+ ) {
154
+ chars = chars[1];
155
+ }
156
+
157
+ if (!chars?.length || (!isPaste && chars === cells[index])) {
158
+ return;
159
+ }
160
+
161
+ const nextCells = [...cells];
162
+
163
+ chars.split("").forEach((char, offset) => {
164
+ const targetIndex = index + offset;
165
+ if (targetIndex < resolvedLength) {
166
+ nextCells[targetIndex] = char;
167
+ }
168
+ });
169
+
170
+ commitValue(nextCells);
171
+ focusCell(
172
+ Math.min(index + Math.max(chars.length, 1), resolvedLength - 1),
173
+ );
174
+ },
175
+ [allowedCharacters, cells, commitValue, focusCell, resolvedLength],
176
+ );
177
+
178
+ const clearAtIndex = useCallback(
179
+ (index: number) => {
180
+ const nextCells = [...cells];
181
+ nextCells[index] = "";
182
+ commitValue(nextCells);
183
+ },
184
+ [cells, commitValue],
185
+ );
186
+
187
+ const handlePaste = useCallback(
188
+ (event: ClipboardEvent<HTMLInputElement>, index: number) => {
189
+ event.preventDefault();
190
+ updateAtIndex(index, event.clipboardData.getData("text"), true);
191
+ },
192
+ [updateAtIndex],
193
+ );
194
+
195
+ const handleKeyDown = useCallback(
196
+ (event: KeyboardEvent<HTMLInputElement>, index: number) => {
197
+ if (event.key === "Backspace") {
198
+ event.preventDefault();
199
+ if (cells[index]) {
200
+ clearAtIndex(index);
201
+ return;
202
+ }
203
+ clearAtIndex(Math.max(index - 1, 0));
204
+ focusCell(index - 1);
205
+ return;
206
+ }
207
+
208
+ if (event.key === "Delete") {
209
+ event.preventDefault();
210
+ clearAtIndex(index);
211
+ return;
212
+ }
213
+
214
+ if (event.key === "ArrowLeft") {
215
+ event.preventDefault();
216
+ focusCell(index - 1);
217
+ return;
218
+ }
219
+
220
+ if (event.key === "ArrowRight") {
221
+ event.preventDefault();
222
+ focusCell(index + 1);
223
+ return;
224
+ }
225
+
226
+ if (event.key === "Home") {
227
+ event.preventDefault();
228
+ focusCell(0);
229
+ return;
230
+ }
231
+
232
+ if (event.key === "End") {
233
+ event.preventDefault();
234
+ focusCell(resolvedLength - 1);
235
+ }
236
+ },
237
+ [cells, clearAtIndex, focusCell, resolvedLength],
238
+ );
239
+
240
+ return (
241
+ <div
242
+ ref={ref}
243
+ id={rootId}
244
+ role="group"
245
+ aria-labelledby={label !== undefined ? labelId : undefined}
246
+ aria-describedby={describedBy || undefined}
247
+ aria-invalid={errorMessage !== undefined ? true : undefined}
248
+ className={cn(zuiOtpRootBase, className)}
249
+ data-disabled={disabled ? "true" : undefined}
250
+ data-slot="otp-input"
251
+ {...rest}
252
+ >
253
+ {label !== undefined && (
254
+ <p id={labelId} className={zuiOtpLabelBase}>
255
+ {label}
256
+ </p>
257
+ )}
258
+ {hint !== undefined && (
259
+ <p id={hintId} className={zuiOtpHintBase}>
260
+ {hint}
261
+ </p>
262
+ )}
263
+ <div className={zuiOtpCellsBase} data-slot="otp-input-cells">
264
+ {cells.map((char, index) => (
265
+ <span
266
+ key={`${rootId}-${index}`}
267
+ className="contents"
268
+ data-slot="otp-input-cell-wrapper"
269
+ >
270
+ <input
271
+ ref={(node) => {
272
+ inputRefs.current[index] = node;
273
+ }}
274
+ aria-label={`Digit ${index + 1} of ${resolvedLength}`}
275
+ autoComplete={index === 0 ? "one-time-code" : "off"}
276
+ autoFocus={autoFocus && index === 0}
277
+ className={cn(
278
+ otpInputCellVariants({ appearance, size }),
279
+ cellClassName,
280
+ )}
281
+ data-slot="otp-input-cell"
282
+ disabled={disabled}
283
+ inputMode={allowedCharacters === "numeric" ? "numeric" : "text"}
284
+ maxLength={resolvedLength}
285
+ onChange={(event) =>
286
+ updateAtIndex(index, event.currentTarget.value, false)
287
+ }
288
+ onFocus={(event) => event.currentTarget.select()}
289
+ onKeyDown={(event) => handleKeyDown(event, index)}
290
+ onPaste={(event) => handlePaste(event, index)}
291
+ pattern={
292
+ allowedCharacters === "numeric" ? "[0-9]*" : "[A-Za-z0-9]*"
293
+ }
294
+ type={mask ? "password" : "text"}
295
+ value={char}
296
+ />
297
+ {separatorEvery &&
298
+ separatorEvery > 0 &&
299
+ index < resolvedLength - 1 &&
300
+ (index + 1) % separatorEvery === 0 && (
301
+ <span
302
+ aria-hidden="true"
303
+ className={zuiOtpSeparatorBase}
304
+ data-slot="otp-input-separator"
305
+ />
306
+ )}
307
+ </span>
308
+ ))}
309
+ </div>
310
+ {name !== undefined && (
311
+ <input
312
+ type="hidden"
313
+ name={name}
314
+ value={sanitizedValue}
315
+ disabled={disabled}
316
+ />
317
+ )}
318
+ {errorMessage !== undefined && (
319
+ <p id={errorId} className={zuiOtpErrorBase}>
320
+ {errorMessage}
321
+ </p>
322
+ )}
323
+ </div>
324
+ );
325
+ }
326
+
327
+ OTPInput.displayName = "OTPInput";
@@ -0,0 +1,32 @@
1
+ import type { VariantProps } from "class-variance-authority";
2
+ import type { ComponentPropsWithRef, ReactNode } from "react";
3
+
4
+ import type { otpInputCellVariants } from "./variants";
5
+
6
+ export type OTPInputAllowedCharacters = "numeric" | "alphanumeric";
7
+
8
+ export type OTPInputCellVariantProps = VariantProps<
9
+ typeof otpInputCellVariants
10
+ >;
11
+
12
+ export type OTPInputProps = OTPInputCellVariantProps &
13
+ Omit<
14
+ ComponentPropsWithRef<"div">,
15
+ "defaultValue" | "dir" | "onChange" | "children"
16
+ > & {
17
+ allowedCharacters?: OTPInputAllowedCharacters;
18
+ autoFocus?: boolean;
19
+ cellClassName?: string;
20
+ defaultValue?: string;
21
+ disabled?: boolean;
22
+ errorMessage?: ReactNode;
23
+ hint?: ReactNode;
24
+ label?: ReactNode;
25
+ length?: number;
26
+ mask?: boolean;
27
+ name?: string;
28
+ onComplete?: (value: string) => void;
29
+ onValueChange?: (value: string) => void;
30
+ separatorEvery?: number;
31
+ value?: string;
32
+ };
@@ -0,0 +1,18 @@
1
+ import { cva } from "class-variance-authority";
2
+
3
+ import {
4
+ zuiOtpAppearances,
5
+ zuiOtpCellBase,
6
+ zuiOtpSizes,
7
+ } from "../../design-system/otp-input";
8
+
9
+ export const otpInputCellVariants = cva(zuiOtpCellBase, {
10
+ variants: {
11
+ appearance: zuiOtpAppearances,
12
+ size: zuiOtpSizes,
13
+ },
14
+ defaultVariants: {
15
+ appearance: "default",
16
+ size: "md",
17
+ },
18
+ });