tessera-learn 0.0.11 → 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 +2 -1
- 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 +85 -10
- package/dist/plugin/index.js.map +1 -1
- package/dist/{validation-D9DXlqNP.js → validation-B-xTvM9B.js} +342 -18
- 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 +17 -3
- 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 +16 -6
- package/src/components/Image.svelte +12 -3
- package/src/components/LockedBanner.svelte +2 -1
- package/src/components/Matching.svelte +48 -19
- package/src/components/MediaTracks.svelte +21 -0
- package/src/components/MultipleChoice.svelte +33 -13
- package/src/components/Quiz.svelte +61 -20
- 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 +21 -18
- package/src/components/util.ts +3 -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 +4 -1
- package/src/plugin/export.ts +42 -14
- package/src/plugin/index.ts +216 -44
- package/src/plugin/manifest.ts +62 -22
- package/src/plugin/validation.ts +736 -122
- package/src/runtime/App.svelte +119 -48
- 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 +55 -33
- package/src/runtime/adapters/index.ts +22 -10
- package/src/runtime/adapters/retry.ts +25 -20
- package/src/runtime/adapters/scorm-base.ts +19 -15
- package/src/runtime/adapters/scorm12.ts +7 -8
- package/src/runtime/adapters/scorm2004.ts +11 -14
- package/src/runtime/adapters/web.ts +1 -1
- package/src/runtime/hooks.svelte.ts +152 -326
- package/src/runtime/interaction-format.ts +30 -12
- package/src/runtime/interaction.ts +44 -11
- package/src/runtime/navigation.svelte.ts +27 -11
- package/src/runtime/persistence.ts +2 -2
- package/src/runtime/progress.svelte.ts +13 -9
- package/src/runtime/quiz-engine.svelte.ts +361 -0
- package/src/runtime/quiz-policy.ts +9 -3
- package/src/runtime/types.ts +24 -2
- package/src/runtime/xapi/agent-rules.ts +4 -1
- package/src/runtime/xapi/client.ts +5 -5
- package/src/runtime/xapi/derive-actor.ts +2 -2
- package/src/runtime/xapi/publisher.ts +32 -29
- 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-D9DXlqNP.js.map +0 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure-JS WCAG contrast helpers. App.svelte's parseColor is canvas-based and
|
|
3
|
+
* browser-only; this runs at build time in the linter (rule 1.7). Only opaque
|
|
4
|
+
* #hex (3/4/6/8) is parsed — other CSS color forms and translucent hex return
|
|
5
|
+
* null and fall through to the Tier-2 axe audit, which uses the browser's own
|
|
6
|
+
* parser.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function parseHex(
|
|
10
|
+
input: string,
|
|
11
|
+
): { r: number; g: number; b: number; a: number } | null {
|
|
12
|
+
const v = input.trim();
|
|
13
|
+
const m = /^#([0-9a-fA-F]{3,8})$/.exec(v);
|
|
14
|
+
if (!m) return null;
|
|
15
|
+
const h = m[1];
|
|
16
|
+
let r: number,
|
|
17
|
+
g: number,
|
|
18
|
+
b: number,
|
|
19
|
+
a = 255;
|
|
20
|
+
if (h.length === 3 || h.length === 4) {
|
|
21
|
+
r = parseInt(h[0] + h[0], 16);
|
|
22
|
+
g = parseInt(h[1] + h[1], 16);
|
|
23
|
+
b = parseInt(h[2] + h[2], 16);
|
|
24
|
+
if (h.length === 4) a = parseInt(h[3] + h[3], 16);
|
|
25
|
+
} else if (h.length === 6 || h.length === 8) {
|
|
26
|
+
r = parseInt(h.slice(0, 2), 16);
|
|
27
|
+
g = parseInt(h.slice(2, 4), 16);
|
|
28
|
+
b = parseInt(h.slice(4, 6), 16);
|
|
29
|
+
if (h.length === 8) a = parseInt(h.slice(6, 8), 16);
|
|
30
|
+
} else {
|
|
31
|
+
return null; // 5/7 hex digits are not a valid color
|
|
32
|
+
}
|
|
33
|
+
return { r, g, b, a };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function linearize(channel: number): number {
|
|
37
|
+
const c = channel / 255;
|
|
38
|
+
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** sRGB hex → relative luminance (0–1), or null if not a parseable opaque hex. */
|
|
42
|
+
export function relativeLuminance(hex: string): number | null {
|
|
43
|
+
const rgb = parseHex(hex);
|
|
44
|
+
if (!rgb) return null;
|
|
45
|
+
// A translucent color's on-screen luminance depends on the backdrop; defer
|
|
46
|
+
// those to the in-browser Tier-2 audit rather than guess a composite.
|
|
47
|
+
if (rgb.a !== 255) return null;
|
|
48
|
+
return (
|
|
49
|
+
0.2126 * linearize(rgb.r) +
|
|
50
|
+
0.7152 * linearize(rgb.g) +
|
|
51
|
+
0.0722 * linearize(rgb.b)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* WCAG contrast ratio between two colors. Order-independent — the lighter/darker
|
|
57
|
+
* ordering is handled internally, so callers may pass the colors in any order.
|
|
58
|
+
* Returns null if either color isn't a parseable hex.
|
|
59
|
+
*/
|
|
60
|
+
export function contrastRatio(a: string, b: string): number | null {
|
|
61
|
+
const la = relativeLuminance(a);
|
|
62
|
+
const lb = relativeLuminance(b);
|
|
63
|
+
if (la === null || lb === null) return null;
|
|
64
|
+
const lighter = Math.max(la, lb);
|
|
65
|
+
const darker = Math.min(la, lb);
|
|
66
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
67
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runAudit, type ImpactLevel } from './a11y/audit.js';
|
|
3
|
+
|
|
4
|
+
const VALID_THRESHOLDS: ImpactLevel[] = [
|
|
5
|
+
'minor',
|
|
6
|
+
'moderate',
|
|
7
|
+
'serious',
|
|
8
|
+
'critical',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
let threshold: ImpactLevel | undefined;
|
|
13
|
+
let rebuild = false;
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < args.length; i++) {
|
|
16
|
+
const arg = args[i];
|
|
17
|
+
if (arg === '--threshold') {
|
|
18
|
+
const value = args[++i] as ImpactLevel;
|
|
19
|
+
if (!VALID_THRESHOLDS.includes(value)) {
|
|
20
|
+
console.error(
|
|
21
|
+
`[tessera a11y] --threshold must be one of: ${VALID_THRESHOLDS.join(', ')}`,
|
|
22
|
+
);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
threshold = value;
|
|
26
|
+
} else if (arg === '--build') {
|
|
27
|
+
rebuild = true;
|
|
28
|
+
} else {
|
|
29
|
+
console.error(`[tessera a11y] Unknown argument: ${arg}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const code = await runAudit(process.cwd(), { threshold, rebuild });
|
|
35
|
+
process.exit(code);
|
package/src/plugin/cli.ts
CHANGED
|
@@ -17,9 +17,12 @@ if (errors.length > 0) {
|
|
|
17
17
|
|
|
18
18
|
if (warnings.length > 0) {
|
|
19
19
|
console.log(
|
|
20
|
-
`\n\x1b[33mValidation passed with ${warnings.length} warning(s).\x1b[0m
|
|
20
|
+
`\n\x1b[33mValidation passed with ${warnings.length} warning(s).\x1b[0m`,
|
|
21
21
|
);
|
|
22
22
|
} else {
|
|
23
23
|
console.log('\x1b[32m[tessera]\x1b[0m Validation passed — no issues found.');
|
|
24
24
|
}
|
|
25
|
+
console.log(
|
|
26
|
+
'\x1b[2m[tessera] Static checks only. For a full runtime accessibility audit, run: npm run accessibility-check\x1b[0m',
|
|
27
|
+
);
|
|
25
28
|
process.exit(0);
|
package/src/plugin/export.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
readdirSync,
|
|
4
|
+
statSync,
|
|
5
|
+
writeFileSync,
|
|
6
|
+
unlinkSync,
|
|
7
|
+
} from 'node:fs';
|
|
2
8
|
import { resolve } from 'node:path';
|
|
3
9
|
import { createWriteStream } from 'node:fs';
|
|
4
10
|
import { createHash } from 'node:crypto';
|
|
@@ -106,7 +112,7 @@ const SCORM_DIALECTS: Record<'1.2' | '2004', ScormManifestDialect> = {
|
|
|
106
112
|
export function generateScormManifest(
|
|
107
113
|
version: '1.2' | '2004',
|
|
108
114
|
config: ExportConfig,
|
|
109
|
-
distDir: string
|
|
115
|
+
distDir: string,
|
|
110
116
|
): string {
|
|
111
117
|
const dialect = SCORM_DIALECTS[version];
|
|
112
118
|
const title = escapeXml(config.title || 'Tessera Course');
|
|
@@ -143,14 +149,14 @@ ${fileElements}
|
|
|
143
149
|
|
|
144
150
|
export function generateSCORM12Manifest(
|
|
145
151
|
config: ExportConfig,
|
|
146
|
-
distDir: string
|
|
152
|
+
distDir: string,
|
|
147
153
|
): string {
|
|
148
154
|
return generateScormManifest('1.2', config, distDir);
|
|
149
155
|
}
|
|
150
156
|
|
|
151
157
|
export function generateSCORM2004Manifest(
|
|
152
158
|
config: ExportConfig,
|
|
153
|
-
distDir: string
|
|
159
|
+
distDir: string,
|
|
154
160
|
): string {
|
|
155
161
|
return generateScormManifest('2004', config, distDir);
|
|
156
162
|
}
|
|
@@ -164,7 +170,7 @@ export function generateCMI5Xml(config: ExportConfig): string {
|
|
|
164
170
|
const auId = stableUrn('au', `tessera-au:${config.title || ''}`);
|
|
165
171
|
// cmi5 §10.2.4 caps masteryScore at 4 decimals; avoid float drift like 0.7000000000000001.
|
|
166
172
|
const masteryScore = Number(
|
|
167
|
-
((config.scoring?.passingScore ?? 70) / 100).toFixed(4)
|
|
173
|
+
((config.scoring?.passingScore ?? 70) / 100).toFixed(4),
|
|
168
174
|
);
|
|
169
175
|
// cmi5 §13.1.4 — `moveOn` decides which verb(s) the LMS treats as
|
|
170
176
|
// satisfying the AU. For graded courses (completion gated on a quiz)
|
|
@@ -193,7 +199,7 @@ export function generateCMI5Xml(config: ExportConfig): string {
|
|
|
193
199
|
|
|
194
200
|
export async function createZip(
|
|
195
201
|
distDir: string,
|
|
196
|
-
outputPath: string
|
|
202
|
+
outputPath: string,
|
|
197
203
|
): Promise<number> {
|
|
198
204
|
return new Promise((res, reject) => {
|
|
199
205
|
const output = createWriteStream(outputPath);
|
|
@@ -207,7 +213,7 @@ export async function createZip(
|
|
|
207
213
|
|
|
208
214
|
archive.pipe(output);
|
|
209
215
|
archive.directory(distDir, false);
|
|
210
|
-
archive.finalize();
|
|
216
|
+
void archive.finalize();
|
|
211
217
|
});
|
|
212
218
|
}
|
|
213
219
|
|
|
@@ -222,7 +228,9 @@ function cleanOldZips(projectRoot: string, slug: string): void {
|
|
|
222
228
|
try {
|
|
223
229
|
for (const f of readdirSync(projectRoot)) {
|
|
224
230
|
if (f.startsWith(`${slug}-`) && f.endsWith('.zip')) {
|
|
225
|
-
try {
|
|
231
|
+
try {
|
|
232
|
+
unlinkSync(resolve(projectRoot, f));
|
|
233
|
+
} catch {}
|
|
226
234
|
}
|
|
227
235
|
}
|
|
228
236
|
} catch {}
|
|
@@ -231,16 +239,32 @@ function cleanOldZips(projectRoot: string, slug: string): void {
|
|
|
231
239
|
/** Packaged (zipped) export targets: which manifest file to write and how. */
|
|
232
240
|
const PACKAGED_EXPORTS: Record<
|
|
233
241
|
'scorm12' | 'scorm2004' | 'cmi5',
|
|
234
|
-
{
|
|
242
|
+
{
|
|
243
|
+
manifestFile: string;
|
|
244
|
+
label: string;
|
|
245
|
+
generate: (config: ExportConfig, distDir: string) => string;
|
|
246
|
+
}
|
|
235
247
|
> = {
|
|
236
|
-
scorm12: {
|
|
237
|
-
|
|
238
|
-
|
|
248
|
+
scorm12: {
|
|
249
|
+
manifestFile: 'imsmanifest.xml',
|
|
250
|
+
label: 'SCORM 1.2',
|
|
251
|
+
generate: generateSCORM12Manifest,
|
|
252
|
+
},
|
|
253
|
+
scorm2004: {
|
|
254
|
+
manifestFile: 'imsmanifest.xml',
|
|
255
|
+
label: 'SCORM 2004',
|
|
256
|
+
generate: generateSCORM2004Manifest,
|
|
257
|
+
},
|
|
258
|
+
cmi5: {
|
|
259
|
+
manifestFile: 'cmi5.xml',
|
|
260
|
+
label: 'CMI5',
|
|
261
|
+
generate: (config) => generateCMI5Xml(config),
|
|
262
|
+
},
|
|
239
263
|
};
|
|
240
264
|
|
|
241
265
|
export async function runExport(
|
|
242
266
|
projectRoot: string,
|
|
243
|
-
config: ExportConfig
|
|
267
|
+
config: ExportConfig,
|
|
244
268
|
): Promise<void> {
|
|
245
269
|
const distDir = resolve(projectRoot, 'dist');
|
|
246
270
|
const standard = config.export?.standard || 'web';
|
|
@@ -260,7 +284,11 @@ export async function runExport(
|
|
|
260
284
|
const spec = PACKAGED_EXPORTS[standard as keyof typeof PACKAGED_EXPORTS];
|
|
261
285
|
if (!spec) return; // unknown standard — the validator rejects these upstream
|
|
262
286
|
|
|
263
|
-
writeFileSync(
|
|
287
|
+
writeFileSync(
|
|
288
|
+
resolve(distDir, spec.manifestFile),
|
|
289
|
+
spec.generate(config, distDir),
|
|
290
|
+
'utf-8',
|
|
291
|
+
);
|
|
264
292
|
cleanOldZips(projectRoot, slug);
|
|
265
293
|
const zipSize = await createZip(distDir, zipPath);
|
|
266
294
|
console.log(`✓ ${spec.label} export: ${zipName} (${formatSize(zipSize)})`);
|