@shaykec/bridge 0.1.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 +65 -0
- package/package.json +26 -0
- package/src/protocol.js +303 -0
- package/src/protocol.test.js +373 -0
- package/src/router.js +149 -0
- package/src/router.test.js +329 -0
- package/src/server.js +497 -0
- package/src/templates.js +155 -0
- package/src/templates.test.js +256 -0
- package/templates/celebrate.html +259 -0
- package/templates/code-playground.html +294 -0
- package/templates/dashboard.html +337 -0
- package/templates/diagram-architecture.html +449 -0
- package/templates/diagram-flow.html +382 -0
- package/templates/diagram-mermaid.html +220 -0
- package/templates/quiz-drag-order.html +375 -0
- package/templates/quiz-fill-blank.html +468 -0
- package/templates/quiz-matching.html +501 -0
- package/templates/quiz-timed-choice.html +361 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>ClaudeTeach — Drag to Order</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
11
|
+
background: #0d1117;
|
|
12
|
+
color: #c9d1d9;
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
min-height: 100vh;
|
|
18
|
+
padding: 2rem;
|
|
19
|
+
}
|
|
20
|
+
h2 {
|
|
21
|
+
color: #58a6ff;
|
|
22
|
+
margin-bottom: 0.5rem;
|
|
23
|
+
font-size: 1.2rem;
|
|
24
|
+
text-align: center;
|
|
25
|
+
}
|
|
26
|
+
.question {
|
|
27
|
+
margin-bottom: 1.5rem;
|
|
28
|
+
font-size: 1.1rem;
|
|
29
|
+
text-align: center;
|
|
30
|
+
max-width: 600px;
|
|
31
|
+
line-height: 1.5;
|
|
32
|
+
}
|
|
33
|
+
.items-container {
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
gap: 0.5rem;
|
|
37
|
+
width: 100%;
|
|
38
|
+
max-width: 500px;
|
|
39
|
+
margin-bottom: 1.5rem;
|
|
40
|
+
}
|
|
41
|
+
.drag-item {
|
|
42
|
+
background: #161b22;
|
|
43
|
+
border: 1px solid #30363d;
|
|
44
|
+
border-radius: 8px;
|
|
45
|
+
padding: 0.8rem 1rem;
|
|
46
|
+
cursor: grab;
|
|
47
|
+
user-select: none;
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: 0.75rem;
|
|
51
|
+
transition: transform 0.15s, box-shadow 0.15s, border-color 0.15s;
|
|
52
|
+
font-size: 0.95rem;
|
|
53
|
+
}
|
|
54
|
+
.drag-item:hover {
|
|
55
|
+
border-color: #58a6ff;
|
|
56
|
+
}
|
|
57
|
+
.drag-item.dragging {
|
|
58
|
+
opacity: 0.5;
|
|
59
|
+
transform: scale(1.02);
|
|
60
|
+
box-shadow: 0 4px 20px rgba(88, 166, 255, 0.2);
|
|
61
|
+
cursor: grabbing;
|
|
62
|
+
}
|
|
63
|
+
.drag-item.over {
|
|
64
|
+
border-color: #58a6ff;
|
|
65
|
+
background: #1c2333;
|
|
66
|
+
}
|
|
67
|
+
.drag-handle {
|
|
68
|
+
color: #484f58;
|
|
69
|
+
font-size: 1.2rem;
|
|
70
|
+
flex-shrink: 0;
|
|
71
|
+
}
|
|
72
|
+
.drag-number {
|
|
73
|
+
background: #30363d;
|
|
74
|
+
color: #8b949e;
|
|
75
|
+
width: 24px;
|
|
76
|
+
height: 24px;
|
|
77
|
+
border-radius: 50%;
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
justify-content: center;
|
|
81
|
+
font-size: 0.75rem;
|
|
82
|
+
font-weight: 600;
|
|
83
|
+
flex-shrink: 0;
|
|
84
|
+
}
|
|
85
|
+
.btn {
|
|
86
|
+
background: #238636;
|
|
87
|
+
color: #fff;
|
|
88
|
+
border: none;
|
|
89
|
+
padding: 0.7rem 2rem;
|
|
90
|
+
border-radius: 6px;
|
|
91
|
+
font-size: 1rem;
|
|
92
|
+
cursor: pointer;
|
|
93
|
+
font-weight: 500;
|
|
94
|
+
transition: background 0.15s;
|
|
95
|
+
}
|
|
96
|
+
.btn:hover { background: #2ea043; }
|
|
97
|
+
.btn:disabled {
|
|
98
|
+
background: #21262d;
|
|
99
|
+
color: #484f58;
|
|
100
|
+
cursor: not-allowed;
|
|
101
|
+
}
|
|
102
|
+
.result {
|
|
103
|
+
margin-top: 1.5rem;
|
|
104
|
+
padding: 1rem 1.5rem;
|
|
105
|
+
border-radius: 8px;
|
|
106
|
+
font-weight: 500;
|
|
107
|
+
text-align: center;
|
|
108
|
+
display: none;
|
|
109
|
+
}
|
|
110
|
+
.result.correct {
|
|
111
|
+
display: block;
|
|
112
|
+
background: rgba(35, 134, 54, 0.15);
|
|
113
|
+
border: 1px solid #238636;
|
|
114
|
+
color: #3fb950;
|
|
115
|
+
}
|
|
116
|
+
.result.incorrect {
|
|
117
|
+
display: block;
|
|
118
|
+
background: rgba(248, 81, 73, 0.15);
|
|
119
|
+
border: 1px solid #f85149;
|
|
120
|
+
color: #f85149;
|
|
121
|
+
}
|
|
122
|
+
</style>
|
|
123
|
+
</head>
|
|
124
|
+
<body>
|
|
125
|
+
<h2>Put these in the correct order</h2>
|
|
126
|
+
<div class="question" id="question">Loading...</div>
|
|
127
|
+
<div class="items-container" id="items"></div>
|
|
128
|
+
<button class="btn" id="submitBtn" onclick="checkAnswer()">Submit</button>
|
|
129
|
+
<div class="result" id="result"></div>
|
|
130
|
+
|
|
131
|
+
<div aria-live="polite" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)" id="announcer"></div>
|
|
132
|
+
|
|
133
|
+
<script>
|
|
134
|
+
/* --- Bridge connection (WebSocket + REST fallback) --- */
|
|
135
|
+
var ws = null;
|
|
136
|
+
function connectBridge() {
|
|
137
|
+
try {
|
|
138
|
+
ws = new WebSocket('ws://' + location.host + '/ws');
|
|
139
|
+
ws.onopen = function() {
|
|
140
|
+
ws.send(JSON.stringify({
|
|
141
|
+
v: 1, type: 'sys:connect',
|
|
142
|
+
payload: { clientType: 'template' },
|
|
143
|
+
source: 'template', timestamp: Date.now()
|
|
144
|
+
}));
|
|
145
|
+
};
|
|
146
|
+
ws.onerror = function() { ws = null; };
|
|
147
|
+
ws.onclose = function() { ws = null; };
|
|
148
|
+
} catch(e) { ws = null; }
|
|
149
|
+
}
|
|
150
|
+
connectBridge();
|
|
151
|
+
|
|
152
|
+
function sendResultToBridge(payload) {
|
|
153
|
+
var msg = {
|
|
154
|
+
v: 1, type: 'event:quiz-answer',
|
|
155
|
+
payload: payload,
|
|
156
|
+
source: 'template', timestamp: Date.now()
|
|
157
|
+
};
|
|
158
|
+
if (ws && ws.readyState === 1) { ws.send(JSON.stringify(msg)); }
|
|
159
|
+
try {
|
|
160
|
+
fetch('/api/event', {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: { 'Content-Type': 'application/json' },
|
|
163
|
+
body: JSON.stringify(msg)
|
|
164
|
+
}).catch(function(){});
|
|
165
|
+
} catch(e) {}
|
|
166
|
+
if (window.parent !== window) { window.parent.postMessage(msg, '*'); }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Data is injected by the bridge server or passed via postMessage
|
|
170
|
+
let quizData = {
|
|
171
|
+
question: 'Order the steps:',
|
|
172
|
+
items: ['Step 1', 'Step 2', 'Step 3', 'Step 4'],
|
|
173
|
+
correctOrder: [0, 1, 2, 3],
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
let dragSrcIndex = null;
|
|
177
|
+
|
|
178
|
+
function init(data) {
|
|
179
|
+
if (data) quizData = { ...quizData, ...data };
|
|
180
|
+
quizData._startTime = Date.now();
|
|
181
|
+
document.getElementById('question').textContent = quizData.question;
|
|
182
|
+
renderItems();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function renderItems() {
|
|
186
|
+
const container = document.getElementById('items');
|
|
187
|
+
container.innerHTML = '';
|
|
188
|
+
|
|
189
|
+
// Shuffle if correctOrder is provided (items are in correct order, shuffle for display)
|
|
190
|
+
const indices = quizData.items.map((_, i) => i);
|
|
191
|
+
if (!quizData._shuffled) {
|
|
192
|
+
for (let i = indices.length - 1; i > 0; i--) {
|
|
193
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
194
|
+
[indices[i], indices[j]] = [indices[j], indices[i]];
|
|
195
|
+
}
|
|
196
|
+
quizData._displayOrder = indices;
|
|
197
|
+
quizData._shuffled = true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const order = quizData._displayOrder || indices;
|
|
201
|
+
|
|
202
|
+
order.forEach((itemIndex, displayPos) => {
|
|
203
|
+
const div = document.createElement('div');
|
|
204
|
+
div.className = 'drag-item';
|
|
205
|
+
div.draggable = true;
|
|
206
|
+
div.dataset.index = displayPos;
|
|
207
|
+
|
|
208
|
+
div.tabIndex = 0;
|
|
209
|
+
div.setAttribute('role', 'listitem');
|
|
210
|
+
div.setAttribute('aria-label', `Item ${displayPos + 1}: ${quizData.items[itemIndex]}. Use arrow keys to reorder.`);
|
|
211
|
+
|
|
212
|
+
div.innerHTML = `
|
|
213
|
+
<span class="drag-handle">☰</span>
|
|
214
|
+
<span class="drag-number">${displayPos + 1}</span>
|
|
215
|
+
<span>${quizData.items[itemIndex]}</span>
|
|
216
|
+
`;
|
|
217
|
+
|
|
218
|
+
div.addEventListener('dragstart', onDragStart);
|
|
219
|
+
div.addEventListener('dragover', onDragOver);
|
|
220
|
+
div.addEventListener('dragenter', onDragEnter);
|
|
221
|
+
div.addEventListener('dragleave', onDragLeave);
|
|
222
|
+
div.addEventListener('drop', onDrop);
|
|
223
|
+
div.addEventListener('dragend', onDragEnd);
|
|
224
|
+
|
|
225
|
+
// Touch support
|
|
226
|
+
div.addEventListener('touchstart', onTouchStart, { passive: false });
|
|
227
|
+
div.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
228
|
+
div.addEventListener('touchend', onTouchEnd);
|
|
229
|
+
|
|
230
|
+
container.appendChild(div);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function onDragStart(e) {
|
|
235
|
+
dragSrcIndex = parseInt(e.currentTarget.dataset.index);
|
|
236
|
+
e.currentTarget.classList.add('dragging');
|
|
237
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function onDragOver(e) {
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
e.dataTransfer.dropEffect = 'move';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function onDragEnter(e) {
|
|
246
|
+
e.currentTarget.classList.add('over');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function onDragLeave(e) {
|
|
250
|
+
e.currentTarget.classList.remove('over');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function onDrop(e) {
|
|
254
|
+
e.preventDefault();
|
|
255
|
+
const targetIndex = parseInt(e.currentTarget.dataset.index);
|
|
256
|
+
e.currentTarget.classList.remove('over');
|
|
257
|
+
|
|
258
|
+
if (dragSrcIndex !== null && dragSrcIndex !== targetIndex) {
|
|
259
|
+
// Swap items in display order
|
|
260
|
+
const order = quizData._displayOrder;
|
|
261
|
+
[order[dragSrcIndex], order[targetIndex]] = [order[targetIndex], order[dragSrcIndex]];
|
|
262
|
+
renderItems();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function onDragEnd(e) {
|
|
267
|
+
document.querySelectorAll('.drag-item').forEach(el => {
|
|
268
|
+
el.classList.remove('dragging', 'over');
|
|
269
|
+
});
|
|
270
|
+
dragSrcIndex = null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Touch drag support
|
|
274
|
+
let touchStartY = 0;
|
|
275
|
+
let touchElement = null;
|
|
276
|
+
|
|
277
|
+
function onTouchStart(e) {
|
|
278
|
+
touchElement = e.currentTarget;
|
|
279
|
+
dragSrcIndex = parseInt(touchElement.dataset.index);
|
|
280
|
+
touchStartY = e.touches[0].clientY;
|
|
281
|
+
touchElement.classList.add('dragging');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function onTouchMove(e) {
|
|
285
|
+
e.preventDefault();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function onTouchEnd(e) {
|
|
289
|
+
if (!touchElement) return;
|
|
290
|
+
const touchEndY = e.changedTouches[0].clientY;
|
|
291
|
+
const items = document.querySelectorAll('.drag-item');
|
|
292
|
+
let targetIndex = dragSrcIndex;
|
|
293
|
+
|
|
294
|
+
items.forEach((item, i) => {
|
|
295
|
+
const rect = item.getBoundingClientRect();
|
|
296
|
+
if (touchEndY >= rect.top && touchEndY <= rect.bottom) {
|
|
297
|
+
targetIndex = i;
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
if (dragSrcIndex !== null && dragSrcIndex !== targetIndex) {
|
|
302
|
+
const order = quizData._displayOrder;
|
|
303
|
+
[order[dragSrcIndex], order[targetIndex]] = [order[targetIndex], order[dragSrcIndex]];
|
|
304
|
+
renderItems();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
touchElement.classList.remove('dragging');
|
|
308
|
+
touchElement = null;
|
|
309
|
+
dragSrcIndex = null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function checkAnswer() {
|
|
313
|
+
const order = quizData._displayOrder;
|
|
314
|
+
const correct = quizData.correctOrder
|
|
315
|
+
? order.every((item, i) => item === quizData.correctOrder[i])
|
|
316
|
+
: false;
|
|
317
|
+
|
|
318
|
+
const resultEl = document.getElementById('result');
|
|
319
|
+
resultEl.className = 'result ' + (correct ? 'correct' : 'incorrect');
|
|
320
|
+
resultEl.textContent = correct
|
|
321
|
+
? 'Correct! Great job!'
|
|
322
|
+
: 'Not quite — try rearranging the items.';
|
|
323
|
+
|
|
324
|
+
document.getElementById('submitBtn').disabled = correct;
|
|
325
|
+
|
|
326
|
+
// Send result back via bridge (WebSocket + REST + postMessage)
|
|
327
|
+
sendResultToBridge({
|
|
328
|
+
quizType: 'drag-order',
|
|
329
|
+
answer: order.map(i => quizData.items[i]),
|
|
330
|
+
correct,
|
|
331
|
+
timeMs: Date.now() - (quizData._startTime || Date.now()),
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
document.getElementById('announcer').textContent =
|
|
335
|
+
correct ? 'Correct! Great job!' : 'Not quite. Try rearranging the items.';
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Keyboard support: move items with arrow keys
|
|
339
|
+
document.addEventListener('keydown', function(e) {
|
|
340
|
+
const focused = document.activeElement;
|
|
341
|
+
if (!focused || !focused.classList.contains('drag-item')) return;
|
|
342
|
+
const items = Array.from(document.querySelectorAll('.drag-item'));
|
|
343
|
+
const idx = items.indexOf(focused);
|
|
344
|
+
if (idx < 0) return;
|
|
345
|
+
if (e.key === 'ArrowUp' && idx > 0) {
|
|
346
|
+
e.preventDefault();
|
|
347
|
+
const order = quizData._displayOrder;
|
|
348
|
+
[order[idx], order[idx - 1]] = [order[idx - 1], order[idx]];
|
|
349
|
+
renderItems();
|
|
350
|
+
document.querySelectorAll('.drag-item')[idx - 1]?.focus();
|
|
351
|
+
} else if (e.key === 'ArrowDown' && idx < items.length - 1) {
|
|
352
|
+
e.preventDefault();
|
|
353
|
+
const order = quizData._displayOrder;
|
|
354
|
+
[order[idx], order[idx + 1]] = [order[idx + 1], order[idx]];
|
|
355
|
+
renderItems();
|
|
356
|
+
document.querySelectorAll('.drag-item')[idx + 1]?.focus();
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Listen for data injection via postMessage
|
|
361
|
+
window.addEventListener('message', (e) => {
|
|
362
|
+
if (e.data && e.data.type === 'quiz-data') {
|
|
363
|
+
init(e.data.payload);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Auto-initialize with embedded data if present
|
|
368
|
+
if (window.__QUIZ_DATA__) {
|
|
369
|
+
init(window.__QUIZ_DATA__);
|
|
370
|
+
} else {
|
|
371
|
+
init();
|
|
372
|
+
}
|
|
373
|
+
</script>
|
|
374
|
+
</body>
|
|
375
|
+
</html>
|