@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.2",
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 = title !== undefined || subtitle !== undefined || actions !== undefined;
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
- {title !== undefined && <CardTitle>{title}</CardTitle>}
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 'This field is required.';
74
- if (f.type === 'email' && val && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(val))) return 'Enter a valid email address.';
75
- if (f.type === 'tel' && val && !/[\d+()-\s]{7,}/.test(String(val))) return 'Enter a valid phone number.';
76
- if (f.type === 'number' && val !== '' && isNaN(Number(val))) return 'Enter a number.';
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({ title, steps, onSubmit, onClose, submitLabel = 'Save', wide, embedded }: FormModalProps) {
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
- Back
140
+ {backLabel}
133
141
  </Button>
134
142
  )}
135
143
  <Button variant="secondary" size="sm" onClick={onClose}>
136
- Cancel
144
+ {cancelLabel}
137
145
  </Button>
138
146
  <Button size="sm" onClick={submit}>
139
- {step < total - 1 ? 'Continue' : submitLabel}
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';