@htlkg/components 0.0.11 → 0.0.13

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 (112) hide show
  1. package/dist/{AdminWrapper.vue_vue_type_script_setup_true_lang-B32IylcT.js → AdminWrapper.vue_vue_type_script_setup_true_lang-BhnWQ-b0.js} +26 -29
  2. package/dist/AdminWrapper.vue_vue_type_script_setup_true_lang-BhnWQ-b0.js.map +1 -0
  3. package/dist/Alert.vue_vue_type_script_setup_true_lang-DxPCS-Hx.js.map +1 -1
  4. package/dist/DateRange.vue_vue_type_script_setup_true_lang-BLVg1Hah.js.map +1 -1
  5. package/dist/ProductBadge.vue_vue_type_script_setup_true_lang-Cmr2f4Cy.js.map +1 -1
  6. package/dist/components.css +4 -4
  7. package/dist/composables/index.js +23 -22
  8. package/dist/data/index.js +10 -10
  9. package/dist/{filterHelpers-DgRyoYSa.js → filterHelpers-DpHSlTuh.js} +11 -11
  10. package/dist/filterHelpers-DpHSlTuh.js.map +1 -0
  11. package/dist/index-QK97OdqQ.js.map +1 -1
  12. package/dist/index.js +34 -33
  13. package/dist/navigation/index.js +1 -1
  14. package/dist/{useAdminPage-GhgXp0x8.js → useAdminPage-AgWRvw6o.js} +150 -26
  15. package/dist/useAdminPage-AgWRvw6o.js.map +1 -0
  16. package/package.json +3 -3
  17. package/src/composables/index.ts +1 -0
  18. package/src/composables/useJsonForm.test.ts +272 -0
  19. package/src/composables/useJsonForm.ts +261 -0
  20. package/src/composables/useModal.test.ts +264 -0
  21. package/src/composables/useModal.ts +54 -8
  22. package/src/data/Chart/index.ts +2 -0
  23. package/src/data/DataList/index.ts +1 -0
  24. package/src/data/{DataTable.vue → DataTable/DataTable.vue} +2 -2
  25. package/src/data/DataTable/index.ts +8 -0
  26. package/src/data/SearchableSelect/index.ts +1 -0
  27. package/src/data/Table/index.ts +1 -0
  28. package/src/data/index.ts +5 -15
  29. package/src/domain/BrandCard/index.ts +1 -0
  30. package/src/domain/BrandSelector/index.ts +1 -0
  31. package/src/domain/ProductBadge/index.ts +1 -0
  32. package/src/domain/UserAvatar/index.ts +1 -0
  33. package/src/domain/index.ts +4 -4
  34. package/src/forms/DateRange/index.ts +2 -0
  35. package/src/forms/JsonSchemaForm/index.ts +1 -0
  36. package/src/forms/index.ts +2 -3
  37. package/src/navigation/{AdminWrapper.vue → AdminWrapper/AdminWrapper.vue} +41 -30
  38. package/src/navigation/AdminWrapper/index.ts +1 -0
  39. package/src/navigation/Breadcrumbs/index.ts +1 -0
  40. package/src/navigation/Stepper/index.ts +2 -0
  41. package/src/navigation/Tabs/index.ts +2 -0
  42. package/src/navigation/index.ts +4 -6
  43. package/src/overlays/Alert/index.ts +1 -0
  44. package/src/overlays/Drawer/index.ts +1 -0
  45. package/src/overlays/Modal/index.ts +1 -0
  46. package/src/overlays/Notification/index.ts +1 -0
  47. package/src/overlays/index.ts +4 -4
  48. package/src/patterns/DASHBOARD_PAGE.md +642 -0
  49. package/src/patterns/DETAIL_PAGE.md +446 -0
  50. package/src/patterns/FORM_PAGE.md +439 -0
  51. package/src/patterns/LIST_PAGE.md +340 -0
  52. package/src/patterns/PAGE_PATTERNS.md +110 -0
  53. package/src/patterns/WIZARD_PAGE.md +733 -0
  54. package/dist/AdminWrapper.vue_vue_type_script_setup_true_lang-B32IylcT.js.map +0 -1
  55. package/dist/filterHelpers-DgRyoYSa.js.map +0 -1
  56. package/dist/useAdminPage-GhgXp0x8.js.map +0 -1
  57. package/src/data/Table.vue +0 -295
  58. /package/src/data/{Chart.demo.vue → Chart/Chart.demo.vue} +0 -0
  59. /package/src/data/{Chart.md → Chart/Chart.md} +0 -0
  60. /package/src/data/{Chart.vue → Chart/Chart.vue} +0 -0
  61. /package/src/data/{DataList.md → DataList/DataList.md} +0 -0
  62. /package/src/data/{DataList.test.ts → DataList/DataList.test.ts} +0 -0
  63. /package/src/data/{DataList.vue → DataList/DataList.vue} +0 -0
  64. /package/src/data/{SearchableSelect.md → SearchableSelect/SearchableSelect.md} +0 -0
  65. /package/src/data/{SearchableSelect.vue → SearchableSelect/SearchableSelect.vue} +0 -0
  66. /package/src/data/{Table.demo.vue → Table/Table.demo.vue} +0 -0
  67. /package/src/data/{Table.md → Table/Table.md} +0 -0
  68. /package/src/data/{Table.property.test.ts → Table/Table.property.test.ts} +0 -0
  69. /package/src/data/{Table.test.ts → Table/Table.test.ts} +0 -0
  70. /package/src/data/{Table.unit.test.ts → Table/Table.unit.test.ts} +0 -0
  71. /package/src/domain/{BrandCard.md → BrandCard/BrandCard.md} +0 -0
  72. /package/src/domain/{BrandCard.vue → BrandCard/BrandCard.vue} +0 -0
  73. /package/src/domain/{BrandSelector.md → BrandSelector/BrandSelector.md} +0 -0
  74. /package/src/domain/{BrandSelector.vue → BrandSelector/BrandSelector.vue} +0 -0
  75. /package/src/domain/{ProductBadge.md → ProductBadge/ProductBadge.md} +0 -0
  76. /package/src/domain/{ProductBadge.vue → ProductBadge/ProductBadge.vue} +0 -0
  77. /package/src/domain/{UserAvatar.md → UserAvatar/UserAvatar.md} +0 -0
  78. /package/src/domain/{UserAvatar.vue → UserAvatar/UserAvatar.vue} +0 -0
  79. /package/src/forms/{DateRange.demo.vue → DateRange/DateRange.demo.vue} +0 -0
  80. /package/src/forms/{DateRange.md → DateRange/DateRange.md} +0 -0
  81. /package/src/forms/{DateRange.vue → DateRange/DateRange.vue} +0 -0
  82. /package/src/forms/{JsonSchemaForm.demo.vue → JsonSchemaForm/JsonSchemaForm.demo.vue} +0 -0
  83. /package/src/forms/{JsonSchemaForm.md → JsonSchemaForm/JsonSchemaForm.md} +0 -0
  84. /package/src/forms/{JsonSchemaForm.property.test.ts → JsonSchemaForm/JsonSchemaForm.property.test.ts} +0 -0
  85. /package/src/forms/{JsonSchemaForm.test.ts → JsonSchemaForm/JsonSchemaForm.test.ts} +0 -0
  86. /package/src/forms/{JsonSchemaForm.unit.test.ts → JsonSchemaForm/JsonSchemaForm.unit.test.ts} +0 -0
  87. /package/src/forms/{JsonSchemaForm.vue → JsonSchemaForm/JsonSchemaForm.vue} +0 -0
  88. /package/src/navigation/{Breadcrumbs.demo.vue → Breadcrumbs/Breadcrumbs.demo.vue} +0 -0
  89. /package/src/navigation/{Breadcrumbs.md → Breadcrumbs/Breadcrumbs.md} +0 -0
  90. /package/src/navigation/{Breadcrumbs.test.ts → Breadcrumbs/Breadcrumbs.test.ts} +0 -0
  91. /package/src/navigation/{Breadcrumbs.vue → Breadcrumbs/Breadcrumbs.vue} +0 -0
  92. /package/src/navigation/{Stepper.demo.vue → Stepper/Stepper.demo.vue} +0 -0
  93. /package/src/navigation/{Stepper.md → Stepper/Stepper.md} +0 -0
  94. /package/src/navigation/{Stepper.vue → Stepper/Stepper.vue} +0 -0
  95. /package/src/navigation/{Tabs.demo.vue → Tabs/Tabs.demo.vue} +0 -0
  96. /package/src/navigation/{Tabs.md → Tabs/Tabs.md} +0 -0
  97. /package/src/navigation/{Tabs.test.ts → Tabs/Tabs.test.ts} +0 -0
  98. /package/src/navigation/{Tabs.vue → Tabs/Tabs.vue} +0 -0
  99. /package/src/overlays/{Alert.demo.vue → Alert/Alert.demo.vue} +0 -0
  100. /package/src/overlays/{Alert.md → Alert/Alert.md} +0 -0
  101. /package/src/overlays/{Alert.test.ts → Alert/Alert.test.ts} +0 -0
  102. /package/src/overlays/{Alert.vue → Alert/Alert.vue} +0 -0
  103. /package/src/overlays/{Drawer.md → Drawer/Drawer.md} +0 -0
  104. /package/src/overlays/{Drawer.test.ts → Drawer/Drawer.test.ts} +0 -0
  105. /package/src/overlays/{Drawer.vue → Drawer/Drawer.vue} +0 -0
  106. /package/src/overlays/{Modal.demo.vue → Modal/Modal.demo.vue} +0 -0
  107. /package/src/overlays/{Modal.md → Modal/Modal.md} +0 -0
  108. /package/src/overlays/{Modal.test.ts → Modal/Modal.test.ts} +0 -0
  109. /package/src/overlays/{Modal.vue → Modal/Modal.vue} +0 -0
  110. /package/src/overlays/{Notification.md → Notification/Notification.md} +0 -0
  111. /package/src/overlays/{Notification.test.ts → Notification/Notification.test.ts} +0 -0
  112. /package/src/overlays/{Notification.vue → Notification/Notification.vue} +0 -0
@@ -0,0 +1,439 @@
1
+ # Form Page Pattern
2
+
3
+ Create or edit entity data using JsonSchemaForm for automatic form generation and validation.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ ┌─────────────────────────────────────────────────────────────┐
9
+ │ AdminLayout │
10
+ ├─────────────────────────────────────────────────────────────┤
11
+ │ Page Header (title + breadcrumbs) │
12
+ ├─────────────────────────────────────────────────────────────┤
13
+ │ Optional: Back Button │
14
+ ├─────────────────────────────────────────────────────────────┤
15
+ │ Form Container (lg:w-2/3 xl:w-1/2) │
16
+ │ ├── Section Title │
17
+ │ ├── Input Fields (auto-generated from schema) │
18
+ │ ├── Section Title │
19
+ │ ├── More Fields... │
20
+ │ └── Submit Button │
21
+ ├─────────────────────────────────────────────────────────────┤
22
+ │ Optional: Unsaved Changes Warning │
23
+ └─────────────────────────────────────────────────────────────┘
24
+ ```
25
+
26
+ ## CRM Template Reference
27
+
28
+ Based on: `ui/src/components/Templates/CRM/SettingsGeneral.vue`
29
+
30
+ ```vue
31
+ <!-- CRM Template Structure (raw @hotelinking/ui) -->
32
+ <uiWrapper :sidebar :topbar>
33
+ <uiViewHeader :pages="pages" title="Hotel Group Information" />
34
+
35
+ <div class="lg:w-2/3 xl:w-1/2">
36
+ <uiInput label="Group Name" :value="formData.groupName"
37
+ @inputChanged="handleInputChange('groupName', $event)" />
38
+
39
+ <uiTextArea label="Description" :value="formData.description"
40
+ @inputChanged="handleInputChange('description', $event)" />
41
+
42
+ <uiSectionTitle title="Corporate Headquarters"
43
+ description="Main office location." />
44
+
45
+ <div class="grid gap-x-4 md:grid-cols-2">
46
+ <uiInput label="City" :value="formData.city" />
47
+ <uiSelectV2 label="Country" :items="countries" />
48
+ </div>
49
+
50
+ <uiButton class="my-8">Save Changes</uiButton>
51
+ </div>
52
+ </uiWrapper>
53
+ ```
54
+
55
+ ## Enhanced Pattern (with @htlkg/*)
56
+
57
+ ### Astro Page
58
+
59
+ ```astro
60
+ ---
61
+ // src/pages/[brandId]/admin/config.astro
62
+ import Layout from '@/layouts/Layout.astro';
63
+ import BrandConfigForm from '@/components/admin/BrandConfigForm.vue';
64
+ import { getBrand } from '@/services/brand';
65
+
66
+ export const prerender = false;
67
+
68
+ const { brandId } = Astro.params;
69
+ const brand = await getBrand(Number(brandId));
70
+
71
+ const layoutProps = {
72
+ title: 'Brand Configuration',
73
+ breadcrumbs: [
74
+ { name: 'Brands', href: '/admin/brands' },
75
+ { name: brand.name, href: `/${brandId}/admin` },
76
+ { name: 'Configuration', current: true },
77
+ ],
78
+ };
79
+ ---
80
+
81
+ <Layout {...layoutProps}>
82
+ <BrandConfigForm client:load initialData={brand} brandId={brandId} />
83
+ </Layout>
84
+ ```
85
+
86
+ ### Vue Component with JsonSchemaForm
87
+
88
+ ```vue
89
+ <!-- src/components/admin/BrandConfigForm.vue -->
90
+ <script setup lang="ts">
91
+ import { ref, computed } from 'vue';
92
+ import { JsonSchemaForm } from '@htlkg/components/forms';
93
+ import { useJsonForm, useConfirmation, useNotifications } from '@htlkg/components/composables';
94
+ import { uiButton, uiSectionTitle } from '@hotelinking/ui';
95
+
96
+ interface Props {
97
+ initialData: Brand;
98
+ brandId: string;
99
+ }
100
+
101
+ const props = defineProps<Props>();
102
+ const emit = defineEmits<{
103
+ saved: [data: Brand];
104
+ }>();
105
+
106
+ // Form state with useJsonForm
107
+ const { formRef, values, isDirty, validate, reset, setValues } = useJsonForm({
108
+ initialValues: props.initialData,
109
+ confirmOnLeave: true,
110
+ });
111
+
112
+ // Confirmation dialog for reset
113
+ const resetConfirmation = useConfirmation();
114
+
115
+ // Notifications
116
+ const { notify } = useNotifications();
117
+
118
+ // JSON Schema for the form
119
+ const schema = {
120
+ type: 'object',
121
+ title: 'Brand Configuration',
122
+ properties: {
123
+ name: {
124
+ type: 'string',
125
+ title: 'Brand Name',
126
+ minLength: 1,
127
+ },
128
+ description: {
129
+ type: 'string',
130
+ title: 'Description',
131
+ minLength: 100, // Will use textarea
132
+ },
133
+ type: {
134
+ type: 'string',
135
+ title: 'Brand Type',
136
+ enum: ['hotel', 'resort', 'boutique', 'chain'],
137
+ },
138
+ totalProperties: {
139
+ type: 'number',
140
+ title: 'Total Properties',
141
+ minimum: 0,
142
+ },
143
+ totalRooms: {
144
+ type: 'number',
145
+ title: 'Total Rooms',
146
+ minimum: 0,
147
+ },
148
+ // Corporate section
149
+ headquartersAddress: {
150
+ type: 'string',
151
+ title: 'Headquarters Address',
152
+ },
153
+ city: {
154
+ type: 'string',
155
+ title: 'City',
156
+ },
157
+ country: {
158
+ type: 'string',
159
+ title: 'Country',
160
+ enum: ['US', 'UK', 'ES', 'FR', 'DE'],
161
+ },
162
+ corporatePhone: {
163
+ type: 'string',
164
+ title: 'Corporate Phone',
165
+ },
166
+ corporateEmail: {
167
+ type: 'string',
168
+ title: 'Corporate Email',
169
+ format: 'email',
170
+ },
171
+ website: {
172
+ type: 'string',
173
+ title: 'Website',
174
+ format: 'uri',
175
+ },
176
+ // Regional settings
177
+ timezone: {
178
+ type: 'string',
179
+ title: 'Timezone',
180
+ enum: ['UTC', 'EST', 'PST', 'CET'],
181
+ },
182
+ language: {
183
+ type: 'string',
184
+ title: 'Language',
185
+ enum: ['en', 'es', 'fr', 'de'],
186
+ },
187
+ currency: {
188
+ type: 'string',
189
+ title: 'Currency',
190
+ enum: ['USD', 'EUR', 'GBP'],
191
+ },
192
+ },
193
+ required: ['name', 'type'],
194
+ };
195
+
196
+ // UI Schema for customization
197
+ const uiSchema = {
198
+ description: {
199
+ 'ui:widget': 'textarea',
200
+ 'ui:placeholder': 'Enter brand description...',
201
+ },
202
+ corporatePhone: {
203
+ 'ui:placeholder': '+1 (000) 000-0000',
204
+ },
205
+ website: {
206
+ 'ui:placeholder': 'https://www.example.com',
207
+ },
208
+ };
209
+
210
+ // Submit handler
211
+ async function handleSubmit(formData: Brand) {
212
+ try {
213
+ const response = await saveBrand(props.brandId, formData);
214
+ notify({ type: 'success', message: 'Configuration saved successfully' });
215
+ setValues(response); // Update initial values after save
216
+ emit('saved', response);
217
+ } catch (error) {
218
+ notify({ type: 'error', message: 'Failed to save configuration' });
219
+ }
220
+ }
221
+
222
+ // Reset handler with confirmation
223
+ function handleReset() {
224
+ resetConfirmation.confirm(
225
+ 'Are you sure you want to reset all changes?',
226
+ () => {
227
+ reset();
228
+ notify({ type: 'info', message: 'Form reset to original values' });
229
+ }
230
+ );
231
+ }
232
+
233
+ // Loading state
234
+ const isLoading = ref(false);
235
+ </script>
236
+
237
+ <template>
238
+ <div class="lg:w-2/3 xl:w-1/2">
239
+ <!-- Unsaved changes indicator -->
240
+ <div v-if="isDirty" class="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
241
+ <p class="text-sm text-yellow-800">You have unsaved changes</p>
242
+ </div>
243
+
244
+ <!-- Basic Information Section -->
245
+ <uiSectionTitle
246
+ class="mb-4"
247
+ title="Basic Information"
248
+ description="Brand identification and details."
249
+ />
250
+
251
+ <JsonSchemaForm
252
+ ref="formRef"
253
+ v-model="values"
254
+ :schema="schema"
255
+ :ui-schema="uiSchema"
256
+ :loading="isLoading"
257
+ @submit="handleSubmit"
258
+ >
259
+ <!-- Custom actions slot -->
260
+ <template #actions>
261
+ <div class="flex gap-4 pt-6 border-t">
262
+ <uiButton
263
+ type="button"
264
+ color="gray"
265
+ :disabled="!isDirty"
266
+ @click="handleReset"
267
+ >
268
+ Reset
269
+ </uiButton>
270
+ <uiButton
271
+ type="submit"
272
+ color="primary"
273
+ :loading="isLoading"
274
+ :disabled="!isDirty"
275
+ >
276
+ Save Changes
277
+ </uiButton>
278
+ </div>
279
+ </template>
280
+ </JsonSchemaForm>
281
+
282
+ <!-- Reset Confirmation Modal -->
283
+ <Modal
284
+ :open="resetConfirmation.isOpen.value"
285
+ title="Reset Form"
286
+ @close="resetConfirmation.cancel"
287
+ >
288
+ <p>{{ resetConfirmation.message.value }}</p>
289
+ <template #actions>
290
+ <uiButton color="gray" @click="resetConfirmation.cancel">Cancel</uiButton>
291
+ <uiButton color="red" @click="resetConfirmation.execute">Reset</uiButton>
292
+ </template>
293
+ </Modal>
294
+ </div>
295
+ </template>
296
+ ```
297
+
298
+ ## Key Components
299
+
300
+ ### JsonSchemaForm
301
+
302
+ The primary form component that auto-generates UI from JSON Schema.
303
+
304
+ ```typescript
305
+ import { JsonSchemaForm } from '@htlkg/components/forms';
306
+ ```
307
+
308
+ **Props:**
309
+ - `schema` - JSON Schema definition
310
+ - `modelValue` - Form data (v-model)
311
+ - `uiSchema` - UI customizations
312
+ - `loading` - Loading state
313
+
314
+ **Events:**
315
+ - `submit` - Form submitted with valid data
316
+ - `validation-error` - Validation failed
317
+
318
+ **Exposed Methods:**
319
+ - `validate()` - Trigger validation
320
+ - `reset()` - Reset form
321
+
322
+ ### useJsonForm Composable
323
+
324
+ Wraps JsonSchemaForm with additional features.
325
+
326
+ ```typescript
327
+ import { useJsonForm } from '@htlkg/components/composables';
328
+
329
+ const {
330
+ formRef, // Ref to JsonSchemaForm instance
331
+ values, // Form data (reactive)
332
+ isDirty, // Has unsaved changes
333
+ validate, // Trigger validation
334
+ reset, // Reset to initial values
335
+ setValues, // Update initial values (after save)
336
+ } = useJsonForm({
337
+ initialValues: props.data,
338
+ confirmOnLeave: true, // Warn before leaving with unsaved changes
339
+ });
340
+ ```
341
+
342
+ ### JSON Schema Field Types
343
+
344
+ ```typescript
345
+ const schema = {
346
+ properties: {
347
+ // Text input
348
+ name: { type: 'string', title: 'Name', minLength: 1 },
349
+
350
+ // Email input (auto-detected from format)
351
+ email: { type: 'string', title: 'Email', format: 'email' },
352
+
353
+ // URL input
354
+ website: { type: 'string', title: 'Website', format: 'uri' },
355
+
356
+ // Number input
357
+ count: { type: 'number', title: 'Count', minimum: 0, maximum: 100 },
358
+
359
+ // Select dropdown (from enum)
360
+ status: { type: 'string', title: 'Status', enum: ['active', 'inactive'] },
361
+
362
+ // Boolean toggle
363
+ enabled: { type: 'boolean', title: 'Enabled' },
364
+
365
+ // Textarea (auto for minLength > 100)
366
+ description: { type: 'string', title: 'Description', minLength: 100 },
367
+
368
+ // Password (via uiSchema)
369
+ password: { type: 'string', title: 'Password' },
370
+
371
+ // Array of strings
372
+ tags: { type: 'array', title: 'Tags', items: { type: 'string' } },
373
+ },
374
+ required: ['name', 'email'], // Required fields
375
+ };
376
+ ```
377
+
378
+ ### UI Schema Customizations
379
+
380
+ ```typescript
381
+ const uiSchema = {
382
+ password: { 'ui:widget': 'password' },
383
+ description: { 'ui:widget': 'textarea' },
384
+ volume: { 'ui:widget': 'slider' },
385
+ field: {
386
+ 'ui:placeholder': 'Enter value...',
387
+ 'ui:autocomplete': 'email',
388
+ },
389
+ };
390
+ ```
391
+
392
+ ## Form Sections
393
+
394
+ For long forms, use section titles to group related fields:
395
+
396
+ ```vue
397
+ <template>
398
+ <div class="lg:w-2/3 xl:w-1/2">
399
+ <!-- Section 1 -->
400
+ <uiSectionTitle title="Basic Info" description="..." />
401
+ <!-- Fields -->
402
+
403
+ <!-- Section 2 -->
404
+ <uiSectionTitle title="Contact" description="..." class="mt-8" />
405
+ <!-- Fields -->
406
+
407
+ <!-- Submit -->
408
+ <uiButton class="my-8">Save</uiButton>
409
+ </div>
410
+ </template>
411
+ ```
412
+
413
+ ## Grid Layouts for Fields
414
+
415
+ Use Tailwind grid for side-by-side fields:
416
+
417
+ ```vue
418
+ <div class="grid gap-x-4 md:grid-cols-2 mb-4">
419
+ <!-- Two columns on medium+ screens -->
420
+ </div>
421
+
422
+ <div class="grid gap-x-4 md:grid-cols-3 mb-4">
423
+ <!-- Three columns on medium+ screens -->
424
+ </div>
425
+ ```
426
+
427
+ ## Checklist
428
+
429
+ - [ ] Astro page with server-side data fetch
430
+ - [ ] Vue component with `JsonSchemaForm`
431
+ - [ ] JSON Schema with validation rules
432
+ - [ ] UI Schema for customizations (if needed)
433
+ - [ ] `useJsonForm` for isDirty, confirmOnLeave
434
+ - [ ] Section titles for grouped fields
435
+ - [ ] Grid layouts for field arrangement
436
+ - [ ] Submit and Reset buttons
437
+ - [ ] `useConfirmation` for reset dialog
438
+ - [ ] `useNotifications` for success/error messages
439
+ - [ ] Loading states during save