@conduction/docusaurus-preset 3.43.0 → 3.44.1
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 +1 -1
- package/src/components/AppsGrid/AppsGrid.module.css +1 -1
- package/src/components/AppsPreview/AppsPreview.module.css +1 -1
- package/src/components/AtomZones/AtomZones.module.css +1 -1
- package/src/components/BlueprintRush/BlueprintRush.module.css +1 -1
- package/src/components/ContentCard/ContentCard.module.css +3 -3
- package/src/components/DetailHero/DetailHero.module.css +10 -2
- package/src/components/DiceDuel/DiceDuel.jsx +190 -0
- package/src/components/DiceDuel/DiceDuel.module.css +191 -0
- package/src/components/DiceDuel/__tests__/engine.test.js +159 -0
- package/src/components/DiceDuel/engine.js +182 -0
- package/src/components/DownloadPanel/DownloadPanel.module.css +1 -1
- package/src/components/EmployeeCard/EmployeeCard.module.css +1 -1
- package/src/components/FacetedFilters/FacetedFilters.module.css +54 -0
- package/src/components/FeaturedCard/FeaturedCard.module.css +1 -1
- package/src/components/Hero/Hero.module.css +1 -1
- package/src/components/HexCard/HexCard.module.css +2 -2
- package/src/components/HowSteps/HowSteps.module.css +1 -1
- package/src/components/LeafCard/LeafCard.module.css +2 -2
- package/src/components/McpToolShelf/McpToolShelf.module.css +2 -2
- package/src/components/ModuleCard/ModuleCard.module.css +1 -1
- package/src/components/NewsletterCta/NewsletterCta.module.css +1 -1
- package/src/components/NextSteps/NextSteps.module.css +1 -1
- package/src/components/PairCard/PairCard.module.css +1 -1
- package/src/components/PartnerCard/PartnerCard.module.css +1 -1
- package/src/components/PartnerDirectory/PartnerDirectory.module.css +1 -1
- package/src/components/PipeFit/PipeFit.jsx +221 -0
- package/src/components/PipeFit/PipeFit.module.css +203 -0
- package/src/components/PipeFit/__tests__/engine.test.js +176 -0
- package/src/components/PipeFit/engine.js +178 -0
- package/src/components/Pipeline/Pipeline.module.css +1 -1
- package/src/components/PlatformOverview/PlatformOverview.module.css +1 -1
- package/src/components/Reconcile/Reconcile.jsx +240 -0
- package/src/components/Reconcile/Reconcile.module.css +217 -0
- package/src/components/Reconcile/__tests__/engine.test.js +168 -0
- package/src/components/Reconcile/engine.js +228 -0
- package/src/components/ReferenceCard/ReferenceCard.module.css +1 -1
- package/src/components/RotatingCards/RotatingCards.module.css +1 -1
- package/src/components/Showcase/Showcase.module.css +2 -2
- package/src/components/SolutionCard/SolutionCard.module.css +1 -1
- package/src/components/StatsStrip/StatsStrip.module.css +1 -1
- package/src/components/WidgetShelf/WidgetShelf.module.css +2 -2
- package/src/components/index.js +3 -0
- package/src/components/primitives/SectionHead.module.css +1 -1
- package/src/components/primitives/icons.jsx +20 -0
- package/src/theme/Navbar/index.jsx +73 -0
- package/src/theme/Navbar/styles.module.css +205 -8
- package/static/lib/canal-footer.css +111 -1
- package/static/lib/platform-diagram.css +35 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconcile — the rules, with no DOM and no clock of its own.
|
|
3
|
+
*
|
|
4
|
+
* Shillinq's game. The bank statement is in and the invoices are open.
|
|
5
|
+
* Match each payment to the invoice it settles, before the month
|
|
6
|
+
* closes. One of the lines on the statement matches nothing at all,
|
|
7
|
+
* and sending that one to an invoice is how money goes missing
|
|
8
|
+
* quietly.
|
|
9
|
+
*
|
|
10
|
+
* So there are three moves, not two: match it, or flag it as the one
|
|
11
|
+
* that does not belong. Flagging a genuine payment is a mistake too,
|
|
12
|
+
* because a bookkeeper who cries wolf at every line is a bookkeeper
|
|
13
|
+
* nobody listens to.
|
|
14
|
+
*
|
|
15
|
+
* Time and randomness are injected; the component owns the clock.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/* Round amounts in whole euros: the game is about matching, not about
|
|
19
|
+
arithmetic with cents. */
|
|
20
|
+
const AMOUNTS = [120, 240, 360, 480, 750, 900, 1240, 1600, 2100, 3400];
|
|
21
|
+
const REFERENCES = ['2026-014', '2026-027', '2026-031', '2026-048', '2026-052', '2026-066'];
|
|
22
|
+
|
|
23
|
+
export const DEFAULTS = {
|
|
24
|
+
lives: 3,
|
|
25
|
+
invoices: 4,
|
|
26
|
+
clockStartMs: 24000,
|
|
27
|
+
clockFloorMs: 9000,
|
|
28
|
+
rampPerPoint: 90,
|
|
29
|
+
pointsPerMatch: 8,
|
|
30
|
+
pointsPerFraud: 20,
|
|
31
|
+
pointsPerSheet: 12,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function mulberry32(seed) {
|
|
35
|
+
let a = seed >>> 0;
|
|
36
|
+
return function random() {
|
|
37
|
+
a |= 0; a = (a + 0x6D2B79F5) | 0;
|
|
38
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
39
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
40
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function shuffled(list, random) {
|
|
45
|
+
const out = [...list];
|
|
46
|
+
for (let i = out.length - 1; i > 0; i--) {
|
|
47
|
+
const j = Math.floor(random() * (i + 1));
|
|
48
|
+
[out[i], out[j]] = [out[j], out[i]];
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function clockMs(state) {
|
|
54
|
+
return Math.max(state.cfg.clockFloorMs, state.cfg.clockStartMs - state.score * state.cfg.rampPerPoint);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Deal a sheet: N invoices, N payments that settle them, and one more
|
|
59
|
+
* payment that settles nothing.
|
|
60
|
+
*
|
|
61
|
+
* The odd one out is built from an amount no invoice carries, because
|
|
62
|
+
* a fraud line that happens to equal a real invoice would make the
|
|
63
|
+
* right answer unknowable.
|
|
64
|
+
*/
|
|
65
|
+
function deal(state, now) {
|
|
66
|
+
const amounts = shuffled(AMOUNTS, state.random).slice(0, state.cfg.invoices);
|
|
67
|
+
const references = shuffled(REFERENCES, state.random);
|
|
68
|
+
|
|
69
|
+
const invoices = amounts.map((amount, i) => ({
|
|
70
|
+
id: `inv-${i}`,
|
|
71
|
+
reference: references[i % references.length],
|
|
72
|
+
amount,
|
|
73
|
+
settled: false,
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
const payments = invoices.map((invoice, i) => ({
|
|
77
|
+
id: `pay-${i}`,
|
|
78
|
+
amount: invoice.amount,
|
|
79
|
+
reference: invoice.reference,
|
|
80
|
+
fraud: false,
|
|
81
|
+
done: false,
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
const spare = AMOUNTS.filter((a) => !amounts.includes(a));
|
|
85
|
+
payments.push({
|
|
86
|
+
id: 'pay-odd',
|
|
87
|
+
amount: spare[Math.floor(state.random() * spare.length)],
|
|
88
|
+
/* A reference nobody issued: close enough to look filed, wrong
|
|
89
|
+
enough to be findable. */
|
|
90
|
+
reference: `2026-${900 + Math.floor(state.random() * 90)}`,
|
|
91
|
+
fraud: true,
|
|
92
|
+
done: false,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
...state,
|
|
97
|
+
sheet: {
|
|
98
|
+
invoices,
|
|
99
|
+
payments: shuffled(payments, state.random),
|
|
100
|
+
startedAt: now,
|
|
101
|
+
expiresAt: now + clockMs(state),
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function createGame({seed = Date.now(), now = 0, config = {}} = {}) {
|
|
107
|
+
const cfg = {...DEFAULTS, ...config};
|
|
108
|
+
const base = {
|
|
109
|
+
cfg,
|
|
110
|
+
random: mulberry32(seed),
|
|
111
|
+
sheet: null,
|
|
112
|
+
score: 0,
|
|
113
|
+
lives: cfg.lives,
|
|
114
|
+
matched: 0,
|
|
115
|
+
caught: 0,
|
|
116
|
+
mistakes: 0,
|
|
117
|
+
sheets: 0,
|
|
118
|
+
last: null,
|
|
119
|
+
over: false,
|
|
120
|
+
};
|
|
121
|
+
return deal(base, now);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function remaining(state, now) {
|
|
125
|
+
if (!state.sheet) return 0;
|
|
126
|
+
const total = state.sheet.expiresAt - state.sheet.startedAt;
|
|
127
|
+
if (total <= 0) return 0;
|
|
128
|
+
return Math.min(1, Math.max(0, (state.sheet.expiresAt - now) / total));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Everything on this sheet handled? */
|
|
132
|
+
export function sheetDone(state) {
|
|
133
|
+
return Boolean(state.sheet) && state.sheet.payments.every((p) => p.done);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function loseLife(state, reason, now) {
|
|
137
|
+
const lives = state.lives - 1;
|
|
138
|
+
return {
|
|
139
|
+
...state,
|
|
140
|
+
lives,
|
|
141
|
+
mistakes: state.mistakes + 1,
|
|
142
|
+
last: {result: reason, at: now},
|
|
143
|
+
over: lives <= 0,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function advance(state, now) {
|
|
148
|
+
if (!sheetDone(state)) return state;
|
|
149
|
+
const cleared = {
|
|
150
|
+
...state,
|
|
151
|
+
score: state.score + state.cfg.pointsPerSheet,
|
|
152
|
+
sheets: state.sheets + 1,
|
|
153
|
+
last: {result: 'sheet', at: now},
|
|
154
|
+
};
|
|
155
|
+
return deal(cleared, now);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Send a payment to an invoice.
|
|
160
|
+
*
|
|
161
|
+
* Right pays, wrong costs a life, and the fraud line costs a life
|
|
162
|
+
* wherever it is sent: that is the mistake the app exists to prevent.
|
|
163
|
+
*/
|
|
164
|
+
export function match(state, paymentId, invoiceId, now) {
|
|
165
|
+
if (state.over || !state.sheet) return state;
|
|
166
|
+
const payment = state.sheet.payments.find((p) => p.id === paymentId);
|
|
167
|
+
const invoice = state.sheet.invoices.find((i) => i.id === invoiceId);
|
|
168
|
+
if (!payment || !invoice || payment.done || invoice.settled) return state;
|
|
169
|
+
|
|
170
|
+
if (payment.fraud || payment.amount !== invoice.amount) {
|
|
171
|
+
return loseLife(state, payment.fraud ? 'paidFraud' : 'mismatch', now);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const next = {
|
|
175
|
+
...state,
|
|
176
|
+
sheet: {
|
|
177
|
+
...state.sheet,
|
|
178
|
+
payments: state.sheet.payments.map((p) => (p.id === paymentId ? {...p, done: true} : p)),
|
|
179
|
+
invoices: state.sheet.invoices.map((i) => (i.id === invoiceId ? {...i, settled: true} : i)),
|
|
180
|
+
},
|
|
181
|
+
score: state.score + state.cfg.pointsPerMatch,
|
|
182
|
+
matched: state.matched + 1,
|
|
183
|
+
last: {result: 'matched', at: now},
|
|
184
|
+
};
|
|
185
|
+
return advance(next, now);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Flag a payment as the one that belongs to nobody.
|
|
190
|
+
*
|
|
191
|
+
* Crying wolf at a genuine payment costs a life too. A game that only
|
|
192
|
+
* punished missing the fraud would teach you to flag everything.
|
|
193
|
+
*/
|
|
194
|
+
export function flag(state, paymentId, now) {
|
|
195
|
+
if (state.over || !state.sheet) return state;
|
|
196
|
+
const payment = state.sheet.payments.find((p) => p.id === paymentId);
|
|
197
|
+
if (!payment || payment.done) return state;
|
|
198
|
+
|
|
199
|
+
if (!payment.fraud) return loseLife(state, 'flaggedGood', now);
|
|
200
|
+
|
|
201
|
+
const next = {
|
|
202
|
+
...state,
|
|
203
|
+
sheet: {
|
|
204
|
+
...state.sheet,
|
|
205
|
+
payments: state.sheet.payments.map((p) => (p.id === paymentId ? {...p, done: true} : p)),
|
|
206
|
+
},
|
|
207
|
+
score: state.score + state.cfg.pointsPerFraud,
|
|
208
|
+
caught: state.caught + 1,
|
|
209
|
+
last: {result: 'caught', at: now},
|
|
210
|
+
};
|
|
211
|
+
return advance(next, now);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The month closes on its own, and an unfinished sheet costs a life. */
|
|
215
|
+
export function step(state, now) {
|
|
216
|
+
if (state.over || !state.sheet) return state;
|
|
217
|
+
if (now < state.sheet.expiresAt) return state;
|
|
218
|
+
const hit = loseLife(state, 'monthClosed', now);
|
|
219
|
+
return hit.over ? {...hit, sheet: null} : deal(hit, now);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** The line that goes on the game-over card and into the post. */
|
|
223
|
+
export function summarise(state, locale = 'en') {
|
|
224
|
+
const n = (v) => Number(v || 0).toLocaleString(locale);
|
|
225
|
+
return locale === 'nl'
|
|
226
|
+
? `${n(state.matched)} regels gematcht · ${n(state.caught)} keer fraude gevonden`
|
|
227
|
+
: `${n(state.matched)} lines matched · ${n(state.caught)} caught out`;
|
|
228
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
.grid { display: grid; gap: 16px; }
|
|
6
6
|
.cols-2 { grid-template-columns: repeat(2, 1fr); }
|
|
7
7
|
.cols-3 { grid-template-columns: repeat(3, 1fr); }
|
|
8
|
-
@media (max-width: 900px) { .grid { grid-template-columns: 1fr; } }
|
|
8
|
+
@media (max-width: 900px) { .grid { grid-template-columns: minmax(0, 1fr); } }
|
|
9
9
|
|
|
10
10
|
.card {
|
|
11
11
|
background: white;
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
/* ---------- Body ---------- */
|
|
34
34
|
.body { display: grid; gap: var(--space-8); align-items: stretch; }
|
|
35
35
|
.layout-side .body { grid-template-columns: 1fr 1.2fr; }
|
|
36
|
-
.layout-stack .body { grid-template-columns: 1fr; gap: var(--space-6); }
|
|
37
|
-
@media (max-width: 900px) { .layout-side .body { grid-template-columns: 1fr; } }
|
|
36
|
+
.layout-stack .body { grid-template-columns: minmax(0, 1fr); gap: var(--space-6); }
|
|
37
|
+
@media (max-width: 900px) { .layout-side .body { grid-template-columns: minmax(0, 1fr); } }
|
|
38
38
|
|
|
39
39
|
/* ---------- List of items ---------- */
|
|
40
40
|
.list { display: flex; flex-direction: column; gap: var(--space-3); }
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
.grid { display: grid; gap: 20px; }
|
|
6
6
|
.cols-2 { grid-template-columns: repeat(2, 1fr); }
|
|
7
7
|
.cols-3 { grid-template-columns: repeat(3, 1fr); }
|
|
8
|
-
@media (max-width: 900px) { .grid { grid-template-columns: 1fr; } }
|
|
8
|
+
@media (max-width: 900px) { .grid { grid-template-columns: minmax(0, 1fr); } }
|
|
9
9
|
|
|
10
10
|
.card {
|
|
11
11
|
background: white;
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
}
|
|
54
54
|
.cols-2 { grid-template-columns: repeat(2, 1fr); }
|
|
55
55
|
.cols-3 { grid-template-columns: repeat(3, 1fr); }
|
|
56
|
-
@media (max-width: 900px) { .grid { grid-template-columns: 1fr; } }
|
|
56
|
+
@media (max-width: 900px) { .grid { grid-template-columns: minmax(0, 1fr); } }
|
|
57
57
|
|
|
58
58
|
/* ---- Carousel (default) ----
|
|
59
59
|
*
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
.carousel .card { width: auto; }
|
|
173
173
|
}
|
|
174
174
|
@media (prefers-reduced-motion: reduce) and (max-width: 900px) {
|
|
175
|
-
.group { grid-template-columns: 1fr; }
|
|
175
|
+
.group { grid-template-columns: minmax(0, 1fr); }
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
.card {
|
package/src/components/index.js
CHANGED
|
@@ -66,6 +66,9 @@ export {default as BlueprintRush} from './BlueprintRush/BlueprintRush.jsx';
|
|
|
66
66
|
export {default as RecordRun} from './RecordRun/RecordRun.jsx';
|
|
67
67
|
export {default as HiddenGame} from './HiddenGame/HiddenGame.jsx';
|
|
68
68
|
export {default as MonsterRun} from './MonsterRun/MonsterRun.jsx';
|
|
69
|
+
export {default as DiceDuel} from './DiceDuel/DiceDuel.jsx';
|
|
70
|
+
export {default as Reconcile} from './Reconcile/Reconcile.jsx';
|
|
71
|
+
export {default as PipeFit} from './PipeFit/PipeFit.jsx';
|
|
69
72
|
export {default as LockPick} from './LockPick/LockPick.jsx';
|
|
70
73
|
export {default as PaintByTokens} from './PaintByTokens/PaintByTokens.jsx';
|
|
71
74
|
export {default as Redaction} from './Redaction/Redaction.jsx';
|
|
@@ -46,6 +46,26 @@ export const ICONS = {
|
|
|
46
46
|
<polyline points="13 6 19 12 13 18"/>
|
|
47
47
|
</svg>
|
|
48
48
|
),
|
|
49
|
+
|
|
50
|
+
/* Hamburger. Opens the navbar drawer below the desktop breakpoint.
|
|
51
|
+
Three bars rather than a "more" glyph because the drawer holds the
|
|
52
|
+
whole primary navigation, not an overflow remainder. */
|
|
53
|
+
menu: (
|
|
54
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
|
|
55
|
+
<line x1="4" y1="7" x2="20" y2="7"/>
|
|
56
|
+
<line x1="4" y1="12" x2="20" y2="12"/>
|
|
57
|
+
<line x1="4" y1="17" x2="20" y2="17"/>
|
|
58
|
+
</svg>
|
|
59
|
+
),
|
|
60
|
+
|
|
61
|
+
/* Close. Dismisses the navbar drawer. Same stroke weight as `menu` so
|
|
62
|
+
the toggle does not visually jump when the icon swaps. */
|
|
63
|
+
close: (
|
|
64
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
|
|
65
|
+
<line x1="6" y1="6" x2="18" y2="18"/>
|
|
66
|
+
<line x1="18" y1="6" x2="6" y2="18"/>
|
|
67
|
+
</svg>
|
|
68
|
+
),
|
|
49
69
|
};
|
|
50
70
|
|
|
51
71
|
/**
|
|
@@ -191,6 +191,44 @@ export default function Navbar() {
|
|
|
191
191
|
const items = navbar.items || [];
|
|
192
192
|
const brand = brandFor(location.pathname, navbar.title);
|
|
193
193
|
|
|
194
|
+
/* Mobile drawer. Below the desktop breakpoint the whole primary
|
|
195
|
+
navigation moves into a drawer, rather than being allowed to
|
|
196
|
+
overflow the bar: at 390px the inline bar needs about 571px, so
|
|
197
|
+
the trailing items used to land off-screen with no way to reach
|
|
198
|
+
them (the site clips horizontal overflow, so they were not even
|
|
199
|
+
scrollable). Everything goes in, links and CTAs both, because the
|
|
200
|
+
bar has to fit an unknown number of items on an unknown wordmark
|
|
201
|
+
length. Keeping any of them inline only moves the cliff. */
|
|
202
|
+
const [menuOpen, setMenuOpen] = React.useState(false);
|
|
203
|
+
const toggleRef = React.useRef(null);
|
|
204
|
+
|
|
205
|
+
/* Close on navigation. Without this the drawer stays open over the
|
|
206
|
+
page the visitor just asked for. */
|
|
207
|
+
React.useEffect(() => {
|
|
208
|
+
setMenuOpen(false);
|
|
209
|
+
}, [location.pathname]);
|
|
210
|
+
|
|
211
|
+
/* While open: Escape closes and returns focus to the toggle, and the
|
|
212
|
+
page behind the drawer does not scroll. Both effects are torn down
|
|
213
|
+
together so a route change mid-gesture cannot strand the body with
|
|
214
|
+
`overflow: hidden`. */
|
|
215
|
+
React.useEffect(() => {
|
|
216
|
+
if (!menuOpen) return undefined;
|
|
217
|
+
const onKeyDown = (event) => {
|
|
218
|
+
if (event.key === 'Escape') {
|
|
219
|
+
setMenuOpen(false);
|
|
220
|
+
toggleRef.current?.focus();
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
document.addEventListener('keydown', onKeyDown);
|
|
224
|
+
const previousOverflow = document.body.style.overflow;
|
|
225
|
+
document.body.style.overflow = 'hidden';
|
|
226
|
+
return () => {
|
|
227
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
228
|
+
document.body.style.overflow = previousOverflow;
|
|
229
|
+
};
|
|
230
|
+
}, [menuOpen]);
|
|
231
|
+
|
|
194
232
|
/* Wordmark resolution order:
|
|
195
233
|
1. ConNext / Common Ground sub-brand → custom JSX (Con<Next>, …)
|
|
196
234
|
2. Conduction product app (Open*, Docu*, My*, …) → prefix-light
|
|
@@ -278,6 +316,41 @@ export default function Navbar() {
|
|
|
278
316
|
<NavItem key={i} item={item} location={location} appVersion={appVersion} />
|
|
279
317
|
))}
|
|
280
318
|
</div>
|
|
319
|
+
|
|
320
|
+
{/* Drawer toggle. Rendered on every viewport and hidden with CSS
|
|
321
|
+
above the breakpoint, so the markup does not depend on a
|
|
322
|
+
client-side width measurement that SSR cannot make. */}
|
|
323
|
+
<button
|
|
324
|
+
ref={toggleRef}
|
|
325
|
+
type="button"
|
|
326
|
+
className={styles.menuToggle}
|
|
327
|
+
aria-expanded={menuOpen}
|
|
328
|
+
aria-controls="navbar-drawer"
|
|
329
|
+
aria-label={menuOpen
|
|
330
|
+
? translate({id: 'preset.navbar.menu.close', message: 'Close menu', description: 'Accessible label for the navbar drawer toggle while the drawer is open'})
|
|
331
|
+
: translate({id: 'preset.navbar.menu.open', message: 'Open menu', description: 'Accessible label for the navbar drawer toggle while the drawer is closed'})}
|
|
332
|
+
onClick={() => setMenuOpen((open) => !open)}
|
|
333
|
+
>
|
|
334
|
+
<span className={styles.iconGlyph} aria-hidden="true">
|
|
335
|
+
{menuOpen ? ICONS.close : ICONS.menu}
|
|
336
|
+
</span>
|
|
337
|
+
</button>
|
|
338
|
+
|
|
339
|
+
{/* Drawer. Kept mounted and hidden so the toggle's aria-controls
|
|
340
|
+
always resolves to a real element. `hidden` also keeps the
|
|
341
|
+
links out of the tab order and out of the accessibility tree
|
|
342
|
+
while closed, which a purely visual `display: none` on the
|
|
343
|
+
parent would not guarantee across the breakpoint. */}
|
|
344
|
+
<div id="navbar-drawer" className={styles.drawer} hidden={!menuOpen}>
|
|
345
|
+
<div className={styles.drawerItems}>
|
|
346
|
+
{leftItems.map((item, i) => (
|
|
347
|
+
<NavItem key={`l${i}`} item={item} location={location} appVersion={appVersion} />
|
|
348
|
+
))}
|
|
349
|
+
{rightItems.map((item, i) => (
|
|
350
|
+
<NavItem key={`r${i}`} item={item} location={location} appVersion={appVersion} />
|
|
351
|
+
))}
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
281
354
|
</nav>
|
|
282
355
|
);
|
|
283
356
|
}
|
|
@@ -193,15 +193,212 @@
|
|
|
193
193
|
white-space: nowrap;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Drawer toggle (hamburger).
|
|
198
|
+
*
|
|
199
|
+
* Hidden above the breakpoint. Always rendered in the markup so the
|
|
200
|
+
* button's `aria-controls` resolves during server rendering, when
|
|
201
|
+
* there is no viewport width to branch on.
|
|
202
|
+
*
|
|
203
|
+
* 44px square. WCAG 2.2 SC 2.5.8 (AA) only requires 24x24, which this
|
|
204
|
+
* clears comfortably; 44 is the size a thumb actually wants, and it is
|
|
205
|
+
* the primary control on every phone-width page.
|
|
206
|
+
*/
|
|
207
|
+
.menuToggle {
|
|
208
|
+
display: none;
|
|
209
|
+
align-items: center;
|
|
210
|
+
justify-content: center;
|
|
211
|
+
width: 44px;
|
|
212
|
+
height: 44px;
|
|
213
|
+
padding: 0;
|
|
214
|
+
border: 1px solid var(--c-cobalt-100);
|
|
215
|
+
border-radius: var(--radius-sm);
|
|
216
|
+
background: white;
|
|
217
|
+
color: var(--c-cobalt-700);
|
|
218
|
+
font-size: 22px;
|
|
219
|
+
cursor: pointer;
|
|
220
|
+
transition: color 160ms ease, background 160ms ease;
|
|
221
|
+
}
|
|
222
|
+
.menuToggle:hover {
|
|
223
|
+
color: var(--c-blue-cobalt);
|
|
224
|
+
background: var(--c-cobalt-50);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Drawer panel.
|
|
229
|
+
*
|
|
230
|
+
* `display: none` by default rather than relying on the `hidden`
|
|
231
|
+
* attribute alone, because the rules below set a display value and any
|
|
232
|
+
* such declaration overrides the UA default for `[hidden]`. Opening is
|
|
233
|
+
* therefore scoped to the breakpoint AND to `:not([hidden])`, so the
|
|
234
|
+
* panel can never appear on desktop even if the state says open.
|
|
235
|
+
*
|
|
236
|
+
* Positioned against .nav, which is `position: sticky` and so already
|
|
237
|
+
* a containing block for absolutely positioned descendants.
|
|
238
|
+
*/
|
|
239
|
+
.drawer {
|
|
240
|
+
display: none;
|
|
241
|
+
position: absolute;
|
|
242
|
+
top: 100%;
|
|
243
|
+
left: 0;
|
|
244
|
+
right: 0;
|
|
245
|
+
background: white;
|
|
246
|
+
border-bottom: 1px solid var(--c-cobalt-100);
|
|
247
|
+
box-shadow: 0 12px 24px rgba(16, 42, 86, 0.10);
|
|
248
|
+
max-height: 75vh;
|
|
249
|
+
overflow-y: auto;
|
|
250
|
+
-webkit-overflow-scrolling: touch;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.drawerItems {
|
|
254
|
+
display: flex;
|
|
255
|
+
flex-direction: column;
|
|
256
|
+
padding: 8px 24px 20px;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* Every interactive row in the drawer gets a full-width 44px target.
|
|
260
|
+
Styling by context rather than by passing a variant down through
|
|
261
|
+
NavItem keeps the item types (link, cta, github, apiDocs, locale)
|
|
262
|
+
rendering exactly as they do in the bar. */
|
|
263
|
+
.drawerItems > a {
|
|
264
|
+
display: flex;
|
|
265
|
+
align-items: center;
|
|
266
|
+
min-height: 44px;
|
|
267
|
+
width: 100%;
|
|
268
|
+
border-bottom: 1px solid var(--c-cobalt-50);
|
|
269
|
+
font-size: 16px;
|
|
270
|
+
}
|
|
271
|
+
.drawerItems > a:last-child { border-bottom: none; }
|
|
272
|
+
|
|
273
|
+
/* The locale item is a list, not a single row, so it sizes to its
|
|
274
|
+
contents and lets the language links below own the 44px rhythm. */
|
|
275
|
+
.drawerItems > .localeWrapper {
|
|
276
|
+
display: block;
|
|
277
|
+
width: 100%;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/* The install CTA keeps its filled treatment but becomes a full-width
|
|
281
|
+
block, so it reads as the primary action of the drawer. */
|
|
282
|
+
.drawerItems > .cta {
|
|
283
|
+
justify-content: center;
|
|
284
|
+
margin-top: 16px;
|
|
285
|
+
border-bottom: none;
|
|
286
|
+
padding: 12px 16px;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* Icon-only items (GitHub) need a visible hit area in a vertical list,
|
|
290
|
+
where there is no neighbouring label to borrow width from. */
|
|
291
|
+
.drawerItems > .iconLink {
|
|
292
|
+
width: 100%;
|
|
293
|
+
justify-content: flex-start;
|
|
294
|
+
height: auto;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* The version pill is a static chip, not a control: keep it inline and
|
|
298
|
+
out of the 44px rhythm. */
|
|
299
|
+
.drawerItems > .versionPill {
|
|
300
|
+
align-self: flex-start;
|
|
301
|
+
margin-top: 12px;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Locale switcher inside the drawer.
|
|
306
|
+
*
|
|
307
|
+
* Two Infima behaviours have to be undone here.
|
|
308
|
+
*
|
|
309
|
+
* First, Infima hides `.navbar__item` outright below 996px, because it
|
|
310
|
+
* expects its own mobile sidebar to take over. Inside this drawer that
|
|
311
|
+
* rule collapses the locale item to a zero-height blank row, so the
|
|
312
|
+
* display value is restored explicitly.
|
|
313
|
+
*
|
|
314
|
+
* Second, the menu is a hover dropdown positioned absolutely. A hover
|
|
315
|
+
* dropdown is the wrong control on touch, and in a vertical panel the
|
|
316
|
+
* absolute menu lands off the side of the screen. Pinning it into the
|
|
317
|
+
* flow and dropping the trigger turns it into what it should be on a
|
|
318
|
+
* phone: a plain list of the available languages.
|
|
319
|
+
*/
|
|
320
|
+
.drawer .localeWrapper :global(.navbar__item) {
|
|
321
|
+
display: block;
|
|
322
|
+
width: 100%;
|
|
323
|
+
padding: 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* The trigger just repeats the current locale, which the list already
|
|
327
|
+
marks as active. Dropping it avoids an "English / English /
|
|
328
|
+
Nederlands" stutter. */
|
|
329
|
+
.drawer .localeWrapper :global(.navbar__link) {
|
|
330
|
+
display: none;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.drawer .localeWrapper :global(.dropdown__menu) {
|
|
334
|
+
position: static;
|
|
335
|
+
display: block;
|
|
336
|
+
transform: none;
|
|
337
|
+
opacity: 1;
|
|
338
|
+
visibility: visible;
|
|
339
|
+
box-shadow: none;
|
|
340
|
+
background: transparent;
|
|
341
|
+
padding: 0;
|
|
342
|
+
margin: 0;
|
|
343
|
+
min-width: 0;
|
|
344
|
+
max-height: none;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.drawer .localeWrapper :global(.dropdown__link) {
|
|
348
|
+
display: flex;
|
|
349
|
+
align-items: center;
|
|
350
|
+
min-height: 44px;
|
|
351
|
+
margin: 0;
|
|
352
|
+
border-radius: 0;
|
|
353
|
+
border-bottom: 1px solid var(--c-cobalt-50);
|
|
354
|
+
font-size: 16px;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Breakpoint.
|
|
359
|
+
*
|
|
360
|
+
* 996px matches Docusaurus's own navbar collapse, so the drawer and
|
|
361
|
+
* the framework's mobile sidebar agree about where "mobile" starts.
|
|
362
|
+
* Measured on conduction.nl: the inline bar needs 571px at its
|
|
363
|
+
* narrowest and 775px with a full item set, so collapsing at 996px
|
|
364
|
+
* clears both with room for sites that add items.
|
|
365
|
+
*/
|
|
366
|
+
@media (max-width: 996px) {
|
|
199
367
|
.nav {
|
|
200
|
-
padding: 16px
|
|
201
|
-
|
|
202
|
-
gap: 16px;
|
|
368
|
+
padding: 12px 16px;
|
|
369
|
+
gap: 12px;
|
|
203
370
|
}
|
|
204
371
|
.left { gap: 16px; }
|
|
205
|
-
|
|
206
|
-
|
|
372
|
+
|
|
373
|
+
/* The bar keeps only the wordmark and the toggle. */
|
|
374
|
+
.links,
|
|
375
|
+
.ctas {
|
|
376
|
+
display: none;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.menuToggle {
|
|
380
|
+
display: inline-flex;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.drawer:not([hidden]) {
|
|
384
|
+
display: block;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/* Long product wordmarks must not push the toggle off the bar. The
|
|
388
|
+
wordmark is also the home link, so it gets the same 44px height as
|
|
389
|
+
the toggle it sits opposite. */
|
|
390
|
+
.wordmark {
|
|
391
|
+
font-size: 19px;
|
|
392
|
+
min-width: 0;
|
|
393
|
+
min-height: 44px;
|
|
394
|
+
}
|
|
395
|
+
.wordmarkText {
|
|
396
|
+
overflow: hidden;
|
|
397
|
+
text-overflow: ellipsis;
|
|
398
|
+
white-space: nowrap;
|
|
399
|
+
}
|
|
400
|
+
.left {
|
|
401
|
+
min-width: 0;
|
|
402
|
+
flex: 1;
|
|
403
|
+
}
|
|
207
404
|
}
|