@arcote.tech/arc-ds 0.7.26 → 0.7.28
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 +2 -2
- package/src/ds/form/fields/index.ts +2 -0
- package/src/ds/form/fields/number-field.tsx +63 -0
- package/src/ds/form/form.tsx +75 -34
- package/src/index.ts +2 -0
- package/src/layout/layout.tsx +20 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/arc-ds",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.28",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Przemysław Krasiński [arcote.tech]",
|
|
7
7
|
"description": "Design System for Arc framework — CVA-based components with display modes and variant overrides",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"tailwind-merge": "^3.5.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@arcote.tech/arc": "^0.7.
|
|
33
|
+
"@arcote.tech/arc": "^0.7.28",
|
|
34
34
|
"framer-motion": "^12.0.0",
|
|
35
35
|
"lucide-react": ">=0.400.0",
|
|
36
36
|
"radix-ui": "^1.0.0",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { TextField } from "./text-field";
|
|
2
2
|
export type { TextFieldProps } from "./text-field";
|
|
3
|
+
export { NumberField } from "./number-field";
|
|
4
|
+
export type { NumberFieldProps } from "./number-field";
|
|
3
5
|
export { TextareaField } from "./textarea-field";
|
|
4
6
|
export type { TextareaFieldProps } from "./textarea-field";
|
|
5
7
|
export { TagField } from "./tag-field";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { Input } from "../../input/input";
|
|
4
|
+
import { FormFieldContext } from "../field";
|
|
5
|
+
|
|
6
|
+
export interface NumberFieldProps {
|
|
7
|
+
label?: ReactNode;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
/** Wartość liczbowa pola; `undefined` = puste. */
|
|
10
|
+
value?: number;
|
|
11
|
+
/** Emituje `number` dla poprawnej liczby albo `undefined` dla pustego/niepełnego wejścia. */
|
|
12
|
+
onChange?: (value: number | undefined) => void;
|
|
13
|
+
min?: number;
|
|
14
|
+
max?: number;
|
|
15
|
+
step?: number;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Pole liczbowe formularza. Bliźniak `TextField`, ale binduje wprost do pola
|
|
21
|
+
* `number()` schematu — emituje `number | undefined`, więc nie trzeba ręcznej
|
|
22
|
+
* koercji `Number(...)` na onSubmit (wcześniejszy wzorzec obejścia).
|
|
23
|
+
*/
|
|
24
|
+
export function NumberField({
|
|
25
|
+
label,
|
|
26
|
+
placeholder,
|
|
27
|
+
value,
|
|
28
|
+
onChange,
|
|
29
|
+
min,
|
|
30
|
+
max,
|
|
31
|
+
step,
|
|
32
|
+
className,
|
|
33
|
+
}: NumberFieldProps) {
|
|
34
|
+
const fieldCtx = useContext(FormFieldContext);
|
|
35
|
+
const hasError = fieldCtx?.errors && fieldCtx.messages?.length > 0;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className={className}>
|
|
39
|
+
{label && (
|
|
40
|
+
<label className="block text-sm font-medium text-muted-foreground mb-1.5">
|
|
41
|
+
{label}
|
|
42
|
+
</label>
|
|
43
|
+
)}
|
|
44
|
+
<Input
|
|
45
|
+
type="number"
|
|
46
|
+
inputMode="decimal"
|
|
47
|
+
value={value ?? ""}
|
|
48
|
+
min={min}
|
|
49
|
+
max={max}
|
|
50
|
+
step={step}
|
|
51
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
52
|
+
// valueAsNumber daje NaN dla pustego/niepełnego wejścia ("", "-") → undefined.
|
|
53
|
+
const next = e.target.valueAsNumber;
|
|
54
|
+
onChange?.(Number.isNaN(next) ? undefined : next);
|
|
55
|
+
}}
|
|
56
|
+
placeholder={placeholder}
|
|
57
|
+
/>
|
|
58
|
+
{hasError && (
|
|
59
|
+
<p className="mt-1 text-xs text-destructive">{fieldCtx.messages[0]}</p>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
package/src/ds/form/form.tsx
CHANGED
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
type ArcObjectAny,
|
|
5
5
|
type ArcObjectKeys,
|
|
6
6
|
type ArcObjectValueByKey,
|
|
7
|
+
type ArcOr,
|
|
8
|
+
type ArcOrAny,
|
|
7
9
|
ArcOptional,
|
|
8
10
|
deepMerge,
|
|
9
11
|
} from "@arcote.tech/arc";
|
|
@@ -18,52 +20,83 @@ import React, {
|
|
|
18
20
|
} from "react";
|
|
19
21
|
import { FormField } from "./field";
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// Schema akceptowane przez Form: pojedynczy obiekt LUB union obiektów (`or()`).
|
|
24
|
+
// Dla unionu pola = suma pól wariantów, walidacja = wariant dopasowany do
|
|
25
|
+
// bieżącej wartości (dyskryminowany union — patrz ArcOr.validate).
|
|
26
|
+
export type FormSchemaAny = ArcObjectAny | ArcOrAny;
|
|
27
|
+
|
|
28
|
+
// Warianty schematu: dla `or(...)` obiektowe człony, w przeciwnym razie sam typ.
|
|
29
|
+
type SchemaVariant<T> = T extends ArcOr<infer Els>
|
|
30
|
+
? Extract<Els[number], ArcObjectAny>
|
|
31
|
+
: T;
|
|
32
|
+
|
|
33
|
+
// Klucze pól = suma kluczy wszystkich wariantów (dystrybucja po unionie).
|
|
34
|
+
type SchemaKeys<T> = SchemaVariant<T> extends infer V
|
|
35
|
+
? V extends ArcObjectAny
|
|
36
|
+
? ArcObjectKeys<V>
|
|
37
|
+
: never
|
|
38
|
+
: never;
|
|
39
|
+
|
|
40
|
+
// Element pola pod kluczem K — z wariantu(ów), które ten klucz posiadają.
|
|
41
|
+
type SchemaValueByKey<T, K extends PropertyKey> = SchemaVariant<T> extends infer V
|
|
42
|
+
? V extends ArcObjectAny
|
|
43
|
+
? K extends ArcObjectKeys<V>
|
|
44
|
+
? ArcObjectValueByKey<V, K>
|
|
45
|
+
: never
|
|
46
|
+
: never
|
|
47
|
+
: never;
|
|
48
|
+
|
|
49
|
+
export type FormContextValue<T extends FormSchemaAny> = {
|
|
50
|
+
values: Partial<Record<SchemaKeys<T>, any>>;
|
|
51
|
+
errors: Partial<Record<SchemaKeys<T>, Record<string, any>>>;
|
|
52
|
+
dirty: Set<SchemaKeys<T>>;
|
|
25
53
|
isSubmitted: boolean;
|
|
26
|
-
setFieldValue: (field:
|
|
27
|
-
setFieldDirty: (field:
|
|
28
|
-
validatePartial: (keys:
|
|
54
|
+
setFieldValue: (field: SchemaKeys<T>, value: any) => void;
|
|
55
|
+
setFieldDirty: (field: SchemaKeys<T>) => void;
|
|
56
|
+
validatePartial: (keys: SchemaKeys<T>[]) => boolean;
|
|
29
57
|
};
|
|
30
58
|
|
|
31
|
-
export type FormRef<T extends
|
|
59
|
+
export type FormRef<T extends FormSchemaAny> = {
|
|
32
60
|
submit: () => Promise<void>;
|
|
33
61
|
getValues: () => Partial<$type<T>>;
|
|
34
62
|
validate: () => boolean;
|
|
35
|
-
setFieldValue: (field:
|
|
63
|
+
setFieldValue: (field: SchemaKeys<T>, value: any) => void;
|
|
36
64
|
};
|
|
37
65
|
|
|
38
|
-
export type FormFields<T extends
|
|
39
|
-
[K in
|
|
66
|
+
export type FormFields<T extends FormSchemaAny> = {
|
|
67
|
+
[K in SchemaKeys<T> as Capitalize<`${K}`>]: SchemaValueByKey<
|
|
40
68
|
T,
|
|
41
69
|
K
|
|
42
70
|
> extends ArcObjectAny
|
|
43
71
|
? ReturnType<
|
|
44
72
|
typeof FormField<
|
|
45
|
-
|
|
46
|
-
FormFields<
|
|
73
|
+
SchemaValueByKey<T, K>,
|
|
74
|
+
FormFields<SchemaValueByKey<T, K>>
|
|
47
75
|
>
|
|
48
76
|
>
|
|
49
|
-
:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
? ReturnType<typeof FormField<ArcObjectValueByKey<T, K>, FormFields<U>>>
|
|
53
|
-
: ReturnType<typeof FormField<ArcObjectValueByKey<T, K>>>;
|
|
77
|
+
: SchemaValueByKey<T, K> extends ArcOptional<infer U extends ArcObjectAny>
|
|
78
|
+
? ReturnType<typeof FormField<SchemaValueByKey<T, K>, FormFields<U>>>
|
|
79
|
+
: ReturnType<typeof FormField<SchemaValueByKey<T, K>>>;
|
|
54
80
|
};
|
|
55
81
|
|
|
56
|
-
export type FormProps<T extends
|
|
82
|
+
export type FormProps<T extends FormSchemaAny> = {
|
|
57
83
|
render: (props: FormFields<T>, values: Partial<$type<T>>) => React.ReactNode;
|
|
58
84
|
schema: T;
|
|
59
85
|
onSubmit: (values: $type<T>) => void | Promise<void>;
|
|
60
86
|
onUnvalidatedSubmit?: (values: Partial<$type<T>>, errors: any) => void;
|
|
61
87
|
defaults?: Partial<$type<T>> | null;
|
|
88
|
+
/**
|
|
89
|
+
* Wywoływane przy każdej zmianie wartości — `isValid` mówi, czy bieżące
|
|
90
|
+
* wartości przechodzą walidację schematu (dla `or()` — wariantu aktywnego).
|
|
91
|
+
* Pozwala stronie obserwować wartości i walidność bez własnego stanu pól
|
|
92
|
+
* (np. by gatować zewnętrzny przycisk lub zbudować snapshot z wartości).
|
|
93
|
+
*/
|
|
94
|
+
onValuesChange?: (values: Partial<$type<T>>, isValid: boolean) => void;
|
|
62
95
|
};
|
|
63
96
|
|
|
64
97
|
export const FormContext = createContext<FormContextValue<any> | null>(null);
|
|
65
98
|
|
|
66
|
-
export function useForm<T extends
|
|
99
|
+
export function useForm<T extends FormSchemaAny>(): FormContextValue<T> {
|
|
67
100
|
const context = React.useContext(FormContext);
|
|
68
101
|
if (!context) {
|
|
69
102
|
throw new Error("useForm must be used within a Form component");
|
|
@@ -100,16 +133,17 @@ function getNestedValue(obj: any, path: string): any {
|
|
|
100
133
|
return path.split(".").reduce((current, key) => current?.[key], obj);
|
|
101
134
|
}
|
|
102
135
|
|
|
103
|
-
export const Form = forwardRef(function Form<T extends
|
|
136
|
+
export const Form = forwardRef(function Form<T extends FormSchemaAny>(
|
|
104
137
|
props: FormProps<T>,
|
|
105
138
|
ref: React.Ref<FormRef<T>>,
|
|
106
139
|
) {
|
|
107
|
-
const { render, schema, onSubmit, defaults, onUnvalidatedSubmit } =
|
|
140
|
+
const { render, schema, onSubmit, defaults, onUnvalidatedSubmit, onValuesChange } =
|
|
141
|
+
props;
|
|
108
142
|
const [values, setValues] = useState<Partial<$type<T>>>({});
|
|
109
143
|
const [errors, setErrors] = useState<
|
|
110
|
-
Partial<Record<
|
|
144
|
+
Partial<Record<SchemaKeys<T>, Record<string, any>>>
|
|
111
145
|
>({});
|
|
112
|
-
const [dirty, setDirty] = useState<Set<
|
|
146
|
+
const [dirty, setDirty] = useState<Set<SchemaKeys<T>>>(new Set());
|
|
113
147
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
|
114
148
|
|
|
115
149
|
// Set initial values from defaults prop
|
|
@@ -125,12 +159,12 @@ export const Form = forwardRef(function Form<T extends ArcObjectAny>(
|
|
|
125
159
|
const validate = useCallback(() => {
|
|
126
160
|
const errors = schema.validate(values);
|
|
127
161
|
setErrors(errors as any);
|
|
128
|
-
setDirty(new Set(schema.entries().map(([key]) => key as
|
|
129
|
-
return Object.values(errors).some((result) => result);
|
|
162
|
+
setDirty(new Set(schema.entries().map(([key]) => key as SchemaKeys<T>)));
|
|
163
|
+
return Object.values(errors as any).some((result) => result);
|
|
130
164
|
}, [schema, values]);
|
|
131
165
|
|
|
132
166
|
const validatePartial = useCallback(
|
|
133
|
-
(keys:
|
|
167
|
+
(keys: SchemaKeys<T>[]) => {
|
|
134
168
|
const partialValues = keys.reduce(
|
|
135
169
|
(acc, key) => {
|
|
136
170
|
const value = getNestedValue(values, key as string);
|
|
@@ -139,7 +173,7 @@ export const Form = forwardRef(function Form<T extends ArcObjectAny>(
|
|
|
139
173
|
}
|
|
140
174
|
return acc;
|
|
141
175
|
},
|
|
142
|
-
{} as Partial<Record<
|
|
176
|
+
{} as Partial<Record<SchemaKeys<T>, any>>,
|
|
143
177
|
);
|
|
144
178
|
const errors = schema.validatePartial(partialValues);
|
|
145
179
|
if (errors) setErrors((prev) => deepMerge(prev, errors as any));
|
|
@@ -153,16 +187,16 @@ export const Form = forwardRef(function Form<T extends ArcObjectAny>(
|
|
|
153
187
|
}
|
|
154
188
|
return newSet;
|
|
155
189
|
});
|
|
156
|
-
return Object.values(errors).some((result) => result);
|
|
190
|
+
return Object.values(errors as any).some((result) => result);
|
|
157
191
|
},
|
|
158
192
|
[schema, values, dirty],
|
|
159
193
|
);
|
|
160
194
|
|
|
161
|
-
const setFieldValue = useCallback((field:
|
|
195
|
+
const setFieldValue = useCallback((field: SchemaKeys<T>, value: any) => {
|
|
162
196
|
setValues((prev) => setNestedValue(prev, field as string, value));
|
|
163
197
|
}, []);
|
|
164
198
|
|
|
165
|
-
const setFieldDirty = useCallback((field:
|
|
199
|
+
const setFieldDirty = useCallback((field: SchemaKeys<T>) => {
|
|
166
200
|
setDirty((prev) => new Set([...prev, field]));
|
|
167
201
|
}, []);
|
|
168
202
|
|
|
@@ -176,12 +210,19 @@ export const Form = forwardRef(function Form<T extends ArcObjectAny>(
|
|
|
176
210
|
}
|
|
177
211
|
return acc;
|
|
178
212
|
},
|
|
179
|
-
{} as Partial<Record<
|
|
213
|
+
{} as Partial<Record<SchemaKeys<T>, any>>,
|
|
180
214
|
);
|
|
181
215
|
const errors = schema.validatePartial(partialValues);
|
|
182
216
|
setErrors(errors as any);
|
|
183
217
|
}, [values, dirty]);
|
|
184
218
|
|
|
219
|
+
// Lift wartości + walidności do rodzica (obserwowalny formularz). Walidność =
|
|
220
|
+
// pełna walidacja schematu (dla `or()` wariantu aktywnego) bez efektów ubocznych.
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
if (!onValuesChange) return;
|
|
223
|
+
onValuesChange(values, !schema.validate(values));
|
|
224
|
+
}, [values, schema, onValuesChange]);
|
|
225
|
+
|
|
185
226
|
const handleSubmit = useCallback(
|
|
186
227
|
async (e?: React.FormEvent) => {
|
|
187
228
|
if (e) {
|
|
@@ -263,7 +304,7 @@ export const Form = forwardRef(function Form<T extends ArcObjectAny>(
|
|
|
263
304
|
.entries()
|
|
264
305
|
.map(([key, value]) => [
|
|
265
306
|
key.charAt(0).toUpperCase() + key.slice(1),
|
|
266
|
-
buildFieldsStructure(value, key, defaults?.[key]),
|
|
307
|
+
buildFieldsStructure(value, key, (defaults as any)?.[key]),
|
|
267
308
|
]),
|
|
268
309
|
);
|
|
269
310
|
}, [schema, defaults, buildFieldsStructure]);
|
|
@@ -294,6 +335,6 @@ export const Form = forwardRef(function Form<T extends ArcObjectAny>(
|
|
|
294
335
|
<form onSubmit={handleSubmit}>{render(Fields as any, values)}</form>
|
|
295
336
|
</FormContext.Provider>
|
|
296
337
|
);
|
|
297
|
-
}) as <T extends
|
|
338
|
+
}) as <T extends FormSchemaAny>(
|
|
298
339
|
props: FormProps<T> & React.RefAttributes<FormRef<T>>,
|
|
299
340
|
) => React.JSX.Element;
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ export {
|
|
|
24
24
|
useFormField,
|
|
25
25
|
useFormPart,
|
|
26
26
|
TextField,
|
|
27
|
+
NumberField,
|
|
27
28
|
TextareaField,
|
|
28
29
|
TagField,
|
|
29
30
|
SelectField,
|
|
@@ -37,6 +38,7 @@ export type {
|
|
|
37
38
|
FormFields,
|
|
38
39
|
FormFieldData,
|
|
39
40
|
TextFieldProps,
|
|
41
|
+
NumberFieldProps,
|
|
40
42
|
TextareaFieldProps,
|
|
41
43
|
TagFieldProps,
|
|
42
44
|
SelectFieldProps,
|
package/src/layout/layout.tsx
CHANGED
|
@@ -310,6 +310,8 @@ function MobileToolbar() {
|
|
|
310
310
|
const toolbarRef = useRef<HTMLDivElement>(null);
|
|
311
311
|
const collapseRef = useRef(collapse);
|
|
312
312
|
collapseRef.current = collapse;
|
|
313
|
+
const isExpandedRef = useRef(isExpanded);
|
|
314
|
+
isExpandedRef.current = isExpanded;
|
|
313
315
|
|
|
314
316
|
useEffect(() => {
|
|
315
317
|
if (isExpanded) {
|
|
@@ -319,10 +321,27 @@ function MobileToolbar() {
|
|
|
319
321
|
}
|
|
320
322
|
}, [isExpanded, requestOverlay, releaseOverlay]);
|
|
321
323
|
|
|
324
|
+
// Blokada scrolla treści pod spodem gdy panel jest rozwinięty — panel nakłada
|
|
325
|
+
// się na stronę (overlay), więc scroll strony musi być zamrożony na miejscu.
|
|
326
|
+
useEffect(() => {
|
|
327
|
+
if (!isExpanded) return;
|
|
328
|
+
const prev = document.body.style.overflow;
|
|
329
|
+
document.body.style.overflow = "hidden";
|
|
330
|
+
return () => {
|
|
331
|
+
document.body.style.overflow = prev;
|
|
332
|
+
};
|
|
333
|
+
}, [isExpanded]);
|
|
334
|
+
|
|
322
335
|
useEffect(() => {
|
|
323
336
|
const el = toolbarRef.current;
|
|
324
337
|
if (!el) return;
|
|
325
|
-
|
|
338
|
+
// Mierzymy tylko wysokość ZWINIĘTEGO paska. Gdy panel jest rozwinięty,
|
|
339
|
+
// ignorujemy zmiany rozmiaru — rozwinięty panel ma się nakładać na treść,
|
|
340
|
+
// a nie wypychać jej w dół rosnącym spacerem.
|
|
341
|
+
const update = () => {
|
|
342
|
+
if (isExpandedRef.current) return;
|
|
343
|
+
setTopHeight(el.offsetHeight);
|
|
344
|
+
};
|
|
326
345
|
update();
|
|
327
346
|
const ro = new ResizeObserver(update);
|
|
328
347
|
ro.observe(el);
|