@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.
- package/ai/skills/stacks-dashboard/SKILL.md +2 -2
- package/ai/skills/stacks-database/SKILL.md +1 -1
- package/ai/skills/stacks-deploy/SKILL.md +3 -2
- package/ai/skills/stacks-migrations/SKILL.md +2 -2
- package/ai/skills/stacks-native/CATALOG.md +8 -8
- package/ai/skills/stacks-realtime/SKILL.md +4 -2
- package/app/Actions/Monitoring/error-provider.ts +129 -12
- package/app/Models/Error.ts +21 -0
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/resources/components/Dashboard/Auth/AuthCard.stx +29 -0
- package/resources/components/Dashboard/Auth/ForgotPassword.stx +65 -79
- package/resources/components/Dashboard/Auth/Login.stx +73 -82
- package/resources/components/Dashboard/Auth/Register.stx +121 -135
- package/resources/components/Dashboard/Auth/ResetPassword.stx +88 -102
- package/resources/components/Dashboard/Buddy/BuddyAssistant.stx +11 -15
- package/resources/components/Dashboard/Commerce/CommerceCategoriesDashboard.stx +8 -20
- package/resources/components/Dashboard/Commerce/CommerceCouponsDashboard.stx +8 -20
- package/resources/components/Dashboard/Commerce/CommerceCustomersDashboard.stx +8 -20
- package/resources/components/Dashboard/Commerce/CommerceGiftCardsDashboard.stx +8 -20
- package/resources/components/Dashboard/Commerce/CommerceManufacturersDashboard.stx +13 -24
- package/resources/components/Dashboard/Commerce/CommerceOrdersDashboard.stx +8 -20
- package/resources/components/Dashboard/Commerce/CommerceOverviewDashboard.stx +17 -21
- package/resources/components/Dashboard/Commerce/CommercePaymentsDashboard.stx +7 -19
- package/resources/components/Dashboard/Commerce/CommercePosDashboard.stx +4 -16
- package/resources/components/Dashboard/Commerce/CommerceProductsDashboard.stx +8 -20
- package/resources/components/Dashboard/Commerce/CommerceReviewsDashboard.stx +7 -18
- package/resources/components/Dashboard/Commerce/CommerceTaxesDashboard.stx +13 -24
- package/resources/components/Dashboard/Commerce/CommerceUnitsDashboard.stx +13 -24
- package/resources/components/Dashboard/Commerce/CommerceVariantsDashboard.stx +7 -18
- package/resources/components/Dashboard/Commerce/Delivery/CouriersDashboard.stx +7 -13
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryOverviewDashboard.stx +12 -16
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesDashboard.stx +7 -13
- package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveriesDashboard.stx +7 -13
- package/resources/components/Dashboard/Commerce/Delivery/LicenseKeysDashboard.stx +7 -13
- package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodsDashboard.stx +7 -13
- package/resources/components/Dashboard/Commerce/Delivery/ShippingRatesDashboard.stx +7 -13
- package/resources/components/Dashboard/Commerce/Delivery/ShippingZonesDashboard.stx +7 -13
- package/resources/components/Dashboard/Commerce/PrintDevicesDashboard.stx +8 -20
- package/resources/components/Dashboard/Commerce/PrintLogsDashboard.stx +7 -19
- package/resources/components/Dashboard/Commerce/ProductWaitlistDashboard.stx +12 -16
- package/resources/components/Dashboard/Commerce/RestaurantWaitlistDashboard.stx +12 -16
- package/resources/components/Dashboard/Deployments/DeploymentList.stx +17 -21
- package/resources/components/Dashboard/Marketing/AbandonedCartsDashboard.stx +6 -10
- package/resources/components/Dashboard/Marketing/CampaignsDashboard.stx +6 -10
- package/resources/components/Dashboard/Marketing/MarketingListsDashboard.stx +9 -13
- package/resources/components/Dashboard/Marketing/SocialPostsDashboard.stx +6 -10
- package/resources/components/Dashboard/Queue/QueueDashboard.stx +12 -16
- package/resources/components/Dashboard/Realtime/RealtimeDashboard.stx +6 -10
- package/resources/components/Dashboard/Releases/ReleaseDashboard.stx +6 -10
- package/resources/components/Dashboard/UI/MetricCards.stx +32 -0
- package/resources/components/Dashboard/UI/PageHeader.stx +25 -66
- package/resources/components/Dashboard/UI/Table.stx +79 -6
|
@@ -189,28 +189,16 @@ onMount(() => {
|
|
|
189
189
|
</script>
|
|
190
190
|
|
|
191
191
|
<div class="space-y-6">
|
|
192
|
-
<
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
<Button variant="secondary" :loading="loading()" @click="loadPrintLogs"><span :if="!loading()" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
199
|
-
<Button tag="a" :href="exportHref()" :download="exportFilename()"><span class="h-4 w-4 i-hugeicons-download-04"></span>Export current page</Button>
|
|
200
|
-
</div>
|
|
201
|
-
</header>
|
|
192
|
+
<PageHeader title="Print logs" description="Inspect native Receipt records from configured print devices.">
|
|
193
|
+
<div class="flex flex-wrap gap-2">
|
|
194
|
+
<Button variant="secondary" :loading="loading()" @click="loadPrintLogs"><span :if="!loading()" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
195
|
+
<Button tag="a" :href="exportHref()" :download="exportFilename()"><span class="h-4 w-4 i-hugeicons-download-04"></span>Export current page</Button>
|
|
196
|
+
</div>
|
|
197
|
+
</PageHeader>
|
|
202
198
|
|
|
203
199
|
<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>
|
|
204
200
|
|
|
205
|
-
<
|
|
206
|
-
<template :for="item in metricCards()" :key="item.label">
|
|
207
|
-
<article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
|
|
208
|
-
<p class="text-gray-500 text-xs dark:text-neutral-400">{{ item.label }}</p>
|
|
209
|
-
<p class="mt-2 font-semibold text-2xl text-gray-900 dark:text-white">{{ item.value }}</p>
|
|
210
|
-
<p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ item.detail }}</p>
|
|
211
|
-
</article>
|
|
212
|
-
</template>
|
|
213
|
-
</section>
|
|
201
|
+
<MetricCards label="Print log metrics" columns="grid-cols-2 lg:grid-cols-4" :items="metricCards()" />
|
|
214
202
|
|
|
215
203
|
<PrintLogBreakdown :records="records()" />
|
|
216
204
|
|
|
@@ -150,22 +150,18 @@ onMount(() => {
|
|
|
150
150
|
</script>
|
|
151
151
|
|
|
152
152
|
<div class="space-y-6">
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
Add entry
|
|
166
|
-
</Button>
|
|
167
|
-
</div>
|
|
168
|
-
</header>
|
|
153
|
+
<PageHeader title="Product Waitlist" description="Native WaitlistProduct records without simulated customers or activity.">
|
|
154
|
+
<div class="flex gap-2">
|
|
155
|
+
<Button variant="secondary" :loading="isLoading()" @click="loadRecords">
|
|
156
|
+
<span :if="!isLoading()" class="h-4 w-4 i-hugeicons-refresh"></span>
|
|
157
|
+
Refresh
|
|
158
|
+
</Button>
|
|
159
|
+
<Button :disabled="isLoading() || options().products.length === 0" @click="openAddDialog()">
|
|
160
|
+
<span class="h-4 w-4 i-hugeicons-add-01"></span>
|
|
161
|
+
Add entry
|
|
162
|
+
</Button>
|
|
163
|
+
</div>
|
|
164
|
+
</PageHeader>
|
|
169
165
|
|
|
170
166
|
<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>
|
|
171
167
|
<div :if="!isLoading() && options().products.length === 0" class="p-4 text-amber-800 text-sm dark:text-amber-200 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 rounded-lg dark:border-amber-900">
|
|
@@ -207,22 +207,18 @@ onMount(() => {
|
|
|
207
207
|
</script>
|
|
208
208
|
|
|
209
209
|
<div class="space-y-6">
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
<
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
Add party
|
|
223
|
-
</Button>
|
|
224
|
-
</div>
|
|
225
|
-
</header>
|
|
210
|
+
<PageHeader title="Restaurant Waitlist" description="Manage persisted WaitlistRestaurant records and seating states.">
|
|
211
|
+
<div class="flex gap-2">
|
|
212
|
+
<Button variant="secondary" :loading="isLoading()" @click="loadRecords">
|
|
213
|
+
<span :if="!isLoading()" class="h-4 w-4 i-hugeicons-refresh"></span>
|
|
214
|
+
Refresh
|
|
215
|
+
</Button>
|
|
216
|
+
<Button :disabled="isLoading()" @click="openAddDialog()">
|
|
217
|
+
<span class="h-4 w-4 i-hugeicons-add-01"></span>
|
|
218
|
+
Add party
|
|
219
|
+
</Button>
|
|
220
|
+
</div>
|
|
221
|
+
</PageHeader>
|
|
226
222
|
|
|
227
223
|
<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>
|
|
228
224
|
|
|
@@ -200,27 +200,23 @@ onMount(() => {
|
|
|
200
200
|
</script>
|
|
201
201
|
|
|
202
202
|
<div class="space-y-6">
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
</
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
<
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
Refresh
|
|
221
|
-
</Button>
|
|
222
|
-
</div>
|
|
223
|
-
</header>
|
|
203
|
+
<PageHeader title="Deployments" description="Deployment model records through the guarded dashboard API.">
|
|
204
|
+
<div class="flex gap-2 items-center">
|
|
205
|
+
<Button :disabled="isStarting() || isPreviewing()" variant="outline" @click="openDeploymentDialog('preview')">
|
|
206
|
+
Preview
|
|
207
|
+
</Button>
|
|
208
|
+
<Button :disabled="isStarting() || isPreviewing() || isRollingBack()" variant="secondary" @click="openRollback">
|
|
209
|
+
Rollback
|
|
210
|
+
</Button>
|
|
211
|
+
<Button :disabled="isStarting() || isPreviewing()" @click="openDeploymentDialog('deploy')">
|
|
212
|
+
Deploy
|
|
213
|
+
</Button>
|
|
214
|
+
<Button :loading="isLoading()" variant="secondary" @click="loadDeployments">
|
|
215
|
+
<span :if="!isLoading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>
|
|
216
|
+
Refresh
|
|
217
|
+
</Button>
|
|
218
|
+
</div>
|
|
219
|
+
</PageHeader>
|
|
224
220
|
|
|
225
221
|
<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>
|
|
226
222
|
|
|
@@ -171,16 +171,12 @@ onMount(() => {
|
|
|
171
171
|
</script>
|
|
172
172
|
|
|
173
173
|
<div class="space-y-6">
|
|
174
|
-
<
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
<Button :loading="loading()" variant="secondary" @click="loadCarts()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
181
|
-
<Button @click="openCompose()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-mail-send-01"></span>New recovery campaign</Button>
|
|
182
|
-
</div>
|
|
183
|
-
</header>
|
|
174
|
+
<PageHeader title="Abandoned Carts" description="The audience that already chose the products. Every other campaign starts by guessing.">
|
|
175
|
+
<div class="flex gap-2">
|
|
176
|
+
<Button :loading="loading()" variant="secondary" @click="loadCarts()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
177
|
+
<Button @click="openCompose()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-mail-send-01"></span>New recovery campaign</Button>
|
|
178
|
+
</div>
|
|
179
|
+
</PageHeader>
|
|
184
180
|
|
|
185
181
|
<div :if="loadError()" role="alert" class="flex gap-4 items-center justify-between 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"><span>{{ loadError() }}</span><Button variant="secondary" size="sm" @click="loadCarts()">Retry</Button></div>
|
|
186
182
|
|
|
@@ -280,16 +280,12 @@ onMount(() => {
|
|
|
280
280
|
</script>
|
|
281
281
|
|
|
282
282
|
<div class="space-y-6">
|
|
283
|
-
<
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
<Button :loading="loading()" variant="secondary" @click="loadCampaigns()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
290
|
-
<Button @click="openAdd()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>Add campaign</Button>
|
|
291
|
-
</div>
|
|
292
|
-
</header>
|
|
283
|
+
<PageHeader title="Campaigns" description="Manage Campaign records with real audience and delivery aggregates.">
|
|
284
|
+
<div class="flex gap-2">
|
|
285
|
+
<Button :loading="loading()" variant="secondary" @click="loadCampaigns()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
286
|
+
<Button @click="openAdd()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>Add campaign</Button>
|
|
287
|
+
</div>
|
|
288
|
+
</PageHeader>
|
|
293
289
|
|
|
294
290
|
<div :if="loadError()" role="alert" class="flex gap-4 items-center justify-between 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"><span>{{ loadError() }}</span><Button variant="secondary" size="sm" @click="loadCampaigns()">Retry</Button></div>
|
|
295
291
|
|
|
@@ -202,19 +202,15 @@ onMount(() => {
|
|
|
202
202
|
</script>
|
|
203
203
|
|
|
204
204
|
<div class="space-y-6">
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
</Button>
|
|
215
|
-
<Button @click="openAdd()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>Add list</Button>
|
|
216
|
-
</div>
|
|
217
|
-
</header>
|
|
205
|
+
<PageHeader title="Email lists" description="Manage EmailList records, signup policy, and real membership totals.">
|
|
206
|
+
<div class="flex gap-2">
|
|
207
|
+
<Button :loading="loading()" variant="secondary" @click="loadLists()">
|
|
208
|
+
<span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>
|
|
209
|
+
Refresh
|
|
210
|
+
</Button>
|
|
211
|
+
<Button @click="openAdd()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>Add list</Button>
|
|
212
|
+
</div>
|
|
213
|
+
</PageHeader>
|
|
218
214
|
|
|
219
215
|
<div :if="loadError()" role="alert" class="flex gap-4 items-center justify-between 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">
|
|
220
216
|
<span>{{ loadError() }}</span>
|
|
@@ -195,16 +195,12 @@ onMount(() => {
|
|
|
195
195
|
</script>
|
|
196
196
|
|
|
197
197
|
<div class="space-y-6">
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
<Button :loading="loading()" variant="secondary" @click="loadPosts()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
205
|
-
<Button @click="openAdd()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>Add post</Button>
|
|
206
|
-
</div>
|
|
207
|
-
</header>
|
|
198
|
+
<PageHeader title="Social posts" description="Create and schedule SocialPost records with persisted User ownership and platform metrics.">
|
|
199
|
+
<div class="flex gap-2">
|
|
200
|
+
<Button :loading="loading()" variant="secondary" @click="loadPosts()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
201
|
+
<Button @click="openAdd()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>Add post</Button>
|
|
202
|
+
</div>
|
|
203
|
+
</PageHeader>
|
|
208
204
|
|
|
209
205
|
<div :if="loadError()" role="alert" class="flex gap-4 items-center justify-between 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"><span>{{ loadError() }}</span><Button variant="secondary" size="sm" @click="loadPosts()">Retry</Button></div>
|
|
210
206
|
<div :if="summary().failed > 0" class="flex gap-3 items-start p-4 text-red-800 text-sm dark:text-red-200 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900"><span class="shrink-0 mt-0.5 h-5 w-5 i-hugeicons-alert-02"></span><p>{{ summary().failed }} {{ summary().failed === 1 ? 'post has' : 'posts have' }} a failed status and may need review.</p></div>
|
|
@@ -82,22 +82,18 @@ onMount(() => {
|
|
|
82
82
|
</script>
|
|
83
83
|
|
|
84
84
|
<div class="space-y-6">
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
Refresh
|
|
98
|
-
</Button>
|
|
99
|
-
</div>
|
|
100
|
-
</header>
|
|
85
|
+
<PageHeader title="Queue" description="Recorded queue state and current-process worker metrics.">
|
|
86
|
+
<div class="flex gap-2 items-center">
|
|
87
|
+
<span :class="'inline-flex items-center px-2.5 py-1 font-medium text-xs rounded-full ' + (queueConnected() ? 'text-green-700 bg-green-100 dark:text-green-300 dark:bg-green-950' : 'text-red-700 bg-red-100 dark:text-red-300 dark:bg-red-950')">
|
|
88
|
+
<span :class="'mr-1.5 h-1.5 w-1.5 rounded-full ' + (queueConnected() ? 'bg-green-500' : 'bg-red-500')"></span>
|
|
89
|
+
{{ queueConnected() ? 'Queue connected' : 'Queue unavailable' }}
|
|
90
|
+
</span>
|
|
91
|
+
<Button variant="secondary" :loading="isLoading()" @click="loadQueue">
|
|
92
|
+
<span :if="!isLoading()" class="h-4 w-4 i-hugeicons-refresh"></span>
|
|
93
|
+
Refresh
|
|
94
|
+
</Button>
|
|
95
|
+
</div>
|
|
96
|
+
</PageHeader>
|
|
101
97
|
|
|
102
98
|
<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>
|
|
103
99
|
|
|
@@ -124,16 +124,12 @@ onDestroy(() => {
|
|
|
124
124
|
</script>
|
|
125
125
|
|
|
126
126
|
<div class="space-y-6">
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
<span :if="!isLoading()" class="h-4 w-4 i-hugeicons-refresh"></span>
|
|
134
|
-
Refresh
|
|
135
|
-
</Button>
|
|
136
|
-
</header>
|
|
127
|
+
<PageHeader title="Realtime" description="Recorded Websocket model events and a native connection inspector.">
|
|
128
|
+
<Button variant="secondary" :loading="isLoading()" @click="loadRealtime">
|
|
129
|
+
<span :if="!isLoading()" class="h-4 w-4 i-hugeicons-refresh"></span>
|
|
130
|
+
Refresh
|
|
131
|
+
</Button>
|
|
132
|
+
</PageHeader>
|
|
137
133
|
|
|
138
134
|
<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>
|
|
139
135
|
|
|
@@ -94,16 +94,12 @@ onMount(() => {
|
|
|
94
94
|
</script>
|
|
95
95
|
|
|
96
96
|
<div class="space-y-6">
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
<span :if="!isLoading()" class="h-4 w-4 i-hugeicons-refresh"></span>
|
|
104
|
-
Refresh
|
|
105
|
-
</Button>
|
|
106
|
-
</header>
|
|
97
|
+
<PageHeader title="Releases" description="Release model records and recorded download totals.">
|
|
98
|
+
<Button variant="secondary" :loading="isLoading()" @click="loadReleases">
|
|
99
|
+
<span :if="!isLoading()" class="h-4 w-4 i-hugeicons-refresh"></span>
|
|
100
|
+
Refresh
|
|
101
|
+
</Button>
|
|
102
|
+
</PageHeader>
|
|
107
103
|
|
|
108
104
|
<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>
|
|
109
105
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script client>
|
|
2
|
+
/**
|
|
3
|
+
* The metric strip above a dashboard list.
|
|
4
|
+
*
|
|
5
|
+
* Twenty-two dashboards carried this markup verbatim (stacksjs/stacks#2917):
|
|
6
|
+
* the same card, the same classes, the same three lines. Five of them omit
|
|
7
|
+
* `detail`, which is the only variation and is why it is optional here.
|
|
8
|
+
*
|
|
9
|
+
* It renders the <section> rather than only the cards, which is not a
|
|
10
|
+
* stylistic choice. stx wraps every component render in a
|
|
11
|
+
* `data-stx-component-wrapper` div, so a component that emitted only the
|
|
12
|
+
* cards would become ONE child of the caller's grid and every card would
|
|
13
|
+
* stack inside a single cell. Owning the grid keeps the articles as its
|
|
14
|
+
* direct children, where the column classes can reach them.
|
|
15
|
+
*
|
|
16
|
+
* `columns` is a prop because the counts genuinely differ per screen: four,
|
|
17
|
+
* five, three, and two different responsive ramps across the callers.
|
|
18
|
+
*/
|
|
19
|
+
const items = useReactiveProp('items', [] as Array<{ label: string, value: string, detail?: string }>)
|
|
20
|
+
const label = useReactiveProp('label', 'Metrics')
|
|
21
|
+
const columns = useReactiveProp('columns', 'grid-cols-2 lg:grid-cols-4')
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<section :aria-label="label()" :class="`grid gap-3 ${columns()}`">
|
|
25
|
+
<template :for="item in items()" :key="item.label">
|
|
26
|
+
<article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
|
|
27
|
+
<p class="text-gray-500 text-xs dark:text-neutral-400">{{ item.label }}</p>
|
|
28
|
+
<p class="mt-2 font-semibold text-2xl text-gray-900 dark:text-white">{{ item.value }}</p>
|
|
29
|
+
<p :if="item.detail" class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ item.detail }}</p>
|
|
30
|
+
</article>
|
|
31
|
+
</template>
|
|
32
|
+
</section>
|
|
@@ -1,69 +1,28 @@
|
|
|
1
|
-
<script
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
<script client>
|
|
2
|
+
/**
|
|
3
|
+
* The title block at the top of a dashboard screen.
|
|
4
|
+
*
|
|
5
|
+
* Thirty-five screens carried the same header, the same heading pair and the
|
|
6
|
+
* same two sets of classes (stacksjs/stacks#2917).
|
|
7
|
+
*
|
|
8
|
+
* The actions stay in the caller, inside the slot, and so does their wrapper.
|
|
9
|
+
* That is deliberate: the wrapper differs six ways across the callers (`flex
|
|
10
|
+
* gap-2`, `flex flex-wrap gap-2`, `flex gap-2 items-center` and three more),
|
|
11
|
+
* and a prop for it would be a class string passed through a component,
|
|
12
|
+
* which is worse than leaving the div where its author put it.
|
|
13
|
+
*
|
|
14
|
+
* Twenty-three other screens have a header that looks like this one and is
|
|
15
|
+
* shaped differently inside. They are deliberately NOT converted; see the
|
|
16
|
+
* issue for the count and the reason.
|
|
17
|
+
*/
|
|
18
|
+
const title = useReactiveProp('title', '')
|
|
19
|
+
const description = useReactiveProp('description', '')
|
|
10
20
|
</script>
|
|
11
|
-
<div class="mb-6">
|
|
12
|
-
<!-- Back link -->
|
|
13
|
-
@if(backLink)
|
|
14
|
-
<a
|
|
15
|
-
href="{{ backLink }}"
|
|
16
|
-
class="inline-flex gap-1.5 items-center mb-3 text-sm text-stone-500 dark:text-neutral-400 hover:text-stone-700 dark:hover:text-neutral-200 transition-colors"
|
|
17
|
-
>
|
|
18
|
-
<div class="h-4 w-4 i-hugeicons-arrow-left-01"></div>
|
|
19
|
-
<span>{{ backLabel }}</span>
|
|
20
|
-
</a>
|
|
21
|
-
@endif
|
|
22
|
-
|
|
23
|
-
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
24
|
-
<div class="flex-1 min-w-0">
|
|
25
|
-
<!-- Title row -->
|
|
26
|
-
<div class="flex gap-3 items-center">
|
|
27
|
-
<slot name="icon" />
|
|
28
|
-
<div>
|
|
29
|
-
<h1 class="font-semibold text-2xl text-stone-900 truncate dark:text-white">
|
|
30
|
-
{{ title }}
|
|
31
|
-
</h1>
|
|
32
|
-
@if(description)
|
|
33
|
-
<p class="mt-1 text-sm text-stone-500 dark:text-neutral-400">
|
|
34
|
-
{{ description }}
|
|
35
|
-
</p>
|
|
36
|
-
@endif
|
|
37
|
-
</div>
|
|
38
|
-
</div>
|
|
39
|
-
|
|
40
|
-
<!-- Tags/badges slot -->
|
|
41
|
-
@if($slots.tags)
|
|
42
|
-
<div class="flex flex-wrap gap-2 mt-2">
|
|
43
|
-
<slot name="tags" />
|
|
44
|
-
</div>
|
|
45
|
-
@endif
|
|
46
|
-
</div>
|
|
47
21
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
</div>
|
|
53
|
-
@endif
|
|
22
|
+
<header class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
23
|
+
<div>
|
|
24
|
+
<h1 class="font-bold text-2xl text-gray-900 dark:text-white">{{ title() }}</h1>
|
|
25
|
+
<p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">{{ description() }}</p>
|
|
54
26
|
</div>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
@if($slots.tabs)
|
|
58
|
-
<div class="mt-4 border-b border-stone-200 dark:border-neutral-700">
|
|
59
|
-
<slot name="tabs" />
|
|
60
|
-
</div>
|
|
61
|
-
@endif
|
|
62
|
-
|
|
63
|
-
<!-- Filters slot -->
|
|
64
|
-
@if($slots.filters)
|
|
65
|
-
<div class="mt-4">
|
|
66
|
-
<slot name="filters" />
|
|
67
|
-
</div>
|
|
68
|
-
@endif
|
|
69
|
-
</div>
|
|
27
|
+
<slot />
|
|
28
|
+
</header>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script client>
|
|
2
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
2
3
|
/**
|
|
3
4
|
* The data table.
|
|
4
5
|
*
|
|
@@ -28,7 +29,43 @@ interface DashboardTableColumn {
|
|
|
28
29
|
* formatter function because props cross an attribute boundary, where a
|
|
29
30
|
* function does not survive.
|
|
30
31
|
*/
|
|
31
|
-
type?: 'text' | 'number' | 'currency' | 'date' | 'boolean'
|
|
32
|
+
type?: 'text' | 'number' | 'currency' | 'date' | 'boolean' | 'badge'
|
|
33
|
+
/**
|
|
34
|
+
* For a `badge` column: the classes each value wears. A value with no
|
|
35
|
+
* entry gets a neutral pill, so a new status never renders unstyled.
|
|
36
|
+
*
|
|
37
|
+
* A map rather than a function because props cross an attribute boundary,
|
|
38
|
+
* which a function does not survive - the same reason `type` is a string.
|
|
39
|
+
*/
|
|
40
|
+
badgeClasses?: Record<string, string>
|
|
41
|
+
/**
|
|
42
|
+
* For a `badge` column: what each value reads as, when the stored value is
|
|
43
|
+
* not the word to show. `active` displaying as "Available" is a real case
|
|
44
|
+
* and is not derivable from the value.
|
|
45
|
+
*
|
|
46
|
+
* Without an entry the value is humanised: `on_delivery` becomes
|
|
47
|
+
* "On delivery".
|
|
48
|
+
*/
|
|
49
|
+
badgeLabels?: Record<string, string>
|
|
50
|
+
/**
|
|
51
|
+
* What an empty cell reads as. Defaults to `-`.
|
|
52
|
+
*
|
|
53
|
+
* Per column because the right word is specific: a shipping table wants
|
|
54
|
+
* "No minimum" for an absent free-shipping threshold, which is a statement
|
|
55
|
+
* about the rate rather than missing data.
|
|
56
|
+
*/
|
|
57
|
+
emptyText?: string
|
|
58
|
+
/**
|
|
59
|
+
* Fixed ISO code for a `currency` column, when every row is the same
|
|
60
|
+
* currency. Defaults to USD, which is what this column did before it
|
|
61
|
+
* could be told otherwise.
|
|
62
|
+
*/
|
|
63
|
+
currency?: string
|
|
64
|
+
/**
|
|
65
|
+
* Row field holding the ISO code, when currency varies per row. Wins over
|
|
66
|
+
* `currency` when both are given and the row actually carries one.
|
|
67
|
+
*/
|
|
68
|
+
currencyKey?: string
|
|
32
69
|
width?: string
|
|
33
70
|
class?: string
|
|
34
71
|
/** Hidden until switched on in the column menu. */
|
|
@@ -106,13 +143,24 @@ function rawValue(row: DashboardTableRow, key: string): any {
|
|
|
106
143
|
function displayValue(row: DashboardTableRow, column: DashboardTableColumn): string {
|
|
107
144
|
const value = rawValue(row, column.key)
|
|
108
145
|
if (value === null || value === undefined || value === '')
|
|
109
|
-
return '-'
|
|
146
|
+
return column.emptyText ?? '-'
|
|
110
147
|
|
|
111
148
|
if (column.type === 'currency') {
|
|
112
149
|
const amount = Number(value)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
150
|
+
if (!Number.isFinite(amount))
|
|
151
|
+
return String(value)
|
|
152
|
+
|
|
153
|
+
// Through @stacksjs/commerce/money, not Intl directly. Stacks stores
|
|
154
|
+
// money in MINOR units, so formatting the stored number as if it were
|
|
155
|
+
// major multiplies every amount by the currency's exponent: a 1200 cent
|
|
156
|
+
// base rate rendered as $1,200.00 instead of $12.00 (stacksjs/stacks#2916).
|
|
157
|
+
//
|
|
158
|
+
// And the exponent is per currency, not always 100 - JPY and KRW have
|
|
159
|
+
// none - so dividing here rather than delegating would be a second bug
|
|
160
|
+
// behind the first. `currencyExponent` is the only thing that knows.
|
|
161
|
+
const perRow = column.currencyKey ? rawValue(row, column.currencyKey) : undefined
|
|
162
|
+
const code = typeof perRow === 'string' && perRow ? perRow : column.currency
|
|
163
|
+
return formatCurrency(amount, code)
|
|
116
164
|
}
|
|
117
165
|
if (column.type === 'number') {
|
|
118
166
|
const amount = Number(value)
|
|
@@ -160,6 +208,30 @@ const filtered = derived(() => {
|
|
|
160
208
|
})
|
|
161
209
|
|
|
162
210
|
/** Nulls sort last in both directions: "missing" is not a small value. */
|
|
211
|
+
const BADGE_BASE = 'inline-flex px-2.5 py-1 font-medium text-xs ring-1 ring-inset rounded-full'
|
|
212
|
+
const BADGE_NEUTRAL = 'bg-stone-100 text-stone-600 ring-stone-500/20 dark:bg-white/5 dark:text-neutral-300 dark:ring-white/10'
|
|
213
|
+
|
|
214
|
+
/** The pill's classes for this row's value, neutral when the value is unmapped. */
|
|
215
|
+
function badgeClass(row: DashboardTableRow, column: DashboardTableColumn): string {
|
|
216
|
+
const key = String(rawValue(row, column.key) ?? '')
|
|
217
|
+
return `${BADGE_BASE} ${column.badgeClasses?.[key] ?? BADGE_NEUTRAL}`
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The pill's text: the mapped label, else the value with its underscores read as spaces. */
|
|
221
|
+
function badgeLabel(row: DashboardTableRow, column: DashboardTableColumn): string {
|
|
222
|
+
const value = rawValue(row, column.key)
|
|
223
|
+
if (value === null || value === undefined || value === '')
|
|
224
|
+
return column.emptyText ?? '-'
|
|
225
|
+
|
|
226
|
+
const key = String(value)
|
|
227
|
+
const mapped = column.badgeLabels?.[key]
|
|
228
|
+
if (mapped)
|
|
229
|
+
return mapped
|
|
230
|
+
|
|
231
|
+
const words = key.replaceAll('_', ' ')
|
|
232
|
+
return words.charAt(0).toUpperCase() + words.slice(1)
|
|
233
|
+
}
|
|
234
|
+
|
|
163
235
|
function compare(a: any, b: any, type: DashboardTableColumn['type']): number {
|
|
164
236
|
const aMissing = a === null || a === undefined || a === ''
|
|
165
237
|
const bMissing = b === null || b === undefined || b === ''
|
|
@@ -570,7 +642,8 @@ function cellClass(column: DashboardTableColumn, index: number): string {
|
|
|
570
642
|
<template :for="(column, columnIndex) in visibleColumns()">
|
|
571
643
|
<td :class="cellClass(column, columnIndex)">
|
|
572
644
|
<slot name="cell" :row="row" :column="column" :value="row[column.key]">
|
|
573
|
-
{{
|
|
645
|
+
<span :if="column.type === 'badge'" :class="badgeClass(row, column)">{{ badgeLabel(row, column) }}</span>
|
|
646
|
+
<span :if="column.type !== 'badge'">{{ displayValue(row, column) }}</span>
|
|
574
647
|
</slot>
|
|
575
648
|
</td>
|
|
576
649
|
</template>
|