tessera-learn 0.0.10 → 0.0.13
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/README.md +1 -0
- package/dist/audit-BBJpQGqb.js +204 -0
- package/dist/audit-BBJpQGqb.js.map +1 -0
- package/dist/plugin/a11y-cli.d.ts +1 -0
- package/dist/plugin/a11y-cli.js +36 -0
- package/dist/plugin/a11y-cli.js.map +1 -0
- package/dist/plugin/cli.js +6 -3
- package/dist/plugin/cli.js.map +1 -1
- package/dist/plugin/index.d.ts +16 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +171 -140
- package/dist/plugin/index.js.map +1 -1
- package/dist/{validation-BxWAMMnJ.js → validation-B-xTvM9B.js} +417 -81
- package/dist/validation-B-xTvM9B.js.map +1 -0
- package/package.json +17 -2
- package/src/components/Accordion.svelte +3 -1
- package/src/components/AccordionItem.svelte +1 -5
- package/src/components/Audio.svelte +22 -5
- package/src/components/Callout.svelte +5 -1
- package/src/components/Carousel.svelte +24 -8
- package/src/components/DefaultLayout.svelte +41 -12
- package/src/components/FillInTheBlank.svelte +75 -103
- package/src/components/Image.svelte +14 -10
- package/src/components/LockedBanner.svelte +5 -5
- package/src/components/Matching.svelte +48 -19
- package/src/components/MediaTracks.svelte +21 -0
- package/src/components/MultipleChoice.svelte +81 -102
- package/src/components/Quiz.svelte +63 -21
- package/src/components/ResultIcon.svelte +20 -4
- package/src/components/RevealModal.svelte +25 -22
- package/src/components/Sorting.svelte +61 -26
- package/src/components/Transcript.svelte +37 -0
- package/src/components/Video.svelte +25 -20
- package/src/components/util.ts +4 -1
- package/src/components/video-embed.ts +25 -0
- package/src/index.ts +2 -7
- package/src/plugin/a11y/audit.ts +299 -0
- package/src/plugin/a11y/contrast.ts +67 -0
- package/src/plugin/a11y-cli.ts +35 -0
- package/src/plugin/cli.ts +6 -8
- package/src/plugin/export.ts +60 -50
- package/src/plugin/index.ts +244 -101
- package/src/plugin/layout.ts +6 -51
- package/src/plugin/manifest.ts +90 -24
- package/src/plugin/override-plugin.ts +68 -0
- package/src/plugin/quiz.ts +9 -54
- package/src/plugin/validation.ts +768 -183
- package/src/runtime/App.svelte +128 -64
- package/src/runtime/LoadingBar.svelte +12 -3
- package/src/runtime/Sidebar.svelte +24 -8
- package/src/runtime/access.ts +15 -3
- package/src/runtime/adapters/cmi5.ts +68 -116
- package/src/runtime/adapters/format.ts +67 -0
- package/src/runtime/adapters/index.ts +45 -34
- package/src/runtime/adapters/retry.ts +25 -84
- package/src/runtime/adapters/scorm-base.ts +19 -15
- package/src/runtime/adapters/scorm12.ts +8 -9
- package/src/runtime/adapters/scorm2004.ts +22 -30
- package/src/runtime/adapters/web.ts +1 -1
- package/src/runtime/hooks.svelte.ts +152 -328
- package/src/runtime/interaction-format.ts +30 -12
- package/src/runtime/interaction.ts +44 -11
- package/src/runtime/navigation.svelte.ts +29 -40
- package/src/runtime/persistence.ts +2 -2
- package/src/runtime/progress.svelte.ts +22 -9
- package/src/runtime/quiz-engine.svelte.ts +361 -0
- package/src/runtime/quiz-policy.ts +28 -179
- package/src/runtime/types.ts +24 -2
- package/src/runtime/xapi/agent-rules.ts +11 -3
- package/src/runtime/xapi/client.ts +5 -5
- package/src/runtime/xapi/derive-actor.ts +2 -2
- package/src/runtime/xapi/publisher.ts +33 -40
- package/src/runtime/xapi/setup.ts +18 -15
- package/src/runtime/xapi/validation.ts +15 -6
- package/src/virtual.d.ts +4 -1
- package/styles/base.css +32 -11
- package/styles/layout.css +39 -18
- package/styles/theme.css +15 -3
- package/dist/validation-BxWAMMnJ.js.map +0 -1
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
function checkAnswer(userAnswer) {
|
|
26
26
|
if (!userAnswer || typeof userAnswer !== 'string') return false;
|
|
27
27
|
const trimmed = userAnswer.trim();
|
|
28
|
-
return answers.some(acceptable => {
|
|
28
|
+
return answers.some((acceptable) => {
|
|
29
29
|
const a = acceptable.trim();
|
|
30
30
|
if (caseSensitive) return trimmed === a;
|
|
31
31
|
return trimmed.toLowerCase() === a.toLowerCase();
|
|
@@ -33,16 +33,24 @@
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const q = useQuestion({
|
|
36
|
-
get id() {
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
get id() {
|
|
37
|
+
return id ?? `fitb-${slugFromQuestion(question)}`;
|
|
38
|
+
},
|
|
39
|
+
get weight() {
|
|
40
|
+
return weight;
|
|
41
|
+
},
|
|
42
|
+
get maxRetries() {
|
|
43
|
+
return maxRetries;
|
|
44
|
+
},
|
|
39
45
|
response: () => ({
|
|
40
46
|
type: 'fill-in',
|
|
41
47
|
response: inputValue,
|
|
42
48
|
correct: Array.isArray(answers) ? answers : [answers],
|
|
43
49
|
caseMatters: !!caseSensitive,
|
|
44
50
|
}),
|
|
45
|
-
reset: () => {
|
|
51
|
+
reset: () => {
|
|
52
|
+
inputValue = '';
|
|
53
|
+
},
|
|
46
54
|
});
|
|
47
55
|
|
|
48
56
|
// `q.mode` is fixed for the lifetime of the widget; capture once.
|
|
@@ -71,63 +79,70 @@
|
|
|
71
79
|
}
|
|
72
80
|
</script>
|
|
73
81
|
|
|
74
|
-
{#
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
82
|
+
{#snippet fitbContent()}
|
|
83
|
+
<label class="tessera-fitb-question" for={inputId}>{question}</label>
|
|
84
|
+
|
|
85
|
+
<div class="tessera-fitb-input-wrapper">
|
|
86
|
+
<input
|
|
87
|
+
type="text"
|
|
88
|
+
id={inputId}
|
|
89
|
+
class="tessera-fitb-input"
|
|
90
|
+
class:correct={q.feedbackVisible && checkAnswer(inputValue)}
|
|
91
|
+
class:incorrect={q.feedbackVisible && !checkAnswer(inputValue)}
|
|
92
|
+
value={inputValue}
|
|
93
|
+
oninput={handleInput}
|
|
94
|
+
onkeydown={handleKeydown}
|
|
95
|
+
onblur={handleBlur}
|
|
96
|
+
disabled={q.locked}
|
|
97
|
+
placeholder="Type your answer..."
|
|
98
|
+
autocomplete="off"
|
|
99
|
+
/>
|
|
100
|
+
{#if !inQuiz && !q.submitted}
|
|
101
|
+
<button
|
|
102
|
+
class="tessera-btn-primary tessera-fitb-check-btn"
|
|
103
|
+
disabled={!inputValue.trim()}
|
|
104
|
+
onclick={() => {
|
|
105
|
+
q.submit();
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
Check
|
|
109
|
+
</button>
|
|
110
|
+
{/if}
|
|
111
|
+
</div>
|
|
102
112
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
{/if}
|
|
114
|
-
{:else}
|
|
115
|
-
<div class="tessera-fitb-result incorrect">
|
|
116
|
-
<ResultIcon kind="incorrect" />
|
|
117
|
-
Incorrect
|
|
118
|
-
</div>
|
|
119
|
-
<p class="tessera-fitb-correct-answer">
|
|
120
|
-
Correct answer{answers.length > 1 ? 's' : ''}: {answers.join(', ')}
|
|
121
|
-
</p>
|
|
122
|
-
{#if incorrectFeedback}
|
|
123
|
-
<p class="tessera-fitb-feedback incorrect">{incorrectFeedback}</p>
|
|
124
|
-
{/if}
|
|
113
|
+
{#if q.feedbackVisible}
|
|
114
|
+
{@const isCorrect = checkAnswer(inputValue)}
|
|
115
|
+
<div class="tessera-fitb-review">
|
|
116
|
+
{#if isCorrect}
|
|
117
|
+
<div class="tessera-fitb-result correct">
|
|
118
|
+
<ResultIcon kind="correct" />
|
|
119
|
+
Correct
|
|
120
|
+
</div>
|
|
121
|
+
{#if correctFeedback}
|
|
122
|
+
<p class="tessera-fitb-feedback correct">{correctFeedback}</p>
|
|
125
123
|
{/if}
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
{:else}
|
|
125
|
+
<div class="tessera-fitb-result incorrect">
|
|
126
|
+
<ResultIcon kind="incorrect" />
|
|
127
|
+
Incorrect
|
|
128
|
+
</div>
|
|
129
|
+
<p class="tessera-fitb-correct-answer">
|
|
130
|
+
Correct answer{answers.length > 1 ? 's' : ''}: {answers.join(', ')}
|
|
131
|
+
</p>
|
|
132
|
+
{#if incorrectFeedback}
|
|
133
|
+
<p class="tessera-fitb-feedback incorrect">{incorrectFeedback}</p>
|
|
128
134
|
{/if}
|
|
129
|
-
|
|
130
|
-
|
|
135
|
+
{/if}
|
|
136
|
+
{#if !inQuiz && q.canRetry}
|
|
137
|
+
<RetryButton onclick={() => q.retry()} />
|
|
138
|
+
{/if}
|
|
139
|
+
</div>
|
|
140
|
+
{/if}
|
|
141
|
+
{/snippet}
|
|
142
|
+
|
|
143
|
+
{#if !inQuiz}
|
|
144
|
+
<div class="tessera-fitb">
|
|
145
|
+
{@render fitbContent()}
|
|
131
146
|
</div>
|
|
132
147
|
{/if}
|
|
133
148
|
|
|
@@ -136,50 +151,7 @@
|
|
|
136
151
|
{#if q.isLockedCorrect}
|
|
137
152
|
<LockedBanner />
|
|
138
153
|
{/if}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
<div class="tessera-fitb-input-wrapper">
|
|
142
|
-
<input
|
|
143
|
-
type="text"
|
|
144
|
-
id={inputId}
|
|
145
|
-
class="tessera-fitb-input"
|
|
146
|
-
class:correct={q.feedbackVisible && checkAnswer(q.answer)}
|
|
147
|
-
class:incorrect={q.feedbackVisible && !checkAnswer(q.answer)}
|
|
148
|
-
value={q.locked ? (q.answer ?? '') : inputValue}
|
|
149
|
-
oninput={handleInput}
|
|
150
|
-
onblur={handleBlur}
|
|
151
|
-
disabled={q.locked}
|
|
152
|
-
placeholder="Type your answer..."
|
|
153
|
-
autocomplete="off"
|
|
154
|
-
/>
|
|
155
|
-
</div>
|
|
156
|
-
|
|
157
|
-
{#if q.feedbackVisible}
|
|
158
|
-
{@const userAnswer = q.answer}
|
|
159
|
-
{@const isCorrect = checkAnswer(userAnswer)}
|
|
160
|
-
<div class="tessera-fitb-review">
|
|
161
|
-
{#if isCorrect}
|
|
162
|
-
<div class="tessera-fitb-result correct">
|
|
163
|
-
<ResultIcon kind="correct" />
|
|
164
|
-
Correct
|
|
165
|
-
</div>
|
|
166
|
-
{#if correctFeedback}
|
|
167
|
-
<p class="tessera-fitb-feedback correct">{correctFeedback}</p>
|
|
168
|
-
{/if}
|
|
169
|
-
{:else}
|
|
170
|
-
<div class="tessera-fitb-result incorrect">
|
|
171
|
-
<ResultIcon kind="incorrect" />
|
|
172
|
-
Incorrect
|
|
173
|
-
</div>
|
|
174
|
-
<p class="tessera-fitb-correct-answer">
|
|
175
|
-
Correct answer{answers.length > 1 ? 's' : ''}: {answers.join(', ')}
|
|
176
|
-
</p>
|
|
177
|
-
{#if incorrectFeedback}
|
|
178
|
-
<p class="tessera-fitb-feedback incorrect">{incorrectFeedback}</p>
|
|
179
|
-
{/if}
|
|
180
|
-
{/if}
|
|
181
|
-
</div>
|
|
182
|
-
{/if}
|
|
154
|
+
{@render fitbContent()}
|
|
183
155
|
</div>
|
|
184
156
|
{/snippet}
|
|
185
157
|
|
|
@@ -4,22 +4,26 @@
|
|
|
4
4
|
* Lazy-loaded image with optional caption, rendered as <figure>.
|
|
5
5
|
*
|
|
6
6
|
* @prop {string} src - Image source URL (supports $assets/ paths)
|
|
7
|
-
* @prop {string} alt - Alt text
|
|
7
|
+
* @prop {string} alt - Alt text. Required unless `decorative` is set; the
|
|
8
|
+
* linter (rule 1.3) enforces exactly one of {non-empty alt, decorative}.
|
|
9
|
+
* @prop {boolean} [decorative] - Mark a purely ornamental image: renders an
|
|
10
|
+
* empty alt and aria-hidden so assistive tech skips it.
|
|
8
11
|
* @prop {string} [caption] - Optional caption below image
|
|
9
12
|
*/
|
|
10
|
-
|
|
13
|
+
import { resolveAsset } from './util.js';
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
);
|
|
15
|
+
let { src, alt, decorative = false, caption = '' } = $props();
|
|
16
|
+
let resolvedSrc = $derived(resolveAsset(src));
|
|
19
17
|
</script>
|
|
20
18
|
|
|
21
19
|
<figure class="tessera-image">
|
|
22
|
-
<img
|
|
20
|
+
<img
|
|
21
|
+
src={resolvedSrc}
|
|
22
|
+
alt={decorative ? '' : alt}
|
|
23
|
+
aria-hidden={decorative ? 'true' : undefined}
|
|
24
|
+
loading="lazy"
|
|
25
|
+
class="tessera-image-img"
|
|
26
|
+
/>
|
|
23
27
|
{#if caption}
|
|
24
28
|
<figcaption class="tessera-image-caption">{caption}</figcaption>
|
|
25
29
|
{/if}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
|
|
2
|
+
import ResultIcon from './ResultIcon.svelte';
|
|
3
|
+
let { message = 'You already got this one right — click Next to continue.' } =
|
|
4
|
+
$props();
|
|
3
5
|
</script>
|
|
4
6
|
|
|
5
7
|
<div class="tessera-quiz-locked-banner">
|
|
6
|
-
<
|
|
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>
|
|
8
|
+
<ResultIcon kind="correct" />
|
|
9
9
|
{message}
|
|
10
10
|
</div>
|
|
11
11
|
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
font-weight: 500;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
.tessera-quiz-locked-banner svg {
|
|
27
|
+
.tessera-quiz-locked-banner :global(svg) {
|
|
28
28
|
flex-shrink: 0;
|
|
29
29
|
}
|
|
30
30
|
</style>
|
|
@@ -18,19 +18,29 @@
|
|
|
18
18
|
} = $props();
|
|
19
19
|
|
|
20
20
|
let shuffledRight = $state([]);
|
|
21
|
-
|
|
21
|
+
const matches = new SvelteMap();
|
|
22
22
|
// Reverse index (right.originalIndex → left index) for O(1) right-column lookups.
|
|
23
|
-
|
|
23
|
+
const rightToLeft = new SvelteMap();
|
|
24
24
|
let selectedLeft = $state(null);
|
|
25
25
|
let selectedRight = $state(null);
|
|
26
26
|
|
|
27
27
|
const pairColors = [
|
|
28
|
-
'#2563eb',
|
|
29
|
-
'#
|
|
28
|
+
'#2563eb',
|
|
29
|
+
'#9333ea',
|
|
30
|
+
'#0891b2',
|
|
31
|
+
'#c2410c',
|
|
32
|
+
'#4f46e5',
|
|
33
|
+
'#0d9488',
|
|
34
|
+
'#b91c1c',
|
|
35
|
+
'#7c3aed',
|
|
36
|
+
'#0369a1',
|
|
37
|
+
'#a16207',
|
|
30
38
|
];
|
|
31
39
|
|
|
32
40
|
function initShuffle() {
|
|
33
|
-
shuffledRight = shuffle(
|
|
41
|
+
shuffledRight = shuffle(
|
|
42
|
+
pairs.map((p, i) => ({ text: p.right, originalIndex: i })),
|
|
43
|
+
);
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
function checkAnswer(answer) {
|
|
@@ -43,17 +53,23 @@
|
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
function resetState() {
|
|
46
|
-
matches
|
|
47
|
-
rightToLeft
|
|
56
|
+
matches.clear();
|
|
57
|
+
rightToLeft.clear();
|
|
48
58
|
selectedLeft = null;
|
|
49
59
|
selectedRight = null;
|
|
50
60
|
initShuffle();
|
|
51
61
|
}
|
|
52
62
|
|
|
53
63
|
const q = useQuestion({
|
|
54
|
-
get id() {
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
get id() {
|
|
65
|
+
return id ?? `matching-${slugFromQuestion(question)}`;
|
|
66
|
+
},
|
|
67
|
+
get weight() {
|
|
68
|
+
return weight;
|
|
69
|
+
},
|
|
70
|
+
get maxRetries() {
|
|
71
|
+
return maxRetries;
|
|
72
|
+
},
|
|
57
73
|
response: () => ({
|
|
58
74
|
type: 'matching',
|
|
59
75
|
response: [...matches.entries()].map(([l, r]) => [String(l), String(r)]),
|
|
@@ -157,11 +173,12 @@
|
|
|
157
173
|
<!-- Left column -->
|
|
158
174
|
<div class="tessera-matching-column">
|
|
159
175
|
<div class="tessera-matching-column-header">Match from</div>
|
|
160
|
-
{#each pairs as pair, i}
|
|
176
|
+
{#each pairs as pair, i (i)}
|
|
161
177
|
{@const color = getMatchColor(i)}
|
|
162
178
|
{@const isSelected = selectedLeft === i}
|
|
163
179
|
{@const matched = matches.has(i)}
|
|
164
|
-
{@const correctMatch =
|
|
180
|
+
{@const correctMatch =
|
|
181
|
+
q.feedbackVisible && matched && isMatchCorrect(i)}
|
|
165
182
|
{@const wrongMatch = q.feedbackVisible && matched && !isMatchCorrect(i)}
|
|
166
183
|
<button
|
|
167
184
|
class="tessera-matching-item left"
|
|
@@ -170,9 +187,12 @@
|
|
|
170
187
|
class:correct={correctMatch}
|
|
171
188
|
class:incorrect={wrongMatch}
|
|
172
189
|
style={color ? `border-color: ${color}; --match-color: ${color}` : ''}
|
|
173
|
-
onclick={() =>
|
|
190
|
+
onclick={() =>
|
|
191
|
+
matched && !q.locked ? removeMatch(i) : handleLeftClick(i)}
|
|
174
192
|
disabled={q.locked}
|
|
175
|
-
aria-label="{pair.left}{matched
|
|
193
|
+
aria-label="{pair.left}{matched
|
|
194
|
+
? ' (matched, activate to unmatch)'
|
|
195
|
+
: ''}"
|
|
176
196
|
>
|
|
177
197
|
{#if matched}
|
|
178
198
|
<span class="tessera-matching-badge" style="background: {color}">
|
|
@@ -190,7 +210,7 @@
|
|
|
190
210
|
<!-- Right column -->
|
|
191
211
|
<div class="tessera-matching-column">
|
|
192
212
|
<div class="tessera-matching-column-header">Match to</div>
|
|
193
|
-
{#each shuffledRight as item}
|
|
213
|
+
{#each shuffledRight as item (item.originalIndex)}
|
|
194
214
|
{@const color = getRightMatchColor(item.originalIndex)}
|
|
195
215
|
{@const isSelected = selectedRight === item.originalIndex}
|
|
196
216
|
{@const matched = isRightMatched(item.originalIndex)}
|
|
@@ -233,8 +253,10 @@
|
|
|
233
253
|
</div>
|
|
234
254
|
<div class="tessera-matching-correct-pairs">
|
|
235
255
|
<p class="tessera-matching-correct-pairs-title">Correct pairs:</p>
|
|
236
|
-
{#each pairs as pair}
|
|
237
|
-
<p class="tessera-matching-correct-pair">
|
|
256
|
+
{#each pairs as pair, i (i)}
|
|
257
|
+
<p class="tessera-matching-correct-pair">
|
|
258
|
+
{pair.left} → {pair.right}
|
|
259
|
+
</p>
|
|
238
260
|
{/each}
|
|
239
261
|
</div>
|
|
240
262
|
{#if incorrectFeedback}
|
|
@@ -306,7 +328,10 @@
|
|
|
306
328
|
border-radius: 8px;
|
|
307
329
|
background: var(--tessera-bg);
|
|
308
330
|
cursor: pointer;
|
|
309
|
-
transition:
|
|
331
|
+
transition:
|
|
332
|
+
border-color 0.2s,
|
|
333
|
+
background 0.2s,
|
|
334
|
+
transform 0.1s;
|
|
310
335
|
font-size: 0.9375rem;
|
|
311
336
|
font-family: var(--tessera-font-family);
|
|
312
337
|
color: var(--tessera-text);
|
|
@@ -326,7 +351,11 @@
|
|
|
326
351
|
}
|
|
327
352
|
|
|
328
353
|
.tessera-matching-item.matched {
|
|
329
|
-
background: color-mix(
|
|
354
|
+
background: color-mix(
|
|
355
|
+
in srgb,
|
|
356
|
+
var(--match-color, var(--tessera-primary)) 8%,
|
|
357
|
+
transparent
|
|
358
|
+
);
|
|
330
359
|
}
|
|
331
360
|
|
|
332
361
|
.tessera-matching-item.correct {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/**
|
|
3
|
+
* @component MediaTracks
|
|
4
|
+
* Renders caption/subtitle <track> elements for a native <audio>/<video>.
|
|
5
|
+
* Used inline as a child of the media element so the tracks attach to it.
|
|
6
|
+
*
|
|
7
|
+
* @prop {Array<{ src: string, kind?: 'captions'|'subtitles', srclang?: string, label?: string }>} [tracks]
|
|
8
|
+
*/
|
|
9
|
+
import { resolveAsset } from './util.js';
|
|
10
|
+
|
|
11
|
+
let { tracks = [] } = $props();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
{#each tracks as track, i (i)}
|
|
15
|
+
<track
|
|
16
|
+
src={resolveAsset(track.src)}
|
|
17
|
+
kind={track.kind ?? 'captions'}
|
|
18
|
+
srclang={track.srclang}
|
|
19
|
+
label={track.label}
|
|
20
|
+
/>
|
|
21
|
+
{/each}
|