@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,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">&#x2630;</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>