@dzhechkov/keysarium 1.4.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.
- package/README.md +50 -3
- package/bin/cli.js +0 -0
- package/package.json +5 -3
- package/templates/.claude/commands/analyst-manual.md +38 -0
- package/templates/.claude/commands/casarium.md +55 -4
- package/templates/.claude/commands/feature-adr.md +8 -0
- package/templates/.claude/rules/trust-tiers.md +5 -0
- package/templates/.claude/shards/feature-adr.shard.md +64 -10
- package/templates/.claude/shards/phase-ai-factory.shard.md +109 -0
- package/templates/.claude/skills/ai-factory-mapper/README.md +90 -0
- package/templates/.claude/skills/ai-factory-mapper/SKILL.md +309 -0
- package/templates/.claude/skills/ai-factory-mapper/assets/report-template.md +123 -0
- package/templates/.claude/skills/ai-factory-mapper/references/catalog.md +230 -0
- package/templates/.claude/skills/ai-factory-mapper/references/coverage-formula.md +219 -0
- package/templates/.claude/skills/ai-factory-mapper/references/decomposition-checklist.md +183 -0
- package/templates/.claude/skills/ai-factory-mapper/references/pattern-library.md +312 -0
- package/templates/.claude/skills/ai-factory-mapper/scripts/build_docx_report.js +423 -0
- package/templates/.claude/skills/analyst-manual-full/SKILL.md +286 -0
- package/templates/.claude/skills/edu-site-generator/SKILL.md +254 -0
- package/templates/.claude/skills/edu-site-generator/examples/sample-course-structure.md +116 -0
- package/templates/.claude/skills/edu-site-generator/modules/00-content-analysis.md +84 -0
- package/templates/.claude/skills/edu-site-generator/modules/01-course-structure.md +75 -0
- package/templates/.claude/skills/edu-site-generator/modules/02-data-generation.md +186 -0
- package/templates/.claude/skills/edu-site-generator/modules/03-scaffold.md +249 -0
- package/templates/.claude/skills/edu-site-generator/modules/04-components.md +130 -0
- package/templates/.claude/skills/edu-site-generator/modules/05-gamification.md +163 -0
- package/templates/.claude/skills/edu-site-generator/modules/06-deploy.md +88 -0
- package/templates/.claude/skills/edu-site-generator/modules/07-verification.md +97 -0
- package/templates/.claude/skills/edu-site-generator/references/component-templates.md +436 -0
- package/templates/.claude/skills/edu-site-generator/references/data-schemas.md +164 -0
- package/templates/.claude/skills/edu-site-generator/references/exercise-catalog.md +110 -0
- package/templates/.claude/skills/edu-site-generator/references/tech-stack.md +69 -0
- package/templates/.claude/skills/feature-adr/SKILL.md +245 -59
- package/templates/.claude/skills/feature-adr/modules/03-adr.md +43 -0
- package/templates/.claude/skills/feature-adr/modules/03.5-ideation-swarm.md +180 -0
- package/templates/.claude/skills/feature-adr/modules/06-implementation-plan.md +56 -7
- package/templates/.claude/skills/feature-adr/modules/08-qe.md +87 -23
- package/templates/.claude/skills/feature-adr/modules/09-fleet-qe.md +305 -0
- package/templates/.claude/skills/feature-adr/references/agentic-qe/README.md +143 -0
- package/templates/.claude/skills/feature-adr/references/agentic-qe/brutal-honesty-review.md +240 -0
- package/templates/.claude/skills/feature-adr/references/agentic-qe/chaos-engineering-resilience.md +163 -0
- package/templates/.claude/skills/feature-adr/references/agentic-qe/code-goal-planner.md +446 -0
- package/templates/.claude/skills/feature-adr/references/agentic-qe/enterprise-integration-testing.md +111 -0
- package/templates/.claude/skills/feature-adr/references/agentic-qe/mutation-testing.md +236 -0
- package/templates/.claude/skills/feature-adr/references/agentic-qe/performance-testing.md +315 -0
- package/templates/.claude/skills/feature-adr/references/agentic-qe/qcsd-ideation-swarm.md +126 -0
- package/templates/.claude/skills/feature-adr/references/agentic-qe/qcsd-production-swarm.md +108 -0
- package/templates/.claude/skills/feature-adr/references/agentic-qe/qe-coverage-analysis.md +193 -0
- package/templates/.claude/skills/feature-adr/references/agentic-qe/qe-requirements-validation.md +254 -0
- package/templates/.claude/skills/feature-adr/references/agentic-qe/regression-testing.md +233 -0
- package/templates/.claude/skills/feature-adr/references/agentic-qe/risk-based-testing.md +212 -0
- package/templates/.claude/skills/feature-adr/references/agentic-qe/security-testing.md +311 -0
- package/templates/.claude/skills/feature-adr/references/agentic-qe/shift-left-testing.md +231 -0
- package/templates/.claude/skills/feature-adr/references/agentic-qe/tdd-london-chicago.md +249 -0
- package/templates/.claude/skills/transcript-site-generator/SKILL.md +287 -0
- package/templates/.claude/skills/transcript-site-generator/examples/sample-text-only-site.md +123 -0
- package/templates/.claude/skills/transcript-site-generator/examples/sample-transcript-site.md +134 -0
- package/templates/.claude/skills/transcript-site-generator/modules/00-input-analysis.md +132 -0
- package/templates/.claude/skills/transcript-site-generator/modules/01-content-parsing.md +117 -0
- package/templates/.claude/skills/transcript-site-generator/modules/02-site-generation.md +293 -0
- package/templates/.claude/skills/transcript-site-generator/modules/03-interactivity.md +406 -0
- package/templates/.claude/skills/transcript-site-generator/modules/04-deploy.md +130 -0
- package/templates/.claude/skills/transcript-site-generator/modules/05-verification.md +155 -0
- package/templates/.claude/skills/transcript-site-generator/references/component-catalog.md +86 -0
- package/templates/.claude/skills/transcript-site-generator/references/data-schemas.md +123 -0
- package/templates/.claude/skills/transcript-site-generator/references/tech-stack.md +132 -0
- package/templates/CLAUDE.md +9 -2
- package/templates/docs/edu-site-generator/01-deployment-guide.md +911 -0
- package/templates/docs/edu-site-generator/02-admin-guide.md +901 -0
- package/templates/docs/edu-site-generator/03-user-guide.md +1008 -0
- package/templates/docs/edu-site-generator/04-infrastructure-requirements.md +489 -0
- package/templates/docs/edu-site-generator/05-architecture.md +902 -0
- package/templates/docs/edu-site-generator/06-user-flows.md +759 -0
- package/templates/docs/transcript-site-generator/01-deployment-guide.md +1042 -0
- package/templates/docs/transcript-site-generator/02-admin-guide.md +918 -0
- package/templates/docs/transcript-site-generator/03-user-guide.md +925 -0
- package/templates/docs/transcript-site-generator/04-infrastructure-requirements.md +482 -0
- package/templates/docs/transcript-site-generator/05-architecture.md +1068 -0
- package/templates/docs/transcript-site-generator/06-user-flows.md +855 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
# Step 3: Interactivity
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Generate `docs/static/app.js` — vanilla JavaScript implementing all interactive features: search, TOC scroll-spy, dark mode, progress bar, copy-quote, and mobile navigation.
|
|
6
|
+
|
|
7
|
+
## Protocol
|
|
8
|
+
|
|
9
|
+
### 1. Generate app.js
|
|
10
|
+
|
|
11
|
+
The JavaScript file follows this structure and implements all features in a single file with no dependencies.
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
// docs/static/app.js
|
|
15
|
+
|
|
16
|
+
// ═══════════════════════════════════════════════════
|
|
17
|
+
// 0. YOUTUBE API SETUP (must register BEFORE DOMContentLoaded)
|
|
18
|
+
// ═══════════════════════════════════════════════════
|
|
19
|
+
// The YouTube IFrame API calls window.onYouTubeIframeAPIReady as soon as
|
|
20
|
+
// the script loads — which may happen BEFORE DOMContentLoaded fires.
|
|
21
|
+
// Register the callback immediately at the top level to avoid the race.
|
|
22
|
+
let ytPlayer = null;
|
|
23
|
+
const ytIframeEl = document.getElementById('yt-player');
|
|
24
|
+
|
|
25
|
+
if (ytIframeEl) {
|
|
26
|
+
const tag = document.createElement('script');
|
|
27
|
+
tag.src = 'https://www.youtube.com/iframe_api';
|
|
28
|
+
document.head.appendChild(tag);
|
|
29
|
+
|
|
30
|
+
window.onYouTubeIframeAPIReady = function() {
|
|
31
|
+
ytPlayer = new YT.Player('yt-player', {
|
|
32
|
+
events: { onReady: () => console.log('YouTube player ready.') }
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function seekTo(seconds) {
|
|
38
|
+
if (ytPlayer && typeof ytPlayer.seekTo === 'function') {
|
|
39
|
+
ytPlayer.seekTo(seconds, true);
|
|
40
|
+
ytPlayer.playVideo();
|
|
41
|
+
document.getElementById('yt-player').scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
42
|
+
} else if (ytIframeEl) {
|
|
43
|
+
// Fallback: postMessage if API not loaded yet
|
|
44
|
+
ytIframeEl.contentWindow.postMessage(JSON.stringify({
|
|
45
|
+
event: 'command', func: 'seekTo', args: [seconds, true]
|
|
46
|
+
}), 'https://www.youtube.com');
|
|
47
|
+
ytIframeEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Event delegation for timestamp links (data-seek) — no inline onclick needed
|
|
52
|
+
document.addEventListener('click', (e) => {
|
|
53
|
+
const seekEl = e.target.closest('[data-seek]');
|
|
54
|
+
if (seekEl) {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
seekTo(Number(seekEl.dataset.seek));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
61
|
+
|
|
62
|
+
// ═══════════════════════════════════════════════════
|
|
63
|
+
// 1. DARK MODE
|
|
64
|
+
// ═══════════════════════════════════════════════════
|
|
65
|
+
const themeToggle = document.getElementById('theme-toggle');
|
|
66
|
+
const themeIcon = document.getElementById('theme-icon');
|
|
67
|
+
const html = document.documentElement;
|
|
68
|
+
|
|
69
|
+
// Restore saved theme or detect system preference
|
|
70
|
+
const savedTheme = localStorage.getItem('theme');
|
|
71
|
+
if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
72
|
+
html.classList.add('dark');
|
|
73
|
+
themeIcon.classList.replace('fa-moon', 'fa-sun');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
themeToggle.addEventListener('click', () => {
|
|
77
|
+
html.classList.toggle('dark');
|
|
78
|
+
const isDark = html.classList.contains('dark');
|
|
79
|
+
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
|
80
|
+
themeIcon.classList.replace(isDark ? 'fa-moon' : 'fa-sun', isDark ? 'fa-sun' : 'fa-moon');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ═══════════════════════════════════════════════════
|
|
84
|
+
// 2. MOBILE NAVIGATION
|
|
85
|
+
// ═══════════════════════════════════════════════════
|
|
86
|
+
const menuToggle = document.getElementById('menu-toggle');
|
|
87
|
+
const sidebar = document.getElementById('sidebar');
|
|
88
|
+
|
|
89
|
+
menuToggle.addEventListener('click', () => {
|
|
90
|
+
sidebar.classList.toggle('-translate-x-full');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Close sidebar when clicking a TOC link on mobile
|
|
94
|
+
sidebar.querySelectorAll('a').forEach(link => {
|
|
95
|
+
link.addEventListener('click', () => {
|
|
96
|
+
if (window.innerWidth < 1024) {
|
|
97
|
+
sidebar.classList.add('-translate-x-full');
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Close sidebar when clicking outside on mobile
|
|
103
|
+
document.addEventListener('click', (e) => {
|
|
104
|
+
if (window.innerWidth < 1024 &&
|
|
105
|
+
!sidebar.contains(e.target) &&
|
|
106
|
+
!menuToggle.contains(e.target) &&
|
|
107
|
+
!sidebar.classList.contains('-translate-x-full')) {
|
|
108
|
+
sidebar.classList.add('-translate-x-full');
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ═══════════════════════════════════════════════════
|
|
113
|
+
// 3. TOC SCROLL-SPY
|
|
114
|
+
// ═══════════════════════════════════════════════════
|
|
115
|
+
const tocLinks = document.querySelectorAll('.toc-link');
|
|
116
|
+
const sections = document.querySelectorAll('main section[id]');
|
|
117
|
+
|
|
118
|
+
const observerOptions = {
|
|
119
|
+
root: null,
|
|
120
|
+
rootMargin: '-80px 0px -70% 0px',
|
|
121
|
+
threshold: 0
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const observer = new IntersectionObserver((entries) => {
|
|
125
|
+
entries.forEach(entry => {
|
|
126
|
+
if (entry.isIntersecting) {
|
|
127
|
+
const id = entry.target.id;
|
|
128
|
+
tocLinks.forEach(link => {
|
|
129
|
+
link.classList.toggle('bg-primary-50', link.dataset.section === id);
|
|
130
|
+
link.classList.toggle('dark:bg-primary-900/30', link.dataset.section === id);
|
|
131
|
+
link.classList.toggle('text-primary-700', link.dataset.section === id);
|
|
132
|
+
link.classList.toggle('dark:text-primary-300', link.dataset.section === id);
|
|
133
|
+
link.classList.toggle('font-medium', link.dataset.section === id);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}, observerOptions);
|
|
138
|
+
|
|
139
|
+
sections.forEach(section => observer.observe(section));
|
|
140
|
+
|
|
141
|
+
// Smooth scroll for TOC links (use scrollIntoView, not hash anchors)
|
|
142
|
+
tocLinks.forEach(link => {
|
|
143
|
+
link.addEventListener('click', (e) => {
|
|
144
|
+
e.preventDefault();
|
|
145
|
+
const targetId = link.dataset.section;
|
|
146
|
+
const target = document.getElementById(targetId);
|
|
147
|
+
if (target) {
|
|
148
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ═══════════════════════════════════════════════════
|
|
154
|
+
// 4. READING PROGRESS BAR
|
|
155
|
+
// ═══════════════════════════════════════════════════
|
|
156
|
+
const progressBar = document.getElementById('progress-bar');
|
|
157
|
+
let ticking = false;
|
|
158
|
+
|
|
159
|
+
window.addEventListener('scroll', () => {
|
|
160
|
+
if (!ticking) {
|
|
161
|
+
requestAnimationFrame(() => {
|
|
162
|
+
const scrollTop = window.scrollY;
|
|
163
|
+
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
|
|
164
|
+
const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
|
|
165
|
+
progressBar.style.width = progress + '%';
|
|
166
|
+
ticking = false;
|
|
167
|
+
});
|
|
168
|
+
ticking = true;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ═══════════════════════════════════════════════════
|
|
173
|
+
// 5. BACK TO TOP
|
|
174
|
+
// ═══════════════════════════════════════════════════
|
|
175
|
+
const backToTop = document.getElementById('back-to-top');
|
|
176
|
+
|
|
177
|
+
window.addEventListener('scroll', () => {
|
|
178
|
+
if (window.scrollY > 500) {
|
|
179
|
+
backToTop.classList.remove('hidden', 'scale-0');
|
|
180
|
+
backToTop.classList.add('scale-100');
|
|
181
|
+
} else {
|
|
182
|
+
backToTop.classList.remove('scale-100');
|
|
183
|
+
backToTop.classList.add('hidden', 'scale-0');
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
backToTop.addEventListener('click', () => {
|
|
188
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ═══════════════════════════════════════════════════
|
|
192
|
+
// 6. SEARCH MODAL
|
|
193
|
+
// ═══════════════════════════════════════════════════
|
|
194
|
+
const searchBtn = document.getElementById('search-btn');
|
|
195
|
+
const searchModal = document.getElementById('search-modal');
|
|
196
|
+
const searchInput = document.getElementById('search-input');
|
|
197
|
+
const searchResults = document.getElementById('search-results');
|
|
198
|
+
|
|
199
|
+
function openSearch() {
|
|
200
|
+
searchModal.classList.remove('hidden');
|
|
201
|
+
searchInput.value = '';
|
|
202
|
+
searchResults.innerHTML = '';
|
|
203
|
+
setTimeout(() => searchInput.focus(), 100);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function closeSearch() {
|
|
207
|
+
searchModal.classList.add('hidden');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
searchBtn.addEventListener('click', openSearch);
|
|
211
|
+
searchModal.addEventListener('click', (e) => {
|
|
212
|
+
if (e.target === searchModal) closeSearch();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Debounced search
|
|
216
|
+
let searchTimeout;
|
|
217
|
+
searchInput.addEventListener('input', () => {
|
|
218
|
+
clearTimeout(searchTimeout);
|
|
219
|
+
searchTimeout = setTimeout(() => performSearch(searchInput.value.trim()), 300);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
function escapeHtml(text) {
|
|
223
|
+
const div = document.createElement('div');
|
|
224
|
+
div.textContent = text;
|
|
225
|
+
return div.innerHTML;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function escapeRegex(str) {
|
|
229
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Split plain text by query matches, escape each segment independently,
|
|
233
|
+
// then wrap matches in <mark>. This prevents highlighting inside HTML
|
|
234
|
+
// entities like & that would be created by escapeHtml.
|
|
235
|
+
// Note: use case-insensitive string comparison instead of regex.test()
|
|
236
|
+
// to avoid the /g flag lastIndex statefulness bug.
|
|
237
|
+
function highlightText(text, query) {
|
|
238
|
+
const regex = new RegExp(`(${escapeRegex(query)})`, 'gi');
|
|
239
|
+
const lowerQuery = query.toLowerCase();
|
|
240
|
+
return text.split(regex).map(part =>
|
|
241
|
+
part.toLowerCase() === lowerQuery
|
|
242
|
+
? `<mark class="bg-yellow-200 dark:bg-yellow-800 rounded px-0.5">${escapeHtml(part)}</mark>`
|
|
243
|
+
: escapeHtml(part)
|
|
244
|
+
).join('');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function performSearch(query) {
|
|
248
|
+
if (query.length < 2) {
|
|
249
|
+
searchResults.innerHTML = '';
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const results = [];
|
|
254
|
+
sections.forEach(section => {
|
|
255
|
+
const title = section.querySelector('h2')?.textContent || '';
|
|
256
|
+
const content = section.querySelector('.prose')?.textContent || '';
|
|
257
|
+
const fullText = title + ' ' + content;
|
|
258
|
+
const lowerQuery = query.toLowerCase();
|
|
259
|
+
|
|
260
|
+
if (fullText.toLowerCase().includes(lowerQuery)) {
|
|
261
|
+
// Extract context around match
|
|
262
|
+
const idx = content.toLowerCase().indexOf(lowerQuery);
|
|
263
|
+
const start = Math.max(0, idx - 60);
|
|
264
|
+
const end = Math.min(content.length, idx + query.length + 60);
|
|
265
|
+
let snippet = (start > 0 ? '...' : '') + content.slice(start, end) + (end < content.length ? '...' : '');
|
|
266
|
+
|
|
267
|
+
results.push({ id: section.id, title, snippet, query });
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (results.length === 0) {
|
|
272
|
+
searchResults.innerHTML = `<p class="p-4 text-center text-gray-500">${NO_RESULTS_TEXT}</p>`;
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
searchResults.innerHTML = results.map(r => {
|
|
277
|
+
// Highlight on PLAIN TEXT first, then escape non-match segments.
|
|
278
|
+
// This avoids matching across HTML entity boundaries (e.g. "&").
|
|
279
|
+
const highlighted = highlightText(r.snippet, r.query);
|
|
280
|
+
return `
|
|
281
|
+
<a href="#${r.id}" class="block p-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors" data-search-result="${r.id}">
|
|
282
|
+
<div class="font-medium text-sm text-gray-900 dark:text-white">${escapeHtml(r.title)}</div>
|
|
283
|
+
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">${highlighted}</div>
|
|
284
|
+
</a>
|
|
285
|
+
`;
|
|
286
|
+
}).join('');
|
|
287
|
+
|
|
288
|
+
// Click handler for search results
|
|
289
|
+
searchResults.querySelectorAll('[data-search-result]').forEach(el => {
|
|
290
|
+
el.addEventListener('click', (e) => {
|
|
291
|
+
e.preventDefault();
|
|
292
|
+
const targetId = el.dataset.searchResult;
|
|
293
|
+
closeSearch();
|
|
294
|
+
const target = document.getElementById(targetId);
|
|
295
|
+
if (target) {
|
|
296
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
297
|
+
// Flash highlight
|
|
298
|
+
target.classList.add('bg-primary-50', 'dark:bg-primary-900/20');
|
|
299
|
+
setTimeout(() => target.classList.remove('bg-primary-50', 'dark:bg-primary-900/20'), 2000);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ═══════════════════════════════════════════════════
|
|
306
|
+
// 7. KEYBOARD SHORTCUTS
|
|
307
|
+
// ═══════════════════════════════════════════════════
|
|
308
|
+
document.addEventListener('keydown', (e) => {
|
|
309
|
+
// Ctrl/Cmd + K → open search
|
|
310
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
311
|
+
e.preventDefault();
|
|
312
|
+
openSearch();
|
|
313
|
+
}
|
|
314
|
+
// Escape → close search
|
|
315
|
+
if (e.key === 'Escape') {
|
|
316
|
+
closeSearch();
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// ═══════════════════════════════════════════════════
|
|
321
|
+
// 8. COPY QUOTE
|
|
322
|
+
// ═══════════════════════════════════════════════════
|
|
323
|
+
// Event delegation for copy buttons (data-copy) — no inline onclick needed.
|
|
324
|
+
// Section IDs are read from data attributes, never interpolated into JS strings.
|
|
325
|
+
document.addEventListener('click', (e) => {
|
|
326
|
+
const copyBtn = e.target.closest('[data-copy]');
|
|
327
|
+
if (!copyBtn) return;
|
|
328
|
+
const sectionId = copyBtn.dataset.copy;
|
|
329
|
+
const section = document.getElementById(sectionId);
|
|
330
|
+
if (!section) return;
|
|
331
|
+
const prose = section.querySelector('.prose');
|
|
332
|
+
if (!prose) return;
|
|
333
|
+
const text = prose.innerText;
|
|
334
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
335
|
+
const icon = copyBtn.querySelector('i');
|
|
336
|
+
if (icon) {
|
|
337
|
+
icon.classList.replace('fa-copy', 'fa-check');
|
|
338
|
+
icon.classList.add('text-green-500');
|
|
339
|
+
setTimeout(() => {
|
|
340
|
+
icon.classList.replace('fa-check', 'fa-copy');
|
|
341
|
+
icon.classList.remove('text-green-500');
|
|
342
|
+
}, 2000);
|
|
343
|
+
}
|
|
344
|
+
}).catch(() => {
|
|
345
|
+
// Fallback for HTTP or unsupported browsers
|
|
346
|
+
const textarea = document.createElement('textarea');
|
|
347
|
+
textarea.value = text;
|
|
348
|
+
textarea.style.position = 'fixed';
|
|
349
|
+
textarea.style.opacity = '0';
|
|
350
|
+
document.body.appendChild(textarea);
|
|
351
|
+
textarea.select();
|
|
352
|
+
document.execCommand('copy');
|
|
353
|
+
document.body.removeChild(textarea);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ═══════════════════════════════════════════════════
|
|
358
|
+
// 9. YOUTUBE — handled above DOMContentLoaded (see Section 0)
|
|
359
|
+
// ═══════════════════════════════════════════════════
|
|
360
|
+
|
|
361
|
+
// ═══════════════════════════════════════════════════
|
|
362
|
+
// INIT COMPLETE
|
|
363
|
+
// ═══════════════════════════════════════════════════
|
|
364
|
+
console.log('Transcript site loaded successfully.');
|
|
365
|
+
console.log('Tips: Ctrl+K to search, click timestamps to seek video.');
|
|
366
|
+
});
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### 2. Localization Variable
|
|
370
|
+
|
|
371
|
+
At the top of the search section, define the "no results" text based on detected language:
|
|
372
|
+
|
|
373
|
+
```javascript
|
|
374
|
+
const NO_RESULTS_TEXT = '{no_results_text}'; // "Ничего не найдено" or "No results found"
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### 3. Conditional YouTube Code
|
|
378
|
+
|
|
379
|
+
If no YouTube URL was provided in Step 0:
|
|
380
|
+
- Omit the `seekTo` function
|
|
381
|
+
- Omit the YouTube embed `<div>` from index.html
|
|
382
|
+
- Omit timestamp `<a>` links from section headers
|
|
383
|
+
|
|
384
|
+
### 4. Performance Notes
|
|
385
|
+
|
|
386
|
+
- `requestAnimationFrame` prevents excessive progress bar updates during scroll
|
|
387
|
+
- `IntersectionObserver` for TOC is more performant than scroll-based position checking
|
|
388
|
+
- Search debounce at 300ms prevents UI jank on fast typing
|
|
389
|
+
- `backdrop-blur-sm` may be expensive on mobile — keep it on header and search modal only
|
|
390
|
+
|
|
391
|
+
## Output
|
|
392
|
+
|
|
393
|
+
Files created:
|
|
394
|
+
- `docs/static/app.js` (~10-12 KB)
|
|
395
|
+
|
|
396
|
+
## Checkpoint
|
|
397
|
+
|
|
398
|
+
```
|
|
399
|
+
Step 3/5: Interactivity Complete
|
|
400
|
+
File: docs/static/app.js ({N} KB)
|
|
401
|
+
Features: search, TOC scroll-spy, dark mode, progress bar, copy-quote, mobile nav
|
|
402
|
+
YouTube sync: {yes | no}
|
|
403
|
+
Keyboard shortcuts: Ctrl+K (search), Esc (close)
|
|
404
|
+
|
|
405
|
+
"ok" to continue | "[feedback]" to adjust
|
|
406
|
+
```
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Step 4: Deployment
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Configure GitHub Pages deployment and create project README.
|
|
6
|
+
|
|
7
|
+
## Protocol
|
|
8
|
+
|
|
9
|
+
### 1. Create GitHub Actions Workflow
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
# .github/workflows/deploy.yml
|
|
13
|
+
name: Deploy to GitHub Pages
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
push:
|
|
17
|
+
branches: [main]
|
|
18
|
+
workflow_dispatch:
|
|
19
|
+
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
pages: write
|
|
23
|
+
id-token: write
|
|
24
|
+
|
|
25
|
+
concurrency:
|
|
26
|
+
group: "pages"
|
|
27
|
+
cancel-in-progress: true
|
|
28
|
+
|
|
29
|
+
jobs:
|
|
30
|
+
deploy:
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
environment:
|
|
33
|
+
name: github-pages
|
|
34
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
35
|
+
steps:
|
|
36
|
+
- name: Checkout
|
|
37
|
+
uses: actions/checkout@v4
|
|
38
|
+
|
|
39
|
+
- name: Setup Pages
|
|
40
|
+
uses: actions/configure-pages@v4
|
|
41
|
+
|
|
42
|
+
- name: Upload artifact
|
|
43
|
+
uses: actions/upload-pages-artifact@v3
|
|
44
|
+
with:
|
|
45
|
+
path: 'docs'
|
|
46
|
+
|
|
47
|
+
- name: Deploy to GitHub Pages
|
|
48
|
+
id: deployment
|
|
49
|
+
uses: actions/deploy-pages@v4
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Key points:**
|
|
53
|
+
- No build step needed — `docs/` is served directly
|
|
54
|
+
- `path: 'docs'` points to the static site directory
|
|
55
|
+
- `workflow_dispatch` allows manual deploys from GitHub UI
|
|
56
|
+
- Concurrency group prevents parallel deploys
|
|
57
|
+
|
|
58
|
+
### 2. Create README.md
|
|
59
|
+
|
|
60
|
+
```markdown
|
|
61
|
+
# {TITLE}
|
|
62
|
+
|
|
63
|
+
{DESCRIPTION}
|
|
64
|
+
|
|
65
|
+
## Live Site
|
|
66
|
+
|
|
67
|
+
[{TITLE}](https://{username}.github.io/{repo}/)
|
|
68
|
+
|
|
69
|
+
## Features
|
|
70
|
+
|
|
71
|
+
- Interactive transcript with section navigation
|
|
72
|
+
- Full-text search (Ctrl+K)
|
|
73
|
+
- Dark/Light mode
|
|
74
|
+
- Reading progress bar
|
|
75
|
+
- Mobile-friendly responsive design
|
|
76
|
+
- SEO optimized (Open Graph, Twitter Cards, JSON-LD)
|
|
77
|
+
{if YouTube:}
|
|
78
|
+
- YouTube video embed with timestamp navigation
|
|
79
|
+
{/if}
|
|
80
|
+
|
|
81
|
+
## Source
|
|
82
|
+
|
|
83
|
+
{if YouTube:}
|
|
84
|
+
Original video: [{YOUTUBE_TITLE}]({YOUTUBE_URL})
|
|
85
|
+
{/if}
|
|
86
|
+
|
|
87
|
+
Transcript by {AUTHOR}
|
|
88
|
+
|
|
89
|
+
## Development
|
|
90
|
+
|
|
91
|
+
This is a static site — no build step required.
|
|
92
|
+
Simply edit `docs/index.html` and push to `main`.
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 3. Repository Settings Reminder
|
|
100
|
+
|
|
101
|
+
After pushing, remind the user:
|
|
102
|
+
```
|
|
103
|
+
To enable GitHub Pages:
|
|
104
|
+
1. Go to repository Settings → Pages
|
|
105
|
+
2. Source: "GitHub Actions" (recommended) or "Deploy from a branch" (main, /docs)
|
|
106
|
+
3. The workflow will auto-deploy on next push to main
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 4. Alternative: Branch-based Deploy
|
|
110
|
+
|
|
111
|
+
If the user prefers branch-based deployment instead of Actions:
|
|
112
|
+
- Set GitHub Pages source to: branch `main`, folder `/docs`
|
|
113
|
+
- The `.github/workflows/deploy.yml` is then optional but still recommended for caching
|
|
114
|
+
|
|
115
|
+
## Output
|
|
116
|
+
|
|
117
|
+
Files created:
|
|
118
|
+
- `.github/workflows/deploy.yml`
|
|
119
|
+
- `README.md`
|
|
120
|
+
|
|
121
|
+
## Checkpoint
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
Step 4/5: Deployment Complete
|
|
125
|
+
Files: .github/workflows/deploy.yml, README.md
|
|
126
|
+
Deploy method: GitHub Actions (docs/ folder, zero build)
|
|
127
|
+
Live URL: https://{username}.github.io/{repo}/
|
|
128
|
+
|
|
129
|
+
"ok" to continue | "[feedback]" to adjust
|
|
130
|
+
```
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Step 5: Verification
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Validate the generated static site for correctness, completeness, and quality.
|
|
6
|
+
|
|
7
|
+
## Protocol
|
|
8
|
+
|
|
9
|
+
### 1. File Existence Checks
|
|
10
|
+
|
|
11
|
+
| File | Required | Check |
|
|
12
|
+
|------|----------|-------|
|
|
13
|
+
| `docs/index.html` | Yes | Exists, > 10 KB |
|
|
14
|
+
| `docs/static/app.js` | Yes | Exists, > 5 KB |
|
|
15
|
+
| `docs/static/style.css` | Yes | Exists |
|
|
16
|
+
| `.github/workflows/deploy.yml` | Yes | Exists |
|
|
17
|
+
| `README.md` | Yes | Exists |
|
|
18
|
+
|
|
19
|
+
### 2. HTML Structure Validation
|
|
20
|
+
|
|
21
|
+
Check `docs/index.html` for:
|
|
22
|
+
|
|
23
|
+
- [ ] `<!DOCTYPE html>` declaration present
|
|
24
|
+
- [ ] `<html lang="...">` with correct language attribute
|
|
25
|
+
- [ ] `<meta charset="UTF-8">` present
|
|
26
|
+
- [ ] `<meta name="viewport">` present (mobile responsive)
|
|
27
|
+
- [ ] `<title>` tag not empty
|
|
28
|
+
- [ ] `<meta name="description">` present and not empty
|
|
29
|
+
- [ ] Open Graph meta tags present (`og:title`, `og:description`, `og:type`)
|
|
30
|
+
- [ ] Twitter Card meta tags present (`twitter:card`, `twitter:title`)
|
|
31
|
+
- [ ] JSON-LD `<script type="application/ld+json">` present with valid structure
|
|
32
|
+
- [ ] Tailwind CSS CDN script tag present
|
|
33
|
+
- [ ] Font Awesome CDN link present
|
|
34
|
+
- [ ] `<script src="static/app.js">` present at end of body
|
|
35
|
+
- [ ] `<link rel="stylesheet" href="static/style.css">` present in head
|
|
36
|
+
|
|
37
|
+
### 3. Content Validation
|
|
38
|
+
|
|
39
|
+
- [ ] All sections have unique `id` attributes
|
|
40
|
+
- [ ] Section count matches parsed section count from Step 1
|
|
41
|
+
- [ ] TOC links match section IDs (one `<a data-section="...">` per section)
|
|
42
|
+
- [ ] No empty sections (each section has text content)
|
|
43
|
+
- [ ] No unescaped HTML entities in transcript text
|
|
44
|
+
- [ ] No `{placeholder}` or `[TODO]` text remaining
|
|
45
|
+
|
|
46
|
+
### 4. Interactivity Validation
|
|
47
|
+
|
|
48
|
+
Check `docs/static/app.js` for:
|
|
49
|
+
|
|
50
|
+
- [ ] `DOMContentLoaded` event listener wraps all code
|
|
51
|
+
- [ ] Theme toggle references `#theme-toggle` and `#theme-icon`
|
|
52
|
+
- [ ] Search references `#search-btn`, `#search-modal`, `#search-input`, `#search-results`
|
|
53
|
+
- [ ] TOC scroll-spy uses `IntersectionObserver`
|
|
54
|
+
- [ ] Progress bar references `#progress-bar`
|
|
55
|
+
- [ ] Back-to-top references `#back-to-top`
|
|
56
|
+
- [ ] Event delegation for `[data-copy]` elements (copy via data attributes, no inline onclick)
|
|
57
|
+
- [ ] `escapeHtml` function present (XSS prevention)
|
|
58
|
+
- [ ] `escapeRegex` function present (search regex injection prevention)
|
|
59
|
+
- [ ] `highlightText` function uses string comparison (not regex.test with /g flag)
|
|
60
|
+
- [ ] If YouTube: `seekTo` function defined at top level (before DOMContentLoaded)
|
|
61
|
+
- [ ] If YouTube: event delegation for `[data-seek]` elements (no inline onclick)
|
|
62
|
+
- [ ] If YouTube: `onYouTubeIframeAPIReady` callback defined
|
|
63
|
+
- [ ] No syntax errors (can validate with `node --check docs/static/app.js`)
|
|
64
|
+
|
|
65
|
+
### 4b. Accessibility Validation
|
|
66
|
+
|
|
67
|
+
- [ ] Skip-nav link (`<a href="#main-content">`) present before header
|
|
68
|
+
- [ ] `<header>` has `role="banner"`
|
|
69
|
+
- [ ] `<main>` has `role="main"` and `id="main-content"`
|
|
70
|
+
- [ ] `<nav>` sidebar has `aria-label`
|
|
71
|
+
- [ ] Search modal has `role="dialog"` and `aria-modal="true"`
|
|
72
|
+
- [ ] Icon-only buttons have accessible text (`aria-label` or visible label)
|
|
73
|
+
|
|
74
|
+
### 5. Responsive Design Checks
|
|
75
|
+
|
|
76
|
+
- [ ] Mobile menu button has `lg:hidden` class
|
|
77
|
+
- [ ] Sidebar has `lg:translate-x-0` and `-translate-x-full` for toggle
|
|
78
|
+
- [ ] Content area uses `flex-1 min-w-0` to prevent overflow
|
|
79
|
+
- [ ] YouTube embed uses `aspect-video` for responsive sizing
|
|
80
|
+
- [ ] Touch targets are at least `p-2` (minimum 44px tap area)
|
|
81
|
+
- [ ] Text is readable without horizontal scrolling on 320px width
|
|
82
|
+
|
|
83
|
+
### 6. Dark Mode Checks
|
|
84
|
+
|
|
85
|
+
- [ ] `<html>` has `class` attribute (not `data-theme`) for Tailwind dark mode
|
|
86
|
+
- [ ] All `bg-white` have corresponding `dark:bg-gray-900` (or similar)
|
|
87
|
+
- [ ] All `text-gray-900` have corresponding `dark:text-gray-100` (or similar)
|
|
88
|
+
- [ ] Search modal has dark variants
|
|
89
|
+
- [ ] TOC active state has dark variants
|
|
90
|
+
- [ ] Tailwind config has `darkMode: 'class'`
|
|
91
|
+
|
|
92
|
+
### 7. SEO Checks
|
|
93
|
+
|
|
94
|
+
- [ ] `og:title` matches `<title>`
|
|
95
|
+
- [ ] `og:description` matches `<meta name="description">`
|
|
96
|
+
- [ ] JSON-LD `headline` matches title
|
|
97
|
+
- [ ] JSON-LD `wordCount` is a number > 0
|
|
98
|
+
- [ ] If YouTube: `og:image` points to valid YouTube thumbnail URL
|
|
99
|
+
- [ ] If YouTube: VideoObject JSON-LD includes `embedUrl`
|
|
100
|
+
|
|
101
|
+
### 8. Print Checks
|
|
102
|
+
|
|
103
|
+
- [ ] `style.css` has `@media print` rules
|
|
104
|
+
- [ ] `.no-print` class hides navigation, progress bar, buttons
|
|
105
|
+
- [ ] Print colors are black on white (overrides dark mode)
|
|
106
|
+
- [ ] Section breaks avoid splitting mid-content
|
|
107
|
+
|
|
108
|
+
### 9. Syntax Validation
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Validate JS syntax
|
|
112
|
+
node --check docs/static/app.js
|
|
113
|
+
|
|
114
|
+
# Check HTML is well-formed (basic check)
|
|
115
|
+
grep -c '</html>' docs/index.html # Should be 1
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 10. Browser Preview (Optional)
|
|
119
|
+
|
|
120
|
+
If a local server is available:
|
|
121
|
+
```bash
|
|
122
|
+
cd docs && python3 -m http.server 8080 &
|
|
123
|
+
echo "Preview at http://localhost:8080"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Error Recovery
|
|
127
|
+
|
|
128
|
+
| Error | Fix |
|
|
129
|
+
|-------|-----|
|
|
130
|
+
| Missing section IDs | Regenerate sections with `id="section-{slug}"` |
|
|
131
|
+
| TOC/section mismatch | Rebuild TOC links from actual section elements |
|
|
132
|
+
| JS syntax error | Fix the specific error, re-validate |
|
|
133
|
+
| Missing dark mode classes | Add `dark:` variant for every light class |
|
|
134
|
+
| Broken YouTube embed | Verify VIDEO_ID format (11 chars), check URL encoding |
|
|
135
|
+
| HTML entities in content | Run through `escapeHtml()` |
|
|
136
|
+
|
|
137
|
+
## Output
|
|
138
|
+
|
|
139
|
+
Verification report:
|
|
140
|
+
```
|
|
141
|
+
Step 5/5: Verification Complete
|
|
142
|
+
|
|
143
|
+
File checks: {N}/{N} passed
|
|
144
|
+
HTML structure: {N}/{N} passed
|
|
145
|
+
Content: {N}/{N} passed
|
|
146
|
+
Interactivity: {N}/{N} passed
|
|
147
|
+
Responsive: {N}/{N} passed
|
|
148
|
+
Dark mode: {N}/{N} passed
|
|
149
|
+
SEO: {N}/{N} passed
|
|
150
|
+
Print: {N}/{N} passed
|
|
151
|
+
|
|
152
|
+
Site ready for deployment!
|
|
153
|
+
Push to main to trigger GitHub Pages deploy.
|
|
154
|
+
Live URL: https://{username}.github.io/{repo}/
|
|
155
|
+
```
|