@conduction/docusaurus-preset 3.38.0 → 3.39.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.
Files changed (37) hide show
  1. package/MISSING_COMPONENTS.md +1 -0
  2. package/package.json +1 -1
  3. package/src/__tests__/no-icu-messages.test.js +57 -0
  4. package/src/components/BlueprintRush/BlueprintRush.jsx +238 -0
  5. package/src/components/BlueprintRush/BlueprintRush.module.css +186 -0
  6. package/src/components/BlueprintRush/__tests__/engine.test.js +154 -0
  7. package/src/components/BlueprintRush/engine.js +172 -0
  8. package/src/components/DeadlineDefender/DeadlineDefender.jsx +237 -0
  9. package/src/components/DeadlineDefender/DeadlineDefender.module.css +193 -0
  10. package/src/components/DeadlineDefender/__tests__/engine.test.js +163 -0
  11. package/src/components/DeadlineDefender/engine.js +188 -0
  12. package/src/components/DetailHero/DetailHero.jsx +11 -4
  13. package/src/components/DetailHero/__tests__/DetailHero.downloads.test.js +160 -0
  14. package/src/components/FeaturedCard/FeaturedCard.jsx +14 -1
  15. package/src/components/FeaturedCard/FeaturedCard.module.css +5 -0
  16. package/src/components/FeaturedCard/__tests__/FeaturedCard.visual.test.js +110 -0
  17. package/src/components/GameModal/GameModal.jsx +255 -50
  18. package/src/components/GameModal/GameModal.module.css +103 -0
  19. package/src/components/GameModal/__tests__/scores.test.js +123 -0
  20. package/src/components/GameModal/__tests__/share.test.js +83 -0
  21. package/src/components/GameModal/scores.js +149 -0
  22. package/src/components/GameModal/share.js +97 -0
  23. package/src/components/RecordRun/RecordRun.jsx +245 -0
  24. package/src/components/RecordRun/RecordRun.module.css +208 -0
  25. package/src/components/RecordRun/__tests__/engine.test.js +191 -0
  26. package/src/components/RecordRun/engine.js +180 -0
  27. package/src/components/StampRush/StampRush.jsx +232 -0
  28. package/src/components/StampRush/StampRush.module.css +188 -0
  29. package/src/components/StampRush/__tests__/engine.test.js +182 -0
  30. package/src/components/StampRush/engine.js +185 -0
  31. package/src/components/ThemeSeamMock/ThemeSeamMock.jsx +79 -0
  32. package/src/components/ThemeSeamMock/ThemeSeamMock.module.css +178 -0
  33. package/src/components/ThemeSeamMock/__tests__/ThemeSeamMock.render.test.js +122 -0
  34. package/src/components/index.js +5 -0
  35. package/src/data/app-downloads.js +21 -0
  36. package/src/index.js +10 -0
  37. package/src/theme/Footer/index.jsx +10 -1
@@ -0,0 +1,122 @@
1
+ /**
2
+ * ThemeSeamMock.render.test.js — renders the real <ThemeSeamMock> JSX
3
+ * to static markup and asserts on the output.
4
+ *
5
+ * What matters about this mock is that both layers are the same
6
+ * application frame and that the themed layer differs only by the
7
+ * wrapper that redefines the tokens. A test that let the two layers
8
+ * drift apart would be checking a screen change, not a finish change,
9
+ * which is the opposite of the claim the component makes.
10
+ *
11
+ * Same esbuild-bundle-then-renderToStaticMarkup technique as
12
+ * AppMock.render.test.js; CSS modules are stubbed with an identity
13
+ * proxy, so class assertions read as the source class names.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const test = require('node:test');
19
+ const {before, after} = test;
20
+ const assert = require('node:assert/strict');
21
+ const path = require('node:path');
22
+ const fs = require('node:fs/promises');
23
+ const {build} = require('esbuild');
24
+ const React = require('react');
25
+ const {renderToStaticMarkup} = require('react-dom/server');
26
+
27
+ const COMPONENT = path.resolve(__dirname, '..', 'ThemeSeamMock.jsx');
28
+ const PRESET_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
29
+ const SCRATCH = path.join(PRESET_ROOT, '.tmp-theme-seam-test');
30
+
31
+ const cssModuleStub = {
32
+ name: 'css-module-stub',
33
+ setup(b) {
34
+ b.onResolve({filter: /\.module\.css$/}, (args) => ({path: args.path, namespace: 'css-stub'}));
35
+ b.onLoad({filter: /.*/, namespace: 'css-stub'}, () => ({
36
+ contents: 'export default new Proxy({}, {get: (_, p) => p});',
37
+ loader: 'js',
38
+ }));
39
+ },
40
+ };
41
+
42
+ let ThemeSeamMock;
43
+
44
+ before(async () => {
45
+ await fs.mkdir(SCRATCH, {recursive: true});
46
+ const tmpDir = await fs.mkdtemp(path.join(SCRATCH, 'run-'));
47
+ const outFile = path.join(tmpDir, 'bundle.cjs');
48
+ await build({
49
+ entryPoints: [COMPONENT],
50
+ outfile: outFile,
51
+ bundle: true,
52
+ format: 'cjs',
53
+ jsx: 'automatic',
54
+ jsxImportSource: 'react',
55
+ tsconfigRaw: {compilerOptions: {jsx: 'react-jsx', jsxImportSource: 'react'}},
56
+ platform: 'node',
57
+ external: ['react'],
58
+ plugins: [cssModuleStub],
59
+ logLevel: 'warning',
60
+ });
61
+ delete require.cache[require.resolve(outFile)];
62
+ ThemeSeamMock = require(outFile).default;
63
+ });
64
+
65
+ after(async () => {
66
+ await fs.rm(SCRATCH, {recursive: true, force: true});
67
+ });
68
+
69
+ function render(props) {
70
+ return renderToStaticMarkup(React.createElement(ThemeSeamMock, props));
71
+ }
72
+
73
+ function frames(html) {
74
+ return html.match(/class="frame size-\w+[^"]*"/g) || [];
75
+ }
76
+
77
+ test('renders two layers of the same app frame, plus the seam', () => {
78
+ const html = render({app: 'openregister'});
79
+ assert.equal(frames(html).length, 2, 'expected a stock layer and a themed layer');
80
+ assert.match(html, /class="layer themed lasuite"/);
81
+ assert.match(html, /class="seam"/);
82
+ assert.doesNotMatch(html, /Unknown app/);
83
+ });
84
+
85
+ test('both layers render the identical frame, so the wipe changes finish and nothing else', () => {
86
+ const html = render({app: 'procest'});
87
+ /* Split on the themed wrapper: what follows must repeat what came
88
+ before it, or the two layers are showing different screens. */
89
+ const [stock, themed] = html.split('class="layer themed lasuite"');
90
+ const bodyOf = (s) => (s.match(/<div class="body[^]*$/) || [''])[0].replace(/\s+/g, '');
91
+ assert.ok(bodyOf(stock).length > 0, 'no stock layer body rendered');
92
+ assert.equal(
93
+ bodyOf(stock).slice(0, 400),
94
+ bodyOf(themed).slice(0, 400),
95
+ 'the themed layer is not the same frame as the stock layer',
96
+ );
97
+ });
98
+
99
+ test('the inner AppMocks never run their own loop, which would compete with the seam', () => {
100
+ const html = render({app: 'openregister'});
101
+ assert.equal(frames(html).filter((c) => c.includes('static')).length, 2);
102
+ });
103
+
104
+ test('running={false} freezes the scene on the themed end state', () => {
105
+ assert.match(render({app: 'openregister', running: false}), /class="seamScene size-md static"/);
106
+ assert.doesNotMatch(render({app: 'openregister'}), /seamScene size-md static/);
107
+ });
108
+
109
+ test('an unknown theme falls back to lasuite rather than rendering unthemed', () => {
110
+ assert.match(render({app: 'openregister', theme: 'nope'}), /class="layer themed lasuite"/);
111
+ });
112
+
113
+ test('size is forwarded to both frames', () => {
114
+ const html = render({app: 'openregister', size: 'sm'});
115
+ assert.match(html, /class="seamScene size-sm"/);
116
+ assert.equal(frames(html).filter((c) => c.includes('size-sm')).length, 2);
117
+ });
118
+
119
+ test('a label renders as the caption, and is absent otherwise', () => {
120
+ assert.match(render({app: 'openregister', label: 'La Suite'}), /<figcaption class="caption">La Suite</);
121
+ assert.doesNotMatch(render({app: 'openregister'}), /figcaption/);
122
+ });
@@ -60,6 +60,10 @@ export {default as Pipeline, PipelineStep, IconList} from './Pipeline/Pipeline.j
60
60
  export {default as FacetedFilters, FilterChip} from './FacetedFilters/FacetedFilters.jsx';
61
61
  export {default as CookieCli} from './CookieCli/CookieCli.jsx';
62
62
  export {default as GameModal} from './GameModal/GameModal.jsx';
63
+ export {default as StampRush} from './StampRush/StampRush.jsx';
64
+ export {default as DeadlineDefender} from './DeadlineDefender/DeadlineDefender.jsx';
65
+ export {default as BlueprintRush} from './BlueprintRush/BlueprintRush.jsx';
66
+ export {default as RecordRun} from './RecordRun/RecordRun.jsx';
63
67
 
64
68
  /* Diagram-set web-component React wrappers (cn-hex, cn-platform,
65
69
  cn-domain-tree, cn-pipeline, cn-side-box, cn-honeycomb-bg, cn-pair,
@@ -77,6 +81,7 @@ export {default as ComposeBlock} from './ComposeBlock/ComposeBlock.jsx';
77
81
  export {default as AppsGrid} from './AppsGrid/AppsGrid.jsx';
78
82
  export {default as AppMock} from './AppMock/AppMock.jsx';
79
83
  export {default as WidgetMock} from './WidgetMock/WidgetMock.jsx';
84
+ export {default as ThemeSeamMock} from './ThemeSeamMock/ThemeSeamMock.jsx';
80
85
  export {default as FlowMock} from './FlowMock/FlowMock.jsx';
81
86
  export {default as LeafMock} from './LeafMock/LeafMock.jsx';
82
87
  export {default as BuildMock} from './BuildMock/BuildMock.jsx';
@@ -65,3 +65,24 @@ export function downloadsForApp(appId) {
65
65
  export function formatDownloads(n, locale = 'en') {
66
66
  return Number(n || 0).toLocaleString(locale);
67
67
  }
68
+
69
+ /* Below this many downloads an app shows no counter at all.
70
+ A freshly published app sits on a handful of downloads for weeks,
71
+ and printing "5 downloads" next to it argues against the app. The
72
+ counter is there to show traction, so it appears once there is
73
+ traction to show. Aggregate figures (totalDownloads on the
74
+ Connext and Common Ground pages) are not subject to this: the
75
+ fleet total is a real number on its own. */
76
+ export const MIN_DISPLAYED_DOWNLOADS = 1000;
77
+
78
+ /**
79
+ * Whether a download count is worth putting on screen.
80
+ *
81
+ * Used for the per-app counter on DetailHero, including its
82
+ * schema.org InteractionCounter: a number we hide from the page does
83
+ * not belong in the structured data either, or search results quote
84
+ * back the figure the page declines to show.
85
+ */
86
+ export function showDownloads(n) {
87
+ return Number(n || 0) >= MIN_DISPLAYED_DOWNLOADS;
88
+ }
package/src/index.js CHANGED
@@ -439,6 +439,14 @@ const baseFooter = () => ({
439
439
  * footer (per-property fallback: any of style/links/copyright the
440
440
  * site omits keeps its brand default — pass `footer: { links: [...] }`
441
441
  * to swap columns while inheriting the KvK/BTW copyright),
442
+ * minigamesRoster (optional; [{id, label}] of the games this site
443
+ * ships, when they are not the preset's default five. A label may
444
+ * be a per-locale map, since themeConfig is never translated.)
445
+ * minigamesShare (optional; {hashtag, url, prize, prizeHref,
446
+ * prizeLinkLabel} passed to
447
+ * the game-over dialog's share block. The hashtag defaults to
448
+ * #IReadTheKit and the url to siteConfig.url; prize renders a line
449
+ * under the share buttons, linked when prizeHref is given.)
442
450
  * minigames (default true; set false to drop the brand canal-footer's
443
451
  * boat-sinking + kade-cyclist mini-games on product pages while
444
452
  * keeping the static skyline + canal decoration),
@@ -574,6 +582,8 @@ function createConfig(opts) {
574
582
  site opts out, while still keeping the static skyline +
575
583
  canal decoration. Default true preserves prior behaviour. */
576
584
  minigames: opts.minigames !== false,
585
+ minigamesShare: opts.minigamesShare,
586
+ minigamesRoster: opts.minigamesRoster,
577
587
  /* Footer brand block (the wordmark + tagline + triad + socials
578
588
  on the left of the canal-footer grid).
579
589
  undefined -> wordmark = 'Conduction' (product-page default;
@@ -52,6 +52,15 @@ export default function Footer() {
52
52
  surfaced through createConfig() opts. See createConfig() in
53
53
  ../../index.js for the option semantics. */
54
54
  const minigamesOn = themeConfig.minigames !== false;
55
+ /* Campaign copy for the game-over dialog's share block (hashtag, a
56
+ prize line and where its rules live). A campaign is per site and
57
+ time-bound, so it lives in themeConfig rather than in the
58
+ component: the preset ships the mechanism, the site ships the
59
+ giveaway. */
60
+ const minigamesShare = themeConfig.minigamesShare || undefined;
61
+ /* The roster of games this site ships, when it is not the preset's
62
+ default five. Labels may be per-locale maps. */
63
+ const minigamesRoster = themeConfig.minigamesRoster || undefined;
55
64
  const footerBrand = themeConfig.footerBrand || null;
56
65
  /* legalLinks: opt-in/out of the Privacy / Terms / ISO links inside
57
66
  the legal-bar, and the two ISO 9001/27001 certification badges on
@@ -479,7 +488,7 @@ export default function Footer() {
479
488
  Only mounted when minigames are on so a product page doesn't
480
489
  carry the dialog DOM + listeners for an interaction it can't
481
490
  trigger. */}
482
- {minigamesOn && <GameModal />}
491
+ {minigamesOn && <GameModal share={minigamesShare} games={minigamesRoster} />}
483
492
  </>
484
493
  );
485
494
  }