qdadm 0.57.0 → 0.58.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 +1 -1
- package/src/auth/SessionAuthAdapter.js +19 -0
- package/src/components/InfoBanner.vue +2 -2
- package/src/components/display/CopyableId.vue +4 -15
- package/src/components/index.js +0 -1
- package/src/components/layout/BaseLayout.vue +2 -6
- package/src/components/layout/PageNav.vue +7 -4
- package/src/components/layout/defaults/DefaultMenu.vue +1 -0
- package/src/components/layout/defaults/index.js +0 -1
- package/src/components/lists/ListPage.vue +1 -0
- package/src/components/pages/LoginPage.vue +13 -32
- package/src/composables/useBareForm.js +8 -2
- package/src/composables/useEntityItemFormPage.js +12 -4
- package/src/composables/useListPage.js +7 -2
- package/src/kernel/Kernel.js +78 -30
- package/src/modules/debug/components/panels/AuthPanel.vue +47 -24
- package/src/modules/debug/components/panels/SignalsPanel.vue +2 -2
- package/src/modules/debug/components/panels/ZonesPanel.vue +12 -10
- package/src/modules/debug/styles.scss +114 -2
- package/src/orchestrator/Orchestrator.js +57 -0
- package/src/styles/_buttons.scss +318 -0
- package/src/styles/_lists.scss +14 -0
- package/src/styles/_responsive.scss +7 -0
- package/src/styles/index.scss +1 -0
- package/src/components/layout/defaults/DefaultToaster.vue +0 -16
package/package.json
CHANGED
|
@@ -143,6 +143,16 @@ export class SessionAuthAdapter {
|
|
|
143
143
|
this._user = null
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Destroy session (catastrophic/silent logout)
|
|
148
|
+
*
|
|
149
|
+
* Clears token WITHOUT triggering normal logout signals.
|
|
150
|
+
* Used by DebugBar to test how the app handles unexpected session loss.
|
|
151
|
+
*/
|
|
152
|
+
destroySession() {
|
|
153
|
+
this._token = null
|
|
154
|
+
}
|
|
155
|
+
|
|
146
156
|
/**
|
|
147
157
|
* Validate that the adapter is properly configured
|
|
148
158
|
*
|
|
@@ -316,6 +326,15 @@ export class LocalStorageSessionAuthAdapter extends SessionAuthAdapter {
|
|
|
316
326
|
return original
|
|
317
327
|
}
|
|
318
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Destroy session - clears token and localStorage
|
|
331
|
+
* @override
|
|
332
|
+
*/
|
|
333
|
+
destroySession = () => {
|
|
334
|
+
this._token = null
|
|
335
|
+
localStorage.removeItem(this._storageKey)
|
|
336
|
+
}
|
|
337
|
+
|
|
319
338
|
// ─────────────────────────────────────────────────────────────────
|
|
320
339
|
// Signal integration
|
|
321
340
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
* <CopyableId :value="entityId" label="ID" />
|
|
7
7
|
* <CopyableId :value="apiKey" label="API Key" />
|
|
8
8
|
*/
|
|
9
|
-
import { ref } from 'vue'
|
|
10
|
-
import { useToast } from 'primevue/usetoast'
|
|
9
|
+
import { ref, inject } from 'vue'
|
|
11
10
|
import Button from 'primevue/button'
|
|
12
11
|
|
|
13
12
|
const props = defineProps({
|
|
@@ -21,29 +20,19 @@ const props = defineProps({
|
|
|
21
20
|
}
|
|
22
21
|
})
|
|
23
22
|
|
|
24
|
-
const
|
|
23
|
+
const orchestrator = inject('qdadmOrchestrator', null)
|
|
25
24
|
const copied = ref(false)
|
|
26
25
|
|
|
27
26
|
async function copyToClipboard() {
|
|
28
27
|
try {
|
|
29
28
|
await navigator.clipboard.writeText(props.value)
|
|
30
29
|
copied.value = true
|
|
31
|
-
toast.
|
|
32
|
-
severity: 'success',
|
|
33
|
-
summary: 'Copied',
|
|
34
|
-
detail: `${props.label} copied to clipboard`,
|
|
35
|
-
life: 2000
|
|
36
|
-
})
|
|
30
|
+
orchestrator?.toast.success('Copied', `${props.label} copied to clipboard`, 'CopyableId')
|
|
37
31
|
setTimeout(() => {
|
|
38
32
|
copied.value = false
|
|
39
33
|
}, 2000)
|
|
40
34
|
} catch {
|
|
41
|
-
toast.
|
|
42
|
-
severity: 'error',
|
|
43
|
-
summary: 'Error',
|
|
44
|
-
detail: 'Failed to copy to clipboard',
|
|
45
|
-
life: 3000
|
|
46
|
-
})
|
|
35
|
+
orchestrator?.toast.error('Error', 'Failed to copy to clipboard', 'CopyableId')
|
|
47
36
|
}
|
|
48
37
|
}
|
|
49
38
|
</script>
|
package/src/components/index.js
CHANGED
|
@@ -18,7 +18,6 @@ export { default as DefaultMenu } from './layout/defaults/DefaultMenu.vue'
|
|
|
18
18
|
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
|
-
export { default as DefaultToaster } from './layout/defaults/DefaultToaster.vue'
|
|
22
21
|
|
|
23
22
|
// Forms
|
|
24
23
|
export { default as FormPage } from './forms/FormPage.vue'
|
|
@@ -44,7 +44,7 @@ import DefaultMenu from './defaults/DefaultMenu.vue'
|
|
|
44
44
|
import DefaultFooter from './defaults/DefaultFooter.vue'
|
|
45
45
|
import DefaultUserInfo from './defaults/DefaultUserInfo.vue'
|
|
46
46
|
import DefaultBreadcrumb from './defaults/DefaultBreadcrumb.vue'
|
|
47
|
-
|
|
47
|
+
// DefaultToaster removed - Toast is now provided by Kernel at root level
|
|
48
48
|
|
|
49
49
|
const slots = useSlots()
|
|
50
50
|
const hasMainSlot = !!slots.main
|
|
@@ -110,11 +110,7 @@ provide('qdadmNavlinksOverride', navlinksOverride)
|
|
|
110
110
|
</div>
|
|
111
111
|
</div>
|
|
112
112
|
|
|
113
|
-
<!--
|
|
114
|
-
<Zone
|
|
115
|
-
:name="LAYOUT_ZONES.TOASTER"
|
|
116
|
-
:default-component="DefaultToaster"
|
|
117
|
-
/>
|
|
113
|
+
<!-- Toast is now provided by Kernel at root level -->
|
|
118
114
|
|
|
119
115
|
<!-- Confirm dialog (global) -->
|
|
120
116
|
<ConfirmDialog />
|
|
@@ -81,6 +81,13 @@ const childNavlinks = computed(() => {
|
|
|
81
81
|
const entityName = route.meta?.entity
|
|
82
82
|
if (!entityName) return []
|
|
83
83
|
|
|
84
|
+
// Get current entity's manager to determine idField
|
|
85
|
+
const currentManager = getManager(entityName)
|
|
86
|
+
const entityId = route.params[currentManager?.idField || 'id']
|
|
87
|
+
|
|
88
|
+
// Only show children when we have an entity ID (not on create pages)
|
|
89
|
+
if (!entityId) return []
|
|
90
|
+
|
|
84
91
|
const children = getChildRoutes(entityName)
|
|
85
92
|
if (children.length === 0) return []
|
|
86
93
|
|
|
@@ -93,10 +100,6 @@ const childNavlinks = computed(() => {
|
|
|
93
100
|
|
|
94
101
|
if (navRoutes.length === 0) return []
|
|
95
102
|
|
|
96
|
-
// Get current entity's manager to determine idField
|
|
97
|
-
const currentManager = getManager(entityName)
|
|
98
|
-
const entityId = route.params[currentManager?.idField || 'id']
|
|
99
|
-
|
|
100
103
|
// Build navlinks to child routes
|
|
101
104
|
return navRoutes.map(childRoute => {
|
|
102
105
|
const childManager = childRoute.meta?.entity ? getManager(childRoute.meta.entity) : null
|
|
@@ -11,7 +11,6 @@ export { default as DefaultMenu } from './DefaultMenu.vue'
|
|
|
11
11
|
export { default as DefaultFooter } from './DefaultFooter.vue'
|
|
12
12
|
export { default as DefaultUserInfo } from './DefaultUserInfo.vue'
|
|
13
13
|
export { default as DefaultBreadcrumb } from './DefaultBreadcrumb.vue'
|
|
14
|
-
export { default as DefaultToaster } from './DefaultToaster.vue'
|
|
15
14
|
|
|
16
15
|
// FormLayout defaults
|
|
17
16
|
export { default as DefaultFormActions } from './DefaultFormActions.vue'
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { ref, inject, computed } from 'vue'
|
|
21
21
|
import { useRouter } from 'vue-router'
|
|
22
|
-
import { useToast } from 'primevue/usetoast'
|
|
23
22
|
import Card from 'primevue/card'
|
|
24
23
|
import InputText from 'primevue/inputtext'
|
|
25
24
|
import Password from 'primevue/password'
|
|
@@ -102,7 +101,6 @@ const props = defineProps({
|
|
|
102
101
|
const emit = defineEmits(['login', 'error'])
|
|
103
102
|
|
|
104
103
|
const router = useRouter()
|
|
105
|
-
const toast = useToast()
|
|
106
104
|
const authAdapter = inject('authAdapter', null)
|
|
107
105
|
const orchestrator = inject('qdadmOrchestrator', null)
|
|
108
106
|
const appConfig = inject('qdadmApp', {})
|
|
@@ -115,12 +113,7 @@ const displayTitle = computed(() => props.title || appConfig.name || 'Admin')
|
|
|
115
113
|
|
|
116
114
|
async function handleLogin() {
|
|
117
115
|
if (!authAdapter?.login) {
|
|
118
|
-
toast.
|
|
119
|
-
severity: 'error',
|
|
120
|
-
summary: 'Configuration Error',
|
|
121
|
-
detail: 'No auth adapter configured',
|
|
122
|
-
life: 5000
|
|
123
|
-
})
|
|
116
|
+
orchestrator?.toast.error('Configuration Error', 'No auth adapter configured', 'LoginPage')
|
|
124
117
|
return
|
|
125
118
|
}
|
|
126
119
|
|
|
@@ -131,17 +124,13 @@ async function handleLogin() {
|
|
|
131
124
|
password: password.value
|
|
132
125
|
})
|
|
133
126
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// Emit business signal if enabled
|
|
142
|
-
if (props.emitSignal && orchestrator?.signals) {
|
|
143
|
-
orchestrator.signals.emit('auth:login', { user: result.user })
|
|
144
|
-
}
|
|
127
|
+
// Emit auth:login signal for debug bar and other listeners
|
|
128
|
+
orchestrator?.signals.emit('auth:login', { user: result.user })
|
|
129
|
+
orchestrator?.toast.success(
|
|
130
|
+
'Welcome',
|
|
131
|
+
`Logged in as ${result.user?.username || result.user?.email || username.value}`,
|
|
132
|
+
'LoginPage'
|
|
133
|
+
)
|
|
145
134
|
|
|
146
135
|
emit('login', result)
|
|
147
136
|
router.push(props.redirectTo)
|
|
@@ -153,20 +142,12 @@ async function handleLogin() {
|
|
|
153
142
|
|| error.message
|
|
154
143
|
|| 'Invalid credentials'
|
|
155
144
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
life: 5000
|
|
145
|
+
orchestrator?.signals.emit('auth:login:error', {
|
|
146
|
+
username: username.value,
|
|
147
|
+
error: message,
|
|
148
|
+
status: error.response?.status
|
|
161
149
|
})
|
|
162
|
-
|
|
163
|
-
if (orchestrator?.signals) {
|
|
164
|
-
orchestrator.signals.emit('auth:login:error', {
|
|
165
|
-
username: username.value,
|
|
166
|
-
error: message,
|
|
167
|
-
status: error.response?.status
|
|
168
|
-
})
|
|
169
|
-
}
|
|
150
|
+
orchestrator?.toast.error('Login Failed', message, 'LoginPage')
|
|
170
151
|
|
|
171
152
|
emit('error', error)
|
|
172
153
|
} finally {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { ref, computed, provide, inject, onUnmounted } from 'vue'
|
|
2
2
|
import { useRoute, useRouter } from 'vue-router'
|
|
3
|
-
import { useToast } from 'primevue/usetoast'
|
|
4
3
|
import { useDirtyState } from './useDirtyState'
|
|
5
4
|
import { useUnsavedChangesGuard } from './useUnsavedChangesGuard'
|
|
6
5
|
import { useBreadcrumb } from './useBreadcrumb'
|
|
@@ -81,7 +80,14 @@ export function useBareForm(options = {}) {
|
|
|
81
80
|
// Router, route, toast - common dependencies
|
|
82
81
|
const router = useRouter()
|
|
83
82
|
const route = useRoute()
|
|
84
|
-
const
|
|
83
|
+
const orchestrator = inject('qdadmOrchestrator', null)
|
|
84
|
+
|
|
85
|
+
// Toast helper - wraps orchestrator.toast for legacy compatibility
|
|
86
|
+
const toast = {
|
|
87
|
+
add({ severity, summary, detail, emitter }) {
|
|
88
|
+
orchestrator?.toast[severity]?.(summary, detail, emitter)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
85
91
|
|
|
86
92
|
// Common state
|
|
87
93
|
const loading = ref(false)
|
|
@@ -79,7 +79,6 @@
|
|
|
79
79
|
*/
|
|
80
80
|
import { ref, computed, watch, onMounted, onUnmounted, provide } from 'vue'
|
|
81
81
|
import { useRouter, useRoute } from 'vue-router'
|
|
82
|
-
import { useToast } from 'primevue/usetoast'
|
|
83
82
|
import { useConfirm } from 'primevue/useconfirm'
|
|
84
83
|
import { useDirtyState } from './useDirtyState'
|
|
85
84
|
import { useUnsavedChangesGuard } from './useUnsavedChangesGuard'
|
|
@@ -117,7 +116,6 @@ export function useEntityItemFormPage(config = {}) {
|
|
|
117
116
|
|
|
118
117
|
const router = useRouter()
|
|
119
118
|
const route = useRoute()
|
|
120
|
-
const toast = useToast()
|
|
121
119
|
const confirm = useConfirm()
|
|
122
120
|
|
|
123
121
|
// Use useEntityItemPage for common infrastructure
|
|
@@ -130,6 +128,13 @@ export function useEntityItemFormPage(config = {}) {
|
|
|
130
128
|
|
|
131
129
|
const { manager, orchestrator, entityId, getInitialDataWithParent, parentConfig, parentId, parentData, parentChain, getChainDepth, hydrator } = itemPage
|
|
132
130
|
|
|
131
|
+
// Toast helper - wraps orchestrator.toast for legacy compatibility
|
|
132
|
+
const toast = {
|
|
133
|
+
add({ severity, summary, detail, emitter }) {
|
|
134
|
+
orchestrator?.toast[severity]?.(summary, detail, emitter)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
133
138
|
// Read config from manager with option overrides
|
|
134
139
|
const entityName = config.entityName ?? manager.label
|
|
135
140
|
const routePrefix = config.routePrefix ?? manager.routePrefix
|
|
@@ -449,11 +454,14 @@ export function useEntityItemFormPage(config = {}) {
|
|
|
449
454
|
}
|
|
450
455
|
|
|
451
456
|
if (listRoute?.name) {
|
|
452
|
-
|
|
457
|
+
// Filter out entity ID param (doesn't exist in create mode)
|
|
458
|
+
const params = { ...route.params }
|
|
459
|
+
delete params[manager.idField]
|
|
460
|
+
return { name: listRoute.name, params }
|
|
453
461
|
}
|
|
454
462
|
}
|
|
455
463
|
|
|
456
|
-
// Default: top-level entity list
|
|
464
|
+
// Default: top-level entity list (no params needed)
|
|
457
465
|
return { name: routePrefix }
|
|
458
466
|
}
|
|
459
467
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { ref, computed, watch, onMounted, inject, provide } from 'vue'
|
|
2
2
|
import { useRouter, useRoute } from 'vue-router'
|
|
3
|
-
import { useToast } from 'primevue/usetoast'
|
|
4
3
|
import { useConfirm } from 'primevue/useconfirm'
|
|
5
4
|
import { useHooks } from './useHooks.js'
|
|
6
5
|
import { useEntityItemPage } from './useEntityItemPage.js'
|
|
@@ -139,11 +138,17 @@ export function useListPage(config = {}) {
|
|
|
139
138
|
|
|
140
139
|
const router = useRouter()
|
|
141
140
|
const route = useRoute()
|
|
142
|
-
const toast = useToast()
|
|
143
141
|
const confirm = useConfirm()
|
|
144
142
|
|
|
145
143
|
// Get EntityManager via orchestrator
|
|
146
144
|
const orchestrator = inject('qdadmOrchestrator')
|
|
145
|
+
|
|
146
|
+
// Toast helper - wraps orchestrator.toast for legacy compatibility
|
|
147
|
+
const toast = {
|
|
148
|
+
add({ severity, summary, detail, emitter }) {
|
|
149
|
+
orchestrator?.toast[severity]?.(summary, detail, emitter)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
147
152
|
if (!orchestrator) {
|
|
148
153
|
throw new Error(
|
|
149
154
|
'[qdadm] Orchestrator not provided.\n' +
|
package/src/kernel/Kernel.js
CHANGED
|
@@ -43,12 +43,14 @@ import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router
|
|
|
43
43
|
import ToastService from 'primevue/toastservice'
|
|
44
44
|
import ConfirmationService from 'primevue/confirmationservice'
|
|
45
45
|
import Tooltip from 'primevue/tooltip'
|
|
46
|
+
import Toast from 'primevue/toast'
|
|
47
|
+
import ToastListener from '../toast/ToastListener.vue'
|
|
46
48
|
|
|
47
49
|
import { createQdadm } from '../plugin.js'
|
|
48
50
|
import { initModules, getRoutes, setSectionOrder, alterMenuSections, registry } from '../module/moduleRegistry.js'
|
|
49
51
|
import { createModuleLoader } from './ModuleLoader.js'
|
|
50
52
|
import { createKernelContext } from './KernelContext.js'
|
|
51
|
-
|
|
53
|
+
// ToastBridgeModule no longer used - Toast/ToastListener rendered at root level
|
|
52
54
|
import { Orchestrator } from '../orchestrator/Orchestrator.js'
|
|
53
55
|
import { createSignalBus } from './SignalBus.js'
|
|
54
56
|
import { createZoneRegistry } from '../zones/ZoneRegistry.js'
|
|
@@ -101,7 +103,7 @@ export class Kernel {
|
|
|
101
103
|
* @param {object} options.eventRouter - EventRouter config { 'source:signal': ['target:signal', ...] }
|
|
102
104
|
* @param {object} options.sse - SSEBridge config { url, reconnectDelay, signalPrefix, autoConnect, events }
|
|
103
105
|
* @param {object} options.debugBar - Debug bar config { module: DebugModule, component: QdadmDebugBar, ...options }
|
|
104
|
-
* @param {boolean} options.toast -
|
|
106
|
+
* @param {boolean} options.toast - Reserved for future use (toast is always enabled when PrimeVue is configured)
|
|
105
107
|
*/
|
|
106
108
|
constructor(options) {
|
|
107
109
|
// Auto-inject DebugModule if debugBar.module is provided
|
|
@@ -127,13 +129,8 @@ export class Kernel {
|
|
|
127
129
|
}
|
|
128
130
|
}
|
|
129
131
|
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
if (options.toast !== false) {
|
|
133
|
-
options.moduleDefs = options.moduleDefs || []
|
|
134
|
-
// Add at beginning for high priority (loads early)
|
|
135
|
-
options.moduleDefs.unshift(new ToastBridgeModule())
|
|
136
|
-
}
|
|
132
|
+
// Note: Toast is now handled at root level via _createVueApp()
|
|
133
|
+
// ToastBridgeModule is no longer auto-injected to avoid duplicate listeners
|
|
137
134
|
|
|
138
135
|
this.options = options
|
|
139
136
|
this.vueApp = null
|
|
@@ -718,7 +715,28 @@ export class Kernel {
|
|
|
718
715
|
const { authAdapter } = this.options
|
|
719
716
|
if (!authAdapter) return
|
|
720
717
|
|
|
718
|
+
// Track if user was ever authenticated (to detect session loss)
|
|
719
|
+
let wasEverAuthenticated = authAdapter.isAuthenticated()
|
|
720
|
+
|
|
721
|
+
// Listen for session loss signal and show toast
|
|
722
|
+
const debug = this.options.debug ?? false
|
|
723
|
+
this.signals.on('auth:session-lost', () => {
|
|
724
|
+
if (debug) {
|
|
725
|
+
console.warn('[Kernel] auth:session-lost received, emitting toast:warn')
|
|
726
|
+
}
|
|
727
|
+
this.orchestrator.toast.warn(
|
|
728
|
+
'Session lost',
|
|
729
|
+
'Your session has expired. Please log in again.',
|
|
730
|
+
'Kernel'
|
|
731
|
+
)
|
|
732
|
+
})
|
|
733
|
+
|
|
721
734
|
this.router.beforeEach((to, from, next) => {
|
|
735
|
+
// Update auth tracking
|
|
736
|
+
if (authAdapter.isAuthenticated()) {
|
|
737
|
+
wasEverAuthenticated = true
|
|
738
|
+
}
|
|
739
|
+
|
|
722
740
|
// Check if route or any parent is explicitly public
|
|
723
741
|
const isPublic = to.matched.some(record =>
|
|
724
742
|
record.meta.public === true || record.meta.requiresAuth === false
|
|
@@ -733,8 +751,24 @@ export class Kernel {
|
|
|
733
751
|
const requiresAuth = to.matched.some(record => record.meta.requiresAuth === true)
|
|
734
752
|
|
|
735
753
|
if (requiresAuth && !authAdapter.isAuthenticated()) {
|
|
736
|
-
//
|
|
737
|
-
|
|
754
|
+
// Session loss detected - emit signal if user was previously authenticated
|
|
755
|
+
if (wasEverAuthenticated) {
|
|
756
|
+
const debug = this.options.debug ?? false
|
|
757
|
+
if (debug) {
|
|
758
|
+
console.warn('[Kernel] Session lost detected, emitting auth:session-lost')
|
|
759
|
+
}
|
|
760
|
+
this.signals.emit('auth:session-lost', {
|
|
761
|
+
reason: 'token_missing',
|
|
762
|
+
redirectTo: 'login'
|
|
763
|
+
})
|
|
764
|
+
// Reset tracking for next session
|
|
765
|
+
wasEverAuthenticated = false
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Redirect to login with session_lost param (if exists) or emit signal
|
|
769
|
+
const loginRoute = this.router.hasRoute('login')
|
|
770
|
+
? { name: 'login', query: { session_lost: '1' } }
|
|
771
|
+
: '/'
|
|
738
772
|
next(loginRoute)
|
|
739
773
|
} else {
|
|
740
774
|
next()
|
|
@@ -847,6 +881,11 @@ export class Kernel {
|
|
|
847
881
|
deferred: this.deferred,
|
|
848
882
|
entityAuthAdapter: this.options.entityAuthAdapter || null
|
|
849
883
|
})
|
|
884
|
+
|
|
885
|
+
// Apply toast config if provided (life defaults per severity)
|
|
886
|
+
if (this.options.toast) {
|
|
887
|
+
this.orchestrator.setToastConfig(this.options.toast)
|
|
888
|
+
}
|
|
850
889
|
}
|
|
851
890
|
|
|
852
891
|
/**
|
|
@@ -1060,35 +1099,44 @@ export class Kernel {
|
|
|
1060
1099
|
/**
|
|
1061
1100
|
* Create Vue app instance
|
|
1062
1101
|
*
|
|
1063
|
-
*
|
|
1064
|
-
*
|
|
1102
|
+
* Always wraps the root component with:
|
|
1103
|
+
* - Toast (for global toast notifications on all pages including login)
|
|
1104
|
+
* - QdadmDebugBar (if debugBar is enabled)
|
|
1065
1105
|
*/
|
|
1066
1106
|
_createVueApp() {
|
|
1067
1107
|
if (!this.options.root) {
|
|
1068
1108
|
throw new Error('[Kernel] root component is required')
|
|
1069
1109
|
}
|
|
1070
1110
|
|
|
1071
|
-
// Always wrap root with Toast (and DebugBar if enabled)
|
|
1072
1111
|
const OriginalRoot = this.options.root
|
|
1073
1112
|
const DebugBarComponent = this.options.debugBar?.component && QdadmDebugBar ? QdadmDebugBar : null
|
|
1113
|
+
const hasPrimeVue = !!this.options.primevue?.plugin
|
|
1114
|
+
|
|
1115
|
+
// Wrap root with Toast + ToastListener (for global toasts on all pages) and optionally DebugBar
|
|
1116
|
+
// Note: Apps should NOT include their own <Toast /> - Kernel provides it at root level
|
|
1117
|
+
const WrappedRoot = defineComponent({
|
|
1118
|
+
name: 'QdadmRootWrapper',
|
|
1119
|
+
setup() {
|
|
1120
|
+
return () => {
|
|
1121
|
+
const children = [h(OriginalRoot)]
|
|
1122
|
+
|
|
1123
|
+
// Add global Toast and ToastListener if PrimeVue is configured
|
|
1124
|
+
if (hasPrimeVue) {
|
|
1125
|
+
children.push(h(Toast))
|
|
1126
|
+
children.push(h(ToastListener))
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Add DebugBar if enabled
|
|
1130
|
+
if (DebugBarComponent) {
|
|
1131
|
+
children.push(h(DebugBarComponent))
|
|
1132
|
+
}
|
|
1074
1133
|
|
|
1075
|
-
|
|
1076
|
-
// Note: Toast must be included by apps in their App.vue for pages outside BaseLayout
|
|
1077
|
-
if (DebugBarComponent) {
|
|
1078
|
-
const WrappedRoot = defineComponent({
|
|
1079
|
-
name: 'QdadmRootWrapper',
|
|
1080
|
-
components: { OriginalRoot, DebugBarComponent },
|
|
1081
|
-
setup() {
|
|
1082
|
-
return () => h('div', { id: 'qdadm-root', style: 'display: contents' }, [
|
|
1083
|
-
h(OriginalRoot),
|
|
1084
|
-
h(DebugBarComponent)
|
|
1085
|
-
])
|
|
1134
|
+
return h('div', { id: 'qdadm-root', style: 'display: contents' }, children)
|
|
1086
1135
|
}
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
}
|
|
1136
|
+
}
|
|
1137
|
+
})
|
|
1138
|
+
|
|
1139
|
+
this.vueApp = createApp(WrappedRoot)
|
|
1092
1140
|
}
|
|
1093
1141
|
|
|
1094
1142
|
/**
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* AuthPanel - Auth collector display with activity indicator
|
|
4
4
|
* Each event has its own timer and fades out before destruction
|
|
5
5
|
*/
|
|
6
|
-
import { onMounted, ref, onUnmounted } from 'vue'
|
|
6
|
+
import { onMounted, ref, onUnmounted, inject } from 'vue'
|
|
7
7
|
import ObjectTree from '../ObjectTree.vue'
|
|
8
8
|
|
|
9
9
|
const props = defineProps({
|
|
@@ -11,6 +11,19 @@ const props = defineProps({
|
|
|
11
11
|
entries: { type: Array, required: true }
|
|
12
12
|
})
|
|
13
13
|
|
|
14
|
+
// Inject auth adapter for token loss simulation
|
|
15
|
+
const authAdapter = inject('authAdapter', null)
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Destroy session without normal logout flow
|
|
19
|
+
* Simulates catastrophic token loss (server revocation, storage corruption)
|
|
20
|
+
*/
|
|
21
|
+
function destroySession() {
|
|
22
|
+
if (authAdapter?.destroySession) {
|
|
23
|
+
authAdapter.destroySession()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
14
27
|
// Local events with fade state
|
|
15
28
|
const localEvents = ref([])
|
|
16
29
|
const timers = new Map()
|
|
@@ -173,34 +186,44 @@ function getEventLabel(event) {
|
|
|
173
186
|
|
|
174
187
|
<template>
|
|
175
188
|
<div class="auth-panel">
|
|
176
|
-
<!--
|
|
177
|
-
<div
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
>
|
|
183
|
-
<div class="auth-header" :class="{ 'auth-header--impersonated': entry.type === 'impersonated' }">
|
|
184
|
-
<i :class="['pi', getIcon(entry.type)]" />
|
|
185
|
-
<span class="auth-label">{{ entry.label || entry.type }}</span>
|
|
186
|
-
</div>
|
|
187
|
-
<div v-if="entry.message" class="auth-message">{{ entry.message }}</div>
|
|
188
|
-
<ObjectTree v-else-if="entry.data" :data="entry.data" :maxDepth="4" />
|
|
189
|
+
<!-- Debug actions -->
|
|
190
|
+
<div class="debug-panel-toolbar">
|
|
191
|
+
<button class="debug-toolbar-btn debug-toolbar-btn--danger" @click="destroySession" title="Clear token without logout (simulate session loss)">
|
|
192
|
+
<i class="pi pi-power-off" />
|
|
193
|
+
Destroy Session
|
|
194
|
+
</button>
|
|
189
195
|
</div>
|
|
190
196
|
|
|
191
|
-
|
|
192
|
-
|
|
197
|
+
<div class="auth-panel-content">
|
|
198
|
+
<!-- Invariant entries (user, token, permissions, adapter) -->
|
|
193
199
|
<div
|
|
194
|
-
v-for="
|
|
195
|
-
:key="
|
|
196
|
-
class="auth-
|
|
197
|
-
:class="
|
|
200
|
+
v-for="(entry, idx) in entries"
|
|
201
|
+
:key="idx"
|
|
202
|
+
class="auth-item"
|
|
203
|
+
:class="{ 'auth-item--impersonated': entry.type === 'impersonated' }"
|
|
198
204
|
>
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
205
|
+
<div class="auth-header" :class="{ 'auth-header--impersonated': entry.type === 'impersonated' }">
|
|
206
|
+
<i :class="['pi', getIcon(entry.type)]" />
|
|
207
|
+
<span class="auth-label">{{ entry.label || entry.type }}</span>
|
|
208
|
+
</div>
|
|
209
|
+
<div v-if="entry.message" class="auth-message">{{ entry.message }}</div>
|
|
210
|
+
<ObjectTree v-else-if="entry.data" :data="entry.data" :maxDepth="4" />
|
|
202
211
|
</div>
|
|
203
|
-
|
|
212
|
+
|
|
213
|
+
<!-- Recent auth events with individual timers -->
|
|
214
|
+
<TransitionGroup name="event">
|
|
215
|
+
<div
|
|
216
|
+
v-for="event in localEvents"
|
|
217
|
+
:key="event.id"
|
|
218
|
+
class="auth-activity"
|
|
219
|
+
:class="[event.type, { fading: event.fading }]"
|
|
220
|
+
>
|
|
221
|
+
<i :class="['pi', getEventIcon(event.type)]" />
|
|
222
|
+
<span>{{ getEventLabel(event) }}</span>
|
|
223
|
+
<span class="auth-time">{{ formatTime(event.timestamp) }}</span>
|
|
224
|
+
</div>
|
|
225
|
+
</TransitionGroup>
|
|
226
|
+
</div>
|
|
204
227
|
</div>
|
|
205
228
|
</template>
|
|
206
229
|
|
|
@@ -132,8 +132,8 @@ function getDomainColor(name) {
|
|
|
132
132
|
<button
|
|
133
133
|
v-for="p in presets"
|
|
134
134
|
:key="p.pattern"
|
|
135
|
-
class="
|
|
136
|
-
:class="{ '
|
|
135
|
+
class="debug-toolbar-btn"
|
|
136
|
+
:class="{ 'debug-toolbar-btn--active': filterPattern === p.pattern }"
|
|
137
137
|
@click="applyPreset(p.pattern)"
|
|
138
138
|
>
|
|
139
139
|
{{ p.label }}
|
|
@@ -45,10 +45,10 @@ function isHighlighted(zoneName) {
|
|
|
45
45
|
|
|
46
46
|
<template>
|
|
47
47
|
<div class="zones-panel">
|
|
48
|
-
<div class="
|
|
48
|
+
<div class="debug-panel-toolbar">
|
|
49
49
|
<button
|
|
50
|
-
class="
|
|
51
|
-
:class="{ '
|
|
50
|
+
class="debug-toolbar-btn"
|
|
51
|
+
:class="{ 'debug-toolbar-btn--active': !isFilterActive() }"
|
|
52
52
|
:title="isFilterActive() ? 'Click to show all zones' : 'Click to show current page only'"
|
|
53
53
|
@click="toggleFilter"
|
|
54
54
|
>
|
|
@@ -56,8 +56,8 @@ function isHighlighted(zoneName) {
|
|
|
56
56
|
All Pages
|
|
57
57
|
</button>
|
|
58
58
|
<button
|
|
59
|
-
class="
|
|
60
|
-
:class="{ '
|
|
59
|
+
class="debug-toolbar-btn"
|
|
60
|
+
:class="{ 'debug-toolbar-btn--active': showInternalZones() }"
|
|
61
61
|
:title="showInternalZones() ? 'Click to hide internal zones' : 'Click to show internal zones (prefixed with _)'"
|
|
62
62
|
@click="toggleInternalFilter"
|
|
63
63
|
>
|
|
@@ -65,11 +65,12 @@ function isHighlighted(zoneName) {
|
|
|
65
65
|
Internal
|
|
66
66
|
</button>
|
|
67
67
|
</div>
|
|
68
|
-
<div
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
<div class="zones-panel-content">
|
|
69
|
+
<div v-if="entries.length === 0" class="zones-empty">
|
|
70
|
+
<i class="pi pi-inbox" />
|
|
71
|
+
<span>No zones</span>
|
|
72
|
+
</div>
|
|
73
|
+
<div v-for="zone in entries" :key="zone.name" class="zone-item">
|
|
73
74
|
<div class="zone-header">
|
|
74
75
|
<button
|
|
75
76
|
class="zone-highlight"
|
|
@@ -96,6 +97,7 @@ function isHighlighted(zoneName) {
|
|
|
96
97
|
Default: {{ zone.defaultName || '(component)' }}
|
|
97
98
|
</div>
|
|
98
99
|
</div>
|
|
100
|
+
</div>
|
|
99
101
|
</div>
|
|
100
102
|
</template>
|
|
101
103
|
|
|
@@ -638,6 +638,104 @@ $debug-info: #3b82f6;
|
|
|
638
638
|
color: #f4f4f5;
|
|
639
639
|
}
|
|
640
640
|
|
|
641
|
+
/* =============================================================================
|
|
642
|
+
PANEL TOOLBAR (unified action bar at top of panels)
|
|
643
|
+
============================================================================= */
|
|
644
|
+
|
|
645
|
+
.debug-panel-toolbar {
|
|
646
|
+
display: flex;
|
|
647
|
+
align-items: center;
|
|
648
|
+
gap: 8px;
|
|
649
|
+
padding: 8px 12px;
|
|
650
|
+
background: #27272a;
|
|
651
|
+
border-bottom: 1px solid #3f3f46;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/* Toolbar button - standard */
|
|
655
|
+
.debug-toolbar-btn {
|
|
656
|
+
display: inline-flex;
|
|
657
|
+
align-items: center;
|
|
658
|
+
gap: 4px;
|
|
659
|
+
padding: 3px 8px;
|
|
660
|
+
background: #3f3f46;
|
|
661
|
+
border: none;
|
|
662
|
+
border-radius: 3px;
|
|
663
|
+
color: #a1a1aa;
|
|
664
|
+
cursor: pointer;
|
|
665
|
+
font-size: 10px;
|
|
666
|
+
white-space: nowrap;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.debug-toolbar-btn:hover {
|
|
670
|
+
background: #52525b;
|
|
671
|
+
color: #f4f4f5;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.debug-toolbar-btn .pi {
|
|
675
|
+
font-size: 10px;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/* Toolbar button - danger variant */
|
|
679
|
+
.debug-toolbar-btn--danger {
|
|
680
|
+
background: rgba(239, 68, 68, 0.15);
|
|
681
|
+
color: #f87171;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.debug-toolbar-btn--danger:hover {
|
|
685
|
+
background: rgba(239, 68, 68, 0.3);
|
|
686
|
+
color: #fca5a5;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/* Toolbar button - active/pressed state */
|
|
690
|
+
.debug-toolbar-btn--active {
|
|
691
|
+
background: #8b5cf6;
|
|
692
|
+
color: white;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.debug-toolbar-btn--active:hover {
|
|
696
|
+
background: #7c3aed;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/* Toolbar input */
|
|
700
|
+
.debug-toolbar-input {
|
|
701
|
+
flex: 1;
|
|
702
|
+
min-width: 0;
|
|
703
|
+
font-size: 11px;
|
|
704
|
+
padding: 4px 8px;
|
|
705
|
+
background: #18181b;
|
|
706
|
+
border: 1px solid #3f3f46;
|
|
707
|
+
border-radius: 4px;
|
|
708
|
+
color: #f4f4f5;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.debug-toolbar-input:focus {
|
|
712
|
+
outline: none;
|
|
713
|
+
border-color: #8b5cf6;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
.debug-toolbar-input::placeholder {
|
|
717
|
+
color: #71717a;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/* Toolbar icon (standalone) */
|
|
721
|
+
.debug-toolbar-icon {
|
|
722
|
+
color: #71717a;
|
|
723
|
+
font-size: 12px;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/* Toolbar label/text */
|
|
727
|
+
.debug-toolbar-label {
|
|
728
|
+
font-size: 10px;
|
|
729
|
+
color: #71717a;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/* Toolbar separator */
|
|
733
|
+
.debug-toolbar-separator {
|
|
734
|
+
width: 1px;
|
|
735
|
+
height: 16px;
|
|
736
|
+
background: #3f3f46;
|
|
737
|
+
}
|
|
738
|
+
|
|
641
739
|
/* =============================================================================
|
|
642
740
|
OBJECT TREE
|
|
643
741
|
============================================================================= */
|
|
@@ -726,7 +824,14 @@ $debug-info: #3b82f6;
|
|
|
726
824
|
============================================================================= */
|
|
727
825
|
|
|
728
826
|
.auth-panel {
|
|
729
|
-
padding:
|
|
827
|
+
padding: 0;
|
|
828
|
+
display: flex;
|
|
829
|
+
flex-direction: column;
|
|
830
|
+
gap: 8px;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
.auth-panel-content {
|
|
834
|
+
padding: 0 8px 8px 8px;
|
|
730
835
|
display: flex;
|
|
731
836
|
flex-direction: column;
|
|
732
837
|
gap: 8px;
|
|
@@ -2420,7 +2525,14 @@ $debug-info: #3b82f6;
|
|
|
2420
2525
|
============================================================================= */
|
|
2421
2526
|
|
|
2422
2527
|
.zones-panel {
|
|
2423
|
-
padding:
|
|
2528
|
+
padding: 0;
|
|
2529
|
+
display: flex;
|
|
2530
|
+
flex-direction: column;
|
|
2531
|
+
gap: 8px;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
.zones-panel-content {
|
|
2535
|
+
padding: 0 8px 8px 8px;
|
|
2424
2536
|
display: flex;
|
|
2425
2537
|
flex-direction: column;
|
|
2426
2538
|
gap: 8px;
|
|
@@ -75,6 +75,63 @@ export class Orchestrator {
|
|
|
75
75
|
return this._signals
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Set toast configuration (life defaults per severity)
|
|
80
|
+
* @param {Object} config - { success: 3000, error: 5000, warn: 5000, info: 3000 }
|
|
81
|
+
*/
|
|
82
|
+
setToastConfig(config) {
|
|
83
|
+
this._toastConfig = { ...this._toastConfig, ...config }
|
|
84
|
+
this._toastHelper = null // Reset helper to pick up new config
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Toast helper - wraps signal-based toasts
|
|
89
|
+
*
|
|
90
|
+
* Usage:
|
|
91
|
+
* orchestrator.toast.success('Saved', 'Item saved successfully')
|
|
92
|
+
* orchestrator.toast.error('Error', 'Failed to save', 'MyComponent')
|
|
93
|
+
* orchestrator.toast.warn('Warning', 'Check your input', { life: 10000, emitter: 'Custom' })
|
|
94
|
+
*
|
|
95
|
+
* Third param can be:
|
|
96
|
+
* - string: treated as emitter
|
|
97
|
+
* - object: { life, emitter }
|
|
98
|
+
*
|
|
99
|
+
* @returns {Object} Toast helper with success/error/warn/info methods
|
|
100
|
+
*/
|
|
101
|
+
get toast() {
|
|
102
|
+
if (!this._toastHelper) {
|
|
103
|
+
// Default life values per severity (can be overridden via setToastConfig)
|
|
104
|
+
const defaults = {
|
|
105
|
+
success: 3000,
|
|
106
|
+
error: 5000,
|
|
107
|
+
warn: 5000,
|
|
108
|
+
info: 3000,
|
|
109
|
+
...this._toastConfig
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const emit = (severity, summary, detail, options) => {
|
|
113
|
+
if (this._signals) {
|
|
114
|
+
// Options can be string (emitter) or object { life, emitter }
|
|
115
|
+
const opts = typeof options === 'string' ? { emitter: options } : (options || {})
|
|
116
|
+
this._signals.emit(`toast:${severity}`, {
|
|
117
|
+
summary,
|
|
118
|
+
detail,
|
|
119
|
+
life: opts.life ?? defaults[severity],
|
|
120
|
+
emitter: opts.emitter
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this._toastHelper = {
|
|
126
|
+
success: (summary, detail, options) => emit('success', summary, detail, options),
|
|
127
|
+
error: (summary, detail, options) => emit('error', summary, detail, options),
|
|
128
|
+
warn: (summary, detail, options) => emit('warn', summary, detail, options),
|
|
129
|
+
info: (summary, detail, options) => emit('info', summary, detail, options)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return this._toastHelper
|
|
133
|
+
}
|
|
134
|
+
|
|
78
135
|
/**
|
|
79
136
|
* Set the entity AuthAdapter for permission checks
|
|
80
137
|
* This adapter will be injected into all newly registered managers
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* qdadm - Button Styles
|
|
3
|
+
*
|
|
4
|
+
* Global button styling - outlined style as default
|
|
5
|
+
* with severity variants (primary, secondary, success, warn, danger)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
@use 'variables' as *;
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Base Button - Outlined by Default
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
.p-button {
|
|
15
|
+
// Outlined style as default (use !important to override PrimeVue theme)
|
|
16
|
+
background: transparent !important;
|
|
17
|
+
border: 1px solid var(--p-primary-500) !important;
|
|
18
|
+
color: var(--p-primary-500) !important;
|
|
19
|
+
transition: background-color 0.2s, border-color 0.2s, color 0.2s;
|
|
20
|
+
|
|
21
|
+
&:hover:not(:disabled) {
|
|
22
|
+
background: var(--p-primary-50) !important;
|
|
23
|
+
border-color: var(--p-primary-600) !important;
|
|
24
|
+
color: var(--p-primary-600) !important;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
&:focus {
|
|
28
|
+
box-shadow: 0 0 0 2px var(--p-surface-0), 0 0 0 4px var(--p-primary-200);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
&:disabled {
|
|
32
|
+
opacity: 0.6;
|
|
33
|
+
cursor: not-allowed;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// Severity Variants
|
|
39
|
+
// =============================================================================
|
|
40
|
+
|
|
41
|
+
// Secondary (gray)
|
|
42
|
+
.p-button-secondary {
|
|
43
|
+
border-color: var(--p-surface-400) !important;
|
|
44
|
+
color: var(--p-surface-600) !important;
|
|
45
|
+
|
|
46
|
+
&:hover:not(:disabled) {
|
|
47
|
+
background: var(--p-surface-100) !important;
|
|
48
|
+
border-color: var(--p-surface-500) !important;
|
|
49
|
+
color: var(--p-surface-700) !important;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
&:focus {
|
|
53
|
+
box-shadow: 0 0 0 2px var(--p-surface-0), 0 0 0 4px var(--p-surface-200);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Success (green)
|
|
58
|
+
.p-button-success {
|
|
59
|
+
border-color: var(--p-green-500) !important;
|
|
60
|
+
color: var(--p-green-600) !important;
|
|
61
|
+
|
|
62
|
+
&:hover:not(:disabled) {
|
|
63
|
+
background: var(--p-green-50) !important;
|
|
64
|
+
border-color: var(--p-green-600) !important;
|
|
65
|
+
color: var(--p-green-700) !important;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
&:focus {
|
|
69
|
+
box-shadow: 0 0 0 2px var(--p-surface-0), 0 0 0 4px var(--p-green-200);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Warning (orange)
|
|
74
|
+
.p-button-warn {
|
|
75
|
+
border-color: var(--p-orange-500) !important;
|
|
76
|
+
color: var(--p-orange-600) !important;
|
|
77
|
+
|
|
78
|
+
&:hover:not(:disabled) {
|
|
79
|
+
background: var(--p-orange-50) !important;
|
|
80
|
+
border-color: var(--p-orange-600) !important;
|
|
81
|
+
color: var(--p-orange-700) !important;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
&:focus {
|
|
85
|
+
box-shadow: 0 0 0 2px var(--p-surface-0), 0 0 0 4px var(--p-orange-200);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Danger (red)
|
|
90
|
+
.p-button-danger {
|
|
91
|
+
border-color: var(--p-red-500) !important;
|
|
92
|
+
color: var(--p-red-600) !important;
|
|
93
|
+
|
|
94
|
+
&:hover:not(:disabled) {
|
|
95
|
+
background: var(--p-red-50) !important;
|
|
96
|
+
border-color: var(--p-red-600) !important;
|
|
97
|
+
color: var(--p-red-700) !important;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
&:focus {
|
|
101
|
+
box-shadow: 0 0 0 2px var(--p-surface-0), 0 0 0 4px var(--p-red-200);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Info (blue)
|
|
106
|
+
.p-button-info {
|
|
107
|
+
border-color: var(--p-blue-500) !important;
|
|
108
|
+
color: var(--p-blue-600) !important;
|
|
109
|
+
|
|
110
|
+
&:hover:not(:disabled) {
|
|
111
|
+
background: var(--p-blue-50) !important;
|
|
112
|
+
border-color: var(--p-blue-600) !important;
|
|
113
|
+
color: var(--p-blue-700) !important;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
&:focus {
|
|
117
|
+
box-shadow: 0 0 0 2px var(--p-surface-0), 0 0 0 4px var(--p-blue-200);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Contrast (dark)
|
|
122
|
+
.p-button-contrast {
|
|
123
|
+
border-color: var(--p-surface-900) !important;
|
|
124
|
+
color: var(--p-surface-900) !important;
|
|
125
|
+
|
|
126
|
+
&:hover:not(:disabled) {
|
|
127
|
+
background: var(--p-surface-900) !important;
|
|
128
|
+
border-color: var(--p-surface-900) !important;
|
|
129
|
+
color: var(--p-surface-0) !important;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// =============================================================================
|
|
134
|
+
// Filled Variant (explicit)
|
|
135
|
+
// =============================================================================
|
|
136
|
+
|
|
137
|
+
.p-button-filled,
|
|
138
|
+
.p-button.p-button-raised {
|
|
139
|
+
background: var(--p-primary-500) !important;
|
|
140
|
+
border-color: var(--p-primary-500) !important;
|
|
141
|
+
color: white !important;
|
|
142
|
+
|
|
143
|
+
&:hover:not(:disabled) {
|
|
144
|
+
background: var(--p-primary-600) !important;
|
|
145
|
+
border-color: var(--p-primary-600) !important;
|
|
146
|
+
color: white !important;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
&.p-button-secondary {
|
|
150
|
+
background: var(--p-surface-500) !important;
|
|
151
|
+
border-color: var(--p-surface-500) !important;
|
|
152
|
+
color: white !important;
|
|
153
|
+
|
|
154
|
+
&:hover:not(:disabled) {
|
|
155
|
+
background: var(--p-surface-600) !important;
|
|
156
|
+
border-color: var(--p-surface-600) !important;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
&.p-button-success {
|
|
161
|
+
background: var(--p-green-500) !important;
|
|
162
|
+
border-color: var(--p-green-500) !important;
|
|
163
|
+
color: white !important;
|
|
164
|
+
|
|
165
|
+
&:hover:not(:disabled) {
|
|
166
|
+
background: var(--p-green-600) !important;
|
|
167
|
+
border-color: var(--p-green-600) !important;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
&.p-button-warn {
|
|
172
|
+
background: var(--p-orange-500) !important;
|
|
173
|
+
border-color: var(--p-orange-500) !important;
|
|
174
|
+
color: white !important;
|
|
175
|
+
|
|
176
|
+
&:hover:not(:disabled) {
|
|
177
|
+
background: var(--p-orange-600) !important;
|
|
178
|
+
border-color: var(--p-orange-600) !important;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
&.p-button-danger {
|
|
183
|
+
background: var(--p-red-500) !important;
|
|
184
|
+
border-color: var(--p-red-500) !important;
|
|
185
|
+
color: white !important;
|
|
186
|
+
|
|
187
|
+
&:hover:not(:disabled) {
|
|
188
|
+
background: var(--p-red-600) !important;
|
|
189
|
+
border-color: var(--p-red-600) !important;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// =============================================================================
|
|
195
|
+
// Text Button (no border)
|
|
196
|
+
// =============================================================================
|
|
197
|
+
|
|
198
|
+
.p-button-text {
|
|
199
|
+
background: transparent !important;
|
|
200
|
+
border-color: transparent !important;
|
|
201
|
+
|
|
202
|
+
&:hover:not(:disabled) {
|
|
203
|
+
background: var(--p-primary-50) !important;
|
|
204
|
+
border-color: transparent !important;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
&.p-button-secondary:hover:not(:disabled) {
|
|
208
|
+
background: var(--p-surface-100) !important;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
&.p-button-success:hover:not(:disabled) {
|
|
212
|
+
background: var(--p-green-50) !important;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
&.p-button-warn:hover:not(:disabled) {
|
|
216
|
+
background: var(--p-orange-50) !important;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
&.p-button-danger:hover:not(:disabled) {
|
|
220
|
+
background: var(--p-red-50) !important;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// =============================================================================
|
|
225
|
+
// Link Button
|
|
226
|
+
// =============================================================================
|
|
227
|
+
|
|
228
|
+
.p-button-link {
|
|
229
|
+
background: transparent !important;
|
|
230
|
+
border-color: transparent !important;
|
|
231
|
+
color: var(--p-primary-500) !important;
|
|
232
|
+
text-decoration: none;
|
|
233
|
+
|
|
234
|
+
&:hover:not(:disabled) {
|
|
235
|
+
background: transparent !important;
|
|
236
|
+
border-color: transparent !important;
|
|
237
|
+
text-decoration: underline;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// =============================================================================
|
|
242
|
+
// Icon-only Button - Keep original PrimeVue style (no outlined override)
|
|
243
|
+
// =============================================================================
|
|
244
|
+
|
|
245
|
+
.p-button-icon-only {
|
|
246
|
+
// Reset to PrimeVue defaults - no outlined style for icon buttons
|
|
247
|
+
background: unset !important;
|
|
248
|
+
border: unset !important;
|
|
249
|
+
color: unset !important;
|
|
250
|
+
|
|
251
|
+
&:hover:not(:disabled) {
|
|
252
|
+
background: unset !important;
|
|
253
|
+
border-color: unset !important;
|
|
254
|
+
color: unset !important;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// =============================================================================
|
|
259
|
+
// Button Sizes
|
|
260
|
+
// =============================================================================
|
|
261
|
+
|
|
262
|
+
.p-button-sm {
|
|
263
|
+
font-size: $font-size-sm;
|
|
264
|
+
padding: 0.375rem 0.75rem;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.p-button-lg {
|
|
268
|
+
font-size: $font-size-lg;
|
|
269
|
+
padding: 0.75rem 1.25rem;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// =============================================================================
|
|
273
|
+
// Tags / Badges - Outlined Style
|
|
274
|
+
// =============================================================================
|
|
275
|
+
|
|
276
|
+
.p-tag {
|
|
277
|
+
background: transparent !important;
|
|
278
|
+
border: 1px solid var(--p-primary-500) !important;
|
|
279
|
+
color: var(--p-primary-500) !important;
|
|
280
|
+
font-weight: $font-weight-medium;
|
|
281
|
+
white-space: nowrap;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.p-tag-secondary {
|
|
285
|
+
background: transparent !important;
|
|
286
|
+
border-color: var(--p-surface-400) !important;
|
|
287
|
+
color: var(--p-surface-600) !important;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.p-tag-success {
|
|
291
|
+
background: transparent !important;
|
|
292
|
+
border-color: var(--p-green-500) !important;
|
|
293
|
+
color: var(--p-green-600) !important;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.p-tag-warn {
|
|
297
|
+
background: transparent !important;
|
|
298
|
+
border-color: var(--p-orange-500) !important;
|
|
299
|
+
color: var(--p-orange-600) !important;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.p-tag-danger {
|
|
303
|
+
background: transparent !important;
|
|
304
|
+
border-color: var(--p-red-500) !important;
|
|
305
|
+
color: var(--p-red-600) !important;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.p-tag-info {
|
|
309
|
+
background: transparent !important;
|
|
310
|
+
border-color: var(--p-blue-500) !important;
|
|
311
|
+
color: var(--p-blue-600) !important;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.p-tag-contrast {
|
|
315
|
+
background: transparent !important;
|
|
316
|
+
border-color: var(--p-surface-900) !important;
|
|
317
|
+
color: var(--p-surface-900) !important;
|
|
318
|
+
}
|
package/src/styles/_lists.scss
CHANGED
|
@@ -12,6 +12,20 @@
|
|
|
12
12
|
|
|
13
13
|
@use 'variables' as *;
|
|
14
14
|
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// DataTable Compact Rows
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
.p-datatable {
|
|
20
|
+
.p-datatable-tbody > tr > td {
|
|
21
|
+
padding: 0.5rem 0.75rem !important;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.p-datatable-thead > tr > th {
|
|
25
|
+
padding: 0.625rem 0.75rem !important;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
// =============================================================================
|
|
16
30
|
// Table Actions
|
|
17
31
|
// =============================================================================
|
|
@@ -195,9 +195,16 @@
|
|
|
195
195
|
.form-actions {
|
|
196
196
|
@include mobile {
|
|
197
197
|
flex-direction: column;
|
|
198
|
+
gap: 0.5rem;
|
|
199
|
+
|
|
200
|
+
.form-actions-left {
|
|
201
|
+
width: 100%;
|
|
202
|
+
flex-direction: column;
|
|
203
|
+
}
|
|
198
204
|
|
|
199
205
|
.p-button {
|
|
200
206
|
width: 100%;
|
|
207
|
+
white-space: nowrap;
|
|
201
208
|
}
|
|
202
209
|
}
|
|
203
210
|
}
|
package/src/styles/index.scss
CHANGED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
/**
|
|
3
|
-
* DefaultToaster - Default toaster component for BaseLayout
|
|
4
|
-
*
|
|
5
|
-
* Renders the Toast component for notifications.
|
|
6
|
-
* Uses PrimeVue Toast with default positioning.
|
|
7
|
-
*
|
|
8
|
-
* NOTE: For pages outside BaseLayout (like LoginPage),
|
|
9
|
-
* apps must include Toast in their App.vue root component.
|
|
10
|
-
*/
|
|
11
|
-
import Toast from 'primevue/toast'
|
|
12
|
-
</script>
|
|
13
|
-
|
|
14
|
-
<template>
|
|
15
|
-
<Toast position="top-right" />
|
|
16
|
-
</template>
|