@conduction/docusaurus-preset 3.9.0 → 3.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/docusaurus-preset",
3
- "version": "3.9.0",
3
+ "version": "3.11.0",
4
4
  "scripts": {
5
5
  "prepack": "node scripts/prepack-bundle-css.js"
6
6
  },
@@ -20,7 +20,7 @@
20
20
  */
21
21
 
22
22
  import React from 'react';
23
- import Head from '@docusaurus/Head';
23
+ import {useLazyStylesheet} from '../../utils/lazyStylesheet';
24
24
  import SectionHead from '../primitives/SectionHead';
25
25
  import {useLazyScript} from '../../utils/lazyScript';
26
26
  import styles from './Clients.module.css';
@@ -121,6 +121,7 @@ function splitIntoRows(items, rowCount) {
121
121
  function ClientsMarquee({head, clients, title, className}) {
122
122
  useLazyScript(LOGO_MEMORY_BASE + '/clients-flow.js', 'clients-flow');
123
123
  useLazyScript(LOGO_MEMORY_BASE + '/logo-memory.js', 'logo-memory');
124
+ useLazyStylesheet(LOGO_MEMORY_BASE + '/logo-memory.css', 'logo-memory');
124
125
  React.useEffect(() => {
125
126
  if (window.ConductionClientsFlow?.hydrate) window.ConductionClientsFlow.hydrate();
126
127
  if (window.LogoMemory?.hydrate) window.LogoMemory.hydrate();
@@ -128,9 +129,6 @@ function ClientsMarquee({head, clients, title, className}) {
128
129
  const clientsJson = React.useMemo(() => JSON.stringify(clients), [clients]);
129
130
  return (
130
131
  <>
131
- <Head>
132
- <link rel="stylesheet" href={LOGO_MEMORY_BASE + '/logo-memory.css'} />
133
- </Head>
134
132
  <section
135
133
  className={[styles.section, className].filter(Boolean).join(' ')}
136
134
  data-logo-memory
@@ -73,6 +73,7 @@ export default function DetailHero({
73
73
  locales,
74
74
  title,
75
75
  tagline,
76
+ intro,
76
77
  primaryCta,
77
78
  secondaryCta,
78
79
  tertiaryCta,
@@ -262,6 +263,7 @@ export default function DetailHero({
262
263
  </h1>
263
264
  )}
264
265
  {tagline && <p className={styles.tagline}>{tagline}</p>}
266
+ {intro && <div className={styles.intro}>{intro}</div>}
265
267
 
266
268
  {(primaryCta || secondaryCta || tertiaryCta) && (
267
269
  <div className={styles.actions}>
@@ -42,6 +42,8 @@
42
42
  .bgCobalt .titleText { color: white; }
43
43
  .bgCobalt .tagline { color: var(--c-cobalt-100); }
44
44
  .bgCobalt .tagline :global(.next-blue) { color: var(--c-nextcloud-cyan); }
45
+ .bgCobalt .intro { color: var(--c-cobalt-100); }
46
+ .bgCobalt .intro :global(.next-blue) { color: var(--c-nextcloud-cyan); }
45
47
 
46
48
  .crumb {
47
49
  font-family: var(--conduction-typography-font-family-code);
@@ -165,6 +167,18 @@
165
167
  }
166
168
  .tagline :global(.next-blue) { color: var(--c-nextcloud-blue); }
167
169
 
170
+ /* Optional supporting paragraph below the tagline. Longer than the
171
+ tagline (SEO/intro lede). Wider read-measure (70ch) and a quieter
172
+ colour so the eye still lands on the title + tagline first. */
173
+ .intro {
174
+ font-size: 17px;
175
+ line-height: 1.6;
176
+ color: var(--c-cobalt-700);
177
+ max-width: 70ch;
178
+ margin: 0 0 32px;
179
+ }
180
+ .intro :global(.next-blue) { color: var(--c-nextcloud-blue); }
181
+
168
182
  .actions {
169
183
  display: flex;
170
184
  gap: 14px;
@@ -33,9 +33,9 @@
33
33
  */
34
34
 
35
35
  import React, {useEffect, useRef} from 'react';
36
- import Head from '@docusaurus/Head';
37
36
  import useIsBrowser from '@docusaurus/useIsBrowser';
38
37
  import {useLazyScript} from '../../utils/lazyScript';
38
+ import {useLazyStylesheet} from '../../utils/lazyStylesheet';
39
39
 
40
40
  const ASSET_BASE = '/lib';
41
41
 
@@ -43,10 +43,12 @@ export default function HexRain({ariaLabel = 'Twelve apps mini-game, click each
43
43
  const isBrowser = useIsBrowser();
44
44
  const ref = useRef(null);
45
45
 
46
- /* hex-rain.js is loaded post-hydration via this hook so the runtime's
47
- DOM mutations don't trip React's hydration mismatch. See
48
- utils/lazyScript.js. */
46
+ /* hex-rain.{js,css} are loaded post-hydration so neither the
47
+ runtime's DOM mutations trip React's hydration mismatch nor the
48
+ stylesheet blocks first paint. See utils/lazyScript.js and
49
+ utils/lazyStylesheet.js. */
49
50
  useLazyScript(ASSET_BASE + '/hex-rain.js', 'hex-rain');
51
+ useLazyStylesheet(ASSET_BASE + '/hex-rain.css', 'hex-rain');
50
52
 
51
53
  /* The runtime exposes window.HexRain.hydrate() once it has registered
52
54
  itself. Poll one rAF tick per frame until it's there, then call it
@@ -67,15 +69,10 @@ export default function HexRain({ariaLabel = 'Twelve apps mini-game, click each
67
69
  }, [isBrowser]);
68
70
 
69
71
  return (
70
- <>
71
- <Head>
72
- <link rel="stylesheet" href={ASSET_BASE + '/hex-rain.css'} />
73
- </Head>
74
- <div
75
- ref={ref}
76
- className={['hex-rain', className].filter(Boolean).join(' ')}
77
- aria-label={ariaLabel}
78
- />
79
- </>
72
+ <div
73
+ ref={ref}
74
+ className={['hex-rain', className].filter(Boolean).join(' ')}
75
+ aria-label={ariaLabel}
76
+ />
80
77
  );
81
78
  }
@@ -44,25 +44,24 @@
44
44
  */
45
45
 
46
46
  import React from 'react';
47
- import Head from '@docusaurus/Head';
48
47
  import {useLazyScript} from '../../utils/lazyScript';
48
+ import {useLazyStylesheet} from '../../utils/lazyStylesheet';
49
49
 
50
50
  const ASSET_BASE = '/lib';
51
51
 
52
52
  export default function PlatformDiagram({workspace, lists = [], flows = []}) {
53
- /* platform-diagram.js is loaded post-hydration. The runtime registers
54
- a custom-element definition that upgrades any <platform-diagram>
55
- element on the page; running it after React hydration prevents the
56
- custom-element constructor from mutating the DOM mid-hydration and
57
- producing #418 / #423 warnings. See utils/lazyScript.js. */
53
+ /* platform-diagram.{js,css} are loaded post-hydration. The runtime
54
+ registers a custom-element definition that upgrades any
55
+ <platform-diagram> element on the page; running it after React
56
+ hydration prevents the custom-element constructor from mutating the
57
+ DOM mid-hydration and producing #418 / #423 warnings. The matching
58
+ stylesheet is held off the render path so its 19 KB doesn't block
59
+ LCP on pages that render this component. */
58
60
  useLazyScript(ASSET_BASE + '/platform-diagram.js', 'platform-diagram');
61
+ useLazyStylesheet(ASSET_BASE + '/platform-diagram.css', 'platform-diagram');
59
62
 
60
63
  return (
61
64
  <>
62
- <Head>
63
- <link rel="stylesheet" href={ASSET_BASE + '/platform-diagram.css'} />
64
- </Head>
65
-
66
65
  <platform-diagram>
67
66
  {workspace && (
68
67
  <pd-workspace
@@ -6,22 +6,16 @@
6
6
  * "section-head" pattern (eyebrow + h2 + lede) above the diagram.
7
7
  *
8
8
  * The platform-diagram custom element is registered by
9
- * /lib/platform-diagram.js, which sites must include via <Head> on
10
- * any page that uses this section. The CSS is similarly served from
11
- * /lib/platform-diagram.css.
9
+ * /lib/platform-diagram.js and styled by /lib/platform-diagram.css.
10
+ * Both are loaded post-hydration by the <PlatformDiagram /> component;
11
+ * sites no longer need to inject anything in <Head> manually.
12
12
  *
13
13
  * Mirrors the .platform section in preview/pages/landing.html.
14
14
  *
15
15
  * Usage in MDX:
16
16
  *
17
- * import Head from '@docusaurus/Head';
18
17
  * import {PlatformOverview} from '@conduction/docusaurus-preset/components';
19
18
  *
20
- * <Head>
21
- * <link rel="stylesheet" href="/lib/platform-diagram.css" />
22
- * <script src="/lib/platform-diagram.js" defer />
23
- * </Head>
24
- *
25
19
  * <PlatformOverview
26
20
  * eyebrow="The platform"
27
21
  * title={<>Five components.<br/>Twenty-four apps. One <span className="next-blue">Nextcloud</span> workspace.</>}
@@ -20,12 +20,12 @@
20
20
 
21
21
  import React, {useEffect} from 'react';
22
22
  import Link from '@docusaurus/Link';
23
- import Head from '@docusaurus/Head';
24
23
  import {useLocation} from '@docusaurus/router';
25
24
  import {useThemeConfig} from '@docusaurus/theme-common';
26
25
  import useIsBrowser from '@docusaurus/useIsBrowser';
27
26
  import {brandFor} from '../brand.jsx';
28
27
  import {useLazyScript} from '../../utils/lazyScript';
28
+ import {useLazyStylesheet} from '../../utils/lazyStylesheet';
29
29
  import GameModal from '../../components/GameModal/GameModal';
30
30
 
31
31
  function FooterLink({label, href, to}) {
@@ -95,20 +95,23 @@ export default function Footer() {
95
95
  const wordmark = brand ? brand.wordmark : defaultWordmark;
96
96
  const brandRow = !brand && Array.isArray(footerBrand?.brands) ? footerBrand.brands : null;
97
97
 
98
- /* canal-footer.js is loaded post-hydration so its DOM mutations
99
- (filling .skyline, animating boats) don't trip React hydration
100
- mismatches. See docs in utils/lazyScript.js. We always load
101
- canal-footer.js even when minigames are off, because the same
102
- script populates the static skyline; canal-footer.js is null-safe
103
- against a missing game-hud, so the game wiring no-ops cleanly. */
98
+ /* canal-footer.{js,css} are loaded post-hydration. The script's DOM
99
+ mutations (filling .skyline, animating boats) don't trip React
100
+ hydration mismatches, and the stylesheet doesn't block first paint.
101
+ See docs in utils/lazyScript.js and utils/lazyStylesheet.js. We
102
+ always load canal-footer.{js,css} even when minigames are off,
103
+ because the same script populates the static skyline and the same
104
+ stylesheet styles the canal frame. */
104
105
  useLazyScript('/lib/canal-footer.js', 'canal-footer');
106
+ useLazyStylesheet('/lib/canal-footer.css', 'canal-footer');
105
107
 
106
- /* kade-cyclist.js is the second hidden footer minigame. Unlike the
107
- canal script it has no static side-effects, so when a product page
108
- opts out of minigames we feed `useLazyScript` a falsy src to skip
109
- the load. The hook still runs unconditionally — rules-of-hooks
110
- stays compliant. */
108
+ /* kade-cyclist.{js,css} are the second hidden footer minigame. Unlike
109
+ the canal script they have no static side-effects, so when a product
110
+ page opts out of minigames we feed both hooks a falsy value to skip
111
+ the load. The hooks still run unconditionally — rules-of-hooks stays
112
+ compliant. */
111
113
  useLazyScript(minigamesOn ? '/lib/kade-cyclist.js' : null, 'kade-cyclist');
114
+ useLazyStylesheet(minigamesOn ? '/lib/kade-cyclist.css' : null, 'kade-cyclist');
112
115
 
113
116
  /* Re-hydrate the canal-footer + kade-cyclist runtimes on every mount,
114
117
  including SPA route changes that re-render this Footer component.
@@ -143,11 +146,6 @@ export default function Footer() {
143
146
 
144
147
  return (
145
148
  <>
146
- <Head>
147
- <link rel="stylesheet" href="/lib/canal-footer.css" />
148
- <link rel="stylesheet" href="/lib/kade-cyclist.css" />
149
- </Head>
150
-
151
149
  <footer className="canal-footer" aria-label="Site footer">
152
150
  {/* Skyline placeholder — canal-footer.js fills it with cloned house templates */}
153
151
  <div className="skyline" role="presentation" />
@@ -0,0 +1,42 @@
1
+ /**
2
+ * useLazyStylesheet: load a runtime stylesheet after React hydration.
3
+ *
4
+ * Mirror of useLazyScript for CSS. The preset's heavier decorative
5
+ * stylesheets (canal-footer, kade-cyclist, hex-rain, logo-memory,
6
+ * platform-diagram) were injected via `<Head><link rel="stylesheet" />`,
7
+ * which Docusaurus SSGs into the rendered `<head>` and the browser
8
+ * treats as render-blocking. With five of these on a site that loads
9
+ * any landing page, LCP and TBT both regress past the "good" CWV
10
+ * threshold (LCP < 2.5s, INP < 200ms).
11
+ *
12
+ * Injecting via useEffect moves the load past hydration, so the
13
+ * stylesheet downloads in parallel with first paint instead of
14
+ * blocking it. Trade-off: a brief style-less flash before the CSS
15
+ * applies. Mitigated for above-fold callers by inlining the small
16
+ * structural rules into brand.css and only lazy-loading the
17
+ * decorative parts.
18
+ *
19
+ * The injected link has data-lazy-stylesheet="<key>" so SPA route
20
+ * changes don't double-inject, mirroring useLazyScript's idempotency.
21
+ */
22
+
23
+ import {useEffect} from 'react';
24
+ import useIsBrowser from '@docusaurus/useIsBrowser';
25
+
26
+ /**
27
+ * Pass a falsy `href` to skip the load entirely. The hook still runs
28
+ * unconditionally so call sites stay rules-of-hooks-compliant.
29
+ */
30
+ export function useLazyStylesheet(href, key) {
31
+ const isBrowser = useIsBrowser();
32
+ useEffect(() => {
33
+ if (!isBrowser) return;
34
+ if (!href) return;
35
+ if (document.querySelector(`link[data-lazy-stylesheet="${key}"]`)) return;
36
+ const link = document.createElement('link');
37
+ link.rel = 'stylesheet';
38
+ link.href = href;
39
+ link.dataset.lazyStylesheet = key;
40
+ document.head.appendChild(link);
41
+ }, [isBrowser, href, key]);
42
+ }