tessera-learn 0.0.1

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 (71) hide show
  1. package/AGENTS.md +1228 -0
  2. package/LICENSE +21 -0
  3. package/README.md +21 -0
  4. package/dist/plugin/index.d.ts +7 -0
  5. package/dist/plugin/index.d.ts.map +1 -0
  6. package/dist/plugin/index.js +1239 -0
  7. package/dist/plugin/index.js.map +1 -0
  8. package/package.json +77 -0
  9. package/src/archiver.d.ts +27 -0
  10. package/src/components/Accordion.svelte +32 -0
  11. package/src/components/AccordionItem.svelte +144 -0
  12. package/src/components/Audio.svelte +38 -0
  13. package/src/components/Callout.svelte +81 -0
  14. package/src/components/Carousel.svelte +194 -0
  15. package/src/components/CarouselSlide.svelte +32 -0
  16. package/src/components/DefaultLayout.svelte +108 -0
  17. package/src/components/FillInTheBlank.svelte +345 -0
  18. package/src/components/Image.svelte +47 -0
  19. package/src/components/Matching.svelte +513 -0
  20. package/src/components/MultipleChoice.svelte +363 -0
  21. package/src/components/Quiz.svelte +569 -0
  22. package/src/components/RevealModal.svelte +228 -0
  23. package/src/components/Sorting.svelte +663 -0
  24. package/src/components/Video.svelte +118 -0
  25. package/src/components/index.ts +15 -0
  26. package/src/components/quiz-payload.ts +71 -0
  27. package/src/components/util.ts +24 -0
  28. package/src/index.ts +56 -0
  29. package/src/plugin/export.ts +264 -0
  30. package/src/plugin/index.ts +464 -0
  31. package/src/plugin/layout.ts +55 -0
  32. package/src/plugin/manifest.ts +330 -0
  33. package/src/plugin/quiz.ts +65 -0
  34. package/src/plugin/validation.ts +838 -0
  35. package/src/runtime/App.svelte +435 -0
  36. package/src/runtime/ErrorPage.svelte +14 -0
  37. package/src/runtime/LoadingSkeleton.svelte +26 -0
  38. package/src/runtime/Sidebar.svelte +76 -0
  39. package/src/runtime/access.ts +55 -0
  40. package/src/runtime/adapters/cmi5.ts +341 -0
  41. package/src/runtime/adapters/discovery.ts +38 -0
  42. package/src/runtime/adapters/index.ts +99 -0
  43. package/src/runtime/adapters/retry.ts +284 -0
  44. package/src/runtime/adapters/scorm12.ts +172 -0
  45. package/src/runtime/adapters/scorm2004.ts +162 -0
  46. package/src/runtime/adapters/web.ts +62 -0
  47. package/src/runtime/contexts.ts +76 -0
  48. package/src/runtime/duration.ts +29 -0
  49. package/src/runtime/hooks.svelte.ts +543 -0
  50. package/src/runtime/interaction-format.ts +132 -0
  51. package/src/runtime/interaction.ts +96 -0
  52. package/src/runtime/navigation.svelte.ts +117 -0
  53. package/src/runtime/persistence.ts +56 -0
  54. package/src/runtime/progress.svelte.ts +168 -0
  55. package/src/runtime/quiz-policy.ts +227 -0
  56. package/src/runtime/slugify.ts +17 -0
  57. package/src/runtime/types.ts +92 -0
  58. package/src/runtime/xapi/agent-rules.ts +93 -0
  59. package/src/runtime/xapi/client.ts +133 -0
  60. package/src/runtime/xapi/derive-actor.ts +90 -0
  61. package/src/runtime/xapi/publisher.ts +604 -0
  62. package/src/runtime/xapi/registry.ts +38 -0
  63. package/src/runtime/xapi/setup.ts +250 -0
  64. package/src/runtime/xapi/types.ts +106 -0
  65. package/src/runtime/xapi/uuid.ts +21 -0
  66. package/src/runtime/xapi/validation.ts +71 -0
  67. package/src/runtime/xapi/version.ts +23 -0
  68. package/src/virtual.d.ts +16 -0
  69. package/styles/base.css +194 -0
  70. package/styles/layout.css +408 -0
  71. package/styles/theme.css +36 -0
@@ -0,0 +1,663 @@
1
+ <script>
2
+ import { getContext, onMount } from 'svelte';
3
+ import { useQuestion } from '../runtime/hooks.svelte.js';
4
+ import { slugFromQuestion } from './util.js';
5
+
6
+ let {
7
+ id,
8
+ question,
9
+ items,
10
+ targets,
11
+ correct,
12
+ correctFeedback = '',
13
+ incorrectFeedback = '',
14
+ maxRetries = Infinity,
15
+ weight = 1,
16
+ } = $props();
17
+
18
+ const quiz = getContext('tessera-quiz');
19
+ const standalone = !quiz;
20
+
21
+ let queue = $state([]); // item indices not yet placed; queue[0] is current
22
+ let placements = $state(new Map()); // itemIdx → targetIdx
23
+ let dragOver = $state(null); // target index highlighted during drag
24
+ let isDragging = $state(false);
25
+ let cardSelected = $state(false); // current card selected via tap/click
26
+
27
+ let saRetryCount = $state(0);
28
+ let saCanRetry = $derived(saRetryCount < maxRetries);
29
+
30
+ const defaultId = `sorting-${slugFromQuestion(question)}`;
31
+
32
+ function shuffle(arr) {
33
+ const a = [...arr];
34
+ for (let i = a.length - 1; i > 0; i--) {
35
+ const j = Math.floor(Math.random() * (i + 1));
36
+ [a[i], a[j]] = [a[j], a[i]];
37
+ }
38
+ return a;
39
+ }
40
+
41
+ function initQueue() {
42
+ queue = shuffle(items.map((_, i) => i));
43
+ placements = new Map();
44
+ cardSelected = false;
45
+ dragOver = null;
46
+ isDragging = false;
47
+ }
48
+
49
+ if (standalone) {
50
+ initQueue();
51
+ } else {
52
+ onMount(() => {
53
+ initQueue();
54
+ quiz.setRender(myIndex, renderQuestion);
55
+ });
56
+ }
57
+
58
+ function checkAnswer(answer) {
59
+ if (!answer || !(answer instanceof Map)) return false;
60
+ if (answer.size !== items.length) return false;
61
+ for (let i = 0; i < items.length; i++) {
62
+ if (answer.get(i) !== correct[i]) return false;
63
+ }
64
+ return true;
65
+ }
66
+
67
+ function resetState() {
68
+ initQueue();
69
+ }
70
+
71
+ // Sorting is semantically a categorization (each item → one target) and maps
72
+ // cleanly to SCORM 2004's `matching` interaction. We emit [itemIdx, targetIdx]
73
+ // pairs as stringified ids.
74
+ const handle = useQuestion({
75
+ id: id ?? defaultId,
76
+ weight,
77
+ response: () => ({
78
+ type: 'matching',
79
+ response: [...placements.entries()].map(([i, t]) => [String(i), String(t)]),
80
+ correct: items.map((_, i) => [String(i), String(correct[i])]),
81
+ }),
82
+ reset: resetState,
83
+ });
84
+
85
+ const myIndex = $derived(handle.quizIndex ?? -1);
86
+
87
+ let currentItemIdx = $derived(queue.length > 0 ? queue[0] : null);
88
+
89
+ let isLocked = $derived(standalone ? false : quiz.isLockedCorrect(myIndex));
90
+ let isDisabled = $derived(standalone ? handle.submitted : quiz.isAnswerLocked(myIndex));
91
+ let showFeedback = $derived(standalone ? handle.submitted : quiz.feedbackVisible(myIndex));
92
+
93
+ function getItemsForTarget(targetIdx) {
94
+ const result = [];
95
+ for (const [itemIdx, tIdx] of placements) {
96
+ if (tIdx === targetIdx) result.push(itemIdx);
97
+ }
98
+ return result;
99
+ }
100
+
101
+ function isCorrectPlacement(itemIdx) {
102
+ return placements.get(itemIdx) === correct[itemIdx];
103
+ }
104
+
105
+ function placeCard(targetIdx) {
106
+ if (isDisabled || currentItemIdx === null) return;
107
+ const itemIdx = queue[0];
108
+ placements.set(itemIdx, targetIdx);
109
+ queue = queue.slice(1);
110
+ cardSelected = false;
111
+ if (!standalone) quiz.setAnswer(myIndex, new Map(placements));
112
+ }
113
+
114
+ function returnCard(itemIdx) {
115
+ if (isDisabled) return;
116
+ placements.delete(itemIdx);
117
+ queue = [itemIdx, ...queue];
118
+ if (!standalone) quiz.setAnswer(myIndex, new Map(placements));
119
+ }
120
+
121
+ // --- Drag handlers ---
122
+
123
+ function onDragStart(e) {
124
+ isDragging = true;
125
+ e.dataTransfer.effectAllowed = 'move';
126
+ }
127
+
128
+ function onDragEnd() {
129
+ isDragging = false;
130
+ dragOver = null;
131
+ }
132
+
133
+ function onDragOver(e, targetIdx) {
134
+ if (isDisabled) return;
135
+ e.preventDefault();
136
+ e.dataTransfer.dropEffect = 'move';
137
+ dragOver = targetIdx;
138
+ }
139
+
140
+ function onDragLeave(e) {
141
+ // Only clear when leaving the target element itself, not a child
142
+ if (!e.currentTarget.contains(e.relatedTarget)) {
143
+ dragOver = null;
144
+ }
145
+ }
146
+
147
+ function onDrop(e, targetIdx) {
148
+ e.preventDefault();
149
+ isDragging = false;
150
+ dragOver = null;
151
+ placeCard(targetIdx);
152
+ }
153
+
154
+ // --- Click / tap handlers ---
155
+
156
+ function onCardClick() {
157
+ if (isDisabled || currentItemIdx === null) return;
158
+ cardSelected = !cardSelected;
159
+ }
160
+
161
+ function onCardKeydown(e) {
162
+ if (e.key === 'Enter' || e.key === ' ') {
163
+ e.preventDefault();
164
+ onCardClick();
165
+ }
166
+ }
167
+
168
+ function onTargetClick(targetIdx) {
169
+ if (isDisabled || !cardSelected) return;
170
+ placeCard(targetIdx);
171
+ }
172
+
173
+ function handleRetry() {
174
+ saRetryCount++;
175
+ handle.reset();
176
+ }
177
+ </script>
178
+
179
+ {#snippet sortingContent()}
180
+ <p class="tessera-sorting-question">{question}</p>
181
+
182
+ <!-- Card deck: shows the current card to be placed -->
183
+ {#if !isDisabled}
184
+ <div class="tessera-sorting-deck" aria-live="polite" aria-atomic="false">
185
+ {#if currentItemIdx !== null}
186
+ <div class="tessera-sorting-deck-inner">
187
+ <div
188
+ class="tessera-sorting-card"
189
+ class:selected={cardSelected}
190
+ class:dragging={isDragging}
191
+ draggable={true}
192
+ role="button"
193
+ tabindex="0"
194
+ aria-label="{cardSelected
195
+ ? 'Selected — click a target to place: '
196
+ : 'Drag or click to sort: '}{items[currentItemIdx]}"
197
+ aria-grabbed={isDragging}
198
+ ondragstart={onDragStart}
199
+ ondragend={onDragEnd}
200
+ onclick={onCardClick}
201
+ onkeydown={onCardKeydown}
202
+ >
203
+ {items[currentItemIdx]}
204
+ </div>
205
+ <p class="tessera-sorting-counter">
206
+ {queue.length} of {items.length} to sort
207
+ </p>
208
+ {#if cardSelected}
209
+ <p class="tessera-sorting-hint">Click a target below to place this card</p>
210
+ {/if}
211
+ </div>
212
+ {:else}
213
+ <div class="tessera-sorting-deck-empty">
214
+ All cards placed — check your answers below.
215
+ </div>
216
+ {/if}
217
+ </div>
218
+ {/if}
219
+
220
+ <!-- Drop targets -->
221
+ <div class="tessera-sorting-targets" class:targets-active={cardSelected && !isDisabled}>
222
+ {#each targets as targetLabel, targetIdx}
223
+ {@const targetItems = getItemsForTarget(targetIdx)}
224
+ <div
225
+ class="tessera-sorting-target"
226
+ class:drag-over={dragOver === targetIdx}
227
+ class:clickable={cardSelected && !isDisabled}
228
+ role="group"
229
+ aria-label="Target: {targetLabel}"
230
+ ondragover={(e) => onDragOver(e, targetIdx)}
231
+ ondragleave={onDragLeave}
232
+ ondrop={(e) => onDrop(e, targetIdx)}
233
+ onclick={() => onTargetClick(targetIdx)}
234
+ >
235
+ <div class="tessera-sorting-target-label">{targetLabel}</div>
236
+ {#if targetItems.length > 0}
237
+ <div class="tessera-sorting-target-items">
238
+ {#each targetItems as itemIdx}
239
+ <div
240
+ class="tessera-sorting-placed-item"
241
+ class:correct={showFeedback && isCorrectPlacement(itemIdx)}
242
+ class:incorrect={showFeedback && !isCorrectPlacement(itemIdx)}
243
+ >
244
+ <span class="tessera-sorting-item-text">{items[itemIdx]}</span>
245
+ {#if !isDisabled}
246
+ <button
247
+ class="tessera-sorting-remove"
248
+ aria-label="Return '{items[itemIdx]}' to deck"
249
+ onclick={(e) => { e.stopPropagation(); returnCard(itemIdx); }}
250
+ >×</button>
251
+ {:else if showFeedback}
252
+ <span class="tessera-sorting-item-icon" aria-hidden="true">
253
+ {isCorrectPlacement(itemIdx) ? '✓' : '✗'}
254
+ </span>
255
+ {/if}
256
+ </div>
257
+ {/each}
258
+ </div>
259
+ {/if}
260
+ </div>
261
+ {/each}
262
+ </div>
263
+
264
+ <!-- Feedback (shown after standalone submit or quiz feedbackVisible) -->
265
+ {#if showFeedback}
266
+ {@const isCorrect = checkAnswer(placements)}
267
+ <div class="tessera-sorting-review">
268
+ {#if isCorrect}
269
+ <div class="tessera-sorting-result correct">
270
+ <svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16" aria-hidden="true">
271
+ <path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
272
+ </svg>
273
+ All items sorted correctly!
274
+ </div>
275
+ {#if correctFeedback}
276
+ <p class="tessera-sorting-feedback correct">{correctFeedback}</p>
277
+ {/if}
278
+ {:else}
279
+ <div class="tessera-sorting-result incorrect">
280
+ <svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16" aria-hidden="true">
281
+ <path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"/>
282
+ </svg>
283
+ Some items are in the wrong category.
284
+ </div>
285
+ <div class="tessera-sorting-correct-list">
286
+ <p class="tessera-sorting-correct-title">Correct arrangement:</p>
287
+ {#each items as item, i}
288
+ <p class="tessera-sorting-correct-item">{item} → {targets[correct[i]]}</p>
289
+ {/each}
290
+ </div>
291
+ {#if incorrectFeedback}
292
+ <p class="tessera-sorting-feedback incorrect">{incorrectFeedback}</p>
293
+ {/if}
294
+ {/if}
295
+ {#if standalone && saCanRetry}
296
+ <button class="tessera-standalone-retry" onclick={handleRetry}>Try again</button>
297
+ {/if}
298
+ </div>
299
+ {/if}
300
+
301
+ <!-- Standalone Check button (shown once all cards are placed) -->
302
+ {#if standalone && !handle.submitted && placements.size === items.length}
303
+ <div class="tessera-sorting-actions">
304
+ <button class="tessera-sorting-check" onclick={() => handle.submit()}>
305
+ Check Answer
306
+ </button>
307
+ </div>
308
+ {/if}
309
+ {/snippet}
310
+
311
+ {#if standalone}
312
+ <div class="tessera-sorting" aria-label={question}>
313
+ {@render sortingContent()}
314
+ </div>
315
+ {/if}
316
+
317
+ {#snippet renderQuestion()}
318
+ <div class="tessera-sorting" aria-label={question}>
319
+ {#if isLocked}
320
+ <div class="tessera-quiz-locked-banner">
321
+ <svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16" aria-hidden="true">
322
+ <path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
323
+ </svg>
324
+ You already got this one right — click Next to continue.
325
+ </div>
326
+ {/if}
327
+ {@render sortingContent()}
328
+ </div>
329
+ {/snippet}
330
+
331
+ <style>
332
+ .tessera-sorting {
333
+ padding: var(--tessera-spacing-md) 0;
334
+ }
335
+
336
+ .tessera-sorting-question {
337
+ font-size: 1.125rem;
338
+ font-weight: 600;
339
+ margin-bottom: var(--tessera-spacing-lg);
340
+ color: var(--tessera-text);
341
+ }
342
+
343
+ /* --- Deck --- */
344
+
345
+ .tessera-sorting-deck {
346
+ margin-bottom: var(--tessera-spacing-lg);
347
+ min-height: 110px;
348
+ display: flex;
349
+ align-items: center;
350
+ justify-content: center;
351
+ }
352
+
353
+ .tessera-sorting-deck-inner {
354
+ display: flex;
355
+ flex-direction: column;
356
+ align-items: center;
357
+ gap: var(--tessera-spacing-sm);
358
+ }
359
+
360
+ .tessera-sorting-card {
361
+ display: flex;
362
+ align-items: center;
363
+ justify-content: center;
364
+ padding: var(--tessera-spacing-md) var(--tessera-spacing-xl);
365
+ min-width: 140px;
366
+ min-height: 64px;
367
+ background: var(--tessera-bg);
368
+ border: 2px solid var(--tessera-border);
369
+ border-radius: 10px;
370
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
371
+ font-size: 1rem;
372
+ font-weight: 500;
373
+ font-family: var(--tessera-font-family);
374
+ color: var(--tessera-text);
375
+ cursor: grab;
376
+ transition: border-color 0.15s, transform 0.15s, box-shadow 0.15s;
377
+ text-align: center;
378
+ user-select: none;
379
+ }
380
+
381
+ .tessera-sorting-card:hover {
382
+ border-color: var(--tessera-primary);
383
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
384
+ transform: translateY(-1px);
385
+ }
386
+
387
+ .tessera-sorting-card:focus-visible {
388
+ outline: var(--tessera-focus-ring, 0 0 0 3px rgba(37, 99, 235, 0.4));
389
+ outline-offset: 2px;
390
+ border-color: var(--tessera-primary);
391
+ }
392
+
393
+ .tessera-sorting-card.selected {
394
+ border-color: var(--tessera-primary);
395
+ background: var(--tessera-primary-light);
396
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
397
+ transform: translateY(-2px) scale(1.02);
398
+ cursor: pointer;
399
+ }
400
+
401
+ .tessera-sorting-card.dragging {
402
+ opacity: 0.5;
403
+ cursor: grabbing;
404
+ }
405
+
406
+ .tessera-sorting-counter {
407
+ font-size: 0.8125rem;
408
+ color: var(--tessera-text-light);
409
+ margin: 0;
410
+ }
411
+
412
+ .tessera-sorting-hint {
413
+ font-size: 0.8125rem;
414
+ color: var(--tessera-primary);
415
+ font-weight: 500;
416
+ margin: 0;
417
+ }
418
+
419
+ .tessera-sorting-deck-empty {
420
+ font-size: 0.9375rem;
421
+ color: var(--tessera-text-light);
422
+ font-style: italic;
423
+ }
424
+
425
+ /* --- Targets --- */
426
+
427
+ .tessera-sorting-targets {
428
+ display: flex;
429
+ gap: var(--tessera-spacing-md);
430
+ flex-wrap: wrap;
431
+ align-items: flex-start;
432
+ }
433
+
434
+ .tessera-sorting-target {
435
+ flex: 1;
436
+ min-width: 140px;
437
+ min-height: 120px;
438
+ border: 2px dashed var(--tessera-border);
439
+ border-radius: 10px;
440
+ background: var(--tessera-bg-secondary);
441
+ transition: border-color 0.15s, background 0.15s;
442
+ overflow: hidden;
443
+ display: flex;
444
+ flex-direction: column;
445
+ justify-content: center;
446
+ }
447
+
448
+ .tessera-sorting-target.drag-over {
449
+ border-color: var(--tessera-primary);
450
+ background: var(--tessera-primary-light);
451
+ border-style: solid;
452
+ }
453
+
454
+ .tessera-sorting-target.clickable {
455
+ cursor: pointer;
456
+ border-color: var(--tessera-primary);
457
+ border-style: dashed;
458
+ }
459
+
460
+ .tessera-sorting-target.clickable:hover {
461
+ background: var(--tessera-primary-light);
462
+ border-style: solid;
463
+ }
464
+
465
+ .tessera-sorting-target-label {
466
+ padding: var(--tessera-spacing-sm) var(--tessera-spacing-md);
467
+ font-size: 0.8125rem;
468
+ font-weight: 700;
469
+ text-transform: uppercase;
470
+ letter-spacing: 0.05em;
471
+ color: var(--tessera-text-light);
472
+ text-align: center;
473
+ pointer-events: none;
474
+ }
475
+
476
+ .tessera-sorting-target-items {
477
+ display: flex;
478
+ flex-direction: column;
479
+ gap: var(--tessera-spacing-xs, 4px);
480
+ padding: var(--tessera-spacing-sm);
481
+ pointer-events: none;
482
+ }
483
+
484
+ .tessera-sorting-placed-item {
485
+ display: flex;
486
+ align-items: center;
487
+ gap: var(--tessera-spacing-xs, 4px);
488
+ padding: 6px var(--tessera-spacing-sm);
489
+ background: var(--tessera-bg);
490
+ border: 1px solid var(--tessera-border);
491
+ border-radius: 6px;
492
+ font-size: 0.875rem;
493
+ font-family: var(--tessera-font-family);
494
+ color: var(--tessera-text);
495
+ transition: border-color 0.15s, background 0.15s;
496
+ pointer-events: all;
497
+ }
498
+
499
+ .tessera-sorting-placed-item.correct {
500
+ border-color: var(--tessera-success);
501
+ background: color-mix(in srgb, var(--tessera-success) 8%, transparent);
502
+ }
503
+
504
+ .tessera-sorting-placed-item.incorrect {
505
+ border-color: var(--tessera-error);
506
+ background: color-mix(in srgb, var(--tessera-error) 8%, transparent);
507
+ }
508
+
509
+ .tessera-sorting-item-text {
510
+ flex: 1;
511
+ min-width: 0;
512
+ overflow-wrap: break-word;
513
+ }
514
+
515
+ .tessera-sorting-item-icon {
516
+ flex-shrink: 0;
517
+ font-size: 0.875rem;
518
+ }
519
+
520
+ .tessera-sorting-placed-item.correct .tessera-sorting-item-icon {
521
+ color: var(--tessera-success);
522
+ }
523
+
524
+ .tessera-sorting-placed-item.incorrect .tessera-sorting-item-icon {
525
+ color: var(--tessera-error);
526
+ }
527
+
528
+ .tessera-sorting-remove {
529
+ flex-shrink: 0;
530
+ margin-left: auto;
531
+ background: none;
532
+ border: none;
533
+ font-size: 1.1rem;
534
+ line-height: 1;
535
+ color: var(--tessera-text-light);
536
+ cursor: pointer;
537
+ padding: 0 2px;
538
+ min-width: 20px;
539
+ min-height: 20px;
540
+ display: flex;
541
+ align-items: center;
542
+ justify-content: center;
543
+ border-radius: 3px;
544
+ pointer-events: all;
545
+ }
546
+
547
+ .tessera-sorting-remove:hover {
548
+ color: var(--tessera-error);
549
+ background: color-mix(in srgb, var(--tessera-error) 10%, transparent);
550
+ }
551
+
552
+ /* --- Feedback --- */
553
+
554
+ .tessera-sorting-review {
555
+ margin-top: var(--tessera-spacing-lg);
556
+ }
557
+
558
+ .tessera-sorting-result {
559
+ display: flex;
560
+ align-items: center;
561
+ gap: var(--tessera-spacing-sm);
562
+ font-weight: 600;
563
+ font-size: 0.9375rem;
564
+ margin-bottom: var(--tessera-spacing-sm);
565
+ }
566
+
567
+ .tessera-sorting-result.correct { color: var(--tessera-success); }
568
+ .tessera-sorting-result.incorrect { color: var(--tessera-error); }
569
+
570
+ .tessera-sorting-correct-list {
571
+ margin: var(--tessera-spacing-sm) 0;
572
+ font-size: 0.875rem;
573
+ color: var(--tessera-text-light);
574
+ }
575
+
576
+ .tessera-sorting-correct-title {
577
+ font-weight: 600;
578
+ margin-bottom: 4px;
579
+ color: var(--tessera-text);
580
+ }
581
+
582
+ .tessera-sorting-correct-item {
583
+ margin: 2px 0;
584
+ }
585
+
586
+ .tessera-sorting-feedback {
587
+ font-size: 0.875rem;
588
+ padding: var(--tessera-spacing-sm) var(--tessera-spacing-md);
589
+ border-radius: 4px;
590
+ margin-top: var(--tessera-spacing-sm);
591
+ }
592
+
593
+ .tessera-sorting-feedback.correct {
594
+ color: var(--tessera-success);
595
+ background: color-mix(in srgb, var(--tessera-success) 8%, transparent);
596
+ }
597
+
598
+ .tessera-sorting-feedback.incorrect {
599
+ color: var(--tessera-error);
600
+ background: color-mix(in srgb, var(--tessera-error) 8%, transparent);
601
+ }
602
+
603
+ /* --- Standalone actions --- */
604
+
605
+ .tessera-sorting-actions {
606
+ margin-top: var(--tessera-spacing-lg);
607
+ }
608
+
609
+ .tessera-sorting-check {
610
+ padding: 0.625rem 1.5rem;
611
+ background: var(--tessera-primary);
612
+ color: #fff;
613
+ border: none;
614
+ border-radius: 6px;
615
+ font-size: 0.9375rem;
616
+ font-weight: 500;
617
+ font-family: var(--tessera-font-family);
618
+ cursor: pointer;
619
+ min-height: 44px;
620
+ transition: background 0.2s;
621
+ }
622
+
623
+ .tessera-sorting-check:hover {
624
+ background: var(--tessera-primary-dark);
625
+ }
626
+
627
+ .tessera-standalone-retry {
628
+ display: inline-block;
629
+ margin-top: var(--tessera-spacing-md);
630
+ padding: 0;
631
+ font-size: 0.875rem;
632
+ font-weight: 600;
633
+ color: var(--tessera-primary);
634
+ background: none;
635
+ border: none;
636
+ cursor: pointer;
637
+ text-decoration: underline;
638
+ text-underline-offset: 2px;
639
+ font-family: var(--tessera-font-family);
640
+ }
641
+
642
+ .tessera-standalone-retry:hover {
643
+ color: var(--tessera-primary-dark);
644
+ }
645
+
646
+ /* --- Mobile --- */
647
+
648
+ @media (max-width: 640px) {
649
+ .tessera-sorting-targets {
650
+ flex-direction: column;
651
+ }
652
+
653
+ .tessera-sorting-target {
654
+ min-width: unset;
655
+ width: 100%;
656
+ }
657
+
658
+ .tessera-sorting-card {
659
+ min-width: 140px;
660
+ font-size: 0.9375rem;
661
+ }
662
+ }
663
+ </style>