@htlkg/components 0.0.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 (79) hide show
  1. package/dist/composables/index.js +388 -0
  2. package/dist/composables/index.js.map +1 -0
  3. package/package.json +41 -0
  4. package/src/composables/index.ts +6 -0
  5. package/src/composables/useForm.test.ts +229 -0
  6. package/src/composables/useForm.ts +130 -0
  7. package/src/composables/useFormValidation.test.ts +189 -0
  8. package/src/composables/useFormValidation.ts +83 -0
  9. package/src/composables/useModal.property.test.ts +164 -0
  10. package/src/composables/useModal.ts +43 -0
  11. package/src/composables/useNotifications.test.ts +166 -0
  12. package/src/composables/useNotifications.ts +81 -0
  13. package/src/composables/useTable.property.test.ts +198 -0
  14. package/src/composables/useTable.ts +134 -0
  15. package/src/composables/useTabs.property.test.ts +247 -0
  16. package/src/composables/useTabs.ts +101 -0
  17. package/src/data/Chart.demo.vue +340 -0
  18. package/src/data/Chart.md +525 -0
  19. package/src/data/Chart.vue +133 -0
  20. package/src/data/DataList.md +80 -0
  21. package/src/data/DataList.test.ts +69 -0
  22. package/src/data/DataList.vue +46 -0
  23. package/src/data/SearchableSelect.md +107 -0
  24. package/src/data/SearchableSelect.vue +124 -0
  25. package/src/data/Table.demo.vue +296 -0
  26. package/src/data/Table.md +588 -0
  27. package/src/data/Table.property.test.ts +548 -0
  28. package/src/data/Table.test.ts +562 -0
  29. package/src/data/Table.unit.test.ts +544 -0
  30. package/src/data/Table.vue +321 -0
  31. package/src/data/index.ts +5 -0
  32. package/src/domain/BrandCard.md +81 -0
  33. package/src/domain/BrandCard.vue +63 -0
  34. package/src/domain/BrandSelector.md +84 -0
  35. package/src/domain/BrandSelector.vue +65 -0
  36. package/src/domain/ProductBadge.md +60 -0
  37. package/src/domain/ProductBadge.vue +47 -0
  38. package/src/domain/UserAvatar.md +84 -0
  39. package/src/domain/UserAvatar.vue +60 -0
  40. package/src/domain/domain-components.property.test.ts +449 -0
  41. package/src/domain/index.ts +4 -0
  42. package/src/forms/DateRange.demo.vue +273 -0
  43. package/src/forms/DateRange.md +337 -0
  44. package/src/forms/DateRange.vue +110 -0
  45. package/src/forms/JsonSchemaForm.demo.vue +549 -0
  46. package/src/forms/JsonSchemaForm.md +112 -0
  47. package/src/forms/JsonSchemaForm.property.test.ts +817 -0
  48. package/src/forms/JsonSchemaForm.test.ts +601 -0
  49. package/src/forms/JsonSchemaForm.unit.test.ts +801 -0
  50. package/src/forms/JsonSchemaForm.vue +615 -0
  51. package/src/forms/index.ts +3 -0
  52. package/src/index.ts +17 -0
  53. package/src/navigation/Breadcrumbs.demo.vue +142 -0
  54. package/src/navigation/Breadcrumbs.md +102 -0
  55. package/src/navigation/Breadcrumbs.test.ts +69 -0
  56. package/src/navigation/Breadcrumbs.vue +58 -0
  57. package/src/navigation/Stepper.demo.vue +337 -0
  58. package/src/navigation/Stepper.md +174 -0
  59. package/src/navigation/Stepper.vue +146 -0
  60. package/src/navigation/Tabs.demo.vue +293 -0
  61. package/src/navigation/Tabs.md +163 -0
  62. package/src/navigation/Tabs.test.ts +176 -0
  63. package/src/navigation/Tabs.vue +104 -0
  64. package/src/navigation/index.ts +5 -0
  65. package/src/overlays/Alert.demo.vue +377 -0
  66. package/src/overlays/Alert.md +248 -0
  67. package/src/overlays/Alert.test.ts +166 -0
  68. package/src/overlays/Alert.vue +70 -0
  69. package/src/overlays/Drawer.md +140 -0
  70. package/src/overlays/Drawer.test.ts +92 -0
  71. package/src/overlays/Drawer.vue +76 -0
  72. package/src/overlays/Modal.demo.vue +149 -0
  73. package/src/overlays/Modal.md +385 -0
  74. package/src/overlays/Modal.test.ts +128 -0
  75. package/src/overlays/Modal.vue +86 -0
  76. package/src/overlays/Notification.md +150 -0
  77. package/src/overlays/Notification.test.ts +96 -0
  78. package/src/overlays/Notification.vue +58 -0
  79. package/src/overlays/index.ts +4 -0
@@ -0,0 +1,229 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { useForm } from './useForm';
3
+
4
+ describe('useForm composable', () => {
5
+ it('initializes with provided values', () => {
6
+ const initialValues = { name: 'John', email: 'john@example.com' };
7
+ const { values } = useForm({ initialValues });
8
+
9
+ expect(values.value).toEqual(initialValues);
10
+ });
11
+
12
+ it('sets field value correctly', () => {
13
+ const { values, setFieldValue } = useForm({
14
+ initialValues: { name: '', email: '' }
15
+ });
16
+
17
+ setFieldValue('name', 'Jane');
18
+ expect(values.value.name).toBe('Jane');
19
+ });
20
+
21
+ it('sets field error correctly', () => {
22
+ const { errors, setFieldError } = useForm({
23
+ initialValues: { name: '' }
24
+ });
25
+
26
+ setFieldError('name', 'Name is required');
27
+ expect(errors.value.name).toBe('Name is required');
28
+ });
29
+
30
+ it('sets field touched correctly', () => {
31
+ const { touched, setFieldTouched } = useForm({
32
+ initialValues: { name: '' }
33
+ });
34
+
35
+ setFieldTouched('name', true);
36
+ expect(touched.value.name).toBe(true);
37
+ });
38
+
39
+ it('validates field with rules', async () => {
40
+ const { validateField, errors } = useForm({
41
+ initialValues: { name: '' },
42
+ validationRules: {
43
+ name: [
44
+ {
45
+ validate: (value) => value.length > 0,
46
+ message: 'Name is required'
47
+ }
48
+ ]
49
+ }
50
+ });
51
+
52
+ const isValid = await validateField('name');
53
+ expect(isValid).toBe(false);
54
+ expect(errors.value.name).toBe('Name is required');
55
+ });
56
+
57
+ it('clears error when validation passes', async () => {
58
+ const { values, validateField, errors, setFieldValue } = useForm({
59
+ initialValues: { name: '' },
60
+ validationRules: {
61
+ name: [
62
+ {
63
+ validate: (value) => value.length > 0,
64
+ message: 'Name is required'
65
+ }
66
+ ]
67
+ }
68
+ });
69
+
70
+ // First validation should fail
71
+ await validateField('name');
72
+ expect(errors.value.name).toBe('Name is required');
73
+
74
+ // Set valid value
75
+ setFieldValue('name', 'John');
76
+ await validateField('name');
77
+ expect(errors.value.name).toBeUndefined();
78
+ });
79
+
80
+ it('validates entire form', async () => {
81
+ const { validateForm, errors } = useForm({
82
+ initialValues: { name: '', email: '' },
83
+ validationRules: {
84
+ name: [
85
+ {
86
+ validate: (value) => value.length > 0,
87
+ message: 'Name is required'
88
+ }
89
+ ],
90
+ email: [
91
+ {
92
+ validate: (value) => value.includes('@'),
93
+ message: 'Invalid email'
94
+ }
95
+ ]
96
+ }
97
+ });
98
+
99
+ const isValid = await validateForm();
100
+ expect(isValid).toBe(false);
101
+ expect(errors.value.name).toBe('Name is required');
102
+ expect(errors.value.email).toBe('Invalid email');
103
+ });
104
+
105
+ it('handles form submission', async () => {
106
+ const onSubmit = vi.fn();
107
+ const { handleSubmit, values, setFieldValue } = useForm({
108
+ initialValues: { name: '' },
109
+ onSubmit
110
+ });
111
+
112
+ setFieldValue('name', 'John');
113
+ await handleSubmit();
114
+
115
+ expect(onSubmit).toHaveBeenCalledWith({ name: 'John' });
116
+ });
117
+
118
+ it('prevents submission when validation fails', async () => {
119
+ const onSubmit = vi.fn();
120
+ const { handleSubmit } = useForm({
121
+ initialValues: { name: '' },
122
+ validationRules: {
123
+ name: [
124
+ {
125
+ validate: (value) => value.length > 0,
126
+ message: 'Name is required'
127
+ }
128
+ ]
129
+ },
130
+ onSubmit
131
+ });
132
+
133
+ await handleSubmit();
134
+ expect(onSubmit).not.toHaveBeenCalled();
135
+ });
136
+
137
+ it('marks all fields as touched on submit', async () => {
138
+ const { handleSubmit, touched } = useForm({
139
+ initialValues: { name: '', email: '' }
140
+ });
141
+
142
+ await handleSubmit();
143
+ expect(touched.value.name).toBe(true);
144
+ expect(touched.value.email).toBe(true);
145
+ });
146
+
147
+ it('resets form to initial values', () => {
148
+ const initialValues = { name: 'John', email: 'john@example.com' };
149
+ const { values, setFieldValue, resetForm } = useForm({ initialValues });
150
+
151
+ setFieldValue('name', 'Jane');
152
+ expect(values.value.name).toBe('Jane');
153
+
154
+ resetForm();
155
+ expect(values.value).toEqual(initialValues);
156
+ });
157
+
158
+ it('clears errors and touched on reset', () => {
159
+ const { errors, touched, setFieldError, setFieldTouched, resetForm } = useForm({
160
+ initialValues: { name: '' }
161
+ });
162
+
163
+ setFieldError('name', 'Error');
164
+ setFieldTouched('name', true);
165
+
166
+ resetForm();
167
+ expect(errors.value).toEqual({});
168
+ expect(touched.value).toEqual({});
169
+ });
170
+
171
+ it('computes isValid correctly', () => {
172
+ const { isValid, setFieldError } = useForm({
173
+ initialValues: { name: '' }
174
+ });
175
+
176
+ expect(isValid.value).toBe(true);
177
+
178
+ setFieldError('name', 'Error');
179
+ expect(isValid.value).toBe(false);
180
+ });
181
+
182
+ it('handles async validation rules', async () => {
183
+ const { validateField, errors } = useForm({
184
+ initialValues: { username: '' },
185
+ validationRules: {
186
+ username: [
187
+ {
188
+ validate: async (value) => {
189
+ await new Promise(resolve => setTimeout(resolve, 10));
190
+ return value === 'available';
191
+ },
192
+ message: 'Username not available'
193
+ }
194
+ ]
195
+ }
196
+ });
197
+
198
+ const isValid = await validateField('username');
199
+ expect(isValid).toBe(false);
200
+ expect(errors.value.username).toBe('Username not available');
201
+ });
202
+
203
+ it('prevents concurrent submissions', async () => {
204
+ const onSubmit = vi.fn(async () => {
205
+ await new Promise(resolve => setTimeout(resolve, 100));
206
+ });
207
+
208
+ const { handleSubmit, isSubmitting } = useForm({
209
+ initialValues: { name: 'John' },
210
+ onSubmit
211
+ });
212
+
213
+ // Start first submission
214
+ const promise1 = handleSubmit();
215
+
216
+ // Wait a tick for the isSubmitting flag to be set
217
+ await new Promise(resolve => setTimeout(resolve, 0));
218
+ expect(isSubmitting.value).toBe(true);
219
+
220
+ // Try second submission while first is in progress
221
+ await handleSubmit();
222
+
223
+ // Wait for first to complete
224
+ await promise1;
225
+
226
+ // Should only be called once
227
+ expect(onSubmit).toHaveBeenCalledTimes(1);
228
+ });
229
+ });
@@ -0,0 +1,130 @@
1
+ import { ref, computed, type Ref, type ComputedRef } from 'vue';
2
+
3
+ export interface ValidationRule<T = any> {
4
+ validate: (value: T) => boolean | Promise<boolean>;
5
+ message: string;
6
+ }
7
+
8
+ export interface FieldConfig {
9
+ rules?: ValidationRule[];
10
+ initialValue?: any;
11
+ }
12
+
13
+ export interface UseFormOptions<T extends Record<string, any>> {
14
+ initialValues: T;
15
+ validationRules?: Partial<Record<keyof T, ValidationRule[]>>;
16
+ onSubmit?: (values: T) => void | Promise<void>;
17
+ }
18
+
19
+ export interface UseFormReturn<T extends Record<string, any>> {
20
+ values: Ref<T>;
21
+ errors: Ref<Partial<Record<keyof T, string>>>;
22
+ touched: Ref<Partial<Record<keyof T, boolean>>>;
23
+ isSubmitting: Ref<boolean>;
24
+ isValid: ComputedRef<boolean>;
25
+ setFieldValue: (field: keyof T, value: any) => void;
26
+ setFieldError: (field: keyof T, error: string) => void;
27
+ setFieldTouched: (field: keyof T, touched: boolean) => void;
28
+ validateField: (field: keyof T) => Promise<boolean>;
29
+ validateForm: () => Promise<boolean>;
30
+ handleSubmit: (event?: Event) => Promise<void>;
31
+ resetForm: () => void;
32
+ }
33
+
34
+ export function useForm<T extends Record<string, any>>(
35
+ options: UseFormOptions<T>
36
+ ): UseFormReturn<T> {
37
+ const values = ref<T>({ ...options.initialValues }) as Ref<T>;
38
+ const errors = ref<Partial<Record<keyof T, string>>>({});
39
+ const touched = ref<Partial<Record<keyof T, boolean>>>({});
40
+ const isSubmitting = ref(false);
41
+
42
+ const isValid = computed(() => Object.keys(errors.value).length === 0);
43
+
44
+ function setFieldValue(field: keyof T, value: any) {
45
+ values.value[field] = value;
46
+ }
47
+
48
+ function setFieldError(field: keyof T, error: string) {
49
+ errors.value[field] = error;
50
+ }
51
+
52
+ function setFieldTouched(field: keyof T, isTouched: boolean) {
53
+ touched.value[field] = isTouched;
54
+ }
55
+
56
+ async function validateField(field: keyof T): Promise<boolean> {
57
+ const rules = options.validationRules?.[field];
58
+ if (!rules || rules.length === 0) return true;
59
+
60
+ const value = values.value[field];
61
+
62
+ for (const rule of rules) {
63
+ const isValid = await rule.validate(value);
64
+ if (!isValid) {
65
+ setFieldError(field, rule.message);
66
+ return false;
67
+ }
68
+ }
69
+
70
+ // Clear error if validation passed
71
+ delete errors.value[field];
72
+ return true;
73
+ }
74
+
75
+ async function validateForm(): Promise<boolean> {
76
+ const fields = Object.keys(values.value) as Array<keyof T>;
77
+ const validationResults = await Promise.all(
78
+ fields.map(field => validateField(field))
79
+ );
80
+
81
+ return validationResults.every(result => result);
82
+ }
83
+
84
+ async function handleSubmit(event?: Event) {
85
+ if (event) {
86
+ event.preventDefault();
87
+ }
88
+
89
+ if (isSubmitting.value) return;
90
+
91
+ // Mark all fields as touched
92
+ for (const field of Object.keys(values.value) as Array<keyof T>) {
93
+ setFieldTouched(field, true);
94
+ }
95
+
96
+ // Validate form
97
+ const isFormValid = await validateForm();
98
+ if (!isFormValid) return;
99
+
100
+ // Submit form
101
+ isSubmitting.value = true;
102
+ try {
103
+ await options.onSubmit?.(values.value);
104
+ } finally {
105
+ isSubmitting.value = false;
106
+ }
107
+ }
108
+
109
+ function resetForm() {
110
+ values.value = { ...options.initialValues };
111
+ errors.value = {};
112
+ touched.value = {};
113
+ isSubmitting.value = false;
114
+ }
115
+
116
+ return {
117
+ values,
118
+ errors,
119
+ touched,
120
+ isSubmitting,
121
+ isValid,
122
+ setFieldValue,
123
+ setFieldError,
124
+ setFieldTouched,
125
+ validateField,
126
+ validateForm,
127
+ handleSubmit,
128
+ resetForm
129
+ };
130
+ }
@@ -0,0 +1,189 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ required,
4
+ minLength,
5
+ maxLength,
6
+ email,
7
+ pattern,
8
+ min,
9
+ max,
10
+ custom,
11
+ useFormValidation
12
+ } from './useFormValidation';
13
+
14
+ describe('useFormValidation composable', () => {
15
+ describe('required', () => {
16
+ it('validates non-empty strings', () => {
17
+ const rule = required();
18
+ expect(rule.validate('test')).toBe(true);
19
+ expect(rule.validate('')).toBe(false);
20
+ expect(rule.validate(' ')).toBe(false);
21
+ });
22
+
23
+ it('validates non-empty arrays', () => {
24
+ const rule = required();
25
+ expect(rule.validate([1, 2, 3])).toBe(true);
26
+ expect(rule.validate([])).toBe(false);
27
+ });
28
+
29
+ it('validates null and undefined', () => {
30
+ const rule = required();
31
+ expect(rule.validate(null)).toBe(false);
32
+ expect(rule.validate(undefined)).toBe(false);
33
+ });
34
+
35
+ it('uses custom message', () => {
36
+ const rule = required('Custom message');
37
+ expect(rule.message).toBe('Custom message');
38
+ });
39
+ });
40
+
41
+ describe('minLength', () => {
42
+ it('validates minimum length', () => {
43
+ const rule = minLength(5);
44
+ expect(rule.validate('hello')).toBe(true);
45
+ expect(rule.validate('hi')).toBe(false);
46
+ });
47
+
48
+ it('allows empty values', () => {
49
+ const rule = minLength(5);
50
+ expect(rule.validate('')).toBe(true);
51
+ });
52
+
53
+ it('uses custom message', () => {
54
+ const rule = minLength(5, 'Too short');
55
+ expect(rule.message).toBe('Too short');
56
+ });
57
+ });
58
+
59
+ describe('maxLength', () => {
60
+ it('validates maximum length', () => {
61
+ const rule = maxLength(5);
62
+ expect(rule.validate('hello')).toBe(true);
63
+ expect(rule.validate('hello world')).toBe(false);
64
+ });
65
+
66
+ it('allows empty values', () => {
67
+ const rule = maxLength(5);
68
+ expect(rule.validate('')).toBe(true);
69
+ });
70
+
71
+ it('uses custom message', () => {
72
+ const rule = maxLength(5, 'Too long');
73
+ expect(rule.message).toBe('Too long');
74
+ });
75
+ });
76
+
77
+ describe('email', () => {
78
+ it('validates email format', () => {
79
+ const rule = email();
80
+ expect(rule.validate('test@example.com')).toBe(true);
81
+ expect(rule.validate('invalid')).toBe(false);
82
+ expect(rule.validate('test@')).toBe(false);
83
+ expect(rule.validate('@example.com')).toBe(false);
84
+ });
85
+
86
+ it('allows empty values', () => {
87
+ const rule = email();
88
+ expect(rule.validate('')).toBe(true);
89
+ });
90
+
91
+ it('uses custom message', () => {
92
+ const rule = email('Invalid email format');
93
+ expect(rule.message).toBe('Invalid email format');
94
+ });
95
+ });
96
+
97
+ describe('pattern', () => {
98
+ it('validates against regex pattern', () => {
99
+ const rule = pattern(/^\d{3}-\d{3}-\d{4}$/);
100
+ expect(rule.validate('123-456-7890')).toBe(true);
101
+ expect(rule.validate('1234567890')).toBe(false);
102
+ });
103
+
104
+ it('allows empty values', () => {
105
+ const rule = pattern(/^\d+$/);
106
+ expect(rule.validate('')).toBe(true);
107
+ });
108
+
109
+ it('uses custom message', () => {
110
+ const rule = pattern(/^\d+$/, 'Must be numbers only');
111
+ expect(rule.message).toBe('Must be numbers only');
112
+ });
113
+ });
114
+
115
+ describe('min', () => {
116
+ it('validates minimum value', () => {
117
+ const rule = min(10);
118
+ expect(rule.validate(10)).toBe(true);
119
+ expect(rule.validate(15)).toBe(true);
120
+ expect(rule.validate(5)).toBe(false);
121
+ });
122
+
123
+ it('allows null and undefined', () => {
124
+ const rule = min(10);
125
+ expect(rule.validate(null)).toBe(true);
126
+ expect(rule.validate(undefined)).toBe(true);
127
+ });
128
+
129
+ it('uses custom message', () => {
130
+ const rule = min(10, 'Too small');
131
+ expect(rule.message).toBe('Too small');
132
+ });
133
+ });
134
+
135
+ describe('max', () => {
136
+ it('validates maximum value', () => {
137
+ const rule = max(10);
138
+ expect(rule.validate(10)).toBe(true);
139
+ expect(rule.validate(5)).toBe(true);
140
+ expect(rule.validate(15)).toBe(false);
141
+ });
142
+
143
+ it('allows null and undefined', () => {
144
+ const rule = max(10);
145
+ expect(rule.validate(null)).toBe(true);
146
+ expect(rule.validate(undefined)).toBe(true);
147
+ });
148
+
149
+ it('uses custom message', () => {
150
+ const rule = max(10, 'Too large');
151
+ expect(rule.message).toBe('Too large');
152
+ });
153
+ });
154
+
155
+ describe('custom', () => {
156
+ it('validates with custom function', () => {
157
+ const rule = custom((value) => value === 'valid', 'Must be valid');
158
+ expect(rule.validate('valid')).toBe(true);
159
+ expect(rule.validate('invalid')).toBe(false);
160
+ });
161
+
162
+ it('supports async validation', async () => {
163
+ const rule = custom(
164
+ async (value) => {
165
+ await new Promise(resolve => setTimeout(resolve, 10));
166
+ return value === 'valid';
167
+ },
168
+ 'Must be valid'
169
+ );
170
+ expect(await rule.validate('valid')).toBe(true);
171
+ expect(await rule.validate('invalid')).toBe(false);
172
+ });
173
+ });
174
+
175
+ describe('useFormValidation', () => {
176
+ it('returns all validation functions', () => {
177
+ const validation = useFormValidation();
178
+
179
+ expect(validation.required).toBeDefined();
180
+ expect(validation.minLength).toBeDefined();
181
+ expect(validation.maxLength).toBeDefined();
182
+ expect(validation.email).toBeDefined();
183
+ expect(validation.pattern).toBeDefined();
184
+ expect(validation.min).toBeDefined();
185
+ expect(validation.max).toBeDefined();
186
+ expect(validation.custom).toBeDefined();
187
+ });
188
+ });
189
+ });
@@ -0,0 +1,83 @@
1
+ import type { ValidationRule } from './useForm';
2
+
3
+ // Common validation rules
4
+ export const required = (message = 'This field is required'): ValidationRule => ({
5
+ validate: (value: any) => {
6
+ if (value === null || value === undefined) return false;
7
+ if (typeof value === 'string') return value.trim().length > 0;
8
+ if (Array.isArray(value)) return value.length > 0;
9
+ return true;
10
+ },
11
+ message
12
+ });
13
+
14
+ export const minLength = (min: number, message?: string): ValidationRule => ({
15
+ validate: (value: string) => {
16
+ if (!value) return true; // Let required handle empty values
17
+ return value.length >= min;
18
+ },
19
+ message: message || `Must be at least ${min} characters`
20
+ });
21
+
22
+ export const maxLength = (max: number, message?: string): ValidationRule => ({
23
+ validate: (value: string) => {
24
+ if (!value) return true;
25
+ return value.length <= max;
26
+ },
27
+ message: message || `Must be at most ${max} characters`
28
+ });
29
+
30
+ export const email = (message = 'Invalid email address'): ValidationRule => ({
31
+ validate: (value: string) => {
32
+ if (!value) return true;
33
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
34
+ return emailRegex.test(value);
35
+ },
36
+ message
37
+ });
38
+
39
+ export const pattern = (regex: RegExp, message = 'Invalid format'): ValidationRule => ({
40
+ validate: (value: string) => {
41
+ if (!value) return true;
42
+ return regex.test(value);
43
+ },
44
+ message
45
+ });
46
+
47
+ export const min = (minValue: number, message?: string): ValidationRule => ({
48
+ validate: (value: number) => {
49
+ if (value === null || value === undefined) return true;
50
+ return value >= minValue;
51
+ },
52
+ message: message || `Must be at least ${minValue}`
53
+ });
54
+
55
+ export const max = (maxValue: number, message?: string): ValidationRule => ({
56
+ validate: (value: number) => {
57
+ if (value === null || value === undefined) return true;
58
+ return value <= maxValue;
59
+ },
60
+ message: message || `Must be at most ${maxValue}`
61
+ });
62
+
63
+ export const custom = (
64
+ validator: (value: any) => boolean | Promise<boolean>,
65
+ message: string
66
+ ): ValidationRule => ({
67
+ validate: validator,
68
+ message
69
+ });
70
+
71
+ // Composable for form validation
72
+ export function useFormValidation() {
73
+ return {
74
+ required,
75
+ minLength,
76
+ maxLength,
77
+ email,
78
+ pattern,
79
+ min,
80
+ max,
81
+ custom
82
+ };
83
+ }