@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.
Files changed (49) hide show
  1. package/package.json +42 -0
  2. package/src/ds/avatar/avatar.tsx +86 -0
  3. package/src/ds/badge/badge.tsx +61 -0
  4. package/src/ds/bento-card/bento-card.tsx +70 -0
  5. package/src/ds/bento-grid/bento-grid.tsx +52 -0
  6. package/src/ds/box/box.tsx +96 -0
  7. package/src/ds/button/button.tsx +191 -0
  8. package/src/ds/card-modal/card-modal.tsx +161 -0
  9. package/src/ds/display-mode.tsx +32 -0
  10. package/src/ds/ds-provider.tsx +85 -0
  11. package/src/ds/form/field.tsx +124 -0
  12. package/src/ds/form/fields/checkbox-select-field.tsx +326 -0
  13. package/src/ds/form/fields/index.ts +14 -0
  14. package/src/ds/form/fields/search-select-field.tsx +41 -0
  15. package/src/ds/form/fields/select-field.tsx +42 -0
  16. package/src/ds/form/fields/suggestion-list-field.tsx +43 -0
  17. package/src/ds/form/fields/tag-field.tsx +39 -0
  18. package/src/ds/form/fields/text-field.tsx +35 -0
  19. package/src/ds/form/fields/textarea-field.tsx +81 -0
  20. package/src/ds/form/form-part.tsx +79 -0
  21. package/src/ds/form/form.tsx +299 -0
  22. package/src/ds/form/index.tsx +5 -0
  23. package/src/ds/form/message.tsx +14 -0
  24. package/src/ds/input/input.tsx +115 -0
  25. package/src/ds/merge-variants.ts +26 -0
  26. package/src/ds/search-select/search-select.tsx +291 -0
  27. package/src/ds/separator/separator.tsx +26 -0
  28. package/src/ds/suggestion-list/suggestion-list.tsx +406 -0
  29. package/src/ds/tag-list/tag-list.tsx +87 -0
  30. package/src/ds/tooltip/tooltip.tsx +33 -0
  31. package/src/ds/transitions.ts +12 -0
  32. package/src/ds/types.ts +131 -0
  33. package/src/index.ts +115 -0
  34. package/src/layout/drag-handle.tsx +117 -0
  35. package/src/layout/dynamic-slot.tsx +95 -0
  36. package/src/layout/expandable-panel.tsx +57 -0
  37. package/src/layout/layout.tsx +323 -0
  38. package/src/layout/overlay-provider.tsx +103 -0
  39. package/src/layout/overlay.tsx +33 -0
  40. package/src/layout/router.tsx +101 -0
  41. package/src/layout/scroll-nav.tsx +121 -0
  42. package/src/layout/slot-render-context.tsx +14 -0
  43. package/src/layout/sub-nav-shell.tsx +41 -0
  44. package/src/layout/toolbar-expand.tsx +70 -0
  45. package/src/layout/transitions.ts +12 -0
  46. package/src/layout/use-expandable.ts +59 -0
  47. package/src/lib/utils.ts +6 -0
  48. package/src/ui/tooltip.tsx +59 -0
  49. 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,5 @@
1
+ export * from "./field";
2
+ export * from "./form";
3
+ export * from "./form-part";
4
+ export * from "./message";
5
+ export * from "./fields";
@@ -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
+ }