@quazardous/qdadm 2.20.0 → 2.22.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/dist/node/vite/qdadmDebugPlugin.d.ts +9 -4
- package/dist/node/vite/qdadmDebugPlugin.js +36 -10
- package/dist/node/vite/qdadmPlugin.js +2 -0
- package/dist/styles/qdadm.css +1 -0
- package/package.json +10 -4
- package/src/components/edit/FormField.vue +51 -4
- package/src/components/edit/FormInput.vue +44 -2
- package/src/components/edit/formFieldIds.ts +11 -0
- package/src/components/editors/RoleGrantsEditor.vue +355 -0
- package/src/components/index.ts +1 -0
- package/src/components/show/ShowPage.vue +4 -2
- package/src/composables/confirmDelete.ts +35 -0
- package/src/composables/index.ts +1 -0
- package/src/composables/useEntityItemFormPage.ts +2 -3
- package/src/composables/useEntityItemFormPage.types.ts +1 -0
- package/src/composables/useEntityItemShowPage.ts +3 -3
- package/src/composables/useListPage.ts +3 -6
- package/src/composables/useListPage.types.ts +1 -0
- package/src/composables/useSecurity.ts +41 -0
- package/src/entity/auth/SecurityChecker.ts +88 -2
- package/src/entity/auth/index.ts +2 -0
- package/src/i18n/defaults/DefaultCoreProvider.ts +4 -2
- package/src/i18n/defaults/__tests__/generated-sync.test.ts +9 -0
- package/src/i18n/defaults/core.en.generated.ts +20 -0
- package/src/i18n/defaults/core.en.yml +20 -0
- package/src/i18n/defaults/core.fr.generated.ts +20 -0
- package/src/i18n/defaults/core.fr.yml +20 -0
- package/src/index.ts +1 -0
- package/src/kernel/Kernel.auth.ts +4 -0
- package/src/kernel/Kernel.registries.ts +24 -5
- package/src/kernel/Kernel.routing.ts +20 -4
- package/src/kernel/Kernel.ts +3 -0
- package/src/kernel/Kernel.types.ts +7 -1
- package/src/kernel/Kernel.vue.ts +3 -0
- package/src/kernel/SignalBus.ts +6 -0
- package/src/kernel/chunkReload.ts +110 -0
- package/src/kernel/vitePluginCheck.ts +61 -0
- package/src/modules/debug/AuthCollector.ts +15 -1
- package/src/modules/debug/DebugModule.ts +9 -0
- package/src/modules/debug/components/panels/AuthPanel.vue +1 -0
- package/src/notifications/NotificationListener.vue +2 -1
- package/src/security/RolesProvider.ts +8 -0
- package/src/security/index.ts +17 -0
- package/src/security/pages/RoleForm.vue +42 -191
- package/src/security/roleGrants.ts +210 -0
- package/src/styles/_forms.scss +10 -2
- package/src/styles/index.d.ts +3 -4
- package/src/toast/ToastListener.vue +2 -1
- package/src/types/index.ts +10 -0
- package/src/vite/qdadmDebugPlugin.ts +46 -10
- package/src/vite/qdadmPlugin.ts +2 -0
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
* GET /__qdadm/snapshot.json — bridge.dump() (live state)
|
|
9
9
|
* POST /__qdadm/call — body {collector, action, args?}
|
|
10
10
|
*
|
|
11
|
-
* Session selection: every browser tab
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Session selection: every browser tab gets a session id (uuid in the
|
|
12
|
+
* injected client), kept in sessionStorage so a reload is the SAME session
|
|
13
|
+
* (#2231). A tab that says bye stays known through a short grace window: a
|
|
14
|
+
* request to it answers "reloading" instead of timing out. All endpoints
|
|
15
|
+
* accept a `?session=<id|latest>` query param. Default is `latest` (the most
|
|
16
|
+
* recently active connected session).
|
|
15
17
|
*
|
|
16
18
|
* Wire-up: the plugin injects a small ESM script via `transformIndexHtml`.
|
|
17
19
|
* That inline script lives in Vite's HMR pipeline (so `import.meta.hot`
|
|
@@ -44,6 +46,7 @@ export interface QdadmDebugPluginApi {
|
|
|
44
46
|
ageMs: number;
|
|
45
47
|
hasDescribe: boolean;
|
|
46
48
|
hasSnapshot: boolean;
|
|
49
|
+
connected: boolean;
|
|
47
50
|
meta: Record<string, unknown>;
|
|
48
51
|
}>;
|
|
49
52
|
prefix: string;
|
|
@@ -55,6 +58,8 @@ export interface QdadmDebugPluginOptions {
|
|
|
55
58
|
timeoutMs?: number;
|
|
56
59
|
/** Drop sessions inactive for this many ms (default 10 min) */
|
|
57
60
|
sessionTtlMs?: number;
|
|
61
|
+
/** Keep a tab that said bye this long, so its reload keeps it (default 30 s) */
|
|
62
|
+
reloadGraceMs?: number;
|
|
58
63
|
}
|
|
59
64
|
export declare function qdadmDebugPlugin(options?: QdadmDebugPluginOptions): Plugin;
|
|
60
65
|
export default qdadmDebugPlugin;
|
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
* GET /__qdadm/snapshot.json — bridge.dump() (live state)
|
|
9
9
|
* POST /__qdadm/call — body {collector, action, args?}
|
|
10
10
|
*
|
|
11
|
-
* Session selection: every browser tab
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Session selection: every browser tab gets a session id (uuid in the
|
|
12
|
+
* injected client), kept in sessionStorage so a reload is the SAME session
|
|
13
|
+
* (#2231). A tab that says bye stays known through a short grace window: a
|
|
14
|
+
* request to it answers "reloading" instead of timing out. All endpoints
|
|
15
|
+
* accept a `?session=<id|latest>` query param. Default is `latest` (the most
|
|
16
|
+
* recently active connected session).
|
|
15
17
|
*
|
|
16
18
|
* Wire-up: the plugin injects a small ESM script via `transformIndexHtml`.
|
|
17
19
|
* That inline script lives in Vite's HMR pipeline (so `import.meta.hot`
|
|
@@ -28,23 +30,31 @@ export function qdadmDebugPlugin(options = {}) {
|
|
|
28
30
|
const prefix = options.prefix ?? '/__qdadm';
|
|
29
31
|
const timeoutMs = options.timeoutMs ?? 3000;
|
|
30
32
|
const sessionTtlMs = options.sessionTtlMs ?? 10 * 60 * 1000;
|
|
33
|
+
const reloadGraceMs = options.reloadGraceMs ?? 30 * 1000;
|
|
31
34
|
const sessions = new Map();
|
|
32
35
|
const pending = new Map();
|
|
33
36
|
let server = null;
|
|
34
37
|
let nextId = 1;
|
|
35
38
|
function pruneStaleSessions() {
|
|
36
|
-
const
|
|
39
|
+
const now = Date.now();
|
|
37
40
|
for (const [id, s] of sessions) {
|
|
38
|
-
if (s.lastSeenAt <
|
|
41
|
+
if (s.lastSeenAt < now - sessionTtlMs)
|
|
42
|
+
sessions.delete(id);
|
|
43
|
+
else if (s.disconnectedAt !== null && s.disconnectedAt < now - reloadGraceMs)
|
|
39
44
|
sessions.delete(id);
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
47
|
function pickSession(sessionParam) {
|
|
43
48
|
pruneStaleSessions();
|
|
44
49
|
if (!sessionParam || sessionParam === 'latest') {
|
|
50
|
+
// A connected tab wins; a reloading one only when nothing else is up,
|
|
51
|
+
// so the caller hears "reloading" rather than "no session".
|
|
45
52
|
let best = null;
|
|
46
53
|
for (const s of sessions.values()) {
|
|
47
|
-
|
|
54
|
+
const better = !best ||
|
|
55
|
+
(best.disconnectedAt !== null && s.disconnectedAt === null) ||
|
|
56
|
+
((best.disconnectedAt === null) === (s.disconnectedAt === null) && s.lastSeenAt > best.lastSeenAt);
|
|
57
|
+
if (better)
|
|
48
58
|
best = s;
|
|
49
59
|
}
|
|
50
60
|
return best;
|
|
@@ -54,6 +64,11 @@ export function qdadmDebugPlugin(options = {}) {
|
|
|
54
64
|
function ask(type, payload, sessionId) {
|
|
55
65
|
if (!server)
|
|
56
66
|
return Promise.reject(new Error('[qdadm-debug] vite server not ready'));
|
|
67
|
+
const target = sessionId ? sessions.get(sessionId) : undefined;
|
|
68
|
+
if (target?.disconnectedAt) {
|
|
69
|
+
const away = Math.round((Date.now() - target.disconnectedAt) / 1000);
|
|
70
|
+
return Promise.reject(new Error(`[qdadm-debug] session ${target.id.slice(0, 8)} left ${away}s ago — the tab is most likely reloading; retry in a few seconds`));
|
|
71
|
+
}
|
|
57
72
|
const id = String(nextId++);
|
|
58
73
|
return new Promise((resolve, reject) => {
|
|
59
74
|
const timeout = setTimeout(() => {
|
|
@@ -105,6 +120,7 @@ export function qdadmDebugPlugin(options = {}) {
|
|
|
105
120
|
ageMs: now - s.lastSeenAt,
|
|
106
121
|
hasDescribe: !!s.describe,
|
|
107
122
|
hasSnapshot: !!s.snapshot,
|
|
123
|
+
connected: s.disconnectedAt === null,
|
|
108
124
|
meta: s.meta,
|
|
109
125
|
}));
|
|
110
126
|
}
|
|
@@ -118,11 +134,13 @@ export function qdadmDebugPlugin(options = {}) {
|
|
|
118
134
|
describe: null,
|
|
119
135
|
snapshot: null,
|
|
120
136
|
meta: meta ?? {},
|
|
137
|
+
disconnectedAt: null,
|
|
121
138
|
};
|
|
122
139
|
sessions.set(id, s);
|
|
123
140
|
}
|
|
124
141
|
else {
|
|
125
142
|
s.lastSeenAt = Date.now();
|
|
143
|
+
s.disconnectedAt = null;
|
|
126
144
|
if (meta)
|
|
127
145
|
s.meta = { ...s.meta, ...meta };
|
|
128
146
|
}
|
|
@@ -180,7 +198,7 @@ export function qdadmDebugPlugin(options = {}) {
|
|
|
180
198
|
session.snapshot = { data: msg.data, at };
|
|
181
199
|
}
|
|
182
200
|
else if (msg.type === 'bye') {
|
|
183
|
-
|
|
201
|
+
session.disconnectedAt = at;
|
|
184
202
|
}
|
|
185
203
|
});
|
|
186
204
|
s.middlewares.use(async (req, res, next) => {
|
|
@@ -261,7 +279,8 @@ function readBody(req) {
|
|
|
261
279
|
* Client-side bridge — injected verbatim into index.html as an ES module.
|
|
262
280
|
* Lives in Vite's HMR pipeline (so `import.meta.hot` works here).
|
|
263
281
|
*
|
|
264
|
-
* Each tab gets its own session id (random uuid
|
|
282
|
+
* Each tab gets its own session id (random uuid, kept across reloads in
|
|
283
|
+
* sessionStorage — #2231). Pushes describe + snapshot
|
|
265
284
|
* on every reactive tick of the bridge so the plugin's per-session cache
|
|
266
285
|
* stays fresh. Listens for ad-hoc requests targeted at this session.
|
|
267
286
|
*/
|
|
@@ -298,7 +317,14 @@ const hot = import.meta.hot
|
|
|
298
317
|
if (!hot) {
|
|
299
318
|
console.warn('[qdadm-debug] import.meta.hot unavailable in injected script')
|
|
300
319
|
} else {
|
|
301
|
-
|
|
320
|
+
// Same tab, same session across reloads (#2231): an agent's target survives F5.
|
|
321
|
+
const sessionId = (() => {
|
|
322
|
+
try {
|
|
323
|
+
let v = sessionStorage.getItem('qdadm-debug:session')
|
|
324
|
+
if (!v) { v = crypto.randomUUID(); sessionStorage.setItem('qdadm-debug:session', v) }
|
|
325
|
+
return v
|
|
326
|
+
} catch (e) { return crypto.randomUUID() }
|
|
327
|
+
})()
|
|
302
328
|
const meta = { userAgent: navigator.userAgent, location: location.pathname }
|
|
303
329
|
let bridge = null
|
|
304
330
|
let lastTick = -1
|
|
@@ -85,6 +85,8 @@ export function qdadmVitePlugin(options = {}) {
|
|
|
85
85
|
exclude: [...widgetExcludes, ...themesExcludes, '@quazardous/qdadm'],
|
|
86
86
|
},
|
|
87
87
|
...(fsAllow.length > 0 ? { server: { fs: { allow: fsAllow } } } : {}),
|
|
88
|
+
// The kernel checks it: an app without this plugin is told what to add (#2259).
|
|
89
|
+
define: { __QDADM_VITE_PLUGIN__: 'true' },
|
|
88
90
|
};
|
|
89
91
|
},
|
|
90
92
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--fad-text-primary: var(--p-text-color);--fad-text-secondary: var(--p-text-muted-color);--fad-text-disabled: var(--p-surface-400);--fad-text-inverse: var(--p-surface-0);--fad-surface-ground: var(--p-surface-ground);--fad-surface-card: var(--p-surface-card);--fad-surface-overlay: var(--p-surface-overlay);--fad-surface-border: var(--p-surface-200);--fad-surface-hover: var(--p-surface-100);--fad-primary: var(--p-primary-color);--fad-primary-hover: var(--p-primary-600);--fad-primary-text: var(--p-primary-contrast-color);--fad-success: var(--p-green-500);--fad-warning: var(--p-orange-500);--fad-danger: var(--p-red-500);--fad-info: var(--p-blue-500);--fad-space-2xs: 0.125rem;--fad-space-xs: 0.25rem;--fad-space-sm: 0.375rem;--fad-space-md: 0.75rem;--fad-space-lg: 1rem;--fad-space-xl: 1.5rem;--fad-space-2xl: 2rem;--fad-font-family: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif;--fad-font-size-2xs: 0.6875rem;--fad-font-size-xs: 0.75rem;--fad-font-size-sm: 0.875rem;--fad-font-size-md: 0.9375rem;--fad-font-size-lg: 1rem;--fad-font-size-xl: 1.125rem;--fad-font-size-2xl: 1.25rem;--fad-font-weight-light: 300;--fad-font-weight-normal: 400;--fad-font-weight-medium: 500;--fad-font-weight-semibold: 600;--fad-font-weight-bold: 700;--fad-line-height-tight: 1.25;--fad-line-height-normal: 1.5;--fad-line-height-relaxed: 1.75;--fad-border-width: 1px;--fad-border-color: var(--fad-surface-border);--fad-radius-sm: 0.25rem;--fad-radius-md: 0.375rem;--fad-radius-lg: 0.5rem;--fad-radius-xl: 0.75rem;--fad-radius-full: 9999px;--fad-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);--fad-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);--fad-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);--fad-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);--fad-transition-instant: 50ms ease;--fad-transition-fast: 100ms ease;--fad-transition-normal: 150ms ease;--fad-transition-slow: 200ms ease;--fad-z-dropdown: 1000;--fad-z-sticky: 1020;--fad-z-fixed: 1030;--fad-z-modal-backdrop: 1040;--fad-z-modal: 1050;--fad-z-popover: 1060;--fad-z-tooltip: 1070;--fad-sidebar-width: 15rem;--fad-sidebar-width-tablet: 13rem;--fad-sidebar-width-collapsed: 2.5rem;--fad-header-height: 2.75rem;--fad-content-max-width: 87.5rem;--fad-nav-item-height: 2rem;--fad-nav-item-padding-x: 0.75rem;--fad-nav-item-padding-y: 0.5rem;--fad-nav-icon-size: 1rem;--fad-nav-section-gap: 0.5rem}.dark-mode{--fad-surface-border: var(--p-surface-700);--fad-surface-hover: var(--p-surface-800);--fad-text-disabled: var(--p-surface-500)}.d-none{display:none !important}.d-block{display:block !important}.d-flex{display:flex !important}.d-inline-flex{display:inline-flex !important}.d-grid{display:grid !important}.flex-row{flex-direction:row}.flex-column{flex-direction:column}.flex-wrap{flex-wrap:wrap}.flex-1{flex:1}.flex-auto{flex:auto}.flex-none{flex:none}.items-start{align-items:flex-start}.items-center{align-items:center}.items-end{align-items:flex-end}.items-stretch{align-items:stretch}.justify-start{justify-content:flex-start}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.gap-xs{gap:.25rem}.gap-sm{gap:.375rem}.gap-md{gap:.75rem}.gap-lg{gap:1rem}.gap-xl{gap:1.5rem}.w-full{width:100% !important}.w-auto{width:auto !important}.w-50{width:50%}.max-w-full{max-width:100%}.m-0{margin:0}.m-auto{margin:auto}.mt-0{margin-top:0}.mt-xs{margin-top:.25rem}.mt-sm{margin-top:.375rem}.mt-md{margin-top:.75rem}.mt-lg{margin-top:1rem}.mt-xl{margin-top:1.5rem}.mb-0{margin-bottom:0}.mb-xs{margin-bottom:.25rem}.mb-sm{margin-bottom:.375rem}.mb-md{margin-bottom:.75rem}.mb-lg{margin-bottom:1rem}.mb-xl{margin-bottom:1.5rem}.ml-auto{margin-left:auto}.mr-auto{margin-right:auto}.mx-auto{margin-left:auto;margin-right:auto}.p-0{padding:0}.p-xs{padding:.25rem}.p-sm{padding:.375rem}.p-md{padding:.75rem}.p-lg{padding:1rem}.p-xl{padding:1.5rem}.px-md{padding-left:.75rem;padding-right:.75rem}.py-md{padding-top:.75rem;padding-bottom:.75rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-xs{font-size:.75rem}.text-sm{font-size:.875rem}.text-md{font-size:.9375rem}.text-lg{font-size:1rem}.text-xl{font-size:1.125rem}.text-2xl{font-size:1.25rem}.font-normal{font-weight:400}.font-medium{font-weight:500}.font-semibold{font-weight:600}.font-bold{font-weight:700}.text-primary{color:var(--fad-text-primary)}.text-secondary{color:var(--fad-text-secondary)}.text-muted{color:var(--fad-text-secondary)}.text-success{color:var(--fad-success)}.text-warning{color:var(--fad-warning)}.text-danger{color:var(--fad-danger)}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.visible{visibility:visible}.invisible{visibility:hidden}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-100{opacity:1}.border{border:1px solid var(--fad-border-color)}.border-0{border:none}.border-top{border-top:1px solid var(--fad-border-color)}.border-bottom{border-bottom:1px solid var(--fad-border-color)}.rounded-sm{border-radius:.25rem}.rounded-md{border-radius:.375rem}.rounded-lg{border-radius:.5rem}.rounded-full{border-radius:9999px}.cursor-pointer{cursor:pointer}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.overflow-hidden{overflow:hidden}.overflow-auto{overflow:auto}.overflow-y-auto{overflow-y:auto}.overflow-x-auto{overflow-x:auto}.relative{position:relative}.absolute{position:absolute}.fixed{position:fixed}.sticky{position:sticky}.fad-card{background:var(--fad-surface-card);border:1px solid var(--fad-border-color);border-radius:.5rem;padding:1rem}.fad-card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:.75rem;padding-bottom:.75rem;border-bottom:1px solid var(--fad-border-color)}.fad-card-title{font-size:1rem;font-weight:600;color:var(--fad-text-primary);margin:0}.fad-card-body{color:var(--fad-text-primary)}.fad-card-footer{display:flex;align-items:center;justify-content:flex-end;gap:.375rem;margin-top:.75rem;padding-top:.75rem;border-top:1px solid var(--fad-border-color)}.fad-form-field{display:flex;flex-direction:column;gap:.375rem;margin-bottom:1rem}.fad-form-label{font-weight:500;color:var(--fad-text-primary);font-size:.875rem}.fad-form-label-required::after{content:" *";color:var(--fad-danger)}.fad-form-help{font-size:.75rem;color:var(--fad-text-secondary)}.fad-form-error{font-size:.75rem;color:var(--fad-danger)}.fad-form-row{display:grid;grid-template-columns:repeat(auto-fit, minmax(250px, 1fr));gap:.75rem}.fad-form-section-title{font-size:1rem;font-weight:600;color:var(--fad-text-primary);margin:1.5rem 0 .75rem 0;padding-bottom:.375rem;border-bottom:1px solid var(--fad-border-color)}.fad-form-section-title:first-child{margin-top:0}.fad-form-field--dirty{position:relative}.fad-form-field--dirty::before{content:"";position:absolute;left:-8px;top:0;bottom:0;width:3px;background:var(--fad-warning);border-radius:9999px}.fad-form-field--error .fad-form-label{color:var(--fad-danger)}.fad-page-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;padding-bottom:.75rem}.fad-page-title{font-size:1.25rem;font-weight:600;color:var(--fad-text-primary);margin:0}.fad-page-actions{display:flex;gap:.375rem}.fad-empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem .75rem;text-align:center;color:var(--fad-text-secondary)}.fad-empty-state-icon{font-size:3rem;opacity:.5;margin-bottom:.75rem}.fad-empty-state-title{font-size:1rem;font-weight:500;color:var(--fad-text-primary);margin:0 0 .375rem 0}.fad-empty-state-description{font-size:.875rem;max-width:400px}.fad-loading{display:flex;align-items:center;justify-content:center;padding:2rem}.fad-loading-spinner{animation:fad-spin 1s linear infinite}@keyframes fad-spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}.fad-badge{display:inline-flex;align-items:center;padding:.25rem .375rem;font-size:.75rem;font-weight:500;border-radius:9999px;text-transform:uppercase;letter-spacing:.025em}.fad-badge--success{background:var(--p-green-100);color:var(--p-green-700)}.fad-badge--warning{background:var(--p-orange-100);color:var(--p-orange-700)}.fad-badge--danger{background:var(--p-red-100);color:var(--p-red-700)}.fad-badge--info{background:var(--p-blue-100);color:var(--p-blue-700)}.fad-badge--neutral{background:var(--p-surface-100);color:var(--p-surface-600)}.fad-tabs{display:flex;gap:.25rem;border-bottom:1px solid var(--fad-border-color);margin-bottom:1rem}.fad-tab{display:flex;align-items:center;gap:.25rem;padding:.375rem .75rem;font-size:.875rem;font-weight:500;color:var(--fad-text-secondary);background:rgba(0,0,0,0);border:none;border-bottom:2px solid rgba(0,0,0,0);cursor:pointer;transition:all 100ms ease;margin-bottom:-1px}.fad-tab:hover{color:var(--fad-text-primary)}.fad-tab--active{color:var(--fad-primary);border-bottom-color:var(--fad-primary)}.fad-tab-count{background:var(--p-surface-100);color:var(--fad-text-secondary);padding:.125rem .375rem;border-radius:9999px;font-size:.75rem}.fad-tab--active .fad-tab-count{background:var(--p-primary-100);color:var(--fad-primary)}.fad-page-nav{display:flex;justify-content:space-between;align-items:center;margin-bottom:.375rem}.fad-page-nav-breadcrumb{flex:1}.fad-page-nav-links{display:flex;align-items:center;gap:.375rem;font-size:.875rem}.fad-page-nav-separator{color:var(--fad-text-secondary)}.fad-page-nav-link{color:var(--fad-primary);text-decoration:none;transition:color 100ms ease}.fad-page-nav-link:hover{color:var(--fad-primary-hover);text-decoration:underline}.fad-page-nav-link--active{color:var(--fad-text-primary);font-weight:500;pointer-events:none}*{margin:0;padding:0;box-sizing:border-box}html,body,#app{height:100%;font-family:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}input::placeholder,textarea::placeholder,.p-inputtext::placeholder,.p-autocomplete-input::placeholder,.p-textarea::placeholder,.p-select-label.p-placeholder,.p-dropdown-label.p-placeholder{color:#a0aec0 !important;opacity:1}.page-content{flex:1;padding:1rem}.page-header{margin-bottom:1rem}.page-title{font-size:1.25rem;font-weight:600;color:var(--p-surface-900)}.header-actions{display:flex;gap:.75rem;align-items:center}.p-tabpanels{padding-top:.75rem}.p-tabpanel>.form-field:first-child,.p-tabpanel>.form-row:first-child,.p-tabpanel>.form-grid:first-child{margin-top:.375rem}.info-block{background:var(--p-surface-50);padding:1rem;border-radius:.5rem}.info-block-title{margin:0 0 .75rem 0;font-size:.9375rem;color:var(--p-surface-600)}.info-block-title i{margin-right:.375rem}.info-label{font-weight:500;color:var(--p-surface-500);display:block;margin-bottom:.25rem}.info-label-sm{font-size:.75rem;font-weight:600;color:var(--p-surface-500);text-transform:uppercase;display:block;margin-bottom:.25rem}.value-lg{font-size:1.25rem;font-weight:600}.accent-block{background:var(--p-surface-50);border-left:3px solid var(--p-primary-500);padding:.75rem;border-radius:.25rem}.code-block{word-break:break-all;font-family:ui-monospace,"Cascadia Code","Source Code Pro",Menlo,Consolas,monospace;background:var(--p-surface-100);padding:.75rem;border-radius:.25rem}.section-header{display:flex;align-items:center;gap:.375rem;margin-bottom:.75rem}.section-header h3{margin:0;font-size:.9375rem;font-weight:600}.section-header i{color:var(--p-primary-500)}.section-description{color:var(--p-surface-500);font-size:.875rem;margin-bottom:.75rem}.section-title{margin:0 0 .75rem 0;font-size:.9375rem;font-weight:600;color:var(--p-surface-700)}.section-title--separator{border-top:1px solid var(--p-surface-200);padding-top:1rem}.section-title i{margin-right:.375rem}.image-gallery{display:grid;grid-template-columns:repeat(auto-fill, minmax(200px, 1fr));gap:.75rem}.image-card{position:relative;border-radius:.5rem;overflow:hidden;border:1px solid var(--p-surface-200)}.image-card img{width:100%;aspect-ratio:16/9;object-fit:cover}.image-card--clickable{cursor:pointer;transition:border-color 150ms ease,box-shadow 150ms ease}.image-card--clickable:hover{border-color:var(--p-primary-300);box-shadow:0 2px 8px rgba(0,0,0,.1)}.image-caption{padding:.375rem;background:var(--p-surface-50);font-size:.75rem;color:var(--p-surface-600)}.info-row{display:flex;justify-content:space-between;align-items:center;padding:.375rem 0;border-bottom:1px solid var(--p-surface-100)}.info-row:last-child{border-bottom:none}.layout-breadcrumb{padding:.75rem 1rem;background:var(--p-surface-0, white);border-bottom:1px solid var(--p-surface-200, #e2e8f0)}.layout-breadcrumb .p-breadcrumb-list{margin:0;padding:0}.breadcrumb-link{display:flex;align-items:center;gap:.375rem;color:var(--p-primary-600, #2563eb);text-decoration:none;font-size:.875rem}.breadcrumb-link:hover{text-decoration:underline}.breadcrumb-link i{font-size:.875rem}.breadcrumb-current{display:flex;align-items:center;gap:.375rem;color:var(--p-surface-600, #475569);font-size:.875rem}.breadcrumb-current i{font-size:.875rem}.breadcrumb{margin-bottom:.5rem}.breadcrumb-list{display:flex;align-items:center;gap:.5rem;list-style:none;margin:0;padding:0;font-size:.875rem}.breadcrumb-item{display:flex;align-items:center;gap:.5rem}.breadcrumb-item.is-current .breadcrumb-text{color:var(--p-text-color, #333);font-weight:500}.breadcrumb-separator{font-size:.75rem;color:var(--p-text-muted-color, #888)}.breadcrumb-item .breadcrumb-link{display:flex;align-items:center;gap:.375rem;color:var(--p-primary-color, #3b82f6);text-decoration:none;transition:color .2s}.breadcrumb-item .breadcrumb-link:hover{color:var(--p-primary-hover-color, #2563eb);text-decoration:underline}.breadcrumb-item .breadcrumb-link i{font-size:.875rem}.breadcrumb-text{display:flex;align-items:center;gap:.375rem;color:var(--p-text-muted-color, #888)}.form-grid{display:grid;grid-template-columns:repeat(2, 1fr);gap:1rem;padding:.75rem 0}@media(max-width: 767px){.form-grid{grid-template-columns:1fr}}.form-row{display:grid;grid-template-columns:repeat(2, 1fr);gap:1rem;align-items:end}.form-row-3{display:grid;grid-template-columns:repeat(3, 1fr);gap:.75rem;align-items:end}.form-row.metadata{display:grid;grid-template-columns:repeat(4, 1fr);gap:.75rem;padding:.75rem;background:var(--p-surface-50);border-radius:.5rem;align-items:end}.form-row.checkboxes{display:flex;gap:3rem}.form-row+.form-field,.form-field+.form-row{margin-top:1.25rem}.form-field{display:flex;flex-direction:column;gap:.375rem}.form-field label{font-weight:500;color:var(--p-surface-700)}.form-field small{color:var(--p-surface-500);font-size:.85rem}.form-field .p-inputtext,.form-field .p-textarea,.form-field .p-select,.form-field .p-multiselect,.form-field .p-inputwrapper,.form-field input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=color]),.form-field textarea{width:100% !important;box-sizing:border-box}.form-field>input[type=checkbox],.form-field>input[type=radio]{align-self:flex-start}.form-field .p-textarea,.form-field textarea{min-height:100px;resize:vertical}.meta-item{display:flex;flex-direction:column;gap:.25rem}.meta-item label{font-size:.85rem;color:var(--p-surface-500)}.meta-item span{font-weight:500}.toggle-field{flex-direction:row;align-items:center;gap:.75rem}.toggle-field label{min-width:120px}.checkbox-field{display:flex;align-items:center;gap:.375rem}.checkbox-field label{margin:0;cursor:pointer}.url-field,.lock-field,.envelope-field{display:flex;gap:.375rem;align-items:center}.url-field input,.lock-field input,.envelope-field input{flex:1}.field-hint,.form-hint{font-size:.8rem;color:var(--p-surface-400);margin-top:.25rem;display:block}.field-error{font-size:.8rem;color:var(--p-red-500);margin-top:.25rem;display:block}.editor-box{border:1px solid var(--p-surface-200);border-radius:.5rem;padding:.75rem;background:var(--p-surface-50)}.editor-chips{display:flex;flex-wrap:wrap;gap:.375rem;margin-bottom:.75rem;padding-bottom:.75rem;border-bottom:1px solid var(--p-surface-200)}.chip-code{font-family:ui-monospace,"Cascadia Code","Source Code Pro",Menlo,Consolas,monospace;font-size:.875rem}.chip-namespace{color:var(--p-surface-500)}.chip-action{color:var(--p-primary-600);font-weight:500}.chip-wildcard{color:var(--p-orange-500);font-weight:700}.info-grid{display:grid;grid-template-columns:repeat(2, 1fr);gap:.75rem}.info-grid>div{display:flex;flex-direction:column;gap:.25rem}.info-grid label{font-size:.85rem;color:var(--p-surface-500)}.info-item{display:flex;flex-direction:column;gap:.25rem}.info-item label{font-size:.75rem;font-weight:600;color:var(--p-surface-500);text-transform:uppercase}.info-value{font-size:.9rem}.timestamps-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:.75rem}.timestamps-grid--separator{padding-top:.75rem;border-top:1px solid var(--p-surface-200)}.timestamp{display:flex;flex-direction:column;gap:.25rem}.timestamp label{font-size:.75rem;color:var(--text-color-secondary);text-transform:uppercase}.form-dirty-indicator{display:flex;align-items:center;gap:.375rem;padding:.375rem .75rem;background:color-mix(in srgb, var(--p-orange-100) 50%, transparent);border:1px solid var(--p-orange-300);border-radius:.375rem;color:var(--p-orange-700);font-size:.875rem;margin-bottom:.75rem}.form-dirty-indicator i{color:var(--p-orange-500)}.form-field.field-dirty .p-inputtext,.form-field.field-dirty .p-select,.form-field.field-dirty .p-textarea,.form-field.field-dirty .p-password input,.form-field.field-dirty .p-multiselect,.form-field.field-dirty .p-calendar input,.form-field.field-dirty .p-inputnumber input,.form-field.field-dirty .p-dropdown{border-color:#f97316 !important;box-shadow:0 0 0 1px #f97316 !important}.form-card{background:var(--p-surface-0);padding:1.5rem;border-radius:.5rem}.config-intro{display:flex;align-items:flex-start;gap:.75rem;padding:.75rem;background:var(--p-surface-50);border-radius:.5rem;margin-bottom:1rem}.config-intro i{font-size:1.25rem;color:var(--p-primary-500);margin-top:.125rem}.config-field{margin-bottom:1rem}.form-actions{display:flex;justify-content:space-between;align-items:center;margin-top:1.5rem;padding-top:1.5rem;border-top:1px solid var(--p-surface-200)}.form-actions-left{display:flex;align-items:center;gap:.5rem}.dirty-indicator{color:var(--p-orange-500);font-size:.5rem;animation:pulse 2s infinite}@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}.field-invalid input,.field-invalid textarea,.field-invalid .p-inputtext,.field-invalid .p-select,.field-invalid .p-dropdown{border-color:var(--p-red-500)}.field-invalid input:focus,.field-invalid textarea:focus,.field-invalid .p-inputtext:focus,.field-invalid .p-select:focus,.field-invalid .p-dropdown:focus{box-shadow:0 0 0 1px var(--p-red-500)}.form-page{display:flex;flex-direction:column;gap:1rem}.form-error-message{margin:1rem 0}.validation-summary{margin-bottom:1rem}.validation-errors{margin:0;padding-left:1.5rem}.validation-errors li{margin:.25rem 0}.stats-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:1rem;margin-bottom:1.5rem}.stat-card{background:var(--p-surface-0);border-radius:.5rem;padding:1rem;border:1px solid var(--p-surface-200)}.stat-card-title{font-size:.875rem;color:var(--p-surface-500);margin-bottom:.375rem}.stat-card-value{font-size:2rem;font-weight:600;color:var(--p-surface-900)}.stat-card-subtitle{font-size:.75rem;color:var(--p-surface-400);margin-top:.25rem}.loading-state{display:flex;justify-content:center;align-items:center;padding:2rem}.loading-state--inline{padding:.75rem;gap:.75rem;color:var(--p-text-muted-color)}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:2rem;color:var(--text-color-secondary)}.empty-state--sm{padding:1rem}.empty-state--lg{padding:3rem}.empty-state-icon{font-size:2rem;margin-bottom:.5rem;opacity:.5}.empty-state--sm .empty-state-icon{font-size:1.5rem}.empty-state--lg .empty-state-icon{font-size:3rem;margin-bottom:1rem}.empty-state-title{margin:0 0 .25rem 0;font-weight:500;color:var(--text-color-secondary)}.empty-state--lg .empty-state-title{font-size:1.1rem}.empty-state-description{opacity:.7;max-width:300px}.empty-state-actions{margin-top:1rem}.error-message{background:var(--p-red-50);border:1px solid var(--p-red-200);border-radius:.375rem;padding:.75rem .75rem;color:var(--p-red-700);font-family:ui-monospace,"Cascadia Code","Source Code Pro",Menlo,Consolas,monospace;font-size:.85rem;white-space:pre-wrap;word-break:break-word}.error-content{background:var(--p-red-50);color:var(--p-red-700);padding:.75rem;border-radius:.5rem;overflow-x:auto;margin:0;font-size:.85rem;white-space:pre-wrap}:root[data-theme=dark] .error-message,:root[data-theme=dark] .error-content{background:rgba(239,68,68,.1);color:var(--p-red-400);border-color:var(--p-red-900)}.p-datatable .p-datatable-tbody>tr>td{padding:.5rem .75rem !important}.p-datatable .p-datatable-thead>tr>th{padding:.625rem .75rem !important}.table-actions{display:flex;gap:.25rem;justify-content:flex-start}.text-muted{color:var(--p-surface-400)}.status-tags,.type-tags{display:flex;gap:.25rem;flex-wrap:wrap}.external-link{display:flex;align-items:center;gap:.375rem;color:var(--p-primary-500);text-decoration:none}.external-link:hover{text-decoration:underline}.external-link i{font-size:.75rem;opacity:.6}.domain-cell{display:flex;align-items:center}.domain-link{display:flex;align-items:center;gap:.375rem;color:var(--p-primary-500);text-decoration:none}.domain-link:hover{text-decoration:underline}.domain-link i{font-size:.75rem;opacity:.6}.domain-tags{display:flex;gap:.25rem}.clickable-cell{cursor:pointer;color:var(--p-primary-color)}.clickable-cell:hover{text-decoration:underline}.entity-link{display:flex;align-items:center;gap:.375rem;text-decoration:none;color:var(--p-primary-color)}.entity-link:hover{text-decoration:underline}.entity-type{font-weight:500}.entity-id{font-size:.75rem;background:var(--p-surface-100);padding:.15rem .35rem;border-radius:3px}.tags-preview{display:flex;gap:.25rem;flex-wrap:wrap}.tags-preview .p-tag{font-size:.75rem}.p-role-label,.p-permission-label{display:inline-flex;align-items:center;background:rgba(0,0,0,0);font-size:.75rem;font-weight:normal !important;padding:.25rem .75rem;border-radius:var(--p-tag-border-radius, 6px);white-space:nowrap}.p-role-label--pill,.p-permission-label--pill{border-radius:999px}.p-role-label--more,.p-permission-label--more{border-style:dashed}.p-role-label{border:1px solid var(--p-blue-500);color:var(--p-blue-600)}.p-permission-label{border:1px solid var(--p-surface-400);color:var(--p-surface-600)}.p-role-labels,.p-permission-labels{display:flex;flex-wrap:wrap;gap:.25rem}.p-chip{background:none !important;border:1px solid var(--p-surface-300) !important;color:var(--p-surface-600) !important;font-size:.75rem !important;padding:.2rem .6rem !important}.datatable-row-clickable .p-datatable-tbody>tr{cursor:pointer}.datatable-row-clickable .p-datatable-tbody>tr:active{background:var(--p-primary-50) !important}.filter-bar{display:flex;justify-content:space-between;align-items:center;gap:.75rem;margin-bottom:.75rem}.filter-bar-left{display:flex;align-items:center;gap:.75rem}.filter-bar-right{display:flex;align-items:center;gap:.75rem}.p-select.filter-default,.p-autocomplete.filter-default .p-inputtext{border-color:var(--p-primary-400, #60a5fa) !important;box-shadow:0 0 0 1px var(--p-primary-400, #60a5fa) !important}.p-select.filter-modified,.p-autocomplete.filter-modified .p-inputtext{border-color:#f97316 !important;box-shadow:0 0 0 1px #f97316 !important}.p-select.filter-active,.p-inputtext.filter-active{border-color:#f97316 !important;box-shadow:0 0 0 1px #f97316 !important}.filter-bar .p-button,.filter-bar-left .p-button,.flex.align-items-end .p-button,.flex.items-end .p-button{height:2.5rem;align-self:flex-end;margin-bottom:0}.spinner-lg{font-size:2rem}.spinner-md{font-size:1.5rem}.card{background:var(--p-surface-0);border-radius:.5rem;padding:1rem;border:1px solid var(--p-surface-200)}.info-card{background:var(--p-surface-50);padding:.75rem;border-radius:.5rem}.card-title{display:flex;align-items:center;justify-content:space-between}.card h3,.card-heading{margin:0 0 .75rem;font-size:.9375rem;font-weight:600;color:var(--p-surface-700)}.warning-card{background:var(--p-yellow-50);padding:.75rem;border-radius:.5rem;border:1px solid var(--p-yellow-200)}.warning-card i{color:var(--p-yellow-600)}.warning-card span{margin-left:.375rem;color:var(--p-yellow-700)}.span-full{grid-column:1/-1}.grid{display:grid}.grid-cols-2{grid-template-columns:repeat(2, 1fr)}.grid-cols-3{grid-template-columns:repeat(3, 1fr)}.text-uppercase{text-transform:uppercase}.text-center{text-align:center}.monospace{font-family:ui-monospace,"Cascadia Code","Source Code Pro",Menlo,Consolas,monospace}.break-all{word-break:break-all}.w-full{width:100% !important}.mb-0{margin-bottom:0}.mb-1{margin-bottom:.375rem}.mb-2{margin-bottom:.75rem}.mb-3{margin-bottom:1rem}.mt-0{margin-top:0}.mt-1{margin-top:.375rem}.mt-2{margin-top:.75rem}.p-2{padding:.75rem}.p-3{padding:1rem}.pt-2{padding-top:.75rem}.gap-1{gap:.375rem}.gap-2{gap:.75rem}.gap-3{gap:1rem}.flex{display:flex}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.flex-1{flex:1}.alert-warning{background:var(--p-yellow-50);padding:.75rem;margin-bottom:.75rem;border-radius:.5rem;border:1px solid var(--p-yellow-200)}.alert-warning-icon{color:var(--p-yellow-600)}.alert-warning-text{margin-left:.375rem;color:var(--p-yellow-700)}.alert-info{color:var(--p-orange-600);margin-bottom:.75rem}.key-info-panel{background:var(--p-surface-50);padding:.75rem;margin-bottom:.75rem;border-radius:.5rem}.key-info-grid{display:flex;flex-wrap:wrap;gap:1.5rem;align-items:center}.key-info-item small{color:var(--p-surface-500);display:block}.info-banner{margin-top:.75rem;margin-bottom:.75rem}.info-banner:first-child{margin-top:0}.info-banner .p-message-content{padding:.375rem .75rem}.info-banner .p-message-icon{display:none}.info-banner-content{display:flex;align-items:center;gap:.625rem}.info-banner-icon{font-size:1.125rem;flex-shrink:0}.info-banner-text{flex:1}.info-banner-text strong{font-weight:600}.p-button{background:rgba(0,0,0,0) !important;border:1px solid var(--p-primary-500) !important;color:var(--p-primary-500) !important;transition:background-color .2s,border-color .2s,color .2s}.p-button:hover:not(:disabled){background:var(--p-primary-50) !important;border-color:var(--p-primary-600) !important;color:var(--p-primary-600) !important}.p-button:focus{box-shadow:0 0 0 2px var(--p-surface-0),0 0 0 4px var(--p-primary-200)}.p-button:disabled{opacity:.6;cursor:not-allowed}.p-button-secondary{border-color:var(--p-surface-400) !important;color:var(--p-surface-600) !important}.p-button-secondary:hover:not(:disabled){background:var(--p-surface-100) !important;border-color:var(--p-surface-500) !important;color:var(--p-surface-700) !important}.p-button-secondary:focus{box-shadow:0 0 0 2px var(--p-surface-0),0 0 0 4px var(--p-surface-200)}.p-button-success{border-color:var(--p-green-500) !important;color:var(--p-green-600) !important}.p-button-success:hover:not(:disabled){background:var(--p-green-50) !important;border-color:var(--p-green-600) !important;color:var(--p-green-700) !important}.p-button-success:focus{box-shadow:0 0 0 2px var(--p-surface-0),0 0 0 4px var(--p-green-200)}.p-button-warn{border-color:var(--p-orange-500) !important;color:var(--p-orange-600) !important}.p-button-warn:hover:not(:disabled){background:var(--p-orange-50) !important;border-color:var(--p-orange-600) !important;color:var(--p-orange-700) !important}.p-button-warn:focus{box-shadow:0 0 0 2px var(--p-surface-0),0 0 0 4px var(--p-orange-200)}.p-button-danger{border-color:var(--p-red-500) !important;color:var(--p-red-600) !important}.p-button-danger:hover:not(:disabled){background:var(--p-red-50) !important;border-color:var(--p-red-600) !important;color:var(--p-red-700) !important}.p-button-danger:focus{box-shadow:0 0 0 2px var(--p-surface-0),0 0 0 4px var(--p-red-200)}.p-button-info{border-color:var(--p-blue-500) !important;color:var(--p-blue-600) !important}.p-button-info:hover:not(:disabled){background:var(--p-blue-50) !important;border-color:var(--p-blue-600) !important;color:var(--p-blue-700) !important}.p-button-info:focus{box-shadow:0 0 0 2px var(--p-surface-0),0 0 0 4px var(--p-blue-200)}.p-button-contrast{border-color:var(--p-surface-900) !important;color:var(--p-surface-900) !important}.p-button-contrast:hover:not(:disabled){background:var(--p-surface-900) !important;border-color:var(--p-surface-900) !important;color:var(--p-surface-0) !important}.p-button-filled,.p-button.p-button-raised{background:var(--p-primary-500) !important;border-color:var(--p-primary-500) !important;color:#fff !important}.p-button-filled:hover:not(:disabled),.p-button.p-button-raised:hover:not(:disabled){background:var(--p-primary-600) !important;border-color:var(--p-primary-600) !important;color:#fff !important}.p-button-filled.p-button-secondary,.p-button.p-button-raised.p-button-secondary{background:var(--p-surface-500) !important;border-color:var(--p-surface-500) !important;color:#fff !important}.p-button-filled.p-button-secondary:hover:not(:disabled),.p-button.p-button-raised.p-button-secondary:hover:not(:disabled){background:var(--p-surface-600) !important;border-color:var(--p-surface-600) !important}.p-button-filled.p-button-success,.p-button.p-button-raised.p-button-success{background:var(--p-green-500) !important;border-color:var(--p-green-500) !important;color:#fff !important}.p-button-filled.p-button-success:hover:not(:disabled),.p-button.p-button-raised.p-button-success:hover:not(:disabled){background:var(--p-green-600) !important;border-color:var(--p-green-600) !important}.p-button-filled.p-button-warn,.p-button.p-button-raised.p-button-warn{background:var(--p-orange-500) !important;border-color:var(--p-orange-500) !important;color:#fff !important}.p-button-filled.p-button-warn:hover:not(:disabled),.p-button.p-button-raised.p-button-warn:hover:not(:disabled){background:var(--p-orange-600) !important;border-color:var(--p-orange-600) !important}.p-button-filled.p-button-danger,.p-button.p-button-raised.p-button-danger{background:var(--p-red-500) !important;border-color:var(--p-red-500) !important;color:#fff !important}.p-button-filled.p-button-danger:hover:not(:disabled),.p-button.p-button-raised.p-button-danger:hover:not(:disabled){background:var(--p-red-600) !important;border-color:var(--p-red-600) !important}.p-button-text{background:rgba(0,0,0,0) !important;border-color:rgba(0,0,0,0) !important}.p-button-text:hover:not(:disabled){background:var(--p-primary-50) !important;border-color:rgba(0,0,0,0) !important}.p-button-text.p-button-secondary:hover:not(:disabled){background:var(--p-surface-100) !important}.p-button-text.p-button-success:hover:not(:disabled){background:var(--p-green-50) !important}.p-button-text.p-button-warn:hover:not(:disabled){background:var(--p-orange-50) !important}.p-button-text.p-button-danger:hover:not(:disabled){background:var(--p-red-50) !important}.p-button-link{background:rgba(0,0,0,0) !important;border-color:rgba(0,0,0,0) !important;color:var(--p-primary-500) !important;text-decoration:none}.p-button-link:hover:not(:disabled){background:rgba(0,0,0,0) !important;border-color:rgba(0,0,0,0) !important;text-decoration:underline}.p-button-icon-only{background:unset !important;border:unset !important;color:unset !important}.p-button-icon-only:hover:not(:disabled){background:unset !important;border-color:unset !important;color:unset !important}.p-button-sm{font-size:.875rem;padding:.375rem .75rem}.p-button-lg{font-size:1rem;padding:.75rem 1.25rem}.p-tag{background:rgba(0,0,0,0) !important;border:1px solid var(--p-primary-500) !important;color:var(--p-primary-500) !important;font-weight:normal !important;white-space:nowrap}.p-tag-secondary{background:rgba(0,0,0,0) !important;border-color:var(--p-surface-400) !important;color:var(--p-surface-600) !important}.p-tag-success{background:rgba(0,0,0,0) !important;border-color:var(--p-green-500) !important;color:var(--p-green-600) !important}.p-tag-warn{background:rgba(0,0,0,0) !important;border-color:var(--p-orange-500) !important;color:var(--p-orange-600) !important}.p-tag-danger{background:rgba(0,0,0,0) !important;border-color:var(--p-red-500) !important;color:var(--p-red-600) !important}.p-tag-info{background:rgba(0,0,0,0) !important;border-color:var(--p-blue-500) !important;color:var(--p-blue-600) !important}.p-tag-contrast{background:rgba(0,0,0,0) !important;border-color:var(--p-surface-900) !important;color:var(--p-surface-900) !important}.p-splitbutton .p-splitbutton-defaultbutton{border:1px solid var(--p-primary-500) !important;border-right:none !important;border-top-right-radius:0 !important;border-bottom-right-radius:0 !important;background:rgba(0,0,0,0) !important;color:var(--p-primary-500) !important}.p-splitbutton .p-splitbutton-defaultbutton:hover:not(:disabled){background:var(--p-primary-50) !important}.p-splitbutton .p-splitbutton-dropdown{border:1px solid var(--p-primary-500) !important;border-left:none !important;border-top-left-radius:0 !important;border-bottom-left-radius:0 !important;background:rgba(0,0,0,0) !important;color:var(--p-primary-500) !important}.p-splitbutton .p-splitbutton-dropdown:hover:not(:disabled){background:var(--p-primary-50) !important}.p-splitbutton.p-splitbutton-danger .p-splitbutton-defaultbutton,.p-splitbutton.p-splitbutton-danger .p-splitbutton-dropdown{border-color:var(--p-red-500) !important;color:var(--p-red-600) !important}.p-splitbutton.p-splitbutton-danger .p-splitbutton-defaultbutton:hover:not(:disabled),.p-splitbutton.p-splitbutton-danger .p-splitbutton-dropdown:hover:not(:disabled){background:var(--p-red-50) !important}.p-splitbutton.p-splitbutton-secondary .p-splitbutton-defaultbutton,.p-splitbutton.p-splitbutton-secondary .p-splitbutton-dropdown{border-color:var(--p-surface-400) !important;color:var(--p-surface-600) !important}.p-splitbutton.p-splitbutton-secondary .p-splitbutton-defaultbutton:hover:not(:disabled),.p-splitbutton.p-splitbutton-secondary .p-splitbutton-dropdown:hover:not(:disabled){background:var(--p-surface-100) !important}.p-splitbutton.p-splitbutton-success .p-splitbutton-defaultbutton,.p-splitbutton.p-splitbutton-success .p-splitbutton-dropdown{border-color:var(--p-green-500) !important;color:var(--p-green-600) !important}.p-splitbutton.p-splitbutton-success .p-splitbutton-defaultbutton:hover:not(:disabled),.p-splitbutton.p-splitbutton-success .p-splitbutton-dropdown:hover:not(:disabled){background:var(--p-green-50) !important}.cards-grid{display:grid;gap:1rem;margin-bottom:1.5rem}.cards-grid--cols-auto{grid-template-columns:repeat(auto-fit, minmax(180px, 1fr))}.cards-grid--cols-2{grid-template-columns:repeat(2, 1fr)}.cards-grid--cols-3{grid-template-columns:repeat(3, 1fr)}.cards-grid--cols-4{grid-template-columns:repeat(4, 1fr)}.stat-card,.card-custom{background:var(--p-surface-0);border:1px solid var(--p-surface-200);border-radius:.5rem;padding:1.25rem}.stat-card{text-align:center}.stat-card--clickable{cursor:pointer;transition:border-color .2s,box-shadow .2s}.stat-card--clickable:hover{border-color:var(--p-primary-300);box-shadow:0 2px 8px rgba(0,0,0,.08)}.stat-value{font-size:2rem;font-weight:600;color:var(--p-primary-500);display:flex;align-items:center;justify-content:center;gap:.5rem}.stat-value--success{color:var(--p-green-500)}.stat-value--danger{color:var(--p-red-500)}.stat-value--warning{color:var(--p-orange-500)}.stat-value--info{color:var(--p-blue-500)}.stat-icon{font-size:1.5rem}.stat-label{font-size:.875rem;color:var(--p-surface-600);margin-top:.25rem}@media(max-width: 768px){.cards-grid--cols-3,.cards-grid--cols-4{grid-template-columns:repeat(2, 1fr)}}@media(max-width: 480px){.cards-grid{grid-template-columns:1fr}}.rich-cards-grid{display:grid;gap:1rem;margin-bottom:1.5rem}.rich-cards-grid--cols-auto{grid-template-columns:repeat(auto-fit, minmax(200px, 1fr))}.rich-cards-grid--cols-2{grid-template-columns:repeat(2, 1fr)}.rich-cards-grid--cols-3{grid-template-columns:repeat(3, 1fr)}.rich-cards-grid--cols-4{grid-template-columns:repeat(4, 1fr)}.rich-cards-grid--cols-5{grid-template-columns:repeat(5, 1fr)}.rich-cards-grid--cols-6{grid-template-columns:repeat(6, 1fr)}.rich-card{background:var(--p-surface-0);border:1px solid var(--p-surface-200);border-radius:.5rem;padding:1.25rem}.rich-card--clickable{cursor:pointer;transition:border-color .2s,box-shadow .2s,transform .2s}.rich-card--clickable:hover{border-color:var(--p-primary-300);box-shadow:0 4px 12px rgba(0,0,0,.1);transform:translateY(-2px)}.rich-card--success .rich-card-value{color:var(--p-green-600)}.rich-card--success .rich-card-icon{color:var(--p-green-500)}.rich-card--danger .rich-card-value{color:var(--p-red-600)}.rich-card--danger .rich-card-icon{color:var(--p-red-500)}.rich-card--warning .rich-card-value{color:var(--p-orange-600)}.rich-card--warning .rich-card-icon{color:var(--p-orange-500)}.rich-card--info .rich-card-value{color:var(--p-blue-600)}.rich-card--info .rich-card-icon{color:var(--p-blue-500)}.rich-card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem}.rich-card-title{font-size:.875rem;font-weight:500;color:var(--p-surface-600)}.rich-card-icon{font-size:1.25rem;color:var(--p-surface-400)}.rich-card-value{font-size:2rem;font-weight:700;color:var(--p-surface-900);line-height:1.2}.rich-card-subtitle{font-size:.8rem;color:var(--p-surface-500);margin-top:.25rem}@media(max-width: 1024px){.rich-cards-grid--cols-5,.rich-cards-grid--cols-6{grid-template-columns:repeat(3, 1fr)}}@media(max-width: 768px){.rich-cards-grid--cols-3,.rich-cards-grid--cols-4,.rich-cards-grid--cols-5,.rich-cards-grid--cols-6{grid-template-columns:repeat(2, 1fr)}}@media(max-width: 480px){.rich-cards-grid{grid-template-columns:1fr}}.code-box{word-break:break-all;font-family:ui-monospace,"Cascadia Code","Source Code Pro",Menlo,Consolas,monospace;background:var(--p-surface-100);padding:.75rem;border-radius:.25rem}.code-box--sm{padding:.375rem;font-size:.875rem}.tag-sm{font-size:.7rem}.tag-xs{font-size:.75rem}.tags-constrained{max-width:300px}.json-viewer{background:var(--p-surface-50);border:1px solid var(--p-surface-200);border-radius:.375rem;overflow:auto;font-family:"Monaco","Menlo","Ubuntu Mono",monospace;font-size:.8125rem;line-height:1.5}.json-content{padding:1rem;margin:0;white-space:pre-wrap;word-break:break-word}.json-key{color:#881391}.json-string{color:#1a1aa6}.json-number{color:#1c00cf}.json-boolean{color:#0d22aa;font-weight:500}.json-null{color:gray;font-style:italic}.dialog-content{display:flex;align-items:flex-start;gap:1rem;margin-bottom:.75rem}.dialog-content p{margin:0;line-height:1.5}.dialog-section-title{font-size:.875rem;font-weight:600;margin-bottom:.375rem;color:var(--p-surface-600)}.dialog-icon{font-size:2rem;color:var(--p-orange-500);flex-shrink:0}.multi-step-dialog{padding:.5rem 0}.step-content{padding:1rem 0}.step-actions{display:flex;justify-content:flex-end;gap:.5rem;margin-top:1.5rem;padding-top:1rem;border-top:1px solid var(--p-surface-200)}.markdown-rendered{background:var(--p-surface-100);padding:.75rem;border-radius:.25rem;max-height:400px;overflow-y:auto;font-size:.875rem}.markdown-rendered h1,.markdown-rendered h2,.markdown-rendered h3,.markdown-rendered h4{margin:.75rem 0 .375rem 0;font-weight:600}.markdown-rendered h1{font-size:1.25rem}.markdown-rendered h2{font-size:1.125rem}.markdown-rendered h3{font-size:1rem}.markdown-rendered p{margin:.375rem 0}.markdown-rendered ul,.markdown-rendered ol{margin:.375rem 0;padding-left:1rem}.markdown-rendered a{color:var(--p-primary-500);text-decoration:underline}.markdown-rendered code{background:var(--p-surface-200);padding:.1rem .3rem;border-radius:.25rem;font-family:ui-monospace,"Cascadia Code","Source Code Pro",Menlo,Consolas,monospace;font-size:.85em}.markdown-rendered pre{background:var(--p-surface-200);padding:.75rem;border-radius:.25rem;overflow-x:auto}.markdown-rendered blockquote{border-left:3px solid var(--p-surface-300);margin:.375rem 0;padding-left:.75rem;color:var(--p-surface-600)}.markdown-rendered .md-figure{margin:.75rem 0;padding:0}.markdown-rendered .md-figure img{max-width:100%;height:auto;border-radius:.25rem;display:block}.markdown-rendered .md-figure figcaption{margin-top:.375rem;font-size:.75rem;color:var(--p-surface-500);font-style:italic;line-height:1.5}.header-left{display:flex;align-items:center;gap:.75rem}.content-grid{display:grid;gap:1rem}.meta-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:1rem}@media(max-width: 767px){.meta-grid{grid-template-columns:1fr}}.summary-text,.intent-text{line-height:1.5}.tags-wrap{display:flex;gap:.375rem;flex-wrap:wrap}.url-actions{display:flex;gap:.375rem;align-items:center}.content-box{background:var(--p-surface-100);padding:.75rem;border-radius:.25rem;white-space:pre-wrap;word-break:break-word}.content-box--scrollable{max-height:300px;overflow-y:auto}.content-box--tall{max-height:400px}.status-row{display:flex;justify-content:space-between;align-items:flex-start}.tags-row{display:flex;gap:.375rem}.show-field{display:flex;flex-direction:column;gap:.25rem}.show-field--horizontal{flex-direction:row;align-items:flex-start;gap:1rem}.show-field__label{font-weight:600;color:var(--p-text-muted-color, #6c757d);font-size:.875rem}.show-field--horizontal .show-field__label{padding-top:.125rem}.show-field__value{color:var(--p-text-color, #1e293b);line-height:1.5}.show-display{display:inline}.show-display--badge{display:inline-flex;align-items:center}.show-display--empty{color:var(--p-text-muted-color, #6c757d)}.show-display--email,.show-display--url,.show-display--reference{color:var(--p-primary-color, #3b82f6);text-decoration:none}.show-display--email:hover,.show-display--url:hover,.show-display--reference:hover{text-decoration:underline}.show-display--password{font-family:monospace;letter-spacing:.1em}.show-display--number,.show-display--currency{font-variant-numeric:tabular-nums}.show-display--boolean{display:inline-flex;align-items:center;gap:.5rem}.show-display--boolean.text-success{color:var(--p-green-500, #22c55e)}.show-display--boolean.text-danger{color:var(--p-red-500, #ef4444)}.show-display--boolean.text-muted{color:var(--p-text-muted-color, #6c757d)}.show-display--textarea{white-space:pre-wrap;line-height:1.6}.show-display--image img{border-radius:4px;object-fit:cover}.show-display--json{background:var(--p-surface-100, #f1f5f9);padding:.75rem;border-radius:4px;font-size:.875rem;overflow-x:auto;margin:0}.sidebar-box{position:relative;z-index:1;padding:.5rem .75rem;border-top:1px solid var(--p-surface-700, #334155);height:3.5rem;box-sizing:border-box;cursor:pointer;transition:background var(--fad-transition-fast);display:flex;align-items:center}.sidebar-box:hover{background:var(--p-surface-700, #334155)}.sidebar-box:focus-within{z-index:20}.sidebar-box-inner{display:flex;align-items:center;gap:.5rem;padding-left:2rem;height:100%;width:100%}.sidebar-box-icon{position:absolute;left:.5rem;top:50%;transform:translateY(-50%);width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:.875rem;color:var(--p-surface-400, #94a3b8)}.sidebar-box:hover .sidebar-box-icon{color:var(--p-surface-200, #e2e8f0)}.sidebar-box-content{flex:1;min-width:0;opacity:1;transition:opacity .1s ease .2s}.sidebar-box-title{font-weight:var(--fad-font-weight-normal);font-size:var(--fad-font-size-sm);color:var(--p-surface-100, #f1f5f9);white-space:nowrap}.sidebar-box-subtitle{font-size:var(--fad-font-size-2xs);color:var(--p-surface-400, #94a3b8);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sidebar-box--impersonator .sidebar-box-icon{color:var(--p-orange-400, #fb923c)}.sidebar-box--impersonator:hover .sidebar-box-icon{color:var(--p-orange-300, #fdba74)}.sidebar-box--impersonator .sidebar-box-title{color:var(--p-orange-400, #fb923c)}.sidebar-box--impersonator .sidebar-box-subtitle{color:var(--p-orange-300, #fdba74)}.sidebar--collapsed .sidebar-box{padding:0}.sidebar--collapsed .sidebar-box-inner{justify-content:center;padding-left:0}.sidebar--collapsed .sidebar-box-content{opacity:0;visibility:hidden;transition:opacity 0s,visibility 0s}.sidebar-box-controls{padding:.5rem 1rem}.sidebar-box-controls-label{font-size:var(--fad-font-size-xs);font-weight:var(--fad-font-weight-medium);color:var(--p-surface-500, #64748b);text-transform:uppercase;letter-spacing:.06em;display:block;margin-bottom:.375rem}.sidebar-box-controls-input{width:100%}.sidebar-box-controls-input :deep(.p-autocomplete-input){width:100%;font-size:var(--fad-font-size-sm)}.sidebar-box-controls-input :deep(.p-autocomplete-overlay){z-index:9999 !important}.sidebar--collapsed .sidebar-box-controls{display:none !important}.sidebar-box--full{height:auto;cursor:default}.sidebar-box--full:hover{background:rgba(0,0,0,0)}.sidebar-box-full{width:100%;padding:.25rem 0;opacity:1;transition:opacity .1s ease .2s}.sidebar-box-full-label{font-size:var(--fad-font-size-xs);font-weight:var(--fad-font-weight-medium);color:var(--p-surface-500, #64748b);text-transform:uppercase;letter-spacing:.06em;display:block;margin-bottom:.375rem}.sidebar-box-full-input{width:100%}.sidebar-box-full-input :deep(.p-autocomplete-input){width:100%;font-size:var(--fad-font-size-sm)}.sidebar-box-full-input :deep(.p-autocomplete-overlay){z-index:9999 !important}.sidebar--collapsed .sidebar-box--full{opacity:0;visibility:hidden;height:0;padding:0;border:none;overflow:hidden;transition:opacity 0s,visibility 0s,height 0s,padding 0s}.sidebar--collapsed .sidebar-box-full{opacity:0;transition:opacity 0s}.sidebar-nav .nav-item{position:relative;display:flex;align-items:center;padding:var(--fad-nav-item-padding-y) var(--fad-nav-item-padding-x);padding-left:calc((var(--fad-sidebar-width-collapsed) + 1.125rem)/2 + .375rem);color:var(--p-surface-300, #cbd5e1);text-decoration:none;font-size:var(--fad-font-size-sm);font-weight:var(--fad-font-weight-normal);border-radius:0;transition:background var(--fad-transition-fast),color var(--fad-transition-fast),padding var(--fad-transition-slow)}.sidebar-nav .nav-item i{position:absolute;left:calc((var(--fad-sidebar-width-collapsed) - 1.125rem)/2);top:50%;transform:translateY(-50%);font-size:var(--fad-nav-icon-size);width:1.125rem;text-align:center;opacity:.8}.sidebar-nav .nav-item span{white-space:nowrap;opacity:1;transition:opacity .1s ease .2s}.sidebar-nav .nav-item:hover{background:var(--p-surface-700, #334155);color:var(--p-surface-0, white)}.sidebar-nav .nav-item-active{background:var(--p-surface-700, #334155);color:var(--p-primary-300, #93c5fd);border-left:2px solid var(--p-primary-400, #60a5fa);padding-left:calc(calc((var(--fad-sidebar-width-collapsed) + 1.125rem) / 2 + 0.375rem) - 2px)}.sidebar-nav .nav-item-active:hover{background:var(--p-surface-600, #475569)}.sidebar-nav .nav-item-active i{opacity:1}.sidebar--collapsed .sidebar-nav .nav-item{padding:.5rem 0;padding-left:calc((var(--fad-sidebar-width-collapsed) + 1.125rem)/2 + .375rem);border-left:none !important}.sidebar--collapsed .sidebar-nav .nav-item-active{border-left:none !important;padding-left:calc((var(--fad-sidebar-width-collapsed) + 1.125rem)/2 + .375rem)}.sidebar--collapsed .sidebar-nav .nav-item span{opacity:0;visibility:hidden;transition:opacity 0s,visibility 0s}@media(max-width: 767px){.sidebar.sidebar--collapsed .sidebar-nav .nav-item{justify-content:flex-start;padding:var(--fad-nav-item-padding-y) var(--fad-nav-item-padding-x);padding-left:calc((var(--fad-sidebar-width-collapsed) + 1.125rem)/2 + .375rem)}.sidebar.sidebar--collapsed .sidebar-nav .nav-item span{display:inline;opacity:1;visibility:visible;width:auto;transition:opacity .1s ease .2s}.sidebar.sidebar--collapsed .sidebar-nav .nav-item i{position:absolute;left:calc((var(--fad-sidebar-width-collapsed) - 1.125rem)/2);font-size:var(--fad-nav-icon-size);width:1.125rem}}:root{--qdadm-sidebar-width: 15rem;--qdadm-sidebar-collapsed-width: 2.5rem;--qdadm-sidebar-transition: 200ms ease;--qdadm-content-padding: 1rem;--qdadm-content-padding-mobile: 0.75rem}@media(max-width: 1023px){.app-layout{--qdadm-sidebar-width: 12rem}}@media(max-width: 767px){.app-layout{--qdadm-sidebar-width: 100vw}}.sidebar{transition:transform var(--qdadm-sidebar-transition),width var(--qdadm-sidebar-transition)}@media(max-width: 767px){.sidebar{transform:translateX(-100%);position:fixed;z-index:1000;box-shadow:none;width:100vw !important;height:100dvh;max-height:100dvh;overflow-y:auto}.sidebar.sidebar--open{transform:translateX(0);box-shadow:none}}.main-content{transition:margin-left var(--qdadm-sidebar-transition)}@media(max-width: 767px){.main-content{margin-left:0 !important}}.sidebar-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:99;opacity:0;transition:opacity .25s}@media(max-width: 767px){.sidebar-overlay{display:block;pointer-events:none}.sidebar-overlay.sidebar-overlay--visible{opacity:1;pointer-events:auto}}.hamburger-btn{display:none !important}@media(max-width: 767px){.hamburger-btn{display:flex !important}}@media(max-width: 767px){.page-content{padding:var(--qdadm-content-padding-mobile)}}@media(max-width: 767px){.page-header{flex-direction:column;gap:1rem;align-items:flex-start !important}}@media(max-width: 767px){.page-header .page-header-actions{width:100%;justify-content:flex-start}}@media(max-width: 767px){.p-datatable .p-datatable-wrapper{overflow-x:auto;-webkit-overflow-scrolling:touch}.p-datatable .p-datatable-table{min-width:600px}}@media(max-width: 479px){.p-datatable .p-datatable-table{min-width:500px}}@media(max-width: 767px){.filter-bar{flex-wrap:wrap;gap:.75rem}}@media(max-width: 767px){.filter-bar-left{flex-wrap:wrap;gap:.5rem;width:100%}}@media(max-width: 767px){.card{border-radius:0;border:none;background:rgba(0,0,0,0);padding:0}}@media(max-width: 767px){.form-grid{grid-template-columns:1fr !important}}@media(max-width: 479px){.form-actions{flex-direction:column;gap:.5rem}.form-actions .form-actions-left{width:100%;flex-direction:column}.form-actions .p-button{width:100%;white-space:nowrap}}@media(max-width: 767px){.cards-grid{grid-template-columns:repeat(2, 1fr)}}@media(max-width: 479px){.cards-grid{grid-template-columns:1fr}}@media(max-width: 767px){.hide-mobile{display:none !important}}.hide-desktop{display:none !important}@media(max-width: 767px){.hide-desktop{display:block !important}}@media(max-width: 767px){.hide-mobile-inline{display:none !important}}.show-mobile-inline{display:none !important}@media(max-width: 767px){.show-mobile-inline{display:inline !important}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quazardous/qdadm",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.22.0",
|
|
4
4
|
"description": "Vue 3 framework for admin dashboards with PrimeVue",
|
|
5
5
|
"author": "quazardous",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"format": "prettier --write src/",
|
|
26
26
|
"build:types": "vue-tsc --declaration --emitDeclarationOnly --outDir dist/types",
|
|
27
27
|
"build:node": "tsc -p tsconfig.node.json",
|
|
28
|
-
"
|
|
28
|
+
"build:styles": "sass --no-source-map --style=compressed src/styles/index.scss dist/styles/qdadm.css",
|
|
29
|
+
"prepare": "npm run build:node && npm run build:styles"
|
|
29
30
|
},
|
|
30
31
|
"exports": {
|
|
31
32
|
".": "./src/index.ts",
|
|
@@ -38,6 +39,10 @@
|
|
|
38
39
|
"./utils": "./src/utils/index.ts",
|
|
39
40
|
"./modules/debug": "./src/modules/debug/index.ts",
|
|
40
41
|
"./styles": {
|
|
42
|
+
"types": "./src/styles/index.d.ts",
|
|
43
|
+
"default": "./dist/styles/qdadm.css"
|
|
44
|
+
},
|
|
45
|
+
"./styles/scss": {
|
|
41
46
|
"types": "./src/styles/index.d.ts",
|
|
42
47
|
"default": "./src/styles/index.scss"
|
|
43
48
|
},
|
|
@@ -51,11 +56,12 @@
|
|
|
51
56
|
"src",
|
|
52
57
|
"README.md",
|
|
53
58
|
"LICENSE",
|
|
54
|
-
"dist/node"
|
|
59
|
+
"dist/node",
|
|
60
|
+
"dist/styles"
|
|
55
61
|
],
|
|
56
62
|
"dependencies": {
|
|
57
63
|
"@quazardous/qdcore": "^1.2.0",
|
|
58
|
-
"@quazardous/qddebug": "^1.
|
|
64
|
+
"@quazardous/qddebug": "^1.3.0",
|
|
59
65
|
"@quazardous/quarkernel": "^2.1.0",
|
|
60
66
|
"yaml": "^2.8.4"
|
|
61
67
|
},
|
|
@@ -12,8 +12,12 @@
|
|
|
12
12
|
* - getFieldError: function to get field error message
|
|
13
13
|
* - handleFieldBlur: function to trigger validation on blur
|
|
14
14
|
* - formSubmitted: ref indicating if form was submitted
|
|
15
|
+
*
|
|
16
|
+
* The label names the control inside (#2268): FormInput binds the field's id, any other control takes it after
|
|
17
|
+
* render, and the slot receives `inputId` and `labelId` for controls that need them.
|
|
15
18
|
*/
|
|
16
|
-
import { inject, computed, type Ref, type CSSProperties } from 'vue'
|
|
19
|
+
import { inject, computed, provide, ref, useId, onMounted, onUpdated, type Ref, type CSSProperties } from 'vue'
|
|
20
|
+
import { FORM_FIELD_IDS } from './formFieldIds'
|
|
17
21
|
|
|
18
22
|
interface Props {
|
|
19
23
|
name: string
|
|
@@ -70,12 +74,55 @@ const fieldStyle = computed((): CSSProperties =>
|
|
|
70
74
|
function onBlur(): void {
|
|
71
75
|
handleFieldBlur(props.name)
|
|
72
76
|
}
|
|
77
|
+
|
|
78
|
+
// ─── The label names the control (#2268) ─────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
// Unique to this field instance: two forms on one page do not share ids.
|
|
81
|
+
const uid = useId()
|
|
82
|
+
const inputId = computed((): string => `${uid}-${props.name.replace(/[^\w-]/g, '-')}`)
|
|
83
|
+
const labelId = computed((): string => `${inputId.value}-label`)
|
|
84
|
+
const slotLabelId = computed((): string | undefined => (props.label ? labelId.value : undefined))
|
|
85
|
+
provide(FORM_FIELD_IDS, { inputId, labelId: slotLabelId })
|
|
86
|
+
|
|
87
|
+
const root = ref<HTMLElement | null>(null)
|
|
88
|
+
/** The control's own id, when it has one: the label points there instead. */
|
|
89
|
+
const ownControlId = ref<string | null>(null)
|
|
90
|
+
const labelFor = computed((): string => ownControlId.value ?? inputId.value)
|
|
91
|
+
|
|
92
|
+
const CONTROLS =
|
|
93
|
+
'input:not([type="hidden"]), select, textarea, button, [role="combobox"], [role="checkbox"], [role="switch"], ' +
|
|
94
|
+
'[role="spinbutton"], [role="textbox"], [role="slider"], [contenteditable="true"]'
|
|
95
|
+
const LABELABLE = new Set(['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON', 'METER', 'OUTPUT', 'PROGRESS'])
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A control FormInput did not render (a widget put directly in the slot) takes the field's id; one with an id of its
|
|
99
|
+
* own keeps it and the label follows. An element `<label for>` cannot name gets aria-labelledby.
|
|
100
|
+
*/
|
|
101
|
+
function tieLabel(): void {
|
|
102
|
+
const el = root.value
|
|
103
|
+
if (!el || !props.label) return
|
|
104
|
+
// Array.from, not a spread: a consumer's tsconfig may lack DOM.Iterable.
|
|
105
|
+
const controls = Array.from(el.querySelectorAll<HTMLElement>(CONTROLS))
|
|
106
|
+
let control = controls.find((c) => c.id === labelFor.value)
|
|
107
|
+
if (!control) {
|
|
108
|
+
control = controls[0]
|
|
109
|
+
if (!control) return
|
|
110
|
+
if (!control.id) control.id = inputId.value
|
|
111
|
+
const target = control.id === inputId.value ? null : control.id
|
|
112
|
+
if (ownControlId.value !== target) ownControlId.value = target
|
|
113
|
+
}
|
|
114
|
+
if (!LABELABLE.has(control.tagName) && !control.hasAttribute('aria-labelledby') && !control.hasAttribute('aria-label')) {
|
|
115
|
+
control.setAttribute('aria-labelledby', labelId.value)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
onMounted(tieLabel)
|
|
119
|
+
onUpdated(tieLabel)
|
|
73
120
|
</script>
|
|
74
121
|
|
|
75
122
|
<template>
|
|
76
|
-
<div :class="fieldClasses" :style="fieldStyle">
|
|
77
|
-
<label v-if="label" :for="
|
|
78
|
-
<slot :onBlur="onBlur"></slot>
|
|
123
|
+
<div ref="root" :class="fieldClasses" :style="fieldStyle">
|
|
124
|
+
<label v-if="label" :id="labelId" :for="labelFor">{{ label }}</label>
|
|
125
|
+
<slot :onBlur="onBlur" :inputId="inputId" :labelId="slotLabelId"></slot>
|
|
79
126
|
<small v-if="showError" class="field-error">{{ fieldError }}</small>
|
|
80
127
|
<small v-else-if="hint" class="field-hint">{{ hint }}</small>
|
|
81
128
|
</div>
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* Usage:
|
|
9
9
|
* <FormInput :field="f" v-model="form.data.value[f.name]" />
|
|
10
10
|
*/
|
|
11
|
-
import { computed, type PropType } from 'vue'
|
|
11
|
+
import { computed, inject, type PropType } from 'vue'
|
|
12
|
+
import { FORM_FIELD_IDS } from './formFieldIds'
|
|
12
13
|
import InputText from 'primevue/inputtext'
|
|
13
14
|
import InputNumber from 'primevue/inputnumber'
|
|
14
15
|
import Textarea from 'primevue/textarea'
|
|
@@ -37,6 +38,12 @@ interface FieldConfig {
|
|
|
37
38
|
options?: SelectOption[] | unknown[]
|
|
38
39
|
optionLabel?: string
|
|
39
40
|
optionValue?: string
|
|
41
|
+
/** number: digits after the decimal point — `2`, or `{ min, max }` (#2316) */
|
|
42
|
+
fractionDigits?: number | { min?: number; max?: number }
|
|
43
|
+
/** number: bounds and increment (#2316) */
|
|
44
|
+
min?: number
|
|
45
|
+
max?: number
|
|
46
|
+
step?: number
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
const props = defineProps({
|
|
@@ -45,9 +52,16 @@ const props = defineProps({
|
|
|
45
52
|
// generateFields loop) must bind without casts (#1387)
|
|
46
53
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
54
|
modelValue: { type: null as unknown as PropType<any>, default: null },
|
|
48
|
-
hint: { type: String as () => InputType | null, default: null } // Optional type hint override
|
|
55
|
+
hint: { type: String as () => InputType | null, default: null }, // Optional type hint override
|
|
56
|
+
/** The control's id. Inside a FormField it defaults to the id the field's label points at (#2268). */
|
|
57
|
+
inputId: { type: String as PropType<string | null>, default: null }
|
|
49
58
|
})
|
|
50
59
|
|
|
60
|
+
// Inside a FormField, its label names this control (#2268): each PrimeVue control takes the id with its own prop.
|
|
61
|
+
const fieldIds = inject(FORM_FIELD_IDS, null)
|
|
62
|
+
const controlId = computed<string | undefined>(() => props.inputId ?? fieldIds?.inputId.value ?? undefined)
|
|
63
|
+
const labelledBy = computed<string | undefined>(() => fieldIds?.labelId.value)
|
|
64
|
+
|
|
51
65
|
const emit = defineEmits<{
|
|
52
66
|
'update:modelValue': [value: ModelValue]
|
|
53
67
|
}>()
|
|
@@ -73,6 +87,20 @@ const booleanValue = computed<boolean>({
|
|
|
73
87
|
set: (v: boolean): void => emit('update:modelValue', v)
|
|
74
88
|
})
|
|
75
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Digits after the decimal point in a number field (#2316): `fractionDigits: 2`, or `{ min, max }`. Without it
|
|
92
|
+
* InputNumber refuses the decimal key, so none is set by default: whole numbers, as before.
|
|
93
|
+
*/
|
|
94
|
+
const fraction = computed<{ min?: number; max?: number }>(() => {
|
|
95
|
+
const digits = props.field?.fractionDigits as number | { min?: number; max?: number } | undefined
|
|
96
|
+
if (typeof digits === 'number') return { min: digits, max: digits }
|
|
97
|
+
if (digits && typeof digits === 'object') {
|
|
98
|
+
const min = digits.min ?? 0
|
|
99
|
+
return { min, max: Math.max(min, digits.max ?? min) }
|
|
100
|
+
}
|
|
101
|
+
return {}
|
|
102
|
+
})
|
|
103
|
+
|
|
76
104
|
// Resolve component type: field.type > hint > 'text'
|
|
77
105
|
const inputType = computed<InputType>(() => (props.field?.type as InputType) || props.hint || 'text')
|
|
78
106
|
|
|
@@ -96,6 +124,7 @@ const dateValue = computed<Date | null>({
|
|
|
96
124
|
<InputText
|
|
97
125
|
v-if="inputType === 'text' || inputType === 'email'"
|
|
98
126
|
v-model="stringValue"
|
|
127
|
+
:id="controlId"
|
|
99
128
|
:placeholder="field.placeholder"
|
|
100
129
|
:disabled="field.disabled"
|
|
101
130
|
:readonly="field.readonly"
|
|
@@ -104,6 +133,7 @@ const dateValue = computed<Date | null>({
|
|
|
104
133
|
<Password
|
|
105
134
|
v-else-if="inputType === 'password'"
|
|
106
135
|
v-model="stringValue"
|
|
136
|
+
:inputId="controlId"
|
|
107
137
|
:placeholder="field.placeholder"
|
|
108
138
|
:disabled="field.disabled"
|
|
109
139
|
:feedback="false"
|
|
@@ -113,15 +143,22 @@ const dateValue = computed<Date | null>({
|
|
|
113
143
|
<InputNumber
|
|
114
144
|
v-else-if="inputType === 'number'"
|
|
115
145
|
v-model="numberValue"
|
|
146
|
+
:inputId="controlId"
|
|
116
147
|
:placeholder="field.placeholder"
|
|
117
148
|
:disabled="field.disabled"
|
|
118
149
|
:readonly="field.readonly"
|
|
119
150
|
:useGrouping="false"
|
|
151
|
+
:minFractionDigits="fraction.min"
|
|
152
|
+
:maxFractionDigits="fraction.max"
|
|
153
|
+
:min="field.min"
|
|
154
|
+
:max="field.max"
|
|
155
|
+
:step="field.step"
|
|
120
156
|
class="w-full"
|
|
121
157
|
/>
|
|
122
158
|
<Textarea
|
|
123
159
|
v-else-if="inputType === 'textarea'"
|
|
124
160
|
v-model="stringValue"
|
|
161
|
+
:id="controlId"
|
|
125
162
|
:placeholder="field.placeholder"
|
|
126
163
|
:disabled="field.disabled"
|
|
127
164
|
:readonly="field.readonly"
|
|
@@ -131,6 +168,8 @@ const dateValue = computed<Date | null>({
|
|
|
131
168
|
<Select
|
|
132
169
|
v-else-if="inputType === 'select'"
|
|
133
170
|
v-model="value"
|
|
171
|
+
:labelId="controlId"
|
|
172
|
+
:ariaLabelledby="labelledBy"
|
|
134
173
|
:options="field.options"
|
|
135
174
|
:optionLabel="field.optionLabel"
|
|
136
175
|
:optionValue="field.optionValue"
|
|
@@ -141,12 +180,14 @@ const dateValue = computed<Date | null>({
|
|
|
141
180
|
<Checkbox
|
|
142
181
|
v-else-if="inputType === 'boolean'"
|
|
143
182
|
v-model="booleanValue"
|
|
183
|
+
:inputId="controlId"
|
|
144
184
|
:disabled="field.disabled"
|
|
145
185
|
binary
|
|
146
186
|
/>
|
|
147
187
|
<DatePicker
|
|
148
188
|
v-else-if="inputType === 'date' || inputType === 'datetime'"
|
|
149
189
|
v-model="dateValue"
|
|
190
|
+
:inputId="controlId"
|
|
150
191
|
:placeholder="field.placeholder"
|
|
151
192
|
:disabled="field.disabled"
|
|
152
193
|
:showTime="inputType === 'datetime'"
|
|
@@ -156,6 +197,7 @@ const dateValue = computed<Date | null>({
|
|
|
156
197
|
<InputText
|
|
157
198
|
v-else
|
|
158
199
|
v-model="stringValue"
|
|
200
|
+
:id="controlId"
|
|
159
201
|
:placeholder="field.placeholder"
|
|
160
202
|
:disabled="field.disabled"
|
|
161
203
|
class="w-full"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ComputedRef, InjectionKey } from 'vue'
|
|
2
|
+
|
|
3
|
+
/** What a FormField gives the control inside it, so its label names that control (#2268). */
|
|
4
|
+
export interface FormFieldIds {
|
|
5
|
+
/** The id the field's label points at: unique to the field instance. */
|
|
6
|
+
inputId: ComputedRef<string>
|
|
7
|
+
/** The label's own id, for controls named with aria-labelledby. Undefined when the field has no label. */
|
|
8
|
+
labelId: ComputedRef<string | undefined>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const FORM_FIELD_IDS: InjectionKey<FormFieldIds> = Symbol('qdadm.formFieldIds')
|