@omnitend/dashboard-for-laravel 0.4.8 → 0.4.9

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": "2025-11-26T12:28:39.315Z",
2
+ "generated": "2025-12-02T13:23:09.065Z",
3
3
  "package": {
4
4
  "name": "@omnitend/dashboard-for-laravel",
5
- "version": "0.4.7"
5
+ "version": "0.4.8"
6
6
  },
7
7
  "components": {
8
8
  "base": [
@@ -1,7 +1,7 @@
1
1
  # Documentation Map
2
2
 
3
3
  > Auto-generated hierarchical overview of all documentation
4
- > Last updated: 2025-11-26T12:28:39.369Z
4
+ > Last updated: 2025-12-02T13:23:09.125Z
5
5
 
6
6
  This file provides a complete map of all available documentation for AI agents and developers.
7
7
 
@@ -12,6 +12,7 @@ This file provides a complete map of all available documentation for AI agents a
12
12
  ### Base
13
13
 
14
14
  - [DAccordion](/components/base/DAccordion): Type-safe wrapper around Bootstrap Vue Next's BAccordion component
15
+ - [DAccordionItem](/components/base/DAccordionItem): Type-safe wrapper around Bootstrap Vue Next's BAccordionItem component
15
16
  - [DAlert](/components/base/DAlert): Type-safe wrapper around Bootstrap Vue Next's BAlert component
16
17
  - [DAvatar](/components/base/DAvatar): Type-safe wrapper around Bootstrap Vue Next's BAvatar component
17
18
  - [DBadge](/components/base/DBadge): Type-safe wrapper around Bootstrap Vue Next's BBadge component
@@ -21,6 +22,7 @@ This file provides a complete map of all available documentation for AI agents a
21
22
  - [DButtonToolbar](/components/base/DButtonToolbar): Type-safe wrapper around Bootstrap Vue Next's BButtonToolbar component
22
23
  - [DCard](/components/base/DCard): Type-safe wrapper around Bootstrap Vue Next's BCard component
23
24
  - [DCarousel](/components/base/DCarousel): Type-safe wrapper around Bootstrap Vue Next's BCarousel component
25
+ - [DCarouselSlide](/components/base/DCarouselSlide): Type-safe wrapper around Bootstrap Vue Next's BCarouselSlide component
24
26
  - [DCol](/components/base/DCol): Type-safe wrapper around Bootstrap Vue Next's BCol component
25
27
  - [DCollapse](/components/base/DCollapse): Type-safe wrapper around Bootstrap Vue Next's BCollapse component
26
28
  - [DContainer](/components/base/DContainer): Type-safe wrapper around Bootstrap Vue Next's BContainer component
@@ -31,19 +33,25 @@ This file provides a complete map of all available documentation for AI agents a
31
33
  - [DFormCheckbox](/components/base/DFormCheckbox): Type-safe wrapper around Bootstrap Vue Next's BFormCheckbox component
32
34
  - [DFormGroup](/components/base/DFormGroup): Type-safe wrapper around Bootstrap Vue Next's BFormGroup component
33
35
  - [DFormInput](/components/base/DFormInput): Type-safe wrapper around Bootstrap Vue Next's BFormInput component
36
+ - [DFormInvalidFeedback](/components/base/DFormInvalidFeedback): Type-safe wrapper around Bootstrap Vue Next's BFormInvalidFeedback component
34
37
  - [DFormRadio](/components/base/DFormRadio): Type-safe wrapper around Bootstrap Vue Next's BFormRadio component
35
38
  - [DFormSelect](/components/base/DFormSelect): Type-safe wrapper around Bootstrap Vue Next's BFormSelect component
36
39
  - [DFormSpinbutton](/components/base/DFormSpinbutton): Type-safe wrapper around Bootstrap Vue Next's BFormSpinbutton component
37
40
  - [DFormTags](/components/base/DFormTags): Type-safe wrapper around Bootstrap Vue Next's BFormTags component
41
+ - [DFormText](/components/base/DFormText): Type-safe wrapper around Bootstrap Vue Next's BFormText component
38
42
  - [DFormTextarea](/components/base/DFormTextarea): Type-safe wrapper around Bootstrap Vue Next's BFormTextarea component
39
43
  - [DImage](/components/base/DImage): Type-safe wrapper around Bootstrap Vue Next's BImage component
40
44
  - [DInputGroup](/components/base/DInputGroup): Type-safe wrapper around Bootstrap Vue Next's BInputGroup component
41
45
  - [DLink](/components/base/DLink): Type-safe wrapper around Bootstrap Vue Next's BLink component
42
46
  - [DListGroup](/components/base/DListGroup): Type-safe wrapper around Bootstrap Vue Next's BListGroup component
47
+ - [DListGroupItem](/components/base/DListGroupItem): Type-safe wrapper around Bootstrap Vue Next's BListGroupItem component
43
48
  - [DModal](/components/base/DModal): Type-safe wrapper around Bootstrap Vue Next's BModal component
44
49
  - [DNav](/components/base/DNav): Type-safe wrapper around Bootstrap Vue Next's BNav component
45
50
  - [DNavItem](/components/base/DNavItem): DNavItem - A type-safe wrapper around Bootstrap Vue Next's BNavItem component. Individual navigation
46
51
  - [DNavbar](/components/base/DNavbar): Type-safe wrapper around Bootstrap Vue Next BNavbar component. Responsive navigation header with sup
52
+ - [DNavbarBrand](/components/base/DNavbarBrand): Type-safe wrapper around Bootstrap Vue Next's BNavbarBrand component
53
+ - [DNavbarNav](/components/base/DNavbarNav): Type-safe wrapper around Bootstrap Vue Next's BNavbarNav component
54
+ - [DNavbarToggle](/components/base/DNavbarToggle): Type-safe wrapper around Bootstrap Vue Next's BNavbarToggle component
47
55
  - [DOffcanvas](/components/base/DOffcanvas): DOffcanvas - A type-safe wrapper around Bootstrap Vue Next's BOffcanvas component. Hidden sidebar co
48
56
  - [DOverlay](/components/base/DOverlay): Type-safe wrapper around Bootstrap Vue Next BOverlay component. Overlay component for indicating loa
49
57
  - [DPagination](/components/base/DPagination): DPagination - A type-safe wrapper around Bootstrap Vue Next's BPagination component. Provides naviga
@@ -52,6 +60,7 @@ This file provides a complete map of all available documentation for AI agents a
52
60
  - [DProgress](/components/base/DProgress): Type-safe wrapper around Bootstrap Vue Next BProgress component. Progress bar for displaying simple
53
61
  - [DRow](/components/base/DRow): DRow - A type-safe wrapper around Bootstrap Vue Next's BRow component. Container for columns within
54
62
  - [DSpinner](/components/base/DSpinner): DSpinner - A type-safe wrapper around Bootstrap Vue Next's BSpinner component. Loading indicator to
63
+ - [DTab](/components/base/DTab): Type-safe wrapper around Bootstrap Vue Next's BTab component
55
64
  - [DTable](/components/base/DTable): DTable - A type-safe wrapper around Bootstrap Vue Next's BTable component. Feature-rich data table w
56
65
  - [DTabs](/components/base/DTabs): Type-safe wrapper around Bootstrap Vue Next BTabs component. Create tabbed panes of local content wi
57
66
  - [DToast](/components/base/DToast): DToast - A type-safe wrapper around Bootstrap Vue Next's BToast component. Lightweight notification
@@ -65,7 +74,7 @@ This file provides a complete map of all available documentation for AI agents a
65
74
  - [DXDashboardNavbar](/components/extended/DXDashboardNavbar): A responsive top navigation bar component for Laravel dashboards with user menu, search functionalit
66
75
  - [DXDashboardSidebar](/components/extended/DXDashboardSidebar): A collapsible sidebar navigation component for Laravel dashboards with support for navigation groups
67
76
  - [DXForm](/components/extended/DXForm): Form object from defineForm
68
- - [DXTable](/components/extended/DXTable): A comprehensive data table component with built-in pagination, loading states, and error handling fo
77
+ - [DXTable](/components/extended/DXTable): A comprehensive data table component with built-in pagination, sorting, filtering, and CRUD operatio
69
78
 
70
79
  ### Examples
71
80
 
@@ -73,13 +82,15 @@ This file provides a complete map of all available documentation for AI agents a
73
82
 
74
83
  ### Guide
75
84
 
76
- - [Forms](/guide/forms): The library includes a powerful type-safe form system with validation, auto-generated forms, and com
77
- - [Getting Started](/guide/getting-started): @omnitend/dashboard-for-laravel is a reusable full-stack component library for building Laravel dash
78
- - [Installation](/guide/installation): Install via npm:
85
+ - [AI Integration](/guide/ai-integration): This library includes an MCP (Model Context Protocol) server that provides AI agents with structured
86
+ - [Contributing](/guide/contributing): This library is open source and welcomes contributions. Start here for setup, testing, and how to su
87
+ - [Forms](/guide/forms): The library includes a type-safe form system with validation, auto-generated forms, and composables.
88
+ - [Getting Started](/guide/getting-started): > **Alpha Notice:** v0.x — expect changes between minor versions.
89
+ - [Installation](/guide/installation): ```bash
79
90
  - [Theming](/guide/theming): The library includes a custom Bootstrap 5 theme that can be easily customised using CSS variables an
80
91
  - [TypeScript](/guide/typescript): The library is written in TypeScript and includes full type definitions for all components, composab
81
92
 
82
93
 
83
94
  ---
84
95
 
85
- **Total Pages**: 58
96
+ **Total Pages**: 69
@@ -11,6 +11,8 @@
11
11
 
12
12
  ## Getting Started
13
13
 
14
+ - [AI Integration](/guide/ai-integration): Documentation guide
15
+ - [Contributing](/guide/contributing): Documentation guide
14
16
  - [Forms](/guide/forms): Type-safe form handling and validation
15
17
  - [Getting Started](/guide/getting-started): Quick start guide and core concepts
16
18
  - [Installation](/guide/installation): Install and configure the package
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnitend/dashboard-for-laravel",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
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",
@@ -114,9 +114,98 @@
114
114
  </template>
115
115
  </DTable>
116
116
 
117
+ <!-- Client-Side Mode: Local filtering, sorting, pagination -->
118
+ <DTable
119
+ v-else-if="isClientSideMode"
120
+ :items="clientSidePaginatedItems"
121
+ :fields="fields"
122
+ :sort-by="effectiveSortBy"
123
+ :multisort="false"
124
+ :no-local-sorting="true"
125
+ :no-sortable-icon="true"
126
+ :striped="striped"
127
+ :hover="hover"
128
+ :responsive="responsive"
129
+ @update:sort-by="handleSortChange"
130
+ @row-clicked="handleRowClick"
131
+ >
132
+ <!-- Inline Filter Row -->
133
+ <template v-if="hasFilters" #thead-top>
134
+ <tr class="filter-row">
135
+ <th v-for="field in fields" :key="`filter-${field.key}`" class="p-2">
136
+ <!-- Text Filter -->
137
+ <DFormInput
138
+ v-if="field.filter === 'text'"
139
+ :model-value="effectiveFilters[field.key] || ''"
140
+ :placeholder="field.filterPlaceholder || `Search ${field.label || field.key}...`"
141
+ size="sm"
142
+ @update:model-value="handleFilterChange(field.key, $event as string)"
143
+ />
144
+
145
+ <!-- Select Filter -->
146
+ <DFormSelect
147
+ v-else-if="field.filter === 'select'"
148
+ :model-value="effectiveFilters[field.key] || ''"
149
+ :options="[{ value: '', text: 'All' }, ...getFieldFilterOptions(field)]"
150
+ size="sm"
151
+ @update:model-value="handleFilterChange(field.key, $event as string)"
152
+ />
153
+
154
+ <!-- Number Filter -->
155
+ <DFormInput
156
+ v-else-if="field.filter === 'number'"
157
+ :model-value="effectiveFilters[field.key] || ''"
158
+ :placeholder="field.filterPlaceholder || `Filter ${field.label || field.key}...`"
159
+ type="number"
160
+ size="sm"
161
+ @update:model-value="handleFilterChange(field.key, $event as string)"
162
+ />
163
+
164
+ <!-- Date Filter -->
165
+ <DFormInput
166
+ v-else-if="field.filter === 'date'"
167
+ :model-value="effectiveFilters[field.key] || ''"
168
+ type="date"
169
+ size="sm"
170
+ @update:model-value="handleFilterChange(field.key, $event as string)"
171
+ />
172
+
173
+ <!-- No filter for this column -->
174
+ <div v-else></div>
175
+ </th>
176
+ </tr>
177
+ </template>
178
+
179
+ <!-- Custom headers for all fields -->
180
+ <template v-for="field in fields" :key="`head-${field.key}`" #[`head(${field.key})`]="{ label }">
181
+ <div class="d-flex align-items-center justify-content-between gap-2">
182
+ <div class="flex-grow-1">
183
+ <div class="fw-semibold">{{ label || field.key }}</div>
184
+ <small v-if="field.hint" class="text-muted d-block" style="font-weight: normal;">{{ field.hint }}</small>
185
+ </div>
186
+ <div v-if="field.sortable" class="sort-indicator text-muted flex-shrink-0" style="font-size: 0.75rem; line-height: 1.1; display: flex; flex-direction: column; align-items: center;">
187
+ <span :style="{ opacity: getFieldSortState(field.key) === 'asc' ? 1 : 0.3 }">▲</span>
188
+ <span :style="{ opacity: getFieldSortState(field.key) === 'desc' ? 1 : 0.3 }">▼</span>
189
+ </div>
190
+ </div>
191
+ </template>
192
+
193
+ <!-- Pass through all cell slots -->
194
+ <template
195
+ v-for="(_, name) in $slots"
196
+ #[name]="slotProps"
197
+ >
198
+ <slot
199
+ v-if="typeof name === 'string' && name.startsWith('cell')"
200
+ :name="name"
201
+ v-bind="slotProps"
202
+ />
203
+ </template>
204
+ </DTable>
205
+
117
206
  <!-- Inertia Mode: Use items prop -->
118
207
  <DTable
119
- v-else
208
+ v-else-if="isInertiaMode"
120
209
  :items="items"
121
210
  :fields="fields"
122
211
  :sort-by="effectiveSortBy"
@@ -204,6 +293,54 @@
204
293
  </template>
205
294
  </DTable>
206
295
 
296
+ <!-- Pagination and Controls (Client-Side mode) -->
297
+ <div v-if="isClientSideMode" class="mt-3">
298
+ <!-- Top row: Pagination and Per-page selector -->
299
+ <div class="d-flex justify-content-between align-items-center mb-2">
300
+ <!-- Pagination controls (only when multiple pages) -->
301
+ <DPagination
302
+ v-if="showPagination && clientSidePagination.total > clientSidePagination.per_page"
303
+ :model-value="clientSidePagination.current_page"
304
+ :total-rows="clientSidePagination.total"
305
+ :per-page="clientSidePagination.per_page"
306
+ size="sm"
307
+ @update:model-value="handleClientSidePageChange"
308
+ />
309
+ <div v-else></div>
310
+
311
+ <!-- Per-page selector -->
312
+ <div v-if="clientSidePagination.total >= Math.min(...perPageOptions)" class="d-flex align-items-center gap-2">
313
+ <label for="perPageSelectClientSide" class="mb-0 small text-muted">Per page</label>
314
+ <DFormSelect
315
+ id="perPageSelectClientSide"
316
+ :model-value="effectivePerPage"
317
+ :options="perPageOptions.map(n => ({ value: n, text: n.toString() }))"
318
+ size="sm"
319
+ style="width: 85px;"
320
+ @update:model-value="handlePerPageChange"
321
+ />
322
+ </div>
323
+ </div>
324
+
325
+ <!-- Bottom row: Info text -->
326
+ <div class="small text-muted">
327
+ <div>
328
+ <template v-if="clientSidePagination.total > clientSidePagination.per_page">
329
+ {{ clientSidePagination.from }} to {{ clientSidePagination.to }} out of {{ clientSidePagination.total }} {{ clientSidePagination.total === 1 ? singularItemName : pluralItemName }}.
330
+ </template>
331
+ <template v-else-if="clientSidePagination.total === 1">
332
+ {{ clientSidePagination.total }} {{ singularItemName }}.
333
+ </template>
334
+ <template v-else>
335
+ {{ clientSidePagination.total }} {{ pluralItemName }}.
336
+ </template>
337
+ </div>
338
+ <div v-if="hasActiveFilters && clientSidePagination.total_unfiltered">
339
+ <small>Filtered from {{ clientSidePagination.total_unfiltered }} {{ clientSidePagination.total_unfiltered === 1 ? singularItemName : pluralItemName }}.</small>
340
+ </div>
341
+ </div>
342
+ </div>
343
+
207
344
  <!-- Pagination and Controls (Inertia mode) -->
208
345
  <div v-if="isInertiaMode && pagination" class="mt-3">
209
346
  <!-- Top row: Pagination and Per-page selector -->
@@ -620,6 +757,9 @@ export interface Props<TItem = any> {
620
757
 
621
758
  /** API endpoint pattern for deletions (e.g., "/api/products/:id") */
622
759
  deleteUrl?: string;
760
+
761
+ /** Enable client-side filtering, sorting, and pagination on items array */
762
+ clientSide?: boolean;
623
763
  }
624
764
 
625
765
  const props = withDefaults(defineProps<Props<T>>(), {
@@ -666,10 +806,16 @@ const emit = defineEmits<{
666
806
  }>();
667
807
 
668
808
  // Mode detection
669
- const isProviderMode = computed(() => !!props.provider || !!props.apiUrl);
670
- const isInertiaMode = computed(() => !props.provider && !props.apiUrl && !!props.items);
809
+ const isProviderMode = computed(() => !props.clientSide && (!!props.provider || !!props.apiUrl));
810
+ const isInertiaMode = computed(() => !props.clientSide && !props.provider && !props.apiUrl && !!props.items);
811
+ const isClientSideMode = computed(() => props.clientSide === true && !!props.items);
671
812
  const hasInertiaUrl = computed(() => !!props.inertiaUrl);
672
813
 
814
+ // Warn about invalid prop combinations in client-side mode
815
+ if (props.clientSide && (props.apiUrl || props.inertiaUrl)) {
816
+ console.warn('[DXTable] clientSide mode ignores apiUrl and inertiaUrl props. Data is processed locally from items.');
817
+ }
818
+
673
819
  // Computed for effective busy state (provider mode uses 'busy', inertia uses 'loading')
674
820
  const effectiveBusy = computed(() => isProviderMode.value ? props.busy : props.loading);
675
821
 
@@ -697,6 +843,94 @@ const hasActiveFilters = computed(() => {
697
843
  // API mode pagination metadata (extracted from responses)
698
844
  const apiPaginationMeta = ref<PaginationData | null>(null);
699
845
 
846
+ // ============================================
847
+ // Client-Side Mode: Filtering, Sorting, Pagination
848
+ // ============================================
849
+
850
+ // Client-side current page
851
+ const clientSideCurrentPage = ref(1);
852
+
853
+ // Client-side filtered items
854
+ const clientSideFilteredItems = computed<T[]>(() => {
855
+ if (!isClientSideMode.value || !props.items) return [];
856
+
857
+ const filters = effectiveFilters.value;
858
+ const filterKeys = Object.keys(filters).filter(key => filters[key] && filters[key].trim() !== '');
859
+
860
+ if (filterKeys.length === 0) {
861
+ return props.items;
862
+ }
863
+
864
+ return props.items.filter(item => {
865
+ return filterKeys.every(key => {
866
+ const filterValue = filters[key].trim().toLowerCase();
867
+ const field = props.fields.find(f => f.key === key);
868
+ const itemValue = (item as any)[key];
869
+
870
+ if (itemValue === null || itemValue === undefined) {
871
+ return false;
872
+ }
873
+
874
+ const filterType = field?.filter;
875
+
876
+ switch (filterType) {
877
+ case 'text':
878
+ // Case-insensitive contains search
879
+ return String(itemValue).toLowerCase().includes(filterValue);
880
+
881
+ case 'select':
882
+ // Exact match
883
+ return String(itemValue) === filters[key];
884
+
885
+ case 'number':
886
+ // Exact numeric match
887
+ return Number(itemValue) === Number(filters[key]);
888
+
889
+ case 'date':
890
+ // Exact date match
891
+ return String(itemValue) === filters[key];
892
+
893
+ default:
894
+ // Default: case-insensitive contains
895
+ return String(itemValue).toLowerCase().includes(filterValue);
896
+ }
897
+ });
898
+ });
899
+ });
900
+
901
+ // Client-side sorted items
902
+ const clientSideSortedItems = computed<T[]>(() => {
903
+ if (!isClientSideMode.value) return [];
904
+
905
+ const items = [...clientSideFilteredItems.value];
906
+ const sortBy = effectiveSortBy.value;
907
+
908
+ if (!sortBy || sortBy.length === 0 || !sortBy[0].key) {
909
+ return items;
910
+ }
911
+
912
+ const { key, order } = sortBy[0];
913
+ const direction = order === 'desc' ? -1 : 1;
914
+
915
+ return items.sort((a, b) => {
916
+ const aVal = (a as any)[key];
917
+ const bVal = (b as any)[key];
918
+
919
+ // Handle null/undefined
920
+ if (aVal == null && bVal == null) return 0;
921
+ if (aVal == null) return direction;
922
+ if (bVal == null) return -direction;
923
+
924
+ // Numeric comparison
925
+ if (typeof aVal === 'number' && typeof bVal === 'number') {
926
+ return (aVal - bVal) * direction;
927
+ }
928
+
929
+ // String comparison (case-insensitive)
930
+ return String(aVal).localeCompare(String(bVal), undefined, { sensitivity: 'base' }) * direction;
931
+ });
932
+ });
933
+
700
934
  // API mode filter values (extracted from responses)
701
935
  const apiFilterValues = ref<Record<string, string[]>>({});
702
936
 
@@ -819,6 +1053,66 @@ const effectivePerPage = computed(() => {
819
1053
  return internalPerPage.value;
820
1054
  });
821
1055
 
1056
+ // ============================================
1057
+ // Client-Side Mode: Pagination (requires effectivePerPage)
1058
+ // ============================================
1059
+
1060
+ // Client-side paginated items (final output)
1061
+ const clientSidePaginatedItems = computed<T[]>(() => {
1062
+ if (!isClientSideMode.value) return [];
1063
+
1064
+ const perPage = effectivePerPage.value;
1065
+ const start = (clientSideCurrentPage.value - 1) * perPage;
1066
+ const end = start + perPage;
1067
+
1068
+ return clientSideSortedItems.value.slice(start, end);
1069
+ });
1070
+
1071
+ // Client-side pagination metadata
1072
+ const clientSidePagination = computed<PaginationData>(() => {
1073
+ const total = clientSideFilteredItems.value.length;
1074
+ const totalUnfiltered = props.items?.length || 0;
1075
+ const perPage = effectivePerPage.value;
1076
+ const currentPage = clientSideCurrentPage.value;
1077
+ const lastPage = Math.max(1, Math.ceil(total / perPage));
1078
+
1079
+ // Ensure current page is valid
1080
+ const validPage = Math.min(Math.max(1, currentPage), lastPage);
1081
+
1082
+ const from = total > 0 ? (validPage - 1) * perPage + 1 : 0;
1083
+ const to = Math.min(validPage * perPage, total);
1084
+
1085
+ return {
1086
+ current_page: validPage,
1087
+ per_page: perPage,
1088
+ total,
1089
+ total_unfiltered: totalUnfiltered !== total ? totalUnfiltered : undefined,
1090
+ from,
1091
+ to,
1092
+ last_page: lastPage,
1093
+ };
1094
+ });
1095
+
1096
+ // Reset to page 1 when filters change in client-side mode
1097
+ watch(effectiveFilters, () => {
1098
+ if (isClientSideMode.value) {
1099
+ clientSideCurrentPage.value = 1;
1100
+ }
1101
+ }, { deep: true });
1102
+
1103
+ // Reset to page 1 when perPage changes in client-side mode
1104
+ watch(effectivePerPage, () => {
1105
+ if (isClientSideMode.value) {
1106
+ clientSideCurrentPage.value = 1;
1107
+ }
1108
+ });
1109
+
1110
+ // Handle client-side page change
1111
+ const handleClientSidePageChange = (page: number) => {
1112
+ clientSideCurrentPage.value = page;
1113
+ emit('pageChange', page);
1114
+ };
1115
+
822
1116
  // Computed: determine if per-page selector should be shown
823
1117
  // Hide it when total items is less than the smallest page size option
824
1118
  const shouldShowPerPageSelector = computed(() => {
@@ -1046,6 +1340,13 @@ const handleFilterChange = (fieldKey: string, value: string) => {
1046
1340
  // Emit v-model update
1047
1341
  emit('update:filters', newFilters);
1048
1342
 
1343
+ // Client-side mode: filtering happens reactively via computed properties
1344
+ // No server requests needed, just emit the event
1345
+ if (isClientSideMode.value) {
1346
+ emit('filterChange', newFilters);
1347
+ return;
1348
+ }
1349
+
1049
1350
  // Debounce server requests for text inputs
1050
1351
  if (filterDebounceTimer) {
1051
1352
  clearTimeout(filterDebounceTimer);