@stacksjs/defaults 0.70.358 → 0.70.363

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/ai/skills/stacks-browse/SKILL.md +7 -5
  2. package/ai/skills/stacks-browse/scripts/browse.ts +68 -9
  3. package/ai/skills/stacks-dashboard/SKILL.md +4 -0
  4. package/app/Actions/AI/AskAction.ts +12 -17
  5. package/app/Actions/AI/SummaryAction.ts +12 -17
  6. package/app/Actions/Dashboard/Models/ModelShowAction.ts +4 -1
  7. package/app/Actions/Dashboard/Models/model-write.ts +5 -0
  8. package/app/Actions/LogAction.ts +11 -11
  9. package/ide/vscode/package.json +1 -1
  10. package/package.json +1 -1
  11. package/resources/components/Buttons/Counter.stx +1 -1
  12. package/resources/components/Buttons/ToggleDark.stx +1 -1
  13. package/resources/components/Dashboard/App/RequestDetailsDialog.stx +42 -0
  14. package/resources/components/Dashboard/App/RequestsOverview.stx +1 -21
  15. package/resources/components/Dashboard/Auth/AccessTokenCreateDialog.stx +155 -0
  16. package/resources/components/Dashboard/Auth/AccessTokens.stx +41 -129
  17. package/resources/components/Dashboard/Ci/CiDashboard.stx +1 -157
  18. package/resources/components/Dashboard/Ci/CiRunHistoryDrawer.stx +127 -0
  19. package/resources/components/Dashboard/Commerce/CommerceCategoryDeleteDialog.stx +10 -8
  20. package/resources/components/Dashboard/Commerce/CommerceCategoryDetailsDialog.stx +22 -17
  21. package/resources/components/Dashboard/Commerce/CommerceCategoryDialog.stx +14 -13
  22. package/resources/components/Dashboard/Commerce/CommerceCouponDeleteDialog.stx +10 -7
  23. package/resources/components/Dashboard/Commerce/CommerceCouponDetailsDialog.stx +25 -19
  24. package/resources/components/Dashboard/Commerce/CommerceCouponDialog.stx +14 -11
  25. package/resources/components/Dashboard/Commerce/CommerceCustomerDeleteDialog.stx +10 -8
  26. package/resources/components/Dashboard/Commerce/CommerceCustomerDetailsDialog.stx +24 -16
  27. package/resources/components/Dashboard/Commerce/CommerceCustomerDialog.stx +14 -13
  28. package/resources/components/Dashboard/Commerce/CommerceGiftCardDeleteDialog.stx +10 -8
  29. package/resources/components/Dashboard/Commerce/CommerceGiftCardDetailsDialog.stx +16 -14
  30. package/resources/components/Dashboard/Commerce/CommerceGiftCardDialog.stx +17 -16
  31. package/resources/components/Dashboard/Commerce/CommerceManufacturerDeleteDialog.stx +14 -16
  32. package/resources/components/Dashboard/Commerce/CommerceManufacturerDialog.stx +17 -16
  33. package/resources/components/Dashboard/Commerce/CommerceOrderDeleteDialog.stx +10 -8
  34. package/resources/components/Dashboard/Commerce/CommerceOrderDetailsDialog.stx +15 -10
  35. package/resources/components/Dashboard/Commerce/CommerceOrderDialog.stx +14 -13
  36. package/resources/components/Dashboard/Commerce/CommercePosCheckoutDialog.stx +30 -25
  37. package/resources/components/Dashboard/Commerce/CommercePosReceiptDialog.stx +27 -20
  38. package/resources/components/Dashboard/Commerce/CommerceProductDeleteDialog.stx +14 -14
  39. package/resources/components/Dashboard/Commerce/CommerceProductDialog.stx +14 -13
  40. package/resources/components/Dashboard/Commerce/CommerceReviewDeleteDialog.stx +10 -18
  41. package/resources/components/Dashboard/Commerce/CommerceReviewEditDialog.stx +17 -16
  42. package/resources/components/Dashboard/Commerce/CommerceTaxDeleteDialog.stx +14 -14
  43. package/resources/components/Dashboard/Commerce/CommerceTaxDialog.stx +17 -16
  44. package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +1 -1
  45. package/resources/components/Dashboard/Commerce/CommerceUnitDeleteDialog.stx +14 -17
  46. package/resources/components/Dashboard/Commerce/CommerceUnitDialog.stx +17 -16
  47. package/resources/components/Dashboard/Commerce/CommerceVariantDeleteDialog.stx +10 -8
  48. package/resources/components/Dashboard/Commerce/CommerceVariantDialog.stx +17 -16
  49. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDeleteDialog.stx +11 -15
  50. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDialog.stx +17 -16
  51. package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryDeleteDialog.stx +11 -15
  52. package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryDialog.stx +17 -16
  53. package/resources/components/Dashboard/Commerce/Delivery/DriverDeleteDialog.stx +11 -15
  54. package/resources/components/Dashboard/Commerce/Delivery/DriverDialog.stx +17 -16
  55. package/resources/components/Dashboard/Commerce/Delivery/LicenseKeyDeleteDialog.stx +11 -15
  56. package/resources/components/Dashboard/Commerce/Delivery/LicenseKeyDialog.stx +17 -16
  57. package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDeleteDialog.stx +11 -15
  58. package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDialog.stx +17 -16
  59. package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDeleteDialog.stx +11 -15
  60. package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDialog.stx +17 -16
  61. package/resources/components/Dashboard/Commerce/Delivery/ShippingZoneDeleteDialog.stx +11 -15
  62. package/resources/components/Dashboard/Commerce/Delivery/ShippingZoneDialog.stx +17 -16
  63. package/resources/components/Dashboard/Commerce/PaymentDetailsDialog.stx +16 -15
  64. package/resources/components/Dashboard/Commerce/PaymentRefundDialog.stx +16 -15
  65. package/resources/components/Dashboard/Commerce/PrintDeviceDeleteDialog.stx +10 -11
  66. package/resources/components/Dashboard/Commerce/PrintDeviceDetailsDialog.stx +16 -14
  67. package/resources/components/Dashboard/Commerce/PrintDeviceDialog.stx +17 -16
  68. package/resources/components/Dashboard/Commerce/PrintLogDeleteDialog.stx +10 -7
  69. package/resources/components/Dashboard/Commerce/PrintLogDetailsDialog.stx +13 -10
  70. package/resources/components/Dashboard/Commerce/ProductWaitlistDeleteDialog.stx +10 -18
  71. package/resources/components/Dashboard/Commerce/ProductWaitlistEntryDialog.stx +96 -105
  72. package/resources/components/Dashboard/Commerce/RestaurantWaitlistDeleteDialog.stx +10 -18
  73. package/resources/components/Dashboard/Commerce/RestaurantWaitlistEntryDialog.stx +92 -95
  74. package/resources/components/Dashboard/Content/AuthorEditorDialog.stx +94 -0
  75. package/resources/components/Dashboard/Content/AuthorsDashboard.stx +39 -31
  76. package/resources/components/Dashboard/Content/BlogDashboard.stx +87 -176
  77. package/resources/components/Dashboard/Content/BlogPostEditorDialog.stx +195 -0
  78. package/resources/components/Dashboard/Content/CommentsDashboard.stx +14 -4
  79. package/resources/components/Dashboard/Content/ContentTaxonomyDashboard.stx +40 -55
  80. package/resources/components/Dashboard/Content/CreateFolderDialog.stx +45 -0
  81. package/resources/components/Dashboard/Content/FileDetailsDrawer.stx +31 -0
  82. package/resources/components/Dashboard/Content/FileManagerDashboard.stx +47 -107
  83. package/resources/components/Dashboard/Content/FileUploadDialog.stx +89 -0
  84. package/resources/components/Dashboard/Content/PageEditorDialog.stx +93 -0
  85. package/resources/components/Dashboard/Content/PagesDashboard.stx +36 -26
  86. package/resources/components/Dashboard/Content/PostEditorDialog.stx +176 -0
  87. package/resources/components/Dashboard/Content/PostsDashboard.stx +40 -64
  88. package/resources/components/Dashboard/Content/SeoDashboard.stx +18 -84
  89. package/resources/components/Dashboard/Content/SeoEditorDialog.stx +108 -0
  90. package/resources/components/Dashboard/Content/TaxonomyEditorDialog.stx +106 -0
  91. package/resources/components/Dashboard/CreateRecordModal.stx +72 -83
  92. package/resources/components/Dashboard/Email/InboxMessageList.stx +1 -1
  93. package/resources/components/Dashboard/GlobalSearch.stx +3 -9
  94. package/resources/components/Dashboard/Infrastructure/CloudDetailsDialog.stx +2 -16
  95. package/resources/components/Dashboard/Infrastructure/DnsRecordDialog.stx +4 -16
  96. package/resources/components/Dashboard/Infrastructure/LogDetailsDialog.stx +4 -19
  97. package/resources/components/Dashboard/Infrastructure/MailboxDetailsDialog.stx +6 -16
  98. package/resources/components/Dashboard/Infrastructure/ServerInspectorDialog.stx +105 -0
  99. package/resources/components/Dashboard/Infrastructure/ServersDashboard.stx +5 -59
  100. package/resources/components/Dashboard/Jobs/JobHistory.stx +1 -11
  101. package/resources/components/Dashboard/Library/ComponentsDashboard.stx +11 -60
  102. package/resources/components/Dashboard/Library/FunctionsDashboard.stx +11 -15
  103. package/resources/components/Dashboard/Library/SourceScaffoldDialog.stx +95 -0
  104. package/resources/components/Dashboard/Management/AssignmentDrawer.stx +93 -0
  105. package/resources/components/Dashboard/Management/PermissionsDashboard.stx +47 -129
  106. package/resources/components/Dashboard/Marketing/CampaignDeleteDialog.stx +11 -12
  107. package/resources/components/Dashboard/Marketing/CampaignDeliveryDialog.stx +5 -8
  108. package/resources/components/Dashboard/Marketing/CampaignDialog.stx +17 -17
  109. package/resources/components/Dashboard/Marketing/MarketingListDeleteDialog.stx +15 -15
  110. package/resources/components/Dashboard/Marketing/MarketingListDialog.stx +17 -17
  111. package/resources/components/Dashboard/Marketing/MarketingListsTable.stx +1 -1
  112. package/resources/components/Dashboard/Marketing/SocialPostDeleteDialog.stx +11 -12
  113. package/resources/components/Dashboard/Marketing/SocialPostDialog.stx +17 -16
  114. package/resources/components/Dashboard/Models/EditRecordModal.stx +81 -0
  115. package/resources/components/Dashboard/Models/ModelRecordActions.stx +30 -0
  116. package/resources/components/Dashboard/Models/ModelRecordsDashboard.stx +42 -94
  117. package/resources/components/Dashboard/Navbar.stx +5 -5
  118. package/resources/components/Dashboard/Notifications/NotificationDeliveryDialog.stx +40 -60
  119. package/resources/components/Dashboard/UI/Button.stx +4 -0
  120. package/resources/components/Dashboard/UI/ConfirmDialog.stx +55 -53
  121. package/resources/components/Dashboard/UI/Drawer.stx +114 -0
  122. package/resources/components/Dashboard/UI/Modal.stx +32 -21
  123. package/resources/components/Docs/Demo/ComboboxDemo.stx +4 -5
  124. package/resources/components/Docs/Demo/DropdownDemo.stx +12 -5
  125. package/resources/components/Docs/Demo/ListboxDemo.stx +14 -6
  126. package/resources/components/Docs/Demo/PopoverDemo.stx +10 -5
  127. package/resources/components/Docs/Demo/RadioGroupDemo.stx +14 -6
  128. package/resources/components/Docs/Demo/StepperDemo.stx +4 -5
  129. package/resources/components/Docs/TeamMember.stx +16 -9
  130. package/resources/components/Forum/ForumReplyForm.stx +7 -7
  131. package/resources/components/Forum/ForumSidebar.stx +10 -6
  132. package/resources/components/Marketing/Faqs.stx +5 -5
  133. package/resources/components/Marketing/Feature.stx +11 -6
  134. package/resources/components/Marketing/Layout.stx +12 -8
  135. package/resources/components/Marketing/Pricing.stx +11 -5
  136. package/resources/components/Marketing/PrimaryFeatures.stx +4 -5
  137. package/resources/components/Marketing/PrimaryFeatures2.stx +6 -5
  138. package/resources/components/Marketing/SecondaryFeatures.stx +10 -5
  139. package/resources/components/Marketing/Testimonials.stx +8 -5
  140. package/resources/views/index.stx +4 -4
  141. package/views/system-tray/index.stx +1 -1
@@ -0,0 +1,127 @@
1
+ <script client>
2
+ import { ciStore } from '~/storage/framework/defaults/views/dashboard/stores/ci'
3
+
4
+ const ci = ciStore
5
+ const target = derived(() => ci.drilldown())
6
+ const runs = derived(() => ci.drilldownRuns())
7
+ const loading = derived(() => ci.loadingDrilldown())
8
+ const error = derived(() => ci.drilldownError())
9
+ const expandedRunId = derived(() => ci.expandedRunId())
10
+ const jobsByRunId = derived(() => ci.drilldownJobsByRunId())
11
+ const loadingJobsRunId = derived(() => ci.loadingJobsForRunId())
12
+ const title = derived(() => target()?.fullName || 'Run history')
13
+
14
+ function runRowClass(conclusion, status) {
15
+ const base = 'px-3 py-2 -mx-3 border-l-2 cursor-pointer hover:bg-zinc-50 dark:hover:bg-zinc-900 transition-colors '
16
+ if (status !== 'completed') return base + 'border-yellow-400'
17
+ if (conclusion === 'success') return base + 'border-emerald-400'
18
+ if (conclusion === 'failure' || conclusion === 'timed_out') return base + 'border-red-400'
19
+ if (conclusion === 'cancelled') return base + 'border-zinc-400'
20
+ return base + 'border-zinc-200 dark:border-zinc-800'
21
+ }
22
+
23
+ function runStatusBadge(conclusion, status) {
24
+ if (status !== 'completed') return { label: status, cls: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300' }
25
+ if (conclusion === 'success') return { label: 'success', cls: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300' }
26
+ if (conclusion === 'failure') return { label: 'failure', cls: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300' }
27
+ return { label: conclusion || 'unknown', cls: 'bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300' }
28
+ }
29
+
30
+ function formatDuration(ms) {
31
+ if (ms == null) return ''
32
+ const seconds = Math.floor(ms / 1000)
33
+ if (seconds < 60) return seconds + 's'
34
+ const minutes = Math.floor(seconds / 60)
35
+ const remainder = seconds % 60
36
+ if (minutes < 60) return minutes + 'm' + (remainder ? ' ' + remainder + 's' : '')
37
+ const hours = Math.floor(minutes / 60)
38
+ return hours + 'h ' + (minutes % 60) + 'm'
39
+ }
40
+
41
+ function timeAgo(dateStr) {
42
+ if (!dateStr) return ''
43
+ const diff = Date.now() - new Date(dateStr).getTime()
44
+ const minutes = Math.floor(diff / 60000)
45
+ if (minutes < 60) return minutes + 'm ago'
46
+ const hours = Math.floor(minutes / 60)
47
+ if (hours < 24) return hours + 'h ago'
48
+ return Math.floor(hours / 24) + 'd ago'
49
+ }
50
+
51
+ function truncate(value, length) {
52
+ if (!value) return ''
53
+ return value.length > length ? value.slice(0, length) + '...' : value
54
+ }
55
+
56
+ function jobsForRun(runId) {
57
+ return jobsByRunId()[runId] || []
58
+ }
59
+
60
+ function failedJobs(runId) {
61
+ return jobsForRun(runId).filter(job => job.conclusion && job.conclusion !== 'success' && job.conclusion !== 'skipped' && job.conclusion !== 'neutral')
62
+ }
63
+ </script>
64
+
65
+ <Drawer
66
+ :isOpen="Boolean(target())"
67
+ :title="title()"
68
+ description="Run history"
69
+ size="lg"
70
+ @close="ci.closeDrilldown()"
71
+ >
72
+ <div :if="error()" class="mt-4 mx-5 px-4 py-3 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900/40">
73
+ <p class="text-red-700 text-xs dark:text-red-300" :text="error()"></p>
74
+ </div>
75
+
76
+ <div :if="loading() && runs().length === 0" class="px-5 py-12 text-center">
77
+ <span aria-hidden="true" class="mx-auto h-6 w-6 text-violet-500 animate-spin i-hugeicons-loading-03"></span>
78
+ <p class="mt-2 text-zinc-500 text-xs">Fetching recent runs...</p>
79
+ </div>
80
+
81
+ <ul :if="runs().length > 0" class="px-5 py-3">
82
+ <li :for="run in runs" class="border-b border-zinc-100 dark:border-zinc-900 last:border-0">
83
+ <div @click="ci.toggleRunJobs(run.id)" :class="runRowClass(run.conclusion, run.status)">
84
+ <div class="flex gap-3 items-start justify-between">
85
+ <div class="flex-1 min-w-0">
86
+ <div class="flex gap-2 items-center">
87
+ <span :class="`px-1.5 py-0.5 text-[10px] font-medium rounded ${runStatusBadge(run.conclusion, run.status).cls}`" :text="runStatusBadge(run.conclusion, run.status).label"></span>
88
+ <span class="font-medium text-sm text-zinc-800 truncate dark:text-zinc-200" :text="run.name"></span>
89
+ </div>
90
+ <p :if="run.commitMessage" class="mt-1 text-zinc-500 text-xs truncate" :text="truncate(run.commitMessage, 80)"></p>
91
+ <div class="flex gap-2 items-center mt-1 text-[10px] text-zinc-400">
92
+ <span class="font-mono" :text="run.headShaShort"></span>
93
+ <span :if="run.headBranch" :text="run.headBranch"></span>
94
+ <span :if="run.commitAuthor">·</span>
95
+ <span :if="run.commitAuthor" :text="run.commitAuthor"></span>
96
+ <span>·</span>
97
+ <span :text="timeAgo(run.updatedAt)"></span>
98
+ <span :if="run.durationMs">·</span>
99
+ <span :if="run.durationMs" :text="formatDuration(run.durationMs)"></span>
100
+ </div>
101
+ </div>
102
+ <a :href="run.url" target="_blank" rel="noreferrer" @click.stop class="flex-shrink-0 no-underline text-violet-600 text-xs dark:text-violet-400 hover:underline">View ↗</a>
103
+ </div>
104
+ </div>
105
+
106
+ <div :if="expandedRunId() === run.id" class="px-3 py-2 bg-zinc-50 dark:bg-zinc-900/50 border-t border-zinc-100 dark:border-zinc-900">
107
+ <div :if="loadingJobsRunId() === run.id" class="py-2 text-zinc-500 text-xs">Loading jobs...</div>
108
+ <div :if="loadingJobsRunId() !== run.id && jobsForRun(run.id).length === 0" class="py-1 italic text-zinc-400 text-xs">No job detail.</div>
109
+ <div :if="loadingJobsRunId() !== run.id && failedJobs(run.id).length > 0" class="space-y-1.5">
110
+ <p class="text-[10px] text-zinc-500 tracking-wide uppercase">Failed jobs</p>
111
+ <a :for="job in failedJobs(run.id)" :href="job.url" target="_blank" rel="noreferrer" @click.stop class="flex gap-1.5 items-center no-underline text-red-700 text-xs dark:text-red-400 hover:underline">
112
+ <span aria-hidden="true" class="h-2.5 w-2.5 i-hugeicons-cancel-circle"></span>
113
+ <span :text="job.name"></span>
114
+ <span :if="job.durationMs" class="text-zinc-400" :text="'(' + formatDuration(job.durationMs) + ')'"></span>
115
+ </a>
116
+ </div>
117
+ <div :if="loadingJobsRunId() !== run.id && jobsForRun(run.id).length > 0 && failedJobs(run.id).length === 0" class="py-1 text-zinc-500 text-xs">
118
+ All <span :text="jobsForRun(run.id).length"></span> jobs passed.
119
+ </div>
120
+ </div>
121
+ </li>
122
+ </ul>
123
+
124
+ <div :if="!loading() && !error() && runs().length === 0" class="px-5 py-12 text-center">
125
+ <p class="text-zinc-500 text-sm">No recent runs.</p>
126
+ </div>
127
+ </Drawer>
@@ -5,13 +5,15 @@ const open = useReactiveProp('open', false)
5
5
  const record = useReactiveProp('record', null as CommerceCategoryRecord | null)
6
6
  const busy = useReactiveProp('busy', false)
7
7
  const emit = defineEmits()
8
+ const description = derived(() => `Delete "${record()?.name || 'Unnamed category'}" permanently? Linked products and child categories may prevent deletion.`)
8
9
  </script>
9
10
 
10
- <div :if="open()" class="flex fixed inset-0 z-50 items-center justify-center p-4 dashboard-modal-layer">
11
- <button type="button" aria-label="Close delete category dialog" class="absolute inset-0 bg-black/45" @click="emit('close')"></button>
12
- <section role="dialog" aria-modal="true" aria-labelledby="category-delete-title" class="relative p-6 max-w-md w-full bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700 shadow-xl">
13
- <h2 id="category-delete-title" class="font-semibold text-gray-900 text-lg dark:text-white">Delete category</h2>
14
- <p class="mt-2 text-gray-500 text-sm dark:text-neutral-400">Delete "{{ record()?.name || 'Unnamed category' }}" permanently? Linked products and child categories may prevent deletion.</p>
15
- <footer class="flex gap-2 justify-end mt-6"><Button variant="secondary" :disabled="busy()" @click="emit('close')">Cancel</Button><Button variant="danger" :loading="busy()" @click="emit('confirm')">Delete category</Button></footer>
16
- </section>
17
- </div>
11
+ <ConfirmDialog
12
+ :isOpen="open()"
13
+ title="Delete category"
14
+ :description="description()"
15
+ confirmLabel="Delete category"
16
+ :busy="busy()"
17
+ @close="emit('close')"
18
+ @confirm="emit('confirm')"
19
+ />
@@ -35,20 +35,25 @@ function deleteRecord(): void {
35
35
  }
36
36
  </script>
37
37
 
38
- <div :if="open()" class="flex fixed inset-0 z-50 items-center justify-center p-4 dashboard-modal-layer">
39
- <button type="button" aria-label="Close category details" class="absolute inset-0 bg-black/45" @click="emit('close')"></button>
40
- <section role="dialog" aria-modal="true" aria-labelledby="category-details-title" class="relative max-w-xl w-full bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700 shadow-xl">
41
- <div class="p-6">
42
- <div class="flex gap-4 items-start justify-between"><div><h2 id="category-details-title" class="font-semibold text-gray-900 text-lg dark:text-white">{{ record()?.name || 'Category' }}</h2><p class="mt-1 font-mono text-gray-400 text-sm dark:text-neutral-500">{{ record()?.slug || 'No slug' }}</p></div><span class="px-2 py-1 font-semibold text-gray-700 text-xs dark:text-neutral-200 bg-gray-100 dark:bg-neutral-700 rounded-full">{{ record()?.isActive ? 'Active' : 'Inactive' }}</span></div>
43
- <p class="mt-5 text-gray-600 text-sm whitespace-pre-wrap dark:text-neutral-300">{{ record()?.description || 'No description recorded.' }}</p>
44
- <dl class="grid gap-4 grid-cols-2 mt-6">
45
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Parent</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ parentName() }}</dd></div>
46
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Display order</dt><dd class="mt-1 font-mono text-gray-900 text-sm dark:text-white">{{ record()?.displayOrder || 0 }}</dd></div>
47
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Direct children</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ children().length }}</dd></div>
48
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Record ID</dt><dd class="mt-1 font-mono text-gray-900 text-sm dark:text-white">{{ record()?.id || 'Not recorded' }}</dd></div>
49
- <div class="col-span-2"><dt class="text-gray-400 text-xs dark:text-neutral-500">Image URL</dt><dd class="mt-1 break-all text-gray-900 text-sm dark:text-white">{{ record()?.imageUrl || 'Not configured' }}</dd></div>
50
- </dl>
51
- </div>
52
- <footer class="flex gap-2 justify-between px-6 py-4 bg-gray-50 dark:bg-neutral-900/50 border-gray-200 border-t dark:border-neutral-700"><Button variant="danger" @click="deleteRecord()">Delete</Button><div class="flex gap-2"><Button variant="secondary" @click="emit('close')">Close</Button><Button @click="editRecord()">Edit category</Button></div></footer>
53
- </section>
54
- </div>
38
+ <Modal
39
+ :isOpen="open()"
40
+ :title="record()?.name || 'Category'"
41
+ :description="record()?.slug || 'No slug'"
42
+ size="lg"
43
+ @close="emit('close')"
44
+ >
45
+ <template #header-trailing>
46
+ <span class="px-2 py-1 font-semibold text-gray-700 text-xs dark:text-neutral-200 bg-gray-100 dark:bg-neutral-700 rounded-full">{{ record()?.isActive ? 'Active' : 'Inactive' }}</span>
47
+ </template>
48
+ <p class="text-gray-600 text-sm whitespace-pre-wrap dark:text-neutral-300">{{ record()?.description || 'No description recorded.' }}</p>
49
+ <dl class="grid gap-4 grid-cols-2 mt-6">
50
+ <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Parent</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ parentName() }}</dd></div>
51
+ <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Display order</dt><dd class="mt-1 font-mono text-gray-900 text-sm dark:text-white">{{ record()?.displayOrder || 0 }}</dd></div>
52
+ <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Direct children</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ children().length }}</dd></div>
53
+ <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Record ID</dt><dd class="mt-1 font-mono text-gray-900 text-sm dark:text-white">{{ record()?.id || 'Not recorded' }}</dd></div>
54
+ <div class="col-span-2"><dt class="text-gray-400 text-xs dark:text-neutral-500">Image URL</dt><dd class="mt-1 break-all text-gray-900 text-sm dark:text-white">{{ record()?.imageUrl || 'Not configured' }}</dd></div>
55
+ </dl>
56
+ <template #footer>
57
+ <div class="flex gap-2 justify-between w-full"><Button variant="danger" @click="deleteRecord()">Delete</Button><div class="flex gap-2"><Button variant="secondary" @click="emit('close')">Close</Button><Button @click="editRecord()">Edit category</Button></div></div>
58
+ </template>
59
+ </Modal>
@@ -120,17 +120,18 @@ function errorMessages(): string[] {
120
120
  }
121
121
  </script>
122
122
 
123
- <div :if="open()" class="flex fixed inset-0 z-50 items-center justify-center p-4 dashboard-modal-layer">
124
- <button type="button" aria-label="Close category dialog" class="absolute inset-0 bg-black/45" @click="emit('close')"></button>
125
- <section role="dialog" aria-modal="true" aria-labelledby="category-dialog-title" class="overflow-y-auto relative max-h-[90dvh] max-w-2xl w-full bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700 shadow-xl">
126
- <form @submit.prevent="submit">
127
- <div class="p-6">
128
- <h2 id="category-dialog-title" class="font-semibold text-gray-900 text-lg dark:text-white">{{ mode() === 'edit' ? 'Edit category' : 'Add category' }}</h2>
129
- <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Store hierarchy fields declared by the Category model.</p>
130
- <ul :if="errorMessages().length > 0" class="mt-5 p-3 space-y-1 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">
123
+ <Modal
124
+ :isOpen="open()"
125
+ :title="mode() === 'edit' ? 'Edit category' : 'Add category'"
126
+ description="Store hierarchy fields declared by the Category model."
127
+ size="lg"
128
+ @close="emit('close')"
129
+ >
130
+ <form id="category-form" @submit.prevent="submit">
131
+ <ul :if="errorMessages().length > 0" class="mb-5 p-3 space-y-1 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">
131
132
  <template :for="message in errorMessages()" :key="message"><li>{{ message }}</li></template>
132
133
  </ul>
133
- <div class="grid gap-4 grid-cols-1 sm:grid-cols-2 mt-5">
134
+ <div class="grid gap-4 grid-cols-1 sm:grid-cols-2">
134
135
  <label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Name</span><input x-model="name" type="text" maxlength="50" required 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>
135
136
  <label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Slug</span><input x-model="slug" type="text" required spellcheck="false" 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" /></label>
136
137
  <label class="block sm:col-span-2"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Description</span><textarea x-model="description" rows="3" 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></label>
@@ -139,8 +140,8 @@ function errorMessages(): string[] {
139
140
  <label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Display order</span><input x-model="displayOrder" type="number" min="0" step="1" required 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>
140
141
  <label class="flex gap-3 items-center sm:col-span-2 p-3 border border-gray-200 rounded-lg dark:border-neutral-700"><input x-model="isActive" type="checkbox" class="h-4 w-4 text-blue-600 border-gray-300 rounded" /><span><span class="block font-medium text-gray-700 text-sm dark:text-neutral-200">Active in the storefront</span><span class="block mt-0.5 text-gray-400 text-xs dark:text-neutral-500">Availability is independent from the hierarchy.</span></span></label>
141
142
  </div>
142
- </div>
143
- <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 variant="secondary" :disabled="busy()" @click="emit('close')">Cancel</Button><Button type="submit" :loading="busy()" :disabled="!valid()">{{ mode() === 'edit' ? 'Save category' : 'Add category' }}</Button></footer>
144
143
  </form>
145
- </section>
146
- </div>
144
+ <template #footer>
145
+ <div class="flex gap-2 justify-end w-full"><Button variant="secondary" :disabled="busy()" @click="emit('close')">Cancel</Button><Button type="submit" form="category-form" :loading="busy()" :disabled="!valid()">{{ mode() === 'edit' ? 'Save category' : 'Add category' }}</Button></div>
146
+ </template>
147
+ </Modal>
@@ -5,12 +5,15 @@ const open = useReactiveProp('open', false)
5
5
  const record = useReactiveProp('record', null as CouponRecord | null)
6
6
  const busy = useReactiveProp('busy', false)
7
7
  const emit = defineEmits()
8
+ const description = derived(() => `Delete "${record()?.code || 'Unnamed coupon'}" permanently? Recorded order relationships may prevent deletion.`)
8
9
  </script>
9
10
 
10
- <div :if="open()" class="flex fixed inset-0 z-50 items-center justify-center p-4 dashboard-modal-layer">
11
- <button type="button" aria-label="Close delete coupon dialog" class="absolute inset-0 bg-black/45" @click="emit('close')"></button>
12
- <section role="dialog" aria-modal="true" aria-labelledby="coupon-delete-title" class="relative p-6 max-w-md w-full bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700 shadow-xl">
13
- <h2 id="coupon-delete-title" class="font-semibold text-gray-900 text-lg dark:text-white">Delete coupon</h2><p class="mt-2 text-gray-500 text-sm dark:text-neutral-400">Delete "{{ record()?.code || 'Unnamed coupon' }}" permanently? Recorded order relationships may prevent deletion.</p>
14
- <footer class="flex gap-2 justify-end mt-6"><Button variant="secondary" :disabled="busy()" @click="emit('close')">Cancel</Button><Button variant="danger" :loading="busy()" @click="emit('confirm')">Delete coupon</Button></footer>
15
- </section>
16
- </div>
11
+ <ConfirmDialog
12
+ :isOpen="open()"
13
+ title="Delete coupon"
14
+ :description="description()"
15
+ confirmLabel="Delete coupon"
16
+ :busy="busy()"
17
+ @close="emit('close')"
18
+ @confirm="emit('confirm')"
19
+ />
@@ -32,22 +32,28 @@ function deleteRecord(): void {
32
32
  }
33
33
  </script>
34
34
 
35
- <div :if="open()" class="flex fixed inset-0 z-50 items-center justify-center p-4 dashboard-modal-layer">
36
- <button type="button" aria-label="Close coupon details" class="absolute inset-0 bg-black/45" @click="emit('close')"></button>
37
- <section role="dialog" aria-modal="true" aria-labelledby="coupon-details-title" class="relative max-w-xl w-full bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700 shadow-xl">
38
- <div class="p-6"><div class="flex gap-4 items-start justify-between"><div><h2 id="coupon-details-title" class="font-mono font-semibold text-gray-900 text-lg dark:text-white">{{ record()?.code || 'Coupon' }}</h2><p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">{{ record()?.description || 'No description recorded.' }}</p></div><span class="px-2 py-1 font-semibold text-gray-700 text-xs dark:text-neutral-200 bg-gray-100 dark:bg-neutral-700 rounded-full">{{ record()?.status || 'Not recorded' }}</span></div>
39
- <dl class="grid gap-4 grid-cols-2 mt-6">
40
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Discount</dt><dd class="mt-1 font-semibold text-gray-900 text-sm dark:text-white">{{ discount(record()) }}</dd></div>
41
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Availability</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ record()?.isActive ? 'Enabled' : 'Disabled' }}</dd></div>
42
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Usage</dt><dd class="mt-1 font-mono text-gray-900 text-sm dark:text-white">{{ record()?.usageCount || 0 }} / {{ record()?.usageLimit || 'No limit' }}</dd></div>
43
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Minimum order</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ record()?.minOrderAmount ? money(record()?.minOrderAmount || 0) : 'No minimum' }}</dd></div>
44
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Maximum discount</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ record()?.maxDiscountAmount ? money(record()?.maxDiscountAmount || 0) : 'No maximum' }}</dd></div>
45
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Product relation ID</dt><dd class="mt-1 font-mono text-gray-900 text-sm dark:text-white">{{ record()?.productId || 'Not linked' }}</dd></div>
46
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Free product reference</dt><dd class="mt-1 font-mono text-gray-900 text-sm dark:text-white">{{ record()?.freeProductId || 'Not configured' }}</dd></div>
47
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Start date</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ record()?.startDate || 'Not recorded' }}</dd></div>
48
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">End date</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ record()?.endDate || 'Not recorded' }}</dd></div>
49
- </dl>
50
- </div>
51
- <footer class="flex gap-2 justify-between px-6 py-4 bg-gray-50 dark:bg-neutral-900/50 border-gray-200 border-t dark:border-neutral-700"><Button variant="danger" @click="deleteRecord()">Delete</Button><div class="flex gap-2"><Button variant="secondary" @click="emit('close')">Close</Button><Button @click="editRecord()">Edit coupon</Button></div></footer>
52
- </section>
53
- </div>
35
+ <Modal
36
+ :isOpen="open()"
37
+ :title="record()?.code || 'Coupon'"
38
+ :description="record()?.description || 'No description recorded.'"
39
+ size="lg"
40
+ @close="emit('close')"
41
+ >
42
+ <template #header-trailing>
43
+ <span class="px-2 py-1 font-semibold text-gray-700 text-xs dark:text-neutral-200 bg-gray-100 dark:bg-neutral-700 rounded-full">{{ record()?.status || 'Not recorded' }}</span>
44
+ </template>
45
+ <dl class="grid gap-4 grid-cols-2">
46
+ <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Discount</dt><dd class="mt-1 font-semibold text-gray-900 text-sm dark:text-white">{{ discount(record()) }}</dd></div>
47
+ <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Availability</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ record()?.isActive ? 'Enabled' : 'Disabled' }}</dd></div>
48
+ <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Usage</dt><dd class="mt-1 font-mono text-gray-900 text-sm dark:text-white">{{ record()?.usageCount || 0 }} / {{ record()?.usageLimit || 'No limit' }}</dd></div>
49
+ <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Minimum order</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ record()?.minOrderAmount ? money(record()?.minOrderAmount || 0) : 'No minimum' }}</dd></div>
50
+ <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Maximum discount</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ record()?.maxDiscountAmount ? money(record()?.maxDiscountAmount || 0) : 'No maximum' }}</dd></div>
51
+ <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Product relation ID</dt><dd class="mt-1 font-mono text-gray-900 text-sm dark:text-white">{{ record()?.productId || 'Not linked' }}</dd></div>
52
+ <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Free product reference</dt><dd class="mt-1 font-mono text-gray-900 text-sm dark:text-white">{{ record()?.freeProductId || 'Not configured' }}</dd></div>
53
+ <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Start date</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ record()?.startDate || 'Not recorded' }}</dd></div>
54
+ <div><dt class="text-gray-400 text-xs dark:text-neutral-500">End date</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ record()?.endDate || 'Not recorded' }}</dd></div>
55
+ </dl>
56
+ <template #footer>
57
+ <div class="flex gap-2 justify-between w-full"><Button variant="danger" @click="deleteRecord()">Delete</Button><div class="flex gap-2"><Button variant="secondary" @click="emit('close')">Close</Button><Button @click="editRecord()">Edit coupon</Button></div></div>
58
+ </template>
59
+ </Modal>
@@ -87,15 +87,18 @@ function errorMessages(): string[] {
87
87
  }
88
88
  </script>
89
89
 
90
- <div :if="open()" class="flex fixed inset-0 z-50 items-center justify-center p-4 dashboard-modal-layer">
91
- <button type="button" aria-label="Close coupon dialog" class="absolute inset-0 bg-black/45" @click="emit('close')"></button>
92
- <section role="dialog" aria-modal="true" aria-labelledby="coupon-dialog-title" class="overflow-y-auto relative max-h-[90dvh] max-w-2xl w-full bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700 shadow-xl">
93
- <form @submit.prevent="submit">
94
- <div class="p-6"><h2 id="coupon-dialog-title" class="font-semibold text-gray-900 text-lg dark:text-white">{{ mode() === 'edit' ? 'Edit coupon' : 'Add coupon' }}</h2><p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Store fields declared by the Coupon model.</p>
95
- <ul :if="errorMessages().length > 0" class="mt-5 p-3 space-y-1 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">
90
+ <Modal
91
+ :isOpen="open()"
92
+ :title="mode() === 'edit' ? 'Edit coupon' : 'Add coupon'"
93
+ description="Store fields declared by the Coupon model."
94
+ size="lg"
95
+ @close="emit('close')"
96
+ >
97
+ <form id="coupon-form" @submit.prevent="submit">
98
+ <ul :if="errorMessages().length > 0" class="mb-5 p-3 space-y-1 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">
96
99
  <template :for="message in errorMessages()" :key="message"><li>{{ message }}</li></template>
97
100
  </ul>
98
- <div class="grid gap-4 grid-cols-1 sm:grid-cols-2 mt-5">
101
+ <div class="grid gap-4 grid-cols-1 sm:grid-cols-2">
99
102
  <label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Code</span><input x-model="code" type="text" maxlength="50" required spellcheck="false" class="mt-1 px-3 py-2 w-full font-mono text-gray-900 text-sm uppercase dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" /></label>
100
103
  <label class="block"><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="Active">Active</option><option value="Scheduled">Scheduled</option><option value="Expired">Expired</option></select></label>
101
104
  <label class="block sm:col-span-2"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Description</span><textarea x-model="description" rows="3" 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></label>
@@ -109,8 +112,8 @@ function errorMessages(): string[] {
109
112
  <label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">End date</span><input x-model="endDate" type="date" 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>
110
113
  <label class="flex gap-3 items-center sm:col-span-2 p-3 border border-gray-200 rounded-lg dark:border-neutral-700"><input x-model="isActive" type="checkbox" class="h-4 w-4 text-blue-600 border-gray-300 rounded" /><span><span class="block font-medium text-gray-700 text-sm dark:text-neutral-200">Enabled for redemption</span><span class="block mt-0.5 text-gray-400 text-xs dark:text-neutral-500">Availability is stored separately from lifecycle status.</span></span></label>
111
114
  </div>
112
- </div>
113
- <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 variant="secondary" :disabled="busy()" @click="emit('close')">Cancel</Button><Button type="submit" :loading="busy()" :disabled="!valid()">{{ mode() === 'edit' ? 'Save coupon' : 'Add coupon' }}</Button></footer>
114
115
  </form>
115
- </section>
116
- </div>
116
+ <template #footer>
117
+ <div class="flex gap-2 justify-end w-full"><Button variant="secondary" :disabled="busy()" @click="emit('close')">Cancel</Button><Button type="submit" form="coupon-form" :loading="busy()" :disabled="!valid()">{{ mode() === 'edit' ? 'Save coupon' : 'Add coupon' }}</Button></div>
118
+ </template>
119
+ </Modal>
@@ -5,13 +5,15 @@ const open = useReactiveProp('open', false)
5
5
  const record = useReactiveProp('record', null as CommerceCustomerRecord | null)
6
6
  const busy = useReactiveProp('busy', false)
7
7
  const emit = defineEmits()
8
+ const description = derived(() => `Delete "${record()?.name || 'Unnamed customer'}" permanently? Linked orders, reviews, payments, and gift cards may prevent deletion.`)
8
9
  </script>
9
10
 
10
- <div :if="open()" class="flex fixed inset-0 z-50 items-center justify-center p-4 dashboard-modal-layer">
11
- <button type="button" aria-label="Close delete customer dialog" class="absolute inset-0 bg-black/45" @click="emit('close')"></button>
12
- <section role="dialog" aria-modal="true" aria-labelledby="customer-delete-title" class="relative p-6 max-w-md w-full bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700 shadow-xl">
13
- <h2 id="customer-delete-title" class="font-semibold text-gray-900 text-lg dark:text-white">Delete customer</h2>
14
- <p class="mt-2 text-gray-500 text-sm dark:text-neutral-400">Delete "{{ record()?.name || 'Unnamed customer' }}" permanently? Linked orders, reviews, payments, and gift cards may prevent deletion.</p>
15
- <footer class="flex gap-2 justify-end mt-6"><Button variant="secondary" :disabled="busy()" @click="emit('close')">Cancel</Button><Button variant="danger" :loading="busy()" @click="emit('confirm')">Delete customer</Button></footer>
16
- </section>
17
- </div>
11
+ <ConfirmDialog
12
+ :isOpen="open()"
13
+ title="Delete customer"
14
+ :description="description()"
15
+ confirmLabel="Delete customer"
16
+ :busy="busy()"
17
+ @close="emit('close')"
18
+ @confirm="emit('confirm')"
19
+ />
@@ -46,19 +46,27 @@ function deleteRecord(): void {
46
46
  }
47
47
  </script>
48
48
 
49
- <div :if="open()" class="flex fixed inset-0 z-50 items-center justify-center p-4 dashboard-modal-layer">
50
- <button type="button" aria-label="Close customer details" class="absolute inset-0 bg-black/45" @click="emit('close')"></button>
51
- <section role="dialog" aria-modal="true" aria-labelledby="customer-details-title" class="relative max-w-xl w-full bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700 shadow-xl">
52
- <div class="p-6">
53
- <div class="flex gap-4 items-start justify-between"><div class="flex gap-3 items-center"><span class="flex items-center justify-center h-11 w-11 font-semibold text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40 rounded-lg">{{ (record()?.name || '?').slice(0, 1).toUpperCase() }}</span><div><h2 id="customer-details-title" class="font-semibold text-gray-900 text-lg dark:text-white">{{ record()?.name || 'Customer' }}</h2><p class="mt-1 text-gray-400 text-sm dark:text-neutral-500">{{ record()?.email || 'No email recorded' }}</p></div></div><span class="px-2 py-1 font-semibold text-gray-700 text-xs dark:text-neutral-200 bg-gray-100 dark:bg-neutral-700 rounded-full">{{ record()?.status || 'Unknown' }}</span></div>
54
- <dl class="grid gap-4 grid-cols-2 mt-6">
55
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Phone</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ record()?.phone || 'Not recorded' }}</dd></div>
56
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Recorded spend</dt><dd class="mt-1 font-medium text-gray-900 text-sm dark:text-white">{{ formatMoney(record()?.totalSpent || 0) }}</dd></div>
57
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Last order</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ formatDate(record()?.lastOrder || '') }}</dd></div>
58
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Record ID</dt><dd class="mt-1 font-mono text-gray-900 text-sm dark:text-white">{{ record()?.id || 'Not recorded' }}</dd></div>
59
- <div class="col-span-2"><dt class="text-gray-400 text-xs dark:text-neutral-500">Avatar URL</dt><dd class="mt-1 break-all text-gray-900 text-sm dark:text-white">{{ record()?.avatar || 'Not configured' }}</dd></div>
60
- </dl>
61
- </div>
62
- <footer class="flex gap-2 justify-between px-6 py-4 bg-gray-50 dark:bg-neutral-900/50 border-gray-200 border-t dark:border-neutral-700"><Button variant="danger" @click="deleteRecord()">Delete</Button><div class="flex gap-2"><Button variant="secondary" @click="emit('close')">Close</Button><Button @click="editRecord()">Edit customer</Button></div></footer>
63
- </section>
64
- </div>
49
+ <Modal
50
+ :isOpen="open()"
51
+ :title="record()?.name || 'Customer'"
52
+ :description="record()?.email || 'No email recorded'"
53
+ size="lg"
54
+ @close="emit('close')"
55
+ >
56
+ <template #header-leading>
57
+ <span class="flex items-center justify-center h-11 w-11 font-semibold text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40 rounded-lg">{{ (record()?.name || '?').slice(0, 1).toUpperCase() }}</span>
58
+ </template>
59
+ <template #header-trailing>
60
+ <span class="px-2 py-1 font-semibold text-gray-700 text-xs dark:text-neutral-200 bg-gray-100 dark:bg-neutral-700 rounded-full">{{ record()?.status || 'Unknown' }}</span>
61
+ </template>
62
+ <dl class="grid gap-4 grid-cols-2">
63
+ <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Phone</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ record()?.phone || 'Not recorded' }}</dd></div>
64
+ <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Recorded spend</dt><dd class="mt-1 font-medium text-gray-900 text-sm dark:text-white">{{ formatMoney(record()?.totalSpent || 0) }}</dd></div>
65
+ <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Last order</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ formatDate(record()?.lastOrder || '') }}</dd></div>
66
+ <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Record ID</dt><dd class="mt-1 font-mono text-gray-900 text-sm dark:text-white">{{ record()?.id || 'Not recorded' }}</dd></div>
67
+ <div class="col-span-2"><dt class="text-gray-400 text-xs dark:text-neutral-500">Avatar URL</dt><dd class="mt-1 break-all text-gray-900 text-sm dark:text-white">{{ record()?.avatar || 'Not configured' }}</dd></div>
68
+ </dl>
69
+ <template #footer>
70
+ <div class="flex gap-2 justify-between w-full"><Button variant="danger" @click="deleteRecord()">Delete</Button><div class="flex gap-2"><Button variant="secondary" @click="emit('close')">Close</Button><Button @click="editRecord()">Edit customer</Button></div></div>
71
+ </template>
72
+ </Modal>
@@ -79,17 +79,18 @@ function errorMessages(): string[] {
79
79
  }
80
80
  </script>
81
81
 
82
- <div :if="open()" class="flex fixed inset-0 z-50 items-center justify-center p-4 dashboard-modal-layer">
83
- <button type="button" aria-label="Close customer dialog" class="absolute inset-0 bg-black/45" @click="emit('close')"></button>
84
- <section role="dialog" aria-modal="true" aria-labelledby="customer-dialog-title" class="overflow-y-auto relative max-h-[90dvh] max-w-2xl w-full bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700 shadow-xl">
85
- <form @submit.prevent="submit">
86
- <div class="p-6">
87
- <h2 id="customer-dialog-title" class="font-semibold text-gray-900 text-lg dark:text-white">{{ mode() === 'edit' ? 'Edit customer' : 'Add customer' }}</h2>
88
- <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Store fields declared by the native Customer model.</p>
89
- <ul :if="errorMessages().length > 0" class="mt-5 p-3 space-y-1 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
+ <Modal
83
+ :isOpen="open()"
84
+ :title="mode() === 'edit' ? 'Edit customer' : 'Add customer'"
85
+ description="Store fields declared by the native Customer model."
86
+ size="lg"
87
+ @close="emit('close')"
88
+ >
89
+ <form id="customer-form" @submit.prevent="submit">
90
+ <ul :if="errorMessages().length > 0" class="mb-5 p-3 space-y-1 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">
90
91
  <template :for="message in errorMessages()" :key="message"><li>{{ message }}</li></template>
91
92
  </ul>
92
- <div class="grid gap-4 grid-cols-1 sm:grid-cols-2 mt-5">
93
+ <div class="grid gap-4 grid-cols-1 sm:grid-cols-2">
93
94
  <label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Name</span><input x-model="name" type="text" minlength="2" maxlength="255" required 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>
94
95
  <label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Email</span><input x-model="email" type="email" required 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>
95
96
  <label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Phone <span class="font-normal text-gray-400 dark:text-neutral-500">(optional)</span></span><input x-model="phone" type="tel" maxlength="50" 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>
@@ -98,8 +99,8 @@ function errorMessages(): string[] {
98
99
  <label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Last order</span><input x-model="lastOrder" type="date" 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>
99
100
  <label class="block sm:col-span-2"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Avatar URL</span><input x-model="avatar" type="url" placeholder="https://cdn.example.com/customers/avatar.jpg" required 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" /><span class="block mt-1 text-gray-400 text-xs dark:text-neutral-500">Use a persisted HTTP asset URL. The dashboard uses initials when rendering the table.</span></label>
100
101
  </div>
101
- </div>
102
- <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 variant="secondary" :disabled="busy()" @click="emit('close')">Cancel</Button><Button type="submit" :loading="busy()" :disabled="!valid()">{{ mode() === 'edit' ? 'Save customer' : 'Add customer' }}</Button></footer>
103
102
  </form>
104
- </section>
105
- </div>
103
+ <template #footer>
104
+ <div class="flex gap-2 justify-end w-full"><Button variant="secondary" :disabled="busy()" @click="emit('close')">Cancel</Button><Button type="submit" form="customer-form" :loading="busy()" :disabled="!valid()">{{ mode() === 'edit' ? 'Save customer' : 'Add customer' }}</Button></div>
105
+ </template>
106
+ </Modal>
@@ -5,13 +5,15 @@ const open = useReactiveProp('open', false)
5
5
  const record = useReactiveProp('record', null as GiftCardRecord | null)
6
6
  const busy = useReactiveProp('busy', false)
7
7
  const emit = defineEmits()
8
+ const description = derived(() => `Delete "${record()?.code || 'Unnamed gift card'}" permanently? Recorded customer or order relationships may prevent deletion.`)
8
9
  </script>
9
10
 
10
- <div :if="open()" class="flex fixed inset-0 z-50 items-center justify-center p-4 dashboard-modal-layer">
11
- <button type="button" aria-label="Close delete gift card dialog" class="absolute inset-0 bg-black/45" @click="emit('close')"></button>
12
- <section role="dialog" aria-modal="true" aria-labelledby="gift-card-delete-title" class="relative p-6 max-w-md w-full bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700 shadow-xl">
13
- <h2 id="gift-card-delete-title" class="font-semibold text-gray-900 text-lg dark:text-white">Delete gift card</h2>
14
- <p class="mt-2 text-gray-500 text-sm dark:text-neutral-400">Delete "{{ record()?.code || 'Unnamed gift card' }}" permanently? Recorded customer or order relationships may prevent deletion.</p>
15
- <footer class="flex gap-2 justify-end mt-6"><Button variant="secondary" :disabled="busy()" @click="emit('close')">Cancel</Button><Button variant="danger" :loading="busy()" @click="emit('confirm')">Delete gift card</Button></footer>
16
- </section>
17
- </div>
11
+ <ConfirmDialog
12
+ :isOpen="open()"
13
+ title="Delete gift card"
14
+ :description="description()"
15
+ confirmLabel="Delete gift card"
16
+ :busy="busy()"
17
+ @close="emit('close')"
18
+ @confirm="emit('confirm')"
19
+ />
@@ -38,15 +38,17 @@ function deleteRecord(): void {
38
38
  }
39
39
  </script>
40
40
 
41
- <div :if="open()" class="flex fixed inset-0 z-50 items-center justify-center p-4 dashboard-modal-layer">
42
- <button type="button" aria-label="Close gift card details" class="absolute inset-0 bg-black/45" @click="emit('close')"></button>
43
- <section role="dialog" aria-modal="true" aria-labelledby="gift-card-details-title" class="overflow-y-auto relative max-h-[90dvh] max-w-2xl w-full bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700 shadow-xl">
44
- <div class="p-6">
45
- <div class="flex gap-4 items-start justify-between">
46
- <div><h2 id="gift-card-details-title" class="font-mono font-semibold text-gray-900 text-lg dark:text-white">{{ record()?.code || 'Gift card' }}</h2><p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">{{ record()?.recipientName || 'No recipient assigned' }}</p></div>
47
- <span class="px-2 py-1 font-semibold text-gray-700 text-xs dark:text-neutral-200 bg-gray-100 dark:bg-neutral-700 rounded-full">{{ record()?.status || 'Not recorded' }}</span>
48
- </div>
49
- <dl class="grid gap-4 grid-cols-1 sm:grid-cols-2 mt-6">
41
+ <Modal
42
+ :isOpen="open()"
43
+ :title="record()?.code || 'Gift card'"
44
+ :description="record()?.recipientName || 'No recipient assigned'"
45
+ size="lg"
46
+ @close="emit('close')"
47
+ >
48
+ <template #header-trailing>
49
+ <span class="px-2 py-1 font-semibold text-gray-700 text-xs dark:text-neutral-200 bg-gray-100 dark:bg-neutral-700 rounded-full">{{ record()?.status || 'Not recorded' }}</span>
50
+ </template>
51
+ <dl class="grid gap-4 grid-cols-1 sm:grid-cols-2">
50
52
  <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Current balance</dt><dd class="mt-1 font-semibold text-gray-900 text-sm dark:text-white">{{ money(record()?.currentBalance || 0, record()?.currency || 'USD') }}</dd></div>
51
53
  <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Initial balance</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ money(record()?.initialBalance || 0, record()?.currency || 'USD') }}</dd></div>
52
54
  <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Availability</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ record()?.isActive ? 'Enabled' : 'Disabled' }}</dd></div>
@@ -59,10 +61,10 @@ function deleteRecord(): void {
59
61
  <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Last used date</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ dateTime(record()?.lastUsedDate || '') }}</dd></div>
60
62
  <div class="sm:col-span-2"><dt class="text-gray-400 text-xs dark:text-neutral-500">Personal message</dt><dd class="mt-1 text-gray-900 text-sm whitespace-pre-wrap dark:text-white">{{ record()?.personalMessage || 'No message recorded.' }}</dd></div>
61
63
  </dl>
62
- </div>
63
- <footer class="flex gap-2 justify-between px-6 py-4 bg-gray-50 dark:bg-neutral-900/50 border-gray-200 border-t dark:border-neutral-700">
64
+ <template #footer>
65
+ <div class="flex gap-2 justify-between w-full">
64
66
  <Button variant="danger" @click="deleteRecord()">Delete</Button>
65
67
  <div class="flex gap-2"><Button variant="secondary" @click="emit('close')">Close</Button><Button @click="editRecord()">Edit gift card</Button></div>
66
- </footer>
67
- </section>
68
- </div>
68
+ </div>
69
+ </template>
70
+ </Modal>