@stacksjs/defaults 0.70.362 → 0.70.363

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 (135) hide show
  1. package/ai/skills/stacks-browse/SKILL.md +2 -1
  2. package/ai/skills/stacks-browse/scripts/browse.ts +20 -2
  3. package/ai/skills/stacks-dashboard/SKILL.md +4 -0
  4. package/app/Actions/Dashboard/Models/ModelShowAction.ts +4 -1
  5. package/app/Actions/Dashboard/Models/model-write.ts +5 -0
  6. package/ide/vscode/package.json +1 -1
  7. package/package.json +1 -1
  8. package/resources/components/Buttons/Counter.stx +1 -1
  9. package/resources/components/Buttons/ToggleDark.stx +1 -1
  10. package/resources/components/Dashboard/App/RequestDetailsDialog.stx +42 -0
  11. package/resources/components/Dashboard/App/RequestsOverview.stx +1 -21
  12. package/resources/components/Dashboard/Auth/AccessTokenCreateDialog.stx +155 -0
  13. package/resources/components/Dashboard/Auth/AccessTokens.stx +41 -129
  14. package/resources/components/Dashboard/Ci/CiDashboard.stx +1 -157
  15. package/resources/components/Dashboard/Ci/CiRunHistoryDrawer.stx +127 -0
  16. package/resources/components/Dashboard/Commerce/CommerceCategoryDeleteDialog.stx +10 -8
  17. package/resources/components/Dashboard/Commerce/CommerceCategoryDetailsDialog.stx +22 -17
  18. package/resources/components/Dashboard/Commerce/CommerceCategoryDialog.stx +14 -13
  19. package/resources/components/Dashboard/Commerce/CommerceCouponDeleteDialog.stx +10 -7
  20. package/resources/components/Dashboard/Commerce/CommerceCouponDetailsDialog.stx +25 -19
  21. package/resources/components/Dashboard/Commerce/CommerceCouponDialog.stx +14 -11
  22. package/resources/components/Dashboard/Commerce/CommerceCustomerDeleteDialog.stx +10 -8
  23. package/resources/components/Dashboard/Commerce/CommerceCustomerDetailsDialog.stx +24 -16
  24. package/resources/components/Dashboard/Commerce/CommerceCustomerDialog.stx +14 -13
  25. package/resources/components/Dashboard/Commerce/CommerceGiftCardDeleteDialog.stx +10 -8
  26. package/resources/components/Dashboard/Commerce/CommerceGiftCardDetailsDialog.stx +16 -14
  27. package/resources/components/Dashboard/Commerce/CommerceGiftCardDialog.stx +17 -16
  28. package/resources/components/Dashboard/Commerce/CommerceManufacturerDeleteDialog.stx +14 -16
  29. package/resources/components/Dashboard/Commerce/CommerceManufacturerDialog.stx +17 -16
  30. package/resources/components/Dashboard/Commerce/CommerceOrderDeleteDialog.stx +10 -8
  31. package/resources/components/Dashboard/Commerce/CommerceOrderDetailsDialog.stx +15 -10
  32. package/resources/components/Dashboard/Commerce/CommerceOrderDialog.stx +14 -13
  33. package/resources/components/Dashboard/Commerce/CommercePosCheckoutDialog.stx +30 -25
  34. package/resources/components/Dashboard/Commerce/CommercePosReceiptDialog.stx +27 -20
  35. package/resources/components/Dashboard/Commerce/CommerceProductDeleteDialog.stx +14 -14
  36. package/resources/components/Dashboard/Commerce/CommerceProductDialog.stx +14 -13
  37. package/resources/components/Dashboard/Commerce/CommerceReviewDeleteDialog.stx +10 -18
  38. package/resources/components/Dashboard/Commerce/CommerceReviewEditDialog.stx +17 -16
  39. package/resources/components/Dashboard/Commerce/CommerceTaxDeleteDialog.stx +14 -14
  40. package/resources/components/Dashboard/Commerce/CommerceTaxDialog.stx +17 -16
  41. package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +1 -1
  42. package/resources/components/Dashboard/Commerce/CommerceUnitDeleteDialog.stx +14 -17
  43. package/resources/components/Dashboard/Commerce/CommerceUnitDialog.stx +17 -16
  44. package/resources/components/Dashboard/Commerce/CommerceVariantDeleteDialog.stx +10 -8
  45. package/resources/components/Dashboard/Commerce/CommerceVariantDialog.stx +17 -16
  46. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDeleteDialog.stx +11 -15
  47. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDialog.stx +17 -16
  48. package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryDeleteDialog.stx +11 -15
  49. package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryDialog.stx +17 -16
  50. package/resources/components/Dashboard/Commerce/Delivery/DriverDeleteDialog.stx +11 -15
  51. package/resources/components/Dashboard/Commerce/Delivery/DriverDialog.stx +17 -16
  52. package/resources/components/Dashboard/Commerce/Delivery/LicenseKeyDeleteDialog.stx +11 -15
  53. package/resources/components/Dashboard/Commerce/Delivery/LicenseKeyDialog.stx +17 -16
  54. package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDeleteDialog.stx +11 -15
  55. package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDialog.stx +17 -16
  56. package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDeleteDialog.stx +11 -15
  57. package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDialog.stx +17 -16
  58. package/resources/components/Dashboard/Commerce/Delivery/ShippingZoneDeleteDialog.stx +11 -15
  59. package/resources/components/Dashboard/Commerce/Delivery/ShippingZoneDialog.stx +17 -16
  60. package/resources/components/Dashboard/Commerce/PaymentDetailsDialog.stx +16 -15
  61. package/resources/components/Dashboard/Commerce/PaymentRefundDialog.stx +16 -15
  62. package/resources/components/Dashboard/Commerce/PrintDeviceDeleteDialog.stx +10 -11
  63. package/resources/components/Dashboard/Commerce/PrintDeviceDetailsDialog.stx +16 -14
  64. package/resources/components/Dashboard/Commerce/PrintDeviceDialog.stx +17 -16
  65. package/resources/components/Dashboard/Commerce/PrintLogDeleteDialog.stx +10 -7
  66. package/resources/components/Dashboard/Commerce/PrintLogDetailsDialog.stx +13 -10
  67. package/resources/components/Dashboard/Commerce/ProductWaitlistDeleteDialog.stx +10 -18
  68. package/resources/components/Dashboard/Commerce/ProductWaitlistEntryDialog.stx +96 -105
  69. package/resources/components/Dashboard/Commerce/RestaurantWaitlistDeleteDialog.stx +10 -18
  70. package/resources/components/Dashboard/Commerce/RestaurantWaitlistEntryDialog.stx +92 -95
  71. package/resources/components/Dashboard/Content/AuthorEditorDialog.stx +94 -0
  72. package/resources/components/Dashboard/Content/AuthorsDashboard.stx +39 -31
  73. package/resources/components/Dashboard/Content/BlogDashboard.stx +87 -176
  74. package/resources/components/Dashboard/Content/BlogPostEditorDialog.stx +195 -0
  75. package/resources/components/Dashboard/Content/CommentsDashboard.stx +14 -4
  76. package/resources/components/Dashboard/Content/ContentTaxonomyDashboard.stx +40 -55
  77. package/resources/components/Dashboard/Content/CreateFolderDialog.stx +45 -0
  78. package/resources/components/Dashboard/Content/FileDetailsDrawer.stx +31 -0
  79. package/resources/components/Dashboard/Content/FileManagerDashboard.stx +47 -107
  80. package/resources/components/Dashboard/Content/FileUploadDialog.stx +89 -0
  81. package/resources/components/Dashboard/Content/PageEditorDialog.stx +93 -0
  82. package/resources/components/Dashboard/Content/PagesDashboard.stx +36 -26
  83. package/resources/components/Dashboard/Content/PostEditorDialog.stx +176 -0
  84. package/resources/components/Dashboard/Content/PostsDashboard.stx +40 -64
  85. package/resources/components/Dashboard/Content/SeoDashboard.stx +18 -84
  86. package/resources/components/Dashboard/Content/SeoEditorDialog.stx +108 -0
  87. package/resources/components/Dashboard/Content/TaxonomyEditorDialog.stx +106 -0
  88. package/resources/components/Dashboard/CreateRecordModal.stx +72 -83
  89. package/resources/components/Dashboard/Email/InboxMessageList.stx +1 -1
  90. package/resources/components/Dashboard/GlobalSearch.stx +3 -9
  91. package/resources/components/Dashboard/Infrastructure/CloudDetailsDialog.stx +2 -16
  92. package/resources/components/Dashboard/Infrastructure/DnsRecordDialog.stx +4 -16
  93. package/resources/components/Dashboard/Infrastructure/LogDetailsDialog.stx +4 -19
  94. package/resources/components/Dashboard/Infrastructure/MailboxDetailsDialog.stx +6 -16
  95. package/resources/components/Dashboard/Infrastructure/ServerInspectorDialog.stx +105 -0
  96. package/resources/components/Dashboard/Infrastructure/ServersDashboard.stx +5 -59
  97. package/resources/components/Dashboard/Jobs/JobHistory.stx +1 -11
  98. package/resources/components/Dashboard/Library/ComponentsDashboard.stx +11 -60
  99. package/resources/components/Dashboard/Library/FunctionsDashboard.stx +11 -15
  100. package/resources/components/Dashboard/Library/SourceScaffoldDialog.stx +95 -0
  101. package/resources/components/Dashboard/Management/AssignmentDrawer.stx +93 -0
  102. package/resources/components/Dashboard/Management/PermissionsDashboard.stx +47 -129
  103. package/resources/components/Dashboard/Marketing/CampaignDeliveryDialog.stx +5 -8
  104. package/resources/components/Dashboard/Marketing/CampaignDialog.stx +17 -17
  105. package/resources/components/Dashboard/Marketing/MarketingListDialog.stx +17 -17
  106. package/resources/components/Dashboard/Marketing/MarketingListsTable.stx +1 -1
  107. package/resources/components/Dashboard/Marketing/SocialPostDialog.stx +17 -16
  108. package/resources/components/Dashboard/Models/EditRecordModal.stx +81 -0
  109. package/resources/components/Dashboard/Models/ModelRecordActions.stx +30 -0
  110. package/resources/components/Dashboard/Models/ModelRecordsDashboard.stx +42 -94
  111. package/resources/components/Dashboard/Navbar.stx +5 -5
  112. package/resources/components/Dashboard/Notifications/NotificationDeliveryDialog.stx +40 -60
  113. package/resources/components/Dashboard/UI/Button.stx +4 -0
  114. package/resources/components/Dashboard/UI/ConfirmDialog.stx +4 -0
  115. package/resources/components/Dashboard/UI/Drawer.stx +114 -0
  116. package/resources/components/Dashboard/UI/Modal.stx +19 -12
  117. package/resources/components/Docs/Demo/ComboboxDemo.stx +4 -5
  118. package/resources/components/Docs/Demo/DropdownDemo.stx +12 -5
  119. package/resources/components/Docs/Demo/ListboxDemo.stx +14 -6
  120. package/resources/components/Docs/Demo/PopoverDemo.stx +10 -5
  121. package/resources/components/Docs/Demo/RadioGroupDemo.stx +14 -6
  122. package/resources/components/Docs/Demo/StepperDemo.stx +4 -5
  123. package/resources/components/Docs/TeamMember.stx +16 -9
  124. package/resources/components/Forum/ForumReplyForm.stx +7 -7
  125. package/resources/components/Forum/ForumSidebar.stx +10 -6
  126. package/resources/components/Marketing/Faqs.stx +5 -5
  127. package/resources/components/Marketing/Feature.stx +11 -6
  128. package/resources/components/Marketing/Layout.stx +12 -8
  129. package/resources/components/Marketing/Pricing.stx +11 -5
  130. package/resources/components/Marketing/PrimaryFeatures.stx +4 -5
  131. package/resources/components/Marketing/PrimaryFeatures2.stx +6 -5
  132. package/resources/components/Marketing/SecondaryFeatures.stx +10 -5
  133. package/resources/components/Marketing/Testimonials.stx +8 -5
  134. package/resources/views/index.stx +4 -4
  135. package/views/system-tray/index.stx +1 -1
@@ -28,13 +28,11 @@ interface CreatedTokenResponse {
28
28
  }
29
29
  }
30
30
 
31
- const availableScopes = [
32
- { name: 'read', description: 'Read application resources' },
33
- { name: 'write', description: 'Create and update resources' },
34
- { name: 'delete', description: 'Delete application resources' },
35
- { name: 'deploy', description: 'Run deployment operations' },
36
- { name: 'admin', description: 'Use administrative operations' },
37
- ]
31
+ interface TokenDraft {
32
+ name: string
33
+ scopes: string[]
34
+ expiresInMinutes: number
35
+ }
38
36
 
39
37
  const tokens = state<AccessTokenRow[]>([])
40
38
  const loading = state(true)
@@ -44,16 +42,16 @@ const actionError = state('')
44
42
  const actionMessage = state('')
45
43
  const showCreate = state(false)
46
44
  const creating = state(false)
47
- const tokenName = state('')
48
- const expiryMinutes = state('10080')
49
- const selectedScopes = state<string[]>(['read'])
50
45
  const createdToken = state('')
51
- const copied = state(false)
52
46
  const confirmingRevoke = state<number | null>(null)
53
47
  const revoking = state<number | null>(null)
54
48
 
55
49
  const activeCount = derived(() => tokens().filter(token => tokenStatus(token) === 'active').length)
56
50
  const expiredCount = derived(() => tokens().filter(token => tokenStatus(token) === 'expired').length)
51
+ const tokenToRevoke = derived(() => tokens().find(token => token.id === confirmingRevoke()) ?? null)
52
+ const revokeDescription = derived(() => tokenToRevoke()
53
+ ? `${tokenToRevoke()?.name} will immediately stop authorizing API requests.`
54
+ : 'This access token will immediately stop authorizing API requests.')
57
55
 
58
56
  function tokenStatus(token: AccessTokenRow): 'active' | 'expired' {
59
57
  if (!token.expires_at)
@@ -74,33 +72,18 @@ function formatDate(value: string | null): string {
74
72
  return Number.isNaN(date.getTime()) ? 'Unknown' : date.toLocaleString()
75
73
  }
76
74
 
77
- function toggleScope(event: Event, scope: string): void {
78
- const enabled = (event.target as HTMLInputElement).checked
79
- selectedScopes.set(enabled
80
- ? [...new Set([...selectedScopes(), scope])]
81
- : selectedScopes().filter(item => item !== scope))
82
- }
83
-
84
- function resetCreateForm(): void {
85
- tokenName.set('')
86
- expiryMinutes.set('10080')
87
- selectedScopes.set(['read'])
88
- createdToken.set('')
89
- copied.set(false)
90
- }
91
-
92
75
  function openCreate(): void {
93
76
  if (authenticationRequired())
94
77
  return
95
78
 
96
- resetCreateForm()
79
+ createdToken.set('')
97
80
  actionError.set('')
98
81
  showCreate.set(true)
99
82
  }
100
83
 
101
84
  function closeCreate(): void {
102
85
  showCreate.set(false)
103
- resetCreateForm()
86
+ createdToken.set('')
104
87
  }
105
88
 
106
89
  async function loadTokens(): Promise<void> {
@@ -139,27 +122,18 @@ async function loadTokens(): Promise<void> {
139
122
  }
140
123
  }
141
124
 
142
- async function createAccessToken(): Promise<void> {
125
+ async function createAccessToken(draft: TokenDraft): Promise<void> {
143
126
  actionError.set('')
144
127
  actionMessage.set('')
145
128
 
146
- if (!tokenName().trim()) {
147
- actionError.set('Enter a name for this token.')
148
- return
149
- }
150
- if (selectedScopes().length === 0) {
151
- actionError.set('Select at least one scope.')
152
- return
153
- }
154
-
155
129
  creating.set(true)
156
130
  try {
157
131
  const response = await dashboardApi<CreatedTokenResponse>('/auth/token', {
158
132
  method: 'POST',
159
133
  body: {
160
- name: tokenName().trim(),
161
- scopes: selectedScopes(),
162
- expires_in_minutes: Number(expiryMinutes()),
134
+ name: draft.name,
135
+ scopes: draft.scopes,
136
+ expires_in_minutes: draft.expiresInMinutes,
163
137
  with_refresh_token: false,
164
138
  },
165
139
  })
@@ -182,22 +156,10 @@ async function createAccessToken(): Promise<void> {
182
156
  }
183
157
  }
184
158
 
185
- async function copyCreatedToken(): Promise<void> {
186
- actionError.set('')
187
- try {
188
- await globalThis.navigator.clipboard.writeText(createdToken())
189
- copied.set(true)
190
- }
191
- catch {
192
- actionError.set('Clipboard access is unavailable. Select and copy the token manually.')
193
- }
194
- }
195
-
196
- async function revokeToken(id: number): Promise<void> {
197
- if (confirmingRevoke() !== id) {
198
- confirmingRevoke.set(id)
159
+ async function revokeToken(): Promise<void> {
160
+ const id = confirmingRevoke()
161
+ if (id === null)
199
162
  return
200
- }
201
163
 
202
164
  actionError.set('')
203
165
  actionMessage.set('')
@@ -260,8 +222,8 @@ onMount(() => {
260
222
  <Button variant="secondary" size="sm" @click="loadTokens">Try again</Button>
261
223
  </div>
262
224
 
263
- <div :if="actionError()" role="alert" class="px-4 py-3 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/40 border border-red-200 rounded-lg dark:border-red-900">{{ actionError() }}</div>
264
- <div :if="actionMessage()" role="status" class="px-4 py-3 text-green-700 text-sm dark:text-green-300 bg-green-50 dark:bg-green-950/40 border border-green-200 rounded-lg dark:border-green-900">{{ actionMessage() }}</div>
225
+ <div :if="actionError() && !showCreate()" role="alert" class="px-4 py-3 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/40 border border-red-200 rounded-lg dark:border-red-900">{{ actionError() }}</div>
226
+ <div :if="actionMessage() && !showCreate()" role="status" class="px-4 py-3 text-green-700 text-sm dark:text-green-300 bg-green-50 dark:bg-green-950/40 border border-green-200 rounded-lg dark:border-green-900">{{ actionMessage() }}</div>
265
227
 
266
228
  <Card padding="none">
267
229
  <div class="flex items-center justify-between p-5 border-b border-neutral-200 dark:border-neutral-700">
@@ -333,8 +295,8 @@ onMount(() => {
333
295
  <td class="px-4 py-3.5"><span :class="'inline-flex px-2 py-0.5 font-medium text-xs capitalize rounded ' + statusClass(token)">{{ tokenStatus(token) }}</span></td>
334
296
  <td class="px-4 py-3.5 text-neutral-500 text-right text-xs whitespace-nowrap dark:text-neutral-400">{{ formatDate(token.expires_at) }}</td>
335
297
  <td class="pr-5 py-3.5 text-right whitespace-nowrap">
336
- <Button variant="danger" size="sm" :loading="revoking() === token.id" @click="revokeToken(token.id)">
337
- {{ revoking() === token.id ? 'Revoking...' : confirmingRevoke() === token.id ? 'Confirm Revoke' : 'Revoke' }}
298
+ <Button variant="danger" size="sm" :loading="revoking() === token.id" @click="confirmingRevoke.set(token.id)">
299
+ {{ revoking() === token.id ? 'Revoking...' : 'Revoke' }}
338
300
  </Button>
339
301
  </td>
340
302
  </tr>
@@ -343,73 +305,23 @@ onMount(() => {
343
305
  </div>
344
306
  </Card>
345
307
 
346
- <div :if="showCreate()" class="flex fixed inset-0 z-50 items-center justify-center p-4 bg-black/50 dashboard-modal-layer" @click.self="closeCreate">
347
- <div role="dialog" aria-modal="true" aria-labelledby="create-token-title" class="p-6 max-w-lg w-full bg-white dark:bg-neutral-900 border border-neutral-200 rounded-xl dark:border-neutral-700 shadow-xl">
348
- <div class="flex gap-4 items-start justify-between">
349
- <div>
350
- <h2 id="create-token-title" class="font-semibold text-lg text-neutral-900 dark:text-white">Create Access Token</h2>
351
- <p class="mt-1 text-neutral-500 text-sm dark:text-neutral-400">Grant only the scopes this integration needs.</p>
352
- </div>
353
- <Button variant="ghost" iconOnly ariaLabel="Close create token dialog" @click="closeCreate">
354
- <span class="h-5 w-5 i-hugeicons-cancel-01"></span>
355
- </Button>
356
- </div>
357
-
358
- <div :if="createdToken()" class="mt-6 space-y-4">
359
- <div role="status" class="px-4 py-3 text-amber-800 text-sm dark:text-amber-200 bg-amber-50 dark:bg-amber-950/40 border border-amber-200 rounded-lg dark:border-amber-900">
360
- {{ actionMessage() }}
361
- </div>
362
- <label class="block">
363
- <span class="font-medium text-neutral-700 text-sm dark:text-neutral-300">New token</span>
364
- <textarea readonly rows="4" class="block mt-2 p-3 w-full font-mono text-neutral-900 text-xs dark:text-white bg-neutral-50 dark:bg-neutral-950 border border-neutral-300 rounded-lg dark:border-neutral-700">{{ createdToken() }}</textarea>
365
- </label>
366
- <div class="flex gap-2 justify-end">
367
- <Button variant="secondary" @click="closeCreate">Done</Button>
368
- <Button @click="copyCreatedToken">
369
- <span class="h-4 w-4 i-hugeicons-copy-01"></span>
370
- {{ copied() ? 'Copied' : 'Copy Token' }}
371
- </Button>
372
- </div>
373
- </div>
374
-
375
- <form :if="!createdToken()" class="mt-6 space-y-5" @submit.prevent="createAccessToken">
376
- <label class="block">
377
- <span class="font-medium text-neutral-700 text-sm dark:text-neutral-300">Token name</span>
378
- <input x-model="tokenName" type="text" maxlength="255" required placeholder="CI deployment" class="block mt-2 px-3 py-2 w-full text-neutral-900 text-sm dark:text-white bg-white dark:bg-neutral-950 border border-neutral-300 rounded-lg dark:border-neutral-700 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20">
379
- </label>
380
-
381
- <label class="block">
382
- <span class="font-medium text-neutral-700 text-sm dark:text-neutral-300">Expires after</span>
383
- <select x-model="expiryMinutes" class="block mt-2 px-3 py-2 w-full text-neutral-900 text-sm dark:text-white bg-white dark:bg-neutral-950 border border-neutral-300 rounded-lg dark:border-neutral-700 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20">
384
- <option value="60">1 hour</option>
385
- <option value="1440">1 day</option>
386
- <option value="10080">7 days</option>
387
- <option value="43200">30 days</option>
388
- </select>
389
- </label>
390
-
391
- <fieldset>
392
- <legend class="font-medium text-neutral-700 text-sm dark:text-neutral-300">Scopes</legend>
393
- <div class="grid gap-2 grid-cols-1 sm:grid-cols-2 mt-2">
394
- <label :for="scope in availableScopes" class="flex gap-3 items-start p-3 border border-neutral-200 rounded-lg dark:border-neutral-700">
395
- <input type="checkbox" :checked="selectedScopes().includes(scope.name)" class="mt-0.5 h-4 w-4 text-blue-600 rounded" @change="toggleScope($event, scope.name)">
396
- <span>
397
- <span class="block font-medium text-neutral-900 text-sm dark:text-white">{{ scope.name }}</span>
398
- <span class="block mt-0.5 text-neutral-500 text-xs dark:text-neutral-400">{{ scope.description }}</span>
399
- </span>
400
- </label>
401
- </div>
402
- </fieldset>
403
-
404
- <div :if="actionError()" role="alert" class="px-3 py-2 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/40 rounded-lg">{{ actionError() }}</div>
405
-
406
- <div class="flex gap-2 justify-end pt-2">
407
- <Button variant="secondary" :disabled="creating()" @click="closeCreate">Cancel</Button>
408
- <Button type="submit" :loading="creating()">
409
- {{ creating() ? 'Creating...' : 'Create Token' }}
410
- </Button>
411
- </div>
412
- </form>
413
- </div>
414
- </div>
308
+ <AccessTokenCreateDialog
309
+ :open="showCreate()"
310
+ :busy="creating()"
311
+ :createdToken="createdToken()"
312
+ :message="actionMessage()"
313
+ :error="actionError()"
314
+ @close="closeCreate"
315
+ @submit="createAccessToken($event)"
316
+ />
317
+
318
+ <ConfirmDialog
319
+ :isOpen="confirmingRevoke() !== null"
320
+ title="Revoke this access token?"
321
+ :description="revokeDescription()"
322
+ :busy="revoking() !== null"
323
+ confirmLabel="Revoke"
324
+ @close="confirmingRevoke.set(null)"
325
+ @confirm="revokeToken"
326
+ />
415
327
  </div>
@@ -83,50 +83,6 @@
83
83
  return str.length > len ? str.slice(0, len) + '...' : str
84
84
  }
85
85
 
86
- // ─── Drilldown drawer state (stacksjs/stacks#1848) ──────────────
87
- const drilldownTarget = derived(() => ci.drilldown())
88
- const drilldownRuns = derived(() => ci.drilldownRuns())
89
- const loadingDrilldown = derived(() => ci.loadingDrilldown())
90
- const drilldownError = derived(() => ci.drilldownError())
91
- const expandedRunId = derived(() => ci.expandedRunId())
92
- const jobsByRunId = derived(() => ci.drilldownJobsByRunId())
93
- const loadingJobsRunId = derived(() => ci.loadingJobsForRunId())
94
-
95
- function runRowClass(conclusion, status) {
96
- const base = 'px-3 py-2 -mx-3 border-l-2 cursor-pointer hover:bg-zinc-50 dark:hover:bg-zinc-900 transition-colors '
97
- if (status !== 'completed') return base + 'border-yellow-400'
98
- if (conclusion === 'success') return base + 'border-emerald-400'
99
- if (conclusion === 'failure' || conclusion === 'timed_out') return base + 'border-red-400'
100
- if (conclusion === 'cancelled') return base + 'border-zinc-400'
101
- return base + 'border-zinc-200 dark:border-zinc-800'
102
- }
103
-
104
- function runStatusBadge(conclusion, status) {
105
- if (status !== 'completed') return { label: status, cls: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300' }
106
- if (conclusion === 'success') return { label: 'success', cls: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300' }
107
- if (conclusion === 'failure') return { label: 'failure', cls: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300' }
108
- return { label: conclusion || 'unknown', cls: 'bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300' }
109
- }
110
-
111
- function formatDuration(ms) {
112
- if (ms == null) return ''
113
- const s = Math.floor(ms / 1000)
114
- if (s < 60) return s + 's'
115
- const m = Math.floor(s / 60)
116
- const rem = s % 60
117
- if (m < 60) return m + 'm' + (rem ? ' ' + rem + 's' : '')
118
- const h = Math.floor(m / 60)
119
- return h + 'h ' + (m % 60) + 'm'
120
- }
121
-
122
- function jobsForRun(runId) {
123
- return jobsByRunId()[runId] || []
124
- }
125
-
126
- function failedJobs(runId) {
127
- return jobsForRun(runId).filter(j => j.conclusion && j.conclusion !== 'success' && j.conclusion !== 'skipped' && j.conclusion !== 'neutral')
128
- }
129
-
130
86
  // ─── Runner pressure sparkline (stacksjs/stacks#1850) ────────────
131
87
  //
132
88
  // Inline SVG sparkline - no chart lib. The path is built off the
@@ -342,118 +298,6 @@
342
298
  </div>
343
299
  </div>
344
300
 
345
- <!-- ────────── Drilldown drawer (#1848) ────────── -->
346
- <!-- Slide-in side panel triggered by a repo-card click. Lists the
347
- last 20 workflow runs as a vertical timeline; failing runs
348
- expand inline to their failed-jobs list. Closes on backdrop
349
- click + Escape. -->
350
- <div
351
- :if="drilldownTarget()"
352
- class="flex fixed inset-0 z-50 justify-end dashboard-modal-layer"
353
- @click="ci.closeDrilldown()"
354
- @keydown.escape.window="ci.closeDrilldown()"
355
- >
356
- <div class="absolute inset-0 bg-black/40"></div>
357
- <aside
358
- @click.stop
359
- class="overflow-y-auto relative h-full max-w-2xl w-full bg-white dark:bg-zinc-950 border-l border-zinc-200 dark:border-zinc-800 shadow-2xl"
360
- >
361
- <!-- Header -->
362
- <div class="flex sticky top-0 z-10 gap-3 items-start px-5 py-4 bg-white dark:bg-zinc-950 border-b border-zinc-200 dark:border-zinc-800">
363
- <div class="flex-1 min-w-0">
364
- <p class="text-xs text-zinc-500 tracking-wide uppercase">Run history</p>
365
- <h3 class="font-semibold text-lg text-zinc-900 truncate dark:text-zinc-100" :text="drilldownTarget().fullName"></h3>
366
- </div>
367
- <Button variant="ghost" size="sm" iconOnly ariaLabel="Close run history" @click="ci.closeDrilldown()">
368
- <span aria-hidden="true" class="h-5 w-5 i-hugeicons-cancel-01"></span>
369
- </Button>
370
- </div>
371
-
372
- <!-- Error -->
373
- <div :if="drilldownError()" class="mt-4 mx-5 px-4 py-3 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900/40">
374
- <p class="text-red-700 text-xs dark:text-red-300" :text="drilldownError()"></p>
375
- </div>
376
-
377
- <!-- Loading -->
378
- <div :if="loadingDrilldown() && drilldownRuns().length === 0" class="px-5 py-12 text-center">
379
- <span aria-hidden="true" class="mx-auto h-6 w-6 text-violet-500 animate-spin i-hugeicons-loading-03"></span>
380
- <p class="mt-2 text-xs text-zinc-500">Fetching recent runs…</p>
381
- </div>
382
-
383
- <!-- Runs list -->
384
- <ul :if="drilldownRuns().length > 0" class="px-5 py-3">
385
- <li
386
- :for="run in drilldownRuns"
387
- class="border-b border-zinc-100 dark:border-zinc-900 last:border-0"
388
- >
389
- <div
390
- @click="ci.toggleRunJobs(run.id)"
391
- :class="runRowClass(run.conclusion, run.status)"
392
- >
393
- <div class="flex gap-3 items-start justify-between">
394
- <div class="flex-1 min-w-0">
395
- <div class="flex gap-2 items-center">
396
- <span
397
- :class="`px-1.5 py-0.5 text-[10px] font-medium rounded ${runStatusBadge(run.conclusion, run.status).cls}`"
398
- :text="runStatusBadge(run.conclusion, run.status).label"
399
- ></span>
400
- <span class="font-medium text-sm text-zinc-800 truncate dark:text-zinc-200" :text="run.name"></span>
401
- </div>
402
- <p :if="run.commitMessage" class="mt-1 text-xs text-zinc-500 truncate" :text="truncate(run.commitMessage, 80)"></p>
403
- <div class="flex gap-2 items-center mt-1 text-[10px] text-zinc-400">
404
- <span class="font-mono" :text="run.headShaShort"></span>
405
- <span :if="run.headBranch" :text="run.headBranch"></span>
406
- <span :if="run.commitAuthor">·</span>
407
- <span :if="run.commitAuthor" :text="run.commitAuthor"></span>
408
- <span>·</span>
409
- <span :text="timeAgo(run.updatedAt)"></span>
410
- <span :if="run.durationMs">·</span>
411
- <span :if="run.durationMs" :text="formatDuration(run.durationMs)"></span>
412
- </div>
413
- </div>
414
- <a
415
- :href="run.url"
416
- target="_blank"
417
- @click.stop
418
- class="flex-shrink-0 no-underline text-violet-600 text-xs dark:text-violet-400 hover:underline"
419
- >view ↗</a>
420
- </div>
421
- </div>
422
-
423
- <!-- Expanded: failed job list -->
424
- <div :if="expandedRunId() === run.id" class="px-3 py-2 bg-zinc-50 dark:bg-zinc-900/50 border-t border-zinc-100 dark:border-zinc-900">
425
- <div :if="loadingJobsRunId() === run.id" class="py-2 text-xs text-zinc-500">
426
- Loading jobs…
427
- </div>
428
- <div :if="loadingJobsRunId() !== run.id && jobsForRun(run.id).length === 0" class="py-1 italic text-xs text-zinc-400">
429
- No job detail.
430
- </div>
431
- <div :if="loadingJobsRunId() !== run.id && failedJobs(run.id).length > 0" class="space-y-1.5">
432
- <p class="text-[10px] text-zinc-500 tracking-wide uppercase">Failed jobs</p>
433
- <a
434
- :for="job in failedJobs(run.id)"
435
- :href="job.url"
436
- target="_blank"
437
- @click.stop
438
- class="flex gap-1.5 items-center no-underline text-red-700 text-xs dark:text-red-400 hover:underline"
439
- >
440
- <span aria-hidden="true" class="h-2.5 w-2.5 i-hugeicons-cancel-circle"></span>
441
- <span :text="job.name"></span>
442
- <span :if="job.durationMs" class="text-zinc-400" :text="'(' + formatDuration(job.durationMs) + ')'"></span>
443
- </a>
444
- </div>
445
- <div :if="loadingJobsRunId() !== run.id && jobsForRun(run.id).length > 0 && failedJobs(run.id).length === 0" class="py-1 text-xs text-zinc-500">
446
- All <span :text="jobsForRun(run.id).length"></span> jobs passed.
447
- </div>
448
- </div>
449
- </li>
450
- </ul>
451
-
452
- <!-- Empty -->
453
- <div :if="!loadingDrilldown() && !drilldownError() && drilldownRuns().length === 0" class="px-5 py-12 text-center">
454
- <p class="text-sm text-zinc-500">No recent runs.</p>
455
- </div>
456
- </aside>
457
- </div>
301
+ <CiRunHistoryDrawer />
458
302
  </div>
459
303
  </template>
@@ -0,0 +1,127 @@
1
+ <script client>
2
+ import { ciStore } from '~/storage/framework/defaults/views/dashboard/stores/ci'
3
+
4
+ const ci = ciStore
5
+ const target = derived(() => ci.drilldown())
6
+ const runs = derived(() => ci.drilldownRuns())
7
+ const loading = derived(() => ci.loadingDrilldown())
8
+ const error = derived(() => ci.drilldownError())
9
+ const expandedRunId = derived(() => ci.expandedRunId())
10
+ const jobsByRunId = derived(() => ci.drilldownJobsByRunId())
11
+ const loadingJobsRunId = derived(() => ci.loadingJobsForRunId())
12
+ const title = derived(() => target()?.fullName || 'Run history')
13
+
14
+ function runRowClass(conclusion, status) {
15
+ const base = 'px-3 py-2 -mx-3 border-l-2 cursor-pointer hover:bg-zinc-50 dark:hover:bg-zinc-900 transition-colors '
16
+ if (status !== 'completed') return base + 'border-yellow-400'
17
+ if (conclusion === 'success') return base + 'border-emerald-400'
18
+ if (conclusion === 'failure' || conclusion === 'timed_out') return base + 'border-red-400'
19
+ if (conclusion === 'cancelled') return base + 'border-zinc-400'
20
+ return base + 'border-zinc-200 dark:border-zinc-800'
21
+ }
22
+
23
+ function runStatusBadge(conclusion, status) {
24
+ if (status !== 'completed') return { label: status, cls: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300' }
25
+ if (conclusion === 'success') return { label: 'success', cls: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300' }
26
+ if (conclusion === 'failure') return { label: 'failure', cls: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300' }
27
+ return { label: conclusion || 'unknown', cls: 'bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300' }
28
+ }
29
+
30
+ function formatDuration(ms) {
31
+ if (ms == null) return ''
32
+ const seconds = Math.floor(ms / 1000)
33
+ if (seconds < 60) return seconds + 's'
34
+ const minutes = Math.floor(seconds / 60)
35
+ const remainder = seconds % 60
36
+ if (minutes < 60) return minutes + 'm' + (remainder ? ' ' + remainder + 's' : '')
37
+ const hours = Math.floor(minutes / 60)
38
+ return hours + 'h ' + (minutes % 60) + 'm'
39
+ }
40
+
41
+ function timeAgo(dateStr) {
42
+ if (!dateStr) return ''
43
+ const diff = Date.now() - new Date(dateStr).getTime()
44
+ const minutes = Math.floor(diff / 60000)
45
+ if (minutes < 60) return minutes + 'm ago'
46
+ const hours = Math.floor(minutes / 60)
47
+ if (hours < 24) return hours + 'h ago'
48
+ return Math.floor(hours / 24) + 'd ago'
49
+ }
50
+
51
+ function truncate(value, length) {
52
+ if (!value) return ''
53
+ return value.length > length ? value.slice(0, length) + '...' : value
54
+ }
55
+
56
+ function jobsForRun(runId) {
57
+ return jobsByRunId()[runId] || []
58
+ }
59
+
60
+ function failedJobs(runId) {
61
+ return jobsForRun(runId).filter(job => job.conclusion && job.conclusion !== 'success' && job.conclusion !== 'skipped' && job.conclusion !== 'neutral')
62
+ }
63
+ </script>
64
+
65
+ <Drawer
66
+ :isOpen="Boolean(target())"
67
+ :title="title()"
68
+ description="Run history"
69
+ size="lg"
70
+ @close="ci.closeDrilldown()"
71
+ >
72
+ <div :if="error()" class="mt-4 mx-5 px-4 py-3 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900/40">
73
+ <p class="text-red-700 text-xs dark:text-red-300" :text="error()"></p>
74
+ </div>
75
+
76
+ <div :if="loading() && runs().length === 0" class="px-5 py-12 text-center">
77
+ <span aria-hidden="true" class="mx-auto h-6 w-6 text-violet-500 animate-spin i-hugeicons-loading-03"></span>
78
+ <p class="mt-2 text-zinc-500 text-xs">Fetching recent runs...</p>
79
+ </div>
80
+
81
+ <ul :if="runs().length > 0" class="px-5 py-3">
82
+ <li :for="run in runs" class="border-b border-zinc-100 dark:border-zinc-900 last:border-0">
83
+ <div @click="ci.toggleRunJobs(run.id)" :class="runRowClass(run.conclusion, run.status)">
84
+ <div class="flex gap-3 items-start justify-between">
85
+ <div class="flex-1 min-w-0">
86
+ <div class="flex gap-2 items-center">
87
+ <span :class="`px-1.5 py-0.5 text-[10px] font-medium rounded ${runStatusBadge(run.conclusion, run.status).cls}`" :text="runStatusBadge(run.conclusion, run.status).label"></span>
88
+ <span class="font-medium text-sm text-zinc-800 truncate dark:text-zinc-200" :text="run.name"></span>
89
+ </div>
90
+ <p :if="run.commitMessage" class="mt-1 text-zinc-500 text-xs truncate" :text="truncate(run.commitMessage, 80)"></p>
91
+ <div class="flex gap-2 items-center mt-1 text-[10px] text-zinc-400">
92
+ <span class="font-mono" :text="run.headShaShort"></span>
93
+ <span :if="run.headBranch" :text="run.headBranch"></span>
94
+ <span :if="run.commitAuthor">·</span>
95
+ <span :if="run.commitAuthor" :text="run.commitAuthor"></span>
96
+ <span>·</span>
97
+ <span :text="timeAgo(run.updatedAt)"></span>
98
+ <span :if="run.durationMs">·</span>
99
+ <span :if="run.durationMs" :text="formatDuration(run.durationMs)"></span>
100
+ </div>
101
+ </div>
102
+ <a :href="run.url" target="_blank" rel="noreferrer" @click.stop class="flex-shrink-0 no-underline text-violet-600 text-xs dark:text-violet-400 hover:underline">View ↗</a>
103
+ </div>
104
+ </div>
105
+
106
+ <div :if="expandedRunId() === run.id" class="px-3 py-2 bg-zinc-50 dark:bg-zinc-900/50 border-t border-zinc-100 dark:border-zinc-900">
107
+ <div :if="loadingJobsRunId() === run.id" class="py-2 text-zinc-500 text-xs">Loading jobs...</div>
108
+ <div :if="loadingJobsRunId() !== run.id && jobsForRun(run.id).length === 0" class="py-1 italic text-zinc-400 text-xs">No job detail.</div>
109
+ <div :if="loadingJobsRunId() !== run.id && failedJobs(run.id).length > 0" class="space-y-1.5">
110
+ <p class="text-[10px] text-zinc-500 tracking-wide uppercase">Failed jobs</p>
111
+ <a :for="job in failedJobs(run.id)" :href="job.url" target="_blank" rel="noreferrer" @click.stop class="flex gap-1.5 items-center no-underline text-red-700 text-xs dark:text-red-400 hover:underline">
112
+ <span aria-hidden="true" class="h-2.5 w-2.5 i-hugeicons-cancel-circle"></span>
113
+ <span :text="job.name"></span>
114
+ <span :if="job.durationMs" class="text-zinc-400" :text="'(' + formatDuration(job.durationMs) + ')'"></span>
115
+ </a>
116
+ </div>
117
+ <div :if="loadingJobsRunId() !== run.id && jobsForRun(run.id).length > 0 && failedJobs(run.id).length === 0" class="py-1 text-zinc-500 text-xs">
118
+ All <span :text="jobsForRun(run.id).length"></span> jobs passed.
119
+ </div>
120
+ </div>
121
+ </li>
122
+ </ul>
123
+
124
+ <div :if="!loading() && !error() && runs().length === 0" class="px-5 py-12 text-center">
125
+ <p class="text-zinc-500 text-sm">No recent runs.</p>
126
+ </div>
127
+ </Drawer>
@@ -5,13 +5,15 @@ const open = useReactiveProp('open', false)
5
5
  const record = useReactiveProp('record', null as CommerceCategoryRecord | null)
6
6
  const busy = useReactiveProp('busy', false)
7
7
  const emit = defineEmits()
8
+ const description = derived(() => `Delete "${record()?.name || 'Unnamed category'}" permanently? Linked products and child categories may prevent deletion.`)
8
9
  </script>
9
10
 
10
- <div :if="open()" class="flex fixed inset-0 z-50 items-center justify-center p-4 dashboard-modal-layer">
11
- <button type="button" aria-label="Close delete category dialog" class="absolute inset-0 bg-black/45" @click="emit('close')"></button>
12
- <section role="dialog" aria-modal="true" aria-labelledby="category-delete-title" class="relative p-6 max-w-md w-full bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700 shadow-xl">
13
- <h2 id="category-delete-title" class="font-semibold text-gray-900 text-lg dark:text-white">Delete category</h2>
14
- <p class="mt-2 text-gray-500 text-sm dark:text-neutral-400">Delete "{{ record()?.name || 'Unnamed category' }}" permanently? Linked products and child categories may prevent deletion.</p>
15
- <footer class="flex gap-2 justify-end mt-6"><Button variant="secondary" :disabled="busy()" @click="emit('close')">Cancel</Button><Button variant="danger" :loading="busy()" @click="emit('confirm')">Delete category</Button></footer>
16
- </section>
17
- </div>
11
+ <ConfirmDialog
12
+ :isOpen="open()"
13
+ title="Delete category"
14
+ :description="description()"
15
+ confirmLabel="Delete category"
16
+ :busy="busy()"
17
+ @close="emit('close')"
18
+ @confirm="emit('confirm')"
19
+ />
@@ -35,20 +35,25 @@ function deleteRecord(): void {
35
35
  }
36
36
  </script>
37
37
 
38
- <div :if="open()" class="flex fixed inset-0 z-50 items-center justify-center p-4 dashboard-modal-layer">
39
- <button type="button" aria-label="Close category details" class="absolute inset-0 bg-black/45" @click="emit('close')"></button>
40
- <section role="dialog" aria-modal="true" aria-labelledby="category-details-title" class="relative max-w-xl w-full bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700 shadow-xl">
41
- <div class="p-6">
42
- <div class="flex gap-4 items-start justify-between"><div><h2 id="category-details-title" class="font-semibold text-gray-900 text-lg dark:text-white">{{ record()?.name || 'Category' }}</h2><p class="mt-1 font-mono text-gray-400 text-sm dark:text-neutral-500">{{ record()?.slug || 'No slug' }}</p></div><span class="px-2 py-1 font-semibold text-gray-700 text-xs dark:text-neutral-200 bg-gray-100 dark:bg-neutral-700 rounded-full">{{ record()?.isActive ? 'Active' : 'Inactive' }}</span></div>
43
- <p class="mt-5 text-gray-600 text-sm whitespace-pre-wrap dark:text-neutral-300">{{ record()?.description || 'No description recorded.' }}</p>
44
- <dl class="grid gap-4 grid-cols-2 mt-6">
45
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Parent</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ parentName() }}</dd></div>
46
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Display order</dt><dd class="mt-1 font-mono text-gray-900 text-sm dark:text-white">{{ record()?.displayOrder || 0 }}</dd></div>
47
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Direct children</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ children().length }}</dd></div>
48
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Record ID</dt><dd class="mt-1 font-mono text-gray-900 text-sm dark:text-white">{{ record()?.id || 'Not recorded' }}</dd></div>
49
- <div class="col-span-2"><dt class="text-gray-400 text-xs dark:text-neutral-500">Image URL</dt><dd class="mt-1 break-all text-gray-900 text-sm dark:text-white">{{ record()?.imageUrl || 'Not configured' }}</dd></div>
50
- </dl>
51
- </div>
52
- <footer class="flex gap-2 justify-between px-6 py-4 bg-gray-50 dark:bg-neutral-900/50 border-gray-200 border-t dark:border-neutral-700"><Button variant="danger" @click="deleteRecord()">Delete</Button><div class="flex gap-2"><Button variant="secondary" @click="emit('close')">Close</Button><Button @click="editRecord()">Edit category</Button></div></footer>
53
- </section>
54
- </div>
38
+ <Modal
39
+ :isOpen="open()"
40
+ :title="record()?.name || 'Category'"
41
+ :description="record()?.slug || 'No slug'"
42
+ size="lg"
43
+ @close="emit('close')"
44
+ >
45
+ <template #header-trailing>
46
+ <span class="px-2 py-1 font-semibold text-gray-700 text-xs dark:text-neutral-200 bg-gray-100 dark:bg-neutral-700 rounded-full">{{ record()?.isActive ? 'Active' : 'Inactive' }}</span>
47
+ </template>
48
+ <p class="text-gray-600 text-sm whitespace-pre-wrap dark:text-neutral-300">{{ record()?.description || 'No description recorded.' }}</p>
49
+ <dl class="grid gap-4 grid-cols-2 mt-6">
50
+ <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Parent</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ parentName() }}</dd></div>
51
+ <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Display order</dt><dd class="mt-1 font-mono text-gray-900 text-sm dark:text-white">{{ record()?.displayOrder || 0 }}</dd></div>
52
+ <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Direct children</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ children().length }}</dd></div>
53
+ <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Record ID</dt><dd class="mt-1 font-mono text-gray-900 text-sm dark:text-white">{{ record()?.id || 'Not recorded' }}</dd></div>
54
+ <div class="col-span-2"><dt class="text-gray-400 text-xs dark:text-neutral-500">Image URL</dt><dd class="mt-1 break-all text-gray-900 text-sm dark:text-white">{{ record()?.imageUrl || 'Not configured' }}</dd></div>
55
+ </dl>
56
+ <template #footer>
57
+ <div class="flex gap-2 justify-between w-full"><Button variant="danger" @click="deleteRecord()">Delete</Button><div class="flex gap-2"><Button variant="secondary" @click="emit('close')">Close</Button><Button @click="editRecord()">Edit category</Button></div></div>
58
+ </template>
59
+ </Modal>