@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.
@@ -0,0 +1,501 @@
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 — Matching Quiz</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
+ .matching-area {
34
+ display: flex;
35
+ gap: 3rem;
36
+ max-width: 700px;
37
+ width: 100%;
38
+ margin-bottom: 1.5rem;
39
+ justify-content: center;
40
+ }
41
+ .column {
42
+ display: flex;
43
+ flex-direction: column;
44
+ gap: 0.75rem;
45
+ flex: 1;
46
+ max-width: 280px;
47
+ }
48
+ .column-label {
49
+ font-size: 0.8rem;
50
+ text-transform: uppercase;
51
+ letter-spacing: 0.05em;
52
+ color: #8b949e;
53
+ text-align: center;
54
+ margin-bottom: 0.25rem;
55
+ }
56
+ .match-item {
57
+ background: #161b22;
58
+ border: 2px solid #30363d;
59
+ border-radius: 8px;
60
+ padding: 0.7rem 1rem;
61
+ cursor: pointer;
62
+ user-select: none;
63
+ text-align: center;
64
+ font-size: 0.95rem;
65
+ transition: border-color 0.15s, background 0.15s, transform 0.1s;
66
+ position: relative;
67
+ }
68
+ .match-item:hover {
69
+ border-color: #58a6ff;
70
+ background: #1c2333;
71
+ }
72
+ .match-item:focus {
73
+ outline: 2px solid #58a6ff;
74
+ outline-offset: 2px;
75
+ }
76
+ .match-item.selected {
77
+ border-color: #58a6ff;
78
+ background: #1c2333;
79
+ transform: scale(1.02);
80
+ }
81
+ .match-item.matched {
82
+ border-color: #238636;
83
+ background: rgba(35, 134, 54, 0.15);
84
+ cursor: default;
85
+ }
86
+ .match-item.matched::after {
87
+ content: attr(data-pair-number);
88
+ position: absolute;
89
+ top: -8px;
90
+ right: -8px;
91
+ background: #238636;
92
+ color: #fff;
93
+ width: 20px;
94
+ height: 20px;
95
+ border-radius: 50%;
96
+ font-size: 0.7rem;
97
+ display: flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ font-weight: 600;
101
+ }
102
+ .match-item.incorrect {
103
+ border-color: #f85149;
104
+ background: rgba(248, 81, 73, 0.15);
105
+ animation: shake 0.3s ease;
106
+ }
107
+ @keyframes shake {
108
+ 0%, 100% { transform: translateX(0); }
109
+ 25% { transform: translateX(-4px); }
110
+ 75% { transform: translateX(4px); }
111
+ }
112
+ .connections-svg {
113
+ position: absolute;
114
+ top: 0;
115
+ left: 0;
116
+ width: 100%;
117
+ height: 100%;
118
+ pointer-events: none;
119
+ z-index: 1;
120
+ }
121
+ .matching-wrapper {
122
+ position: relative;
123
+ width: 100%;
124
+ max-width: 700px;
125
+ }
126
+ .btn {
127
+ background: #238636;
128
+ color: #fff;
129
+ border: none;
130
+ padding: 0.7rem 2rem;
131
+ border-radius: 6px;
132
+ font-size: 1rem;
133
+ cursor: pointer;
134
+ font-weight: 500;
135
+ transition: background 0.15s;
136
+ }
137
+ .btn:hover { background: #2ea043; }
138
+ .btn:focus { outline: 2px solid #58a6ff; outline-offset: 2px; }
139
+ .btn:disabled {
140
+ background: #21262d;
141
+ color: #484f58;
142
+ cursor: not-allowed;
143
+ }
144
+ .btn-row {
145
+ display: flex;
146
+ gap: 0.75rem;
147
+ margin-bottom: 1rem;
148
+ }
149
+ .btn-secondary {
150
+ background: #21262d;
151
+ color: #c9d1d9;
152
+ border: 1px solid #30363d;
153
+ }
154
+ .btn-secondary:hover { background: #30363d; }
155
+ .result {
156
+ margin-top: 1rem;
157
+ padding: 1rem 1.5rem;
158
+ border-radius: 8px;
159
+ font-weight: 500;
160
+ text-align: center;
161
+ display: none;
162
+ }
163
+ .result.correct {
164
+ display: block;
165
+ background: rgba(35, 134, 54, 0.15);
166
+ border: 1px solid #238636;
167
+ color: #3fb950;
168
+ }
169
+ .result.incorrect {
170
+ display: block;
171
+ background: rgba(248, 81, 73, 0.15);
172
+ border: 1px solid #f85149;
173
+ color: #f85149;
174
+ }
175
+ .score-display {
176
+ color: #8b949e;
177
+ font-size: 0.85rem;
178
+ margin-top: 0.5rem;
179
+ }
180
+ .sr-only {
181
+ position: absolute;
182
+ width: 1px;
183
+ height: 1px;
184
+ padding: 0;
185
+ margin: -1px;
186
+ overflow: hidden;
187
+ clip: rect(0, 0, 0, 0);
188
+ border: 0;
189
+ }
190
+ </style>
191
+ </head>
192
+ <body>
193
+ <h2>Match the Items</h2>
194
+ <div class="question" id="question">{{question}}</div>
195
+ <div aria-live="polite" class="sr-only" id="announcer"></div>
196
+ <div class="matching-wrapper" id="matchingWrapper">
197
+ <div class="matching-area" id="matchingArea">
198
+ <div class="column" id="leftColumn">
199
+ <div class="column-label">Terms</div>
200
+ </div>
201
+ <div class="column" id="rightColumn">
202
+ <div class="column-label">Definitions</div>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ <div class="btn-row">
207
+ <button class="btn" id="submitBtn" onclick="submitAnswer()" disabled>Submit</button>
208
+ <button class="btn btn-secondary" onclick="resetMatches()">Reset</button>
209
+ </div>
210
+ <div class="result" id="result"></div>
211
+ <div class="score-display" id="scoreDisplay"></div>
212
+
213
+ <script>
214
+ /* --- Bridge connection (WebSocket + REST fallback) --- */
215
+ let ws = null;
216
+ function connectBridge() {
217
+ try {
218
+ ws = new WebSocket('ws://' + location.host + '/ws');
219
+ ws.onopen = function() {
220
+ ws.send(JSON.stringify({
221
+ v: 1, type: 'sys:connect',
222
+ payload: { clientType: 'template' },
223
+ source: 'template', timestamp: Date.now()
224
+ }));
225
+ };
226
+ ws.onerror = function() { ws = null; };
227
+ ws.onclose = function() { ws = null; };
228
+ } catch(e) { ws = null; }
229
+ }
230
+ connectBridge();
231
+
232
+ function sendResult(payload) {
233
+ var msg = {
234
+ v: 1, type: 'event:quiz-answer',
235
+ payload: payload,
236
+ source: 'template', timestamp: Date.now()
237
+ };
238
+ if (ws && ws.readyState === 1) {
239
+ ws.send(JSON.stringify(msg));
240
+ }
241
+ // REST fallback
242
+ try {
243
+ fetch('/api/event', {
244
+ method: 'POST',
245
+ headers: { 'Content-Type': 'application/json' },
246
+ body: JSON.stringify(msg)
247
+ }).catch(function(){});
248
+ } catch(e) {}
249
+ // postMessage for iframe embedding
250
+ if (window.parent !== window) {
251
+ window.parent.postMessage(msg, '*');
252
+ }
253
+ }
254
+
255
+ /* --- Quiz data --- */
256
+ var quizData = {
257
+ question: 'Match each term with its definition',
258
+ left: ['Term A', 'Term B', 'Term C', 'Term D'],
259
+ right: ['Definition A', 'Definition B', 'Definition C', 'Definition D'],
260
+ correctPairs: { 0: 0, 1: 1, 2: 2, 3: 3 }
261
+ };
262
+
263
+ var selectedLeft = null;
264
+ var selectedRight = null;
265
+ var matches = {}; // leftIndex -> rightIndex
266
+ var matchCount = 0;
267
+ var startTime = Date.now();
268
+
269
+ function init(data) {
270
+ if (data) quizData = Object.assign({}, quizData, data);
271
+
272
+ // Parse items array if provided instead of left/right
273
+ if (quizData.items && !quizData.left) {
274
+ quizData.left = quizData.items.map(function(item) { return item.term || item.left || item[0]; });
275
+ quizData.right = quizData.items.map(function(item) { return item.definition || item.right || item[1]; });
276
+ quizData.correctPairs = {};
277
+ quizData.items.forEach(function(_, i) { quizData.correctPairs[i] = i; });
278
+ }
279
+
280
+ document.getElementById('question').textContent = quizData.question;
281
+ startTime = Date.now();
282
+ matches = {};
283
+ matchCount = 0;
284
+ selectedLeft = null;
285
+ selectedRight = null;
286
+ renderColumns();
287
+ }
288
+
289
+ function renderColumns() {
290
+ var leftCol = document.getElementById('leftColumn');
291
+ var rightCol = document.getElementById('rightColumn');
292
+ leftCol.innerHTML = '<div class="column-label">Terms</div>';
293
+ rightCol.innerHTML = '<div class="column-label">Definitions</div>';
294
+
295
+ // Shuffle right side for display
296
+ var rightIndices = quizData.right.map(function(_, i) { return i; });
297
+ if (!quizData._shuffledRight) {
298
+ for (var i = rightIndices.length - 1; i > 0; i--) {
299
+ var j = Math.floor(Math.random() * (i + 1));
300
+ var tmp = rightIndices[i];
301
+ rightIndices[i] = rightIndices[j];
302
+ rightIndices[j] = tmp;
303
+ }
304
+ quizData._shuffledRight = rightIndices;
305
+ }
306
+
307
+ quizData.left.forEach(function(text, idx) {
308
+ var div = document.createElement('div');
309
+ div.className = 'match-item';
310
+ div.textContent = text;
311
+ div.dataset.side = 'left';
312
+ div.dataset.index = idx;
313
+ div.tabIndex = 0;
314
+ div.setAttribute('role', 'button');
315
+ div.setAttribute('aria-label', 'Term: ' + text);
316
+ if (matches[idx] !== undefined) {
317
+ div.classList.add('matched');
318
+ div.dataset.pairNumber = Object.keys(matches).indexOf(String(idx)) + 1;
319
+ }
320
+ div.addEventListener('click', function() { selectItem('left', idx); });
321
+ div.addEventListener('keydown', function(e) {
322
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectItem('left', idx); }
323
+ });
324
+ leftCol.appendChild(div);
325
+ });
326
+
327
+ var shuffled = quizData._shuffledRight;
328
+ shuffled.forEach(function(origIdx) {
329
+ var div = document.createElement('div');
330
+ div.className = 'match-item';
331
+ div.textContent = quizData.right[origIdx];
332
+ div.dataset.side = 'right';
333
+ div.dataset.index = origIdx;
334
+ div.tabIndex = 0;
335
+ div.setAttribute('role', 'button');
336
+ div.setAttribute('aria-label', 'Definition: ' + quizData.right[origIdx]);
337
+ // Check if this right item is already matched
338
+ var isMatched = false;
339
+ for (var key in matches) {
340
+ if (matches[key] === origIdx) { isMatched = true; break; }
341
+ }
342
+ if (isMatched) {
343
+ div.classList.add('matched');
344
+ }
345
+ div.addEventListener('click', function() { selectItem('right', origIdx); });
346
+ div.addEventListener('keydown', function(e) {
347
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectItem('right', origIdx); }
348
+ });
349
+ rightCol.appendChild(div);
350
+ });
351
+ }
352
+
353
+ function selectItem(side, index) {
354
+ // Don't allow selecting already matched items
355
+ if (side === 'left' && matches[index] !== undefined) return;
356
+ if (side === 'right') {
357
+ for (var key in matches) {
358
+ if (matches[key] === index) return;
359
+ }
360
+ }
361
+
362
+ if (side === 'left') {
363
+ selectedLeft = index;
364
+ highlightSelection();
365
+ } else {
366
+ selectedRight = index;
367
+ highlightSelection();
368
+ }
369
+
370
+ // If both sides selected, make the match
371
+ if (selectedLeft !== null && selectedRight !== null) {
372
+ makeMatch(selectedLeft, selectedRight);
373
+ }
374
+ }
375
+
376
+ function highlightSelection() {
377
+ document.querySelectorAll('.match-item').forEach(function(el) {
378
+ if (!el.classList.contains('matched')) {
379
+ el.classList.remove('selected');
380
+ }
381
+ });
382
+ if (selectedLeft !== null) {
383
+ var leftItem = document.querySelector('.match-item[data-side="left"][data-index="' + selectedLeft + '"]');
384
+ if (leftItem) leftItem.classList.add('selected');
385
+ }
386
+ if (selectedRight !== null) {
387
+ var rightItem = document.querySelector('.match-item[data-side="right"][data-index="' + selectedRight + '"]');
388
+ if (rightItem) rightItem.classList.add('selected');
389
+ }
390
+ }
391
+
392
+ function makeMatch(leftIdx, rightIdx) {
393
+ matches[leftIdx] = rightIdx;
394
+ matchCount++;
395
+
396
+ var leftItem = document.querySelector('.match-item[data-side="left"][data-index="' + leftIdx + '"]');
397
+ var rightItem = document.querySelector('.match-item[data-side="right"][data-index="' + rightIdx + '"]');
398
+
399
+ if (leftItem) {
400
+ leftItem.classList.remove('selected');
401
+ leftItem.classList.add('matched');
402
+ leftItem.dataset.pairNumber = matchCount;
403
+ }
404
+ if (rightItem) {
405
+ rightItem.classList.remove('selected');
406
+ rightItem.classList.add('matched');
407
+ rightItem.dataset.pairNumber = matchCount;
408
+ }
409
+
410
+ announce('Matched: ' + quizData.left[leftIdx] + ' with ' + quizData.right[rightIdx]);
411
+
412
+ selectedLeft = null;
413
+ selectedRight = null;
414
+
415
+ // Enable submit when all matched
416
+ if (Object.keys(matches).length === quizData.left.length) {
417
+ document.getElementById('submitBtn').disabled = false;
418
+ }
419
+ }
420
+
421
+ function resetMatches() {
422
+ matches = {};
423
+ matchCount = 0;
424
+ selectedLeft = null;
425
+ selectedRight = null;
426
+ document.getElementById('result').className = 'result';
427
+ document.getElementById('result').style.display = 'none';
428
+ document.getElementById('scoreDisplay').textContent = '';
429
+ document.getElementById('submitBtn').disabled = true;
430
+ renderColumns();
431
+ announce('All matches reset');
432
+ }
433
+
434
+ function submitAnswer() {
435
+ var correctCount = 0;
436
+ var totalPairs = quizData.left.length;
437
+
438
+ for (var leftIdx in matches) {
439
+ var rightIdx = matches[leftIdx];
440
+ if (quizData.correctPairs[leftIdx] === rightIdx) {
441
+ correctCount++;
442
+ }
443
+ }
444
+
445
+ var allCorrect = correctCount === totalPairs;
446
+ var timeMs = Date.now() - startTime;
447
+
448
+ var resultEl = document.getElementById('result');
449
+ resultEl.className = 'result ' + (allCorrect ? 'correct' : 'incorrect');
450
+ resultEl.textContent = allCorrect
451
+ ? 'Perfect! All matches are correct!'
452
+ : correctCount + ' of ' + totalPairs + ' matches correct.';
453
+
454
+ document.getElementById('scoreDisplay').textContent =
455
+ 'Score: ' + correctCount + '/' + totalPairs + ' | Time: ' + (timeMs / 1000).toFixed(1) + 's';
456
+
457
+ document.getElementById('submitBtn').disabled = true;
458
+
459
+ // Highlight incorrect matches
460
+ if (!allCorrect) {
461
+ for (var leftIdx in matches) {
462
+ if (quizData.correctPairs[leftIdx] !== matches[leftIdx]) {
463
+ var leftItem = document.querySelector('.match-item[data-side="left"][data-index="' + leftIdx + '"]');
464
+ var rightItem = document.querySelector('.match-item[data-side="right"][data-index="' + matches[leftIdx] + '"]');
465
+ if (leftItem) { leftItem.classList.remove('matched'); leftItem.classList.add('incorrect'); }
466
+ if (rightItem) { rightItem.classList.remove('matched'); rightItem.classList.add('incorrect'); }
467
+ }
468
+ }
469
+ }
470
+
471
+ sendResult({
472
+ quizType: 'matching',
473
+ answer: matches,
474
+ correct: allCorrect,
475
+ score: correctCount,
476
+ total: totalPairs,
477
+ timeMs: timeMs
478
+ });
479
+
480
+ announce(allCorrect ? 'All matches correct!' : correctCount + ' of ' + totalPairs + ' correct');
481
+ }
482
+
483
+ function announce(msg) {
484
+ document.getElementById('announcer').textContent = msg;
485
+ }
486
+
487
+ /* --- Initialization --- */
488
+ window.addEventListener('message', function(e) {
489
+ if (e.data && e.data.type === 'quiz-data') {
490
+ init(e.data.payload);
491
+ }
492
+ });
493
+
494
+ if (window.__QUIZ_DATA__) {
495
+ init(window.__QUIZ_DATA__);
496
+ } else {
497
+ init();
498
+ }
499
+ </script>
500
+ </body>
501
+ </html>