@stacksjs/defaults 0.75.128 → 0.75.130
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.
- package/ai/skills/stacks-lint/SKILL.md +3 -2
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/resources/components/CookieConsent.stx +13 -13
- package/resources/components/Dashboard/Ci/CiDashboard.stx +1 -1
- package/resources/components/Dashboard/Ci/CiRunHistoryDrawer.stx +6 -6
- package/resources/components/Dashboard/Content/BlogDashboard.stx +3 -3
- package/resources/components/Dashboard/Content/BlogPostEditorDialog.stx +2 -2
- package/resources/components/Dashboard/Content/ContentTaxonomyDashboard.stx +2 -2
- package/resources/components/Dashboard/Content/CreateFolderDialog.stx +2 -2
- package/resources/components/Dashboard/Content/FileUploadDialog.stx +1 -1
- package/resources/components/Dashboard/Deployments/DeploymentList.stx +1 -1
- package/resources/components/Dashboard/Deployments/DeploymentPreviewDialog.stx +5 -5
- package/resources/components/Dashboard/Email/CapturedMailDashboard.stx +2 -2
- package/resources/components/Dashboard/Feedback/FeedbackForm.stx +10 -10
- package/resources/components/Dashboard/Kanban/KanbanCardDialog.stx +20 -20
- package/resources/components/Dashboard/Management/AssignmentDrawer.stx +6 -6
- package/resources/components/Dashboard/Marketing/AbandonedCartsDashboard.stx +1 -1
- package/resources/components/Dashboard/Marketing/AbandonedCartsTable.stx +1 -1
- package/resources/components/Dashboard/Operations/AuditDashboard.stx +1 -1
- package/resources/components/Dashboard/Operations/ChangeDashboard.stx +2 -2
- package/resources/components/Dashboard/Operations/IncidentDashboard.stx +1 -1
- package/resources/components/Dashboard/Operations/MigrationDashboard.stx +7 -7
- package/resources/components/Dashboard/Operations/OperationHistory.stx +2 -2
- package/resources/components/Dashboard/Operations/RecoveryDashboard.stx +3 -3
- package/resources/components/Dashboard/Operations/RecoveryDestinationDialog.stx +3 -3
- package/resources/components/Dashboard/Operations/RecoveryJobList.stx +3 -3
- package/resources/components/Dashboard/Operations/RecoveryRestoreDialog.stx +1 -1
- package/resources/components/Dashboard/Operations/SchedulerDashboard.stx +1 -1
- package/resources/components/NativeNavBar.stx +1 -1
- package/resources/views/auth/oauth/consent.stx +22 -22
- package/resources/views/cms/blocks/columns.stx +1 -1
- package/resources/views/cms/blocks/cta.stx +4 -4
- package/resources/views/cms/blocks/embed.stx +1 -1
- package/resources/views/cms/blocks/form.stx +14 -14
- package/resources/views/cms/blocks/hero.stx +3 -3
- package/resources/views/cms/blocks/image.stx +1 -1
- package/resources/views/cms/blocks/rich-text.stx +1 -1
|
@@ -32,8 +32,9 @@ selects that file. Keep the command and CI on the same policy.
|
|
|
32
32
|
|
|
33
33
|
Buddy discovers tracked files plus untracked files allowed by `.gitignore`.
|
|
34
34
|
Its extension and path predicate lives in `@stacksjs/actions/lint/files`;
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
Code-style discovery includes ts, js, stx, json, md, yaml and yml, including
|
|
36
|
+
Crosswind checks in tracked and newly created STX templates. Use Pickier directly
|
|
37
|
+
to include shell sources. Generated framework
|
|
37
38
|
assets belong to the upstream framework's checks, not application edits.
|
|
38
39
|
|
|
39
40
|
## Conventions
|
package/ide/vscode/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"publisher": "Stacks",
|
|
3
3
|
"name": "vscode-stacks",
|
|
4
4
|
"displayName": "Stacks",
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.130",
|
|
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.
|
|
5
|
+
"version": "0.75.130",
|
|
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.
|
|
58
|
+
"@stacksjs/mobile": "0.75.130",
|
|
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="
|
|
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
|
|
78
|
-
<h2 class="
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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
|
|
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
|
|
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
|
|
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
|
|
108
|
-
<div :if="loadingJobsRunId() !== run.id && jobsForRun(run.id).length === 0" class="py-1 italic text-zinc-400
|
|
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
|
|
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
|
|
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
|
|
13
|
-
<div class="flex flex-col gap-6 mx-auto
|
|
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
|
|
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
|
|
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
|
|
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="
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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]
|
|
34
|
-
<div class="grid gap-px grid-cols-2 md:grid-cols-4 bg-neutral-200 dark:bg-neutral-700 border-neutral-200
|
|
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
|
|
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
|
|
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
|
|
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="
|
|
110
|
-
<span class="mt-0.5 h-3.5 w-3.5
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
425
|
-
<input type="date" x-model="dueDateDraft" name="due_date" @change="commitDueDate()" class="px-2 py-1 w-full text-zinc-800
|
|
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">
|
|
@@ -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
|
|
65
|
-
<select id="assignment-guard" x-model="guardValue" name="guard" class="px-3 py-1.5 w-full text-zinc-900
|
|
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
|
|
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
|
|
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
|
|
82
|
-
<span class="text-zinc-500
|
|
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
|
|
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
|
|
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]
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
105
|
-
<div :for="operation in data()?.operations || []" class="flex items-start justify-between
|
|
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
|
|
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
|
|
124
|
-
<div :for="entry in data()?.ledger.entries || []" class="flex items-center justify-between
|
|
125
|
-
<span class="min-w-0
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
23
|
-
<section class="w-
|
|
24
|
-
<div class="
|
|
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
|
|
30
|
-
<header class="
|
|
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
|
|
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="
|
|
41
|
-
<dl class="grid gap-4
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
74
|
-
<i class="mt-0.5 h-5 w-5
|
|
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
|
|
90
|
-
<i class="mt-0.5 h-5 w-5
|
|
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
|
|
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="
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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="
|
|
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="
|
|
3
|
-
<h2 class="text-3xl tracking-tight
|
|
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
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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
|
|
231
|
-
<p :if="errors()._form" class="mt-3
|
|
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="
|
|
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
|
|
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]
|
|
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
|
|
240
|
-
<select :else-if="field.type === 'select'" :required="field.required" @change="values[field.name] = $event.target.value" class="mt-1.5
|
|
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
|
|
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="
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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="
|
|
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="
|
|
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-
|
|
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="
|
|
2
|
+
<div class="mx-auto px-6 py-8 max-w-3xl leading-relaxed cms-rich-text">
|
|
3
3
|
{!! props.html !!}
|
|
4
4
|
</div>
|