@conduction/docusaurus-preset 3.27.2 → 3.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/package.json +6 -5
- package/src/components/AiDisclosure/AiDisclosure.jsx +82 -0
- package/src/components/AiDisclosure/AiDisclosure.module.css +35 -0
- package/src/components/AiDisclosure/__tests__/AiDisclosure.render.test.js +127 -0
- package/src/components/AiDisclosure/__tests__/disclosure.test.js +218 -0
- package/src/components/AiDisclosure/disclosure.js +169 -0
- package/src/components/AppMock/AppMock.jsx +16 -0
- package/src/components/AppMock/variants/AppVersionsMock.jsx +83 -0
- package/src/components/AppMock/variants/DoriathMock.jsx +79 -0
- package/src/components/AppMock/variants/HermiqMock.jsx +71 -0
- package/src/components/AppMock/variants/HrmqMock.jsx +77 -0
- package/src/components/AppMock/variants/PlanixMock.jsx +80 -0
- package/src/components/AppMock/variants/PortaliqMock.jsx +63 -0
- package/src/components/AppMock/variants/ScholiqMock.jsx +74 -0
- package/src/components/AppMock/variants/ShillinqMock.jsx +90 -0
- package/src/components/CookieCli/CookieCli.jsx +197 -24
- package/src/components/CookieCli/CookieCli.module.css +139 -1
- package/src/components/CookieCli/shell.js +161 -0
- package/src/components/CookieCli/spaceInvaders.js +359 -0
- package/src/components/CookieCli/spaceInvaders.test.js +112 -0
- package/src/components/index.js +7 -0
- package/src/data/app-glyphs.json +4 -0
- package/src/theme/BlogPostItem/Content/index.jsx +43 -0
- package/src/theme/DocItem/Content/index.jsx +15 -0
- package/static/img/ai-disclosure/PROVENANCE.md +87 -0
- package/static/img/ai-disclosure/ai-black-transparent.svg +20 -0
- package/static/img/ai-disclosure/ai-black.svg +19 -0
- package/static/img/ai-disclosure/ai-generated-black-transparent.svg +31 -0
- package/static/img/ai-disclosure/ai-generated-black.svg +30 -0
- package/static/img/ai-disclosure/ai-generated-white-transparent.svg +32 -0
- package/static/img/ai-disclosure/ai-generated-white.svg +31 -0
- package/static/img/ai-disclosure/ai-modified-black-transparent.svg +30 -0
- package/static/img/ai-disclosure/ai-modified-black.svg +29 -0
- package/static/img/ai-disclosure/ai-modified-white-transparent.svg +31 -0
- package/static/img/ai-disclosure/ai-modified-white.svg +30 -0
- package/static/img/ai-disclosure/ai-white-transparent.svg +21 -0
- package/static/img/ai-disclosure/ai-white.svg +20 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scholiq abstract — learning environment.
|
|
3
|
+
*
|
|
4
|
+
* Inferred from the app role (courses, lessons, credentials): centre
|
|
5
|
+
* shows a course-card grid, each card carrying a progress bar, with a
|
|
6
|
+
* credential chip on the completed course. Left nav for courses /
|
|
7
|
+
* lessons / learners / certificates. Tone: forest — data you can
|
|
8
|
+
* trust a diploma to.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import styles from '../AppMock.module.css';
|
|
13
|
+
|
|
14
|
+
const PROGRESS = [
|
|
15
|
+
{ico: 'b', fill: '100%', done: true},
|
|
16
|
+
{ico: 'b', fill: '65%'},
|
|
17
|
+
{ico: null, fill: '40%'},
|
|
18
|
+
{ico: 'b', fill: '80%'},
|
|
19
|
+
{ico: 'e', fill: '25%'},
|
|
20
|
+
{ico: 'b', fill: '10%'},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export default function ScholiqMock() {
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<div className={styles.topbar}>
|
|
27
|
+
<div className={styles.logo}></div>
|
|
28
|
+
{Array.from({length: 14}).map((_, i) => <div key={i} className={styles.icon}></div>)}
|
|
29
|
+
<div className={styles.spacer}></div>
|
|
30
|
+
<div className={styles.bell}></div>
|
|
31
|
+
<div className={styles.avatar}></div>
|
|
32
|
+
</div>
|
|
33
|
+
<div className={[styles.body, styles.opencatalogi].filter(Boolean).join(' ')}>
|
|
34
|
+
<div className={styles.nav}>
|
|
35
|
+
<div className={styles.navHead}><div className={styles.h}></div><div className={styles.l}></div></div>
|
|
36
|
+
{[true, false, false, false].map((active, i) => (
|
|
37
|
+
<div key={i} className={[styles.item, active && styles.active].filter(Boolean).join(' ')}>
|
|
38
|
+
<div className={styles.ico}></div>
|
|
39
|
+
<div className={styles.l}></div>
|
|
40
|
+
</div>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
<div className={styles.col}>
|
|
44
|
+
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 6}}>
|
|
45
|
+
<div className={styles.row + ' ' + styles.head} style={{width: '30%'}}></div>
|
|
46
|
+
<div className={styles.actions}>
|
|
47
|
+
<div className={styles.btn}></div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
{/* Course cards with progress bars */}
|
|
51
|
+
<div className={styles.grid}>
|
|
52
|
+
{PROGRESS.map(({ico, fill, done}, i) => (
|
|
53
|
+
<div key={i} className={[styles.card, ico && styles[ico]].filter(Boolean).join(' ')}>
|
|
54
|
+
<div className={styles.ico}></div>
|
|
55
|
+
<div className={styles.row}></div>
|
|
56
|
+
<div className={styles.row + ' ' + styles.short}></div>
|
|
57
|
+
<div style={{height: 4, background: 'var(--c-cobalt-100)', borderRadius: 2}}>
|
|
58
|
+
<div style={{width: fill, height: 4, background: 'var(--c-forest-500)', borderRadius: 2}}></div>
|
|
59
|
+
</div>
|
|
60
|
+
{done && (
|
|
61
|
+
/* Credential chip — course completed, certificate issued */
|
|
62
|
+
<div style={{display: 'inline-flex', alignItems: 'center', gap: 3, padding: '2px 6px', borderRadius: 999, background: 'var(--c-forest-300)', alignSelf: 'flex-start'}}>
|
|
63
|
+
<div style={{width: 6, height: 7, clipPath: 'var(--hex-pointy-top)', background: 'var(--c-forest-500)'}}></div>
|
|
64
|
+
<div style={{height: 3, width: 22, background: 'var(--c-forest-500)', borderRadius: 1}}></div>
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shillinq abstract — bookkeeping ledger.
|
|
3
|
+
*
|
|
4
|
+
* Inferred from the app role (invoices, VAT, bank reconciliation):
|
|
5
|
+
* a VAT/summary KPI strip over an invoice table — document icon,
|
|
6
|
+
* description, two amount columns (net / VAT), and a reconciliation
|
|
7
|
+
* tick column where a mint hex = matched against the bank and a
|
|
8
|
+
* muted pip = still open. Tone: terracotta — documents, human work.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import styles from '../AppMock.module.css';
|
|
13
|
+
|
|
14
|
+
const INVOICES = [
|
|
15
|
+
{amount: 26, vat: 16, matched: true},
|
|
16
|
+
{amount: 20, vat: 12, matched: true},
|
|
17
|
+
{amount: 30, vat: 18, matched: false},
|
|
18
|
+
{amount: 22, vat: 14, matched: true},
|
|
19
|
+
{amount: 18, vat: 10, matched: false},
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export default function ShillinqMock() {
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
<div className={styles.topbar}>
|
|
26
|
+
<div className={styles.logo}></div>
|
|
27
|
+
{Array.from({length: 14}).map((_, i) => <div key={i} className={styles.icon}></div>)}
|
|
28
|
+
<div className={styles.spacer}></div>
|
|
29
|
+
<div className={styles.bell}></div>
|
|
30
|
+
<div className={styles.avatar}></div>
|
|
31
|
+
</div>
|
|
32
|
+
<div className={[styles.body, styles.decidesk].filter(Boolean).join(' ')}>
|
|
33
|
+
<div className={styles.nav}>
|
|
34
|
+
<div className={styles.navHead}><div className={styles.h}></div><div className={styles.l}></div></div>
|
|
35
|
+
{[true, false, false, false, false].map((active, i) => (
|
|
36
|
+
<div key={i} className={[styles.item, active && styles.active].filter(Boolean).join(' ')}>
|
|
37
|
+
<div className={styles.ico}></div>
|
|
38
|
+
<div className={styles.l}></div>
|
|
39
|
+
</div>
|
|
40
|
+
))}
|
|
41
|
+
</div>
|
|
42
|
+
<div className={styles.col}>
|
|
43
|
+
<div className={styles.head}>
|
|
44
|
+
<div className={styles.row + ' ' + styles.head} style={{width: '30%'}}></div>
|
|
45
|
+
<div className={styles.actions}>
|
|
46
|
+
<div className={styles.btn + ' ' + styles.ghost}></div>
|
|
47
|
+
<div className={styles.btn}></div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
{/* VAT / summary strip */}
|
|
51
|
+
<div className={styles.kpiRow}>
|
|
52
|
+
<div className={styles.kpi}>
|
|
53
|
+
<div className={styles.ico} style={{background: 'var(--c-terracotta-500)'}}></div>
|
|
54
|
+
<div className={styles.meta}><div className={styles.num}></div><div className={styles.label}></div></div>
|
|
55
|
+
</div>
|
|
56
|
+
<div className={styles.kpi}>
|
|
57
|
+
<div className={styles.ico} style={{background: 'var(--c-terracotta-300)'}}></div>
|
|
58
|
+
<div className={styles.meta}><div className={styles.num}></div><div className={styles.label}></div></div>
|
|
59
|
+
</div>
|
|
60
|
+
<div className={styles.kpi}>
|
|
61
|
+
<div className={styles.ico}></div>
|
|
62
|
+
<div className={styles.meta}><div className={styles.num}></div><div className={styles.label}></div></div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
{/* Invoice table: doc · description · net · VAT · reconciled */}
|
|
66
|
+
<div className={styles.panel} style={{flex: 1}}>
|
|
67
|
+
<div className={styles.head}><div className={styles.title}></div></div>
|
|
68
|
+
<div className={styles.stack}>
|
|
69
|
+
{INVOICES.map(({amount, vat, matched}, i) => (
|
|
70
|
+
<div key={i} className={styles.item}>
|
|
71
|
+
<div style={{width: 11, height: 13, clipPath: 'var(--hex-pointy-top)', background: 'var(--c-terracotta-300)', flexShrink: 0}}></div>
|
|
72
|
+
<div className={styles.lines}><div className={styles.l1}></div></div>
|
|
73
|
+
<div style={{width: amount, height: 4, background: 'var(--c-cobalt-700)', borderRadius: 1, flexShrink: 0}}></div>
|
|
74
|
+
<div style={{width: vat, height: 4, background: 'var(--c-cobalt-300)', borderRadius: 1, flexShrink: 0}}></div>
|
|
75
|
+
{/* Bank-reconciliation tick: mint hex = matched */}
|
|
76
|
+
<div style={{width: 9, height: 10, clipPath: 'var(--hex-pointy-top)', background: matched ? 'var(--c-mint-500)' : 'var(--c-cobalt-200)', flexShrink: 0}}></div>
|
|
77
|
+
</div>
|
|
78
|
+
))}
|
|
79
|
+
</div>
|
|
80
|
+
{/* Totals line */}
|
|
81
|
+
<div style={{display: 'flex', justifyContent: 'flex-end', gap: 6, paddingTop: 4, borderTop: '1px solid var(--c-cobalt-100)'}}>
|
|
82
|
+
<div style={{width: 32, height: 6, background: 'var(--c-cobalt-900)', borderRadius: 1}}></div>
|
|
83
|
+
<div style={{width: 20, height: 6, background: 'var(--c-cobalt-400)', borderRadius: 1}}></div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -31,9 +31,11 @@
|
|
|
31
31
|
* on every save.
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
-
import React, {useState, useEffect, useMemo, useCallback} from 'react';
|
|
34
|
+
import React, {useState, useEffect, useMemo, useCallback, useRef} from 'react';
|
|
35
35
|
import useIsBrowser from '@docusaurus/useIsBrowser';
|
|
36
36
|
import styles from './CookieCli.module.css';
|
|
37
|
+
import SpaceInvaders from './spaceInvaders';
|
|
38
|
+
import {runCommand} from './shell';
|
|
37
39
|
|
|
38
40
|
const STORAGE_KEY = 'conduction:cookie-cli';
|
|
39
41
|
|
|
@@ -136,25 +138,127 @@ export default function CookieCli({
|
|
|
136
138
|
save(minimal);
|
|
137
139
|
}, [categories, save]);
|
|
138
140
|
|
|
139
|
-
/*
|
|
140
|
-
|
|
141
|
+
/* ---- the shell, and the game hiding in it ---- */
|
|
142
|
+
|
|
143
|
+
const [buffer, setBuffer] = useState('');
|
|
144
|
+
const [history, setHistory] = useState([]); // [{cmd, lines}]
|
|
145
|
+
const [mode, setMode] = useState('shell'); // 'shell' | 'game'
|
|
146
|
+
const gameRef = useRef(null);
|
|
147
|
+
const screenRef = useRef(null);
|
|
148
|
+
const hudRef = useRef(null);
|
|
149
|
+
const footerRef = useRef(null);
|
|
150
|
+
const promptRef = useRef(null);
|
|
151
|
+
|
|
152
|
+
const emit = useCallback((cmd, lines) => {
|
|
153
|
+
setHistory((prev) => [...prev, {cmd, lines}]);
|
|
154
|
+
}, []);
|
|
155
|
+
|
|
156
|
+
const exitGame = useCallback(() => {
|
|
157
|
+
if (gameRef.current) gameRef.current.stop();
|
|
158
|
+
gameRef.current = null;
|
|
159
|
+
setMode('shell');
|
|
160
|
+
emit(null, [{text: '# thanks for playing. back to your regularly scheduled cookies.', tone: 'cmt'}]);
|
|
161
|
+
}, [emit]);
|
|
162
|
+
|
|
163
|
+
const startGame = useCallback(() => {
|
|
164
|
+
emit(null, [
|
|
165
|
+
{text: 'Loading game.exe ...', tone: 'cmt'},
|
|
166
|
+
{text: '[OK] CONDUCTION SPACE INVADERS v0.1', tone: 'ok'},
|
|
167
|
+
]);
|
|
168
|
+
setMode('game');
|
|
169
|
+
}, [emit]);
|
|
170
|
+
|
|
171
|
+
/* Construct the engine once the game surface is actually on screen.
|
|
172
|
+
Building it in the click handler would hand it refs that are still
|
|
173
|
+
null, because the panel it draws into has not rendered yet. */
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
if (mode !== 'game' || gameRef.current) return undefined;
|
|
176
|
+
gameRef.current = new SpaceInvaders({
|
|
177
|
+
onFrame: ({screen, hud, footer}) => {
|
|
178
|
+
if (screenRef.current) screenRef.current.innerHTML = screen;
|
|
179
|
+
if (hudRef.current) hudRef.current.innerHTML = hud;
|
|
180
|
+
if (footerRef.current) footerRef.current.innerHTML = footer;
|
|
181
|
+
},
|
|
182
|
+
onExit: exitGame,
|
|
183
|
+
});
|
|
184
|
+
return () => {
|
|
185
|
+
if (gameRef.current) { gameRef.current.stop(); gameRef.current = null; }
|
|
186
|
+
};
|
|
187
|
+
}, [mode, exitGame]);
|
|
188
|
+
|
|
189
|
+
const submit = useCallback((line) => {
|
|
190
|
+
const result = runCommand(line);
|
|
191
|
+
if (result.clear) { setHistory([]); return; }
|
|
192
|
+
emit(line, result.lines);
|
|
193
|
+
if (result.effect === 'game') startGame();
|
|
194
|
+
}, [emit, startGame]);
|
|
195
|
+
|
|
196
|
+
/* One key handler for both modes.
|
|
197
|
+
In game mode every key belongs to the game. In shell mode the rule is
|
|
198
|
+
the kit specimen's, exactly: digits pick cookie options while the
|
|
199
|
+
prompt is empty, and everything else types.
|
|
200
|
+
|
|
201
|
+
There used to be [A] [S] [R] accelerators here, live whenever the
|
|
202
|
+
prompt was empty. They quietly ate the first letter of any command
|
|
203
|
+
starting with those letters, which made `sudo` and `rm` unreachable:
|
|
204
|
+
typing `sudo` answered the consent dialog and closed the banner
|
|
205
|
+
instead. A shell that silently refuses a third of the alphabet is
|
|
206
|
+
worse than one with no shortcuts, and the three actions are buttons
|
|
207
|
+
you can click or tab to. */
|
|
141
208
|
useEffect(() => {
|
|
142
|
-
if (!isBrowser || decided) return;
|
|
209
|
+
if (!isBrowser || decided) return undefined;
|
|
210
|
+
|
|
143
211
|
function onKey(e) {
|
|
144
212
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
213
|
+
|
|
214
|
+
if (mode === 'game') {
|
|
215
|
+
if (gameRef.current) gameRef.current.onKey(e);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (e.key === 'Enter') {
|
|
151
220
|
e.preventDefault();
|
|
152
|
-
|
|
221
|
+
const line = buffer;
|
|
222
|
+
setBuffer('');
|
|
223
|
+
submit(line);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (e.key === 'Backspace') {
|
|
227
|
+
e.preventDefault();
|
|
228
|
+
setBuffer((b) => b.slice(0, -1));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (e.key === 'Escape') {
|
|
232
|
+
e.preventDefault();
|
|
233
|
+
setBuffer('');
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* Digits toggle only from an empty prompt, so a filename or a version
|
|
238
|
+
number can still be typed. */
|
|
239
|
+
if (buffer.length === 0) {
|
|
240
|
+
const idx = parseInt(e.key, 10);
|
|
241
|
+
if (!Number.isNaN(idx) && idx >= 1 && idx <= categories.length) {
|
|
242
|
+
e.preventDefault();
|
|
243
|
+
toggle(categories[idx - 1]);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (e.key.length === 1) {
|
|
249
|
+
e.preventDefault();
|
|
250
|
+
setBuffer((b) => b + e.key);
|
|
153
251
|
}
|
|
154
252
|
}
|
|
253
|
+
|
|
155
254
|
window.addEventListener('keydown', onKey);
|
|
156
255
|
return () => window.removeEventListener('keydown', onKey);
|
|
157
|
-
}, [isBrowser, decided,
|
|
256
|
+
}, [isBrowser, decided, mode, buffer, submit, toggle, categories]);
|
|
257
|
+
|
|
258
|
+
/* Keep the prompt in view as output accumulates. */
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
if (promptRef.current) promptRef.current.scrollIntoView({block: 'nearest'});
|
|
261
|
+
}, [history, mode]);
|
|
158
262
|
|
|
159
263
|
/* Hide the banner once the user has made a decision; the prompt
|
|
160
264
|
reappears via window.ConductionCookieCli.reset(). */
|
|
@@ -184,7 +288,7 @@ export default function CookieCli({
|
|
|
184
288
|
</div>
|
|
185
289
|
<p className={styles.comment}># Pick what you're OK with. Essential cookies are required.</p>
|
|
186
290
|
|
|
187
|
-
<ul className={styles.opts}>
|
|
291
|
+
<ul className={styles.opts} style={mode === 'game' ? {display: 'none'} : undefined}>
|
|
188
292
|
{categories.map((c, i) => {
|
|
189
293
|
const on = !!selected[c.key] || !!c.required;
|
|
190
294
|
return (
|
|
@@ -206,17 +310,86 @@ export default function CookieCli({
|
|
|
206
310
|
})}
|
|
207
311
|
</ul>
|
|
208
312
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
<
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
313
|
+
{/* Scrollback. Command output is plain text with a tone class; the
|
|
314
|
+
game is the only thing that writes HTML, and it escapes its own
|
|
315
|
+
content in spaceInvaders.js. */}
|
|
316
|
+
{history.length > 0 && (
|
|
317
|
+
<div className={styles.scrollback}>
|
|
318
|
+
{history.map((block, i) => (
|
|
319
|
+
<div key={i} className={styles.outBlock}>
|
|
320
|
+
{block.cmd != null && (
|
|
321
|
+
<div className={styles.prompt}>
|
|
322
|
+
<span className={styles.user}>you</span>
|
|
323
|
+
<span className={styles.sigil}>@</span>
|
|
324
|
+
<span className={styles.host}>{siteHost}</span>
|
|
325
|
+
<span className={styles.sigil}>:</span>
|
|
326
|
+
<span className={styles.path}>~</span>
|
|
327
|
+
<span className={styles.sigil}>$</span>{' '}
|
|
328
|
+
<span className={styles.cmd}>{block.cmd}</span>
|
|
329
|
+
</div>
|
|
330
|
+
)}
|
|
331
|
+
{block.lines.length > 0 && (
|
|
332
|
+
<pre className={styles.out}>
|
|
333
|
+
{block.lines.map((l, j) => (
|
|
334
|
+
<div key={j} className={l.tone ? styles[`tone_${l.tone}`] : undefined}>{l.text}</div>
|
|
335
|
+
))}
|
|
336
|
+
</pre>
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
339
|
+
))}
|
|
340
|
+
</div>
|
|
341
|
+
)}
|
|
342
|
+
|
|
343
|
+
{mode === 'game' ? (
|
|
344
|
+
<div className={styles.game}>
|
|
345
|
+
<div className={styles.gameHud} ref={hudRef} />
|
|
346
|
+
<pre className={styles.gameScreen} ref={screenRef} />
|
|
347
|
+
<div className={styles.gameFooter} ref={footerRef} />
|
|
348
|
+
</div>
|
|
349
|
+
) : (
|
|
350
|
+
<>
|
|
351
|
+
{/* The live prompt. Typing is captured on window rather than in an
|
|
352
|
+
<input>, because the banner is not focused when it appears and
|
|
353
|
+
we do not want to steal focus from the page to make a joke
|
|
354
|
+
work. The hidden input keeps mobile keyboards reachable. */}
|
|
355
|
+
<div
|
|
356
|
+
className={styles.prompt}
|
|
357
|
+
ref={promptRef}
|
|
358
|
+
title="you can type here ;)"
|
|
359
|
+
style={{cursor: 'text'}}
|
|
360
|
+
onClick={() => { if (promptRef.current) promptRef.current.querySelector('input')?.focus(); }}
|
|
361
|
+
>
|
|
362
|
+
<span className={styles.user}>you</span>
|
|
363
|
+
<span className={styles.sigil}>@</span>
|
|
364
|
+
<span className={styles.host}>{siteHost}</span>
|
|
365
|
+
<span className={styles.sigil}>:</span>
|
|
366
|
+
<span className={styles.path}>~</span>
|
|
367
|
+
<span className={styles.sigil}>$</span>{' '}
|
|
368
|
+
<span className={styles.cmd}>{buffer}</span>
|
|
369
|
+
<span className={styles.cursor} aria-hidden="true" />
|
|
370
|
+
<input
|
|
371
|
+
type="text"
|
|
372
|
+
value={buffer}
|
|
373
|
+
onChange={() => {}}
|
|
374
|
+
className={styles.hiddenInput}
|
|
375
|
+
aria-label="Terminal input. Type help for commands."
|
|
376
|
+
tabIndex={-1}
|
|
377
|
+
/>
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<div className={styles.actions}>
|
|
381
|
+
<button type="button" className={[styles.btn, styles.btnPrimary].join(' ')} onClick={acceptAll}>
|
|
382
|
+
<span className={styles.btnKey}>A</span>Accept all
|
|
383
|
+
</button>
|
|
384
|
+
<button type="button" className={styles.btn} onClick={saveCurrent}>
|
|
385
|
+
<span className={styles.btnKey}>S</span>Save selection
|
|
386
|
+
</button>
|
|
387
|
+
<button type="button" className={styles.btn} onClick={rejectNonEssential}>
|
|
388
|
+
<span className={styles.btnKey}>R</span>Reject non-essential
|
|
389
|
+
</button>
|
|
390
|
+
</div>
|
|
391
|
+
</>
|
|
392
|
+
)}
|
|
220
393
|
</div>
|
|
221
394
|
</div>
|
|
222
395
|
);
|
|
@@ -32,6 +32,15 @@
|
|
|
32
32
|
width: calc(100% - 48px);
|
|
33
33
|
max-width: 640px;
|
|
34
34
|
overflow: hidden;
|
|
35
|
+
|
|
36
|
+
/* Cap the panel to the viewport and let the body scroll inside it.
|
|
37
|
+
Without this the panel grows with the shell scrollback and the game
|
|
38
|
+
surface until its title bar is pushed off the top of the screen: the
|
|
39
|
+
consent buttons then sit above the fold with no way to reach them.
|
|
40
|
+
Flex column so the title bar stays put and only .body scrolls. */
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
max-height: calc(100vh - 48px);
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
.bar {
|
|
@@ -52,7 +61,14 @@
|
|
|
52
61
|
.title { flex: 1; text-align: center; }
|
|
53
62
|
.title b { color: var(--term-fg); }
|
|
54
63
|
|
|
55
|
-
|
|
64
|
+
/* min-height:0 is load-bearing: a flex child defaults to min-height:auto,
|
|
65
|
+
which refuses to shrink below its content, so overflow-y would never
|
|
66
|
+
engage and the panel would grow past the viewport anyway. */
|
|
67
|
+
.body {
|
|
68
|
+
padding: 18px 20px;
|
|
69
|
+
overflow-y: auto;
|
|
70
|
+
min-height: 0;
|
|
71
|
+
}
|
|
56
72
|
|
|
57
73
|
.prompt { color: var(--term-fg); margin: 6px 0; }
|
|
58
74
|
.user { color: var(--term-green); }
|
|
@@ -164,3 +180,125 @@
|
|
|
164
180
|
.actions { flex-direction: column; }
|
|
165
181
|
.btn { justify-content: center; }
|
|
166
182
|
}
|
|
183
|
+
|
|
184
|
+
/* ============================================================
|
|
185
|
+
* The shell, and the game hiding in it
|
|
186
|
+
* ============================================================
|
|
187
|
+
* Colours mirror the kit specimen (preview/cookie-cli.html):
|
|
188
|
+
* cobalt UFOs, KNVB-orange lasers, green player.
|
|
189
|
+
*
|
|
190
|
+
* The game paints by writing HTML into a ref, so its class names are
|
|
191
|
+
* plain strings that never pass through the CSS-modules rewriter. They
|
|
192
|
+
* have to be :global() or they would style nothing at all, silently. */
|
|
193
|
+
|
|
194
|
+
.scrollback {
|
|
195
|
+
max-height: 40vh;
|
|
196
|
+
overflow-y: auto;
|
|
197
|
+
margin-bottom: 4px;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.outBlock { margin-bottom: 6px; }
|
|
201
|
+
|
|
202
|
+
.out {
|
|
203
|
+
margin: 4px 0;
|
|
204
|
+
font-family: inherit;
|
|
205
|
+
font-size: 12px;
|
|
206
|
+
line-height: 1.5;
|
|
207
|
+
color: var(--term-fg-dim);
|
|
208
|
+
white-space: pre-wrap;
|
|
209
|
+
background: none;
|
|
210
|
+
border: 0;
|
|
211
|
+
padding: 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.tone_ok { color: var(--term-green); }
|
|
215
|
+
.tone_warn { color: #F7C76F; }
|
|
216
|
+
.tone_cmt { color: var(--term-fg-dim); }
|
|
217
|
+
.tone_dir { color: var(--term-cobalt); }
|
|
218
|
+
|
|
219
|
+
/* Blinking block cursor after the typed text. */
|
|
220
|
+
.cursor {
|
|
221
|
+
display: inline-block;
|
|
222
|
+
width: 8px;
|
|
223
|
+
height: 14px;
|
|
224
|
+
margin-left: 2px;
|
|
225
|
+
vertical-align: text-bottom;
|
|
226
|
+
background: var(--term-green);
|
|
227
|
+
animation: cookieCliBlink 1s steps(2, start) infinite;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
@keyframes cookieCliBlink {
|
|
231
|
+
to { visibility: hidden; }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* Respect a reduced-motion preference: keep the cursor, drop the blink. */
|
|
235
|
+
@media (prefers-reduced-motion: reduce) {
|
|
236
|
+
.cursor { animation: none; }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* Off-screen, not display:none — a hidden input still has to be reachable
|
|
240
|
+
for mobile keyboards, and display:none would remove it from the a11y
|
|
241
|
+
tree entirely. */
|
|
242
|
+
.hiddenInput {
|
|
243
|
+
position: absolute;
|
|
244
|
+
opacity: 0;
|
|
245
|
+
width: 1px;
|
|
246
|
+
height: 1px;
|
|
247
|
+
pointer-events: none;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.game { padding-top: 4px; }
|
|
251
|
+
|
|
252
|
+
.gameHud {
|
|
253
|
+
display: flex;
|
|
254
|
+
gap: 18px;
|
|
255
|
+
align-items: center;
|
|
256
|
+
border-top: 1px dashed #1F2A45;
|
|
257
|
+
border-bottom: 1px dashed #1F2A45;
|
|
258
|
+
padding: 6px 0;
|
|
259
|
+
margin-bottom: 8px;
|
|
260
|
+
font-size: 12px;
|
|
261
|
+
letter-spacing: 0.06em;
|
|
262
|
+
text-transform: uppercase;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.gameHud :global(.hud-label) { color: var(--term-fg-dim); font-size: 10px; }
|
|
266
|
+
.gameHud :global(.hud-val) { color: var(--term-fg); font-weight: 600; }
|
|
267
|
+
|
|
268
|
+
.gameScreen {
|
|
269
|
+
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
|
270
|
+
font-size: 14px;
|
|
271
|
+
line-height: 1;
|
|
272
|
+
letter-spacing: 0.06em;
|
|
273
|
+
white-space: pre;
|
|
274
|
+
padding: 10px 12px;
|
|
275
|
+
margin: 0;
|
|
276
|
+
color: var(--term-fg);
|
|
277
|
+
background: rgba(6, 9, 18, 0.5);
|
|
278
|
+
border: 1px solid #15203A;
|
|
279
|
+
border-radius: 4px;
|
|
280
|
+
overflow: hidden;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.gameFooter {
|
|
284
|
+
margin-top: 8px;
|
|
285
|
+
padding-top: 6px;
|
|
286
|
+
border-top: 1px dashed #1F2A45;
|
|
287
|
+
font-size: 11px;
|
|
288
|
+
color: var(--term-fg-dim);
|
|
289
|
+
text-align: center;
|
|
290
|
+
letter-spacing: 0.04em;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.gameScreen :global(.g-ufo) { color: var(--term-cobalt); text-shadow: 0 0 6px rgba(104, 146, 217, 0.55); }
|
|
294
|
+
.gameScreen :global(.g-player) { color: var(--term-green); text-shadow: 0 0 6px rgba(95, 227, 154, 0.55); font-weight: 600; }
|
|
295
|
+
.gameScreen :global(.g-laser) { color: #F36C21; text-shadow: 0 0 8px rgba(243, 108, 33, 0.95); font-weight: 700; }
|
|
296
|
+
.gameScreen :global(.g-bomb) { color: #FF6B7A; text-shadow: 0 0 6px rgba(255, 107, 122, 0.6); }
|
|
297
|
+
.gameScreen :global(.g-boom) { color: #F7C76F; text-shadow: 0 0 10px rgba(247, 199, 111, 0.85); font-weight: 700; }
|
|
298
|
+
|
|
299
|
+
.gameHud :global(.g-laser) { color: #F36C21; }
|
|
300
|
+
.gameHud :global(.g-player) { color: var(--term-green); }
|
|
301
|
+
|
|
302
|
+
.gameFooter :global(.ok) { color: var(--term-green); }
|
|
303
|
+
.gameFooter :global(.warn) { color: #F7C76F; }
|
|
304
|
+
.gameFooter :global(.cmt) { color: var(--term-fg-dim); }
|