@stacksjs/defaults 0.70.362 → 0.70.365

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 (137) 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/Auth/LogoutAction.ts +11 -4
  5. package/app/Actions/Auth/LogoutAllAction.ts +8 -4
  6. package/app/Actions/Dashboard/Models/ModelShowAction.ts +4 -1
  7. package/app/Actions/Dashboard/Models/model-write.ts +5 -0
  8. package/ide/vscode/package.json +1 -1
  9. package/package.json +1 -1
  10. package/resources/components/Buttons/Counter.stx +1 -1
  11. package/resources/components/Buttons/ToggleDark.stx +1 -1
  12. package/resources/components/Dashboard/App/RequestDetailsDialog.stx +42 -0
  13. package/resources/components/Dashboard/App/RequestsOverview.stx +1 -21
  14. package/resources/components/Dashboard/Auth/AccessTokenCreateDialog.stx +155 -0
  15. package/resources/components/Dashboard/Auth/AccessTokens.stx +41 -129
  16. package/resources/components/Dashboard/Ci/CiDashboard.stx +1 -157
  17. package/resources/components/Dashboard/Ci/CiRunHistoryDrawer.stx +127 -0
  18. package/resources/components/Dashboard/Commerce/CommerceCategoryDeleteDialog.stx +10 -8
  19. package/resources/components/Dashboard/Commerce/CommerceCategoryDetailsDialog.stx +22 -17
  20. package/resources/components/Dashboard/Commerce/CommerceCategoryDialog.stx +14 -13
  21. package/resources/components/Dashboard/Commerce/CommerceCouponDeleteDialog.stx +10 -7
  22. package/resources/components/Dashboard/Commerce/CommerceCouponDetailsDialog.stx +25 -19
  23. package/resources/components/Dashboard/Commerce/CommerceCouponDialog.stx +14 -11
  24. package/resources/components/Dashboard/Commerce/CommerceCustomerDeleteDialog.stx +10 -8
  25. package/resources/components/Dashboard/Commerce/CommerceCustomerDetailsDialog.stx +24 -16
  26. package/resources/components/Dashboard/Commerce/CommerceCustomerDialog.stx +14 -13
  27. package/resources/components/Dashboard/Commerce/CommerceGiftCardDeleteDialog.stx +10 -8
  28. package/resources/components/Dashboard/Commerce/CommerceGiftCardDetailsDialog.stx +16 -14
  29. package/resources/components/Dashboard/Commerce/CommerceGiftCardDialog.stx +17 -16
  30. package/resources/components/Dashboard/Commerce/CommerceManufacturerDeleteDialog.stx +14 -16
  31. package/resources/components/Dashboard/Commerce/CommerceManufacturerDialog.stx +17 -16
  32. package/resources/components/Dashboard/Commerce/CommerceOrderDeleteDialog.stx +10 -8
  33. package/resources/components/Dashboard/Commerce/CommerceOrderDetailsDialog.stx +15 -10
  34. package/resources/components/Dashboard/Commerce/CommerceOrderDialog.stx +14 -13
  35. package/resources/components/Dashboard/Commerce/CommercePosCheckoutDialog.stx +30 -25
  36. package/resources/components/Dashboard/Commerce/CommercePosReceiptDialog.stx +27 -20
  37. package/resources/components/Dashboard/Commerce/CommerceProductDeleteDialog.stx +14 -14
  38. package/resources/components/Dashboard/Commerce/CommerceProductDialog.stx +14 -13
  39. package/resources/components/Dashboard/Commerce/CommerceReviewDeleteDialog.stx +10 -18
  40. package/resources/components/Dashboard/Commerce/CommerceReviewEditDialog.stx +17 -16
  41. package/resources/components/Dashboard/Commerce/CommerceTaxDeleteDialog.stx +14 -14
  42. package/resources/components/Dashboard/Commerce/CommerceTaxDialog.stx +17 -16
  43. package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +1 -1
  44. package/resources/components/Dashboard/Commerce/CommerceUnitDeleteDialog.stx +14 -17
  45. package/resources/components/Dashboard/Commerce/CommerceUnitDialog.stx +17 -16
  46. package/resources/components/Dashboard/Commerce/CommerceVariantDeleteDialog.stx +10 -8
  47. package/resources/components/Dashboard/Commerce/CommerceVariantDialog.stx +17 -16
  48. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDeleteDialog.stx +11 -15
  49. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDialog.stx +17 -16
  50. package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryDeleteDialog.stx +11 -15
  51. package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryDialog.stx +17 -16
  52. package/resources/components/Dashboard/Commerce/Delivery/DriverDeleteDialog.stx +11 -15
  53. package/resources/components/Dashboard/Commerce/Delivery/DriverDialog.stx +17 -16
  54. package/resources/components/Dashboard/Commerce/Delivery/LicenseKeyDeleteDialog.stx +11 -15
  55. package/resources/components/Dashboard/Commerce/Delivery/LicenseKeyDialog.stx +17 -16
  56. package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDeleteDialog.stx +11 -15
  57. package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDialog.stx +17 -16
  58. package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDeleteDialog.stx +11 -15
  59. package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDialog.stx +17 -16
  60. package/resources/components/Dashboard/Commerce/Delivery/ShippingZoneDeleteDialog.stx +11 -15
  61. package/resources/components/Dashboard/Commerce/Delivery/ShippingZoneDialog.stx +17 -16
  62. package/resources/components/Dashboard/Commerce/PaymentDetailsDialog.stx +16 -15
  63. package/resources/components/Dashboard/Commerce/PaymentRefundDialog.stx +16 -15
  64. package/resources/components/Dashboard/Commerce/PrintDeviceDeleteDialog.stx +10 -11
  65. package/resources/components/Dashboard/Commerce/PrintDeviceDetailsDialog.stx +16 -14
  66. package/resources/components/Dashboard/Commerce/PrintDeviceDialog.stx +17 -16
  67. package/resources/components/Dashboard/Commerce/PrintLogDeleteDialog.stx +10 -7
  68. package/resources/components/Dashboard/Commerce/PrintLogDetailsDialog.stx +13 -10
  69. package/resources/components/Dashboard/Commerce/ProductWaitlistDeleteDialog.stx +10 -18
  70. package/resources/components/Dashboard/Commerce/ProductWaitlistEntryDialog.stx +96 -105
  71. package/resources/components/Dashboard/Commerce/RestaurantWaitlistDeleteDialog.stx +10 -18
  72. package/resources/components/Dashboard/Commerce/RestaurantWaitlistEntryDialog.stx +92 -95
  73. package/resources/components/Dashboard/Content/AuthorEditorDialog.stx +94 -0
  74. package/resources/components/Dashboard/Content/AuthorsDashboard.stx +39 -31
  75. package/resources/components/Dashboard/Content/BlogDashboard.stx +87 -176
  76. package/resources/components/Dashboard/Content/BlogPostEditorDialog.stx +195 -0
  77. package/resources/components/Dashboard/Content/CommentsDashboard.stx +14 -4
  78. package/resources/components/Dashboard/Content/ContentTaxonomyDashboard.stx +40 -55
  79. package/resources/components/Dashboard/Content/CreateFolderDialog.stx +45 -0
  80. package/resources/components/Dashboard/Content/FileDetailsDrawer.stx +31 -0
  81. package/resources/components/Dashboard/Content/FileManagerDashboard.stx +47 -107
  82. package/resources/components/Dashboard/Content/FileUploadDialog.stx +89 -0
  83. package/resources/components/Dashboard/Content/PageEditorDialog.stx +93 -0
  84. package/resources/components/Dashboard/Content/PagesDashboard.stx +36 -26
  85. package/resources/components/Dashboard/Content/PostEditorDialog.stx +176 -0
  86. package/resources/components/Dashboard/Content/PostsDashboard.stx +40 -64
  87. package/resources/components/Dashboard/Content/SeoDashboard.stx +18 -84
  88. package/resources/components/Dashboard/Content/SeoEditorDialog.stx +108 -0
  89. package/resources/components/Dashboard/Content/TaxonomyEditorDialog.stx +106 -0
  90. package/resources/components/Dashboard/CreateRecordModal.stx +72 -83
  91. package/resources/components/Dashboard/Email/InboxMessageList.stx +1 -1
  92. package/resources/components/Dashboard/GlobalSearch.stx +3 -9
  93. package/resources/components/Dashboard/Infrastructure/CloudDetailsDialog.stx +2 -16
  94. package/resources/components/Dashboard/Infrastructure/DnsRecordDialog.stx +4 -16
  95. package/resources/components/Dashboard/Infrastructure/LogDetailsDialog.stx +4 -19
  96. package/resources/components/Dashboard/Infrastructure/MailboxDetailsDialog.stx +6 -16
  97. package/resources/components/Dashboard/Infrastructure/ServerInspectorDialog.stx +105 -0
  98. package/resources/components/Dashboard/Infrastructure/ServersDashboard.stx +5 -59
  99. package/resources/components/Dashboard/Jobs/JobHistory.stx +1 -11
  100. package/resources/components/Dashboard/Library/ComponentsDashboard.stx +11 -60
  101. package/resources/components/Dashboard/Library/FunctionsDashboard.stx +11 -15
  102. package/resources/components/Dashboard/Library/SourceScaffoldDialog.stx +95 -0
  103. package/resources/components/Dashboard/Management/AssignmentDrawer.stx +93 -0
  104. package/resources/components/Dashboard/Management/PermissionsDashboard.stx +47 -129
  105. package/resources/components/Dashboard/Marketing/CampaignDeliveryDialog.stx +5 -8
  106. package/resources/components/Dashboard/Marketing/CampaignDialog.stx +17 -17
  107. package/resources/components/Dashboard/Marketing/MarketingListDialog.stx +17 -17
  108. package/resources/components/Dashboard/Marketing/MarketingListsTable.stx +1 -1
  109. package/resources/components/Dashboard/Marketing/SocialPostDialog.stx +17 -16
  110. package/resources/components/Dashboard/Models/EditRecordModal.stx +81 -0
  111. package/resources/components/Dashboard/Models/ModelRecordActions.stx +30 -0
  112. package/resources/components/Dashboard/Models/ModelRecordsDashboard.stx +42 -94
  113. package/resources/components/Dashboard/Navbar.stx +5 -5
  114. package/resources/components/Dashboard/Notifications/NotificationDeliveryDialog.stx +40 -60
  115. package/resources/components/Dashboard/UI/Button.stx +4 -0
  116. package/resources/components/Dashboard/UI/ConfirmDialog.stx +4 -0
  117. package/resources/components/Dashboard/UI/Drawer.stx +114 -0
  118. package/resources/components/Dashboard/UI/Modal.stx +19 -12
  119. package/resources/components/Docs/Demo/ComboboxDemo.stx +4 -5
  120. package/resources/components/Docs/Demo/DropdownDemo.stx +12 -5
  121. package/resources/components/Docs/Demo/ListboxDemo.stx +14 -6
  122. package/resources/components/Docs/Demo/PopoverDemo.stx +10 -5
  123. package/resources/components/Docs/Demo/RadioGroupDemo.stx +14 -6
  124. package/resources/components/Docs/Demo/StepperDemo.stx +4 -5
  125. package/resources/components/Docs/TeamMember.stx +16 -9
  126. package/resources/components/Forum/ForumReplyForm.stx +7 -7
  127. package/resources/components/Forum/ForumSidebar.stx +10 -6
  128. package/resources/components/Marketing/Faqs.stx +5 -5
  129. package/resources/components/Marketing/Feature.stx +11 -6
  130. package/resources/components/Marketing/Layout.stx +12 -8
  131. package/resources/components/Marketing/Pricing.stx +11 -5
  132. package/resources/components/Marketing/PrimaryFeatures.stx +4 -5
  133. package/resources/components/Marketing/PrimaryFeatures2.stx +6 -5
  134. package/resources/components/Marketing/SecondaryFeatures.stx +10 -5
  135. package/resources/components/Marketing/Testimonials.stx +8 -5
  136. package/resources/views/index.stx +4 -4
  137. package/views/system-tray/index.stx +1 -1
@@ -46,6 +46,7 @@ interface ResponseShape {
46
46
  writable: boolean
47
47
  writeCapabilities: WriteCapabilities
48
48
  createFields: CreateField[]
49
+ updateColumns: string[]
49
50
  error: string | null
50
51
  }
51
52
 
@@ -66,15 +67,14 @@ const search = state(navigation.urlQuery('q') || '')
66
67
  const filters = state<Record<string, string>>({})
67
68
  const writeCapabilities = state<WriteCapabilities>({ create: false, update: false, destroy: false })
68
69
  const createFields = state<CreateField[]>([])
70
+ const updateColumns = state<string[]>([])
69
71
  const error = state<string | null>(null)
70
72
  const loading = state(true)
71
73
  const showFilters = state(false)
72
74
 
73
- // Row being edited, plus the working copy of its values. `null` closes the
74
- // editor; the copy is kept separate so cancelling leaves the table row as
75
- // it was and a failed save does not desync the two.
75
+ // The row being edited. `null` closes the editor; EditRecordModal owns the
76
+ // working copy so cancelling leaves the table row unchanged.
76
77
  const editing = state<Record<string, unknown> | null>(null)
77
- const editValues = state<Record<string, string>>({})
78
78
  const saving = state(false)
79
79
  const confirmingDelete = state<Record<string, unknown> | null>(null)
80
80
  const showCreate = state(false)
@@ -113,9 +113,9 @@ const deleteDescription = derived(() => {
113
113
  return `Row ${row.id} will be removed through the model, so soft deletes and observers apply.`
114
114
  })
115
115
 
116
- // Fields for the edit sheet, carrying their starting value. Derived from the
117
- // row being edited rather than from `editValues`, so the list changes exactly
118
- // once per open: deriving it from the working copy instead would rebuild
116
+ // Fields for the edit modal, carrying their starting value. Derived from the
117
+ // row being edited rather than from its mutable draft, so the list changes
118
+ // exactly once per open: deriving it from the working copy would rebuild
119
119
  // every input on each keystroke and steal focus mid-word.
120
120
  const editFields = derived(() => {
121
121
  const row = editing()
@@ -124,10 +124,9 @@ const editFields = derived(() => {
124
124
  name: column.name,
125
125
  label: column.label,
126
126
  value: row[column.name] === null || row[column.name] === undefined ? '' : String(row[column.name]),
127
- // The primary key, the uuid and the timestamps are shown for context but
128
- // the API refuses to write them, so the input says so rather than
129
- // accepting an edit that will be silently dropped.
130
- readonly: column.name === 'id' || column.name === 'uuid' || column.name.endsWith('_at'),
127
+ // The model API accepts only fillable attributes and declared belongs-to
128
+ // keys. Every other database column stays visible as read-only context.
129
+ readonly: !updateColumns().includes(column.name),
131
130
  }))
132
131
  })
133
132
 
@@ -213,6 +212,7 @@ async function load() {
213
212
  sort.set(data.sort ?? '')
214
213
  writeCapabilities.set(data.writeCapabilities ?? { create: false, update: false, destroy: false })
215
214
  createFields.set(data.createFields ?? [])
215
+ updateColumns.set(data.updateColumns ?? [])
216
216
  error.set(data.error ?? null)
217
217
  }
218
218
  catch (e) {
@@ -230,26 +230,17 @@ function toggleSort(column: ColumnMeta) {
230
230
  void load()
231
231
  }
232
232
 
233
- // Debounced so a query is not fired per keystroke. The timer id is module
234
- // scope rather than signal state: it is bookkeeping, not something the
235
- // template renders.
236
- let searchTimer: ReturnType<typeof setTimeout> | undefined
237
-
238
- function onSearchInput() {
239
- clearTimeout(searchTimer)
240
- searchTimer = setTimeout(() => {
241
- page.set(1)
242
- void load()
243
- }, 250)
244
- }
233
+ // Debounced so a query is not fired per keystroke. Search and the per-column
234
+ // filters deliberately share one debounce, the way they shared one timer id
235
+ // before: typing in either should schedule a single reload, not two.
236
+ const onSearchInput = useDebounce(() => {
237
+ page.set(1)
238
+ void load()
239
+ }, 250)
245
240
 
246
241
  function onFilterInput(column: string, event: Event) {
247
242
  filters.set({ ...filters(), [column]: (event.target as HTMLInputElement).value })
248
- clearTimeout(searchTimer)
249
- searchTimer = setTimeout(() => {
250
- page.set(1)
251
- void load()
252
- }, 250)
243
+ onSearchInput()
253
244
  }
254
245
 
255
246
  function clearFilters() {
@@ -272,12 +263,6 @@ function onPerPageChange() {
272
263
  }
273
264
 
274
265
  function startEdit(row: Record<string, unknown>) {
275
- const values: Record<string, string> = {}
276
- for (const column of columnMeta()) {
277
- const raw = row[column.name]
278
- values[column.name] = raw === null || raw === undefined ? '' : String(raw)
279
- }
280
- editValues.set(values)
281
266
  editing.set(row)
282
267
  }
283
268
 
@@ -319,17 +304,13 @@ async function createRecord(fields: Record<string, unknown>): Promise<void> {
319
304
  }
320
305
  }
321
306
 
322
- function onEditInput(column: string, event: Event) {
323
- editValues.set({ ...editValues(), [column]: (event.target as HTMLInputElement).value })
324
- }
325
-
326
- async function saveEdit() {
307
+ async function saveEdit(values: Record<string, string>) {
327
308
  const row = editing()
328
309
  if (!row) return
329
310
  // Send only what actually changed, so a save never rewrites columns the
330
311
  // editor merely displayed.
331
312
  const fields: Record<string, unknown> = {}
332
- for (const [column, value] of Object.entries(editValues())) {
313
+ for (const [column, value] of Object.entries(values)) {
333
314
  const original = row[column]
334
315
  const originalText = original === null || original === undefined ? '' : String(original)
335
316
  if (value !== originalText) fields[column] = value === '' ? null : value
@@ -492,7 +473,7 @@ void load()
492
473
  </button>
493
474
  </th>
494
475
  </template>
495
- <th :if="canManageRows()" scope="col" class="px-3 py-2 w-24 font-medium text-[11px] text-neutral-500 text-right tracking-wide uppercase dark:text-neutral-400">Actions</th>
476
+ <th :if="canManageRows()" scope="col" class="right-0 sticky z-20 px-3 py-2 w-24 font-medium text-[11px] text-neutral-500 text-right tracking-wide uppercase dark:text-neutral-400 bg-neutral-50 dark:bg-neutral-800 border-black/5 border-l dark:border-white/10">Actions</th>
496
477
  </tr>
497
478
 
498
479
  <!-- Per-column filter row -->
@@ -508,7 +489,7 @@ void load()
508
489
  >
509
490
  </th>
510
491
  </template>
511
- <th :if="canManageRows()"></th>
492
+ <th :if="canManageRows()" class="right-0 sticky z-20 bg-white dark:bg-neutral-900 border-black/5 border-l dark:border-white/10"></th>
512
493
  </tr>
513
494
  </thead>
514
495
 
@@ -524,7 +505,7 @@ void load()
524
505
  <div class="h-3 bg-neutral-200 dark:bg-white/10 rounded animate-pulse"></div>
525
506
  </td>
526
507
  </template>
527
- <td :if="canManageRows()" class="px-3 py-2">
508
+ <td :if="canManageRows()" class="right-0 sticky z-10 px-3 py-2 bg-white dark:bg-neutral-900 border-black/5 border-l dark:border-white/10">
528
509
  <div class="ml-auto h-3 w-16 bg-neutral-200 dark:bg-white/10 rounded animate-pulse"></div>
529
510
  </td>
530
511
  </tr>
@@ -533,19 +514,19 @@ void load()
533
514
 
534
515
  <tbody :if="!loading()" class="divide-black/5 divide-y dark:divide-white/8">
535
516
  <template :for="row in rows()">
536
- <tr class="hover:bg-neutral-50 dark:hover:bg-white/5 transition-colors">
517
+ <tr class="hover:bg-neutral-50 dark:hover:bg-white/5 transition-colors group">
537
518
  <template :for="column in columnMeta()">
538
519
  <td :class="cellClass(column)" :title="cellValue(row, column)">{{ cellValue(row, column) }}</td>
539
520
  </template>
540
- <td :if="canManageRows()" class="px-3 py-2 text-right">
541
- <div class="flex gap-1 items-center justify-end">
542
- <Button :if="writeCapabilities().update" variant="ghost" size="sm" iconOnly :ariaLabel="'Edit ' + modelName() + ' ' + row.id" @click="startEdit(row)">
543
- <span class="h-4 w-4 i-hugeicons-edit-02"></span>
544
- </Button>
545
- <Button :if="writeCapabilities().destroy" variant="ghost" size="sm" iconOnly :ariaLabel="'Delete ' + modelName() + ' ' + row.id" @click="confirmingDelete.set(row)">
546
- <span class="h-4 w-4 text-red-500 i-hugeicons-delete-02"></span>
547
- </Button>
548
- </div>
521
+ <td :if="canManageRows()" class="right-0 sticky z-10 px-3 py-2 text-right bg-white dark:bg-neutral-900 group-hover:bg-neutral-50 dark:group-hover:bg-white/5 border-black/5 border-l dark:border-white/10 transition-colors">
522
+ <ModelRecordActions
523
+ :record="row"
524
+ :modelName="modelName()"
525
+ :canUpdate="writeCapabilities().update"
526
+ :canDestroy="writeCapabilities().destroy"
527
+ @edit="startEdit"
528
+ @delete="confirmingDelete.set($event)"
529
+ />
549
530
  </td>
550
531
  </tr>
551
532
  </template>
@@ -565,47 +546,14 @@ void load()
565
546
  </div>
566
547
  </div>
567
548
 
568
- <!-- Edit sheet -->
569
- <div :if="editing()" class="flex fixed inset-0 z-50 items-center justify-center p-4 bg-black/30 dashboard-modal-layer" @click="editing.set(null)">
570
- <div
571
- role="dialog"
572
- aria-modal="true"
573
- aria-labelledby="model-edit-title"
574
- class="flex overflow-hidden flex-col max-h-[85vh] max-w-lg w-full bg-white dark:bg-neutral-900 rounded-xl shadow-2xl"
575
- @click.stop
576
- >
577
- <div class="flex items-center justify-between px-4 py-3 border-b border-black/5 dark:border-white/10">
578
- <h2 id="model-edit-title" class="font-semibold text-[14px] text-neutral-900 dark:text-white">Edit <span :text="modelName()"></span></h2>
579
- <Button variant="ghost" size="sm" iconOnly ariaLabel="Close record editor" @click="editing.set(null)">
580
- <span class="h-4 w-4 i-hugeicons-cancel-01"></span>
581
- </Button>
582
- </div>
583
-
584
- <div class="overflow-y-auto flex-1 p-4 space-y-3">
585
- <template :for="field in editFields()">
586
- <div class="flex flex-col gap-1.5">
587
- <label :for="'model-edit-' + field.name" class="font-medium text-[12px] text-neutral-600 dark:text-neutral-300">{{ field.label }}</label>
588
- <input
589
- :id="'model-edit-' + field.name"
590
- type="text"
591
- :value="field.value"
592
- @input="onEditInput(field.name, $event)"
593
- :readonly="field.readonly"
594
- class="px-2.5 py-1.5 text-[13px] text-neutral-900 dark:text-neutral-100 read-only:text-neutral-500 dark:read-only:text-neutral-400 bg-white dark:bg-white/10 read-only:bg-neutral-50 dark:read-only:bg-white/5 border border-black/10 rounded-lg dark:border-white/15 focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-500/20"
595
- >
596
- </div>
597
- </template>
598
- </div>
599
-
600
- <div class="flex gap-2 items-center justify-end px-4 py-3 bg-neutral-50 dark:bg-white/5 border-black/5 border-t dark:border-white/10">
601
- <Button :disabled="saving()" variant="secondary" @click="editing.set(null)">Cancel</Button>
602
- <Button :loading="saving()" @click="saveEdit()">
603
- <span :if="!saving()">Save changes</span>
604
- <span :if="saving()">Saving</span>
605
- </Button>
606
- </div>
607
- </div>
608
- </div>
549
+ <EditRecordModal
550
+ :open="Boolean(editing())"
551
+ :modelName="modelName()"
552
+ :fields="editFields()"
553
+ :busy="saving()"
554
+ @close="editing.set(null)"
555
+ @submit="saveEdit($event)"
556
+ />
609
557
 
610
558
  <ConfirmDialog
611
559
  :isOpen="Boolean(confirmingDelete())"
@@ -69,27 +69,27 @@ useClickOutside(userMenu, () => setDropdownOpen(false))
69
69
  </StxLink>
70
70
 
71
71
  <nav class="flex gap-x-4 items-center lg:gap-x-6" aria-label="Dashboard utilities">
72
- <a href="/errors" data-stx-link class="header-icon-button header-link {{ isActiveRoute('/errors') ? 'active-header-link' : '' }}" title="View errors">
72
+ <a href="/errors" data-stx-link class="header-icon-button header-link {{ isActiveRoute('/errors') ? 'active-header-link' : '' }}" x-tooltip="View errors" title="View errors">
73
73
  <span class="sr-only">View errors</span>
74
74
  <span aria-hidden="true" class="h-6 w-6 duration-150 ease-in-out transition i-hugeicons-alert-01"></span>
75
75
  </a>
76
76
 
77
- <a href="/health" data-stx-link class="header-icon-button header-link {{ isActiveRoute('/health') ? 'active-header-link' : '' }}" title="View health">
77
+ <a href="/health" data-stx-link class="header-icon-button header-link {{ isActiveRoute('/health') ? 'active-header-link' : '' }}" x-tooltip="View health" title="View health">
78
78
  <span class="sr-only">View health</span>
79
79
  <span aria-hidden="true" class="h-6 w-6 duration-150 ease-in-out transition i-hugeicons-health"></span>
80
80
  </a>
81
81
 
82
- <a href="/insights" data-stx-link class="header-icon-button header-link {{ isActiveRoute('/insights') ? 'active-header-link' : '' }}" title="View cloud insights">
82
+ <a href="/insights" data-stx-link class="header-icon-button header-link {{ isActiveRoute('/insights') ? 'active-header-link' : '' }}" x-tooltip="View cloud insights" title="View cloud insights">
83
83
  <span class="sr-only">View cloud insights</span>
84
84
  <span aria-hidden="true" class="h-6 w-6 duration-150 ease-in-out transition i-hugeicons-pulse-01"></span>
85
85
  </a>
86
86
 
87
- <a href="/inbox" data-stx-link class="header-icon-button header-link {{ isActiveRoute('/inbox') ? 'active-header-link' : '' }}" title="View inbox">
87
+ <a href="/inbox" data-stx-link class="header-icon-button header-link {{ isActiveRoute('/inbox') ? 'active-header-link' : '' }}" x-tooltip="View inbox" title="View inbox">
88
88
  <span class="sr-only">View inbox</span>
89
89
  <span aria-hidden="true" class="h-6 w-6 duration-150 ease-in-out transition i-hugeicons-mail-02"></span>
90
90
  </a>
91
91
 
92
- <a href="/notifications/dashboard" data-stx-link class="header-icon-button header-link {{ isActiveRoute('/notifications/dashboard') ? 'active-header-link' : '' }}" title="View notifications">
92
+ <a href="/notifications/dashboard" data-stx-link class="header-icon-button header-link {{ isActiveRoute('/notifications/dashboard') ? 'active-header-link' : '' }}" x-tooltip="View notifications" title="View notifications">
93
93
  <span class="sr-only">View notifications</span>
94
94
  <span aria-hidden="true" class="h-6 w-6 i-hugeicons-notification-02"></span>
95
95
  </a>
@@ -21,64 +21,44 @@ function metadataText(): string {
21
21
  }
22
22
  </script>
23
23
 
24
- <div
25
- :show="open"
26
- role="dialog"
27
- aria-modal="true"
28
- aria-label="Notification delivery details"
29
- class="fixed inset-0 overflow-y-auto z-30 dashboard-modal-layer"
24
+ <Modal
25
+ :isOpen="open()"
26
+ :title="`Delivery attempt #${delivery()?.id || 'not recorded'}`"
27
+ :description="delivery()?.channel || 'Notification'"
28
+ size="lg"
29
+ @close="emit('close')"
30
30
  >
31
- <div class="flex items-end justify-center sm:items-center p-4 sm:p-0 min-h-full text-center">
32
- <button
33
- type="button"
34
- aria-label="Close notification delivery details"
35
- class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
36
- @click="emit('close')"
37
- ></button>
38
-
39
- <section class="overflow-hidden relative pb-4 pt-5 px-4 sm:my-8 sm:p-6 sm:max-w-2xl sm:w-full text-left bg-white dark:bg-neutral-800 rounded-lg shadow-xl transition-all transform">
40
- <div class="flex gap-4 items-start justify-between">
41
- <div>
42
- <p class="font-medium text-blue-600 text-xs tracking-wide uppercase dark:text-blue-400">{{ delivery?.channel || 'notification' }}</p>
43
- <h3 class="mt-1 font-semibold leading-6 text-gray-900 text-lg dark:text-white">Delivery attempt #{{ delivery?.id || '' }}</h3>
44
- </div>
45
- <Button variant="ghost" iconOnly ariaLabel="Close notification delivery details" @click="emit('close')">
46
- <span class="block h-5 w-5 i-hugeicons-cancel-01"></span>
47
- </Button>
48
- </div>
49
-
50
- <dl class="grid gap-x-6 gap-y-4 grid-cols-1 sm:grid-cols-2 mt-6">
51
- <div>
52
- <dt class="font-medium text-gray-500 text-xs uppercase dark:text-neutral-400">Recipient</dt>
53
- <dd class="mt-1 break-all text-gray-900 text-sm dark:text-neutral-100">{{ delivery?.recipient || 'Not recorded' }}</dd>
54
- </div>
55
- <div>
56
- <dt class="font-medium text-gray-500 text-xs uppercase dark:text-neutral-400">Attempted</dt>
57
- <dd class="mt-1 text-gray-900 text-sm dark:text-neutral-100">{{ formatDate(delivery?.sent_at) }}</dd>
58
- </div>
59
- <div class="sm:col-span-2">
60
- <dt class="font-medium text-gray-500 text-xs uppercase dark:text-neutral-400">Subject</dt>
61
- <dd class="mt-1 text-gray-900 text-sm dark:text-neutral-100">{{ delivery?.subject || 'No subject' }}</dd>
62
- </div>
63
- <div class="sm:col-span-2">
64
- <dt class="font-medium text-gray-500 text-xs uppercase dark:text-neutral-400">Body</dt>
65
- <dd class="mt-1 p-3 text-gray-800 text-sm whitespace-pre-wrap dark:text-neutral-200 bg-gray-50 dark:bg-neutral-900 rounded-md">{{ delivery?.body || 'No body was recorded.' }}</dd>
66
- </div>
67
- <div :if="delivery?.error" class="sm:col-span-2">
68
- <dt class="font-medium text-red-600 text-xs uppercase dark:text-red-400">Error</dt>
69
- <dd class="mt-1 p-3 text-red-800 text-sm whitespace-pre-wrap dark:text-red-200 bg-red-50 dark:bg-red-950/40 rounded-md">{{ delivery?.error }}</dd>
70
- </div>
71
- <div class="sm:col-span-2">
72
- <dt class="font-medium text-gray-500 text-xs uppercase dark:text-neutral-400">Metadata</dt>
73
- <dd class="overflow-x-auto mt-1 p-3 font-mono text-gray-700 text-xs whitespace-pre dark:text-neutral-300 bg-gray-50 dark:bg-neutral-900 rounded-md">{{ metadataText() }}</dd>
74
- </div>
75
- </dl>
76
-
77
- <div class="flex justify-end mt-6">
78
- <Button variant="secondary" @click="emit('close')">
79
- Close
80
- </Button>
81
- </div>
82
- </section>
83
- </div>
84
- </div>
31
+ <dl class="grid gap-x-6 gap-y-4 grid-cols-1 sm:grid-cols-2">
32
+ <div>
33
+ <dt class="font-medium text-gray-500 text-xs uppercase dark:text-neutral-400">Recipient</dt>
34
+ <dd class="mt-1 break-all text-gray-900 text-sm dark:text-neutral-100">{{ delivery()?.recipient || 'Not recorded' }}</dd>
35
+ </div>
36
+ <div>
37
+ <dt class="font-medium text-gray-500 text-xs uppercase dark:text-neutral-400">Attempted</dt>
38
+ <dd class="mt-1 text-gray-900 text-sm dark:text-neutral-100">{{ formatDate(delivery()?.sent_at) }}</dd>
39
+ </div>
40
+ <div class="sm:col-span-2">
41
+ <dt class="font-medium text-gray-500 text-xs uppercase dark:text-neutral-400">Subject</dt>
42
+ <dd class="mt-1 text-gray-900 text-sm dark:text-neutral-100">{{ delivery()?.subject || 'No subject' }}</dd>
43
+ </div>
44
+ <div class="sm:col-span-2">
45
+ <dt class="font-medium text-gray-500 text-xs uppercase dark:text-neutral-400">Body</dt>
46
+ <dd class="mt-1 p-3 text-gray-800 text-sm whitespace-pre-wrap dark:text-neutral-200 bg-gray-50 dark:bg-neutral-900 rounded-md">{{ delivery()?.body || 'No body was recorded.' }}</dd>
47
+ </div>
48
+ <div :if="delivery()?.error" class="sm:col-span-2">
49
+ <dt class="font-medium text-red-600 text-xs uppercase dark:text-red-400">Error</dt>
50
+ <dd class="mt-1 p-3 text-red-800 text-sm whitespace-pre-wrap dark:text-red-200 bg-red-50 dark:bg-red-950/40 rounded-md">{{ delivery()?.error }}</dd>
51
+ </div>
52
+ <div class="sm:col-span-2">
53
+ <dt class="font-medium text-gray-500 text-xs uppercase dark:text-neutral-400">Metadata</dt>
54
+ <dd class="overflow-x-auto mt-1 p-3 font-mono text-gray-700 text-xs whitespace-pre dark:text-neutral-300 bg-gray-50 dark:bg-neutral-900 rounded-md">{{ metadataText() }}</dd>
55
+ </div>
56
+ </dl>
57
+ <template #footer>
58
+ <div class="flex justify-end w-full">
59
+ <Button variant="secondary" @click="emit('close')">
60
+ Close
61
+ </Button>
62
+ </div>
63
+ </template>
64
+ </Modal>
@@ -15,6 +15,7 @@ interface ButtonProps {
15
15
  rel?: string
16
16
  ariaLabel?: string
17
17
  type?: string
18
+ form?: string
18
19
  class?: string
19
20
  }
20
21
 
@@ -83,6 +84,7 @@ const liveDownload = useReactiveProp('download', '')
83
84
  const liveTarget = useReactiveProp('target', '')
84
85
  const liveRel = useReactiveProp('rel', '')
85
86
  const liveType = useReactiveProp('type', 'button')
87
+ const liveForm = useReactiveProp('form', '')
86
88
  const liveAriaLabel = useReactiveProp('ariaLabel', '')
87
89
  </script>
88
90
  @if(tag === 'a' || (href && href.length > 0))
@@ -106,6 +108,7 @@ const liveAriaLabel = useReactiveProp('ariaLabel', '')
106
108
  @if(interaction === 'toggle')
107
109
  <button
108
110
  :type="liveType()"
111
+ :form="liveForm() || undefined"
109
112
  class="{{ buttonClass }}"
110
113
  :aria-label="liveAriaLabel() || undefined"
111
114
  :aria-busy="liveLoading() ? 'true' : 'false'"
@@ -120,6 +123,7 @@ const liveAriaLabel = useReactiveProp('ariaLabel', '')
120
123
  @else
121
124
  <button
122
125
  :type="liveType()"
126
+ :form="liveForm() || undefined"
123
127
  class="{{ buttonClass }}"
124
128
  :aria-label="liveAriaLabel() || undefined"
125
129
  :aria-busy="liveLoading() ? 'true' : 'false'"
@@ -9,6 +9,7 @@ const cancelLabel = useReactiveProp('cancelLabel', 'Cancel')
9
9
  const variant = useReactiveProp('variant', 'danger' as ConfirmVariant)
10
10
  const busy = useReactiveProp('busy', false)
11
11
  const disabled = useReactiveProp('disabled', false)
12
+ const warning = useReactiveProp('warning', '')
12
13
  const error = useReactiveProp('error', '')
13
14
  const emit = defineEmits()
14
15
 
@@ -51,6 +52,9 @@ function iconName(): string {
51
52
 
52
53
  <template #footer>
53
54
  <div class="w-full">
55
+ <div :if="warning()" role="note" class="mb-4 p-3 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">
56
+ {{ warning() }}
57
+ </div>
54
58
  <div :if="error()" role="alert" class="mb-4 p-3 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">
55
59
  {{ error() }}
56
60
  </div>
@@ -0,0 +1,114 @@
1
+ <script server>
2
+ interface DrawerProps {
3
+ isOpen?: boolean
4
+ title?: string
5
+ description?: string
6
+ size?: 'sm' | 'md' | 'lg'
7
+ closable?: boolean
8
+ }
9
+
10
+ const { isOpen = false, title = '', description = '', size = 'md', closable = true } = defineProps<DrawerProps>()
11
+
12
+ const sizeClasses = {
13
+ sm: 'max-w-sm',
14
+ md: 'max-w-lg',
15
+ lg: 'max-w-2xl',
16
+ }
17
+ </script>
18
+
19
+ <script client>
20
+ const open = useReactiveProp('isOpen', false)
21
+ const liveClosable = useReactiveProp('closable', true)
22
+ const liveTitle = useReactiveProp('title', '')
23
+ const liveDescription = useReactiveProp('description', '')
24
+ const dialog = useRef<HTMLDialogElement>('dashboardDrawerDialog')
25
+ const activeElement = useActiveElement()
26
+ const scrollLocked = useScrollLock()
27
+ const emit = defineEmits()
28
+ let restoreFocus: HTMLElement | null = null
29
+
30
+ function close(): void {
31
+ if (liveClosable())
32
+ emit('close')
33
+ }
34
+
35
+ function handleCancel(event: Event): void {
36
+ event.preventDefault()
37
+ close()
38
+ }
39
+
40
+ function handleBackdropClick(event: MouseEvent): void {
41
+ if (event.target === event.currentTarget)
42
+ close()
43
+ }
44
+
45
+ effect(() => {
46
+ const isOpen = open()
47
+ scrollLocked.value = isOpen
48
+
49
+ if (isOpen) {
50
+ const focused = peek(() => activeElement.value)
51
+ if (focused instanceof HTMLElement)
52
+ restoreFocus = focused
53
+ void nextTick(() => {
54
+ const element = dialog.current
55
+ if (!element)
56
+ return
57
+ if (!element.open)
58
+ element.showModal()
59
+ element.focus()
60
+ })
61
+ return
62
+ }
63
+
64
+ if (dialog.current?.open)
65
+ dialog.current.close()
66
+ const target = restoreFocus
67
+ restoreFocus = null
68
+ if (target)
69
+ void nextTick(() => target.focus())
70
+ })
71
+ </script>
72
+
73
+ <dialog
74
+ :if="open()"
75
+ ref="dashboardDrawerDialog"
76
+ role="dialog"
77
+ aria-modal="true"
78
+ :aria-label="liveTitle() || 'Drawer'"
79
+ class="fixed inset-0 z-[55] m-0 p-0 h-dvh max-h-none max-w-none w-screen bg-transparent dashboard-drawer-dialog dashboard-modal-layer"
80
+ @cancel="handleCancel"
81
+ >
82
+ <div class="flex justify-end min-h-full" @click="handleBackdropClick">
83
+ <section class="flex relative flex-col h-dvh w-full bg-white dark:bg-neutral-950 border-neutral-200 border-l dark:border-neutral-800 shadow-2xl {{ sizeClasses[size] }}" @click.stop>
84
+ <header class="flex gap-4 items-start px-5 py-4 border-b border-neutral-200 dark:border-neutral-800">
85
+ <slot name="header-leading" />
86
+ <div class="flex-1 min-w-0">
87
+ <h2 :if="liveTitle()" class="font-semibold text-lg text-neutral-900 truncate dark:text-white">{{ liveTitle() }}</h2>
88
+ <p :if="liveDescription()" class="mt-1 text-neutral-500 text-sm dark:text-neutral-400">{{ liveDescription() }}</p>
89
+ </div>
90
+ <slot name="header-trailing" />
91
+ <Button :if="liveClosable()" variant="ghost" size="sm" iconOnly ariaLabel="Close drawer" @click="close">
92
+ <span aria-hidden="true" class="h-5 w-5 i-hugeicons-cancel-01"></span>
93
+ </Button>
94
+ </header>
95
+
96
+ <div class="overflow-y-auto flex-1 min-h-0">
97
+ <slot />
98
+ </div>
99
+
100
+ @if($slots.footer)
101
+ <footer class="px-5 py-4 border-neutral-200 border-t dark:border-neutral-800">
102
+ <slot name="footer" />
103
+ </footer>
104
+ @endif
105
+ </section>
106
+ </div>
107
+ </dialog>
108
+
109
+ <style>
110
+ .dashboard-drawer-dialog::backdrop {
111
+ background: rgb(0 0 0 / 45%);
112
+ backdrop-filter: blur(2px);
113
+ }
114
+ </style>
@@ -58,7 +58,11 @@ effect(() => {
58
58
  if (!element) return
59
59
  if (!element.open)
60
60
  element.showModal()
61
- element.focus()
61
+ const autofocusTarget = element.querySelector<HTMLElement>('[autofocus]:not([disabled])')
62
+ if (autofocusTarget)
63
+ autofocusTarget.focus()
64
+ else
65
+ element.focus()
62
66
  })
63
67
  return
64
68
  }
@@ -77,7 +81,7 @@ effect(() => {
77
81
  :role="liveRole()"
78
82
  aria-modal="true"
79
83
  :aria-label="liveTitle() || 'Dialog'"
80
- class="fixed inset-0 m-0 p-0 h-dvh max-h-none w-screen max-w-none overflow-y-auto z-[55] bg-transparent dashboard-modal-layer dashboard-modal-dialog"
84
+ class="fixed inset-0 overflow-y-auto z-[55] m-0 p-0 h-dvh max-h-none max-w-none w-screen bg-transparent dashboard-modal-dialog dashboard-modal-layer"
81
85
  @cancel="handleCancel"
82
86
  >
83
87
  <!-- Modal container -->
@@ -99,16 +103,19 @@ effect(() => {
99
103
  </div>
100
104
  </div>
101
105
 
102
- <Button
103
- :if="liveClosable()"
104
- variant="ghost"
105
- size="sm"
106
- iconOnly
107
- ariaLabel="Close modal"
108
- @click="closeModal"
109
- >
110
- <div class="h-5 w-5 i-hugeicons-cancel-01"></div>
111
- </Button>
106
+ <div class="flex flex-shrink-0 gap-2 items-center">
107
+ <slot name="header-trailing" />
108
+ <Button
109
+ :if="liveClosable()"
110
+ variant="ghost"
111
+ size="sm"
112
+ iconOnly
113
+ ariaLabel="Close modal"
114
+ @click="closeModal"
115
+ >
116
+ <div class="h-5 w-5 i-hugeicons-cancel-01"></div>
117
+ </Button>
118
+ </div>
112
119
  </div>
113
120
 
114
121
  <!-- Body -->
@@ -1,19 +1,18 @@
1
1
  <script>
2
2
  interface ComboboxDemoProps {
3
- listItems?: string
3
+ listItems?: Array<{ id: number, name: string }>
4
4
  selected?: any
5
5
  query?: string
6
6
  isOpen?: boolean
7
7
  }
8
8
 
9
- const { listItems = [, selected = null, query = '', isOpen = false } = defineProps<ComboboxDemoProps>()
10
-
11
- { id: 1, name: 'Chris Breuer' },
9
+ const { listItems = [
10
+ { id: 1, name: 'Chris Breuer' },
12
11
  { id: 2, name: 'Avery Hill' },
13
12
  { id: 3, name: 'Glenn Michael' },
14
13
  { id: 4, name: 'Michael Vincent' },
15
14
  { id: 5, name: 'Blake Ayer' },
16
- ]
15
+ ], selected = null, query = '', isOpen = false } = defineProps<ComboboxDemoProps>()
17
16
  function filterItems(items, query) {
18
17
  if (query === '') return items
19
18
  return items.filter(item => item.name.toLowerCase().includes(query.toLowerCase()))