@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.
Files changed (51) hide show
  1. package/dist/node/vite/qdadmDebugPlugin.d.ts +9 -4
  2. package/dist/node/vite/qdadmDebugPlugin.js +36 -10
  3. package/dist/node/vite/qdadmPlugin.js +2 -0
  4. package/dist/styles/qdadm.css +1 -0
  5. package/package.json +10 -4
  6. package/src/components/edit/FormField.vue +51 -4
  7. package/src/components/edit/FormInput.vue +44 -2
  8. package/src/components/edit/formFieldIds.ts +11 -0
  9. package/src/components/editors/RoleGrantsEditor.vue +355 -0
  10. package/src/components/index.ts +1 -0
  11. package/src/components/show/ShowPage.vue +4 -2
  12. package/src/composables/confirmDelete.ts +35 -0
  13. package/src/composables/index.ts +1 -0
  14. package/src/composables/useEntityItemFormPage.ts +2 -3
  15. package/src/composables/useEntityItemFormPage.types.ts +1 -0
  16. package/src/composables/useEntityItemShowPage.ts +3 -3
  17. package/src/composables/useListPage.ts +3 -6
  18. package/src/composables/useListPage.types.ts +1 -0
  19. package/src/composables/useSecurity.ts +41 -0
  20. package/src/entity/auth/SecurityChecker.ts +88 -2
  21. package/src/entity/auth/index.ts +2 -0
  22. package/src/i18n/defaults/DefaultCoreProvider.ts +4 -2
  23. package/src/i18n/defaults/__tests__/generated-sync.test.ts +9 -0
  24. package/src/i18n/defaults/core.en.generated.ts +20 -0
  25. package/src/i18n/defaults/core.en.yml +20 -0
  26. package/src/i18n/defaults/core.fr.generated.ts +20 -0
  27. package/src/i18n/defaults/core.fr.yml +20 -0
  28. package/src/index.ts +1 -0
  29. package/src/kernel/Kernel.auth.ts +4 -0
  30. package/src/kernel/Kernel.registries.ts +24 -5
  31. package/src/kernel/Kernel.routing.ts +20 -4
  32. package/src/kernel/Kernel.ts +3 -0
  33. package/src/kernel/Kernel.types.ts +7 -1
  34. package/src/kernel/Kernel.vue.ts +3 -0
  35. package/src/kernel/SignalBus.ts +6 -0
  36. package/src/kernel/chunkReload.ts +110 -0
  37. package/src/kernel/vitePluginCheck.ts +61 -0
  38. package/src/modules/debug/AuthCollector.ts +15 -1
  39. package/src/modules/debug/DebugModule.ts +9 -0
  40. package/src/modules/debug/components/panels/AuthPanel.vue +1 -0
  41. package/src/notifications/NotificationListener.vue +2 -1
  42. package/src/security/RolesProvider.ts +8 -0
  43. package/src/security/index.ts +17 -0
  44. package/src/security/pages/RoleForm.vue +42 -191
  45. package/src/security/roleGrants.ts +210 -0
  46. package/src/styles/_forms.scss +10 -2
  47. package/src/styles/index.d.ts +3 -4
  48. package/src/toast/ToastListener.vue +2 -1
  49. package/src/types/index.ts +10 -0
  50. package/src/vite/qdadmDebugPlugin.ts +46 -10
  51. 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 that loads the app gets a fresh
12
- * session id (uuid in the injected client). All endpoints accept a
13
- * `?session=<id|latest>` query param. Default is `latest` (the most recently
14
- * active session).
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 that loads the app gets a fresh
12
- * session id (uuid in the injected client). All endpoints accept a
13
- * `?session=<id|latest>` query param. Default is `latest` (the most recently
14
- * active session).
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 cutoff = Date.now() - sessionTtlMs;
39
+ const now = Date.now();
37
40
  for (const [id, s] of sessions) {
38
- if (s.lastSeenAt < cutoff)
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
- if (!best || s.lastSeenAt > best.lastSeenAt)
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
- sessions.delete(msg.sessionId);
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). Pushes describe + snapshot
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
- const sessionId = crypto.randomUUID()
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.20.0",
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
- "prepare": "npm run build:node"
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.2.0",
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="name">{{ label }}</label>
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')