@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
@@ -106,6 +106,46 @@ minimumIntervalMinutes? }`and`associatedDomains` (universal links). Leave
106
106
  `buddy build:ios` warns. The Liquid Glass chrome needs iOS 26 at run time;
107
107
  the default `deploymentTarget` stays, older systems drawing classic bars.
108
108
 
109
+ ## Native screen bindings
110
+
111
+ Screens under `resources/native/` use the same STX client syntax as web views.
112
+ Declare reactive data in `<script client>` and bind text with `:text`, props
113
+ with `:prop`, and native presses with `@click`. Pull-to-refresh uses `@refresh`
114
+ and a boolean `:refreshing` signal. Bare signals auto-unwrap in bindings;
115
+ read them with `signal()` and update them with `signal.set()` inside scripts.
116
+ Imported helpers used only in bindings are retained in the client setup.
117
+
118
+ ```stx
119
+ <script client>
120
+ const title = state('Today')
121
+ const refreshing = state(false)
122
+ async function refresh() {
123
+ refreshing.set(true)
124
+ try {
125
+ const response = await fetch('https://example.com/api/title')
126
+ if (!response.ok) throw new Error('Could not refresh')
127
+ title.set((await response.json()).title)
128
+ }
129
+ finally {
130
+ refreshing.set(false)
131
+ }
132
+ }
133
+ </script>
134
+ <ScrollView @refresh="refresh()" :refreshing="refreshing">
135
+ <Text :text="title" />
136
+ </ScrollView>
137
+ ```
138
+
139
+ A single-brace JSX expression such as `{title}` is plain text in STX. Keep
140
+ startup requests and timers in `onMount()`, returning a cleanup for timers so
141
+ leaving a native screen releases them. Test the minified `compileNativeBundle`
142
+ output with the native bridge, including refresh, keyed rows, conditions and
143
+ navigation; compiling a plausible tree alone does not prove it updates.
144
+
145
+ While an app is in TestFlight before its first public release, keep the iOS
146
+ marketing version at `1.0.0` and run `buddy release:ios --bump build`. Each
147
+ upload needs a new build number, not a patch to the marketing version.
148
+
109
149
  ## Device search index
110
150
 
111
151
  `spotlight` says what of the application's own content a device may index, so
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.127",
5
+ "version": "0.75.129",
6
6
  "description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.127",
5
+ "version": "0.75.129",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -55,7 +55,7 @@
55
55
  "dependencies": {
56
56
  "@iconify-json/f7": "^1.2.2",
57
57
  "@iconify-json/hugeicons": "^1.2.35",
58
- "@stacksjs/mobile": "0.75.127",
58
+ "@stacksjs/mobile": "0.75.129",
59
59
  "@stacksjs/sanitizer": "^0.2.422",
60
60
  "@xterm/addon-fit": "^0.11.0",
61
61
  "@xterm/xterm": "^6.0.0",
@@ -72,12 +72,12 @@ function saveChoices() {
72
72
  <div
73
73
  role="region"
74
74
  aria-label="Cookie consent"
75
- class="fixed bottom-0 inset-x-0 z-50 p-4 sm:p-6"
75
+ class="bottom-0 fixed inset-x-0 z-50 p-4 sm:p-6"
76
76
  >
77
- <div class="mx-auto max-w-3xl rounded-lg border border-gray-200 bg-white p-4 shadow-lg dark:border-neutral-700 dark:bg-neutral-900 sm:p-6">
78
- <h2 class="text-base font-semibold text-gray-900 dark:text-white">{{ title }}</h2>
77
+ <div class="mx-auto p-4 sm:p-6 max-w-3xl bg-white dark:bg-neutral-900 border border-gray-200 rounded-lg dark:border-neutral-700 shadow-lg">
78
+ <h2 class="font-semibold text-base text-gray-900 dark:text-white">{{ title }}</h2>
79
79
 
80
- <p class="mt-2 text-sm text-gray-600 dark:text-neutral-300">
80
+ <p class="mt-2 text-gray-600 text-sm dark:text-neutral-300">
81
81
  {{ message }}
82
82
  <a :href="policyUrl" class="underline hover:no-underline">Read our policy</a>.
83
83
  </p>
@@ -86,7 +86,7 @@ function saveChoices() {
86
86
  <fieldset class="mt-4 space-y-3">
87
87
  <legend class="sr-only">Cookie categories</legend>
88
88
 
89
- <label class="flex items-start gap-3 text-sm">
89
+ <label class="flex gap-3 items-start text-sm">
90
90
  <input type="checkbox" checked disabled class="mt-0.5" />
91
91
  <span>
92
92
  <span class="font-medium text-gray-900 dark:text-white">Necessary</span>
@@ -94,7 +94,7 @@ function saveChoices() {
94
94
  </span>
95
95
  </label>
96
96
 
97
- <label class="flex items-start gap-3 text-sm">
97
+ <label class="flex gap-3 items-start text-sm">
98
98
  <input type="checkbox" :checked="preferences()" @change="preferences.set(!preferences())" class="mt-0.5" />
99
99
  <span>
100
100
  <span class="font-medium text-gray-900 dark:text-white">Preferences</span>
@@ -102,7 +102,7 @@ function saveChoices() {
102
102
  </span>
103
103
  </label>
104
104
 
105
- <label class="flex items-start gap-3 text-sm">
105
+ <label class="flex gap-3 items-start text-sm">
106
106
  <input type="checkbox" :checked="analytics()" @change="analytics.set(!analytics())" class="mt-0.5" />
107
107
  <span>
108
108
  <span class="font-medium text-gray-900 dark:text-white">Analytics</span>
@@ -110,7 +110,7 @@ function saveChoices() {
110
110
  </span>
111
111
  </label>
112
112
 
113
- <label class="flex items-start gap-3 text-sm">
113
+ <label class="flex gap-3 items-start text-sm">
114
114
  <input type="checkbox" :checked="marketing()" @change="marketing.set(!marketing())" class="mt-0.5" />
115
115
  <span>
116
116
  <span class="font-medium text-gray-900 dark:text-white">Marketing</span>
@@ -121,22 +121,22 @@ function saveChoices() {
121
121
  @endif
122
122
 
123
123
  {{-- Decline is a peer of Accept, not a lesser link. --}}
124
- <div class="mt-4 flex flex-col gap-2 sm:flex-row sm:justify-end">
124
+ <div class="flex flex-col gap-2 sm:flex-row sm:justify-end mt-4">
125
125
  @if(showChoices())
126
- <button type="button" @click="saveChoices()" class="rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-700 dark:bg-white dark:text-gray-900 dark:hover:bg-neutral-200">
126
+ <button type="button" @click="saveChoices()" class="px-4 py-2 font-medium text-sm text-white dark:text-gray-900 bg-gray-900 dark:bg-white hover:bg-gray-700 dark:hover:bg-neutral-200 rounded-md">
127
127
  Save choices
128
128
  </button>
129
129
  @else
130
- <button type="button" @click="showChoices.set(true)" class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-900 hover:bg-gray-50 dark:border-neutral-600 dark:text-white dark:hover:bg-neutral-800">
130
+ <button type="button" @click="showChoices.set(true)" class="px-4 py-2 font-medium text-gray-900 text-sm dark:text-white hover:bg-gray-50 dark:hover:bg-neutral-800 border border-gray-300 rounded-md dark:border-neutral-600">
131
131
  Choose
132
132
  </button>
133
133
  @endif
134
134
 
135
- <button type="button" @click="declineAll()" class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-900 hover:bg-gray-50 dark:border-neutral-600 dark:text-white dark:hover:bg-neutral-800">
135
+ <button type="button" @click="declineAll()" class="px-4 py-2 font-medium text-gray-900 text-sm dark:text-white hover:bg-gray-50 dark:hover:bg-neutral-800 border border-gray-300 rounded-md dark:border-neutral-600">
136
136
  Decline
137
137
  </button>
138
138
 
139
- <button type="button" @click="acceptAll()" class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-900 hover:bg-gray-50 dark:border-neutral-600 dark:text-white dark:hover:bg-neutral-800">
139
+ <button type="button" @click="acceptAll()" class="px-4 py-2 font-medium text-gray-900 text-sm dark:text-white hover:bg-gray-50 dark:hover:bg-neutral-800 border border-gray-300 rounded-md dark:border-neutral-600">
140
140
  Accept all
141
141
  </button>
142
142
  </div>
@@ -220,7 +220,7 @@
220
220
  <path :d="sparklinePath()" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" />
221
221
  </svg>
222
222
  </div>
223
- <div :if="loadingHistory() && !hasHistory()" role="status" class="flex gap-2 items-center mb-5 text-zinc-500 text-xs dark:text-zinc-400">
223
+ <div :if="loadingHistory() && !hasHistory()" role="status" class="flex gap-2 items-center mb-5 text-xs text-zinc-500 dark:text-zinc-400">
224
224
  <span aria-hidden="true" class="h-3.5 w-3.5 animate-spin i-hugeicons-loading-03"></span>
225
225
  <span>Loading runner history</span>
226
226
  </div>
@@ -75,7 +75,7 @@ function failedJobs(runId: number) {
75
75
 
76
76
  <div :if="loading() && runs().length === 0" class="px-5 py-12 text-center">
77
77
  <span aria-hidden="true" class="mx-auto h-6 w-6 text-violet-500 animate-spin i-hugeicons-loading-03"></span>
78
- <p class="mt-2 text-zinc-500 text-xs">Fetching recent runs...</p>
78
+ <p class="mt-2 text-xs text-zinc-500">Fetching recent runs...</p>
79
79
  </div>
80
80
 
81
81
  <ul :if="runs().length > 0" class="px-5 py-3">
@@ -87,7 +87,7 @@ function failedJobs(runId: number) {
87
87
  <span :class="`px-1.5 py-0.5 text-[10px] font-medium rounded ${runStatusBadge(run.conclusion, run.status).cls}`" :text="runStatusBadge(run.conclusion, run.status).label"></span>
88
88
  <span class="font-medium text-sm text-zinc-800 truncate dark:text-zinc-200" :text="run.name"></span>
89
89
  </div>
90
- <p :if="run.commitMessage" class="mt-1 text-zinc-500 text-xs truncate" :text="truncate(run.commitMessage, 80)"></p>
90
+ <p :if="run.commitMessage" class="mt-1 text-xs text-zinc-500 truncate" :text="truncate(run.commitMessage, 80)"></p>
91
91
  <div class="flex gap-2 items-center mt-1 text-[10px] text-zinc-400">
92
92
  <span class="font-mono" :text="run.headShaShort"></span>
93
93
  <span :if="run.headBranch" :text="run.headBranch"></span>
@@ -104,8 +104,8 @@ function failedJobs(runId: number) {
104
104
  </div>
105
105
 
106
106
  <div :if="expandedRunId() === run.id" class="px-3 py-2 bg-zinc-50 dark:bg-zinc-900/50 border-t border-zinc-100 dark:border-zinc-900">
107
- <div :if="loadingJobsRunId() === run.id" class="py-2 text-zinc-500 text-xs">Loading jobs...</div>
108
- <div :if="loadingJobsRunId() !== run.id && jobsForRun(run.id).length === 0" class="py-1 italic text-zinc-400 text-xs">No job detail.</div>
107
+ <div :if="loadingJobsRunId() === run.id" class="py-2 text-xs text-zinc-500">Loading jobs...</div>
108
+ <div :if="loadingJobsRunId() !== run.id && jobsForRun(run.id).length === 0" class="py-1 italic text-xs text-zinc-400">No job detail.</div>
109
109
  <div :if="loadingJobsRunId() !== run.id && failedJobs(run.id).length > 0" class="space-y-1.5">
110
110
  <p class="text-[10px] text-zinc-500 tracking-wide uppercase">Failed jobs</p>
111
111
  <a :for="job in failedJobs(run.id)" :href="job.url" target="_blank" rel="noreferrer" @click.stop class="flex gap-1.5 items-center no-underline text-red-700 text-xs dark:text-red-400 hover:underline">
@@ -114,7 +114,7 @@ function failedJobs(runId: number) {
114
114
  <span :if="job.durationMs" class="text-zinc-400" :text="'(' + formatDuration(job.durationMs) + ')'"></span>
115
115
  </a>
116
116
  </div>
117
- <div :if="loadingJobsRunId() !== run.id && jobsForRun(run.id).length > 0 && failedJobs(run.id).length === 0" class="py-1 text-zinc-500 text-xs">
117
+ <div :if="loadingJobsRunId() !== run.id && jobsForRun(run.id).length > 0 && failedJobs(run.id).length === 0" class="py-1 text-xs text-zinc-500">
118
118
  All <span :text="jobsForRun(run.id).length"></span> jobs passed.
119
119
  </div>
120
120
  </div>
@@ -122,6 +122,6 @@ function failedJobs(runId: number) {
122
122
  </ul>
123
123
 
124
124
  <div :if="!loading() && !error() && runs().length === 0" class="px-5 py-12 text-center">
125
- <p class="text-zinc-500 text-sm">No recent runs.</p>
125
+ <p class="text-sm text-zinc-500">No recent runs.</p>
126
126
  </div>
127
127
  </Drawer>
@@ -9,8 +9,8 @@ defineClientPayload({ blogUrl })
9
9
  </script>
10
10
 
11
11
  <template>
12
- <div class="p-6 min-h-screen max-w-full text-neutral-950 dark:text-neutral-100 bg-neutral-50 dark:bg-neutral-950">
13
- <div class="flex flex-col gap-6 mx-auto min-w-0 max-w-7xl w-full">
12
+ <div class="p-6 max-w-full min-h-screen text-neutral-950 dark:text-neutral-100 bg-neutral-50 dark:bg-neutral-950">
13
+ <div class="flex flex-col gap-6 mx-auto max-w-7xl min-w-0 w-full">
14
14
 
15
15
  <header class="flex flex-col gap-4 md:flex-row md:items-end md:justify-between pb-5 border-b border-neutral-200 dark:border-neutral-800">
16
16
  <div>
@@ -50,7 +50,7 @@ defineClientPayload({ blogUrl })
50
50
  </div>
51
51
  </section>
52
52
 
53
- <section class="overflow-hidden min-w-0 max-w-full bg-white dark:bg-neutral-900 border border-neutral-200 rounded-md dark:border-neutral-800">
53
+ <section class="overflow-hidden max-w-full min-w-0 bg-white dark:bg-neutral-900 border border-neutral-200 rounded-md dark:border-neutral-800">
54
54
  <div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between p-4 border-b border-neutral-200 dark:border-neutral-800">
55
55
  <div class="relative flex-1 max-w-md">
56
56
  <span class="absolute left-3 top-1/2 h-4 w-4 text-neutral-500 pointer-events-none -translate-y-1/2 i-hugeicons-search-01"></span>
@@ -160,14 +160,14 @@ function submit(): void {
160
160
  </label>
161
161
 
162
162
  <label class="flex flex-col gap-2 sm:col-span-2">
163
- <span class="flex items-center justify-between gap-3">
163
+ <span class="flex gap-3 items-center justify-between">
164
164
  <span class="font-medium text-sm">Content</span>
165
165
  <span :if="bodyLoading()" role="status" class="inline-flex gap-1.5 items-center text-neutral-500 text-xs dark:text-neutral-400">
166
166
  <span class="h-3.5 w-3.5 animate-spin i-hugeicons-loading-03"></span>
167
167
  Loading content
168
168
  </span>
169
169
  </span>
170
- <textarea x-model="body" name="body" rows="14" :disabled="bodyLoading() || bodyLoadFailed()" class="px-3 py-2 font-mono text-neutral-900 text-sm dark:text-neutral-100 bg-white dark:bg-neutral-950 border border-neutral-300 outline-none rounded-md disabled:opacity-60 dark:border-neutral-700 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/30 resize-y"></textarea>
170
+ <textarea x-model="body" name="body" rows="14" :disabled="bodyLoading() || bodyLoadFailed()" class="px-3 py-2 font-mono text-neutral-900 text-sm dark:text-neutral-100 bg-white dark:bg-neutral-950 border border-neutral-300 outline-none rounded-md dark:border-neutral-700 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/30 disabled:opacity-60 resize-y"></textarea>
171
171
  <span class="text-neutral-500 text-xs dark:text-neutral-400">Markdown. {{ wordCount() }} words, about {{ readingTime() }} min read.</span>
172
172
  </label>
173
173
  </div>
@@ -162,7 +162,7 @@ onMount(() => {
162
162
  })
163
163
  </script>
164
164
 
165
- <div class="min-w-0 max-w-full space-y-6">
165
+ <div class="space-y-6 max-w-full min-w-0">
166
166
  <header class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
167
167
  <div>
168
168
  <p class="font-semibold text-blue-600 text-xs tracking-wide uppercase dark:text-blue-400">CMS</p>
@@ -183,7 +183,7 @@ onMount(() => {
183
183
  <article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700"><p class="text-gray-500 text-xs dark:text-neutral-400">Newest</p><p class="mt-2 font-semibold text-gray-900 text-lg truncate dark:text-white">{{ records()[0]?.name || 'None' }}</p></article>
184
184
  </section>
185
185
 
186
- <section class="overflow-hidden min-w-0 max-w-full bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
186
+ <section class="overflow-hidden max-w-full min-w-0 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
187
187
  <div class="p-4 border-b border-gray-200 dark:border-neutral-700"><label><span class="sr-only">Search {{ resource() }}</span><input x-model="search" type="search" :placeholder="'Search ' + resource()" class="px-3 py-2 max-w-md w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" /></label></div>
188
188
  <div class="overflow-x-auto max-w-full w-full">
189
189
  <table class="min-w-full divide-gray-200 divide-y dark:divide-neutral-700">
@@ -29,8 +29,8 @@ function submit(): void {
29
29
  <Modal :isOpen="open()" title="Create folder" :description="'Add a folder inside ' + location() + '.'" size="sm" :closable="!busy()" @close="close">
30
30
  <form id="create-folder-form" @submit.prevent="submit">
31
31
  <label class="block">
32
- <span class="font-medium text-stone-700 text-sm dark:text-neutral-200">Folder name</span>
33
- <input x-model.trim="folderName" name="name" required maxlength="255" autocomplete="off" class="mt-1 px-3 py-2 w-full text-stone-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-stone-300 rounded-lg dark:border-neutral-700" />
32
+ <span class="font-medium text-sm text-stone-700 dark:text-neutral-200">Folder name</span>
33
+ <input x-model.trim="folderName" name="name" required maxlength="255" autocomplete="off" class="mt-1 px-3 py-2 w-full text-sm text-stone-900 dark:text-white bg-white dark:bg-neutral-900 border border-stone-300 rounded-lg dark:border-neutral-700" />
34
34
  </label>
35
35
  </form>
36
36
 
@@ -67,7 +67,7 @@ function submit(): void {
67
67
  <form id="file-upload-form" @submit.prevent="submit">
68
68
  <label class="flex flex-col items-center justify-center p-8 text-center border-2 border-dashed border-stone-300 rounded-xl dark:border-neutral-600 hover:border-blue-400 dark:hover:border-blue-600 cursor-pointer" @dragover.prevent @drop.prevent="handleDrop($event)">
69
69
  <span class="h-10 w-10 text-stone-400 i-hugeicons-upload-01"></span>
70
- <span class="mt-3 font-semibold text-stone-800 text-sm dark:text-white">Choose files or drop them here</span>
70
+ <span class="mt-3 font-semibold text-sm text-stone-800 dark:text-white">Choose files or drop them here</span>
71
71
  <span class="mt-1 text-stone-500 text-xs dark:text-neutral-400">{{ selectedFilesLabel() }}</span>
72
72
  <input ref="dashboardFileUploadInput" name="files" type="file" class="sr-only" multiple @change="handleFileSelection($event)" />
73
73
  </label>
@@ -327,7 +327,7 @@ onMount(() => {
327
327
  <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">{{ rollbackPlan()?.site || 'All sites' }} in {{ rollbackPlan()?.environment }}</p>
328
328
  </div>
329
329
  </div>
330
- <pre class="mt-4 p-3 max-h-40 overflow-auto whitespace-pre-wrap font-mono text-neutral-600 text-xs dark:text-neutral-300 bg-white dark:bg-neutral-900 border border-neutral-200 rounded-md dark:border-neutral-800">{{ rollbackPlan()?.output }}</pre>
330
+ <pre class="overflow-auto mt-4 p-3 max-h-40 font-mono text-neutral-600 text-xs whitespace-pre-wrap dark:text-neutral-300 bg-white dark:bg-neutral-900 border border-neutral-200 rounded-md dark:border-neutral-800">{{ rollbackPlan()?.output }}</pre>
331
331
  </div>
332
332
  <div :for="warning in rollbackPlan()?.warnings || []" class="flex gap-2 items-start text-orange-800 text-sm dark:text-orange-200">
333
333
  <span aria-hidden="true" class="shrink-0 mt-0.5 h-4 w-4 i-hugeicons-alert-02"></span>
@@ -30,8 +30,8 @@ function icon(operation: DeploymentPreviewOperation): string {
30
30
  contentClassName="p-0"
31
31
  @close="emit('close')"
32
32
  >
33
- <div :if="plan()" class="max-h-[65vh] overflow-y-auto">
34
- <div class="grid gap-px grid-cols-2 md:grid-cols-4 bg-neutral-200 dark:bg-neutral-700 border-neutral-200 border-b dark:border-neutral-700">
33
+ <div :if="plan()" class="overflow-y-auto max-h-[65vh]">
34
+ <div class="grid gap-px grid-cols-2 md:grid-cols-4 bg-neutral-200 dark:bg-neutral-700 border-b border-neutral-200 dark:border-neutral-700">
35
35
  <div class="p-4 bg-white dark:bg-neutral-900">
36
36
  <p class="text-neutral-500 text-xs uppercase dark:text-neutral-400">Environment</p>
37
37
  <p class="mt-1 capitalize font-semibold text-neutral-900 text-sm dark:text-white">{{ plan()?.environment }}</p>
@@ -76,10 +76,10 @@ function icon(operation: DeploymentPreviewOperation): string {
76
76
  </div>
77
77
  <ol class="mt-4 space-y-3">
78
78
  <li :for="operation, index in plan()?.operations || []" class="flex gap-4 items-start p-4 bg-neutral-50 dark:bg-neutral-800/70 border border-neutral-200 rounded-xl dark:border-neutral-700">
79
- <div class="flex shrink-0 items-center justify-center h-9 w-9 text-blue-700 dark:text-blue-300 bg-blue-100 dark:bg-blue-950/60 rounded-lg">
79
+ <div class="flex items-center justify-center shrink-0 h-9 w-9 text-blue-700 dark:text-blue-300 bg-blue-100 dark:bg-blue-950/60 rounded-lg">
80
80
  <span aria-hidden="true" class="h-5 w-5 {{ icon(operation) }}"></span>
81
81
  </div>
82
- <div class="min-w-0 flex-1">
82
+ <div class="flex-1 min-w-0">
83
83
  <div class="flex gap-2 items-center">
84
84
  <span class="font-mono text-neutral-400 text-xs">{{ index + 1 }}</span>
85
85
  <h5 class="font-semibold text-neutral-900 text-sm dark:text-white">{{ operation.label }}</h5>
@@ -97,7 +97,7 @@ function icon(operation: DeploymentPreviewOperation): string {
97
97
  <article :for="site in plan()?.sites || []" class="p-4 bg-white dark:bg-neutral-900 border border-neutral-200 rounded-xl dark:border-neutral-700">
98
98
  <div class="flex gap-3 items-center justify-between">
99
99
  <h5 class="font-semibold text-neutral-900 text-sm dark:text-white">{{ site.name }}</h5>
100
- <span class="px-2 py-1 font-mono text-neutral-600 text-[11px] dark:text-neutral-300 bg-neutral-100 dark:bg-neutral-800 rounded-md">{{ site.kind }}</span>
100
+ <span class="px-2 py-1 font-mono text-[11px] text-neutral-600 dark:text-neutral-300 bg-neutral-100 dark:bg-neutral-800 rounded-md">{{ site.kind }}</span>
101
101
  </div>
102
102
  <p class="mt-2 text-neutral-500 text-xs dark:text-neutral-400">{{ site.domains.join(', ') || 'Private service' }}{{ site.path !== '/' ? site.path : '' }}</p>
103
103
  </article>
@@ -106,8 +106,8 @@ onMount(() => {
106
106
  <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">
107
107
  Outbound messages recorded by the local log driver. Active mailer: {{ activeDriver() || 'log' }}.
108
108
  </p>
109
- <div :if="unreadable().length > 0" class="mt-2 flex gap-2 items-start px-3 py-2 bg-amber-50 border border-amber-200 rounded-md text-amber-800 text-xs dark:bg-amber-900/20 dark:border-amber-800/40 dark:text-amber-200">
110
- <span class="mt-0.5 h-3.5 w-3.5 shrink-0 i-hugeicons-alert-02"></span>
109
+ <div :if="unreadable().length > 0" class="flex gap-2 items-start mt-2 px-3 py-2 text-amber-800 text-xs dark:text-amber-200 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 rounded-md dark:border-amber-800/40">
110
+ <span class="shrink-0 mt-0.5 h-3.5 w-3.5 i-hugeicons-alert-02"></span>
111
111
  <div>
112
112
  <p class="font-medium">
113
113
  {{ unreadable().length }} capture{{ unreadable().length === 1 ? '' : 's' }} could not be read and {{ unreadable().length === 1 ? 'was' : 'were' }} left out of this list.
@@ -176,7 +176,7 @@ function another() {
176
176
  <div class="inline-flex items-center justify-center mb-4 h-12 w-12 bg-red-100 dark:bg-red-900/30 rounded-full">
177
177
  <span class="h-6 w-6 text-red-600 dark:text-red-400 i-hugeicons-alert-circle"></span>
178
178
  </div>
179
- <h1 class="mb-2 font-semibold text-xl text-neutral-900 tracking-tight dark:text-white">This feedback link is not valid</h1>
179
+ <h1 class="mb-2 font-semibold text-neutral-900 text-xl tracking-tight dark:text-white">This feedback link is not valid</h1>
180
180
  <p class="text-[15px] text-neutral-500 dark:text-neutral-400">
181
181
  It may have been turned off, or it may have expired. Ask whoever sent it for a fresh one.
182
182
  </p>
@@ -186,9 +186,9 @@ function another() {
186
186
  <div class="inline-flex items-center justify-center mb-4 h-12 w-12 bg-green-100 dark:bg-green-900/30 rounded-full">
187
187
  <span class="h-6 w-6 text-green-600 dark:text-green-400 i-hugeicons-checkmark-circle-02"></span>
188
188
  </div>
189
- <h1 class="mb-2 font-semibold text-xl text-neutral-900 tracking-tight dark:text-white">Thank you</h1>
189
+ <h1 class="mb-2 font-semibold text-neutral-900 text-xl tracking-tight dark:text-white">Thank you</h1>
190
190
  <p class="mb-6 text-[15px] text-neutral-500 dark:text-neutral-400">Your feedback is on the board.</p>
191
- <button type="button" @click="another" class="px-5 py-2.5 font-medium text-sm text-white bg-neutral-900 rounded-lg transition-transform dark:bg-white dark:text-neutral-900 active:scale-[0.98]">
191
+ <button type="button" @click="another" class="px-5 py-2.5 font-medium text-sm text-white dark:text-neutral-900 bg-neutral-900 dark:bg-white rounded-lg transition-transform active:scale-[0.98]">
192
192
  Send another
193
193
  </button>
194
194
  </div>
@@ -203,7 +203,7 @@ function another() {
203
203
 
204
204
  <form @submit.prevent="submit" @paste="paste($event)" class="p-6 bg-white dark:bg-neutral-900 border border-neutral-200/60 rounded-2xl dark:border-neutral-800 shadow-black/5 shadow-sm">
205
205
  <div :if="error()" role="alert" class="flex gap-2 items-center mb-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 rounded-lg dark:border-red-800">
206
- <span class="h-4 w-4 flex-shrink-0 text-red-600 dark:text-red-400 i-hugeicons-alert-circle"></span>
206
+ <span class="flex-shrink-0 h-4 w-4 text-red-600 dark:text-red-400 i-hugeicons-alert-circle"></span>
207
207
  <p class="text-red-600 text-sm dark:text-red-400">{{ error() }}</p>
208
208
  </div>
209
209
 
@@ -216,7 +216,7 @@ function another() {
216
216
  :disabled="status() === 'sending'"
217
217
  required
218
218
  placeholder="A short summary"
219
- class="block px-3 py-2.5 w-full text-neutral-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-neutral-300 rounded-lg transition-colors dark:border-neutral-600 placeholder:text-neutral-400 hover:border-neutral-400 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20"
219
+ class="block px-3 py-2.5 w-full text-neutral-900 text-sm dark:text-white placeholder:text-neutral-400 bg-white dark:bg-neutral-900 border border-neutral-300 rounded-lg dark:border-neutral-600 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 hover:border-neutral-400 transition-colors"
220
220
  />
221
221
  </label>
222
222
 
@@ -228,7 +228,7 @@ function another() {
228
228
  :maxlength="maxDescription()"
229
229
  :disabled="status() === 'sending'"
230
230
  placeholder="Where you were, what you expected, and what you got instead"
231
- class="block px-3 py-2.5 w-full text-neutral-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-neutral-300 rounded-lg resize-y transition-colors dark:border-neutral-600 placeholder:text-neutral-400 hover:border-neutral-400 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20"
231
+ class="block px-3 py-2.5 w-full text-neutral-900 text-sm dark:text-white placeholder:text-neutral-400 bg-white dark:bg-neutral-900 border border-neutral-300 rounded-lg dark:border-neutral-600 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 hover:border-neutral-400 transition-colors resize-y"
232
232
  ></textarea>
233
233
  </label>
234
234
 
@@ -236,14 +236,14 @@ function another() {
236
236
  <span class="block mb-1.5 font-medium text-neutral-700 text-sm dark:text-neutral-200">Screenshot <span class="font-normal text-neutral-400">(optional)</span></span>
237
237
 
238
238
  <div :if="screenshot()" class="flex gap-3 items-center p-3 bg-neutral-50 dark:bg-neutral-800/50 border border-neutral-200 rounded-lg dark:border-neutral-700">
239
- <img :src="preview()" alt="The screenshot you chose" class="h-14 w-14 bg-white dark:bg-neutral-900 border border-neutral-200 rounded object-cover dark:border-neutral-700" />
239
+ <img :src="preview()" alt="The screenshot you chose" class="object-cover h-14 w-14 bg-white dark:bg-neutral-900 border border-neutral-200 rounded dark:border-neutral-700" />
240
240
  <span class="flex-1 min-w-0 text-neutral-600 text-sm truncate dark:text-neutral-300">{{ screenshot()?.name || 'Pasted image' }}</span>
241
- <button type="button" @click="clear" class="flex-shrink-0 px-2.5 py-1.5 font-medium text-neutral-600 text-xs dark:text-neutral-300 rounded-md hover:bg-neutral-200 dark:hover:bg-neutral-700">
241
+ <button type="button" @click="clear" class="flex-shrink-0 px-2.5 py-1.5 font-medium text-neutral-600 text-xs dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-700 rounded-md">
242
242
  Remove
243
243
  </button>
244
244
  </div>
245
245
 
246
- <label :else class="flex flex-col items-center justify-center px-4 py-6 text-center border border-neutral-300 border-dashed rounded-lg cursor-pointer transition-colors dark:border-neutral-600 hover:border-blue-400 dark:hover:border-blue-600" @dragover.prevent @drop.prevent="drop($event)">
246
+ <label :else class="flex flex-col items-center justify-center px-4 py-6 text-center border border-dashed border-neutral-300 rounded-lg dark:border-neutral-600 hover:border-blue-400 dark:hover:border-blue-600 transition-colors cursor-pointer" @dragover.prevent @drop.prevent="drop($event)">
247
247
  <span class="h-6 w-6 text-neutral-400 i-hugeicons-image-upload"></span>
248
248
  <span class="mt-2 font-medium text-neutral-700 text-sm dark:text-neutral-200">Choose an image, drop it here, or paste it</span>
249
249
  <span class="mt-1 text-neutral-400 text-xs">PNG, JPEG, WebP or GIF, up to {{ megabytes(sizeLimit()) }}</span>
@@ -262,7 +262,7 @@ function another() {
262
262
  <button
263
263
  type="submit"
264
264
  :disabled="!canSubmit()"
265
- class="flex gap-2 items-center justify-center px-5 py-2.5 w-full font-medium text-sm text-white bg-neutral-900 rounded-lg transition-transform dark:bg-white dark:text-neutral-900 disabled:cursor-not-allowed disabled:opacity-50 active:scale-[0.99]"
265
+ class="flex gap-2 items-center justify-center px-5 py-2.5 w-full font-medium text-sm text-white dark:text-neutral-900 bg-neutral-900 dark:bg-white rounded-lg disabled:opacity-50 transition-transform active:scale-[0.99] disabled:cursor-not-allowed"
266
266
  >
267
267
  <span :if="status() === 'sending'" class="h-4 w-4 animate-spin i-hugeicons-loading-03"></span>
268
268
  <span>{{ status() === 'sending' ? 'Sending' : 'Send feedback' }}</span>
@@ -245,7 +245,7 @@ function formatTimestamp(iso: string | null | undefined): string {
245
245
  >
246
246
  <div :if="openCard()" class="grid gap-6">
247
247
  <label class="grid gap-2">
248
- <span class="font-medium text-zinc-500 text-xs tracking-wide uppercase">Title</span>
248
+ <span class="font-medium text-xs text-zinc-500 tracking-wide uppercase">Title</span>
249
249
  <input
250
250
  x-model="titleDraft"
251
251
  name="card_title"
@@ -257,7 +257,7 @@ function formatTimestamp(iso: string | null | undefined): string {
257
257
  />
258
258
  </label>
259
259
 
260
- <div :if="loadingCard()" role="status" class="flex gap-2 items-center text-zinc-500 text-sm">
260
+ <div :if="loadingCard()" role="status" class="flex gap-2 items-center text-sm text-zinc-500">
261
261
  <span class="h-4 w-4 animate-spin i-hugeicons-loading-03"></span>
262
262
  Refreshing card details
263
263
  </div>
@@ -269,24 +269,24 @@ function formatTimestamp(iso: string | null | undefined): string {
269
269
  <div class="grid gap-6 grid-cols-1 md:grid-cols-3">
270
270
  <div class="md:col-span-2 space-y-6">
271
271
  <section>
272
- <h4 class="mb-2 font-medium text-zinc-500 text-xs tracking-wide uppercase">Description</h4>
272
+ <h4 class="mb-2 font-medium text-xs text-zinc-500 tracking-wide uppercase">Description</h4>
273
273
  <textarea
274
274
  x-model="descDraft"
275
275
  name="card_description"
276
276
  @blur="commitDescription()"
277
277
  rows="4"
278
278
  placeholder="Add a more detailed description"
279
- class="px-3 py-2 w-full text-zinc-800 text-sm dark:text-zinc-200 bg-white dark:bg-zinc-950 border border-zinc-200 rounded dark:border-zinc-800 focus:border-violet-400 focus:outline-none dark:focus:border-violet-600 resize-y"
279
+ class="px-3 py-2 w-full text-sm text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-950 border border-zinc-200 rounded dark:border-zinc-800 focus:border-violet-400 focus:outline-none dark:focus:border-violet-600 resize-y"
280
280
  ></textarea>
281
281
  </section>
282
282
 
283
283
  <section :if="cardAttachments().length">
284
- <h4 class="mb-2 font-medium text-zinc-500 text-xs tracking-wide uppercase">Attachments</h4>
284
+ <h4 class="mb-2 font-medium text-xs text-zinc-500 tracking-wide uppercase">Attachments</h4>
285
285
  <ul class="grid gap-3 grid-cols-2 sm:grid-cols-3">
286
286
  <li :for="attachment in cardAttachments" class="overflow-hidden bg-white dark:bg-zinc-950 border border-zinc-200 rounded dark:border-zinc-800">
287
287
  <!-- The URL is signed and expires, so it is opened in a new tab rather than linked anywhere that outlives the modal. -->
288
288
  <a :if="attachment.url" :href="attachment.url" target="_blank" rel="noopener noreferrer" class="block">
289
- <img :src="attachment.url" :alt="'Attachment ' + attachment.id" loading="lazy" class="h-28 w-full bg-zinc-50 dark:bg-zinc-900 object-cover" />
289
+ <img :src="attachment.url" :alt="'Attachment ' + attachment.id" loading="lazy" class="object-cover h-28 w-full bg-zinc-50 dark:bg-zinc-900" />
290
290
  </a>
291
291
  <!-- Listed even without a URL: a screenshot nobody can open is worth chasing, and is not the same as no screenshot. -->
292
292
  <div :else class="flex items-center justify-center h-28 bg-zinc-50 dark:bg-zinc-900">
@@ -301,7 +301,7 @@ function formatTimestamp(iso: string | null | undefined): string {
301
301
  </section>
302
302
 
303
303
  <section>
304
- <h4 class="mb-2 font-medium text-zinc-500 text-xs tracking-wide uppercase">Activity</h4>
304
+ <h4 class="mb-2 font-medium text-xs text-zinc-500 tracking-wide uppercase">Activity</h4>
305
305
  <div class="mb-4">
306
306
  <textarea
307
307
  x-model="commentDraft"
@@ -310,27 +310,27 @@ function formatTimestamp(iso: string | null | undefined): string {
310
310
  @keydown.ctrl.enter.prevent="submitComment()"
311
311
  rows="2"
312
312
  placeholder="Add a comment, then press Command+Enter to send"
313
- class="px-3 py-2 w-full text-zinc-800 text-sm dark:text-zinc-200 bg-white dark:bg-zinc-950 border border-zinc-200 rounded dark:border-zinc-800 focus:border-violet-400 focus:outline-none dark:focus:border-violet-600 resize-none"
313
+ class="px-3 py-2 w-full text-sm text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-950 border border-zinc-200 rounded dark:border-zinc-800 focus:border-violet-400 focus:outline-none dark:focus:border-violet-600 resize-none"
314
314
  ></textarea>
315
315
  <div class="flex justify-end mt-2">
316
316
  <Button size="sm" :disabled="!commentDraft().trim()" @click="submitComment()">Comment</Button>
317
317
  </div>
318
318
  </div>
319
319
 
320
- <div :if="cardComments().length === 0 && !loadingCard()" class="italic text-zinc-400 text-xs">No comments yet.</div>
320
+ <div :if="cardComments().length === 0 && !loadingCard()" class="italic text-xs text-zinc-400">No comments yet.</div>
321
321
  <ul class="space-y-3">
322
322
  <li :for="comment in cardComments" class="flex gap-3">
323
323
  <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>
324
324
  <div class="flex-1 min-w-0">
325
325
  <div class="flex flex-wrap gap-2 items-baseline">
326
- <span class="font-medium text-zinc-700 text-xs dark:text-zinc-200" :text="comment.authorName || comment.authorEmail || 'Unknown'"></span>
326
+ <span class="font-medium text-xs text-zinc-700 dark:text-zinc-200" :text="comment.authorName || comment.authorEmail || 'Unknown'"></span>
327
327
  <span class="text-[10px] text-zinc-400" :text="formatTimestamp(comment.createdAt)"></span>
328
328
  <template :if="editingCommentId() !== comment.id">
329
329
  <Button variant="ghost" size="xs" @click="startCommentEdit(comment.id, comment.body)">Edit</Button>
330
330
  <Button variant="ghost" size="xs" @click="requestDelete('comment', comment.id)"><span class="text-red-500">Delete</span></Button>
331
331
  </template>
332
332
  </div>
333
- <p :if="editingCommentId() !== comment.id" class="mt-0.5 text-zinc-700 text-xs whitespace-pre-wrap dark:text-zinc-300" :text="comment.body"></p>
333
+ <p :if="editingCommentId() !== comment.id" class="mt-0.5 text-xs text-zinc-700 whitespace-pre-wrap dark:text-zinc-300" :text="comment.body"></p>
334
334
  <div :if="editingCommentId() === comment.id" class="mt-2">
335
335
  <textarea
336
336
  x-model="commentEditDraft"
@@ -340,7 +340,7 @@ function formatTimestamp(iso: string | null | undefined): string {
340
340
  rows="3"
341
341
  maxlength="10000"
342
342
  :aria-label="'Edit comment by ' + (comment.authorName || comment.authorEmail || 'Unknown')"
343
- class="px-3 py-2 w-full text-zinc-800 text-sm dark:text-zinc-200 bg-white dark:bg-zinc-950 border border-zinc-200 rounded dark:border-zinc-800 focus:border-violet-400 focus:outline-none dark:focus:border-violet-600 resize-y"
343
+ class="px-3 py-2 w-full text-sm text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-950 border border-zinc-200 rounded dark:border-zinc-800 focus:border-violet-400 focus:outline-none dark:focus:border-violet-600 resize-y"
344
344
  ></textarea>
345
345
  <div class="flex gap-2 justify-end mt-2">
346
346
  <Button variant="secondary" size="xs" :disabled="savingCommentId() === comment.id" @click="cancelCommentEdit()">Cancel</Button>
@@ -356,7 +356,7 @@ function formatTimestamp(iso: string | null | undefined): string {
356
356
  <aside class="space-y-5">
357
357
  <section>
358
358
  <div class="flex items-center justify-between mb-2">
359
- <h4 class="font-medium text-zinc-500 text-xs tracking-wide uppercase">Labels</h4>
359
+ <h4 class="font-medium text-xs text-zinc-500 tracking-wide uppercase">Labels</h4>
360
360
  <Button variant="ghost" size="xs" interaction="toggle" :pressed="showLabelPicker()" @click="showLabelPicker.set(!showLabelPicker())">
361
361
  <span :if="!showLabelPicker()">Edit</span>
362
362
  <span :if="showLabelPicker()">Done</span>
@@ -365,7 +365,7 @@ function formatTimestamp(iso: string | null | undefined): string {
365
365
 
366
366
  <div :if="!showLabelPicker()" class="flex flex-wrap gap-1">
367
367
  <span :for="label in cardLabels" :class="`px-2 py-0.5 text-xs font-medium rounded ${labelChipClass(label.color)}`" :text="label.name"></span>
368
- <span :if="cardLabels().length === 0" class="italic text-zinc-400 text-xs">None</span>
368
+ <span :if="cardLabels().length === 0" class="italic text-xs text-zinc-400">None</span>
369
369
  </div>
370
370
 
371
371
  <div :if="showLabelPicker()" class="space-y-2">
@@ -395,7 +395,7 @@ function formatTimestamp(iso: string | null | undefined): string {
395
395
 
396
396
  <section>
397
397
  <div class="flex items-center justify-between mb-2">
398
- <h4 class="font-medium text-zinc-500 text-xs tracking-wide uppercase">Assignees</h4>
398
+ <h4 class="font-medium text-xs text-zinc-500 tracking-wide uppercase">Assignees</h4>
399
399
  <Button variant="ghost" size="xs" interaction="toggle" :pressed="showAssigneePicker()" @click="showAssigneePicker.set(!showAssigneePicker())">
400
400
  <span :if="!showAssigneePicker()">Edit</span>
401
401
  <span :if="showAssigneePicker()">Done</span>
@@ -403,15 +403,15 @@ function formatTimestamp(iso: string | null | undefined): string {
403
403
  </div>
404
404
 
405
405
  <div :if="!showAssigneePicker()" class="flex flex-wrap gap-2">
406
- <div :for="assignee in cardAssignees" class="flex gap-1.5 items-center text-zinc-700 text-xs dark:text-zinc-200">
406
+ <div :for="assignee in cardAssignees" class="flex gap-1.5 items-center text-xs text-zinc-700 dark:text-zinc-200">
407
407
  <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>
408
408
  <span :text="assignee.name || assignee.email || 'Unknown'"></span>
409
409
  </div>
410
- <span :if="cardAssignees().length === 0" class="italic text-zinc-400 text-xs">Unassigned</span>
410
+ <span :if="cardAssignees().length === 0" class="italic text-xs text-zinc-400">Unassigned</span>
411
411
  </div>
412
412
 
413
413
  <div :if="showAssigneePicker()" class="overflow-y-auto space-y-1 max-h-48">
414
- <p :if="kanban.users().length === 0" class="italic text-zinc-400 text-xs">No users to assign.</p>
414
+ <p :if="kanban.users().length === 0" class="italic text-xs text-zinc-400">No users to assign.</p>
415
415
  <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">
416
416
  <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>
417
417
  <span class="flex-1 text-zinc-700 truncate dark:text-zinc-200" :text="user.name || user.email"></span>
@@ -421,8 +421,8 @@ function formatTimestamp(iso: string | null | undefined): string {
421
421
  </section>
422
422
 
423
423
  <section>
424
- <h4 class="mb-2 font-medium text-zinc-500 text-xs tracking-wide uppercase">Due date</h4>
425
- <input type="date" x-model="dueDateDraft" name="due_date" @change="commitDueDate()" class="px-2 py-1 w-full text-zinc-800 text-xs dark:text-zinc-200 bg-white dark:bg-zinc-950 border border-zinc-200 rounded dark:border-zinc-800 focus:border-violet-400 focus:outline-none" />
424
+ <h4 class="mb-2 font-medium text-xs text-zinc-500 tracking-wide uppercase">Due date</h4>
425
+ <input type="date" x-model="dueDateDraft" name="due_date" @change="commitDueDate()" class="px-2 py-1 w-full text-xs text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-950 border border-zinc-200 rounded dark:border-zinc-800 focus:border-violet-400 focus:outline-none" />
426
426
  </section>
427
427
 
428
428
  <section class="pt-3 border-t border-zinc-200 dark:border-zinc-800">