@stacksjs/defaults 0.75.279 → 0.75.281

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 (60) hide show
  1. package/ai/skills/stacks-database/SKILL.md +1 -3
  2. package/ai/skills/stacks-native/CATALOG.md +4 -4
  3. package/ide/vscode/package.json +1 -1
  4. package/package.json +3 -3
  5. package/project/bun-version +1 -1
  6. package/resources/components/Dashboard/Analytics/CampaignAnalytics.stx +28 -30
  7. package/resources/components/Dashboard/Commerce/CommerceCategoriesDashboard.stx +8 -28
  8. package/resources/components/Dashboard/Commerce/CommerceCategoriesTable.stx +72 -29
  9. package/resources/components/Dashboard/Commerce/CommerceCouponsDashboard.stx +8 -29
  10. package/resources/components/Dashboard/Commerce/CommerceCouponsTable.stx +69 -49
  11. package/resources/components/Dashboard/Commerce/CommerceCustomersDashboard.stx +8 -27
  12. package/resources/components/Dashboard/Commerce/CommerceCustomersTable.stx +70 -48
  13. package/resources/components/Dashboard/Commerce/CommerceGiftCardsDashboard.stx +8 -29
  14. package/resources/components/Dashboard/Commerce/CommerceGiftCardsTable.stx +72 -82
  15. package/resources/components/Dashboard/Commerce/CommerceManufacturersDashboard.stx +1 -1
  16. package/resources/components/Dashboard/Commerce/CommerceManufacturersTable.stx +3 -0
  17. package/resources/components/Dashboard/Commerce/CommerceOrdersDashboard.stx +8 -28
  18. package/resources/components/Dashboard/Commerce/CommerceOrdersTable.stx +76 -49
  19. package/resources/components/Dashboard/Commerce/CommercePaymentsDashboard.stx +1 -1
  20. package/resources/components/Dashboard/Commerce/CommercePaymentsTable.stx +3 -0
  21. package/resources/components/Dashboard/Commerce/CommerceProductsDashboard.stx +8 -28
  22. package/resources/components/Dashboard/Commerce/CommerceProductsTable.stx +81 -34
  23. package/resources/components/Dashboard/Commerce/CommerceReviewsDashboard.stx +11 -37
  24. package/resources/components/Dashboard/Commerce/CommerceReviewsTable.stx +106 -77
  25. package/resources/components/Dashboard/Commerce/CommerceTaxesDashboard.stx +1 -1
  26. package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +3 -0
  27. package/resources/components/Dashboard/Commerce/CommerceUnitsDashboard.stx +1 -1
  28. package/resources/components/Dashboard/Commerce/CommerceUnitsTable.stx +3 -0
  29. package/resources/components/Dashboard/Commerce/CommerceVariantsDashboard.stx +1 -1
  30. package/resources/components/Dashboard/Commerce/CommerceVariantsTable.stx +3 -0
  31. package/resources/components/Dashboard/Commerce/Delivery/CouriersDashboard.stx +1 -11
  32. package/resources/components/Dashboard/Commerce/Delivery/CouriersTable.stx +55 -82
  33. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesDashboard.stx +1 -12
  34. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesTable.stx +57 -86
  35. package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveriesDashboard.stx +1 -24
  36. package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryTable.stx +62 -80
  37. package/resources/components/Dashboard/Commerce/Delivery/LicenseKeysDashboard.stx +1 -12
  38. package/resources/components/Dashboard/Commerce/Delivery/LicenseKeysTable.stx +58 -99
  39. package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodsDashboard.stx +1 -23
  40. package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodsTable.stx +51 -86
  41. package/resources/components/Dashboard/Commerce/Delivery/ShippingRatesDashboard.stx +1 -12
  42. package/resources/components/Dashboard/Commerce/Delivery/ShippingRatesTable.stx +58 -71
  43. package/resources/components/Dashboard/Commerce/Delivery/ShippingZonesDashboard.stx +1 -12
  44. package/resources/components/Dashboard/Commerce/Delivery/ShippingZonesTable.stx +85 -110
  45. package/resources/components/Dashboard/Commerce/PrintDevicesDashboard.stx +8 -28
  46. package/resources/components/Dashboard/Commerce/PrintDevicesTable.stx +62 -57
  47. package/resources/components/Dashboard/Marketing/AbandonedCartsDashboard.stx +7 -24
  48. package/resources/components/Dashboard/Marketing/AbandonedCartsTable.stx +63 -59
  49. package/resources/components/Dashboard/Marketing/CampaignsDashboard.stx +7 -25
  50. package/resources/components/Dashboard/Marketing/CampaignsTable.stx +80 -57
  51. package/resources/components/Dashboard/Marketing/MarketingListsDashboard.stx +7 -27
  52. package/resources/components/Dashboard/Marketing/MarketingListsTable.stx +66 -71
  53. package/resources/components/Dashboard/Marketing/SocialPostsDashboard.stx +7 -25
  54. package/resources/components/Dashboard/Marketing/SocialPostsTable.stx +75 -62
  55. package/resources/components/Dashboard/Monitoring/ErrorDashboard.stx +9 -50
  56. package/resources/components/Dashboard/Monitoring/ErrorTable.stx +68 -97
  57. package/resources/components/Dashboard/Notifications/NotificationDeliveries.stx +22 -44
  58. package/resources/components/Dashboard/Notifications/NotificationDeliveryHistory.stx +13 -4
  59. package/resources/components/Dashboard/Notifications/NotificationDeliveryTable.stx +74 -86
  60. package/resources/components/Dashboard/UI/Table.stx +41 -2
@@ -29,8 +29,6 @@ const records = state<ShippingMethodRecord[]>([])
29
29
  const loading = state(true)
30
30
  const loadError = state('')
31
31
  const search = state('')
32
- const page = state(1)
33
- const perPage = state(10)
34
32
  const editorOpen = state(false)
35
33
  const editorMode = state<'create' | 'edit'>('create')
36
34
  const editing = state<ShippingMethodRecord | null>(null)
@@ -51,11 +49,6 @@ function filteredRecords(): ShippingMethodRecord[] {
51
49
  ].some(value => String(value || '').toLowerCase().includes(query)))
52
50
  }
53
51
 
54
- function paginatedRecords(): ShippingMethodRecord[] {
55
- const start = (page() - 1) * perPage()
56
- return filteredRecords().slice(start, start + perPage())
57
- }
58
-
59
52
  function activeCount(): number {
60
53
  return records().filter(record => record.status === 'active').length
61
54
  }
@@ -80,9 +73,6 @@ async function loadMethods(): Promise<void> {
80
73
  if (!Array.isArray(data))
81
74
  throw new TypeError('Shipping methods response was not an array.')
82
75
  records.set(data)
83
- const lastPage = Math.max(1, Math.ceil(filteredRecords().length / perPage()))
84
- if (page() > lastPage)
85
- page.set(lastPage)
86
76
  }
87
77
  catch (error) {
88
78
  loadError.set(error instanceof Error ? error.message : String(error))
@@ -95,7 +85,6 @@ async function loadMethods(): Promise<void> {
95
85
 
96
86
  function handleSearch(value: string): void {
97
87
  search.set(value)
98
- page.set(1)
99
88
  }
100
89
 
101
90
  function openCreate(): void {
@@ -184,9 +173,6 @@ async function confirmDelete(): Promise<void> {
184
173
  })
185
174
  records.set(records().filter(record => record.id !== current.id))
186
175
  deleting.set(null)
187
- const lastPage = Math.max(1, Math.ceil(filteredRecords().length / perPage()))
188
- if (page() > lastPage)
189
- page.set(lastPage)
190
176
  pushToast('success', 'Shipping method deleted', { detail: current.name })
191
177
  }
192
178
  catch (error) {
@@ -197,10 +183,6 @@ async function confirmDelete(): Promise<void> {
197
183
  }
198
184
  }
199
185
 
200
- function goToPage(value: number): void {
201
- page.set(value)
202
- }
203
-
204
186
  onMount(() => {
205
187
  void loadMethods()
206
188
  })
@@ -225,11 +207,7 @@ onMount(() => {
225
207
  <div><h2 class="font-semibold text-gray-900 dark:text-white">Configured methods</h2><p class="mt-0.5 text-gray-500 text-xs dark:text-neutral-400">{{ filteredRecords().length }} matching records</p></div>
226
208
  <SearchFilter placeholder="Search shipping methods" class="w-full sm:w-80" @search="handleSearch" />
227
209
  </div>
228
- <div :if="loading()" class="flex items-center justify-center py-24 text-gray-500 text-sm dark:text-neutral-400"><span class="mr-2 h-4 w-4 animate-spin i-hugeicons-loading-03"></span>Loading shipping methods</div>
229
- <ShippingMethodsTable :if="!loading()" :methods="paginatedRecords()" @edit="openEdit" @delete="requestDelete" />
230
- <div :if="!loading()" class="px-4 py-3 bg-gray-50 dark:bg-neutral-900/50 border-gray-200 border-t dark:border-neutral-700">
231
- <DeliveryPagination :current-page="page()" :total-items="filteredRecords().length" :items-per-page="perPage()" @select="goToPage" />
232
- </div>
210
+ <ShippingMethodsTable :methods="filteredRecords()" :loading="loading()" :page-reset-key="search()" @edit="openEdit" @delete="requestDelete" />
233
211
  </section>
234
212
  </div>
235
213
 
@@ -1,5 +1,6 @@
1
+ @import('../../UI/Table')
2
+
1
3
  <script client>
2
- import { formatCurrency as formatMinorUnits } from '@stacksjs/commerce/money'
3
4
  interface ShippingZoneSummary {
4
5
  id: number
5
6
  name: string
@@ -16,96 +17,60 @@ interface ShippingMethodRow {
16
17
  }
17
18
 
18
19
  const methods = useReactiveProp('methods', [] as ShippingMethodRow[])
20
+ const loading = useReactiveProp('loading', false)
21
+ /** The parent's search: a change returns to the first page. */
22
+ const pageResetKey = useReactiveProp<unknown>('pageResetKey', '')
19
23
  const emit = defineEmits()
20
24
 
21
- // Shipping amounts are stored as integer cents, in USD.
22
- function formatCurrency(cents?: number | null): string {
23
- if (cents === null || cents === undefined)
24
- return 'Not available'
25
+ /** Shipping amounts are stored as integer cents, in USD: currency columns fixed to it. */
26
+ const COLUMNS = [
27
+ { key: 'name', label: 'Name', sortable: true, class: 'font-medium text-stone-800 whitespace-nowrap dark:text-white' },
28
+ { key: 'description', label: 'Description', emptyText: 'No description', class: 'max-w-xs truncate' },
29
+ { key: 'base_rate', label: 'Base rate', type: 'currency' as const, currency: 'USD', sortable: true, emptyText: 'Not available', class: 'tabular-nums whitespace-nowrap' },
30
+ { key: 'free_shipping', label: 'Free shipping', type: 'currency' as const, currency: 'USD', sortable: true, emptyText: 'No minimum', class: 'tabular-nums whitespace-nowrap' },
31
+ { key: 'zoneCount', label: 'Zones', type: 'number' as const, sortable: true, class: 'tabular-nums' },
32
+ {
33
+ key: 'status',
34
+ label: 'Status',
35
+ type: 'badge' as const,
36
+ sortable: true,
37
+ badgeClasses: {
38
+ active: 'bg-emerald-50 text-emerald-700 dark:bg-emerald-400/10 dark:text-emerald-300',
39
+ draft: 'bg-amber-50 text-amber-700 dark:bg-amber-400/10 dark:text-amber-300',
40
+ },
41
+ },
42
+ ]
25
43
 
26
- return formatMinorUnits(cents, 'USD', 'en-US')
27
- }
44
+ const ROW_ACTIONS = [
45
+ { action: 'edit', label: 'Edit', icon: 'i-hugeicons-edit-01' },
46
+ { action: 'delete', label: 'Delete', icon: 'i-hugeicons-waste', class: 'text-red-500' },
47
+ ]
28
48
 
29
- function statusClass(status: string): string {
30
- if (status === 'active')
31
- return 'bg-emerald-50 text-emerald-700 ring-emerald-600/20 dark:bg-emerald-400/10 dark:text-emerald-300 dark:ring-emerald-400/20'
32
- if (status === 'draft')
33
- return 'bg-amber-50 text-amber-700 ring-amber-600/20 dark:bg-amber-400/10 dark:text-amber-300 dark:ring-amber-400/20'
49
+ const rows = derived(() => methods().map(method => ({
50
+ ...method,
51
+ zoneCount: method.shipping_zones?.length || 0,
52
+ })))
34
53
 
35
- return 'bg-stone-100 text-stone-600 ring-stone-500/20 dark:bg-white/5 dark:text-neutral-300 dark:ring-white/10'
36
- }
37
-
38
- function formatStatus(status: string): string {
39
- return status.charAt(0).toUpperCase() + status.slice(1)
54
+ function handleRowAction(payload: { action: string, row: ShippingMethodRow }): void {
55
+ emit(payload.action, payload.row)
40
56
  }
41
57
  </script>
42
58
 
43
- <div class="overflow-x-auto border border-stone-200/80 rounded-xl dark:border-neutral-700 shadow-sm">
44
- <table class="min-w-full divide-stone-100 divide-y dark:divide-neutral-700">
45
- <thead class="bg-stone-50/80 dark:bg-neutral-800">
46
- <tr>
47
- <th scope="col" class="pl-6 pr-3 py-3.5 font-semibold text-left text-stone-500 text-xs tracking-wider uppercase dark:text-neutral-400">Name</th>
48
- <th scope="col" class="px-4 py-3.5 font-semibold text-left text-stone-500 text-xs tracking-wider uppercase dark:text-neutral-400">Description</th>
49
- <th scope="col" class="px-4 py-3.5 font-semibold text-left text-stone-500 text-xs tracking-wider uppercase dark:text-neutral-400">Base Rate</th>
50
- <th scope="col" class="px-4 py-3.5 font-semibold text-left text-stone-500 text-xs tracking-wider uppercase dark:text-neutral-400">Free Shipping</th>
51
- <th scope="col" class="px-4 py-3.5 font-semibold text-left text-stone-500 text-xs tracking-wider uppercase dark:text-neutral-400">Zones</th>
52
- <th scope="col" class="px-4 py-3.5 font-semibold text-left text-stone-500 text-xs tracking-wider uppercase dark:text-neutral-400">Status</th>
53
- <th scope="col" class="relative pl-3 pr-6 py-3.5">
54
- <span class="sr-only">Actions</span>
55
- </th>
56
- </tr>
57
- </thead>
58
- <tbody class="bg-white dark:bg-neutral-800 divide-stone-100 divide-y dark:divide-neutral-700">
59
- <template :for="method in methods">
60
- <tr class="hover:bg-stone-50/50 dark:hover:bg-neutral-700/50 transition-colors">
61
- <td class="pl-6 pr-3 py-3.5 font-medium text-sm text-stone-800 whitespace-nowrap dark:text-white">
62
- {{ method.name }}
63
- </td>
64
- <td class="px-4 py-3.5 max-w-xs text-sm text-stone-500 truncate dark:text-neutral-300">
65
- {{ method.description || 'No description' }}
66
- </td>
67
- <td class="px-4 py-3.5 tabular-nums text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
68
- {{ formatCurrency(method.base_rate) }}
69
- </td>
70
- <td class="px-4 py-3.5 tabular-nums text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
71
- {{ method.free_shipping === null || method.free_shipping === undefined ? 'No minimum' : formatCurrency(method.free_shipping) }}
72
- </td>
73
- <td class="px-4 py-3.5 tabular-nums text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
74
- {{ method.shipping_zones?.length || 0 }}
75
- </td>
76
- <td class="px-4 py-3.5 text-sm whitespace-nowrap">
77
- <span :class="'inline-flex px-2.5 py-1 font-medium text-xs ring-1 ring-inset rounded-full ' + statusClass(method.status)">
78
- {{ formatStatus(method.status) }}
79
- </span>
80
- </td>
81
- <td class="relative pl-3 pr-6 py-3.5 font-medium text-right text-sm whitespace-nowrap">
82
- <Button
83
- variant="ghost"
84
- size="sm"
85
- iconOnly
86
- @click="emit('edit', method)"
87
- :ariaLabel="'Edit ' + method.name"
88
- >
89
- <span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-01"></span>
90
- </Button>
91
- <Button
92
- variant="ghost"
93
- size="sm"
94
- iconOnly
95
- @click="emit('delete', method)"
96
- :ariaLabel="'Delete ' + method.name"
97
- >
98
- <span class="block h-4 w-4 text-red-500 i-hugeicons-waste"></span>
99
- </Button>
100
- </td>
101
- </tr>
102
- </template>
103
-
104
- <tr :if="methods().length === 0">
105
- <td colspan="7" class="px-6 py-12 text-center text-sm text-stone-500 dark:text-neutral-400">
106
- No shipping methods found.
107
- </td>
108
- </tr>
109
- </tbody>
110
- </table>
111
- </div>
59
+ {{--
60
+ No `searchable` or `filterable`: the parent dashboard owns the search box.
61
+ Table pages the whole filtered list the parent passes, and stays mounted
62
+ while it loads.
63
+ --}}
64
+ <Table
65
+ :page-reset-key="pageResetKey()"
66
+ :columns="COLUMNS"
67
+ :data="rows()"
68
+ :loading="loading()"
69
+ :row-actions="ROW_ACTIONS"
70
+ action-label-key="name"
71
+ paginated
72
+ :page-size="10"
73
+ empty-title="No shipping methods found"
74
+ empty-description="Add a method, or adjust the search."
75
+ @row-action="handleRowAction"
76
+ />
@@ -44,8 +44,6 @@ const {
44
44
  loading,
45
45
  loadError,
46
46
  search,
47
- page,
48
- perPage,
49
47
  editorOpen,
50
48
  editorMode,
51
49
  editing,
@@ -55,8 +53,6 @@ const {
55
53
  deleteBusy,
56
54
  deleteError,
57
55
  filteredRecords,
58
- paginatedRecords,
59
- goToPage,
60
56
  handleSearch,
61
57
  openCreate,
62
58
  openEdit,
@@ -65,7 +61,6 @@ const {
65
61
  closeDelete,
66
62
  upsert,
67
63
  remove,
68
- clampPage,
69
64
  } = useRecordsDashboard<ShippingRateRecord>({ filter: matchesQuery })
70
65
 
71
66
  const methods = state<ShippingResourceOption[]>([])
@@ -122,7 +117,6 @@ async function loadRates(): Promise<void> {
122
117
  methods.set(methodData)
123
118
  zones.set(zoneData)
124
119
  records.set(rateData.map(withRelations))
125
- clampPage()
126
120
  }
127
121
  catch (error) {
128
122
  loadError.set(error instanceof Error ? error.message : String(error))
@@ -179,7 +173,6 @@ async function confirmDelete(): Promise<void> {
179
173
  })
180
174
  remove(current)
181
175
  deleting.set(null)
182
- clampPage()
183
176
  pushToast('success', 'Shipping rate deleted', {
184
177
  detail: `${current.shipping_method?.name || 'Method'} for ${current.shipping_zone?.name || 'zone'}`,
185
178
  })
@@ -217,11 +210,7 @@ onMount(() => {
217
210
  <div><h2 class="font-semibold text-gray-900 dark:text-white">Configured weight bands</h2><p class="mt-0.5 text-gray-500 text-xs dark:text-neutral-400">{{ filteredRecords().length }} matching records</p></div>
218
211
  <SearchFilter placeholder="Search shipping rates" class="w-full sm:w-80" @search="handleSearch" />
219
212
  </div>
220
- <div :if="loading()" class="flex items-center justify-center py-24 text-gray-500 text-sm dark:text-neutral-400"><span class="mr-2 h-4 w-4 animate-spin i-hugeicons-loading-03"></span>Loading shipping rates</div>
221
- <ShippingRatesTable :if="!loading()" :rates="paginatedRecords()" @edit="openEdit" @delete="requestDelete" />
222
- <div :if="!loading()" class="px-4 py-3 bg-gray-50 dark:bg-neutral-900/50 border-gray-200 border-t dark:border-neutral-700">
223
- <DeliveryPagination :current-page="page()" :total-items="filteredRecords().length" :items-per-page="perPage()" @select="goToPage" />
224
- </div>
213
+ <ShippingRatesTable :rates="filteredRecords()" :loading="loading()" :page-reset-key="search()" @edit="openEdit" @delete="requestDelete" />
225
214
  </section>
226
215
  </div>
227
216
 
@@ -1,5 +1,6 @@
1
+ @import('../../UI/Table')
2
+
1
3
  <script client>
2
- import { formatCurrency as formatMinorUnits } from '@stacksjs/commerce/money'
3
4
  interface ResourceSummary {
4
5
  id: number
5
6
  name: string
@@ -17,84 +18,70 @@ interface ShippingRateRow {
17
18
  }
18
19
 
19
20
  const rates = useReactiveProp('rates', [] as ShippingRateRow[])
21
+ const loading = useReactiveProp('loading', false)
22
+ /** The parent's search: a change returns to the first page. */
23
+ const pageResetKey = useReactiveProp<unknown>('pageResetKey', '')
20
24
  const emit = defineEmits()
21
25
 
22
- // Shipping amounts are stored as integer cents, in USD.
23
- function formatCurrency(cents: number): string {
24
- return formatMinorUnits(cents, 'USD', 'en-US')
25
- }
26
-
27
26
  function formatWeight(weight: number): string {
28
27
  return `${new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 }).format(weight)} kg`
29
28
  }
30
29
 
31
- function actionLabel(action: string, rate: ShippingRateRow): string {
30
+ /**
31
+ * Shipping amounts are stored as integer cents, in USD, so the rate is a
32
+ * currency column with that code. Weights carry their unit and sort by the number.
33
+ */
34
+ const COLUMNS = [
35
+ { key: 'methodLabel', label: 'Method', sortable: true, class: 'font-medium text-stone-800 whitespace-nowrap dark:text-white' },
36
+ { key: 'zoneLabel', label: 'Zone', sortable: true, class: 'whitespace-nowrap' },
37
+ { key: 'weight_from', label: 'From', sortable: true, slotted: true },
38
+ { key: 'weight_to', label: 'To', sortable: true, slotted: true },
39
+ { key: 'rate', label: 'Rate', type: 'currency' as const, currency: 'USD', sortable: true, class: 'font-medium tabular-nums text-stone-800 whitespace-nowrap dark:text-white' },
40
+ ]
41
+
42
+ const ROW_ACTIONS = [
43
+ { action: 'edit', label: 'Edit', icon: 'i-hugeicons-edit-01' },
44
+ { action: 'delete', label: 'Delete', icon: 'i-hugeicons-waste', class: 'text-red-500' },
45
+ ]
46
+
47
+ const rows = derived(() => rates().map((rate) => {
32
48
  const method = rate.shipping_method?.name || `method ${rate.shipping_method_id}`
33
49
  const zone = rate.shipping_zone?.name || `zone ${rate.shipping_zone_id}`
34
- return `${action} ${method} rate for ${zone}, ${formatWeight(rate.weight_from)} to ${formatWeight(rate.weight_to)}`
50
+ return {
51
+ ...rate,
52
+ methodLabel: rate.shipping_method?.name || `Method #${rate.shipping_method_id}`,
53
+ zoneLabel: rate.shipping_zone?.name || `Zone #${rate.shipping_zone_id}`,
54
+ fromLabel: formatWeight(rate.weight_from),
55
+ toLabel: formatWeight(rate.weight_to),
56
+ actionName: `${method} rate for ${zone}, ${formatWeight(rate.weight_from)} to ${formatWeight(rate.weight_to)}`,
57
+ }
58
+ }))
59
+
60
+ function handleRowAction(payload: { action: string, row: ShippingRateRow }): void {
61
+ emit(payload.action, payload.row)
35
62
  }
36
63
  </script>
37
64
 
38
- <div class="overflow-x-auto border border-stone-200/80 rounded-xl dark:border-neutral-700 shadow-sm">
39
- <table class="min-w-full divide-stone-100 divide-y dark:divide-neutral-700">
40
- <thead class="bg-stone-50/80 dark:bg-neutral-800">
41
- <tr>
42
- <th scope="col" class="pl-6 pr-3 py-3.5 font-semibold text-left text-stone-500 text-xs tracking-wider uppercase dark:text-neutral-400">Method</th>
43
- <th scope="col" class="px-4 py-3.5 font-semibold text-left text-stone-500 text-xs tracking-wider uppercase dark:text-neutral-400">Zone</th>
44
- <th scope="col" class="px-4 py-3.5 font-semibold text-left text-stone-500 text-xs tracking-wider uppercase dark:text-neutral-400">Weight From</th>
45
- <th scope="col" class="px-4 py-3.5 font-semibold text-left text-stone-500 text-xs tracking-wider uppercase dark:text-neutral-400">Weight To</th>
46
- <th scope="col" class="px-4 py-3.5 font-semibold text-left text-stone-500 text-xs tracking-wider uppercase dark:text-neutral-400">Rate</th>
47
- <th scope="col" class="relative pl-3 pr-6 py-3.5">
48
- <span class="sr-only">Actions</span>
49
- </th>
50
- </tr>
51
- </thead>
52
- <tbody class="bg-white dark:bg-neutral-800 divide-stone-100 divide-y dark:divide-neutral-700">
53
- <template :for="rate in rates" :key="rate.id">
54
- <tr class="hover:bg-stone-50/50 dark:hover:bg-neutral-700/50 transition-colors">
55
- <td class="pl-6 pr-3 py-3.5 font-medium text-sm text-stone-800 whitespace-nowrap dark:text-white">
56
- {{ rate.shipping_method?.name || 'Method #' + rate.shipping_method_id }}
57
- </td>
58
- <td class="px-4 py-3.5 text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
59
- {{ rate.shipping_zone?.name || 'Zone #' + rate.shipping_zone_id }}
60
- </td>
61
- <td class="px-4 py-3.5 tabular-nums text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
62
- {{ formatWeight(rate.weight_from) }}
63
- </td>
64
- <td class="px-4 py-3.5 tabular-nums text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
65
- {{ formatWeight(rate.weight_to) }}
66
- </td>
67
- <td class="px-4 py-3.5 font-medium tabular-nums text-sm text-stone-800 whitespace-nowrap dark:text-white">
68
- {{ formatCurrency(rate.rate) }}
69
- </td>
70
- <td class="relative pl-3 pr-6 py-3.5 font-medium text-right text-sm whitespace-nowrap">
71
- <Button
72
- variant="ghost"
73
- size="sm"
74
- iconOnly
75
- @click="emit('edit', rate)"
76
- :ariaLabel="actionLabel('Edit', rate)"
77
- >
78
- <span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-01"></span>
79
- </Button>
80
- <Button
81
- variant="ghost"
82
- size="sm"
83
- iconOnly
84
- @click="emit('delete', rate)"
85
- :ariaLabel="actionLabel('Delete', rate)"
86
- >
87
- <span class="block h-4 w-4 text-red-500 i-hugeicons-waste"></span>
88
- </Button>
89
- </td>
90
- </tr>
91
- </template>
92
-
93
- <tr :if="rates().length === 0">
94
- <td colspan="6" class="px-6 py-12 text-center text-sm text-stone-500 dark:text-neutral-400">
95
- No shipping rates found.
96
- </td>
97
- </tr>
98
- </tbody>
99
- </table>
100
- </div>
65
+ {{--
66
+ No `searchable` or `filterable`: the parent dashboard owns the search box.
67
+ Table pages the whole filtered list the parent passes, and stays mounted
68
+ while it loads.
69
+ --}}
70
+ <Table
71
+ :page-reset-key="pageResetKey()"
72
+ :columns="COLUMNS"
73
+ :data="rows()"
74
+ :loading="loading()"
75
+ :row-actions="ROW_ACTIONS"
76
+ action-label-key="actionName"
77
+ paginated
78
+ :page-size="10"
79
+ empty-title="No shipping rates found"
80
+ empty-description="Add a rate, or adjust the search."
81
+ @row-action="handleRowAction"
82
+ >
83
+ <template #cell="{ row, column }">
84
+ <span :if="column.key === 'weight_from'" class="tabular-nums whitespace-nowrap" x-text="row.fromLabel"></span>
85
+ <span :if="column.key === 'weight_to'" class="tabular-nums whitespace-nowrap" x-text="row.toLabel"></span>
86
+ </template>
87
+ </Table>
@@ -47,8 +47,6 @@ const {
47
47
  loading,
48
48
  loadError,
49
49
  search,
50
- page,
51
- perPage,
52
50
  editorOpen,
53
51
  editorMode,
54
52
  editing,
@@ -58,8 +56,6 @@ const {
58
56
  deleteBusy,
59
57
  deleteError,
60
58
  filteredRecords,
61
- paginatedRecords,
62
- goToPage,
63
59
  handleSearch,
64
60
  openCreate,
65
61
  openEdit,
@@ -68,7 +64,6 @@ const {
68
64
  closeDelete,
69
65
  upsert,
70
66
  remove,
71
- clampPage,
72
67
  } = useRecordsDashboard<ShippingZoneRecord>({ filter: matchesQuery })
73
68
 
74
69
  const methods = state<ShippingMethodOption[]>([])
@@ -130,7 +125,6 @@ async function loadZones(): Promise<void> {
130
125
  throw new TypeError('Shipping zone resources were not returned as arrays.')
131
126
  methods.set(methodData)
132
127
  records.set(zoneData.map(withMethod))
133
- clampPage()
134
128
  }
135
129
  catch (error) {
136
130
  loadError.set(error instanceof Error ? error.message : String(error))
@@ -187,7 +181,6 @@ async function confirmDelete(): Promise<void> {
187
181
  })
188
182
  remove(current)
189
183
  deleting.set(null)
190
- clampPage()
191
184
  pushToast('success', 'Shipping zone deleted', { detail: current.name })
192
185
  }
193
186
  catch (error) {
@@ -223,11 +216,7 @@ onMount(() => {
223
216
  <div><h2 class="font-semibold text-gray-900 dark:text-white">Configured coverage</h2><p class="mt-0.5 text-gray-500 text-xs dark:text-neutral-400">{{ filteredRecords().length }} matching records</p></div>
224
217
  <SearchFilter placeholder="Search shipping zones" class="w-full sm:w-80" @search="handleSearch" />
225
218
  </div>
226
- <div :if="loading()" class="flex items-center justify-center py-24 text-gray-500 text-sm dark:text-neutral-400"><span class="mr-2 h-4 w-4 animate-spin i-hugeicons-loading-03"></span>Loading shipping zones</div>
227
- <ShippingZonesTable :if="!loading()" :zones="paginatedRecords()" @edit="openEdit" @delete="requestDelete" />
228
- <div :if="!loading()" class="px-4 py-3 bg-gray-50 dark:bg-neutral-900/50 border-gray-200 border-t dark:border-neutral-700">
229
- <DeliveryPagination :current-page="page()" :total-items="filteredRecords().length" :items-per-page="perPage()" @select="goToPage" />
230
- </div>
219
+ <ShippingZonesTable :zones="filteredRecords()" :loading="loading()" :page-reset-key="search()" @edit="openEdit" @delete="requestDelete" />
231
220
  </section>
232
221
  </div>
233
222