cyborg-hunter 0.9.0 → 0.9.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/CHANGELOG.md +25 -0
- package/CITATION.cff +2 -2
- package/README.md +5 -3
- package/dist/cyborg-hunter-replay.js +2 -2
- package/dist/cyborg-hunter.esm.js +1 -1
- package/dist/cyborg-hunter.min.js +2 -2
- package/dist/extension-cyborg-hunter.js +1 -1
- package/package.json +1 -1
- package/src/cli/preview-entry.js +6 -0
- package/src/cli/renderers/font-face-css.js +16 -0
- package/src/cli/renderers/fonts/FONTS_MANIFEST.json +69 -0
- package/src/cli/renderers/fonts/majormonodisplay/OFL.txt +93 -0
- package/src/cli/renderers/fonts/majormonodisplay/majormonodisplay-400.woff2 +0 -0
- package/src/cli/renderers/fonts/recursive/OFL.txt +93 -0
- package/src/cli/renderers/fonts/recursive/recursive-300-1000.woff2 +0 -0
- package/src/cli/renderers/fonts/sofiasans/OFL.txt +93 -0
- package/src/cli/renderers/fonts/sofiasans/sofiasans-1-1000.woff2 +0 -0
- package/src/cli/renderers/fonts/sora/OFL.txt +93 -0
- package/src/cli/renderers/fonts/sora/sora-100-800.woff2 +0 -0
- package/src/cli/renderers/fonts/spacegrotesk/OFL.txt +93 -0
- package/src/cli/renderers/fonts/spacegrotesk/spacegrotesk-300-700.woff2 +0 -0
- package/src/cli/renderers/fonts/tomorrow/OFL.txt +93 -0
- package/src/cli/renderers/fonts/tomorrow/tomorrow-400.woff2 +0 -0
- package/src/cli/renderers/html-index-core.js +98 -96
- package/src/cli/renderers/html-index.js +7 -2
- package/src/cli/renderers/replay-styles.js +61 -0
- package/src/cli/renderers/replay-viewer.client.js +8 -6
- package/src/cli/renderers/report-fonts.js +24 -0
- package/src/jspsych/extension-cyborg-hunter-replay.js +1 -1
- package/src/jspsych/extension-cyborg-hunter.js +1 -1
- package/src/shared/constants.js +1 -1
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { VERSION, sanitizeId } from '../../shared/constants.js';
|
|
23
23
|
import { decomposeScore } from '../analyzers/triage.js';
|
|
24
24
|
import { resolveScoreWeights, customWeightsText, formatScore } from '../analyzers/score-weights.js';
|
|
25
|
+
import { REPLAY_STYLES_CSS } from './replay-styles.js';
|
|
25
26
|
import { getByPath } from '../../shared/paths.js';
|
|
26
27
|
import { inferTier } from '../../replay/viewer-model.js';
|
|
27
28
|
import { inlineSafeJson, inlineSafeSrc } from '../../shared/inline-safe.js';
|
|
@@ -44,6 +45,10 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
44
45
|
// was hand-rolled in nine places and missing from exactly this one, which is
|
|
45
46
|
// why the rule now lives in one module (T5 Task 10 + its fix round 3).
|
|
46
47
|
const replayClientSrc = inlineSafeSrc(opts.replayClientSrc);
|
|
48
|
+
// @font-face rules for the report's typefaces (report-fonts.js builds them
|
|
49
|
+
// as base64 data URIs for the CLI). Absent → no faces, and every role
|
|
50
|
+
// renders in its fallback stack (the demo bundle and the snapshot tests).
|
|
51
|
+
const fontFaceCss = opts.fontFaceCss ? String(opts.fontFaceCss) + '\n' : '';
|
|
47
52
|
const visualsUnavailableNote = opts.visualsUnavailableNote
|
|
48
53
|
?? 'Visual renderers not available (install the canvas package).';
|
|
49
54
|
const imageSources = opts.imageSources ?? null; // pid → {typingProfile, sessionTimeline, trajectories} data URIs (null entry = omit that img)
|
|
@@ -105,28 +110,41 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
105
110
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
106
111
|
<title>Cyborg Hunter Report</title>
|
|
107
112
|
<style>
|
|
108
|
-
:root {
|
|
109
|
-
--bg: #fafafa; --surface: #fff; --ink: #
|
|
110
|
-
--dim: #
|
|
113
|
+
${fontFaceCss} :root {
|
|
114
|
+
--bg: #fafafa; --surface: #fff; --ink: #0f0f0f;
|
|
115
|
+
--dim: #4f4a40; --line: #b9b2a2;
|
|
111
116
|
--hard: #d32f2f; --soft: #f57c00; --clean: #388e3c;
|
|
117
|
+
/* Typefaces (embedded by report-fonts.js; picked in the 2026-09 report
|
|
118
|
+
palette). Each stack falls back to the system face when a font is
|
|
119
|
+
absent (demo bundle) or lacks a glyph (≥ ≤ ▶ ⓘ). */
|
|
120
|
+
--ff-space: "Space Grotesk", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
121
|
+
--ff-tomorrow: "Tomorrow", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
122
|
+
--ff-sofia: "Sofia Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
123
|
+
--ff-sora: "Sora", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
124
|
+
--ff-recursive: "Recursive", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
125
|
+
--ff-majormono: "Major Mono Display", ui-monospace, SFMono-Regular, monospace;
|
|
112
126
|
}
|
|
113
127
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
114
128
|
html, body { height: 100%; }
|
|
115
129
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
116
130
|
color: var(--ink); background: var(--bg); display: grid; grid-template-rows: auto 1fr; height: 100vh; }
|
|
117
131
|
.topbar { display: flex; align-items: center; gap: 16px; padding: 12px 20px;
|
|
118
|
-
background: var(--surface); border-bottom:
|
|
119
|
-
.topbar h1 { font-size:
|
|
120
|
-
.topbar .meta { color: var(--dim); font-size: 13px; flex: 1; }
|
|
132
|
+
background: var(--surface); border-bottom: 3px double var(--ink); }
|
|
133
|
+
.topbar h1 { font-family: var(--ff-space); font-size: 32.4px; font-weight: 550; letter-spacing: 0.01em; }
|
|
134
|
+
.topbar .meta { font-family: var(--ff-tomorrow); color: var(--dim); font-size: 13px; flex: 1; }
|
|
121
135
|
.topbar button { padding: 6px 12px; border: 1px solid var(--line); background: var(--surface);
|
|
122
|
-
color: var(--ink); border-radius: 4px; cursor: pointer;
|
|
136
|
+
color: var(--ink); border-radius: 4px; cursor: pointer;
|
|
137
|
+
font-family: var(--ff-recursive); font-size: 19.5px; font-weight: 400; letter-spacing: 0; }
|
|
123
138
|
.topbar button:hover { background: var(--bg); }
|
|
124
139
|
.layout { display: grid; grid-template-columns: 360px 1fr; height: 100%; overflow: hidden; }
|
|
125
140
|
.rail { background: var(--surface); border-right: 1px solid var(--line); overflow-y: auto; }
|
|
126
141
|
.detail { overflow-y: auto; padding: 20px; }
|
|
127
|
-
.note { color: var(--dim); font-style: italic; font-size: 13px; }
|
|
142
|
+
.note { font-family: var(--ff-tomorrow); color: var(--dim); font-style: italic; font-size: 13px; }
|
|
143
|
+
/* .mono marks data values (ids, scores, counts, timestamps). It was a
|
|
144
|
+
system monospace; it now defaults to Sora with tabular figures, and the
|
|
145
|
+
roles below override it (timestamps, evidence text). */
|
|
128
146
|
.mono {
|
|
129
|
-
font: 13px/1.3
|
|
147
|
+
font: 13px/1.3 var(--ff-sora);
|
|
130
148
|
font-variant-numeric: tabular-nums;
|
|
131
149
|
}
|
|
132
150
|
[hidden] { display: none !important; }
|
|
@@ -142,34 +160,43 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
142
160
|
padding: 10px 12px; border-top: 1px solid var(--line);
|
|
143
161
|
background: var(--surface); font-size: 12px;
|
|
144
162
|
}
|
|
145
|
-
.totals-title { color: var(--dim); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 4px; }
|
|
146
|
-
.totals-row { display: flex; align-items: center; gap: 6px; padding: 2px 0; }
|
|
147
|
-
.totals-row .count { margin-left: auto; }
|
|
163
|
+
.totals-title { font-family: var(--ff-sora); color: var(--dim); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 4px; }
|
|
164
|
+
.totals-row { font-family: var(--ff-sora); display: flex; align-items: center; gap: 6px; padding: 2px 0; }
|
|
165
|
+
.totals-row .count { font-family: var(--ff-sora); margin-left: auto; }
|
|
148
166
|
|
|
149
167
|
/* Search */
|
|
150
168
|
.search-wrap input {
|
|
151
169
|
width: 100%; padding: 6px 8px; border: 1px solid var(--line);
|
|
152
|
-
border-radius: 4px;
|
|
170
|
+
border-radius: 4px; background: var(--bg);
|
|
153
171
|
color: var(--ink);
|
|
172
|
+
font-family: var(--ff-recursive); font-size: 19.5px; font-weight: 400; letter-spacing: 0;
|
|
154
173
|
}
|
|
155
174
|
.search-wrap input:focus { outline: 1px solid var(--ink); border-color: var(--ink); }
|
|
156
175
|
|
|
157
|
-
/* Filter chips
|
|
158
|
-
|
|
176
|
+
/* Filter chips — one segmented control: joined buttons, square inner
|
|
177
|
+
edges, outer corners 4px. 15px with 4px 8px padding keeps four 3-digit
|
|
178
|
+
counts on one line in the 360px rail (measured 2026-09-30). */
|
|
179
|
+
.filter-chips { display: flex; flex-wrap: wrap; gap: 0; }
|
|
159
180
|
.filter-chip {
|
|
160
|
-
padding:
|
|
161
|
-
color: var(--ink); border-radius:
|
|
181
|
+
padding: 4px 8px; border: 1px solid var(--line); background: var(--surface);
|
|
182
|
+
color: var(--ink); border-radius: 0; margin-left: -1px; cursor: pointer;
|
|
183
|
+
font-family: var(--ff-recursive); font-size: 15px; font-weight: 400; letter-spacing: 0;
|
|
162
184
|
}
|
|
185
|
+
.filter-chip:first-child { border-radius: 4px 0 0 4px; margin-left: 0; }
|
|
186
|
+
.filter-chip:last-child { border-radius: 0 4px 4px 0; }
|
|
163
187
|
.filter-chip:hover { background: var(--bg); }
|
|
188
|
+
/* Raised so the next chip's -1px overlap can't paint over its ink edge. */
|
|
164
189
|
.filter-chip.active {
|
|
165
190
|
background: var(--ink); color: var(--surface); border-color: var(--ink);
|
|
191
|
+
position: relative; z-index: 1;
|
|
166
192
|
}
|
|
167
193
|
|
|
168
194
|
/* Sort selector */
|
|
169
|
-
.sort-wrap { font-size: 12px; color: var(--dim); }
|
|
195
|
+
.sort-wrap { font-family: var(--ff-tomorrow); font-size: 12px; color: var(--dim); }
|
|
170
196
|
.sort-wrap select {
|
|
171
197
|
background: var(--surface); color: var(--ink); border: 1px solid var(--line);
|
|
172
|
-
border-radius: 4px; padding: 2px 4px;
|
|
198
|
+
border-radius: 4px; padding: 2px 4px;
|
|
199
|
+
font-family: var(--ff-recursive); font-size: 18px; font-weight: 400; letter-spacing: 0;
|
|
173
200
|
}
|
|
174
201
|
|
|
175
202
|
/* Cohort rows */
|
|
@@ -178,23 +205,27 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
178
205
|
cursor: pointer;
|
|
179
206
|
}
|
|
180
207
|
.cohort-row:hover { background: var(--bg); }
|
|
208
|
+
/* Selected row: a 2px inset ink outline (no layout shift, row tone kept). */
|
|
181
209
|
.cohort-row.selected {
|
|
182
|
-
border-left:
|
|
183
|
-
padding-left:
|
|
184
|
-
background:
|
|
210
|
+
border-left: 0;
|
|
211
|
+
padding-left: 12px;
|
|
212
|
+
background: transparent;
|
|
213
|
+
box-shadow: inset 0 0 0 2px var(--ink);
|
|
185
214
|
}
|
|
186
215
|
.cohort-row-top { display: flex; align-items: center; gap: 8px; }
|
|
187
216
|
.cohort-row-top .pid {
|
|
217
|
+
font-family: var(--ff-sora);
|
|
188
218
|
flex: 1; min-width: 0; color: var(--ink);
|
|
189
219
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
190
220
|
}
|
|
191
|
-
.cohort-row-top .score { color: var(--dim); flex-shrink: 0; }
|
|
221
|
+
.cohort-row-top .score { font-family: var(--ff-sora); color: var(--dim); flex-shrink: 0; }
|
|
192
222
|
.cohort-row.selected .cohort-row-top .score { color: var(--ink); font-weight: 600; }
|
|
193
223
|
.cohort-row-bot {
|
|
194
224
|
display: flex; align-items: flex-start; gap: 8px;
|
|
195
225
|
margin-top: 4px;
|
|
196
226
|
}
|
|
197
227
|
.reason-excerpt {
|
|
228
|
+
font-family: var(--ff-recursive);
|
|
198
229
|
flex: 1; color: var(--dim); font-size: 12px;
|
|
199
230
|
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
|
|
200
231
|
overflow: hidden; text-overflow: ellipsis;
|
|
@@ -209,9 +240,10 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
209
240
|
.tier-dot[data-tier="soft"] { background: var(--soft); }
|
|
210
241
|
.tier-dot[data-tier="clean"] { background: var(--clean); }
|
|
211
242
|
|
|
212
|
-
/* Tier badge — small
|
|
243
|
+
/* Tier badge — small square tag, filled for hard/soft, outline for clean */
|
|
213
244
|
.tier-badge {
|
|
214
|
-
font-
|
|
245
|
+
font-family: var(--ff-sora);
|
|
246
|
+
font-size: 10px; padding: 2px 6px; border-radius: 0;
|
|
215
247
|
font-weight: 600; letter-spacing: 0.04em; flex-shrink: 0;
|
|
216
248
|
}
|
|
217
249
|
.tier-badge[data-tier="hard"] { background: var(--hard); color: white; }
|
|
@@ -245,17 +277,21 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
245
277
|
|
|
246
278
|
/* Header strip */
|
|
247
279
|
.detail-header {
|
|
248
|
-
border-bottom: 1px solid var(--
|
|
280
|
+
border-bottom: 1px solid var(--ink);
|
|
249
281
|
padding-bottom: 12px; margin-bottom: 16px;
|
|
250
282
|
}
|
|
251
283
|
.detail-header-top { display: flex; align-items: center; gap: 12px; }
|
|
252
|
-
.detail-header-top .pid-full { flex: 1; font-size: 13px; color: var(--ink); }
|
|
253
|
-
.detail-header-top .score-big { font-size: 36px; font-weight: 600; color: var(--ink); }
|
|
254
|
-
.detail-header-sub { color: var(--dim); font-size: 12px; margin-top: 4px; }
|
|
255
|
-
|
|
256
|
-
|
|
284
|
+
.detail-header-top .pid-full { font-family: var(--ff-sora); flex: 1; font-size: 13px; color: var(--ink); }
|
|
285
|
+
.detail-header-top .score-big { font-family: var(--ff-sora); font-size: 36px; font-weight: 600; color: var(--ink); }
|
|
286
|
+
.detail-header-sub { font-family: var(--ff-tomorrow); color: var(--dim); font-size: 12px; margin-top: 4px; }
|
|
287
|
+
/* The optional platform-ID line is an identifier, so it stays in the ID
|
|
288
|
+
face rather than taking the meta-line face by source order. */
|
|
289
|
+
.detail-header-sub.mono { font-family: var(--ff-sora); }
|
|
290
|
+
|
|
291
|
+
/* Tier pill — bigger version of the tier badge in the rail (square tag) */
|
|
257
292
|
.tier-pill {
|
|
258
|
-
font-
|
|
293
|
+
font-family: var(--ff-sora);
|
|
294
|
+
font-size: 11px; padding: 3px 8px; border-radius: 0;
|
|
259
295
|
font-weight: 600; letter-spacing: 0.04em;
|
|
260
296
|
}
|
|
261
297
|
.tier-pill[data-tier="hard"] { background: var(--hard); color: white; }
|
|
@@ -276,7 +312,7 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
276
312
|
}
|
|
277
313
|
.signal-tile {
|
|
278
314
|
border: 1px solid var(--line);
|
|
279
|
-
border-radius:
|
|
315
|
+
border-radius: 0;
|
|
280
316
|
padding: 8px 10px;
|
|
281
317
|
display: flex;
|
|
282
318
|
flex-direction: column;
|
|
@@ -290,8 +326,8 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
290
326
|
.signal-tile.tone-warn { color: var(--soft); border-color: var(--soft); background: rgba(245, 124, 0, 0.04); }
|
|
291
327
|
.signal-tile.tone-muted { color: var(--ink); border-color: var(--ink); background: rgba(26, 24, 20, 0.03); }
|
|
292
328
|
.signal-tile.tone-zero { color: #b8b1a0; border-color: #ebe6d8; background: transparent; }
|
|
293
|
-
.signal-value { font-size: 18px; font-weight: 700; line-height: 1; }
|
|
294
|
-
.signal-label { font-size: 10px; letter-spacing: 0.4px; text-transform: uppercase; opacity: 0.85; }
|
|
329
|
+
.signal-value { font-family: var(--ff-sora); font-size: 18px; font-weight: 700; line-height: 1; }
|
|
330
|
+
.signal-label { font-family: var(--ff-recursive); font-size: 10px; letter-spacing: 0.4px; text-transform: uppercase; opacity: 0.85; }
|
|
295
331
|
|
|
296
332
|
/* Score breakdown — horizontal flex of weighted contributions to t.score,
|
|
297
333
|
ending in "Total: N". Only non-zero terms render; the bar widths are
|
|
@@ -304,21 +340,22 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
304
340
|
display: inline-flex; gap: 6px; align-items: center;
|
|
305
341
|
font-size: 12px;
|
|
306
342
|
}
|
|
307
|
-
.score-term .label { color: var(--dim); }
|
|
308
|
-
.score-term .contrib { color: var(--ink); }
|
|
343
|
+
.score-term .label { font-family: var(--ff-recursive); color: var(--dim); }
|
|
344
|
+
.score-term .contrib { font-family: var(--ff-sora); color: var(--ink); }
|
|
309
345
|
.score-term .bar {
|
|
310
|
-
height: 6px; background: var(--ink); opacity: 0.4; border-radius:
|
|
346
|
+
height: 6px; background: var(--ink); opacity: 0.4; border-radius: 2px;
|
|
311
347
|
}
|
|
312
|
-
.score-total { margin-left: auto; font-weight: 600; color: var(--ink); }
|
|
348
|
+
.score-total { font-family: var(--ff-sora); margin-left: auto; font-weight: 600; color: var(--ink); }
|
|
313
349
|
|
|
314
|
-
/* Reason
|
|
315
|
-
|
|
316
|
-
it shifts cleanly when the palette changes. */
|
|
350
|
+
/* Reason note — a hairline box on the surface (the earlier warm tint and
|
|
351
|
+
3px left rule were dropped in the 2026-09 reshape). */
|
|
317
352
|
.reason {
|
|
318
|
-
|
|
319
|
-
|
|
353
|
+
background: var(--surface);
|
|
354
|
+
border: 1px solid var(--line);
|
|
355
|
+
border-radius: 4px;
|
|
320
356
|
padding: 10px 14px;
|
|
321
357
|
margin: 0 0 16px;
|
|
358
|
+
font-family: var(--ff-tomorrow);
|
|
322
359
|
font-size: 14px;
|
|
323
360
|
color: var(--ink);
|
|
324
361
|
}
|
|
@@ -326,6 +363,7 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
326
363
|
/* Session-level signals — small cards in a responsive grid. The grid
|
|
327
364
|
collapses from 3 cells to 1 in narrow detail panes via auto-fit. */
|
|
328
365
|
.section-heading {
|
|
366
|
+
font-family: var(--ff-sofia);
|
|
329
367
|
font-size: 13px; color: var(--dim);
|
|
330
368
|
text-transform: uppercase; letter-spacing: 0.04em;
|
|
331
369
|
margin: 18px 0 8px;
|
|
@@ -337,14 +375,16 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
337
375
|
}
|
|
338
376
|
.sig-cell {
|
|
339
377
|
background: var(--surface); border: 1px solid var(--line);
|
|
340
|
-
border-radius:
|
|
378
|
+
border-radius: 0; padding: 10px;
|
|
379
|
+
box-shadow: 0 1px 2px rgba(0,0,0,0.06), 0 2px 8px rgba(0,0,0,0.05);
|
|
341
380
|
}
|
|
342
381
|
.sig-cell-title {
|
|
382
|
+
font-family: var(--ff-sofia);
|
|
343
383
|
font-size: 11px; color: var(--dim);
|
|
344
384
|
text-transform: uppercase; letter-spacing: 0.04em;
|
|
345
385
|
}
|
|
346
386
|
.sig-cell ul { list-style: none; margin-top: 4px; font-size: 12px; padding: 0; }
|
|
347
|
-
.sig-cell li { padding: 2px 0; word-break: break-word; }
|
|
387
|
+
.sig-cell li { font-family: var(--ff-recursive); padding: 2px 0; word-break: break-word; }
|
|
348
388
|
.muted { color: var(--dim); }
|
|
349
389
|
|
|
350
390
|
/* Paste evidence — list of expandable entries */
|
|
@@ -359,10 +399,11 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
359
399
|
}
|
|
360
400
|
.paste-toggle:disabled { cursor: default; }
|
|
361
401
|
.paste-toggle:hover:not(:disabled) { color: var(--ink); }
|
|
362
|
-
.paste-trial { color: var(--dim); flex-shrink: 0; }
|
|
402
|
+
.paste-trial { font-family: var(--ff-majormono); color: var(--dim); flex-shrink: 0; }
|
|
363
403
|
.paste-preview, .paste-full {
|
|
364
|
-
background:
|
|
365
|
-
padding: 4px 8px; border-radius:
|
|
404
|
+
background: var(--surface); border: 1px solid var(--line); /* hairline box, like .reason */
|
|
405
|
+
padding: 4px 8px; border-radius: 0;
|
|
406
|
+
font-family: var(--ff-tomorrow);
|
|
366
407
|
font-size: 12px;
|
|
367
408
|
word-break: break-word; white-space: pre-wrap;
|
|
368
409
|
flex: 1; min-width: 0; /* allow flex item to shrink and wrap correctly */
|
|
@@ -370,7 +411,7 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
370
411
|
/* When expanded by Task 11's JS: hide preview, show full text */
|
|
371
412
|
.paste-entry.expanded .paste-preview { display: none; }
|
|
372
413
|
.paste-entry:not(.expanded) .paste-full { display: none; }
|
|
373
|
-
.paste-overflow { font-size: 12px; margin-top: 4px; }
|
|
414
|
+
.paste-overflow { font-family: var(--ff-tomorrow); font-size: 12px; margin-top: 4px; }
|
|
374
415
|
|
|
375
416
|
/* Image blocks — wrapper containers so a missing PNG hides its heading too.
|
|
376
417
|
Each .image-block contains a <h4 class="section-heading"> + a zoomable
|
|
@@ -383,7 +424,8 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
383
424
|
.image-block .section-heading { margin: 0 0 6px; }
|
|
384
425
|
.detail img {
|
|
385
426
|
max-width: 100%; display: block; margin: 6px 0 0;
|
|
386
|
-
border: 1px solid var(--line); border-radius:
|
|
427
|
+
border: 1px solid var(--line); border-radius: 0;
|
|
428
|
+
box-shadow: 0 1px 2px rgba(0,0,0,0.06), 0 2px 8px rgba(0,0,0,0.05);
|
|
387
429
|
}
|
|
388
430
|
.zoomable {
|
|
389
431
|
display: block; cursor: zoom-in;
|
|
@@ -423,7 +465,7 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
423
465
|
max-width: 560px; max-height: calc(100vh - 16vh);
|
|
424
466
|
margin: 8vh auto 0;
|
|
425
467
|
padding: 20px 24px;
|
|
426
|
-
border-radius:
|
|
468
|
+
border-radius: 0;
|
|
427
469
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
|
|
428
470
|
overflow-y: auto;
|
|
429
471
|
}
|
|
@@ -436,6 +478,7 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
436
478
|
}
|
|
437
479
|
.modal-close:hover { color: var(--ink); }
|
|
438
480
|
.modal-card h3 {
|
|
481
|
+
font-family: var(--ff-space);
|
|
439
482
|
font-size: 16px; margin-bottom: 12px;
|
|
440
483
|
}
|
|
441
484
|
.legend-table {
|
|
@@ -447,7 +490,9 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
447
490
|
border-bottom: 1px solid var(--line);
|
|
448
491
|
vertical-align: top;
|
|
449
492
|
}
|
|
493
|
+
.legend-table td { font-family: var(--ff-recursive); }
|
|
450
494
|
.legend-table th {
|
|
495
|
+
font-family: var(--ff-sofia);
|
|
451
496
|
color: var(--dim); font-weight: 500; font-size: 12px;
|
|
452
497
|
text-transform: uppercase; letter-spacing: 0.04em;
|
|
453
498
|
}
|
|
@@ -729,50 +774,7 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
729
774
|
})();
|
|
730
775
|
</script>
|
|
731
776
|
<style>
|
|
732
|
-
|
|
733
|
-
.replay-header { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 8px 0; }
|
|
734
|
-
.replay-badge { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 10px;
|
|
735
|
-
background: var(--ink); color: var(--surface); }
|
|
736
|
-
.replay-badge[data-tier="trace"] { background: var(--surface); color: var(--ink);
|
|
737
|
-
border: 1px solid var(--line); }
|
|
738
|
-
.replay-stage { position: relative; overflow: hidden; background: var(--surface);
|
|
739
|
-
border: 1px solid var(--line); border-radius: 4px; }
|
|
740
|
-
.replay-frame { position: absolute; top: 0; left: 0; border: 0; }
|
|
741
|
-
.replay-overlay { position: absolute; top: 0; left: 0; pointer-events: none; }
|
|
742
|
-
.replay-neutral { background: #e8e6e0; }
|
|
743
|
-
.replay-neutral-label { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
|
|
744
|
-
color: var(--dim); font-size: 13px; }
|
|
745
|
-
.replay-lane { display: block; margin-top: 6px; border-radius: 2px; }
|
|
746
|
-
.replay-scrub { display: block; margin: 2px 0 4px; }
|
|
747
|
-
.replay-ticker { font: 12px/1.3 ui-monospace, SFMono-Regular, 'IBM Plex Mono', monospace;
|
|
748
|
-
color: var(--dim); font-variant-numeric: tabular-nums;
|
|
749
|
-
height: 1.4em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
750
|
-
.replay-note { font-size: 12px; color: var(--dim); }
|
|
751
|
-
.replay-warn { color: var(--hard); }
|
|
752
|
-
/* Controls share the report's flat, line-bordered button style */
|
|
753
|
-
.replay-play, .replay-load-btn, .replay-css-btn, .replay-segment-select, .replay-speed {
|
|
754
|
-
padding: 4px 10px; border: 1px solid var(--line); background: var(--surface);
|
|
755
|
-
color: var(--ink); border-radius: 4px; cursor: pointer; font-size: 13px; }
|
|
756
|
-
.replay-play:hover, .replay-load-btn:hover, .replay-css-btn:hover { background: var(--bg); }
|
|
757
|
-
.replay-play:disabled, .replay-load-btn:disabled { opacity: 0.6; cursor: default; }
|
|
758
|
-
.replay-fetch-css-label { margin-left: 10px; font-size: 12px; color: #555; }
|
|
759
|
-
.replay-unstyled { position: absolute; left: 0; right: 0; top: 0; z-index: 3; background: #fff4dd; color: #7a4b00; border-bottom: 1px solid #e8b24a; padding: 6px 10px; font-size: 12px; line-height: 1.4; }
|
|
760
|
-
.replay-css-btn { font-size: 12px; padding: 2px 8px; }
|
|
761
|
-
.replay-clock { font: 12px/1.3 ui-monospace, SFMono-Regular, 'IBM Plex Mono', monospace;
|
|
762
|
-
font-variant-numeric: tabular-nums; }
|
|
763
|
-
.replay-keycast { position: absolute; left: 0; right: 0; bottom: 0; display: flex;
|
|
764
|
-
gap: 4px; padding: 5px 6px; pointer-events: none; flex-wrap: wrap-reverse; }
|
|
765
|
-
.replay-key-chip { font: 11px/1.2 ui-monospace, SFMono-Regular, 'IBM Plex Mono', monospace;
|
|
766
|
-
background: rgba(0,0,0,0.72); color: #fff; padding: 2px 7px;
|
|
767
|
-
border-radius: 3px; white-space: nowrap; }
|
|
768
|
-
.replay-key-chip--redacted { background: rgba(0,0,0,0.5); font-style: italic; }
|
|
769
|
-
/* Media is reported as state, never played (design §7): the badges say what
|
|
770
|
-
the recorded element was doing, in the stage's top-left. */
|
|
771
|
-
.replay-media { position: absolute; left: 0; top: 0; display: flex; gap: 4px;
|
|
772
|
-
padding: 5px 6px; pointer-events: none; flex-wrap: wrap; }
|
|
773
|
-
.replay-media-badge { font: 11px/1.2 ui-monospace, SFMono-Regular, 'IBM Plex Mono', monospace;
|
|
774
|
-
background: rgba(0,137,123,0.85); color: #fff; padding: 2px 7px;
|
|
775
|
-
border-radius: 3px; white-space: nowrap; }
|
|
777
|
+
${REPLAY_STYLES_CSS}
|
|
776
778
|
</style>
|
|
777
779
|
${preloadedReplayScript}<script>
|
|
778
780
|
${replayClientSrc}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// src/cli/renderers/html-index.js
|
|
2
2
|
// Thin Node wrapper around the pure render core (html-index-core.js):
|
|
3
|
-
// reads the replay viewer client from disk, writes
|
|
3
|
+
// reads the replay viewer client and the embedded fonts from disk, writes
|
|
4
|
+
// index.html to outputDir.
|
|
4
5
|
// Public API unchanged — report.js and adopters keep calling renderHtmlIndex.
|
|
5
6
|
import { writeFileSync } from 'fs';
|
|
6
7
|
import { join } from 'path';
|
|
7
8
|
import { renderIndexHtml } from './html-index-core.js';
|
|
8
9
|
import { readReplayClientSrc } from './replay-client-source.js';
|
|
10
|
+
import { buildFontFaceCss } from './report-fonts.js';
|
|
9
11
|
|
|
10
12
|
// The replay viewer client is developed as real JS files (linted, syntax-
|
|
11
13
|
// highlighted) and embedded verbatim at render time — same file://-safe
|
|
@@ -13,10 +15,13 @@ import { readReplayClientSrc } from './replay-client-source.js';
|
|
|
13
15
|
// CONCATENATION: the client calls into src/replay/dom-instantiate.js, which
|
|
14
16
|
// ships ahead of it in the same script (see replay-client-source.js).
|
|
15
17
|
const REPLAY_CLIENT_SRC = readReplayClientSrc();
|
|
18
|
+
// The report's six typefaces as base64 @font-face rules (report-fonts.js),
|
|
19
|
+
// built once per process like the replay client.
|
|
20
|
+
const FONT_FACE_CSS = buildFontFaceCss();
|
|
16
21
|
|
|
17
22
|
export async function renderHtmlIndex(summaries, triage, participants, config, visualsRendered) {
|
|
18
23
|
const html = await renderIndexHtml(summaries, triage, participants, config,
|
|
19
|
-
visualsRendered, { replayClientSrc: REPLAY_CLIENT_SRC });
|
|
24
|
+
visualsRendered, { replayClientSrc: REPLAY_CLIENT_SRC, fontFaceCss: FONT_FACE_CSS });
|
|
20
25
|
writeFileSync(join(config.outputDir, 'index.html'), html);
|
|
21
26
|
console.log(' index.html — report page');
|
|
22
27
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// src/cli/renderers/replay-styles.js
|
|
2
|
+
// The replay viewer's CSS (.replay-* rules), shared by the CLI report
|
|
3
|
+
// (html-index-core.js puts it in the report's second <style>) and the demo's
|
|
4
|
+
// replay-host iframe (demo/replay-host.js, which receives it from results.js
|
|
5
|
+
// through the preview-core bundle). One copy: the demo used to keep a
|
|
6
|
+
// hand-synced duplicate, which had drifted. Rules reference the report's
|
|
7
|
+
// tokens (--ink, --surface, --line, --bg, --dim, --hard and the --ff-* font
|
|
8
|
+
// stacks); a host document must declare them.
|
|
9
|
+
|
|
10
|
+
export const REPLAY_STYLES_CSS = ` /* Replay viewer (see replay-viewer.client.js) — matches report house style */
|
|
11
|
+
.replay-header { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 8px 0; }
|
|
12
|
+
.replay-badge { font-family: var(--ff-sora); font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 0;
|
|
13
|
+
background: var(--ink); color: var(--surface); }
|
|
14
|
+
.replay-badge[data-tier="trace"] { background: var(--surface); color: var(--ink);
|
|
15
|
+
border: 1px solid var(--line); }
|
|
16
|
+
.replay-stage { position: relative; overflow: hidden; background: var(--surface);
|
|
17
|
+
border: 1px solid var(--line); border-radius: 0; }
|
|
18
|
+
.replay-frame { position: absolute; top: 0; left: 0; border: 0; }
|
|
19
|
+
.replay-overlay { position: absolute; top: 0; left: 0; pointer-events: none; }
|
|
20
|
+
.replay-neutral { background: #e8e6e0; }
|
|
21
|
+
.replay-neutral-label { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
|
|
22
|
+
font-family: var(--ff-recursive); color: var(--dim); font-size: 13px; }
|
|
23
|
+
.replay-lane { display: block; margin-top: 6px; border-radius: 2px; }
|
|
24
|
+
.replay-scrub { display: block; margin: 2px 0 4px; }
|
|
25
|
+
.replay-ticker { font: 12px/1.3 var(--ff-majormono);
|
|
26
|
+
color: var(--dim); font-variant-numeric: tabular-nums;
|
|
27
|
+
height: 1.4em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
28
|
+
.replay-note { font-family: var(--ff-recursive); font-size: 12px; color: var(--dim); }
|
|
29
|
+
.replay-warn { color: var(--hard); }
|
|
30
|
+
.replay-pause-toggle { font-family: var(--ff-recursive); }
|
|
31
|
+
/* Controls share the report's flat, line-bordered button style */
|
|
32
|
+
.replay-play, .replay-load-btn, .replay-css-btn, .replay-segment-select, .replay-speed {
|
|
33
|
+
padding: 4px 10px; border: 1px solid var(--line); background: var(--surface);
|
|
34
|
+
color: var(--ink); border-radius: 4px; cursor: pointer;
|
|
35
|
+
font-family: var(--ff-recursive); font-size: 13px; }
|
|
36
|
+
/* Play is the transport's primary action: a round ink button. Its hover
|
|
37
|
+
is its own (darker ink); the light shared hover below would leave a
|
|
38
|
+
white ▶ on a light ground. */
|
|
39
|
+
.replay-play { background: var(--ink); color: var(--surface); border-color: var(--ink);
|
|
40
|
+
width: 36px; height: 30px; padding: 0; border-radius: 50%; }
|
|
41
|
+
.replay-play:hover { background: #2b2b2b; }
|
|
42
|
+
.replay-load-btn:hover, .replay-css-btn:hover { background: var(--bg); }
|
|
43
|
+
.replay-play:disabled, .replay-load-btn:disabled { opacity: 0.6; cursor: default; }
|
|
44
|
+
.replay-fetch-css-label { font-family: var(--ff-recursive); margin-left: 10px; font-size: 12px; color: #555; }
|
|
45
|
+
.replay-unstyled { position: absolute; left: 0; right: 0; top: 0; z-index: 3; background: #fff4dd; color: #7a4b00; border-bottom: 1px solid #e8b24a; padding: 6px 10px; font-family: var(--ff-recursive); font-size: 12px; line-height: 1.4; }
|
|
46
|
+
.replay-css-btn { font-size: 12px; padding: 2px 8px; }
|
|
47
|
+
.replay-clock { font: 12px/1.3 var(--ff-majormono);
|
|
48
|
+
font-variant-numeric: tabular-nums; }
|
|
49
|
+
.replay-keycast { position: absolute; left: 0; right: 0; bottom: 0; display: flex;
|
|
50
|
+
gap: 4px; padding: 5px 6px; pointer-events: none; flex-wrap: wrap-reverse; }
|
|
51
|
+
.replay-key-chip { font: 400 13px/1.2 var(--ff-tomorrow);
|
|
52
|
+
background: rgba(0,0,0,0.72); color: #fff; padding: 3px 9px;
|
|
53
|
+
border-radius: 2px; white-space: nowrap; }
|
|
54
|
+
.replay-key-chip--redacted { background: rgba(0,0,0,0.5); font-style: italic; }
|
|
55
|
+
/* Media is reported as state, never played (design §7): the badges say what
|
|
56
|
+
the recorded element was doing, in the stage's top-left. */
|
|
57
|
+
.replay-media { position: absolute; left: 0; top: 0; display: flex; gap: 4px;
|
|
58
|
+
padding: 5px 6px; pointer-events: none; flex-wrap: wrap; }
|
|
59
|
+
.replay-media-badge { font: 11px/1.2 var(--ff-tomorrow);
|
|
60
|
+
background: rgba(0,137,123,0.85); color: #fff; padding: 2px 7px;
|
|
61
|
+
border-radius: 3px; white-space: nowrap; }`;
|
|
@@ -1881,16 +1881,18 @@
|
|
|
1881
1881
|
if (e.t >= playhead - 600) ripples.push(q);
|
|
1882
1882
|
}
|
|
1883
1883
|
}
|
|
1884
|
-
// Cursor trail:
|
|
1884
|
+
// Cursor trail: breadcrumb dots, one per sampled point (after a
|
|
1885
|
+
// segment's first), fading with age. Gaps between dots show pointer
|
|
1886
|
+
// speed; a severed segment simply has no dot bridging the break. Drawn
|
|
1887
|
+
// FIRST, and never at radius 2, 5 or 7: the alignment battery reads the
|
|
1888
|
+
// cursor glyph off the final arcs (r=5 confident, r=7 then r=2 uncertain).
|
|
1885
1889
|
segs.forEach(function (line) {
|
|
1886
1890
|
for (var s = 1; s < line.length; s++) {
|
|
1887
1891
|
var age = (playhead - line[s].t) / 2500;
|
|
1888
|
-
ctx.
|
|
1889
|
-
ctx.lineWidth = 2;
|
|
1892
|
+
ctx.fillStyle = 'rgba(211,47,47,' + (0.85 * (1 - age)).toFixed(3) + ')';
|
|
1890
1893
|
ctx.beginPath();
|
|
1891
|
-
ctx.
|
|
1892
|
-
ctx.
|
|
1893
|
-
ctx.stroke();
|
|
1894
|
+
ctx.arc(line[s].x, line[s].y, 2.2, 0, Math.PI * 2);
|
|
1895
|
+
ctx.fill();
|
|
1894
1896
|
}
|
|
1895
1897
|
});
|
|
1896
1898
|
// Click ripples: expanding rings over 600ms
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// src/cli/renderers/report-fonts.js
|
|
2
|
+
// The report's typefaces, embedded so the report stays a single offline file.
|
|
3
|
+
// Reads the committed WOFF2 files listed in fonts/FONTS_MANIFEST.json and
|
|
4
|
+
// returns one @font-face block of base64 data URIs (≈227 KB for the six
|
|
5
|
+
// families). html-index.js passes it to renderIndexHtml as opts.fontFaceCss,
|
|
6
|
+
// the same way it passes the replay viewer source, so html-index-core.js
|
|
7
|
+
// stays pure (no fs) and can be bundled for the browser demo.
|
|
8
|
+
//
|
|
9
|
+
// Node only: keep this module out of anything src/cli/preview-entry.js
|
|
10
|
+
// imports (npm run demo:preview fails the build if it leaks in).
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from 'fs';
|
|
13
|
+
import { join, dirname } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
import { fontFaceCss } from './font-face-css.js';
|
|
16
|
+
|
|
17
|
+
export const FONTS_DIR = join(dirname(fileURLToPath(import.meta.url)), 'fonts');
|
|
18
|
+
|
|
19
|
+
// The rule format lives in font-face-css.js (shared with the browser demo);
|
|
20
|
+
// this only supplies the bytes from disk.
|
|
21
|
+
export function buildFontFaceCss(dir = FONTS_DIR) {
|
|
22
|
+
const manifest = JSON.parse(readFileSync(join(dir, 'FONTS_MANIFEST.json'), 'utf8'));
|
|
23
|
+
return fontFaceCss(manifest.files, p => readFileSync(join(dir, p)).toString('base64'));
|
|
24
|
+
}
|
|
@@ -35,7 +35,7 @@ import { buildReplayMeta } from '../replay/persistence.js';
|
|
|
35
35
|
class CyborgHunterReplayExtension {
|
|
36
36
|
static info = {
|
|
37
37
|
name: 'cyborg-hunter-replay',
|
|
38
|
-
version: '0.9.
|
|
38
|
+
version: '0.9.1',
|
|
39
39
|
data: {} // per-trial return is {}; session meta goes via addProperties
|
|
40
40
|
};
|
|
41
41
|
|
|
@@ -10,7 +10,7 @@ class CyborgHunterExtension {
|
|
|
10
10
|
// and package.json). Hand-bumped on each release; if you forget, the
|
|
11
11
|
// jsPsych developer console shows a stale number — the actual version
|
|
12
12
|
// attached to data is read from window.CyborgHunter.VERSION at runtime.
|
|
13
|
-
version: '0.9.
|
|
13
|
+
version: '0.9.1',
|
|
14
14
|
data: { integrity: { type: 'object' } }
|
|
15
15
|
};
|
|
16
16
|
|
package/src/shared/constants.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Default thresholds and preset configurations for Cyborg Hunter.
|
|
3
3
|
// Single source of truth — used by both the browser library and CLI report tool.
|
|
4
4
|
|
|
5
|
-
export const VERSION = "0.9.
|
|
5
|
+
export const VERSION = "0.9.1";
|
|
6
6
|
|
|
7
7
|
// Default detection thresholds shared across presets.
|
|
8
8
|
// Researchers can override any value at init() time.
|