@omnitend/dashboard-for-laravel 0.44.0 → 0.45.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.
@@ -1,8 +1,8 @@
1
1
  {
2
- "generated": "2026-10-09T16:42:41.495Z",
2
+ "generated": "2026-10-11T09:08:08.777Z",
3
3
  "package": {
4
4
  "name": "@omnitend/dashboard-for-laravel",
5
- "version": "0.44.0"
5
+ "version": "0.45.0"
6
6
  },
7
7
  "components": {
8
8
  "base": [
@@ -2932,8 +2932,8 @@
2932
2932
  "name": "delay",
2933
2933
  "type": "number",
2934
2934
  "required": false,
2935
- "default": "300",
2936
- "description": ""
2935
+ "default": "600",
2936
+ "description": "Milliseconds to wait before showing anything. Most loads finish\n sooner, and one that has already taken this long is likely to take\n longer, so a quick load never flashes a loader."
2937
2937
  }
2938
2938
  ],
2939
2939
  "events": [],
@@ -1,7 +1,7 @@
1
1
  # Documentation Map
2
2
 
3
3
  > Auto-generated hierarchical overview of all documentation
4
- > Last updated: 2026-10-09T16:42:41.546Z
4
+ > Last updated: 2026-10-11T09:08:08.837Z
5
5
 
6
6
  This file provides a complete map of all available documentation for AI agents and developers.
7
7
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnitend/dashboard-for-laravel",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "description": "Vue 3 dashboard components for Laravel with Bootstrap Vue Next",
5
5
  "type": "module",
6
6
  "main": "./dist/dashboard-for-laravel.umd.cjs",
@@ -1992,3 +1992,123 @@ $dx-chart-edge-dark: $body-bg-dark;
1992
1992
  thead tr:has(.dx-table-filter-cell) > td {
1993
1993
  padding: 0.5rem;
1994
1994
  }
1995
+
1996
+ // ————————————————— DXLoading (the soft loader)
1997
+ //
1998
+ // Modelled on Omni Tend's legacy loader: a small grey spinner beside the
1999
+ // text on one line, the text in the display face with a slow light sweep
2000
+ // across it, fading in after DXLoading's delay. Quieter than a navy spinner,
2001
+ // but the text keeps AA contrast (#6c757d, 4.69:1 on white; the legacy
2002
+ // gray-300 was 1.5:1). The sweep passes through a lighter grey, so a moment
2003
+ // of the text sits below AA while it moves. Global, not scoped: DXTable and
2004
+ // consumers render it, and its spinner is a bvn component.
2005
+ //
2006
+ // The `role="status"` box renders at once with its contents hidden, and
2007
+ // `.dx-loading--shown` reveals them after the delay. Two reasons: a live
2008
+ // region that is already in the page announces content added to it (one
2009
+ // inserted together with its text can go unannounced), and the space the
2010
+ // loader will take is held from the start, so nothing jumps when it appears.
2011
+ @mixin dx-loading-light-vars {
2012
+ --dx-loading-color: #{$gray-600};
2013
+ --dx-loading-sweep-color: #{$gray-500};
2014
+ --dx-loading-spinner-color: #{$gray-500};
2015
+ }
2016
+
2017
+ // `[data-bs-theme="light"]` too, so a light section inside a dark page gets
2018
+ // the light set back (as the chart tokens do).
2019
+ :root,
2020
+ [data-bs-theme="light"] {
2021
+ @include dx-loading-light-vars;
2022
+ // Slower than Bootstrap's 0.75 s a turn: a calmer spin to match the soft
2023
+ // look (1.1 s chosen against 1.5 s and 2 s side by side).
2024
+ --dx-loading-spinner-speed: 1.1s;
2025
+ }
2026
+
2027
+ [data-bs-theme="dark"] {
2028
+ --dx-loading-color: #{$gray-500};
2029
+ --dx-loading-sweep-color: #{$gray-600};
2030
+ --dx-loading-spinner-color: #{$gray-600};
2031
+ }
2032
+
2033
+ .dx-loading {
2034
+ display: flex;
2035
+ justify-content: center;
2036
+ align-items: center;
2037
+ gap: 1rem;
2038
+ color: var(--dx-loading-color);
2039
+
2040
+ // Hidden, not removed: `visibility` keeps the box's size and keeps the
2041
+ // contents out of the accessibility tree until they are revealed.
2042
+ > * {
2043
+ visibility: hidden;
2044
+ }
2045
+
2046
+ // Set on the spinner itself: Bootstrap declares its speed on
2047
+ // `.spinner-border`, so a value inherited from `.dx-loading` would lose.
2048
+ .spinner-border {
2049
+ flex: none;
2050
+ color: var(--dx-loading-spinner-color);
2051
+ --bs-spinner-animation-speed: var(--dx-loading-spinner-speed);
2052
+ }
2053
+ }
2054
+
2055
+ .dx-loading--shown {
2056
+ animation: dx-loading-fade-in 0.4s ease-out both;
2057
+
2058
+ > * {
2059
+ visibility: visible;
2060
+ }
2061
+ }
2062
+
2063
+ .dx-loading__text {
2064
+ margin: 0;
2065
+ font-family: var(--dx-font-family-display);
2066
+ font-weight: 500;
2067
+ font-size: $h2-font-size;
2068
+ line-height: $headings-line-height;
2069
+ background-image: linear-gradient(
2070
+ to right,
2071
+ var(--dx-loading-color) 20%,
2072
+ var(--dx-loading-color) 40%,
2073
+ var(--dx-loading-sweep-color) 60%,
2074
+ var(--dx-loading-sweep-color) 80%,
2075
+ var(--dx-loading-color) 99%
2076
+ );
2077
+ background-size: 200% auto;
2078
+ -webkit-background-clip: text;
2079
+ background-clip: text;
2080
+ -webkit-text-fill-color: transparent;
2081
+ animation: dx-loading-sweep 3s linear infinite;
2082
+ }
2083
+
2084
+ @keyframes dx-loading-fade-in {
2085
+ from {
2086
+ opacity: 0;
2087
+ }
2088
+ }
2089
+
2090
+ @keyframes dx-loading-sweep {
2091
+ to {
2092
+ background-position: 200% center;
2093
+ }
2094
+ }
2095
+
2096
+ // For people who asked for less motion: no sweep and no fade (the text is
2097
+ // plain grey and appears at once), and the spinner slows to double its turn,
2098
+ // as Bootstrap's own reduced-motion rule does for its spinners. That rule
2099
+ // cannot reach this one, because the speed above out-ranks it.
2100
+ @media (prefers-reduced-motion: reduce) {
2101
+ .dx-loading--shown,
2102
+ .dx-loading__text {
2103
+ animation: none;
2104
+ }
2105
+
2106
+ .dx-loading__text {
2107
+ background-image: none;
2108
+ -webkit-text-fill-color: currentColor;
2109
+ }
2110
+
2111
+ .dx-loading .spinner-border {
2112
+ --bs-spinner-animation-speed: calc(2 * var(--dx-loading-spinner-speed));
2113
+ }
2114
+ }
@@ -405,7 +405,14 @@ import DXNumericField from "./DXNumericField.vue";
405
405
  import DXChoiceField from "./DXChoiceField.vue";
406
406
  import { useAsyncOptions } from "../../composables/useAsyncOptions";
407
407
  import type { UseFormReturn } from "../../composables/useForm";
408
- import type { FieldDefinition, FieldOption, FieldType, LabelCols } from "../../types";
408
+ import type {
409
+ FieldDefinition,
410
+ FieldOption,
411
+ FieldType,
412
+ LabelCols,
413
+ MaybeFn,
414
+ } from "../../types";
415
+ import { humaniseErrorKey } from "../../utils/formErrorTargets";
409
416
  import { getByPath, setByPath } from "../../utils/objectPath";
410
417
 
411
418
  // Async to break the DXField <-> DXRepeater circular import.
@@ -619,6 +626,21 @@ function resolveMaybe<TValue>(
619
626
  return value;
620
627
  }
621
628
 
629
+ /** Resolve display TEXT (label, hint, info, switch text) the way
630
+ * resolveMaybe does, but a function that throws (a repeater row missing a
631
+ * property it reads) gives `undefined` instead of breaking the render (#204).
632
+ * Behaviour options (disabled, readonly, plaintext) stay unguarded: no
633
+ * fallback for those is safe to guess. */
634
+ function resolveText(
635
+ value: MaybeFn<string> | undefined,
636
+ ): string | undefined {
637
+ try {
638
+ return resolveMaybe(value);
639
+ } catch {
640
+ return undefined;
641
+ }
642
+ }
643
+
622
644
  const fieldValue = computed({
623
645
  get: () =>
624
646
  usePathSemantics.value
@@ -691,13 +713,19 @@ function setValue(value: any): void {
691
713
 
692
714
  const fieldState = computed(() => props.form.getState(errorKey.value));
693
715
 
694
- const resolvedLabel = computed(
695
- () => resolveMaybe(props.field.label) ?? props.field.key,
696
- );
716
+ // A label function that throws falls back to the humanised key, as the
717
+ // repeater's headers and the error summary do; a missing label still shows
718
+ // the raw key, as it always has.
719
+ const resolvedLabel = computed(() => {
720
+ if (typeof props.field.label !== "function") {
721
+ return props.field.label ?? props.field.key;
722
+ }
723
+ return resolveText(props.field.label) ?? humaniseErrorKey(props.field.key);
724
+ });
697
725
 
698
- const resolvedHint = computed(() => resolveMaybe(props.field.hint));
726
+ const resolvedHint = computed(() => resolveText(props.field.hint));
699
727
 
700
- const resolvedInfo = computed(() => resolveMaybe(props.field.info));
728
+ const resolvedInfo = computed(() => resolveText(props.field.info));
701
729
 
702
730
  // ————————————————— switch-list (#160)
703
731
 
@@ -763,8 +791,8 @@ const switchModel = computed({
763
791
  */
764
792
  const switchText = computed(() => {
765
793
  const contextual = switchIsOn.value
766
- ? resolveMaybe(props.field.textWhenTrue)
767
- : resolveMaybe(props.field.textWhenFalse);
794
+ ? resolveText(props.field.textWhenTrue)
795
+ : resolveText(props.field.textWhenFalse);
768
796
  return contextual ?? resolvedLabel.value;
769
797
  });
770
798
 
@@ -1,12 +1,16 @@
1
1
  <template>
2
+ <!-- The status box is there from the start, holding the loader's space
3
+ with its contents hidden (theme.scss); the delay only reveals them.
4
+ Content added to a live region already in the page is announced,
5
+ where a region inserted with its text can be missed. -->
2
6
  <div
3
- v-if="isVisible"
4
- class="dx-loading text-center text-muted py-5"
7
+ class="dx-loading py-5"
8
+ :class="{ 'dx-loading--shown': isVisible }"
5
9
  role="status"
6
10
  aria-live="polite"
7
11
  >
8
- <DSpinner variant="primary" aria-hidden="true" />
9
- <div class="mt-2">{{ text }}</div>
12
+ <DSpinner small aria-hidden="true" />
13
+ <p class="dx-loading__text">{{ text }}</p>
10
14
  </div>
11
15
  </template>
12
16
 
@@ -14,14 +18,20 @@
14
18
  import { onBeforeUnmount, onMounted, ref } from "vue";
15
19
  import DSpinner from "../base/DSpinner.vue";
16
20
 
21
+ // Styles (one line, grey, a slow sweep across the text, a fade-in, contents
22
+ // hidden until `.dx-loading--shown`) are global in theme.scss under
23
+ // `.dx-loading`, with `--dx-loading-*` tokens.
17
24
  const props = withDefaults(
18
25
  defineProps<{
19
26
  text?: string;
27
+ /** Milliseconds to wait before showing anything. Most loads finish
28
+ * sooner, and one that has already taken this long is likely to take
29
+ * longer, so a quick load never flashes a loader. */
20
30
  delay?: number;
21
31
  }>(),
22
32
  {
23
33
  text: "Loading…",
24
- delay: 300,
34
+ delay: 600,
25
35
  },
26
36
  );
27
37
 
@@ -29,14 +39,11 @@ const isVisible = ref(false);
29
39
  let showTimer: ReturnType<typeof setTimeout> | undefined;
30
40
 
31
41
  onMounted(() => {
32
- if (props.delay <= 0) {
33
- isVisible.value = true;
34
- return;
35
- }
36
-
42
+ // Always a timer, even for no delay: the empty status box must reach the
43
+ // page a task before its contents, or the announcement can be missed.
37
44
  showTimer = setTimeout(() => {
38
45
  isVisible.value = true;
39
- }, props.delay);
46
+ }, Math.max(props.delay, 0));
40
47
  });
41
48
 
42
49
  onBeforeUnmount(() => {
@@ -35,10 +35,7 @@
35
35
  {{ error || apiError }}
36
36
  </div>
37
37
 
38
- <div v-if="effectiveBusy && !isProviderMode" class="text-center py-5">
39
- <DSpinner variant="primary" />
40
- <p class="mt-2">{{ loadingText }}</p>
41
- </div>
38
+ <DXLoading v-if="effectiveBusy && !isProviderMode" :text="loadingText" />
42
39
 
43
40
  <!-- One table for all three data modes. The modes differ only
44
41
  in a few bound props, supplied by `tableModeBindings`, so
@@ -326,7 +323,7 @@ import DContainer from "../base/DContainer.vue";
326
323
  import DRow from "../base/DRow.vue";
327
324
  import DCol from "../base/DCol.vue";
328
325
  import DXTableShell from "./DXTableShell.vue";
329
- import DSpinner from "../base/DSpinner.vue";
326
+ import DXLoading from "./DXLoading.vue";
330
327
  import DTable from "../base/DTable.vue";
331
328
  import DFormInput from "../base/DFormInput.vue";
332
329
  import DFormSelect from "../base/DFormSelect.vue";