@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.
- package/README.md +30 -2
- package/package.json +79 -4
- package/src/bridge/DraftBlocks.tsx +70 -0
- package/src/bridge/DraftEntries.tsx +41 -0
- package/src/bridge/PreviewCommitBoundary.tsx +19 -0
- package/src/bridge/U90Bridge.tsx +54 -0
- package/src/bridge/caret.ts +317 -0
- package/src/bridge/client.tsx +66 -0
- package/src/bridge/createBridge.ts +1015 -0
- package/src/bridge/hooks.tsx +56 -0
- package/src/bridge/index.ts +9 -0
- package/src/bridge/lexical.ts +177 -0
- package/src/bridge/lexicalDom.ts +212 -0
- package/src/bridge/preserveBlockMoves.ts +88 -0
- package/src/bridge/preview.ts +179 -0
- package/src/bridge/protocol.ts +273 -0
- package/src/bridge/registry.ts +30 -0
- package/src/bridge/styles.ts +137 -0
- package/src/components/DateText.tsx +9 -0
- package/src/components/Entries.tsx +16 -0
- package/src/components/File.tsx +9 -0
- package/src/components/Image.tsx +30 -0
- package/src/components/Link.tsx +24 -0
- package/src/components/NumberText.tsx +9 -0
- package/src/components/RichText.tsx +47 -0
- package/src/components/Text.tsx +15 -0
- package/src/components/Video.tsx +12 -0
- package/src/components/VideoPlayback.tsx +19 -0
- package/src/components/draft.ts +9 -0
- package/src/components/imageProps.ts +26 -0
- package/src/components/index.ts +13 -0
- package/src/components/videoProps.ts +6 -0
- package/src/elements/containers/group.ts +33 -0
- package/src/elements/containers/list.ts +31 -0
- package/src/elements/containers/zone.ts +41 -0
- package/src/elements/declaration.ts +118 -0
- package/src/elements/define.ts +195 -0
- package/src/elements/ids.ts +50 -0
- package/src/elements/index.ts +35 -0
- package/src/elements/jsonSchema.ts +36 -0
- package/src/elements/leaf.ts +98 -0
- package/src/elements/leaves/boolean.ts +30 -0
- package/src/elements/leaves/choice.ts +53 -0
- package/src/elements/leaves/date.ts +39 -0
- package/src/elements/leaves/file.ts +56 -0
- package/src/elements/leaves/image.ts +160 -0
- package/src/elements/leaves/link.ts +149 -0
- package/src/elements/leaves/number.ts +52 -0
- package/src/elements/leaves/query.ts +101 -0
- package/src/elements/leaves/relation.ts +69 -0
- package/src/elements/leaves/rich-text.ts +155 -0
- package/src/elements/leaves/selection.ts +40 -0
- package/src/elements/leaves/single.ts +29 -0
- package/src/elements/leaves/text.ts +103 -0
- package/src/elements/leaves/video.ts +108 -0
- package/src/elements/ops/expand.ts +94 -0
- package/src/elements/ops/overlay.ts +59 -0
- package/src/elements/ops/refs.ts +45 -0
- package/src/elements/ops/seed.ts +185 -0
- package/src/elements/ops/text.ts +20 -0
- package/src/elements/ops/translatable.ts +19 -0
- package/src/elements/ops/upgrade.ts +43 -0
- package/src/elements/ops/validate.ts +56 -0
- package/src/elements/order.ts +10 -0
- package/src/elements/path.ts +43 -0
- package/src/elements/prefix.ts +2 -0
- package/src/elements/registry.ts +48 -0
- package/src/elements/tokens.ts +14 -0
- package/src/elements/walk.ts +42 -0
- package/src/features/client/AskAiProvider.tsx +551 -0
- package/src/features/client/AskAiWidget.tsx +222 -0
- package/src/features/client/ConsentRuntime.tsx +95 -0
- package/src/features/client/FeedbackProvider.tsx +231 -0
- package/src/features/client/FeedbackWidget.tsx +101 -0
- package/src/features/client/PageReporter.tsx +26 -0
- package/src/features/client/README.md +131 -0
- package/src/features/client/SiteFeatures.tsx +250 -0
- package/src/features/client/askAiRecovery.ts +92 -0
- package/src/features/client/askAiStream.ts +59 -0
- package/src/features/client/context.tsx +24 -0
- package/src/features/client/index.ts +24 -0
- package/src/features/client/overlay.tsx +129 -0
- package/src/features/client/styles.css +337 -0
- package/src/features/client/transport.ts +310 -0
- package/src/features/client/useAskAI.ts +45 -0
- package/src/features/client/useConsent.ts +15 -0
- package/src/features/client/useFeedback.ts +33 -0
- package/src/features/client/useSiteSearch.ts +50 -0
- package/src/features/client/verification.ts +141 -0
- package/src/features/consent/README.md +70 -0
- package/src/features/consent/cookieScript.ts +53 -0
- package/src/features/consent/custom.ts +89 -0
- package/src/features/consent/defaults.ts +40 -0
- package/src/features/consent/drivers.ts +233 -0
- package/src/features/consent/loader.ts +248 -0
- package/src/features/consent/registry.ts +398 -0
- package/src/features/contracts/askAi.ts +166 -0
- package/src/features/contracts/askAiSettings.ts +42 -0
- package/src/features/contracts/base.ts +107 -0
- package/src/features/contracts/consent.ts +150 -0
- package/src/features/contracts/feedback.ts +22 -0
- package/src/features/contracts/feedbackSettings.ts +49 -0
- package/src/features/contracts/index.ts +61 -0
- package/src/features/contracts/knowledge.ts +99 -0
- package/src/features/contracts/knowledgePolicy.ts +249 -0
- package/src/features/contracts/search.ts +24 -0
- package/src/features/contracts/verification.ts +52 -0
- package/src/features/contracts/verificationConfig.ts +9 -0
- package/src/features/server/ConsentDefaults.tsx +15 -0
- package/src/features/server/README.md +185 -0
- package/src/features/server/gateway.ts +318 -0
- package/src/features/server/http.ts +179 -0
- package/src/features/server/index.ts +6 -0
- package/src/features/server/session.ts +23 -0
- package/src/features/server/verification.ts +39 -0
- package/src/features/server/verifyVisitor.ts +69 -0
- package/src/languages/config.d.mts +5 -0
- package/src/languages/config.mjs +53 -0
- package/src/languages/decide.ts +72 -0
- package/src/languages/index.ts +4 -0
- package/src/languages/proxy.ts +108 -0
- package/src/languages/routing.ts +93 -0
- package/src/languages/wire.ts +19 -0
- package/src/legacy/bridge.tsx +1987 -0
- package/src/legacy/caret.ts +246 -0
- package/src/legacy/cli.mjs +189 -0
- package/src/legacy/content.ts +993 -0
- package/src/legacy/contract.ts +664 -0
- package/src/legacy/defineBlocks.ts +1602 -0
- package/src/legacy/defineSite.ts +623 -0
- package/src/legacy/defineSite.types.ts +270 -0
- package/src/legacy/draft.tsx +128 -0
- package/src/legacy/image.ts +433 -0
- package/src/legacy/index.ts +209 -0
- package/src/legacy/language-switcher.tsx +58 -0
- package/src/legacy/languageHints.ts +153 -0
- package/src/legacy/languages/fields.ts +131 -0
- package/src/legacy/languages/hash.ts +98 -0
- package/src/legacy/languages/health.ts +97 -0
- package/src/legacy/languages/identity.ts +247 -0
- package/src/legacy/languages/links.ts +81 -0
- package/src/legacy/languages/resolution.ts +56 -0
- package/src/legacy/languages/routing.ts +169 -0
- package/src/legacy/languages/runtime.ts +216 -0
- package/src/legacy/languages/seo.ts +95 -0
- package/src/legacy/languages/settings.ts +211 -0
- package/src/legacy/languages/types.ts +85 -0
- package/src/legacy/languages.ts +8 -0
- package/src/legacy/lexical.ts +170 -0
- package/src/legacy/lexicalDom.ts +166 -0
- package/src/legacy/media.ts +272 -0
- package/src/legacy/nav.ts +176 -0
- package/src/legacy/preview.ts +79 -0
- package/src/legacy/redirects.ts +474 -0
- package/src/legacy/richText.tsx +104 -0
- package/src/legacy/routes.ts +414 -0
- package/src/legacy/seo.ts +327 -0
- package/src/legacy/shape.ts +412 -0
- package/src/legacy/uploads.ts +233 -0
- package/src/legacy/verification.ts +42 -0
- package/src/preview-lock/contracts.ts +42 -0
- package/src/preview-lock/index.ts +3 -0
- package/src/preview-lock/screen.tsx +244 -0
- package/src/preview-lock/server.ts +416 -0
- package/src/redirects/config.d.mts +5 -0
- package/src/redirects/config.mjs +22 -0
- package/src/redirects/index.ts +3 -0
- package/src/redirects/location.ts +31 -0
- package/src/redirects/match.ts +199 -0
- package/src/redirects/proxy.ts +91 -0
- package/src/redirects/wire.ts +30 -0
- package/src/seo/jsonLd.ts +70 -0
- package/src/seo/metadata.ts +127 -0
- package/src/seo/robots.ts +14 -0
- package/src/seo/sitemap.ts +22 -0
- package/src/seo/title.ts +22 -0
- package/src/server/adapters.ts +73 -0
- package/src/server/createU90.ts +96 -0
- package/src/server/handlers.ts +164 -0
- package/src/server/index.ts +6 -0
- package/src/server/read.ts +207 -0
- package/src/server/render.tsx +46 -0
- package/src/server/routes.tsx +171 -0
- package/src/site/assemble.ts +109 -0
- package/src/site/defineSite.ts +132 -0
- package/src/site/documents.ts +18 -0
- package/src/site/fields.ts +59 -0
- package/src/site/index.ts +14 -0
- package/src/site/replies.ts +118 -0
- package/src/site/store.ts +45 -0
- package/src/site/types.ts +75 -0
- package/src/site/version.ts +1 -0
- package/src/site/wire.ts +125 -0
- package/src/switcher/LanguageSwitcher.tsx +42 -0
- package/src/test-preview/README.md +90 -0
- package/src/test-preview/binding.ts +54 -0
- package/src/test-preview/bridge.ts +16 -0
- package/src/test-preview/guard.ts +186 -0
- package/src/test-preview/media.ts +23 -0
- package/src/test-preview/protocol.d.mts +67 -0
- package/src/test-preview/protocol.mjs +222 -0
- package/src/test-preview/runtime.ts +168 -0
- package/src/test-preview/server.ts +10 -0
- package/src/testing/fake.ts +148 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/seed.ts +2 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { PageDeclaration } from './contract'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deriving a site's page declarations from the navigation it already has.
|
|
5
|
+
*
|
|
6
|
+
* A site's information architecture is already written down — it is the header nav, and
|
|
7
|
+
* usually a `brand.ts` array of `{ label, href, items }`. Asking a site to *also* hand-write
|
|
8
|
+
* a flat list of `{ label, slug, parent }` for the manifest would be asking it to maintain
|
|
9
|
+
* the same tree twice, and the copy the platform reads would be the one that goes stale.
|
|
10
|
+
* JDSi's nav is forty routes deep in three levels; nobody is keeping a second copy of that
|
|
11
|
+
* correct by hand.
|
|
12
|
+
*
|
|
13
|
+
* So the manifest's `pages` is generated from the nav, and the nav stays the single place a
|
|
14
|
+
* route is added. What this file does is entirely mechanical — flatten, resolve, dedupe —
|
|
15
|
+
* and every rule below exists because a real nav has entries in it that are not pages.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A nav entry, as loosely as a site is likely to have written one.
|
|
20
|
+
*
|
|
21
|
+
* Every field optional and unknown keys allowed on purpose: a real nav carries `blurb`,
|
|
22
|
+
* `image`, `external` and whatever else its own menu renders, and a site should be able to
|
|
23
|
+
* pass `brand.nav` straight in without reshaping it. Anything not named here is ignored.
|
|
24
|
+
*/
|
|
25
|
+
export type NavEntry = {
|
|
26
|
+
href?: null | string
|
|
27
|
+
items?: readonly NavEntry[]
|
|
28
|
+
label?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The shape the platform accepts for a page slug.
|
|
33
|
+
*
|
|
34
|
+
* A deliberate duplicate of `PAGE_SLUG` in the platform's `manifest/validate.ts`, which is
|
|
35
|
+
* the authority. It is repeated here because of what happens when the two disagree: **one bad
|
|
36
|
+
* slug rejects the entire manifest**, taking the tenant's blocks and their whole editor with
|
|
37
|
+
* it, and disqualifying the cached fallback because an invalid manifest is not "the site is
|
|
38
|
+
* unreachable". A helper that is the documented way to produce `pages` must not be able to do
|
|
39
|
+
* that to the site that uses it.
|
|
40
|
+
*/
|
|
41
|
+
const PAGE_SLUG = /^[a-z0-9]+(?:[/-][a-z0-9]+)*$/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A nav href as a page slug, or null when the entry is not a page of its own.
|
|
45
|
+
*
|
|
46
|
+
* Five things get rejected, and each one is in a real nav:
|
|
47
|
+
*
|
|
48
|
+
* - **A fragment link.** `/careers#how-to-apply` is a position on the careers page, not a
|
|
49
|
+
* page. Listed as one it would appear in the sitemap as a page with no content that can
|
|
50
|
+
* never be published, and "add a page here" would offer to create a route the site does
|
|
51
|
+
* not serve.
|
|
52
|
+
* - **An external link.** `https://linkedin.com/…` is not this site's to route.
|
|
53
|
+
* - **A group with no href.** A label that only opens a submenu is a menu affordance; its
|
|
54
|
+
* children belong to whatever the group itself sits under.
|
|
55
|
+
* - **A query string.** Same route, different state.
|
|
56
|
+
* - **Anything still not slug-shaped** after normalising — an underscore, an ampersand, an
|
|
57
|
+
* encoded character, a space. Dropped rather than emitted, because emitting it would take
|
|
58
|
+
* down the whole manifest (see `PAGE_SLUG`), and because a route the platform cannot name
|
|
59
|
+
* is one it could not offer to edit anyway. The site keeps serving it either way; it
|
|
60
|
+
* simply does not appear on the client's map.
|
|
61
|
+
*
|
|
62
|
+
* Case and repeated slashes are *normalised* rather than rejected, since `/About-Us` and
|
|
63
|
+
* `/about//us` name routes the platform can perfectly well express and a site that writes
|
|
64
|
+
* either meant the ordinary thing.
|
|
65
|
+
*/
|
|
66
|
+
const slugFromHref = (href: null | string | undefined): null | string => {
|
|
67
|
+
if (typeof href !== 'string') return null
|
|
68
|
+
|
|
69
|
+
const trimmed = href.trim()
|
|
70
|
+
if (trimmed.length === 0) return null
|
|
71
|
+
// `//host` is protocol-relative and therefore external, despite starting with a slash.
|
|
72
|
+
if (trimmed.startsWith('//') || /^[a-z][a-z0-9+.-]*:/i.test(trimmed)) return null
|
|
73
|
+
if (!trimmed.startsWith('/')) return null
|
|
74
|
+
if (trimmed.includes('#') || trimmed.includes('?')) return null
|
|
75
|
+
|
|
76
|
+
const slug = trimmed
|
|
77
|
+
.toLowerCase()
|
|
78
|
+
// `/about//us` and `/about/us/` are the same route as `/about/us`.
|
|
79
|
+
.replace(/\/+/g, '/')
|
|
80
|
+
.replace(/^\/+/, '')
|
|
81
|
+
.replace(/\/+$/, '')
|
|
82
|
+
|
|
83
|
+
/*
|
|
84
|
+
* The landing page is `home`, not the empty string.
|
|
85
|
+
*
|
|
86
|
+
* That is the slug the platform's own page documents use for it — the `pages` collection
|
|
87
|
+
* describes it as such and every site's seed is written against it — so emitting `''`
|
|
88
|
+
* here would produce a landing page that never matched the document holding its content.
|
|
89
|
+
*/
|
|
90
|
+
if (slug.length === 0) return 'home'
|
|
91
|
+
|
|
92
|
+
return PAGE_SLUG.test(slug) ? slug : null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Flatten a nav tree into the page declarations a manifest publishes.
|
|
97
|
+
*
|
|
98
|
+
* Pass the nav with the landing page in front of it, which is the whole call:
|
|
99
|
+
*
|
|
100
|
+
* ```ts
|
|
101
|
+
* export const pages = pagesFromNav([
|
|
102
|
+
* { label: 'Home', href: '/' },
|
|
103
|
+
* ...brand.nav,
|
|
104
|
+
* ...brand.navUtility,
|
|
105
|
+
* ])
|
|
106
|
+
* ```
|
|
107
|
+
*
|
|
108
|
+
* Structure is preserved: an entry's `items` become its children, so the platform's sitemap
|
|
109
|
+
* is the menu the visitor sees. Order is preserved too — the manifest's order is the order
|
|
110
|
+
* the sitemap draws, and nav order is the site's own considered answer to what comes first.
|
|
111
|
+
*
|
|
112
|
+
* **A slug appears once, at the shallowest place the nav mentions it.** Nav trees repeat
|
|
113
|
+
* themselves constantly: every JDSi submenu opens with an "Overview" pointing back at the
|
|
114
|
+
* section landing page it hangs under, and `/resources` appears in the header, in a hero slide
|
|
115
|
+
* and in a footer column. Deduping means a site can concatenate every list of links it has
|
|
116
|
+
* without producing a sitemap with four "Overview" nodes under each section.
|
|
117
|
+
*
|
|
118
|
+
* *Shallowest* rather than first, so the result does not depend on the order a site happens to
|
|
119
|
+
* concatenate its lists in. A page cross-linked from deep inside one section and also present
|
|
120
|
+
* as a section of its own belongs at the top level — first-occurrence would have filed
|
|
121
|
+
* "Work With Us" under "Who We Are" purely because a link to it appeared there first, and
|
|
122
|
+
* reordering a footer column would silently reshape the client's map. At equal depth the first
|
|
123
|
+
* still wins, and the shallowest mention is also the one carrying the real label — the deeper
|
|
124
|
+
* repeat is the one called "Overview".
|
|
125
|
+
*/
|
|
126
|
+
export const pagesFromNav = (nav: readonly NavEntry[]): PageDeclaration[] => {
|
|
127
|
+
/** Insertion-ordered, so declaration order stays nav order. Depth is kept to compare repeats. */
|
|
128
|
+
const found = new Map<string, { depth: number; page: PageDeclaration }>()
|
|
129
|
+
|
|
130
|
+
const walk = (entries: readonly NavEntry[], parent: string | undefined, depth: number): void => {
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
if (!entry || typeof entry !== 'object') continue
|
|
133
|
+
|
|
134
|
+
const slug = slugFromHref(entry.href)
|
|
135
|
+
|
|
136
|
+
/*
|
|
137
|
+
* A group with no page of its own passes its parent down rather than becoming one.
|
|
138
|
+
*
|
|
139
|
+
* Without this its children would be orphaned to the top level, and a nav that groups
|
|
140
|
+
* "Services" purely as a heading would flatten every service into a root page. Its
|
|
141
|
+
* depth is not spent either — the children sit where the group sits.
|
|
142
|
+
*/
|
|
143
|
+
const inherited = slug === null ? parent : slug
|
|
144
|
+
|
|
145
|
+
if (slug !== null) {
|
|
146
|
+
const existing = found.get(slug)
|
|
147
|
+
|
|
148
|
+
if (!existing || depth < existing.depth) {
|
|
149
|
+
const page: PageDeclaration = {
|
|
150
|
+
label:
|
|
151
|
+
typeof entry.label === 'string' && entry.label.trim().length > 0
|
|
152
|
+
? entry.label.trim()
|
|
153
|
+
: slug,
|
|
154
|
+
// Omitted rather than undefined-valued, so the emitted JSON has no null parents.
|
|
155
|
+
...(parent === undefined || parent === slug ? {} : { parent }),
|
|
156
|
+
slug,
|
|
157
|
+
}
|
|
158
|
+
/*
|
|
159
|
+
* `set` on an existing key keeps its original position in the Map, which is what
|
|
160
|
+
* should happen: a shallower mention corrects a page's *parent and label*, not the
|
|
161
|
+
* order its section reads in.
|
|
162
|
+
*/
|
|
163
|
+
found.set(slug, { depth, page })
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (Array.isArray(entry.items) && entry.items.length > 0) {
|
|
168
|
+
walk(entry.items, inherited, slug === null ? depth : depth + 1)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
walk(nav, undefined, 0)
|
|
174
|
+
|
|
175
|
+
return [...found.values()].map((entry) => entry.page)
|
|
176
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long a preview lasts, and what says so.
|
|
3
|
+
*
|
|
4
|
+
* Draft mode is two cookies — Next's own `__prerender_bypass` and the `payload-token`
|
|
5
|
+
* this kit stashes — and both are *session* cookies with no expiry. `createExitPreviewRoute`
|
|
6
|
+
* clears them, and `DraftNotice` offers the link that calls it, but both of those need
|
|
7
|
+
* somebody to click. Nothing clicks on behalf of a browser whose tab was closed, whose
|
|
8
|
+
* machine slept, or which was framed inside Canvas — where the notice deliberately hides
|
|
9
|
+
* itself, so the exit is not on offer at all.
|
|
10
|
+
*
|
|
11
|
+
* The consequence is not theoretical. A browser left in draft mode reads staged content on
|
|
12
|
+
* every page of the site, forever: modern browsers restore sessions rather than ending
|
|
13
|
+
* them, so "until it is quit" never arrives. Once the staged content it is reading is
|
|
14
|
+
* discarded or its token goes stale, that one browser sees a broken or empty version of a
|
|
15
|
+
* site which is perfectly healthy for everybody else — including, when it is the client's
|
|
16
|
+
* browser, on their own live domain.
|
|
17
|
+
*
|
|
18
|
+
* So the window is stamped into a cookie of our own and checked on read. A preview that
|
|
19
|
+
* nobody exits now lapses on its own, and the failure mode is the safe direction: the
|
|
20
|
+
* published site, which is what a visitor sees anyway.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Next's token, stashed so a preview reads drafts as the signed-in client. */
|
|
24
|
+
export const PREVIEW_TOKEN_COOKIE = 'payload-token'
|
|
25
|
+
|
|
26
|
+
/** When the current preview stops counting, as epoch milliseconds in a cookie. */
|
|
27
|
+
export const PREVIEW_UNTIL_COOKIE = 'u90-preview-until'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Long enough for a working session, short enough that a forgotten tab heals by morning.
|
|
31
|
+
*
|
|
32
|
+
* Erring long is the wrong way to err: the cost of a window that is too short is one
|
|
33
|
+
* re-click of Preview, and the cost of one that is too long is the failure above.
|
|
34
|
+
*/
|
|
35
|
+
export const PREVIEW_MAX_AGE_SECONDS = 8 * 60 * 60
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether a stamp means "no longer previewing".
|
|
39
|
+
*
|
|
40
|
+
* A missing stamp counts as closed, which is what makes this self-healing: a browser that
|
|
41
|
+
* entered draft mode before this existed has no stamp, so the first request after the
|
|
42
|
+
* deploy puts it back on the published site without anyone knowing to clear anything.
|
|
43
|
+
* Unparseable counts as closed for the same reason — the only value worth trusting is a
|
|
44
|
+
* deadline we can actually read, still in the future.
|
|
45
|
+
*/
|
|
46
|
+
export const previewWindowClosed = (stamp: undefined | string, now = Date.now()): boolean => {
|
|
47
|
+
if (!stamp) return true
|
|
48
|
+
const deadline = Number(stamp)
|
|
49
|
+
return !Number.isFinite(deadline) || deadline <= now
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** One frame's locale and generation. Revision changes when its locale/context is replaced. */
|
|
53
|
+
export type LanguageFrameContext = { locale: string; revision: number }
|
|
54
|
+
export function acceptsLanguageFrame(
|
|
55
|
+
context: LanguageFrameContext | undefined,
|
|
56
|
+
message: Record<string, unknown>,
|
|
57
|
+
): boolean {
|
|
58
|
+
return (
|
|
59
|
+
!context || (message.locale === context.locale && message.languageRevision === context.revision)
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Preserve a preview artboard's generation through same-origin site navigation. */
|
|
64
|
+
export function languageFrameNavigation(
|
|
65
|
+
href: string,
|
|
66
|
+
base: string,
|
|
67
|
+
context: LanguageFrameContext | undefined,
|
|
68
|
+
): string | null {
|
|
69
|
+
if (!context) return null
|
|
70
|
+
try {
|
|
71
|
+
const target = new URL(href, base)
|
|
72
|
+
if (target.origin !== new URL(base).origin || !['http:', 'https:'].includes(target.protocol))
|
|
73
|
+
return null
|
|
74
|
+
target.searchParams.set('languageRevision', String(context.revision))
|
|
75
|
+
return target.href
|
|
76
|
+
} catch {
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
}
|