@stacksjs/defaults 0.72.98 → 0.72.99

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 (87) hide show
  1. package/ai/skills/stacks-auth/SKILL.md +35 -5
  2. package/ai/skills/stacks-config/SKILL.md +11 -1
  3. package/ai/skills/stacks-desktop/SKILL.md +46 -0
  4. package/ai/skills/stacks-env/SKILL.md +26 -0
  5. package/ai/skills/stacks-events/SKILL.md +76 -60
  6. package/ai/skills/stacks-jobs/SKILL.md +3 -0
  7. package/ai/skills/stacks-listeners/SKILL.md +39 -9
  8. package/ai/skills/stacks-middleware/SKILL.md +50 -12
  9. package/ai/skills/stacks-queue/SKILL.md +5 -0
  10. package/ai/skills/stacks-router/SKILL.md +17 -7
  11. package/ai/skills/stacks-scheduler/SKILL.md +1 -1
  12. package/app/Actions/Dashboard/Commerce/CommerceCustomersAction.ts +1 -1
  13. package/app/Actions/Dashboard/Commerce/CommerceDeliveryAction.ts +1 -1
  14. package/app/Actions/Dashboard/Commerce/CommerceOrdersAction.ts +1 -1
  15. package/app/Actions/Dashboard/Commerce/CommercePosAction.ts +1 -1
  16. package/app/Actions/Dashboard/Commerce/CommercePosCheckoutAction.ts +1 -1
  17. package/app/Actions/Dashboard/Commerce/CommerceProductDetailAction.ts +1 -1
  18. package/app/Actions/Dashboard/Commerce/CommerceProductsAction.ts +1 -1
  19. package/app/Actions/Dashboard/Content/content-input.ts +2 -2
  20. package/app/Actions/Dashboard/Content/post-input.ts +4 -4
  21. package/app/Actions/Dashboard/Email/inbox-request.ts +1 -1
  22. package/app/Actions/Dashboard/Email/mail-preference.ts +1 -1
  23. package/app/Actions/Dashboard/Marketing/CampaignIndexAction.ts +1 -1
  24. package/app/Actions/Dashboard/Marketing/CampaignStoreAction.ts +1 -1
  25. package/app/Actions/Dashboard/Marketing/CampaignUpdateAction.ts +1 -1
  26. package/app/Actions/Dashboard/Teams/TeamInvitationDestroyAction.ts +2 -2
  27. package/app/Actions/Dashboard/Teams/TeamInvitationResendAction.ts +2 -2
  28. package/app/Actions/Dashboard/Teams/TeamInviteAction.ts +5 -5
  29. package/app/Actions/Dashboard/Teams/TeamMemberDestroyAction.ts +2 -2
  30. package/app/Actions/Dashboard/Teams/TeamMemberUpdateAction.ts +2 -2
  31. package/app/Actions/Dashboard/Teams/TeamPeopleIndexAction.ts +2 -2
  32. package/app/Actions/Dashboard/Teams/team-invitation-delivery.ts +2 -2
  33. package/app/Events.ts +9 -4
  34. package/app/Gates.ts +83 -85
  35. package/app/Listener.ts +34 -81
  36. package/app/Middleware.ts +22 -7
  37. package/ide/vscode/package.json +1 -1
  38. package/package.json +2 -2
  39. package/project/storage/framework/server/tsconfig.docker.json +1 -2
  40. package/resources/components/Buttons/Counter.stx +30 -21
  41. package/resources/components/Dashboard/Analytics/WebAnalyticsDashboard.stx +1 -3
  42. package/resources/components/Dashboard/Auth/ForgotPasswordDashboard.stx +1 -1
  43. package/resources/components/Dashboard/Auth/LoginDashboard.stx +2 -1
  44. package/resources/components/Dashboard/Auth/RegisterDashboard.stx +2 -1
  45. package/resources/components/Dashboard/Auth/ResetPasswordDashboard.stx +1 -1
  46. package/resources/components/Dashboard/Ci/CiDashboard.stx +9 -9
  47. package/resources/components/Dashboard/Ci/CiRunHistoryDrawer.stx +7 -7
  48. package/resources/components/Dashboard/Commerce/CommerceOverviewCharts.stx +2 -1
  49. package/resources/components/Dashboard/Commerce/CommercePaymentsDashboard.stx +1 -1
  50. package/resources/components/Dashboard/Content/BlogDashboard.stx +37 -15
  51. package/resources/components/Dashboard/Content/ContentDashboard.stx +3 -26
  52. package/resources/components/Dashboard/Email/EmailActivityDashboard.stx +2 -15
  53. package/resources/components/Dashboard/Environment/EnvironmentDashboard.stx +9 -1
  54. package/resources/components/Dashboard/Kanban/KanbanBoardDashboard.stx +15 -13
  55. package/resources/components/Dashboard/Kanban/KanbanBoardsDashboard.stx +10 -8
  56. package/resources/components/Dashboard/Kanban/KanbanCardDialog.stx +10 -10
  57. package/resources/components/Dashboard/Management/PermissionsDashboard.stx +3 -3
  58. package/resources/components/Dashboard/Modals/Popups/Alert.stx +4 -1
  59. package/resources/components/Dashboard/Navbar.stx +1 -1
  60. package/resources/components/Dashboard/Settings/AppearanceSettingsDashboard.stx +11 -6
  61. package/resources/components/Dashboard/Settings/SettingsDashboard.stx +2 -2
  62. package/resources/components/Dashboard/Transaction/index.stx +6 -6
  63. package/resources/components/Dashboard/UI/Avatar.stx +2 -2
  64. package/resources/components/Dashboard/UI/DropdownItem.stx +4 -4
  65. package/resources/components/Dashboard/UI/WindowControls.stx +17 -2
  66. package/resources/components/Docs/Demo/ComboboxDemo.stx +8 -3
  67. package/resources/components/Docs/Demo/DropdownDemo.stx +3 -3
  68. package/resources/components/Docs/Demo/StepperDemo.stx +1 -1
  69. package/resources/components/Forum/ForumLayout.stx +1 -1
  70. package/resources/components/Forum/ForumReplyForm.stx +1 -1
  71. package/resources/components/Marketing/ComingSoon.stx +5 -5
  72. package/resources/components/Storefront/CartDrawer.stx +44 -22
  73. package/resources/components/Storefront/ProductGallery.stx +3 -2
  74. package/resources/components/Storefront/QuantitySelector.stx +12 -7
  75. package/resources/functions/storefront/cart-cookie.ts +58 -0
  76. package/resources/layouts/storefront.stx +3 -2
  77. package/resources/views/cart.stx +19 -3
  78. package/resources/views/checkout/contact.stx +14 -3
  79. package/resources/views/checkout/payment.stx +14 -3
  80. package/resources/views/checkout/shipping.stx +14 -3
  81. package/resources/views/cms/blocks/form.stx +16 -11
  82. package/resources/views/cms/page.stx +28 -0
  83. package/resources/views/coming-soon.stx +0 -6
  84. package/resources/views/orders/[id].stx +19 -3
  85. package/views/dashboard/composables/index.ts +1 -1
  86. package/views/dashboard/composables/useChart.ts +26 -14
  87. package/views/dashboard/layouts/default.stx +42 -25
@@ -29,7 +29,7 @@
29
29
  const columns = derived(() => kanban.currentColumns())
30
30
  // Inline composer state. Each column has its own (independent) "add
31
31
  // card" composer; the boolean tracks whether the input is open.
32
- const composingCardForColumnId = state(null)
32
+ const composingCardForColumnId = state<number | null>(null)
33
33
  const draftCardTitle = state('')
34
34
  const composingNewColumn = state(false)
35
35
  const draftColumnName = state('')
@@ -111,15 +111,16 @@
111
111
  // Column color → header accent class. Free-form string in the
112
112
  // model, mapped here so a new hue without an explicit mapping
113
113
  // falls back to slate.
114
- function columnAccentClass(color) {
115
- return ({
114
+ function columnAccentClass(color: string) {
115
+ const swatches: Record<string, string> = {
116
116
  slate: 'bg-slate-100 dark:bg-slate-800',
117
117
  amber: 'bg-amber-100 dark:bg-amber-900/40',
118
118
  blue: 'bg-blue-100 dark:bg-blue-900/40',
119
119
  emerald: 'bg-emerald-100 dark:bg-emerald-900/40',
120
120
  red: 'bg-red-100 dark:bg-red-900/40',
121
121
  violet: 'bg-violet-100 dark:bg-violet-900/40',
122
- })[color] || 'bg-slate-100 dark:bg-slate-800'
122
+ }
123
+ return swatches[color] || 'bg-slate-100 dark:bg-slate-800'
123
124
  }
124
125
 
125
126
  // Pull the id from the URL through the navigation composable.
@@ -130,7 +131,7 @@
130
131
 
131
132
  // ─── Composer handlers ────────────────────────────────────────────
132
133
 
133
- function openCardComposer(columnId) {
134
+ function openCardComposer(columnId: number) {
134
135
  composingCardForColumnId.set(columnId)
135
136
  draftCardTitle.set('')
136
137
  }
@@ -138,7 +139,7 @@
138
139
  composingCardForColumnId.set(null)
139
140
  draftCardTitle.set('')
140
141
  }
141
- async function submitCard(columnId) {
142
+ async function submitCard(columnId: number) {
142
143
  const title = draftCardTitle().trim()
143
144
  if (!title) {
144
145
  closeCardComposer()
@@ -147,7 +148,7 @@
147
148
  await kanban.createCard({ columnId, title })
148
149
  closeCardComposer()
149
150
  }
150
- function isComposingFor(columnId) {
151
+ function isComposingFor(columnId: number) {
151
152
  return composingCardForColumnId() === columnId
152
153
  }
153
154
 
@@ -165,21 +166,22 @@
165
166
  closeColumnComposer()
166
167
  return
167
168
  }
168
- if (!board()) return
169
- await kanban.createColumn({ boardId: board().id, name })
169
+ const current = board()
170
+ if (!current) return
171
+ await kanban.createColumn({ boardId: current.id, name })
170
172
  closeColumnComposer()
171
173
  }
172
174
 
173
175
  // ─── Card detail launcher ─────────────────────────────────────────
174
176
 
175
- function openDetail(cardId) {
177
+ function openDetail(cardId: number) {
176
178
  kanban.loadUsers() // background-fetch for assignee picker
177
179
  kanban.openCardDetail(cardId)
178
180
  }
179
181
 
180
182
  // Label color → tailwind hue. Mirrors the column-accent map but
181
183
  // returns a more saturated chip class.
182
- function labelChipClass(color) {
184
+ function labelChipClass(color: string) {
183
185
  return ({
184
186
  slate: 'bg-slate-100 text-slate-700 dark:bg-slate-800/60 dark:text-slate-200',
185
187
  amber: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-200',
@@ -191,7 +193,7 @@
191
193
  })[color] || 'bg-slate-100 text-slate-700 dark:bg-slate-800/60 dark:text-slate-200'
192
194
  }
193
195
 
194
- function initialsOf(name, email) {
196
+ function initialsOf(name: string | null | undefined, email: string | null | undefined) {
195
197
  const source = (name || email || '?').trim()
196
198
  const parts = source.split(/\s+/).filter(Boolean)
197
199
  if (parts.length === 0) return '?'
@@ -199,7 +201,7 @@
199
201
  return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
200
202
  }
201
203
 
202
- function formatDueDate(iso) {
204
+ function formatDueDate(iso: string | null | undefined) {
203
205
  if (!iso) return ''
204
206
  const d = new Date(iso)
205
207
  if (Number.isNaN(d.getTime())) return ''
@@ -99,18 +99,19 @@
99
99
  }
100
100
  }
101
101
 
102
- function colorSwatchClass(c) {
103
- return ({
102
+ function colorSwatchClass(c: string) {
103
+ const swatches: Record<string, string> = {
104
104
  violet: 'bg-violet-500',
105
105
  blue: 'bg-blue-500',
106
106
  emerald: 'bg-emerald-500',
107
107
  amber: 'bg-amber-500',
108
108
  rose: 'bg-rose-500',
109
109
  slate: 'bg-slate-500',
110
- })[c] || 'bg-violet-500'
110
+ }
111
+ return swatches[c] || 'bg-violet-500'
111
112
  }
112
113
 
113
- function colorButtonClass(c) {
114
+ function colorButtonClass(c: string) {
114
115
  const base = `w-6 h-6 rounded-full ${colorSwatchClass(c)} cursor-pointer transition-all `
115
116
  return draftColor() === c
116
117
  ? base + 'ring-2 ring-offset-2 ring-violet-500 dark:ring-offset-zinc-950'
@@ -120,18 +121,19 @@
120
121
  // Color → tailwind class. Free-form string in the model, mapped here
121
122
  // so the UI stays stable when a new hue is added without an explicit
122
123
  // mapping (falls back to violet).
123
- function accentClass(color) {
124
- return ({
124
+ function accentClass(color: string) {
125
+ const swatches: Record<string, string> = {
125
126
  violet: 'bg-violet-500',
126
127
  blue: 'bg-blue-500',
127
128
  emerald: 'bg-emerald-500',
128
129
  amber: 'bg-amber-500',
129
130
  rose: 'bg-rose-500',
130
131
  slate: 'bg-slate-500',
131
- })[color] || 'bg-violet-500'
132
+ }
133
+ return swatches[color] || 'bg-violet-500'
132
134
  }
133
135
 
134
- function cardCountText(n) {
136
+ function cardCountText(n: number) {
135
137
  if (n === 0) return 'No cards'
136
138
  if (n === 1) return '1 card'
137
139
  return n + ' cards'
@@ -214,7 +214,7 @@ function initialsOf(name: string | null, email: string | null): string {
214
214
  return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase()
215
215
  }
216
216
 
217
- function formatTimestamp(iso: string | null): string {
217
+ function formatTimestamp(iso: string | null | undefined): string {
218
218
  if (!iso)
219
219
  return ''
220
220
  const date = new Date(iso)
@@ -286,7 +286,7 @@ function formatTimestamp(iso: string | null): string {
286
286
  </div>
287
287
  </div>
288
288
 
289
- <div :if="openCard().comments.length === 0 && !loadingCard()" class="italic text-zinc-400 text-xs">No comments yet.</div>
289
+ <div :if="cardComments().length === 0 && !loadingCard()" class="italic text-zinc-400 text-xs">No comments yet.</div>
290
290
  <ul class="space-y-3">
291
291
  <li :for="comment in cardComments" class="flex gap-3">
292
292
  <span class="inline-flex flex-shrink-0 items-center justify-center h-7 w-7 font-medium text-[10px] text-white bg-violet-500 rounded-full" :text="initialsOf(comment.authorName, comment.authorEmail)"></span>
@@ -334,14 +334,14 @@ function formatTimestamp(iso: string | null): string {
334
334
 
335
335
  <div :if="!showLabelPicker()" class="flex flex-wrap gap-1">
336
336
  <span :for="label in cardLabels" :class="`px-2 py-0.5 text-xs font-medium rounded ${labelChipClass(label.color)}`" :text="label.name"></span>
337
- <span :if="openCard().labels.length === 0" class="italic text-zinc-400 text-xs">None</span>
337
+ <span :if="cardLabels().length === 0" class="italic text-zinc-400 text-xs">None</span>
338
338
  </div>
339
339
 
340
340
  <div :if="showLabelPicker()" class="space-y-2">
341
341
  <div :for="label in allLabels" :class="`flex items-center gap-1 px-1 py-0.5 w-full rounded ${labelChipClass(label.color)}`">
342
- <button type="button" class="flex flex-1 items-center justify-between px-1 py-0.5 min-w-0 text-left text-xs rounded hover:opacity-90" :aria-pressed="String(openCard().labels.some(cardLabel => cardLabel.id === label.id))" @click="toggleLabel(label.id)">
342
+ <button type="button" class="flex flex-1 items-center justify-between px-1 py-0.5 min-w-0 text-left text-xs rounded hover:opacity-90" :aria-pressed="String(cardLabels().some(cardLabel => cardLabel.id === label.id))" @click="toggleLabel(label.id)">
343
343
  <span class="truncate" :text="label.name"></span>
344
- <span :if="openCard().labels.some(cardLabel => cardLabel.id === label.id)" class="h-3 w-3 i-hugeicons-tick-02"></span>
344
+ <span :if="cardLabels().some(cardLabel => cardLabel.id === label.id)" class="h-3 w-3 i-hugeicons-tick-02"></span>
345
345
  </button>
346
346
  <Button variant="ghost" size="xs" iconOnly :ariaLabel="'Delete label ' + label.name" @click="requestDelete('label', label.id)">
347
347
  <span class="block h-3 w-3 text-red-500 i-hugeicons-cancel-01"></span>
@@ -376,15 +376,15 @@ function formatTimestamp(iso: string | null): string {
376
376
  <span class="inline-flex items-center justify-center h-6 w-6 font-medium text-[10px] text-white bg-violet-500 rounded-full" :text="initialsOf(assignee.name, assignee.email)"></span>
377
377
  <span :text="assignee.name || assignee.email || 'Unknown'"></span>
378
378
  </div>
379
- <span :if="openCard().assignees.length === 0" class="italic text-zinc-400 text-xs">Unassigned</span>
379
+ <span :if="cardAssignees().length === 0" class="italic text-zinc-400 text-xs">Unassigned</span>
380
380
  </div>
381
381
 
382
382
  <div :if="showAssigneePicker()" class="overflow-y-auto space-y-1 max-h-48">
383
383
  <p :if="kanban.users().length === 0" class="italic text-zinc-400 text-xs">No users to assign.</p>
384
- <button :for="user in kanban.users" type="button" :aria-pressed="String(openCard().assignees.some(assignee => assignee.userId === user.id))" @click="toggleAssignee(user.id)" class="flex gap-2 items-center px-2 py-1.5 w-full text-left text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded">
384
+ <button :for="user in kanban.users" type="button" :aria-pressed="String(cardAssignees().some(assignee => assignee.userId === user.id))" @click="toggleAssignee(user.id)" class="flex gap-2 items-center px-2 py-1.5 w-full text-left text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded">
385
385
  <span class="inline-flex flex-shrink-0 items-center justify-center h-6 w-6 font-medium text-[10px] text-white bg-violet-500 rounded-full" :text="initialsOf(user.name, user.email)"></span>
386
386
  <span class="flex-1 text-zinc-700 truncate dark:text-zinc-200" :text="user.name || user.email"></span>
387
- <span :if="openCard().assignees.some(assignee => assignee.userId === user.id)" class="h-3.5 w-3.5 text-violet-500 i-hugeicons-tick-02"></span>
387
+ <span :if="cardAssignees().some(assignee => assignee.userId === user.id)" class="h-3.5 w-3.5 text-violet-500 i-hugeicons-tick-02"></span>
388
388
  </button>
389
389
  </div>
390
390
  </section>
@@ -395,8 +395,8 @@ function formatTimestamp(iso: string | null): string {
395
395
  </section>
396
396
 
397
397
  <section class="pt-3 border-t border-zinc-200 dark:border-zinc-800">
398
- <p class="text-[10px] text-zinc-400">Created <span :text="formatTimestamp(openCard().createdAt)"></span></p>
399
- <p :if="openCard().updatedAt && openCard().updatedAt !== openCard().createdAt" class="text-[10px] text-zinc-400">Updated <span :text="formatTimestamp(openCard().updatedAt)"></span></p>
398
+ <p class="text-[10px] text-zinc-400">Created <span :text="formatTimestamp(openCard()?.createdAt)"></span></p>
399
+ <p :if="openCard()?.updatedAt && openCard()?.updatedAt !== openCard()?.createdAt" class="text-[10px] text-zinc-400">Updated <span :text="formatTimestamp(openCard()?.updatedAt)"></span></p>
400
400
  </section>
401
401
  </aside>
402
402
  </div>
@@ -63,14 +63,14 @@
63
63
 
64
64
  const activeTab = state('roles')
65
65
 
66
- function tabClass(id) {
66
+ function tabClass(id: string) {
67
67
  const base = 'px-4 py-2 font-medium text-sm whitespace-nowrap bg-transparent border-b-2 transition-colors cursor-pointer '
68
68
  return activeTab() === id
69
69
  ? base + 'border-violet-400 text-violet-600 dark:text-violet-400'
70
70
  : base + 'border-transparent text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300'
71
71
  }
72
72
 
73
- function selectTab(id) {
73
+ function selectTab(id: string) {
74
74
  activeTab.set(id)
75
75
  selectedUser.set(null)
76
76
  selectedRole.set(null)
@@ -315,7 +315,7 @@
315
315
 
316
316
  // ─── Tiny formatters ──────────────────────────────────────────
317
317
 
318
- function formatDate(iso) {
318
+ function formatDate(iso: string | null | undefined) {
319
319
  if (!iso) return ''
320
320
  const d = new Date(iso)
321
321
  if (Number.isNaN(d.getTime())) return ''
@@ -9,7 +9,10 @@ const liveAbortText = useReactiveProp('abortText', 'Cancel')
9
9
  const liveShow = useReactiveProp('show', true)
10
10
  const emit = defineEmits()
11
11
 
12
- const normalizedType = derived(() => liveType() === 'danger' ? 'error' : liveType())
12
+ const normalizedType = derived((): Exclude<AlertType, 'danger'> => {
13
+ const type = liveType()
14
+ return type === 'danger' ? 'error' : type
15
+ })
13
16
  const iconClass = derived(() => ({
14
17
  info: 'i-hugeicons-information-circle text-blue-600',
15
18
  warning: 'i-hugeicons-alert-02 text-yellow-600',
@@ -9,7 +9,7 @@ interface NavbarProps {
9
9
 
10
10
  const { isSidebarCollapsed = false, currentPath = '/' } = defineProps<NavbarProps>()
11
11
 
12
- function isActiveRoute(path) {
12
+ function isActiveRoute(path: string) {
13
13
  return currentPath === path || currentPath.startsWith(path + '/')
14
14
  }
15
15
 
@@ -12,9 +12,14 @@ export const navSections = buildWebSidebarSections()
12
12
  label: section.label,
13
13
  count: section.items.length,
14
14
  }))
15
+
16
+ // Declared so the client block below gets `navSections` typed rather than
17
+ // inferred off the implicit bridge.
18
+ defineClientPayload({ navSections })
15
19
  </script>
16
20
 
17
21
  <script client>
22
+ import type { ColorMode, SidebarStyle } from '~/storage/framework/defaults/views/dashboard/composables/useAppearance'
18
23
  import {
19
24
  COLOR_MODE_OPTIONS,
20
25
  SIDEBAR_STYLE_OPTIONS,
@@ -37,20 +42,20 @@ const hiddenSections = appearance.hiddenSections
37
42
 
38
43
  const hiddenCount = derived(() => hiddenSections().length)
39
44
 
40
- function selectStyle(id) {
45
+ function selectStyle(id: SidebarStyle) {
41
46
  appearance.setSidebarStyle(id)
42
47
  }
43
48
 
44
- function selectColorMode(id) {
49
+ function selectColorMode(id: ColorMode) {
45
50
  appearance.setColorMode(id)
46
51
  }
47
52
 
48
- function isVisible(id) {
53
+ function isVisible(id: string) {
49
54
  return !hiddenSections().includes(id)
50
55
  }
51
56
 
52
- function toggleSection(id, event) {
53
- appearance.toggleSection(id, event.target.checked)
57
+ function toggleSection(id: string, event: Event) {
58
+ appearance.toggleSection(id, (event.target as HTMLInputElement).checked)
54
59
  }
55
60
 
56
61
  function resetAll() {
@@ -64,7 +69,7 @@ function resetAll() {
64
69
  // upstream. It stays because it is the better shape regardless: one selector
65
70
  // per state in the block below, rather than two class strings kept in sync by
66
71
  // hand inside a ternary.
67
- const selected = (a, b) => String(a === b)
72
+ const selected = (a: unknown, b: unknown) => String(a === b)
68
73
  </script>
69
74
 
70
75
  <style>
@@ -247,8 +247,8 @@ void loadFiles()
247
247
  @elseif(selectedFile())
248
248
  <div class="flex flex-wrap gap-4 items-center justify-between px-6 py-5 border-b border-neutral-200/60 dark:border-white/5">
249
249
  <div>
250
- <h2 class="font-semibold text-lg text-neutral-900 dark:text-white">{{ selectedFile().title }}</h2>
251
- <p class="mt-1 font-mono text-neutral-500 text-xs dark:text-neutral-400">config/{{ selectedFile().name }}.ts</p>
250
+ <h2 class="font-semibold text-lg text-neutral-900 dark:text-white">{{ selectedFile()?.title }}</h2>
251
+ <p class="mt-1 font-mono text-neutral-500 text-xs dark:text-neutral-400">config/{{ selectedFile()?.name }}.ts</p>
252
252
  </div>
253
253
  <div class="flex gap-2 items-center">
254
254
  <Badge :if="isDirty()" variant="warning" size="sm">Unsaved changes</Badge>
@@ -5,21 +5,21 @@ interface indexProps {
5
5
 
6
6
  const { transactionHistory = [] } = defineProps<indexProps>()
7
7
 
8
- function formatTimestampDate(timestamp) {
8
+ function formatTimestampDate(timestamp: number) {
9
9
  if (!timestamp) return ''
10
10
  const date = new Date(timestamp * 1000)
11
11
  return date.toLocaleDateString()
12
12
  }
13
13
 
14
- function getTransactionType(lineData) {
15
- return lineData[0]?.plan?.interval || ''
14
+ function getTransactionType(lineData: Array<{ plan?: { interval?: string } }> | undefined) {
15
+ return lineData?.[0]?.plan?.interval || ''
16
16
  }
17
17
 
18
- function getUnitPrice(lineData) {
19
- return (Number(lineData[0]?.price?.unit_amount || 0) / 100).toFixed(2)
18
+ function getUnitPrice(lineData: Array<{ price?: { unit_amount?: number | string } }> | undefined) {
19
+ return (Number(lineData?.[0]?.price?.unit_amount || 0) / 100).toFixed(2)
20
20
  }
21
21
 
22
- function getLastDigits(paymentIntent) {
22
+ function getLastDigits(paymentIntent: { payment_method?: { card?: { last4?: string } } } | undefined) {
23
23
  return paymentIntent?.payment_method?.card?.last4 || ''
24
24
  }
25
25
  </script>
@@ -43,7 +43,7 @@ const statusSizes = {
43
43
  }
44
44
 
45
45
  // Generate initials from name
46
- function getInitials(fullName) {
46
+ function getInitials(fullName: string) {
47
47
  if (!fullName) return ''
48
48
  const parts = fullName.trim().split(/\s+/)
49
49
  if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase()
@@ -66,7 +66,7 @@ const bgColors = [
66
66
  'bg-pink-500/15 text-pink-600 dark:bg-pink-400/20 dark:text-pink-400'
67
67
  ]
68
68
 
69
- function getBgColorIndex(fullName) {
69
+ function getBgColorIndex(fullName: string) {
70
70
  if (!fullName) return 0
71
71
  let hash = 0
72
72
  for (let i = 0; i < fullName.length; i++) {
@@ -47,7 +47,7 @@ const classes = getClasses()
47
47
  >
48
48
  <!-- Leading icon slot -->
49
49
  <span class="flex items-center justify-center h-4 w-4 text-current opacity-70">
50
- {{ iconSlot }}
50
+ <slot name="icon" />
51
51
  </span>
52
52
 
53
53
  <!-- Content -->
@@ -56,7 +56,7 @@ const classes = getClasses()
56
56
  </span>
57
57
 
58
58
  <!-- Trailing content slot (for shortcuts, badges, etc.) -->
59
- {{ trailingSlot }}
59
+ <slot name="trailing" />
60
60
  </a>
61
61
  @else
62
62
  <button
@@ -66,7 +66,7 @@ const classes = getClasses()
66
66
  >
67
67
  <!-- Leading icon slot -->
68
68
  <span class="flex items-center justify-center h-4 w-4 text-current opacity-70">
69
- {{ iconSlot }}
69
+ <slot name="icon" />
70
70
  </span>
71
71
 
72
72
  <!-- Content -->
@@ -75,6 +75,6 @@ const classes = getClasses()
75
75
  </span>
76
76
 
77
77
  <!-- Trailing content slot (for shortcuts, badges, etc.) -->
78
- {{ trailingSlot }}
78
+ <slot name="trailing" />
79
79
  </button>
80
80
  @endif
@@ -1,3 +1,18 @@
1
+ <!--
2
+ Replica window controls, for chrome the page draws itself.
3
+
4
+ They yield to the real thing. Wherever the platform already drew its own
5
+ close/minimise/zoom buttons - which on macOS is every Craft window that is not
6
+ frameless - rendering these as well puts six circles in the corner, three of
7
+ them the wrong shade, without hover glyphs, and dead to the keyboard. Craft
8
+ says so by setting `--craft-window-controls-replicas: none` before the
9
+ document is parsed, and both variants below read it as a fallback, so a
10
+ browser (where nothing is published) still shows them and neither case needs
11
+ JavaScript or can flash.
12
+
13
+ Use them for a frameless window, where the page really does own its chrome,
14
+ and for a web dashboard that wants the look of one.
15
+ -->
1
16
  <script client>
2
17
  import { useWindowControls } from '../../../../functions/craft'
3
18
 
@@ -41,7 +56,7 @@ async function handleMaximize(): Promise<void> {
41
56
  }
42
57
  </script>
43
58
 
44
- <div :if="liveVariant() === 'macos'" class="flex items-center" :class="config().gap" data-window-controls>
59
+ <div :if="liveVariant() === 'macos'" class="items-center" style="display: var(--craft-window-controls-replicas, flex)" :class="config().gap" data-window-controls>
45
60
  <span class="flex gap-1.5 items-center">
46
61
  <button
47
62
  type="button"
@@ -85,7 +100,7 @@ async function handleMaximize(): Promise<void> {
85
100
  </span>
86
101
  </div>
87
102
 
88
- <div :else class="flex items-center">
103
+ <div :else class="items-center" style="display: var(--craft-window-controls-replicas, flex)" data-window-controls>
89
104
  <button
90
105
  type="button"
91
106
  class="flex items-center justify-center h-8 w-12 text-neutral-600 dark:text-neutral-400 hover:bg-neutral-200 dark:hover:bg-neutral-700 transition-colors"
@@ -1,7 +1,12 @@
1
1
  <script>
2
+ interface ComboboxItem {
3
+ id: number
4
+ name: string
5
+ }
6
+
2
7
  interface ComboboxDemoProps {
3
- listItems?: Array<{ id: number, name: string }>
4
- selected?: any
8
+ listItems?: ComboboxItem[]
9
+ selected?: ComboboxItem | null
5
10
  query?: string
6
11
  isOpen?: boolean
7
12
  }
@@ -13,7 +18,7 @@ const { listItems = [
13
18
  { id: 4, name: 'Michael Vincent' },
14
19
  { id: 5, name: 'Blake Ayer' },
15
20
  ], selected = null, query = '', isOpen = false } = defineProps<ComboboxDemoProps>()
16
- function filterItems(items, query) {
21
+ function filterItems(items: ComboboxItem[], query: string): ComboboxItem[] {
17
22
  if (query === '') return items
18
23
  return items.filter(item => item.name.toLowerCase().includes(query.toLowerCase()))
19
24
  }
@@ -21,9 +21,9 @@ const { menuItems = [
21
21
  { divider: true },
22
22
  { icon: 'i-hugeicons-trash-20-solid', label: 'Delete', variant: 'danger' },
23
23
  ], isOpen = false } = defineProps<DropdownDemoProps>()
24
- function getMenuSections(items) {
25
- const sections = []
26
- let currentSection = []
24
+ function getMenuSections(items: DropdownMenuItem[]): DropdownMenuItem[][] {
25
+ const sections: DropdownMenuItem[][] = []
26
+ let currentSection: DropdownMenuItem[] = []
27
27
 
28
28
  for (const item of items) {
29
29
  if (item.divider) {
@@ -10,7 +10,7 @@ const { currentStep = 0, steps = [
10
10
  { title: 'Step 3', description: 'Third step', icon: 'i-heroicons-check' },
11
11
  ] } = defineProps<StepperDemoProps>()
12
12
 
13
- function getStepContent(step) {
13
+ function getStepContent(step: number): { title: string, description: string } {
14
14
  switch (step) {
15
15
  case 0:
16
16
  return { title: 'Personal Information', description: 'Please provide your basic details' }
@@ -5,7 +5,7 @@ interface ForumLayoutProps {
5
5
 
6
6
  const { route = 'forum' } = defineProps<ForumLayoutProps>()
7
7
 
8
- function isActiveRoute(currentRoute, targetRoute) {
8
+ function isActiveRoute(currentRoute: string, targetRoute: string): boolean {
9
9
  return currentRoute === targetRoute
10
10
  }
11
11
  </script>
@@ -6,7 +6,7 @@ interface ForumReplyFormProps {
6
6
 
7
7
  const { replyContent = '', notifyOnReply = true } = defineProps<ForumReplyFormProps>()
8
8
 
9
- function canSubmit(content) {
9
+ function canSubmit(content: string) {
10
10
  return content.trim().length > 0
11
11
  }
12
12
  </script>
@@ -84,15 +84,15 @@ defineProps<ComingSoonProps>()
84
84
  // Client-side wiring for the holding-page email form. The endpoint
85
85
  // itself (/api/email/subscribe) is rate-limited server-side; this
86
86
  // handler just gives the visitor immediate feedback.
87
- const form = document.getElementById('coming-soon-form')
88
- const emailInput = document.getElementById('coming-soon-email')
89
- const submitBtn = document.getElementById('coming-soon-submit')
87
+ const form = document.getElementById('coming-soon-form') as HTMLFormElement | null
88
+ const emailInput = document.getElementById('coming-soon-email') as HTMLInputElement | null
89
+ const submitBtn = document.getElementById('coming-soon-submit') as HTMLButtonElement | null
90
90
  const submitLabel = document.getElementById('coming-soon-submit-label')
91
91
  const spinner = document.getElementById('coming-soon-spinner')
92
92
  const errorEl = document.getElementById('coming-soon-error')
93
93
  const successEl = document.getElementById('coming-soon-success')
94
94
 
95
- function setLoading(loading) {
95
+ function setLoading(loading: boolean) {
96
96
  if (!submitBtn || !submitLabel || !spinner) return
97
97
  submitBtn.disabled = loading
98
98
  submitBtn.classList.toggle('opacity-50', loading)
@@ -100,7 +100,7 @@ function setLoading(loading) {
100
100
  spinner.classList.toggle('hidden', !loading)
101
101
  }
102
102
 
103
- function showError(message) {
103
+ function showError(message: string) {
104
104
  if (!errorEl) return
105
105
  errorEl.textContent = message
106
106
  errorEl.classList.remove('hidden')