@djangocfg/ui-core 2.1.412 → 2.1.415

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 (51) hide show
  1. package/package.json +4 -4
  2. package/src/components/data/avatar-group/index.tsx +224 -0
  3. package/src/components/data/badge-overflow/index.tsx +259 -0
  4. package/src/components/data/circular-progress/index.tsx +358 -0
  5. package/src/components/data/relative-time-card/index.tsx +191 -0
  6. package/src/components/data/stat/index.tsx +140 -0
  7. package/src/components/data/status/index.tsx +80 -0
  8. package/src/components/effects/GlowBackground.tsx +9 -1
  9. package/src/components/effects/swap/index.tsx +289 -0
  10. package/src/components/feedback/banner/index.tsx +693 -0
  11. package/src/components/forms/checkbox-group/index.tsx +243 -0
  12. package/src/components/forms/editable/index.tsx +420 -0
  13. package/src/components/forms/input-otp/index.tsx +12 -3
  14. package/src/components/forms/mask-input/index.tsx +466 -0
  15. package/src/components/forms/otp/index.tsx +12 -8
  16. package/src/components/forms/segmented-input/index.tsx +319 -0
  17. package/src/components/forms/tags-input/index.tsx +896 -0
  18. package/src/components/forms/time-picker/index.tsx +285 -0
  19. package/src/components/index.ts +51 -0
  20. package/src/components/layout/key-value/index.tsx +884 -0
  21. package/src/components/layout/stack/index.tsx +349 -0
  22. package/src/components/navigation/context-menu/index.tsx +9 -6
  23. package/src/components/navigation/stepper/index.tsx +1307 -0
  24. package/src/components/select/multi-select-pro-async.tsx +11 -2
  25. package/src/components/select/multi-select-pro.tsx +11 -2
  26. package/src/components/specialized/presence/index.tsx +181 -0
  27. package/src/components/specialized/primitive/index.tsx +83 -0
  28. package/src/components/specialized/visually-hidden/index.tsx +19 -0
  29. package/src/components/specialized/visually-hidden-input/index.tsx +99 -0
  30. package/src/hooks/dom/index.ts +4 -0
  31. package/src/hooks/dom/useFormReset.ts +49 -0
  32. package/src/hooks/dom/useLayoutEffect.ts +16 -0
  33. package/src/hooks/dom/useSize.ts +57 -0
  34. package/src/hooks/state/index.ts +4 -0
  35. package/src/hooks/state/useCallbackRef.ts +25 -0
  36. package/src/hooks/state/usePrevious.ts +20 -0
  37. package/src/hooks/state/useStateMachine.ts +29 -0
  38. package/src/lib/compose-event-handlers.ts +22 -0
  39. package/src/lib/compose-refs.ts +65 -0
  40. package/src/lib/create-context.tsx +62 -0
  41. package/src/lib/get-element-ref.ts +33 -0
  42. package/src/lib/index.ts +5 -0
  43. package/src/lib/styles.ts +103 -0
  44. package/src/styles/README.md +43 -0
  45. package/src/styles/palette/utils.ts +15 -5
  46. package/src/styles/utilities/animations.css +135 -0
  47. package/src/styles/utilities/display.css +62 -0
  48. package/src/styles/utilities/glass.css +57 -0
  49. package/src/styles/utilities/marquee.css +69 -0
  50. package/src/styles/utilities/step.css +25 -0
  51. package/src/styles/utilities.css +6 -259
@@ -0,0 +1,243 @@
1
+ "use client"
2
+
3
+ import { Check } from "lucide-react"
4
+ import * as React from "react"
5
+
6
+ import { cn } from "../../../lib/utils"
7
+
8
+ // ─────────────────────────────────────────────────────────────────────────────
9
+ // Types
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+
12
+ interface CheckboxGroupContextValue {
13
+ value: string[]
14
+ onValueChange: (value: string[]) => void
15
+ disabled: boolean
16
+ name?: string
17
+ }
18
+
19
+ const CheckboxGroupContext = React.createContext<CheckboxGroupContextValue | null>(null)
20
+
21
+ function useCheckboxGroupContext(consumerName: string) {
22
+ const context = React.useContext(CheckboxGroupContext)
23
+ if (!context) {
24
+ throw new Error(`\`${consumerName}\` must be used within \`CheckboxGroup\``)
25
+ }
26
+ return context
27
+ }
28
+
29
+ // ─────────────────────────────────────────────────────────────────────────────
30
+ // CheckboxGroup Root
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+
33
+ interface CheckboxGroupProps extends React.ComponentProps<"div"> {
34
+ value?: string[]
35
+ defaultValue?: string[]
36
+ onValueChange?: (value: string[]) => void
37
+ disabled?: boolean
38
+ name?: string
39
+ }
40
+
41
+ function CheckboxGroup({
42
+ className,
43
+ value: valueProp,
44
+ defaultValue,
45
+ onValueChange,
46
+ disabled = false,
47
+ name,
48
+ ...props
49
+ }: CheckboxGroupProps) {
50
+ const isControlled = valueProp !== undefined
51
+ const [uncontrolledValue, setUncontrolledValue] = React.useState<string[]>(defaultValue ?? [])
52
+
53
+ const value = isControlled ? valueProp : uncontrolledValue
54
+
55
+ const handleValueChange = React.useCallback(
56
+ (nextValue: string[]) => {
57
+ if (!isControlled) {
58
+ setUncontrolledValue(nextValue)
59
+ }
60
+ onValueChange?.(nextValue)
61
+ },
62
+ [isControlled, onValueChange],
63
+ )
64
+
65
+ const contextValue = React.useMemo<CheckboxGroupContextValue>(
66
+ () => ({
67
+ value,
68
+ onValueChange: handleValueChange,
69
+ disabled,
70
+ name,
71
+ }),
72
+ [value, handleValueChange, disabled, name],
73
+ )
74
+
75
+ return (
76
+ <CheckboxGroupContext.Provider value={contextValue}>
77
+ <div
78
+ data-slot="checkbox-group"
79
+ data-disabled={disabled ? "" : undefined}
80
+ className={cn("peer flex flex-col gap-3.5", className)}
81
+ {...props}
82
+ />
83
+ </CheckboxGroupContext.Provider>
84
+ )
85
+ }
86
+
87
+ // ─────────────────────────────────────────────────────────────────────────────
88
+ // CheckboxGroupLabel
89
+ // ─────────────────────────────────────────────────────────────────────────────
90
+
91
+ function CheckboxGroupLabel({ className, ...props }: React.ComponentProps<"div">) {
92
+ return (
93
+ <div
94
+ data-slot="checkbox-group-label"
95
+ className={cn(
96
+ "text-foreground/70 text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
97
+ className,
98
+ )}
99
+ {...props}
100
+ />
101
+ )
102
+ }
103
+
104
+ // ─────────────────────────────────────────────────────────────────────────────
105
+ // CheckboxGroupList
106
+ // ─────────────────────────────────────────────────────────────────────────────
107
+
108
+ interface CheckboxGroupListProps extends React.ComponentProps<"div"> {
109
+ orientation?: "horizontal" | "vertical"
110
+ }
111
+
112
+ function CheckboxGroupList({
113
+ className,
114
+ orientation = "vertical",
115
+ ...props
116
+ }: CheckboxGroupListProps) {
117
+ return (
118
+ <div
119
+ data-slot="checkbox-group-list"
120
+ data-orientation={orientation}
121
+ className={cn(
122
+ "flex gap-3",
123
+ orientation === "horizontal" ? "flex-row" : "flex-col",
124
+ className,
125
+ )}
126
+ {...props}
127
+ />
128
+ )
129
+ }
130
+
131
+ // ─────────────────────────────────────────────────────────────────────────────
132
+ // CheckboxGroupItem
133
+ // ─────────────────────────────────────────────────────────────────────────────
134
+
135
+ interface CheckboxGroupItemProps extends Omit<React.ComponentProps<"button">, "value"> {
136
+ value: string
137
+ disabled?: boolean
138
+ }
139
+
140
+ function CheckboxGroupItem({
141
+ className,
142
+ children,
143
+ value,
144
+ disabled: disabledProp,
145
+ onClick,
146
+ ...props
147
+ }: CheckboxGroupItemProps) {
148
+ const context = useCheckboxGroupContext("CheckboxGroupItem")
149
+ const isDisabled = disabledProp || context.disabled
150
+ const isChecked = context.value.includes(value)
151
+
152
+ const handleClick = React.useCallback(
153
+ (event: React.MouseEvent<HTMLButtonElement>) => {
154
+ onClick?.(event)
155
+ if (isDisabled) return
156
+
157
+ const nextValue = isChecked
158
+ ? context.value.filter((v) => v !== value)
159
+ : [...context.value, value]
160
+
161
+ context.onValueChange(nextValue)
162
+ },
163
+ [onClick, isDisabled, isChecked, context, value],
164
+ )
165
+
166
+ return (
167
+ <label
168
+ className={cn(
169
+ "flex w-fit select-none items-center gap-2 text-sm leading-none",
170
+ "has-data-disabled:cursor-not-allowed has-data-disabled:opacity-50",
171
+ )}
172
+ >
173
+ <button
174
+ type="button"
175
+ role="checkbox"
176
+ aria-checked={isChecked}
177
+ data-slot="checkbox-group-item"
178
+ data-state={isChecked ? "checked" : "unchecked"}
179
+ data-disabled={isDisabled ? "" : undefined}
180
+ disabled={isDisabled}
181
+ onClick={handleClick}
182
+ className={cn(
183
+ "size-4 shrink-0 rounded-sm border border-primary shadow-sm",
184
+ "focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring",
185
+ "data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
186
+ className,
187
+ )}
188
+ {...props}
189
+ >
190
+ {isChecked && (
191
+ <span className="flex items-center justify-center text-current">
192
+ <Check className="size-3.5" />
193
+ </span>
194
+ )}
195
+ </button>
196
+ {children}
197
+ </label>
198
+ )
199
+ }
200
+
201
+ // ─────────────────────────────────────────────────────────────────────────────
202
+ // CheckboxGroupDescription
203
+ // ─────────────────────────────────────────────────────────────────────────────
204
+
205
+ function CheckboxGroupDescription({ className, ...props }: React.ComponentProps<"div">) {
206
+ return (
207
+ <div
208
+ data-slot="checkbox-group-description"
209
+ className={cn(
210
+ "text-[0.8rem] text-muted-foreground leading-none",
211
+ className,
212
+ )}
213
+ {...props}
214
+ />
215
+ )
216
+ }
217
+
218
+ // ─────────────────────────────────────────────────────────────────────────────
219
+ // CheckboxGroupMessage
220
+ // ─────────────────────────────────────────────────────────────────────────────
221
+
222
+ function CheckboxGroupMessage({ className, ...props }: React.ComponentProps<"div">) {
223
+ return (
224
+ <div
225
+ data-slot="checkbox-group-message"
226
+ className={cn(
227
+ "text-[0.8rem] text-muted-foreground leading-none",
228
+ className,
229
+ )}
230
+ {...props}
231
+ />
232
+ )
233
+ }
234
+
235
+ export {
236
+ CheckboxGroup,
237
+ CheckboxGroupDescription,
238
+ CheckboxGroupItem,
239
+ CheckboxGroupLabel,
240
+ CheckboxGroupList,
241
+ CheckboxGroupMessage,
242
+ }
243
+ export type { CheckboxGroupProps, CheckboxGroupItemProps, CheckboxGroupListProps }
@@ -0,0 +1,420 @@
1
+ "use client"
2
+
3
+ import * as React from "react";
4
+
5
+ import { cn } from "../../../lib/utils";
6
+
7
+ // =============================================================================
8
+ // Types
9
+ // =============================================================================
10
+
11
+ export interface EditableRootProps
12
+ extends Omit<React.ComponentPropsWithoutRef<"div">, "value" | "defaultValue" | "onChange" | "onSubmit" | "children"> {
13
+ /** Controlled value */
14
+ value?: string;
15
+ /** Default uncontrolled value */
16
+ defaultValue?: string;
17
+ /** Callback when value changes */
18
+ onValueChange?: (value: string) => void;
19
+ /** Callback when value is submitted (Enter or blur) */
20
+ onValueSubmit?: (value: string) => void;
21
+ /** Callback when editing is cancelled (Escape) */
22
+ onValueCancel?: (previousValue: string) => void;
23
+ /** Whether editing is active (controlled) */
24
+ editing?: boolean;
25
+ /** Callback when editing state changes */
26
+ onEditingChange?: (editing: boolean) => void;
27
+ /** Whether the editable is disabled. @default false */
28
+ disabled?: boolean;
29
+ /** Whether the editable is read-only. @default false */
30
+ readOnly?: boolean;
31
+ /** Whether to select all text on focus. @default true */
32
+ selectAllOnFocus?: boolean;
33
+ /** Whether to submit on blur. @default true */
34
+ submitOnBlur?: boolean;
35
+ /** Placeholder when empty */
36
+ placeholder?: string;
37
+ /** Form field name */
38
+ name?: string;
39
+ /** Children or render prop */
40
+ children?: React.ReactNode;
41
+ }
42
+
43
+ export interface EditablePreviewProps extends React.ComponentPropsWithoutRef<"span"> {}
44
+
45
+ export interface EditableInputProps extends Omit<React.ComponentPropsWithoutRef<"input">, "value" | "defaultValue"> {}
46
+
47
+ export interface EditableTextareaProps extends Omit<React.ComponentPropsWithoutRef<"textarea">, "value" | "defaultValue"> {}
48
+
49
+ // =============================================================================
50
+ // Context
51
+ // =============================================================================
52
+
53
+ interface EditableContextValue {
54
+ value: string;
55
+ setValue: (value: string) => void;
56
+ isEditing: boolean;
57
+ setIsEditing: (editing: boolean) => void;
58
+ submit: () => void;
59
+ cancel: () => void;
60
+ disabled: boolean;
61
+ readOnly: boolean;
62
+ selectAllOnFocus: boolean;
63
+ submitOnBlur: boolean;
64
+ placeholder?: string;
65
+ inputRef: React.RefObject<HTMLInputElement | null>;
66
+ textareaRef: React.RefObject<HTMLTextAreaElement | null>;
67
+ previousValueRef: React.MutableRefObject<string>;
68
+ }
69
+
70
+ const EditableContext = React.createContext<EditableContextValue | null>(null);
71
+
72
+ function useEditable(componentName: string) {
73
+ const context = React.useContext(EditableContext);
74
+ if (!context) {
75
+ throw new Error(`${componentName} must be used within <Editable>`);
76
+ }
77
+ return context;
78
+ }
79
+
80
+ // =============================================================================
81
+ // Root
82
+ // =============================================================================
83
+
84
+ const Editable = React.forwardRef<HTMLDivElement, EditableRootProps>(
85
+ (props, ref) => {
86
+ const {
87
+ value: valueProp,
88
+ defaultValue = "",
89
+ onValueChange,
90
+ onValueSubmit,
91
+ onValueCancel,
92
+ editing: editingProp,
93
+ onEditingChange,
94
+ disabled = false,
95
+ readOnly = false,
96
+ selectAllOnFocus = true,
97
+ submitOnBlur = true,
98
+ placeholder,
99
+ name,
100
+ children,
101
+ className,
102
+ ...rootProps
103
+ } = props;
104
+
105
+ const isControlledValue = valueProp !== undefined;
106
+ const isControlledEditing = editingProp !== undefined;
107
+
108
+ const [internalValue, setInternalValue] = React.useState(defaultValue);
109
+ const [internalEditing, setInternalEditing] = React.useState(false);
110
+
111
+ const resolvedValue = isControlledValue ? valueProp : internalValue;
112
+ const resolvedEditing = isControlledEditing ? editingProp : internalEditing;
113
+
114
+ const previousValueRef = React.useRef(resolvedValue);
115
+ const inputRef = React.useRef<HTMLInputElement>(null);
116
+ const textareaRef = React.useRef<HTMLTextAreaElement>(null);
117
+ const isFormControl = React.useRef(false);
118
+ const rootRef = React.useRef<HTMLDivElement>(null);
119
+
120
+ React.useImperativeHandle(ref, () => rootRef.current!);
121
+
122
+ React.useEffect(() => {
123
+ if (rootRef.current) {
124
+ isFormControl.current = !!rootRef.current.closest("form");
125
+ }
126
+ }, []);
127
+
128
+ const setValue = React.useCallback(
129
+ (newValue: string) => {
130
+ if (!isControlledValue) {
131
+ setInternalValue(newValue);
132
+ }
133
+ onValueChange?.(newValue);
134
+ },
135
+ [isControlledValue, onValueChange]
136
+ );
137
+
138
+ const setEditing = React.useCallback(
139
+ (editing: boolean) => {
140
+ if (!isControlledEditing) {
141
+ setInternalEditing(editing);
142
+ }
143
+ onEditingChange?.(editing);
144
+ },
145
+ [isControlledEditing, onEditingChange]
146
+ );
147
+
148
+ const submit = React.useCallback(() => {
149
+ const currentValue = resolvedValue;
150
+ previousValueRef.current = currentValue;
151
+ onValueSubmit?.(currentValue);
152
+ setEditing(false);
153
+ }, [resolvedValue, onValueSubmit, setEditing]);
154
+
155
+ const cancel = React.useCallback(() => {
156
+ const previousValue = previousValueRef.current;
157
+ setValue(previousValue);
158
+ onValueCancel?.(previousValue);
159
+ setEditing(false);
160
+ }, [setValue, onValueCancel, setEditing]);
161
+
162
+ const startEditing = React.useCallback(() => {
163
+ if (disabled || readOnly) return;
164
+ previousValueRef.current = resolvedValue;
165
+ setEditing(true);
166
+ }, [disabled, readOnly, resolvedValue, setEditing]);
167
+
168
+ React.useEffect(() => {
169
+ if (resolvedEditing) {
170
+ requestAnimationFrame(() => {
171
+ inputRef.current?.focus();
172
+ textareaRef.current?.focus();
173
+ });
174
+ }
175
+ }, [resolvedEditing]);
176
+
177
+ return (
178
+ <EditableContext.Provider
179
+ value={{
180
+ value: resolvedValue,
181
+ setValue,
182
+ isEditing: resolvedEditing,
183
+ setIsEditing: setEditing,
184
+ submit,
185
+ cancel,
186
+ disabled,
187
+ readOnly,
188
+ selectAllOnFocus,
189
+ submitOnBlur,
190
+ placeholder,
191
+ inputRef,
192
+ textareaRef,
193
+ previousValueRef,
194
+ }}
195
+ >
196
+ <div
197
+ ref={rootRef}
198
+ data-editing={resolvedEditing ? "" : undefined}
199
+ data-disabled={disabled ? "" : undefined}
200
+ data-readonly={readOnly ? "" : undefined}
201
+ className={cn(
202
+ "inline-flex items-center gap-1",
203
+ disabled && "cursor-not-allowed opacity-50",
204
+ className
205
+ )}
206
+ {...rootProps}
207
+ >
208
+ {children}
209
+ {isFormControl.current && name && (
210
+ <input type="hidden" name={name} value={resolvedValue} disabled={disabled} />
211
+ )}
212
+ </div>
213
+ </EditableContext.Provider>
214
+ );
215
+ }
216
+ );
217
+
218
+ Editable.displayName = "Editable";
219
+
220
+ // =============================================================================
221
+ // Preview
222
+ // =============================================================================
223
+
224
+ const EditablePreview = React.forwardRef<HTMLSpanElement, EditablePreviewProps>(
225
+ (props, ref) => {
226
+ const context = useEditable("EditablePreview");
227
+
228
+ if (context.isEditing) return null;
229
+
230
+ return (
231
+ <span
232
+ ref={ref}
233
+ role="button"
234
+ tabIndex={context.disabled || context.readOnly ? undefined : 0}
235
+ aria-disabled={context.disabled}
236
+ data-placeholder={!context.value ? "" : undefined}
237
+ className={cn(
238
+ "cursor-pointer select-none rounded-[var(--radius)] px-2 py-1 hover:bg-accent transition-colors",
239
+ !context.value && "text-muted-foreground italic",
240
+ (context.disabled || context.readOnly) && "cursor-default hover:bg-transparent",
241
+ props.className
242
+ )}
243
+ onClick={(event) => {
244
+ props.onClick?.(event);
245
+ if (!context.disabled && !context.readOnly) {
246
+ context.setIsEditing(true);
247
+ }
248
+ }}
249
+ onKeyDown={(event) => {
250
+ props.onKeyDown?.(event);
251
+ if (event.key === "Enter" || event.key === " ") {
252
+ event.preventDefault();
253
+ if (!context.disabled && !context.readOnly) {
254
+ context.setIsEditing(true);
255
+ }
256
+ }
257
+ }}
258
+ {...props}
259
+ >
260
+ {context.value || context.placeholder || "Click to edit"}
261
+ </span>
262
+ );
263
+ }
264
+ );
265
+
266
+ EditablePreview.displayName = "EditablePreview";
267
+
268
+ // =============================================================================
269
+ // Input
270
+ // =============================================================================
271
+
272
+ const EditableInput = React.forwardRef<HTMLInputElement, EditableInputProps>(
273
+ (props, ref) => {
274
+ const context = useEditable("EditableInput");
275
+
276
+ const composedRef = React.useCallback(
277
+ (node: HTMLInputElement | null) => {
278
+ context.inputRef.current = node;
279
+ if (typeof ref === "function") {
280
+ ref(node);
281
+ } else if (ref) {
282
+ (ref as React.MutableRefObject<HTMLInputElement | null>).current = node;
283
+ }
284
+ },
285
+ [ref, context.inputRef]
286
+ );
287
+
288
+ if (!context.isEditing) return null;
289
+
290
+ return (
291
+ <input
292
+ type="text"
293
+ ref={composedRef}
294
+ value={context.value}
295
+ disabled={context.disabled}
296
+ aria-label="Edit value"
297
+ className={cn(
298
+ "flex h-10 rounded-[var(--radius)] border border-input bg-transparent px-3 py-2 text-sm shadow-sm transition-colors",
299
+ "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
300
+ "disabled:cursor-not-allowed disabled:opacity-50",
301
+ props.className
302
+ )}
303
+ onChange={(event) => {
304
+ props.onChange?.(event);
305
+ context.setValue(event.target.value);
306
+ }}
307
+ onKeyDown={(event) => {
308
+ props.onKeyDown?.(event);
309
+ switch (event.key) {
310
+ case "Enter":
311
+ event.preventDefault();
312
+ context.submit();
313
+ break;
314
+ case "Escape":
315
+ event.preventDefault();
316
+ context.cancel();
317
+ break;
318
+ }
319
+ }}
320
+ onBlur={(event) => {
321
+ props.onBlur?.(event);
322
+ if (context.submitOnBlur) {
323
+ context.submit();
324
+ }
325
+ }}
326
+ onFocus={(event) => {
327
+ props.onFocus?.(event);
328
+ if (context.selectAllOnFocus) {
329
+ event.target.select();
330
+ }
331
+ }}
332
+ {...props}
333
+ />
334
+ );
335
+ }
336
+ );
337
+
338
+ EditableInput.displayName = "EditableInput";
339
+
340
+ // =============================================================================
341
+ // Textarea
342
+ // =============================================================================
343
+
344
+ const EditableTextarea = React.forwardRef<HTMLTextAreaElement, EditableTextareaProps>(
345
+ (props, ref) => {
346
+ const context = useEditable("EditableTextarea");
347
+
348
+ const composedRef = React.useCallback(
349
+ (node: HTMLTextAreaElement | null) => {
350
+ context.textareaRef.current = node;
351
+ if (typeof ref === "function") {
352
+ ref(node);
353
+ } else if (ref) {
354
+ (ref as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;
355
+ }
356
+ },
357
+ [ref, context.textareaRef]
358
+ );
359
+
360
+ if (!context.isEditing) return null;
361
+
362
+ return (
363
+ <textarea
364
+ ref={composedRef}
365
+ value={context.value}
366
+ disabled={context.disabled}
367
+ aria-label="Edit value"
368
+ className={cn(
369
+ "flex min-h-[80px] w-full rounded-[var(--radius)] border border-input bg-transparent px-3 py-2 text-sm shadow-sm transition-colors",
370
+ "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
371
+ "disabled:cursor-not-allowed disabled:opacity-50 resize-none",
372
+ props.className
373
+ )}
374
+ onChange={(event) => {
375
+ props.onChange?.(event);
376
+ context.setValue(event.target.value);
377
+ }}
378
+ onKeyDown={(event) => {
379
+ props.onKeyDown?.(event);
380
+ if (event.key === "Escape") {
381
+ event.preventDefault();
382
+ context.cancel();
383
+ }
384
+ // Submit on Ctrl+Enter or Meta+Enter
385
+ if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
386
+ event.preventDefault();
387
+ context.submit();
388
+ }
389
+ }}
390
+ onBlur={(event) => {
391
+ props.onBlur?.(event);
392
+ if (context.submitOnBlur) {
393
+ context.submit();
394
+ }
395
+ }}
396
+ onFocus={(event) => {
397
+ props.onFocus?.(event);
398
+ if (context.selectAllOnFocus) {
399
+ event.target.select();
400
+ }
401
+ }}
402
+ {...props}
403
+ />
404
+ );
405
+ }
406
+ );
407
+
408
+ EditableTextarea.displayName = "EditableTextarea";
409
+
410
+ // =============================================================================
411
+ // Exports
412
+ // =============================================================================
413
+
414
+ export {
415
+ Editable,
416
+ EditablePreview,
417
+ EditableInput,
418
+ EditableTextarea,
419
+ };
420
+
@@ -53,8 +53,17 @@ const InputOTPSlot = React.forwardRef<
53
53
  <div
54
54
  ref={ref}
55
55
  className={cn(
56
- "relative flex items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
57
- isActive && "z-10 ring-1 ring-ring",
56
+ // Vercel-style slot: subtle near-black card, soft border, muted
57
+ // digit colour, sharp focus ring. h/w come from `className`
58
+ // (OTPInput sets either a fixed size or `flex-1 aspect-square`
59
+ // for fluid layouts).
60
+ "relative flex items-center justify-center rounded-md border border-border bg-card tabular-nums text-muted-foreground transition-colors",
61
+ "hover:!border-foreground/40",
62
+ // The library renders a single hidden <input> for the whole OTP, so
63
+ // :focus-within fires on every slot at once. Visual focus must come
64
+ // from the per-slot isActive flag instead. Use ! to win specificity
65
+ // against the base `border-border`.
66
+ isActive && "z-10 !border-foreground !text-foreground",
58
67
  className
59
68
  )}
60
69
  {...props}
@@ -62,7 +71,7 @@ const InputOTPSlot = React.forwardRef<
62
71
  {char}
63
72
  {hasFakeCaret && (
64
73
  <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
65
- <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
74
+ <div className="h-5 w-px animate-caret-blink bg-foreground duration-1000" />
66
75
  </div>
67
76
  )}
68
77
  </div>