tessera-learn 0.0.1 → 0.0.2
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/AGENTS.md +93 -75
- package/README.md +11 -0
- package/dist/plugin/index.js +79 -78
- package/dist/plugin/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/FillInTheBlank.svelte +19 -69
- package/src/components/LockedBanner.svelte +30 -0
- package/src/components/Matching.svelte +44 -80
- package/src/components/MultipleChoice.svelte +14 -43
- package/src/components/Quiz.svelte +69 -263
- package/src/components/ResultIcon.svelte +13 -0
- package/src/components/RetryButton.svelte +25 -0
- package/src/components/Sorting.svelte +33 -76
- package/src/components/util.ts +10 -0
- package/src/plugin/export.ts +39 -33
- package/src/plugin/manifest.ts +38 -12
- package/src/plugin/validation.ts +36 -69
- package/src/runtime/App.svelte +15 -20
- package/src/runtime/ErrorPage.svelte +1 -1
- package/src/runtime/adapters/retry.ts +48 -41
- package/src/runtime/adapters/scorm-base.ts +143 -0
- package/src/runtime/adapters/scorm12.ts +37 -117
- package/src/runtime/adapters/scorm2004.ts +34 -115
- package/src/runtime/hooks.svelte.ts +63 -29
- package/src/runtime/xapi/client.ts +2 -2
- package/src/runtime/xapi/publisher.ts +15 -6
- package/src/runtime/xapi/setup.ts +8 -15
- package/styles/layout.css +21 -10
- package/styles/theme.css +4 -0
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
import { getContext, onMount } from 'svelte';
|
|
3
3
|
import { useQuestion } from '../runtime/hooks.svelte.js';
|
|
4
4
|
import { slugFromQuestion } from './util.js';
|
|
5
|
+
import LockedBanner from './LockedBanner.svelte';
|
|
6
|
+
import ResultIcon from './ResultIcon.svelte';
|
|
7
|
+
import RetryButton from './RetryButton.svelte';
|
|
5
8
|
|
|
6
9
|
let {
|
|
7
10
|
id,
|
|
@@ -18,11 +21,9 @@
|
|
|
18
21
|
const standalone = !quiz;
|
|
19
22
|
|
|
20
23
|
let inputValue = $state('');
|
|
21
|
-
let saRetryCount = $state(0);
|
|
22
|
-
let saCanRetry = $derived(saRetryCount < maxRetries);
|
|
23
24
|
|
|
24
|
-
const
|
|
25
|
-
const
|
|
25
|
+
const componentId = $props.id();
|
|
26
|
+
const inputId = `fitb-${componentId}`;
|
|
26
27
|
|
|
27
28
|
function checkAnswer(userAnswer) {
|
|
28
29
|
if (!userAnswer || typeof userAnswer !== 'string') return false;
|
|
@@ -35,8 +36,9 @@
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
const handle = useQuestion({
|
|
38
|
-
id
|
|
39
|
-
weight,
|
|
39
|
+
get id() { return id ?? `fitb-${slugFromQuestion(question)}`; },
|
|
40
|
+
get weight() { return weight; },
|
|
41
|
+
get maxRetries() { return maxRetries; },
|
|
40
42
|
response: () => ({
|
|
41
43
|
type: 'fill-in',
|
|
42
44
|
response: inputValue,
|
|
@@ -73,11 +75,6 @@
|
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
function handleRetry() {
|
|
77
|
-
saRetryCount++;
|
|
78
|
-
inputValue = '';
|
|
79
|
-
handle.reset();
|
|
80
|
-
}
|
|
81
78
|
</script>
|
|
82
79
|
|
|
83
80
|
{#if standalone}
|
|
@@ -100,7 +97,7 @@
|
|
|
100
97
|
/>
|
|
101
98
|
{#if !handle.submitted}
|
|
102
99
|
<button
|
|
103
|
-
class="tessera-fitb-check-btn"
|
|
100
|
+
class="tessera-btn-primary tessera-fitb-check-btn"
|
|
104
101
|
disabled={!inputValue.trim()}
|
|
105
102
|
onclick={() => { handle.submit(); }}
|
|
106
103
|
>
|
|
@@ -114,9 +111,7 @@
|
|
|
114
111
|
<div class="tessera-fitb-review">
|
|
115
112
|
{#if isCorrect}
|
|
116
113
|
<div class="tessera-fitb-result correct">
|
|
117
|
-
<
|
|
118
|
-
<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"/>
|
|
119
|
-
</svg>
|
|
114
|
+
<ResultIcon kind="correct" />
|
|
120
115
|
Correct
|
|
121
116
|
</div>
|
|
122
117
|
{#if correctFeedback}
|
|
@@ -124,9 +119,7 @@
|
|
|
124
119
|
{/if}
|
|
125
120
|
{:else}
|
|
126
121
|
<div class="tessera-fitb-result incorrect">
|
|
127
|
-
<
|
|
128
|
-
<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"/>
|
|
129
|
-
</svg>
|
|
122
|
+
<ResultIcon kind="incorrect" />
|
|
130
123
|
Incorrect
|
|
131
124
|
</div>
|
|
132
125
|
<p class="tessera-fitb-correct-answer">
|
|
@@ -136,8 +129,8 @@
|
|
|
136
129
|
<p class="tessera-fitb-feedback incorrect">{incorrectFeedback}</p>
|
|
137
130
|
{/if}
|
|
138
131
|
{/if}
|
|
139
|
-
{#if
|
|
140
|
-
<
|
|
132
|
+
{#if handle.canRetry}
|
|
133
|
+
<RetryButton onclick={() => handle.retry()} />
|
|
141
134
|
{/if}
|
|
142
135
|
</div>
|
|
143
136
|
{/if}
|
|
@@ -147,12 +140,7 @@
|
|
|
147
140
|
{#snippet renderQuestion()}
|
|
148
141
|
<div class="tessera-fitb">
|
|
149
142
|
{#if isLocked}
|
|
150
|
-
<
|
|
151
|
-
<svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16" aria-hidden="true">
|
|
152
|
-
<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"/>
|
|
153
|
-
</svg>
|
|
154
|
-
You already got this one right — click Next to continue.
|
|
155
|
-
</div>
|
|
143
|
+
<LockedBanner />
|
|
156
144
|
{/if}
|
|
157
145
|
<label class="tessera-fitb-question" for={inputId}>{question}</label>
|
|
158
146
|
|
|
@@ -177,9 +165,7 @@
|
|
|
177
165
|
<div class="tessera-fitb-review">
|
|
178
166
|
{#if isCorrect}
|
|
179
167
|
<div class="tessera-fitb-result correct">
|
|
180
|
-
<
|
|
181
|
-
<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"/>
|
|
182
|
-
</svg>
|
|
168
|
+
<ResultIcon kind="correct" />
|
|
183
169
|
Correct
|
|
184
170
|
</div>
|
|
185
171
|
{#if correctFeedback}
|
|
@@ -187,9 +173,7 @@
|
|
|
187
173
|
{/if}
|
|
188
174
|
{:else}
|
|
189
175
|
<div class="tessera-fitb-result incorrect">
|
|
190
|
-
<
|
|
191
|
-
<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"/>
|
|
192
|
-
</svg>
|
|
176
|
+
<ResultIcon kind="incorrect" />
|
|
193
177
|
Incorrect
|
|
194
178
|
</div>
|
|
195
179
|
<p class="tessera-fitb-correct-answer">
|
|
@@ -237,7 +221,7 @@
|
|
|
237
221
|
.tessera-fitb-input:focus {
|
|
238
222
|
outline: none;
|
|
239
223
|
border-color: var(--tessera-primary);
|
|
240
|
-
box-shadow: var(--tessera-focus-ring
|
|
224
|
+
box-shadow: var(--tessera-focus-ring);
|
|
241
225
|
}
|
|
242
226
|
|
|
243
227
|
.tessera-fitb-input:disabled {
|
|
@@ -288,12 +272,12 @@
|
|
|
288
272
|
|
|
289
273
|
.tessera-fitb-feedback.correct {
|
|
290
274
|
color: var(--tessera-success);
|
|
291
|
-
background:
|
|
275
|
+
background: var(--tessera-success-bg);
|
|
292
276
|
}
|
|
293
277
|
|
|
294
278
|
.tessera-fitb-feedback.incorrect {
|
|
295
279
|
color: var(--tessera-error);
|
|
296
|
-
background:
|
|
280
|
+
background: var(--tessera-error-bg);
|
|
297
281
|
}
|
|
298
282
|
|
|
299
283
|
.tessera-fitb-check-btn {
|
|
@@ -301,40 +285,6 @@
|
|
|
301
285
|
padding: 0.5rem 1rem;
|
|
302
286
|
font-size: 0.875rem;
|
|
303
287
|
font-weight: 600;
|
|
304
|
-
color: #fff;
|
|
305
|
-
background: var(--tessera-primary);
|
|
306
|
-
border: none;
|
|
307
|
-
border-radius: 6px;
|
|
308
|
-
cursor: pointer;
|
|
309
|
-
min-height: 44px;
|
|
310
|
-
transition: background 0.2s, opacity 0.2s;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
.tessera-fitb-check-btn:hover:not(:disabled) {
|
|
314
|
-
background: var(--tessera-primary-dark);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
.tessera-fitb-check-btn:disabled {
|
|
318
|
-
opacity: 0.4;
|
|
319
|
-
cursor: not-allowed;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
.tessera-standalone-retry {
|
|
323
|
-
display: inline-block;
|
|
324
|
-
margin-top: var(--tessera-spacing-md);
|
|
325
|
-
padding: 0;
|
|
326
|
-
font-size: 0.875rem;
|
|
327
|
-
font-weight: 600;
|
|
328
|
-
color: var(--tessera-primary);
|
|
329
|
-
background: none;
|
|
330
|
-
border: none;
|
|
331
|
-
cursor: pointer;
|
|
332
|
-
text-decoration: underline;
|
|
333
|
-
text-underline-offset: 2px;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
.tessera-standalone-retry:hover {
|
|
337
|
-
color: var(--tessera-primary-dark);
|
|
338
288
|
}
|
|
339
289
|
|
|
340
290
|
@media (max-width: 640px) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
let { message = 'You already got this one right — click Next to continue.' } = $props();
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<div class="tessera-quiz-locked-banner">
|
|
6
|
+
<svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16" aria-hidden="true">
|
|
7
|
+
<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"/>
|
|
8
|
+
</svg>
|
|
9
|
+
{message}
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<style>
|
|
13
|
+
.tessera-quiz-locked-banner {
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
gap: var(--tessera-spacing-sm);
|
|
17
|
+
padding: var(--tessera-spacing-md);
|
|
18
|
+
margin-bottom: var(--tessera-spacing-md);
|
|
19
|
+
background: var(--tessera-success-bg);
|
|
20
|
+
border: 1px solid var(--tessera-success-border);
|
|
21
|
+
border-radius: 6px;
|
|
22
|
+
color: var(--tessera-success);
|
|
23
|
+
font-size: 0.9375rem;
|
|
24
|
+
font-weight: 500;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.tessera-quiz-locked-banner svg {
|
|
28
|
+
flex-shrink: 0;
|
|
29
|
+
}
|
|
30
|
+
</style>
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
import { getContext, onMount } from 'svelte';
|
|
3
3
|
import { SvelteMap } from 'svelte/reactivity';
|
|
4
4
|
import { useQuestion } from '../runtime/hooks.svelte.js';
|
|
5
|
-
import { slugFromQuestion } from './util.js';
|
|
5
|
+
import { slugFromQuestion, shuffle } from './util.js';
|
|
6
|
+
import LockedBanner from './LockedBanner.svelte';
|
|
7
|
+
import ResultIcon from './ResultIcon.svelte';
|
|
8
|
+
import RetryButton from './RetryButton.svelte';
|
|
6
9
|
|
|
7
10
|
let {
|
|
8
11
|
id,
|
|
@@ -19,31 +22,19 @@
|
|
|
19
22
|
|
|
20
23
|
let shuffledRight = $state([]);
|
|
21
24
|
let matches = $state(new SvelteMap());
|
|
25
|
+
// Reverse index (right.originalIndex → left index) for O(1) right-column lookups.
|
|
26
|
+
let rightToLeft = $state(new SvelteMap());
|
|
22
27
|
let selectedLeft = $state(null);
|
|
23
28
|
let selectedRight = $state(null);
|
|
24
29
|
|
|
25
|
-
let saRetryCount = $state(0);
|
|
26
|
-
let saCanRetry = $derived(saRetryCount < maxRetries);
|
|
27
|
-
let saAllMatched = $derived(matches.size === pairs.length);
|
|
28
|
-
|
|
29
|
-
const defaultId = `matching-${slugFromQuestion(question)}`;
|
|
30
30
|
|
|
31
31
|
const pairColors = [
|
|
32
32
|
'#2563eb', '#9333ea', '#0891b2', '#c2410c', '#4f46e5',
|
|
33
33
|
'#0d9488', '#b91c1c', '#7c3aed', '#0369a1', '#a16207',
|
|
34
34
|
];
|
|
35
35
|
|
|
36
|
-
function shuffleArray(arr) {
|
|
37
|
-
const shuffled = [...arr];
|
|
38
|
-
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
39
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
40
|
-
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
41
|
-
}
|
|
42
|
-
return shuffled;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
36
|
function initShuffle() {
|
|
46
|
-
shuffledRight =
|
|
37
|
+
shuffledRight = shuffle(pairs.map((p, i) => ({ text: p.right, originalIndex: i })));
|
|
47
38
|
}
|
|
48
39
|
|
|
49
40
|
if (standalone) {
|
|
@@ -66,14 +57,16 @@
|
|
|
66
57
|
|
|
67
58
|
function resetState() {
|
|
68
59
|
matches = new SvelteMap();
|
|
60
|
+
rightToLeft = new SvelteMap();
|
|
69
61
|
selectedLeft = null;
|
|
70
62
|
selectedRight = null;
|
|
71
63
|
initShuffle();
|
|
72
64
|
}
|
|
73
65
|
|
|
74
66
|
const handle = useQuestion({
|
|
75
|
-
id
|
|
76
|
-
weight,
|
|
67
|
+
get id() { return id ?? `matching-${slugFromQuestion(question)}`; },
|
|
68
|
+
get weight() { return weight; },
|
|
69
|
+
get maxRetries() { return maxRetries; },
|
|
77
70
|
response: () => ({
|
|
78
71
|
type: 'matching',
|
|
79
72
|
response: [...matches.entries()].map(([l, r]) => [String(l), String(r)]),
|
|
@@ -87,13 +80,6 @@
|
|
|
87
80
|
let isLocked = $derived(standalone ? false : quiz.isLockedCorrect(myIndex));
|
|
88
81
|
let quizLocked = $derived(standalone ? handle.submitted : quiz.isAnswerLocked(myIndex));
|
|
89
82
|
|
|
90
|
-
// Auto-submit in standalone mode when all pairs matched
|
|
91
|
-
$effect(() => {
|
|
92
|
-
if (standalone && saAllMatched && !handle.submitted) {
|
|
93
|
-
handle.submit();
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
|
|
97
83
|
function handleLeftClick(leftIndex) {
|
|
98
84
|
if (standalone) {
|
|
99
85
|
if (handle.submitted) return;
|
|
@@ -133,17 +119,26 @@
|
|
|
133
119
|
}
|
|
134
120
|
|
|
135
121
|
function createMatch(leftIndex, rightOriginalIndex) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
122
|
+
// Free any prior partner on either side so both maps stay consistent.
|
|
123
|
+
const priorRightForLeft = matches.get(leftIndex);
|
|
124
|
+
if (priorRightForLeft !== undefined) {
|
|
125
|
+
rightToLeft.delete(priorRightForLeft);
|
|
126
|
+
}
|
|
127
|
+
const priorLeftForRight = rightToLeft.get(rightOriginalIndex);
|
|
128
|
+
if (priorLeftForRight !== undefined) {
|
|
129
|
+
matches.delete(priorLeftForRight);
|
|
140
130
|
}
|
|
141
131
|
|
|
142
132
|
matches.set(leftIndex, rightOriginalIndex);
|
|
133
|
+
rightToLeft.set(rightOriginalIndex, leftIndex);
|
|
143
134
|
selectedLeft = null;
|
|
144
135
|
selectedRight = null;
|
|
145
136
|
|
|
146
|
-
if (
|
|
137
|
+
if (standalone) {
|
|
138
|
+
if (matches.size === pairs.length && !handle.submitted) {
|
|
139
|
+
handle.submit();
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
147
142
|
quiz.setAnswer(myIndex, new Map(matches));
|
|
148
143
|
}
|
|
149
144
|
}
|
|
@@ -154,34 +149,30 @@
|
|
|
154
149
|
} else {
|
|
155
150
|
if (quizLocked) return;
|
|
156
151
|
}
|
|
152
|
+
const right = matches.get(leftIndex);
|
|
157
153
|
matches.delete(leftIndex);
|
|
154
|
+
if (right !== undefined) rightToLeft.delete(right);
|
|
158
155
|
if (!standalone) {
|
|
159
156
|
quiz.setAnswer(myIndex, new Map(matches));
|
|
160
157
|
}
|
|
161
158
|
}
|
|
162
159
|
|
|
163
|
-
function handleRetry() {
|
|
164
|
-
saRetryCount++;
|
|
165
|
-
handle.reset();
|
|
166
|
-
}
|
|
167
|
-
|
|
168
160
|
function getMatchColor(leftIndex) {
|
|
169
161
|
if (!matches.has(leftIndex)) return null;
|
|
170
162
|
return pairColors[leftIndex % pairColors.length];
|
|
171
163
|
}
|
|
172
164
|
|
|
173
165
|
function getRightMatchColor(rightOriginalIndex) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
return null;
|
|
166
|
+
const l = rightToLeft.get(rightOriginalIndex);
|
|
167
|
+
return l === undefined ? null : pairColors[l % pairColors.length];
|
|
178
168
|
}
|
|
179
169
|
|
|
180
170
|
function isRightMatched(rightOriginalIndex) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
171
|
+
return rightToLeft.has(rightOriginalIndex);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getLeftForRight(rightOriginalIndex) {
|
|
175
|
+
return rightToLeft.get(rightOriginalIndex);
|
|
185
176
|
}
|
|
186
177
|
|
|
187
178
|
function isMatchCorrect(leftIndex) {
|
|
@@ -246,7 +237,7 @@
|
|
|
246
237
|
aria-label="{item.text}{matched ? ' (matched)' : ''}"
|
|
247
238
|
>
|
|
248
239
|
{#if matched}
|
|
249
|
-
{@const leftIdx =
|
|
240
|
+
{@const leftIdx = getLeftForRight(item.originalIndex)}
|
|
250
241
|
<span class="tessera-matching-badge" style="background: {color}">
|
|
251
242
|
{leftIdx !== undefined ? leftIdx + 1 : ''}
|
|
252
243
|
</span>
|
|
@@ -262,9 +253,7 @@
|
|
|
262
253
|
<div class="tessera-matching-review">
|
|
263
254
|
{#if isCorrect}
|
|
264
255
|
<div class="tessera-matching-result correct">
|
|
265
|
-
<
|
|
266
|
-
<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"/>
|
|
267
|
-
</svg>
|
|
256
|
+
<ResultIcon kind="correct" />
|
|
268
257
|
All pairs matched correctly!
|
|
269
258
|
</div>
|
|
270
259
|
{#if correctFeedback}
|
|
@@ -272,9 +261,7 @@
|
|
|
272
261
|
{/if}
|
|
273
262
|
{:else}
|
|
274
263
|
<div class="tessera-matching-result incorrect">
|
|
275
|
-
<
|
|
276
|
-
<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"/>
|
|
277
|
-
</svg>
|
|
264
|
+
<ResultIcon kind="incorrect" />
|
|
278
265
|
Some pairs are incorrect
|
|
279
266
|
</div>
|
|
280
267
|
<div class="tessera-matching-correct-pairs">
|
|
@@ -287,8 +274,8 @@
|
|
|
287
274
|
<p class="tessera-matching-feedback incorrect">{incorrectFeedback}</p>
|
|
288
275
|
{/if}
|
|
289
276
|
{/if}
|
|
290
|
-
{#if standalone &&
|
|
291
|
-
<
|
|
277
|
+
{#if standalone && handle.canRetry}
|
|
278
|
+
<RetryButton onclick={() => handle.retry()} />
|
|
292
279
|
{/if}
|
|
293
280
|
</div>
|
|
294
281
|
{/if}
|
|
@@ -303,12 +290,7 @@
|
|
|
303
290
|
{#snippet renderQuestion()}
|
|
304
291
|
<div class="tessera-matching" aria-label={question}>
|
|
305
292
|
{#if isLocked}
|
|
306
|
-
<
|
|
307
|
-
<svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16" aria-hidden="true">
|
|
308
|
-
<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"/>
|
|
309
|
-
</svg>
|
|
310
|
-
You already got this one right — click Next to continue.
|
|
311
|
-
</div>
|
|
293
|
+
<LockedBanner />
|
|
312
294
|
{/if}
|
|
313
295
|
{@render matchingContent()}
|
|
314
296
|
</div>
|
|
@@ -382,12 +364,12 @@
|
|
|
382
364
|
|
|
383
365
|
.tessera-matching-item.correct {
|
|
384
366
|
border-color: var(--tessera-success) !important;
|
|
385
|
-
background:
|
|
367
|
+
background: var(--tessera-success-bg) !important;
|
|
386
368
|
}
|
|
387
369
|
|
|
388
370
|
.tessera-matching-item.incorrect {
|
|
389
371
|
border-color: var(--tessera-error) !important;
|
|
390
|
-
background:
|
|
372
|
+
background: var(--tessera-error-bg) !important;
|
|
391
373
|
}
|
|
392
374
|
|
|
393
375
|
.tessera-matching-item:disabled {
|
|
@@ -474,30 +456,12 @@
|
|
|
474
456
|
|
|
475
457
|
.tessera-matching-feedback.correct {
|
|
476
458
|
color: var(--tessera-success);
|
|
477
|
-
background:
|
|
459
|
+
background: var(--tessera-success-bg);
|
|
478
460
|
}
|
|
479
461
|
|
|
480
462
|
.tessera-matching-feedback.incorrect {
|
|
481
463
|
color: var(--tessera-error);
|
|
482
|
-
background:
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
.tessera-standalone-retry {
|
|
486
|
-
display: inline-block;
|
|
487
|
-
margin-top: var(--tessera-spacing-md);
|
|
488
|
-
padding: 0;
|
|
489
|
-
font-size: 0.875rem;
|
|
490
|
-
font-weight: 600;
|
|
491
|
-
color: var(--tessera-primary);
|
|
492
|
-
background: none;
|
|
493
|
-
border: none;
|
|
494
|
-
cursor: pointer;
|
|
495
|
-
text-decoration: underline;
|
|
496
|
-
text-underline-offset: 2px;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
.tessera-standalone-retry:hover {
|
|
500
|
-
color: var(--tessera-primary-dark);
|
|
464
|
+
background: var(--tessera-error-bg);
|
|
501
465
|
}
|
|
502
466
|
|
|
503
467
|
@media (max-width: 640px) {
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { getContext, onMount } from 'svelte';
|
|
3
3
|
import { useQuestion } from '../runtime/hooks.svelte.js';
|
|
4
4
|
import { slugFromQuestion } from './util.js';
|
|
5
|
+
import LockedBanner from './LockedBanner.svelte';
|
|
6
|
+
import RetryButton from './RetryButton.svelte';
|
|
5
7
|
|
|
6
8
|
let {
|
|
7
9
|
id,
|
|
@@ -19,16 +21,14 @@
|
|
|
19
21
|
const standalone = !quiz;
|
|
20
22
|
|
|
21
23
|
let selectedOption = $state(null);
|
|
22
|
-
let saRetryCount = $state(0);
|
|
23
|
-
let saCanRetry = $derived(saRetryCount < maxRetries);
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
const groupId = `mc-${
|
|
27
|
-
const defaultId = `mc-${slugFromQuestion(question)}`;
|
|
25
|
+
const componentId = $props.id();
|
|
26
|
+
const groupId = `mc-${componentId}`;
|
|
28
27
|
|
|
29
28
|
const handle = useQuestion({
|
|
30
|
-
id
|
|
31
|
-
weight,
|
|
29
|
+
get id() { return id ?? `mc-${slugFromQuestion(question)}`; },
|
|
30
|
+
get weight() { return weight; },
|
|
31
|
+
get maxRetries() { return maxRetries; },
|
|
32
32
|
response: () => ({
|
|
33
33
|
type: 'choice',
|
|
34
34
|
response: selectedOption !== null ? [String(selectedOption)] : [],
|
|
@@ -55,12 +55,6 @@
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function handleRetry() {
|
|
59
|
-
saRetryCount++;
|
|
60
|
-
selectedOption = null;
|
|
61
|
-
handle.reset();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
58
|
function isCorrectOption(optIndex) {
|
|
65
59
|
return optIndex === correct;
|
|
66
60
|
}
|
|
@@ -129,8 +123,8 @@
|
|
|
129
123
|
{:else if selectedOption !== correct && incorrectFeedback && !optionFeedback[selectedOption]}
|
|
130
124
|
<div class="tessera-mc-overall-feedback incorrect">{incorrectFeedback}</div>
|
|
131
125
|
{/if}
|
|
132
|
-
{#if
|
|
133
|
-
<
|
|
126
|
+
{#if handle.canRetry}
|
|
127
|
+
<RetryButton onclick={() => handle.retry()} />
|
|
134
128
|
{/if}
|
|
135
129
|
{/if}
|
|
136
130
|
</div>
|
|
@@ -139,12 +133,7 @@
|
|
|
139
133
|
{#snippet renderQuestion()}
|
|
140
134
|
<div class="tessera-mc" role="radiogroup" aria-labelledby="{groupId}-label">
|
|
141
135
|
{#if isLocked}
|
|
142
|
-
<
|
|
143
|
-
<svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16" aria-hidden="true">
|
|
144
|
-
<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"/>
|
|
145
|
-
</svg>
|
|
146
|
-
You already got this one right — click Next to continue.
|
|
147
|
-
</div>
|
|
136
|
+
<LockedBanner />
|
|
148
137
|
{/if}
|
|
149
138
|
<p class="tessera-mc-question" id="{groupId}-label">{question}</p>
|
|
150
139
|
|
|
@@ -237,12 +226,12 @@
|
|
|
237
226
|
|
|
238
227
|
.tessera-mc-option.correct {
|
|
239
228
|
border-color: var(--tessera-success);
|
|
240
|
-
background:
|
|
229
|
+
background: var(--tessera-success-bg);
|
|
241
230
|
}
|
|
242
231
|
|
|
243
232
|
.tessera-mc-option.incorrect {
|
|
244
233
|
border-color: var(--tessera-error);
|
|
245
|
-
background:
|
|
234
|
+
background: var(--tessera-error-bg);
|
|
246
235
|
}
|
|
247
236
|
|
|
248
237
|
.tessera-mc-option input[type="radio"] {
|
|
@@ -323,33 +312,15 @@
|
|
|
323
312
|
}
|
|
324
313
|
|
|
325
314
|
.tessera-mc-overall-feedback.correct {
|
|
326
|
-
background:
|
|
315
|
+
background: var(--tessera-success-bg);
|
|
327
316
|
color: var(--tessera-success);
|
|
328
317
|
}
|
|
329
318
|
|
|
330
319
|
.tessera-mc-overall-feedback.incorrect {
|
|
331
|
-
background:
|
|
320
|
+
background: var(--tessera-error-bg);
|
|
332
321
|
color: var(--tessera-error);
|
|
333
322
|
}
|
|
334
323
|
|
|
335
|
-
.tessera-standalone-retry {
|
|
336
|
-
display: inline-block;
|
|
337
|
-
margin-top: var(--tessera-spacing-md);
|
|
338
|
-
padding: 0;
|
|
339
|
-
font-size: 0.875rem;
|
|
340
|
-
font-weight: 600;
|
|
341
|
-
color: var(--tessera-primary);
|
|
342
|
-
background: none;
|
|
343
|
-
border: none;
|
|
344
|
-
cursor: pointer;
|
|
345
|
-
text-decoration: underline;
|
|
346
|
-
text-underline-offset: 2px;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
.tessera-standalone-retry:hover {
|
|
350
|
-
color: var(--tessera-primary-dark);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
324
|
.tessera-mc-option:has(input:focus-visible) {
|
|
354
325
|
outline: 2px solid var(--tessera-primary);
|
|
355
326
|
outline-offset: 2px;
|