@conduction/docusaurus-preset 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/components/DetailHero/DetailHero.jsx +22 -2
- package/src/components/primitives/Button.jsx +8 -0
- package/src/components/primitives/Button.module.css +23 -0
- package/src/index.js +23 -0
- package/src/theme/Footer/index.jsx +129 -37
- package/src/utils/lazyScript.js +8 -0
- package/static/lib/canal-footer.js +15 -0
package/README.md
CHANGED
|
@@ -60,6 +60,8 @@ The function returns a complete Docusaurus config with brand defaults pre-applie
|
|
|
60
60
|
| `i18n` | | nl / en / de / fr, NL default | Override the brand-default i18n block. |
|
|
61
61
|
| `navbar` | | locale dropdown + GitHub | Merged into the brand-default navbar object. |
|
|
62
62
|
| `footer` | | three-column link grid + KvK/BTW copyright | Per-property fallback: any of `style` / `links` / `copyright` you omit keeps the brand default. Pass `footer: { links: [...] }` to swap columns and inherit the brand copyright. |
|
|
63
|
+
| `footerBrand` | | `{ wordmark: 'Conduction' }` | Overrides the wordmark in the canal-footer's left brand block. Pass `{ wordmark: 'X' }` for a single brand or `{ brands: [{wordmark, logo, href}, ...] }` for product pages co-built with a partner (rendered side by side). |
|
|
64
|
+
| `minigames` | | `true` | Set `false` to drop the brand canal-footer's boat-sinking + kade-cyclist mini-games on product pages. The static skyline + canal decoration are kept. |
|
|
63
65
|
| `customCss` | | `[]` | Site-specific CSS, appended to `brand.css`. |
|
|
64
66
|
| `presets` | | `[['classic', …]]` | Replaces the default preset list. |
|
|
65
67
|
| `plugins` | | `[]` | Docusaurus plugins. |
|
package/package.json
CHANGED
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
* iconColor="var(--c-blue-cobalt)"
|
|
34
34
|
* illustration={<AppMock app="mydash" />}
|
|
35
35
|
* />
|
|
36
|
+
*
|
|
37
|
+
* Each cta object also accepts `tone: "orange"` to flip the primary
|
|
38
|
+
* (or secondary) variant to the KNVB-orange accent. Reserved for
|
|
39
|
+
* product pages with an orange-leaning brand identity (mydash).
|
|
36
40
|
*/
|
|
37
41
|
|
|
38
42
|
import React from 'react';
|
|
@@ -127,8 +131,24 @@ export default function DetailHero({
|
|
|
127
131
|
|
|
128
132
|
{(primaryCta || secondaryCta || tertiaryCta) && (
|
|
129
133
|
<div className={styles.actions}>
|
|
130
|
-
{primaryCta &&
|
|
131
|
-
|
|
134
|
+
{primaryCta && (
|
|
135
|
+
<Button
|
|
136
|
+
variant="primary"
|
|
137
|
+
tone={primaryCta.tone}
|
|
138
|
+
href={primaryCta.href}
|
|
139
|
+
>
|
|
140
|
+
{primaryCta.label}
|
|
141
|
+
</Button>
|
|
142
|
+
)}
|
|
143
|
+
{secondaryCta && (
|
|
144
|
+
<Button
|
|
145
|
+
variant="secondary"
|
|
146
|
+
tone={secondaryCta.tone}
|
|
147
|
+
href={secondaryCta.href}
|
|
148
|
+
>
|
|
149
|
+
{secondaryCta.label}
|
|
150
|
+
</Button>
|
|
151
|
+
)}
|
|
132
152
|
{tertiaryCta && <Button variant="ghost" href={tertiaryCta.href}>{tertiaryCta.label} →</Button>}
|
|
133
153
|
</div>
|
|
134
154
|
)}
|
|
@@ -14,11 +14,17 @@
|
|
|
14
14
|
* - ghost: plain text + arrow
|
|
15
15
|
* - on-dark: primary variant inverted for use inside cobalt CTA panels
|
|
16
16
|
*
|
|
17
|
+
* Tone (optional): override the primary/secondary fill colour. The
|
|
18
|
+
* brand default is cobalt; product pages with an orange-accent identity
|
|
19
|
+
* (e.g. mydash) pass `tone="orange"` to flip the primary CTA to
|
|
20
|
+
* KNVB-orange while keeping the rest of the brand chrome intact.
|
|
21
|
+
*
|
|
17
22
|
* Usage:
|
|
18
23
|
*
|
|
19
24
|
* <Button href="/apps">Install</Button>
|
|
20
25
|
* <Button variant="secondary" href="/partners">Get a demo</Button>
|
|
21
26
|
* <Button variant="ghost" href="https://github.com/...">View on GitHub →</Button>
|
|
27
|
+
* <Button tone="orange" href="/install">Install from app store</Button>
|
|
22
28
|
*/
|
|
23
29
|
|
|
24
30
|
import React from 'react';
|
|
@@ -26,6 +32,7 @@ import styles from './Button.module.css';
|
|
|
26
32
|
|
|
27
33
|
export default function Button({
|
|
28
34
|
variant = 'primary',
|
|
35
|
+
tone,
|
|
29
36
|
href,
|
|
30
37
|
size = 'md',
|
|
31
38
|
className,
|
|
@@ -36,6 +43,7 @@ export default function Button({
|
|
|
36
43
|
styles.btn,
|
|
37
44
|
styles['v-' + variant],
|
|
38
45
|
styles['s-' + size],
|
|
46
|
+
tone && styles['t-' + tone],
|
|
39
47
|
className,
|
|
40
48
|
].filter(Boolean).join(' ');
|
|
41
49
|
|
|
@@ -86,3 +86,26 @@
|
|
|
86
86
|
|
|
87
87
|
.v-on-dark-primary :global(.next-blue),
|
|
88
88
|
.v-primary :global(.next-blue) { color: var(--c-nextcloud-cyan); }
|
|
89
|
+
|
|
90
|
+
/* Tone modifier for the primary variant. The brand default is the
|
|
91
|
+
cobalt fill above; product pages with an orange-accent identity
|
|
92
|
+
(mydash is the first) opt in by passing tone="orange". The brand
|
|
93
|
+
rule "one orange accent per screen" still applies; this just lets
|
|
94
|
+
the screen's single accent live on the primary CTA instead of
|
|
95
|
+
elsewhere. */
|
|
96
|
+
.t-orange.v-primary {
|
|
97
|
+
background: var(--c-orange-knvb);
|
|
98
|
+
border-color: var(--c-orange-knvb);
|
|
99
|
+
}
|
|
100
|
+
.t-orange.v-primary:hover {
|
|
101
|
+
background: var(--c-orange-knvb-700, #c8482f);
|
|
102
|
+
border-color: var(--c-orange-knvb-700, #c8482f);
|
|
103
|
+
}
|
|
104
|
+
.t-orange.v-secondary {
|
|
105
|
+
color: var(--c-orange-knvb);
|
|
106
|
+
border-color: var(--c-orange-knvb);
|
|
107
|
+
}
|
|
108
|
+
.t-orange.v-secondary:hover {
|
|
109
|
+
border-color: var(--c-orange-knvb-700, #c8482f);
|
|
110
|
+
color: var(--c-orange-knvb-700, #c8482f);
|
|
111
|
+
}
|
package/src/index.js
CHANGED
|
@@ -125,6 +125,9 @@ const baseFooter = () => ({
|
|
|
125
125
|
* footer (per-property fallback: any of style/links/copyright the
|
|
126
126
|
* site omits keeps its brand default — pass `footer: { links: [...] }`
|
|
127
127
|
* to swap columns while inheriting the KvK/BTW copyright),
|
|
128
|
+
* minigames (default true; set false to drop the brand canal-footer's
|
|
129
|
+
* boat-sinking + kade-cyclist mini-games on product pages while
|
|
130
|
+
* keeping the static skyline + canal decoration),
|
|
128
131
|
* customCss[] (appended to brand.css), plugins[], presets,
|
|
129
132
|
* i18n (overrides defaults)
|
|
130
133
|
*/
|
|
@@ -212,6 +215,26 @@ function createConfig(opts) {
|
|
|
212
215
|
copyright: f.copyright || baseFooterCopyright(),
|
|
213
216
|
};
|
|
214
217
|
})(),
|
|
218
|
+
/* The brand canal-footer carries two interactive mini-games
|
|
219
|
+
(boat-sinking + kade-cyclist). They're a kit-flavour win on
|
|
220
|
+
the design-system showcase but distract on product-page
|
|
221
|
+
landings. Surface as a top-level themeConfig flag so the
|
|
222
|
+
Footer swizzle can drop the game DOM + lazy scripts when a
|
|
223
|
+
site opts out, while still keeping the static skyline +
|
|
224
|
+
canal decoration. Default true preserves prior behaviour. */
|
|
225
|
+
minigames: opts.minigames !== false,
|
|
226
|
+
/* Footer brand block (the wordmark + tagline + triad + socials
|
|
227
|
+
on the left of the canal-footer grid).
|
|
228
|
+
undefined -> wordmark = 'Conduction' (product-page default;
|
|
229
|
+
previously this fell back to the site title,
|
|
230
|
+
which made every product-page footer wear the
|
|
231
|
+
product's wordmark instead of the company's).
|
|
232
|
+
{ wordmark: 'X' } -> single custom brand
|
|
233
|
+
{ brands: [{wordmark, logo, href}, ...] } -> dual-brand row,
|
|
234
|
+
rendered side by side. Used by product pages
|
|
235
|
+
built jointly with a partner (mydash + Sendent
|
|
236
|
+
is the first case). */
|
|
237
|
+
footerBrand: opts.footerBrand || null,
|
|
215
238
|
},
|
|
216
239
|
opts.themeConfig || {}
|
|
217
240
|
),
|
|
@@ -45,24 +45,44 @@ function FooterLink({label, href, to}) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export default function Footer() {
|
|
48
|
-
const
|
|
48
|
+
const themeConfig = useThemeConfig();
|
|
49
|
+
const {footer, navbar} = themeConfig;
|
|
50
|
+
/* `minigames` and `footerBrand` are top-level themeConfig flags
|
|
51
|
+
surfaced through createConfig() opts. See createConfig() in
|
|
52
|
+
../../index.js for the option semantics. */
|
|
53
|
+
const minigamesOn = themeConfig.minigames !== false;
|
|
54
|
+
const footerBrand = themeConfig.footerBrand || null;
|
|
55
|
+
|
|
49
56
|
const location = useLocation();
|
|
50
57
|
/* Brand switch follows the pathname: /connext or /commonground sections
|
|
51
58
|
show their styled wordmark and slot themselves into the triad row.
|
|
52
|
-
Outside sub-brand sections the wordmark
|
|
53
|
-
|
|
59
|
+
Outside sub-brand sections the wordmark defaults to "Conduction"
|
|
60
|
+
for the company anchor. Sites can override the default via
|
|
61
|
+
`themeConfig.footerBrand = { wordmark: '...' }`, or render a dual
|
|
62
|
+
brand row via `{ brands: [{wordmark, logo, href}, ...] }` for
|
|
63
|
+
product pages co-branded with a partner. The legacy fallback to
|
|
64
|
+
`navbar.title` was misleading on product-page footers (mydash
|
|
65
|
+
showing "MyDash" rather than "Conduction"); the company-anchor
|
|
66
|
+
reading wins. */
|
|
54
67
|
const brand = brandFor(location.pathname, navbar?.title);
|
|
55
|
-
const
|
|
68
|
+
const defaultWordmark = footerBrand?.wordmark || 'Conduction';
|
|
69
|
+
const wordmark = brand ? brand.wordmark : defaultWordmark;
|
|
70
|
+
const brandRow = !brand && Array.isArray(footerBrand?.brands) ? footerBrand.brands : null;
|
|
56
71
|
|
|
57
72
|
/* canal-footer.js is loaded post-hydration so its DOM mutations
|
|
58
73
|
(filling .skyline, animating boats) don't trip React hydration
|
|
59
|
-
mismatches. See docs in utils/lazyScript.js.
|
|
74
|
+
mismatches. See docs in utils/lazyScript.js. We always load
|
|
75
|
+
canal-footer.js even when minigames are off, because the same
|
|
76
|
+
script populates the static skyline; canal-footer.js is null-safe
|
|
77
|
+
against a missing game-hud, so the game wiring no-ops cleanly. */
|
|
60
78
|
useLazyScript('/lib/canal-footer.js', 'canal-footer');
|
|
61
79
|
|
|
62
|
-
/* kade-cyclist.js is the second hidden footer minigame.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
80
|
+
/* kade-cyclist.js is the second hidden footer minigame. Unlike the
|
|
81
|
+
canal script it has no static side-effects, so when a product page
|
|
82
|
+
opts out of minigames we feed `useLazyScript` a falsy src to skip
|
|
83
|
+
the load. The hook still runs unconditionally — rules-of-hooks
|
|
84
|
+
stays compliant. */
|
|
85
|
+
useLazyScript(minigamesOn ? '/lib/kade-cyclist.js' : null, 'kade-cyclist');
|
|
66
86
|
|
|
67
87
|
/* Re-hydrate the canal-footer + kade-cyclist runtimes on every mount,
|
|
68
88
|
including SPA route changes that re-render this Footer component.
|
|
@@ -80,14 +100,17 @@ export default function Footer() {
|
|
|
80
100
|
function tryHydrate() {
|
|
81
101
|
if (cancelled) return;
|
|
82
102
|
const canalReady = !!window.CanalFooter?.hydrate;
|
|
83
|
-
|
|
103
|
+
/* When minigames are off, kade-cyclist.js was never loaded, so
|
|
104
|
+
polling for it would loop forever. Treat it as ready in that
|
|
105
|
+
case so the loop exits after canal-footer is wired. */
|
|
106
|
+
const kadeReady = !minigamesOn || !!window.KadeCyclist?.hydrate;
|
|
84
107
|
if (canalReady) window.CanalFooter.hydrate();
|
|
85
|
-
if (kadeReady)
|
|
108
|
+
if (kadeReady && minigamesOn) window.KadeCyclist.hydrate();
|
|
86
109
|
if (!canalReady || !kadeReady) requestAnimationFrame(tryHydrate);
|
|
87
110
|
}
|
|
88
111
|
tryHydrate();
|
|
89
112
|
return () => { cancelled = true; };
|
|
90
|
-
}, [isBrowser, location.pathname]);
|
|
113
|
+
}, [isBrowser, location.pathname, minigamesOn]);
|
|
91
114
|
|
|
92
115
|
if (!footer) return null;
|
|
93
116
|
const {links = [], copyright} = footer;
|
|
@@ -219,23 +242,30 @@ export default function Footer() {
|
|
|
219
242
|
</svg>
|
|
220
243
|
</div>
|
|
221
244
|
|
|
222
|
-
{/* Mini-game HUD
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
245
|
+
{/* Mini-game HUD + game-over dialog + restart button. canal-footer.js
|
|
246
|
+
wires these via querySelector; the script's null-guards (added in
|
|
247
|
+
the same change) let it skip game initialisation cleanly when
|
|
248
|
+
they're absent on a product page. */}
|
|
249
|
+
{minigamesOn && (
|
|
250
|
+
<>
|
|
251
|
+
<div className="game-hud" aria-live="polite" aria-label="Boat-sinking mini game">
|
|
252
|
+
<div className="hud-block hud-counter">
|
|
253
|
+
<span className="hud-num" data-counter="">100</span>
|
|
254
|
+
<span className="hud-label">Boats left</span>
|
|
255
|
+
</div>
|
|
256
|
+
<div className="hud-block hud-timer">
|
|
257
|
+
<span className="hud-num" data-timer="">60</span>
|
|
258
|
+
<span className="hud-label">Seconds</span>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
233
261
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
262
|
+
<div className="game-over" role="dialog" aria-label="Mini game over">
|
|
263
|
+
<p className="go-title" data-go-title="">Time's up</p>
|
|
264
|
+
<p className="go-stat"><span data-go-sunk="">0</span> sunk</p>
|
|
265
|
+
<button type="button" data-restart="">Play again</button>
|
|
266
|
+
</div>
|
|
267
|
+
</>
|
|
268
|
+
)}
|
|
239
269
|
|
|
240
270
|
<svg className="canal-waves" viewBox="0 0 1400 500" preserveAspectRatio="none" aria-hidden="true">
|
|
241
271
|
<path className="w1" d="M 0,96 L 140,82 L 280,96 L 420,82 L 560,96 L 700,82 L 840,96 L 980,82 L 1120,96 L 1260,82 L 1400,96"/>
|
|
@@ -245,7 +275,43 @@ export default function Footer() {
|
|
|
245
275
|
|
|
246
276
|
<div className="footer-grid">
|
|
247
277
|
<div className="brand">
|
|
248
|
-
|
|
278
|
+
{brandRow ? (
|
|
279
|
+
/* Dual-brand row: product pages co-built with a partner
|
|
280
|
+
render both wordmarks/logos side by side. The triad
|
|
281
|
+
below grows a partner segment in the same order. */
|
|
282
|
+
<div
|
|
283
|
+
className="wm-row"
|
|
284
|
+
style={{display: 'flex', alignItems: 'center', gap: '1.25rem', flexWrap: 'wrap'}}
|
|
285
|
+
>
|
|
286
|
+
{brandRow.map((b, i) => {
|
|
287
|
+
const inner = b.logo ? (
|
|
288
|
+
<img
|
|
289
|
+
src={b.logo}
|
|
290
|
+
alt={b.wordmark}
|
|
291
|
+
style={{height: 32, width: 'auto', display: 'block'}}
|
|
292
|
+
/>
|
|
293
|
+
) : (
|
|
294
|
+
<span className="wm">{b.wordmark}</span>
|
|
295
|
+
);
|
|
296
|
+
return b.href ? (
|
|
297
|
+
<a
|
|
298
|
+
key={i}
|
|
299
|
+
href={b.href}
|
|
300
|
+
target={b.href.startsWith('http') ? '_blank' : undefined}
|
|
301
|
+
rel={b.href.startsWith('http') ? 'noopener noreferrer' : undefined}
|
|
302
|
+
aria-label={b.wordmark}
|
|
303
|
+
style={{textDecoration: 'none'}}
|
|
304
|
+
>
|
|
305
|
+
{inner}
|
|
306
|
+
</a>
|
|
307
|
+
) : (
|
|
308
|
+
<React.Fragment key={i}>{inner}</React.Fragment>
|
|
309
|
+
);
|
|
310
|
+
})}
|
|
311
|
+
</div>
|
|
312
|
+
) : (
|
|
313
|
+
<div className="wm">{wordmark}</div>
|
|
314
|
+
)}
|
|
249
315
|
<p>
|
|
250
316
|
Open-source apps for <span className="next-blue">Nextcloud</span>. Built and
|
|
251
317
|
maintained by Conduction in Amsterdam, released under EUPL-1.2.
|
|
@@ -253,7 +319,14 @@ export default function Footer() {
|
|
|
253
319
|
<div className="triad">
|
|
254
320
|
<span>
|
|
255
321
|
<span className="h"></span>
|
|
256
|
-
Conduction
|
|
322
|
+
Conduction
|
|
323
|
+
{brand && <> · {brand.label}</>}
|
|
324
|
+
{brandRow && brandRow
|
|
325
|
+
.filter((b) => b.wordmark && b.wordmark !== 'Conduction')
|
|
326
|
+
.map((b, i) => (
|
|
327
|
+
<React.Fragment key={i}> · {b.wordmark}</React.Fragment>
|
|
328
|
+
))}
|
|
329
|
+
{' '}· <span className="next-blue">Nextcloud</span>
|
|
257
330
|
</span>
|
|
258
331
|
</div>
|
|
259
332
|
<div className="socials">
|
|
@@ -330,13 +403,20 @@ export default function Footer() {
|
|
|
330
403
|
|
|
331
404
|
{/* Hidden templates cloned by canal-footer.js to populate the
|
|
332
405
|
skyline (5 trapgevel variants) and to spawn boats during the
|
|
333
|
-
mini-game (sailing ship, cargo, frigate, battleship boss).
|
|
334
|
-
|
|
406
|
+
mini-game (sailing ship, cargo, frigate, battleship boss).
|
|
407
|
+
When minigames are off the boat templates aren't reachable
|
|
408
|
+
anyway (canal-footer.js's game wiring is null-guarded), but
|
|
409
|
+
dropping them shaves a few KB of inline SVG from the SSR
|
|
410
|
+
payload on product pages. */}
|
|
411
|
+
<FooterTemplates includeBoats={minigamesOn} />
|
|
335
412
|
|
|
336
413
|
{/* Cross-game completion dialog. Listens for connext:gameend (HexRain,
|
|
337
414
|
canal mini-game) and reports the result with replay + cross-game
|
|
338
|
-
progress. Position: fixed via CSS module so it overlays the page.
|
|
339
|
-
|
|
415
|
+
progress. Position: fixed via CSS module so it overlays the page.
|
|
416
|
+
Only mounted when minigames are on so a product page doesn't
|
|
417
|
+
carry the dialog DOM + listeners for an interaction it can't
|
|
418
|
+
trigger. */}
|
|
419
|
+
{minigamesOn && <GameModal />}
|
|
340
420
|
</>
|
|
341
421
|
);
|
|
342
422
|
}
|
|
@@ -347,17 +427,22 @@ export default function Footer() {
|
|
|
347
427
|
via .content. Because JSX puts children directly under the template
|
|
348
428
|
instead of in its .content DocumentFragment, we inject the templates
|
|
349
429
|
as raw HTML so the browser's HTML parser slots them correctly. */
|
|
350
|
-
function FooterTemplates() {
|
|
430
|
+
function FooterTemplates({includeBoats = true}) {
|
|
431
|
+
const html = HOUSE_TEMPLATES_HTML + (includeBoats ? BOAT_TEMPLATES_HTML : '');
|
|
351
432
|
return (
|
|
352
433
|
<div
|
|
353
434
|
style={{display: 'none'}}
|
|
354
435
|
aria-hidden="true"
|
|
355
|
-
dangerouslySetInnerHTML={{__html:
|
|
436
|
+
dangerouslySetInnerHTML={{__html: html}}
|
|
356
437
|
/>
|
|
357
438
|
);
|
|
358
439
|
}
|
|
359
440
|
|
|
360
|
-
|
|
441
|
+
/* Skyline templates (5 trapgevel variants). canal-footer.js queries
|
|
442
|
+
#tpl-h-a … #tpl-h-e to clone them across the skyline width. Always
|
|
443
|
+
shipped — the static skyline is part of the brand decoration even
|
|
444
|
+
on product pages that opt out of minigames. */
|
|
445
|
+
const HOUSE_TEMPLATES_HTML = `
|
|
361
446
|
<template id="tpl-h-a">
|
|
362
447
|
<svg class="house h-a" viewBox="0 -2 80 202" xmlns="http://www.w3.org/2000/svg">
|
|
363
448
|
<path d="M 0,200 L 0,38 L 8,38 L 8,26 L 16,26 L 16,14 L 24,14 L 24,8 L 56,8 L 56,14 L 64,14 L 64,26 L 72,26 L 72,38 L 80,38 L 80,200 Z" fill="var(--c-orange-knvb)"/>
|
|
@@ -426,7 +511,14 @@ const TEMPLATES_HTML = `
|
|
|
426
511
|
<rect x="39.5" y="175" width="16" height="34" fill="rgba(11,32,73,0.55)"/>
|
|
427
512
|
</svg>
|
|
428
513
|
</template>
|
|
514
|
+
`;
|
|
429
515
|
|
|
516
|
+
/* Boat templates (sailing ship, cargo, frigate, battleship boss).
|
|
517
|
+
canal-footer.js spawns these as the boat-sinking mini-game escalates;
|
|
518
|
+
when a site opts out of minigames the script's null-guard skips
|
|
519
|
+
spawning and these templates are unreachable, so they're elided
|
|
520
|
+
from the SSR payload. */
|
|
521
|
+
const BOAT_TEMPLATES_HTML = `
|
|
430
522
|
<template id="tpl-ship-sailing">
|
|
431
523
|
<svg class="ci ci-sailing" width="100" height="60" viewBox="-4 -42 108 60" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
432
524
|
<path d="M 0,12 L 6,16 L 90,16 L 96,12 L 96,8 L 0,8 Z" fill="var(--c-orange-knvb)"/>
|
package/src/utils/lazyScript.js
CHANGED
|
@@ -23,10 +23,18 @@
|
|
|
23
23
|
import {useEffect} from 'react';
|
|
24
24
|
import useIsBrowser from '@docusaurus/useIsBrowser';
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Pass a falsy `src` to skip the load entirely. The hook still runs
|
|
28
|
+
* unconditionally so call sites stay rules-of-hooks-compliant: the
|
|
29
|
+
* Footer swizzle uses this to gate /lib/kade-cyclist.js behind the
|
|
30
|
+
* `minigames` themeConfig flag without splitting into conditional
|
|
31
|
+
* hook calls.
|
|
32
|
+
*/
|
|
26
33
|
export function useLazyScript(src, key) {
|
|
27
34
|
const isBrowser = useIsBrowser();
|
|
28
35
|
useEffect(() => {
|
|
29
36
|
if (!isBrowser) return;
|
|
37
|
+
if (!src) return;
|
|
30
38
|
if (document.querySelector(`script[data-lazy-script="${key}"]`)) return;
|
|
31
39
|
const script = document.createElement('script');
|
|
32
40
|
script.src = src;
|
|
@@ -126,6 +126,21 @@
|
|
|
126
126
|
const goSunk = root.querySelector('[data-go-sunk]');
|
|
127
127
|
const goRestart = root.querySelector('[data-restart]');
|
|
128
128
|
|
|
129
|
+
/* Product pages opt out of the boat-sinking mini-game by setting
|
|
130
|
+
`themeConfig.minigames = false` in createConfig(); the Footer
|
|
131
|
+
swizzle then doesn't render the .game-hud / .game-over / boat
|
|
132
|
+
templates. The skyline + house listeners + drifting fleet (built
|
|
133
|
+
above this point) are still wanted. Bail out before any game
|
|
134
|
+
wiring rather than null-deref'ing on the missing elements. */
|
|
135
|
+
if (!hud || !goRestart || !goPanel) {
|
|
136
|
+
/* Mark hydrated and expose a no-op API so the Footer's
|
|
137
|
+
useEffect re-hydrate loop exits cleanly on SPA route changes. */
|
|
138
|
+
window.CanalFooter = window.CanalFooter || {};
|
|
139
|
+
window.CanalFooter.hydrate = window.CanalFooter.hydrate || function () {};
|
|
140
|
+
window.CanalFooter._cleanup = window.CanalFooter._cleanup || function () {};
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
129
144
|
let game;
|
|
130
145
|
|
|
131
146
|
function newGameState() {
|