qdadm 0.16.0 → 0.18.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/README.md +153 -1
- package/package.json +15 -2
- package/src/components/forms/FormField.vue +64 -6
- package/src/components/forms/FormPage.vue +276 -0
- package/src/components/index.js +11 -0
- package/src/components/layout/BaseLayout.vue +183 -0
- package/src/components/layout/DashboardLayout.vue +100 -0
- package/src/components/layout/FormLayout.vue +261 -0
- package/src/components/layout/ListLayout.vue +334 -0
- package/src/components/layout/Zone.vue +165 -0
- package/src/components/layout/defaults/DefaultBreadcrumb.vue +140 -0
- package/src/components/layout/defaults/DefaultFooter.vue +56 -0
- package/src/components/layout/defaults/DefaultFormActions.vue +53 -0
- package/src/components/layout/defaults/DefaultHeader.vue +69 -0
- package/src/components/layout/defaults/DefaultMenu.vue +197 -0
- package/src/components/layout/defaults/DefaultPagination.vue +79 -0
- package/src/components/layout/defaults/DefaultTable.vue +130 -0
- package/src/components/layout/defaults/DefaultToaster.vue +16 -0
- package/src/components/layout/defaults/DefaultUserInfo.vue +96 -0
- package/src/components/layout/defaults/index.js +17 -0
- package/src/composables/index.js +6 -6
- package/src/composables/useForm.js +135 -0
- package/src/composables/useFormPageBuilder.js +1154 -0
- package/src/composables/useHooks.js +53 -0
- package/src/composables/useLayoutResolver.js +260 -0
- package/src/composables/useListPageBuilder.js +336 -52
- package/src/composables/useNavigation.js +38 -2
- package/src/composables/useSignals.js +49 -0
- package/src/composables/useZoneRegistry.js +162 -0
- package/src/core/bundles.js +406 -0
- package/src/core/decorator.js +322 -0
- package/src/core/extension.js +386 -0
- package/src/core/index.js +28 -0
- package/src/entity/EntityManager.js +359 -16
- package/src/entity/auth/AuthAdapter.js +184 -0
- package/src/entity/auth/PermissiveAdapter.js +64 -0
- package/src/entity/auth/RoleHierarchy.js +153 -0
- package/src/entity/auth/SecurityChecker.js +167 -0
- package/src/entity/auth/index.js +18 -0
- package/src/entity/index.js +3 -0
- package/src/entity/storage/MockApiStorage.js +349 -0
- package/src/entity/storage/SdkStorage.js +478 -0
- package/src/entity/storage/index.js +2 -0
- package/src/hooks/HookRegistry.js +411 -0
- package/src/hooks/index.js +12 -0
- package/src/index.js +13 -0
- package/src/kernel/Kernel.js +206 -5
- package/src/kernel/SignalBus.js +180 -0
- package/src/kernel/index.js +7 -0
- package/src/module/moduleRegistry.js +155 -28
- package/src/orchestrator/Orchestrator.js +73 -1
- package/src/zones/ZoneRegistry.js +828 -0
- package/src/zones/index.js +16 -0
- package/src/zones/zones.js +189 -0
- package/src/composables/useEntityTitle.js +0 -121
- package/src/composables/useManager.js +0 -20
- package/src/composables/usePageBuilder.js +0 -334
- package/src/composables/useStatus.js +0 -146
- package/src/composables/useSubEditor.js +0 -165
- package/src/composables/useTabSync.js +0 -110
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* BaseLayout - Root of the 3-level layout inheritance chain
|
|
4
|
+
*
|
|
5
|
+
* Defines all standard zones for the admin application:
|
|
6
|
+
* - header: Top bar with branding
|
|
7
|
+
* - menu: Navigation sidebar
|
|
8
|
+
* - breadcrumb: Breadcrumb trail
|
|
9
|
+
* - sidebar: Optional secondary sidebar (empty by default)
|
|
10
|
+
* - main: Primary content area (slot for child layouts)
|
|
11
|
+
* - footer: Footer with optional branding
|
|
12
|
+
* - toaster: Toast notifications overlay
|
|
13
|
+
* - user-info: User info in sidebar (separate from footer)
|
|
14
|
+
*
|
|
15
|
+
* Each zone renders blocks from ZoneRegistry, with defaults
|
|
16
|
+
* provided when no blocks are registered.
|
|
17
|
+
*
|
|
18
|
+
* Inheritance pattern (Twig-style):
|
|
19
|
+
* BaseLayout renders Zone components for each area
|
|
20
|
+
* -> Child layouts (List, Form, Dashboard) extend by providing content to main zone
|
|
21
|
+
* -> Entity pages extend child layouts with entity-specific customizations
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* <BaseLayout>
|
|
25
|
+
* <template #main>
|
|
26
|
+
* <!-- Page content here -->
|
|
27
|
+
* </template>
|
|
28
|
+
* </BaseLayout>
|
|
29
|
+
*
|
|
30
|
+
* Or with RouterView for nested routes:
|
|
31
|
+
* <BaseLayout />
|
|
32
|
+
*/
|
|
33
|
+
import { useSlots, provide, ref, inject } from 'vue'
|
|
34
|
+
import { RouterView } from 'vue-router'
|
|
35
|
+
import ConfirmDialog from 'primevue/confirmdialog'
|
|
36
|
+
import Zone from './Zone.vue'
|
|
37
|
+
import { LAYOUT_ZONES } from '../../zones/zones.js'
|
|
38
|
+
import { useGuardDialog } from '../../composables/useGuardStore'
|
|
39
|
+
import UnsavedChangesDialog from '../dialogs/UnsavedChangesDialog.vue'
|
|
40
|
+
|
|
41
|
+
// Default components for zones
|
|
42
|
+
import DefaultHeader from './defaults/DefaultHeader.vue'
|
|
43
|
+
import DefaultMenu from './defaults/DefaultMenu.vue'
|
|
44
|
+
import DefaultFooter from './defaults/DefaultFooter.vue'
|
|
45
|
+
import DefaultUserInfo from './defaults/DefaultUserInfo.vue'
|
|
46
|
+
import DefaultBreadcrumb from './defaults/DefaultBreadcrumb.vue'
|
|
47
|
+
import DefaultToaster from './defaults/DefaultToaster.vue'
|
|
48
|
+
|
|
49
|
+
const slots = useSlots()
|
|
50
|
+
const hasMainSlot = !!slots.main
|
|
51
|
+
|
|
52
|
+
// Guard dialog from shared store (registered by useBareForm/useForm when a form is active)
|
|
53
|
+
const guardDialog = useGuardDialog()
|
|
54
|
+
|
|
55
|
+
// Provide breadcrumb/navlinks override mechanism for child pages
|
|
56
|
+
const breadcrumbOverride = ref(null)
|
|
57
|
+
const navlinksOverride = ref(null)
|
|
58
|
+
provide('qdadmBreadcrumbOverride', breadcrumbOverride)
|
|
59
|
+
provide('qdadmNavlinksOverride', navlinksOverride)
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<template>
|
|
63
|
+
<div class="base-layout">
|
|
64
|
+
<!-- Sidebar (contains header, menu, user-info, footer) -->
|
|
65
|
+
<aside class="sidebar">
|
|
66
|
+
<!-- Header zone: branding, logo -->
|
|
67
|
+
<Zone
|
|
68
|
+
:name="LAYOUT_ZONES.HEADER"
|
|
69
|
+
:default-component="DefaultHeader"
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
<!-- Menu zone: navigation -->
|
|
73
|
+
<Zone
|
|
74
|
+
:name="LAYOUT_ZONES.MENU"
|
|
75
|
+
:default-component="DefaultMenu"
|
|
76
|
+
/>
|
|
77
|
+
|
|
78
|
+
<!-- User info (special zone between menu and footer) -->
|
|
79
|
+
<DefaultUserInfo />
|
|
80
|
+
|
|
81
|
+
<!-- Footer zone: powered by, etc. -->
|
|
82
|
+
<Zone
|
|
83
|
+
:name="LAYOUT_ZONES.FOOTER"
|
|
84
|
+
:default-component="DefaultFooter"
|
|
85
|
+
/>
|
|
86
|
+
</aside>
|
|
87
|
+
|
|
88
|
+
<!-- Main content area -->
|
|
89
|
+
<div class="main-area">
|
|
90
|
+
<!-- Breadcrumb zone -->
|
|
91
|
+
<Zone
|
|
92
|
+
:name="LAYOUT_ZONES.BREADCRUMB"
|
|
93
|
+
:default-component="DefaultBreadcrumb"
|
|
94
|
+
/>
|
|
95
|
+
|
|
96
|
+
<!-- Sidebar zone (optional secondary sidebar) -->
|
|
97
|
+
<div class="content-with-sidebar">
|
|
98
|
+
<Zone :name="LAYOUT_ZONES.SIDEBAR" />
|
|
99
|
+
|
|
100
|
+
<!-- Main zone: primary content -->
|
|
101
|
+
<main class="main-content">
|
|
102
|
+
<Zone :name="LAYOUT_ZONES.MAIN">
|
|
103
|
+
<!-- Allow slot override or RouterView for nested routes -->
|
|
104
|
+
<template v-if="hasMainSlot">
|
|
105
|
+
<slot name="main" />
|
|
106
|
+
</template>
|
|
107
|
+
<RouterView v-else />
|
|
108
|
+
</Zone>
|
|
109
|
+
</main>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<!-- Toaster zone: toast notifications -->
|
|
114
|
+
<Zone
|
|
115
|
+
:name="LAYOUT_ZONES.TOASTER"
|
|
116
|
+
:default-component="DefaultToaster"
|
|
117
|
+
/>
|
|
118
|
+
|
|
119
|
+
<!-- Confirm dialog (global) -->
|
|
120
|
+
<ConfirmDialog />
|
|
121
|
+
|
|
122
|
+
<!-- Unsaved Changes Dialog (auto-rendered when a form registers guardDialog) -->
|
|
123
|
+
<UnsavedChangesDialog
|
|
124
|
+
v-if="guardDialog"
|
|
125
|
+
:visible="guardDialog.visible.value"
|
|
126
|
+
:saving="guardDialog.saving.value"
|
|
127
|
+
:message="guardDialog.message"
|
|
128
|
+
:hasOnSave="guardDialog.hasOnSave"
|
|
129
|
+
@saveAndLeave="guardDialog.onSaveAndLeave"
|
|
130
|
+
@leave="guardDialog.onLeave"
|
|
131
|
+
@stay="guardDialog.onStay"
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
</template>
|
|
135
|
+
|
|
136
|
+
<style scoped>
|
|
137
|
+
.base-layout {
|
|
138
|
+
display: flex;
|
|
139
|
+
min-height: 100vh;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.sidebar {
|
|
143
|
+
width: var(--fad-sidebar-width, 15rem);
|
|
144
|
+
background: var(--p-surface-800, #1e293b);
|
|
145
|
+
color: var(--p-surface-0, white);
|
|
146
|
+
display: flex;
|
|
147
|
+
flex-direction: column;
|
|
148
|
+
position: fixed;
|
|
149
|
+
top: 0;
|
|
150
|
+
left: 0;
|
|
151
|
+
bottom: 0;
|
|
152
|
+
z-index: 100;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.main-area {
|
|
156
|
+
flex: 1;
|
|
157
|
+
margin-left: var(--fad-sidebar-width, 15rem);
|
|
158
|
+
background: var(--p-surface-50, #f8fafc);
|
|
159
|
+
min-height: 100vh;
|
|
160
|
+
display: flex;
|
|
161
|
+
flex-direction: column;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.content-with-sidebar {
|
|
165
|
+
display: flex;
|
|
166
|
+
flex: 1;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.main-content {
|
|
170
|
+
flex: 1;
|
|
171
|
+
padding: 1.5rem;
|
|
172
|
+
overflow-y: auto;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* Dark mode support */
|
|
176
|
+
.dark-mode .sidebar {
|
|
177
|
+
background: var(--p-surface-900);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.dark-mode .main-area {
|
|
181
|
+
background: var(--p-surface-900);
|
|
182
|
+
}
|
|
183
|
+
</style>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* DashboardLayout - Second level of layout inheritance for dashboard pages
|
|
4
|
+
*
|
|
5
|
+
* Extends BaseLayout by providing content to its `main` zone and defining
|
|
6
|
+
* dashboard-specific zones:
|
|
7
|
+
* - stats: Top area for KPI cards (CardsGrid component)
|
|
8
|
+
* - widgets: Main dashboard widgets area
|
|
9
|
+
* - recent-activity: Recent changes and activity log
|
|
10
|
+
*
|
|
11
|
+
* Each zone renders blocks from ZoneRegistry. Empty zones render nothing
|
|
12
|
+
* (no default widgets). Applications provide dashboard content by
|
|
13
|
+
* registering blocks to these zones.
|
|
14
|
+
*
|
|
15
|
+
* Inheritance pattern:
|
|
16
|
+
* BaseLayout renders Zone components for each area
|
|
17
|
+
* -> DashboardLayout fills main zone with dashboard structure
|
|
18
|
+
* -> Dashboard pages extend with entity-specific customizations
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* <DashboardLayout>
|
|
22
|
+
* <!-- Optional: Override specific zones via slots -->
|
|
23
|
+
* </DashboardLayout>
|
|
24
|
+
*
|
|
25
|
+
* Or register blocks via ZoneRegistry:
|
|
26
|
+
* registry.registerBlock(DASHBOARD_ZONES.STATS, {
|
|
27
|
+
* component: StatsWidget,
|
|
28
|
+
* id: 'kpi-cards',
|
|
29
|
+
* weight: 10
|
|
30
|
+
* })
|
|
31
|
+
*/
|
|
32
|
+
import { useSlots } from 'vue'
|
|
33
|
+
import BaseLayout from './BaseLayout.vue'
|
|
34
|
+
import Zone from './Zone.vue'
|
|
35
|
+
import { DASHBOARD_ZONES } from '../../zones/zones.js'
|
|
36
|
+
|
|
37
|
+
const slots = useSlots()
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<BaseLayout>
|
|
42
|
+
<template #main>
|
|
43
|
+
<div class="dashboard-layout">
|
|
44
|
+
<!-- Stats zone: KPI cards, metrics -->
|
|
45
|
+
<section v-if="slots.stats" class="dashboard-stats">
|
|
46
|
+
<slot name="stats" />
|
|
47
|
+
</section>
|
|
48
|
+
<section v-else class="dashboard-stats">
|
|
49
|
+
<Zone :name="DASHBOARD_ZONES.STATS" />
|
|
50
|
+
</section>
|
|
51
|
+
|
|
52
|
+
<!-- Widgets zone: main dashboard widgets -->
|
|
53
|
+
<section v-if="slots.widgets" class="dashboard-widgets">
|
|
54
|
+
<slot name="widgets" />
|
|
55
|
+
</section>
|
|
56
|
+
<section v-else class="dashboard-widgets">
|
|
57
|
+
<Zone :name="DASHBOARD_ZONES.WIDGETS" />
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
<!-- Recent activity zone: activity log, recent changes -->
|
|
61
|
+
<section v-if="slots['recent-activity']" class="dashboard-recent-activity">
|
|
62
|
+
<slot name="recent-activity" />
|
|
63
|
+
</section>
|
|
64
|
+
<section v-else class="dashboard-recent-activity">
|
|
65
|
+
<Zone :name="DASHBOARD_ZONES.RECENT_ACTIVITY" />
|
|
66
|
+
</section>
|
|
67
|
+
</div>
|
|
68
|
+
</template>
|
|
69
|
+
</BaseLayout>
|
|
70
|
+
</template>
|
|
71
|
+
|
|
72
|
+
<style scoped>
|
|
73
|
+
.dashboard-layout {
|
|
74
|
+
display: flex;
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
gap: 1.5rem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.dashboard-stats {
|
|
80
|
+
/* Stats area typically uses CardsGrid */
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.dashboard-widgets {
|
|
84
|
+
/* Main widgets area */
|
|
85
|
+
display: flex;
|
|
86
|
+
flex-direction: column;
|
|
87
|
+
gap: 1rem;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.dashboard-recent-activity {
|
|
91
|
+
/* Recent activity area */
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* Empty zones should collapse - Zone renders nothing when no blocks */
|
|
95
|
+
.dashboard-stats:empty,
|
|
96
|
+
.dashboard-widgets:empty,
|
|
97
|
+
.dashboard-recent-activity:empty {
|
|
98
|
+
display: none;
|
|
99
|
+
}
|
|
100
|
+
</style>
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* FormLayout - Second level of the 3-level layout inheritance chain
|
|
4
|
+
*
|
|
5
|
+
* Extends BaseLayout by providing content to the `main` zone, and defines
|
|
6
|
+
* form-specific zones within that main area:
|
|
7
|
+
* - form-header: Title, back button, status indicators
|
|
8
|
+
* - form-fields: Main form fields area
|
|
9
|
+
* - form-tabs: Optional tabbed sections
|
|
10
|
+
* - actions: Save, cancel, delete buttons
|
|
11
|
+
*
|
|
12
|
+
* Inheritance pattern (Twig-style):
|
|
13
|
+
* BaseLayout renders all standard zones (header, menu, footer, main...)
|
|
14
|
+
* -> FormLayout fills the main zone with form structure
|
|
15
|
+
* -> Entity pages (BooksEditPage.vue) fill form zones with entity fields
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* <FormLayout>
|
|
19
|
+
* <template #form-fields>
|
|
20
|
+
* <FormField v-model="data.title" name="title" />
|
|
21
|
+
* </template>
|
|
22
|
+
* <template #actions>
|
|
23
|
+
* <Button @click="save">Save</Button>
|
|
24
|
+
* </template>
|
|
25
|
+
* </FormLayout>
|
|
26
|
+
*
|
|
27
|
+
* Or using Zone API for extension points:
|
|
28
|
+
* <FormLayout /> <!-- Renders zones from registry -->
|
|
29
|
+
*/
|
|
30
|
+
import { useSlots, provide, computed } from 'vue'
|
|
31
|
+
import BaseLayout from './BaseLayout.vue'
|
|
32
|
+
import Zone from './Zone.vue'
|
|
33
|
+
import { FORM_ZONES } from '../../zones/zones.js'
|
|
34
|
+
|
|
35
|
+
// Default components for form zones
|
|
36
|
+
import DefaultFormActions from './defaults/DefaultFormActions.vue'
|
|
37
|
+
|
|
38
|
+
const props = defineProps({
|
|
39
|
+
/**
|
|
40
|
+
* Form loading state (displays spinner instead of fields)
|
|
41
|
+
*/
|
|
42
|
+
loading: {
|
|
43
|
+
type: Boolean,
|
|
44
|
+
default: false
|
|
45
|
+
},
|
|
46
|
+
/**
|
|
47
|
+
* Form saving state (passed to actions)
|
|
48
|
+
*/
|
|
49
|
+
saving: {
|
|
50
|
+
type: Boolean,
|
|
51
|
+
default: false
|
|
52
|
+
},
|
|
53
|
+
/**
|
|
54
|
+
* Form has unsaved changes (passed to actions)
|
|
55
|
+
*/
|
|
56
|
+
dirty: {
|
|
57
|
+
type: Boolean,
|
|
58
|
+
default: false
|
|
59
|
+
},
|
|
60
|
+
/**
|
|
61
|
+
* Edit mode (changes action button labels)
|
|
62
|
+
*/
|
|
63
|
+
isEdit: {
|
|
64
|
+
type: Boolean,
|
|
65
|
+
default: false
|
|
66
|
+
},
|
|
67
|
+
/**
|
|
68
|
+
* Whether to wrap form in a card component
|
|
69
|
+
*/
|
|
70
|
+
cardWrapper: {
|
|
71
|
+
type: Boolean,
|
|
72
|
+
default: true
|
|
73
|
+
},
|
|
74
|
+
/**
|
|
75
|
+
* Whether to show the actions zone
|
|
76
|
+
*/
|
|
77
|
+
showActions: {
|
|
78
|
+
type: Boolean,
|
|
79
|
+
default: true
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const emit = defineEmits(['save', 'saveAndClose', 'cancel', 'delete'])
|
|
84
|
+
|
|
85
|
+
const slots = useSlots()
|
|
86
|
+
|
|
87
|
+
// Check which slots are provided by the parent
|
|
88
|
+
const hasFormHeaderSlot = computed(() => !!slots['form-header'])
|
|
89
|
+
const hasFormFieldsSlot = computed(() => !!slots['form-fields'])
|
|
90
|
+
const hasFormTabsSlot = computed(() => !!slots['form-tabs'])
|
|
91
|
+
const hasActionsSlot = computed(() => !!slots.actions)
|
|
92
|
+
|
|
93
|
+
// Provide form state to child zones (DefaultFormActions needs these)
|
|
94
|
+
provide('qdadmFormState', computed(() => ({
|
|
95
|
+
loading: props.loading,
|
|
96
|
+
saving: props.saving,
|
|
97
|
+
dirty: props.dirty,
|
|
98
|
+
isEdit: props.isEdit
|
|
99
|
+
})))
|
|
100
|
+
|
|
101
|
+
// Provide form events for child zones to emit
|
|
102
|
+
provide('qdadmFormEmit', {
|
|
103
|
+
save: () => emit('save'),
|
|
104
|
+
saveAndClose: () => emit('saveAndClose'),
|
|
105
|
+
cancel: () => emit('cancel'),
|
|
106
|
+
delete: () => emit('delete')
|
|
107
|
+
})
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<template>
|
|
111
|
+
<BaseLayout>
|
|
112
|
+
<template #main>
|
|
113
|
+
<div class="form-layout" :class="{ loading }">
|
|
114
|
+
<!-- Form Header Zone: title, back button, status indicators -->
|
|
115
|
+
<div class="form-header-zone">
|
|
116
|
+
<Zone :name="FORM_ZONES.FORM_HEADER">
|
|
117
|
+
<template v-if="hasFormHeaderSlot">
|
|
118
|
+
<slot name="form-header" />
|
|
119
|
+
</template>
|
|
120
|
+
</Zone>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<!-- Loading State -->
|
|
124
|
+
<div v-if="loading" class="form-loading-state">
|
|
125
|
+
<i class="pi pi-spin pi-spinner loading-spinner"></i>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<!-- Form Content -->
|
|
129
|
+
<template v-else>
|
|
130
|
+
<!-- Card wrapper or direct content -->
|
|
131
|
+
<div v-if="cardWrapper" class="form-card">
|
|
132
|
+
<!-- Form Fields Zone: main form fields area -->
|
|
133
|
+
<div class="form-fields-zone">
|
|
134
|
+
<Zone :name="FORM_ZONES.FORM_FIELDS">
|
|
135
|
+
<template v-if="hasFormFieldsSlot">
|
|
136
|
+
<slot name="form-fields" />
|
|
137
|
+
</template>
|
|
138
|
+
</Zone>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<!-- Form Tabs Zone: optional tabbed sections -->
|
|
142
|
+
<div v-if="hasFormTabsSlot" class="form-tabs-zone">
|
|
143
|
+
<Zone :name="FORM_ZONES.FORM_TABS">
|
|
144
|
+
<slot name="form-tabs" />
|
|
145
|
+
</Zone>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<!-- Actions Zone: save, cancel, delete buttons -->
|
|
149
|
+
<div v-if="showActions" class="form-actions-zone">
|
|
150
|
+
<!-- Slot override takes priority over Zone -->
|
|
151
|
+
<template v-if="hasActionsSlot">
|
|
152
|
+
<slot name="actions" />
|
|
153
|
+
</template>
|
|
154
|
+
<!-- Otherwise use Zone with default component -->
|
|
155
|
+
<Zone
|
|
156
|
+
v-else
|
|
157
|
+
:name="FORM_ZONES.ACTIONS"
|
|
158
|
+
:default-component="DefaultFormActions"
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<!-- Without card wrapper -->
|
|
164
|
+
<template v-else>
|
|
165
|
+
<!-- Form Fields Zone -->
|
|
166
|
+
<div class="form-fields-zone">
|
|
167
|
+
<Zone :name="FORM_ZONES.FORM_FIELDS">
|
|
168
|
+
<template v-if="hasFormFieldsSlot">
|
|
169
|
+
<slot name="form-fields" />
|
|
170
|
+
</template>
|
|
171
|
+
</Zone>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<!-- Form Tabs Zone -->
|
|
175
|
+
<div v-if="hasFormTabsSlot" class="form-tabs-zone">
|
|
176
|
+
<Zone :name="FORM_ZONES.FORM_TABS">
|
|
177
|
+
<slot name="form-tabs" />
|
|
178
|
+
</Zone>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<!-- Actions Zone -->
|
|
182
|
+
<div v-if="showActions" class="form-actions-zone">
|
|
183
|
+
<!-- Slot override takes priority over Zone -->
|
|
184
|
+
<template v-if="hasActionsSlot">
|
|
185
|
+
<slot name="actions" />
|
|
186
|
+
</template>
|
|
187
|
+
<!-- Otherwise use Zone with default component -->
|
|
188
|
+
<Zone
|
|
189
|
+
v-else
|
|
190
|
+
:name="FORM_ZONES.ACTIONS"
|
|
191
|
+
:default-component="DefaultFormActions"
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
</template>
|
|
195
|
+
</template>
|
|
196
|
+
</div>
|
|
197
|
+
</template>
|
|
198
|
+
</BaseLayout>
|
|
199
|
+
</template>
|
|
200
|
+
|
|
201
|
+
<style scoped>
|
|
202
|
+
.form-layout {
|
|
203
|
+
display: flex;
|
|
204
|
+
flex-direction: column;
|
|
205
|
+
gap: 1rem;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.form-layout.loading {
|
|
209
|
+
min-height: 300px;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.form-loading-state {
|
|
213
|
+
display: flex;
|
|
214
|
+
justify-content: center;
|
|
215
|
+
align-items: center;
|
|
216
|
+
padding: 3rem;
|
|
217
|
+
flex: 1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.loading-spinner {
|
|
221
|
+
font-size: 2rem;
|
|
222
|
+
color: var(--p-primary-500);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.form-card {
|
|
226
|
+
background: var(--p-surface-0);
|
|
227
|
+
border-radius: var(--p-border-radius);
|
|
228
|
+
padding: 1.5rem;
|
|
229
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.form-header-zone {
|
|
233
|
+
/* Form header inherits page header styles */
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.form-fields-zone {
|
|
237
|
+
display: flex;
|
|
238
|
+
flex-direction: column;
|
|
239
|
+
gap: 1rem;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.form-tabs-zone {
|
|
243
|
+
margin-top: 1rem;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.form-actions-zone {
|
|
247
|
+
margin-top: 1.5rem;
|
|
248
|
+
padding-top: 1.5rem;
|
|
249
|
+
border-top: 1px solid var(--p-surface-200);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* Dark mode support */
|
|
253
|
+
.dark-mode .form-card {
|
|
254
|
+
background: var(--p-surface-800);
|
|
255
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.dark-mode .form-actions-zone {
|
|
259
|
+
border-top-color: var(--p-surface-700);
|
|
260
|
+
}
|
|
261
|
+
</style>
|