@stacksjs/defaults 0.75.233 → 0.75.236

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 (53) hide show
  1. package/ai/skills/stacks-dashboard/SKILL.md +2 -2
  2. package/ai/skills/stacks-database/SKILL.md +1 -1
  3. package/ai/skills/stacks-deploy/SKILL.md +3 -2
  4. package/ai/skills/stacks-migrations/SKILL.md +2 -2
  5. package/ai/skills/stacks-native/CATALOG.md +8 -8
  6. package/ai/skills/stacks-realtime/SKILL.md +4 -2
  7. package/app/Actions/Monitoring/error-provider.ts +129 -12
  8. package/app/Models/Error.ts +21 -0
  9. package/ide/vscode/package.json +1 -1
  10. package/package.json +2 -2
  11. package/resources/components/Dashboard/Auth/AuthCard.stx +29 -0
  12. package/resources/components/Dashboard/Auth/ForgotPassword.stx +65 -79
  13. package/resources/components/Dashboard/Auth/Login.stx +73 -82
  14. package/resources/components/Dashboard/Auth/Register.stx +121 -135
  15. package/resources/components/Dashboard/Auth/ResetPassword.stx +88 -102
  16. package/resources/components/Dashboard/Buddy/BuddyAssistant.stx +11 -15
  17. package/resources/components/Dashboard/Commerce/CommerceCategoriesDashboard.stx +8 -20
  18. package/resources/components/Dashboard/Commerce/CommerceCouponsDashboard.stx +8 -20
  19. package/resources/components/Dashboard/Commerce/CommerceCustomersDashboard.stx +8 -20
  20. package/resources/components/Dashboard/Commerce/CommerceGiftCardsDashboard.stx +8 -20
  21. package/resources/components/Dashboard/Commerce/CommerceManufacturersDashboard.stx +13 -24
  22. package/resources/components/Dashboard/Commerce/CommerceOrdersDashboard.stx +8 -20
  23. package/resources/components/Dashboard/Commerce/CommerceOverviewDashboard.stx +17 -21
  24. package/resources/components/Dashboard/Commerce/CommercePaymentsDashboard.stx +7 -19
  25. package/resources/components/Dashboard/Commerce/CommercePosDashboard.stx +4 -16
  26. package/resources/components/Dashboard/Commerce/CommerceProductsDashboard.stx +8 -20
  27. package/resources/components/Dashboard/Commerce/CommerceReviewsDashboard.stx +7 -18
  28. package/resources/components/Dashboard/Commerce/CommerceTaxesDashboard.stx +13 -24
  29. package/resources/components/Dashboard/Commerce/CommerceUnitsDashboard.stx +13 -24
  30. package/resources/components/Dashboard/Commerce/CommerceVariantsDashboard.stx +7 -18
  31. package/resources/components/Dashboard/Commerce/Delivery/CouriersDashboard.stx +7 -13
  32. package/resources/components/Dashboard/Commerce/Delivery/DeliveryOverviewDashboard.stx +12 -16
  33. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesDashboard.stx +7 -13
  34. package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveriesDashboard.stx +7 -13
  35. package/resources/components/Dashboard/Commerce/Delivery/LicenseKeysDashboard.stx +7 -13
  36. package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodsDashboard.stx +7 -13
  37. package/resources/components/Dashboard/Commerce/Delivery/ShippingRatesDashboard.stx +7 -13
  38. package/resources/components/Dashboard/Commerce/Delivery/ShippingZonesDashboard.stx +7 -13
  39. package/resources/components/Dashboard/Commerce/PrintDevicesDashboard.stx +8 -20
  40. package/resources/components/Dashboard/Commerce/PrintLogsDashboard.stx +7 -19
  41. package/resources/components/Dashboard/Commerce/ProductWaitlistDashboard.stx +12 -16
  42. package/resources/components/Dashboard/Commerce/RestaurantWaitlistDashboard.stx +12 -16
  43. package/resources/components/Dashboard/Deployments/DeploymentList.stx +17 -21
  44. package/resources/components/Dashboard/Marketing/AbandonedCartsDashboard.stx +6 -10
  45. package/resources/components/Dashboard/Marketing/CampaignsDashboard.stx +6 -10
  46. package/resources/components/Dashboard/Marketing/MarketingListsDashboard.stx +9 -13
  47. package/resources/components/Dashboard/Marketing/SocialPostsDashboard.stx +6 -10
  48. package/resources/components/Dashboard/Queue/QueueDashboard.stx +12 -16
  49. package/resources/components/Dashboard/Realtime/RealtimeDashboard.stx +6 -10
  50. package/resources/components/Dashboard/Releases/ReleaseDashboard.stx +6 -10
  51. package/resources/components/Dashboard/UI/MetricCards.stx +32 -0
  52. package/resources/components/Dashboard/UI/PageHeader.stx +25 -66
  53. package/resources/components/Dashboard/UI/Table.stx +79 -6
@@ -43,121 +43,107 @@ function handleSubmit(): void {
43
43
  }
44
44
  </script>
45
45
 
46
- <div class="flex items-center justify-center px-4 py-12 min-h-screen">
47
- <div class="max-w-sm w-full">
48
- <!-- Logo/Brand -->
49
- <div class="mb-8 text-center">
50
- <div class="inline-flex items-center justify-center mb-4 h-12 w-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl shadow-blue-500/25 shadow-lg">
51
- <span class="h-7 w-7 text-white i-hugeicons-cube"></span>
46
+ <AuthCard :title="title()" :subtitle="subtitle()">
47
+
48
+ <!-- Form Card -->
49
+ <div class="p-6 bg-white dark:bg-neutral-900 border border-neutral-200/60 rounded-2xl dark:border-neutral-800 shadow-black/5 shadow-xl dark:shadow-black/20">
50
+ <div :if="checking()">
51
+ <div class="py-4 text-center">
52
+ <div class="inline-flex items-center justify-center mb-4 h-12 w-12 bg-neutral-100 dark:bg-neutral-800 rounded-full">
53
+ <div class="h-6 w-6 text-neutral-500 animate-spin i-hugeicons-loading-03"></div>
54
+ </div>
55
+ <p class="text-neutral-500 text-sm dark:text-neutral-400">Checking your link</p>
52
56
  </div>
53
- <h1 class="font-semibold text-2xl text-neutral-900 dark:text-white">
54
- {{ title() }}
55
- </h1>
56
- <p class="mt-2 text-neutral-500 text-sm dark:text-neutral-400">
57
- {{ subtitle() }}
58
- </p>
59
57
  </div>
60
58
 
61
- <!-- Form Card -->
62
- <div class="p-6 bg-white dark:bg-neutral-900 border border-neutral-200/60 rounded-2xl dark:border-neutral-800 shadow-black/5 shadow-xl dark:shadow-black/20">
63
- <div :if="checking()">
64
- <div class="py-4 text-center">
65
- <div class="inline-flex items-center justify-center mb-4 h-12 w-12 bg-neutral-100 dark:bg-neutral-800 rounded-full">
66
- <div class="h-6 w-6 text-neutral-500 animate-spin i-hugeicons-loading-03"></div>
67
- </div>
68
- <p class="text-neutral-500 text-sm dark:text-neutral-400">Checking your link</p>
59
+ <div :else-if="invalid()">
60
+ <div class="py-4 text-center">
61
+ <div class="inline-flex items-center justify-center mb-4 h-12 w-12 bg-red-100 dark:bg-red-900/30 rounded-full">
62
+ <div class="h-6 w-6 text-red-600 dark:text-red-400 i-hugeicons-alert-circle"></div>
69
63
  </div>
64
+ <h2 class="mb-2 font-medium text-lg text-neutral-900 dark:text-white">
65
+ This link no longer works
66
+ </h2>
67
+ <p class="mb-6 text-neutral-500 text-sm dark:text-neutral-400">
68
+ Reset links expire, and each one can be used only once. Ask for a fresh link and it will arrive in a moment.
69
+ </p>
70
+ <Button href="/forgot-password">
71
+ Send a new link
72
+ </Button>
70
73
  </div>
74
+ </div>
71
75
 
72
- <div :else-if="invalid()">
73
- <div class="py-4 text-center">
74
- <div class="inline-flex items-center justify-center mb-4 h-12 w-12 bg-red-100 dark:bg-red-900/30 rounded-full">
75
- <div class="h-6 w-6 text-red-600 dark:text-red-400 i-hugeicons-alert-circle"></div>
76
- </div>
77
- <h2 class="mb-2 font-medium text-lg text-neutral-900 dark:text-white">
78
- This link no longer works
79
- </h2>
80
- <p class="mb-6 text-neutral-500 text-sm dark:text-neutral-400">
81
- Reset links expire, and each one can be used only once. Ask for a fresh link and it will arrive in a moment.
82
- </p>
83
- <Button href="/forgot-password">
84
- Send a new link
85
- </Button>
76
+ <div :else-if="success()">
77
+ <div class="py-4 text-center">
78
+ <div class="inline-flex items-center justify-center mb-4 h-12 w-12 bg-green-100 dark:bg-green-900/30 rounded-full">
79
+ <div class="h-6 w-6 text-green-600 dark:text-green-400 i-hugeicons-checkmark-circle-02"></div>
86
80
  </div>
81
+ <h2 class="mb-2 font-medium text-lg text-neutral-900 dark:text-white">
82
+ Password changed
83
+ </h2>
84
+ <p class="mb-6 text-neutral-500 text-sm dark:text-neutral-400">
85
+ You can sign in with your new password now.
86
+ </p>
87
+ <Button href="/login">
88
+ Go to sign in
89
+ </Button>
87
90
  </div>
91
+ </div>
88
92
 
89
- <div :else-if="success()">
90
- <div class="py-4 text-center">
91
- <div class="inline-flex items-center justify-center mb-4 h-12 w-12 bg-green-100 dark:bg-green-900/30 rounded-full">
92
- <div class="h-6 w-6 text-green-600 dark:text-green-400 i-hugeicons-checkmark-circle-02"></div>
93
- </div>
94
- <h2 class="mb-2 font-medium text-lg text-neutral-900 dark:text-white">
95
- Password changed
96
- </h2>
97
- <p class="mb-6 text-neutral-500 text-sm dark:text-neutral-400">
98
- You can sign in with your new password now.
99
- </p>
100
- <Button href="/login">
101
- Go to sign in
102
- </Button>
93
+ <div :else>
94
+ <div :if="error()" role="alert" class="mb-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 rounded-lg dark:border-red-800">
95
+ <div class="flex gap-2 items-center">
96
+ <div class="h-4 w-4 text-red-600 dark:text-red-400 i-hugeicons-alert-circle"></div>
97
+ <p class="text-red-600 text-sm dark:text-red-400">{{ error() }}</p>
103
98
  </div>
104
99
  </div>
105
100
 
106
- <div :else>
107
- <div :if="error()" role="alert" class="mb-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 rounded-lg dark:border-red-800">
108
- <div class="flex gap-2 items-center">
109
- <div class="h-4 w-4 text-red-600 dark:text-red-400 i-hugeicons-alert-circle"></div>
110
- <p class="text-red-600 text-sm dark:text-red-400">{{ error() }}</p>
111
- </div>
101
+ <form @submit.prevent="handleSubmit">
102
+ <div class="mb-4">
103
+ <Input
104
+ type="password"
105
+ label="New password"
106
+ placeholder="At least 8 characters"
107
+ autocomplete="new-password"
108
+ v-model:value="password"
109
+ required
110
+ :disabled="isLoading()"
111
+ />
112
112
  </div>
113
113
 
114
- <form @submit.prevent="handleSubmit">
115
- <div class="mb-4">
116
- <Input
117
- type="password"
118
- label="New password"
119
- placeholder="At least 8 characters"
120
- autocomplete="new-password"
121
- v-model:value="password"
122
- required
123
- :disabled="isLoading()"
124
- />
125
- </div>
126
-
127
- <div class="mb-6">
128
- <Input
129
- type="password"
130
- label="Confirm new password"
131
- placeholder="Repeat your new password"
132
- autocomplete="new-password"
133
- v-model:value="confirmation"
134
- :error="mismatch() ? 'Both passwords must match' : ''"
135
- required
136
- :disabled="isLoading()"
137
- />
138
- </div>
139
-
140
- <Button
141
- type="submit"
142
- variant="primary"
143
- size="lg"
144
- fullWidth
145
- :loading="isLoading()"
146
- :disabled="!canSubmit()"
147
- >
148
- <span :if="isLoading()">Saving...</span>
149
- <span :if="!isLoading()">Change password</span>
150
- </Button>
151
- </form>
152
- </div>
153
- </div>
114
+ <div class="mb-6">
115
+ <Input
116
+ type="password"
117
+ label="Confirm new password"
118
+ placeholder="Repeat your new password"
119
+ autocomplete="new-password"
120
+ v-model:value="confirmation"
121
+ :error="mismatch() ? 'Both passwords must match' : ''"
122
+ required
123
+ :disabled="isLoading()"
124
+ />
125
+ </div>
154
126
 
155
- <!-- Back to Login -->
156
- <p class="mt-6 text-center text-neutral-500 text-sm dark:text-neutral-400">
157
- <a href="/login" class="inline-flex gap-1 items-center font-medium text-blue-600 dark:text-blue-400 hover:text-blue-500 dark:hover:text-blue-300">
158
- <div class="h-4 w-4 i-hugeicons-arrow-left-01"></div>
159
- Back to sign in
160
- </a>
161
- </p>
127
+ <Button
128
+ type="submit"
129
+ variant="primary"
130
+ size="lg"
131
+ fullWidth
132
+ :loading="isLoading()"
133
+ :disabled="!canSubmit()"
134
+ >
135
+ <span :if="isLoading()">Saving...</span>
136
+ <span :if="!isLoading()">Change password</span>
137
+ </Button>
138
+ </form>
139
+ </div>
162
140
  </div>
163
- </div>
141
+
142
+ <!-- Back to Login -->
143
+ <p class="mt-6 text-center text-neutral-500 text-sm dark:text-neutral-400">
144
+ <a href="/login" class="inline-flex gap-1 items-center font-medium text-blue-600 dark:text-blue-400 hover:text-blue-500 dark:hover:text-blue-300">
145
+ <div class="h-4 w-4 i-hugeicons-arrow-left-01"></div>
146
+ Back to sign in
147
+ </a>
148
+ </p>
149
+ </AuthCard>
@@ -84,21 +84,17 @@ onMount(() => {
84
84
  </script>
85
85
 
86
86
  <div class="space-y-6">
87
- <header class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
88
- <div>
89
- <h1 class="font-bold text-2xl text-gray-900 dark:text-white">Buddy</h1>
90
- <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Ask framework questions through your configured Stacks AI provider.</p>
91
- </div>
92
- <div class="flex gap-2 items-center">
93
- <span :class="'inline-flex items-center px-2.5 py-1 font-medium text-xs rounded-full ' + (chatState()?.configured ? 'text-green-700 bg-green-100 dark:text-green-300 dark:bg-green-950' : 'text-amber-700 bg-amber-100 dark:text-amber-300 dark:bg-amber-950')">
94
- <span :class="'mr-1.5 h-1.5 w-1.5 rounded-full ' + (chatState()?.configured ? 'bg-green-500' : 'bg-amber-500')"></span>
95
- {{ providerName() }} {{ chatState()?.configured ? 'ready' : 'needs configuration' }}
96
- </span>
97
- <Button :if="messages().length > 0" variant="secondary" size="sm" @click="clearHistory">
98
- Clear
99
- </Button>
100
- </div>
101
- </header>
87
+ <PageHeader title="Buddy" description="Ask framework questions through your configured Stacks AI provider.">
88
+ <div class="flex gap-2 items-center">
89
+ <span :class="'inline-flex items-center px-2.5 py-1 font-medium text-xs rounded-full ' + (chatState()?.configured ? 'text-green-700 bg-green-100 dark:text-green-300 dark:bg-green-950' : 'text-amber-700 bg-amber-100 dark:text-amber-300 dark:bg-amber-950')">
90
+ <span :class="'mr-1.5 h-1.5 w-1.5 rounded-full ' + (chatState()?.configured ? 'bg-green-500' : 'bg-amber-500')"></span>
91
+ {{ providerName() }} {{ chatState()?.configured ? 'ready' : 'needs configuration' }}
92
+ </span>
93
+ <Button :if="messages().length > 0" variant="secondary" size="sm" @click="clearHistory">
94
+ Clear
95
+ </Button>
96
+ </div>
97
+ </PageHeader>
102
98
 
103
99
  <div :if="loadError()" class="p-4 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900">
104
100
  {{ loadError() }}
@@ -239,29 +239,17 @@ onMount(() => {
239
239
  </script>
240
240
 
241
241
  <div class="space-y-6">
242
- <header class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
243
- <div>
244
- <h1 class="font-bold text-2xl text-gray-900 dark:text-white">Product Categories</h1>
245
- <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Manage the native Category hierarchy, display order, and storefront availability.</p>
246
- </div>
247
- <div class="flex flex-wrap gap-2">
248
- <Button variant="secondary" :loading="loading()" @click="loadCategories()"><span :if="!loading()" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
249
- <Button tag="a" variant="secondary" :href="exportHref()" :download="'product-categories-' + new Date().toISOString().slice(0, 10) + '.csv'"><span class="h-4 w-4 i-hugeicons-download-04"></span>Export filtered</Button>
250
- <Button @click="openAdd()"><span class="h-4 w-4 i-hugeicons-add-01"></span>Add category</Button>
251
- </div>
252
- </header>
242
+ <PageHeader title="Product Categories" description="Manage the native Category hierarchy, display order, and storefront availability.">
243
+ <div class="flex flex-wrap gap-2">
244
+ <Button variant="secondary" :loading="loading()" @click="loadCategories()"><span :if="!loading()" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
245
+ <Button tag="a" variant="secondary" :href="exportHref()" :download="'product-categories-' + new Date().toISOString().slice(0, 10) + '.csv'"><span class="h-4 w-4 i-hugeicons-download-04"></span>Export filtered</Button>
246
+ <Button @click="openAdd()"><span class="h-4 w-4 i-hugeicons-add-01"></span>Add category</Button>
247
+ </div>
248
+ </PageHeader>
253
249
 
254
250
  <div :if="loadError()" class="p-4 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900">{{ loadError() }}</div>
255
251
 
256
- <section aria-label="Product category metrics" class="grid gap-3 grid-cols-2 lg:grid-cols-4">
257
- <template :for="item in metricCards()" :key="item.label">
258
- <article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
259
- <p class="text-gray-500 text-xs dark:text-neutral-400">{{ item.label }}</p>
260
- <p class="mt-2 font-semibold text-2xl text-gray-900 dark:text-white">{{ item.value }}</p>
261
- <p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ item.detail }}</p>
262
- </article>
263
- </template>
264
- </section>
252
+ <MetricCards label="Product category metrics" columns="grid-cols-2 lg:grid-cols-4" :items="metricCards()" />
265
253
 
266
254
  <CommerceCategoryHierarchy :records="records()" />
267
255
 
@@ -250,29 +250,17 @@ onMount(() => {
250
250
  </script>
251
251
 
252
252
  <div class="space-y-6">
253
- <header class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
254
- <div>
255
- <h1 class="font-bold text-2xl text-gray-900 dark:text-white">Coupons</h1>
256
- <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Manage native Coupon records, availability, limits, and redemption state.</p>
257
- </div>
258
- <div class="flex flex-wrap gap-2">
259
- <Button variant="secondary" :loading="loading()" @click="loadCoupons()"><span :if="!loading()" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
260
- <Button tag="a" variant="secondary" :href="exportHref()" :download="'coupons-' + new Date().toISOString().slice(0, 10) + '.csv'"><span class="h-4 w-4 i-hugeicons-download-04"></span>Export filtered</Button>
261
- <Button @click="openAdd()"><span class="h-4 w-4 i-hugeicons-add-01"></span>Add coupon</Button>
262
- </div>
263
- </header>
253
+ <PageHeader title="Coupons" description="Manage native Coupon records, availability, limits, and redemption state.">
254
+ <div class="flex flex-wrap gap-2">
255
+ <Button variant="secondary" :loading="loading()" @click="loadCoupons()"><span :if="!loading()" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
256
+ <Button tag="a" variant="secondary" :href="exportHref()" :download="'coupons-' + new Date().toISOString().slice(0, 10) + '.csv'"><span class="h-4 w-4 i-hugeicons-download-04"></span>Export filtered</Button>
257
+ <Button @click="openAdd()"><span class="h-4 w-4 i-hugeicons-add-01"></span>Add coupon</Button>
258
+ </div>
259
+ </PageHeader>
264
260
 
265
261
  <div :if="loadError()" class="p-4 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900">{{ loadError() }}</div>
266
262
 
267
- <section aria-label="Coupon metrics" class="grid gap-3 grid-cols-2 lg:grid-cols-4">
268
- <template :for="item in metricCards()" :key="item.label">
269
- <article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
270
- <p class="text-gray-500 text-xs dark:text-neutral-400">{{ item.label }}</p>
271
- <p class="mt-2 font-semibold text-2xl text-gray-900 dark:text-white">{{ item.value }}</p>
272
- <p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ item.detail }}</p>
273
- </article>
274
- </template>
275
- </section>
263
+ <MetricCards label="Coupon metrics" columns="grid-cols-2 lg:grid-cols-4" :items="metricCards()" />
276
264
 
277
265
  <CommerceCouponBreakdown :records="records()" />
278
266
 
@@ -252,29 +252,17 @@ onMount(() => {
252
252
  </script>
253
253
 
254
254
  <div class="space-y-6">
255
- <header class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
256
- <div>
257
- <h1 class="font-bold text-2xl text-gray-900 dark:text-white">Customers</h1>
258
- <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Manage native Customer records, contact details, status, and recorded spend.</p>
259
- </div>
260
- <div class="flex flex-wrap gap-2">
261
- <Button variant="secondary" :loading="loading()" @click="loadCustomers()"><span :if="!loading()" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
262
- <Button tag="a" variant="secondary" :href="exportHref()" :download="'customers-' + new Date().toISOString().slice(0, 10) + '.csv'"><span class="h-4 w-4 i-hugeicons-download-04"></span>Export filtered</Button>
263
- <Button @click="openAdd()"><span class="h-4 w-4 i-hugeicons-add-01"></span>Add customer</Button>
264
- </div>
265
- </header>
255
+ <PageHeader title="Customers" description="Manage native Customer records, contact details, status, and recorded spend.">
256
+ <div class="flex flex-wrap gap-2">
257
+ <Button variant="secondary" :loading="loading()" @click="loadCustomers()"><span :if="!loading()" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
258
+ <Button tag="a" variant="secondary" :href="exportHref()" :download="'customers-' + new Date().toISOString().slice(0, 10) + '.csv'"><span class="h-4 w-4 i-hugeicons-download-04"></span>Export filtered</Button>
259
+ <Button @click="openAdd()"><span class="h-4 w-4 i-hugeicons-add-01"></span>Add customer</Button>
260
+ </div>
261
+ </PageHeader>
266
262
 
267
263
  <div :if="loadError()" class="p-4 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900">{{ loadError() }}</div>
268
264
 
269
- <section aria-label="Customer metrics" class="grid gap-3 grid-cols-2 lg:grid-cols-4">
270
- <template :for="item in metricCards()" :key="item.label">
271
- <article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
272
- <p class="text-gray-500 text-xs dark:text-neutral-400">{{ item.label }}</p>
273
- <p class="mt-2 font-semibold text-2xl text-gray-900 dark:text-white">{{ item.value }}</p>
274
- <p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ item.detail }}</p>
275
- </article>
276
- </template>
277
- </section>
265
+ <MetricCards label="Customer metrics" columns="grid-cols-2 lg:grid-cols-4" :items="metricCards()" />
278
266
 
279
267
  <section class="space-y-3">
280
268
  <div class="grid gap-3 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
@@ -266,29 +266,17 @@ onMount(() => {
266
266
  </script>
267
267
 
268
268
  <div class="space-y-6">
269
- <header class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
270
- <div>
271
- <h1 class="font-bold text-2xl text-gray-900 dark:text-white">Gift Cards</h1>
272
- <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Manage native GiftCard balances, recipients, delivery, and lifecycle state.</p>
273
- </div>
274
- <div class="flex flex-wrap gap-2">
275
- <Button variant="secondary" :loading="loading()" @click="loadGiftCards()"><span :if="!loading()" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
276
- <Button tag="a" variant="secondary" :href="exportHref()" :download="'gift-cards-' + new Date().toISOString().slice(0, 10) + '.csv'"><span class="h-4 w-4 i-hugeicons-download-04"></span>Export filtered</Button>
277
- <Button @click="openAdd()"><span class="h-4 w-4 i-hugeicons-add-01"></span>Add gift card</Button>
278
- </div>
279
- </header>
269
+ <PageHeader title="Gift Cards" description="Manage native GiftCard balances, recipients, delivery, and lifecycle state.">
270
+ <div class="flex flex-wrap gap-2">
271
+ <Button variant="secondary" :loading="loading()" @click="loadGiftCards()"><span :if="!loading()" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
272
+ <Button tag="a" variant="secondary" :href="exportHref()" :download="'gift-cards-' + new Date().toISOString().slice(0, 10) + '.csv'"><span class="h-4 w-4 i-hugeicons-download-04"></span>Export filtered</Button>
273
+ <Button @click="openAdd()"><span class="h-4 w-4 i-hugeicons-add-01"></span>Add gift card</Button>
274
+ </div>
275
+ </PageHeader>
280
276
 
281
277
  <div :if="loadError()" class="p-4 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900">{{ loadError() }}</div>
282
278
 
283
- <section aria-label="Gift card metrics" class="grid gap-3 grid-cols-2 lg:grid-cols-4">
284
- <template :for="item in metricCards()" :key="item.label">
285
- <article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
286
- <p class="text-gray-500 text-xs dark:text-neutral-400">{{ item.label }}</p>
287
- <p class="mt-2 font-semibold text-2xl text-gray-900 dark:text-white">{{ item.value }}</p>
288
- <p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ item.detail }}</p>
289
- </article>
290
- </template>
291
- </section>
279
+ <MetricCards label="Gift card metrics" columns="grid-cols-2 lg:grid-cols-4" :items="metricCards()" />
292
280
 
293
281
  <CommerceGiftCardBreakdown :summary="summary()" />
294
282
 
@@ -175,33 +175,22 @@ onMount(() => {
175
175
  </script>
176
176
 
177
177
  <div class="space-y-6">
178
- <header class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
179
- <div>
180
- <h1 class="font-bold text-2xl text-gray-900 dark:text-white">Manufacturers</h1>
181
- <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Manage native Manufacturer records and their product relationships.</p>
182
- </div>
183
- <div class="flex gap-2">
184
- <Button variant="secondary" :loading="loading()" @click="loadManufacturers">
185
- <span :if="!loading()" class="h-4 w-4 i-hugeicons-refresh"></span>
186
- Refresh
187
- </Button>
188
- <Button @click="openAdd()">
189
- <span class="h-4 w-4 i-hugeicons-add-01"></span>
190
- Add manufacturer
191
- </Button>
192
- </div>
193
- </header>
178
+ <PageHeader title="Manufacturers" description="Manage native Manufacturer records and their product relationships.">
179
+ <div class="flex gap-2">
180
+ <Button variant="secondary" :loading="loading()" @click="loadManufacturers">
181
+ <span :if="!loading()" class="h-4 w-4 i-hugeicons-refresh"></span>
182
+ Refresh
183
+ </Button>
184
+ <Button @click="openAdd()">
185
+ <span class="h-4 w-4 i-hugeicons-add-01"></span>
186
+ Add manufacturer
187
+ </Button>
188
+ </div>
189
+ </PageHeader>
194
190
 
195
191
  <div :if="loadError()" class="p-4 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900">{{ loadError() }}</div>
196
192
 
197
- <section aria-label="Manufacturer metrics" class="grid gap-3 grid-cols-2 lg:grid-cols-4">
198
- <template :for="item in metricCards()" :key="item.label">
199
- <article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
200
- <p class="text-gray-500 text-xs dark:text-neutral-400">{{ item.label }}</p>
201
- <p class="mt-2 font-semibold text-2xl text-gray-900 dark:text-white">{{ item.value }}</p>
202
- </article>
203
- </template>
204
- </section>
193
+ <MetricCards label="Manufacturer metrics" columns="grid-cols-2 lg:grid-cols-4" :items="metricCards()" />
205
194
 
206
195
  <section class="space-y-3">
207
196
  <div class="grid gap-3 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
@@ -293,29 +293,17 @@ onMount(() => {
293
293
  </script>
294
294
 
295
295
  <div class="space-y-6">
296
- <header class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
297
- <div>
298
- <h1 class="font-bold text-2xl text-gray-900 dark:text-white">Orders</h1>
299
- <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Manage native Order records, customer assignments, totals, and fulfillment state.</p>
300
- </div>
301
- <div class="flex flex-wrap gap-2">
302
- <Button variant="secondary" :loading="loading()" @click="loadOrders()"><span :if="!loading()" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
303
- <Button tag="a" variant="secondary" :href="exportHref()" :download="'orders-' + new Date().toISOString().slice(0, 10) + '.csv'"><span class="h-4 w-4 i-hugeicons-download-04"></span>Export filtered</Button>
304
- <Button @click="openAdd()"><span class="h-4 w-4 i-hugeicons-add-01"></span>Add order</Button>
305
- </div>
306
- </header>
296
+ <PageHeader title="Orders" description="Manage native Order records, customer assignments, totals, and fulfillment state.">
297
+ <div class="flex flex-wrap gap-2">
298
+ <Button variant="secondary" :loading="loading()" @click="loadOrders()"><span :if="!loading()" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
299
+ <Button tag="a" variant="secondary" :href="exportHref()" :download="'orders-' + new Date().toISOString().slice(0, 10) + '.csv'"><span class="h-4 w-4 i-hugeicons-download-04"></span>Export filtered</Button>
300
+ <Button @click="openAdd()"><span class="h-4 w-4 i-hugeicons-add-01"></span>Add order</Button>
301
+ </div>
302
+ </PageHeader>
307
303
 
308
304
  <div :if="loadError()" class="p-4 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900">{{ loadError() }}</div>
309
305
 
310
- <section aria-label="Order metrics" class="grid gap-3 grid-cols-2 lg:grid-cols-5">
311
- <template :for="item in metricCards()" :key="item.label">
312
- <article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
313
- <p class="text-gray-500 text-xs dark:text-neutral-400">{{ item.label }}</p>
314
- <p class="mt-2 font-semibold text-2xl text-gray-900 dark:text-white">{{ item.value }}</p>
315
- <p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ item.detail }}</p>
316
- </article>
317
- </template>
318
- </section>
306
+ <MetricCards label="Order metrics" columns="grid-cols-2 lg:grid-cols-5" :items="metricCards()" />
319
307
 
320
308
  <section :if="summary().currencyTotals.length > 0" aria-label="Order currency totals" class="flex flex-wrap gap-2 items-center">
321
309
  <span class="text-gray-500 text-xs dark:text-neutral-400">Recorded order value</span>
@@ -62,27 +62,23 @@ onMount(() => {
62
62
  </script>
63
63
 
64
64
  <div class="space-y-8">
65
- <header class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
66
- <div>
67
- <h1 class="font-bold text-2xl text-gray-900 dark:text-white">Commerce Dashboard</h1>
68
- <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Persisted order performance for {{ rangeLabel().toLowerCase() }}.</p>
69
- </div>
70
- <div class="flex gap-2">
71
- <Button variant="secondary" :loading="loading()" @click="loadDashboard">
72
- <span :if="!loading()" class="h-4 w-4 i-hugeicons-refresh"></span>
73
- Refresh
74
- </Button>
75
- <label class="sr-only" for="commerce-dashboard-range">Reporting range</label>
76
- <select id="commerce-dashboard-range" x-model="range" class="block pl-3 pr-10 py-2 text-gray-900 sm:leading-6 sm:text-sm dark:text-white dark:bg-neutral-800 border-0 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-700 focus:ring-2 focus:ring-blue-600" @change="reloadSelectedRange($event)">
77
- <option value="today">Today</option>
78
- <option value="7d">Last 7 days</option>
79
- <option value="30d">Last 30 days</option>
80
- <option value="90d">Last 90 days</option>
81
- <option value="year">Last year</option>
82
- <option value="all">All time</option>
83
- </select>
84
- </div>
85
- </header>
65
+ <PageHeader title="Commerce Dashboard" :description="`Persisted order performance for ${rangeLabel().toLowerCase()}.`">
66
+ <div class="flex gap-2">
67
+ <Button variant="secondary" :loading="loading()" @click="loadDashboard">
68
+ <span :if="!loading()" class="h-4 w-4 i-hugeicons-refresh"></span>
69
+ Refresh
70
+ </Button>
71
+ <label class="sr-only" for="commerce-dashboard-range">Reporting range</label>
72
+ <select id="commerce-dashboard-range" x-model="range" class="block pl-3 pr-10 py-2 text-gray-900 sm:leading-6 sm:text-sm dark:text-white dark:bg-neutral-800 border-0 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-700 focus:ring-2 focus:ring-blue-600" @change="reloadSelectedRange($event)">
73
+ <option value="today">Today</option>
74
+ <option value="7d">Last 7 days</option>
75
+ <option value="30d">Last 30 days</option>
76
+ <option value="90d">Last 90 days</option>
77
+ <option value="year">Last year</option>
78
+ <option value="all">All time</option>
79
+ </select>
80
+ </div>
81
+ </PageHeader>
86
82
 
87
83
  <div :if="loadError()" class="px-4 py-3 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/30 border border-red-300 rounded-md dark:border-red-800">
88
84
  {{ loadError() }}
@@ -170,28 +170,16 @@ onMount(() => {
170
170
  </script>
171
171
 
172
172
  <div class="space-y-6">
173
- <header class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
174
- <div>
175
- <h1 class="font-bold text-2xl text-gray-900 dark:text-white">Payments</h1>
176
- <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Inspect native Payment records, export the current view, and record confirmed refunds.</p>
177
- </div>
178
- <div class="flex flex-wrap gap-2">
179
- <Button variant="secondary" :loading="loading()" @click="loadPayments"><span :if="!loading()" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
180
- <Button tag="a" :href="exportHref()" :download="exportFilename()"><span class="h-4 w-4 i-hugeicons-download-04"></span>Export current view</Button>
181
- </div>
182
- </header>
173
+ <PageHeader title="Payments" description="Inspect native Payment records, export the current view, and record confirmed refunds.">
174
+ <div class="flex flex-wrap gap-2">
175
+ <Button variant="secondary" :loading="loading()" @click="loadPayments"><span :if="!loading()" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
176
+ <Button tag="a" :href="exportHref()" :download="exportFilename()"><span class="h-4 w-4 i-hugeicons-download-04"></span>Export current view</Button>
177
+ </div>
178
+ </PageHeader>
183
179
 
184
180
  <div :if="loadError()" class="p-4 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900">{{ loadError() }}</div>
185
181
 
186
- <section aria-label="Payment metrics" class="grid gap-3 grid-cols-2 lg:grid-cols-4">
187
- <template :for="item in metricCards()" :key="item.label">
188
- <article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
189
- <p class="text-gray-500 text-xs dark:text-neutral-400">{{ item.label }}</p>
190
- <p class="mt-2 font-semibold text-2xl text-gray-900 dark:text-white">{{ item.value }}</p>
191
- <p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ item.detail }}</p>
192
- </article>
193
- </template>
194
- </section>
182
+ <MetricCards label="Payment metrics" columns="grid-cols-2 lg:grid-cols-4" :items="metricCards()" />
195
183
 
196
184
  <CommercePaymentsBreakdown :records="visibleRecords()" />
197
185