@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,540 @@
1
+ /* Logo Memory: in-place transformation of the Clients hex marquee.
2
+ No overlay, no clones-with-new-DOM-tiles — the actual <a class="hex">
3
+ anchors that are already on screen become the game tiles.
4
+
5
+ Phases on click:
6
+ 1. Freeze the sideways scroll, restore color (drop the grayscale).
7
+ 2. Lift every visible hex out of its track into absolute positioning
8
+ anchored to its current snapshot coords. Pick the 4 hexes nearest
9
+ the horizontal centre per row as keepers (12 total). Animate the
10
+ rest off-screen left or right with stagger + fade.
11
+ 3. Reposition the 12 keepers into a clean centred 4×3 honeycomb
12
+ cluster.
13
+ 4. Clone each keeper, slide the clone down 3 rows so we have a
14
+ 4×6 = 24-tile cluster.
15
+ 5. Blink-shuffle: 12 random client logos picked, each duplicated
16
+ into pair assignments, each tile fades out, swaps to its
17
+ assigned logo, fades back in. Stagger across all 24.
18
+ 6. Flip every tile to a cobalt back face with the Conduction C-in-
19
+ hex avatar. Stagger.
20
+
21
+ From there it's classic memory: click two, match holds at orange-glow
22
+ ring, mismatch flips back. Win fires `connext:gameend` on window so
23
+ the GameModal picks it up.
24
+
25
+ The tear-down restores the original marquee innerHTML so the scroll
26
+ animation resumes cleanly.
27
+ */
28
+ (function () {
29
+ const PAIRS = 12;
30
+ const KEEP_PER_ROW = 4;
31
+ const FLIP_MS = 600;
32
+ const MISMATCH_MS = 900;
33
+ const STAGGER_MS = 35;
34
+ const BLINK_OUT_MS = 180;
35
+ const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
36
+
37
+ const CONDUCTION_BACK_SVG =
38
+ '<svg class="lm-back-svg" viewBox="-86.6 -100 173.2 200" aria-hidden="true">' +
39
+ '<polygon fill="#21468B" points="0,-100 86.6,-50 86.6,50 0,100 -86.6,50 -86.6,-50"/>' +
40
+ '<polygon fill="#FFFFFF" points="0,-74.5 64.5,-37.3 64.5,37.3 0,74.5 -64.5,37.3 -64.5,-37.3"/>' +
41
+ '<polygon fill="#21468B" points="-0.2,-25.2 20.1,-13.5 43.7,-27.1 -0.2,-52.4 -45.6,-26.2 -45.6,26.2 -0.2,52.4 43.7,27.1 20.1,13.5 -0.2,25.2 -22,12.6 -22,-12.6"/>' +
42
+ '</svg>';
43
+
44
+ function shuffle(arr) {
45
+ const a = arr.slice();
46
+ for (let i = a.length - 1; i > 0; i--) {
47
+ const j = Math.floor(Math.random() * (i + 1));
48
+ const t = a[i]; a[i] = a[j]; a[j] = t;
49
+ }
50
+ return a;
51
+ }
52
+
53
+ function wait(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }
54
+
55
+ function collectLogos(marquee) {
56
+ const seen = new Map();
57
+ marquee.querySelectorAll('a').forEach(function (a) {
58
+ const img = a.querySelector('img');
59
+ if (!img) return;
60
+ const src = img.getAttribute('src');
61
+ if (!src || seen.has(src)) return;
62
+ const name = a.getAttribute('aria-label') || img.getAttribute('alt') || '';
63
+ seen.set(src, name);
64
+ });
65
+ return Array.from(seen, function (e) { return { src: e[0], name: e[1] }; });
66
+ }
67
+
68
+ function hydrateContainer(container) {
69
+ if (container.dataset.memoryHydrated === '1') return;
70
+ container.dataset.memoryHydrated = '1';
71
+
72
+ const marquee = container.querySelector('[data-memory-marquee]') ||
73
+ container.querySelector('.marquee');
74
+ if (!marquee) return;
75
+
76
+ /* On replay (modal "Play again" button) we want a fresh round. The
77
+ runtime listens for connext:gamereplay and re-arms the marquee. */
78
+ window.addEventListener('connext:gamereplay', function (e) {
79
+ if (e.detail && e.detail.id === 'logo-memory') {
80
+ if (container.dataset.memoryActive === '1') {
81
+ tearDownActive(container, marquee);
82
+ }
83
+ }
84
+ });
85
+ /* On Close — restore the marquee. The cluster is no longer needed
86
+ once the user is done with the round. */
87
+ window.addEventListener('connext:gameclose', function (e) {
88
+ if (e.detail && e.detail.id === 'logo-memory') {
89
+ if (container.dataset.memoryActive === '1') {
90
+ tearDownActive(container, marquee);
91
+ }
92
+ }
93
+ });
94
+
95
+ marquee.addEventListener('click', function (e) {
96
+ if (container.dataset.memoryActive === '1') return;
97
+ const a = e.target.closest('a');
98
+ if (!a || !marquee.contains(a)) return;
99
+ e.preventDefault();
100
+ const logos = collectLogos(marquee);
101
+ if (logos.length < PAIRS) return;
102
+ startGame(container, marquee, logos);
103
+ });
104
+ }
105
+
106
+ /* Stash for active-game state so tearDown can clean up keyboard
107
+ listeners and revert the marquee. */
108
+ const ACTIVE = new WeakMap();
109
+
110
+ function tearDownActive(container, marquee) {
111
+ const state = ACTIVE.get(container);
112
+ if (!state) return;
113
+ document.removeEventListener('keydown', state.onKey);
114
+ /* The marquee is now driven by clients-flow.js which manages its
115
+ own hex pool. Calling reset() removes every existing hex (the
116
+ ones the game lifted out of rows AND any leftovers) and re-fills
117
+ the rows from scratch. Falls back to an innerHTML restore for
118
+ the transitional case where the flow runtime hasn't loaded. */
119
+ if (window.ConductionClientsFlow && typeof window.ConductionClientsFlow.reset === 'function') {
120
+ /* Strip everything we appended during the game (lifted hex
121
+ anchors, the HUD pill, the win panel, etc) — anything that
122
+ isn't a row container. The runtime's reset() will repopulate
123
+ the rows from scratch. */
124
+ Array.from(marquee.children).forEach(function (child) {
125
+ if (!child.hasAttribute('data-flow-row')) child.remove();
126
+ });
127
+ window.ConductionClientsFlow.reset();
128
+ } else if (state.originalHTML) {
129
+ marquee.innerHTML = state.originalHTML;
130
+ }
131
+ marquee.classList.remove('lm-active', 'lm-overflow');
132
+ delete container.dataset.memoryActive;
133
+ ACTIVE.delete(container);
134
+ }
135
+
136
+ async function startGame(container, marquee, logos) {
137
+ container.dataset.memoryActive = '1';
138
+ const originalHTML = marquee.innerHTML;
139
+
140
+ /* Freeze the continuous-spawn runtime so hexes stop drifting and
141
+ stay in their snapshot positions for phase 2 to lift cleanly. */
142
+ if (window.ConductionClientsFlow && typeof window.ConductionClientsFlow.pause === 'function') {
143
+ window.ConductionClientsFlow.pause();
144
+ }
145
+
146
+ /* === Phase 1: Freeze + colorize === */
147
+ marquee.classList.add('lm-active');
148
+ /* Briefly let the CSS color transition + animation pause take effect
149
+ so the snapshot rects are taken from a still frame. */
150
+ await wait(reduceMotion ? 30 : 280);
151
+
152
+ /* === Phase 2: Snapshot, lift to absolute, pick keepers === */
153
+ /* The row class is hashed by CSS modules in the React build (e.g.
154
+ row_cD73), so .row doesn't match there. Match any descendant
155
+ whose class string contains "row" — works for both the literal
156
+ "row row1" of the preview HTML and the CSS-modules-hashed
157
+ "row_cD73 row2_w4Mq" of the React build. */
158
+ const rows = Array.from(marquee.querySelectorAll('[class*="row"]'));
159
+ const marqueeRect = marquee.getBoundingClientRect();
160
+
161
+ const rowsHexes = rows.map(function (row, rowIdx) {
162
+ const hexes = Array.from(row.querySelectorAll('a'));
163
+ hexes.forEach(function (hex) {
164
+ const r = hex.getBoundingClientRect();
165
+ hex._lmX = r.left - marqueeRect.left;
166
+ hex._lmY = r.top - marqueeRect.top;
167
+ hex._lmW = r.width;
168
+ hex._lmH = r.height;
169
+ hex._lmRowIdx = rowIdx;
170
+ });
171
+ /* Don't filter by viewport bounds — we always need 4 keepers per
172
+ row, and a hex slightly off-screen will get repositioned
173
+ centred anyway. Just exclude zero-size hexes (in case any are
174
+ display:none for some other reason). The blink-shuffle re-
175
+ assigns logos so it doesn't matter which DOM duplicate gets
176
+ picked. */
177
+ return hexes.filter(function (h) { return h._lmW > 0; });
178
+ });
179
+
180
+ const stageWidth = marqueeRect.width;
181
+ const stageCenterX = stageWidth / 2;
182
+
183
+ /* Pick the keepers per row, but guarantee a total of PAIRS keepers
184
+ so we always end up with 24 tiles. If a row's first 4 picks
185
+ overlap with another row's (rare in practice), we fall back to
186
+ picking the next-nearest hex from the densest row. */
187
+ const allKept = [];
188
+ const allDropping = [];
189
+
190
+ rowsHexes.forEach(function (rowHexes) {
191
+ const sorted = rowHexes.slice().sort(function (a, b) {
192
+ const da = Math.abs((a._lmX + a._lmW / 2) - stageCenterX);
193
+ const db = Math.abs((b._lmX + b._lmW / 2) - stageCenterX);
194
+ return da - db;
195
+ });
196
+ const pickCount = Math.min(KEEP_PER_ROW, sorted.length);
197
+ for (let k = 0; k < pickCount; k++) {
198
+ sorted[k]._lmKept = true;
199
+ allKept.push(sorted[k]);
200
+ }
201
+ for (let k = pickCount; k < sorted.length; k++) {
202
+ sorted[k]._lmKept = false;
203
+ allDropping.push(sorted[k]);
204
+ }
205
+ });
206
+ /* Top up keepers from droppers' nearest-to-centre if any row was
207
+ short. Pulls from the front of allDropping (which is row-sorted),
208
+ preferring the nearest-to-centre droppers. */
209
+ while (allKept.length < PAIRS && allDropping.length > 0) {
210
+ const sortedDrops = allDropping.slice().sort(function (a, b) {
211
+ const da = Math.abs((a._lmX + a._lmW / 2) - stageCenterX);
212
+ const db = Math.abs((b._lmX + b._lmW / 2) - stageCenterX);
213
+ return da - db;
214
+ });
215
+ const promote = sortedDrops[0];
216
+ promote._lmKept = true;
217
+ allKept.push(promote);
218
+ const idx = allDropping.indexOf(promote);
219
+ if (idx >= 0) allDropping.splice(idx, 1);
220
+ }
221
+
222
+ /* Lift every visible hex to position:absolute at its snapshot coords
223
+ so it's no longer affected by track flex layout. The flow runtime
224
+ drives drift via inline transform: translateX(...) — clear that
225
+ so the game's own left/top take over cleanly. Otherwise the hex
226
+ would render at left + transform, leaking the pre-pause drift. */
227
+ const allLifted = allKept.concat(allDropping);
228
+ allLifted.forEach(function (hex) {
229
+ hex.style.transform = '';
230
+ hex.style.position = 'absolute';
231
+ hex.style.left = hex._lmX + 'px';
232
+ hex.style.top = hex._lmY + 'px';
233
+ hex.style.width = hex._lmW + 'px';
234
+ hex.style.height = hex._lmH + 'px';
235
+ hex.style.margin = '0';
236
+ hex.style.transition = 'transform 700ms cubic-bezier(0.65, 0, 0.35, 1), opacity 600ms ease, left 700ms cubic-bezier(0.65, 0, 0.35, 1), top 700ms cubic-bezier(0.65, 0, 0.35, 1)';
237
+ hex.style.zIndex = '5';
238
+ marquee.appendChild(hex);
239
+ });
240
+ /* Hide the leftover (aria-hidden duplicates and any non-visible
241
+ originals) hexes so the rows collapse without ghost slots. */
242
+ marquee.querySelectorAll('a').forEach(function (h) {
243
+ if (allLifted.indexOf(h) === -1) h.style.display = 'none';
244
+ });
245
+ /* Collapse rows so there's no empty band where the tracks used to
246
+ sit. Marquee gets its own min-height set below for the cluster.
247
+ Reuse the rows array we captured at snapshot — `marquee.children`
248
+ now includes the lifted anchors as well so re-querying is risky. */
249
+ rows.forEach(function (r) {
250
+ r.style.height = '0';
251
+ r.style.margin = '0';
252
+ r.style.overflow = 'visible';
253
+ });
254
+ /* Allow the cluster to expand below the marquee's original 3-row
255
+ height — overflow:visible during play. */
256
+ marquee.classList.add('lm-overflow');
257
+
258
+ /* === Phase 3: Drop edges off-screen === */
259
+ allDropping.forEach(function (hex, i) {
260
+ const cx = hex._lmX + hex._lmW / 2;
261
+ const goLeft = cx < stageCenterX;
262
+ const tx = goLeft ? -(stageWidth / 2 + 120) : (stageWidth / 2 + 120);
263
+ setTimeout(function () {
264
+ hex.style.transform = 'translateX(' + tx + 'px) rotate(' + (goLeft ? -8 : 8) + 'deg)';
265
+ hex.style.opacity = '0';
266
+ }, i * 22);
267
+ });
268
+ await wait(reduceMotion ? 30 : 700 + allDropping.length * 22);
269
+ /* Drop fully done — remove dropped tiles. */
270
+ allDropping.forEach(function (h) { h.remove(); });
271
+
272
+ /* Track the originalHTML in ACTIVE before any phase that might
273
+ mutate the marquee, so the early-abort path can fall back to
274
+ tearDownActive without needing setupGame to have run. */
275
+ ACTIVE.set(container, { onKey: function () {}, originalHTML: originalHTML });
276
+
277
+ /* === Phase 4: Reposition keepers into clean centred 4×3 grid === */
278
+ if (allKept.length === 0) {
279
+ tearDownActive(container, marquee);
280
+ return;
281
+ }
282
+ const hexW = allKept[0]._lmW;
283
+ const hexH = allKept[0]._lmH;
284
+ /* Read the actual --gap from the marquee so the cluster math
285
+ matches whatever the CSS sets (8px desktop, 6px mobile, etc).
286
+ Fallback to 8 if the variable isn't resolvable. */
287
+ const cssGap = parseFloat(getComputedStyle(marquee).getPropertyValue('--gap'));
288
+ const gap = isFinite(cssGap) && cssGap >= 0 ? cssGap : 8;
289
+ const cellW = hexW + gap;
290
+ /* Isotropic row spacing: vertical pitch matches the horizontal
291
+ pitch on the diagonal axis, same formula as the marquee CSS. */
292
+ const rowSpacingY = (hexW + gap) * 0.866;
293
+
294
+ /* Group keepers by their original row to preserve the 3-row visual,
295
+ then space them evenly within each row centred on stageCenterX. */
296
+ const keptByRow = {};
297
+ allKept.forEach(function (h) {
298
+ const r = h._lmRowIdx;
299
+ (keptByRow[r] = keptByRow[r] || []).push(h);
300
+ });
301
+ const sortedRowIds = Object.keys(keptByRow).map(Number).sort(function (a, b) { return a - b; });
302
+ /* Top y of the cluster — start where the topmost keeper currently
303
+ sits so the relayout doesn't jump vertically. */
304
+ const minTopY = Math.min.apply(null, allKept.map(function (h) { return h._lmY; }));
305
+ sortedRowIds.forEach(function (rIdx, i) {
306
+ const rowHexes = keptByRow[rIdx].slice().sort(function (a, b) { return a._lmX - b._lmX; });
307
+ const stagger = (i % 2 === 1) ? cellW / 2 : 0;
308
+ const totalRowWidth = (rowHexes.length - 1) * cellW + hexW;
309
+ const startX = stageCenterX - totalRowWidth / 2 + (stagger - cellW / 4);
310
+ const newY = minTopY + i * rowSpacingY;
311
+ rowHexes.forEach(function (hex, j) {
312
+ const newX = startX + j * cellW;
313
+ hex.style.left = newX + 'px';
314
+ hex.style.top = newY + 'px';
315
+ hex._lmFinalX = newX;
316
+ hex._lmFinalY = newY;
317
+ });
318
+ });
319
+
320
+ await wait(reduceMotion ? 30 : 720);
321
+
322
+ /* === Phase 5: Duplicate keepers downward === */
323
+ const downOffset = 3 * rowSpacingY;
324
+ const halfCell = cellW / 2;
325
+ /* Each clone needs its lateral position shifted so the bottom 3
326
+ rows continue the honeycomb stagger pattern. The top half has
327
+ stagger [0, ½, 0] for layout rows 0/1/2; the bottom half is
328
+ rows 3/4/5 which have stagger [½, 0, ½]. The shift is the
329
+ difference. Pre-compute per clone for use during both the
330
+ slide-in animation and the final position lock. */
331
+ function lateralShiftFor(orig) {
332
+ const origLayoutRow = sortedRowIds.indexOf(orig._lmRowIdx);
333
+ const origStaggered = (origLayoutRow % 2 === 1);
334
+ const cloneStaggered = ((origLayoutRow + 3) % 2 === 1);
335
+ return (cloneStaggered ? halfCell : 0) - (origStaggered ? halfCell : 0);
336
+ }
337
+ const clones = allKept.map(function (orig) {
338
+ const clone = orig.cloneNode(true);
339
+ clone.classList.add('lm-clone');
340
+ clone.style.position = 'absolute';
341
+ clone.style.left = orig._lmFinalX + 'px';
342
+ clone.style.top = orig._lmFinalY + 'px';
343
+ clone.style.width = orig._lmW + 'px';
344
+ clone.style.height = orig._lmH + 'px';
345
+ clone.style.margin = '0';
346
+ clone.style.transition = orig.style.transition;
347
+ clone.style.opacity = '0';
348
+ clone.style.transform = 'scale(0.6)';
349
+ clone.style.zIndex = '5';
350
+ marquee.appendChild(clone);
351
+ return clone;
352
+ });
353
+ /* One frame for the clones to be in DOM before the transition. */
354
+ await wait(50);
355
+ clones.forEach(function (clone, i) {
356
+ const orig = allKept[i];
357
+ const dx = lateralShiftFor(orig);
358
+ setTimeout(function () {
359
+ clone.style.opacity = '1';
360
+ clone.style.transform = 'translate(' + dx + 'px, ' + downOffset + 'px) scale(1)';
361
+ }, i * 30);
362
+ });
363
+ await wait(reduceMotion ? 30 : 700 + clones.length * 30);
364
+ /* After slide-down, lock clones at their final left/top and clear
365
+ transform so flip rotateY is purely Y-rotation. */
366
+ clones.forEach(function (clone, i) {
367
+ const orig = allKept[i];
368
+ const dx = lateralShiftFor(orig);
369
+ clone.style.transition = 'opacity 200ms ease';
370
+ clone.style.transform = '';
371
+ clone.style.left = (orig._lmFinalX + dx) + 'px';
372
+ clone.style.top = (orig._lmFinalY + downOffset) + 'px';
373
+ clone._lmFinalX = orig._lmFinalX + dx;
374
+ clone._lmFinalY = orig._lmFinalY + downOffset;
375
+ });
376
+
377
+ /* === Phase 6: Blink-shuffle the logos === */
378
+ const allTiles = allKept.concat(clones);
379
+ const picked = shuffle(logos).slice(0, PAIRS);
380
+ const pairAssignments = shuffle(picked.flatMap(function (l, i) {
381
+ return [
382
+ { src: l.src, name: l.name, pairId: i },
383
+ { src: l.src, name: l.name, pairId: i },
384
+ ];
385
+ }));
386
+ /* Every tile fades, swaps img src to its assigned logo, fades back. */
387
+ const shuffleStagger = reduceMotion ? 0 : STAGGER_MS;
388
+ allTiles.forEach(function (tile, i) {
389
+ setTimeout(function () {
390
+ tile.style.opacity = '0';
391
+ setTimeout(function () {
392
+ const a = pairAssignments[i];
393
+ const img = tile.querySelector('img');
394
+ if (img) {
395
+ img.src = a.src;
396
+ img.alt = a.name;
397
+ }
398
+ tile.dataset.lmPair = String(a.pairId);
399
+ tile.setAttribute('aria-label', 'Memory tile: ' + a.name);
400
+ tile.style.opacity = '1';
401
+ }, BLINK_OUT_MS);
402
+ }, i * shuffleStagger);
403
+ });
404
+ await wait(reduceMotion ? 30 : allTiles.length * shuffleStagger + BLINK_OUT_MS + 240);
405
+
406
+ /* === Phase 7: Flip every tile to the cobalt back face === */
407
+ allTiles.forEach(function (tile) { enhanceTileForFlip(tile); });
408
+ /* Force reflow before flip transition so the new transform-style
409
+ takes effect. */
410
+ void marquee.offsetHeight;
411
+ const flipStagger = reduceMotion ? 0 : 30;
412
+ allTiles.forEach(function (tile, i) {
413
+ setTimeout(function () { tile.classList.add('lm-back-up'); }, i * flipStagger);
414
+ });
415
+ await wait(reduceMotion ? 30 : allTiles.length * flipStagger + 600);
416
+
417
+ /* === Phase 8: Game ready === */
418
+ setupGame(container, marquee, allTiles, originalHTML);
419
+ }
420
+
421
+ function enhanceTileForFlip(tile) {
422
+ if (tile.classList.contains('lm-tile')) return;
423
+ tile.classList.add('lm-tile');
424
+ const img = tile.querySelector('img');
425
+ const flipper = document.createElement('div');
426
+ flipper.className = 'lm-flipper';
427
+ const front = document.createElement('div');
428
+ front.className = 'lm-face lm-front';
429
+ if (img) front.appendChild(img);
430
+ const back = document.createElement('div');
431
+ back.className = 'lm-face lm-back';
432
+ back.innerHTML = CONDUCTION_BACK_SVG;
433
+ flipper.appendChild(front);
434
+ flipper.appendChild(back);
435
+ tile.appendChild(flipper);
436
+ }
437
+
438
+ function setupGame(container, marquee, tiles, originalHTML) {
439
+ /* HUD pill positioned at the top of the marquee (over the cluster). */
440
+ const hud = document.createElement('div');
441
+ hud.className = 'lm-hud';
442
+ hud.innerHTML =
443
+ '<div class="lm-counter"><span data-matched>0</span> / ' + PAIRS + '</div>' +
444
+ '<div class="lm-moves"><span data-moves>0</span> zetten</div>' +
445
+ '<button class="lm-close" type="button" aria-label="Sluit spel">&times;</button>';
446
+ marquee.appendChild(hud);
447
+
448
+ const matchedSpan = hud.querySelector('[data-matched]');
449
+ const movesSpan = hud.querySelector('[data-moves]');
450
+ const closeBtn = hud.querySelector('.lm-close');
451
+
452
+ let firstTile = null;
453
+ let busy = false;
454
+ let moves = 0;
455
+ let matched = 0;
456
+ let phase = 'play';
457
+ const startTime = performance.now();
458
+
459
+ function onTileClick(tile) {
460
+ if (busy || phase !== 'play') return;
461
+ if (tile.classList.contains('lm-matched')) return;
462
+ if (!tile.classList.contains('lm-back-up')) return;
463
+ tile.classList.remove('lm-back-up');
464
+ if (!firstTile) {
465
+ firstTile = tile;
466
+ return;
467
+ }
468
+ const second = tile;
469
+ moves++;
470
+ movesSpan.textContent = String(moves);
471
+ const t1 = firstTile;
472
+ firstTile = null;
473
+ if (t1.dataset.lmPair === second.dataset.lmPair) {
474
+ busy = true;
475
+ setTimeout(function () {
476
+ t1.classList.add('lm-matched');
477
+ second.classList.add('lm-matched');
478
+ matched++;
479
+ matchedSpan.textContent = String(matched);
480
+ busy = false;
481
+ if (matched === PAIRS) onWin();
482
+ }, FLIP_MS);
483
+ } else {
484
+ busy = true;
485
+ setTimeout(function () {
486
+ t1.classList.add('lm-back-up');
487
+ second.classList.add('lm-back-up');
488
+ busy = false;
489
+ }, MISMATCH_MS);
490
+ }
491
+ }
492
+
493
+ tiles.forEach(function (tile) {
494
+ tile.addEventListener('click', function (e) {
495
+ e.preventDefault();
496
+ onTileClick(tile);
497
+ });
498
+ });
499
+
500
+ function onWin() {
501
+ phase = 'won';
502
+ const elapsedSec = Math.round((performance.now() - startTime) / 1000);
503
+ window.dispatchEvent(new CustomEvent('connext:gameend', {
504
+ detail: {
505
+ id: 'logo-memory',
506
+ won: true,
507
+ score: matched,
508
+ summary: matched + ' pairs · ' + moves + ' zetten · ' + elapsedSec + 's',
509
+ title: 'Klaar.',
510
+ subtitle: 'You matched all 12 pairs across the clients marquee.',
511
+ }
512
+ }));
513
+ }
514
+
515
+ function tearDown() {
516
+ tearDownActive(container, marquee);
517
+ }
518
+
519
+ function onKey(e) {
520
+ if (e.key === 'Escape') tearDown();
521
+ }
522
+ document.addEventListener('keydown', onKey);
523
+ closeBtn.addEventListener('click', tearDown);
524
+
525
+ ACTIVE.set(container, { onKey: onKey, originalHTML: originalHTML });
526
+ }
527
+
528
+ function hydrate() {
529
+ document.querySelectorAll('[data-logo-memory]').forEach(hydrateContainer);
530
+ }
531
+
532
+ window.LogoMemory = window.LogoMemory || {};
533
+ window.LogoMemory.hydrate = hydrate;
534
+
535
+ if (document.readyState === 'loading') {
536
+ document.addEventListener('DOMContentLoaded', hydrate);
537
+ } else {
538
+ hydrate();
539
+ }
540
+ })();