@omnitend/dashboard-for-laravel 0.41.1 → 0.42.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 (62) hide show
  1. package/README.md +2 -1
  2. package/dist/assets/MavenPro-Bold-35b16f06.woff2 +0 -0
  3. package/dist/assets/MavenPro-Medium-febc8674.woff2 +0 -0
  4. package/dist/assets/MavenPro-Regular-aee3df9e.woff2 +0 -0
  5. package/dist/assets/MavenPro-SemiBold-c555dbfa.woff2 +0 -0
  6. package/dist/assets/Poppins-Medium-5800fa57.woff2 +0 -0
  7. package/dist/assets/Poppins-SemiBold-94a36267.woff2 +0 -0
  8. package/dist/charts.js +148 -130
  9. package/dist/charts.js.map +1 -1
  10. package/dist/charts.umd.cjs +1 -1
  11. package/dist/charts.umd.cjs.map +1 -1
  12. package/dist/components/base/DModal.vue.d.ts +26 -2
  13. package/dist/components/charts/chartTheme.d.ts +14 -2
  14. package/dist/components/extended/DXField.vue.d.ts +16 -5
  15. package/dist/components/extended/DXForm.vue.d.ts +25 -9
  16. package/dist/components/extended/DXLoading.vue.d.ts +9 -0
  17. package/dist/components/extended/DXModalActions.vue.d.ts +12 -0
  18. package/dist/components/extended/DXNumberStepper.vue.d.ts +33 -0
  19. package/dist/components/extended/DXSaveButton.vue.d.ts +38 -0
  20. package/dist/components/extended/DXSwitch.vue.d.ts +1 -1
  21. package/dist/components/extended/DXTable.vue.d.ts +8 -2
  22. package/dist/composables/useForm.d.ts +7 -0
  23. package/dist/dashboard-for-laravel.js +9547 -9246
  24. package/dist/dashboard-for-laravel.js.map +1 -1
  25. package/dist/dashboard-for-laravel.umd.cjs +7 -7
  26. package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
  27. package/dist/index.d.ts +5 -0
  28. package/dist/style.css +2 -2
  29. package/dist/types/index.d.ts +20 -3
  30. package/docs/public/api-reference.json +219 -17
  31. package/docs/public/docs-map.md +4 -2
  32. package/docs/public/llms.txt +7 -2
  33. package/package.json +1 -1
  34. package/resources/css/theme.scss +524 -103
  35. package/resources/fonts/maven-pro/MavenPro-Bold.woff2 +0 -0
  36. package/resources/fonts/maven-pro/MavenPro-Medium.woff2 +0 -0
  37. package/resources/fonts/maven-pro/MavenPro-Regular.woff2 +0 -0
  38. package/resources/fonts/maven-pro/MavenPro-SemiBold.woff2 +0 -0
  39. package/resources/fonts/maven-pro/OFL.txt +93 -0
  40. package/resources/fonts/poppins/OFL.txt +93 -0
  41. package/resources/fonts/poppins/Poppins-Medium.woff2 +0 -0
  42. package/resources/fonts/poppins/Poppins-SemiBold.woff2 +0 -0
  43. package/resources/js/components/base/DModal.vue +13 -2
  44. package/resources/js/components/charts/DXBarChart.vue +2 -1
  45. package/resources/js/components/charts/DXDoughnutChart.vue +2 -1
  46. package/resources/js/components/charts/DXLineChart.vue +2 -1
  47. package/resources/js/components/charts/chartTheme.ts +68 -21
  48. package/resources/js/components/extended/DXDashboard.vue +4 -2
  49. package/resources/js/components/extended/DXDashboardSidebar.vue +52 -20
  50. package/resources/js/components/extended/DXField.vue +59 -28
  51. package/resources/js/components/extended/DXForm.vue +122 -21
  52. package/resources/js/components/extended/DXLoading.vue +47 -0
  53. package/resources/js/components/extended/DXModalActions.vue +13 -0
  54. package/resources/js/components/extended/DXNumberStepper.vue +243 -0
  55. package/resources/js/components/extended/DXSaveButton.vue +82 -0
  56. package/resources/js/components/extended/DXSwitch.vue +8 -5
  57. package/resources/js/components/extended/DXTable.vue +58 -22
  58. package/resources/js/components/extended/DXTablePagination.vue +1 -1
  59. package/resources/js/composables/useForm.ts +11 -0
  60. package/resources/js/composables/useToast.ts +1 -1
  61. package/resources/js/index.ts +5 -0
  62. package/resources/js/types/index.ts +21 -3
@@ -100,7 +100,7 @@
100
100
  <DFormInput
101
101
  v-if="field.filter === 'text'"
102
102
  :model-value="effectiveFilters[filterKeyFor(field)] || ''"
103
- :placeholder="field.filterPlaceholder || `Search ${field.label || field.key}...`"
103
+ :placeholder="field.filterPlaceholder || `${field.label || field.key}…`"
104
104
  size="sm"
105
105
  @update:model-value="handleFilterChange(filterKeyFor(field), $event as string)"
106
106
  />
@@ -149,7 +149,7 @@
149
149
  <DFormInput
150
150
  v-else-if="field.filter === 'number'"
151
151
  :model-value="effectiveFilters[filterKeyFor(field)] || ''"
152
- :placeholder="field.filterPlaceholder || `Filter ${field.label || field.key}...`"
152
+ :placeholder="field.filterPlaceholder || `${field.label || field.key}…`"
153
153
  type="number"
154
154
  size="sm"
155
155
  @update:model-value="handleFilterChange(filterKeyFor(field), $event as string)"
@@ -174,7 +174,10 @@
174
174
  <template v-for="field in fields" :key="`head-${field.key}`" #[`head(${field.key})`]="{ label }">
175
175
  <div class="d-flex align-items-center justify-content-between gap-2">
176
176
  <div class="flex-grow-1">
177
- <div class="fw-semibold">{{ headerLabel(field, label) }}</div>
177
+ <div class="fw-semibold">
178
+ <DXFieldLabel v-if="field.info" :label="headerLabel(field, label)" :info="field.info" />
179
+ <template v-else>{{ headerLabel(field, label) }}</template>
180
+ </div>
178
181
  <small v-if="field.hint" class="text-muted d-block" style="font-weight: normal;">{{ field.hint }}</small>
179
182
  </div>
180
183
 
@@ -326,6 +329,7 @@ import DTable from "../base/DTable.vue";
326
329
  import DFormInput from "../base/DFormInput.vue";
327
330
  import DFormSelect from "../base/DFormSelect.vue";
328
331
  import DAutocomplete from "../base/DAutocomplete.vue";
332
+ import DXFieldLabel from "./DXFieldLabel.vue";
329
333
  import DButton from "../base/DButton.vue";
330
334
  import DXTablePagination from "./DXTablePagination.vue";
331
335
  import DXTableEditorModal from "./DXTableEditorModal.vue";
@@ -404,6 +408,8 @@ export interface TableField {
404
408
  filterAllText?: string;
405
409
  sortable?: boolean;
406
410
  hint?: string;
411
+ /** Help revealed from a focusable info icon beside the column label. */
412
+ info?: string;
407
413
  /**
408
414
  * The inline filter control for this column, or `false`/omit for none. See
409
415
  * `FilterType`. `'select'` (typeahead) and `'select-native'` (plain
@@ -571,7 +577,10 @@ export interface Props<TItem = any> {
571
577
  /** Error message */
572
578
  error?: string | null;
573
579
 
574
- /** Pagination data (Inertia mode) */
580
+ /**
581
+ * Pagination data (Inertia mode). Omit it on a plain `items` table and the
582
+ * footer counts the rows given as one page.
583
+ */
575
584
  pagination?: PaginationData;
576
585
 
577
586
  /** Show pagination controls */
@@ -679,7 +688,8 @@ export interface Props<TItem = any> {
679
688
  /**
680
689
  * Label column width for the edit modal's horizontal layout, forwarded to
681
690
  * `DXForm`'s `labelCols` (a single width or a per-breakpoint object). Only
682
- * meaningful when `editLayout` is "horizontal".
691
+ * meaningful when `editLayout` is "horizontal". Omitted: DXForm's default
692
+ * 45% label column.
683
693
  */
684
694
  editLabelCols?: LabelCols;
685
695
 
@@ -983,8 +993,11 @@ if (
983
993
  console.warn('[DXTable] clientSide mode ignores provider, apiUrl and inertiaUrl props. Data is processed locally from items.');
984
994
  }
985
995
 
986
- // Computed for effective busy state (provider mode uses 'busy', inertia uses 'loading')
987
- const effectiveBusy = computed(() => isProviderMode.value ? props.busy : props.loading);
996
+ // Effective busy state. Provider mode: `busy` (two-way with BTable's own
997
+ // loading). Items and client-side modes: `busy`, the documented prop, or the
998
+ // deprecated `loading`; these modes used to read only `loading`, so a page
999
+ // passing `:busy` showed the empty text while its rows were still loading.
1000
+ const effectiveBusy = computed(() => isProviderMode.value ? props.busy : props.busy || props.loading);
988
1001
 
989
1002
  /*
990
1003
  * `perPage`, `sortBy` and `filters` are dual-purpose: with a `v-model` they are
@@ -1767,6 +1780,7 @@ watch(clientSideLastPage, (lastPage) => {
1767
1780
  // Client-side paginated items (final output)
1768
1781
  const clientSidePaginatedItems = computed<T[]>(() => {
1769
1782
  if (!isClientSideMode.value) return [];
1783
+ if (!props.showPagination) return clientSideSortedItems.value;
1770
1784
 
1771
1785
  const perPage = effectivePerPage.value;
1772
1786
  const start = (clientSidePage.value - 1) * perPage;
@@ -1783,19 +1797,19 @@ const clientSidePagination = computed<PaginationData>(() => {
1783
1797
  const lastPage = clientSideLastPage.value;
1784
1798
 
1785
1799
  // Same clamped page the slice uses — see `clientSidePage` (#118).
1786
- const validPage = clientSidePage.value;
1800
+ const validPage = props.showPagination ? clientSidePage.value : 1;
1787
1801
 
1788
- const from = total > 0 ? (validPage - 1) * perPage + 1 : 0;
1789
- const to = Math.min(validPage * perPage, total);
1802
+ const from = total > 0 ? (props.showPagination ? (validPage - 1) * perPage + 1 : 1) : 0;
1803
+ const to = props.showPagination ? Math.min(validPage * perPage, total) : total;
1790
1804
 
1791
1805
  return {
1792
1806
  current_page: validPage,
1793
- per_page: perPage,
1807
+ per_page: props.showPagination ? perPage : Math.max(1, total),
1794
1808
  total,
1795
1809
  total_unfiltered: totalUnfiltered !== total ? totalUnfiltered : undefined,
1796
1810
  from,
1797
1811
  to,
1798
- last_page: lastPage,
1812
+ last_page: props.showPagination ? lastPage : 1,
1799
1813
  };
1800
1814
  });
1801
1815
 
@@ -1822,7 +1836,7 @@ const handleClientSidePageChange = (page: number) => {
1822
1836
  // Computed: determine if per-page selector should be shown
1823
1837
  // Hide it when total items is less than the smallest page size option
1824
1838
  const shouldShowPerPageSelector = computed(() => {
1825
- if (!props.showPerPageSelector) return false;
1839
+ if (!props.showPagination || !props.showPerPageSelector) return false;
1826
1840
 
1827
1841
  const smallestOption = Math.min(...props.perPageOptions);
1828
1842
  const total = isClientSideMode.value
@@ -2016,9 +2030,10 @@ const handleApiPageChange = (page: number) => {
2016
2030
  * `currentPage` and `perPage`; `busy` is the raw prop (two-way with BTable's
2017
2031
  * provider loading via `@update:busy`).
2018
2032
  * - client-side: rows are pre-sliced by `clientSidePaginatedItems`, and local
2019
- * sorting is off (we sort in `clientSideSortedItems`).
2033
+ * sorting is off (we sort in `clientSideSortedItems`); `busy` is
2034
+ * `effectiveBusy`.
2020
2035
  * - inertia: server-paginated `items`, local sorting off, `busy` is
2021
- * `effectiveBusy` (the `loading` prop in this mode).
2036
+ * `effectiveBusy` (`busy` or the deprecated `loading` in this mode).
2022
2037
  *
2023
2038
  * A key absent from the returned object is simply not bound, which is what the
2024
2039
  * original blocks did (e.g. non-provider modes never set `provider`/`currentPage`,
@@ -2034,7 +2049,7 @@ const tableModeBindings = computed<Record<string, unknown>>(() => {
2034
2049
  };
2035
2050
  }
2036
2051
  if (isClientSideMode.value) {
2037
- return { items: clientSidePaginatedItems.value, noLocalSorting: true };
2052
+ return { items: clientSidePaginatedItems.value, noLocalSorting: true, busy: effectiveBusy.value };
2038
2053
  }
2039
2054
  // inertia
2040
2055
  return { items: resolvedItems.value, noLocalSorting: true, busy: effectiveBusy.value };
@@ -2043,13 +2058,32 @@ const tableModeBindings = computed<Record<string, unknown>>(() => {
2043
2058
  /*
2044
2059
  * The pagination metadata the single footer renders, by mode (#123). Null means
2045
2060
  * no footer: only provider mode has that case (a custom provider with no
2046
- * `pagination` prop). Client-side always has a computed object; inertia's
2047
- * `pagination` prop carries a default object, so both always render a footer —
2048
- * matching the three original per-mode `v-if`s exactly.
2061
+ * `pagination` prop). Client-side and inertia always render a footer.
2062
+ *
2063
+ * Inertia mode is also what a plain `:items` table with no `pagination` lands
2064
+ * in. Its footer read the `withDefaults` placeholder (`total: 0`), so two
2065
+ * rendered rows sat above "0 items.". With no `pagination` passed, the rows
2066
+ * given ARE the whole set: describe them as one page.
2049
2067
  */
2068
+ const unpaginatedItemsPagination = computed<PaginationData>(() => {
2069
+ const itemCount = resolvedItems.value?.length ?? 0;
2070
+ return {
2071
+ current_page: 1,
2072
+ // One page holding every row, so the pager (shown only when
2073
+ // total > per_page) never appears. Floor of 1 keeps the page
2074
+ // maths away from a zero divisor.
2075
+ per_page: Math.max(itemCount, 1),
2076
+ total: itemCount,
2077
+ from: itemCount > 0 ? 1 : 0,
2078
+ to: itemCount,
2079
+ };
2080
+ });
2081
+
2050
2082
  const activePagination = computed<PaginationData | null>(() => {
2051
2083
  if (isClientSideMode.value) return clientSidePagination.value;
2052
- if (isInertiaMode.value) return props.pagination;
2084
+ if (isInertiaMode.value) {
2085
+ return paginationWasProvided ? props.pagination : unpaginatedItemsPagination.value;
2086
+ }
2053
2087
  if (isProviderMode.value) return providerPagination.value;
2054
2088
  return null;
2055
2089
  });
@@ -2340,7 +2374,9 @@ const tableSlotSignature = (slots: Record<string, unknown>): string => {
2340
2374
  // three data modes from one place.
2341
2375
  const tablePassthroughProps = computed(() => ({
2342
2376
  footClone: props.footClone,
2343
- showEmpty: props.showEmpty,
2377
+ // No empty text while rows are loading: "No … found" during a load reads as
2378
+ // "there is no data". Provider mode is unchanged by this.
2379
+ showEmpty: props.showEmpty && (isProviderMode.value || effectiveBusy.value === false),
2344
2380
  emptyText:
2345
2381
  props.emptyText ??
2346
2382
  (hasActiveFilters.value
@@ -2450,7 +2486,7 @@ defineExpose({
2450
2486
  }
2451
2487
 
2452
2488
  /* Muted header titles (#157): in a data table the CONTENT is what matters —
2453
- near-black bold headers compete with it. Grey (still bold) keeps the
2489
+ near-black bold headers compete with it. Grey (weight 500) keeps the
2454
2490
  structure without the shout. Consumers re-louden via the token. */
2455
2491
  :deep(thead th) {
2456
2492
  color: var(--dx-table-header-color, var(--bs-secondary-color));
@@ -1,6 +1,6 @@
1
1
  <!--
2
2
  @component
3
- Internal pagination + per-page footer for `DXTable`. Renders the pager, the
3
+ Pagination + per-page footer shared by `DXTable` and standalone tables. Renders the pager, the
4
4
  per-page selector, and the "X to Y out of Z items" info line (with the
5
5
  "Filtered from N" note when a column filter is active).
6
6
 
@@ -31,6 +31,13 @@ export interface FormState<TData extends Record<string, any>> {
31
31
  message: string;
32
32
  touched: Record<string, boolean>;
33
33
  recentlySuccessful: boolean;
34
+ /**
35
+ * Whether the most recent submission succeeded. Unlike
36
+ * `recentlySuccessful` (which clears itself after 1.5s) this stays true
37
+ * until the next submission starts, and is false after a failure — the
38
+ * Inertia `wasSuccessful` semantics. DXForm's saved state reads it.
39
+ */
40
+ wasSuccessful: boolean;
34
41
  shouldShowMessage: boolean;
35
42
  }
36
43
 
@@ -172,6 +179,7 @@ export function useForm<TData extends Record<string, any>>(
172
179
  message: "",
173
180
  touched: {},
174
181
  recentlySuccessful: false,
182
+ wasSuccessful: false,
175
183
  shouldShowMessage: false,
176
184
  });
177
185
 
@@ -263,6 +271,7 @@ export function useForm<TData extends Record<string, any>>(
263
271
  ): Promise<TResponse> => {
264
272
  inFlightCount += 1;
265
273
  state.processing = true;
274
+ state.wasSuccessful = false;
266
275
  if (!options.preserveErrors) clearErrors();
267
276
 
268
277
  const payloadRaw = (
@@ -326,6 +335,7 @@ export function useForm<TData extends Record<string, any>>(
326
335
  });
327
336
 
328
337
  state.recentlySuccessful = true;
338
+ state.wasSuccessful = true;
329
339
  if (successTimer !== null) clearTimeout(successTimer);
330
340
  successTimer = setTimeout(() => {
331
341
  state.recentlySuccessful = false;
@@ -338,6 +348,7 @@ export function useForm<TData extends Record<string, any>>(
338
348
  }
339
349
  return data;
340
350
  } catch (err) {
351
+ state.wasSuccessful = false;
341
352
  const { errors, message } = errorsFromLaravel(err);
342
353
  setErrors(errors);
343
354
  setMessage(message);
@@ -13,7 +13,7 @@ import type {
13
13
  // class, driving Bootstrap's own `--bs-toast-*` variables cleanly. An
14
14
  // unrecognised variant (e.g. "primary", "dark", or none) falls through to
15
15
  // BVN's default `.text-bg-*` styling unchanged.
16
- const THEMED_TOAST_VARIANTS = new Set(["success", "danger", "warning", "info"]);
16
+ const THEMED_TOAST_VARIANTS = new Set(["success", "danger", "warning", "info", "pending"]);
17
17
 
18
18
  function themeToastParam(param: Record<string, unknown>): Record<string, unknown> {
19
19
  const variant = param.variant as string | null | undefined;
@@ -9,8 +9,12 @@ export { default as DXDashboard } from "./components/extended/DXDashboard.vue";
9
9
  export { default as DXForm } from "./components/extended/DXForm.vue";
10
10
  export { default as DXField } from "./components/extended/DXField.vue";
11
11
  export { default as DXFieldLabel } from "./components/extended/DXFieldLabel.vue";
12
+ export { default as DXModalActions } from "./components/extended/DXModalActions.vue";
13
+ export { default as DXSaveButton } from "./components/extended/DXSaveButton.vue";
14
+ export { default as DXLoading } from "./components/extended/DXLoading.vue";
12
15
  export { default as DXSwitch } from "./components/extended/DXSwitch.vue";
13
16
  export { default as DXCurrencyInput } from "./components/extended/DXCurrencyInput.vue";
17
+ export { default as DXNumberStepper } from "./components/extended/DXNumberStepper.vue";
14
18
  export { default as DXStatCard } from "./components/extended/DXStatCard.vue";
15
19
  // Chart components moved to the `@omnitend/dashboard-for-laravel/charts` entry
16
20
  // (#142) so chart.js / vue-chartjs stay genuinely optional peers — the main
@@ -23,6 +27,7 @@ export { default as DXRepeater } from "./components/extended/DXRepeater.vue";
23
27
  */
24
28
  export { default as DXBasicForm } from "./components/extended/DXBasicForm.vue";
25
29
  export { default as DXTable } from "./components/extended/DXTable.vue";
30
+ export { default as DXTablePagination } from "./components/extended/DXTablePagination.vue";
26
31
  export { default as DXDashboardSidebar } from "./components/extended/DXDashboardSidebar.vue";
27
32
  export { default as DXDashboardNavbar } from "./components/extended/DXDashboardNavbar.vue";
28
33
  export { default as DXUserAvatar } from "./components/extended/DXUserAvatar.vue";
@@ -73,8 +73,9 @@ export type OptionsLoader = (model: any) => Promise<FieldOption[]>;
73
73
  * Deliberately excludes `boolean` (which BFormGroup's own `labelCols` prop
74
74
  * accepts): Vue's type-based `defineProps` auto-defaults an omitted prop to
75
75
  * `false` (not `undefined`) whenever its declared type includes `boolean`,
76
- * which would silently break the `field.labelCols ?? formLabelCols ?? 3`
77
- * fallback chain used to resolve this value.
76
+ * which would silently break the `field.labelCols ?? formLabelCols` fallback
77
+ * chain used to resolve this value (`undefined` at the end of it means the
78
+ * default 45% label column, `--dx-form-label-width`).
78
79
  */
79
80
  export type LabelCols =
80
81
  | number
@@ -241,7 +242,8 @@ export interface FieldDefinition {
241
242
 
242
243
  /**
243
244
  * Per-field override of DXForm's `labelCols` prop, for horizontal
244
- * layout. Falls back to the form-level `labelCols` when omitted.
245
+ * layout. Falls back to the form-level `labelCols` when omitted, and to
246
+ * the default 45% label column (`--dx-form-label-width`) when neither is set.
245
247
  */
246
248
  labelCols?: LabelCols;
247
249
 
@@ -636,3 +638,19 @@ export interface FormTab {
636
638
  /** Lazily mount tab content until first activated */
637
639
  lazy?: boolean;
638
640
  }
641
+
642
+ /**
643
+ * The theme's extra colour variant, `pending`: waiting, and the next move is
644
+ * not yours ("awaiting payment", "sent to printer, not yet confirmed").
645
+ * theme.scss paints it everywhere a stock variant is painted (badge, soft and
646
+ * outline button, alert, text, subtle utilities, table row, list-group item,
647
+ * toast). Registering it on bootstrap-vue-next's `BaseColorVariant` adds it to
648
+ * every variant union derived from that interface (`ColorVariant`,
649
+ * `ButtonVariant` incl. `outline-pending`, the `-subtle` and `-emphasis`
650
+ * forms), so `<DBadge variant="pending">` type-checks.
651
+ */
652
+ declare module "bootstrap-vue-next" {
653
+ interface BaseColorVariant {
654
+ pending: unknown;
655
+ }
656
+ }