@quazardous/qdadm 2.12.0 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quazardous/qdadm",
3
- "version": "2.12.0",
3
+ "version": "2.14.0",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -71,6 +71,9 @@
71
71
  },
72
72
  "vite": {
73
73
  "optional": true
74
+ },
75
+ "primevue": {
76
+ "optional": true
74
77
  }
75
78
  },
76
79
  "optionalDependencies": {
@@ -20,7 +20,7 @@
20
20
  * Just some information.
21
21
  * </InfoBanner>
22
22
  */
23
- import Message from 'primevue/message'
23
+ import QdMessage from './base/QdMessage.vue'
24
24
  import { computed } from 'vue'
25
25
 
26
26
  type Severity = 'info' | 'success' | 'warn' | 'error' | 'secondary' | 'contrast'
@@ -58,7 +58,7 @@ const iconClass = computed(() => {
58
58
  </script>
59
59
 
60
60
  <template>
61
- <Message
61
+ <QdMessage
62
62
  :severity="severity"
63
63
  :closable="closable"
64
64
  class="info-banner"
@@ -69,5 +69,5 @@ const iconClass = computed(() => {
69
69
  <slot />
70
70
  </span>
71
71
  </div>
72
- </Message>
72
+ </QdMessage>
73
73
  </template>
@@ -0,0 +1,20 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * QdButton — the single indirection point over the widget library's Button
4
+ * (#1391 exit-readiness). Generic pass-through: every attr/prop/listener and
5
+ * every slot is forwarded verbatim, so swapping or shimming the underlying
6
+ * widget is a one-file change. Imports `primevue/button`, which the
7
+ * qdadmVitePlugin flavor alias (#1393) traverses.
8
+ */
9
+ import Button from 'primevue/button'
10
+
11
+ defineOptions({ inheritAttrs: false })
12
+ </script>
13
+
14
+ <template>
15
+ <Button v-bind="$attrs">
16
+ <template v-for="(_, name) in $slots" #[name]="slotProps">
17
+ <slot :name="name" v-bind="slotProps ?? {}" />
18
+ </template>
19
+ </Button>
20
+ </template>
@@ -0,0 +1,17 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * QdMessage — the single indirection point over the widget library's Message
4
+ * (#1391 exit-readiness). Generic pass-through, same contract as QdButton.
5
+ */
6
+ import Message from 'primevue/message'
7
+
8
+ defineOptions({ inheritAttrs: false })
9
+ </script>
10
+
11
+ <template>
12
+ <Message v-bind="$attrs">
13
+ <template v-for="(_, name) in $slots" #[name]="slotProps">
14
+ <slot :name="name" v-bind="slotProps ?? {}" />
15
+ </template>
16
+ </Message>
17
+ </template>
@@ -58,7 +58,7 @@
58
58
  */
59
59
  import { computed, watch, type PropType } from 'vue'
60
60
  import Dialog from 'primevue/dialog'
61
- import Button from 'primevue/button'
61
+ import QdButton from '../base/QdButton.vue'
62
62
  import Stepper from 'primevue/stepper'
63
63
  import StepList from 'primevue/steplist'
64
64
  import Step from 'primevue/step'
@@ -284,7 +284,7 @@ const slotActions = computed<SlotActions>(() => ({
284
284
  <div class="step-actions">
285
285
  <slot name="actions" v-bind="slotActions">
286
286
  <!-- Back button -->
287
- <Button
287
+ <QdButton
288
288
  v-if="!isFirstStep || showBackOnFirst"
289
289
  :label="backLabel"
290
290
  icon="pi pi-arrow-left"
@@ -294,7 +294,7 @@ const slotActions = computed<SlotActions>(() => ({
294
294
  />
295
295
 
296
296
  <!-- Cancel button -->
297
- <Button
297
+ <QdButton
298
298
  :label="cancelLabel"
299
299
  severity="secondary"
300
300
  @click="cancel"
@@ -302,7 +302,7 @@ const slotActions = computed<SlotActions>(() => ({
302
302
  />
303
303
 
304
304
  <!-- Next button (not on last step) -->
305
- <Button
305
+ <QdButton
306
306
  v-if="!isLastStep"
307
307
  :label="nextLabel"
308
308
  icon="pi pi-arrow-right"
@@ -312,7 +312,7 @@ const slotActions = computed<SlotActions>(() => ({
312
312
  />
313
313
 
314
314
  <!-- Submit button (only on last step) -->
315
- <Button
315
+ <QdButton
316
316
  v-if="isLastStep"
317
317
  :label="submitLabel"
318
318
  icon="pi pi-check"
@@ -26,13 +26,13 @@
26
26
  * <SimpleDialog v-model:visible="showDialog" title="View Key" :showConfirm="false">
27
27
  * <p>{{ key }}</p>
28
28
  * <template #actions>
29
- * <Button label="Copy" icon="pi pi-copy" @click="copyKey" />
29
+ * <QdButton label="Copy" icon="pi pi-copy" @click="copyKey" />
30
30
  * </template>
31
31
  * </SimpleDialog>
32
32
  */
33
33
  import { computed } from 'vue'
34
34
  import Dialog from 'primevue/dialog'
35
- import Button from 'primevue/button'
35
+ import QdButton from '../base/QdButton.vue'
36
36
 
37
37
  interface Props {
38
38
  visible?: boolean
@@ -101,14 +101,14 @@ const canClose = computed((): boolean => props.closable && !props.loading)
101
101
 
102
102
  <template #footer v-if="showCancel || showConfirm || $slots.actions">
103
103
  <slot name="actions" />
104
- <Button
104
+ <QdButton
105
105
  v-if="showCancel"
106
106
  :label="cancelLabel"
107
107
  severity="secondary"
108
108
  @click="onCancel"
109
109
  :disabled="loading"
110
110
  />
111
- <Button
111
+ <QdButton
112
112
  v-if="showConfirm"
113
113
  :label="confirmLabel"
114
114
  :icon="confirmIcon"
@@ -8,7 +8,7 @@
8
8
  * - Stay: cancels navigation
9
9
  */
10
10
  import SimpleDialog from './SimpleDialog.vue'
11
- import Button from 'primevue/button'
11
+ import QdButton from '../base/QdButton.vue'
12
12
 
13
13
  interface Props {
14
14
  visible?: boolean
@@ -67,7 +67,7 @@ function onSaveAndLeave(): void {
67
67
  </div>
68
68
 
69
69
  <template #actions>
70
- <Button
70
+ <QdButton
71
71
  v-if="hasOnSave"
72
72
  label="Save & Leave"
73
73
  icon="pi pi-save"
@@ -7,7 +7,7 @@
7
7
  * <CopyableId :value="apiKey" label="API Key" />
8
8
  */
9
9
  import { ref, inject } from 'vue'
10
- import Button from 'primevue/button'
10
+ import QdButton from '../base/QdButton.vue'
11
11
  import type { Orchestrator } from '../../orchestrator/Orchestrator'
12
12
 
13
13
  interface Props {
@@ -40,7 +40,7 @@ async function copyToClipboard(): Promise<void> {
40
40
  <div class="form-field">
41
41
  <label class="form-field-label">{{ label }}</label>
42
42
  <div class="copyable-id">
43
- <Button
43
+ <QdButton
44
44
  type="button"
45
45
  :icon="copied ? 'pi pi-check' : 'pi pi-copy'"
46
46
  :severity="copied ? 'success' : 'secondary'"
@@ -13,7 +13,7 @@
13
13
  * - cancel: Cancel/close without saving
14
14
  */
15
15
 
16
- import Button from 'primevue/button'
16
+ import QdButton from '../base/QdButton.vue'
17
17
  import { useI18n } from '../../i18n/useI18n'
18
18
 
19
19
  const { t } = useI18n()
@@ -42,7 +42,7 @@ const emit = defineEmits<{
42
42
  <template>
43
43
  <div class="form-actions">
44
44
  <div class="form-actions-left">
45
- <Button
45
+ <QdButton
46
46
  type="button"
47
47
  :label="isEdit ? t('core.actions.update') : t('core.actions.create')"
48
48
  :loading="saving"
@@ -51,7 +51,7 @@ const emit = defineEmits<{
51
51
  @click="emit('save')"
52
52
  v-tooltip.top="t('core.tooltips.saveAndContinue')"
53
53
  />
54
- <Button
54
+ <QdButton
55
55
  v-if="showSaveAndClose"
56
56
  type="button"
57
57
  :label="isEdit ? t('core.actions.updateAndClose') : t('core.actions.createAndClose')"
@@ -66,7 +66,7 @@ const emit = defineEmits<{
66
66
  <i class="pi pi-circle-fill"></i>
67
67
  </span>
68
68
  </div>
69
- <Button
69
+ <QdButton
70
70
  type="button"
71
71
  :label="t('core.actions.cancel')"
72
72
  severity="secondary"
@@ -38,8 +38,8 @@ import FormActions from './FormActions.vue'
38
38
  import UnsavedChangesDialog from '../dialogs/UnsavedChangesDialog.vue'
39
39
  import CardShell from '../layout/CardShell.vue'
40
40
  import { formatFetchError } from '../../utils/errors'
41
- import Button from 'primevue/button'
42
- import Message from 'primevue/message'
41
+ import QdButton from '../base/QdButton.vue'
42
+ import QdMessage from '../base/QdMessage.vue'
43
43
  import type { ResolvedAction, ResolvedFieldConfig } from '../../composables/useEntityItemFormPage'
44
44
  import type { GuardDialogState } from '../../composables/useUnsavedChangesGuard'
45
45
 
@@ -151,7 +151,7 @@ function onGuardStay(): void {
151
151
  <template #actions>
152
152
  <slot name="header-actions" />
153
153
  <!-- Header actions from builder (excludes save/delete/cancel) -->
154
- <Button
154
+ <QdButton
155
155
  v-for="action in headerActions"
156
156
  :key="action.name"
157
157
  :label="action.label"
@@ -180,16 +180,16 @@ function onGuardStay(): void {
180
180
  <!-- Error State (fetch error) -->
181
181
  <template v-else-if="fetchError">
182
182
  <slot name="error" :error="fetchError">
183
- <Message severity="error" :closable="false" class="form-error-message">
183
+ <QdMessage severity="error" :closable="false" class="form-error-message">
184
184
  {{ fetchErrorMessage }}
185
- </Message>
185
+ </QdMessage>
186
186
  </slot>
187
187
  </template>
188
188
 
189
189
  <!-- Form Content -->
190
190
  <template v-else>
191
191
  <!-- Validation Error Summary -->
192
- <Message
192
+ <QdMessage
193
193
  v-if="errorSummary && errorSummary.length > 0"
194
194
  severity="warn"
195
195
  :closable="false"
@@ -200,7 +200,7 @@ function onGuardStay(): void {
200
200
  <strong>{{ error.label }}:</strong> {{ error.message }}
201
201
  </li>
202
202
  </ul>
203
- </Message>
203
+ </QdMessage>
204
204
 
205
205
  <!-- Single content body; Card wrapper is conditional (#1193) -->
206
206
  <CardShell :card="cardWrapper">
@@ -12,7 +12,7 @@ import Dialog from 'primevue/dialog'
12
12
  import DataTable from 'primevue/datatable'
13
13
  import Column from 'primevue/column'
14
14
  import InputText from 'primevue/inputtext'
15
- import Button from 'primevue/button'
15
+ import QdButton from '../base/QdButton.vue'
16
16
  import { useI18n } from '../../i18n/useI18n'
17
17
 
18
18
  const { t } = useI18n()
@@ -208,12 +208,12 @@ const confirmLabel = computed(() => {
208
208
  </DataTable>
209
209
 
210
210
  <template #footer>
211
- <Button
211
+ <QdButton
212
212
  :label="t('core.actions.cancel')"
213
213
  severity="secondary"
214
214
  @click="onCancel"
215
215
  />
216
- <Button
216
+ <QdButton
217
217
  :label="confirmLabel"
218
218
  icon="pi pi-check"
219
219
  :disabled="confirmDisabled"
@@ -10,10 +10,10 @@
10
10
  */
11
11
 
12
12
  import { ref, computed, watch } from 'vue'
13
- import Button from 'primevue/button'
13
+ import QdButton from '../base/QdButton.vue'
14
14
  import Textarea from 'primevue/textarea'
15
15
  import InputText from 'primevue/inputtext'
16
- import Message from 'primevue/message'
16
+ import QdMessage from '../base/QdMessage.vue'
17
17
  import { getJsonValueType, getJsonPreview } from '../../composables/useJsonSyntax'
18
18
 
19
19
  interface Props {
@@ -165,7 +165,7 @@ function isMultiline(value: unknown): boolean {
165
165
  <!-- Toolbar -->
166
166
  <div class="editor-toolbar">
167
167
  <div class="toolbar-left">
168
- <Button
168
+ <QdButton
169
169
  :icon="rawJsonMode ? 'pi pi-list' : 'pi pi-code'"
170
170
  :label="rawJsonMode ? 'Structured' : 'Raw JSON'"
171
171
  size="small"
@@ -175,7 +175,7 @@ function isMultiline(value: unknown): boolean {
175
175
  />
176
176
  </div>
177
177
  <div class="toolbar-right" v-if="!rawJsonMode">
178
- <Button
178
+ <QdButton
179
179
  icon="pi pi-plus"
180
180
  label="Expand All"
181
181
  size="small"
@@ -183,7 +183,7 @@ function isMultiline(value: unknown): boolean {
183
183
  text
184
184
  @click="expandAll"
185
185
  />
186
- <Button
186
+ <QdButton
187
187
  icon="pi pi-minus"
188
188
  label="Collapse All"
189
189
  size="small"
@@ -282,12 +282,12 @@ function isMultiline(value: unknown): boolean {
282
282
  spellcheck="false"
283
283
  ></textarea>
284
284
  <div v-if="parseError" class="raw-json-actions">
285
- <Message severity="error" :closable="false" class="parse-error">
285
+ <QdMessage severity="error" :closable="false" class="parse-error">
286
286
  {{ parseError }}
287
- </Message>
287
+ </QdMessage>
288
288
  </div>
289
289
  <div v-else class="raw-json-actions">
290
- <Button
290
+ <QdButton
291
291
  label="Apply Changes"
292
292
  icon="pi pi-check"
293
293
  size="small"
@@ -42,7 +42,7 @@
42
42
 
43
43
  import { ref, computed, watch } from 'vue'
44
44
  import SelectButton from 'primevue/selectbutton'
45
- import Message from 'primevue/message'
45
+ import QdMessage from '../base/QdMessage.vue'
46
46
  import VanillaJsonEditor from './VanillaJsonEditor.vue'
47
47
  import type { Mode } from 'vanilla-jsoneditor'
48
48
 
@@ -179,14 +179,14 @@ watch(() => props.mode, (newMode: ViewMode | null | undefined) => {
179
179
 
180
180
  <!-- JSON View -->
181
181
  <div v-if="viewMode === 'json'" class="json-view">
182
- <Message
182
+ <QdMessage
183
183
  v-if="guardInvalidJson && hasJsonError"
184
184
  severity="error"
185
185
  :closable="false"
186
186
  class="json-error"
187
187
  >
188
188
  {{ invalidJsonMessage }}
189
- </Message>
189
+ </QdMessage>
190
190
  <VanillaJsonEditor
191
191
  :modelValue="effectiveJsonValue"
192
192
  :mode="jsonMode"
@@ -18,7 +18,7 @@
18
18
  */
19
19
 
20
20
  import { ref, computed } from 'vue'
21
- import Button from 'primevue/button'
21
+ import QdButton from '../base/QdButton.vue'
22
22
  import InputText from 'primevue/inputtext'
23
23
  import AutoComplete from 'primevue/autocomplete'
24
24
  import Slider from 'primevue/slider'
@@ -161,7 +161,7 @@ function updateItemValue(index: number, value: string | number): void {
161
161
  />
162
162
  </template>
163
163
  </div>
164
- <Button
164
+ <QdButton
165
165
  icon="pi pi-times"
166
166
  severity="danger"
167
167
  text
@@ -172,7 +172,7 @@ function updateItemValue(index: number, value: string | number): void {
172
172
  </div>
173
173
  </div>
174
174
  <div class="kv-add">
175
- <Button
175
+ <QdButton
176
176
  icon="pi pi-plus"
177
177
  @click="addItem"
178
178
  :disabled="!newItem.key || (isTextMode && !newItem.value)"
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { ref, computed } from 'vue'
12
- import Button from 'primevue/button'
12
+ import QdButton from '../base/QdButton.vue'
13
13
  import InputText from 'primevue/inputtext'
14
14
  import Slider from 'primevue/slider'
15
15
  import Checkbox from 'primevue/checkbox'
@@ -113,7 +113,7 @@ function updateFluency(index: number, fluency: number | number[]): void {
113
113
  style="width: 100px"
114
114
  />
115
115
  </div>
116
- <Button
116
+ <QdButton
117
117
  v-if="lang.primary"
118
118
  icon="pi pi-star-fill"
119
119
  severity="warning"
@@ -122,7 +122,7 @@ function updateFluency(index: number, fluency: number | number[]): void {
122
122
  size="small"
123
123
  v-tooltip.top="'Primary language'"
124
124
  />
125
- <Button
125
+ <QdButton
126
126
  v-else
127
127
  icon="pi pi-star"
128
128
  severity="secondary"
@@ -132,7 +132,7 @@ function updateFluency(index: number, fluency: number | number[]): void {
132
132
  @click="setPrimaryLanguage(index)"
133
133
  v-tooltip.top="'Set as primary'"
134
134
  />
135
- <Button
135
+ <QdButton
136
136
  icon="pi pi-times"
137
137
  severity="danger"
138
138
  text
@@ -143,7 +143,7 @@ function updateFluency(index: number, fluency: number | number[]): void {
143
143
  </div>
144
144
  </div>
145
145
  <div class="lang-add">
146
- <Button
146
+ <QdButton
147
147
  icon="pi pi-plus"
148
148
  @click="addLanguage"
149
149
  :disabled="!newLanguage.code"
@@ -3,7 +3,7 @@ import { ref, computed, onMounted, watch, inject } from 'vue'
3
3
  import type { HttpClient } from '../../entity/storage/ApiStorage'
4
4
  import AutoComplete from 'primevue/autocomplete'
5
5
  import Select from 'primevue/select'
6
- import Button from 'primevue/button'
6
+ import QdButton from '../base/QdButton.vue'
7
7
 
8
8
  interface ScopeRow {
9
9
  resource: string
@@ -266,7 +266,7 @@ onMounted(async () => {
266
266
  />
267
267
  <span v-if="isRowComplete(row)" class="scope-valid">✓</span>
268
268
  <span v-else class="scope-incomplete">...</span>
269
- <Button
269
+ <QdButton
270
270
  icon="pi pi-trash"
271
271
  severity="danger"
272
272
  text
@@ -279,7 +279,7 @@ onMounted(async () => {
279
279
  </div>
280
280
 
281
281
  <div class="scope-add">
282
- <Button
282
+ <QdButton
283
283
  label="Add Scope"
284
284
  icon="pi pi-plus"
285
285
  severity="secondary"
@@ -3,6 +3,10 @@
3
3
  */
4
4
 
5
5
  // Layout
6
+ // Base widget wrappers — the single indirection points over the widget
7
+ // library (#1391 exit-readiness)
8
+ export { default as QdButton } from './base/QdButton.vue'
9
+ export { default as QdMessage } from './base/QdMessage.vue'
6
10
  export { default as AppLayout } from './layout/AppLayout.vue'
7
11
  export { default as BaseLayout } from './layout/BaseLayout.vue'
8
12
  export { default as PageLayout } from './layout/PageLayout.vue'
@@ -21,7 +21,7 @@ import { useApp } from '../../composables/useApp'
21
21
  import { useAuth } from '../../composables/useAuth'
22
22
  import { useGuardDialog } from '../../composables/useGuardStore'
23
23
  import { useNavContext, type BreadcrumbItem, type NavLinkItem } from '../../composables/useNavContext'
24
- import Button from 'primevue/button'
24
+ import QdButton from '../base/QdButton.vue'
25
25
  import Breadcrumb from 'primevue/breadcrumb'
26
26
  import UnsavedChangesDialog from '../dialogs/UnsavedChangesDialog.vue'
27
27
  import SidebarBox from './SidebarBox.vue'
@@ -366,7 +366,7 @@ const showBreadcrumb = computed<boolean>(() => {
366
366
  <main class="main-content" :class="{ 'main-content--sidebar-collapsed': sidebarCollapsed }">
367
367
  <!-- Mobile header bar -->
368
368
  <div class="mobile-header">
369
- <Button
369
+ <QdButton
370
370
  icon="pi pi-bars"
371
371
  severity="secondary"
372
372
  text
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import PageHeader from './PageHeader.vue'
19
19
  import CardsGrid from '../display/CardsGrid.vue'
20
- import Button from 'primevue/button'
20
+ import QdButton from '../base/QdButton.vue'
21
21
 
22
22
  interface TitleParts {
23
23
  action?: string
@@ -89,7 +89,7 @@ function resolveLabel(label: string | (() => string)): string {
89
89
  </template>
90
90
  <template #actions>
91
91
  <slot name="header-actions" ></slot>
92
- <Button
92
+ <QdButton
93
93
  v-for="action in headerActions"
94
94
  :key="action.name"
95
95
  :label="resolveLabel(action.label)"
@@ -11,7 +11,7 @@ import { computed, type ComputedRef } from 'vue'
11
11
  import { useRouter } from 'vue-router'
12
12
  import { prettifyRole } from '../../../utils/formatters'
13
13
  import { useAuth } from '../../../composables/useAuth'
14
- import Button from 'primevue/button'
14
+ import QdButton from '../../base/QdButton.vue'
15
15
 
16
16
  interface UserData {
17
17
  username?: string
@@ -56,7 +56,7 @@ function handleLogout(): void {
56
56
  <div class="user-name">{{ userDisplayName }}</div>
57
57
  <div class="user-role">{{ userSubtitle }}</div>
58
58
  </div>
59
- <Button
59
+ <QdButton
60
60
  icon="pi pi-sign-out"
61
61
  severity="secondary"
62
62
  text
@@ -12,7 +12,7 @@
12
12
  * - handler: Click handler function
13
13
  * - isDisabled: Boolean disabled state
14
14
  */
15
- import Button from 'primevue/button'
15
+ import QdButton from '../base/QdButton.vue'
16
16
 
17
17
  interface Action {
18
18
  name: string
@@ -32,7 +32,7 @@ defineProps<Props>()
32
32
 
33
33
  <template>
34
34
  <div class="table-actions">
35
- <Button
35
+ <QdButton
36
36
  v-for="action in actions"
37
37
  :key="action.name"
38
38
  :icon="action.icon"
@@ -21,7 +21,7 @@ import FilterBar from './FilterBar.vue'
21
21
  import ActionButtons from './ActionButtons.vue'
22
22
  import DataTable from 'primevue/datatable'
23
23
  import Column from 'primevue/column'
24
- import Button from 'primevue/button'
24
+ import QdButton from '../base/QdButton.vue'
25
25
  import Select from 'primevue/select'
26
26
  import AutoComplete from 'primevue/autocomplete'
27
27
  import SplitButton from 'primevue/splitbutton'
@@ -333,7 +333,7 @@ function getFilterClass(filter: FilterConfig): Record<string, boolean> {
333
333
  <!-- Mobile: split button (primary action + dropdown) -->
334
334
  <template v-if="isMobile && headerActions.length > 0 && primaryHeaderAction">
335
335
  <!-- Single action: just a button -->
336
- <Button
336
+ <QdButton
337
337
  v-if="headerActions.length === 1"
338
338
  :label="resolveLabel(primaryHeaderAction.label)"
339
339
  :icon="primaryHeaderAction.icon"
@@ -355,7 +355,7 @@ function getFilterClass(filter: FilterConfig): Record<string, boolean> {
355
355
  </template>
356
356
  <!-- Desktop: individual buttons -->
357
357
  <template v-else>
358
- <Button
358
+ <QdButton
359
359
  v-for="action in headerActions"
360
360
  :key="action.name"
361
361
  :label="resolveLabel(action.label)"
@@ -417,7 +417,7 @@ function getFilterClass(filter: FilterConfig): Record<string, boolean> {
417
417
  />
418
418
  </template>
419
419
  <slot name="filters" ></slot>
420
- <Button
420
+ <QdButton
421
421
  v-if="hasActiveFilters"
422
422
  icon="pi pi-filter-slash"
423
423
  severity="secondary"
@@ -22,7 +22,7 @@ import { useRouter } from 'vue-router'
22
22
  import Card from 'primevue/card'
23
23
  import InputText from 'primevue/inputtext'
24
24
  import Password from 'primevue/password'
25
- import Button from 'primevue/button'
25
+ import QdButton from '../base/QdButton.vue'
26
26
  import type { OrchestratorLike } from '../../entity/EntityManager.interface'
27
27
 
28
28
  /**
@@ -245,7 +245,7 @@ async function handleLogin(): Promise<void> {
245
245
  :disabled="loading"
246
246
  />
247
247
  </div>
248
- <Button
248
+ <QdButton
249
249
  type="submit"
250
250
  :label="submitLabel"
251
251
  icon="pi pi-sign-in"
@@ -41,8 +41,8 @@ import { computed, type PropType } from 'vue'
41
41
  import PageHeader from '../layout/PageHeader.vue'
42
42
  import CardShell from '../layout/CardShell.vue'
43
43
  import { formatFetchError } from '../../utils/errors'
44
- import Button from 'primevue/button'
45
- import Message from 'primevue/message'
44
+ import QdButton from '../base/QdButton.vue'
45
+ import QdMessage from '../base/QdMessage.vue'
46
46
  import FieldGroups from '../item/FieldGroups.vue'
47
47
  import ShowField from './ShowField.vue'
48
48
  import type { ButtonSeverity } from '../../types'
@@ -187,7 +187,7 @@ type ShowFieldValue = string | number | boolean | Date | Record<string, unknown>
187
187
  <template #actions>
188
188
  <slot name="header-actions" />
189
189
  <!-- Header actions from builder (e.g., edit) -->
190
- <Button
190
+ <QdButton
191
191
  v-for="action in headerActions"
192
192
  :key="action.name"
193
193
  :label="action.label"
@@ -215,9 +215,9 @@ type ShowFieldValue = string | number | boolean | Date | Record<string, unknown>
215
215
  <!-- Error State (fetch error) -->
216
216
  <template v-else-if="fetchError">
217
217
  <slot name="error" :error="fetchError">
218
- <Message severity="error" :closable="false" class="show-error-message">
218
+ <QdMessage severity="error" :closable="false" class="show-error-message">
219
219
  {{ fetchErrorMessage }}
220
- </Message>
220
+ </QdMessage>
221
221
  </slot>
222
222
  </template>
223
223
 
@@ -269,7 +269,7 @@ type ShowFieldValue = string | number | boolean | Date | Record<string, unknown>
269
269
  <template v-if="showActions && footerActions.length > 0">
270
270
  <slot name="footer">
271
271
  <div class="show-actions">
272
- <Button
272
+ <QdButton
273
273
  v-for="action in footerActions"
274
274
  :key="action.name"
275
275
  :label="action.label"
@@ -59,6 +59,10 @@ export function useDirtyState<T extends FormState = FormState>(
59
59
  // initial side each time doubled the per-key work, and the old
60
60
  // JSON.parse(JSON.stringify(state)) deep clone tripled the snapshot cost.
61
61
  let initialFieldSnapshots: Map<string, string> | null = null
62
+ // Parsed deep copy of the initial form, kept for path-aware isFieldDirty
63
+ // (#1396). Built once per snapshot; the per-keystroke checkDirty path
64
+ // (#1194) never touches it.
65
+ let initialForm: Record<string, unknown> | null = null
62
66
  const ready = ref(false)
63
67
 
64
68
  /** Resolve the form object inside the state (common 'form'/'formData' patterns). */
@@ -76,6 +80,16 @@ export function useDirtyState<T extends FormState = FormState>(
76
80
  return s === undefined ? 'undefined' : s
77
81
  }
78
82
 
83
+ /** Resolve a dot path ('config.login') in an object, undefined-safe. */
84
+ function getByPath(obj: unknown, path: string): unknown {
85
+ let cur: unknown = obj
86
+ for (const seg of path.split('.')) {
87
+ if (cur === null || cur === undefined || typeof cur !== 'object') return undefined
88
+ cur = (cur as Record<string, unknown>)[seg]
89
+ }
90
+ return cur
91
+ }
92
+
79
93
  function getFormSnapshot(): string {
80
94
  return JSON.stringify(getState())
81
95
  }
@@ -96,6 +110,7 @@ export function useDirtyState<T extends FormState = FormState>(
96
110
  fieldSnapshots.set(key, stringifyField(form[key]))
97
111
  }
98
112
  initialFieldSnapshots = fieldSnapshots
113
+ initialForm = JSON.parse(JSON.stringify(form)) as Record<string, unknown>
99
114
  ready.value = true
100
115
  })
101
116
  }
@@ -126,7 +141,21 @@ export function useDirtyState<T extends FormState = FormState>(
126
141
  }
127
142
 
128
143
  function isFieldDirty(fieldName: string): boolean {
129
- return dirtyFields.value.includes(fieldName)
144
+ // Dot-free names: exact historical behavior.
145
+ if (!fieldName.includes('.')) {
146
+ return dirtyFields.value.includes(fieldName)
147
+ }
148
+ // Path-aware (#1396): a sub-field can only be dirty if its ROOT key is —
149
+ // the pre-filter reuses the per-key work checkDirty already did, and
150
+ // reading dirtyFields keeps the same reactive trigger as flat names.
151
+ const root = fieldName.slice(0, fieldName.indexOf('.'))
152
+ if (!dirtyFields.value.includes(root)) return false
153
+ if (!initialForm) return false
154
+ const currentForm = resolveForm(getState())
155
+ return (
156
+ stringifyField(getByPath(currentForm, fieldName)) !==
157
+ stringifyField(getByPath(initialForm, fieldName))
158
+ )
130
159
  }
131
160
 
132
161
  function reset(): void {
@@ -134,6 +163,7 @@ export function useDirtyState<T extends FormState = FormState>(
134
163
  dirtyFields.value = []
135
164
  initialSnapshot.value = null
136
165
  initialFieldSnapshots = null
166
+ initialForm = null
137
167
  ready.value = false
138
168
  }
139
169
 
@@ -0,0 +1,45 @@
1
+ /**
2
+ * TestManager - Auto-generated EntityManager
3
+ *
4
+ * Generated by qdadm/gen/generateManagers
5
+ * DO NOT EDIT MANUALLY - Changes will be overwritten
6
+ *
7
+ * Entity: test
8
+ * Endpoint: /test
9
+ */
10
+
11
+ import { EntityManager } from '@quazardous/qdadm'
12
+ import { ApiStorage } from '@quazardous/qdadm'
13
+
14
+ /**
15
+ * Schema definition for test
16
+ * @type {import('qdadm/gen').UnifiedEntitySchema}
17
+ */
18
+ export const testSchema = {
19
+ name: "test",
20
+ endpoint: "/test",
21
+ fields: {}
22
+ }
23
+
24
+ /**
25
+ * Storage options for test
26
+ */
27
+ const storageOptions = {
28
+ endpoint: "/test"
29
+ }
30
+
31
+ /**
32
+ * TestManager instance
33
+ *
34
+ * Provides CRUD operations for test entity.
35
+ *
36
+ * @type {EntityManager}
37
+ */
38
+ export const testManager = new EntityManager({
39
+ ...{
40
+ name: "test",
41
+ idField: "id",
42
+ fields: {}
43
+ },
44
+ storage: new ApiStorage(storageOptions)
45
+ })
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { useListPage, ListPage, useOrchestrator } from '../../index.js'
7
7
  import Column from 'primevue/column'
8
- import Message from 'primevue/message'
8
+ import QdMessage from '../../components/base/QdMessage.vue'
9
9
  import Chip from 'primevue/chip'
10
10
 
11
11
  /**
@@ -49,7 +49,7 @@ const storageCanPersist: boolean = manager?.rolesProvider?.canPersist ?? false
49
49
  <ListPage v-bind="list.props.value" v-on="list.events">
50
50
  <!-- Read-only Warning -->
51
51
  <template #before-table>
52
- <Message v-if="!storageCanPersist" severity="info" :closable="false" class="mb-4">
52
+ <QdMessage v-if="!storageCanPersist" severity="info" :closable="false" class="mb-4">
53
53
  <div class="flex align-items-center gap-2">
54
54
  <i class="pi pi-info-circle text-xl"></i>
55
55
  <span>
@@ -57,7 +57,7 @@ const storageCanPersist: boolean = manager?.rolesProvider?.canPersist ?? false
57
57
  Use a PersistableRoleProvider for editing.
58
58
  </span>
59
59
  </div>
60
- </Message>
60
+ </QdMessage>
61
61
  </template>
62
62
 
63
63
  <template #columns>
@@ -34,9 +34,22 @@ interface PendingRequest {
34
34
  timeout: ReturnType<typeof setTimeout>
35
35
  }
36
36
 
37
+ /** Typed page-side queries (#1398) — served by the injected client. */
38
+ export type DebugQueryType =
39
+ | 'describe'
40
+ | 'dump'
41
+ | 'call'
42
+ | 'sessionInfo'
43
+ | 'routes'
44
+ | 'entityState'
45
+ | 'entityCall'
46
+ | 'storageDump'
47
+ | 'bootlog'
48
+ | 'recentSignals'
49
+
37
50
  interface DebugRequest {
38
51
  id: string
39
- type: 'describe' | 'dump' | 'call'
52
+ type: DebugQueryType
40
53
  payload?: unknown
41
54
  /** Target a specific session (server → client). */
42
55
  sessionId?: string
@@ -67,6 +80,26 @@ interface SessionState {
67
80
  meta: Record<string, unknown>
68
81
  }
69
82
 
83
+ /** Broker surface exposed to sibling plugins via `plugin.api` (#1398). */
84
+ export interface QdadmDebugPluginApi {
85
+ /** Send a typed query to the page and await its reply. */
86
+ ask: (type: DebugQueryType, payload?: unknown, sessionId?: string) => Promise<unknown>
87
+ /** Resolve a session param ('latest' or id) to a live session, if any. */
88
+ pickSession: (
89
+ sessionParam: string | null
90
+ ) => { id: string; lastSeenAt: number; meta: Record<string, unknown> } | null
91
+ listSessions: () => Array<{
92
+ id: string
93
+ firstSeenAt: number
94
+ lastSeenAt: number
95
+ ageMs: number
96
+ hasDescribe: boolean
97
+ hasSnapshot: boolean
98
+ meta: Record<string, unknown>
99
+ }>
100
+ prefix: string
101
+ }
102
+
70
103
  export interface QdadmDebugPluginOptions {
71
104
  /** URL prefix (default '/__qdadm') */
72
105
  prefix?: string
@@ -203,9 +236,21 @@ export function qdadmDebugPlugin(options: QdadmDebugPluginOptions = {}): Plugin
203
236
  return s
204
237
  }
205
238
 
239
+ /** Inter-plugin API (#1398) — consumed by @quazardous/qdmcp. */
240
+ const api: QdadmDebugPluginApi = {
241
+ ask,
242
+ pickSession: (sessionParam) => {
243
+ const s = pickSession(sessionParam)
244
+ return s ? { id: s.id, lastSeenAt: s.lastSeenAt, meta: s.meta } : null
245
+ },
246
+ listSessions,
247
+ prefix,
248
+ }
249
+
206
250
  return {
207
251
  name: 'qdadm-debug',
208
252
  apply: 'serve',
253
+ api,
209
254
 
210
255
  transformIndexHtml: {
211
256
  order: 'pre',
@@ -294,7 +339,10 @@ export function qdadmDebugPlugin(options: QdadmDebugPluginOptions = {}): Plugin
294
339
  res.setHeader('x-qdadm-session', session.id)
295
340
  return sendJson(res, 200, data)
296
341
  }
297
- return sendJson(res, 404, { error: `unknown ${req.method} ${path}` })
342
+ // Unknown subpath: fall through — sibling plugins (e.g.
343
+ // @quazardous/qdadm-mcp) mount their own endpoints under the
344
+ // same prefix (#1398).
345
+ return next()
298
346
  } catch (e) {
299
347
  return sendJson(res, 500, { error: (e as Error).message })
300
348
  }
@@ -333,6 +381,34 @@ function readBody(req: IncomingMessage): Promise<string> {
333
381
  * stays fresh. Listens for ad-hoc requests targeted at this session.
334
382
  */
335
383
  const HMR_CLIENT_SCRIPT = String.raw`
384
+ // ── Boot capture (#1398): this head module runs BEFORE the app's entry
385
+ // module, so console/window errors from the very first app statement are
386
+ // buffered — visible over MCP even when the app dies before the bridge
387
+ // exists (the class of failure a bridge-only design can never see).
388
+ const __bootAt = Date.now()
389
+ const __bootlog = { errors: [], warns: [], pageErrors: [], rejections: [] }
390
+ function __cap(arr, entry) { arr.push(entry); if (arr.length > 200) arr.shift() }
391
+ function __fmt(args) {
392
+ try {
393
+ return Array.from(args).map((a) => {
394
+ if (a instanceof Error) return a.stack || a.message
395
+ if (typeof a === 'object') { try { return JSON.stringify(a) } catch (e) { return String(a) } }
396
+ return String(a)
397
+ }).join(' ').slice(0, 2000)
398
+ } catch (e) { return '[unformattable]' }
399
+ }
400
+ const __origError = console.error.bind(console)
401
+ const __origWarn = console.warn.bind(console)
402
+ console.error = function () { __cap(__bootlog.errors, { at: Date.now(), msg: __fmt(arguments) }); __origError.apply(null, arguments) }
403
+ console.warn = function () { __cap(__bootlog.warns, { at: Date.now(), msg: __fmt(arguments) }); __origWarn.apply(null, arguments) }
404
+ window.addEventListener('error', (e) => {
405
+ __cap(__bootlog.pageErrors, { at: Date.now(), msg: String(e.message || e), source: e.filename ? e.filename + ':' + e.lineno : undefined })
406
+ })
407
+ window.addEventListener('unhandledrejection', (e) => {
408
+ let msg; try { msg = e.reason && (e.reason.stack || e.reason.message) || String(e.reason) } catch (err) { msg = '[unreadable reason]' }
409
+ __cap(__bootlog.rejections, { at: Date.now(), msg: String(msg).slice(0, 2000) })
410
+ })
411
+
336
412
  const hot = import.meta.hot
337
413
  if (!hot) {
338
414
  console.warn('[qdadm-debug] import.meta.hot unavailable in injected script')
@@ -344,6 +420,95 @@ if (!hot) {
344
420
 
345
421
  const w = /** @type {any} */ (window)
346
422
 
423
+ // Signal ring buffer — armed once __qdadm.signals is reachable.
424
+ const __signals = []
425
+ let __signalsArmed = false
426
+ function __armSignals() {
427
+ if (__signalsArmed) return
428
+ const q = w.__qdadm
429
+ if (!q || !q.signals || typeof q.signals.on !== 'function') return
430
+ try {
431
+ q.signals.on('**', (event) => { __cap(__signals, { at: Date.now(), name: event && event.name }) ; if (__signals.length > 100) __signals.shift() })
432
+ __signalsArmed = true
433
+ } catch (e) {}
434
+ }
435
+
436
+ function __sessionInfo() {
437
+ const q = w.__qdadm || {}
438
+ let app = {}
439
+ try { app = (q.kernel && q.kernel.options && q.kernel.options.app) || {} } catch (e) {}
440
+ let route = null
441
+ try { const r = q.router && q.router.currentRoute && q.router.currentRoute.value; route = r ? { name: r.name, fullPath: r.fullPath } : null } catch (e) {}
442
+ return {
443
+ sessionId, bootAt: __bootAt, now: Date.now(), url: location.href,
444
+ app: { name: app.name, version: typeof app.version === 'function' ? app.version() : app.version },
445
+ route, bridgeReady: !!bridge, signalsArmed: __signalsArmed,
446
+ bootlogCounts: { errors: __bootlog.errors.length, warns: __bootlog.warns.length, pageErrors: __bootlog.pageErrors.length, rejections: __bootlog.rejections.length },
447
+ }
448
+ }
449
+
450
+ function __routes() {
451
+ const q = w.__qdadm
452
+ if (!q || !q.router) throw new Error('router not ready')
453
+ return q.router.getRoutes().map((r) => ({
454
+ name: r.name, path: r.path,
455
+ meta: { entity: r.meta && r.meta.entity, layout: r.meta && r.meta.layout, requiresAuth: r.meta && r.meta.requiresAuth, public: r.meta && r.meta.public },
456
+ }))
457
+ }
458
+
459
+ function __manager(entity) {
460
+ const q = w.__qdadm
461
+ if (!q || !q.orchestrator) throw new Error('orchestrator not ready')
462
+ if (!q.orchestrator.isRegistered(entity)) throw new Error('entity not registered: ' + entity + ' (known: ' + q.orchestrator.getRegisteredNames().join(', ') + ')')
463
+ return q.orchestrator.get(entity)
464
+ }
465
+
466
+ function __entityState(payload) {
467
+ const q = w.__qdadm
468
+ if (!payload || !payload.entity) {
469
+ return { entities: q && q.orchestrator ? q.orchestrator.getRegisteredNames() : [] }
470
+ }
471
+ const m = __manager(payload.entity)
472
+ const safe = (fn) => { try { return fn() } catch (e) { return '[error: ' + (e && e.message) + ']' } }
473
+ const fields = {}
474
+ try {
475
+ const fs = m.fields || m._fields || {}
476
+ for (const k in fs) fields[k] = { type: fs[k] && fs[k].type, label: fs[k] && fs[k].label, required: !!(fs[k] && fs[k].required) }
477
+ } catch (e) {}
478
+ return {
479
+ entity: payload.entity,
480
+ idField: safe(() => m.idField), labelField: safe(() => m.labelField),
481
+ label: safe(() => m.label), labelPlural: safe(() => m.labelPlural),
482
+ can: { create: safe(() => m.canCreate()), read: safe(() => m.canRead()), update: safe(() => m.canUpdate()), delete: safe(() => m.canDelete()) },
483
+ fields,
484
+ storage: safe(() => { const st = m.storage || m._storage; return st ? { kind: st.constructor && st.constructor.name, storageKey: st._storageKey } : null }),
485
+ }
486
+ }
487
+
488
+ async function __entityCall(payload) {
489
+ if (!payload || !payload.entity || !payload.op) throw new Error('entityCall requires { entity, op, ... }')
490
+ const m = __manager(payload.entity)
491
+ const op = payload.op
492
+ if (op === 'list') return await m.list(payload.params || {})
493
+ if (op === 'get') return await m.get(payload.id)
494
+ if (op === 'create') return await m.create(payload.data || {})
495
+ if (op === 'update') return await m.update(payload.id, payload.data || {})
496
+ if (op === 'delete') { await m.delete(payload.id); return { deleted: payload.id } }
497
+ throw new Error('unknown op "' + op + '" (list|get|create|update|delete)')
498
+ }
499
+
500
+ function __storageDump(payload) {
501
+ if (!payload || !payload.entity) throw new Error('storageDump requires { entity }')
502
+ const m = __manager(payload.entity)
503
+ const st = m.storage || m._storage
504
+ const key = st && st._storageKey
505
+ if (!key) return { storageKey: null, note: 'storage exposes no localStorage key (' + (st && st.constructor && st.constructor.name) + ')' }
506
+ const raw = localStorage.getItem(key)
507
+ let parsed = null
508
+ try { parsed = raw ? JSON.parse(raw) : null } catch (e) { parsed = '[unparsable]' }
509
+ return { storageKey: key, count: Array.isArray(parsed) ? parsed.length : null, data: parsed }
510
+ }
511
+
347
512
  function helloIfNeeded() {
348
513
  try { hot.send('qdadm:debug:push', { type: 'hello', sessionId, meta }) } catch (e) {}
349
514
  }
@@ -365,6 +530,16 @@ if (!hot) {
365
530
  if (msg && msg.sessionId && msg.sessionId !== sessionId) return
366
531
  const id = (msg && msg.id) || ''
367
532
  try {
533
+ // Typed queries (#1398): bridge-independent — they must answer even
534
+ // when the app died before the bridge existed.
535
+ if (msg.type === 'bootlog') { hot.send('qdadm:debug:reply', { id, ok: true, data: __bootlog, sessionId }); return }
536
+ if (msg.type === 'sessionInfo') { hot.send('qdadm:debug:reply', { id, ok: true, data: __sessionInfo(), sessionId }); return }
537
+ if (msg.type === 'recentSignals') { __armSignals(); hot.send('qdadm:debug:reply', { id, ok: true, data: { armed: __signalsArmed, events: __signals }, sessionId }); return }
538
+ if (msg.type === 'routes') { hot.send('qdadm:debug:reply', { id, ok: true, data: __routes(), sessionId }); return }
539
+ if (msg.type === 'entityState') { hot.send('qdadm:debug:reply', { id, ok: true, data: __entityState(msg.payload), sessionId }); return }
540
+ if (msg.type === 'entityCall') { const data = await __entityCall(msg.payload); hot.send('qdadm:debug:reply', { id, ok: true, data, sessionId }); return }
541
+ if (msg.type === 'storageDump') { hot.send('qdadm:debug:reply', { id, ok: true, data: __storageDump(msg.payload), sessionId }); return }
542
+
368
543
  const b = w.__qdadm && w.__qdadm.debug && w.__qdadm.debug.bridge
369
544
  if (!b) throw new Error('debug bridge not ready')
370
545
  let data
@@ -385,6 +560,7 @@ if (!hot) {
385
560
  bridge = w.__qdadm && w.__qdadm.debug && w.__qdadm.debug.bridge
386
561
  if (!bridge) return
387
562
  clearInterval(attachTimer)
563
+ __armSignals()
388
564
  helloIfNeeded()
389
565
  pushDescribe()
390
566
  pushSnapshot()
@@ -53,6 +53,26 @@ export interface QdadmVitePluginOptions {
53
53
  * (vue, vue-router, primevue, pinia).
54
54
  */
55
55
  dedupe?: string[]
56
+ /**
57
+ * PrimeVue "flavor" (#1393): alias every `primevue/*` import to a
58
+ * compatible fork (e.g. OpenVue). qdadm officially maintains and tests
59
+ * `primevue@4` (MIT) only — a flavor keeping the v4 module layout/API is
60
+ * drop-in; divergences are the consumer's to absorb.
61
+ *
62
+ * ```ts
63
+ * qdadmVitePlugin({ primevue: { package: 'openvue' } })
64
+ * ```
65
+ */
66
+ primevue?: {
67
+ /** Package that replaces `primevue` (e.g. 'openvue'). */
68
+ package: string
69
+ /**
70
+ * Optional package replacing `@primeuix/themes` — only needed if the
71
+ * flavor ships its own themes fork (none does today; OpenVue keeps the
72
+ * MIT @primeuix/* versions).
73
+ */
74
+ themes?: string
75
+ }
56
76
  }
57
77
 
58
78
  export function qdadmVitePlugin(options: QdadmVitePluginOptions = {}): Plugin {
@@ -76,12 +96,33 @@ export function qdadmVitePlugin(options: QdadmVitePluginOptions = {}): Plugin {
76
96
  // not installed under root node_modules (aliased/hoisted) — nothing to allow
77
97
  }
78
98
 
99
+ // Flavor aliasing (#1393): after the alias, no `primevue`/themes ids
100
+ // remain in the graph — dedupe/exclude lists must name the flavor.
101
+ const flavor = options.primevue ?? null
102
+ const widgetPkg = flavor?.package ?? 'primevue'
103
+ const themesPkg = flavor?.themes ?? '@primeuix/themes'
104
+ const alias = []
105
+ if (flavor) {
106
+ alias.push({ find: /^primevue(\/|$)/, replacement: `${flavor.package}$1` })
107
+ if (flavor.themes) {
108
+ alias.push({ find: /^@primeuix\/themes(\/|$)/, replacement: `${flavor.themes}$1` })
109
+ }
110
+ }
111
+
112
+ // With a flavor, the original packages may still be installed — keep
113
+ // them excluded too, or the optimizer pre-bundles a stray
114
+ // `primevue/*` id that bypassed the alias (split-instance again).
115
+ const widgetExcludes = flavor ? [widgetPkg, 'primevue'] : [widgetPkg]
116
+ const themesExcludes =
117
+ flavor?.themes ? [themesPkg, '@primeuix/themes'] : [themesPkg]
118
+
79
119
  return {
80
120
  resolve: {
81
- dedupe: ['vue', 'vue-router', 'primevue', 'pinia', ...(options.dedupe ?? [])],
121
+ ...(alias.length > 0 ? { alias } : {}),
122
+ dedupe: ['vue', 'vue-router', widgetPkg, 'pinia', ...(options.dedupe ?? [])],
82
123
  },
83
124
  optimizeDeps: {
84
- exclude: ['primevue', '@primeuix/themes', '@quazardous/qdadm'],
125
+ exclude: [...widgetExcludes, ...themesExcludes, '@quazardous/qdadm'],
85
126
  // One form per install mode: the nested path resolves through a
86
127
  // real node_modules chain (npm installs), the plain form through a
87
128
  // symlink realpath — declaring the unused one logs a "Failed to