@omnitend/dashboard-for-laravel 0.13.1 → 0.15.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-07-04T17:33:54.873Z",
2
+ "generated": "2026-07-05T08:20:04.640Z",
3
3
  "package": {
4
4
  "name": "@omnitend/dashboard-for-laravel",
5
- "version": "0.13.1"
5
+ "version": "0.14.0"
6
6
  },
7
7
  "components": {
8
8
  "base": [
@@ -2475,6 +2475,13 @@
2475
2475
  "default": "\"12\"",
2476
2476
  "description": "Column size (Bootstrap grid)"
2477
2477
  },
2478
+ {
2479
+ "name": "card",
2480
+ "type": "boolean",
2481
+ "required": false,
2482
+ "default": "true",
2483
+ "description": "Wrap the table in a `DCard`. Defaults to `true`. Set `false` for a plain,\nborderless variant — the same header, filter row, table and pagination\nrendered directly on the page background (no card border/shadow), for\ndata-heavy admin index pages that read as busy inside a card."
2484
+ },
2478
2485
  {
2479
2486
  "name": "editFields",
2480
2487
  "type": "Array",
@@ -2794,12 +2801,41 @@
2794
2801
  }
2795
2802
  ],
2796
2803
  "methods": []
2804
+ },
2805
+ {
2806
+ "name": "DXTableShell",
2807
+ "category": "extended",
2808
+ "filePath": "resources/js/components/extended/DXTableShell.vue",
2809
+ "description": "Internal chrome wrapper for `DXTable`. Renders its content either inside a `DCard` (the default) or as a plain, borderless block on the page background (`:card=\"false\"`). Extracted so `DXTable`'s ~460-line body and all its slots aren't duplicated across a card/plain `v-if`. Slots: - `#header` — the title / action row. In card mode it fills `DCard`'s header; in plain mode it renders above the table with a subtle bottom rule. - default — the table body (spinner/error/table/pagination).",
2810
+ "props": [
2811
+ {
2812
+ "name": "card",
2813
+ "type": "boolean",
2814
+ "required": false,
2815
+ "default": "true",
2816
+ "description": "Wrap the content in a `DCard`. When false, render plain/borderless."
2817
+ }
2818
+ ],
2819
+ "events": [],
2820
+ "slots": [
2821
+ {
2822
+ "name": "header",
2823
+ "description": "",
2824
+ "bindings": []
2825
+ },
2826
+ {
2827
+ "name": "default",
2828
+ "description": "",
2829
+ "bindings": []
2830
+ }
2831
+ ],
2832
+ "methods": []
2797
2833
  }
2798
2834
  ]
2799
2835
  },
2800
2836
  "stats": {
2801
- "totalComponents": 70,
2837
+ "totalComponents": 71,
2802
2838
  "baseComponents": 57,
2803
- "extendedComponents": 13
2839
+ "extendedComponents": 14
2804
2840
  }
2805
2841
  }
@@ -1,7 +1,7 @@
1
1
  # Documentation Map
2
2
 
3
3
  > Auto-generated hierarchical overview of all documentation
4
- > Last updated: 2026-07-04T17:33:54.935Z
4
+ > Last updated: 2026-07-05T08:20:04.705Z
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.13.1",
3
+ "version": "0.15.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",
@@ -21,3 +21,38 @@ defineOptions({
21
21
  </template>
22
22
  </BAutocomplete>
23
23
  </template>
24
+
25
+ <style scoped>
26
+ /*
27
+ BAutocomplete renders `.input-group > .b-autocomplete-input-wrapper +
28
+ .b-autocomplete-trigger`. The input wrapper takes the full width, so under
29
+ Bootstrap's default `flex-wrap: wrap` the trigger chevron wraps onto its own
30
+ line below the input. Keep the group on a single row and let the input wrapper
31
+ flex, so the trigger sits inline as an append. (#53)
32
+ */
33
+ :deep(.input-group) {
34
+ flex-wrap: nowrap;
35
+ }
36
+
37
+ :deep(.b-autocomplete-input-wrapper) {
38
+ flex: 1 1 auto;
39
+ min-width: 0;
40
+ }
41
+
42
+ /*
43
+ The trigger renders as a heavy `btn btn-secondary` (dark), which reads as a
44
+ detached dark block. Soften it to a subtle bordered chevron that matches the
45
+ input and reads as a combobox affordance. Uses theme variables (no hardcoded
46
+ colours) and a more specific selector than `.btn-secondary` (no `!important`).
47
+ */
48
+ :deep(.b-autocomplete-trigger) {
49
+ background-color: var(--bs-body-bg);
50
+ border-color: var(--bs-border-color);
51
+ color: var(--bs-secondary-color);
52
+ }
53
+
54
+ :deep(.b-autocomplete-trigger:hover) {
55
+ background-color: var(--bs-tertiary-bg);
56
+ color: var(--bs-body-color);
57
+ }
58
+ </style>
@@ -590,15 +590,51 @@ onBeforeUnmount(() => {
590
590
  border-radius: var(--bs-border-radius);
591
591
  }
592
592
 
593
- /* Switch field: contextual styling that responds to the on/off state.
594
- Off is muted; on turns the control and label a filled success green. */
593
+ /* Switch field: a full-width "filled box" toggle, the same height as a select
594
+ or text input, that colour-codes its state — a neutral box when off, a filled
595
+ success green when on. The label sits on the left and the toggle on the right.
596
+ Renders the underlying `.form-check` (from the switch checkbox) as the box. */
597
+ .dx-switch :deep(.form-check) {
598
+ display: flex;
599
+ align-items: center;
600
+ justify-content: space-between;
601
+ gap: 0.75rem;
602
+ width: 100%;
603
+ min-height: calc(1.5em + 0.75rem + 2px); /* match .form-control height */
604
+ margin: 0;
605
+ /* Override Bootstrap `.form-switch`'s 2.5em left padding for the floated input. */
606
+ padding: 0.375rem 0.75rem;
607
+ border: 1px solid var(--bs-border-color);
608
+ border-radius: var(--bs-border-radius);
609
+ background-color: var(--bs-body-bg);
610
+ transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
611
+ }
612
+
613
+ /* Label on the left, toggle on the right (DOM order is input then label). */
595
614
  .dx-switch :deep(.form-check-label) {
615
+ order: 1;
616
+ margin: 0;
596
617
  color: var(--bs-secondary-color);
618
+ cursor: pointer;
597
619
  transition: color 0.15s ease-in-out;
598
620
  }
599
621
 
622
+ .dx-switch :deep(.form-check-input) {
623
+ order: 2;
624
+ flex-shrink: 0;
625
+ /* Override `.form-switch`'s negative left margin now that we're flex-laid-out. */
626
+ margin: 0;
627
+ cursor: pointer;
628
+ }
629
+
630
+ /* On-state: filled success green. */
631
+ .dx-switch--on :deep(.form-check) {
632
+ background-color: var(--bs-success-bg-subtle);
633
+ border-color: var(--bs-success);
634
+ }
635
+
600
636
  .dx-switch--on :deep(.form-check-label) {
601
- color: var(--bs-success);
637
+ color: var(--bs-success-text-emphasis);
602
638
  font-weight: 500;
603
639
  }
604
640
 
@@ -9,7 +9,7 @@
9
9
  <DContainer :fluid="fluid" :class="containerClass">
10
10
  <DRow class="justify-content-center">
11
11
  <DCol :md="columnSize">
12
- <DCard>
12
+ <DXTableShell :card="card">
13
13
  <template v-if="title || createUrl || $slots.header" #header>
14
14
  <!-- @slot Card header content; overrides the default title heading and the "New {item}" button. -->
15
15
  <slot name="header">
@@ -69,13 +69,15 @@
69
69
  @update:model-value="handleFilterChange(field.key, $event as string)"
70
70
  />
71
71
 
72
- <!-- Select Filter -->
73
- <DFormSelect
72
+ <!-- Select Filter: typeahead — browse the full list on focus, or type to narrow; clear (✕) resets to "no filter" -->
73
+ <DAutocomplete
74
74
  v-else-if="field.filter === 'select'"
75
75
  :model-value="effectiveFilters[field.key] || ''"
76
- :options="[{ value: '', text: 'All' }, ...getFieldFilterOptions(field)]"
76
+ :options="getFieldFilterOptions(field)"
77
+ :placeholder="field.filterPlaceholder || `All ${field.label || field.key}`"
77
78
  size="sm"
78
- @update:model-value="handleFilterChange(field.key, $event as string)"
79
+ open-on-focus
80
+ @update:model-value="handleFilterChange(field.key, ($event ?? '') as string)"
79
81
  />
80
82
 
81
83
  <!-- Number Filter -->
@@ -165,13 +167,15 @@
165
167
  @update:model-value="handleFilterChange(field.key, $event as string)"
166
168
  />
167
169
 
168
- <!-- Select Filter -->
169
- <DFormSelect
170
+ <!-- Select Filter: typeahead — browse the full list on focus, or type to narrow; clear (✕) resets to "no filter" -->
171
+ <DAutocomplete
170
172
  v-else-if="field.filter === 'select'"
171
173
  :model-value="effectiveFilters[field.key] || ''"
172
- :options="[{ value: '', text: 'All' }, ...getFieldFilterOptions(field)]"
174
+ :options="getFieldFilterOptions(field)"
175
+ :placeholder="field.filterPlaceholder || `All ${field.label || field.key}`"
173
176
  size="sm"
174
- @update:model-value="handleFilterChange(field.key, $event as string)"
177
+ open-on-focus
178
+ @update:model-value="handleFilterChange(field.key, ($event ?? '') as string)"
175
179
  />
176
180
 
177
181
  <!-- Number Filter -->
@@ -262,13 +266,15 @@
262
266
  @update:model-value="handleFilterChange(field.key, $event as string)"
263
267
  />
264
268
 
265
- <!-- Select Filter -->
266
- <DFormSelect
269
+ <!-- Select Filter: typeahead — browse the full list on focus, or type to narrow; clear (✕) resets to "no filter" -->
270
+ <DAutocomplete
267
271
  v-else-if="field.filter === 'select'"
268
272
  :model-value="effectiveFilters[field.key] || ''"
269
- :options="[{ value: '', text: 'All' }, ...getFieldFilterOptions(field)]"
273
+ :options="getFieldFilterOptions(field)"
274
+ :placeholder="field.filterPlaceholder || `All ${field.label || field.key}`"
270
275
  size="sm"
271
- @update:model-value="handleFilterChange(field.key, $event as string)"
276
+ open-on-focus
277
+ @update:model-value="handleFilterChange(field.key, ($event ?? '') as string)"
272
278
  />
273
279
 
274
280
  <!-- Number Filter -->
@@ -473,7 +479,7 @@
473
479
  </div>
474
480
  </div>
475
481
  </div>
476
- </DCard>
482
+ </DXTableShell>
477
483
  </DCol>
478
484
  </DRow>
479
485
 
@@ -628,15 +634,17 @@ import { router } from "@inertiajs/vue3";
628
634
  import axios from "axios";
629
635
  import pluralize from "pluralize";
630
636
  import { useToast } from "../../composables/useToast";
637
+ import { useForm } from "../../composables/useForm";
631
638
  import DContainer from "../base/DContainer.vue";
632
639
  import DRow from "../base/DRow.vue";
633
640
  import DCol from "../base/DCol.vue";
634
- import DCard from "../base/DCard.vue";
641
+ import DXTableShell from "./DXTableShell.vue";
635
642
  import DSpinner from "../base/DSpinner.vue";
636
643
  import DTable from "../base/DTable.vue";
637
644
  import DPagination from "../base/DPagination.vue";
638
645
  import DFormInput from "../base/DFormInput.vue";
639
646
  import DFormSelect from "../base/DFormSelect.vue";
647
+ import DAutocomplete from "../base/DAutocomplete.vue";
640
648
  import DModal from "../base/DModal.vue";
641
649
  import DButton from "../base/DButton.vue";
642
650
  import DXForm from "./DXForm.vue";
@@ -785,6 +793,14 @@ export interface Props<TItem = any> {
785
793
  /** Column size (Bootstrap grid) */
786
794
  columnSize?: string | number;
787
795
 
796
+ /**
797
+ * Wrap the table in a `DCard`. Defaults to `true`. Set `false` for a plain,
798
+ * borderless variant — the same header, filter row, table and pagination
799
+ * rendered directly on the page background (no card border/shadow), for
800
+ * data-heavy admin index pages that read as busy inside a card.
801
+ */
802
+ card?: boolean;
803
+
788
804
  // Edit Modal Props
789
805
  /** Form field definitions for edit modal (if provided, enables edit on row click) */
790
806
  editFields?: any[]; // FieldDefinition[] - using any to avoid circular import
@@ -855,6 +871,7 @@ const props = withDefaults(defineProps<Props<T>>(), {
855
871
  fluid: false,
856
872
  containerClass: "py-5",
857
873
  columnSize: "12",
874
+ card: true,
858
875
  editModalSize: "lg",
859
876
  });
860
877
 
@@ -1501,12 +1518,6 @@ const editLoading = ref(false);
1501
1518
  // the form after the user has since opened a different row.
1502
1519
  let editFetchToken = 0;
1503
1520
 
1504
- // Monotonic token for opening the modal itself. The first edit dynamically
1505
- // imports useForm; if the user clicks row A then row B before that import
1506
- // resolves, A's callback must not seed/open/fetch over B. Each open captures
1507
- // the current token and bails if it's since been superseded.
1508
- let modalOpenToken = 0;
1509
-
1510
1521
  // Toast (may not be available in test environment)
1511
1522
  let createToast: ((obj: any) => any) | undefined;
1512
1523
  try {
@@ -1587,40 +1598,27 @@ const handleRowClick = (item: T, index: number, event: MouseEvent) => {
1587
1598
  // Reset to first tab
1588
1599
  activeTabIndex.value = 0;
1589
1600
 
1590
- // Capture this open so a slow first-time useForm import can't seed the
1591
- // form after the user has since opened a different row (or create).
1592
- const openToken = ++modalOpenToken;
1593
-
1594
- // Initialize form with item data
1601
+ // Initialize form with item data. `useForm` is statically imported, so
1602
+ // seeding is synchronous — no interleaving between successive row opens.
1595
1603
  if (!editForm.value) {
1596
- // Dynamically import useForm to avoid circular dependency
1597
- import('../../composables/useForm').then(({ useForm }) => {
1598
- if (openToken !== modalOpenToken) return; // superseded
1599
- const formData: Record<string, any> = {};
1600
- props.editFields!.forEach(field => {
1601
- formData[field.key] = (item as any)[field.key] ?? field.default ?? '';
1602
- });
1603
- editForm.value = useForm(formData);
1604
-
1605
- // Open modal
1606
- showEditModal.value = true;
1607
-
1608
- // Optionally replace the row-seeded data with the full record.
1609
- if (props.showUrl) void fetchFullRecordForEdit(item);
1604
+ const formData: Record<string, any> = {};
1605
+ props.editFields!.forEach(field => {
1606
+ formData[field.key] = (item as any)[field.key] ?? field.default ?? '';
1610
1607
  });
1608
+ editForm.value = useForm(formData);
1611
1609
  } else {
1612
1610
  // Update existing form
1613
1611
  props.editFields.forEach(field => {
1614
1612
  editForm.value.data[field.key] = (item as any)[field.key] ?? field.default ?? '';
1615
1613
  });
1616
1614
  editForm.value.clearErrors();
1615
+ }
1617
1616
 
1618
- // Open modal
1619
- showEditModal.value = true;
1617
+ // Open modal
1618
+ showEditModal.value = true;
1620
1619
 
1621
- // Optionally replace the row-seeded data with the full record.
1622
- if (props.showUrl) void fetchFullRecordForEdit(item);
1623
- }
1620
+ // Optionally replace the row-seeded data with the full record.
1621
+ if (props.showUrl) void fetchFullRecordForEdit(item);
1624
1622
  }
1625
1623
  };
1626
1624
 
@@ -1673,34 +1671,25 @@ const handleCreateNew = () => {
1673
1671
  selectedItem.value = null;
1674
1672
  activeTabIndex.value = 0;
1675
1673
 
1676
- // Supersede any in-flight row-open (and its pending fetch) so a slow
1677
- // useForm import from an earlier edit can't seed over this create.
1678
- const openToken = ++modalOpenToken;
1674
+ // Supersede any in-flight row-open fetch so a slow showUrl fetch from an
1675
+ // earlier edit can't seed over this create.
1679
1676
  editFetchToken++;
1680
1677
  editLoading.value = false;
1681
1678
 
1682
- const initForm = (useForm: any) => {
1679
+ if (!editForm.value) {
1683
1680
  const formData: Record<string, any> = {};
1684
1681
  props.editFields!.forEach(field => {
1685
1682
  formData[field.key] = field.default ?? '';
1686
1683
  });
1687
1684
  editForm.value = useForm(formData);
1688
- showEditModal.value = true;
1689
- };
1690
-
1691
- if (!editForm.value) {
1692
- import('../../composables/useForm').then(({ useForm }) => {
1693
- if (openToken !== modalOpenToken) return; // superseded
1694
- initForm(useForm);
1695
- });
1696
1685
  } else {
1697
1686
  // Reset existing form to defaults
1698
1687
  props.editFields!.forEach(field => {
1699
1688
  editForm.value.data[field.key] = field.default ?? '';
1700
1689
  });
1701
1690
  editForm.value.clearErrors();
1702
- showEditModal.value = true;
1703
1691
  }
1692
+ showEditModal.value = true;
1704
1693
  };
1705
1694
 
1706
1695
  // Handle save from edit modal
@@ -0,0 +1,54 @@
1
+ <!--
2
+ @component
3
+ Internal chrome wrapper for `DXTable`. Renders its content either inside a
4
+ `DCard` (the default) or as a plain, borderless block on the page background
5
+ (`:card="false"`). Extracted so `DXTable`'s ~460-line body and all its slots
6
+ aren't duplicated across a card/plain `v-if`.
7
+
8
+ Slots:
9
+ - `#header` — the title / action row. In card mode it fills `DCard`'s header;
10
+ in plain mode it renders above the table with a subtle bottom rule.
11
+ - default — the table body (spinner/error/table/pagination).
12
+ -->
13
+ <template>
14
+ <DCard v-if="card">
15
+ <template v-if="$slots.header" #header>
16
+ <slot name="header" />
17
+ </template>
18
+ <slot />
19
+ </DCard>
20
+
21
+ <div v-else class="dx-table-plain">
22
+ <div v-if="$slots.header" class="dx-table-plain__header">
23
+ <slot name="header" />
24
+ </div>
25
+ <slot />
26
+ </div>
27
+ </template>
28
+
29
+ <script setup lang="ts">
30
+ import DCard from '../base/DCard.vue';
31
+
32
+ interface Props {
33
+ /** Wrap the content in a `DCard`. When false, render plain/borderless. */
34
+ card?: boolean;
35
+ }
36
+
37
+ withDefaults(defineProps<Props>(), {
38
+ card: true,
39
+ });
40
+ </script>
41
+
42
+ <style scoped>
43
+ /*
44
+ Plain variant: no card border/shadow/background — the table sits directly on
45
+ the page. The header row gets a subtle bottom rule so it still reads as a
46
+ distinct band above the table (mirrors the card header's separation without
47
+ the full card chrome).
48
+ */
49
+ .dx-table-plain__header {
50
+ padding-bottom: 0.75rem;
51
+ margin-bottom: 0.75rem;
52
+ border-bottom: 1px solid var(--bs-border-color);
53
+ }
54
+ </style>