@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.
- package/package.json +4 -4
- package/src/components/data/avatar-group/index.tsx +224 -0
- package/src/components/data/badge-overflow/index.tsx +259 -0
- package/src/components/data/circular-progress/index.tsx +358 -0
- package/src/components/data/relative-time-card/index.tsx +191 -0
- package/src/components/data/stat/index.tsx +140 -0
- package/src/components/data/status/index.tsx +80 -0
- package/src/components/effects/GlowBackground.tsx +9 -1
- package/src/components/effects/swap/index.tsx +289 -0
- package/src/components/feedback/banner/index.tsx +693 -0
- package/src/components/forms/checkbox-group/index.tsx +243 -0
- package/src/components/forms/editable/index.tsx +420 -0
- package/src/components/forms/input-otp/index.tsx +12 -3
- package/src/components/forms/mask-input/index.tsx +466 -0
- package/src/components/forms/otp/index.tsx +12 -8
- package/src/components/forms/segmented-input/index.tsx +319 -0
- package/src/components/forms/tags-input/index.tsx +896 -0
- package/src/components/forms/time-picker/index.tsx +285 -0
- package/src/components/index.ts +51 -0
- package/src/components/layout/key-value/index.tsx +884 -0
- package/src/components/layout/stack/index.tsx +349 -0
- package/src/components/navigation/context-menu/index.tsx +9 -6
- package/src/components/navigation/stepper/index.tsx +1307 -0
- package/src/components/select/multi-select-pro-async.tsx +11 -2
- package/src/components/select/multi-select-pro.tsx +11 -2
- package/src/components/specialized/presence/index.tsx +181 -0
- package/src/components/specialized/primitive/index.tsx +83 -0
- package/src/components/specialized/visually-hidden/index.tsx +19 -0
- package/src/components/specialized/visually-hidden-input/index.tsx +99 -0
- package/src/hooks/dom/index.ts +4 -0
- package/src/hooks/dom/useFormReset.ts +49 -0
- package/src/hooks/dom/useLayoutEffect.ts +16 -0
- package/src/hooks/dom/useSize.ts +57 -0
- package/src/hooks/state/index.ts +4 -0
- package/src/hooks/state/useCallbackRef.ts +25 -0
- package/src/hooks/state/usePrevious.ts +20 -0
- package/src/hooks/state/useStateMachine.ts +29 -0
- package/src/lib/compose-event-handlers.ts +22 -0
- package/src/lib/compose-refs.ts +65 -0
- package/src/lib/create-context.tsx +62 -0
- package/src/lib/get-element-ref.ts +33 -0
- package/src/lib/index.ts +5 -0
- package/src/lib/styles.ts +103 -0
- package/src/styles/README.md +43 -0
- package/src/styles/palette/utils.ts +15 -5
- package/src/styles/utilities/animations.css +135 -0
- package/src/styles/utilities/display.css +62 -0
- package/src/styles/utilities/glass.css +57 -0
- package/src/styles/utilities/marquee.css +69 -0
- package/src/styles/utilities/step.css +25 -0
- 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
|
-
|
|
57
|
-
|
|
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-
|
|
74
|
+
<div className="h-5 w-px animate-caret-blink bg-foreground duration-1000" />
|
|
66
75
|
</div>
|
|
67
76
|
)}
|
|
68
77
|
</div>
|