octocode-cli 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +129 -28
  2. package/out/chunks/chunk-7476PETK.js +309 -0
  3. package/out/chunks/chunk-CVNNNSMQ.js +26 -0
  4. package/out/chunks/chunk-OQBJTZWK.js +60 -0
  5. package/out/chunks/chunk-UCZCF3BQ.js +9 -0
  6. package/out/chunks/command-help-specs-JZXVSLZ5.js +8 -0
  7. package/out/chunks/commands-XBFPLHSQ.js +8 -0
  8. package/out/chunks/help-P7TCOYAJ.js +10 -0
  9. package/out/chunks/main-help-ULF5PAQY.js +10 -0
  10. package/out/chunks/prompts-5E6VKRX5.js +8 -0
  11. package/out/chunks/spinner-URV2OX6O.js +8 -0
  12. package/out/chunks/tool-command-M6VA7P2F.js +8 -0
  13. package/out/octocode-cli.js +1 -1
  14. package/package.json +5 -3
  15. package/skills/README.md +60 -58
  16. package/skills/agentic-flow-best-practices/SKILL.md +280 -0
  17. package/skills/agentic-flow-best-practices/references/agent-collaboration-patterns.md +75 -0
  18. package/skills/agentic-flow-best-practices/references/pr-review-agent-example.md +47 -0
  19. package/skills/agentic-flow-best-practices/references/resources.md +112 -0
  20. package/skills/octocode-brainstorming/.env.example +11 -0
  21. package/skills/octocode-brainstorming/SKILL.md +262 -0
  22. package/skills/octocode-brainstorming/scripts/tavily-search.mjs +138 -0
  23. package/skills/octocode-chrome-devtools/README.md +541 -0
  24. package/skills/octocode-chrome-devtools/SKILL.md +197 -0
  25. package/skills/octocode-chrome-devtools/agents/openai.yaml +7 -0
  26. package/skills/octocode-chrome-devtools/references/CDP_AGENT_REFERENCE.md +401 -0
  27. package/skills/octocode-chrome-devtools/references/CHROME_FLAGS.md +234 -0
  28. package/skills/octocode-chrome-devtools/references/INTENTS.md +108 -0
  29. package/skills/octocode-chrome-devtools/references/INTENTS_AUTH.md +179 -0
  30. package/skills/octocode-chrome-devtools/references/INTENTS_AUTOMATION.md +214 -0
  31. package/skills/octocode-chrome-devtools/references/INTENTS_DEBUG.md +329 -0
  32. package/skills/octocode-chrome-devtools/references/INTENTS_ENVIRONMENT.md +237 -0
  33. package/skills/octocode-chrome-devtools/references/INTENTS_INSPECT.md +263 -0
  34. package/skills/octocode-chrome-devtools/references/INTENTS_STORAGE_CONSENT.md +214 -0
  35. package/skills/octocode-chrome-devtools/references/RECOVERY.md +39 -0
  36. package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS.md +43 -0
  37. package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_ASYNC_WORKERS.md +345 -0
  38. package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_BROWSER.md +403 -0
  39. package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_OBSERVE.md +275 -0
  40. package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_SPECIAL.md +18 -0
  41. package/skills/octocode-chrome-devtools/scripts/cdp-runner.mjs +503 -0
  42. package/skills/octocode-chrome-devtools/scripts/cdp-sandbox.mjs +123 -0
  43. package/skills/octocode-chrome-devtools/scripts/cdp-template.mjs +81 -0
  44. package/skills/octocode-chrome-devtools/scripts/octocode-chrome-devtools.vpn.example.json +8 -0
  45. package/skills/octocode-chrome-devtools/scripts/open-browser.mjs +362 -0
  46. package/skills/octocode-chrome-devtools/scripts/sourcemap-resolver.mjs +193 -0
  47. package/skills/octocode-chrome-devtools/scripts/undercover.mjs +226 -0
  48. package/skills/octocode-design/README.md +2 -2
  49. package/skills/octocode-documentation-writer/README.md +1 -1
  50. package/skills/octocode-engineer/README.md +1 -1
  51. package/skills/octocode-engineer/SKILL.md +137 -306
  52. package/skills/octocode-engineer/references/cli-reference.md +13 -0
  53. package/skills/octocode-engineer/references/output-files.md +3 -3
  54. package/skills/octocode-engineer/scripts/run.js +146 -146
  55. package/skills/octocode-engineer/src/pipeline/main.ts +1 -17
  56. package/skills/octocode-engineer/src/pipeline/progress.ts +4 -0
  57. package/skills/octocode-engineer/src/reporting/summary-md.test.ts +48 -0
  58. package/skills/octocode-engineer/src/reporting/summary-md.ts +43 -2
  59. package/skills/octocode-install/SKILL.md +1 -1
  60. package/skills/octocode-pull-request-reviewer/README.md +5 -5
  61. package/skills/octocode-pull-request-reviewer/SKILL.md +1 -1
  62. package/skills/octocode-research/AGENTS.md +1 -1
  63. package/skills/octocode-research/README.md +2 -2
  64. package/skills/octocode-research/docs/ARCHITECTURE.md +1 -1
  65. package/skills/octocode-research/scripts/server.js +184 -239
  66. package/skills/octocode-research/src/routes/github.ts +4 -21
  67. package/skills/octocode-research/src/routes/local.ts +4 -21
  68. package/skills/octocode-research/src/utils/fileContentTransform.ts +44 -0
  69. package/skills/octocode-search-skill/SKILL.md +337 -0
  70. package/skills/octocode-search-skill/references/agent-skills-guide.md +177 -0
  71. package/skills/octocode-search-skill/references/discovery-surfaces.md +162 -0
  72. package/skills/octocode-search-skill/references/fetch-and-create-locally.md +57 -0
  73. package/skills/octocode-search-skill/references/install-reference.md +130 -0
  74. package/skills/octocode-search-skill/references/references-template.md +27 -0
  75. package/skills/octocode-search-skill/references/references.md +62 -0
  76. package/skills/octocode-slides/README.md +307 -0
  77. package/skills/octocode-slides/SKILL.md +410 -0
  78. package/skills/octocode-slides/references/01-brief.md +156 -0
  79. package/skills/octocode-slides/references/02-research.md +149 -0
  80. package/skills/octocode-slides/references/03-outline.md +172 -0
  81. package/skills/octocode-slides/references/04-design.md +301 -0
  82. package/skills/octocode-slides/references/05-implementation.md +213 -0
  83. package/skills/octocode-slides/references/06-review.md +258 -0
  84. package/skills/octocode-slides/references/animation.md +281 -0
  85. package/skills/octocode-slides/references/design-system.md +316 -0
  86. package/skills/octocode-slides/references/html-templates.md +673 -0
  87. package/skills/octocode-slides/references/image-generation.md +448 -0
  88. package/skills/octocode-slides/references/resources.md +840 -0
  89. package/skills/octocode-slides/references/slide-rules.md +541 -0
  90. package/skills/octocode-slides/references/wireframes.md +727 -0
  91. package/skills/octocode-slides/scripts/animation.js +182 -0
  92. package/skills/octocode-slides/scripts/base.css +353 -0
  93. package/skills/octocode-slides/scripts/base.html +655 -0
  94. package/skills/octocode-slides/scripts/generate_image.py +221 -0
  95. package/skills/octocode-slides/scripts/navbridge.js +79 -0
  96. package/skills/octocode-slides/scripts/presenter.js +316 -0
  97. package/skills/octocode-slides/scripts/slide.html +248 -0
  98. package/skills/octocode-stats/SKILL.md +73 -0
  99. package/skills/octocode-stats/assets/template.html +1332 -0
  100. package/skills/octocode-stats/scripts/build_dashboard.mjs +407 -0
  101. package/out/chunks/chunk-LH4AZJPA.js +0 -389
  102. package/out/chunks/command-help-specs-CQ3RBLP6.js +0 -8
  103. package/out/chunks/commands-M3QTWKWE.js +0 -51
  104. package/out/chunks/help-XPXP46ZT.js +0 -10
  105. package/out/chunks/main-help-HXFAFHPG.js +0 -10
  106. package/out/chunks/tool-command-VHFLPIHY.js +0 -8
@@ -0,0 +1,655 @@
1
+ <!DOCTYPE html>
2
+ <!--
3
+ ╔══════════════════════════════════════════════════════════════════════════╗
4
+ ║ OCTOCODE SLIDES — NAVIGATION CONTROLLER ║
5
+ ║ Generated from: skills/octocode-slides/scripts/base.html ║
6
+ ║ ║
7
+ ║ LLM INSTRUCTIONS (remove all <!-- LLM: --> comments in final output): ║
8
+ ║ 1. Replace every <!-- LLM: ... --> placeholder with actual values ║
9
+ ║ 2. Fill the const slides = [...] array — one { path, hidden, name } ║
10
+ ║ object per slide, in the order you want them shown. ║
11
+ ║ • path – path to the slide HTML relative to index.html ║
12
+ ║ • name – unique slug used in the URL hash (e.g. 'my-slide') ║
13
+ ║ Do NOT use numbers — order is controlled by the array. ║
14
+ ║ • hidden – true = skip during playback + hide from overview grid ║
15
+ ║ 3. Set the <title> to the deck title ║
16
+ ║ 4. Adjust #progress background to match the theme accent color ║
17
+ ║ 5. Make sure every slide HTML file includes js/navbridge.js ║
18
+ ║ (see scripts/slide.html — the navbridge script tag is already ║
19
+ ║ included in the template) ║
20
+ ║ ║
21
+ ║ KEYBOARD SHORTCUTS: ║
22
+ ║ → / ↓ / Space next slide ║
23
+ ║ ← / ↑ previous slide ║
24
+ ║ Home / End first / last slide ║
25
+ ║ F toggle fullscreen ║
26
+ ║ G toggle overview grid (thumbnail navigator) ║
27
+ ║ Escape close overview ║
28
+ ║ B / W black / white screen (press again to dismiss) ║
29
+ ║ Scroll wheel next / previous slide ║
30
+ ╚══════════════════════════════════════════════════════════════════════════╝
31
+ -->
32
+ <html lang="en">
33
+ <head>
34
+ <meta charset="UTF-8">
35
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
36
+
37
+ <!-- LLM: Set this to the deck title (e.g., "Octocode MCP — 20-min Tech Talk") -->
38
+ <title><!-- LLM: Deck Title --></title>
39
+
40
+ <style>
41
+ /* ── Reset ─────────────────────────────────────────── */
42
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
43
+
44
+ /* ── Full-window letterbox ──────────────────────────── */
45
+ html, body {
46
+ width: 100%; height: 100%;
47
+ /* LLM: Change to theme --bg value (e.g., #0D1117 for Terminal, oklch(16% 0.02 290) for Midnight) */
48
+ background: #000;
49
+ overflow: hidden;
50
+ display: grid;
51
+ place-items: center;
52
+ }
53
+
54
+ /* ── 1280×720 stage ─────────────────────────────────── */
55
+ /* JS scales this to fill the window while keeping 16:9. */
56
+ #stage {
57
+ width: 1280px;
58
+ height: 720px;
59
+ position: relative;
60
+ transform-origin: center center;
61
+ }
62
+
63
+ /* ── Slide cell wrapper ─────────────────────────────── */
64
+ /* All slide cells stack at inset:0. The topmost (last in DOM) cell would
65
+ otherwise swallow every mouse-down — blocking text selection inside the
66
+ active iframe — so inactive cells must not accept pointer events.
67
+ Pair this with .slide-frame[data-active] below; both rules MUST flip
68
+ in lockstep on slide change (`go()` toggles `is-active` + `data-active`). */
69
+ .slide-cell {
70
+ position: absolute;
71
+ inset: 0;
72
+ width: 100%;
73
+ height: 100%;
74
+ pointer-events: none;
75
+ }
76
+ .slide-cell.is-active { pointer-events: auto; }
77
+
78
+ /* ── Slide iframes ──────────────────────────────────── */
79
+ /* Inactive iframes are invisible AND non-interactive. Without
80
+ pointer-events:none they would still capture clicks (preventing the
81
+ active iframe behind them from receiving selection or click events). */
82
+ .slide-frame {
83
+ position: absolute;
84
+ inset: 0;
85
+ width: 100%; height: 100%;
86
+ border: none;
87
+ opacity: 0;
88
+ transition: opacity 150ms ease;
89
+ pointer-events: none;
90
+ }
91
+ /* Active frame is visible and accepts pointer events (enables navbridge,
92
+ text selection, copy/paste, and link clicks inside the slide). */
93
+ .slide-frame[data-active] { opacity: 1; pointer-events: auto; }
94
+
95
+ /* ── Progress bar ───────────────────────────────────── */
96
+ #progress {
97
+ position: fixed;
98
+ bottom: 0; left: 0;
99
+ height: 3px;
100
+ /* LLM: Set to theme --accent (e.g., #39D353 for Terminal, #C9B37A for Midnight) */
101
+ background: rgba(255,255,255,0.5);
102
+ transition: width 200ms ease;
103
+ pointer-events: none;
104
+ z-index: 10;
105
+ }
106
+
107
+ /* ── Slide counter ──────────────────────────────────── */
108
+ #counter {
109
+ position: fixed;
110
+ bottom: 10px; right: 18px;
111
+ font: 11px/1 system-ui, sans-serif;
112
+ color: rgba(255,255,255,0.3);
113
+ letter-spacing: 0.06em;
114
+ pointer-events: none;
115
+ z-index: 10;
116
+ }
117
+
118
+ /* ── HUD (keyboard hint pill) ───────────────────────── */
119
+ /* Fades in on activity, fades out after 1.6 s of idle. */
120
+ #hud {
121
+ position: fixed; right: 18px; top: 18px;
122
+ display: flex; align-items: center; gap: 10px;
123
+ font: 11px/1 system-ui, sans-serif; letter-spacing: 0.06em;
124
+ color: rgba(255,255,255,0.4);
125
+ background: rgba(0,0,0,0.55); border: 1px solid rgba(255,255,255,0.1);
126
+ padding: 6px 12px; border-radius: 999px;
127
+ backdrop-filter: blur(4px);
128
+ opacity: 0; transition: opacity 0.25s ease; pointer-events: none;
129
+ z-index: 50;
130
+ }
131
+ #hud.show { opacity: 1; }
132
+ .hud-sep { color: rgba(255,255,255,0.15); }
133
+ #hud kbd {
134
+ font: inherit; background: rgba(255,255,255,0.08);
135
+ border: 1px solid rgba(255,255,255,0.15); border-radius: 3px;
136
+ padding: 1px 5px; font-size: 10px;
137
+ }
138
+
139
+ /* ════════════════════════════════════════════════════
140
+ OVERVIEW MODE (body.overview)
141
+ Press G to open, press G or Escape to close.
142
+ Shows all slides as scaled thumbnails.
143
+ Click any thumbnail to jump to that slide.
144
+ ════════════════════════════════════════════════════ */
145
+
146
+ body.overview {
147
+ overflow-y: auto;
148
+ background: #0e0e0e;
149
+ display: block;
150
+ }
151
+
152
+ body.overview #stage {
153
+ width: 100%;
154
+ height: auto;
155
+ min-height: 100vh;
156
+ position: static;
157
+ transform: none !important;
158
+ display: grid;
159
+ grid-template-columns: repeat(auto-fill, 256px);
160
+ gap: 1.25rem;
161
+ padding: 2.5rem 2rem 4rem;
162
+ align-content: start;
163
+ }
164
+
165
+ body.overview .slide-cell {
166
+ position: relative;
167
+ width: 256px;
168
+ height: 144px;
169
+ overflow: hidden;
170
+ border-radius: 6px;
171
+ border: 2px solid rgba(255,255,255,0.08);
172
+ cursor: pointer;
173
+ transition: border-color 120ms ease, box-shadow 120ms ease;
174
+ }
175
+ body.overview .slide-cell:hover {
176
+ border-color: rgba(255,255,255,0.4);
177
+ }
178
+ body.overview .slide-cell.is-active {
179
+ border-color: rgba(255,255,255,0.75);
180
+ box-shadow: 0 0 0 1px rgba(255,255,255,0.3);
181
+ }
182
+
183
+ /* Scale 1280×720 iframe down to 256×144 (factor 0.2) */
184
+ body.overview .slide-frame {
185
+ position: absolute;
186
+ top: 0; left: 0;
187
+ width: 1280px !important;
188
+ height: 720px !important;
189
+ transform: scale(0.2);
190
+ transform-origin: top left;
191
+ opacity: 1;
192
+ pointer-events: none;
193
+ transition: none;
194
+ }
195
+
196
+ /* Slide number badge — only visible in overview */
197
+ .slide-cell .slide-num {
198
+ display: none;
199
+ position: absolute;
200
+ bottom: 4px; right: 6px;
201
+ font: 700 9px/1 system-ui, sans-serif;
202
+ color: rgba(255,255,255,0.45);
203
+ letter-spacing: 0.04em;
204
+ z-index: 10;
205
+ pointer-events: none;
206
+ }
207
+ body.overview .slide-cell .slide-num { display: block; }
208
+
209
+ /* Slide name label in overview — shows the slug under each thumbnail */
210
+ .slide-cell .slide-label {
211
+ display: none;
212
+ position: absolute;
213
+ bottom: 0; left: 0; right: 0;
214
+ padding: 4px 6px 5px;
215
+ background: rgba(0,0,0,0.6);
216
+ font: 9px/1.3 system-ui, sans-serif;
217
+ color: rgba(255,255,255,0.55);
218
+ letter-spacing: 0.02em;
219
+ white-space: nowrap;
220
+ overflow: hidden;
221
+ text-overflow: ellipsis;
222
+ pointer-events: none;
223
+ z-index: 11;
224
+ }
225
+ body.overview .slide-cell .slide-label { display: block; }
226
+
227
+ /* Overview hint strip at top */
228
+ #overview-hint {
229
+ display: none;
230
+ position: fixed;
231
+ top: 0; left: 0; right: 0;
232
+ padding: 0.5rem 1.5rem;
233
+ font: 10px/1.5 system-ui, sans-serif;
234
+ color: rgba(255,255,255,0.25);
235
+ letter-spacing: 0.08em;
236
+ text-align: right;
237
+ pointer-events: none;
238
+ z-index: 200;
239
+ }
240
+ body.overview #overview-hint { display: block; }
241
+
242
+ /* Progress bar, counter, and HUD hidden in overview */
243
+ body.overview #progress,
244
+ body.overview #counter,
245
+ body.overview #hud { display: none; }
246
+
247
+ /* ── Blackout overlay (B = black screen, W = white screen) ───────────── */
248
+ #blackout {
249
+ position: fixed;
250
+ inset: 0;
251
+ z-index: 80;
252
+ pointer-events: none;
253
+ opacity: 0;
254
+ transition: opacity 200ms ease;
255
+ }
256
+ #blackout.is-black { background: #000; opacity: 1; pointer-events: auto; }
257
+ #blackout.is-white { background: #fff; opacity: 1; pointer-events: auto; }
258
+ </style>
259
+ </head>
260
+ <body>
261
+
262
+ <!-- ── Stage ────────────────────────────────────────────────────────────── -->
263
+ <!-- All slide cells (each wrapping an iframe) are injected here by JS. -->
264
+ <div id="stage">
265
+ <!-- LLM: DO NOT add content here manually. JS builds slide cells from slides[]. -->
266
+ </div>
267
+
268
+ <!-- ── UI chrome ─────────────────────────────────────────────────────────── -->
269
+ <div id="progress"></div>
270
+ <div id="counter"></div>
271
+
272
+ <!-- HUD: keyboard hint pill — fades in on activity, fades out after 1.6 s -->
273
+ <div id="hud">
274
+ <span id="hud-count">— / —</span>
275
+ <span class="hud-sep">/</span>
276
+ <span><kbd>&larr;</kbd> <kbd>&rarr;</kbd> nav</span>
277
+ <span class="hud-sep">/</span>
278
+ <kbd>G</kbd> grid
279
+ <span class="hud-sep">/</span>
280
+ <kbd>F</kbd> full
281
+ <span class="hud-sep">/</span>
282
+ <kbd>P</kbd> notes
283
+ <span class="hud-sep">/</span>
284
+ <kbd>B</kbd><kbd>W</kbd> blank
285
+ </div>
286
+
287
+ <div id="overview-hint">Overview — click a slide to jump &nbsp;·&nbsp; G or Esc to close</div>
288
+
289
+ <!-- ── Blackout overlay ───────────────────────────────────────────────────── -->
290
+ <!-- B = black screen · W = white screen · press again to dismiss -->
291
+ <div id="blackout" aria-hidden="true"></div>
292
+
293
+ <script src="js/presenter.js"></script>
294
+ <script>
295
+ // ═══════════════════════════════════════════════════════════════════════════
296
+ // SLIDE MANIFEST
297
+ //
298
+ // Each entry: { path, hidden, name }
299
+ // path – slide HTML path relative to this index.html (forward slashes)
300
+ // name – unique slug for the URL hash (e.g. 'problem' → #problem)
301
+ // Do NOT use numbers — playback order is controlled by this array.
302
+ // hidden – true = skip during playback AND hide from overview grid
303
+ //
304
+ // Filename numbers (01-, 02-…) do NOT drive playback order.
305
+ // This array is the single source of truth for slide order.
306
+ // LLM: Replace the placeholder entries below with the actual slide list.
307
+ // ═══════════════════════════════════════════════════════════════════════════
308
+ const slides = [
309
+ // LLM: List every slide as { path, hidden, name }, in presentation order.
310
+ // { path: 'slides/title.html', hidden: false, name: 'title' },
311
+ // { path: 'slides/problem.html', hidden: false, name: 'problem' },
312
+ // { path: 'slides/solution.html',hidden: false, name: 'solution' },
313
+ // { path: 'slides/closing.html', hidden: false, name: 'closing' },
314
+ ];
315
+
316
+ // ═══════════════════════════════════════════════════════════════════════════
317
+ // DERIVED STATE — do not edit below this line
318
+ // ═══════════════════════════════════════════════════════════════════════════
319
+
320
+ /** Visible playlist — what nav, grid, HUD, and URL hash all index into. */
321
+ const playable = slides.filter(s => !s.hidden);
322
+ const total = playable.length;
323
+ const nameToIdx = Object.fromEntries(playable.map((s, i) => [s.name, i]));
324
+
325
+ let current = 0;
326
+ let inOverview = false;
327
+ let presenter = null;
328
+
329
+ const stage = document.getElementById('stage');
330
+ const bar = document.getElementById('progress');
331
+ const counter = document.getElementById('counter');
332
+ const hud = document.getElementById('hud');
333
+ const hudCount = document.getElementById('hud-count');
334
+ const blackoutEl = document.getElementById('blackout');
335
+ let blackoutMode = null; // 'black' | 'white' | null
336
+
337
+ function toggleBlackout(mode) {
338
+ if (blackoutMode === mode) {
339
+ blackoutMode = null;
340
+ blackoutEl.className = '';
341
+ } else {
342
+ blackoutMode = mode;
343
+ blackoutEl.className = mode === 'black' ? 'is-black' : 'is-white';
344
+ }
345
+ }
346
+
347
+ // ═══════════════════════════════════════════════════════════════════════════
348
+ // BUILD SLIDE CELLS
349
+ // Each visible iframe is wrapped in a .slide-cell div.
350
+ // Normal mode : cells are absolute overlays stacked on the stage.
351
+ // Overview mode: cells become 256×144 grid items (iframe scaled 0.2×).
352
+ // ═══════════════════════════════════════════════════════════════════════════
353
+ playable.forEach((slide, i) => {
354
+ const cell = document.createElement('div');
355
+ cell.className = 'slide-cell';
356
+ cell.addEventListener('click', () => {
357
+ if (inOverview) { closeOverview(); go(i); }
358
+ });
359
+
360
+ const f = document.createElement('iframe');
361
+ f.src = slide.path;
362
+ f.className = 'slide-frame';
363
+ f.setAttribute('tabindex', '-1');
364
+ f.setAttribute('title', `Slide ${i + 1}: ${slide.name}`);
365
+
366
+ // Slide number badge (overview only)
367
+ const num = document.createElement('span');
368
+ num.className = 'slide-num';
369
+ num.textContent = String(i + 1).padStart(2, '0');
370
+
371
+ // Name label (overview only)
372
+ const label = document.createElement('span');
373
+ label.className = 'slide-label';
374
+ label.textContent = slide.name.replaceAll('-', ' ');
375
+
376
+ cell.appendChild(f);
377
+ cell.appendChild(num);
378
+ cell.appendChild(label);
379
+ stage.appendChild(cell);
380
+ });
381
+
382
+ const frames = () => stage.querySelectorAll('.slide-frame');
383
+ const cells = () => stage.querySelectorAll('.slide-cell');
384
+
385
+ if (typeof initPresenter === 'function') {
386
+ presenter = initPresenter(
387
+ () => stage.querySelector('.slide-frame[data-active]'),
388
+ playable,
389
+ () => current
390
+ );
391
+ }
392
+
393
+ // ═══════════════════════════════════════════════════════════════════════════
394
+ // SCALE STAGE TO WINDOW
395
+ // Maintains 1280×720 (16:9) at any window size.
396
+ // ═══════════════════════════════════════════════════════════════════════════
397
+ function scaleStage() {
398
+ if (inOverview) return;
399
+ const s = Math.min(
400
+ window.innerWidth / 1280,
401
+ window.innerHeight / 720
402
+ );
403
+ stage.style.transform = `scale(${s})`;
404
+ }
405
+
406
+ const ro = new ResizeObserver(scaleStage);
407
+ ro.observe(document.documentElement);
408
+ scaleStage();
409
+
410
+ // ═══════════════════════════════════════════════════════════════════════════
411
+ // URL HASH → INDEX
412
+ // Accepts a slide name (#problem) or a legacy 1-based number (#5).
413
+ // ═══════════════════════════════════════════════════════════════════════════
414
+ function indexFromHash(h) {
415
+ const raw = (h || '').replace(/^#/, '');
416
+ if (!raw) return 0;
417
+ const decoded = (() => { try { return decodeURIComponent(raw); } catch (_) { return raw; } })();
418
+ if (Object.prototype.hasOwnProperty.call(nameToIdx, decoded)) return nameToIdx[decoded];
419
+ const n = parseInt(decoded, 10);
420
+ if (Number.isFinite(n) && n >= 1) return n - 1;
421
+ return 0;
422
+ }
423
+
424
+ function clampIndex(n) {
425
+ if (total === 0) return 0;
426
+ return Math.max(0, Math.min(total - 1, n));
427
+ }
428
+
429
+ // ═══════════════════════════════════════════════════════════════════════════
430
+ // NAVIGATION
431
+ // ═══════════════════════════════════════════════════════════════════════════
432
+ function go(idx) {
433
+ const allFrames = frames();
434
+ const allCells = cells();
435
+ if (idx < 0 || idx >= total || !allFrames.length) return;
436
+
437
+ allFrames[current]?.removeAttribute('data-active');
438
+ allCells[current]?.classList.remove('is-active');
439
+
440
+ current = idx;
441
+
442
+ allFrames[current].setAttribute('data-active', '');
443
+ allCells[current].classList.add('is-active');
444
+
445
+ // ── Transfer focus to the newly active slide ────────────────────────────
446
+ // Without this, focus stays on the last-clicked iframe. Two failure modes:
447
+ // 1. Stale animation.js on the old (invisible) slide silently consumes
448
+ // keystrokes — the user sees nothing happen.
449
+ // 2. Stale navbridge on the old slide forwards keys straight to the parent
450
+ // (passthrough:true), bypassing the new slide's animation.js entirely.
451
+ // requestAnimationFrame lets the opacity transition start before we steal
452
+ // focus; the try/catch handles the initial go(0) call before iframes load.
453
+ ;(function (f) {
454
+ requestAnimationFrame(function () {
455
+ try { if (f.contentWindow) f.contentWindow.focus(); } catch (_) {}
456
+ });
457
+ })(allFrames[current]);
458
+
459
+ const slide = playable[current];
460
+ const pct = ((current + 1) / total) * 100;
461
+ bar.style.width = pct + '%';
462
+ counter.textContent = `${current + 1} / ${total}`;
463
+ if (hudCount) hudCount.textContent = `${String(current + 1).padStart(2, '0')} / ${total}`;
464
+
465
+ // Name-based hash — stable across slide reorders
466
+ history.replaceState(null, '', '#' + encodeURIComponent(slide.name));
467
+ presenter?.onSlideChange();
468
+ }
469
+
470
+ // ═══════════════════════════════════════════════════════════════════════════
471
+ // HUD — auto-hide keyboard hint pill
472
+ // Show on any activity; fade after 1.6 s of idle.
473
+ // ═══════════════════════════════════════════════════════════════════════════
474
+ let hudTimer;
475
+ function showHud() {
476
+ hud.classList.add('show');
477
+ clearTimeout(hudTimer);
478
+ hudTimer = setTimeout(() => hud.classList.remove('show'), 1600);
479
+ }
480
+ window.addEventListener('mousemove', showHud);
481
+
482
+ // ═══════════════════════════════════════════════════════════════════════════
483
+ // OVERVIEW MODE
484
+ // G key opens the thumbnail grid. G or Escape closes it.
485
+ // Clicking a thumbnail closes overview and jumps to that slide.
486
+ // ═══════════════════════════════════════════════════════════════════════════
487
+ function openOverview() {
488
+ inOverview = true;
489
+ document.body.classList.add('overview');
490
+ document.body.style.overflow = '';
491
+ }
492
+
493
+ function closeOverview() {
494
+ inOverview = false;
495
+ document.body.classList.remove('overview');
496
+ requestAnimationFrame(scaleStage);
497
+ }
498
+
499
+ // ═══════════════════════════════════════════════════════════════════════════
500
+ // KEYBOARD — single point of truth
501
+ // • Parent window focused: keydown listener below fires directly.
502
+ // • Iframe focused: js/navbridge.js inside the slide posts a message,
503
+ // which the window message listener converts into a handleKey() call.
504
+ // Do NOT attach a second keydown listener to the iframe — that would
505
+ // double-fire and advance two slides per key press.
506
+ // ═══════════════════════════════════════════════════════════════════════════
507
+ function handleKey(e) {
508
+ if (inOverview) {
509
+ if (e.key === 'Escape' || e.key === 'g' || e.key === 'G') closeOverview();
510
+ return;
511
+ }
512
+
513
+ // ── Step-animation routing ──────────────────────────────────────────────
514
+ // When the parent window has focus (initial load, browser chrome refocus,
515
+ // overview close, or other parent-level interaction), nav keys fire here
516
+ // directly and bypass animation.js inside the iframe entirely. Fix: forward
517
+ // those keys into the active iframe so
518
+ // animation.js can intercept them in capture phase. animation.js consumes the
519
+ // key (stopImmediatePropagation) if a step is pending; navbridge inside the
520
+ // iframe then posts back only once all steps are exhausted, at which point
521
+ // the message handler below calls handleKey with passthrough:true and normal
522
+ // slide navigation resumes.
523
+ // passthrough:true is set only by the message handler — it marks a reply from
524
+ // navbridge meaning "nothing left to consume, please advance/retreat."
525
+ if (!e.passthrough) {
526
+ var _stepKeys = { ArrowRight: 1, ArrowDown: 1, ' ': 1, ArrowLeft: 1, ArrowUp: 1 };
527
+ if (_stepKeys[e.key]) {
528
+ var _af = stage.querySelector('.slide-frame[data-active]');
529
+ if (_af) {
530
+ try { _af.contentWindow.postMessage({ type: 'octocode-slides:key', key: e.key }, '*'); }
531
+ catch (_) {}
532
+ }
533
+ showHud();
534
+ if (e.preventDefault) e.preventDefault();
535
+ return;
536
+ }
537
+ }
538
+
539
+ let handled = true;
540
+ switch (e.key) {
541
+ case 'ArrowRight':
542
+ case 'ArrowDown':
543
+ case ' ':
544
+ go(current + 1);
545
+ break;
546
+ case 'ArrowLeft':
547
+ case 'ArrowUp':
548
+ go(current - 1);
549
+ break;
550
+ case 'Home': go(0); break;
551
+ case 'End': go(total - 1); break;
552
+ case 'f':
553
+ case 'F':
554
+ document.fullscreenElement
555
+ ? document.exitFullscreen?.()
556
+ : document.documentElement.requestFullscreen?.();
557
+ break;
558
+ case 'g':
559
+ case 'G':
560
+ openOverview();
561
+ break;
562
+ case 'p':
563
+ case 'P':
564
+ presenter?.open();
565
+ break;
566
+ case 'b':
567
+ case 'B':
568
+ toggleBlackout('black');
569
+ break;
570
+ case 'w':
571
+ case 'W':
572
+ toggleBlackout('white');
573
+ break;
574
+ default:
575
+ handled = false;
576
+ }
577
+
578
+ if (handled) {
579
+ showHud();
580
+ if (e.preventDefault) e.preventDefault();
581
+ }
582
+ }
583
+
584
+ document.addEventListener('keydown', handleKey, true);
585
+
586
+ // ═══════════════════════════════════════════════════════════════════════════
587
+ // NAVBRIDGE — postMessage receiver
588
+ // js/navbridge.js inside each slide iframe forwards nav keys via postMessage
589
+ // when the iframe has focus (user clicked inside the slide).
590
+ // ═══════════════════════════════════════════════════════════════════════════
591
+ window.addEventListener('message', function (event) {
592
+ var data = event.data;
593
+ if (!data || typeof data !== 'object') return;
594
+ if (data.type === 'octocode-slides:nav' && data.key) {
595
+ // passthrough:true tells handleKey this reply came from navbridge inside the
596
+ // iframe — animation.js had nothing left to consume, so navigate normally.
597
+ handleKey({ key: data.key, passthrough: true, preventDefault: function () {} });
598
+ } else if (data.type === 'octocode-slides:activity') {
599
+ showHud();
600
+ } else if (data.type === 'octocode-slides:presenter-goto') {
601
+ go(clampIndex(data.index));
602
+ }
603
+ });
604
+
605
+ // ═══════════════════════════════════════════════════════════════════════════
606
+ // TOUCH / SWIPE (left swipe = next, right swipe = previous)
607
+ // ═══════════════════════════════════════════════════════════════════════════
608
+ let touchStartX = null;
609
+
610
+ document.addEventListener('touchstart', e => {
611
+ if (!inOverview) touchStartX = e.touches[0].clientX;
612
+ }, { passive: true });
613
+
614
+ document.addEventListener('touchend', e => {
615
+ if (touchStartX === null || inOverview) return;
616
+ const diff = touchStartX - e.changedTouches[0].clientX;
617
+ if (Math.abs(diff) > 40) go(current + (diff > 0 ? 1 : -1));
618
+ touchStartX = null;
619
+ });
620
+
621
+ // ═══════════════════════════════════════════════════════════════════════════
622
+ // WHEEL NAVIGATION (scroll down = next, scroll up = previous)
623
+ // ═══════════════════════════════════════════════════════════════════════════
624
+ let wheelTimer = null;
625
+ document.addEventListener('wheel', function(e) {
626
+ if (inOverview) return;
627
+ if (Math.abs(e.deltaY) < 20) return;
628
+ clearTimeout(wheelTimer);
629
+ wheelTimer = setTimeout(function() {
630
+ go(current + (e.deltaY > 0 ? 1 : -1));
631
+ }, 60);
632
+ }, { passive: true });
633
+
634
+ // ═══════════════════════════════════════════════════════════════════════════
635
+ // HASH NAVIGATION
636
+ // index.html#my-slide jumps to that slide directly.
637
+ // Accepts name slugs (#problem) and legacy 1-based numbers (#5).
638
+ // URL is updated automatically on every slide change via history.replaceState.
639
+ // ═══════════════════════════════════════════════════════════════════════════
640
+ window.addEventListener('hashchange', function () {
641
+ const next = clampIndex(indexFromHash(location.hash));
642
+ if (next !== current) go(next);
643
+ });
644
+
645
+ // ═══════════════════════════════════════════════════════════════════════════
646
+ // INIT — respect hash on first load, else start at slide 1
647
+ // ═══════════════════════════════════════════════════════════════════════════
648
+ // Do not pre-mark slide 1 active; go() is the single activation path so
649
+ // direct hash loads (#problem) never leave two visible iframes.
650
+ go(clampIndex(indexFromHash(location.hash)));
651
+ showHud();
652
+ </script>
653
+
654
+ </body>
655
+ </html>