@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,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-only assertions about what `defineSite` infers.
|
|
3
|
+
*
|
|
4
|
+
* There is no test framework here and type errors do not surface at runtime, so these are
|
|
5
|
+
* asserted the only way types can be: this file must typecheck, and the lines marked
|
|
6
|
+
* `@ts-expect-error` must not. `pnpm typecheck` fails either way round — an unused
|
|
7
|
+
* `@ts-expect-error` is itself an error.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
array,
|
|
12
|
+
block,
|
|
13
|
+
blocksField,
|
|
14
|
+
collection,
|
|
15
|
+
defineSite,
|
|
16
|
+
form,
|
|
17
|
+
group,
|
|
18
|
+
link,
|
|
19
|
+
number,
|
|
20
|
+
relationship,
|
|
21
|
+
richText,
|
|
22
|
+
select,
|
|
23
|
+
textField as text,
|
|
24
|
+
upload,
|
|
25
|
+
} from './index'
|
|
26
|
+
|
|
27
|
+
text({ localized: false })
|
|
28
|
+
richText({ localized: false })
|
|
29
|
+
// @ts-expect-error numeric facts are structurally shared, not localizable copy
|
|
30
|
+
number({ localized: false })
|
|
31
|
+
|
|
32
|
+
const lexical = { root: { children: [], type: 'root' } }
|
|
33
|
+
|
|
34
|
+
const site = defineSite({
|
|
35
|
+
hero: block({
|
|
36
|
+
headline: text('Building Western Australia.'),
|
|
37
|
+
slides: array({ sector: text() }, [{ sector: 'Urban Development' }]),
|
|
38
|
+
/** Declared for the client to fill, committed by nobody — see the assertions below. */
|
|
39
|
+
gallery: array({ caption: text() }),
|
|
40
|
+
}),
|
|
41
|
+
/**
|
|
42
|
+
* A block a site publishes without seeding: every field is a slot, none is committed.
|
|
43
|
+
*
|
|
44
|
+
* One of each helper that builds its descriptor its own way rather than through `Scalar`'s
|
|
45
|
+
* shared body, since each is its own chance to answer the wrong thing.
|
|
46
|
+
*/
|
|
47
|
+
pageBody: block(
|
|
48
|
+
{
|
|
49
|
+
heading: text(),
|
|
50
|
+
photo: upload(),
|
|
51
|
+
align: select({ options: ['left', 'right'] }),
|
|
52
|
+
/**
|
|
53
|
+
* A zone is created empty and commits nothing, which is the *normal* case for it — and
|
|
54
|
+
* the last helper with no line of its own here. `Aggregate` decides a group is absent
|
|
55
|
+
* by testing every member against `undefined`, so a helper answering `Unnamed<unknown>`
|
|
56
|
+
* instead makes any group holding one read as present while the runtime has nothing
|
|
57
|
+
* there. Every other helper is pinned below; without this line `blocksField` could
|
|
58
|
+
* regress to that and only a site would find out.
|
|
59
|
+
*/
|
|
60
|
+
body: blocksField({ blocks: [] }),
|
|
61
|
+
},
|
|
62
|
+
{ seed: false },
|
|
63
|
+
),
|
|
64
|
+
banner: block({
|
|
65
|
+
cta: link({ default: { href: '/contact-us', label: 'Talk to us' } }),
|
|
66
|
+
/** No committed button: five children, none of which carries anything. */
|
|
67
|
+
secondary: link(),
|
|
68
|
+
}),
|
|
69
|
+
panel: block({
|
|
70
|
+
/**
|
|
71
|
+
* A page reference beside a caption the client fills in — an ordinary block shape, and
|
|
72
|
+
* the one that caught `relationship` answering `unknown`. Nothing here is committed, so
|
|
73
|
+
* the group is absent at runtime; the crash would land on `related`, not on `caption`.
|
|
74
|
+
*/
|
|
75
|
+
related: group({ fields: { page: relationship({ relationTo: 'pages' }), caption: text() } }),
|
|
76
|
+
/** The same shape with one committed child: that child really is there, so it is typed. */
|
|
77
|
+
feature: group({
|
|
78
|
+
fields: { page: relationship({ relationTo: 'pages' }), caption: text('Our team at work.') },
|
|
79
|
+
}),
|
|
80
|
+
}),
|
|
81
|
+
contact: block({
|
|
82
|
+
/** One child committed, one not. */
|
|
83
|
+
address: group({ fields: { street: text('123 Main St'), suburb: text() } }),
|
|
84
|
+
/** No child committed: the group itself is what is missing, not just its children. */
|
|
85
|
+
hours: group({ fields: { open: text(), close: text() } }),
|
|
86
|
+
}),
|
|
87
|
+
story: block({
|
|
88
|
+
/** Bare copy: a string, like every other scalar. */
|
|
89
|
+
intro: richText('We have been building since 1974.'),
|
|
90
|
+
/** Stored one way, rendered another: the *rendered* form is what a component reads. */
|
|
91
|
+
body: richText({ default: lexical, value: 'Two paragraphs of it.' }),
|
|
92
|
+
/** Rendered as a document: the site reads the document back, not a string. */
|
|
93
|
+
note: richText({ value: lexical }),
|
|
94
|
+
}),
|
|
95
|
+
projects: collection(
|
|
96
|
+
{ name: text(), image: upload() },
|
|
97
|
+
{
|
|
98
|
+
titleField: 'name',
|
|
99
|
+
rows: [{ name: 'Port Coogee', image: '/media/port-coogee.webp' }],
|
|
100
|
+
},
|
|
101
|
+
),
|
|
102
|
+
logo: { ratio: 241 / 105, src: '/brand/logo-mark.png' },
|
|
103
|
+
/** A form commits no copy — see the assertion below. Same shape `syncForms` reads off it. */
|
|
104
|
+
contactForm: form({
|
|
105
|
+
fields: [{ label: 'Email', name: 'email', type: 'email', role: 'email' }],
|
|
106
|
+
label: 'Contact',
|
|
107
|
+
}),
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
export const brand = site.values
|
|
111
|
+
export type Brand = typeof brand
|
|
112
|
+
|
|
113
|
+
const headline: string = brand.hero.headline
|
|
114
|
+
const sector: string = brand.hero.slides[0]!.sector
|
|
115
|
+
const street: string = brand.contact.address.street
|
|
116
|
+
const ratio: number = brand.logo.ratio
|
|
117
|
+
const first: string = brand.projects[0]!.name
|
|
118
|
+
const intro: string = brand.story.intro
|
|
119
|
+
const body: string = brand.story.body
|
|
120
|
+
const note: unknown = brand.story.note.root
|
|
121
|
+
|
|
122
|
+
/*
|
|
123
|
+
* A bare entry is carried through with its own type, not widened to `unknown`.
|
|
124
|
+
*
|
|
125
|
+
* Both halves are the assertion: `unknown` would fail the annotation on `ratio` above, and
|
|
126
|
+
* this line is what says the type is `number` in particular rather than something a `string`
|
|
127
|
+
* would also satisfy.
|
|
128
|
+
*/
|
|
129
|
+
// @ts-expect-error a bare entry keeps the type it was written with
|
|
130
|
+
const wrong: string = brand.logo.ratio
|
|
131
|
+
|
|
132
|
+
// @ts-expect-error a key that was never declared does not exist on the inferred type
|
|
133
|
+
const missing = brand.hero.subheadline
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A form commits no copy to `site.values` — there is nothing for a component to render,
|
|
137
|
+
* only a schema the platform validates submissions against and a mailing list it seeds
|
|
138
|
+
* once. `defineSite`'s loop diverts a `form()` entry into `forms` and never writes
|
|
139
|
+
* `values[key]`, exactly as it does for a layout; this pins that a form key reads the same
|
|
140
|
+
* way a layout key does, `TS2339` rather than a compiling `undefined`.
|
|
141
|
+
*/
|
|
142
|
+
// @ts-expect-error a form key does not exist on site.values — see `contactForm` above
|
|
143
|
+
const noForm = brand.contactForm
|
|
144
|
+
|
|
145
|
+
// @ts-expect-error a group's children are read through their own carriers, not as EditorField[]
|
|
146
|
+
const nested: number = brand.contact.address.street
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* A field nobody committed reads as `undefined`, not as the type it would have had.
|
|
150
|
+
*
|
|
151
|
+
* This is the half of the bargain a hand-written `Brand` got right by saying `heading?:
|
|
152
|
+
* string`. A generated type that answered `string` here would compile
|
|
153
|
+
* `brand.pageBody.heading.trim()` — which throws, on the fallback-render path this whole
|
|
154
|
+
* document exists to keep working. Each of these needs a guard to be read, which is the
|
|
155
|
+
* assertion: the type admits nothing until the caller has checked.
|
|
156
|
+
*/
|
|
157
|
+
const uncommitted: undefined = brand.pageBody.heading
|
|
158
|
+
|
|
159
|
+
// @ts-expect-error an uncommitted scalar is not silently the type it would have had
|
|
160
|
+
const assumed: string = brand.pageBody.heading
|
|
161
|
+
|
|
162
|
+
// @ts-expect-error nor is it dereferenceable without a guard — this is the crash, caught
|
|
163
|
+
const trimmed = brand.pageBody.heading.trim()
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* An array with no committed items is `undefined`, not `[]`.
|
|
167
|
+
*
|
|
168
|
+
* The sharpest of them, because the reader is a `.map()` rather than a property access.
|
|
169
|
+
* `defineSite` writes no empty list here and must not: a declared-but-uncommitted array
|
|
170
|
+
* genuinely holds nothing, and a site that means an empty one writes `array(fields, [])`.
|
|
171
|
+
* Contrast the collection below, where `rows ?? []` makes `[]` true in both the type and the
|
|
172
|
+
* runtime.
|
|
173
|
+
*/
|
|
174
|
+
const noItems: undefined = brand.hero.gallery
|
|
175
|
+
|
|
176
|
+
// @ts-expect-error mapping an array nobody committed is the TypeError this type now refuses
|
|
177
|
+
const mapped = brand.hero.gallery.map(String)
|
|
178
|
+
|
|
179
|
+
/** The helpers that build their own descriptor answer the same way. */
|
|
180
|
+
const noPhoto: undefined = brand.pageBody.photo
|
|
181
|
+
const noChoice: undefined = brand.pageBody.align
|
|
182
|
+
const noZone: undefined = brand.pageBody.body
|
|
183
|
+
|
|
184
|
+
// @ts-expect-error an empty zone is absent, not a list of sections waiting to be mapped
|
|
185
|
+
const zoned = brand.pageBody.body.map(String)
|
|
186
|
+
|
|
187
|
+
/** A committed button is read back as written; an uncommitted one is not there at all. */
|
|
188
|
+
const href: string = brand.banner.cta.href
|
|
189
|
+
const label: string = brand.banner.cta.label
|
|
190
|
+
const noButton: undefined = brand.banner.secondary
|
|
191
|
+
|
|
192
|
+
// @ts-expect-error a link nobody committed has no href to read
|
|
193
|
+
const missingHref = brand.banner.secondary.href
|
|
194
|
+
|
|
195
|
+
/** A group commits what its children commit — per child, and not at all when none do. */
|
|
196
|
+
const committedChild: string = brand.contact.address.street
|
|
197
|
+
const uncommittedChild: undefined = brand.contact.address.suburb
|
|
198
|
+
const noChildren: undefined = brand.contact.hours
|
|
199
|
+
|
|
200
|
+
// @ts-expect-error a group whose children all carry nothing is absent, not an empty object
|
|
201
|
+
const reached = brand.contact.hours.open
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* A reference commits nothing, so it counts as absent rather than as unreadable.
|
|
205
|
+
*
|
|
206
|
+
* The distinction is the whole of it. A member typed `unknown` refuses every dereference of
|
|
207
|
+
* *itself*, but it also makes the group around it look present — and then the crash lands on
|
|
208
|
+
* the group, one level above where anything could refuse it.
|
|
209
|
+
*/
|
|
210
|
+
const noReference: undefined = brand.panel.related
|
|
211
|
+
|
|
212
|
+
// @ts-expect-error a group of an uncommitted reference and an uncommitted caption is not there
|
|
213
|
+
const captionOfNothing = brand.panel.related.caption
|
|
214
|
+
|
|
215
|
+
/** One committed child is enough to make the group present — and only that child is typed. */
|
|
216
|
+
const featureCaption: string = brand.panel.feature.caption
|
|
217
|
+
const featurePage: undefined = brand.panel.feature.page
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* A collection declared without rows still reads as a list, never as `undefined`.
|
|
221
|
+
*
|
|
222
|
+
* `defineSite` writes `rows ?? []`, so the type and the runtime agree: a site that has
|
|
223
|
+
* declared the record type but committed no rows maps over an empty array rather than
|
|
224
|
+
* crashing on a property of `undefined`.
|
|
225
|
+
*/
|
|
226
|
+
const empty = defineSite({
|
|
227
|
+
team: collection({ name: text() }, { titleField: 'name' }),
|
|
228
|
+
}).values.team
|
|
229
|
+
const count: number = empty.length
|
|
230
|
+
|
|
231
|
+
// @ts-expect-error rich text rendered as a document is not silently a string
|
|
232
|
+
const flattened: string = brand.story.note
|
|
233
|
+
|
|
234
|
+
export const used = [
|
|
235
|
+
headline,
|
|
236
|
+
sector,
|
|
237
|
+
street,
|
|
238
|
+
ratio,
|
|
239
|
+
first,
|
|
240
|
+
intro,
|
|
241
|
+
body,
|
|
242
|
+
note,
|
|
243
|
+
wrong,
|
|
244
|
+
missing,
|
|
245
|
+
noForm,
|
|
246
|
+
nested,
|
|
247
|
+
flattened,
|
|
248
|
+
count,
|
|
249
|
+
uncommitted,
|
|
250
|
+
assumed,
|
|
251
|
+
trimmed,
|
|
252
|
+
noItems,
|
|
253
|
+
mapped,
|
|
254
|
+
noPhoto,
|
|
255
|
+
noChoice,
|
|
256
|
+
noZone,
|
|
257
|
+
zoned,
|
|
258
|
+
href,
|
|
259
|
+
label,
|
|
260
|
+
noButton,
|
|
261
|
+
missingHref,
|
|
262
|
+
noReference,
|
|
263
|
+
captionOfNothing,
|
|
264
|
+
featureCaption,
|
|
265
|
+
featurePage,
|
|
266
|
+
committedChild,
|
|
267
|
+
uncommittedChild,
|
|
268
|
+
noChildren,
|
|
269
|
+
reached,
|
|
270
|
+
]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
import { useHasMounted, useIsFramed } from './bridge'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The bar that says "you are looking at a draft, and nobody else is".
|
|
9
|
+
*
|
|
10
|
+
* Draft mode is a cookie, and cookies are invisible. Without this, the only signal that
|
|
11
|
+
* a browser is reading staged content instead of the live site is that the content
|
|
12
|
+
* looks wrong — which reads as "the site published something I never approved", because
|
|
13
|
+
* from the operator's side that is exactly what it looks like. This is the missing half
|
|
14
|
+
* of `createExitPreviewRoute`: one tells you, the other lets you leave.
|
|
15
|
+
*
|
|
16
|
+
* Render it from the site's root layout, behind the server's own draft check:
|
|
17
|
+
*
|
|
18
|
+
* const { isEnabled } = await draftMode()
|
|
19
|
+
* ...
|
|
20
|
+
* {isEnabled ? <DraftNotice /> : null}
|
|
21
|
+
*
|
|
22
|
+
* **Hidden inside the dashboard's Canvas frame.** There, a draft is the entire point and
|
|
23
|
+
* a bar would be noise sitting over the surface the client is editing. This component waits
|
|
24
|
+
* until hydration before drawing anything, so `useIsFramed` can perform its reliable mounted
|
|
25
|
+
* check without painting the public-preview bar for one commit inside Canvas.
|
|
26
|
+
*
|
|
27
|
+
* `position: fixed` so it takes no space in the document. Canvas measures elements in
|
|
28
|
+
* the frame to place its selection overlays, and a bar that pushed the page down by its
|
|
29
|
+
* own height would put every one of those overlays out by that much.
|
|
30
|
+
*
|
|
31
|
+
* Inline styles, deliberately: the kit cannot assume the consuming site's Tailwind
|
|
32
|
+
* content glob reaches into `node_modules`, and a bar styled with classes that were
|
|
33
|
+
* never generated is an invisible warning.
|
|
34
|
+
*/
|
|
35
|
+
export const DraftNotice = ({
|
|
36
|
+
/** Where the exit route is mounted, if a site moves it. */
|
|
37
|
+
exitPath = '/api/exit-preview',
|
|
38
|
+
label = 'Viewing staged changes — visitors still see the published site.',
|
|
39
|
+
}: {
|
|
40
|
+
exitPath?: string
|
|
41
|
+
label?: string
|
|
42
|
+
} = {}) => {
|
|
43
|
+
const framed = useIsFramed()
|
|
44
|
+
const hydrated = useHasMounted()
|
|
45
|
+
|
|
46
|
+
if (!hydrated || framed) return null
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
* The page you are on is read at click time rather than baked into the href.
|
|
50
|
+
*
|
|
51
|
+
* A layout has no pathname to render into the link, and the obvious fix — fill it in
|
|
52
|
+
* after mount — is wrong twice over: it sets state from an effect, and it goes stale
|
|
53
|
+
* the moment the visitor navigates client-side, so Exit would return them to whatever
|
|
54
|
+
* page they first landed on. Reading `location` in the handler is always current.
|
|
55
|
+
*
|
|
56
|
+
* The plain `href` stays as the no-JS path; it just returns you to the home page.
|
|
57
|
+
*/
|
|
58
|
+
const exitHere = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
|
59
|
+
// Let ⌘-click, middle-click and the rest keep their normal meaning.
|
|
60
|
+
if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey) return
|
|
61
|
+
event.preventDefault()
|
|
62
|
+
const here = window.location.pathname + window.location.search
|
|
63
|
+
window.location.href = `${exitPath}?to=${encodeURIComponent(here)}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
role="status"
|
|
69
|
+
style={{
|
|
70
|
+
bottom: 'max(16px, env(safe-area-inset-bottom))',
|
|
71
|
+
display: 'flex',
|
|
72
|
+
justifyContent: 'center',
|
|
73
|
+
left: 0,
|
|
74
|
+
// The bar itself is the only thing clickable; the rest of this strip is not.
|
|
75
|
+
pointerEvents: 'none',
|
|
76
|
+
position: 'fixed',
|
|
77
|
+
right: 0,
|
|
78
|
+
zIndex: 2147483000,
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
<div
|
|
82
|
+
style={{
|
|
83
|
+
alignItems: 'center',
|
|
84
|
+
background: '#111111',
|
|
85
|
+
border: '1px solid #333333',
|
|
86
|
+
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.35)',
|
|
87
|
+
color: '#f5f5f5',
|
|
88
|
+
display: 'flex',
|
|
89
|
+
fontFamily:
|
|
90
|
+
'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
|
91
|
+
fontSize: '13px',
|
|
92
|
+
gap: '14px',
|
|
93
|
+
lineHeight: 1.3,
|
|
94
|
+
margin: '0 16px',
|
|
95
|
+
maxWidth: 'calc(100vw - 32px)',
|
|
96
|
+
padding: '9px 14px',
|
|
97
|
+
pointerEvents: 'auto',
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
<span
|
|
101
|
+
aria-hidden
|
|
102
|
+
style={{
|
|
103
|
+
background: '#e5a000',
|
|
104
|
+
borderRadius: '50%',
|
|
105
|
+
flex: 'none',
|
|
106
|
+
height: '7px',
|
|
107
|
+
width: '7px',
|
|
108
|
+
}}
|
|
109
|
+
/>
|
|
110
|
+
<span style={{ minWidth: 0 }}>{label}</span>
|
|
111
|
+
<a
|
|
112
|
+
href={exitPath}
|
|
113
|
+
onClick={exitHere}
|
|
114
|
+
style={{
|
|
115
|
+
color: '#ffffff',
|
|
116
|
+
flex: 'none',
|
|
117
|
+
fontWeight: 600,
|
|
118
|
+
textDecoration: 'underline',
|
|
119
|
+
textUnderlineOffset: '3px',
|
|
120
|
+
whiteSpace: 'nowrap',
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
Exit preview
|
|
124
|
+
</a>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|