@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.
- package/CHANGELOG.md +34 -0
- package/design-system/tokens.css +17 -0
- package/design-system/tokens.json +35 -1
- package/design-system/tokens.scss +15 -0
- package/design-system/tokens.tailwind.cjs +14 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +2 -2
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/lib/batch-wizard.test.d.ts +2 -0
- package/dist/__tests__/lib/batch-wizard.test.d.ts.map +1 -0
- package/dist/__tests__/lib/batch-wizard.test.js +144 -0
- package/dist/__tests__/lib/batch-wizard.test.js.map +1 -0
- package/dist/__tests__/lib/job-drawer.test.js +2 -0
- package/dist/__tests__/lib/job-drawer.test.js.map +1 -1
- package/dist/__tests__/lib/job-view.test.js +100 -3
- package/dist/__tests__/lib/job-view.test.js.map +1 -1
- package/dist/__tests__/lib/jobs-service.test.js +56 -2
- package/dist/__tests__/lib/jobs-service.test.js.map +1 -1
- package/dist/__tests__/views/appearance-chrome.render.test.d.ts +2 -0
- package/dist/__tests__/views/appearance-chrome.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/appearance-chrome.render.test.js +688 -0
- package/dist/__tests__/views/appearance-chrome.render.test.js.map +1 -0
- package/dist/__tests__/views/header-footer-form.test.d.ts +2 -0
- package/dist/__tests__/views/header-footer-form.test.d.ts.map +1 -0
- package/dist/__tests__/views/header-footer-form.test.js +266 -0
- package/dist/__tests__/views/header-footer-form.test.js.map +1 -0
- package/dist/__tests__/views/jobs-drawer.render.test.js +442 -2
- package/dist/__tests__/views/jobs-drawer.render.test.js.map +1 -1
- package/dist/__tests__/views/jobs-list.render.test.js +7 -5
- package/dist/__tests__/views/jobs-list.render.test.js.map +1 -1
- package/dist/__tests__/views/jobs-wizard.render.test.d.ts +2 -0
- package/dist/__tests__/views/jobs-wizard.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/jobs-wizard.render.test.js +588 -0
- package/dist/__tests__/views/jobs-wizard.render.test.js.map +1 -0
- package/dist/lib/api.d.ts +2 -0
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +8 -1
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/batch-wizard.d.ts +84 -0
- package/dist/lib/batch-wizard.d.ts.map +1 -0
- package/dist/lib/batch-wizard.js +122 -0
- package/dist/lib/batch-wizard.js.map +1 -0
- package/dist/lib/chrome-service.d.ts +69 -0
- package/dist/lib/chrome-service.d.ts.map +1 -0
- package/dist/lib/chrome-service.js +108 -0
- package/dist/lib/chrome-service.js.map +1 -0
- package/dist/lib/job-view.d.ts +43 -0
- package/dist/lib/job-view.d.ts.map +1 -1
- package/dist/lib/job-view.js +57 -1
- package/dist/lib/job-view.js.map +1 -1
- package/dist/lib/jobs-service.d.ts +19 -1
- package/dist/lib/jobs-service.d.ts.map +1 -1
- package/dist/lib/jobs-service.js +41 -0
- package/dist/lib/jobs-service.js.map +1 -1
- package/dist/prenta-admin.css +1 -1
- package/dist/views/AppearanceChrome.d.ts +22 -4
- package/dist/views/AppearanceChrome.d.ts.map +1 -1
- package/dist/views/AppearanceChrome.js +451 -15
- package/dist/views/AppearanceChrome.js.map +1 -1
- package/dist/views/Jobs/CancelJobDialog.d.ts +7 -1
- package/dist/views/Jobs/CancelJobDialog.d.ts.map +1 -1
- package/dist/views/Jobs/CancelJobDialog.js +13 -3
- package/dist/views/Jobs/CancelJobDialog.js.map +1 -1
- package/dist/views/Jobs/DrawerReview.d.ts +7 -0
- package/dist/views/Jobs/DrawerReview.d.ts.map +1 -1
- package/dist/views/Jobs/DrawerReview.js +1 -1
- package/dist/views/Jobs/DrawerReview.js.map +1 -1
- package/dist/views/Jobs/JobDrawer.d.ts +23 -3
- package/dist/views/Jobs/JobDrawer.d.ts.map +1 -1
- package/dist/views/Jobs/JobDrawer.js +30 -5
- package/dist/views/Jobs/JobDrawer.js.map +1 -1
- package/dist/views/Jobs/JobsTable.d.ts +4 -2
- package/dist/views/Jobs/JobsTable.d.ts.map +1 -1
- package/dist/views/Jobs/JobsTable.js +6 -5
- package/dist/views/Jobs/JobsTable.js.map +1 -1
- package/dist/views/Jobs/NewBatchJob.d.ts +34 -0
- package/dist/views/Jobs/NewBatchJob.d.ts.map +1 -0
- package/dist/views/Jobs/NewBatchJob.js +282 -0
- package/dist/views/Jobs/NewBatchJob.js.map +1 -0
- package/dist/views/Jobs/WorkerStrip.d.ts +3 -1
- package/dist/views/Jobs/WorkerStrip.d.ts.map +1 -1
- package/dist/views/Jobs/WorkerStrip.js +2 -2
- package/dist/views/Jobs/WorkerStrip.js.map +1 -1
- package/dist/views/Jobs/focus.d.ts +6 -0
- package/dist/views/Jobs/focus.d.ts.map +1 -1
- package/dist/views/Jobs/focus.js +10 -0
- package/dist/views/Jobs/focus.js.map +1 -1
- package/dist/views/Jobs.d.ts +12 -3
- package/dist/views/Jobs.d.ts.map +1 -1
- package/dist/views/Jobs.js +109 -31
- package/dist/views/Jobs.js.map +1 -1
- package/dist/views/header-footer/AnnouncementTab.d.ts +15 -0
- package/dist/views/header-footer/AnnouncementTab.d.ts.map +1 -0
- package/dist/views/header-footer/AnnouncementTab.js +43 -0
- package/dist/views/header-footer/AnnouncementTab.js.map +1 -0
- package/dist/views/header-footer/ChromeField.d.ts +25 -0
- package/dist/views/header-footer/ChromeField.d.ts.map +1 -0
- package/dist/views/header-footer/ChromeField.js +99 -0
- package/dist/views/header-footer/ChromeField.js.map +1 -0
- package/dist/views/header-footer/ChromePreview.d.ts +31 -0
- package/dist/views/header-footer/ChromePreview.d.ts.map +1 -0
- package/dist/views/header-footer/ChromePreview.js +195 -0
- package/dist/views/header-footer/ChromePreview.js.map +1 -0
- package/dist/views/header-footer/DeveloperTab.d.ts +17 -0
- package/dist/views/header-footer/DeveloperTab.d.ts.map +1 -0
- package/dist/views/header-footer/DeveloperTab.js +98 -0
- package/dist/views/header-footer/DeveloperTab.js.map +1 -0
- package/dist/views/header-footer/RegionTab.d.ts +27 -0
- package/dist/views/header-footer/RegionTab.d.ts.map +1 -0
- package/dist/views/header-footer/RegionTab.js +80 -0
- package/dist/views/header-footer/RegionTab.js.map +1 -0
- package/dist/views/header-footer/chrome-form.d.ts +137 -0
- package/dist/views/header-footer/chrome-form.d.ts.map +1 -0
- package/dist/views/header-footer/chrome-form.js +518 -0
- package/dist/views/header-footer/chrome-form.js.map +1 -0
- package/dist/views/header-footer/code-samples.d.ts +30 -0
- package/dist/views/header-footer/code-samples.d.ts.map +1 -0
- package/dist/views/header-footer/code-samples.js +127 -0
- package/dist/views/header-footer/code-samples.js.map +1 -0
- package/dist/views/header-footer/controls.d.ts +57 -0
- package/dist/views/header-footer/controls.d.ts.map +1 -0
- package/dist/views/header-footer/controls.js +43 -0
- package/dist/views/header-footer/controls.js.map +1 -0
- package/package.json +3 -3
- package/src/AdminRoot.tsx +16 -2
- package/src/__tests__/lib/batch-wizard.test.ts +199 -0
- package/src/__tests__/lib/job-drawer.test.ts +2 -0
- package/src/__tests__/lib/job-view.test.ts +123 -2
- package/src/__tests__/lib/jobs-service.test.ts +72 -3
- package/src/__tests__/views/appearance-chrome.render.test.tsx +818 -0
- package/src/__tests__/views/header-footer-form.test.ts +333 -0
- package/src/__tests__/views/jobs-drawer.render.test.tsx +508 -2
- package/src/__tests__/views/jobs-list.render.test.tsx +8 -5
- package/src/__tests__/views/jobs-wizard.render.test.tsx +707 -0
- package/src/lib/api.ts +12 -1
- package/src/lib/batch-wizard.ts +188 -0
- package/src/lib/chrome-service.ts +200 -0
- package/src/lib/job-view.ts +95 -1
- package/src/lib/jobs-service.ts +56 -1
- package/src/styles/prenta-tokens.css +17 -0
- package/src/styles/theme.css +15 -0
- package/src/views/AppearanceChrome.tsx +704 -46
- package/src/views/Jobs/CancelJobDialog.tsx +20 -2
- package/src/views/Jobs/DrawerReview.tsx +1 -1
- package/src/views/Jobs/JobDrawer.tsx +83 -3
- package/src/views/Jobs/JobsTable.tsx +11 -4
- package/src/views/Jobs/NewBatchJob.tsx +857 -0
- package/src/views/Jobs/WorkerStrip.tsx +16 -11
- package/src/views/Jobs/focus.ts +11 -0
- package/src/views/Jobs.tsx +163 -49
- package/src/views/header-footer/AnnouncementTab.tsx +231 -0
- package/src/views/header-footer/ChromeField.tsx +358 -0
- package/src/views/header-footer/ChromePreview.tsx +497 -0
- package/src/views/header-footer/DeveloperTab.tsx +331 -0
- package/src/views/header-footer/RegionTab.tsx +398 -0
- package/src/views/header-footer/chrome-form.ts +603 -0
- package/src/views/header-footer/code-samples.ts +146 -0
- package/src/views/header-footer/controls.tsx +166 -0
- package/dist/views/Jobs/placeholders.d.ts +0 -6
- package/dist/views/Jobs/placeholders.d.ts.map +0 -1
- package/dist/views/Jobs/placeholders.js +0 -20
- package/dist/views/Jobs/placeholders.js.map +0 -1
- package/src/views/Jobs/placeholders.tsx +0 -66
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent } from 'react'
|
|
4
|
+
import type { FieldDef, RegionSchema } from '@prenta/core'
|
|
5
|
+
|
|
6
|
+
import type { ChromeMenuItem, ChromeMenuOption } from '../../lib/chrome-service.js'
|
|
7
|
+
import {
|
|
8
|
+
fieldEntries,
|
|
9
|
+
type AnnouncementForm,
|
|
10
|
+
type PreviewChrome,
|
|
11
|
+
type RegionForm,
|
|
12
|
+
} from './chrome-form.js'
|
|
13
|
+
import { CARD } from './controls.js'
|
|
14
|
+
|
|
15
|
+
type Rec = Record<string, unknown>
|
|
16
|
+
type Viewport = 'desk' | 'mob'
|
|
17
|
+
|
|
18
|
+
function rec(value: unknown): Rec {
|
|
19
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) ? (value as Rec) : {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function text(value: unknown): string {
|
|
23
|
+
return typeof value === 'string' ? value : ''
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The message a Preview URL page receives, and the one it sends back when it's ready. */
|
|
27
|
+
const PREVIEW_MESSAGE = 'prenta:chrome-preview'
|
|
28
|
+
const PREVIEW_READY_MESSAGE = 'prenta:chrome-preview-ready'
|
|
29
|
+
|
|
30
|
+
function randomToken(): string {
|
|
31
|
+
const bytes = new Uint8Array(12)
|
|
32
|
+
globalThis.crypto?.getRandomValues?.(bytes)
|
|
33
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The Preview URL's origin, or null when it isn't an http(s) URL. */
|
|
37
|
+
function previewOrigin(url: string | null): string | null {
|
|
38
|
+
if (!url || !/^https?:\/\//i.test(url)) return null
|
|
39
|
+
try {
|
|
40
|
+
return new URL(url).origin
|
|
41
|
+
} catch {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The frame's address: the Preview URL with `draft={token}` in its query.
|
|
48
|
+
* Set through `URL`, so a fragment stays last and a `draft` already in the
|
|
49
|
+
* Preview URL is replaced rather than read first.
|
|
50
|
+
*/
|
|
51
|
+
function previewFrameSrc(url: string, token: string): string {
|
|
52
|
+
const src = new URL(url)
|
|
53
|
+
src.searchParams.set('draft', token)
|
|
54
|
+
return src.toString()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── The built-in mock (appearance-hf.jsx `Preview`) ────────────────────
|
|
58
|
+
//
|
|
59
|
+
// The mock draws the kit's example site from whatever the schema declares,
|
|
60
|
+
// picking fields by type (the region's menu, its call-to-action link, a
|
|
61
|
+
// `tel` text…), so a field the mock can't place is simply not drawn.
|
|
62
|
+
|
|
63
|
+
interface Found<T> {
|
|
64
|
+
key: string
|
|
65
|
+
def: T
|
|
66
|
+
value: unknown
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function entries(schema: RegionSchema | undefined, form: RegionForm | undefined) {
|
|
70
|
+
if (!schema || !form) return []
|
|
71
|
+
return fieldEntries(schema).map(([key, def]) => ({ key, def, value: form.fields[key] }))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The first field of a type that passes `test`, in schema order. */
|
|
75
|
+
function find<T extends FieldDef['type']>(
|
|
76
|
+
list: Array<Found<FieldDef>>,
|
|
77
|
+
type: T,
|
|
78
|
+
test: (def: Extract<FieldDef, { type: T }>, key: string) => boolean = () => true,
|
|
79
|
+
): Found<Extract<FieldDef, { type: T }>> | undefined {
|
|
80
|
+
const item = list.find(
|
|
81
|
+
(entry) =>
|
|
82
|
+
entry.def.type === type && test(entry.def as Extract<FieldDef, { type: T }>, entry.key),
|
|
83
|
+
)
|
|
84
|
+
return item as Found<Extract<FieldDef, { type: T }>> | undefined
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A toggled value's content when it's on; a plain value as it is. */
|
|
88
|
+
function onValue(def: { optional?: boolean }, value: unknown): unknown {
|
|
89
|
+
if (!def.optional) return value
|
|
90
|
+
const toggle = rec(value)
|
|
91
|
+
return toggle.on === true ? toggle : null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function menuOf(menus: ChromeMenuOption[], slug: unknown): ChromeMenuOption | undefined {
|
|
95
|
+
return typeof slug === 'string' && slug ? menus.find((menu) => menu.slug === slug) : undefined
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function topItems(menu: ChromeMenuOption | undefined): ChromeMenuItem[] {
|
|
99
|
+
return menu ? menu.items : []
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const TONE_BACKGROUND: Record<string, string> = {
|
|
103
|
+
brand: 'var(--acc)',
|
|
104
|
+
dark: 'var(--site-txt)',
|
|
105
|
+
alert: 'var(--site-alert)',
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function MockSite({
|
|
109
|
+
header,
|
|
110
|
+
footer,
|
|
111
|
+
announcement,
|
|
112
|
+
menus,
|
|
113
|
+
siteTitle,
|
|
114
|
+
viewport,
|
|
115
|
+
year,
|
|
116
|
+
}: {
|
|
117
|
+
header?: { schema: RegionSchema; form: RegionForm }
|
|
118
|
+
footer?: { schema: RegionSchema; form: RegionForm }
|
|
119
|
+
announcement: AnnouncementForm | null
|
|
120
|
+
menus: ChromeMenuOption[]
|
|
121
|
+
siteTitle: string
|
|
122
|
+
viewport: Viewport
|
|
123
|
+
year: string
|
|
124
|
+
}) {
|
|
125
|
+
const mob = viewport === 'mob'
|
|
126
|
+
const h = entries(header?.schema, header?.form)
|
|
127
|
+
const f = entries(footer?.schema, footer?.form)
|
|
128
|
+
|
|
129
|
+
// Header
|
|
130
|
+
const mainMenu = find(h, 'menu', (def) => !def.list && !def.optional)
|
|
131
|
+
const topMenu = find(
|
|
132
|
+
h,
|
|
133
|
+
'menu',
|
|
134
|
+
(def, key) => !def.list && !!def.optional && key !== mainMenu?.key,
|
|
135
|
+
)
|
|
136
|
+
const cta = find(h, 'link')
|
|
137
|
+
const phone = find(h, 'text', (def) => def.format === 'tel')
|
|
138
|
+
const search = find(h, 'boolean', (_def, key) => key === 'search')
|
|
139
|
+
const headerVariant = header?.form.variant ?? ''
|
|
140
|
+
const minimal = headerVariant === 'minimal'
|
|
141
|
+
const centered = /center/.test(headerVariant)
|
|
142
|
+
const nav = topItems(
|
|
143
|
+
menuOf(menus, mainMenu ? text(mainMenu.value) || mainMenu.def.default : undefined),
|
|
144
|
+
).filter((item) => !item.button)
|
|
145
|
+
const top = topMenu ? onValue(topMenu.def, topMenu.value) : null
|
|
146
|
+
const topLinks = top ? topItems(menuOf(menus, rec(top).value)) : []
|
|
147
|
+
const ctaValue = cta ? rec(onValue(cta.def, cta.value)) : {}
|
|
148
|
+
const ctaLabel = text(ctaValue.label)
|
|
149
|
+
const phoneValue = phone
|
|
150
|
+
? phone.def.optional
|
|
151
|
+
? text(rec(onValue(phone.def, phone.value)).value)
|
|
152
|
+
: text(phone.value)
|
|
153
|
+
: ''
|
|
154
|
+
|
|
155
|
+
// Footer
|
|
156
|
+
const tone = find(f, 'select', (def) => def.options.includes('light'))
|
|
157
|
+
const light = tone ? tone.value === 'light' : false
|
|
158
|
+
const about = find(f, 'textarea')
|
|
159
|
+
const cols = find(f, 'menu', (def) => !!def.list)
|
|
160
|
+
const copy = find(f, 'text', (def) => !!def.tokens?.includes('{year}'))
|
|
161
|
+
const email = find(f, 'text', (def) => def.format === 'email')
|
|
162
|
+
const footPhone = find(f, 'text', (def) => def.format === 'tel')
|
|
163
|
+
const address = find(
|
|
164
|
+
f,
|
|
165
|
+
'text',
|
|
166
|
+
(def, key) => !def.format && !def.tokens?.length && key !== copy?.key,
|
|
167
|
+
)
|
|
168
|
+
const news = find(
|
|
169
|
+
f,
|
|
170
|
+
'boolean',
|
|
171
|
+
(def, key) => key === 'news' || /newsletter/i.test(def.label ?? ''),
|
|
172
|
+
)
|
|
173
|
+
const social = find(f, 'social')
|
|
174
|
+
const legal = find(f, 'menu', (def) => !def.list)
|
|
175
|
+
const footerVariant = footer?.form.variant ?? ''
|
|
176
|
+
const fMinimal = footerVariant === 'minimal'
|
|
177
|
+
const compact = footerVariant === 'compact'
|
|
178
|
+
const colMenus = (Array.isArray(cols?.value) ? (cols.value as unknown[]) : [])
|
|
179
|
+
.map((slug) => menuOf(menus, slug))
|
|
180
|
+
.filter((menu): menu is ChromeMenuOption => !!menu)
|
|
181
|
+
const shownCols = compact ? colMenus.slice(0, 1) : colMenus
|
|
182
|
+
const legalValue = legal ? onValue(legal.def, legal.value) : null
|
|
183
|
+
const legalLinks = legal
|
|
184
|
+
? topItems(menuOf(menus, legal.def.optional ? rec(legalValue).value : legalValue))
|
|
185
|
+
: []
|
|
186
|
+
const textOf = (item: Found<Extract<FieldDef, { type: 'text' }>> | undefined) =>
|
|
187
|
+
item
|
|
188
|
+
? item.def.optional
|
|
189
|
+
? text(rec(onValue(item.def, item.value)).value)
|
|
190
|
+
: text(item.value)
|
|
191
|
+
: ''
|
|
192
|
+
const copyText = (textOf(copy) || `© {year} ${siteTitle}`).split('{year}').join(year)
|
|
193
|
+
const contact = [textOf(email), textOf(footPhone)].filter(Boolean).join(' · ')
|
|
194
|
+
const socialLinks = Array.isArray(social?.value) ? (social.value as unknown[]).map(rec) : []
|
|
195
|
+
|
|
196
|
+
const toneColor = TONE_BACKGROUND[announcement?.tone ?? 'brand'] ?? TONE_BACKGROUND.brand
|
|
197
|
+
const footMuted = light ? 'text-site-sub' : 'text-site-foot-txt'
|
|
198
|
+
const footStrong = light ? 'text-site-txt' : 'text-site-bg'
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div
|
|
202
|
+
data-preview-site=""
|
|
203
|
+
data-viewport={viewport}
|
|
204
|
+
className={`border-bdr bg-site-bg text-site-txt rounded-10 w-full overflow-hidden border text-[13px] leading-[normal] transition-[max-width] duration-200 ${
|
|
205
|
+
mob ? 'max-w-[360px]' : 'max-w-[860px]'
|
|
206
|
+
}`}
|
|
207
|
+
>
|
|
208
|
+
{announcement?.on ? (
|
|
209
|
+
<div
|
|
210
|
+
data-preview-announcement=""
|
|
211
|
+
className="flex items-center justify-center gap-2.5 px-3 py-[7px] text-center text-[12px] font-semibold text-white"
|
|
212
|
+
style={{ background: toneColor }}
|
|
213
|
+
>
|
|
214
|
+
<span>{announcement.text}</span>
|
|
215
|
+
{announcement.linkLabel ? <u className="font-bold">{announcement.linkLabel} →</u> : null}
|
|
216
|
+
{announcement.dismissible ? <span className="ml-1.5 opacity-75">✕</span> : null}
|
|
217
|
+
</div>
|
|
218
|
+
) : null}
|
|
219
|
+
{header ? (
|
|
220
|
+
<>
|
|
221
|
+
{topLinks.length > 0 && !mob && !minimal ? (
|
|
222
|
+
<div className="bg-site-alt text-site-sub flex justify-end gap-4 px-[18px] py-[5px] text-[11.5px]">
|
|
223
|
+
{topLinks.map((item) => (
|
|
224
|
+
<span key={item.id}>{item.label}</span>
|
|
225
|
+
))}
|
|
226
|
+
</div>
|
|
227
|
+
) : null}
|
|
228
|
+
<div
|
|
229
|
+
data-preview-header={headerVariant}
|
|
230
|
+
className={`border-site-bdr flex items-center gap-5 border-b px-[18px] py-3.5 ${
|
|
231
|
+
centered ? 'flex-col gap-2.5' : ''
|
|
232
|
+
}`}
|
|
233
|
+
>
|
|
234
|
+
<span className="text-[16px] font-extrabold tracking-[-0.4px]">{siteTitle}</span>
|
|
235
|
+
{mob && !minimal ? (
|
|
236
|
+
<span
|
|
237
|
+
aria-hidden
|
|
238
|
+
className="border-site-txt after:border-site-txt relative ml-auto h-3.5 w-[22px] border-y-2 after:absolute after:inset-x-0 after:top-1 after:border-t-2 after:content-['']"
|
|
239
|
+
/>
|
|
240
|
+
) : (
|
|
241
|
+
<>
|
|
242
|
+
{minimal ? (
|
|
243
|
+
<span className="flex-1" />
|
|
244
|
+
) : (
|
|
245
|
+
<nav
|
|
246
|
+
className={`flex flex-1 flex-wrap gap-4 ${centered ? 'justify-center' : 'justify-end'}`}
|
|
247
|
+
>
|
|
248
|
+
{nav.map((item) => (
|
|
249
|
+
<span key={item.id} className="whitespace-nowrap">
|
|
250
|
+
{item.label}
|
|
251
|
+
</span>
|
|
252
|
+
))}
|
|
253
|
+
</nav>
|
|
254
|
+
)}
|
|
255
|
+
<span className="flex items-center gap-2.5 whitespace-nowrap">
|
|
256
|
+
{search?.value === true && !minimal ? <span>⌕</span> : null}
|
|
257
|
+
{phoneValue && !mob ? <span>{phoneValue}</span> : null}
|
|
258
|
+
{ctaLabel ? (
|
|
259
|
+
<span className="bg-acc rounded-7 px-3 py-[7px] text-[12.5px] font-semibold text-white">
|
|
260
|
+
{ctaLabel}
|
|
261
|
+
</span>
|
|
262
|
+
) : null}
|
|
263
|
+
</span>
|
|
264
|
+
</>
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
</>
|
|
268
|
+
) : null}
|
|
269
|
+
<div
|
|
270
|
+
className={`text-site-muted flex items-center justify-center bg-[repeating-linear-gradient(135deg,var(--site-alt)_0_10px,var(--site-alt2)_10px_20px)] text-[12px] ${
|
|
271
|
+
mob ? 'h-20' : 'h-[120px]'
|
|
272
|
+
}`}
|
|
273
|
+
>
|
|
274
|
+
Page content
|
|
275
|
+
</div>
|
|
276
|
+
{footer ? (
|
|
277
|
+
<div
|
|
278
|
+
data-preview-footer={footerVariant}
|
|
279
|
+
className={`px-[18px] pt-5 pb-3.5 text-[12px] ${light ? 'bg-site-alt text-site-sub' : 'bg-site-txt text-site-foot-txt'}`}
|
|
280
|
+
>
|
|
281
|
+
{!fMinimal ? (
|
|
282
|
+
<div
|
|
283
|
+
className={`grid gap-[18px] ${
|
|
284
|
+
compact
|
|
285
|
+
? mob
|
|
286
|
+
? 'grid-cols-1'
|
|
287
|
+
: 'grid-cols-[1.3fr_1fr]'
|
|
288
|
+
: mob
|
|
289
|
+
? 'grid-cols-2'
|
|
290
|
+
: 'grid-cols-[1.3fr_repeat(3,1fr)]'
|
|
291
|
+
}`}
|
|
292
|
+
>
|
|
293
|
+
<div>
|
|
294
|
+
<div
|
|
295
|
+
className={`mb-1.5 text-[15px] font-extrabold tracking-[-0.3px] ${footStrong}`}
|
|
296
|
+
>
|
|
297
|
+
{siteTitle}
|
|
298
|
+
</div>
|
|
299
|
+
{about ? <div>{text(about.value)}</div> : null}
|
|
300
|
+
{!compact ? (
|
|
301
|
+
<>
|
|
302
|
+
{textOf(address) ? <span className="mt-1 block">{textOf(address)}</span> : null}
|
|
303
|
+
{contact ? <span className="mt-1 block">{contact}</span> : null}
|
|
304
|
+
</>
|
|
305
|
+
) : null}
|
|
306
|
+
{news?.value === true ? (
|
|
307
|
+
<div className="mt-2 flex gap-1.5">
|
|
308
|
+
<i
|
|
309
|
+
className={`rounded-6 h-[26px] flex-1 border ${
|
|
310
|
+
light ? 'border-site-bdr bg-site-bg' : 'border-white/15 bg-white/8'
|
|
311
|
+
}`}
|
|
312
|
+
/>
|
|
313
|
+
<em className="bg-acc rounded-6 px-[9px] py-1 font-semibold text-white not-italic">
|
|
314
|
+
Subscribe
|
|
315
|
+
</em>
|
|
316
|
+
</div>
|
|
317
|
+
) : null}
|
|
318
|
+
</div>
|
|
319
|
+
{shownCols.map((menu) => (
|
|
320
|
+
<div key={menu.slug}>
|
|
321
|
+
<b className={`mb-1.5 block text-[12px] ${footStrong}`}>
|
|
322
|
+
{menu.name.replace(/^Footer · /, '')}
|
|
323
|
+
</b>
|
|
324
|
+
{menu.items.map((item) => (
|
|
325
|
+
<span key={item.id} className="mt-1 block">
|
|
326
|
+
{item.label}
|
|
327
|
+
</span>
|
|
328
|
+
))}
|
|
329
|
+
</div>
|
|
330
|
+
))}
|
|
331
|
+
</div>
|
|
332
|
+
) : null}
|
|
333
|
+
<div
|
|
334
|
+
className={`flex flex-wrap gap-3 text-[11px] ${
|
|
335
|
+
fMinimal
|
|
336
|
+
? ''
|
|
337
|
+
: `mt-4 border-t pt-2.5 ${light ? 'border-site-bdr' : 'border-white/12'}`
|
|
338
|
+
} ${footMuted}`}
|
|
339
|
+
>
|
|
340
|
+
<span>{copyText}</span>
|
|
341
|
+
{legalLinks.map((item) => (
|
|
342
|
+
<span key={item.id}>{item.label}</span>
|
|
343
|
+
))}
|
|
344
|
+
<span className="ml-auto flex gap-2">
|
|
345
|
+
{socialLinks.map((link, index) => (
|
|
346
|
+
<i
|
|
347
|
+
key={index}
|
|
348
|
+
title={text(link.platform)}
|
|
349
|
+
className={`block h-[18px] w-[18px] rounded-full ${light ? 'bg-site-chip' : 'bg-white/15'}`}
|
|
350
|
+
/>
|
|
351
|
+
))}
|
|
352
|
+
</span>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
) : null}
|
|
356
|
+
</div>
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ─── The card ───────────────────────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
export interface ChromePreviewProps {
|
|
363
|
+
header?: { schema: RegionSchema; form: RegionForm }
|
|
364
|
+
footer?: { schema: RegionSchema; form: RegionForm }
|
|
365
|
+
announcement: AnnouncementForm | null
|
|
366
|
+
menus: ChromeMenuOption[]
|
|
367
|
+
siteTitle: string
|
|
368
|
+
/** Settings → Preview URL. Set, it renders the site's own components. */
|
|
369
|
+
previewUrl: string | null
|
|
370
|
+
/** The draft in the `/public/chrome` shape, sent to the Preview URL page. */
|
|
371
|
+
draft: PreviewChrome
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* The preview card (`.hf-pv`): a bar with the Desktop/Mobile switch over a
|
|
376
|
+
* stage that shows the site's Preview URL page, or the built-in mock.
|
|
377
|
+
*
|
|
378
|
+
* The Preview URL page loads as `{previewUrl}?draft={token}`. It gets the
|
|
379
|
+
* draft as `{ type: 'prenta:chrome-preview', token, chrome }`, posted to its
|
|
380
|
+
* origin only, when it loads, whenever the draft changes, and again whenever
|
|
381
|
+
* it posts `{ type: 'prenta:chrome-preview-ready', token }` to its parent.
|
|
382
|
+
*/
|
|
383
|
+
export function ChromePreview({
|
|
384
|
+
header,
|
|
385
|
+
footer,
|
|
386
|
+
announcement,
|
|
387
|
+
menus,
|
|
388
|
+
siteTitle,
|
|
389
|
+
previewUrl,
|
|
390
|
+
draft,
|
|
391
|
+
}: ChromePreviewProps) {
|
|
392
|
+
const [viewport, setViewport] = useState<Viewport>('desk')
|
|
393
|
+
const origin = previewOrigin(previewUrl)
|
|
394
|
+
const token = useMemo(() => randomToken(), [])
|
|
395
|
+
const frame = useRef<HTMLIFrameElement | null>(null)
|
|
396
|
+
const src = origin && previewUrl ? previewFrameSrc(previewUrl, token) : null
|
|
397
|
+
|
|
398
|
+
const send = useCallback(() => {
|
|
399
|
+
const target = frame.current?.contentWindow
|
|
400
|
+
if (!target || !origin) return
|
|
401
|
+
target.postMessage({ type: PREVIEW_MESSAGE, token, chrome: draft }, origin)
|
|
402
|
+
}, [draft, origin, token])
|
|
403
|
+
|
|
404
|
+
useEffect(() => {
|
|
405
|
+
send()
|
|
406
|
+
}, [send])
|
|
407
|
+
|
|
408
|
+
useEffect(() => {
|
|
409
|
+
if (!origin) return
|
|
410
|
+
function onMessage(event: MessageEvent) {
|
|
411
|
+
if (event.origin !== origin || event.source !== frame.current?.contentWindow) return
|
|
412
|
+
const data = rec(event.data)
|
|
413
|
+
if (data.type === PREVIEW_READY_MESSAGE && data.token === token) send()
|
|
414
|
+
}
|
|
415
|
+
window.addEventListener('message', onMessage)
|
|
416
|
+
return () => window.removeEventListener('message', onMessage)
|
|
417
|
+
}, [origin, send, token])
|
|
418
|
+
|
|
419
|
+
const options: Array<[Viewport, string]> = [
|
|
420
|
+
['desk', 'Desktop'],
|
|
421
|
+
['mob', 'Mobile'],
|
|
422
|
+
]
|
|
423
|
+
function onKeyDown(event: KeyboardEvent<HTMLDivElement>) {
|
|
424
|
+
if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) return
|
|
425
|
+
event.preventDefault()
|
|
426
|
+
const next = viewport === 'desk' ? 'mob' : 'desk'
|
|
427
|
+
setViewport(next)
|
|
428
|
+
event.currentTarget.querySelector<HTMLButtonElement>(`[data-viewport="${next}"]`)?.focus()
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
let host = ''
|
|
432
|
+
if (origin) host = new URL(origin).host
|
|
433
|
+
|
|
434
|
+
return (
|
|
435
|
+
<section aria-label="Preview" className={`${CARD} sticky top-3 [@media(width<=1100px)]:static`}>
|
|
436
|
+
<div className="border-bdr text-sub flex flex-wrap items-center gap-2 border-b px-3 py-[9px] text-[12px] leading-[normal] font-semibold">
|
|
437
|
+
Preview
|
|
438
|
+
<span className="text-faint font-mono text-[11px] font-medium">
|
|
439
|
+
{origin
|
|
440
|
+
? `${host} · draft values`
|
|
441
|
+
: 'built-in mock · set a Preview URL to render your components'}
|
|
442
|
+
</span>
|
|
443
|
+
<div
|
|
444
|
+
role="radiogroup"
|
|
445
|
+
aria-label="Preview width"
|
|
446
|
+
onKeyDown={onKeyDown}
|
|
447
|
+
className="rounded-8 border-bdr bg-bg ml-auto flex gap-0.5 border p-0.5"
|
|
448
|
+
>
|
|
449
|
+
{options.map(([id, label]) => {
|
|
450
|
+
const on = viewport === id
|
|
451
|
+
return (
|
|
452
|
+
<button
|
|
453
|
+
key={id}
|
|
454
|
+
type="button"
|
|
455
|
+
role="radio"
|
|
456
|
+
aria-checked={on}
|
|
457
|
+
tabIndex={on ? 0 : -1}
|
|
458
|
+
data-viewport={id}
|
|
459
|
+
onClick={() => setViewport(id)}
|
|
460
|
+
className={`rounded-6 cursor-pointer border-0 px-[9px] py-[3px] text-[12px] leading-[normal] font-semibold ${
|
|
461
|
+
on ? 'bg-card text-txt shadow-shd' : 'text-sub bg-transparent'
|
|
462
|
+
}`}
|
|
463
|
+
>
|
|
464
|
+
{label}
|
|
465
|
+
</button>
|
|
466
|
+
)
|
|
467
|
+
})}
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
<div className="rounded-b-12 bg-bg flex justify-center p-[18px]">
|
|
471
|
+
{src ? (
|
|
472
|
+
<iframe
|
|
473
|
+
ref={frame}
|
|
474
|
+
src={src}
|
|
475
|
+
title="Site preview with draft values"
|
|
476
|
+
onLoad={send}
|
|
477
|
+
sandbox="allow-scripts allow-same-origin"
|
|
478
|
+
referrerPolicy="no-referrer"
|
|
479
|
+
className={`border-bdr bg-site-bg rounded-10 block h-[560px] w-full border transition-[max-width] duration-200 ${
|
|
480
|
+
viewport === 'mob' ? 'max-w-[360px]' : 'max-w-[860px]'
|
|
481
|
+
}`}
|
|
482
|
+
/>
|
|
483
|
+
) : (
|
|
484
|
+
<MockSite
|
|
485
|
+
header={header}
|
|
486
|
+
footer={footer}
|
|
487
|
+
announcement={announcement}
|
|
488
|
+
menus={menus}
|
|
489
|
+
siteTitle={siteTitle}
|
|
490
|
+
viewport={viewport}
|
|
491
|
+
year={String(new Date().getFullYear())}
|
|
492
|
+
/>
|
|
493
|
+
)}
|
|
494
|
+
</div>
|
|
495
|
+
</section>
|
|
496
|
+
)
|
|
497
|
+
}
|