@conduction/docusaurus-preset 0.1.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 (163) hide show
  1. package/MISSING_COMPONENTS.md +109 -0
  2. package/README.md +171 -0
  3. package/package.json +59 -0
  4. package/src/components/AgentTrace/AgentTrace.jsx +128 -0
  5. package/src/components/AgentTrace/AgentTrace.module.css +115 -0
  6. package/src/components/AppMock/AppMock.jsx +86 -0
  7. package/src/components/AppMock/AppMock.module.css +629 -0
  8. package/src/components/AppMock/variants/DeciDeskMock.jsx +71 -0
  9. package/src/components/AppMock/variants/DocuDeskMock.jsx +69 -0
  10. package/src/components/AppMock/variants/LarpingAppMock.jsx +59 -0
  11. package/src/components/AppMock/variants/MyDashBiMock.jsx +135 -0
  12. package/src/components/AppMock/variants/MyDashMock.jsx +96 -0
  13. package/src/components/AppMock/variants/MyDashTilesMock.jsx +103 -0
  14. package/src/components/AppMock/variants/MyDashWidgetsMock.jsx +123 -0
  15. package/src/components/AppMock/variants/NLDesignMock.jsx +70 -0
  16. package/src/components/AppMock/variants/OpenCatalogiMock.jsx +61 -0
  17. package/src/components/AppMock/variants/OpenConnectorMock.jsx +83 -0
  18. package/src/components/AppMock/variants/OpenRegisterMock.jsx +100 -0
  19. package/src/components/AppMock/variants/OpenWooMock.jsx +61 -0
  20. package/src/components/AppMock/variants/PipelinQMock.jsx +88 -0
  21. package/src/components/AppMock/variants/ProcestMock.jsx +87 -0
  22. package/src/components/AppMock/variants/SoftwareCatalogMock.jsx +71 -0
  23. package/src/components/AppMock/variants/ZaakAfhandelAppMock.jsx +71 -0
  24. package/src/components/AppsGrid/AppsGrid.jsx +84 -0
  25. package/src/components/AppsGrid/AppsGrid.module.css +46 -0
  26. package/src/components/AppsPreview/AppsPreview.jsx +85 -0
  27. package/src/components/AppsPreview/AppsPreview.module.css +128 -0
  28. package/src/components/Clients/Clients.jsx +205 -0
  29. package/src/components/Clients/Clients.module.css +166 -0
  30. package/src/components/ComposeBlock/ComposeBlock.jsx +70 -0
  31. package/src/components/ComposeBlock/ComposeBlock.module.css +74 -0
  32. package/src/components/ConductionBg/ConductionBg.jsx +150 -0
  33. package/src/components/ConductionBg/ConductionBg.module.css +41 -0
  34. package/src/components/ContentCard/ContentCard.jsx +126 -0
  35. package/src/components/ContentCard/ContentCard.module.css +84 -0
  36. package/src/components/ContentDetailHero/ContentDetailHero.jsx +136 -0
  37. package/src/components/ContentDetailHero/ContentDetailHero.module.css +96 -0
  38. package/src/components/ContentTypeFilter/ContentTypeFilter.jsx +103 -0
  39. package/src/components/ContentTypeFilter/ContentTypeFilter.module.css +60 -0
  40. package/src/components/ContentTypeFilter/contentTypes.js +58 -0
  41. package/src/components/CookieCli/CookieCli.jsx +223 -0
  42. package/src/components/CookieCli/CookieCli.module.css +166 -0
  43. package/src/components/CtaBanner/CtaBanner.jsx +61 -0
  44. package/src/components/CtaBanner/CtaBanner.module.css +65 -0
  45. package/src/components/DetailHero/DetailHero.jsx +143 -0
  46. package/src/components/DetailHero/DetailHero.module.css +154 -0
  47. package/src/components/Diagrams/Diagrams.jsx +148 -0
  48. package/src/components/EmployeeCard/EmployeeCard.jsx +127 -0
  49. package/src/components/EmployeeCard/EmployeeCard.module.css +144 -0
  50. package/src/components/ExternalAppShelf/ExternalAppShelf.jsx +61 -0
  51. package/src/components/ExternalAppShelf/ExternalAppShelf.module.css +90 -0
  52. package/src/components/FAQ/FAQ.jsx +42 -0
  53. package/src/components/FAQ/FAQ.module.css +74 -0
  54. package/src/components/FacetedFilters/FacetedFilters.jsx +125 -0
  55. package/src/components/FacetedFilters/FacetedFilters.module.css +133 -0
  56. package/src/components/FeatureGrid/FeatureGrid.jsx +94 -0
  57. package/src/components/FeatureGrid/FeatureGrid.module.css +114 -0
  58. package/src/components/FeatureList/FeatureList.jsx +54 -0
  59. package/src/components/FeatureList/FeatureList.module.css +52 -0
  60. package/src/components/FeaturedCard/FeaturedCard.jsx +101 -0
  61. package/src/components/FeaturedCard/FeaturedCard.module.css +98 -0
  62. package/src/components/GameModal/GameModal.jsx +197 -0
  63. package/src/components/GameModal/GameModal.module.css +184 -0
  64. package/src/components/Hero/Hero.jsx +101 -0
  65. package/src/components/Hero/Hero.module.css +95 -0
  66. package/src/components/HexBackground/HexBackground.jsx +56 -0
  67. package/src/components/HexBackground/HexBackground.module.css +73 -0
  68. package/src/components/HexNetwork/HexNetwork.jsx +141 -0
  69. package/src/components/HexNetwork/HexNetwork.module.css +187 -0
  70. package/src/components/HexRain/HexRain.jsx +81 -0
  71. package/src/components/HowSteps/HowSteps.jsx +57 -0
  72. package/src/components/HowSteps/HowSteps.module.css +52 -0
  73. package/src/components/ManagedCommonGround/ManagedCommonGround.jsx +78 -0
  74. package/src/components/ManagedCommonGround/ManagedCommonGround.module.css +16 -0
  75. package/src/components/NewsletterCta/NewsletterCta.jsx +83 -0
  76. package/src/components/NewsletterCta/NewsletterCta.module.css +103 -0
  77. package/src/components/PairCard/PairCard.jsx +58 -0
  78. package/src/components/PairCard/PairCard.module.css +54 -0
  79. package/src/components/PartnerCard/PartnerCard.jsx +130 -0
  80. package/src/components/PartnerCard/PartnerCard.module.css +198 -0
  81. package/src/components/PartnerDirectory/PartnerDirectory.jsx +122 -0
  82. package/src/components/PartnerDirectory/PartnerDirectory.module.css +25 -0
  83. package/src/components/PartnerSidecard/PartnerSidecard.jsx +116 -0
  84. package/src/components/PartnerSidecard/PartnerSidecard.module.css +185 -0
  85. package/src/components/Pipeline/Pipeline.jsx +198 -0
  86. package/src/components/Pipeline/Pipeline.module.css +206 -0
  87. package/src/components/PlatformDiagram/PlatformDiagram.jsx +110 -0
  88. package/src/components/PlatformOverview/PlatformOverview.jsx +68 -0
  89. package/src/components/PlatformOverview/PlatformOverview.module.css +71 -0
  90. package/src/components/ReferenceCard/ReferenceCard.jsx +44 -0
  91. package/src/components/ReferenceCard/ReferenceCard.module.css +57 -0
  92. package/src/components/RelatedPosts/RelatedPosts.jsx +58 -0
  93. package/src/components/RelatedPosts/RelatedPosts.module.css +51 -0
  94. package/src/components/RotatingCards/RotatingCards.jsx +98 -0
  95. package/src/components/RotatingCards/RotatingCards.module.css +153 -0
  96. package/src/components/Showcase/Showcase.jsx +129 -0
  97. package/src/components/Showcase/Showcase.module.css +168 -0
  98. package/src/components/SolutionCard/SolutionCard.jsx +83 -0
  99. package/src/components/SolutionCard/SolutionCard.module.css +99 -0
  100. package/src/components/StatsStrip/StatsStrip.jsx +38 -0
  101. package/src/components/StatsStrip/StatsStrip.module.css +53 -0
  102. package/src/components/WidgetShelf/WidgetShelf.jsx +67 -0
  103. package/src/components/WidgetShelf/WidgetShelf.module.css +73 -0
  104. package/src/components/index.js +96 -0
  105. package/src/components/primitives/AuthorByline.jsx +85 -0
  106. package/src/components/primitives/AuthorByline.module.css +57 -0
  107. package/src/components/primitives/BrandCitation.jsx +71 -0
  108. package/src/components/primitives/Button.jsx +46 -0
  109. package/src/components/primitives/Button.module.css +88 -0
  110. package/src/components/primitives/Card.jsx +42 -0
  111. package/src/components/primitives/Card.module.css +42 -0
  112. package/src/components/primitives/Eyebrow.jsx +37 -0
  113. package/src/components/primitives/Eyebrow.module.css +19 -0
  114. package/src/components/primitives/HexBullet.jsx +37 -0
  115. package/src/components/primitives/HexBullet.module.css +16 -0
  116. package/src/components/primitives/HexThumbnail.jsx +70 -0
  117. package/src/components/primitives/HexThumbnail.module.css +45 -0
  118. package/src/components/primitives/Pill.jsx +42 -0
  119. package/src/components/primitives/Pill.module.css +30 -0
  120. package/src/components/primitives/Section.jsx +51 -0
  121. package/src/components/primitives/Section.module.css +31 -0
  122. package/src/components/primitives/SectionHead.jsx +36 -0
  123. package/src/components/primitives/SectionHead.module.css +43 -0
  124. package/src/components/primitives/index.js +22 -0
  125. package/src/css/brand.css +158 -0
  126. package/src/css/tokens.css +12 -0
  127. package/src/data/app-downloads.js +42 -0
  128. package/src/diagrams/README.md +74 -0
  129. package/src/diagrams/cn-domain-tree.js +105 -0
  130. package/src/diagrams/cn-hex-prism.js +163 -0
  131. package/src/diagrams/cn-hex.js +181 -0
  132. package/src/diagrams/cn-honeycomb-bg.js +135 -0
  133. package/src/diagrams/cn-pipeline.js +150 -0
  134. package/src/diagrams/cn-platform.js +156 -0
  135. package/src/diagrams/cn-side-box.js +104 -0
  136. package/src/diagrams/index.js +28 -0
  137. package/src/index.js +183 -0
  138. package/src/theme/Footer/index.jsx +516 -0
  139. package/src/theme/MDXPage/index.jsx +134 -0
  140. package/src/theme/Navbar/index.jsx +120 -0
  141. package/src/theme/Navbar/styles.module.css +114 -0
  142. package/src/theme/brand.jsx +63 -0
  143. package/src/theme.js +45 -0
  144. package/src/utils/lazyScript.js +37 -0
  145. package/static/img/favicon.svg +14 -0
  146. package/static/img/honeycomb-scatter.svg +23 -0
  147. package/static/img/honeycomb-watermark.svg +108 -0
  148. package/static/img/logo-dark.svg +11 -0
  149. package/static/img/logo.svg +14 -0
  150. package/static/img/nextcloud-logo.svg +5 -0
  151. package/static/lib/canal-footer.css +418 -0
  152. package/static/lib/canal-footer.js +499 -0
  153. package/static/lib/clients-flow.js +317 -0
  154. package/static/lib/conduction-bg.css +50 -0
  155. package/static/lib/conduction-bg.js +122 -0
  156. package/static/lib/hex-rain.css +128 -0
  157. package/static/lib/hex-rain.js +284 -0
  158. package/static/lib/kade-cyclist.css +264 -0
  159. package/static/lib/kade-cyclist.js +420 -0
  160. package/static/lib/logo-memory.css +219 -0
  161. package/static/lib/logo-memory.js +540 -0
  162. package/static/lib/platform-diagram.css +458 -0
  163. package/static/lib/platform-diagram.js +414 -0
@@ -0,0 +1,54 @@
1
+ /**
2
+ * <FeatureList /> + <FeatureItem />
3
+ *
4
+ * Long, single-column feature list from preview/components/feature-list.html.
5
+ * Used in the broad column on app and solution detail pages, scales
6
+ * cleanly to 6-12 items.
7
+ *
8
+ * Each feature pairs a cobalt-50 hex glyph with a heading and a short
9
+ * body paragraph.
10
+ *
11
+ * Usage in MDX:
12
+ *
13
+ * <FeatureList items={[
14
+ * {icon: <svg>...</svg>, title: 'Schemas you write once.', body: '...'},
15
+ * {icon: <svg>...</svg>, title: 'One backbone, every app.', body: '...'},
16
+ * ]} />
17
+ *
18
+ * Or with children for full MDX flexibility:
19
+ *
20
+ * <FeatureList>
21
+ * <FeatureItem icon={<svg/>} title="...">
22
+ * Body paragraph...
23
+ * </FeatureItem>
24
+ * </FeatureList>
25
+ */
26
+
27
+ import React from 'react';
28
+ import styles from './FeatureList.module.css';
29
+
30
+ export function FeatureItem({icon, title, body, children}) {
31
+ return (
32
+ <div className={styles.feature}>
33
+ <div className={styles.glyph}>{icon}</div>
34
+ <div>
35
+ {title && <h3 className={styles.title}>{title}</h3>}
36
+ {/* `body` prop is a string or inline JSX, wrap in <p> for typography.
37
+ `children` come from MDX which already wraps loose text in <p>;
38
+ using <div> here avoids the nested <p> DOM-validation warning. */}
39
+ {body && <p className={styles.body}>{body}</p>}
40
+ {!body && children && <div className={styles.body}>{children}</div>}
41
+ </div>
42
+ </div>
43
+ );
44
+ }
45
+
46
+ export default function FeatureList({items, children, className}) {
47
+ return (
48
+ <div className={[styles.list, className].filter(Boolean).join(' ')}>
49
+ {items
50
+ ? items.map((it, i) => <FeatureItem key={i} {...it} />)
51
+ : children}
52
+ </div>
53
+ );
54
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * <FeatureList /> styles. Mirrors feature-list.css exactly.
3
+ */
4
+
5
+ .list {
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: 40px;
9
+ font-family: var(--conduction-typography-font-family-body);
10
+ }
11
+
12
+ .feature {
13
+ display: grid;
14
+ grid-template-columns: 64px 1fr;
15
+ gap: 24px;
16
+ }
17
+
18
+ .glyph {
19
+ width: 56px;
20
+ height: 64px;
21
+ clip-path: var(--hex-pointy-top);
22
+ background: var(--c-cobalt-50);
23
+ color: var(--c-blue-cobalt);
24
+ display: flex;
25
+ align-items: center;
26
+ justify-content: center;
27
+ flex-shrink: 0;
28
+ }
29
+ .glyph svg {
30
+ width: 26px;
31
+ height: 26px;
32
+ stroke: currentColor;
33
+ stroke-width: 2;
34
+ fill: none;
35
+ }
36
+
37
+ .title {
38
+ font-size: 22px;
39
+ font-weight: 700;
40
+ letter-spacing: -0.01em;
41
+ margin: 0 0 8px;
42
+ color: var(--c-cobalt-900);
43
+ }
44
+ .title :global(.next-blue) { color: var(--c-nextcloud-blue); }
45
+
46
+ .body {
47
+ font-size: 15px;
48
+ color: var(--c-cobalt-700);
49
+ line-height: 1.6;
50
+ margin: 0;
51
+ }
52
+ .body :global(.next-blue) { color: var(--c-nextcloud-blue); }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * <FeaturedCard />
3
+ *
4
+ * Hero spot at the top of the academy landing. Cobalt-900 ground, body
5
+ * copy on the left, a large pointy-top hex with two satellite hexes on
6
+ * the right. Used to surface one editorial pick per page.
7
+ *
8
+ * Mirrors the .featured-card section in preview/components/academy.html.
9
+ *
10
+ * Per huisstijl, the satellite-3 hex is the page's single KNVB orange
11
+ * accent; the rest is solid cobalt + white. Pass `accent="cobalt"` to
12
+ * suppress the orange accent on screens that already use orange
13
+ * elsewhere.
14
+ *
15
+ * Usage in MDX:
16
+ *
17
+ * <FeaturedCard
18
+ * href="/posts/install-opencatalogi"
19
+ * eyebrow="Featured guide"
20
+ * title="Install OpenCatalogi in two minutes."
21
+ * lede="From app store to first register, no terminal required."
22
+ * ctaLabel="Read the guide"
23
+ * author={{ name: "Ruben van der Linde" }}
24
+ * date="2026-05-05"
25
+ * thumbnail={{ icon: <svg>...</svg> }}
26
+ * />
27
+ */
28
+
29
+ import React from 'react';
30
+ import HexThumbnail from '../primitives/HexThumbnail';
31
+ import HexBullet from '../primitives/HexBullet';
32
+ import AuthorByline from '../primitives/AuthorByline';
33
+ import styles from './FeaturedCard.module.css';
34
+
35
+ export default function FeaturedCard({
36
+ href,
37
+ eyebrow,
38
+ title,
39
+ lede,
40
+ ctaLabel = 'Read more',
41
+ author,
42
+ date,
43
+ dateLabel,
44
+ locale,
45
+ thumbnail,
46
+ accent = 'orange',
47
+ className,
48
+ }) {
49
+ const Tag = href ? 'a' : 'div';
50
+ const composed = [styles.card, className].filter(Boolean).join(' ');
51
+ const thumbProps = thumbnail || {};
52
+
53
+ return (
54
+ <Tag href={href} className={composed}>
55
+ <div className={styles.body}>
56
+ {eyebrow && (
57
+ <span className={styles.eyebrow}>
58
+ <HexBullet size="md" color="var(--c-orange-knvb)" />
59
+ <span>{eyebrow}</span>
60
+ </span>
61
+ )}
62
+ {title && <h2 className={styles.title}>{title}</h2>}
63
+ {lede && <p className={styles.lede}>{lede}</p>}
64
+
65
+ {(author || date) && (
66
+ <div className={styles.meta}>
67
+ <AuthorByline
68
+ name={author && author.name}
69
+ avatarSrc={author && author.avatarSrc}
70
+ initials={author && author.initials}
71
+ date={date}
72
+ dateLabel={dateLabel}
73
+ locale={locale}
74
+ tone="on-dark"
75
+ />
76
+ </div>
77
+ )}
78
+
79
+ {ctaLabel && (
80
+ <span className={styles.cta}>
81
+ <span>{ctaLabel}</span>
82
+ <span aria-hidden="true">→</span>
83
+ </span>
84
+ )}
85
+ </div>
86
+
87
+ <div className={styles.visual} aria-hidden="true">
88
+ {thumbProps.src
89
+ ? <HexThumbnail size="xl" tone="cobalt" src={thumbProps.src} alt={thumbProps.alt} />
90
+ : <HexThumbnail size="xl" tone={thumbProps.tone || 'cobalt'}>{thumbProps.icon}</HexThumbnail>}
91
+ <span className={[styles.satellite, styles.s1].join(' ')} />
92
+ <span className={[styles.satellite, styles.s2].join(' ')} />
93
+ <span className={[
94
+ styles.satellite,
95
+ styles.s3,
96
+ accent === 'orange' ? styles.s3Orange : styles.s3Cobalt,
97
+ ].join(' ')} />
98
+ </div>
99
+ </Tag>
100
+ );
101
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * <FeaturedCard /> styles. Mirrors .featured-card in
3
+ * preview/components/academy.css.
4
+ */
5
+
6
+ .card {
7
+ display: grid;
8
+ grid-template-columns: 1fr 1fr;
9
+ gap: var(--space-12);
10
+ padding: var(--space-12);
11
+ background: var(--c-cobalt-900);
12
+ color: white;
13
+ border-radius: var(--radius-xl);
14
+ align-items: center;
15
+ position: relative;
16
+ overflow: hidden;
17
+ text-decoration: none;
18
+ font-family: var(--conduction-typography-font-family-body);
19
+ }
20
+ .card:hover { text-decoration: none; color: white; }
21
+
22
+ @media (max-width: 900px) {
23
+ .card { grid-template-columns: 1fr; padding: var(--space-8); }
24
+ }
25
+
26
+ .body { min-width: 0; }
27
+
28
+ .eyebrow {
29
+ display: inline-flex;
30
+ align-items: center;
31
+ gap: 8px;
32
+ font-family: var(--conduction-typography-font-family-code);
33
+ font-size: 12px;
34
+ letter-spacing: 0.1em;
35
+ text-transform: uppercase;
36
+ color: var(--c-cobalt-300);
37
+ margin-bottom: var(--space-4);
38
+ }
39
+
40
+ .title {
41
+ font-size: 36px;
42
+ font-weight: 700;
43
+ letter-spacing: -0.02em;
44
+ line-height: 1.15;
45
+ color: white;
46
+ margin: 0 0 var(--space-4);
47
+ }
48
+ @media (max-width: 700px) { .title { font-size: 28px; } }
49
+
50
+ .lede {
51
+ font-size: 17px;
52
+ color: var(--c-cobalt-200);
53
+ line-height: 1.55;
54
+ margin: 0 0 var(--space-6);
55
+ max-width: 50ch;
56
+ }
57
+
58
+ .meta { margin-bottom: var(--space-5); }
59
+
60
+ .cta {
61
+ display: inline-flex;
62
+ align-items: center;
63
+ gap: 8px;
64
+ padding: 12px 22px;
65
+ background: white;
66
+ color: var(--c-cobalt-900);
67
+ font-weight: 600;
68
+ font-size: 14px;
69
+ border-radius: var(--radius-pill);
70
+ text-decoration: none;
71
+ transition: background 120ms ease;
72
+ }
73
+ .card:hover .cta { background: var(--c-cobalt-50); }
74
+
75
+ .visual {
76
+ display: flex;
77
+ justify-content: center;
78
+ align-items: center;
79
+ position: relative;
80
+ min-height: 300px;
81
+ }
82
+
83
+ .satellite {
84
+ position: absolute;
85
+ width: 64px;
86
+ height: 74px;
87
+ clip-path: var(--hex-pointy-top);
88
+ }
89
+ .s1 { top: 8px; left: calc(50% - 160px); background: var(--c-cobalt-700); }
90
+ .s2 { bottom: 12px; right: calc(50% - 168px); background: var(--c-cobalt-700); }
91
+ .s3 { top: 40%; right: calc(50% - 172px); }
92
+ .s3Orange { background: var(--c-orange-knvb); }
93
+ .s3Cobalt { background: var(--c-cobalt-700); }
94
+
95
+ @media (max-width: 900px) {
96
+ .visual { min-height: 240px; }
97
+ .satellite { width: 48px; height: 56px; }
98
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * <GameModal />
3
+ *
4
+ * The end-of-game dialog for the Conduction mini-games (hex-rain, the
5
+ * canal-footer boats, future invaders / domino / hex-tetris). Mounts
6
+ * once per page; listens for `connext:gameend` CustomEvents (the event
7
+ * name is a brand-internal identifier, retained for compatibility),
8
+ * opens
9
+ * with the matching copy, tracks total games found in localStorage so
10
+ * the cross-site progress bar is consistent.
11
+ *
12
+ * Mirrors preview/components/game-modal.html. The previous JS-only
13
+ * version (game-modal.js) gets replaced by this component on any
14
+ * Docusaurus surface that mounts <GameModal/>.
15
+ *
16
+ * Game payload shape, fired by the playing component:
17
+ *
18
+ * window.dispatchEvent(new CustomEvent('connext:gameend', {
19
+ * detail: {
20
+ * id: 'hexrain',
21
+ * won: true, // or false
22
+ * score: 12, // numeric
23
+ * summary: '12 / 12 collected'
24
+ * }
25
+ * }));
26
+ *
27
+ * Usage in MDX (mount once on the layout, not per page):
28
+ *
29
+ * import {GameModal} from '@conduction/docusaurus-preset/components';
30
+ * <GameModal />
31
+ *
32
+ * Custom games table (default covers the five planned mini-games):
33
+ *
34
+ * <GameModal games={[
35
+ * {id: 'hexrain', label: 'Twelve apps'},
36
+ * {id: 'boats', label: 'Sink the boats'},
37
+ * {id: 'invaders', label: 'Hex-vaders'},
38
+ * {id: 'domino', label: 'Hex-domino'},
39
+ * {id: 'tetris', label: 'Hex-tris'},
40
+ * ]} />
41
+ */
42
+
43
+ import React, {useEffect, useState, useCallback, useMemo} from 'react';
44
+ import useIsBrowser from '@docusaurus/useIsBrowser';
45
+ import styles from './GameModal.module.css';
46
+
47
+ const STORAGE_KEY = 'conduction:minigames';
48
+
49
+ const DEFAULT_GAMES = [
50
+ {id: 'hexrain', label: 'Twelve apps · hex rain'},
51
+ {id: 'boats', label: 'Sink the boats · footer canal'},
52
+ {id: 'invaders', label: 'Hex-vaders · cookie CLI'},
53
+ {id: 'logo-memory', label: 'Logo memory · clients marquee'},
54
+ {id: 'kade-cyclist', label: 'Kade cyclist · footer kade'},
55
+ ];
56
+
57
+ function readFound() {
58
+ if (typeof window === 'undefined') return {};
59
+ try {
60
+ const raw = window.localStorage.getItem(STORAGE_KEY);
61
+ return raw ? JSON.parse(raw) : {};
62
+ } catch (e) { return {}; }
63
+ }
64
+
65
+ function writeFound(found) {
66
+ if (typeof window === 'undefined') return;
67
+ try {
68
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(found));
69
+ } catch (e) {/* fail open */}
70
+ }
71
+
72
+ export default function GameModal({games = DEFAULT_GAMES, className}) {
73
+ const isBrowser = useIsBrowser();
74
+ const [open, setOpen] = useState(false);
75
+ const [event, setEvent] = useState(null);
76
+ const [found, setFound] = useState({});
77
+
78
+ /* On mount: read found-games table from localStorage and subscribe
79
+ to the `connext:gameend` event. Each event opens the modal with
80
+ the supplied copy and (if won) marks the game as found. */
81
+ useEffect(() => {
82
+ if (!isBrowser) return;
83
+ setFound(readFound());
84
+
85
+ function onEnd(e) {
86
+ const detail = e.detail || {};
87
+ setEvent(detail);
88
+ setOpen(true);
89
+ /* Discovery vs. victory: any game-end counts the game as "found"
90
+ because a few of the games (kade-cyclist, future endless
91
+ runners) never reach a clean win state. The scoreboard pill
92
+ still reflects the actual performance for that round. */
93
+ if (detail.id) {
94
+ setFound((prev) => {
95
+ if (prev[detail.id]) return prev;
96
+ const next = {...prev, [detail.id]: true};
97
+ writeFound(next);
98
+ return next;
99
+ });
100
+ }
101
+ }
102
+ window.addEventListener('connext:gameend', onEnd);
103
+ return () => window.removeEventListener('connext:gameend', onEnd);
104
+ }, [isBrowser]);
105
+
106
+ /* Escape closes the modal. */
107
+ useEffect(() => {
108
+ if (!isBrowser || !open) return;
109
+ function onKey(e) { if (e.key === 'Escape') setOpen(false); }
110
+ window.addEventListener('keydown', onKey);
111
+ return () => window.removeEventListener('keydown', onKey);
112
+ }, [isBrowser, open]);
113
+
114
+ const close = useCallback(() => {
115
+ /* Notify the playing runtime so it can tear down its in-page UI
116
+ (lifted hexes, kade stage, etc.) and restore the original
117
+ surface. Without this the marquee or kade stays in its game-
118
+ over visual until the next page load. */
119
+ if (event?.id) {
120
+ Promise.resolve().then(() => {
121
+ window.dispatchEvent(new CustomEvent('connext:gameclose', {detail: {id: event.id}}));
122
+ });
123
+ }
124
+ setOpen(false);
125
+ }, [event]);
126
+ const replay = useCallback(() => {
127
+ /* Fire a `connext:gamereplay` event so the playing component (which
128
+ listens for it) can re-init. We don't dispatch from inside an
129
+ event handler that came from React, so use a microtask to keep
130
+ the call stack clean. */
131
+ if (event?.id) {
132
+ Promise.resolve().then(() => {
133
+ window.dispatchEvent(new CustomEvent('connext:gamereplay', {detail: {id: event.id}}));
134
+ });
135
+ }
136
+ setOpen(false);
137
+ }, [event]);
138
+
139
+ const foundCount = useMemo(() => Object.values(found).filter(Boolean).length, [found]);
140
+ const total = games.length;
141
+ const percent = total > 0 ? Math.round((foundCount / total) * 100) : 0;
142
+
143
+ if (!isBrowser || !open || !event) return null;
144
+
145
+ const eyebrow = event.won ? 'Mini-game complete' : 'Game over';
146
+ const title = event.title || (event.won ? 'Nice run.' : "That's all of them.");
147
+ const subtitle = event.subtitle ||
148
+ (event.won
149
+ ? "You've cleared a hidden Conduction mini-game."
150
+ : "Try again any time, the rain doesn't stop.");
151
+
152
+ return (
153
+ <div className={[styles.modal, className].filter(Boolean).join(' ')} role="dialog" aria-modal="true" aria-labelledby="gm-title">
154
+ <div className={styles.overlay} onClick={close} />
155
+ <div className={styles.panel}>
156
+ <button type="button" className={styles.close} onClick={close} aria-label="Close">×</button>
157
+ <p className={styles.eyebrow}>{eyebrow}</p>
158
+ <h2 className={styles.title} id="gm-title">{title}</h2>
159
+ <p className={styles.subtitle}>{subtitle}</p>
160
+
161
+ {typeof event.score !== 'undefined' && (
162
+ <span className={styles.scorePill}>{event.summary || `score: ${event.score}`}</span>
163
+ )}
164
+
165
+ <div className={styles.progress}>
166
+ <div className={styles.progressLabel}>
167
+ <span><strong>{foundCount}</strong> / {total} mini-games found</span>
168
+ <span>{percent}%</span>
169
+ </div>
170
+ <div className={styles.progressBar}>
171
+ <div className={styles.progressFill} style={{width: percent + '%'}} />
172
+ </div>
173
+ </div>
174
+
175
+ <ul className={styles.grid}>
176
+ {games.map((g) => (
177
+ <li key={g.id} className={found[g.id] ? styles.gridItemFound : styles.gridItem}>
178
+ <span className={styles.gridHex} aria-hidden="true" />
179
+ <span>{g.label}</span>
180
+ </li>
181
+ ))}
182
+ </ul>
183
+
184
+ <p className={styles.cta}>
185
+ {foundCount < total
186
+ ? `${total - foundCount} more game${total - foundCount === 1 ? '' : 's'} hidden somewhere. Keep clicking.`
187
+ : "All five found. You read the kit."}
188
+ </p>
189
+
190
+ <div className={styles.actions}>
191
+ <button type="button" className={styles.btnSecondary} onClick={close}>Close</button>
192
+ <button type="button" className={styles.btnPrimary} onClick={replay}>Play again</button>
193
+ </div>
194
+ </div>
195
+ </div>
196
+ );
197
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * <GameModal /> styles. Mirrors preview/components/game-modal.css
3
+ * for the panel + overlay; cobalt-on-white per huisstijl.
4
+ */
5
+
6
+ .modal {
7
+ position: fixed;
8
+ inset: 0;
9
+ z-index: 10000;
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ font-family: var(--conduction-typography-font-family-body);
14
+ }
15
+
16
+ .overlay {
17
+ position: absolute;
18
+ inset: 0;
19
+ background: rgba(10, 23, 47, 0.55);
20
+ backdrop-filter: blur(2px);
21
+ cursor: pointer;
22
+ }
23
+
24
+ .panel {
25
+ position: relative;
26
+ background: white;
27
+ color: var(--c-cobalt-700);
28
+ border-radius: var(--radius-xl);
29
+ padding: 40px 36px 32px;
30
+ width: calc(100% - 48px);
31
+ max-width: 520px;
32
+ box-shadow: var(--shadow-3, 0 30px 80px -20px rgba(10, 23, 47, 0.4));
33
+ }
34
+
35
+ .close {
36
+ position: absolute;
37
+ top: 14px;
38
+ right: 14px;
39
+ width: 32px;
40
+ height: 32px;
41
+ border-radius: 50%;
42
+ border: none;
43
+ background: var(--c-cobalt-50);
44
+ color: var(--c-cobalt-700);
45
+ font-size: 20px;
46
+ cursor: pointer;
47
+ line-height: 1;
48
+ font-family: inherit;
49
+ transition: background 120ms ease;
50
+ }
51
+ .close:hover { background: var(--c-cobalt-100); }
52
+
53
+ .eyebrow {
54
+ font-family: var(--conduction-typography-font-family-code);
55
+ font-size: 11px;
56
+ letter-spacing: 0.14em;
57
+ text-transform: uppercase;
58
+ color: var(--c-orange-knvb);
59
+ margin: 0 0 8px;
60
+ font-weight: 500;
61
+ }
62
+
63
+ .title {
64
+ font-size: 28px;
65
+ font-weight: 700;
66
+ letter-spacing: -0.02em;
67
+ color: var(--c-cobalt-900);
68
+ margin: 0 0 8px;
69
+ }
70
+
71
+ .subtitle {
72
+ font-size: 15px;
73
+ color: var(--c-cobalt-700);
74
+ line-height: 1.55;
75
+ margin: 0 0 16px;
76
+ }
77
+
78
+ .scorePill {
79
+ display: inline-flex;
80
+ align-items: center;
81
+ gap: 6px;
82
+ background: var(--c-cobalt-50);
83
+ color: var(--c-cobalt-900);
84
+ font-family: var(--conduction-typography-font-family-code);
85
+ font-size: 12px;
86
+ letter-spacing: 0.04em;
87
+ padding: 6px 12px;
88
+ border-radius: var(--radius-pill);
89
+ margin-bottom: 24px;
90
+ }
91
+
92
+ .progress {
93
+ margin: 24px 0 16px;
94
+ }
95
+ .progressLabel {
96
+ display: flex;
97
+ justify-content: space-between;
98
+ font-family: var(--conduction-typography-font-family-code);
99
+ font-size: 11px;
100
+ letter-spacing: 0.06em;
101
+ text-transform: uppercase;
102
+ color: var(--c-cobalt-400);
103
+ margin-bottom: 8px;
104
+ }
105
+ .progressLabel strong { color: var(--c-cobalt-900); font-weight: 700; }
106
+
107
+ .progressBar {
108
+ height: 6px;
109
+ background: var(--c-cobalt-50);
110
+ border-radius: 3px;
111
+ overflow: hidden;
112
+ }
113
+ .progressFill {
114
+ height: 100%;
115
+ background: var(--c-orange-knvb);
116
+ transition: width 240ms ease;
117
+ }
118
+
119
+ .grid {
120
+ list-style: none;
121
+ padding: 0;
122
+ margin: 0 0 20px;
123
+ display: flex;
124
+ flex-direction: column;
125
+ gap: 6px;
126
+ }
127
+ .gridItem,
128
+ .gridItemFound {
129
+ display: flex;
130
+ align-items: center;
131
+ gap: 10px;
132
+ font-size: 14px;
133
+ color: var(--c-cobalt-400);
134
+ }
135
+ .gridItemFound { color: var(--c-cobalt-900); font-weight: 500; }
136
+ .gridHex {
137
+ width: 10px;
138
+ height: 12px;
139
+ clip-path: var(--hex-pointy-top);
140
+ background: var(--c-cobalt-200);
141
+ flex-shrink: 0;
142
+ }
143
+ .gridItemFound .gridHex { background: var(--c-mint-500); }
144
+
145
+ .cta {
146
+ font-size: 13px;
147
+ color: var(--c-cobalt-400);
148
+ margin: 16px 0 24px;
149
+ font-style: italic;
150
+ }
151
+
152
+ .actions {
153
+ display: flex;
154
+ gap: 8px;
155
+ justify-content: flex-end;
156
+ }
157
+
158
+ .btnSecondary {
159
+ background: white;
160
+ color: var(--c-cobalt-700);
161
+ border: 1px solid var(--c-cobalt-200);
162
+ padding: 10px 18px;
163
+ border-radius: var(--radius-md);
164
+ font-weight: 500;
165
+ font-size: 14px;
166
+ cursor: pointer;
167
+ font-family: inherit;
168
+ transition: border-color 120ms ease;
169
+ }
170
+ .btnSecondary:hover { border-color: var(--c-blue-cobalt); color: var(--c-blue-cobalt); }
171
+
172
+ .btnPrimary {
173
+ background: var(--c-blue-cobalt);
174
+ color: white;
175
+ border: 1px solid var(--c-blue-cobalt);
176
+ padding: 10px 18px;
177
+ border-radius: var(--radius-md);
178
+ font-weight: 500;
179
+ font-size: 14px;
180
+ cursor: pointer;
181
+ font-family: inherit;
182
+ transition: background 120ms ease;
183
+ }
184
+ .btnPrimary:hover { background: var(--c-cobalt-700); border-color: var(--c-cobalt-700); }