qdadm 1.1.6 → 1.2.0

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": "qdadm",
3
- "version": "1.1.6",
3
+ "version": "1.2.0",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -2,23 +2,16 @@
2
2
  /**
3
3
  * FormTab - Normalized tab header component
4
4
  *
5
- * Provides consistent tab headers with optional icon, count badge, and visibility control.
5
+ * @deprecated Use FieldGroups with layout="tabs" instead. FieldGroups now supports
6
+ * icon, badge, count, visible, and disabled options on groups.
6
7
  *
7
- * Props:
8
- * - value: Tab identifier (required)
9
- * - label: Display text (required)
10
- * - icon: PrimeIcon class (e.g., 'pi-cog')
11
- * - count: Number to display as badge
12
- * - badge: Custom badge text/value
13
- * - badgeSeverity: Badge color ('secondary', 'info', 'success', 'warn', 'danger')
14
- * - visible: Show/hide tab (default: true)
15
- * - disabled: Disable tab interaction
8
+ * ```ts
9
+ * // Before (deprecated)
10
+ * <FormTab value="general" label="General" icon="pi-cog" :count="5" />
16
11
  *
17
- * Usage:
18
- * <FormTab value="general" label="General" icon="pi-cog" />
19
- * <FormTab value="items" label="Items" :count="5" />
20
- * <FormTab value="errors" label="Errors" :count="errors.length" badge-severity="danger" />
21
- * <FormTab value="advanced" label="Advanced" :visible="isEdit" />
12
+ * // After (recommended)
13
+ * form.group('general', ['field1'], { label: 'General', icon: 'cog', count: 5 })
14
+ * ```
22
15
  */
23
16
 
24
17
  import { type PropType } from 'vue'
@@ -2,22 +2,28 @@
2
2
  /**
3
3
  * FormTabs - Normalized tab container for forms
4
4
  *
5
- * Provides consistent styling and behavior for form tabs.
6
- * Works with FormTab component for individual tabs.
5
+ * @deprecated Use FieldGroups with layout="tabs" instead. FieldGroups now supports
6
+ * icon, badge, count, visible, and disabled options on groups.
7
7
  *
8
- * Usage:
9
- * <FormTabs v-model="activeTab" @update:modelValue="onTabChange">
10
- * <template #tabs>
11
- * <FormTab value="general" label="General" icon="pi-cog" />
12
- * <FormTab value="items" label="Items" icon="pi-list" :count="items.length" />
13
- * <FormTab value="advanced" label="Advanced" :visible="isEdit" />
14
- * </template>
15
- * <template #panels>
16
- * <TabPanel value="general">...</TabPanel>
17
- * <TabPanel value="items">...</TabPanel>
18
- * <TabPanel value="advanced">...</TabPanel>
19
- * </template>
20
- * </FormTabs>
8
+ * ```vue
9
+ * <!-- Before (deprecated) -->
10
+ * <FormTabs v-model="activeTab">
11
+ * <template #tabs>
12
+ * <FormTab value="general" label="General" icon="pi-cog" />
13
+ * </template>
14
+ * <template #panels>
15
+ * <TabPanel value="general">...</TabPanel>
16
+ * </template>
17
+ * </FormTabs>
18
+ *
19
+ * <!-- After (recommended) -->
20
+ * form.group('general', ['field1', 'field2'], { label: 'General', icon: 'cog' })
21
+ * <FieldGroups :groups="form.groups.value" layout="tabs">
22
+ * <template #field="{ field }">
23
+ * <FormField :field="field" />
24
+ * </template>
25
+ * </FieldGroups>
26
+ * ```
21
27
  */
22
28
 
23
29
  import Tabs from 'primevue/tabs'
@@ -19,13 +19,16 @@ export { default as DefaultFooter } from './layout/defaults/DefaultFooter.vue'
19
19
  export { default as DefaultUserInfo } from './layout/defaults/DefaultUserInfo.vue'
20
20
  export { default as DefaultBreadcrumb } from './layout/defaults/DefaultBreadcrumb.vue'
21
21
 
22
- // Forms
23
- export { default as FormPage } from './forms/FormPage.vue'
24
- export { default as FormField } from './forms/FormField.vue'
25
- export { default as FormInput } from './forms/FormInput.vue'
26
- export { default as FormActions } from './forms/FormActions.vue'
27
- export { default as FormTabs } from './forms/FormTabs.vue'
28
- export { default as FormTab } from './forms/FormTab.vue'
22
+ // Item (shared between edit and show)
23
+ export { default as FieldGroups } from './item/FieldGroups.vue'
24
+
25
+ // Edit (form pages)
26
+ export { default as FormPage } from './edit/FormPage.vue'
27
+ export { default as FormField } from './edit/FormField.vue'
28
+ export { default as FormInput } from './edit/FormInput.vue'
29
+ export { default as FormActions } from './edit/FormActions.vue'
30
+ export { default as FormTabs } from './edit/FormTabs.vue' // Deprecated: use FieldGroups with layout="tabs"
31
+ export { default as FormTab } from './edit/FormTab.vue' // Deprecated: use FieldGroups with layout="tabs"
29
32
 
30
33
  // Show (read-only detail pages)
31
34
  export { default as ShowPage } from './show/ShowPage.vue'
@@ -0,0 +1,388 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * FieldGroups - Generic field group renderer with configurable layouts
4
+ *
5
+ * Shared component for both edit (form) and show pages.
6
+ * Uses slots for field rendering to support different field components.
7
+ *
8
+ * Supports multiple layout modes:
9
+ * - flat: Simple sections with headers
10
+ * - sections: Fieldset-style sections
11
+ * - cards: Each group in a card
12
+ * - tabs: TabView for top-level groups
13
+ * - accordion: Collapsible panels
14
+ *
15
+ * ```vue
16
+ * <FieldGroups :groups="groups" :data="data" layout="tabs">
17
+ * <template #field="{ field, value }">
18
+ * <FormField :field="field" v-model="data[field.name]" />
19
+ * </template>
20
+ * </FieldGroups>
21
+ * ```
22
+ */
23
+ import { computed, type PropType } from 'vue'
24
+ import Tabs from 'primevue/tabs'
25
+ import TabList from 'primevue/tablist'
26
+ import Tab from 'primevue/tab'
27
+ import TabPanels from 'primevue/tabpanels'
28
+ import TabPanel from 'primevue/tabpanel'
29
+ import Accordion from 'primevue/accordion'
30
+ import AccordionPanel from 'primevue/accordionpanel'
31
+ import AccordionHeader from 'primevue/accordionheader'
32
+ import AccordionContent from 'primevue/accordioncontent'
33
+ import Fieldset from 'primevue/fieldset'
34
+ import Card from 'primevue/card'
35
+ import Tag from 'primevue/tag'
36
+
37
+ type BadgeSeverity = 'secondary' | 'info' | 'success' | 'warn' | 'danger' | 'contrast'
38
+
39
+ /**
40
+ * Base field config interface
41
+ */
42
+ interface BaseFieldConfig {
43
+ name: string
44
+ type: string
45
+ label: string
46
+ [key: string]: unknown
47
+ }
48
+
49
+ /**
50
+ * Field group structure
51
+ */
52
+ interface FieldGroup<T extends BaseFieldConfig = BaseFieldConfig> {
53
+ name: string
54
+ label: string
55
+ fields: T[]
56
+ children: FieldGroup<T>[]
57
+ parent?: string
58
+ // Tab/accordion options
59
+ icon?: string
60
+ badge?: string | number
61
+ badgeSeverity?: BadgeSeverity
62
+ count?: number
63
+ visible?: boolean
64
+ disabled?: boolean
65
+ }
66
+
67
+ export type LayoutMode = 'flat' | 'sections' | 'cards' | 'tabs' | 'accordion'
68
+
69
+ const props = defineProps({
70
+ groups: {
71
+ type: Array as PropType<FieldGroup[]>,
72
+ required: true,
73
+ },
74
+ data: {
75
+ type: Object as PropType<Record<string, unknown> | null>,
76
+ default: null,
77
+ },
78
+ layout: {
79
+ type: String as PropType<LayoutMode>,
80
+ default: 'flat',
81
+ },
82
+ // Nested layout (for children groups)
83
+ childLayout: {
84
+ type: String as PropType<LayoutMode>,
85
+ default: 'sections',
86
+ },
87
+ })
88
+
89
+ // Expose slots for field rendering
90
+ defineSlots<{
91
+ field: (props: { field: BaseFieldConfig; value: unknown; groupName: string }) => unknown
92
+ // Optional: custom group header
93
+ 'group-header'?: (props: { group: FieldGroup; layout: LayoutMode }) => unknown
94
+ // Optional: custom group content wrapper
95
+ 'group-content'?: (props: { group: FieldGroup; layout: LayoutMode }) => unknown
96
+ }>()
97
+
98
+ // Filter out default group for labeling purposes and hidden groups
99
+ const displayGroups = computed(() => {
100
+ return props.groups.filter((g) => {
101
+ // Filter by visibility (default to true)
102
+ if (g.visible === false) return false
103
+ // Filter out empty default groups
104
+ return g.name !== '_default' || g.fields.length > 0
105
+ })
106
+ })
107
+
108
+ // Get value from data
109
+ function getValue(fieldName: string): unknown {
110
+ return props.data?.[fieldName]
111
+ }
112
+
113
+ // Normalize icon class
114
+ function getIconClass(icon: string | undefined): string | null {
115
+ if (!icon) return null
116
+ // Already has 'pi' prefix
117
+ if (icon.startsWith('pi')) {
118
+ return icon.includes(' ') ? icon : `pi ${icon}`
119
+ }
120
+ // Just icon name
121
+ return `pi pi-${icon}`
122
+ }
123
+
124
+ // Get badge value to display
125
+ function getBadgeValue(group: FieldGroup): string | number | null {
126
+ if (group.badge !== undefined && group.badge !== null) return group.badge
127
+ if (group.count !== undefined && group.count !== null && group.count > 0) return group.count
128
+ return null
129
+ }
130
+ </script>
131
+
132
+ <template>
133
+ <div class="field-groups" :class="`field-groups--${layout}`">
134
+ <!-- Flat layout: simple sections with optional headers -->
135
+ <template v-if="layout === 'flat'">
136
+ <div v-for="group in displayGroups" :key="group.name" class="field-group">
137
+ <slot name="group-header" :group="group" :layout="layout">
138
+ <h3 v-if="group.label && group.name !== '_default'" class="field-group-header">
139
+ {{ group.label }}
140
+ </h3>
141
+ </slot>
142
+ <div class="field-group-fields">
143
+ <template v-for="field in group.fields" :key="field.name">
144
+ <slot
145
+ name="field"
146
+ :field="field"
147
+ :value="getValue(field.name)"
148
+ :group-name="group.name"
149
+ />
150
+ </template>
151
+ </div>
152
+ <!-- Recursive children -->
153
+ <FieldGroups
154
+ v-if="group.children.length > 0"
155
+ :groups="group.children"
156
+ :data="data"
157
+ :layout="childLayout"
158
+ >
159
+ <template #field="slotProps">
160
+ <slot name="field" v-bind="slotProps" />
161
+ </template>
162
+ </FieldGroups>
163
+ </div>
164
+ </template>
165
+
166
+ <!-- Sections layout: Fieldset style -->
167
+ <template v-else-if="layout === 'sections'">
168
+ <Fieldset
169
+ v-for="group in displayGroups"
170
+ :key="group.name"
171
+ :legend="group.label || undefined"
172
+ :toggleable="false"
173
+ >
174
+ <div class="field-group-fields">
175
+ <template v-for="field in group.fields" :key="field.name">
176
+ <slot
177
+ name="field"
178
+ :field="field"
179
+ :value="getValue(field.name)"
180
+ :group-name="group.name"
181
+ />
182
+ </template>
183
+ </div>
184
+ <!-- Recursive children -->
185
+ <FieldGroups
186
+ v-if="group.children.length > 0"
187
+ :groups="group.children"
188
+ :data="data"
189
+ :layout="childLayout"
190
+ >
191
+ <template #field="slotProps">
192
+ <slot name="field" v-bind="slotProps" />
193
+ </template>
194
+ </FieldGroups>
195
+ </Fieldset>
196
+ </template>
197
+
198
+ <!-- Cards layout -->
199
+ <template v-else-if="layout === 'cards'">
200
+ <Card v-for="group in displayGroups" :key="group.name">
201
+ <template #title>{{ group.label }}</template>
202
+ <template #content>
203
+ <div class="field-group-fields">
204
+ <template v-for="field in group.fields" :key="field.name">
205
+ <slot
206
+ name="field"
207
+ :field="field"
208
+ :value="getValue(field.name)"
209
+ :group-name="group.name"
210
+ />
211
+ </template>
212
+ </div>
213
+ <!-- Recursive children -->
214
+ <FieldGroups
215
+ v-if="group.children.length > 0"
216
+ :groups="group.children"
217
+ :data="data"
218
+ :layout="childLayout"
219
+ >
220
+ <template #field="slotProps">
221
+ <slot name="field" v-bind="slotProps" />
222
+ </template>
223
+ </FieldGroups>
224
+ </template>
225
+ </Card>
226
+ </template>
227
+
228
+ <!-- Tabs layout -->
229
+ <template v-else-if="layout === 'tabs'">
230
+ <Tabs :value="displayGroups[0]?.name || '0'">
231
+ <TabList>
232
+ <Tab
233
+ v-for="group in displayGroups"
234
+ :key="group.name"
235
+ :value="group.name"
236
+ :disabled="group.disabled"
237
+ >
238
+ <i v-if="group.icon" :class="getIconClass(group.icon)" class="tab-icon" />
239
+ <span class="tab-label">{{ group.label }}</span>
240
+ <Tag
241
+ v-if="getBadgeValue(group) !== null"
242
+ :value="String(getBadgeValue(group))"
243
+ :severity="group.badgeSeverity || 'secondary'"
244
+ class="tab-badge"
245
+ />
246
+ </Tab>
247
+ </TabList>
248
+ <TabPanels>
249
+ <TabPanel v-for="group in displayGroups" :key="group.name" :value="group.name">
250
+ <div class="field-group-fields">
251
+ <template v-for="field in group.fields" :key="field.name">
252
+ <slot
253
+ name="field"
254
+ :field="field"
255
+ :value="getValue(field.name)"
256
+ :group-name="group.name"
257
+ />
258
+ </template>
259
+ </div>
260
+ <!-- Recursive children -->
261
+ <FieldGroups
262
+ v-if="group.children.length > 0"
263
+ :groups="group.children"
264
+ :data="data"
265
+ :layout="childLayout"
266
+ >
267
+ <template #field="slotProps">
268
+ <slot name="field" v-bind="slotProps" />
269
+ </template>
270
+ </FieldGroups>
271
+ </TabPanel>
272
+ </TabPanels>
273
+ </Tabs>
274
+ </template>
275
+
276
+ <!-- Accordion layout -->
277
+ <template v-else-if="layout === 'accordion'">
278
+ <Accordion :value="[displayGroups[0]?.name || '0']" multiple>
279
+ <AccordionPanel
280
+ v-for="group in displayGroups"
281
+ :key="group.name"
282
+ :value="group.name"
283
+ :disabled="group.disabled"
284
+ >
285
+ <AccordionHeader>
286
+ <i v-if="group.icon" :class="getIconClass(group.icon)" class="accordion-icon" />
287
+ <span class="accordion-label">{{ group.label }}</span>
288
+ <Tag
289
+ v-if="getBadgeValue(group) !== null"
290
+ :value="String(getBadgeValue(group))"
291
+ :severity="group.badgeSeverity || 'secondary'"
292
+ class="accordion-badge"
293
+ />
294
+ </AccordionHeader>
295
+ <AccordionContent>
296
+ <div class="field-group-fields">
297
+ <template v-for="field in group.fields" :key="field.name">
298
+ <slot
299
+ name="field"
300
+ :field="field"
301
+ :value="getValue(field.name)"
302
+ :group-name="group.name"
303
+ />
304
+ </template>
305
+ </div>
306
+ <!-- Recursive children -->
307
+ <FieldGroups
308
+ v-if="group.children.length > 0"
309
+ :groups="group.children"
310
+ :data="data"
311
+ :layout="childLayout"
312
+ >
313
+ <template #field="slotProps">
314
+ <slot name="field" v-bind="slotProps" />
315
+ </template>
316
+ </FieldGroups>
317
+ </AccordionContent>
318
+ </AccordionPanel>
319
+ </Accordion>
320
+ </template>
321
+ </div>
322
+ </template>
323
+
324
+ <style scoped>
325
+ .field-groups {
326
+ display: flex;
327
+ flex-direction: column;
328
+ gap: 1rem;
329
+ }
330
+
331
+ .field-group {
332
+ display: flex;
333
+ flex-direction: column;
334
+ gap: 0.5rem;
335
+ }
336
+
337
+ .field-group-header {
338
+ font-size: 1rem;
339
+ font-weight: 600;
340
+ margin: 0 0 0.5rem 0;
341
+ padding-bottom: 0.5rem;
342
+ border-bottom: 1px solid var(--p-surface-200, #e2e8f0);
343
+ color: var(--p-text-color);
344
+ }
345
+
346
+ .field-group-fields {
347
+ display: flex;
348
+ flex-direction: column;
349
+ gap: 0.75rem;
350
+ }
351
+
352
+ /* Cards layout spacing */
353
+ .field-groups--cards {
354
+ gap: 1rem;
355
+ }
356
+
357
+ /* Tabs and accordion panels padding */
358
+ .field-groups--tabs :deep(.p-tabpanel),
359
+ .field-groups--accordion :deep(.p-accordioncontent-content) {
360
+ padding-top: 1rem;
361
+ }
362
+
363
+ /* Tab icon and badge styling */
364
+ .tab-icon {
365
+ margin-right: 0.5rem;
366
+ }
367
+
368
+ .tab-badge {
369
+ margin-left: 0.5rem;
370
+ font-size: 0.7rem;
371
+ padding: 0.15rem 0.4rem;
372
+ min-width: 1.25rem;
373
+ text-align: center;
374
+ }
375
+
376
+ /* Accordion icon and badge styling */
377
+ .accordion-icon {
378
+ margin-right: 0.5rem;
379
+ }
380
+
381
+ .accordion-badge {
382
+ margin-left: 0.5rem;
383
+ font-size: 0.7rem;
384
+ padding: 0.15rem 0.4rem;
385
+ min-width: 1.25rem;
386
+ text-align: center;
387
+ }
388
+ </style>
@@ -15,7 +15,7 @@
15
15
  * - qdadmFormEmit: { save, saveAndClose, cancel, delete }
16
16
  */
17
17
  import { inject, computed, type Ref } from 'vue'
18
- import FormActions from '../../forms/FormActions.vue'
18
+ import FormActions from '../../edit/FormActions.vue'
19
19
 
20
20
  interface FormState {
21
21
  isEdit?: boolean
@@ -42,6 +42,8 @@ import PageHeader from '../layout/PageHeader.vue'
42
42
  import Card from 'primevue/card'
43
43
  import Button from 'primevue/button'
44
44
  import Message from 'primevue/message'
45
+ import FieldGroups from '../item/FieldGroups.vue'
46
+ import ShowField from './ShowField.vue'
45
47
 
46
48
  /**
47
49
  * Page title parts for PageHeader
@@ -83,6 +85,22 @@ interface FetchError {
83
85
  detail?: string
84
86
  }
85
87
 
88
+ /**
89
+ * Field group structure
90
+ */
91
+ interface FieldGroup {
92
+ name: string
93
+ label: string
94
+ fields: ResolvedFieldConfig[]
95
+ children: FieldGroup[]
96
+ parent?: string
97
+ }
98
+
99
+ /**
100
+ * Layout mode for groups
101
+ */
102
+ type LayoutMode = 'flat' | 'sections' | 'cards' | 'tabs' | 'accordion'
103
+
86
104
  const props = defineProps({
87
105
  // State
88
106
  loading: { type: Boolean, default: false },
@@ -94,6 +112,9 @@ const props = defineProps({
94
112
  // Fields (for auto-rendering - optional, can use #fields slot instead)
95
113
  fields: { type: Array as PropType<ResolvedFieldConfig[]>, default: () => [] },
96
114
 
115
+ // Groups (hierarchical field organization)
116
+ groups: { type: Array as PropType<FieldGroup[]>, default: () => [] },
117
+
97
118
  // Data (entity data for field values)
98
119
  data: { type: Object as PropType<Record<string, unknown> | null>, default: null },
99
120
 
@@ -109,7 +130,13 @@ const props = defineProps({
109
130
  horizontalFields: { type: Boolean, default: true },
110
131
  labelWidth: { type: String, default: '140px' },
111
132
  // Media zone options
112
- mediaWidth: { type: String, default: '200px' }
133
+ mediaWidth: { type: String, default: '200px' },
134
+
135
+ // Group layout mode: flat, sections, cards, tabs, accordion
136
+ layout: { type: String as PropType<LayoutMode>, default: 'flat' },
137
+
138
+ // Child group layout mode (for nested groups)
139
+ childLayout: { type: String as PropType<LayoutMode>, default: 'sections' }
113
140
  })
114
141
 
115
142
  // Check if media slot is used
@@ -119,11 +146,19 @@ const slots = defineSlots<{
119
146
  'header-actions'?: () => unknown
120
147
  media?: () => unknown
121
148
  fields?: () => unknown
149
+ groups?: () => unknown
122
150
  footer?: () => unknown
123
151
  error?: (props: { error: unknown }) => unknown
124
152
  loading?: () => unknown
125
153
  }>()
126
154
 
155
+ // Determine if we should use group rendering
156
+ const useGroupLayout = computed(() => {
157
+ // Use groups if: layout is not flat, or groups are explicitly defined (not just _default)
158
+ const hasRealGroups = props.groups.length > 0 && !(props.groups.length === 1 && props.groups[0]?.name === '_default')
159
+ return hasRealGroups && props.layout !== 'flat'
160
+ })
161
+
127
162
  const emit = defineEmits<{
128
163
  (e: 'edit'): void
129
164
  (e: 'delete'): void
@@ -207,9 +242,32 @@ const fetchErrorMessage = computed<string | null>(() => {
207
242
  <slot name="media" />
208
243
  </div>
209
244
 
210
- <!-- Fields zone -->
211
- <div class="show-fields" :class="{ 'show-fields--horizontal': horizontalFields }">
212
- <slot name="fields" />
245
+ <!-- Fields/Groups zone -->
246
+ <div class="show-fields" :class="{ 'show-fields--horizontal': horizontalFields && !useGroupLayout }">
247
+ <!-- Group layout mode -->
248
+ <template v-if="useGroupLayout">
249
+ <slot name="groups">
250
+ <FieldGroups
251
+ :groups="groups"
252
+ :data="data"
253
+ :layout="layout"
254
+ :child-layout="childLayout"
255
+ >
256
+ <template #field="{ field, value }">
257
+ <ShowField
258
+ :field="field"
259
+ :value="value as string | number | boolean | Date | Record<string, unknown> | unknown[]"
260
+ :horizontal="horizontalFields"
261
+ :label-width="labelWidth"
262
+ />
263
+ </template>
264
+ </FieldGroups>
265
+ </slot>
266
+ </template>
267
+ <!-- Flat fields mode (default) -->
268
+ <template v-else>
269
+ <slot name="fields" />
270
+ </template>
213
271
  </div>
214
272
  </div>
215
273
 
@@ -245,9 +303,32 @@ const fetchErrorMessage = computed<string | null>(() => {
245
303
  <slot name="media" />
246
304
  </div>
247
305
 
248
- <!-- Fields zone -->
249
- <div class="show-fields" :class="{ 'show-fields--horizontal': horizontalFields }">
250
- <slot name="fields" />
306
+ <!-- Fields/Groups zone -->
307
+ <div class="show-fields" :class="{ 'show-fields--horizontal': horizontalFields && !useGroupLayout }">
308
+ <!-- Group layout mode -->
309
+ <template v-if="useGroupLayout">
310
+ <slot name="groups">
311
+ <FieldGroups
312
+ :groups="groups"
313
+ :data="data"
314
+ :layout="layout"
315
+ :child-layout="childLayout"
316
+ >
317
+ <template #field="{ field, value }">
318
+ <ShowField
319
+ :field="field"
320
+ :value="value as string | number | boolean | Date | Record<string, unknown> | unknown[]"
321
+ :horizontal="horizontalFields"
322
+ :label-width="labelWidth"
323
+ />
324
+ </template>
325
+ </FieldGroups>
326
+ </slot>
327
+ </template>
328
+ <!-- Flat fields mode (default) -->
329
+ <template v-else>
330
+ <slot name="fields" />
331
+ </template>
251
332
  </div>
252
333
  </div>
253
334
 
@@ -132,3 +132,17 @@ export {
132
132
  type UserRecord,
133
133
  } from './useUserImpersonator'
134
134
  export { useCurrentEntity, type UseCurrentEntityReturn } from './useCurrentEntity'
135
+ export {
136
+ useFieldManager,
137
+ snakeCaseToTitle,
138
+ type BaseFieldDefinition,
139
+ type ResolvedFieldConfig as FieldManagerFieldConfig,
140
+ type FieldGroup,
141
+ type GroupDefinition,
142
+ type GroupOptions,
143
+ type GenerateFieldsOptions as FieldManagerGenerateOptions,
144
+ type AddFieldOptions as FieldManagerAddOptions,
145
+ type MoveFieldPosition,
146
+ type UseFieldManagerOptions,
147
+ type UseFieldManagerReturn,
148
+ } from './useFieldManager'