@unitninety/site-kit 0.0.0-stage → 1.6.2

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 (206) hide show
  1. package/README.md +30 -2
  2. package/package.json +79 -4
  3. package/src/bridge/DraftBlocks.tsx +70 -0
  4. package/src/bridge/DraftEntries.tsx +41 -0
  5. package/src/bridge/PreviewCommitBoundary.tsx +19 -0
  6. package/src/bridge/U90Bridge.tsx +54 -0
  7. package/src/bridge/caret.ts +317 -0
  8. package/src/bridge/client.tsx +66 -0
  9. package/src/bridge/createBridge.ts +1015 -0
  10. package/src/bridge/hooks.tsx +56 -0
  11. package/src/bridge/index.ts +9 -0
  12. package/src/bridge/lexical.ts +177 -0
  13. package/src/bridge/lexicalDom.ts +212 -0
  14. package/src/bridge/preserveBlockMoves.ts +88 -0
  15. package/src/bridge/preview.ts +179 -0
  16. package/src/bridge/protocol.ts +273 -0
  17. package/src/bridge/registry.ts +30 -0
  18. package/src/bridge/styles.ts +137 -0
  19. package/src/components/DateText.tsx +9 -0
  20. package/src/components/Entries.tsx +16 -0
  21. package/src/components/File.tsx +9 -0
  22. package/src/components/Image.tsx +30 -0
  23. package/src/components/Link.tsx +24 -0
  24. package/src/components/NumberText.tsx +9 -0
  25. package/src/components/RichText.tsx +47 -0
  26. package/src/components/Text.tsx +15 -0
  27. package/src/components/Video.tsx +12 -0
  28. package/src/components/VideoPlayback.tsx +19 -0
  29. package/src/components/draft.ts +9 -0
  30. package/src/components/imageProps.ts +26 -0
  31. package/src/components/index.ts +13 -0
  32. package/src/components/videoProps.ts +6 -0
  33. package/src/elements/containers/group.ts +33 -0
  34. package/src/elements/containers/list.ts +31 -0
  35. package/src/elements/containers/zone.ts +41 -0
  36. package/src/elements/declaration.ts +118 -0
  37. package/src/elements/define.ts +195 -0
  38. package/src/elements/ids.ts +50 -0
  39. package/src/elements/index.ts +35 -0
  40. package/src/elements/jsonSchema.ts +36 -0
  41. package/src/elements/leaf.ts +98 -0
  42. package/src/elements/leaves/boolean.ts +30 -0
  43. package/src/elements/leaves/choice.ts +53 -0
  44. package/src/elements/leaves/date.ts +39 -0
  45. package/src/elements/leaves/file.ts +56 -0
  46. package/src/elements/leaves/image.ts +160 -0
  47. package/src/elements/leaves/link.ts +149 -0
  48. package/src/elements/leaves/number.ts +52 -0
  49. package/src/elements/leaves/query.ts +101 -0
  50. package/src/elements/leaves/relation.ts +69 -0
  51. package/src/elements/leaves/rich-text.ts +155 -0
  52. package/src/elements/leaves/selection.ts +40 -0
  53. package/src/elements/leaves/single.ts +29 -0
  54. package/src/elements/leaves/text.ts +103 -0
  55. package/src/elements/leaves/video.ts +108 -0
  56. package/src/elements/ops/expand.ts +94 -0
  57. package/src/elements/ops/overlay.ts +59 -0
  58. package/src/elements/ops/refs.ts +45 -0
  59. package/src/elements/ops/seed.ts +185 -0
  60. package/src/elements/ops/text.ts +20 -0
  61. package/src/elements/ops/translatable.ts +19 -0
  62. package/src/elements/ops/upgrade.ts +43 -0
  63. package/src/elements/ops/validate.ts +56 -0
  64. package/src/elements/order.ts +10 -0
  65. package/src/elements/path.ts +43 -0
  66. package/src/elements/prefix.ts +2 -0
  67. package/src/elements/registry.ts +48 -0
  68. package/src/elements/tokens.ts +14 -0
  69. package/src/elements/walk.ts +42 -0
  70. package/src/features/client/AskAiProvider.tsx +551 -0
  71. package/src/features/client/AskAiWidget.tsx +222 -0
  72. package/src/features/client/ConsentRuntime.tsx +95 -0
  73. package/src/features/client/FeedbackProvider.tsx +231 -0
  74. package/src/features/client/FeedbackWidget.tsx +101 -0
  75. package/src/features/client/PageReporter.tsx +26 -0
  76. package/src/features/client/README.md +131 -0
  77. package/src/features/client/SiteFeatures.tsx +250 -0
  78. package/src/features/client/askAiRecovery.ts +92 -0
  79. package/src/features/client/askAiStream.ts +59 -0
  80. package/src/features/client/context.tsx +24 -0
  81. package/src/features/client/index.ts +24 -0
  82. package/src/features/client/overlay.tsx +129 -0
  83. package/src/features/client/styles.css +337 -0
  84. package/src/features/client/transport.ts +310 -0
  85. package/src/features/client/useAskAI.ts +45 -0
  86. package/src/features/client/useConsent.ts +15 -0
  87. package/src/features/client/useFeedback.ts +33 -0
  88. package/src/features/client/useSiteSearch.ts +50 -0
  89. package/src/features/client/verification.ts +141 -0
  90. package/src/features/consent/README.md +70 -0
  91. package/src/features/consent/cookieScript.ts +53 -0
  92. package/src/features/consent/custom.ts +89 -0
  93. package/src/features/consent/defaults.ts +40 -0
  94. package/src/features/consent/drivers.ts +233 -0
  95. package/src/features/consent/loader.ts +248 -0
  96. package/src/features/consent/registry.ts +398 -0
  97. package/src/features/contracts/askAi.ts +166 -0
  98. package/src/features/contracts/askAiSettings.ts +42 -0
  99. package/src/features/contracts/base.ts +107 -0
  100. package/src/features/contracts/consent.ts +150 -0
  101. package/src/features/contracts/feedback.ts +22 -0
  102. package/src/features/contracts/feedbackSettings.ts +49 -0
  103. package/src/features/contracts/index.ts +61 -0
  104. package/src/features/contracts/knowledge.ts +99 -0
  105. package/src/features/contracts/knowledgePolicy.ts +249 -0
  106. package/src/features/contracts/search.ts +24 -0
  107. package/src/features/contracts/verification.ts +52 -0
  108. package/src/features/contracts/verificationConfig.ts +9 -0
  109. package/src/features/server/ConsentDefaults.tsx +15 -0
  110. package/src/features/server/README.md +185 -0
  111. package/src/features/server/gateway.ts +318 -0
  112. package/src/features/server/http.ts +179 -0
  113. package/src/features/server/index.ts +6 -0
  114. package/src/features/server/session.ts +23 -0
  115. package/src/features/server/verification.ts +39 -0
  116. package/src/features/server/verifyVisitor.ts +69 -0
  117. package/src/languages/config.d.mts +5 -0
  118. package/src/languages/config.mjs +53 -0
  119. package/src/languages/decide.ts +72 -0
  120. package/src/languages/index.ts +4 -0
  121. package/src/languages/proxy.ts +108 -0
  122. package/src/languages/routing.ts +93 -0
  123. package/src/languages/wire.ts +19 -0
  124. package/src/legacy/bridge.tsx +1987 -0
  125. package/src/legacy/caret.ts +246 -0
  126. package/src/legacy/cli.mjs +189 -0
  127. package/src/legacy/content.ts +993 -0
  128. package/src/legacy/contract.ts +664 -0
  129. package/src/legacy/defineBlocks.ts +1602 -0
  130. package/src/legacy/defineSite.ts +623 -0
  131. package/src/legacy/defineSite.types.ts +270 -0
  132. package/src/legacy/draft.tsx +128 -0
  133. package/src/legacy/image.ts +433 -0
  134. package/src/legacy/index.ts +209 -0
  135. package/src/legacy/language-switcher.tsx +58 -0
  136. package/src/legacy/languageHints.ts +153 -0
  137. package/src/legacy/languages/fields.ts +131 -0
  138. package/src/legacy/languages/hash.ts +98 -0
  139. package/src/legacy/languages/health.ts +97 -0
  140. package/src/legacy/languages/identity.ts +247 -0
  141. package/src/legacy/languages/links.ts +81 -0
  142. package/src/legacy/languages/resolution.ts +56 -0
  143. package/src/legacy/languages/routing.ts +169 -0
  144. package/src/legacy/languages/runtime.ts +216 -0
  145. package/src/legacy/languages/seo.ts +95 -0
  146. package/src/legacy/languages/settings.ts +211 -0
  147. package/src/legacy/languages/types.ts +85 -0
  148. package/src/legacy/languages.ts +8 -0
  149. package/src/legacy/lexical.ts +170 -0
  150. package/src/legacy/lexicalDom.ts +166 -0
  151. package/src/legacy/media.ts +272 -0
  152. package/src/legacy/nav.ts +176 -0
  153. package/src/legacy/preview.ts +79 -0
  154. package/src/legacy/redirects.ts +474 -0
  155. package/src/legacy/richText.tsx +104 -0
  156. package/src/legacy/routes.ts +414 -0
  157. package/src/legacy/seo.ts +327 -0
  158. package/src/legacy/shape.ts +412 -0
  159. package/src/legacy/uploads.ts +233 -0
  160. package/src/legacy/verification.ts +42 -0
  161. package/src/preview-lock/contracts.ts +42 -0
  162. package/src/preview-lock/index.ts +3 -0
  163. package/src/preview-lock/screen.tsx +244 -0
  164. package/src/preview-lock/server.ts +416 -0
  165. package/src/redirects/config.d.mts +5 -0
  166. package/src/redirects/config.mjs +22 -0
  167. package/src/redirects/index.ts +3 -0
  168. package/src/redirects/location.ts +31 -0
  169. package/src/redirects/match.ts +199 -0
  170. package/src/redirects/proxy.ts +91 -0
  171. package/src/redirects/wire.ts +30 -0
  172. package/src/seo/jsonLd.ts +70 -0
  173. package/src/seo/metadata.ts +127 -0
  174. package/src/seo/robots.ts +14 -0
  175. package/src/seo/sitemap.ts +22 -0
  176. package/src/seo/title.ts +22 -0
  177. package/src/server/adapters.ts +73 -0
  178. package/src/server/createU90.ts +96 -0
  179. package/src/server/handlers.ts +164 -0
  180. package/src/server/index.ts +6 -0
  181. package/src/server/read.ts +207 -0
  182. package/src/server/render.tsx +46 -0
  183. package/src/server/routes.tsx +171 -0
  184. package/src/site/assemble.ts +109 -0
  185. package/src/site/defineSite.ts +132 -0
  186. package/src/site/documents.ts +18 -0
  187. package/src/site/fields.ts +59 -0
  188. package/src/site/index.ts +14 -0
  189. package/src/site/replies.ts +118 -0
  190. package/src/site/store.ts +45 -0
  191. package/src/site/types.ts +75 -0
  192. package/src/site/version.ts +1 -0
  193. package/src/site/wire.ts +125 -0
  194. package/src/switcher/LanguageSwitcher.tsx +42 -0
  195. package/src/test-preview/README.md +90 -0
  196. package/src/test-preview/binding.ts +54 -0
  197. package/src/test-preview/bridge.ts +16 -0
  198. package/src/test-preview/guard.ts +186 -0
  199. package/src/test-preview/media.ts +23 -0
  200. package/src/test-preview/protocol.d.mts +67 -0
  201. package/src/test-preview/protocol.mjs +222 -0
  202. package/src/test-preview/runtime.ts +168 -0
  203. package/src/test-preview/server.ts +10 -0
  204. package/src/testing/fake.ts +148 -0
  205. package/src/testing/index.ts +2 -0
  206. package/src/testing/seed.ts +2 -0
@@ -0,0 +1,42 @@
1
+ /** Public platform wire contract. Credentials and manager settings never belong here. */
2
+ export type PreviewLockAppearance = {
3
+ partnerName: string
4
+ partnerLogoUrl: string | null
5
+ prospectName: string
6
+ prospectLogoUrl: string | null
7
+ backgroundColor: string
8
+ textColor: string
9
+ accentColor: string
10
+ heading: string
11
+ message: string
12
+ instructions: string
13
+ }
14
+ export type PreviewLockStatus = {
15
+ /** Explicit independently verified Canvas proof; never inferred from ordinary open status. */
16
+ canvasVerified?: boolean
17
+ state: 'open' | 'locked'
18
+ protected: boolean
19
+ appearance?: PreviewLockAppearance
20
+ }
21
+ export const LOCK_PATH = '/u90-preview-lock'
22
+ export const LOCK_ENDPOINT = '/api/u90/preview-lock'
23
+ export const ACCESS_COOKIE = 'u90-preview-access'
24
+ export const HANDOFF_QUERY = 'u90-preview-handoff'
25
+ /** Browser URL parsing semantics, including backslashes and normalised dot segments. */
26
+ export function safeReturnTo(value: string | null | undefined): string {
27
+ if (!value || value.length > 2048 || !value.startsWith('/')) return '/'
28
+ try {
29
+ const url = new URL(value, 'https://u90.invalid')
30
+ if (
31
+ url.origin !== 'https://u90.invalid' ||
32
+ url.pathname.startsWith('//') ||
33
+ url.pathname === LOCK_PATH ||
34
+ url.pathname === LOCK_ENDPOINT
35
+ )
36
+ return '/'
37
+ url.searchParams.delete(HANDOFF_QUERY)
38
+ return url.pathname + url.search + url.hash
39
+ } catch {
40
+ return '/'
41
+ }
42
+ }
@@ -0,0 +1,3 @@
1
+ export { createPreviewLock } from './server'
2
+ export type { PreviewLockOptions, PreviewLockGate } from './server'
3
+ export type { PreviewLockAppearance, PreviewLockStatus } from './contracts'
@@ -0,0 +1,244 @@
1
+ 'use client'
2
+
3
+ /* eslint-disable @next/next/no-img-element -- Public branded logos must not require an exempt image-optimisation endpoint. */
4
+
5
+ import { useEffect, useRef, useState } from 'react'
6
+ import type { CSSProperties, FormEvent, KeyboardEvent } from 'react'
7
+ import { HANDOFF_QUERY, LOCK_ENDPOINT, safeReturnTo } from './contracts'
8
+ import type { PreviewLockAppearance } from './contracts'
9
+
10
+ export type PreviewLockScreenProps = {
11
+ appearance: PreviewLockAppearance
12
+ endpoint?: string
13
+ returnTo?: string
14
+ preview?: boolean
15
+ }
16
+ const css = `
17
+ .u90-lock{box-sizing:border-box;position:relative;isolation:isolate;min-height:100svh;display:flex;flex-direction:column;justify-content:center;align-items:center;padding:64px 24px 30px;background:var(--u90-lock-background);color:var(--u90-lock-text);font-family:Arial,Helvetica,sans-serif;text-align:center;overflow:hidden}
18
+ .u90-lock *{box-sizing:border-box}.u90-lock:before{content:"";position:absolute;inset:0;z-index:-1;background:radial-gradient(ellipse at 50% 0%,color-mix(in srgb,var(--u90-lock-accent) 8%,transparent),transparent 65%)}
19
+ .u90-lock[data-preview=true]{min-height:560px;height:100%;padding:48px 22px 26px}.u90-lock-content{width:100%;max-width:680px;margin:auto;animation:u90-lock-enter .55s ease-out}
20
+ .u90-lock-brands{display:flex;align-items:center;justify-content:center;gap:26px;margin-bottom:58px}.u90-lock-brand{display:flex;flex-direction:column;gap:12px;align-items:center;min-width:0;max-width:42%;font-size:15px;font-weight:600;letter-spacing:.02em;overflow-wrap:anywhere}.u90-lock-brand img{max-width:160px;max-height:52px;object-fit:contain}.u90-lock-cross{opacity:.35;font-size:24px;font-weight:300}
21
+ .u90-lock-eyebrow{display:flex;align-items:center;justify-content:center;gap:13px;font-size:10px;letter-spacing:.24em;text-transform:uppercase;font-weight:600;color:var(--u90-lock-accent);margin:0 0 18px}.u90-lock-eyebrow:before,.u90-lock-eyebrow:after{content:"";height:1px;width:27px;background:currentColor;opacity:.6}
22
+ .u90-lock h1{font-size:clamp(30px,5vw,50px);line-height:1.12;letter-spacing:-.045em;font-weight:500;margin:0 auto 22px;max-width:600px;text-wrap:balance}.u90-lock-message{opacity:.75;font-size:16px;line-height:1.7;max-width:440px;margin:0 auto 36px;white-space:pre-line;overflow-wrap:anywhere}
23
+ .u90-lock form{max-width:380px;margin:0 auto}.u90-lock-instructions{display:block;font-size:12px;line-height:1.6;opacity:.8;margin-bottom:16px;white-space:pre-line;overflow-wrap:anywhere}.u90-lock-digits{display:flex;gap:10px;justify-content:center}.u90-lock input{min-width:0;width:50px;height:61px;border:1px solid color-mix(in srgb,var(--u90-lock-text) 25%,transparent);background:color-mix(in srgb,var(--u90-lock-text) 4%,transparent);color:inherit;border-radius:7px;text-align:center;font-size:24px;font-family:inherit;caret-color:var(--u90-lock-accent);outline:none;transition:border-color .15s,background .15s}.u90-lock input:focus{border-color:var(--u90-lock-accent);box-shadow:0 0 0 2px color-mix(in srgb,var(--u90-lock-accent) 16%,transparent)}.u90-lock input[aria-invalid=true]{border-color:#e99788}
24
+ .u90-lock button{width:100%;min-height:48px;margin-top:18px;border:0;border-radius:7px;background:var(--u90-lock-accent);color:var(--u90-lock-button-text);font:600 13px Arial,Helvetica,sans-serif;cursor:pointer;transition:opacity .15s}.u90-lock button:focus-visible{outline:2px solid var(--u90-lock-text);outline-offset:4px}.u90-lock button:disabled{cursor:default;opacity:.55}.u90-lock input:disabled{opacity:.6}.u90-lock-status{min-height:40px;margin:14px 0 0;font-size:12px;line-height:1.6}.u90-lock-footer{margin:46px 0 0;font-size:10px;line-height:1.7;letter-spacing:.09em;opacity:.5;text-transform:uppercase;overflow-wrap:anywhere}
25
+ @keyframes u90-lock-enter{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
26
+ @media(max-width:420px){.u90-lock{padding:44px 20px 24px}.u90-lock-brands{gap:18px;margin-bottom:44px}.u90-lock-brand{font-size:13px}.u90-lock-brand img{max-width:120px;max-height:42px}.u90-lock h1{font-size:34px}.u90-lock-message{font-size:14px;margin-bottom:30px}.u90-lock-digits{gap:7px}.u90-lock input{width:42px;height:54px}.u90-lock-footer{margin-top:34px}}
27
+ @media(prefers-reduced-motion:reduce){.u90-lock-content{animation:none}.u90-lock input,.u90-lock button{transition:none}}
28
+ `
29
+ const logo = (value: string | null) =>
30
+ value && (/^https?:\/\//i.test(value) || /^\/(?!\/)/.test(value)) ? value : undefined
31
+ const buttonText = (hex: string) => {
32
+ const n = hex.replace('#', '')
33
+ const rgb = [0, 2, 4].map((i) => parseInt(n.slice(i, i + 2), 16))
34
+ return (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000 > 150 ? '#102020' : '#ffffff'
35
+ }
36
+
37
+ /** Same component in /manage uses preview=true: contained, disabled, no effects or requests. */
38
+ export function PreviewLockScreen({
39
+ appearance,
40
+ endpoint = LOCK_ENDPOINT,
41
+ returnTo = '/',
42
+ preview = false,
43
+ }: PreviewLockScreenProps) {
44
+ const [digits, setDigits] = useState(['', '', '', '', '', '']),
45
+ [pending, setPending] = useState(false),
46
+ [message, setMessage] = useState(''),
47
+ [invalid, setInvalid] = useState(false)
48
+ const inputs = useRef<(HTMLInputElement | null)[]>([]),
49
+ busy = useRef(false),
50
+ attempt = useRef<{ code: string; id: string } | null>(null)
51
+ useEffect(() => {
52
+ if (!preview) inputs.current[0]?.focus()
53
+ }, [preview])
54
+ useEffect(() => {
55
+ if (invalid && !pending && !preview) inputs.current[0]?.focus()
56
+ }, [invalid, pending, preview])
57
+ function update(index: number, value: string) {
58
+ if (preview || busy.current) return
59
+ const numbers = value.replace(/\D/g, '').slice(0, 6)
60
+ setInvalid(false)
61
+ setMessage('')
62
+ setDigits((current) => {
63
+ const next = [...current]
64
+ if (numbers.length > 1) {
65
+ next.fill('')
66
+ numbers.split('').forEach((n, i) => {
67
+ next[i] = n
68
+ })
69
+ } else next[index] = numbers
70
+ return next
71
+ })
72
+ inputs.current[
73
+ numbers.length > 1 ? Math.min(numbers.length, 6) - 1 : Math.min(index + 1, 5)
74
+ ]?.focus()
75
+ }
76
+ function keyboard(index: number, event: KeyboardEvent<HTMLInputElement>) {
77
+ if (preview || busy.current) return
78
+ if (event.key === 'Backspace') {
79
+ event.preventDefault()
80
+ const at = digits[index] ? index : Math.max(index - 1, 0)
81
+ setDigits((current) => current.map((n, i) => (i === at ? '' : n)))
82
+ inputs.current[at]?.focus()
83
+ setInvalid(false)
84
+ setMessage('')
85
+ }
86
+ if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
87
+ event.preventDefault()
88
+ inputs.current[
89
+ Math.max(0, Math.min(5, index + (event.key === 'ArrowLeft' ? -1 : 1)))
90
+ ]?.focus()
91
+ }
92
+ }
93
+ async function submit(event: FormEvent) {
94
+ event.preventDefault()
95
+ if (preview || busy.current) return
96
+ const code = digits.join('')
97
+ if (!/^\d{6}$/.test(code)) {
98
+ setInvalid(true)
99
+ setMessage('Enter all six digits to continue.')
100
+ inputs.current[digits.findIndex((x) => !x)]?.focus()
101
+ return
102
+ }
103
+ if (!attempt.current || attempt.current.code !== code)
104
+ attempt.current = { code, id: crypto.randomUUID() }
105
+ busy.current = true
106
+ setPending(true)
107
+ setInvalid(false)
108
+ setMessage('Checking your access code…')
109
+ const controller = new AbortController(),
110
+ timeout = setTimeout(() => controller.abort(), 10000)
111
+ try {
112
+ const response = await fetch(endpoint, {
113
+ method: 'POST',
114
+ credentials: 'same-origin',
115
+ cache: 'no-store',
116
+ signal: controller.signal,
117
+ headers: { 'content-type': 'application/json' },
118
+ body: JSON.stringify({ code, requestId: attempt.current.id, path: safeReturnTo(returnTo) }),
119
+ })
120
+ const result = await response.json()
121
+ if (response.status === 428) {
122
+ setMessage(
123
+ typeof result?.error === 'string'
124
+ ? result.error.slice(0, 240)
125
+ : 'Allow cookies for this site, then try again.',
126
+ )
127
+ return
128
+ }
129
+ if (response.ok && result?.ok === true && typeof result.returnTo === 'string') {
130
+ setMessage('Welcome. Opening your preview…')
131
+ window.location.assign(safeReturnTo(result.returnTo))
132
+ return
133
+ }
134
+ if (![400, 401, 429].includes(response.status)) throw Error('Unavailable')
135
+ attempt.current = null
136
+ setInvalid(true)
137
+ setMessage(
138
+ typeof result?.error === 'string'
139
+ ? result.error.slice(0, 240)
140
+ : 'That code was not accepted. Please try again.',
141
+ )
142
+ setDigits(['', '', '', '', '', ''])
143
+ inputs.current[0]?.focus()
144
+ } catch {
145
+ setMessage('We couldn’t connect. Check your connection and try again.')
146
+ } finally {
147
+ clearTimeout(timeout)
148
+ busy.current = false
149
+ setPending(false)
150
+ }
151
+ }
152
+ const style = {
153
+ '--u90-lock-background': appearance.backgroundColor,
154
+ '--u90-lock-text': appearance.textColor,
155
+ '--u90-lock-accent': appearance.accentColor,
156
+ '--u90-lock-button-text': buttonText(appearance.accentColor),
157
+ } as CSSProperties
158
+ return (
159
+ <section
160
+ className="u90-lock"
161
+ data-u90-preview-lock
162
+ data-preview={preview ? 'true' : 'false'}
163
+ style={style}
164
+ aria-label="Private website preview"
165
+ >
166
+ <style>{css}</style>
167
+ <div className="u90-lock-content">
168
+ <div className="u90-lock-brands">
169
+ <div className="u90-lock-brand">
170
+ {logo(appearance.prospectLogoUrl) && (
171
+ <img src={logo(appearance.prospectLogoUrl)} alt="" referrerPolicy="no-referrer" />
172
+ )}
173
+ <span>{appearance.prospectName}</span>
174
+ </div>
175
+ <span className="u90-lock-cross" aria-hidden="true">
176
+ ×
177
+ </span>
178
+ <div className="u90-lock-brand">
179
+ {logo(appearance.partnerLogoUrl) && (
180
+ <img src={logo(appearance.partnerLogoUrl)} alt="" referrerPolicy="no-referrer" />
181
+ )}
182
+ <span>{appearance.partnerName}</span>
183
+ </div>
184
+ </div>
185
+ <p className="u90-lock-eyebrow">A private preview</p>
186
+ <h1>{appearance.heading}</h1>
187
+ <p className="u90-lock-message">{appearance.message}</p>
188
+ <form onSubmit={submit} aria-label="Preview access code">
189
+ <p id="u90-lock-instructions" className="u90-lock-instructions">
190
+ {appearance.instructions}
191
+ </p>
192
+ <div className="u90-lock-digits">
193
+ {digits.map((digit, index) => (
194
+ <input
195
+ key={index}
196
+ ref={(element) => {
197
+ inputs.current[index] = element
198
+ }}
199
+ type="text"
200
+ inputMode="numeric"
201
+ autoComplete={index === 0 ? 'one-time-code' : 'off'}
202
+ maxLength={6}
203
+ aria-label={`Digit ${index + 1}`}
204
+ aria-describedby="u90-lock-instructions u90-lock-status"
205
+ aria-invalid={invalid}
206
+ value={digit}
207
+ disabled={preview || pending}
208
+ onFocus={(event) => event.target.select()}
209
+ onChange={(event) => update(index, event.target.value)}
210
+ onKeyDown={(event) => keyboard(index, event)}
211
+ onPaste={(event) => {
212
+ event.preventDefault()
213
+ update(index, event.clipboardData.getData('text'))
214
+ }}
215
+ />
216
+ ))}
217
+ </div>
218
+ <button type="submit" disabled={preview || pending}>
219
+ {pending ? 'Checking…' : 'Enter preview'}
220
+ </button>
221
+ <p id="u90-lock-status" className="u90-lock-status" role="status" aria-live="polite">
222
+ {message ||
223
+ (preview
224
+ ? 'Appearance preview · access codes are disabled'
225
+ : 'Access lasts for one hour.')}
226
+ </p>
227
+ </form>
228
+ </div>
229
+ <p className="u90-lock-footer">Presented &amp; built by {appearance.partnerName}</p>
230
+ </section>
231
+ )
232
+ }
233
+ /** Remove the bounded handoff from browser history before onward navigation. */
234
+ export function PreviewHandoffCleanup() {
235
+ useEffect(() => {
236
+ const url = new URL(window.location.href)
237
+ if (url.searchParams.has(HANDOFF_QUERY)) {
238
+ url.searchParams.delete(HANDOFF_QUERY)
239
+ window.history.replaceState(window.history.state, '', url.pathname + url.search + url.hash)
240
+ }
241
+ }, [])
242
+ return null
243
+ }
244
+ export type { PreviewLockAppearance }