@arcote.tech/arc-ds 0.4.1
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 +42 -0
- package/src/ds/avatar/avatar.tsx +86 -0
- package/src/ds/badge/badge.tsx +61 -0
- package/src/ds/bento-card/bento-card.tsx +70 -0
- package/src/ds/bento-grid/bento-grid.tsx +52 -0
- package/src/ds/box/box.tsx +96 -0
- package/src/ds/button/button.tsx +191 -0
- package/src/ds/card-modal/card-modal.tsx +161 -0
- package/src/ds/display-mode.tsx +32 -0
- package/src/ds/ds-provider.tsx +85 -0
- package/src/ds/form/field.tsx +124 -0
- package/src/ds/form/fields/checkbox-select-field.tsx +326 -0
- package/src/ds/form/fields/index.ts +14 -0
- package/src/ds/form/fields/search-select-field.tsx +41 -0
- package/src/ds/form/fields/select-field.tsx +42 -0
- package/src/ds/form/fields/suggestion-list-field.tsx +43 -0
- package/src/ds/form/fields/tag-field.tsx +39 -0
- package/src/ds/form/fields/text-field.tsx +35 -0
- package/src/ds/form/fields/textarea-field.tsx +81 -0
- package/src/ds/form/form-part.tsx +79 -0
- package/src/ds/form/form.tsx +299 -0
- package/src/ds/form/index.tsx +5 -0
- package/src/ds/form/message.tsx +14 -0
- package/src/ds/input/input.tsx +115 -0
- package/src/ds/merge-variants.ts +26 -0
- package/src/ds/search-select/search-select.tsx +291 -0
- package/src/ds/separator/separator.tsx +26 -0
- package/src/ds/suggestion-list/suggestion-list.tsx +406 -0
- package/src/ds/tag-list/tag-list.tsx +87 -0
- package/src/ds/tooltip/tooltip.tsx +33 -0
- package/src/ds/transitions.ts +12 -0
- package/src/ds/types.ts +131 -0
- package/src/index.ts +115 -0
- package/src/layout/drag-handle.tsx +117 -0
- package/src/layout/dynamic-slot.tsx +95 -0
- package/src/layout/expandable-panel.tsx +57 -0
- package/src/layout/layout.tsx +323 -0
- package/src/layout/overlay-provider.tsx +103 -0
- package/src/layout/overlay.tsx +33 -0
- package/src/layout/router.tsx +101 -0
- package/src/layout/scroll-nav.tsx +121 -0
- package/src/layout/slot-render-context.tsx +14 -0
- package/src/layout/sub-nav-shell.tsx +41 -0
- package/src/layout/toolbar-expand.tsx +70 -0
- package/src/layout/transitions.ts +12 -0
- package/src/layout/use-expandable.ts +59 -0
- package/src/lib/utils.ts +6 -0
- package/src/ui/tooltip.tsx +59 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type ArcObjectAny } from "@arcote.tech/arc";
|
|
4
|
+
import React, { createContext, useCallback, useContext, useState } from "react";
|
|
5
|
+
import { FormContext } from "./form";
|
|
6
|
+
|
|
7
|
+
export type FormPartContextValue = {
|
|
8
|
+
registerField: (field: string) => void;
|
|
9
|
+
unregisterField: (field: string) => void;
|
|
10
|
+
validatePart: () => boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const FormPartContext = createContext<FormPartContextValue | null>(null);
|
|
14
|
+
|
|
15
|
+
export function FormPart({ children }: { children: React.ReactNode }) {
|
|
16
|
+
const formContext = useContext(FormContext);
|
|
17
|
+
if (!formContext) {
|
|
18
|
+
throw new Error("FormPart must be used within a Form");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const [registeredFields, setRegisteredFields] = useState<Set<string>>(
|
|
22
|
+
new Set(),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const registerField = useCallback((field: string) => {
|
|
26
|
+
setRegisteredFields((prev) => new Set([...prev, field]));
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
const unregisterField = useCallback((field: string) => {
|
|
30
|
+
setRegisteredFields((prev) => {
|
|
31
|
+
const newSet = new Set(prev);
|
|
32
|
+
newSet.delete(field);
|
|
33
|
+
return newSet;
|
|
34
|
+
});
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
const validatePart = useCallback(() => {
|
|
38
|
+
const { validatePartial } = formContext;
|
|
39
|
+
return validatePartial(Array.from(registeredFields));
|
|
40
|
+
}, [formContext, registeredFields]);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<FormPartContext.Provider
|
|
44
|
+
value={{
|
|
45
|
+
registerField,
|
|
46
|
+
unregisterField,
|
|
47
|
+
validatePart,
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
</FormPartContext.Provider>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
FormPart.displayName = "FormPart";
|
|
56
|
+
|
|
57
|
+
export function useFormPart<T extends ArcObjectAny>() {
|
|
58
|
+
const context = useContext(FormPartContext);
|
|
59
|
+
if (!context) {
|
|
60
|
+
throw new Error("useFormPart must be used within a FormPart");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return context;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function useFormPartField(fieldName: string) {
|
|
67
|
+
const context = useContext(FormPartContext);
|
|
68
|
+
|
|
69
|
+
if (!context) return null;
|
|
70
|
+
|
|
71
|
+
React.useEffect(() => {
|
|
72
|
+
context.registerField(fieldName);
|
|
73
|
+
return () => {
|
|
74
|
+
context.unregisterField(fieldName);
|
|
75
|
+
};
|
|
76
|
+
}, [fieldName]);
|
|
77
|
+
|
|
78
|
+
return context;
|
|
79
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
type $type,
|
|
4
|
+
type ArcObjectAny,
|
|
5
|
+
type ArcObjectKeys,
|
|
6
|
+
type ArcObjectValueByKey,
|
|
7
|
+
ArcOptional,
|
|
8
|
+
deepMerge,
|
|
9
|
+
} from "@arcote.tech/arc";
|
|
10
|
+
import React, {
|
|
11
|
+
createContext,
|
|
12
|
+
forwardRef,
|
|
13
|
+
useCallback,
|
|
14
|
+
useEffect,
|
|
15
|
+
useImperativeHandle,
|
|
16
|
+
useMemo,
|
|
17
|
+
useState,
|
|
18
|
+
} from "react";
|
|
19
|
+
import { FormField } from "./field";
|
|
20
|
+
|
|
21
|
+
export type FormContextValue<T extends ArcObjectAny> = {
|
|
22
|
+
values: Partial<Record<ArcObjectKeys<T>, any>>;
|
|
23
|
+
errors: Partial<Record<ArcObjectKeys<T>, Record<string, any>>>;
|
|
24
|
+
dirty: Set<ArcObjectKeys<T>>;
|
|
25
|
+
isSubmitted: boolean;
|
|
26
|
+
setFieldValue: (field: ArcObjectKeys<T>, value: any) => void;
|
|
27
|
+
setFieldDirty: (field: ArcObjectKeys<T>) => void;
|
|
28
|
+
validatePartial: (keys: ArcObjectKeys<T>[]) => boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type FormRef<T extends ArcObjectAny> = {
|
|
32
|
+
submit: () => Promise<void>;
|
|
33
|
+
getValues: () => Partial<$type<T>>;
|
|
34
|
+
validate: () => boolean;
|
|
35
|
+
setFieldValue: (field: ArcObjectKeys<T>, value: any) => void;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type FormFields<T extends ArcObjectAny> = {
|
|
39
|
+
[K in ArcObjectKeys<T> as Capitalize<`${K}`>]: ArcObjectValueByKey<
|
|
40
|
+
T,
|
|
41
|
+
K
|
|
42
|
+
> extends ArcObjectAny
|
|
43
|
+
? ReturnType<
|
|
44
|
+
typeof FormField<
|
|
45
|
+
ArcObjectValueByKey<T, K>,
|
|
46
|
+
FormFields<ArcObjectValueByKey<T, K>>
|
|
47
|
+
>
|
|
48
|
+
>
|
|
49
|
+
: ArcObjectValueByKey<T, K> extends ArcOptional<
|
|
50
|
+
infer U extends ArcObjectAny
|
|
51
|
+
>
|
|
52
|
+
? ReturnType<typeof FormField<ArcObjectValueByKey<T, K>, FormFields<U>>>
|
|
53
|
+
: ReturnType<typeof FormField<ArcObjectValueByKey<T, K>>>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type FormProps<T extends ArcObjectAny> = {
|
|
57
|
+
render: (props: FormFields<T>, values: Partial<$type<T>>) => React.ReactNode;
|
|
58
|
+
schema: T;
|
|
59
|
+
onSubmit: (values: $type<T>) => void | Promise<void>;
|
|
60
|
+
onUnvalidatedSubmit?: (values: Partial<$type<T>>, errors: any) => void;
|
|
61
|
+
defaults?: Partial<$type<T>> | null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const FormContext = createContext<FormContextValue<any> | null>(null);
|
|
65
|
+
|
|
66
|
+
export function useForm<T extends ArcObjectAny>(): FormContextValue<T> {
|
|
67
|
+
const context = React.useContext(FormContext);
|
|
68
|
+
if (!context) {
|
|
69
|
+
throw new Error("useForm must be used within a Form component");
|
|
70
|
+
}
|
|
71
|
+
return context;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Helper function to set nested value using dot notation
|
|
75
|
+
function setNestedValue(obj: any, path: string, value: any): any {
|
|
76
|
+
const keys = path.split(".");
|
|
77
|
+
const result = { ...obj };
|
|
78
|
+
let current = result;
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
81
|
+
const key = keys[i];
|
|
82
|
+
if (
|
|
83
|
+
!(key in current) ||
|
|
84
|
+
typeof current[key] !== "object" ||
|
|
85
|
+
current[key] === null
|
|
86
|
+
) {
|
|
87
|
+
current[key] = {};
|
|
88
|
+
} else {
|
|
89
|
+
current[key] = { ...current[key] };
|
|
90
|
+
}
|
|
91
|
+
current = current[key];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
current[keys[keys.length - 1]] = value;
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Helper function to get nested value using dot notation
|
|
99
|
+
function getNestedValue(obj: any, path: string): any {
|
|
100
|
+
return path.split(".").reduce((current, key) => current?.[key], obj);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const Form = forwardRef(function Form<T extends ArcObjectAny>(
|
|
104
|
+
props: FormProps<T>,
|
|
105
|
+
ref: React.Ref<FormRef<T>>,
|
|
106
|
+
) {
|
|
107
|
+
const { render, schema, onSubmit, defaults, onUnvalidatedSubmit } = props;
|
|
108
|
+
const [values, setValues] = useState<Partial<$type<T>>>({});
|
|
109
|
+
const [errors, setErrors] = useState<
|
|
110
|
+
Partial<Record<ArcObjectKeys<T>, Record<string, any>>>
|
|
111
|
+
>({});
|
|
112
|
+
const [dirty, setDirty] = useState<Set<ArcObjectKeys<T>>>(new Set());
|
|
113
|
+
const [isSubmitted, setIsSubmitted] = useState(false);
|
|
114
|
+
|
|
115
|
+
// Set initial values from defaults prop
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (defaults) {
|
|
118
|
+
setValues((prev) => ({
|
|
119
|
+
...prev,
|
|
120
|
+
...defaults,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
}, [defaults]); // Update when defaults change
|
|
124
|
+
|
|
125
|
+
const validate = useCallback(() => {
|
|
126
|
+
const errors = schema.validate(values);
|
|
127
|
+
setErrors(errors as any);
|
|
128
|
+
setDirty(new Set(schema.entries().map(([key]) => key as ArcObjectKeys<T>)));
|
|
129
|
+
return Object.values(errors).some((result) => result);
|
|
130
|
+
}, [schema, values]);
|
|
131
|
+
|
|
132
|
+
const validatePartial = useCallback(
|
|
133
|
+
(keys: ArcObjectKeys<T>[]) => {
|
|
134
|
+
const partialValues = keys.reduce(
|
|
135
|
+
(acc, key) => {
|
|
136
|
+
const value = getNestedValue(values, key as string);
|
|
137
|
+
if (value !== undefined) {
|
|
138
|
+
return setNestedValue(acc, key as string, value);
|
|
139
|
+
}
|
|
140
|
+
return acc;
|
|
141
|
+
},
|
|
142
|
+
{} as Partial<Record<ArcObjectKeys<T>, any>>,
|
|
143
|
+
);
|
|
144
|
+
const errors = schema.validatePartial(partialValues);
|
|
145
|
+
if (errors) setErrors((prev) => deepMerge(prev, errors as any));
|
|
146
|
+
setDirty((prev) => {
|
|
147
|
+
const newSet = new Set([...prev, ...keys]);
|
|
148
|
+
if (
|
|
149
|
+
newSet.size === prev.size &&
|
|
150
|
+
[...prev].every((key) => newSet.has(key))
|
|
151
|
+
) {
|
|
152
|
+
return prev;
|
|
153
|
+
}
|
|
154
|
+
return newSet;
|
|
155
|
+
});
|
|
156
|
+
return Object.values(errors).some((result) => result);
|
|
157
|
+
},
|
|
158
|
+
[schema, values, dirty],
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const setFieldValue = useCallback((field: ArcObjectKeys<T>, value: any) => {
|
|
162
|
+
setValues((prev) => setNestedValue(prev, field as string, value));
|
|
163
|
+
}, []);
|
|
164
|
+
|
|
165
|
+
const setFieldDirty = useCallback((field: ArcObjectKeys<T>) => {
|
|
166
|
+
setDirty((prev) => new Set([...prev, field]));
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
169
|
+
// Add effect to revalidate dirty fields when values change
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
const partialValues = Array.from(dirty).reduce(
|
|
172
|
+
(acc, key) => {
|
|
173
|
+
const value = getNestedValue(values, key as string);
|
|
174
|
+
if (value !== undefined) {
|
|
175
|
+
return setNestedValue(acc, key as string, value);
|
|
176
|
+
}
|
|
177
|
+
return acc;
|
|
178
|
+
},
|
|
179
|
+
{} as Partial<Record<ArcObjectKeys<T>, any>>,
|
|
180
|
+
);
|
|
181
|
+
const errors = schema.validatePartial(partialValues);
|
|
182
|
+
setErrors(errors as any);
|
|
183
|
+
}, [values, dirty]);
|
|
184
|
+
|
|
185
|
+
const handleSubmit = useCallback(
|
|
186
|
+
async (e?: React.FormEvent) => {
|
|
187
|
+
if (e) {
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
}
|
|
190
|
+
setIsSubmitted(true);
|
|
191
|
+
|
|
192
|
+
const hasErrors = validate();
|
|
193
|
+
if (!hasErrors) {
|
|
194
|
+
await onSubmit(values as $type<T>);
|
|
195
|
+
} else {
|
|
196
|
+
onUnvalidatedSubmit?.(values, errors);
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
[schema, values, onSubmit, validate, errors, onUnvalidatedSubmit],
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
useImperativeHandle(
|
|
203
|
+
ref,
|
|
204
|
+
() => ({
|
|
205
|
+
submit: handleSubmit,
|
|
206
|
+
getValues: () => values,
|
|
207
|
+
validate,
|
|
208
|
+
setFieldValue,
|
|
209
|
+
}),
|
|
210
|
+
[handleSubmit, values, validate],
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// Recursive function to build field structure according to FormFields type
|
|
214
|
+
const buildFieldsStructure = useCallback(
|
|
215
|
+
(element: any, fieldName: string, defaultValue?: any): any => {
|
|
216
|
+
// Check if it's an optional element first using duck typing
|
|
217
|
+
// ArcOptional has a unique toJsonSchema() that returns { anyOf: [...] }
|
|
218
|
+
const isOptional =
|
|
219
|
+
element?.toJsonSchema &&
|
|
220
|
+
typeof element.toJsonSchema === "function" &&
|
|
221
|
+
element.parent !== undefined;
|
|
222
|
+
|
|
223
|
+
if (isOptional) {
|
|
224
|
+
// Unwrap the optional and recurse with the parent element
|
|
225
|
+
return buildFieldsStructure(
|
|
226
|
+
(element as any).parent,
|
|
227
|
+
fieldName,
|
|
228
|
+
defaultValue,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Check if it's an object element using duck typing
|
|
233
|
+
// ArcObject has entries() method
|
|
234
|
+
const isObject =
|
|
235
|
+
element?.entries && typeof element.entries === "function";
|
|
236
|
+
|
|
237
|
+
if (isObject) {
|
|
238
|
+
// Build subfields for nested structure
|
|
239
|
+
const subFields = Object.fromEntries(
|
|
240
|
+
element.entries().map(([key, value]: [string, any]) => [
|
|
241
|
+
key.charAt(0).toUpperCase() + key.slice(1), // Capitalize key to match FormFields type
|
|
242
|
+
buildFieldsStructure(
|
|
243
|
+
value,
|
|
244
|
+
fieldName ? `${fieldName}.${key}` : key, // Build dot notation path for nested access
|
|
245
|
+
defaultValue?.[key],
|
|
246
|
+
),
|
|
247
|
+
]),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// For objects, create FormField with subFields
|
|
251
|
+
return FormField(fieldName, defaultValue, subFields);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// For primitive elements, create FormField
|
|
255
|
+
return FormField(fieldName, defaultValue);
|
|
256
|
+
},
|
|
257
|
+
[],
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const Fields = useMemo(() => {
|
|
261
|
+
return Object.fromEntries(
|
|
262
|
+
schema
|
|
263
|
+
.entries()
|
|
264
|
+
.map(([key, value]) => [
|
|
265
|
+
key.charAt(0).toUpperCase() + key.slice(1),
|
|
266
|
+
buildFieldsStructure(value, key, defaults?.[key]),
|
|
267
|
+
]),
|
|
268
|
+
);
|
|
269
|
+
}, [schema, defaults, buildFieldsStructure]);
|
|
270
|
+
|
|
271
|
+
const contextValue = useMemo(
|
|
272
|
+
() => ({
|
|
273
|
+
values,
|
|
274
|
+
errors,
|
|
275
|
+
dirty,
|
|
276
|
+
isSubmitted,
|
|
277
|
+
setFieldValue: setFieldValue as any,
|
|
278
|
+
setFieldDirty: setFieldDirty as any,
|
|
279
|
+
validatePartial: validatePartial as any,
|
|
280
|
+
}),
|
|
281
|
+
[
|
|
282
|
+
values,
|
|
283
|
+
errors,
|
|
284
|
+
dirty,
|
|
285
|
+
isSubmitted,
|
|
286
|
+
setFieldValue,
|
|
287
|
+
setFieldDirty,
|
|
288
|
+
validatePartial,
|
|
289
|
+
],
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<FormContext.Provider value={contextValue}>
|
|
294
|
+
<form onSubmit={handleSubmit}>{render(Fields as any, values)}</form>
|
|
295
|
+
</FormContext.Provider>
|
|
296
|
+
);
|
|
297
|
+
}) as <T extends ArcObjectAny>(
|
|
298
|
+
props: FormProps<T> & React.RefAttributes<FormRef<T>>,
|
|
299
|
+
) => React.JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type HTMLAttributes } from "react";
|
|
4
|
+
import { useFormField } from "./field";
|
|
5
|
+
|
|
6
|
+
export function FormMessage({ ...props }: HTMLAttributes<HTMLSpanElement>) {
|
|
7
|
+
const { messages } = useFormField();
|
|
8
|
+
|
|
9
|
+
if (messages.length === 0) return null;
|
|
10
|
+
|
|
11
|
+
return <span {...props}>{messages[0]}</span>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
FormMessage.displayName = "FormMessage";
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { cva } from "class-variance-authority";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
import { useDisplayMode } from "../display-mode";
|
|
5
|
+
import type { InputProps } from "../types";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// CVA
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
export const inputVariants = cva(
|
|
12
|
+
"w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
|
13
|
+
{
|
|
14
|
+
variants: {
|
|
15
|
+
size: {
|
|
16
|
+
default: "h-10 md:h-9",
|
|
17
|
+
sm: "h-9 text-sm md:h-8",
|
|
18
|
+
xs: "h-7 text-xs px-2 md:h-6",
|
|
19
|
+
lg: "h-11 md:h-10",
|
|
20
|
+
},
|
|
21
|
+
display: {
|
|
22
|
+
default: "",
|
|
23
|
+
compact: "h-8 text-sm",
|
|
24
|
+
minimal: "h-6 text-xs px-2",
|
|
25
|
+
expanded: "",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: {
|
|
29
|
+
size: "default",
|
|
30
|
+
display: "default",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Sub-CVA — icon wrapper
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
export const inputIconVariants = cva(
|
|
40
|
+
"absolute top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none",
|
|
41
|
+
{
|
|
42
|
+
variants: {
|
|
43
|
+
side: {
|
|
44
|
+
left: "left-2.5",
|
|
45
|
+
right: "right-2.5",
|
|
46
|
+
},
|
|
47
|
+
size: {
|
|
48
|
+
default: "size-4",
|
|
49
|
+
sm: "size-3.5",
|
|
50
|
+
xs: "size-3",
|
|
51
|
+
lg: "size-4",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
defaultVariants: {
|
|
55
|
+
side: "left",
|
|
56
|
+
size: "default",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Input
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
66
|
+
(
|
|
67
|
+
{
|
|
68
|
+
icon: Icon,
|
|
69
|
+
iconSide = "left",
|
|
70
|
+
size: sizeProp,
|
|
71
|
+
display: displayProp,
|
|
72
|
+
className,
|
|
73
|
+
...props
|
|
74
|
+
},
|
|
75
|
+
ref,
|
|
76
|
+
) => {
|
|
77
|
+
const contextMode = useDisplayMode();
|
|
78
|
+
const display = displayProp ?? contextMode;
|
|
79
|
+
const size = sizeProp ?? "default";
|
|
80
|
+
|
|
81
|
+
if (Icon) {
|
|
82
|
+
return (
|
|
83
|
+
<div className="relative">
|
|
84
|
+
<Icon
|
|
85
|
+
className={inputIconVariants({
|
|
86
|
+
side: iconSide,
|
|
87
|
+
size,
|
|
88
|
+
})}
|
|
89
|
+
/>
|
|
90
|
+
<input
|
|
91
|
+
ref={ref}
|
|
92
|
+
data-slot="input"
|
|
93
|
+
className={cn(
|
|
94
|
+
inputVariants({ size, display }),
|
|
95
|
+
iconSide === "left" ? "pl-8" : "pr-8",
|
|
96
|
+
className,
|
|
97
|
+
)}
|
|
98
|
+
{...props}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<input
|
|
106
|
+
ref={ref}
|
|
107
|
+
data-slot="input"
|
|
108
|
+
className={cn(inputVariants({ size, display }), className)}
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
Input.displayName = "Input";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CVAVariantOverride } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deep merge CVA variant config.
|
|
5
|
+
*
|
|
6
|
+
* Dodaje nowe warianty do istniejących — nie kasuje domyślnych.
|
|
7
|
+
* Jeśli override zawiera istniejący klucz, nadpisuje go.
|
|
8
|
+
*/
|
|
9
|
+
export function mergeVariants(
|
|
10
|
+
base: CVAVariantOverride,
|
|
11
|
+
override: CVAVariantOverride | undefined,
|
|
12
|
+
): CVAVariantOverride {
|
|
13
|
+
if (!override) return base;
|
|
14
|
+
|
|
15
|
+
const result: CVAVariantOverride = {};
|
|
16
|
+
|
|
17
|
+
for (const key of Object.keys(base)) {
|
|
18
|
+
result[key] = { ...base[key] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (const key of Object.keys(override)) {
|
|
22
|
+
result[key] = { ...result[key], ...override[key] };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return result;
|
|
26
|
+
}
|