@prenta/admin 2.7.0 → 2.8.0

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 (163) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/design-system/tokens.css +17 -0
  3. package/design-system/tokens.json +35 -1
  4. package/design-system/tokens.scss +15 -0
  5. package/design-system/tokens.tailwind.cjs +14 -0
  6. package/dist/AdminRoot.d.ts.map +1 -1
  7. package/dist/AdminRoot.js +2 -2
  8. package/dist/AdminRoot.js.map +1 -1
  9. package/dist/__tests__/lib/batch-wizard.test.d.ts +2 -0
  10. package/dist/__tests__/lib/batch-wizard.test.d.ts.map +1 -0
  11. package/dist/__tests__/lib/batch-wizard.test.js +144 -0
  12. package/dist/__tests__/lib/batch-wizard.test.js.map +1 -0
  13. package/dist/__tests__/lib/job-drawer.test.js +2 -0
  14. package/dist/__tests__/lib/job-drawer.test.js.map +1 -1
  15. package/dist/__tests__/lib/job-view.test.js +100 -3
  16. package/dist/__tests__/lib/job-view.test.js.map +1 -1
  17. package/dist/__tests__/lib/jobs-service.test.js +56 -2
  18. package/dist/__tests__/lib/jobs-service.test.js.map +1 -1
  19. package/dist/__tests__/views/appearance-chrome.render.test.d.ts +2 -0
  20. package/dist/__tests__/views/appearance-chrome.render.test.d.ts.map +1 -0
  21. package/dist/__tests__/views/appearance-chrome.render.test.js +688 -0
  22. package/dist/__tests__/views/appearance-chrome.render.test.js.map +1 -0
  23. package/dist/__tests__/views/header-footer-form.test.d.ts +2 -0
  24. package/dist/__tests__/views/header-footer-form.test.d.ts.map +1 -0
  25. package/dist/__tests__/views/header-footer-form.test.js +266 -0
  26. package/dist/__tests__/views/header-footer-form.test.js.map +1 -0
  27. package/dist/__tests__/views/jobs-drawer.render.test.js +442 -2
  28. package/dist/__tests__/views/jobs-drawer.render.test.js.map +1 -1
  29. package/dist/__tests__/views/jobs-list.render.test.js +7 -5
  30. package/dist/__tests__/views/jobs-list.render.test.js.map +1 -1
  31. package/dist/__tests__/views/jobs-wizard.render.test.d.ts +2 -0
  32. package/dist/__tests__/views/jobs-wizard.render.test.d.ts.map +1 -0
  33. package/dist/__tests__/views/jobs-wizard.render.test.js +588 -0
  34. package/dist/__tests__/views/jobs-wizard.render.test.js.map +1 -0
  35. package/dist/lib/api.d.ts +2 -0
  36. package/dist/lib/api.d.ts.map +1 -1
  37. package/dist/lib/api.js +8 -1
  38. package/dist/lib/api.js.map +1 -1
  39. package/dist/lib/batch-wizard.d.ts +84 -0
  40. package/dist/lib/batch-wizard.d.ts.map +1 -0
  41. package/dist/lib/batch-wizard.js +122 -0
  42. package/dist/lib/batch-wizard.js.map +1 -0
  43. package/dist/lib/chrome-service.d.ts +69 -0
  44. package/dist/lib/chrome-service.d.ts.map +1 -0
  45. package/dist/lib/chrome-service.js +108 -0
  46. package/dist/lib/chrome-service.js.map +1 -0
  47. package/dist/lib/job-view.d.ts +43 -0
  48. package/dist/lib/job-view.d.ts.map +1 -1
  49. package/dist/lib/job-view.js +57 -1
  50. package/dist/lib/job-view.js.map +1 -1
  51. package/dist/lib/jobs-service.d.ts +19 -1
  52. package/dist/lib/jobs-service.d.ts.map +1 -1
  53. package/dist/lib/jobs-service.js +41 -0
  54. package/dist/lib/jobs-service.js.map +1 -1
  55. package/dist/prenta-admin.css +1 -1
  56. package/dist/views/AppearanceChrome.d.ts +22 -4
  57. package/dist/views/AppearanceChrome.d.ts.map +1 -1
  58. package/dist/views/AppearanceChrome.js +451 -15
  59. package/dist/views/AppearanceChrome.js.map +1 -1
  60. package/dist/views/Jobs/CancelJobDialog.d.ts +7 -1
  61. package/dist/views/Jobs/CancelJobDialog.d.ts.map +1 -1
  62. package/dist/views/Jobs/CancelJobDialog.js +13 -3
  63. package/dist/views/Jobs/CancelJobDialog.js.map +1 -1
  64. package/dist/views/Jobs/DrawerReview.d.ts +7 -0
  65. package/dist/views/Jobs/DrawerReview.d.ts.map +1 -1
  66. package/dist/views/Jobs/DrawerReview.js +1 -1
  67. package/dist/views/Jobs/DrawerReview.js.map +1 -1
  68. package/dist/views/Jobs/JobDrawer.d.ts +23 -3
  69. package/dist/views/Jobs/JobDrawer.d.ts.map +1 -1
  70. package/dist/views/Jobs/JobDrawer.js +30 -5
  71. package/dist/views/Jobs/JobDrawer.js.map +1 -1
  72. package/dist/views/Jobs/JobsTable.d.ts +4 -2
  73. package/dist/views/Jobs/JobsTable.d.ts.map +1 -1
  74. package/dist/views/Jobs/JobsTable.js +6 -5
  75. package/dist/views/Jobs/JobsTable.js.map +1 -1
  76. package/dist/views/Jobs/NewBatchJob.d.ts +34 -0
  77. package/dist/views/Jobs/NewBatchJob.d.ts.map +1 -0
  78. package/dist/views/Jobs/NewBatchJob.js +282 -0
  79. package/dist/views/Jobs/NewBatchJob.js.map +1 -0
  80. package/dist/views/Jobs/WorkerStrip.d.ts +3 -1
  81. package/dist/views/Jobs/WorkerStrip.d.ts.map +1 -1
  82. package/dist/views/Jobs/WorkerStrip.js +2 -2
  83. package/dist/views/Jobs/WorkerStrip.js.map +1 -1
  84. package/dist/views/Jobs/focus.d.ts +6 -0
  85. package/dist/views/Jobs/focus.d.ts.map +1 -1
  86. package/dist/views/Jobs/focus.js +10 -0
  87. package/dist/views/Jobs/focus.js.map +1 -1
  88. package/dist/views/Jobs.d.ts +12 -3
  89. package/dist/views/Jobs.d.ts.map +1 -1
  90. package/dist/views/Jobs.js +109 -31
  91. package/dist/views/Jobs.js.map +1 -1
  92. package/dist/views/header-footer/AnnouncementTab.d.ts +15 -0
  93. package/dist/views/header-footer/AnnouncementTab.d.ts.map +1 -0
  94. package/dist/views/header-footer/AnnouncementTab.js +43 -0
  95. package/dist/views/header-footer/AnnouncementTab.js.map +1 -0
  96. package/dist/views/header-footer/ChromeField.d.ts +25 -0
  97. package/dist/views/header-footer/ChromeField.d.ts.map +1 -0
  98. package/dist/views/header-footer/ChromeField.js +99 -0
  99. package/dist/views/header-footer/ChromeField.js.map +1 -0
  100. package/dist/views/header-footer/ChromePreview.d.ts +31 -0
  101. package/dist/views/header-footer/ChromePreview.d.ts.map +1 -0
  102. package/dist/views/header-footer/ChromePreview.js +195 -0
  103. package/dist/views/header-footer/ChromePreview.js.map +1 -0
  104. package/dist/views/header-footer/DeveloperTab.d.ts +17 -0
  105. package/dist/views/header-footer/DeveloperTab.d.ts.map +1 -0
  106. package/dist/views/header-footer/DeveloperTab.js +98 -0
  107. package/dist/views/header-footer/DeveloperTab.js.map +1 -0
  108. package/dist/views/header-footer/RegionTab.d.ts +27 -0
  109. package/dist/views/header-footer/RegionTab.d.ts.map +1 -0
  110. package/dist/views/header-footer/RegionTab.js +80 -0
  111. package/dist/views/header-footer/RegionTab.js.map +1 -0
  112. package/dist/views/header-footer/chrome-form.d.ts +137 -0
  113. package/dist/views/header-footer/chrome-form.d.ts.map +1 -0
  114. package/dist/views/header-footer/chrome-form.js +518 -0
  115. package/dist/views/header-footer/chrome-form.js.map +1 -0
  116. package/dist/views/header-footer/code-samples.d.ts +30 -0
  117. package/dist/views/header-footer/code-samples.d.ts.map +1 -0
  118. package/dist/views/header-footer/code-samples.js +127 -0
  119. package/dist/views/header-footer/code-samples.js.map +1 -0
  120. package/dist/views/header-footer/controls.d.ts +57 -0
  121. package/dist/views/header-footer/controls.d.ts.map +1 -0
  122. package/dist/views/header-footer/controls.js +43 -0
  123. package/dist/views/header-footer/controls.js.map +1 -0
  124. package/package.json +3 -3
  125. package/src/AdminRoot.tsx +16 -2
  126. package/src/__tests__/lib/batch-wizard.test.ts +199 -0
  127. package/src/__tests__/lib/job-drawer.test.ts +2 -0
  128. package/src/__tests__/lib/job-view.test.ts +123 -2
  129. package/src/__tests__/lib/jobs-service.test.ts +72 -3
  130. package/src/__tests__/views/appearance-chrome.render.test.tsx +818 -0
  131. package/src/__tests__/views/header-footer-form.test.ts +333 -0
  132. package/src/__tests__/views/jobs-drawer.render.test.tsx +508 -2
  133. package/src/__tests__/views/jobs-list.render.test.tsx +8 -5
  134. package/src/__tests__/views/jobs-wizard.render.test.tsx +707 -0
  135. package/src/lib/api.ts +12 -1
  136. package/src/lib/batch-wizard.ts +188 -0
  137. package/src/lib/chrome-service.ts +200 -0
  138. package/src/lib/job-view.ts +95 -1
  139. package/src/lib/jobs-service.ts +56 -1
  140. package/src/styles/prenta-tokens.css +17 -0
  141. package/src/styles/theme.css +15 -0
  142. package/src/views/AppearanceChrome.tsx +704 -46
  143. package/src/views/Jobs/CancelJobDialog.tsx +20 -2
  144. package/src/views/Jobs/DrawerReview.tsx +1 -1
  145. package/src/views/Jobs/JobDrawer.tsx +83 -3
  146. package/src/views/Jobs/JobsTable.tsx +11 -4
  147. package/src/views/Jobs/NewBatchJob.tsx +857 -0
  148. package/src/views/Jobs/WorkerStrip.tsx +16 -11
  149. package/src/views/Jobs/focus.ts +11 -0
  150. package/src/views/Jobs.tsx +163 -49
  151. package/src/views/header-footer/AnnouncementTab.tsx +231 -0
  152. package/src/views/header-footer/ChromeField.tsx +358 -0
  153. package/src/views/header-footer/ChromePreview.tsx +497 -0
  154. package/src/views/header-footer/DeveloperTab.tsx +331 -0
  155. package/src/views/header-footer/RegionTab.tsx +398 -0
  156. package/src/views/header-footer/chrome-form.ts +603 -0
  157. package/src/views/header-footer/code-samples.ts +146 -0
  158. package/src/views/header-footer/controls.tsx +166 -0
  159. package/dist/views/Jobs/placeholders.d.ts +0 -6
  160. package/dist/views/Jobs/placeholders.d.ts.map +0 -1
  161. package/dist/views/Jobs/placeholders.js +0 -20
  162. package/dist/views/Jobs/placeholders.js.map +0 -1
  163. package/src/views/Jobs/placeholders.tsx +0 -66
@@ -0,0 +1,331 @@
1
+ 'use client'
2
+
3
+ import { useId, useRef, useState, type KeyboardEvent } from 'react'
4
+ import { toast } from 'sonner'
5
+ import type { ChromeSyncStatus } from '@prenta/core'
6
+
7
+ import {
8
+ CODE_SAMPLES,
9
+ LOCAL_DEV_SAMPLE,
10
+ highlight,
11
+ type CodeSampleId,
12
+ type CodeTokenKind,
13
+ } from './code-samples.js'
14
+ import { shortCommit, timeAgo } from './chrome-form.js'
15
+ import { CARD, CardHead, FieldLabel, Help, INPUT, KeyChip, MONO } from './controls.js'
16
+
17
+ const TOKEN_CLASS: Record<CodeTokenKind, string | undefined> = {
18
+ comment: 'text-code-cmt',
19
+ string: 'text-code-str',
20
+ keyword: 'text-code-kw',
21
+ plain: undefined,
22
+ }
23
+
24
+ /** `.hf-code`: the dark code block, 12px DM Mono at 1.65. */
25
+ const CODE_BLOCK =
26
+ 'bg-code-bg text-code-txt relative m-0 overflow-auto rounded-b-12 px-4 py-3.5 font-mono text-[12px] leading-[1.65] whitespace-pre'
27
+
28
+ function Highlighted({ src }: { src: string }) {
29
+ return (
30
+ <>
31
+ {highlight(src).map((token, index) =>
32
+ TOKEN_CLASS[token.kind] ? (
33
+ <span key={index} className={TOKEN_CLASS[token.kind]}>
34
+ {token.text}
35
+ </span>
36
+ ) : (
37
+ token.text
38
+ ),
39
+ )}
40
+ </>
41
+ )
42
+ }
43
+
44
+ /** Put text on the clipboard and say so. */
45
+ async function copy(text: string, done = 'Copied') {
46
+ try {
47
+ if (!navigator.clipboard?.writeText) throw new Error('No clipboard')
48
+ await navigator.clipboard.writeText(text)
49
+ toast(done)
50
+ } catch {
51
+ toast.error('Couldn’t copy. Select the text and copy it instead.')
52
+ }
53
+ }
54
+
55
+ function HowItsWired() {
56
+ const id = useId()
57
+ const [current, setCurrent] = useState<CodeSampleId>('config')
58
+ const refs = useRef<Array<HTMLButtonElement | null>>([])
59
+ const index = Math.max(
60
+ 0,
61
+ CODE_SAMPLES.findIndex((sample) => sample.id === current),
62
+ )
63
+ const sample = CODE_SAMPLES[index]!
64
+
65
+ function onKeyDown(event: KeyboardEvent<HTMLDivElement>) {
66
+ const last = CODE_SAMPLES.length - 1
67
+ let next: number
68
+ if (event.key === 'ArrowRight') next = index === last ? 0 : index + 1
69
+ else if (event.key === 'ArrowLeft') next = index === 0 ? last : index - 1
70
+ else if (event.key === 'Home') next = 0
71
+ else if (event.key === 'End') next = last
72
+ else return
73
+ event.preventDefault()
74
+ setCurrent(CODE_SAMPLES[next]!.id)
75
+ refs.current[next]?.focus()
76
+ }
77
+
78
+ return (
79
+ <section aria-labelledby={`${id}-title`} className={`${CARD} overflow-hidden`}>
80
+ <CardHead id={`${id}-title`} title="How it’s wired">
81
+ <div
82
+ role="tablist"
83
+ aria-label="Code sample"
84
+ onKeyDown={onKeyDown}
85
+ className="rounded-8 border-bdr bg-bg ml-auto flex gap-0.5 border p-0.5"
86
+ >
87
+ {CODE_SAMPLES.map((item, i) => {
88
+ const on = item.id === current
89
+ return (
90
+ <button
91
+ key={item.id}
92
+ ref={(el) => {
93
+ refs.current[i] = el
94
+ }}
95
+ id={`${id}-tab-${item.id}`}
96
+ type="button"
97
+ role="tab"
98
+ aria-selected={on}
99
+ aria-controls={`${id}-panel`}
100
+ tabIndex={on ? 0 : -1}
101
+ onClick={() => setCurrent(item.id)}
102
+ className={`rounded-6 cursor-pointer border-0 px-[9px] py-[3px] text-[12px] leading-[normal] font-semibold ${
103
+ on ? 'bg-card text-txt shadow-shd' : 'text-sub bg-transparent'
104
+ }`}
105
+ >
106
+ {item.label}
107
+ </button>
108
+ )
109
+ })}
110
+ </div>
111
+ </CardHead>
112
+ <pre
113
+ id={`${id}-panel`}
114
+ role="tabpanel"
115
+ aria-labelledby={`${id}-tab-${sample.id}`}
116
+ // Scrolls, so it takes focus for keyboard scrolling.
117
+ tabIndex={0}
118
+ className={`${CODE_BLOCK} max-h-[520px]`}
119
+ >
120
+ <button
121
+ type="button"
122
+ onClick={() => void copy(sample.src)}
123
+ aria-label={`Copy ${sample.file}`}
124
+ className="rounded-6 border-code-btn-bdr bg-code-btn-bg text-code-btn-txt absolute top-2.5 right-2.5 cursor-pointer border px-2 py-[3px] font-mono text-[11px] leading-[1.65] font-medium"
125
+ >
126
+ Copy
127
+ </button>
128
+ <span className="text-code-file mb-2 block text-[11px]">{sample.file}</span>
129
+ <Highlighted src={sample.src} />
130
+ </pre>
131
+ </section>
132
+ )
133
+ }
134
+
135
+ function SchemaCard({ sync, now }: { sync: ChromeSyncStatus | null; now: number }) {
136
+ const id = useId()
137
+ const synced = sync?.status === 'synced' && sync.source
138
+ const source = sync?.source ?? null
139
+ const regions = [...(sync?.regions ?? []), 'announcement'].join(' · ')
140
+ const rows: Array<[string, string, boolean]> = [
141
+ [
142
+ 'Source',
143
+ source ? `${source.repo || 'unknown repo'}@${source.branch || 'unknown'}` : '—',
144
+ true,
145
+ ],
146
+ [
147
+ 'Last read',
148
+ source
149
+ ? `${shortCommit(source.commit) || 'unknown'} · ${timeAgo(source.readAt, now)}`
150
+ : 'never',
151
+ true,
152
+ ],
153
+ ['Regions', regions, false],
154
+ ['Fields', String(sync?.fieldCount ?? 0), false],
155
+ ]
156
+ return (
157
+ <section aria-labelledby={`${id}-title`} className={CARD}>
158
+ <CardHead id={`${id}-title`} title="Schema">
159
+ {synced ? (
160
+ <span className="rounded-pill bg-suc-l text-suc inline-flex items-center gap-[5px] px-2 py-px text-[11px] leading-[normal] font-bold">
161
+ ● In sync
162
+ </span>
163
+ ) : (
164
+ <span className="rounded-pill bg-warn-l text-warn inline-flex items-center gap-[5px] px-2 py-px text-[11px] leading-[normal] font-bold">
165
+ ● Not read yet
166
+ </span>
167
+ )}
168
+ </CardHead>
169
+ <dl className="m-0 flex flex-col">
170
+ {rows.map(([label, value, mono]) => (
171
+ <div
172
+ key={label}
173
+ className="border-bdr text-sm2 text-sub flex items-center gap-2.5 border-t px-4 py-[9px] leading-[normal] first:border-t-0"
174
+ >
175
+ <dt>{label}</dt>
176
+ <dd
177
+ className={`text-txt m-0 ml-auto min-w-0 truncate text-right font-semibold ${
178
+ mono ? 'font-mono text-[11.5px]' : ''
179
+ }`}
180
+ >
181
+ {value}
182
+ </dd>
183
+ </div>
184
+ ))}
185
+ </dl>
186
+ <div className="text-hint text-faint px-4 pb-3 leading-[1.45]">
187
+ {synced ? null : (
188
+ <>
189
+ Add <KeyChip>prenta push chrome</KeyChip> to your deploy to send the header and
190
+ footer.{' '}
191
+ </>
192
+ )}
193
+ Read on every deploy. Removed fields keep their values for 30 days; renamed fields need{' '}
194
+ <KeyChip>renamedFrom</KeyChip>.
195
+ </div>
196
+ </section>
197
+ )
198
+ }
199
+
200
+ export interface ChromeSettingsForm {
201
+ revalidateUrl: string
202
+ previewUrl: string
203
+ }
204
+
205
+ function OnSaveCard({
206
+ form,
207
+ onChange,
208
+ canEdit,
209
+ secret,
210
+ problem,
211
+ }: {
212
+ form: ChromeSettingsForm
213
+ onChange: (form: ChromeSettingsForm) => void
214
+ /** Only admins see the revalidate URL and can change these. */
215
+ canEdit: boolean
216
+ /** Shown once, after a save creates the revalidate webhook. */
217
+ secret: string | null
218
+ problem: string | null
219
+ }) {
220
+ const id = useId()
221
+ return (
222
+ <section aria-labelledby={`${id}-title`} className={CARD}>
223
+ <CardHead id={`${id}-title`} title="On save" />
224
+ <div className="flex flex-col gap-3.5 px-4 py-3.5">
225
+ <div>
226
+ <FieldLabel htmlFor={`${id}-revalidate`}>Revalidate URL</FieldLabel>
227
+ <input
228
+ id={`${id}-revalidate`}
229
+ className={`${INPUT} ${MONO}`}
230
+ value={form.revalidateUrl}
231
+ placeholder={canEdit ? 'https://' : 'Only admins can see this'}
232
+ disabled={!canEdit}
233
+ spellCheck={false}
234
+ onChange={(event) => onChange({ ...form, revalidateUrl: event.target.value })}
235
+ />
236
+ <Help>
237
+ POSTs <KeyChip>chrome.updated</KeyChip> with the changed region. Also fires when a
238
+ scheduled announcement starts or ends.
239
+ </Help>
240
+ {secret ? (
241
+ <div
242
+ role="status"
243
+ className="rounded-9 border-bdr bg-bg text-hint text-sub mt-2 flex flex-wrap items-center gap-2 border px-3 py-2 leading-[1.45]"
244
+ >
245
+ <span className="min-w-0 flex-1">
246
+ Signing secret <KeyChip>{secret}</KeyChip>. Copy it now; it won’t be shown again.
247
+ </span>
248
+ <button
249
+ type="button"
250
+ onClick={() => void copy(secret, 'Secret copied')}
251
+ className="rounded-8 border-bdr bg-card text-meta text-txt hover:border-acc-m hover:text-acc inline-flex h-7 cursor-pointer items-center border px-[9px] font-[550]"
252
+ >
253
+ Copy
254
+ </button>
255
+ </div>
256
+ ) : null}
257
+ </div>
258
+ <div>
259
+ <FieldLabel htmlFor={`${id}-preview`}>Preview URL</FieldLabel>
260
+ <input
261
+ id={`${id}-preview`}
262
+ className={`${INPUT} ${MONO}`}
263
+ value={form.previewUrl}
264
+ placeholder={canEdit ? 'https://' : undefined}
265
+ disabled={!canEdit}
266
+ spellCheck={false}
267
+ onChange={(event) => onChange({ ...form, previewUrl: event.target.value })}
268
+ />
269
+ <Help>
270
+ Renders your real components with draft values. Leave blank to use the built-in mock.
271
+ </Help>
272
+ </div>
273
+ {problem ? (
274
+ <Help tone="error">
275
+ <span role="alert">{problem}</span>
276
+ </Help>
277
+ ) : null}
278
+ </div>
279
+ </section>
280
+ )
281
+ }
282
+
283
+ function LocalDevCard() {
284
+ const id = useId()
285
+ return (
286
+ <section aria-labelledby={`${id}-title`} className={`${CARD} overflow-hidden`}>
287
+ <CardHead id={`${id}-title`} title="Local dev" />
288
+ <pre tabIndex={0} aria-label="Local dev commands" className={CODE_BLOCK}>
289
+ <Highlighted src={LOCAL_DEV_SAMPLE} />
290
+ </pre>
291
+ </section>
292
+ )
293
+ }
294
+
295
+ export interface DeveloperTabProps {
296
+ sync: ChromeSyncStatus | null
297
+ settings: ChromeSettingsForm
298
+ onSettingsChange: (form: ChromeSettingsForm) => void
299
+ canEditSettings: boolean
300
+ secret: string | null
301
+ settingsProblem: string | null
302
+ now: number
303
+ }
304
+
305
+ /** The Developer tab: the code samples, the live schema status, the On save settings and local dev. */
306
+ export function DeveloperTab({
307
+ sync,
308
+ settings,
309
+ onSettingsChange,
310
+ canEditSettings,
311
+ secret,
312
+ settingsProblem,
313
+ now,
314
+ }: DeveloperTabProps) {
315
+ return (
316
+ <div className="grid grid-cols-[minmax(0,1fr)_minmax(0,340px)] items-start gap-[18px] [@media(width<=1100px)]:grid-cols-[minmax(0,1fr)]">
317
+ <HowItsWired />
318
+ <div className="flex min-w-0 flex-col gap-3.5">
319
+ <SchemaCard sync={sync} now={now} />
320
+ <OnSaveCard
321
+ form={settings}
322
+ onChange={onSettingsChange}
323
+ canEdit={canEditSettings}
324
+ secret={secret}
325
+ problem={settingsProblem}
326
+ />
327
+ <LocalDevCard />
328
+ </div>
329
+ </div>
330
+ )
331
+ }