@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,398 @@
1
+ 'use client'
2
+
3
+ import { AlertTriangle, Code } from 'lucide-react'
4
+ import { useRef, type KeyboardEvent, type MouseEvent } from 'react'
5
+ import type {
6
+ ChromeOrphan,
7
+ ChromeOverridesList,
8
+ ChromeRegionName,
9
+ RegionSchema,
10
+ } from '@prenta/core'
11
+
12
+ import { Button } from '../../components/ui/Button.js'
13
+ import type { ChromeMenuOption } from '../../lib/chrome-service.js'
14
+ import { ChromeField } from './ChromeField.js'
15
+ import {
16
+ componentPath,
17
+ fieldEntries,
18
+ overrideChips,
19
+ repoFileUrl,
20
+ shortCommit,
21
+ timeAgo,
22
+ type RegionForm,
23
+ } from './chrome-form.js'
24
+ import { CARD, CardHead, KeyChip, LockPill } from './controls.js'
25
+
26
+ const ORPHAN_TTL_MS = 30 * 86_400_000
27
+
28
+ /** The kit's variant thumbnails, by variant id; other ids draw the default. */
29
+ function VariantThumb({ region, id }: { region: ChromeRegionName; id: string }) {
30
+ const bar = 'block h-[5px] rounded-[3px] bg-bdr'
31
+ const logo = 'block h-[9px] w-4 rounded-[2px] bg-sub'
32
+ const cta = 'block h-[9px] w-3.5 rounded-[2px] bg-acc'
33
+ if (region === 'footer') {
34
+ const blocks = id === 'minimal' ? 0 : id === 'compact' ? 2 : 4
35
+ return (
36
+ <span
37
+ aria-hidden
38
+ className="rounded-6 bg-site-thumb flex h-[30px] items-center gap-1 px-[7px]"
39
+ >
40
+ {blocks === 0 ? (
41
+ <i className="bg-site-thumb-bar block h-[5px] w-full rounded-[3px]" />
42
+ ) : (
43
+ Array.from({ length: blocks }, (_, index) => (
44
+ <i key={index} className="bg-site-thumb-bar block h-3.5 flex-1 rounded-[3px]" />
45
+ ))
46
+ )}
47
+ </span>
48
+ )
49
+ }
50
+ const centered = /center/.test(id)
51
+ const minimal = id === 'minimal'
52
+ return (
53
+ <span
54
+ aria-hidden
55
+ className={`rounded-6 bg-bg flex h-[30px] items-center gap-1 px-[7px] ${
56
+ centered ? 'justify-center' : 'justify-between'
57
+ }`}
58
+ >
59
+ <i className={logo} />
60
+ {centered ? null : minimal ? (
61
+ <i className={cta} />
62
+ ) : (
63
+ <span className="flex gap-[3px]">
64
+ <i className={`${bar} w-2.5`} />
65
+ <i className={`${bar} w-2.5`} />
66
+ <i className={cta} />
67
+ </span>
68
+ )}
69
+ </span>
70
+ )
71
+ }
72
+
73
+ function VariantCards({
74
+ region,
75
+ schema,
76
+ value,
77
+ onChange,
78
+ disabled,
79
+ }: {
80
+ region: ChromeRegionName
81
+ schema: RegionSchema
82
+ value: string
83
+ onChange: (variant: string) => void
84
+ disabled: boolean
85
+ }) {
86
+ const ids = Object.keys(schema.variants).filter((id) => Object.hasOwn(schema.variants, id))
87
+ const refs = useRef<Array<HTMLButtonElement | null>>([])
88
+ const selected = Math.max(0, ids.indexOf(value))
89
+
90
+ function onKeyDown(event: KeyboardEvent<HTMLDivElement>) {
91
+ const last = ids.length - 1
92
+ let next: number
93
+ if (event.key === 'ArrowRight' || event.key === 'ArrowDown')
94
+ next = selected === last ? 0 : selected + 1
95
+ else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp')
96
+ next = selected === 0 ? last : selected - 1
97
+ else return
98
+ event.preventDefault()
99
+ if (disabled) return
100
+ onChange(ids[next]!)
101
+ refs.current[next]?.focus()
102
+ }
103
+
104
+ return (
105
+ <div
106
+ role="radiogroup"
107
+ aria-label={`${region} variant`}
108
+ onKeyDown={onKeyDown}
109
+ className="grid grid-cols-3 gap-2 [@media(width<=560px)]:grid-cols-2"
110
+ >
111
+ {ids.map((id, index) => {
112
+ const on = id === value
113
+ return (
114
+ <button
115
+ key={id}
116
+ ref={(el) => {
117
+ refs.current[index] = el
118
+ }}
119
+ type="button"
120
+ role="radio"
121
+ aria-checked={on}
122
+ tabIndex={index === selected ? 0 : -1}
123
+ disabled={disabled}
124
+ onClick={() => onChange(id)}
125
+ className={`rounded-10 bg-card flex min-w-0 cursor-pointer flex-col gap-1.5 border p-2 text-left leading-[normal] disabled:cursor-not-allowed ${
126
+ on ? 'border-acc shadow-[0_0_0_3px_var(--acc-l)]' : 'border-bdr'
127
+ }`}
128
+ >
129
+ <VariantThumb region={region} id={id} />
130
+ <b className="text-txt text-[12px] leading-[normal] font-semibold">
131
+ {schema.variants[id]}
132
+ </b>
133
+ <KeyChip bare size={10.5}>
134
+ {id}
135
+ </KeyChip>
136
+ </button>
137
+ )
138
+ })}
139
+ </div>
140
+ )
141
+ }
142
+
143
+ function OrphanRow({
144
+ region,
145
+ orphans,
146
+ now,
147
+ removing,
148
+ disabled,
149
+ onRemove,
150
+ }: {
151
+ region: ChromeRegionName
152
+ orphans: ChromeOrphan[]
153
+ now: number
154
+ removing: boolean
155
+ disabled: boolean
156
+ onRemove: () => void
157
+ }) {
158
+ const expires = Math.min(...orphans.map((o) => Date.parse(o.orphanedAt) + ORPHAN_TTL_MS))
159
+ const days = Math.max(0, Math.ceil((expires - now) / 86_400_000))
160
+ const when = days <= 0 ? 'today' : `in ${days} ${days === 1 ? 'day' : 'days'}`
161
+ const one = orphans.length === 1
162
+ return (
163
+ <div className="border-bdr bg-bg text-sub flex items-start gap-2.5 border-t px-4 py-2.5 text-[12px] leading-[normal]">
164
+ <span className="text-warn mt-px shrink-0">
165
+ <AlertTriangle size={14} strokeWidth={1.8} aria-hidden />
166
+ </span>
167
+ <span className="min-w-0">
168
+ <b className="text-txt font-semibold">
169
+ {one
170
+ ? '1 saved value isn’t in the schema anymore:'
171
+ : `${orphans.length} saved values aren’t in the schema anymore:`}
172
+ </b>{' '}
173
+ {orphans.map((orphan, index) => (
174
+ <span key={orphan.key}>
175
+ {index > 0 ? ', ' : null}
176
+ <KeyChip>{`${region}.${orphan.key}`}</KeyChip>
177
+ </span>
178
+ ))}
179
+ .{' '}
180
+ {one
181
+ ? `It’s no longer sent to the site and will be removed ${when}.`
182
+ : `They’re no longer sent to the site and will be removed ${when}.`}
183
+ </span>
184
+ <Button
185
+ variant="ghost"
186
+ size="sm"
187
+ className="ml-auto"
188
+ loading={removing}
189
+ disabled={disabled}
190
+ onClick={onRemove}
191
+ >
192
+ Remove now
193
+ </Button>
194
+ </div>
195
+ )
196
+ }
197
+
198
+ export interface RegionTabProps {
199
+ region: ChromeRegionName
200
+ schema: RegionSchema
201
+ form: RegionForm
202
+ onChange: (form: RegionForm) => void
203
+ problems: Record<string, string>
204
+ orphans: ChromeOrphan[]
205
+ removingOrphans: boolean
206
+ onRemoveOrphans: () => void
207
+ overrides: ChromeOverridesList | 'unavailable' | 'error'
208
+ menus: ChromeMenuOption[]
209
+ siteTitle: string
210
+ readOnly: boolean
211
+ /** A save is out: Remove now waits, so the save's If-Match still holds. */
212
+ saving: boolean
213
+ now: number
214
+ href: (path: string) => string
215
+ onNavigate: (path: string) => void
216
+ /** The admin route that edits a page, by collection and id. */
217
+ editPath: (collection: string, id: string) => string
218
+ }
219
+
220
+ /** One region's cards, top to bottom: Source, Variant, Fields (with orphans), Page overrides. */
221
+ export function RegionTab({
222
+ region,
223
+ schema,
224
+ form,
225
+ onChange,
226
+ problems,
227
+ orphans,
228
+ removingOrphans,
229
+ onRemoveOrphans,
230
+ overrides,
231
+ menus,
232
+ siteTitle,
233
+ readOnly,
234
+ saving,
235
+ now,
236
+ href,
237
+ onNavigate,
238
+ editPath,
239
+ }: RegionTabProps) {
240
+ const { source } = schema
241
+ const repoUrl = repoFileUrl(source, schema.component)
242
+ const fields = fieldEntries(schema)
243
+ const pages =
244
+ typeof overrides === 'object'
245
+ ? overrides.pages.filter((page) => page[region] !== undefined)
246
+ : []
247
+ const brandPath = '/settings?tab=general'
248
+
249
+ function go(event: MouseEvent<HTMLAnchorElement>, path: string) {
250
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0) return
251
+ event.preventDefault()
252
+ onNavigate(path)
253
+ }
254
+
255
+ return (
256
+ <>
257
+ <section aria-label="Source" className={CARD}>
258
+ <div className="flex flex-wrap items-center gap-3 px-4 py-3">
259
+ <Code size={15} strokeWidth={1.8} aria-hidden className="text-txt shrink-0" />
260
+ {/* The kit sets the path on its 16px body line: 20px tall. */}
261
+ <div className="min-w-0 leading-5">
262
+ <code className="text-txt font-mono text-[12.5px] font-medium break-all">
263
+ {componentPath(schema.component)}
264
+ </code>
265
+ <div className="text-hint text-faint flex flex-wrap items-center gap-2 leading-[normal]">
266
+ <i aria-hidden className="bg-suc block h-[7px] w-[7px] rounded-full" />
267
+ <span>{`Schema in sync · ${source.repo || 'unknown repo'}@${shortCommit(source.commit) || 'unknown'} · ${timeAgo(source.readAt, now)}`}</span>
268
+ </div>
269
+ </div>
270
+ {repoUrl ? (
271
+ <a
272
+ href={repoUrl}
273
+ target="_blank"
274
+ rel="noopener noreferrer"
275
+ className="text-sm2 text-acc ml-auto font-semibold whitespace-nowrap no-underline"
276
+ >
277
+ View in repo ↗
278
+ </a>
279
+ ) : null}
280
+ </div>
281
+ </section>
282
+
283
+ <section aria-labelledby={`hf-${region}-variant`} className={CARD}>
284
+ <CardHead
285
+ id={`hf-${region}-variant`}
286
+ title="Variant"
287
+ description="Site default. Pages can pick a different one."
288
+ >
289
+ <LockPill>Defined in code</LockPill>
290
+ </CardHead>
291
+ <div className="flex flex-col gap-3.5 px-4 py-3.5">
292
+ <VariantCards
293
+ region={region}
294
+ schema={schema}
295
+ value={form.variant}
296
+ disabled={readOnly}
297
+ onChange={(variant) => onChange({ ...form, variant })}
298
+ />
299
+ </div>
300
+ </section>
301
+
302
+ <section aria-labelledby={`hf-${region}-fields`} className={CARD}>
303
+ <CardHead
304
+ id={`hf-${region}-fields`}
305
+ title="Fields"
306
+ description="Your developer chose what’s editable here."
307
+ />
308
+ <div className="flex flex-col gap-3.5 px-4 py-3.5">
309
+ {fields.length === 0 ? (
310
+ <p className="text-meta text-sub m-0">This region has no editable fields.</p>
311
+ ) : (
312
+ fields.map(([key, def]) => (
313
+ <ChromeField
314
+ key={key}
315
+ region={region}
316
+ fieldKey={key}
317
+ def={def}
318
+ value={form.fields[key]}
319
+ menus={menus}
320
+ siteTitle={siteTitle}
321
+ disabled={readOnly}
322
+ problem={problems[key]}
323
+ brandHref={href(brandPath)}
324
+ onOpenBrand={() => onNavigate(brandPath)}
325
+ onChange={(value) =>
326
+ onChange({ ...form, fields: { ...form.fields, [key]: value } })
327
+ }
328
+ />
329
+ ))
330
+ )}
331
+ </div>
332
+ {orphans.length > 0 ? (
333
+ <OrphanRow
334
+ region={region}
335
+ orphans={orphans}
336
+ now={now}
337
+ removing={removingOrphans}
338
+ disabled={readOnly || saving}
339
+ onRemove={onRemoveOrphans}
340
+ />
341
+ ) : null}
342
+ </section>
343
+
344
+ <section aria-labelledby={`hf-${region}-overrides`} className={CARD}>
345
+ <CardHead
346
+ id={`hf-${region}-overrides`}
347
+ title="Page overrides"
348
+ description="Set in each page’s settings. Child pages inherit."
349
+ />
350
+ <div className="flex flex-col">
351
+ {overrides === 'error' ? (
352
+ <p className="text-meta text-faint m-0 px-4 py-2.5" role="alert">
353
+ Couldn’t load the pages that override the {region}.
354
+ </p>
355
+ ) : overrides === 'unavailable' ? (
356
+ <p className="text-meta text-faint m-0 px-4 py-2.5">
357
+ Update the CMS to list the pages that override the {region}.
358
+ </p>
359
+ ) : pages.length === 0 ? (
360
+ <p className="text-meta text-faint m-0 px-4 py-2.5">
361
+ No page overrides the {region}. Every page uses the site default.
362
+ </p>
363
+ ) : (
364
+ pages.map((page) => {
365
+ const path = editPath(page.collection, page.id)
366
+ return (
367
+ <a
368
+ key={page.id}
369
+ href={href(path)}
370
+ onClick={(event) => go(event, path)}
371
+ className="border-bdr text-sm2 text-txt hover:bg-bg flex items-center gap-2.5 border-t px-4 py-[9px] no-underline first:border-t-0"
372
+ >
373
+ <span className="min-w-0 truncate">{page.title || page.path}</span>
374
+ {overrideChips(page, region).map((chip) => (
375
+ <span
376
+ key={chip}
377
+ className="rounded-5 border-bdr bg-bg text-sub border px-1.5 font-mono text-[11px] font-medium whitespace-nowrap"
378
+ >
379
+ {chip}
380
+ </span>
381
+ ))}
382
+ <code className="text-faint ml-auto font-mono text-[11.5px] whitespace-nowrap">
383
+ {page.path}
384
+ </code>
385
+ </a>
386
+ )
387
+ })
388
+ )}
389
+ {typeof overrides === 'object' && overrides.truncated ? (
390
+ <p className="border-bdr text-hint text-faint m-0 border-t px-4 py-2">
391
+ Showing the 200 most recently changed pages with an override.
392
+ </p>
393
+ ) : null}
394
+ </div>
395
+ </section>
396
+ </>
397
+ )
398
+ }