@umituz/web-design-system 4.7.1 → 4.7.3

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.1",
3
+ "version": "4.7.3",
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.",
@@ -32,6 +32,7 @@ import { Drawer } from '../presentation/organisms/Drawer';
32
32
  import { Timeline } from '../presentation/organisms/Timeline';
33
33
  import { Checklist } from '../presentation/organisms/Checklist';
34
34
  import { FormModal } from '../presentation/organisms/FormModal';
35
+ import { ConfirmDialog } from '../presentation/organisms/ConfirmDialog';
35
36
  import { BatteryPill } from '../presentation/atoms/BatteryPill';
36
37
  import { Kbd } from '../presentation/atoms/Kbd';
37
38
 
@@ -880,4 +881,53 @@ describe('accessia supersets', () => {
880
881
  fireEvent.click(screen.getByRole('button', { name: 'Create' }));
881
882
  expect(onSubmit).toHaveBeenCalledWith({ welcome: true });
882
883
  });
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
+
918
+ it('ConfirmDialog confirms and closes, cancel only closes', () => {
919
+ const onConfirm = vi.fn();
920
+ const onClose = vi.fn();
921
+ render(
922
+ <ConfirmDialog title="Delete row?" body="This cannot be undone." confirmLabel="Delete" onConfirm={onConfirm} onClose={onClose} />,
923
+ );
924
+ expect(screen.getByRole('dialog')).toBeTruthy();
925
+ expect(screen.getByText('This cannot be undone.')).toBeTruthy();
926
+ fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
927
+ expect(onClose).toHaveBeenCalledTimes(1);
928
+ expect(onConfirm).not.toHaveBeenCalled();
929
+ fireEvent.click(screen.getByRole('button', { name: 'Delete' }));
930
+ expect(onConfirm).toHaveBeenCalledTimes(1);
931
+ expect(onClose).toHaveBeenCalledTimes(2);
932
+ });
883
933
  });
@@ -0,0 +1,42 @@
1
+ /**
2
+ * ConfirmDialog Component (Organism)
3
+ * @description Destructive/primary confirmation built on Modal: title, body and
4
+ * Cancel + confirm actions; unmounts itself after confirming
5
+ */
6
+
7
+ import type { ReactNode } from 'react';
8
+ import { Button } from '../atoms/Button';
9
+ import { Modal } from './Modal';
10
+
11
+ export interface ConfirmDialogProps {
12
+ title: ReactNode;
13
+ body?: ReactNode;
14
+ confirmLabel?: string;
15
+ cancelLabel?: string;
16
+ /** Visual weight of the confirm action; defaults to danger. */
17
+ tone?: 'danger' | 'primary';
18
+ onConfirm: () => void;
19
+ onClose: () => void;
20
+ }
21
+
22
+ export function ConfirmDialog({ title, body, confirmLabel = 'Confirm', cancelLabel = 'Cancel', tone = 'danger', onConfirm, onClose }: ConfirmDialogProps) {
23
+ return (
24
+ <Modal
25
+ open
26
+ danger={tone === 'danger'}
27
+ title={title}
28
+ description={body}
29
+ onClose={onClose}
30
+ footer={
31
+ <>
32
+ <Button variant="secondary" size="sm" onClick={onClose}>
33
+ {cancelLabel}
34
+ </Button>
35
+ <Button variant={tone === 'danger' ? 'danger' : 'default'} size="sm" onClick={() => { onConfirm(); onClose(); }}>
36
+ {confirmLabel}
37
+ </Button>
38
+ </>
39
+ }
40
+ />
41
+ );
42
+ }
@@ -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
  );
@@ -137,3 +137,6 @@ export type {
137
137
  FormModalValues,
138
138
  FormStep,
139
139
  } from './FormModal';
140
+
141
+ export { ConfirmDialog } from './ConfirmDialog';
142
+ export type { ConfirmDialogProps } from './ConfirmDialog';