@umituz/web-design-system 4.7.2 → 4.7.4
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/web-design-system",
|
|
3
|
-
"version": "4.7.
|
|
3
|
+
"version": "4.7.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Web Design System - Tailwind v4 authored, precompiled CSS shipped. Atomic Design components (Atoms, Molecules, Organisms, Templates) for React applications.",
|
|
@@ -882,6 +882,39 @@ describe('accessia supersets', () => {
|
|
|
882
882
|
expect(onSubmit).toHaveBeenCalledWith({ welcome: true });
|
|
883
883
|
});
|
|
884
884
|
|
|
885
|
+
it('FormModal honours custom button and validation labels', () => {
|
|
886
|
+
const onSubmit = vi.fn();
|
|
887
|
+
render(
|
|
888
|
+
<FormModal
|
|
889
|
+
title="FM i18n"
|
|
890
|
+
cancelLabel="Vazgeç"
|
|
891
|
+
continueLabel="Devam"
|
|
892
|
+
backLabel="Geri"
|
|
893
|
+
submitLabel="Kaydet"
|
|
894
|
+
requiredMessage="Zorunlu alan."
|
|
895
|
+
steps={[
|
|
896
|
+
{
|
|
897
|
+
title: 'Kimlik',
|
|
898
|
+
fields: [
|
|
899
|
+
{ name: 'name', label: 'Ad', type: 'text', required: true },
|
|
900
|
+
],
|
|
901
|
+
},
|
|
902
|
+
{ title: 'İletişim', fields: [{ name: 'email', label: 'E-posta', type: 'email' }] },
|
|
903
|
+
]}
|
|
904
|
+
onSubmit={onSubmit}
|
|
905
|
+
onClose={() => {}}
|
|
906
|
+
/>,
|
|
907
|
+
);
|
|
908
|
+
fireEvent.click(screen.getByRole('button', { name: 'Devam' }));
|
|
909
|
+
expect(screen.getByText('Zorunlu alan.')).toBeTruthy();
|
|
910
|
+
fireEvent.change(screen.getByLabelText(/Ad/), { target: { value: 'Ada' } });
|
|
911
|
+
fireEvent.click(screen.getByRole('button', { name: 'Devam' }));
|
|
912
|
+
fireEvent.click(screen.getByRole('button', { name: 'Geri' }));
|
|
913
|
+
fireEvent.click(screen.getByRole('button', { name: 'Devam' }));
|
|
914
|
+
fireEvent.click(screen.getByRole('button', { name: 'Kaydet' }));
|
|
915
|
+
expect(onSubmit).toHaveBeenCalledWith({ name: 'Ada', email: '' });
|
|
916
|
+
});
|
|
917
|
+
|
|
885
918
|
it('ConfirmDialog confirms and closes, cancel only closes', () => {
|
|
886
919
|
const onConfirm = vi.fn();
|
|
887
920
|
const onClose = vi.fn();
|
|
@@ -37,17 +37,21 @@ Card.displayName = 'Card';
|
|
|
37
37
|
export interface CardHeaderProps extends Omit<CardProps, 'title'> {
|
|
38
38
|
title?: React.ReactNode;
|
|
39
39
|
subtitle?: React.ReactNode;
|
|
40
|
+
eyebrow?: React.ReactNode;
|
|
41
|
+
icon?: React.ReactNode;
|
|
40
42
|
actions?: React.ReactNode;
|
|
43
|
+
bordered?: boolean;
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
export const CardHeader = React.memo(forwardRef<HTMLDivElement, CardHeaderProps>(
|
|
44
|
-
({ className, title, subtitle, actions, children, ...props }, ref) => {
|
|
45
|
-
const hasStructuralProps =
|
|
47
|
+
({ className, title, subtitle, eyebrow, icon, actions, bordered = false, children, ...props }, ref) => {
|
|
48
|
+
const hasStructuralProps =
|
|
49
|
+
title !== undefined || subtitle !== undefined || actions !== undefined || eyebrow !== undefined || icon !== undefined;
|
|
46
50
|
if (!hasStructuralProps) {
|
|
47
51
|
return (
|
|
48
52
|
<div
|
|
49
53
|
ref={ref}
|
|
50
|
-
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
|
54
|
+
className={cn('flex flex-col space-y-1.5 p-6', bordered && 'border-b border-border', className)}
|
|
51
55
|
{...props}
|
|
52
56
|
>
|
|
53
57
|
{children}
|
|
@@ -57,11 +61,19 @@ export const CardHeader = React.memo(forwardRef<HTMLDivElement, CardHeaderProps>
|
|
|
57
61
|
return (
|
|
58
62
|
<div
|
|
59
63
|
ref={ref}
|
|
60
|
-
className={cn('flex items-start justify-between gap-4 p-6', className)}
|
|
64
|
+
className={cn('flex items-start justify-between gap-4 p-6', bordered && 'border-b border-border', className)}
|
|
61
65
|
{...props}
|
|
62
66
|
>
|
|
63
|
-
<div className="flex flex-col space-y-1.5">
|
|
64
|
-
{
|
|
67
|
+
<div className="flex min-w-0 flex-col space-y-1.5">
|
|
68
|
+
{eyebrow !== undefined && (
|
|
69
|
+
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">{eyebrow}</p>
|
|
70
|
+
)}
|
|
71
|
+
{title !== undefined && (
|
|
72
|
+
<div className="flex items-center gap-2">
|
|
73
|
+
{icon}
|
|
74
|
+
<CardTitle>{title}</CardTitle>
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
65
77
|
{subtitle !== undefined && <CardDescription>{subtitle}</CardDescription>}
|
|
66
78
|
{children}
|
|
67
79
|
</div>
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FormModal Component (Organism)
|
|
3
|
-
* @description Schema-driven multi-step form inside a modal (or embedded card).
|
|
4
|
-
* Steps declare typed fields; the engine renders controls, validates per step
|
|
5
|
-
* and submits the collected values as a flat record.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { useState } from 'react';
|
|
9
2
|
import type { ReactNode } from 'react';
|
|
10
3
|
import { ChevronLeft } from 'lucide-react';
|
|
@@ -32,7 +25,6 @@ export interface FormModalField {
|
|
|
32
25
|
hint?: string;
|
|
33
26
|
placeholder?: string;
|
|
34
27
|
options?: FormModalOption[];
|
|
35
|
-
/** Span both grid columns on wide layouts. */
|
|
36
28
|
span?: boolean;
|
|
37
29
|
min?: number;
|
|
38
30
|
max?: number;
|
|
@@ -54,8 +46,21 @@ export interface FormModalProps {
|
|
|
54
46
|
onClose: () => void;
|
|
55
47
|
submitLabel?: string;
|
|
56
48
|
wide?: boolean;
|
|
57
|
-
/** Render as an inline card instead of an overlay dialog. */
|
|
58
49
|
embedded?: boolean;
|
|
50
|
+
cancelLabel?: string;
|
|
51
|
+
backLabel?: string;
|
|
52
|
+
continueLabel?: string;
|
|
53
|
+
requiredMessage?: string;
|
|
54
|
+
invalidEmailMessage?: string;
|
|
55
|
+
invalidTelMessage?: string;
|
|
56
|
+
invalidNumberMessage?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface FormModalMessages {
|
|
60
|
+
required: string;
|
|
61
|
+
invalidEmail: string;
|
|
62
|
+
invalidTel: string;
|
|
63
|
+
invalidNumber: string;
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
function initialValues(steps: FormStep[]): FormModalValues {
|
|
@@ -69,15 +74,18 @@ function initialValues(steps: FormStep[]): FormModalValues {
|
|
|
69
74
|
return v;
|
|
70
75
|
}
|
|
71
76
|
|
|
72
|
-
function validateField(f: FormModalField, val: string | number | boolean | undefined): string {
|
|
73
|
-
if (f.required && (val === '' || val === undefined || val === null)) return
|
|
74
|
-
if (f.type === 'email' && val && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(val))) return
|
|
75
|
-
if (f.type === 'tel' && val && !/[\d+()-\s]{7,}/.test(String(val))) return
|
|
76
|
-
if (f.type === 'number' && val !== '' && isNaN(Number(val))) return
|
|
77
|
+
function validateField(f: FormModalField, val: string | number | boolean | undefined, m: FormModalMessages): string {
|
|
78
|
+
if (f.required && (val === '' || val === undefined || val === null)) return m.required;
|
|
79
|
+
if (f.type === 'email' && val && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(val))) return m.invalidEmail;
|
|
80
|
+
if (f.type === 'tel' && val && !/[\d+()-\s]{7,}/.test(String(val))) return m.invalidTel;
|
|
81
|
+
if (f.type === 'number' && val !== '' && isNaN(Number(val))) return m.invalidNumber;
|
|
77
82
|
return '';
|
|
78
83
|
}
|
|
79
84
|
|
|
80
|
-
export function FormModal(
|
|
85
|
+
export function FormModal(
|
|
86
|
+
{ title, steps, onSubmit, onClose, submitLabel = 'Save', wide, embedded, cancelLabel = 'Cancel', backLabel = 'Back', continueLabel = 'Continue', requiredMessage = 'This field is required.', invalidEmailMessage = 'Enter a valid email address.', invalidTelMessage = 'Enter a valid phone number.', invalidNumberMessage = 'Enter a number.' }: FormModalProps
|
|
87
|
+
) {
|
|
88
|
+
const messages: FormModalMessages = { required: requiredMessage, invalidEmail: invalidEmailMessage, invalidTel: invalidTelMessage, invalidNumber: invalidNumberMessage };
|
|
81
89
|
const [values, setValues] = useState<FormModalValues>(() => initialValues(steps));
|
|
82
90
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
83
91
|
const [step, setStep] = useState(0);
|
|
@@ -93,7 +101,7 @@ export function FormModal({ title, steps, onSubmit, onClose, submitLabel = 'Save
|
|
|
93
101
|
const validateStep = (idx: number): boolean => {
|
|
94
102
|
const errs: Record<string, string> = {};
|
|
95
103
|
for (const f of steps[idx]!.fields) {
|
|
96
|
-
const err = validateField(f, values[f.name]);
|
|
104
|
+
const err = validateField(f, values[f.name], messages);
|
|
97
105
|
if (err) errs[f.name] = err;
|
|
98
106
|
}
|
|
99
107
|
setErrors(errs);
|
|
@@ -108,7 +116,7 @@ export function FormModal({ title, steps, onSubmit, onClose, submitLabel = 'Save
|
|
|
108
116
|
}
|
|
109
117
|
for (let i = 0; i < total; i++) {
|
|
110
118
|
for (const f of steps[i]!.fields) {
|
|
111
|
-
const err = validateField(f, values[f.name]);
|
|
119
|
+
const err = validateField(f, values[f.name], messages);
|
|
112
120
|
if (err) {
|
|
113
121
|
setStep(i);
|
|
114
122
|
setErrors({ [f.name]: err });
|
|
@@ -129,14 +137,14 @@ export function FormModal({ title, steps, onSubmit, onClose, submitLabel = 'Save
|
|
|
129
137
|
)}
|
|
130
138
|
{step > 0 && (
|
|
131
139
|
<Button variant="secondary" size="sm" onClick={() => setStep(step - 1)}>
|
|
132
|
-
|
|
140
|
+
{backLabel}
|
|
133
141
|
</Button>
|
|
134
142
|
)}
|
|
135
143
|
<Button variant="secondary" size="sm" onClick={onClose}>
|
|
136
|
-
|
|
144
|
+
{cancelLabel}
|
|
137
145
|
</Button>
|
|
138
146
|
<Button size="sm" onClick={submit}>
|
|
139
|
-
{step < total - 1 ?
|
|
147
|
+
{step < total - 1 ? continueLabel : submitLabel}
|
|
140
148
|
</Button>
|
|
141
149
|
</>
|
|
142
150
|
);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card';
|
|
8
|
-
export type { CardProps } from './Card';
|
|
8
|
+
export type { CardProps, CardHeaderProps } from './Card';
|
|
9
9
|
|
|
10
10
|
export { Alert, AlertTitle, AlertDescription } from './Alert';
|
|
11
11
|
export type { AlertProps } from './Alert';
|