@omnitend/dashboard-for-laravel 0.41.2 → 0.42.1

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 (55) 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/DAutocomplete.vue.d.ts +2 -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/DXNumberStepper.vue.d.ts +33 -0
  17. package/dist/components/extended/DXSaveButton.vue.d.ts +38 -0
  18. package/dist/components/extended/DXTable.vue.d.ts +6 -2
  19. package/dist/composables/useForm.d.ts +7 -0
  20. package/dist/dashboard-for-laravel.js +9317 -9052
  21. package/dist/dashboard-for-laravel.js.map +1 -1
  22. package/dist/dashboard-for-laravel.umd.cjs +7 -7
  23. package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
  24. package/dist/index.d.ts +2 -0
  25. package/dist/style.css +2 -2
  26. package/dist/types/index.d.ts +20 -3
  27. package/docs/public/api-reference.json +165 -14
  28. package/docs/public/docs-map.md +4 -2
  29. package/docs/public/llms.txt +4 -2
  30. package/package.json +1 -1
  31. package/resources/css/theme.scss +558 -89
  32. package/resources/fonts/maven-pro/MavenPro-Bold.woff2 +0 -0
  33. package/resources/fonts/maven-pro/MavenPro-Medium.woff2 +0 -0
  34. package/resources/fonts/maven-pro/MavenPro-Regular.woff2 +0 -0
  35. package/resources/fonts/maven-pro/MavenPro-SemiBold.woff2 +0 -0
  36. package/resources/fonts/maven-pro/OFL.txt +93 -0
  37. package/resources/fonts/poppins/OFL.txt +93 -0
  38. package/resources/fonts/poppins/Poppins-Medium.woff2 +0 -0
  39. package/resources/fonts/poppins/Poppins-SemiBold.woff2 +0 -0
  40. package/resources/js/components/base/DAutocomplete.vue +73 -2
  41. package/resources/js/components/charts/DXBarChart.vue +2 -1
  42. package/resources/js/components/charts/DXDoughnutChart.vue +2 -1
  43. package/resources/js/components/charts/DXLineChart.vue +2 -1
  44. package/resources/js/components/charts/chartTheme.ts +68 -21
  45. package/resources/js/components/extended/DXDashboard.vue +4 -2
  46. package/resources/js/components/extended/DXDashboardSidebar.vue +52 -20
  47. package/resources/js/components/extended/DXField.vue +59 -28
  48. package/resources/js/components/extended/DXForm.vue +122 -21
  49. package/resources/js/components/extended/DXNumberStepper.vue +243 -0
  50. package/resources/js/components/extended/DXSaveButton.vue +82 -0
  51. package/resources/js/components/extended/DXTable.vue +44 -15
  52. package/resources/js/composables/useForm.ts +11 -0
  53. package/resources/js/composables/useToast.ts +1 -1
  54. package/resources/js/index.ts +2 -0
  55. package/resources/js/types/index.ts +21 -3
@@ -0,0 +1,82 @@
1
+ <!--
2
+ DXSaveButton — a save button that confirms the save in place.
3
+
4
+ Three states, driven by two booleans the consumer owns:
5
+
6
+ - idle (`saving` and `saved` both false) — the normal Save button.
7
+ - saving (`saving`) — DButton's busy state: disabled at once, spinner after
8
+ the anti-flash delay, `savingText` if given.
9
+ - saved (`saved` and not `saving`) — "✓ Saved" (tick icon) and genuinely
10
+ `disabled`, until the consumer clears `saved` (typically on the next edit
11
+ to the form). The colour does not change: the button keeps its own
12
+ variant (navy primary by default) at full strength, neither the neutral
13
+ grey of a disabled button nor a faded one. Only the label and the
14
+ disabled state say it is saved.
15
+
16
+ `saving` wins over `saved`, so re-saving from the saved state shows busy.
17
+ The label change is announced: the button itself is a polite live region,
18
+ and while it is `aria-busy` (saving) announcements wait until it settles,
19
+ so a screen reader hears "Saved" once the save lands.
20
+
21
+ Every other attribute (type, block, size, class, listeners…) passes through
22
+ to DButton. DXForm uses this for its submit button.
23
+ -->
24
+ <template>
25
+ <DButton
26
+ v-bind="$attrs"
27
+ :variant="variant"
28
+ :icon="isShowingSaved ? 'check-lg' : icon"
29
+ :loading="saving"
30
+ :loading-text="savingText"
31
+ :disabled="disabled || isShowingSaved"
32
+ :class="{ 'dx-save-button--saved': isShowingSaved }"
33
+ aria-live="polite"
34
+ >
35
+ <template v-if="isShowingSaved">{{ savedText }}</template>
36
+ <slot v-else>Save</slot>
37
+ </DButton>
38
+ </template>
39
+
40
+ <script setup lang="ts">
41
+ import { computed } from "vue";
42
+ import type { ButtonVariant } from "bootstrap-vue-next";
43
+ import DButton from "../base/DButton.vue";
44
+
45
+ defineOptions({ inheritAttrs: false });
46
+
47
+ interface Props {
48
+ /** A save is in flight: busy/spinner state (DButton's `loading`). */
49
+ saving?: boolean;
50
+
51
+ /**
52
+ * The form's current contents have been saved: show `savedText` with a
53
+ * tick and disable the button. Clear it when the form changes.
54
+ */
55
+ saved?: boolean;
56
+
57
+ /** Label for the saved state. @default 'Saved' */
58
+ savedText?: string;
59
+
60
+ /** Label shown beside the spinner while saving (DButton's `loadingText`). */
61
+ savingText?: string;
62
+
63
+ /** Variant in every state; the saved state keeps it. @default 'primary' */
64
+ variant?: ButtonVariant | null;
65
+
66
+ /** Leading icon when not saved. The saved state shows `check-lg`. */
67
+ icon?: string;
68
+
69
+ /** Disable the button regardless of state. */
70
+ disabled?: boolean;
71
+ }
72
+
73
+ const props = withDefaults(defineProps<Props>(), {
74
+ saving: false,
75
+ saved: false,
76
+ savedText: "Saved",
77
+ variant: "primary",
78
+ disabled: false,
79
+ });
80
+
81
+ const isShowingSaved = computed(() => props.saved && !props.saving);
82
+ </script>
@@ -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)"
@@ -577,7 +577,10 @@ export interface Props<TItem = any> {
577
577
  /** Error message */
578
578
  error?: string | null;
579
579
 
580
- /** 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
+ */
581
584
  pagination?: PaginationData;
582
585
 
583
586
  /** Show pagination controls */
@@ -685,7 +688,8 @@ export interface Props<TItem = any> {
685
688
  /**
686
689
  * Label column width for the edit modal's horizontal layout, forwarded to
687
690
  * `DXForm`'s `labelCols` (a single width or a per-breakpoint object). Only
688
- * meaningful when `editLayout` is "horizontal".
691
+ * meaningful when `editLayout` is "horizontal". Omitted: DXForm's default
692
+ * 45% label column.
689
693
  */
690
694
  editLabelCols?: LabelCols;
691
695
 
@@ -989,8 +993,11 @@ if (
989
993
  console.warn('[DXTable] clientSide mode ignores provider, apiUrl and inertiaUrl props. Data is processed locally from items.');
990
994
  }
991
995
 
992
- // Computed for effective busy state (provider mode uses 'busy', inertia uses 'loading')
993
- 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);
994
1001
 
995
1002
  /*
996
1003
  * `perPage`, `sortBy` and `filters` are dual-purpose: with a `v-model` they are
@@ -2023,9 +2030,10 @@ const handleApiPageChange = (page: number) => {
2023
2030
  * `currentPage` and `perPage`; `busy` is the raw prop (two-way with BTable's
2024
2031
  * provider loading via `@update:busy`).
2025
2032
  * - client-side: rows are pre-sliced by `clientSidePaginatedItems`, and local
2026
- * sorting is off (we sort in `clientSideSortedItems`).
2033
+ * sorting is off (we sort in `clientSideSortedItems`); `busy` is
2034
+ * `effectiveBusy`.
2027
2035
  * - inertia: server-paginated `items`, local sorting off, `busy` is
2028
- * `effectiveBusy` (the `loading` prop in this mode).
2036
+ * `effectiveBusy` (`busy` or the deprecated `loading` in this mode).
2029
2037
  *
2030
2038
  * A key absent from the returned object is simply not bound, which is what the
2031
2039
  * original blocks did (e.g. non-provider modes never set `provider`/`currentPage`,
@@ -2041,7 +2049,7 @@ const tableModeBindings = computed<Record<string, unknown>>(() => {
2041
2049
  };
2042
2050
  }
2043
2051
  if (isClientSideMode.value) {
2044
- return { items: clientSidePaginatedItems.value, noLocalSorting: true };
2052
+ return { items: clientSidePaginatedItems.value, noLocalSorting: true, busy: effectiveBusy.value };
2045
2053
  }
2046
2054
  // inertia
2047
2055
  return { items: resolvedItems.value, noLocalSorting: true, busy: effectiveBusy.value };
@@ -2050,13 +2058,32 @@ const tableModeBindings = computed<Record<string, unknown>>(() => {
2050
2058
  /*
2051
2059
  * The pagination metadata the single footer renders, by mode (#123). Null means
2052
2060
  * no footer: only provider mode has that case (a custom provider with no
2053
- * `pagination` prop). Client-side always has a computed object; inertia's
2054
- * `pagination` prop carries a default object, so both always render a footer —
2055
- * 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.
2056
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
+
2057
2082
  const activePagination = computed<PaginationData | null>(() => {
2058
2083
  if (isClientSideMode.value) return clientSidePagination.value;
2059
- if (isInertiaMode.value) return props.pagination;
2084
+ if (isInertiaMode.value) {
2085
+ return paginationWasProvided ? props.pagination : unpaginatedItemsPagination.value;
2086
+ }
2060
2087
  if (isProviderMode.value) return providerPagination.value;
2061
2088
  return null;
2062
2089
  });
@@ -2347,7 +2374,9 @@ const tableSlotSignature = (slots: Record<string, unknown>): string => {
2347
2374
  // three data modes from one place.
2348
2375
  const tablePassthroughProps = computed(() => ({
2349
2376
  footClone: props.footClone,
2350
- 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),
2351
2380
  emptyText:
2352
2381
  props.emptyText ??
2353
2382
  (hasActiveFilters.value
@@ -2457,7 +2486,7 @@ defineExpose({
2457
2486
  }
2458
2487
 
2459
2488
  /* Muted header titles (#157): in a data table the CONTENT is what matters —
2460
- 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
2461
2490
  structure without the shout. Consumers re-louden via the token. */
2462
2491
  :deep(thead th) {
2463
2492
  color: var(--dx-table-header-color, var(--bs-secondary-color));
@@ -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;
@@ -10,9 +10,11 @@ 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
12
  export { default as DXModalActions } from "./components/extended/DXModalActions.vue";
13
+ export { default as DXSaveButton } from "./components/extended/DXSaveButton.vue";
13
14
  export { default as DXLoading } from "./components/extended/DXLoading.vue";
14
15
  export { default as DXSwitch } from "./components/extended/DXSwitch.vue";
15
16
  export { default as DXCurrencyInput } from "./components/extended/DXCurrencyInput.vue";
17
+ export { default as DXNumberStepper } from "./components/extended/DXNumberStepper.vue";
16
18
  export { default as DXStatCard } from "./components/extended/DXStatCard.vue";
17
19
  // Chart components moved to the `@omnitend/dashboard-for-laravel/charts` entry
18
20
  // (#142) so chart.js / vue-chartjs stay genuinely optional peers — the main
@@ -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
+ }