@stacksjs/defaults 0.75.127 → 0.75.129

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 (38) hide show
  1. package/ai/skills/stacks-mobile/SKILL.md +40 -0
  2. package/ide/vscode/package.json +1 -1
  3. package/package.json +2 -2
  4. package/resources/components/CookieConsent.stx +13 -13
  5. package/resources/components/Dashboard/Ci/CiDashboard.stx +1 -1
  6. package/resources/components/Dashboard/Ci/CiRunHistoryDrawer.stx +6 -6
  7. package/resources/components/Dashboard/Content/BlogDashboard.stx +3 -3
  8. package/resources/components/Dashboard/Content/BlogPostEditorDialog.stx +2 -2
  9. package/resources/components/Dashboard/Content/ContentTaxonomyDashboard.stx +2 -2
  10. package/resources/components/Dashboard/Content/CreateFolderDialog.stx +2 -2
  11. package/resources/components/Dashboard/Content/FileUploadDialog.stx +1 -1
  12. package/resources/components/Dashboard/Deployments/DeploymentList.stx +1 -1
  13. package/resources/components/Dashboard/Deployments/DeploymentPreviewDialog.stx +5 -5
  14. package/resources/components/Dashboard/Email/CapturedMailDashboard.stx +2 -2
  15. package/resources/components/Dashboard/Feedback/FeedbackForm.stx +10 -10
  16. package/resources/components/Dashboard/Kanban/KanbanCardDialog.stx +20 -20
  17. package/resources/components/Dashboard/Management/AssignmentDrawer.stx +6 -6
  18. package/resources/components/Dashboard/Marketing/AbandonedCartsDashboard.stx +1 -1
  19. package/resources/components/Dashboard/Marketing/AbandonedCartsTable.stx +1 -1
  20. package/resources/components/Dashboard/Operations/AuditDashboard.stx +1 -1
  21. package/resources/components/Dashboard/Operations/ChangeDashboard.stx +2 -2
  22. package/resources/components/Dashboard/Operations/IncidentDashboard.stx +1 -1
  23. package/resources/components/Dashboard/Operations/MigrationDashboard.stx +7 -7
  24. package/resources/components/Dashboard/Operations/OperationHistory.stx +2 -2
  25. package/resources/components/Dashboard/Operations/RecoveryDashboard.stx +3 -3
  26. package/resources/components/Dashboard/Operations/RecoveryDestinationDialog.stx +3 -3
  27. package/resources/components/Dashboard/Operations/RecoveryJobList.stx +3 -3
  28. package/resources/components/Dashboard/Operations/RecoveryRestoreDialog.stx +1 -1
  29. package/resources/components/Dashboard/Operations/SchedulerDashboard.stx +1 -1
  30. package/resources/components/NativeNavBar.stx +1 -1
  31. package/resources/views/auth/oauth/consent.stx +22 -22
  32. package/resources/views/cms/blocks/columns.stx +1 -1
  33. package/resources/views/cms/blocks/cta.stx +4 -4
  34. package/resources/views/cms/blocks/embed.stx +1 -1
  35. package/resources/views/cms/blocks/form.stx +14 -14
  36. package/resources/views/cms/blocks/hero.stx +3 -3
  37. package/resources/views/cms/blocks/image.stx +1 -1
  38. package/resources/views/cms/blocks/rich-text.stx +1 -1
@@ -61,8 +61,8 @@ function save(): void {
61
61
  @close="close"
62
62
  >
63
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">
64
+ <label for="assignment-guard" class="block mb-1.5 font-medium text-xs text-zinc-600 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-sm text-zinc-900 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
66
  <template :for="option in guardOptions()">
67
67
  <option :value="option.value">{{ option.label }}</option>
68
68
  </template>
@@ -70,16 +70,16 @@ function save(): void {
70
70
  </div>
71
71
 
72
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">
73
+ <div :if="loading()" class="flex gap-2 items-center py-6 text-sm text-zinc-500">
74
74
  <span class="h-4 w-4 animate-spin i-hugeicons-loading-03"></span>
75
75
  Loading assignments...
76
76
  </div>
77
- <div :if="!loading() && options().length === 0" class="text-zinc-500 text-sm">{{ emptyMessage() }}</div>
77
+ <div :if="!loading() && options().length === 0" class="text-sm text-zinc-500">{{ emptyMessage() }}</div>
78
78
  <template :if="!loading()" :for="option in options()">
79
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
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>
81
+ <span class="text-sm text-zinc-900 dark:text-zinc-100">{{ option.name }}</span>
82
+ <span class="text-xs text-zinc-500">{{ option.description || '' }}</span>
83
83
  </label>
84
84
  </template>
85
85
  </div>
@@ -204,7 +204,7 @@ onMount(() => {
204
204
  <p class="mt-1 text-gray-500 text-xs dark:text-neutral-400">Ordinary campaigns aimed at cold carts. They send, report and are edited on the campaigns screen like any other.</p>
205
205
  <ul class="mt-3 divide-gray-200 divide-y dark:divide-neutral-700">
206
206
  <template :for="campaign in campaigns">
207
- <li class="flex flex-col gap-1 py-3 sm:flex-row sm:items-center sm:justify-between">
207
+ <li class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between py-3">
208
208
  <div>
209
209
  <p class="font-medium text-gray-900 text-sm dark:text-white">{{ campaign.name }}</p>
210
210
  <p class="mt-1 text-gray-500 text-xs dark:text-neutral-400">Carts idle {{ campaign.idleHours }}h or more<span :if="campaign.minimumValue > 0"> · {{ money(campaign.minimumValue) }} and up</span> · {{ campaignTiming(campaign) }}</p>
@@ -80,7 +80,7 @@ function stateClass(record: AbandonedCartRecord): string {
80
80
  <p class="text-gray-900 text-sm dark:text-white">{{ contents(record) }}</p>
81
81
  <p class="mt-1 text-gray-500 text-xs dark:text-neutral-400">Cart #{{ record.id }}</p>
82
82
  </td>
83
- <td class="px-4 py-4"><p class="font-medium text-gray-900 text-sm tabular-nums dark:text-white">{{ money(record.value, record.currency) }}</p></td>
83
+ <td class="px-4 py-4"><p class="font-medium tabular-nums text-gray-900 text-sm dark:text-white">{{ money(record.value, record.currency) }}</p></td>
84
84
  <td class="px-4 py-4"><p class="text-gray-900 text-sm dark:text-white">{{ idle(record.idleHours) }}</p></td>
85
85
  <td class="px-4 py-4"><span :class="'inline-flex px-2 py-1 font-medium text-xs rounded-full ' + stateClass(record)">{{ stateLabel(record) }}</span></td>
86
86
  </tr>
@@ -12,5 +12,5 @@ onMount(() => void load())
12
12
  <PageHeader title="Operations" description="Inspect the append-only operator event stream with actor and operation correlation."><template #actions><Button variant="secondary" :loading="loading()" @click="load"><span :if="!loading()" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button></template></PageHeader>
13
13
  <OperationsNavigation active="audit" />
14
14
  <section class="grid gap-3 sm:grid-cols-2 lg:grid-cols-4"><StatsCard title="Events" :value="String(data()?.summary.events || 0)" icon="i-hugeicons-activity-03" iconBg="primary" :loading="loading()" /><StatsCard title="Operators" :value="String(data()?.summary.operators || 0)" icon="i-hugeicons-user-group" iconBg="neutral" :loading="loading()" /><StatsCard title="Failures" :value="String(data()?.summary.failures || 0)" icon="i-hugeicons-alert-02" iconBg="warning" :loading="loading()" /><StatsCard title="Latest sequence" :value="String(data()?.summary.latestSequence || 0)" icon="i-hugeicons-list-view" iconBg="success" :loading="loading()" /></section>
15
- <section class="overflow-hidden bg-white dark:bg-neutral-900 border border-gray-200 rounded-lg dark:border-neutral-800"><div class="px-4 py-4 border-b border-gray-200 dark:border-neutral-800"><h2 class="font-semibold text-gray-900 dark:text-white">Operator events</h2><p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Events are sequence ordered and retained in the ts-cloud control plane.</p></div><div :if="!loading() && !data()?.events.length" class="px-6 py-12 text-center text-gray-500 text-sm dark:text-neutral-400">No operator events have been recorded.</div><div class="max-h-[42rem] overflow-auto divide-y divide-gray-100 dark:divide-neutral-800"><div :for="event in data()?.events || []" class="grid gap-2 px-4 py-3 md:grid-cols-[5rem_1fr_10rem_12rem]"><span class="font-mono text-gray-400 text-xs">#{{ event.sequence }}</span><div><p class="font-medium text-gray-900 text-sm dark:text-white">{{ event.type }}</p><p class="mt-1 font-mono text-gray-500 text-xs dark:text-neutral-400">{{ event.operationId || event.id }}</p></div><span class="text-gray-600 text-sm dark:text-neutral-300">{{ event.actorName }}</span><time class="text-gray-500 text-xs dark:text-neutral-400">{{ date(event.createdAt) }}</time></div></div></section>
15
+ <section class="overflow-hidden bg-white dark:bg-neutral-900 border border-gray-200 rounded-lg dark:border-neutral-800"><div class="px-4 py-4 border-b border-gray-200 dark:border-neutral-800"><h2 class="font-semibold text-gray-900 dark:text-white">Operator events</h2><p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Events are sequence ordered and retained in the ts-cloud control plane.</p></div><div :if="!loading() && !data()?.events.length" class="px-6 py-12 text-center text-gray-500 text-sm dark:text-neutral-400">No operator events have been recorded.</div><div class="overflow-auto max-h-[42rem] divide-gray-100 divide-y dark:divide-neutral-800"><div :for="event in data()?.events || []" class="grid gap-2 md:grid-cols-[5rem_1fr_10rem_12rem] px-4 py-3"><span class="font-mono text-gray-400 text-xs">#{{ event.sequence }}</span><div><p class="font-medium text-gray-900 text-sm dark:text-white">{{ event.type }}</p><p class="mt-1 font-mono text-gray-500 text-xs dark:text-neutral-400">{{ event.operationId || event.id }}</p></div><span class="text-gray-600 text-sm dark:text-neutral-300">{{ event.actorName }}</span><time class="text-gray-500 text-xs dark:text-neutral-400">{{ date(event.createdAt) }}</time></div></div></section>
16
16
  </div>
@@ -46,7 +46,7 @@ onMount(() => void load())
46
46
  <section class="grid gap-6 lg:grid-cols-2">
47
47
  <div class="overflow-hidden bg-white dark:bg-neutral-900 border border-gray-200 rounded-lg dark:border-neutral-800">
48
48
  <div class="px-4 py-4 border-b border-gray-200 dark:border-neutral-800"><h2 class="font-semibold text-gray-900 dark:text-white">Change plan</h2><p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Current model and runtime changes for {{ data()?.environment?.name || 'this environment' }}.</p></div>
49
- <div class="divide-y divide-gray-100 dark:divide-neutral-800">
49
+ <div class="divide-gray-100 divide-y dark:divide-neutral-800">
50
50
  <div class="flex items-center justify-between px-4 py-3"><span class="text-gray-700 text-sm dark:text-neutral-300">Model-derived schema operations</span><Badge :variant="data()?.summary.destructiveChanges ? 'warning' : 'info'">{{ data()?.summary.migrationChanges || 0 }}</Badge></div>
51
51
  <div class="flex items-center justify-between px-4 py-3"><span class="text-gray-700 text-sm dark:text-neutral-300">Migration ledger drift</span><Badge :variant="data()?.migrations.summary.drift ? 'warning' : 'success'">{{ data()?.migrations.summary.drift ? 'Review' : 'Clean' }}</Badge></div>
52
52
  <div class="flex items-center justify-between px-4 py-3"><span class="text-gray-700 text-sm dark:text-neutral-300">Running or queued control-plane work</span><Badge variant="default">{{ data()?.activeOperations.length || 0 }}</Badge></div>
@@ -55,7 +55,7 @@ onMount(() => void load())
55
55
  <div class="overflow-hidden bg-white dark:bg-neutral-900 border border-gray-200 rounded-lg dark:border-neutral-800">
56
56
  <div class="px-4 py-4 border-b border-gray-200 dark:border-neutral-800"><h2 class="font-semibold text-gray-900 dark:text-white">Release approvals</h2><p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Only native releases explicitly marked as approval-required appear here.</p></div>
57
57
  <div :if="!loading() && !data()?.pendingReleases.length" class="px-6 py-10 text-center text-gray-500 text-sm dark:text-neutral-400">No releases are awaiting approval.</div>
58
- <button :for="release in data()?.pendingReleases || []" type="button" class="flex w-full items-center justify-between gap-3 px-4 py-3 text-left hover:bg-gray-50 dark:hover:bg-neutral-800" @click="selected.set(release)"><div><p class="font-medium text-gray-900 text-sm dark:text-white">{{ release.sourceSha || release.id }}</p><p class="mt-1 text-gray-500 text-xs dark:text-neutral-400">{{ release.kind }} · {{ release.strategy }}</p></div><Badge variant="warning">Review</Badge></button>
58
+ <button :for="release in data()?.pendingReleases || []" type="button" class="flex gap-3 items-center justify-between px-4 py-3 w-full text-left hover:bg-gray-50 dark:hover:bg-neutral-800" @click="selected.set(release)"><div><p class="font-medium text-gray-900 text-sm dark:text-white">{{ release.sourceSha || release.id }}</p><p class="mt-1 text-gray-500 text-xs dark:text-neutral-400">{{ release.kind }} · {{ release.strategy }}</p></div><Badge variant="warning">Review</Badge></button>
59
59
  </div>
60
60
  </section>
61
61
  <OperationHistory :operations="data()?.recentOperations || []" :loading="loading()" />
@@ -32,7 +32,7 @@ onMount(() => void load())
32
32
  <section class="overflow-hidden bg-white dark:bg-neutral-900 border border-gray-200 rounded-lg dark:border-neutral-800">
33
33
  <div class="px-4 py-4 border-b border-gray-200 dark:border-neutral-800"><h2 class="font-semibold text-gray-900 dark:text-white">Incidents</h2><p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Acknowledgement does not resolve an alert. Resolution follows recovered signal evidence.</p></div>
34
34
  <div :if="!loading() && !data()?.alerts.length" class="px-6 py-12 text-center"><span class="mx-auto h-8 w-8 text-green-500 i-hugeicons-checkmark-circle-02"></span><p class="mt-3 font-medium text-gray-900 dark:text-white">No incidents</p><p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">No native alert is pending, firing, resolved, or silenced in this environment.</p></div>
35
- <button :for="alert in data()?.alerts || []" type="button" class="flex w-full items-start justify-between gap-4 px-4 py-4 text-left border-b border-gray-100 last:border-0 dark:border-neutral-800 hover:bg-gray-50 dark:hover:bg-neutral-800" @click="selected.set(alert)"><div><div class="flex gap-2 items-center"><p class="font-medium text-gray-900 text-sm dark:text-white">{{ alert.title }}</p><Badge :variant="severityVariant(alert.severity)">{{ alert.severity }}</Badge></div><p class="mt-1 text-gray-500 text-xs dark:text-neutral-400">{{ alert.state }} · {{ alert.occurrenceCount }} occurrences · {{ alert.ownerName || 'Unassigned' }}</p></div><span class="h-4 w-4 text-gray-400 i-hugeicons-arrow-right-01"></span></button>
35
+ <button :for="alert in data()?.alerts || []" type="button" class="flex gap-4 items-start justify-between px-4 py-4 w-full text-left hover:bg-gray-50 dark:hover:bg-neutral-800 border-b border-gray-100 dark:border-neutral-800 last:border-0" @click="selected.set(alert)"><div><div class="flex gap-2 items-center"><p class="font-medium text-gray-900 text-sm dark:text-white">{{ alert.title }}</p><Badge :variant="severityVariant(alert.severity)">{{ alert.severity }}</Badge></div><p class="mt-1 text-gray-500 text-xs dark:text-neutral-400">{{ alert.state }} · {{ alert.occurrenceCount }} occurrences · {{ alert.ownerName || 'Unassigned' }}</p></div><span class="h-4 w-4 text-gray-400 i-hugeicons-arrow-right-01"></span></button>
36
36
  </section>
37
37
  <OperationHistory :operations="data()?.operations || []" :loading="loading()" />
38
38
  <Modal :isOpen="Boolean(selected())" title="Incident actions" :description="selected()?.title || ''" size="sm" @close="selected.set(null)"><div class="grid gap-2"><Button variant="secondary" :loading="busy() === 'acknowledge'" @click="mutate('acknowledge')">Acknowledge</Button><Button variant="secondary" :loading="busy() === 'assign_self'" @click="mutate('assign_self')">Assign to me</Button><Button variant="secondary" :loading="busy() === 'silence'" @click="mutate('silence')">Silence for one hour</Button></div></Modal>
@@ -89,7 +89,7 @@ onMount(() => void load())
89
89
  </section>
90
90
 
91
91
  <section class="overflow-hidden bg-white dark:bg-neutral-900 border border-gray-200 rounded-lg dark:border-neutral-800">
92
- <div class="flex flex-col gap-3 px-4 py-4 border-b border-gray-200 dark:border-neutral-800 sm:flex-row sm:items-center sm:justify-between">
92
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between px-4 py-4 border-b border-gray-200 dark:border-neutral-800">
93
93
  <div>
94
94
  <h2 class="font-semibold text-gray-900 dark:text-white">Schema plan</h2>
95
95
  <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">{{ data()?.dialect || 'database' }} in {{ data()?.environment || 'current environment' }}. The plan revision is rechecked at execution.</p>
@@ -101,8 +101,8 @@ onMount(() => void load())
101
101
  <p class="mt-3 font-medium text-gray-900 dark:text-white">Schema is current</p>
102
102
  <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Models and the connected database have no pending structural changes.</p>
103
103
  </div>
104
- <div :if="data()?.operations.length" class="divide-y divide-gray-100 dark:divide-neutral-800">
105
- <div :for="operation in data()?.operations || []" class="flex items-start justify-between gap-4 px-4 py-3">
104
+ <div :if="data()?.operations.length" class="divide-gray-100 divide-y dark:divide-neutral-800">
105
+ <div :for="operation in data()?.operations || []" class="flex gap-4 items-start justify-between px-4 py-3">
106
106
  <div>
107
107
  <p class="font-medium text-gray-900 text-sm dark:text-white">{{ operation.kind.replaceAll('_', ' ') }}</p>
108
108
  <p class="mt-0.5 font-mono text-gray-500 text-xs dark:text-neutral-400">{{ operation.table }}{{ operation.column ? '.' + operation.column : '' }}</p>
@@ -113,16 +113,16 @@ onMount(() => void load())
113
113
  </section>
114
114
 
115
115
  <section class="overflow-hidden bg-white dark:bg-neutral-900 border border-gray-200 rounded-lg dark:border-neutral-800">
116
- <div class="flex items-center justify-between gap-3 px-4 py-4 border-b border-gray-200 dark:border-neutral-800">
116
+ <div class="flex gap-3 items-center justify-between px-4 py-4 border-b border-gray-200 dark:border-neutral-800">
117
117
  <div>
118
118
  <h2 class="font-semibold text-gray-900 dark:text-white">Migration ledger</h2>
119
119
  <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Compared with both migration files and live schema effects.</p>
120
120
  </div>
121
121
  <Button variant="secondary" :disabled="!data()?.summary.drift" @click="openDialog('reconcile')">Reconcile provable drift</Button>
122
122
  </div>
123
- <div class="max-h-96 overflow-auto divide-y divide-gray-100 dark:divide-neutral-800">
124
- <div :for="entry in data()?.ledger.entries || []" class="flex items-center justify-between gap-4 px-4 py-3">
125
- <span class="min-w-0 truncate font-mono text-gray-700 text-xs dark:text-neutral-300">{{ entry.file }}</span>
123
+ <div class="overflow-auto max-h-96 divide-gray-100 divide-y dark:divide-neutral-800">
124
+ <div :for="entry in data()?.ledger.entries || []" class="flex gap-4 items-center justify-between px-4 py-3">
125
+ <span class="min-w-0 font-mono text-gray-700 text-xs truncate dark:text-neutral-300">{{ entry.file }}</span>
126
126
  <span :class="'shrink-0 px-2 py-1 font-medium text-xs rounded-md ' + statusClass(entry.status)">{{ entry.status }}</span>
127
127
  </div>
128
128
  </div>
@@ -46,7 +46,7 @@ function statusVariant(state: OperatorOperation['state']): 'default' | 'primary'
46
46
  <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">{{ emptyDescription() }}</p>
47
47
  </div>
48
48
  <ul :if="rows().length > 0" class="divide-gray-200 divide-y dark:divide-neutral-700">
49
- <li :for="operation in rows()" class="flex flex-col gap-3 px-5 py-4 sm:flex-row sm:items-center sm:justify-between">
49
+ <li :for="operation in rows()" class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between px-5 py-4">
50
50
  <div class="min-w-0">
51
51
  <div class="flex flex-wrap gap-2 items-center">
52
52
  <p class="font-medium text-gray-900 text-sm dark:text-white">{{ actionLabel(operation) }}</p>
@@ -55,7 +55,7 @@ function statusVariant(state: OperatorOperation['state']): 'default' | 'primary'
55
55
  <p class="mt-1 text-gray-500 text-xs dark:text-neutral-400">{{ operation.actorName }} at {{ formatDate(operation.finishedAt || operation.createdAt) }}</p>
56
56
  <p :if="operation.error" class="mt-1 text-red-600 text-xs dark:text-red-400">{{ operation.error }}</p>
57
57
  </div>
58
- <code class="font-mono text-gray-400 text-[11px] dark:text-neutral-500">{{ operation.id }}</code>
58
+ <code class="font-mono text-[11px] text-gray-400 dark:text-neutral-500">{{ operation.id }}</code>
59
59
  </li>
60
60
  </ul>
61
61
  </Card>
@@ -178,9 +178,9 @@ onMount(() => { void loadRecovery() })
178
178
  </PageHeader>
179
179
  <OperationsNavigation active="recovery" />
180
180
 
181
- <div :if="loadError()" role="alert" class="flex flex-col gap-3 px-4 py-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 sm:flex-row sm:items-center sm:justify-between"><span>{{ loadError() }}</span><Button size="sm" variant="secondary" @click="loadRecovery">Try again</Button></div>
181
+ <div :if="loadError()" role="alert" class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between px-4 py-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"><span>{{ loadError() }}</span><Button size="sm" variant="secondary" @click="loadRecovery">Try again</Button></div>
182
182
 
183
- <div :if="!isLoading() && (data()?.destinations.length || 0) === 0" class="flex flex-col gap-4 p-5 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 sm:flex-row sm:items-center sm:justify-between">
183
+ <div :if="!isLoading() && (data()?.destinations.length || 0) === 0" class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between p-5 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">
184
184
  <div><p class="font-semibold">No recovery destination is connected</p><p class="mt-1 text-sm">Nothing is protected yet. Connect object storage or AWS Backup, then run its health test before creating policies.</p></div>
185
185
  <Button @click="showDestination.set(true)">Connect destination</Button>
186
186
  </div>
@@ -194,7 +194,7 @@ onMount(() => { void loadRecovery() })
194
194
  </section>
195
195
 
196
196
  <section>
197
- <div class="flex flex-col gap-3 mb-3 sm:flex-row sm:items-end sm:justify-between">
197
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between mb-3">
198
198
  <div><h2 class="font-semibold text-gray-900 dark:text-white">Destinations and policies</h2><p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Coverage is healthy only when a destination is healthy and the latest point meets its RPO.</p></div>
199
199
  <Button size="sm" variant="outline" :loading="busyId() === 'retention'" :disabled="busyId() !== ''" @click="retention">Run retention</Button>
200
200
  </div>
@@ -84,12 +84,12 @@ function submit(): void {
84
84
  </div>
85
85
 
86
86
  <details :if="provider() !== 'aws_backup'" class="p-4 bg-gray-50 dark:bg-neutral-900/60 border border-gray-200 rounded-lg dark:border-neutral-700">
87
- <summary class="font-medium text-gray-900 text-sm cursor-pointer dark:text-white">Static credentials, optional</summary>
87
+ <summary class="font-medium text-gray-900 text-sm dark:text-white cursor-pointer">Static credentials, optional</summary>
88
88
  <p class="mt-1 text-gray-500 text-xs dark:text-neutral-400">Leave blank to use the runtime's AWS credential chain. Submitted credentials are encrypted in the control plane.</p>
89
- <div class="grid gap-4 mt-4 sm:grid-cols-2">
89
+ <div class="grid gap-4 sm:grid-cols-2 mt-4">
90
90
  <input x-model.trim="accessKeyId" autocomplete="off" placeholder="Access key id" aria-label="Access key id" class="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-lg dark:border-neutral-700" />
91
91
  <input x-model="secretAccessKey" type="password" autocomplete="new-password" placeholder="Secret access key" aria-label="Secret access key" class="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-lg dark:border-neutral-700" />
92
- <input x-model="sessionToken" type="password" autocomplete="new-password" placeholder="Session token, optional" aria-label="Session token" class="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-lg dark:border-neutral-700 sm:col-span-2" />
92
+ <input x-model="sessionToken" type="password" autocomplete="new-password" placeholder="Session token, optional" aria-label="Session token" class="sm:col-span-2 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-lg dark:border-neutral-700" />
93
93
  </div>
94
94
  </details>
95
95
 
@@ -20,13 +20,13 @@ function variant(status: RecoveryJob['status']): 'default' | 'primary' | 'succes
20
20
  <Card title="Recovery jobs" subtitle="Durable backup, verification, restore, drill, and retention work." padding="none">
21
21
  <div :if="jobs().length === 0" class="px-5 py-10 text-center text-gray-500 text-sm dark:text-neutral-400">No recovery jobs queued</div>
22
22
  <ul :if="jobs().length > 0" class="divide-gray-200 divide-y dark:divide-neutral-700">
23
- <li :for="job in jobs().slice(0, 12)" class="flex flex-col gap-3 px-5 py-4 sm:flex-row sm:items-center sm:justify-between">
23
+ <li :for="job in jobs().slice(0, 12)" class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between px-5 py-4">
24
24
  <div>
25
- <div class="flex gap-2 items-center"><p class="font-medium text-gray-900 text-sm capitalize dark:text-white">{{ job.kind }}</p><Badge :variant="variant(job.status)">{{ job.status }}</Badge></div>
25
+ <div class="flex gap-2 items-center"><p class="capitalize font-medium text-gray-900 text-sm dark:text-white">{{ job.kind }}</p><Badge :variant="variant(job.status)">{{ job.status }}</Badge></div>
26
26
  <p class="mt-1 text-gray-500 text-xs dark:text-neutral-400">{{ job.progress.phase || 'queued' }} · {{ formatDate(job.finishedAt || job.startedAt || job.createdAt) }}</p>
27
27
  <p :if="job.error" class="mt-1 text-red-600 text-xs dark:text-red-400">{{ job.error }}</p>
28
28
  </div>
29
- <code class="font-mono text-gray-400 text-[11px] dark:text-neutral-500">{{ job.id }}</code>
29
+ <code class="font-mono text-[11px] text-gray-400 dark:text-neutral-500">{{ job.id }}</code>
30
30
  </li>
31
31
  </ul>
32
32
  </Card>
@@ -59,7 +59,7 @@ function execute(): void { emit('execute', input(true)) }
59
59
  </div>
60
60
  </div>
61
61
  <label :if="mode() === 'isolated'" class="inline-flex gap-2 items-center text-gray-700 text-sm dark:text-neutral-200"><input x-model="drill" type="checkbox" /> Run as a recovery drill and clean the isolated target after health validation</label>
62
- <div :if="mode() === 'in_place'" class="space-y-4 p-4 text-amber-900 dark:text-amber-100 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 rounded-lg dark:border-amber-900">
62
+ <div :if="mode() === 'in_place'" class="p-4 space-y-4 text-amber-900 dark:text-amber-100 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 rounded-lg dark:border-amber-900">
63
63
  <p class="font-medium text-sm">In-place restore replaces live data and requires a distinct verified safety point.</p>
64
64
  <div>
65
65
  <label for="restore-safety" class="block mb-1.5 font-medium text-sm">Safety recovery point</label>
@@ -97,7 +97,7 @@ onMount(() => {
97
97
 
98
98
  <OperationsNavigation active="scheduler" />
99
99
 
100
- <div :if="loadError()" role="alert" class="flex flex-col gap-3 px-4 py-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 sm:flex-row sm:items-center sm:justify-between">
100
+ <div :if="loadError()" role="alert" class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between px-4 py-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">
101
101
  <span>{{ loadError() }}</span>
102
102
  <Button size="sm" variant="secondary" @click="loadOperations">Try again</Button>
103
103
  </div>
@@ -92,7 +92,7 @@ function tapBack(event: MouseEvent): void {
92
92
  <slot name="leading" />
93
93
  </div>
94
94
  <p class="native-nav-title" aria-hidden="{{ large ? 'true' : 'false' }}">{{ navTitle() }}</p>
95
- <div class="native-nav-side native-nav-actions">
95
+ <div class="native-nav-actions native-nav-side">
96
96
  <slot name="actions" />
97
97
  </div>
98
98
  </header>
@@ -19,28 +19,28 @@ declare const csrfToken: string
19
19
  const clientTypeLabel = consent.client.type === 'public' ? 'Public client' : 'Confidential client'
20
20
  </script>
21
21
 
22
- <main class="flex min-h-dvh items-center justify-center bg-neutral-50 px-4 py-10 text-neutral-950 dark:bg-neutral-950 dark:text-white sm:px-6">
23
- <section class="w-full max-w-xl" aria-labelledby="oauth-consent-title">
24
- <div class="mb-5 flex items-center justify-center gap-2 text-neutral-500 text-sm dark:text-neutral-400">
22
+ <main class="flex items-center justify-center px-4 py-10 sm:px-6 min-h-dvh text-neutral-950 dark:text-white bg-neutral-50 dark:bg-neutral-950">
23
+ <section class="max-w-xl w-full" aria-labelledby="oauth-consent-title">
24
+ <div class="flex gap-2 items-center justify-center mb-5 text-neutral-500 text-sm dark:text-neutral-400">
25
25
  <i class="h-5 w-5 i-hugeicons-shield-key" aria-hidden="true"></i>
26
26
  <span>Secure authorization</span>
27
27
  </div>
28
28
 
29
- <div class="overflow-hidden border border-neutral-200 rounded-2xl bg-white shadow-sm dark:border-neutral-800 dark:bg-neutral-900">
30
- <header class="border-neutral-200 border-b px-6 py-6 dark:border-neutral-800 sm:px-8">
29
+ <div class="overflow-hidden bg-white dark:bg-neutral-900 border border-neutral-200 rounded-2xl dark:border-neutral-800 shadow-sm">
30
+ <header class="px-6 py-6 sm:px-8 border-b border-neutral-200 dark:border-neutral-800">
31
31
  <p class="font-medium text-neutral-500 text-sm dark:text-neutral-400">Authorization request</p>
32
32
  <h1 id="oauth-consent-title" class="mt-2 font-semibold text-2xl tracking-tight sm:text-3xl">
33
33
  {{ consent.client.name }} wants to connect
34
34
  </h1>
35
- <p class="mt-3 text-neutral-600 text-sm leading-6 dark:text-neutral-300">
35
+ <p class="mt-3 leading-6 text-neutral-600 text-sm dark:text-neutral-300">
36
36
  Review exactly what this application can access before you continue.
37
37
  </p>
38
38
  </header>
39
39
 
40
- <div class="space-y-7 px-6 py-6 sm:px-8">
41
- <dl class="grid gap-4 rounded-xl bg-neutral-50 p-4 dark:bg-neutral-800/60">
40
+ <div class="px-6 py-6 space-y-7 sm:px-8">
41
+ <dl class="grid gap-4 p-4 bg-neutral-50 dark:bg-neutral-800/60 rounded-xl">
42
42
  <div class="flex gap-3 items-start">
43
- <i class="mt-0.5 h-5 w-5 shrink-0 text-neutral-500 i-hugeicons-user-circle dark:text-neutral-400" aria-hidden="true"></i>
43
+ <i class="shrink-0 mt-0.5 h-5 w-5 text-neutral-500 dark:text-neutral-400 i-hugeicons-user-circle" aria-hidden="true"></i>
44
44
  <div>
45
45
  <dt class="font-medium text-neutral-500 text-xs tracking-wide uppercase dark:text-neutral-400">Signed in as</dt>
46
46
  <dd class="mt-1 font-medium text-neutral-900 text-sm dark:text-white">{{ signedInIdentity }}</dd>
@@ -49,7 +49,7 @@ const clientTypeLabel = consent.client.type === 'public' ? 'Public client' : 'Co
49
49
 
50
50
  @if (selectedWorkspace)
51
51
  <div class="flex gap-3 items-start">
52
- <i class="mt-0.5 h-5 w-5 shrink-0 text-neutral-500 i-hugeicons-building-03 dark:text-neutral-400" aria-hidden="true"></i>
52
+ <i class="shrink-0 mt-0.5 h-5 w-5 text-neutral-500 dark:text-neutral-400 i-hugeicons-building-03" aria-hidden="true"></i>
53
53
  <div>
54
54
  <dt class="font-medium text-neutral-500 text-xs tracking-wide uppercase dark:text-neutral-400">Workspace</dt>
55
55
  <dd class="mt-1 font-medium text-neutral-900 text-sm dark:text-white">{{ selectedWorkspace }}</dd>
@@ -58,7 +58,7 @@ const clientTypeLabel = consent.client.type === 'public' ? 'Public client' : 'Co
58
58
  @endif
59
59
 
60
60
  <div class="flex gap-3 items-start">
61
- <i class="mt-0.5 h-5 w-5 shrink-0 text-neutral-500 i-hugeicons-computer-programming-01 dark:text-neutral-400" aria-hidden="true"></i>
61
+ <i class="shrink-0 mt-0.5 h-5 w-5 text-neutral-500 dark:text-neutral-400 i-hugeicons-computer-programming-01" aria-hidden="true"></i>
62
62
  <div>
63
63
  <dt class="font-medium text-neutral-500 text-xs tracking-wide uppercase dark:text-neutral-400">Application type</dt>
64
64
  <dd class="mt-1 font-medium text-neutral-900 text-sm dark:text-white">{{ clientTypeLabel }}</dd>
@@ -68,10 +68,10 @@ const clientTypeLabel = consent.client.type === 'public' ? 'Public client' : 'Co
68
68
 
69
69
  <section aria-labelledby="oauth-permissions-title">
70
70
  <h2 id="oauth-permissions-title" class="font-semibold text-base">Permissions requested</h2>
71
- <ul class="mt-3 divide-y divide-neutral-200 border border-neutral-200 rounded-xl dark:divide-neutral-800 dark:border-neutral-800">
71
+ <ul class="mt-3 border border-neutral-200 divide-neutral-200 divide-y rounded-xl dark:border-neutral-800 dark:divide-neutral-800">
72
72
  @foreach (consent.permissions as permission)
73
- <li class="flex gap-3 px-4 py-4 items-start">
74
- <i class="mt-0.5 h-5 w-5 shrink-0 text-emerald-600 i-hugeicons-key-01 dark:text-emerald-400" aria-hidden="true"></i>
73
+ <li class="flex gap-3 items-start px-4 py-4">
74
+ <i class="shrink-0 mt-0.5 h-5 w-5 text-emerald-600 dark:text-emerald-400 i-hugeicons-key-01" aria-hidden="true"></i>
75
75
  <div>
76
76
  <p class="font-medium text-neutral-900 text-sm dark:text-white">{{ permission.description }}</p>
77
77
  <p class="mt-1 font-mono text-neutral-500 text-xs dark:text-neutral-400">{{ permission.name }}</p>
@@ -86,12 +86,12 @@ const clientTypeLabel = consent.client.type === 'public' ? 'Public client' : 'Co
86
86
  <h2 id="oauth-resources-title" class="font-semibold text-base">Data access</h2>
87
87
  <ul class="mt-3 space-y-2">
88
88
  @foreach (consent.resources as resource)
89
- <li class="flex gap-3 rounded-xl bg-neutral-50 px-4 py-3 items-start dark:bg-neutral-800/60">
90
- <i class="mt-0.5 h-5 w-5 shrink-0 text-neutral-500 i-hugeicons-database-02 dark:text-neutral-400" aria-hidden="true"></i>
89
+ <li class="flex gap-3 items-start px-4 py-3 bg-neutral-50 dark:bg-neutral-800/60 rounded-xl">
90
+ <i class="shrink-0 mt-0.5 h-5 w-5 text-neutral-500 dark:text-neutral-400 i-hugeicons-database-02" aria-hidden="true"></i>
91
91
  <div>
92
92
  <p class="font-medium text-neutral-900 text-sm dark:text-white">{{ resource.name }}</p>
93
93
  @if (resource.description)
94
- <p class="mt-1 text-neutral-600 text-sm leading-5 dark:text-neutral-300">{{ resource.description }}</p>
94
+ <p class="mt-1 leading-5 text-neutral-600 text-sm dark:text-neutral-300">{{ resource.description }}</p>
95
95
  @endif
96
96
  </div>
97
97
  </li>
@@ -100,19 +100,19 @@ const clientTypeLabel = consent.client.type === 'public' ? 'Public client' : 'Co
100
100
  </section>
101
101
  @endif
102
102
 
103
- <p class="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-amber-950 text-sm leading-5 dark:border-amber-900/70 dark:bg-amber-950/40 dark:text-amber-100">
103
+ <p class="px-4 py-3 leading-5 text-amber-950 text-sm dark:text-amber-100 bg-amber-50 dark:bg-amber-950/40 border border-amber-200 rounded-xl dark:border-amber-900/70">
104
104
  Only approve if you recognize this application and trust it with the permissions listed above.
105
105
  </p>
106
106
  </div>
107
107
 
108
- <form method="POST" action="{{ consentAction }}" class="flex flex-col-reverse gap-3 border-neutral-200 border-t px-6 py-5 dark:border-neutral-800 sm:flex-row sm:justify-end sm:px-8">
108
+ <form method="POST" action="{{ consentAction }}" class="flex flex-col-reverse gap-3 sm:flex-row sm:justify-end px-6 py-5 sm:px-8 border-neutral-200 border-t dark:border-neutral-800">
109
109
  <input type="hidden" name="_token" value="{{ csrfToken }}">
110
110
  <input type="hidden" name="request_id" value="{{ consent.requestId }}">
111
111
  <button
112
112
  type="submit"
113
113
  name="decision"
114
114
  value="deny"
115
- class="inline-flex min-h-11 items-center justify-center rounded-lg border border-neutral-300 px-5 py-2.5 font-semibold text-neutral-800 text-sm hover:bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-neutral-400 focus:ring-offset-2 dark:border-neutral-700 dark:text-neutral-100 dark:hover:bg-neutral-800 dark:focus:ring-offset-neutral-900"
115
+ class="inline-flex items-center justify-center px-5 py-2.5 min-h-11 font-semibold text-neutral-800 text-sm dark:text-neutral-100 hover:bg-neutral-50 dark:hover:bg-neutral-800 border border-neutral-300 rounded-lg dark:border-neutral-700 focus:outline-none focus:ring-2 focus:ring-neutral-400 focus:ring-offset-2 dark:focus:ring-offset-neutral-900"
116
116
  >
117
117
  Deny
118
118
  </button>
@@ -120,14 +120,14 @@ const clientTypeLabel = consent.client.type === 'public' ? 'Public client' : 'Co
120
120
  type="submit"
121
121
  name="decision"
122
122
  value="approve"
123
- class="inline-flex min-h-11 items-center justify-center rounded-lg bg-neutral-950 px-5 py-2.5 font-semibold text-sm text-white hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 dark:bg-white dark:text-neutral-950 dark:hover:bg-neutral-200 dark:focus:ring-white dark:focus:ring-offset-neutral-900"
123
+ class="inline-flex items-center justify-center px-5 py-2.5 min-h-11 font-semibold text-sm text-white dark:text-neutral-950 bg-neutral-950 dark:bg-white hover:bg-neutral-800 dark:hover:bg-neutral-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 dark:focus:ring-offset-neutral-900 dark:focus:ring-white"
124
124
  >
125
125
  Allow access
126
126
  </button>
127
127
  </form>
128
128
  </div>
129
129
 
130
- <p class="mt-5 text-center text-neutral-500 text-xs leading-5 dark:text-neutral-400">
130
+ <p class="mt-5 leading-5 text-center text-neutral-500 text-xs dark:text-neutral-400">
131
131
  You can revoke this connection later from your connected applications settings.
132
132
  </p>
133
133
  </section>
@@ -1,5 +1,5 @@
1
1
  {{-- Columns block: 1-4 sanitized HTML columns on a responsive grid. --}}
2
- <div class="cms-columns mx-auto max-w-6xl px-6 py-8">
2
+ <div class="mx-auto px-6 py-8 max-w-6xl cms-columns">
3
3
  {{-- Literal class names, not `md:grid-cols-{{ n }}`: Crosswind extracts
4
4
  classes from template SOURCE, so a composed name generates no CSS. --}}
5
5
  <div class="grid grid-cols-1 gap-8 {{ props.columns.length === 4 ? 'md:grid-cols-4' : (props.columns.length === 3 ? 'md:grid-cols-3' : (props.columns.length === 2 ? 'md:grid-cols-2' : '')) }}">
@@ -1,8 +1,8 @@
1
1
  {{-- Call-to-action band. --}}
2
- <section class="cms-cta mx-auto max-w-4xl px-6 py-16 text-center">
3
- <h2 class="text-3xl tracking-tight font-semibold">{{ props.heading }}</h2>
2
+ <section class="mx-auto px-6 py-16 max-w-4xl text-center cms-cta">
3
+ <h2 class="font-semibold text-3xl tracking-tight">{{ props.heading }}</h2>
4
4
  @if (props.body)
5
- <p class="mt-4 text-lg text-gray-600 max-w-[65ch] mx-auto">{{ props.body }}</p>
5
+ <p class="mt-4 mx-auto max-w-[65ch] text-gray-600 text-lg">{{ props.body }}</p>
6
6
  @endif
7
- <a href="{{ props.buttonHref }}" class="mt-8 inline-block rounded-lg px-6 py-3 font-medium bg-gray-900 text-white transition-transform active:scale-[0.98]">{{ props.buttonLabel }}</a>
7
+ <a href="{{ props.buttonHref }}" class="inline-block mt-8 px-6 py-3 font-medium text-white bg-gray-900 rounded-lg transition-transform active:scale-[0.98]">{{ props.buttonLabel }}</a>
8
8
  </section>
@@ -1,5 +1,5 @@
1
1
  {{-- Embed block: sandboxed iframe, never inline third-party markup. --}}
2
- <div class="cms-embed mx-auto max-w-4xl px-6 py-8">
2
+ <div class="mx-auto px-6 py-8 max-w-4xl cms-embed">
3
3
  <div class="relative w-full overflow-hidden rounded-lg {{ props.aspect === '4:3' ? 'aspect-[4/3]' : (props.aspect === '1:1' ? 'aspect-square' : 'aspect-video') }}">
4
4
  <iframe
5
5
  src="{{ props.src }}"
@@ -5,7 +5,7 @@
5
5
  and submits through useMutation, which repeats the page's CSRF cookie in
6
6
  the header the submit route checks.
7
7
  --}}
8
- <div class="cms-form mx-auto max-w-xl px-6 py-8" data-form-uuid="{{ props.formUuid }}">
8
+ <div class="mx-auto px-6 py-8 max-w-xl cms-form" data-form-uuid="{{ props.formUuid }}">
9
9
  <script client>
10
10
  import type { PublicFormDefinition, PublicFormField, SubmissionErrors } from '@stacksjs/forms'
11
11
  import { moneyInputError, moneyInputStep, parseMoneyInput } from '@stacksjs/commerce/money'
@@ -225,29 +225,29 @@
225
225
  </script>
226
226
 
227
227
  <div ref="host" data-form-uuid="{{ props.formUuid }}">
228
- <div :if="done()" class="rounded-lg border border-green-200 bg-green-50 px-5 py-4 text-[15px] text-green-900">{{ done() }}</div>
228
+ <div :if="done()" class="px-5 py-4 text-[15px] text-green-900 bg-green-50 border border-green-200 rounded-lg">{{ done() }}</div>
229
229
  <form :else-if="definition()" @submit="submit" novalidate>
230
- <h2 class="text-2xl font-semibold tracking-tight">{{ definition()?.name }}</h2>
231
- <p :if="errors()._form" class="mt-3 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-[14px] text-red-800">{{ errors()._form }}</p>
230
+ <h2 class="font-semibold text-2xl tracking-tight">{{ definition()?.name }}</h2>
231
+ <p :if="errors()._form" class="mt-3 px-4 py-3 text-[14px] text-red-800 bg-red-50 border border-red-200 rounded-lg">{{ errors()._form }}</p>
232
232
 
233
- <div class="mt-6 grid grid-cols-1 gap-5 sm:grid-cols-2">
233
+ <div class="grid gap-5 grid-cols-1 sm:grid-cols-2 mt-6">
234
234
  <template :for="field in definition()?.fields ?? []">
235
235
  <div :if="fieldVisible(field)" class="{{ field.width === 'half' ? '' : 'sm:col-span-2' }}">
236
- <h3 :if="field.type === 'section_break'" class="mt-2 border-t border-gray-200 pt-4 text-lg font-medium">{{ field.label }}</h3>
236
+ <h3 :if="field.type === 'section_break'" class="mt-2 pt-4 font-medium text-lg border-gray-200 border-t">{{ field.label }}</h3>
237
237
  <template :else>
238
- <label class="block text-[14px] font-medium text-gray-800">{{ field.label }}<span :if="field.required" aria-hidden="true" class="text-red-500"> *</span></label>
239
- <textarea :if="field.type === 'textarea'" rows="4" :required="field.required" placeholder="{{ field.options.placeholder ?? '' }}" @input="values[field.name] = $event.target.value" class="mt-1.5 w-full rounded-lg border border-gray-300 px-3.5 py-2.5 text-[15px]"></textarea>
240
- <select :else-if="field.type === 'select'" :required="field.required" @change="values[field.name] = $event.target.value" class="mt-1.5 w-full rounded-lg border border-gray-300 px-3.5 py-2.5 text-[15px] bg-white">
238
+ <label class="block font-medium text-[14px] text-gray-800">{{ field.label }}<span :if="field.required" aria-hidden="true" class="text-red-500"> *</span></label>
239
+ <textarea :if="field.type === 'textarea'" rows="4" :required="field.required" placeholder="{{ field.options.placeholder ?? '' }}" @input="values[field.name] = $event.target.value" class="mt-1.5 px-3.5 py-2.5 w-full text-[15px] border border-gray-300 rounded-lg"></textarea>
240
+ <select :else-if="field.type === 'select'" :required="field.required" @change="values[field.name] = $event.target.value" class="mt-1.5 px-3.5 py-2.5 w-full text-[15px] bg-white border border-gray-300 rounded-lg">
241
241
  <option value="">Choose...</option>
242
242
  <option :for="choice in field.options.choices ?? []" value="{{ choice.value }}">{{ choice.label }}</option>
243
243
  </select>
244
244
  <div :else-if="field.type === 'radio'" class="mt-2 space-y-2">
245
- <label :for="choice in field.options.choices ?? []" class="flex items-center gap-2.5 text-[15px]">
245
+ <label :for="choice in field.options.choices ?? []" class="flex gap-2.5 items-center text-[15px]">
246
246
  <input type="radio" name="{{ field.name }}" value="{{ choice.value }}" @change="values[field.name] = $event.target.value" class="h-4 w-4">
247
247
  {{ choice.label }}
248
248
  </label>
249
249
  </div>
250
- <label :else-if="field.type === 'checkbox'" class="mt-2 flex items-center gap-2.5 text-[15px]">
250
+ <label :else-if="field.type === 'checkbox'" class="flex gap-2.5 items-center mt-2 text-[15px]">
251
251
  <input type="checkbox" @change="values[field.name] = $event.target.checked" class="h-4 w-4 rounded">
252
252
  {{ field.options.placeholder ?? 'Yes' }}
253
253
  </label>
@@ -255,11 +255,11 @@
255
255
  a person typed prose into what should have been a picker and
256
256
  the submission could never satisfy it. --}}
257
257
  <div :else-if="field.type === 'file'" class="mt-1.5">
258
- <input type="file" :required="field.required && !values[field.name]" :disabled="uploading[field.name]" accept="{{ acceptAttr(field) }}" @change="uploadFile(field, $event)" class="block w-full text-[15px] file:mr-3 file:rounded-lg file:border-0 file:bg-gray-100 file:px-3.5 file:py-2 file:text-[14px] file:font-medium">
258
+ <input type="file" :required="field.required && !values[field.name]" :disabled="uploading[field.name]" accept="{{ acceptAttr(field) }}" @change="uploadFile(field, $event)" class="block file:mr-3 file:px-3.5 file:py-2 w-full text-[15px] file:font-medium file:text-[14px] file:bg-gray-100 file:border-0 file:rounded-lg">
259
259
  <p :if="uploading[field.name]" class="mt-1.5 text-[13px] text-gray-500">Uploading...</p>
260
260
  <p :else-if="attached[field.name]" class="mt-1.5 text-[13px] text-green-700">Attached {{ attached[field.name] }}</p>
261
261
  </div>
262
- <input :else type="{{ field.type === 'email' ? 'email' : (field.type === 'phone' ? 'tel' : (field.type === 'date' ? 'date' : (field.type === 'currency' ? 'number' : 'text'))) }}" :required="field.required" placeholder="{{ field.options.placeholder ?? '' }}" :step="field.type === 'currency' ? moneyInputStep(formCurrency()) : null" @input="setInput(field, $event.target.value)" class="mt-1.5 w-full rounded-lg border border-gray-300 px-3.5 py-2.5 text-[15px]">
262
+ <input :else type="{{ field.type === 'email' ? 'email' : (field.type === 'phone' ? 'tel' : (field.type === 'date' ? 'date' : (field.type === 'currency' ? 'number' : 'text'))) }}" :required="field.required" placeholder="{{ field.options.placeholder ?? '' }}" :step="field.type === 'currency' ? moneyInputStep(formCurrency()) : null" @input="setInput(field, $event.target.value)" class="mt-1.5 px-3.5 py-2.5 w-full text-[15px] border border-gray-300 rounded-lg">
263
263
  <p :if="errors()[field.name]" class="mt-1.5 text-[13px] text-red-600">{{ errors()[field.name] }}</p>
264
264
  </template>
265
265
  </div>
@@ -268,7 +268,7 @@
268
268
 
269
269
  <input type="text" name="website" tabindex="-1" autocomplete="off" aria-hidden="true" class="hidden" @input="values._hp = $event.target.value">
270
270
 
271
- <button type="submit" :disabled="sending() || anyUploading()" class="mt-7 rounded-lg px-6 py-3 font-medium bg-gray-900 text-white transition-transform active:scale-[0.98] disabled:opacity-60">
271
+ <button type="submit" :disabled="sending() || anyUploading()" class="mt-7 px-6 py-3 font-medium text-white bg-gray-900 rounded-lg disabled:opacity-60 transition-transform active:scale-[0.98]">
272
272
  {{ sending() ? 'Sending...' : (definition()?.submitLabel ?? 'Submit') }}
273
273
  </button>
274
274
  </form>
@@ -1,16 +1,16 @@
1
1
  {{-- Hero block: heading, optional subheading/image/CTA. --}}
2
2
  <section class="cms-hero relative overflow-hidden {{ props.align === 'center' ? 'text-center' : 'text-left' }}">
3
3
  @if (props.imageUrl)
4
- <img src="{{ props.imageUrl }}" alt="" class="absolute inset-0 h-full w-full object-cover" aria-hidden="true">
4
+ <img src="{{ props.imageUrl }}" alt="" class="absolute inset-0 object-cover h-full w-full" aria-hidden="true">
5
5
  <div class="absolute inset-0 bg-black/45" aria-hidden="true"></div>
6
6
  @endif
7
7
  <div class="relative mx-auto max-w-4xl px-6 py-24 {{ props.imageUrl ? 'text-white' : '' }}">
8
- <h1 class="text-4xl md:text-6xl tracking-tighter leading-none font-semibold">{{ props.heading }}</h1>
8
+ <h1 class="font-semibold leading-none text-4xl tracking-tighter md:text-6xl">{{ props.heading }}</h1>
9
9
  @if (props.subheading)
10
10
  <p class="mt-5 text-lg md:text-xl max-w-[65ch] {{ props.align === 'center' ? 'mx-auto' : '' }} opacity-90">{{ props.subheading }}</p>
11
11
  @endif
12
12
  @if (props.ctaLabel && props.ctaHref)
13
- <a href="{{ props.ctaHref }}" class="mt-8 inline-block rounded-lg px-6 py-3 font-medium bg-white text-gray-900 shadow-sm transition-transform active:scale-[0.98]">{{ props.ctaLabel }}</a>
13
+ <a href="{{ props.ctaHref }}" class="inline-block mt-8 px-6 py-3 font-medium text-gray-900 bg-white rounded-lg shadow-sm transition-transform active:scale-[0.98]">{{ props.ctaLabel }}</a>
14
14
  @endif
15
15
  </div>
16
16
  </section>
@@ -2,6 +2,6 @@
2
2
  <figure class="cms-image mx-auto px-6 py-8 {{ props.width === 'full' ? 'max-w-none px-0' : (props.width === 'wide' ? 'max-w-6xl' : 'max-w-3xl') }}">
3
3
  <img src="{{ props.src }}" alt="{{ props.alt }}" class="w-full rounded-lg" loading="lazy">
4
4
  @if (props.caption)
5
- <figcaption class="mt-3 text-sm text-gray-500">{{ props.caption }}</figcaption>
5
+ <figcaption class="mt-3 text-gray-500 text-sm">{{ props.caption }}</figcaption>
6
6
  @endif
7
7
  </figure>
@@ -1,4 +1,4 @@
1
1
  {{-- Rich text block. `props.html` is sanitized by the render pipeline before it gets here. --}}
2
- <div class="cms-rich-text mx-auto max-w-3xl px-6 py-8 leading-relaxed">
2
+ <div class="mx-auto px-6 py-8 max-w-3xl leading-relaxed cms-rich-text">
3
3
  {!! props.html !!}
4
4
  </div>