@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
@@ -15,23 +15,9 @@ function formatDate(value: string | undefined): string {
15
15
  }
16
16
  </script>
17
17
 
18
- <div :if="open() && record()" class="flex fixed inset-0 z-50 items-center justify-center p-4 dashboard-modal-layer">
19
- <button type="button" aria-label="Close log details" class="absolute inset-0 bg-black/45" @click="emit('close')"></button>
20
- <section role="dialog" aria-modal="true" aria-labelledby="log-details-title" class="overflow-y-auto relative max-h-[90vh] max-w-2xl w-full bg-white dark:bg-neutral-900 border border-neutral-200 rounded-xl dark:border-neutral-700 shadow-xl">
21
- <header class="flex gap-4 items-start justify-between px-5 py-4 border-b border-neutral-200 dark:border-neutral-700">
22
- <div class="min-w-0">
23
- <div class="flex gap-2 items-center">
24
- <p class="text-neutral-500 text-xs dark:text-neutral-400">Log #{{ record()?.id }}</p>
25
- <LogLevelBadge :type="record()?.type || 'unknown'" />
26
- </div>
27
- <h2 id="log-details-title" class="mt-2 font-semibold text-lg text-neutral-900 dark:text-white">{{ record()?.message }}</h2>
28
- </div>
29
- <Button variant="ghost" iconOnly ariaLabel="Close log details" @click="emit('close')">
30
- <span class="block h-5 w-5 i-hugeicons-cancel-01"></span>
31
- </Button>
32
- </header>
33
-
34
- <dl class="grid gap-5 sm:grid-cols-2 p-5 text-sm">
18
+ <Modal :isOpen="open() && Boolean(record())" :title="record()?.message || 'Log details'" :description="`Log #${record()?.id || 'not recorded'}`" size="lg" @close="emit('close')">
19
+ <template #header-trailing><LogLevelBadge :type="record()?.type || 'unknown'" /></template>
20
+ <dl class="grid gap-5 sm:grid-cols-2 text-sm">
35
21
  <div><dt class="text-neutral-500 dark:text-neutral-400">Occurred</dt><dd class="mt-1 text-neutral-900 dark:text-white">{{ formatDate(record()?.timestamp) }}</dd></div>
36
22
  <div><dt class="text-neutral-500 dark:text-neutral-400">Persisted</dt><dd class="mt-1 text-neutral-900 dark:text-white">{{ formatDate(record()?.createdAt) }}</dd></div>
37
23
  <div><dt class="text-neutral-500 dark:text-neutral-400">Source</dt><dd class="mt-1 font-mono text-neutral-900 dark:text-white">{{ record()?.source || 'Not recorded' }}</dd></div>
@@ -42,5 +28,4 @@ function formatDate(value: string | undefined): string {
42
28
  <dd><pre class="overflow-x-auto mt-2 p-4 font-mono text-neutral-800 text-xs whitespace-pre-wrap dark:text-neutral-200 bg-neutral-50 dark:bg-neutral-950 border border-neutral-200 rounded-lg dark:border-neutral-800">{{ record()?.stacktrace }}</pre></dd>
43
29
  </div>
44
30
  </dl>
45
- </section>
46
- </div>
31
+ </Modal>
@@ -6,26 +6,16 @@ const mailbox = useReactiveProp('mailbox', null as DashboardMailbox | null)
6
6
  const emit = defineEmits()
7
7
  </script>
8
8
 
9
- <div :if="open() && mailbox()" class="flex fixed inset-0 z-50 items-center justify-center p-4 dashboard-modal-layer">
10
- <button type="button" aria-label="Close mailbox details" class="absolute inset-0 bg-black/45" @click="emit('close')"></button>
11
- <section role="dialog" aria-modal="true" aria-labelledby="mailbox-details-title" class="overflow-y-auto relative max-h-[90vh] max-w-lg w-full bg-white dark:bg-neutral-900 border border-neutral-200 rounded-xl dark:border-neutral-700 shadow-xl">
12
- <header class="flex gap-4 items-start justify-between px-5 py-4 border-b border-neutral-200 dark:border-neutral-700">
13
- <div>
14
- <p class="text-neutral-500 text-xs dark:text-neutral-400">Configured mailbox</p>
15
- <h2 id="mailbox-details-title" class="mt-1 break-all font-semibold text-lg text-neutral-900 dark:text-white">{{ mailbox()?.email }}</h2>
16
- </div>
17
- <Button variant="ghost" iconOnly ariaLabel="Close mailbox details" @click="emit('close')">
18
- <span class="block h-5 w-5 i-hugeicons-cancel-01"></span>
19
- </Button>
20
- </header>
21
- <dl class="grid gap-5 sm:grid-cols-2 p-5 text-sm">
9
+ <Modal :isOpen="open() && Boolean(mailbox())" :title="mailbox()?.email || 'Mailbox'" description="Configured mailbox" size="md" @close="emit('close')">
10
+ <dl class="grid gap-5 sm:grid-cols-2 text-sm">
22
11
  <div><dt class="text-neutral-500 dark:text-neutral-400">Username</dt><dd class="mt-1 font-mono text-neutral-900 dark:text-white">{{ mailbox()?.username }}</dd></div>
23
12
  <div><dt class="text-neutral-500 dark:text-neutral-400">State</dt><dd class="mt-1"><CloudStatusBadge :status="mailbox()?.status || 'inactive'" /></dd></div>
24
13
  <div class="sm:col-span-2"><dt class="text-neutral-500 dark:text-neutral-400">Address</dt><dd class="mt-1 break-all font-mono text-neutral-900 dark:text-white">{{ mailbox()?.email }}</dd></div>
25
14
  </dl>
26
- <div class="flex gap-3 justify-end px-5 py-4 bg-neutral-50 dark:bg-neutral-800/60 border-neutral-200 border-t dark:border-neutral-700">
15
+ <template #footer>
16
+ <div class="flex gap-3 justify-end w-full">
27
17
  <Button tag="a" href="/inbox/activity" variant="secondary">Mail activity</Button>
28
18
  <Button tag="a" href="/inbox">Open inbox</Button>
29
19
  </div>
30
- </section>
31
- </div>
20
+ </template>
21
+ </Modal>
@@ -0,0 +1,105 @@
1
+ <script client>
2
+ interface ServerDefinition {
3
+ id: string
4
+ key: string
5
+ name: string
6
+ role: string
7
+ region: string
8
+ size: string
9
+ instances: number
10
+ diskGb: number | null
11
+ diskType: string | null
12
+ encrypted: boolean
13
+ domain: string | null
14
+ database: string | null
15
+ }
16
+
17
+ interface CloudOperation {
18
+ phase: string
19
+ sourceType: string | null
20
+ targetType: string | null
21
+ updatedAt: string | null
22
+ attempts: number
23
+ }
24
+
25
+ interface ServerDeployment {
26
+ id: string
27
+ stackName: string
28
+ environment: string | null
29
+ serverName: string
30
+ provider: string
31
+ publicIp: string | null
32
+ sshUser: string | null
33
+ recordedAt: string
34
+ status: 'attention' | 'deployed'
35
+ operation: CloudOperation | null
36
+ }
37
+
38
+ const server = useReactiveProp('server', null as ServerDefinition | null)
39
+ const deployment = useReactiveProp('deployment', null as ServerDeployment | null)
40
+ const emit = defineEmits()
41
+ const title = derived(() => server()?.name || deployment()?.serverName || 'Server details')
42
+ const detailsPath = derived(() => {
43
+ const identifier = server()?.key || deployment()?.id
44
+ return identifier ? `/servers/${encodeURIComponent(identifier)}` : '/servers'
45
+ })
46
+
47
+ function formatDate(value: string | null | undefined): string {
48
+ if (!value)
49
+ return 'Not recorded'
50
+ const date = new Date(value)
51
+ return Number.isNaN(date.getTime())
52
+ ? 'Not recorded'
53
+ : new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(date)
54
+ }
55
+
56
+ function diskLabel(value: ServerDefinition | null): string {
57
+ if (!value || value.diskGb === null)
58
+ return 'Not set'
59
+ return `${value.diskGb} GB${value.diskType ? ` ${value.diskType}` : ''}`
60
+ }
61
+ </script>
62
+
63
+ <Modal
64
+ :isOpen="Boolean(server() || deployment())"
65
+ :title="title()"
66
+ :description="server() ? 'Configuration' : 'Deployment state'"
67
+ size="lg"
68
+ @close="emit('close')"
69
+ >
70
+ <dl :if="server()" class="grid gap-x-6 gap-y-4 grid-cols-1 sm:grid-cols-2 text-sm">
71
+ <div><dt class="text-neutral-500 dark:text-neutral-400">Role</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ server()?.role }}</dd></div>
72
+ <div><dt class="text-neutral-500 dark:text-neutral-400">Region</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ server()?.region }}</dd></div>
73
+ <div><dt class="text-neutral-500 dark:text-neutral-400">Size</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ server()?.size }}</dd></div>
74
+ <div><dt class="text-neutral-500 dark:text-neutral-400">Instances</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ server()?.instances }}</dd></div>
75
+ <div><dt class="text-neutral-500 dark:text-neutral-400">Disk</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ diskLabel(server()) }}</dd></div>
76
+ <div><dt class="text-neutral-500 dark:text-neutral-400">Encryption</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ server()?.encrypted ? 'Enabled' : 'Not enabled' }}</dd></div>
77
+ <div><dt class="text-neutral-500 dark:text-neutral-400">Domain</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ server()?.domain || 'Not set' }}</dd></div>
78
+ <div><dt class="text-neutral-500 dark:text-neutral-400">Database</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ server()?.database || 'Not set' }}</dd></div>
79
+ </dl>
80
+
81
+ <div :if="deployment()">
82
+ <dl class="grid gap-x-6 gap-y-4 grid-cols-1 sm:grid-cols-2 text-sm">
83
+ <div><dt class="text-neutral-500 dark:text-neutral-400">Stack</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ deployment()?.stackName }}</dd></div>
84
+ <div><dt class="text-neutral-500 dark:text-neutral-400">Environment</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ deployment()?.environment || 'Not mapped' }}</dd></div>
85
+ <div><dt class="text-neutral-500 dark:text-neutral-400">Provider</dt><dd class="mt-1 capitalize font-medium text-neutral-900 dark:text-white">{{ deployment()?.provider }}</dd></div>
86
+ <div><dt class="text-neutral-500 dark:text-neutral-400">Public IPv4</dt><dd class="mt-1 font-mono text-neutral-900 dark:text-white">{{ deployment()?.publicIp || 'Not recorded' }}</dd></div>
87
+ <div><dt class="text-neutral-500 dark:text-neutral-400">SSH user</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ deployment()?.sshUser || 'Not recorded' }}</dd></div>
88
+ <div><dt class="text-neutral-500 dark:text-neutral-400">State recorded</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ formatDate(deployment()?.recordedAt) }}</dd></div>
89
+ </dl>
90
+ <div :if="deployment()?.operation" class="mt-6 p-4 text-amber-900 dark:text-amber-100 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 rounded-xl dark:border-amber-900/70">
91
+ <div class="flex gap-3 items-center justify-between"><h3 class="font-semibold text-sm">Resize operation</h3><CloudStatusBadge :status="deployment()?.status || 'attention'" /></div>
92
+ <p class="mt-2 text-sm">Phase {{ deployment()?.operation?.phase }}. Requested {{ deployment()?.operation?.sourceType || 'current size' }} to {{ deployment()?.operation?.targetType || 'target size' }}.</p>
93
+ <p class="mt-2 text-amber-700 text-xs dark:text-amber-300">Updated {{ formatDate(deployment()?.operation?.updatedAt) }} · {{ deployment()?.operation?.attempts || 0 }} attempts</p>
94
+ </div>
95
+ </div>
96
+
97
+ <template #footer>
98
+ <div class="flex justify-end w-full">
99
+ <Button tag="a" :href="detailsPath()" @click="emit('close')">
100
+ Open full details
101
+ <span class="h-4 w-4 i-hugeicons-arrow-right-01"></span>
102
+ </Button>
103
+ </div>
104
+ </template>
105
+ </Modal>
@@ -140,10 +140,6 @@ const attentionDeployments = derived(() => deployments().filter(deployment => de
140
140
  const dnsResource = derived(() => network().find(resource => resource.id === 'network:dns') ?? null)
141
141
  const loadBalancer = derived(() => network().find(resource => resource.id === 'network:load-balancer') ?? null)
142
142
  const hasResults = derived(() => filteredServers().length > 0 || filteredDeployments().length > 0)
143
- const selectedServerDisk = derived(() => {
144
- const server = selectedServer()
145
- return server ? diskLabel(server) : ''
146
- })
147
143
 
148
144
  function formatDate(value: string | null): string {
149
145
  if (!value)
@@ -183,11 +179,6 @@ function closeInspector(): void {
183
179
  selectedDeployment.set(null)
184
180
  }
185
181
 
186
- function selectedDetailsPath(): string {
187
- const identifier = selectedServer()?.key || selectedDeployment()?.id
188
- return identifier ? `/servers/${encodeURIComponent(identifier)}` : '/servers'
189
- }
190
-
191
182
  async function loadServers(): Promise<void> {
192
183
  loading.set(true)
193
184
  loadError.set('')
@@ -214,11 +205,6 @@ async function loadServers(): Promise<void> {
214
205
  }
215
206
  }
216
207
 
217
- useEventListener('keydown', (event: KeyboardEvent) => {
218
- if (event.key === 'Escape')
219
- closeInspector()
220
- })
221
-
222
208
  onMount(() => {
223
209
  void loadServers()
224
210
  })
@@ -384,49 +370,9 @@ onMount(() => {
384
370
  </div>
385
371
  </section>
386
372
 
387
- <div :if="selectedServer() || selectedDeployment()" class="flex fixed inset-0 z-50 items-center justify-center p-4 bg-black/50 dashboard-modal-layer" @click.self="closeInspector">
388
- <section role="dialog" aria-modal="true" aria-labelledby="server-inspector-title" class="overflow-y-auto relative p-6 max-h-[90dvh] max-w-xl w-full bg-white dark:bg-neutral-900 border border-neutral-200 rounded-xl dark:border-neutral-700 shadow-xl">
389
- <div class="flex gap-4 items-start justify-between">
390
- <div>
391
- <p class="font-semibold text-blue-600 text-xs uppercase dark:text-blue-400">{{ selectedServer() ? 'Configuration' : 'Deployment state' }}</p>
392
- <h2 id="server-inspector-title" class="mt-1 font-semibold text-neutral-900 text-xl dark:text-white">{{ selectedServer()?.name || selectedDeployment()?.serverName }}</h2>
393
- </div>
394
- <Button variant="ghost" iconOnly ariaLabel="Close server details" @click="closeInspector"><span class="h-5 w-5 i-hugeicons-cancel-01"></span></Button>
395
- </div>
396
-
397
- <dl :if="selectedServer()" class="grid gap-x-6 gap-y-4 grid-cols-1 sm:grid-cols-2 mt-6 text-sm">
398
- <div><dt class="text-neutral-500 dark:text-neutral-400">Role</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ selectedServer()?.role }}</dd></div>
399
- <div><dt class="text-neutral-500 dark:text-neutral-400">Region</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ selectedServer()?.region }}</dd></div>
400
- <div><dt class="text-neutral-500 dark:text-neutral-400">Size</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ selectedServer()?.size }}</dd></div>
401
- <div><dt class="text-neutral-500 dark:text-neutral-400">Instances</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ selectedServer()?.instances }}</dd></div>
402
- <div><dt class="text-neutral-500 dark:text-neutral-400">Disk</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ selectedServerDisk() }}</dd></div>
403
- <div><dt class="text-neutral-500 dark:text-neutral-400">Encryption</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ selectedServer()?.encrypted ? 'Enabled' : 'Not enabled' }}</dd></div>
404
- <div><dt class="text-neutral-500 dark:text-neutral-400">Domain</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ selectedServer()?.domain || 'Not set' }}</dd></div>
405
- <div><dt class="text-neutral-500 dark:text-neutral-400">Database</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ selectedServer()?.database || 'Not set' }}</dd></div>
406
- </dl>
407
-
408
- <div :if="selectedDeployment()" class="mt-6">
409
- <dl class="grid gap-x-6 gap-y-4 grid-cols-1 sm:grid-cols-2 text-sm">
410
- <div><dt class="text-neutral-500 dark:text-neutral-400">Stack</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ selectedDeployment()?.stackName }}</dd></div>
411
- <div><dt class="text-neutral-500 dark:text-neutral-400">Environment</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ selectedDeployment()?.environment || 'Not mapped' }}</dd></div>
412
- <div><dt class="text-neutral-500 dark:text-neutral-400">Provider</dt><dd class="mt-1 capitalize font-medium text-neutral-900 dark:text-white">{{ selectedDeployment()?.provider }}</dd></div>
413
- <div><dt class="text-neutral-500 dark:text-neutral-400">Public IPv4</dt><dd class="mt-1 font-mono text-neutral-900 dark:text-white">{{ selectedDeployment()?.publicIp || 'Not recorded' }}</dd></div>
414
- <div><dt class="text-neutral-500 dark:text-neutral-400">SSH user</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ selectedDeployment()?.sshUser || 'Not recorded' }}</dd></div>
415
- <div><dt class="text-neutral-500 dark:text-neutral-400">State recorded</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ formatDate(selectedDeployment()?.recordedAt || null) }}</dd></div>
416
- </dl>
417
- <div :if="selectedDeployment()?.operation" class="mt-6 p-4 text-amber-900 dark:text-amber-100 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 rounded-xl dark:border-amber-900/70">
418
- <div class="flex gap-3 items-center justify-between"><h3 class="font-semibold text-sm">Resize operation</h3><CloudStatusBadge :status="selectedDeployment()?.status || 'attention'" /></div>
419
- <p class="mt-2 text-sm">Phase {{ selectedDeployment()?.operation?.phase }}. Requested {{ selectedDeployment()?.operation?.sourceType || 'current size' }} to {{ selectedDeployment()?.operation?.targetType || 'target size' }}.</p>
420
- <p class="mt-2 text-amber-700 text-xs dark:text-amber-300">Updated {{ formatDate(selectedDeployment()?.operation?.updatedAt || null) }} · {{ selectedDeployment()?.operation?.attempts || 0 }} attempts</p>
421
- </div>
422
- </div>
423
-
424
- <div class="flex justify-end mt-6 pt-5 border-neutral-200 border-t dark:border-neutral-700">
425
- <Button tag="a" :href="selectedDetailsPath()" @click="closeInspector">
426
- Open full details
427
- <span class="h-4 w-4 i-hugeicons-arrow-right-01"></span>
428
- </Button>
429
- </div>
430
- </section>
431
- </div>
373
+ <ServerInspectorDialog
374
+ :server="selectedServer()"
375
+ :deployment="selectedDeployment()"
376
+ @close="closeInspector"
377
+ />
432
378
  </div>
@@ -13,7 +13,6 @@ const selectedStatus = state('all')
13
13
  const searchQuery = state('')
14
14
  const isLoading = state(true)
15
15
  const loadError = state('')
16
- let searchTimer: ReturnType<typeof setTimeout> | undefined
17
16
 
18
17
  function totalPages(): number {
19
18
  return Math.max(1, Math.ceil(total() / perPage()))
@@ -32,11 +31,7 @@ function reloadFilters(): void {
32
31
  void loadJobs()
33
32
  }
34
33
 
35
- function reloadSearch(): void {
36
- if (searchTimer)
37
- clearTimeout(searchTimer)
38
- searchTimer = setTimeout(reloadFilters, 250)
39
- }
34
+ const reloadSearch = useDebounce(reloadFilters, 250)
40
35
 
41
36
  async function loadJobs(): Promise<void> {
42
37
  isLoading.set(true)
@@ -93,11 +88,6 @@ async function handleRetry(id: string | CustomEvent<string>): Promise<void> {
93
88
  onMount(() => {
94
89
  void loadJobs()
95
90
  })
96
-
97
- onDestroy(() => {
98
- if (searchTimer)
99
- clearTimeout(searchTimer)
100
- })
101
91
  </script>
102
92
 
103
93
  <div class="space-y-6">
@@ -28,7 +28,6 @@ const loadError = state('')
28
28
  const search = state('')
29
29
  const selectedCategory = state('All')
30
30
  const dialogOpen = state(false)
31
- const componentName = state('')
32
31
  const saving = state(false)
33
32
  const saveError = state('')
34
33
 
@@ -79,7 +78,6 @@ async function loadComponents(): Promise<void> {
79
78
  }
80
79
 
81
80
  function openCreate(): void {
82
- componentName.set('')
83
81
  saveError.set('')
84
82
  dialogOpen.set(true)
85
83
  }
@@ -89,20 +87,16 @@ function closeCreate(): void {
89
87
  dialogOpen.set(false)
90
88
  }
91
89
 
92
- function validName(): boolean {
93
- return /^[A-Z][A-Za-z0-9]*$/.test(componentName().trim())
94
- }
95
-
96
- async function createComponent(): Promise<void> {
90
+ async function createComponent(name: string): Promise<void> {
97
91
  saving.set(true)
98
92
  saveError.set('')
99
93
  try {
100
94
  await dashboardApi('/api/dashboard/library/components', {
101
95
  method: 'POST',
102
- body: { name: componentName().trim() },
96
+ body: { name },
103
97
  })
104
98
  dialogOpen.set(false)
105
- pushToast('success', 'Component created', { detail: `${componentName().trim()}.stx` })
99
+ pushToast('success', 'Component created', { detail: `${name}.stx` })
106
100
  await loadComponents()
107
101
  }
108
102
  catch (error) {
@@ -243,54 +237,11 @@ onMount(() => {
243
237
  </section>
244
238
  </div>
245
239
 
246
- <div :if="dialogOpen()" class="flex fixed inset-y-0 right-0 z-[55] items-center justify-center p-4 dashboard-modal-layer">
247
- <button type="button" aria-label="Close component dialog" class="absolute inset-0 bg-black/45" @click="closeCreate()"></button>
248
- <section
249
- role="dialog"
250
- aria-modal="true"
251
- aria-label="Create component"
252
- class="relative max-w-md w-full bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700 shadow-xl"
253
- >
254
- <form @submit.prevent="createComponent()">
255
- <div class="p-6">
256
- <h2 class="font-semibold text-gray-900 text-lg dark:text-white">Create component</h2>
257
- <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Uses the native Stacks component scaffold in resources/components.</p>
258
- <div
259
- :if="saveError()"
260
- role="alert"
261
- 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"
262
- :text="saveError()"
263
- ></div>
264
- <label class="block mt-5">
265
- <span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Name</span>
266
- <input
267
- x-model.trim="componentName"
268
- required
269
- pattern="[A-Z][A-Za-z0-9]*"
270
- placeholder="OrderSummary"
271
- 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"
272
- >
273
- <span class="block mt-1 text-gray-500 text-xs dark:text-neutral-400">Use a PascalCase component name.</span>
274
- </label>
275
- </div>
276
- <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">
277
- <Button
278
- type="button"
279
- variant="secondary"
280
- :disabled="saving()"
281
- @click="closeCreate()"
282
- >
283
- Cancel
284
- </Button>
285
- <Button
286
- type="submit"
287
- variant="primary"
288
- :loading="saving()"
289
- :disabled="saving() || !validName()"
290
- >
291
- Create component
292
- </Button>
293
- </footer>
294
- </form>
295
- </section>
296
- </div>
240
+ <SourceScaffoldDialog
241
+ :open="dialogOpen()"
242
+ kind="component"
243
+ :busy="saving()"
244
+ :error="saveError()"
245
+ @close="closeCreate"
246
+ @submit="createComponent"
247
+ />
@@ -20,7 +20,6 @@ const loading = state(true)
20
20
  const loadError = state('')
21
21
  const search = state('')
22
22
  const dialogOpen = state(false)
23
- const functionName = state('')
24
23
  const saving = state(false)
25
24
  const saveError = state('')
26
25
 
@@ -61,7 +60,6 @@ async function loadFunctions(): Promise<void> {
61
60
  }
62
61
 
63
62
  function openCreate(): void {
64
- functionName.set('')
65
63
  saveError.set('')
66
64
  dialogOpen.set(true)
67
65
  }
@@ -71,20 +69,16 @@ function closeCreate(): void {
71
69
  dialogOpen.set(false)
72
70
  }
73
71
 
74
- function validName(): boolean {
75
- return /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(functionName().trim())
76
- }
77
-
78
- async function createFunction(): Promise<void> {
72
+ async function createFunction(name: string): Promise<void> {
79
73
  saving.set(true)
80
74
  saveError.set('')
81
75
  try {
82
76
  await dashboardApi('/api/dashboard/library/functions', {
83
77
  method: 'POST',
84
- body: { name: functionName().trim() },
78
+ body: { name },
85
79
  })
86
80
  dialogOpen.set(false)
87
- pushToast('success', 'Function created', { detail: `${functionName().trim()}.ts` })
81
+ pushToast('success', 'Function created', { detail: `${name}.ts` })
88
82
  await loadFunctions()
89
83
  }
90
84
  catch (error) {
@@ -122,9 +116,11 @@ onMount(() => { void loadFunctions() })
122
116
  </section>
123
117
  </div>
124
118
 
125
- <div :if="dialogOpen()" class="flex fixed inset-y-0 right-0 z-[55] items-center justify-center p-4 dashboard-modal-layer">
126
- <button type="button" aria-label="Close function dialog" class="absolute inset-0 bg-black/45" @click="closeCreate()"></button>
127
- <section role="dialog" aria-modal="true" aria-label="Create function" class="relative max-w-md w-full bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700 shadow-xl">
128
- <form @submit.prevent="createFunction()"><div class="p-6"><h2 class="font-semibold text-gray-900 text-lg dark:text-white">Create function</h2><p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Uses the native Stacks function scaffold in resources/functions.</p><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><label class="block mt-5"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Name</span><input x-model.trim="functionName" required pattern="[a-z][a-z0-9]*(?:-[a-z0-9]+)*" placeholder="send-invoice" 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" /><span class="block mt-1 text-gray-500 text-xs dark:text-neutral-400">Lowercase kebab-case. The generated export uses camelCase.</span></label></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 type="button" variant="secondary" :disabled="saving()" @click="closeCreate()">Cancel</Button><Button type="submit" variant="primary" :loading="saving()" :disabled="saving() || !validName()">Create function</Button></footer></form>
129
- </section>
130
- </div>
119
+ <SourceScaffoldDialog
120
+ :open="dialogOpen()"
121
+ kind="function"
122
+ :busy="saving()"
123
+ :error="saveError()"
124
+ @close="closeCreate"
125
+ @submit="createFunction"
126
+ />
@@ -0,0 +1,95 @@
1
+ <script client>
2
+ type SourceKind = 'component' | 'function'
3
+
4
+ const open = useReactiveProp('open', false)
5
+ const kind = useReactiveProp('kind', 'component' as SourceKind)
6
+ const busy = useReactiveProp('busy', false)
7
+ const error = useReactiveProp('error', '')
8
+ const sourceName = state('')
9
+ const emit = defineEmits()
10
+ const actionLabel = derived(() => `Create ${kind()}`)
11
+ let wasOpen = false
12
+
13
+ effect(() => {
14
+ const isOpen = open()
15
+ if (isOpen && !wasOpen)
16
+ sourceName.set('')
17
+ wasOpen = isOpen
18
+ })
19
+
20
+ function description(): string {
21
+ return kind() === 'component'
22
+ ? 'Uses the native Stacks component scaffold in resources/components.'
23
+ : 'Uses the native Stacks function scaffold in resources/functions.'
24
+ }
25
+
26
+ function pattern(): string {
27
+ return kind() === 'component'
28
+ ? '[A-Z][A-Za-z0-9]*'
29
+ : '[a-z][a-z0-9]*(?:-[a-z0-9]+)*'
30
+ }
31
+
32
+ function placeholder(): string {
33
+ return kind() === 'component' ? 'OrderSummary' : 'send-invoice'
34
+ }
35
+
36
+ function hint(): string {
37
+ return kind() === 'component'
38
+ ? 'Use a PascalCase component name.'
39
+ : 'Use lowercase kebab-case. The generated export uses camelCase.'
40
+ }
41
+
42
+ function valid(): boolean {
43
+ const value = sourceName().trim()
44
+ return kind() === 'component'
45
+ ? /^[A-Z][A-Za-z0-9]*$/.test(value)
46
+ : /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(value)
47
+ }
48
+
49
+ function close(): void {
50
+ if (!busy())
51
+ emit('close')
52
+ }
53
+
54
+ function submit(): void {
55
+ if (valid())
56
+ emit('submit', sourceName().trim())
57
+ }
58
+ </script>
59
+
60
+ <Modal
61
+ :isOpen="open()"
62
+ :title="actionLabel()"
63
+ :description="description()"
64
+ size="sm"
65
+ :closable="!busy()"
66
+ @close="close"
67
+ >
68
+ <form id="source-scaffold-form" @submit.prevent="submit">
69
+ <label class="block">
70
+ <span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Name</span>
71
+ <input
72
+ x-model.trim="sourceName"
73
+ name="name"
74
+ required
75
+ :pattern="pattern()"
76
+ :placeholder="placeholder()"
77
+ 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"
78
+ >
79
+ <span class="block mt-1 text-gray-500 text-xs dark:text-neutral-400">{{ hint() }}</span>
80
+ </label>
81
+ </form>
82
+
83
+ <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">
84
+ {{ error() }}
85
+ </div>
86
+
87
+ <template #footer>
88
+ <div class="flex gap-2 justify-end w-full">
89
+ <Button type="button" variant="secondary" :disabled="busy()" @click="close">Cancel</Button>
90
+ <Button type="submit" form="source-scaffold-form" :loading="busy()" :disabled="!valid()">
91
+ {{ actionLabel() }}
92
+ </Button>
93
+ </div>
94
+ </template>
95
+ </Modal>
@@ -0,0 +1,93 @@
1
+ <script client>
2
+ interface AssignmentOption {
3
+ name: string
4
+ description?: string | null
5
+ }
6
+
7
+ interface GuardOption {
8
+ value: string
9
+ label: string
10
+ }
11
+
12
+ const open = useReactiveProp('open', false)
13
+ const title = useReactiveProp('title', 'Manage assignments')
14
+ const description = useReactiveProp('description', '')
15
+ const options = useReactiveProp('options', [] as AssignmentOption[])
16
+ const initialSelected = useReactiveProp('selected', [] as string[])
17
+ const guard = useReactiveProp('guard', '')
18
+ const guardOptions = useReactiveProp('guardOptions', [] as GuardOption[])
19
+ const loading = useReactiveProp('loading', false)
20
+ const busy = useReactiveProp('busy', false)
21
+ const emptyMessage = useReactiveProp('emptyMessage', 'No assignments are available.')
22
+ const selected = state<string[]>([])
23
+ const guardValue = state('')
24
+ const emit = defineEmits()
25
+
26
+ effect(() => {
27
+ selected.set([...initialSelected()])
28
+ })
29
+
30
+ effect(() => {
31
+ guardValue.set(guard())
32
+ })
33
+
34
+ function toggle(name: string): void {
35
+ const current = selected()
36
+ selected.set(current.includes(name)
37
+ ? current.filter(value => value !== name)
38
+ : [...current, name])
39
+ }
40
+
41
+ function changeGuard(): void {
42
+ emit('guard-change', guardValue())
43
+ }
44
+
45
+ function close(): void {
46
+ if (!busy())
47
+ emit('close')
48
+ }
49
+
50
+ function save(): void {
51
+ if (!loading() && !busy())
52
+ emit('save', [...selected()])
53
+ }
54
+ </script>
55
+
56
+ <Drawer
57
+ :isOpen="open()"
58
+ :title="title()"
59
+ :description="description()"
60
+ :closable="!busy()"
61
+ @close="close"
62
+ >
63
+ <div :if="guardOptions().length > 0" class="px-5 py-3 border-b border-zinc-200 dark:border-zinc-800">
64
+ <label for="assignment-guard" class="block mb-1.5 font-medium text-zinc-600 text-xs dark:text-zinc-400">Guard</label>
65
+ <select id="assignment-guard" x-model="guardValue" name="guard" class="px-3 py-1.5 w-full text-zinc-900 text-sm dark:text-zinc-100 bg-white dark:bg-neutral-800 border border-zinc-300 rounded-md dark:border-zinc-700 focus:border-violet-500 focus:outline-none focus:ring-2 focus:ring-violet-500/20" @change="changeGuard">
66
+ <template :for="option in guardOptions()">
67
+ <option :value="option.value">{{ option.label }}</option>
68
+ </template>
69
+ </select>
70
+ </div>
71
+
72
+ <div class="px-5 py-4 space-y-2">
73
+ <div :if="loading()" class="flex gap-2 items-center py-6 text-zinc-500 text-sm">
74
+ <span class="h-4 w-4 animate-spin i-hugeicons-loading-03"></span>
75
+ Loading assignments...
76
+ </div>
77
+ <div :if="!loading() && options().length === 0" class="text-zinc-500 text-sm">{{ emptyMessage() }}</div>
78
+ <template :if="!loading()" :for="option in options()">
79
+ <label class="flex gap-2 items-center px-2 py-1.5 hover:bg-zinc-50 dark:hover:bg-neutral-800 rounded-md cursor-pointer">
80
+ <input name="assignments" type="checkbox" :value="option.name" :checked="selected().includes(option.name)" class="h-4 w-4 text-violet-600 border-zinc-300 rounded dark:border-zinc-700 focus:ring-violet-500" @change="toggle(option.name)" />
81
+ <span class="text-zinc-900 text-sm dark:text-zinc-100">{{ option.name }}</span>
82
+ <span class="text-zinc-500 text-xs">{{ option.description || '' }}</span>
83
+ </label>
84
+ </template>
85
+ </div>
86
+
87
+ <template #footer>
88
+ <div class="flex gap-2 items-center justify-end w-full">
89
+ <Button variant="secondary" :disabled="busy()" @click="close">Cancel</Button>
90
+ <Button :disabled="loading()" :loading="busy()" @click="save">Save</Button>
91
+ </div>
92
+ </template>
93
+ </Drawer>