@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
@@ -32,6 +32,12 @@ interface SeoEntry extends PostRow {
32
32
  score: number
33
33
  }
34
34
 
35
+ interface SeoDraft {
36
+ focusKeyword: string | null
37
+ metaDescription: string | null
38
+ canonicalUrl: string | null
39
+ }
40
+
35
41
  const entries = state<SeoEntry[]>([])
36
42
  const loading = state(true)
37
43
  const saving = state(false)
@@ -40,9 +46,6 @@ const editError = state('')
40
46
  const searchQuery = state('')
41
47
  const scoreFilter = state<'all' | 'good' | 'medium' | 'poor'>('all')
42
48
  const editingEntry = state<SeoEntry | null>(null)
43
- const editFocusKeyword = state('')
44
- const editMetaDescription = state('')
45
- const editCanonical = state('')
46
49
 
47
50
  function seoScore(post: PostRow): number {
48
51
  const title = String(post.title || '').trim()
@@ -143,9 +146,6 @@ function formatDate(value?: string | null): string {
143
146
 
144
147
  function openEdit(entry: SeoEntry): void {
145
148
  editingEntry.set(entry)
146
- editFocusKeyword.set(entry.focus_keyword || '')
147
- editMetaDescription.set(entry.meta_description || '')
148
- editCanonical.set(entry.canonical_url || '')
149
149
  editError.set('')
150
150
  }
151
151
 
@@ -156,7 +156,7 @@ function closeEdit(): void {
156
156
  editError.set('')
157
157
  }
158
158
 
159
- async function saveEdit(): Promise<void> {
159
+ async function saveEdit(draft: SeoDraft): Promise<void> {
160
160
  const entry = editingEntry()
161
161
  if (!entry || saving())
162
162
  return
@@ -170,9 +170,9 @@ async function saveEdit(): Promise<void> {
170
170
  method: 'PATCH',
171
171
  body: {
172
172
  fields: {
173
- focus_keyword: editFocusKeyword().trim() || null,
174
- meta_description: editMetaDescription().trim() || null,
175
- canonical_url: editCanonical().trim() || null,
173
+ focus_keyword: draft.focusKeyword,
174
+ meta_description: draft.metaDescription,
175
+ canonical_url: draft.canonicalUrl,
176
176
  },
177
177
  },
178
178
  },
@@ -311,79 +311,13 @@ void loadEntries()
311
311
  @endif
312
312
  </Card>
313
313
 
314
- <div
315
- :if="editingEntry()"
316
- class="flex fixed inset-0 z-50 items-center justify-center p-4 bg-black/40 dashboard-modal-layer"
317
- @click="closeEdit()"
318
- >
319
- <form
320
- role="dialog"
321
- aria-modal="true"
322
- aria-labelledby="seo-edit-title"
323
- class="overflow-hidden max-w-lg w-full bg-white dark:bg-neutral-900 rounded-xl shadow-2xl"
324
- @click.stop
325
- @submit.prevent="saveEdit()"
326
- >
327
- <div class="flex items-center justify-between px-5 py-4 border-b border-neutral-200/60 dark:border-white/10">
328
- <div>
329
- <h2 id="seo-edit-title" class="font-semibold text-neutral-900 dark:text-white">Edit SEO metadata</h2>
330
- <p class="mt-1 text-neutral-500 text-xs dark:text-neutral-400">{{ editingEntry()?.title }}</p>
331
- </div>
332
- <Button variant="ghost" size="sm" iconOnly ariaLabel="Close SEO editor" @click="closeEdit()">
333
- <span class="block h-4 w-4 i-hugeicons-cancel-01"></span>
334
- </Button>
335
- </div>
336
-
337
- <div class="p-5 space-y-4">
338
- @if(editError())
339
- <div role="alert" class="px-3 py-2 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-500/10 border border-red-200 rounded-lg dark:border-red-500/20">
340
- {{ editError() }}
341
- </div>
342
- @endif
343
-
344
- <div>
345
- <label for="seo-focus-keyword" class="block font-medium text-neutral-700 text-sm dark:text-neutral-300">Focus keyword</label>
346
- <input
347
- id="seo-focus-keyword"
348
- type="text"
349
- maxlength="100"
350
- x-model="editFocusKeyword"
351
- class="block mt-1.5 px-3 py-2 w-full text-neutral-900 text-sm dark:text-white bg-white dark:bg-white/5 border border-black/10 rounded-lg dark:border-white/15 focus:border-blue-500/50 focus:outline-none focus:ring-2 focus:ring-blue-500/30"
352
- >
353
- </div>
354
- <div>
355
- <div class="flex items-center justify-between">
356
- <label for="seo-meta-description" class="block font-medium text-neutral-700 text-sm dark:text-neutral-300">Meta description</label>
357
- <span class="tabular-nums text-neutral-400 text-xs">{{ editMetaDescription().length }}/160</span>
358
- </div>
359
- <textarea
360
- id="seo-meta-description"
361
- rows="4"
362
- maxlength="160"
363
- x-model="editMetaDescription"
364
- class="block mt-1.5 px-3 py-2 w-full text-neutral-900 text-sm dark:text-white bg-white dark:bg-white/5 border border-black/10 rounded-lg dark:border-white/15 focus:border-blue-500/50 focus:outline-none focus:ring-2 focus:ring-blue-500/30"
365
- ></textarea>
366
- </div>
367
- <div>
368
- <label for="seo-canonical-url" class="block font-medium text-neutral-700 text-sm dark:text-neutral-300">Canonical URL</label>
369
- <input
370
- id="seo-canonical-url"
371
- type="url"
372
- placeholder="https://example.com/post"
373
- x-model="editCanonical"
374
- class="block mt-1.5 px-3 py-2 w-full text-neutral-900 text-sm dark:text-white placeholder:text-neutral-400 bg-white dark:bg-white/5 border border-black/10 rounded-lg dark:border-white/15 focus:border-blue-500/50 focus:outline-none focus:ring-2 focus:ring-blue-500/30"
375
- >
376
- </div>
377
- </div>
378
-
379
- <div class="flex gap-2 items-center justify-end px-5 py-4 bg-neutral-50 dark:bg-white/[0.03] border-neutral-200/60 border-t dark:border-white/10">
380
- <Button type="button" variant="secondary" size="sm" :disabled="saving()" @click="closeEdit()">Cancel</Button>
381
- <Button type="submit" variant="primary" size="sm" :disabled="saving()">
382
- <span :if="!saving()">Save metadata</span>
383
- <span :if="saving()">Saving...</span>
384
- </Button>
385
- </div>
386
- </form>
387
- </div>
314
+ <SeoEditorDialog
315
+ :open="Boolean(editingEntry())"
316
+ :entry="editingEntry()"
317
+ :busy="saving()"
318
+ :error="editError()"
319
+ @close="closeEdit"
320
+ @submit="saveEdit"
321
+ />
388
322
  </div>
389
323
  </template>
@@ -0,0 +1,108 @@
1
+ <script client>
2
+ interface SeoEntry {
3
+ title: string
4
+ focus_keyword?: string | null
5
+ meta_description?: string | null
6
+ canonical_url?: string | null
7
+ }
8
+
9
+ interface SeoDraft {
10
+ focusKeyword: string | null
11
+ metaDescription: string | null
12
+ canonicalUrl: string | null
13
+ }
14
+
15
+ const open = useReactiveProp('open', false)
16
+ const entry = useReactiveProp('entry', null as SeoEntry | null)
17
+ const busy = useReactiveProp('busy', false)
18
+ const error = useReactiveProp('error', '')
19
+ const focusKeyword = state('')
20
+ const metaDescription = state('')
21
+ const canonicalUrl = state('')
22
+ const emit = defineEmits()
23
+ let wasOpen = false
24
+
25
+ effect(() => {
26
+ const isOpen = open()
27
+ if (isOpen && !wasOpen) {
28
+ const record = entry()
29
+ focusKeyword.set(record?.focus_keyword || '')
30
+ metaDescription.set(record?.meta_description || '')
31
+ canonicalUrl.set(record?.canonical_url || '')
32
+ }
33
+ wasOpen = isOpen
34
+ })
35
+
36
+ function close(): void {
37
+ if (!busy())
38
+ emit('close')
39
+ }
40
+
41
+ function submit(): void {
42
+ const draft: SeoDraft = {
43
+ focusKeyword: focusKeyword().trim() || null,
44
+ metaDescription: metaDescription().trim() || null,
45
+ canonicalUrl: canonicalUrl().trim() || null,
46
+ }
47
+ emit('submit', draft)
48
+ }
49
+ </script>
50
+
51
+ <Modal
52
+ :isOpen="open()"
53
+ title="Edit SEO metadata"
54
+ :description="entry()?.title || ''"
55
+ :closable="!busy()"
56
+ @close="close"
57
+ >
58
+ <form id="seo-editor-form" class="space-y-4" @submit.prevent="submit">
59
+ <div>
60
+ <label for="seo-focus-keyword" class="block font-medium text-neutral-700 text-sm dark:text-neutral-300">Focus keyword</label>
61
+ <input
62
+ id="seo-focus-keyword"
63
+ name="focus_keyword"
64
+ type="text"
65
+ maxlength="100"
66
+ x-model="focusKeyword"
67
+ class="block mt-1.5 px-3 py-2 w-full text-neutral-900 text-sm dark:text-white bg-white dark:bg-white/5 border border-black/10 rounded-lg dark:border-white/15 focus:border-blue-500/50 focus:outline-none focus:ring-2 focus:ring-blue-500/30"
68
+ >
69
+ </div>
70
+ <div>
71
+ <div class="flex items-center justify-between">
72
+ <label for="seo-meta-description" class="block font-medium text-neutral-700 text-sm dark:text-neutral-300">Meta description</label>
73
+ <span class="tabular-nums text-neutral-400 text-xs">{{ metaDescription().length }}/160</span>
74
+ </div>
75
+ <textarea
76
+ id="seo-meta-description"
77
+ name="meta_description"
78
+ rows="4"
79
+ maxlength="160"
80
+ x-model="metaDescription"
81
+ class="block mt-1.5 px-3 py-2 w-full text-neutral-900 text-sm dark:text-white bg-white dark:bg-white/5 border border-black/10 rounded-lg dark:border-white/15 focus:border-blue-500/50 focus:outline-none focus:ring-2 focus:ring-blue-500/30"
82
+ ></textarea>
83
+ </div>
84
+ <div>
85
+ <label for="seo-canonical-url" class="block font-medium text-neutral-700 text-sm dark:text-neutral-300">Canonical URL</label>
86
+ <input
87
+ id="seo-canonical-url"
88
+ name="canonical_url"
89
+ type="url"
90
+ inputmode="url"
91
+ placeholder="https://example.com/post"
92
+ x-model="canonicalUrl"
93
+ class="block mt-1.5 px-3 py-2 w-full text-neutral-900 text-sm dark:text-white placeholder:text-neutral-400 bg-white dark:bg-white/5 border border-black/10 rounded-lg dark:border-white/15 focus:border-blue-500/50 focus:outline-none focus:ring-2 focus:ring-blue-500/30"
94
+ >
95
+ </div>
96
+ </form>
97
+
98
+ <div :if="error()" role="alert" class="mt-4 px-3 py-2 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-500/10 border border-red-200 rounded-lg dark:border-red-500/20">
99
+ {{ error() }}
100
+ </div>
101
+
102
+ <template #footer>
103
+ <div class="flex gap-2 items-center justify-end w-full">
104
+ <Button type="button" variant="secondary" :disabled="busy()" @click="close">Cancel</Button>
105
+ <Button type="submit" form="seo-editor-form" :loading="busy()">Save metadata</Button>
106
+ </div>
107
+ </template>
108
+ </Modal>
@@ -0,0 +1,106 @@
1
+ <script client>
2
+ interface TaxonomyDraft {
3
+ name: string
4
+ slug: string
5
+ description: string
6
+ }
7
+
8
+ const open = useReactiveProp('open', false)
9
+ const kind = useReactiveProp('kind', 'category')
10
+ const busy = useReactiveProp('busy', false)
11
+ const error = useReactiveProp('error', '')
12
+ const name = state('')
13
+ const slug = state('')
14
+ const description = state('')
15
+ const slugTouched = state(false)
16
+ const emit = defineEmits()
17
+ let wasOpen = false
18
+
19
+ function slugify(value: string): string {
20
+ return value
21
+ .normalize('NFKD')
22
+ .replace(/[\u0300-\u036F]/g, '')
23
+ .trim()
24
+ .toLowerCase()
25
+ .replace(/['"]/g, '')
26
+ .replace(/[^a-z0-9]+/g, '-')
27
+ .replace(/^-+|-+$/g, '')
28
+ }
29
+
30
+ effect(() => {
31
+ const isOpen = open()
32
+ if (isOpen && !wasOpen) {
33
+ name.set('')
34
+ slug.set('')
35
+ description.set('')
36
+ slugTouched.set(false)
37
+ }
38
+ wasOpen = isOpen
39
+ })
40
+
41
+ effect(() => {
42
+ if (!open() || slugTouched())
43
+ return
44
+ slug.set(slugify(name()))
45
+ })
46
+
47
+ function markSlugTouched(): void {
48
+ slugTouched.set(true)
49
+ }
50
+
51
+ function normalizeSlug(): void {
52
+ const normalized = slugify(slug())
53
+ if (normalized !== slug())
54
+ slug.set(normalized)
55
+ }
56
+
57
+ function close(): void {
58
+ if (!busy())
59
+ emit('close')
60
+ }
61
+
62
+ function submit(): void {
63
+ const draft: TaxonomyDraft = {
64
+ name: name().trim(),
65
+ slug: slugify(slug()),
66
+ description: description().trim(),
67
+ }
68
+ emit('submit', draft)
69
+ }
70
+ </script>
71
+
72
+ <Modal
73
+ :isOpen="open()"
74
+ :title="'New ' + kind()"
75
+ description="Add a reusable classification for your published content."
76
+ :closable="!busy()"
77
+ @close="close"
78
+ >
79
+ <form id="taxonomy-editor-form" class="grid gap-4" @submit.prevent="submit">
80
+ <label>
81
+ <span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Name</span>
82
+ <input x-model="name" name="name" required maxlength="255" class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" />
83
+ </label>
84
+ <label>
85
+ <span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Slug</span>
86
+ <input x-model="slug" name="slug" required maxlength="255" class="mt-1 px-3 py-2 w-full font-mono text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" @input="markSlugTouched" @blur="normalizeSlug" />
87
+ </label>
88
+ <label>
89
+ <span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Description</span>
90
+ <textarea x-model="description" name="description" rows="3" maxlength="1000" class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700"></textarea>
91
+ </label>
92
+ </form>
93
+
94
+ <div :if="error()" role="alert" class="mt-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">
95
+ {{ error() }}
96
+ </div>
97
+
98
+ <template #footer>
99
+ <div class="flex gap-2 justify-end w-full">
100
+ <Button :disabled="busy()" variant="secondary" @click="close">Cancel</Button>
101
+ <Button type="submit" form="taxonomy-editor-form" :disabled="!name().trim() || !slug().trim()" :loading="busy()">
102
+ Create {{ kind() }}
103
+ </Button>
104
+ </div>
105
+ </template>
106
+ </Modal>
@@ -63,87 +63,76 @@ function close(): void {
63
63
  }
64
64
  </script>
65
65
 
66
- <template :if="open()">
67
- <div class="fixed inset-0 overflow-y-auto z-[55] dashboard-modal-layer" @keydown.escape="close">
68
- <button type="button" class="fixed inset-0 w-full bg-black/50 backdrop-blur-sm cursor-default" aria-label="Close create record dialog" @click="close"></button>
69
-
70
- <div class="flex relative items-center justify-center p-4 min-h-full">
71
- <section role="dialog" aria-modal="true" :aria-label="'Create ' + modelName()" class="overflow-hidden relative max-w-lg w-full bg-white dark:bg-neutral-900 rounded-xl shadow-2xl">
72
- <form @submit.prevent="submit">
73
- <header class="flex gap-4 items-start justify-between px-5 py-4 bg-neutral-50/80 dark:bg-neutral-800/80 border-b border-neutral-200 dark:border-neutral-700">
74
- <div>
75
- <h2 class="font-semibold text-base text-neutral-900 dark:text-white">
76
- Create {{ modelName() }}
77
- </h2>
78
- <p class="mt-0.5 text-neutral-500 text-xs dark:text-neutral-400">
79
- Fields come from the model's fillable attributes and useApi store contract.
80
- </p>
81
- </div>
82
- <Button type="button" variant="ghost" size="sm" iconOnly ariaLabel="Close create record dialog" @click="close">
83
- <span class="block h-4 w-4 i-hugeicons-cancel-01"></span>
84
- </Button>
85
- </header>
86
-
87
- <div class="overflow-y-auto px-5 py-4 space-y-4 max-h-[65vh]">
88
- <div :if="error()" role="alert" class="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">
89
- {{ error() }}
90
- </div>
91
-
92
- <template :for="field in fields()">
93
- <label :if="field.type === 'boolean'" class="flex gap-3 items-start p-3 border border-neutral-200 rounded-lg dark:border-neutral-700">
94
- <input
95
- type="checkbox"
96
- :checked="Boolean(values()[field.name])"
97
- :disabled="busy()"
98
- class="mt-0.5 h-4 w-4 text-blue-600 border-neutral-300 rounded"
99
- @change="setValue(field.name, $event.target.checked)"
100
- />
101
- <span>
102
- <span class="block font-medium text-neutral-700 text-sm dark:text-neutral-200">{{ field.label }}</span>
103
- <span :if="fieldError(field.name)" class="block mt-0.5 text-red-600 text-xs dark:text-red-400">{{ fieldError(field.name) }}</span>
104
- </span>
105
- </label>
106
-
107
- <Select
108
- :if="field.type === 'enum'"
109
- :value="fieldValue(field.name)"
110
- :options="field.options"
111
- :label="field.label"
112
- :ariaLabel="field.label"
113
- :required="field.required"
114
- :disabled="busy()"
115
- :error="fieldError(field.name)"
116
- @update:value="setValue(field.name, $event)"
117
- />
118
-
119
- <Input
120
- :if="field.type !== 'boolean' && field.type !== 'enum'"
121
- :value="fieldValue(field.name)"
122
- :type="inputType(field)"
123
- :label="field.label"
124
- :ariaLabel="field.label"
125
- :required="field.required"
126
- :disabled="busy()"
127
- :error="fieldError(field.name)"
128
- @update:value="setValue(field.name, $event)"
129
- />
130
- </template>
131
-
132
- <p :if="fields().length === 0" class="py-8 text-center text-neutral-500 text-sm dark:text-neutral-400">
133
- This model exposes no safe fillable fields.
134
- </p>
135
- </div>
136
-
137
- <footer class="flex gap-2 items-center justify-end px-5 py-4 bg-neutral-50/50 dark:bg-neutral-800/50 border-neutral-200 border-t dark:border-neutral-700">
138
- <Button :disabled="busy()" variant="secondary" @click="close">
139
- Cancel
140
- </Button>
141
- <Button type="submit" :disabled="busy() || fields().length === 0" :loading="busy()">
142
- {{ busy() ? 'Creating' : `Create ${modelName()}` }}
143
- </Button>
144
- </footer>
145
- </form>
146
- </section>
66
+ <Modal
67
+ :isOpen="open()"
68
+ :title="'Create ' + modelName()"
69
+ description="Fields come from the model's fillable attributes and useApi store contract."
70
+ :closable="!busy()"
71
+ @close="close"
72
+ >
73
+ <form id="create-record-form" @submit.prevent="submit">
74
+ <div class="overflow-y-auto space-y-4 max-h-[65vh]">
75
+ <div :if="error()" role="alert" class="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">
76
+ {{ error() }}
77
+ </div>
78
+
79
+ <template :for="field in fields()">
80
+ <label :if="field.type === 'boolean'" class="flex gap-3 items-start p-3 border border-neutral-200 rounded-lg dark:border-neutral-700">
81
+ <input
82
+ :name="field.name"
83
+ type="checkbox"
84
+ :checked="Boolean(values()[field.name])"
85
+ :disabled="busy()"
86
+ class="mt-0.5 h-4 w-4 text-blue-600 border-neutral-300 rounded"
87
+ @change="setValue(field.name, $event.target.checked)"
88
+ />
89
+ <span>
90
+ <span class="block font-medium text-neutral-700 text-sm dark:text-neutral-200">{{ field.label }}</span>
91
+ <span :if="fieldError(field.name)" class="block mt-0.5 text-red-600 text-xs dark:text-red-400">{{ fieldError(field.name) }}</span>
92
+ </span>
93
+ </label>
94
+
95
+ <Select
96
+ :if="field.type === 'enum'"
97
+ :name="field.name"
98
+ :value="fieldValue(field.name)"
99
+ :options="field.options"
100
+ :label="field.label"
101
+ :ariaLabel="field.label"
102
+ :required="field.required"
103
+ :disabled="busy()"
104
+ :error="fieldError(field.name)"
105
+ @update:value="setValue(field.name, $event)"
106
+ />
107
+
108
+ <Input
109
+ :if="field.type !== 'boolean' && field.type !== 'enum'"
110
+ :name="field.name"
111
+ :value="fieldValue(field.name)"
112
+ :type="inputType(field)"
113
+ :label="field.label"
114
+ :ariaLabel="field.label"
115
+ :required="field.required"
116
+ :disabled="busy()"
117
+ :error="fieldError(field.name)"
118
+ @update:value="setValue(field.name, $event)"
119
+ />
120
+ </template>
121
+
122
+ <p :if="fields().length === 0" class="py-8 text-center text-neutral-500 text-sm dark:text-neutral-400">
123
+ This model exposes no safe fillable fields.
124
+ </p>
125
+ </div>
126
+ </form>
127
+
128
+ <template #footer>
129
+ <div class="flex gap-2 items-center justify-end w-full">
130
+ <Button :disabled="busy()" variant="secondary" @click="close">
131
+ Cancel
132
+ </Button>
133
+ <Button type="submit" form="create-record-form" :disabled="busy() || fields().length === 0" :loading="busy()">
134
+ {{ busy() ? 'Creating' : `Create ${modelName()}` }}
135
+ </Button>
147
136
  </div>
148
- </div>
149
- </template>
137
+ </template>
138
+ </Modal>
@@ -68,7 +68,7 @@ function listClass(): string {
68
68
  <p class="mt-1 text-gray-500 text-xs truncate dark:text-neutral-400">{{ email.preview }}</p>
69
69
  </div>
70
70
  <div class="flex flex-shrink-0 gap-2 items-center">
71
- <span :if="email.hasAttachments" class="h-4 w-4 text-gray-400 i-hugeicons-attachment-01" title="Has attachments"></span>
71
+ <span :if="email.hasAttachments" class="h-4 w-4 text-gray-400 i-hugeicons-attachment-01" x-tooltip="Has attachments" title="Has attachments"></span>
72
72
  <time class="text-gray-500 text-xs dark:text-neutral-400" :datetime="email.date">{{ formatDate(email.date) }}</time>
73
73
  </div>
74
74
  </div>
@@ -10,7 +10,6 @@ const isOpen = state(false)
10
10
  const selectedIndex = state(0)
11
11
  const error = state('')
12
12
  const searchInput = useRef('globalSearchInput')
13
- let searchTimer: ReturnType<typeof setTimeout> | null = null
14
13
  let searchRequest: AbortController | null = null
15
14
 
16
15
  function flatResults(): SearchResultItem[] {
@@ -74,13 +73,9 @@ async function runSearch(value: string): Promise<void> {
74
73
  }
75
74
  }
76
75
 
77
- function scheduleSearch(value: string): void {
78
- if (searchTimer) clearTimeout(searchTimer)
79
- searchTimer = setTimeout(() => {
80
- searchTimer = null
81
- void runSearch(value)
82
- }, 250)
83
- }
76
+ const scheduleSearch = useDebounce((value: string) => {
77
+ void runSearch(value)
78
+ }, 250)
84
79
 
85
80
  function moveUp(): void {
86
81
  const items = flatResults()
@@ -137,7 +132,6 @@ function handleGlobalKeydown(event: KeyboardEvent): void {
137
132
  useEventListener('keydown', handleGlobalKeydown)
138
133
 
139
134
  onDestroy(() => {
140
- if (searchTimer) clearTimeout(searchTimer)
141
135
  searchRequest?.abort()
142
136
  })
143
137
  </script>
@@ -6,19 +6,7 @@ const item = useReactiveProp('item', null as DashboardCloudResource | null)
6
6
  const emit = defineEmits()
7
7
  </script>
8
8
 
9
- <div :if="open() && item()" class="flex fixed inset-0 z-50 items-center justify-center p-4 dashboard-modal-layer">
10
- <button type="button" aria-label="Close cloud resource details" class="absolute inset-0 bg-black/45" @click="emit('close')"></button>
11
- <section role="dialog" aria-modal="true" aria-labelledby="cloud-resource-title" class="overflow-y-auto relative max-h-[90vh] max-w-lg w-full bg-white dark:bg-neutral-900 border border-neutral-200 rounded-xl dark:border-neutral-700 shadow-xl">
12
- <header class="flex gap-4 items-start justify-between px-5 py-4 border-b border-neutral-200 dark:border-neutral-700">
13
- <div>
14
- <p class="capitalize text-neutral-500 text-xs dark:text-neutral-400">{{ item()?.category }} resource</p>
15
- <h2 id="cloud-resource-title" class="mt-1 font-semibold text-lg text-neutral-900 dark:text-white">{{ item()?.name }}</h2>
16
- </div>
17
- <Button variant="ghost" iconOnly ariaLabel="Close cloud resource details" @click="emit('close')">
18
- <span class="block h-5 w-5 i-hugeicons-cancel-01"></span>
19
- </Button>
20
- </header>
21
- <div class="p-5">
9
+ <Modal :isOpen="open() && Boolean(item())" :title="item()?.name || 'Cloud resource'" :description="`${item()?.category || 'Cloud'} resource`" size="md" @close="emit('close')">
22
10
  <div class="flex gap-3 items-center">
23
11
  <CloudStatusBadge :status="item()?.status || 'inactive'" />
24
12
  <span class="capitalize text-neutral-500 text-sm dark:text-neutral-400">{{ item()?.type }}</span>
@@ -31,6 +19,4 @@ const emit = defineEmits()
31
19
  </div>
32
20
  </dl>
33
21
  <p :if="(item()?.details || []).length === 0" class="mt-5 p-3 text-neutral-500 text-sm dark:text-neutral-400 bg-neutral-50 dark:bg-neutral-800 rounded-lg">No additional configuration fields are recorded for this resource.</p>
34
- </div>
35
- </section>
36
- </div>
22
+ </Modal>
@@ -10,25 +10,13 @@ function ttlLabel(): string {
10
10
  }
11
11
  </script>
12
12
 
13
- <div :if="open() && record()" class="flex fixed inset-0 z-50 items-center justify-center p-4 dashboard-modal-layer">
14
- <button type="button" aria-label="Close DNS record details" class="absolute inset-0 bg-black/45" @click="emit('close')"></button>
15
- <section role="dialog" aria-modal="true" aria-labelledby="dns-record-title" class="overflow-y-auto relative max-h-[90vh] max-w-lg w-full bg-white dark:bg-neutral-900 border border-neutral-200 rounded-xl dark:border-neutral-700 shadow-xl">
16
- <header class="flex gap-4 items-start justify-between px-5 py-4 border-b border-neutral-200 dark:border-neutral-700">
17
- <div>
18
- <p class="text-neutral-500 text-xs dark:text-neutral-400">Configured DNS record</p>
19
- <h2 id="dns-record-title" class="mt-1 font-semibold text-lg text-neutral-900 dark:text-white">{{ record()?.type }} · {{ record()?.name }}</h2>
20
- </div>
21
- <Button variant="ghost" iconOnly ariaLabel="Close DNS record details" @click="emit('close')">
22
- <span class="block h-5 w-5 i-hugeicons-cancel-01"></span>
23
- </Button>
24
- </header>
25
- <dl class="grid gap-5 sm:grid-cols-2 p-5 text-sm">
13
+ <Modal :isOpen="open() && Boolean(record())" :title="`${record()?.type || 'DNS'} · ${record()?.name || 'record'}`" description="Configured DNS record" size="md" @close="emit('close')">
14
+ <dl class="grid gap-5 sm:grid-cols-2 text-sm">
26
15
  <div><dt class="text-neutral-500 dark:text-neutral-400">Type</dt><dd class="mt-1 font-semibold text-neutral-900 dark:text-white">{{ record()?.type }}</dd></div>
27
16
  <div><dt class="text-neutral-500 dark:text-neutral-400">Name</dt><dd class="mt-1 break-all font-mono text-neutral-900 dark:text-white">{{ record()?.name }}</dd></div>
28
17
  <div class="sm:col-span-2"><dt class="text-neutral-500 dark:text-neutral-400">Value</dt><dd class="mt-1 break-all font-mono text-neutral-900 dark:text-white">{{ record()?.value || 'Not set' }}</dd></div>
29
18
  <div><dt class="text-neutral-500 dark:text-neutral-400">TTL</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ ttlLabel() }}</dd></div>
30
19
  <div :if="record()?.priority !== null"><dt class="text-neutral-500 dark:text-neutral-400">Priority</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ record()?.priority }}</dd></div>
31
20
  </dl>
32
- <p class="mb-5 mx-5 p-3 text-neutral-500 text-xs dark:text-neutral-400 bg-neutral-50 dark:bg-neutral-800 rounded-lg">Source: config/dns.ts</p>
33
- </section>
34
- </div>
21
+ <p class="mt-5 p-3 text-neutral-500 text-xs dark:text-neutral-400 bg-neutral-50 dark:bg-neutral-800 rounded-lg">Source: config/dns.ts</p>
22
+ </Modal>