@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
@@ -0,0 +1,93 @@
1
+ <script client>
2
+ interface PageRecord {
3
+ id: string
4
+ title: string
5
+ template: string
6
+ publishedAt: string
7
+ }
8
+
9
+ interface PageDraft {
10
+ title: string
11
+ template: string
12
+ published: boolean
13
+ }
14
+
15
+ const open = useReactiveProp('open', false)
16
+ const page = useReactiveProp('page', null as PageRecord | null)
17
+ const busy = useReactiveProp('busy', false)
18
+ const error = useReactiveProp('error', '')
19
+ const title = state('')
20
+ const template = state('default')
21
+ const published = state(false)
22
+ const emit = defineEmits()
23
+ let wasOpen = false
24
+
25
+ effect(() => {
26
+ const isOpen = open()
27
+ if (isOpen && !wasOpen) {
28
+ const record = page()
29
+ title.set(record?.title || '')
30
+ template.set(record?.template || 'default')
31
+ published.set(Boolean(record?.publishedAt))
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: PageDraft = {
43
+ title: title().trim(),
44
+ template: template(),
45
+ published: published(),
46
+ }
47
+ emit('submit', draft)
48
+ }
49
+ </script>
50
+
51
+ <Modal
52
+ :isOpen="open()"
53
+ :title="page() ? 'Edit page' : 'New page'"
54
+ description="Configure the page template and publishing state."
55
+ :closable="!busy()"
56
+ @close="close"
57
+ >
58
+ <form id="page-editor-form" class="grid gap-4" @submit.prevent="submit">
59
+ <label>
60
+ <span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Title</span>
61
+ <input x-model="title" name="title" required minlength="3" 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" />
62
+ </label>
63
+ <label>
64
+ <span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Template</span>
65
+ <select x-model="template" name="template" 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">
66
+ <option value="default">Default</option>
67
+ <option value="landing">Landing</option>
68
+ <option value="blog">Blog</option>
69
+ <option value="contact">Contact</option>
70
+ </select>
71
+ </label>
72
+ <label class="flex gap-3 items-start p-3 bg-gray-50 dark:bg-neutral-900/50 border border-gray-200 rounded-lg dark:border-neutral-700">
73
+ <input x-model="published" name="published" type="checkbox" class="mt-0.5 h-4 w-4 rounded" />
74
+ <span>
75
+ <span class="block font-medium text-gray-700 text-sm dark:text-neutral-200">Published</span>
76
+ <span class="block mt-1 text-gray-500 text-xs dark:text-neutral-400">Published pages receive a timestamp. Clearing this returns the page to draft.</span>
77
+ </span>
78
+ </label>
79
+ </form>
80
+
81
+ <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">
82
+ {{ error() }}
83
+ </div>
84
+
85
+ <template #footer>
86
+ <div class="flex gap-2 justify-end w-full">
87
+ <Button :disabled="busy()" variant="secondary" @click="close">Cancel</Button>
88
+ <Button type="submit" form="page-editor-form" :disabled="title().trim().length < 3" :loading="busy()">
89
+ {{ page() ? 'Save changes' : 'Create page' }}
90
+ </Button>
91
+ </div>
92
+ </template>
93
+ </Modal>
@@ -11,15 +11,18 @@ interface PageRecord {
11
11
  publishedAt: string
12
12
  }
13
13
 
14
+ interface PageDraft {
15
+ title: string
16
+ template: string
17
+ published: boolean
18
+ }
19
+
14
20
  const pages = state<PageRecord[]>([])
15
21
  const loading = state(true)
16
22
  const loadError = state('')
17
23
  const search = state('')
18
24
  const formOpen = state(false)
19
25
  const editingPage = state<PageRecord | null>(null)
20
- const title = state('')
21
- const template = state('default')
22
- const published = state(false)
23
26
  const saving = state(false)
24
27
  const saveError = state('')
25
28
  const deletingPage = state<PageRecord | null>(null)
@@ -82,21 +85,15 @@ async function loadPages(): Promise<void> {
82
85
  }
83
86
  }
84
87
 
85
- function resetForm(page: PageRecord | null): void {
86
- editingPage.set(page)
87
- title.set(page?.title || '')
88
- template.set(page?.template || 'default')
89
- published.set(Boolean(page?.publishedAt))
90
- saveError.set('')
91
- }
92
-
93
88
  function openCreate(): void {
94
- resetForm(null)
89
+ editingPage.set(null)
90
+ saveError.set('')
95
91
  formOpen.set(true)
96
92
  }
97
93
 
98
94
  function openEdit(page: PageRecord): void {
99
- resetForm(page)
95
+ editingPage.set(page)
96
+ saveError.set('')
100
97
  formOpen.set(true)
101
98
  }
102
99
 
@@ -105,14 +102,14 @@ function closeForm(): void {
105
102
  formOpen.set(false)
106
103
  }
107
104
 
108
- async function savePage(): Promise<void> {
105
+ async function savePage(draft: PageDraft): Promise<void> {
109
106
  const page = editingPage()
110
107
  saving.set(true)
111
108
  saveError.set('')
112
109
  try {
113
110
  await dashboardApi(page ? `/api/dashboard/pages/${page.id}` : '/api/dashboard/pages', {
114
111
  method: page ? 'PATCH' : 'POST',
115
- body: { title: title().trim(), template: template(), published: published() },
112
+ body: draft,
116
113
  })
117
114
  formOpen.set(false)
118
115
  pushToast('success', page ? 'Page updated' : 'Page created')
@@ -136,6 +133,11 @@ function closeDelete(): void {
136
133
  deletingPage.set(null)
137
134
  }
138
135
 
136
+ function deleteDescription(): string {
137
+ const page = deletingPage()
138
+ return page ? `${page.title} will be permanently removed.` : ''
139
+ }
140
+
139
141
  async function deletePage(): Promise<void> {
140
142
  const page = deletingPage()
141
143
  if (!page)
@@ -186,14 +188,22 @@ onMount(() => {
186
188
  </section>
187
189
  </div>
188
190
 
189
- <div :if="formOpen()" class="flex fixed inset-y-0 right-0 z-[55] items-center justify-center p-4 dashboard-modal-layer">
190
- <button type="button" aria-label="Close page dialog" class="absolute inset-0 bg-black/45" @click="closeForm()"></button>
191
- <section role="dialog" aria-modal="true" :aria-label="editingPage() ? 'Edit page' : 'New page'" class="relative max-w-lg w-full bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700 shadow-xl">
192
- <form @submit.prevent="savePage"><div class="p-6"><h2 class="font-semibold text-gray-900 text-lg dark:text-white">{{ editingPage() ? 'Edit page' : 'New page' }}</h2><div :if="saveError()" 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">{{ saveError() }}</div><div class="grid gap-4 mt-5"><label><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Title</span><input x-model="title" required minlength="3" 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" /></label><label><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Template</span><select x-model="template" 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"><option value="default">Default</option><option value="landing">Landing</option><option value="blog">Blog</option><option value="contact">Contact</option></select></label><label class="flex gap-3 items-start p-3 bg-gray-50 dark:bg-neutral-900/50 border border-gray-200 rounded-lg dark:border-neutral-700"><input x-model="published" type="checkbox" class="mt-0.5 h-4 w-4 rounded" /><span><span class="block font-medium text-gray-700 text-sm dark:text-neutral-200">Published</span><span class="block mt-1 text-gray-500 text-xs dark:text-neutral-400">Published pages receive a timestamp. Clearing this returns the page to draft.</span></span></label></div></div><footer class="flex gap-2 justify-end px-6 py-4 bg-gray-50 dark:bg-neutral-900/50 border-gray-200 border-t dark:border-neutral-700"><Button :disabled="saving()" variant="secondary" @click="closeForm()">Cancel</Button><Button type="submit" :disabled="saving() || title().trim().length < 3" :loading="saving()">{{ editingPage() ? 'Save changes' : 'Create page' }}</Button></footer></form>
193
- </section>
194
- </div>
195
-
196
- <div :if="Boolean(deletingPage())" class="flex fixed inset-y-0 right-0 z-[55] items-center justify-center p-4 dashboard-modal-layer">
197
- <button type="button" aria-label="Close delete page dialog" class="absolute inset-0 bg-black/45" @click="closeDelete()"></button>
198
- <section role="dialog" aria-modal="true" aria-label="Delete page?" 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"><h2 class="font-semibold text-gray-900 text-lg dark:text-white">Delete page?</h2><p class="mt-2 text-gray-500 text-sm dark:text-neutral-400"><strong>{{ deletingPage()?.title || '' }}</strong> will be permanently removed.</p><div :if="deleteError()" 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">{{ deleteError() }}</div><div class="flex gap-2 justify-end mt-6"><Button :disabled="deleting()" variant="secondary" @click="closeDelete()">Cancel</Button><Button :loading="deleting()" variant="danger" @click="deletePage()">Delete page</Button></div></section>
199
- </div>
191
+ <PageEditorDialog
192
+ :open="formOpen()"
193
+ :page="editingPage()"
194
+ :busy="saving()"
195
+ :error="saveError()"
196
+ @close="closeForm"
197
+ @submit="savePage"
198
+ />
199
+
200
+ <ConfirmDialog
201
+ :isOpen="Boolean(deletingPage())"
202
+ title="Delete page?"
203
+ :description="deleteDescription()"
204
+ confirmLabel="Delete page"
205
+ :busy="deleting()"
206
+ :error="deleteError()"
207
+ @close="closeDelete"
208
+ @confirm="deletePage"
209
+ />
@@ -0,0 +1,176 @@
1
+ <script client>
2
+ type PostStatus = 'draft' | 'published' | 'archived'
3
+ interface OptionRecord { id: string, name: string, slug: string }
4
+ interface AuthorRecord { id: string, name: string, email: string }
5
+ interface PostRecord {
6
+ id: string
7
+ title: string
8
+ excerpt: string
9
+ content: string
10
+ poster: string
11
+ status: PostStatus
12
+ authorId: string
13
+ featured: boolean
14
+ categoryIds: string[]
15
+ tagIds: string[]
16
+ }
17
+
18
+ interface PostDraft {
19
+ title: string
20
+ excerpt: string
21
+ content: string
22
+ poster: string
23
+ status: PostStatus
24
+ authorId: string | null
25
+ featured: boolean
26
+ categoryIds: string[]
27
+ tagIds: string[]
28
+ }
29
+
30
+ const open = useReactiveProp('open', false)
31
+ const post = useReactiveProp('post', null as PostRecord | null)
32
+ const categories = useReactiveProp('categories', [] as OptionRecord[])
33
+ const tags = useReactiveProp('tags', [] as OptionRecord[])
34
+ const authors = useReactiveProp('authors', [] as AuthorRecord[])
35
+ const busy = useReactiveProp('busy', false)
36
+ const error = useReactiveProp('error', '')
37
+ const title = state('')
38
+ const excerpt = state('')
39
+ const content = state('')
40
+ const poster = state('')
41
+ const status = state<PostStatus>('draft')
42
+ const authorId = state('')
43
+ const featured = state(false)
44
+ const categoryIds = state<string[]>([])
45
+ const tagIds = state<string[]>([])
46
+ const emit = defineEmits()
47
+ let wasOpen = false
48
+
49
+ effect(() => {
50
+ const isOpen = open()
51
+ if (isOpen && !wasOpen) {
52
+ const record = post()
53
+ title.set(record?.title || '')
54
+ excerpt.set(record?.excerpt || '')
55
+ content.set(record?.content || '')
56
+ poster.set(record?.poster || '')
57
+ status.set(record?.status || 'draft')
58
+ authorId.set(record?.authorId || '')
59
+ featured.set(Boolean(record?.featured))
60
+ categoryIds.set(record ? [...record.categoryIds] : [])
61
+ tagIds.set(record ? [...record.tagIds] : [])
62
+ }
63
+ wasOpen = isOpen
64
+ })
65
+
66
+ function valid(): boolean {
67
+ const excerptLength = excerpt().trim().length
68
+ return title().trim().length >= 3 && title().trim().length <= 255
69
+ && content().trim().length >= 10 && content().length <= 100000
70
+ && (excerptLength === 0 || excerptLength >= 10) && excerpt().length <= 500
71
+ }
72
+
73
+ function close(): void {
74
+ if (!busy())
75
+ emit('close')
76
+ }
77
+
78
+ function submit(): void {
79
+ const draft: PostDraft = {
80
+ title: title().trim(),
81
+ excerpt: excerpt().trim(),
82
+ content: content().trim(),
83
+ poster: poster().trim(),
84
+ status: status(),
85
+ authorId: authorId() || null,
86
+ featured: featured(),
87
+ categoryIds: [...categoryIds()],
88
+ tagIds: [...tagIds()],
89
+ }
90
+ emit('submit', draft)
91
+ }
92
+ </script>
93
+
94
+ <Modal
95
+ :isOpen="open()"
96
+ :title="post() ? 'Edit post' : 'New post'"
97
+ description="Content, ownership, and taxonomies are persisted together."
98
+ size="xl"
99
+ :closable="!busy()"
100
+ @close="close"
101
+ >
102
+ <form id="post-editor-form" class="grid gap-5" @submit.prevent="submit">
103
+ <label>
104
+ <span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Title</span>
105
+ <input x-model.trim="title" name="title" required minlength="3" 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" />
106
+ </label>
107
+ <div class="grid gap-4 sm:grid-cols-3">
108
+ <label>
109
+ <span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Status</span>
110
+ <select x-model="status" name="status" 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">
111
+ <option value="draft">Draft</option>
112
+ <option value="published">Published</option>
113
+ <option value="archived">Archived</option>
114
+ </select>
115
+ </label>
116
+ <label>
117
+ <span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Author</span>
118
+ <select x-model="authorId" name="author_id" 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">
119
+ <option value="">No author</option>
120
+ <template :for="author in authors()"><option :value="author.id">{{ author.name }} ({{ author.email }})</option></template>
121
+ </select>
122
+ </label>
123
+ <label class="flex gap-3 items-center self-end px-3 py-2 min-h-10 bg-gray-50 dark:bg-neutral-900/60 border border-gray-200 rounded-md dark:border-neutral-700">
124
+ <input x-model="featured" name="featured" type="checkbox" class="h-4 w-4 text-blue-600 border-gray-300 rounded" />
125
+ <span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Featured post</span>
126
+ </label>
127
+ </div>
128
+ <label>
129
+ <span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Poster URL</span>
130
+ <input x-model.trim="poster" name="poster" type="url" inputmode="url" maxlength="255" placeholder="https://example.com/poster.jpg" 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" />
131
+ </label>
132
+ <label>
133
+ <span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Excerpt</span>
134
+ <textarea x-model="excerpt" name="excerpt" rows="3" maxlength="500" placeholder="Optional, at least 10 characters when provided" 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>
135
+ <span class="block mt-1 text-gray-400 text-xs">{{ excerpt().length }}/500</span>
136
+ </label>
137
+ <label>
138
+ <span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Content</span>
139
+ <textarea x-model="content" name="content" required minlength="10" maxlength="100000" rows="10" 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"></textarea>
140
+ <span class="block mt-1 text-gray-400 text-xs">{{ content().length }}/100000</span>
141
+ </label>
142
+ <div class="grid gap-4 lg:grid-cols-2">
143
+ <fieldset class="p-4 border border-gray-200 rounded-lg dark:border-neutral-700">
144
+ <legend class="px-1 font-semibold text-gray-700 text-sm dark:text-neutral-200">Categories, {{ categoryIds().length }} selected</legend>
145
+ <div :if="categories().length === 0" class="text-gray-500 text-sm dark:text-neutral-400">No categories available.</div>
146
+ <div :else class="grid gap-2">
147
+ <template :for="categoryOption in categories()">
148
+ <label class="flex gap-2 items-center"><input x-model="categoryIds" name="category_ids" type="checkbox" :value="categoryOption.id" class="h-4 w-4 text-blue-600 border-gray-300 rounded" /><span class="text-gray-700 text-sm dark:text-neutral-200">{{ categoryOption.name }}</span></label>
149
+ </template>
150
+ </div>
151
+ </fieldset>
152
+ <fieldset class="p-4 border border-gray-200 rounded-lg dark:border-neutral-700">
153
+ <legend class="px-1 font-semibold text-gray-700 text-sm dark:text-neutral-200">Tags, {{ tagIds().length }} selected</legend>
154
+ <div :if="tags().length === 0" class="text-gray-500 text-sm dark:text-neutral-400">No tags available.</div>
155
+ <div :else class="grid gap-2 sm:grid-cols-2">
156
+ <template :for="tag in tags()">
157
+ <label class="flex gap-2 items-center"><input x-model="tagIds" name="tag_ids" type="checkbox" :value="tag.id" class="h-4 w-4 text-blue-600 border-gray-300 rounded" /><span class="text-gray-700 text-sm dark:text-neutral-200">{{ tag.name }}</span></label>
158
+ </template>
159
+ </div>
160
+ </fieldset>
161
+ </div>
162
+ </form>
163
+
164
+ <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">
165
+ {{ error() }}
166
+ </div>
167
+
168
+ <template #footer>
169
+ <div class="flex gap-2 justify-end w-full">
170
+ <Button :disabled="busy()" variant="secondary" @click="close">Cancel</Button>
171
+ <Button type="submit" form="post-editor-form" :disabled="!valid()" :loading="busy()">
172
+ {{ post() ? 'Save changes' : 'Create post' }}
173
+ </Button>
174
+ </div>
175
+ </template>
176
+ </Modal>
@@ -25,6 +25,17 @@ interface PostRecord {
25
25
  categoryIds: string[]
26
26
  tagIds: string[]
27
27
  }
28
+ interface PostDraft {
29
+ title: string
30
+ excerpt: string
31
+ content: string
32
+ poster: string
33
+ status: PostStatus
34
+ authorId: string | null
35
+ featured: boolean
36
+ categoryIds: string[]
37
+ tagIds: string[]
38
+ }
28
39
 
29
40
  const posts = state<PostRecord[]>([])
30
41
  const categories = state<OptionRecord[]>([])
@@ -37,15 +48,6 @@ const statusFilter = state('all')
37
48
 
38
49
  const formOpen = state(false)
39
50
  const editingPost = state<PostRecord | null>(null)
40
- const title = state('')
41
- const excerpt = state('')
42
- const content = state('')
43
- const poster = state('')
44
- const status = state<PostStatus>('draft')
45
- const authorId = state('')
46
- const featured = state(false)
47
- const categoryIds = state<string[]>([])
48
- const tagIds = state<string[]>([])
49
51
  const saving = state(false)
50
52
  const saveError = state('')
51
53
  const deletingPost = state<PostRecord | null>(null)
@@ -151,51 +153,25 @@ async function loadPosts(): Promise<void> {
151
153
  }
152
154
  }
153
155
 
154
- function resetForm(post: PostRecord | null): void {
155
- editingPost.set(post)
156
- title.set(post?.title || '')
157
- excerpt.set(post?.excerpt || '')
158
- content.set(post?.content || '')
159
- poster.set(post?.poster || '')
160
- status.set(post?.status || 'draft')
161
- authorId.set(post?.authorId || '')
162
- featured.set(Boolean(post?.featured))
163
- categoryIds.set(post ? [...post.categoryIds] : [])
164
- tagIds.set(post ? [...post.tagIds] : [])
165
- saveError.set('')
166
- }
167
-
168
- function openCreate(): void { resetForm(null); formOpen.set(true) }
169
- function openEdit(post: PostRecord): void { resetForm(post); formOpen.set(true) }
156
+ function openCreate(): void { editingPost.set(null); saveError.set(''); formOpen.set(true) }
157
+ function openEdit(post: PostRecord): void { editingPost.set(post); saveError.set(''); formOpen.set(true) }
170
158
  function closeForm(): void { if (!saving()) formOpen.set(false) }
171
159
  function openDelete(post: PostRecord): void { deletingPost.set(post); deleteError.set('') }
172
160
  function closeDelete(): void { if (!deleting()) deletingPost.set(null) }
173
161
 
174
- function formValid(): boolean {
175
- const excerptLength = excerpt().trim().length
176
- return title().trim().length >= 3 && title().trim().length <= 255
177
- && content().trim().length >= 10 && content().length <= 100000
178
- && (excerptLength === 0 || excerptLength >= 10) && excerpt().length <= 500
162
+ function deleteDescription(): string {
163
+ const post = deletingPost()
164
+ return post ? `${post.title} and its category and tag links will be permanently removed.` : ''
179
165
  }
180
166
 
181
- async function savePost(): Promise<void> {
167
+ async function savePost(draft: PostDraft): Promise<void> {
182
168
  const post = editingPost()
183
169
  saving.set(true)
184
170
  saveError.set('')
185
171
  try {
186
172
  await dashboardApi(post ? `/api/dashboard/posts/${post.id}` : '/api/dashboard/posts', {
187
173
  method: post ? 'PATCH' : 'POST',
188
- body: {
189
- title: title().trim(),
190
- excerpt: excerpt().trim(),
191
- content: content().trim(),
192
- poster: poster().trim(),
193
- status: status(),
194
- authorId: authorId() || null,
195
- featured: featured(),
196
- categoryIds: categoryIds(),
197
- tagIds: tagIds(),
198
- },
174
+ body: draft,
199
175
  })
200
176
  formOpen.set(false)
201
177
  pushToast('success', post ? 'Post updated' : 'Post created')
@@ -252,30 +228,30 @@ onMount(() => { void loadPosts() })
252
228
  <div class="overflow-x-auto"><table class="min-w-full divide-gray-200 divide-y dark:divide-neutral-700"><thead class="bg-gray-50 dark:bg-neutral-900/50"><tr><th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Post</th><th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Taxonomy</th><th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Publishing</th><th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Views</th><th class="px-4 py-3"><span class="sr-only">Actions</span></th></tr></thead><tbody class="divide-gray-200 divide-y dark:divide-neutral-700">
253
229
  <template :if="loading()"><tr><td colspan="5" class="px-4 py-12 text-center text-gray-500 text-sm dark:text-neutral-400"><span class="inline-block mr-2 h-4 w-4 animate-spin i-hugeicons-loading-03"></span>Loading posts</td></tr></template>
254
230
  <template :else-if="visiblePosts().length === 0"><tr><td colspan="5" class="px-4 py-14 text-center"><span class="inline-block h-6 w-6 text-gray-400 i-hugeicons-quill-write-02"></span><p class="mt-2 font-medium text-gray-900 text-sm dark:text-white">No posts found</p><p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Adjust the filters or create a post.</p></td></tr></template>
255
- <template :else><template :for="post in visiblePosts()"><tr><td class="px-4 py-3 max-w-md"><div class="flex gap-2 items-center"><p class="font-medium text-gray-900 text-sm truncate dark:text-white">{{ post.title }}</p><span :if="post.featured" title="Featured" class="flex-shrink-0 h-4 w-4 text-amber-500 i-hugeicons-star"></span></div><p class="mt-1 text-gray-500 text-xs truncate dark:text-neutral-400">{{ post.excerpt || post.content }}</p><p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ authorName(post) }}</p></td><td class="px-4 py-3 max-w-xs text-gray-500 text-xs dark:text-neutral-400"><p class="truncate"><span class="font-medium text-gray-700 dark:text-neutral-200">Categories:</span> {{ optionNames(post.categoryIds, categories()) }}</p><p class="mt-1 truncate"><span class="font-medium text-gray-700 dark:text-neutral-200">Tags:</span> {{ optionNames(post.tagIds, tags()) }}</p></td><td class="px-4 py-3"><span :class="statusClass(post.status)">{{ post.status }}</span><p class="mt-2 text-gray-400 text-xs dark:text-neutral-500">{{ formatDate(post.publishedAt) }}</p></td><td class="px-4 py-3 text-gray-500 text-sm dark:text-neutral-400">{{ formatNumber(post.views) }}</td><td class="px-4 py-3"><div class="flex justify-end"><Button variant="ghost" size="sm" iconOnly :ariaLabel="'Edit post ' + post.title" @click="openEdit(post)"><span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-02"></span></Button><Button variant="ghost" size="sm" iconOnly :ariaLabel="'Delete post ' + post.title" @click="openDelete(post)"><span class="block h-4 w-4 text-red-500 i-hugeicons-delete-02"></span></Button></div></td></tr></template></template>
231
+ <template :else><template :for="post in visiblePosts()"><tr><td class="px-4 py-3 max-w-md"><div class="flex gap-2 items-center"><p class="font-medium text-gray-900 text-sm truncate dark:text-white">{{ post.title }}</p><span :if="post.featured" x-tooltip="Featured" title="Featured" class="flex-shrink-0 h-4 w-4 text-amber-500 i-hugeicons-star"></span></div><p class="mt-1 text-gray-500 text-xs truncate dark:text-neutral-400">{{ post.excerpt || post.content }}</p><p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ authorName(post) }}</p></td><td class="px-4 py-3 max-w-xs text-gray-500 text-xs dark:text-neutral-400"><p class="truncate"><span class="font-medium text-gray-700 dark:text-neutral-200">Categories:</span> {{ optionNames(post.categoryIds, categories()) }}</p><p class="mt-1 truncate"><span class="font-medium text-gray-700 dark:text-neutral-200">Tags:</span> {{ optionNames(post.tagIds, tags()) }}</p></td><td class="px-4 py-3"><span :class="statusClass(post.status)">{{ post.status }}</span><p class="mt-2 text-gray-400 text-xs dark:text-neutral-500">{{ formatDate(post.publishedAt) }}</p></td><td class="px-4 py-3 text-gray-500 text-sm dark:text-neutral-400">{{ formatNumber(post.views) }}</td><td class="px-4 py-3"><div class="flex justify-end"><Button variant="ghost" size="sm" iconOnly :ariaLabel="'Edit post ' + post.title" @click="openEdit(post)"><span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-02"></span></Button><Button variant="ghost" size="sm" iconOnly :ariaLabel="'Delete post ' + post.title" @click="openDelete(post)"><span class="block h-4 w-4 text-red-500 i-hugeicons-delete-02"></span></Button></div></td></tr></template></template>
256
232
  </tbody></table></div>
257
233
  </section>
258
234
  </div>
259
235
 
260
- <div :if="formOpen()" class="flex fixed inset-y-0 right-0 z-[55] items-center justify-center p-4 dashboard-modal-layer">
261
- <button type="button" aria-label="Close post editor" class="absolute inset-0 bg-black/45" @click="closeForm()"></button>
262
- <section role="dialog" aria-modal="true" :aria-label="editingPost() ? 'Edit post' : 'New post'" class="flex relative flex-col max-h-[calc(100dvh-2rem)] max-w-4xl w-full bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700 shadow-xl">
263
- <form class="flex flex-col min-h-0" @submit.prevent="savePost()">
264
- <header class="px-6 py-4 border-b border-gray-200 dark:border-neutral-700"><h2 class="font-semibold text-gray-900 text-lg dark:text-white">{{ editingPost() ? 'Edit post' : 'New post' }}</h2><p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Content, ownership, and taxonomies are persisted together.</p></header>
265
- <div class="overflow-y-auto p-6"><div :if="saveError()" 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">{{ saveError() }}</div><div class="grid gap-5">
266
- <label><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Title</span><input x-model.trim="title" required minlength="3" 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" /></label>
267
- <div class="grid gap-4 sm:grid-cols-3"><label><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Status</span><select x-model="status" 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"><option value="draft">Draft</option><option value="published">Published</option><option value="archived">Archived</option></select></label><label><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Author</span><select x-model="authorId" 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"><option value="">No author</option><template :for="author in authors()"><option :value="author.id">{{ author.name }} ({{ author.email }})</option></template></select></label><label class="flex gap-3 items-center self-end px-3 py-2 min-h-10 bg-gray-50 dark:bg-neutral-900/60 border border-gray-200 rounded-md dark:border-neutral-700"><input x-model="featured" type="checkbox" class="h-4 w-4 text-blue-600 border-gray-300 rounded" /><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Featured post</span></label></div>
268
- <label><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Poster URL</span><input x-model.trim="poster" type="url" maxlength="255" placeholder="https://example.com/poster.jpg" 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" /></label>
269
- <label><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Excerpt</span><textarea x-model="excerpt" rows="3" maxlength="500" placeholder="Optional, at least 10 characters when provided" 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><span class="block mt-1 text-gray-400 text-xs">{{ excerpt().length }}/500</span></label>
270
- <label><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Content</span><textarea x-model="content" required minlength="10" maxlength="100000" rows="10" 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"></textarea><span class="block mt-1 text-gray-400 text-xs">{{ content().length }}/100000</span></label>
271
- <div class="grid gap-4 lg:grid-cols-2"><fieldset class="p-4 border border-gray-200 rounded-lg dark:border-neutral-700"><legend class="px-1 font-semibold text-gray-700 text-sm dark:text-neutral-200">Categories, {{ categoryIds().length }} selected</legend><div :if="categories().length === 0" class="text-gray-500 text-sm dark:text-neutral-400">No categories available.</div><div :else class="grid gap-2"><template :for="categoryOption in categories()"><label class="flex gap-2 items-center"><input x-model="categoryIds" type="checkbox" :value="categoryOption.id" class="h-4 w-4 text-blue-600 border-gray-300 rounded" /><span class="text-gray-700 text-sm dark:text-neutral-200">{{ categoryOption.name }}</span></label></template></div></fieldset><fieldset class="p-4 border border-gray-200 rounded-lg dark:border-neutral-700"><legend class="px-1 font-semibold text-gray-700 text-sm dark:text-neutral-200">Tags, {{ tagIds().length }} selected</legend><div :if="tags().length === 0" class="text-gray-500 text-sm dark:text-neutral-400">No tags available.</div><div :else class="grid gap-2 sm:grid-cols-2"><template :for="tag in tags()"><label class="flex gap-2 items-center"><input x-model="tagIds" type="checkbox" :value="tag.id" class="h-4 w-4 text-blue-600 border-gray-300 rounded" /><span class="text-gray-700 text-sm dark:text-neutral-200">{{ tag.name }}</span></label></template></div></fieldset></div>
272
- </div></div>
273
- <footer class="flex gap-2 justify-end px-6 py-4 bg-gray-50 dark:bg-neutral-900/50 border-gray-200 border-t dark:border-neutral-700"><Button :disabled="saving()" variant="secondary" @click="closeForm()">Cancel</Button><Button type="submit" :disabled="saving() || !formValid()" :loading="saving()">{{ editingPost() ? 'Save changes' : 'Create post' }}</Button></footer>
274
- </form>
275
- </section>
276
- </div>
236
+ <PostEditorDialog
237
+ :open="formOpen()"
238
+ :post="editingPost()"
239
+ :categories="categories()"
240
+ :tags="tags()"
241
+ :authors="authors()"
242
+ :busy="saving()"
243
+ :error="saveError()"
244
+ @close="closeForm"
245
+ @submit="savePost"
246
+ />
277
247
 
278
- <div :if="Boolean(deletingPost())" class="flex fixed inset-y-0 right-0 z-[55] items-center justify-center p-4 dashboard-modal-layer">
279
- <button type="button" aria-label="Close delete post dialog" class="absolute inset-0 bg-black/45" @click="closeDelete()"></button>
280
- <section role="dialog" aria-modal="true" aria-label="Delete post?" 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"><h2 class="font-semibold text-gray-900 text-lg dark:text-white">Delete post?</h2><p class="mt-2 text-gray-500 text-sm dark:text-neutral-400"><strong>{{ deletingPost()?.title || '' }}</strong> and its category and tag links will be permanently removed.</p><div :if="deleteError()" 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">{{ deleteError() }}</div><div class="flex gap-2 justify-end mt-6"><Button :disabled="deleting()" variant="secondary" @click="closeDelete()">Cancel</Button><Button :loading="deleting()" variant="danger" @click="deletePost()">Delete post</Button></div></section>
281
- </div>
248
+ <ConfirmDialog
249
+ :isOpen="Boolean(deletingPost())"
250
+ title="Delete post?"
251
+ :description="deleteDescription()"
252
+ confirmLabel="Delete post"
253
+ :busy="deleting()"
254
+ :error="deleteError()"
255
+ @close="closeDelete"
256
+ @confirm="deletePost"
257
+ />