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.
- package/AGENTS.md +1228 -0
- package/LICENSE +21 -0
- package/README.md +21 -0
- package/dist/plugin/index.d.ts +7 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/index.js +1239 -0
- package/dist/plugin/index.js.map +1 -0
- package/package.json +77 -0
- package/src/archiver.d.ts +27 -0
- package/src/components/Accordion.svelte +32 -0
- package/src/components/AccordionItem.svelte +144 -0
- package/src/components/Audio.svelte +38 -0
- package/src/components/Callout.svelte +81 -0
- package/src/components/Carousel.svelte +194 -0
- package/src/components/CarouselSlide.svelte +32 -0
- package/src/components/DefaultLayout.svelte +108 -0
- package/src/components/FillInTheBlank.svelte +345 -0
- package/src/components/Image.svelte +47 -0
- package/src/components/Matching.svelte +513 -0
- package/src/components/MultipleChoice.svelte +363 -0
- package/src/components/Quiz.svelte +569 -0
- package/src/components/RevealModal.svelte +228 -0
- package/src/components/Sorting.svelte +663 -0
- package/src/components/Video.svelte +118 -0
- package/src/components/index.ts +15 -0
- package/src/components/quiz-payload.ts +71 -0
- package/src/components/util.ts +24 -0
- package/src/index.ts +56 -0
- package/src/plugin/export.ts +264 -0
- package/src/plugin/index.ts +464 -0
- package/src/plugin/layout.ts +55 -0
- package/src/plugin/manifest.ts +330 -0
- package/src/plugin/quiz.ts +65 -0
- package/src/plugin/validation.ts +838 -0
- package/src/runtime/App.svelte +435 -0
- package/src/runtime/ErrorPage.svelte +14 -0
- package/src/runtime/LoadingSkeleton.svelte +26 -0
- package/src/runtime/Sidebar.svelte +76 -0
- package/src/runtime/access.ts +55 -0
- package/src/runtime/adapters/cmi5.ts +341 -0
- package/src/runtime/adapters/discovery.ts +38 -0
- package/src/runtime/adapters/index.ts +99 -0
- package/src/runtime/adapters/retry.ts +284 -0
- package/src/runtime/adapters/scorm12.ts +172 -0
- package/src/runtime/adapters/scorm2004.ts +162 -0
- package/src/runtime/adapters/web.ts +62 -0
- package/src/runtime/contexts.ts +76 -0
- package/src/runtime/duration.ts +29 -0
- package/src/runtime/hooks.svelte.ts +543 -0
- package/src/runtime/interaction-format.ts +132 -0
- package/src/runtime/interaction.ts +96 -0
- package/src/runtime/navigation.svelte.ts +117 -0
- package/src/runtime/persistence.ts +56 -0
- package/src/runtime/progress.svelte.ts +168 -0
- package/src/runtime/quiz-policy.ts +227 -0
- package/src/runtime/slugify.ts +17 -0
- package/src/runtime/types.ts +92 -0
- package/src/runtime/xapi/agent-rules.ts +93 -0
- package/src/runtime/xapi/client.ts +133 -0
- package/src/runtime/xapi/derive-actor.ts +90 -0
- package/src/runtime/xapi/publisher.ts +604 -0
- package/src/runtime/xapi/registry.ts +38 -0
- package/src/runtime/xapi/setup.ts +250 -0
- package/src/runtime/xapi/types.ts +106 -0
- package/src/runtime/xapi/uuid.ts +21 -0
- package/src/runtime/xapi/validation.ts +71 -0
- package/src/runtime/xapi/version.ts +23 -0
- package/src/virtual.d.ts +16 -0
- package/styles/base.css +194 -0
- package/styles/layout.css +408 -0
- package/styles/theme.css +36 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/**
|
|
3
|
+
* @component Image
|
|
4
|
+
* Lazy-loaded image with optional caption, rendered as <figure>.
|
|
5
|
+
*
|
|
6
|
+
* @prop {string} src - Image source URL (supports $assets/ paths)
|
|
7
|
+
* @prop {string} alt - Alt text (required for accessibility)
|
|
8
|
+
* @prop {string} [caption] - Optional caption below image
|
|
9
|
+
*/
|
|
10
|
+
let { src, alt, caption = '' } = $props();
|
|
11
|
+
|
|
12
|
+
// Resolve $assets/ prefix to the assets directory.
|
|
13
|
+
// In dev, Vite serves from project root so /assets/ works.
|
|
14
|
+
// In build, the Vite alias handles JS imports but not HTML attrs,
|
|
15
|
+
// so we rewrite to a root-relative path that Vite can serve.
|
|
16
|
+
let resolvedSrc = $derived(
|
|
17
|
+
src.startsWith('$assets/') ? src.replace('$assets/', '/assets/') : src
|
|
18
|
+
);
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<figure class="tessera-image">
|
|
22
|
+
<img src={resolvedSrc} {alt} loading="lazy" class="tessera-image-img" />
|
|
23
|
+
{#if caption}
|
|
24
|
+
<figcaption class="tessera-image-caption">{caption}</figcaption>
|
|
25
|
+
{/if}
|
|
26
|
+
</figure>
|
|
27
|
+
|
|
28
|
+
<style>
|
|
29
|
+
.tessera-image {
|
|
30
|
+
margin: var(--tessera-spacing-lg) 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.tessera-image-img {
|
|
34
|
+
max-width: 100%;
|
|
35
|
+
height: auto;
|
|
36
|
+
border-radius: 8px;
|
|
37
|
+
display: block;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.tessera-image-caption {
|
|
41
|
+
margin-top: var(--tessera-spacing-sm);
|
|
42
|
+
font-size: 0.875rem;
|
|
43
|
+
color: var(--tessera-text-light);
|
|
44
|
+
text-align: center;
|
|
45
|
+
font-style: italic;
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext, onMount } from 'svelte';
|
|
3
|
+
import { SvelteMap } from 'svelte/reactivity';
|
|
4
|
+
import { useQuestion } from '../runtime/hooks.svelte.js';
|
|
5
|
+
import { slugFromQuestion } from './util.js';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
id,
|
|
9
|
+
question,
|
|
10
|
+
pairs,
|
|
11
|
+
correctFeedback = '',
|
|
12
|
+
incorrectFeedback = '',
|
|
13
|
+
maxRetries = Infinity,
|
|
14
|
+
weight = 1,
|
|
15
|
+
} = $props();
|
|
16
|
+
|
|
17
|
+
const quiz = getContext('tessera-quiz');
|
|
18
|
+
const standalone = !quiz;
|
|
19
|
+
|
|
20
|
+
let shuffledRight = $state([]);
|
|
21
|
+
let matches = $state(new SvelteMap());
|
|
22
|
+
let selectedLeft = $state(null);
|
|
23
|
+
let selectedRight = $state(null);
|
|
24
|
+
|
|
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
|
+
|
|
31
|
+
const pairColors = [
|
|
32
|
+
'#2563eb', '#9333ea', '#0891b2', '#c2410c', '#4f46e5',
|
|
33
|
+
'#0d9488', '#b91c1c', '#7c3aed', '#0369a1', '#a16207',
|
|
34
|
+
];
|
|
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
|
+
function initShuffle() {
|
|
46
|
+
shuffledRight = shuffleArray(pairs.map((p, i) => ({ text: p.right, originalIndex: i })));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (standalone) {
|
|
50
|
+
initShuffle();
|
|
51
|
+
} else {
|
|
52
|
+
onMount(() => {
|
|
53
|
+
initShuffle();
|
|
54
|
+
quiz.setRender(myIndex, renderQuestion);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function checkAnswer(answer) {
|
|
59
|
+
if (!answer || !(answer instanceof Map)) return false;
|
|
60
|
+
if (answer.size !== pairs.length) return false;
|
|
61
|
+
for (let i = 0; i < pairs.length; i++) {
|
|
62
|
+
if (answer.get(i) !== i) return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resetState() {
|
|
68
|
+
matches = new SvelteMap();
|
|
69
|
+
selectedLeft = null;
|
|
70
|
+
selectedRight = null;
|
|
71
|
+
initShuffle();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const handle = useQuestion({
|
|
75
|
+
id: id ?? defaultId,
|
|
76
|
+
weight,
|
|
77
|
+
response: () => ({
|
|
78
|
+
type: 'matching',
|
|
79
|
+
response: [...matches.entries()].map(([l, r]) => [String(l), String(r)]),
|
|
80
|
+
correct: pairs.map((_, i) => [String(i), String(i)]),
|
|
81
|
+
}),
|
|
82
|
+
reset: resetState,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const myIndex = $derived(handle.quizIndex ?? -1);
|
|
86
|
+
|
|
87
|
+
let isLocked = $derived(standalone ? false : quiz.isLockedCorrect(myIndex));
|
|
88
|
+
let quizLocked = $derived(standalone ? handle.submitted : quiz.isAnswerLocked(myIndex));
|
|
89
|
+
|
|
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
|
+
function handleLeftClick(leftIndex) {
|
|
98
|
+
if (standalone) {
|
|
99
|
+
if (handle.submitted) return;
|
|
100
|
+
} else {
|
|
101
|
+
if (quizLocked) return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (selectedLeft === leftIndex) {
|
|
105
|
+
selectedLeft = null;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
selectedLeft = leftIndex;
|
|
110
|
+
|
|
111
|
+
if (selectedRight !== null) {
|
|
112
|
+
createMatch(leftIndex, selectedRight);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function handleRightClick(rightOriginalIndex) {
|
|
117
|
+
if (standalone) {
|
|
118
|
+
if (handle.submitted) return;
|
|
119
|
+
} else {
|
|
120
|
+
if (quizLocked) return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (selectedRight === rightOriginalIndex) {
|
|
124
|
+
selectedRight = null;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
selectedRight = rightOriginalIndex;
|
|
129
|
+
|
|
130
|
+
if (selectedLeft !== null) {
|
|
131
|
+
createMatch(selectedLeft, selectedRight);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function createMatch(leftIndex, rightOriginalIndex) {
|
|
136
|
+
for (const [l, r] of matches) {
|
|
137
|
+
if (l === leftIndex || r === rightOriginalIndex) {
|
|
138
|
+
matches.delete(l);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
matches.set(leftIndex, rightOriginalIndex);
|
|
143
|
+
selectedLeft = null;
|
|
144
|
+
selectedRight = null;
|
|
145
|
+
|
|
146
|
+
if (!standalone) {
|
|
147
|
+
quiz.setAnswer(myIndex, new Map(matches));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function removeMatch(leftIndex) {
|
|
152
|
+
if (standalone) {
|
|
153
|
+
if (handle.submitted) return;
|
|
154
|
+
} else {
|
|
155
|
+
if (quizLocked) return;
|
|
156
|
+
}
|
|
157
|
+
matches.delete(leftIndex);
|
|
158
|
+
if (!standalone) {
|
|
159
|
+
quiz.setAnswer(myIndex, new Map(matches));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function handleRetry() {
|
|
164
|
+
saRetryCount++;
|
|
165
|
+
handle.reset();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getMatchColor(leftIndex) {
|
|
169
|
+
if (!matches.has(leftIndex)) return null;
|
|
170
|
+
return pairColors[leftIndex % pairColors.length];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getRightMatchColor(rightOriginalIndex) {
|
|
174
|
+
for (const [l, r] of matches) {
|
|
175
|
+
if (r === rightOriginalIndex) return pairColors[l % pairColors.length];
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isRightMatched(rightOriginalIndex) {
|
|
181
|
+
for (const [, r] of matches) {
|
|
182
|
+
if (r === rightOriginalIndex) return true;
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function isMatchCorrect(leftIndex) {
|
|
188
|
+
return matches.get(leftIndex) === leftIndex;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let showFeedback = $derived(standalone ? handle.submitted : quiz.feedbackVisible(myIndex));
|
|
192
|
+
let isDisabled = $derived(standalone ? handle.submitted : quizLocked);
|
|
193
|
+
</script>
|
|
194
|
+
|
|
195
|
+
{#snippet matchingContent()}
|
|
196
|
+
<p class="tessera-matching-question">{question}</p>
|
|
197
|
+
|
|
198
|
+
<div class="tessera-matching-grid">
|
|
199
|
+
<!-- Left column -->
|
|
200
|
+
<div class="tessera-matching-column">
|
|
201
|
+
<div class="tessera-matching-column-header">Match from</div>
|
|
202
|
+
{#each pairs as pair, i}
|
|
203
|
+
{@const color = getMatchColor(i)}
|
|
204
|
+
{@const isSelected = selectedLeft === i}
|
|
205
|
+
{@const matched = matches.has(i)}
|
|
206
|
+
{@const correctMatch = showFeedback && matched && isMatchCorrect(i)}
|
|
207
|
+
{@const wrongMatch = showFeedback && matched && !isMatchCorrect(i)}
|
|
208
|
+
<button
|
|
209
|
+
class="tessera-matching-item left"
|
|
210
|
+
class:selected={isSelected}
|
|
211
|
+
class:matched
|
|
212
|
+
class:correct={correctMatch}
|
|
213
|
+
class:incorrect={wrongMatch}
|
|
214
|
+
style={color ? `border-color: ${color}; --match-color: ${color}` : ''}
|
|
215
|
+
onclick={() => matched && !isDisabled ? removeMatch(i) : handleLeftClick(i)}
|
|
216
|
+
disabled={isDisabled}
|
|
217
|
+
aria-label="{pair.left}{matched ? ' (matched, activate to unmatch)' : ''}"
|
|
218
|
+
>
|
|
219
|
+
{#if matched}
|
|
220
|
+
<span class="tessera-matching-badge" style="background: {color}">
|
|
221
|
+
{i + 1}
|
|
222
|
+
</span>
|
|
223
|
+
{/if}
|
|
224
|
+
<span>{pair.left}</span>
|
|
225
|
+
{#if matched && !isDisabled}
|
|
226
|
+
<span class="tessera-matching-unmatch" aria-hidden="true">×</span>
|
|
227
|
+
{/if}
|
|
228
|
+
</button>
|
|
229
|
+
{/each}
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<!-- Right column -->
|
|
233
|
+
<div class="tessera-matching-column">
|
|
234
|
+
<div class="tessera-matching-column-header">Match to</div>
|
|
235
|
+
{#each shuffledRight as item}
|
|
236
|
+
{@const color = getRightMatchColor(item.originalIndex)}
|
|
237
|
+
{@const isSelected = selectedRight === item.originalIndex}
|
|
238
|
+
{@const matched = isRightMatched(item.originalIndex)}
|
|
239
|
+
<button
|
|
240
|
+
class="tessera-matching-item right"
|
|
241
|
+
class:selected={isSelected}
|
|
242
|
+
class:matched
|
|
243
|
+
style={color ? `border-color: ${color}; --match-color: ${color}` : ''}
|
|
244
|
+
onclick={() => handleRightClick(item.originalIndex)}
|
|
245
|
+
disabled={isDisabled}
|
|
246
|
+
aria-label="{item.text}{matched ? ' (matched)' : ''}"
|
|
247
|
+
>
|
|
248
|
+
{#if matched}
|
|
249
|
+
{@const leftIdx = [...matches.entries()].find(([, r]) => r === item.originalIndex)?.[0]}
|
|
250
|
+
<span class="tessera-matching-badge" style="background: {color}">
|
|
251
|
+
{leftIdx !== undefined ? leftIdx + 1 : ''}
|
|
252
|
+
</span>
|
|
253
|
+
{/if}
|
|
254
|
+
<span>{item.text}</span>
|
|
255
|
+
</button>
|
|
256
|
+
{/each}
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
{#if showFeedback}
|
|
261
|
+
{@const isCorrect = checkAnswer(matches)}
|
|
262
|
+
<div class="tessera-matching-review">
|
|
263
|
+
{#if isCorrect}
|
|
264
|
+
<div class="tessera-matching-result correct">
|
|
265
|
+
<svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16" aria-hidden="true">
|
|
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>
|
|
268
|
+
All pairs matched correctly!
|
|
269
|
+
</div>
|
|
270
|
+
{#if correctFeedback}
|
|
271
|
+
<p class="tessera-matching-feedback correct">{correctFeedback}</p>
|
|
272
|
+
{/if}
|
|
273
|
+
{:else}
|
|
274
|
+
<div class="tessera-matching-result incorrect">
|
|
275
|
+
<svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16" aria-hidden="true">
|
|
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>
|
|
278
|
+
Some pairs are incorrect
|
|
279
|
+
</div>
|
|
280
|
+
<div class="tessera-matching-correct-pairs">
|
|
281
|
+
<p class="tessera-matching-correct-pairs-title">Correct pairs:</p>
|
|
282
|
+
{#each pairs as pair}
|
|
283
|
+
<p class="tessera-matching-correct-pair">{pair.left} → {pair.right}</p>
|
|
284
|
+
{/each}
|
|
285
|
+
</div>
|
|
286
|
+
{#if incorrectFeedback}
|
|
287
|
+
<p class="tessera-matching-feedback incorrect">{incorrectFeedback}</p>
|
|
288
|
+
{/if}
|
|
289
|
+
{/if}
|
|
290
|
+
{#if standalone && saCanRetry}
|
|
291
|
+
<button class="tessera-standalone-retry" onclick={handleRetry}>Try again</button>
|
|
292
|
+
{/if}
|
|
293
|
+
</div>
|
|
294
|
+
{/if}
|
|
295
|
+
{/snippet}
|
|
296
|
+
|
|
297
|
+
{#if standalone}
|
|
298
|
+
<div class="tessera-matching" aria-label={question}>
|
|
299
|
+
{@render matchingContent()}
|
|
300
|
+
</div>
|
|
301
|
+
{/if}
|
|
302
|
+
|
|
303
|
+
{#snippet renderQuestion()}
|
|
304
|
+
<div class="tessera-matching" aria-label={question}>
|
|
305
|
+
{#if isLocked}
|
|
306
|
+
<div class="tessera-quiz-locked-banner">
|
|
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>
|
|
312
|
+
{/if}
|
|
313
|
+
{@render matchingContent()}
|
|
314
|
+
</div>
|
|
315
|
+
{/snippet}
|
|
316
|
+
|
|
317
|
+
<style>
|
|
318
|
+
.tessera-matching {
|
|
319
|
+
padding: var(--tessera-spacing-md) 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.tessera-matching-question {
|
|
323
|
+
font-size: 1.125rem;
|
|
324
|
+
font-weight: 600;
|
|
325
|
+
margin-bottom: var(--tessera-spacing-lg);
|
|
326
|
+
color: var(--tessera-text);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.tessera-matching-grid {
|
|
330
|
+
display: grid;
|
|
331
|
+
grid-template-columns: 1fr 1fr;
|
|
332
|
+
gap: var(--tessera-spacing-lg);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.tessera-matching-column {
|
|
336
|
+
display: flex;
|
|
337
|
+
flex-direction: column;
|
|
338
|
+
gap: var(--tessera-spacing-sm);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.tessera-matching-column-header {
|
|
342
|
+
font-size: 0.75rem;
|
|
343
|
+
font-weight: 600;
|
|
344
|
+
text-transform: uppercase;
|
|
345
|
+
letter-spacing: 0.05em;
|
|
346
|
+
color: var(--tessera-text-light);
|
|
347
|
+
padding-bottom: var(--tessera-spacing-sm);
|
|
348
|
+
border-bottom: 1px solid var(--tessera-border);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.tessera-matching-item {
|
|
352
|
+
display: flex;
|
|
353
|
+
align-items: center;
|
|
354
|
+
gap: var(--tessera-spacing-sm);
|
|
355
|
+
padding: var(--tessera-spacing-md);
|
|
356
|
+
border: 2px solid var(--tessera-border);
|
|
357
|
+
border-radius: 8px;
|
|
358
|
+
background: var(--tessera-bg);
|
|
359
|
+
cursor: pointer;
|
|
360
|
+
transition: border-color 0.2s, background 0.2s, transform 0.1s;
|
|
361
|
+
font-size: 0.9375rem;
|
|
362
|
+
font-family: var(--tessera-font-family);
|
|
363
|
+
color: var(--tessera-text);
|
|
364
|
+
text-align: left;
|
|
365
|
+
min-height: 44px;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.tessera-matching-item:hover:not(:disabled) {
|
|
369
|
+
border-color: var(--tessera-primary);
|
|
370
|
+
background: var(--tessera-bg-secondary);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.tessera-matching-item.selected {
|
|
374
|
+
border-color: var(--tessera-primary);
|
|
375
|
+
background: var(--tessera-primary-light);
|
|
376
|
+
transform: scale(1.02);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.tessera-matching-item.matched {
|
|
380
|
+
background: color-mix(in srgb, var(--match-color, var(--tessera-primary)) 8%, transparent);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.tessera-matching-item.correct {
|
|
384
|
+
border-color: var(--tessera-success) !important;
|
|
385
|
+
background: color-mix(in srgb, var(--tessera-success) 8%, transparent) !important;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.tessera-matching-item.incorrect {
|
|
389
|
+
border-color: var(--tessera-error) !important;
|
|
390
|
+
background: color-mix(in srgb, var(--tessera-error) 8%, transparent) !important;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.tessera-matching-item:disabled {
|
|
394
|
+
cursor: default;
|
|
395
|
+
opacity: 0.9;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.tessera-matching-badge {
|
|
399
|
+
flex-shrink: 0;
|
|
400
|
+
width: 22px;
|
|
401
|
+
height: 22px;
|
|
402
|
+
border-radius: 50%;
|
|
403
|
+
color: #fff;
|
|
404
|
+
font-size: 0.75rem;
|
|
405
|
+
font-weight: 700;
|
|
406
|
+
display: flex;
|
|
407
|
+
align-items: center;
|
|
408
|
+
justify-content: center;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.tessera-matching-unmatch {
|
|
412
|
+
margin-left: auto;
|
|
413
|
+
background: none;
|
|
414
|
+
border: none;
|
|
415
|
+
font-size: 1.25rem;
|
|
416
|
+
color: var(--tessera-text-light);
|
|
417
|
+
cursor: pointer;
|
|
418
|
+
padding: 0 4px;
|
|
419
|
+
min-width: 24px;
|
|
420
|
+
min-height: 24px;
|
|
421
|
+
display: flex;
|
|
422
|
+
align-items: center;
|
|
423
|
+
justify-content: center;
|
|
424
|
+
border-radius: 4px;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.tessera-matching-unmatch:hover {
|
|
428
|
+
color: var(--tessera-error);
|
|
429
|
+
background: color-mix(in srgb, var(--tessera-error) 10%, transparent);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.tessera-matching-review {
|
|
433
|
+
margin-top: var(--tessera-spacing-lg);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.tessera-matching-result {
|
|
437
|
+
display: flex;
|
|
438
|
+
align-items: center;
|
|
439
|
+
gap: var(--tessera-spacing-sm);
|
|
440
|
+
font-weight: 600;
|
|
441
|
+
font-size: 0.9375rem;
|
|
442
|
+
margin-bottom: var(--tessera-spacing-sm);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.tessera-matching-result.correct {
|
|
446
|
+
color: var(--tessera-success);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.tessera-matching-result.incorrect {
|
|
450
|
+
color: var(--tessera-error);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.tessera-matching-correct-pairs {
|
|
454
|
+
margin: var(--tessera-spacing-sm) 0;
|
|
455
|
+
font-size: 0.875rem;
|
|
456
|
+
color: var(--tessera-text-light);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.tessera-matching-correct-pairs-title {
|
|
460
|
+
font-weight: 600;
|
|
461
|
+
margin-bottom: 4px;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.tessera-matching-correct-pair {
|
|
465
|
+
margin: 2px 0;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.tessera-matching-feedback {
|
|
469
|
+
font-size: 0.875rem;
|
|
470
|
+
padding: var(--tessera-spacing-sm) var(--tessera-spacing-md);
|
|
471
|
+
border-radius: 4px;
|
|
472
|
+
margin-top: var(--tessera-spacing-sm);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.tessera-matching-feedback.correct {
|
|
476
|
+
color: var(--tessera-success);
|
|
477
|
+
background: color-mix(in srgb, var(--tessera-success) 8%, transparent);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.tessera-matching-feedback.incorrect {
|
|
481
|
+
color: var(--tessera-error);
|
|
482
|
+
background: color-mix(in srgb, var(--tessera-error) 8%, transparent);
|
|
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);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
@media (max-width: 640px) {
|
|
504
|
+
.tessera-matching-grid {
|
|
505
|
+
grid-template-columns: 1fr;
|
|
506
|
+
gap: var(--tessera-spacing-xl);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.tessera-matching-item {
|
|
510
|
+
padding: var(--tessera-spacing-md);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
</style>
|