@unitninety/site-kit 0.0.0-stage → 1.6.1

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 (204) hide show
  1. package/README.md +12 -2
  2. package/package.json +79 -4
  3. package/src/bridge/DraftBlocks.tsx +70 -0
  4. package/src/bridge/PreviewCommitBoundary.tsx +19 -0
  5. package/src/bridge/U90Bridge.tsx +54 -0
  6. package/src/bridge/caret.ts +317 -0
  7. package/src/bridge/client.tsx +66 -0
  8. package/src/bridge/createBridge.ts +1010 -0
  9. package/src/bridge/hooks.tsx +56 -0
  10. package/src/bridge/index.ts +9 -0
  11. package/src/bridge/lexical.ts +177 -0
  12. package/src/bridge/lexicalDom.ts +212 -0
  13. package/src/bridge/preserveBlockMoves.ts +88 -0
  14. package/src/bridge/preview.ts +179 -0
  15. package/src/bridge/protocol.ts +271 -0
  16. package/src/bridge/registry.ts +27 -0
  17. package/src/bridge/styles.ts +137 -0
  18. package/src/components/DateText.tsx +9 -0
  19. package/src/components/File.tsx +9 -0
  20. package/src/components/Image.tsx +30 -0
  21. package/src/components/Link.tsx +24 -0
  22. package/src/components/NumberText.tsx +9 -0
  23. package/src/components/RichText.tsx +47 -0
  24. package/src/components/Text.tsx +15 -0
  25. package/src/components/Video.tsx +12 -0
  26. package/src/components/VideoPlayback.tsx +19 -0
  27. package/src/components/draft.ts +9 -0
  28. package/src/components/imageProps.ts +26 -0
  29. package/src/components/index.ts +12 -0
  30. package/src/components/videoProps.ts +6 -0
  31. package/src/elements/containers/group.ts +33 -0
  32. package/src/elements/containers/list.ts +31 -0
  33. package/src/elements/containers/zone.ts +41 -0
  34. package/src/elements/declaration.ts +118 -0
  35. package/src/elements/define.ts +195 -0
  36. package/src/elements/ids.ts +50 -0
  37. package/src/elements/index.ts +35 -0
  38. package/src/elements/jsonSchema.ts +36 -0
  39. package/src/elements/leaf.ts +98 -0
  40. package/src/elements/leaves/boolean.ts +30 -0
  41. package/src/elements/leaves/choice.ts +53 -0
  42. package/src/elements/leaves/date.ts +39 -0
  43. package/src/elements/leaves/file.ts +56 -0
  44. package/src/elements/leaves/image.ts +160 -0
  45. package/src/elements/leaves/link.ts +149 -0
  46. package/src/elements/leaves/number.ts +52 -0
  47. package/src/elements/leaves/query.ts +101 -0
  48. package/src/elements/leaves/relation.ts +69 -0
  49. package/src/elements/leaves/rich-text.ts +155 -0
  50. package/src/elements/leaves/selection.ts +40 -0
  51. package/src/elements/leaves/single.ts +29 -0
  52. package/src/elements/leaves/text.ts +103 -0
  53. package/src/elements/leaves/video.ts +108 -0
  54. package/src/elements/ops/expand.ts +94 -0
  55. package/src/elements/ops/overlay.ts +59 -0
  56. package/src/elements/ops/refs.ts +45 -0
  57. package/src/elements/ops/seed.ts +185 -0
  58. package/src/elements/ops/text.ts +20 -0
  59. package/src/elements/ops/translatable.ts +19 -0
  60. package/src/elements/ops/upgrade.ts +43 -0
  61. package/src/elements/ops/validate.ts +56 -0
  62. package/src/elements/order.ts +10 -0
  63. package/src/elements/path.ts +43 -0
  64. package/src/elements/prefix.ts +2 -0
  65. package/src/elements/registry.ts +48 -0
  66. package/src/elements/tokens.ts +14 -0
  67. package/src/elements/walk.ts +42 -0
  68. package/src/features/client/AskAiProvider.tsx +551 -0
  69. package/src/features/client/AskAiWidget.tsx +222 -0
  70. package/src/features/client/ConsentRuntime.tsx +95 -0
  71. package/src/features/client/FeedbackProvider.tsx +231 -0
  72. package/src/features/client/FeedbackWidget.tsx +101 -0
  73. package/src/features/client/PageReporter.tsx +26 -0
  74. package/src/features/client/README.md +131 -0
  75. package/src/features/client/SiteFeatures.tsx +250 -0
  76. package/src/features/client/askAiRecovery.ts +92 -0
  77. package/src/features/client/askAiStream.ts +59 -0
  78. package/src/features/client/context.tsx +24 -0
  79. package/src/features/client/index.ts +24 -0
  80. package/src/features/client/overlay.tsx +129 -0
  81. package/src/features/client/styles.css +337 -0
  82. package/src/features/client/transport.ts +310 -0
  83. package/src/features/client/useAskAI.ts +45 -0
  84. package/src/features/client/useConsent.ts +15 -0
  85. package/src/features/client/useFeedback.ts +33 -0
  86. package/src/features/client/useSiteSearch.ts +50 -0
  87. package/src/features/client/verification.ts +141 -0
  88. package/src/features/consent/README.md +70 -0
  89. package/src/features/consent/cookieScript.ts +53 -0
  90. package/src/features/consent/custom.ts +89 -0
  91. package/src/features/consent/defaults.ts +40 -0
  92. package/src/features/consent/drivers.ts +233 -0
  93. package/src/features/consent/loader.ts +248 -0
  94. package/src/features/consent/registry.ts +398 -0
  95. package/src/features/contracts/askAi.ts +166 -0
  96. package/src/features/contracts/askAiSettings.ts +42 -0
  97. package/src/features/contracts/base.ts +107 -0
  98. package/src/features/contracts/consent.ts +150 -0
  99. package/src/features/contracts/feedback.ts +22 -0
  100. package/src/features/contracts/feedbackSettings.ts +49 -0
  101. package/src/features/contracts/index.ts +61 -0
  102. package/src/features/contracts/knowledge.ts +99 -0
  103. package/src/features/contracts/knowledgePolicy.ts +249 -0
  104. package/src/features/contracts/search.ts +24 -0
  105. package/src/features/contracts/verification.ts +52 -0
  106. package/src/features/contracts/verificationConfig.ts +9 -0
  107. package/src/features/server/ConsentDefaults.tsx +15 -0
  108. package/src/features/server/README.md +185 -0
  109. package/src/features/server/gateway.ts +318 -0
  110. package/src/features/server/http.ts +179 -0
  111. package/src/features/server/index.ts +6 -0
  112. package/src/features/server/session.ts +23 -0
  113. package/src/features/server/verification.ts +39 -0
  114. package/src/features/server/verifyVisitor.ts +69 -0
  115. package/src/languages/config.d.mts +5 -0
  116. package/src/languages/config.mjs +53 -0
  117. package/src/languages/decide.ts +72 -0
  118. package/src/languages/index.ts +4 -0
  119. package/src/languages/proxy.ts +108 -0
  120. package/src/languages/routing.ts +93 -0
  121. package/src/languages/wire.ts +19 -0
  122. package/src/legacy/bridge.tsx +1987 -0
  123. package/src/legacy/caret.ts +246 -0
  124. package/src/legacy/cli.mjs +189 -0
  125. package/src/legacy/content.ts +993 -0
  126. package/src/legacy/contract.ts +664 -0
  127. package/src/legacy/defineBlocks.ts +1602 -0
  128. package/src/legacy/defineSite.ts +623 -0
  129. package/src/legacy/defineSite.types.ts +270 -0
  130. package/src/legacy/draft.tsx +128 -0
  131. package/src/legacy/image.ts +433 -0
  132. package/src/legacy/index.ts +209 -0
  133. package/src/legacy/language-switcher.tsx +58 -0
  134. package/src/legacy/languageHints.ts +153 -0
  135. package/src/legacy/languages/fields.ts +131 -0
  136. package/src/legacy/languages/hash.ts +98 -0
  137. package/src/legacy/languages/health.ts +97 -0
  138. package/src/legacy/languages/identity.ts +247 -0
  139. package/src/legacy/languages/links.ts +81 -0
  140. package/src/legacy/languages/resolution.ts +56 -0
  141. package/src/legacy/languages/routing.ts +169 -0
  142. package/src/legacy/languages/runtime.ts +216 -0
  143. package/src/legacy/languages/seo.ts +95 -0
  144. package/src/legacy/languages/settings.ts +211 -0
  145. package/src/legacy/languages/types.ts +85 -0
  146. package/src/legacy/languages.ts +8 -0
  147. package/src/legacy/lexical.ts +170 -0
  148. package/src/legacy/lexicalDom.ts +166 -0
  149. package/src/legacy/media.ts +272 -0
  150. package/src/legacy/nav.ts +176 -0
  151. package/src/legacy/preview.ts +79 -0
  152. package/src/legacy/redirects.ts +474 -0
  153. package/src/legacy/richText.tsx +104 -0
  154. package/src/legacy/routes.ts +414 -0
  155. package/src/legacy/seo.ts +327 -0
  156. package/src/legacy/shape.ts +412 -0
  157. package/src/legacy/uploads.ts +233 -0
  158. package/src/legacy/verification.ts +42 -0
  159. package/src/preview-lock/contracts.ts +42 -0
  160. package/src/preview-lock/index.ts +3 -0
  161. package/src/preview-lock/screen.tsx +244 -0
  162. package/src/preview-lock/server.ts +416 -0
  163. package/src/redirects/config.d.mts +5 -0
  164. package/src/redirects/config.mjs +22 -0
  165. package/src/redirects/index.ts +3 -0
  166. package/src/redirects/location.ts +31 -0
  167. package/src/redirects/match.ts +199 -0
  168. package/src/redirects/proxy.ts +91 -0
  169. package/src/redirects/wire.ts +30 -0
  170. package/src/seo/jsonLd.ts +70 -0
  171. package/src/seo/metadata.ts +127 -0
  172. package/src/seo/robots.ts +14 -0
  173. package/src/seo/sitemap.ts +22 -0
  174. package/src/seo/title.ts +22 -0
  175. package/src/server/adapters.ts +73 -0
  176. package/src/server/createU90.ts +96 -0
  177. package/src/server/handlers.ts +164 -0
  178. package/src/server/index.ts +6 -0
  179. package/src/server/read.ts +207 -0
  180. package/src/server/render.tsx +46 -0
  181. package/src/server/routes.tsx +171 -0
  182. package/src/site/assemble.ts +109 -0
  183. package/src/site/defineSite.ts +132 -0
  184. package/src/site/documents.ts +18 -0
  185. package/src/site/fields.ts +59 -0
  186. package/src/site/index.ts +14 -0
  187. package/src/site/replies.ts +118 -0
  188. package/src/site/store.ts +45 -0
  189. package/src/site/types.ts +75 -0
  190. package/src/site/version.ts +1 -0
  191. package/src/site/wire.ts +125 -0
  192. package/src/switcher/LanguageSwitcher.tsx +42 -0
  193. package/src/test-preview/README.md +90 -0
  194. package/src/test-preview/binding.ts +54 -0
  195. package/src/test-preview/bridge.ts +16 -0
  196. package/src/test-preview/guard.ts +186 -0
  197. package/src/test-preview/media.ts +23 -0
  198. package/src/test-preview/protocol.d.mts +67 -0
  199. package/src/test-preview/protocol.mjs +222 -0
  200. package/src/test-preview/runtime.ts +168 -0
  201. package/src/test-preview/server.ts +10 -0
  202. package/src/testing/fake.ts +148 -0
  203. package/src/testing/index.ts +2 -0
  204. 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
+ }