@omnitend/dashboard-for-laravel 0.45.0 → 0.47.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/assets/IBMPlexSans-Bold-Latin1-914f1400.woff2 +0 -0
  2. package/dist/assets/IBMPlexSans-Medium-Latin1-b5610af0.woff2 +0 -0
  3. package/dist/assets/IBMPlexSans-Regular-Latin1-b5ad7bd3.woff2 +0 -0
  4. package/dist/assets/IBMPlexSans-SemiBold-Latin1-fff0ab3a.woff2 +0 -0
  5. package/dist/components/base/DFormFile.vue.d.ts +17 -0
  6. package/dist/components/base/DFormValidFeedback.vue.d.ts +12 -0
  7. package/dist/components/extended/DXDashboard.vue.d.ts +95 -111
  8. package/dist/components/extended/DXDashboardNavbar.vue.d.ts +2 -36
  9. package/dist/components/extended/DXDashboardSidebar.vue.d.ts +60 -79
  10. package/dist/components/extended/DXField.vue.d.ts +16 -0
  11. package/dist/components/extended/DXTable.vue.d.ts +10 -1
  12. package/dist/components/extended/DXTableEditorModal.vue.d.ts +13 -3
  13. package/dist/dashboard-for-laravel.js +11767 -11479
  14. package/dist/dashboard-for-laravel.js.map +1 -1
  15. package/dist/dashboard-for-laravel.umd.cjs +7 -7
  16. package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
  17. package/dist/index.d.ts +4 -2
  18. package/dist/style.css +2 -2
  19. package/dist/types/index.d.ts +60 -0
  20. package/dist/types/navigation.d.ts +153 -3
  21. package/dist/utils/formErrorTargets.d.ts +6 -2
  22. package/dist/utils/formSchema.d.ts +16 -0
  23. package/dist/utils/statCardFit.d.ts +19 -0
  24. package/docs/public/api-reference.json +171 -9
  25. package/docs/public/docs-map.md +4 -2
  26. package/docs/public/llms.txt +4 -2
  27. package/package.json +3 -2
  28. package/resources/css/theme.scss +339 -4
  29. package/resources/fonts/ibm-plex-sans/IBMPlexSans-Bold-Latin1.woff2 +0 -0
  30. package/resources/fonts/ibm-plex-sans/IBMPlexSans-Medium-Latin1.woff2 +0 -0
  31. package/resources/fonts/ibm-plex-sans/IBMPlexSans-Regular-Latin1.woff2 +0 -0
  32. package/resources/fonts/ibm-plex-sans/IBMPlexSans-SemiBold-Latin1.woff2 +0 -0
  33. package/resources/fonts/ibm-plex-sans/OFL.txt +93 -0
  34. package/resources/fonts/ibm-plex-sans/SOURCE.txt +18 -0
  35. package/resources/js/components/base/DFormFile.vue +103 -0
  36. package/resources/js/components/base/DFormValidFeedback.vue +16 -0
  37. package/resources/js/components/extended/DXDashboard.vue +41 -18
  38. package/resources/js/components/extended/DXDashboardNavbar.vue +9 -1
  39. package/resources/js/components/extended/DXDashboardSidebar.vue +25 -3
  40. package/resources/js/components/extended/DXField.vue +86 -23
  41. package/resources/js/components/extended/DXFormField.vue +3 -1
  42. package/resources/js/components/extended/DXStatCard.vue +154 -11
  43. package/resources/js/components/extended/DXTable.vue +62 -6
  44. package/resources/js/components/extended/DXTableEditorModal.vue +35 -2
  45. package/resources/js/composables/useResourceEditor.ts +64 -26
  46. package/resources/js/index.ts +11 -0
  47. package/resources/js/types/index.ts +64 -0
  48. package/resources/js/types/navigation.ts +175 -3
  49. package/resources/js/utils/formErrorTargets.ts +28 -2
  50. package/resources/js/utils/formSchema.ts +27 -0
  51. package/resources/js/utils/statCardFit.ts +210 -0
@@ -47,7 +47,7 @@
47
47
  v-else-if="isProviderMode || isClientSideMode || isInertiaMode"
48
48
  ref="tableRef"
49
49
  :key="activeMode + ':' + tableSlotSignature($slots)"
50
- :fields="fields"
50
+ :fields="renderedFields"
51
51
  :sort-by="effectiveSortBy"
52
52
  :multisort="false"
53
53
  :no-sortable-icon="true"
@@ -245,8 +245,10 @@
245
245
  PaginationData; `handleActivePageChange` routes the page
246
246
  event to the right per-mode handler. Previously three
247
247
  byte-identical copies, one per mode. -->
248
+ <!-- Not while the loader shows: the count would read "0 items"
249
+ before the rows have arrived. -->
248
250
  <DXTablePagination
249
- v-if="activePagination"
251
+ v-if="activePagination && !(effectiveBusy && !isProviderMode)"
250
252
  :pagination="activePagination"
251
253
  :per-page="effectivePerPage"
252
254
  :per-page-options="perPageOptions"
@@ -264,7 +266,8 @@
264
266
  </DRow>
265
267
 
266
268
  <!-- Edit/create modal (only when editFields provided). The consumer's
267
- edit-value / edit-span / tab-* slots are forwarded through to it
269
+ edit-value / edit-span / edit-password-actions / tab-* slots are
270
+ forwarded through to it
268
271
  generically; it maps them onto DXForm and provides the row context
269
272
  + a close handle (#129). -->
270
273
  <DXTableEditorModal
@@ -297,6 +300,7 @@
297
300
  >
298
301
  <!--
299
302
  @slot edit-value(<fieldKey>) / edit-span(<fieldKey>) /
303
+ edit-password-actions(<fieldKey>) /
300
304
  tab-content|tab-before|tab-after(<tabKey>) — forwarded to the edit
301
305
  modal (see DXTableEditorModal for their bindings).
302
306
  -->
@@ -340,7 +344,8 @@ import DXTableEditorModal from "./DXTableEditorModal.vue";
340
344
  export type FilterType = 'text' | 'select' | 'select-native' | 'number' | 'date' | false;
341
345
 
342
346
  export interface FilterOption {
343
- value: string;
347
+ /** Numbers and booleans are accepted and compared as their string form. */
348
+ value: string | number | boolean | null;
344
349
  text: string;
345
350
  }
346
351
 
@@ -449,6 +454,15 @@ export interface TableField {
449
454
  * A number is pixels; a string is used verbatim.
450
455
  */
451
456
  minWidth?: string | number;
457
+
458
+ /**
459
+ * Render this column's figures tabular (every digit the same width), so
460
+ * amounts line up down the column: adds the global `.dx-tabular` class to
461
+ * the column's body cells and its header cell. Composes with the field's
462
+ * own `tdClass`/`thClass` (string, array, object or `tdClass` function)
463
+ * rather than replacing them. Default `false`.
464
+ */
465
+ tabular?: boolean;
452
466
  [key: string]: any;
453
467
  }
454
468
 
@@ -1379,6 +1393,35 @@ const columnStyleFor = (field: TableField): Record<string, string> | undefined =
1379
1393
  // content-sized default byte-for-byte.
1380
1394
  const fieldsHaveWidths = computed(() => props.fields.some(hasDeclaredWidth));
1381
1395
 
1396
+ /*
1397
+ * Tabular columns (#215). A `tabular: true` field gets `.dx-tabular` added
1398
+ * to its `tdClass` and `thClass`, appended to the consumer's own value
1399
+ * (wrapped in an array, or called through when `tdClass` is a function) so
1400
+ * their classes survive. Fields without the flag pass through as the same
1401
+ * objects, so a table using no tabular column renders exactly as before.
1402
+ */
1403
+ const TABULAR_CLASS = 'dx-tabular';
1404
+
1405
+ const withTabularClass = (consumerClass: unknown): unknown[] =>
1406
+ consumerClass === undefined || consumerClass === null
1407
+ ? [TABULAR_CLASS]
1408
+ : [consumerClass, TABULAR_CLASS];
1409
+
1410
+ const renderedFields = computed<TableField[]>(() =>
1411
+ props.fields.map((field) => {
1412
+ if (field.tabular !== true) return field;
1413
+ const consumerTdClass = field.tdClass;
1414
+ return {
1415
+ ...field,
1416
+ tdClass:
1417
+ typeof consumerTdClass === 'function'
1418
+ ? (...args: unknown[]) => withTabularClass(consumerTdClass(...args))
1419
+ : withTabularClass(consumerTdClass),
1420
+ thClass: withTabularClass(field.thClass),
1421
+ };
1422
+ }),
1423
+ );
1424
+
1382
1425
  // Computed: Get effective filter options for a field
1383
1426
  // The key a column's filter is SENT under, which is not always the column's own
1384
1427
  // key: a display column can filter on a different server param via `filterKey`.
@@ -1579,9 +1622,21 @@ const getFieldFilterOptions = (field: TableField): FilterOption[] => {
1579
1622
  options.push({ value: filterNullValueFor(field), text: field.filterNullText });
1580
1623
  }
1581
1624
 
1582
- // If field has static filterOptions, use those
1625
+ // If field has static filterOptions, use those. Their values are
1626
+ // stringified: options built from records carry numbers (ids) and booleans
1627
+ // (flags), but the filter map, the URL and the row match all speak strings,
1628
+ // and DAutocomplete matches its model to an option by strict equality — so a
1629
+ // raw `1` never re-selected the stored `'1'`, and picking it put a number in
1630
+ // the map that the active-filter check could not `.trim()`.
1583
1631
  if (field.filterOptions && field.filterOptions.length > 0) {
1584
- return [...options, ...field.filterOptions];
1632
+ return [
1633
+ ...options,
1634
+ ...field.filterOptions.map((option) =>
1635
+ option.value === null || option.value === undefined
1636
+ ? option
1637
+ : { ...option, value: String(option.value) },
1638
+ ),
1639
+ ];
1585
1640
  }
1586
1641
 
1587
1642
  // Otherwise, check for server-provided values (keyed by the filter key)
@@ -2461,6 +2516,7 @@ const handleDelete = editor.remove;
2461
2516
  const EDIT_MODAL_SLOT_PREFIXES = [
2462
2517
  'edit-value(',
2463
2518
  'edit-span(',
2519
+ 'edit-password-actions(',
2464
2520
  'tab-content(',
2465
2521
  'tab-before(',
2466
2522
  'tab-after(',
@@ -1,8 +1,8 @@
1
1
  <!--
2
2
  @component
3
3
  Internal edit/create modal for `DXTable` (#129). Renders the `DModal` +
4
- `DXForm` and maps DXTable's `edit-value`/`edit-span`/`tab-*` slots onto
5
- DXForm's `value`/`span`/`tab-*` slots, providing the row context and a `close`
4
+ `DXForm` and maps DXTable's `edit-value`/`edit-span`/`edit-password-actions`/
5
+ `tab-*` slots onto DXForm's `value`/`span`/`password-actions`/`tab-*` slots, providing the row context and a `close`
6
6
  handle. Extracted so `DXTable` is a thin façade over the table render, the
7
7
  pagination, the editor logic (`useResourceEditor`) and this modal — rather than
8
8
  owning all four.
@@ -115,6 +115,36 @@
115
115
  />
116
116
  </template>
117
117
 
118
+ <!-- Forward edit-password-actions(key) → password-actions(key):
119
+ content beside the password input, inside its input group,
120
+ with a handle on dfl's own reveal (eye) state. -->
121
+ <template
122
+ v-for="key in editPasswordActionsSlotKeys"
123
+ :key="`epa-${key}`"
124
+ #[`password-actions(${key})`]="sp"
125
+ >
126
+ <!--
127
+ @slot Extra controls beside password field `<key>`'s input in the edit/create modal, inside its input group after the reveal (eye) toggle — e.g. a "Generate password" button. Name it `edit-password-actions(<fieldKey>)`.
128
+ @binding {object} item The row being edited (null in create mode).
129
+ @binding {any} value The current field value.
130
+ @binding {Function} update Call with a new value to update the field.
131
+ @binding {boolean} revealed Whether the password is currently shown as plain text.
132
+ @binding {Function} setRevealed Call with true to show the password (as the eye does) or false to mask it.
133
+ @binding {boolean} disabled Whether the field is disabled or read-only.
134
+ @binding {object} field The field definition.
135
+ -->
136
+ <slot
137
+ :name="`edit-password-actions(${key})`"
138
+ :item="item"
139
+ :value="sp.value"
140
+ :update="sp.update"
141
+ :revealed="sp.revealed"
142
+ :set-revealed="sp.setRevealed"
143
+ :disabled="sp.disabled"
144
+ :field="sp.field"
145
+ />
146
+ </template>
147
+
118
148
  <!-- Forward tab-content / tab-before / tab-after slots -->
119
149
  <template
120
150
  v-for="key in tabContentSlotKeys"
@@ -318,6 +348,9 @@ const editValueSlotKeys = computed(() =>
318
348
  const editSpanSlotKeys = computed(() =>
319
349
  fieldKeys.value.filter((key) => !!slots[`edit-span(${key})`]),
320
350
  );
351
+ const editPasswordActionsSlotKeys = computed(() =>
352
+ fieldKeys.value.filter((key) => !!slots[`edit-password-actions(${key})`]),
353
+ );
321
354
  const tabContentSlotKeys = computed(() =>
322
355
  tabKeys.value.filter((key) => !!slots[`tab-content(${key})`]),
323
356
  );
@@ -7,6 +7,7 @@ import {
7
7
  resolveFieldDefault,
8
8
  isFieldVisible as isFieldVisibleFor,
9
9
  isSubmittableField,
10
+ nonSubmittedFieldKeys as nonSubmittedFieldKeysFor,
10
11
  } from "../utils/formSchema";
11
12
 
12
13
  /**
@@ -207,24 +208,67 @@ export function useResourceEditor<T = any>(
207
208
  // Enforced at SUBMIT, not just at seeding: the modal still renders every field,
208
209
  // so a `submit: false` control — or a `span` slot calling `update` — can write
209
210
  // its key back into the form data after seeding. Strip them on the way out.
210
- const nonSubmittedFieldKeys = computed(
211
- () =>
212
- new Set(
213
- (props.editFields ?? [])
214
- .filter(
215
- (field) =>
216
- // Presentational: lays the form out, holds no data (#110).
217
- !isSubmittableField(field) ||
218
- // Hidden by `when` at submit time (#117). Submitting a field
219
- // the user cannot see is a silent write — and with `default`
220
- // set it writes a value they never chose. Omitting the key
221
- // leaves the stored value alone.
222
- !isEditFieldVisible(field),
223
- )
224
- .map((field) => field.key),
225
- ),
211
+ //
212
+ // Decided per KEY (#228): a key is dropped only when no field with that key
213
+ // is both submitting and visible at submit time. A presentational field
214
+ // (#110) holds no data, and a field hidden by `when` (#117) must not write a
215
+ // value the user cannot see — but a display-only twin sharing its key with
216
+ // the visible editable field must not strip the editable field's value.
217
+ const nonSubmittedFieldKeys = computed(() =>
218
+ nonSubmittedFieldKeysFor(props.editFields ?? [], isEditFieldVisible),
226
219
  );
227
220
 
221
+ /*
222
+ * Seed the form's data, ONE value per key (#228). Presentational fields are
223
+ * never seeded. When several submitting fields share a key (alternative
224
+ * controls for one value, switched by `when`), the key is seeded from the
225
+ * first of them that is visible against the seeded model, else the first
226
+ * declared. This used to be last-declared-wins, so a hidden alternative
227
+ * declared after the visible field replaced its default.
228
+ *
229
+ * Two submitting fields with one key that are visible TOGETHER are almost
230
+ * certainly a mistake (two controls bound to one value): the first declared
231
+ * wins, and DXTable warns once per key.
232
+ */
233
+ const warnedSharedVisibleKeys = new Set<string>();
234
+ const seedFormData = (
235
+ valueFor: (field: FieldDefinition) => any,
236
+ context: Record<string, any>,
237
+ ): Record<string, any> => {
238
+ const fieldsByKey = new Map<string, FieldDefinition[]>();
239
+ submittableEditFields.value.forEach((field) => {
240
+ const sharing = fieldsByKey.get(field.key);
241
+ if (sharing) sharing.push(field);
242
+ else fieldsByKey.set(field.key, [field]);
243
+ });
244
+
245
+ const formData: Record<string, any> = {};
246
+ fieldsByKey.forEach((fields, key) => {
247
+ formData[key] = valueFor(fields[0]);
248
+ });
249
+
250
+ const seededModel = { ...context, ...formData };
251
+ fieldsByKey.forEach((fields, key) => {
252
+ if (fields.length < 2) return;
253
+ const visibleFields = fields.filter((field) =>
254
+ isFieldVisibleFor(field, seededModel),
255
+ );
256
+ if (visibleFields.length > 1 && !warnedSharedVisibleKeys.has(key)) {
257
+ warnedSharedVisibleKeys.add(key);
258
+ // eslint-disable-next-line no-console
259
+ console.warn(
260
+ `[DXTable] ${visibleFields.length} submitting edit fields share the key "${key}" and are visible together, ` +
261
+ 'so they edit one value; the first declared seeds it. ' +
262
+ 'Give alternatives mutually exclusive `when` predicates, or mark a display-only one `submit: false`.',
263
+ );
264
+ }
265
+ if (visibleFields.length > 0 && visibleFields[0] !== fields[0]) {
266
+ formData[key] = valueFor(visibleFields[0]);
267
+ }
268
+ });
269
+ return formData;
270
+ };
271
+
228
272
  const stripNonSubmittedFields = (data: Record<string, any>): Record<string, any> => {
229
273
  if (nonSubmittedFieldKeys.value.size === 0) return data;
230
274
  return Object.fromEntries(
@@ -265,11 +309,9 @@ export function useResourceEditor<T = any>(
265
309
  // between successive row opens. Presentational fields (submit: false)
266
310
  // lay the form out; they hold no data and must not be POSTed just
267
311
  // because they were declared.
268
- const formData: Record<string, any> = {};
269
- submittableEditFields.value.forEach(field => {
270
- formData[field.key] = seedValueFor(field, item);
271
- });
272
- startSession(formData);
312
+ startSession(
313
+ seedFormData((field) => seedValueFor(field, item), item as any),
314
+ );
273
315
 
274
316
  // Open modal
275
317
  editFormInstanceKey.value++;
@@ -332,11 +374,7 @@ export function useResourceEditor<T = any>(
332
374
  editLoading.value = false;
333
375
 
334
376
  // A fresh form seeded with the field defaults (see `startSession`).
335
- const formData: Record<string, any> = {};
336
- submittableEditFields.value.forEach(field => {
337
- formData[field.key] = resolveFieldDefault(field);
338
- });
339
- startSession(formData);
377
+ startSession(seedFormData(resolveFieldDefault, {}));
340
378
  editFormInstanceKey.value++;
341
379
  showEditModal.value = true;
342
380
  };
@@ -62,9 +62,11 @@ export { default as DDropdownItem } from "./components/base/DDropdownItem.vue";
62
62
  export { default as DForm } from "./components/base/DForm.vue";
63
63
  export { default as DFormCheckbox } from "./components/base/DFormCheckbox.vue";
64
64
  export { default as DFormCheckboxGroup } from "./components/base/DFormCheckboxGroup.vue";
65
+ export { default as DFormFile } from "./components/base/DFormFile.vue";
65
66
  export { default as DFormGroup } from "./components/base/DFormGroup.vue";
66
67
  export { default as DFormInput } from "./components/base/DFormInput.vue";
67
68
  export { default as DFormInvalidFeedback } from "./components/base/DFormInvalidFeedback.vue";
69
+ export { default as DFormValidFeedback } from "./components/base/DFormValidFeedback.vue";
68
70
  export { default as DFormOtp } from "./components/base/DFormOtp.vue";
69
71
  export { default as DFormRadio } from "./components/base/DFormRadio.vue";
70
72
  export { default as DFormRadioGroup } from "./components/base/DFormRadioGroup.vue";
@@ -146,6 +148,8 @@ export type {
146
148
  FormTab,
147
149
  MaybeFn,
148
150
  OptionsLoader,
151
+ PasswordActionsSlotProps,
152
+ EditPasswordActionsSlotProps,
149
153
  // Strict, discriminated field types (#131) — opt-in; canonical in a future
150
154
  // major. `FieldDef` is the union; the rest are its per-type members.
151
155
  FieldDef,
@@ -216,7 +220,14 @@ export type {
216
220
  NavigationItem,
217
221
  NavigationGroup,
218
222
  Navigation,
223
+ NavigationItemOf,
219
224
  NavbarSearchAlign,
220
225
  NavbarActionsOnMobile,
221
226
  SidebarVariant,
227
+ DashboardUser,
228
+ DashboardNavbarSlots,
229
+ DashboardSidebarSlots,
230
+ DashboardSlotExtras,
231
+ DashboardSlots,
232
+ PrefixedDashboardSlots,
222
233
  } from "./types/navigation";
@@ -274,6 +274,20 @@ export interface FieldDefinition {
274
274
  */
275
275
  default?: any;
276
276
 
277
+ /**
278
+ * For a field whose value is a list of rows (a `repeater`, or a span
279
+ * field that renders its own rows and owns `key.N.*` error keys): the
280
+ * row's own name, called with the row (`model[key][index]`, or `{}` when
281
+ * the model has no such row) and its 0-based index. When it returns a
282
+ * non-empty string, the error summary lists that row's errors under it
283
+ * alone ("Chaucer Ale (Pint): Enter a price.") instead of
284
+ * "<sub-field label> (line N)". Returning `undefined`, `null` or `""`
285
+ * (a row with no name yet), or throwing, keeps the line-number label.
286
+ * Only labels: which field owns the error, and what a click on the
287
+ * summary row focuses, are unchanged.
288
+ */
289
+ rowLabel?: (row: Record<string, any>, index: number) => string | null | undefined;
290
+
277
291
  /** Label for a repeater's "add row" button (default: "Add"). */
278
292
  addLabel?: string;
279
293
 
@@ -364,6 +378,10 @@ export interface FieldDefinition {
364
378
  * every `editFields` key, so those decorative fields are POSTed alongside
365
379
  * the real ones (an empty string, an empty array) purely because they were
366
380
  * declared.
381
+ *
382
+ * Applies per KEY (#228): DXTable drops a key only when no field with that
383
+ * key is both submitting and visible, so a `submit: false` display twin
384
+ * does not strip the value of a visible editable field sharing its key.
367
385
  */
368
386
  submit?: boolean;
369
387
 
@@ -579,6 +597,7 @@ export interface ComponentFieldDef extends BaseFieldDef {
579
597
  export interface RepeaterFieldDef extends BaseFieldDef {
580
598
  type: "repeater";
581
599
  fields?: FieldDef[];
600
+ rowLabel?: (row: Record<string, any>, index: number) => string | null | undefined;
582
601
  addLabel?: string;
583
602
  minItems?: number;
584
603
  maxItems?: number;
@@ -652,6 +671,51 @@ export interface FormTab {
652
671
  lazy?: boolean;
653
672
  }
654
673
 
674
+ /**
675
+ * Slot props of DXField's `password-actions` slot (DXForm:
676
+ * `password-actions(<key>)`): content placed beside a password input, inside
677
+ * its input group after the reveal (eye) toggle, such as a "Generate password"
678
+ * button. `setRevealed` drives the same state the eye toggles.
679
+ *
680
+ * ```vue
681
+ * <template #password-actions(password)="{ update, setRevealed, disabled }">
682
+ * <DButton variant="outline-secondary" :disabled="disabled"
683
+ * @click="update(generatePassword()); setRevealed(true)">Generate</DButton>
684
+ * </template>
685
+ * ```
686
+ */
687
+ export interface PasswordActionsSlotProps {
688
+ /** The field definition being rendered. */
689
+ field: FieldDefinition;
690
+ /** The model passed to field predicates (defaults to the live form data). */
691
+ model: any;
692
+ /** The current field value. */
693
+ value: any;
694
+ /** Write a new value (also marks the field touched and clears its error). */
695
+ update: (value: any) => void;
696
+ /** Whether the password is currently shown as plain text. */
697
+ revealed: boolean;
698
+ /**
699
+ * Show (`true`) or mask (`false`) the password, exactly as the eye does.
700
+ * A no-op on a `revealable: false` field, which never unmasks.
701
+ */
702
+ setRevealed: (revealed: boolean) => void;
703
+ /** Whether the field is disabled or read-only. */
704
+ disabled: boolean;
705
+ }
706
+
707
+ /**
708
+ * Slot props of DXTable's `edit-password-actions(<key>)` slot, the edit/create
709
+ * modal's form of {@link PasswordActionsSlotProps}. `item` is the row being
710
+ * edited, `null` in create mode. (`model` is not passed, as with the modal's
711
+ * other field slots.)
712
+ */
713
+ export interface EditPasswordActionsSlotProps
714
+ extends Omit<PasswordActionsSlotProps, "model"> {
715
+ /** The row being edited; `null` in create mode. */
716
+ item: any;
717
+ }
718
+
655
719
  /**
656
720
  * The theme's extra colour variant, `pending`: waiting, and the next move is
657
721
  * not yours ("awaiting payment", "sent to printer, not yet confirmed").
@@ -8,7 +8,13 @@ export interface NavigationItem {
8
8
  visible?: boolean;
9
9
  }
10
10
 
11
- export interface NavigationGroup {
11
+ /**
12
+ * A group of sidebar links. `TItem` is the app's own item type, for menus whose
13
+ * items carry extra keys (a permission, a route name): DXDashboard and
14
+ * DXDashboardSidebar infer it from the `navigation` prop, so the `link` slot's
15
+ * `item` has those keys too. Defaults to `NavigationItem`.
16
+ */
17
+ export interface NavigationGroup<TItem extends NavigationItem = NavigationItem> {
12
18
  label?: string;
13
19
  /**
14
20
  * Stable identifier for the group's collapsible open/closed state. Defaults
@@ -17,7 +23,7 @@ export interface NavigationGroup {
17
23
  * state attached to the right group.
18
24
  */
19
25
  key?: string;
20
- items: NavigationItem[];
26
+ items: TItem[];
21
27
  /**
22
28
  * Per-group override for the sidebar's `collapsibleGroups` behaviour.
23
29
  * Set to `false` to keep a group permanently expanded (no toggle header)
@@ -29,7 +35,26 @@ export interface NavigationGroup {
29
35
  visible?: boolean;
30
36
  }
31
37
 
32
- export type Navigation = NavigationGroup[];
38
+ /** The sidebar's navigation: its groups, in order. See `NavigationGroup` for `TItem`. */
39
+ export type Navigation<TItem extends NavigationItem = NavigationItem> = NavigationGroup<TItem>[];
40
+
41
+ /**
42
+ * The item type of a navigation, as the `link` slot's `item` receives it:
43
+ * `NavigationItemOf<Navigation<AppItem>>` is `AppItem`. An `any` navigation
44
+ * (untyped page props) gives `any`, and an empty `[]` gives `NavigationItem`.
45
+ *
46
+ * DXDashboard and DXDashboardSidebar are generic over the whole navigation
47
+ * (`TNavigation`) and derive the item from it, rather than generic over the
48
+ * item: TypeScript infers `any` for a type parameter that IS the prop's type,
49
+ * but not for one nested inside it (`{ items: TItem[] }[]`), where an `any`
50
+ * navigation would have fallen back to `NavigationItem` and rejected the app's
51
+ * extra keys.
52
+ */
53
+ export type NavigationItemOf<TNavigation extends Navigation> = [
54
+ TNavigation[number]["items"][number],
55
+ ] extends [never]
56
+ ? NavigationItem
57
+ : TNavigation[number]["items"][number];
33
58
 
34
59
  /**
35
60
  * Horizontal alignment of the navbar search slot content within its region:
@@ -54,3 +79,150 @@ export type SidebarVariant = "light" | "dark";
54
79
  * Shared by DXDashboard (which forwards it) and DXDashboardNavbar.
55
80
  */
56
81
  export type NavbarActionsOnMobile = "wrap" | "hide";
82
+
83
+ /**
84
+ * The signed-in user DXDashboardNavbar (and DXDashboard, which forwards it)
85
+ * shows in the avatar dropdown. Extra keys are allowed and reach the
86
+ * user-menu slots.
87
+ */
88
+ export interface DashboardUser {
89
+ name: string;
90
+ email: string;
91
+ [key: string]: any;
92
+ }
93
+
94
+ /** A slot that receives no props. */
95
+ type NoSlotProps = Record<never, never>;
96
+
97
+ /*
98
+ * The slot interfaces below are what `defineSlots` uses and what an editor
99
+ * shows on hover. The API docs (`docs:generate:ai`) still read each slot from
100
+ * the `@slot`/`@binding` comments in the component's template: vue-docgen-api
101
+ * only reads `defineSlots` members whose keys are bare identifiers, and it
102
+ * drops the binding types when it does, so every key here is QUOTED on
103
+ * purpose. Keep the text in step with the template comments.
104
+ */
105
+
106
+ /** Slots of DXDashboardNavbar, with their slot props. All are optional. */
107
+ export interface DashboardNavbarSlots {
108
+ /** Custom hamburger/menu icon. Defaults to a three-line SVG. */
109
+ "menu-icon"?(props: NoSlotProps): any;
110
+ /**
111
+ * Search input or component. Fills the middle of the bar at every width,
112
+ * aligned per `searchAlign`. The region is a size container named
113
+ * `dx-navbar-search`; a `<kbd>` hint inside it is hidden when it is
114
+ * narrower than 16rem.
115
+ */
116
+ "search"?(props: NoSlotProps): any;
117
+ /**
118
+ * Page-level primary actions (e.g. a Create button). Right-aligned next to
119
+ * the user menu from `md` up; below, wraps to its own full-width row or is
120
+ * hidden entirely per `actionsOnMobile`.
121
+ */
122
+ "actions"?(props: {
123
+ /** The current page title, for context. */
124
+ pageTitle: string;
125
+ }): any;
126
+ /** Replaces the entire user menu (the default avatar dropdown). */
127
+ "user-menu"?(props: {
128
+ /** The signed-in user; `null` for a guest. */
129
+ user: DashboardUser | null;
130
+ }): any;
131
+ /**
132
+ * Custom user avatar/icon in the dropdown trigger. To decorate the default
133
+ * avatar (e.g. add a notification dot) rather than replace it, render
134
+ * `DXUserAvatar`. Only rendered for a signed-in user.
135
+ */
136
+ "user-icon"?(props: {
137
+ /** The first letter of the user's name. */
138
+ initial: string;
139
+ /** The signed-in user. */
140
+ user: DashboardUser;
141
+ }): any;
142
+ /** Items for the default user dropdown menu. Only rendered for a signed-in user. */
143
+ "user-menu-items"?(props: {
144
+ /** The signed-in user. */
145
+ user: DashboardUser;
146
+ }): any;
147
+ }
148
+
149
+ /**
150
+ * Slots of DXDashboardSidebar, with their slot props. All are optional.
151
+ * `TItem` is the navigation's item type (see `NavigationGroup`).
152
+ */
153
+ export interface DashboardSidebarSlots<TItem extends NavigationItem = NavigationItem> {
154
+ /**
155
+ * Brand/logo area in the sidebar header. Defaults to the title's initial
156
+ * plus the full title (hidden when collapsed).
157
+ */
158
+ "brand"?(props: {
159
+ /** Whether the sidebar rail is collapsed to icons only. */
160
+ collapsed: boolean;
161
+ /** The sidebar title text. */
162
+ title: string;
163
+ }): any;
164
+ /**
165
+ * Renders a single navigation item link. Defaults to an anchor with
166
+ * optional icon, label, and badge.
167
+ */
168
+ "link"?(props: {
169
+ /**
170
+ * The navigation item (label, url, icon, badge, badgeColor), typed as the
171
+ * app's own item type when its items carry extra keys.
172
+ */
173
+ item: TItem;
174
+ /** Whether this item matches the current route. */
175
+ isActive: boolean;
176
+ /** Whether the sidebar rail is collapsed to icons only. */
177
+ collapsed: boolean;
178
+ /** Whether the item's group is currently expanded. */
179
+ isExpanded: boolean;
180
+ }): any;
181
+ /**
182
+ * Utility/secondary links pinned to the bottom of the sidebar (help,
183
+ * changelog, sign-out, …), below the nav groups.
184
+ */
185
+ "footer"?(props: {
186
+ /** Whether the sidebar rail is collapsed to icons only. */
187
+ collapsed: boolean;
188
+ }): any;
189
+ }
190
+
191
+ /**
192
+ * The two bindings DXDashboard adds to every forwarded `sidebar-*` and
193
+ * `navbar-*` slot. They are bound after the child's own slot props, so they
194
+ * win a name clash.
195
+ */
196
+ export interface DashboardSlotExtras {
197
+ /** Shows/hides the sidebar. */
198
+ toggleSidebar: () => void;
199
+ /** Whether the sidebar is currently hidden. */
200
+ sidebarHidden: boolean;
201
+ }
202
+
203
+ /** The props a (possibly optional) slot function receives. */
204
+ type SlotPropsOf<Slot> = NonNullable<Slot> extends (props: infer Props) => any ? Props : never;
205
+
206
+ /**
207
+ * A child component's slots renamed with `Prefix` and given the dashboard's
208
+ * extra bindings: `{ brand(props: P) }` becomes
209
+ * `{ "sidebar-brand"(props: P & DashboardSlotExtras) }`. Derived from the
210
+ * child's slot types, so a slot added to the child is typed here for free.
211
+ */
212
+ export type PrefixedDashboardSlots<Slots, Prefix extends string> = {
213
+ [Name in keyof Slots as `${Prefix}${Name & string}`]?: (
214
+ props: Omit<SlotPropsOf<Slots[Name]>, keyof DashboardSlotExtras> & DashboardSlotExtras,
215
+ ) => any;
216
+ };
217
+
218
+ /**
219
+ * Slots of DXDashboard: the page content (`default`), every
220
+ * DXDashboardSidebar slot as `sidebar-<name>` and every DXDashboardNavbar
221
+ * slot as `navbar-<name>`. An unprefixed name (`#search`) is not forwarded.
222
+ * `TItem` is the navigation's item type (see `NavigationGroup`).
223
+ */
224
+ export type DashboardSlots<TItem extends NavigationItem = NavigationItem> = {
225
+ /** The main page content. */
226
+ default?(props: NoSlotProps): any;
227
+ } & PrefixedDashboardSlots<DashboardSidebarSlots<TItem>, "sidebar-"> &
228
+ PrefixedDashboardSlots<DashboardNavbarSlots, "navbar-">;