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
|
@@ -1,115 +1,45 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import { getContext
|
|
3
|
-
import {
|
|
4
|
-
import { buildQuizInteractions } from './quiz-payload.js';
|
|
2
|
+
import { getContext } from 'svelte';
|
|
3
|
+
import { useQuiz } from '../runtime/hooks.svelte.js';
|
|
5
4
|
|
|
6
5
|
let { children } = $props();
|
|
7
6
|
let quizElement = $state(null);
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
const handle = useQuiz({ element: () => quizElement });
|
|
9
|
+
|
|
10
10
|
const pageCtx = getContext('tessera-page');
|
|
11
|
-
|
|
11
|
+
let quizConfig = $derived(pageCtx?.quiz ?? {});
|
|
12
|
+
let passingScore = $derived(pageCtx?.passingScore ?? 70);
|
|
13
|
+
let showFeedback = $derived(quizConfig.showFeedback ?? true);
|
|
14
|
+
let maxAttempts = $derived(quizConfig.maxAttempts ?? Infinity);
|
|
15
|
+
let isImmediateMode = $derived(showFeedback && quizConfig.feedbackMode === 'immediate');
|
|
12
16
|
|
|
13
|
-
// State
|
|
14
|
-
let questions = $state([]);
|
|
15
17
|
let currentQuestionIndex = $state(0);
|
|
16
|
-
let answers = $state(new SvelteMap());
|
|
17
|
-
let submitted = $state(false);
|
|
18
|
-
let score = $state(0);
|
|
19
|
-
let correctCount = $state(0);
|
|
20
|
-
let attemptCount = $state(0);
|
|
21
|
-
let reviewing = $state(false);
|
|
22
18
|
let reviewIndex = $state(0);
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
let
|
|
26
|
-
|
|
27
|
-
let lockedCorrect = $state(new SvelteSet());
|
|
28
|
-
|
|
29
|
-
// Derived
|
|
30
|
-
let totalQuestions = $derived(questions.length);
|
|
31
|
-
let maxAttempts = $derived(quizConfig.maxAttempts ?? Infinity);
|
|
32
|
-
let showFeedback = $derived(quizConfig.showFeedback ?? true);
|
|
33
|
-
let passingScore = $derived(pageCtx?.passingScore ?? 70);
|
|
34
|
-
let canRetry = $derived(attemptCount < maxAttempts);
|
|
35
|
-
let passed = $derived(score >= passingScore);
|
|
36
|
-
let allAnswered = $derived(totalQuestions > 0 && answers.size >= totalQuestions);
|
|
37
|
-
let feedbackMode = $derived(
|
|
38
|
-
(quizConfig.showFeedback && quizConfig.feedbackMode === 'immediate') ? 'immediate' : 'review'
|
|
20
|
+
let totalQuestions = $derived(handle.questions.length);
|
|
21
|
+
let correctCount = $derived(
|
|
22
|
+
handle.questions.reduce((sum, q) => sum + (q.correct ? 1 : 0), 0)
|
|
39
23
|
);
|
|
40
|
-
let
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Hand the Quiz the render snippet for a registered question.
|
|
58
|
-
// Snippets aren't available at a child's script-top (they live in the template
|
|
59
|
-
// block), so built-ins call this from onMount once the snippet has compiled.
|
|
60
|
-
function setRender(index, render) {
|
|
61
|
-
questions[index].render = render;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function setAnswer(questionIndex, answer) {
|
|
65
|
-
answers.set(questionIndex, answer);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function getAnswer(questionIndex) {
|
|
69
|
-
return answers.get(questionIndex);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Provide context to child question components
|
|
73
|
-
setContext('tessera-quiz', {
|
|
74
|
-
get registerQuestion() { return registerQuestion; },
|
|
75
|
-
get setRender() { return setRender; },
|
|
76
|
-
get setAnswer() { return setAnswer; },
|
|
77
|
-
get getAnswer() { return getAnswer; },
|
|
78
|
-
get submitted() { return submitted; },
|
|
79
|
-
get reviewing() { return reviewing; },
|
|
80
|
-
get showFeedback() { return showFeedback; },
|
|
81
|
-
get currentQuestionIndex() { return reviewing ? reviewIndex : currentQuestionIndex; },
|
|
82
|
-
get feedbackVisible() {
|
|
83
|
-
return (index) => {
|
|
84
|
-
if (feedbackMode === 'immediate' && showFeedback && feedbackShown.has(index)) return true;
|
|
85
|
-
if (submitted && reviewing && showFeedback) return true;
|
|
86
|
-
return false;
|
|
87
|
-
};
|
|
88
|
-
},
|
|
89
|
-
get isAnswerLocked() {
|
|
90
|
-
return (index) => {
|
|
91
|
-
// Locked during immediate feedback (answer already revealed)
|
|
92
|
-
if (feedbackMode === 'immediate' && feedbackShown.has(index)) return true;
|
|
93
|
-
// Locked after submission
|
|
94
|
-
if (submitted) return true;
|
|
95
|
-
// Locked from incorrect-only retry
|
|
96
|
-
if (lockedCorrect.has(index)) return true;
|
|
97
|
-
return false;
|
|
98
|
-
};
|
|
99
|
-
},
|
|
100
|
-
get isLockedCorrect() {
|
|
101
|
-
return (index) => lockedCorrect.has(index);
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// Navigation
|
|
24
|
+
let passed = $derived(handle.score >= passingScore);
|
|
25
|
+
|
|
26
|
+
function isAnswered(i) {
|
|
27
|
+
return handle.getAnswer(i) !== undefined || handle.isLockedCorrect(i);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function needsReveal(i) {
|
|
31
|
+
return (
|
|
32
|
+
isImmediateMode &&
|
|
33
|
+
isAnswered(i) &&
|
|
34
|
+
!handle.isLockedCorrect(i) &&
|
|
35
|
+
!handle.feedbackVisible(i)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
106
39
|
function goNextQuestion() {
|
|
107
|
-
// Immediate
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
&& !feedbackShown.has(currentQuestionIndex)
|
|
111
|
-
&& !lockedCorrect.has(currentQuestionIndex)) {
|
|
112
|
-
feedbackShown.add(currentQuestionIndex);
|
|
40
|
+
// Immediate-mode: first click reveals feedback, second advances.
|
|
41
|
+
if (needsReveal(currentQuestionIndex)) {
|
|
42
|
+
handle.revealFeedback(currentQuestionIndex);
|
|
113
43
|
return;
|
|
114
44
|
}
|
|
115
45
|
if (currentQuestionIndex < totalQuestions - 1) {
|
|
@@ -118,143 +48,56 @@
|
|
|
118
48
|
}
|
|
119
49
|
|
|
120
50
|
function goPrevQuestion() {
|
|
121
|
-
if (currentQuestionIndex > 0)
|
|
122
|
-
currentQuestionIndex--;
|
|
123
|
-
}
|
|
51
|
+
if (currentQuestionIndex > 0) currentQuestionIndex--;
|
|
124
52
|
}
|
|
125
53
|
|
|
126
54
|
function goNextReview() {
|
|
127
|
-
if (reviewIndex < totalQuestions - 1)
|
|
128
|
-
reviewIndex++;
|
|
129
|
-
}
|
|
55
|
+
if (reviewIndex < totalQuestions - 1) reviewIndex++;
|
|
130
56
|
}
|
|
131
57
|
|
|
132
58
|
function goPrevReview() {
|
|
133
|
-
if (reviewIndex > 0)
|
|
134
|
-
reviewIndex--;
|
|
135
|
-
}
|
|
59
|
+
if (reviewIndex > 0) reviewIndex--;
|
|
136
60
|
}
|
|
137
61
|
|
|
138
|
-
// Check if the current question needs immediate feedback before advancing/submitting
|
|
139
|
-
function needsImmediateFeedback() {
|
|
140
|
-
return feedbackMode === 'immediate'
|
|
141
|
-
&& !feedbackShown.has(currentQuestionIndex)
|
|
142
|
-
&& !lockedCorrect.has(currentQuestionIndex)
|
|
143
|
-
&& answers.has(currentQuestionIndex);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function showImmediateFeedback() {
|
|
147
|
-
feedbackShown.add(currentQuestionIndex);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Submission
|
|
151
62
|
function handleSubmit() {
|
|
152
|
-
if (!
|
|
153
|
-
|
|
154
|
-
// Weighted rollup: Σ(w·correct)/Σ(w)·100. With every weight = 1 (the
|
|
155
|
-
// default) this collapses to the unweighted mean, so existing courses
|
|
156
|
-
// that never set a `weight` prop see no change.
|
|
157
|
-
let count = 0;
|
|
158
|
-
let weighted = 0;
|
|
159
|
-
let totalWeight = 0;
|
|
160
|
-
for (let i = 0; i < questions.length; i++) {
|
|
161
|
-
const q = questions[i];
|
|
162
|
-
const answer = answers.get(i);
|
|
163
|
-
const ok = q.checkAnswer(answer);
|
|
164
|
-
totalWeight += q.weight;
|
|
165
|
-
if (ok) {
|
|
166
|
-
weighted += q.weight;
|
|
167
|
-
count++;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
correctCount = count;
|
|
172
|
-
score = totalWeight === 0 ? 0 : Math.round((weighted / totalWeight) * 100);
|
|
173
|
-
submitted = true;
|
|
174
|
-
attemptCount++;
|
|
175
|
-
|
|
176
|
-
// Report score and per-question interactions via custom event.
|
|
177
|
-
// `interactions` is empty for built-in components until Phase 2 migrates
|
|
178
|
-
// them onto the new useQuestion API — additive and back-compat.
|
|
179
|
-
const interactions = buildQuizInteractions(questions, answers);
|
|
180
|
-
const event = new CustomEvent('tessera-quiz-complete', {
|
|
181
|
-
detail: { score, interactions },
|
|
182
|
-
bubbles: true,
|
|
183
|
-
});
|
|
184
|
-
quizElement?.dispatchEvent(event);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Review
|
|
188
|
-
function startReview() {
|
|
189
|
-
reviewing = true;
|
|
190
|
-
reviewIndex = 0;
|
|
63
|
+
if (!handle.canSubmit) return;
|
|
64
|
+
handle.submit();
|
|
191
65
|
}
|
|
192
66
|
|
|
193
|
-
function
|
|
194
|
-
|
|
67
|
+
function handleStartReview() {
|
|
68
|
+
reviewIndex = 0;
|
|
69
|
+
handle.startReview();
|
|
195
70
|
}
|
|
196
71
|
|
|
197
|
-
// Retry
|
|
198
72
|
function handleRetry() {
|
|
199
|
-
|
|
200
|
-
reviewing = false;
|
|
201
|
-
score = 0;
|
|
202
|
-
correctCount = 0;
|
|
73
|
+
handle.retry();
|
|
203
74
|
currentQuestionIndex = 0;
|
|
204
75
|
reviewIndex = 0;
|
|
205
|
-
feedbackShown = new SvelteSet();
|
|
206
|
-
|
|
207
|
-
if (retryMode === 'incorrect-only') {
|
|
208
|
-
// Identify correct questions
|
|
209
|
-
const newLockedCorrect = new SvelteSet();
|
|
210
|
-
const preservedAnswers = new SvelteMap();
|
|
211
|
-
for (let i = 0; i < questions.length; i++) {
|
|
212
|
-
const answer = answers.get(i);
|
|
213
|
-
if (questions[i].checkAnswer(answer)) {
|
|
214
|
-
newLockedCorrect.add(i);
|
|
215
|
-
preservedAnswers.set(i, answer);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
lockedCorrect = newLockedCorrect;
|
|
219
|
-
answers = preservedAnswers;
|
|
220
|
-
// Only reset incorrect questions
|
|
221
|
-
for (let i = 0; i < questions.length; i++) {
|
|
222
|
-
if (!newLockedCorrect.has(i) && questions[i].reset) {
|
|
223
|
-
questions[i].reset();
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
} else {
|
|
227
|
-
lockedCorrect = new SvelteSet();
|
|
228
|
-
answers = new SvelteMap();
|
|
229
|
-
for (const q of questions) {
|
|
230
|
-
if (q.reset) q.reset();
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
76
|
}
|
|
234
|
-
|
|
235
77
|
</script>
|
|
236
78
|
|
|
237
79
|
<div class="tessera-quiz" bind:this={quizElement} role="region" aria-label="Quiz">
|
|
238
|
-
{#if
|
|
80
|
+
{#if handle.state === 'answering'}
|
|
239
81
|
<!-- Question phase -->
|
|
240
82
|
<div class="tessera-quiz-progress" aria-live="polite">
|
|
241
83
|
<span class="tessera-quiz-progress-text">
|
|
242
84
|
<span class="tessera-quiz-progress-desktop">Question {currentQuestionIndex + 1} of {totalQuestions}</span>
|
|
243
85
|
<span class="tessera-quiz-progress-mobile">{currentQuestionIndex + 1}/{totalQuestions}</span>
|
|
244
86
|
</span>
|
|
245
|
-
<div class="tessera-
|
|
87
|
+
<div class="tessera-progress-track">
|
|
246
88
|
<div
|
|
247
|
-
class="tessera-
|
|
89
|
+
class="tessera-progress-fill"
|
|
248
90
|
style="width: {totalQuestions > 0 ? ((currentQuestionIndex + 1) / totalQuestions) * 100 : 0}%"
|
|
249
91
|
></div>
|
|
250
92
|
</div>
|
|
251
93
|
</div>
|
|
252
94
|
|
|
253
95
|
<div class="tessera-quiz-questions">
|
|
254
|
-
{#each questions as q, i (q.id)}
|
|
96
|
+
{#each handle.questions as q, i (q.id)}
|
|
97
|
+
{@const render = handle.getRender(i)}
|
|
255
98
|
<div class="tessera-quiz-question-wrapper" class:active={i === currentQuestionIndex} aria-hidden={i !== currentQuestionIndex}>
|
|
256
|
-
{#if
|
|
257
|
-
{@render
|
|
99
|
+
{#if render}
|
|
100
|
+
{@render render()}
|
|
258
101
|
{/if}
|
|
259
102
|
</div>
|
|
260
103
|
{/each}
|
|
@@ -270,23 +113,23 @@
|
|
|
270
113
|
</button>
|
|
271
114
|
{#if currentQuestionIndex < totalQuestions - 1}
|
|
272
115
|
<button
|
|
273
|
-
class="tessera-quiz-btn tessera-
|
|
274
|
-
disabled={!
|
|
116
|
+
class="tessera-quiz-btn tessera-btn-primary"
|
|
117
|
+
disabled={!isAnswered(currentQuestionIndex)}
|
|
275
118
|
onclick={goNextQuestion}
|
|
276
119
|
>
|
|
277
|
-
{
|
|
120
|
+
{handle.feedbackVisible(currentQuestionIndex) && isImmediateMode ? 'Continue' : 'Next'}
|
|
278
121
|
</button>
|
|
279
|
-
{:else if
|
|
122
|
+
{:else if needsReveal(currentQuestionIndex)}
|
|
280
123
|
<button
|
|
281
|
-
class="tessera-quiz-btn tessera-
|
|
282
|
-
onclick={
|
|
124
|
+
class="tessera-quiz-btn tessera-btn-primary"
|
|
125
|
+
onclick={() => handle.revealFeedback(currentQuestionIndex)}
|
|
283
126
|
>
|
|
284
127
|
Check Answer
|
|
285
128
|
</button>
|
|
286
129
|
{:else}
|
|
287
130
|
<button
|
|
288
|
-
class="tessera-quiz-btn tessera-
|
|
289
|
-
disabled={!
|
|
131
|
+
class="tessera-quiz-btn tessera-btn-primary tessera-quiz-btn-submit"
|
|
132
|
+
disabled={!handle.canSubmit}
|
|
290
133
|
onclick={handleSubmit}
|
|
291
134
|
>
|
|
292
135
|
Submit
|
|
@@ -294,7 +137,7 @@
|
|
|
294
137
|
{/if}
|
|
295
138
|
</div>
|
|
296
139
|
|
|
297
|
-
{:else if reviewing}
|
|
140
|
+
{:else if handle.state === 'reviewing'}
|
|
298
141
|
<!-- Review phase -->
|
|
299
142
|
<div class="tessera-quiz-progress" aria-live="polite">
|
|
300
143
|
<span class="tessera-quiz-progress-text">
|
|
@@ -304,10 +147,11 @@
|
|
|
304
147
|
</div>
|
|
305
148
|
|
|
306
149
|
<div class="tessera-quiz-questions">
|
|
307
|
-
{#each questions as q, i (q.id)}
|
|
150
|
+
{#each handle.questions as q, i (q.id)}
|
|
151
|
+
{@const render = handle.getRender(i)}
|
|
308
152
|
<div class="tessera-quiz-question-wrapper" class:active={i === reviewIndex} aria-hidden={i !== reviewIndex}>
|
|
309
|
-
{#if
|
|
310
|
-
{@render
|
|
153
|
+
{#if render}
|
|
154
|
+
{@render render()}
|
|
311
155
|
{/if}
|
|
312
156
|
</div>
|
|
313
157
|
{/each}
|
|
@@ -323,15 +167,15 @@
|
|
|
323
167
|
</button>
|
|
324
168
|
{#if reviewIndex < totalQuestions - 1}
|
|
325
169
|
<button
|
|
326
|
-
class="tessera-quiz-btn tessera-
|
|
170
|
+
class="tessera-quiz-btn tessera-btn-primary"
|
|
327
171
|
onclick={goNextReview}
|
|
328
172
|
>
|
|
329
173
|
Next
|
|
330
174
|
</button>
|
|
331
175
|
{:else}
|
|
332
176
|
<button
|
|
333
|
-
class="tessera-quiz-btn tessera-
|
|
334
|
-
onclick={exitReview}
|
|
177
|
+
class="tessera-quiz-btn tessera-btn-primary"
|
|
178
|
+
onclick={() => handle.exitReview()}
|
|
335
179
|
>
|
|
336
180
|
Done
|
|
337
181
|
</button>
|
|
@@ -343,7 +187,7 @@
|
|
|
343
187
|
<div class="tessera-quiz-results" role="status" aria-live="polite">
|
|
344
188
|
<h2 class="tessera-quiz-results-title">Quiz Results</h2>
|
|
345
189
|
<div class="tessera-quiz-score">
|
|
346
|
-
<span class="tessera-quiz-score-value">{score}%</span>
|
|
190
|
+
<span class="tessera-quiz-score-value">{handle.score}%</span>
|
|
347
191
|
<span class="tessera-quiz-score-label" class:passed class:failed={!passed}>
|
|
348
192
|
{passed ? 'Passed' : 'Not Passed'}
|
|
349
193
|
</span>
|
|
@@ -356,21 +200,21 @@
|
|
|
356
200
|
{#if showFeedback}
|
|
357
201
|
<button
|
|
358
202
|
class="tessera-quiz-btn tessera-quiz-btn-secondary"
|
|
359
|
-
onclick={
|
|
203
|
+
onclick={handleStartReview}
|
|
360
204
|
>
|
|
361
205
|
Review Answers
|
|
362
206
|
</button>
|
|
363
207
|
{/if}
|
|
364
|
-
{#if canRetry}
|
|
208
|
+
{#if handle.canRetry}
|
|
365
209
|
<button
|
|
366
|
-
class="tessera-quiz-btn tessera-
|
|
210
|
+
class="tessera-quiz-btn tessera-btn-primary"
|
|
367
211
|
onclick={handleRetry}
|
|
368
212
|
>
|
|
369
213
|
Retry Quiz
|
|
370
214
|
</button>
|
|
371
215
|
{/if}
|
|
372
|
-
{#if maxAttempts !== Infinity && attemptCount >= maxAttempts}
|
|
373
|
-
<p class="tessera-quiz-attempts-exhausted">All attempts used ({attemptCount}/{maxAttempts})</p>
|
|
216
|
+
{#if maxAttempts !== Infinity && handle.attemptCount >= maxAttempts}
|
|
217
|
+
<p class="tessera-quiz-attempts-exhausted">All attempts used ({handle.attemptCount}/{maxAttempts})</p>
|
|
374
218
|
{/if}
|
|
375
219
|
</div>
|
|
376
220
|
</div>
|
|
@@ -403,19 +247,8 @@
|
|
|
403
247
|
display: none;
|
|
404
248
|
}
|
|
405
249
|
|
|
406
|
-
.tessera-quiz-progress-
|
|
250
|
+
.tessera-quiz-progress :global(.tessera-progress-track) {
|
|
407
251
|
flex: 1;
|
|
408
|
-
height: 4px;
|
|
409
|
-
background: var(--tessera-border);
|
|
410
|
-
border-radius: 2px;
|
|
411
|
-
overflow: hidden;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
.tessera-quiz-progress-fill {
|
|
415
|
-
height: 100%;
|
|
416
|
-
background: var(--tessera-primary);
|
|
417
|
-
border-radius: 2px;
|
|
418
|
-
transition: width 0.3s ease;
|
|
419
252
|
}
|
|
420
253
|
|
|
421
254
|
.tessera-quiz-question-wrapper {
|
|
@@ -452,15 +285,6 @@
|
|
|
452
285
|
cursor: not-allowed;
|
|
453
286
|
}
|
|
454
287
|
|
|
455
|
-
.tessera-quiz-btn-primary {
|
|
456
|
-
background: var(--tessera-primary);
|
|
457
|
-
color: #fff;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
.tessera-quiz-btn-primary:hover:not(:disabled) {
|
|
461
|
-
background: var(--tessera-primary-dark);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
288
|
.tessera-quiz-btn-secondary {
|
|
465
289
|
background: var(--tessera-bg-secondary);
|
|
466
290
|
color: var(--tessera-text);
|
|
@@ -530,24 +354,6 @@
|
|
|
530
354
|
font-style: italic;
|
|
531
355
|
}
|
|
532
356
|
|
|
533
|
-
.tessera-quiz-locked-banner {
|
|
534
|
-
display: flex;
|
|
535
|
-
align-items: center;
|
|
536
|
-
gap: var(--tessera-spacing-sm);
|
|
537
|
-
padding: var(--tessera-spacing-md);
|
|
538
|
-
margin-bottom: var(--tessera-spacing-md);
|
|
539
|
-
background: color-mix(in srgb, var(--tessera-success) 8%, transparent);
|
|
540
|
-
border: 1px solid color-mix(in srgb, var(--tessera-success) 25%, transparent);
|
|
541
|
-
border-radius: 6px;
|
|
542
|
-
color: var(--tessera-success);
|
|
543
|
-
font-size: 0.9375rem;
|
|
544
|
-
font-weight: 500;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
.tessera-quiz-locked-banner svg {
|
|
548
|
-
flex-shrink: 0;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
357
|
/* Mobile */
|
|
552
358
|
@media (max-width: 640px) {
|
|
553
359
|
.tessera-quiz-progress-desktop { display: none; }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
let { kind, size = 16 } = $props();
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
{#if kind === 'correct'}
|
|
6
|
+
<svg viewBox="0 0 16 16" fill="currentColor" width={size} height={size} 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
|
+
{:else}
|
|
10
|
+
<svg viewBox="0 0 16 16" fill="currentColor" width={size} height={size} aria-hidden="true">
|
|
11
|
+
<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"/>
|
|
12
|
+
</svg>
|
|
13
|
+
{/if}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
let { onclick, label = 'Try again' } = $props();
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<button class="tessera-standalone-retry" {onclick}>{label}</button>
|
|
6
|
+
|
|
7
|
+
<style>
|
|
8
|
+
.tessera-standalone-retry {
|
|
9
|
+
display: inline-block;
|
|
10
|
+
margin-top: var(--tessera-spacing-md);
|
|
11
|
+
padding: 0;
|
|
12
|
+
font-size: 0.875rem;
|
|
13
|
+
font-weight: 600;
|
|
14
|
+
color: var(--tessera-primary);
|
|
15
|
+
background: none;
|
|
16
|
+
border: none;
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
text-decoration: underline;
|
|
19
|
+
text-underline-offset: 2px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.tessera-standalone-retry:hover {
|
|
23
|
+
color: var(--tessera-primary-dark);
|
|
24
|
+
}
|
|
25
|
+
</style>
|