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
|
@@ -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
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { validateProject } from './validation.js';
|
|
2
|
+
import { validateProject, reportValidationIssues } from './validation.js';
|
|
3
3
|
|
|
4
4
|
const projectRoot = process.cwd();
|
|
5
5
|
const { errors, warnings } = validateProject(projectRoot);
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
console.warn(`\x1b[33m[tessera warning]\x1b[0m ${warning}`);
|
|
9
|
-
}
|
|
10
|
-
for (const error of errors) {
|
|
11
|
-
console.error(`\x1b[31m[tessera error]\x1b[0m ${error}`);
|
|
12
|
-
}
|
|
7
|
+
reportValidationIssues({ errors, warnings });
|
|
13
8
|
|
|
14
9
|
if (errors.length > 0) {
|
|
15
10
|
const summary =
|
|
@@ -22,9 +17,12 @@ if (errors.length > 0) {
|
|
|
22
17
|
|
|
23
18
|
if (warnings.length > 0) {
|
|
24
19
|
console.log(
|
|
25
|
-
`\n\x1b[33mValidation passed with ${warnings.length} warning(s).\x1b[0m
|
|
20
|
+
`\n\x1b[33mValidation passed with ${warnings.length} warning(s).\x1b[0m`,
|
|
26
21
|
);
|
|
27
22
|
} else {
|
|
28
23
|
console.log('\x1b[32m[tessera]\x1b[0m Validation passed — no issues found.');
|
|
29
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
|
+
);
|
|
30
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,15 +228,43 @@ 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 {}
|
|
229
237
|
}
|
|
230
238
|
|
|
239
|
+
/** Packaged (zipped) export targets: which manifest file to write and how. */
|
|
240
|
+
const PACKAGED_EXPORTS: Record<
|
|
241
|
+
'scorm12' | 'scorm2004' | 'cmi5',
|
|
242
|
+
{
|
|
243
|
+
manifestFile: string;
|
|
244
|
+
label: string;
|
|
245
|
+
generate: (config: ExportConfig, distDir: string) => string;
|
|
246
|
+
}
|
|
247
|
+
> = {
|
|
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
|
+
},
|
|
263
|
+
};
|
|
264
|
+
|
|
231
265
|
export async function runExport(
|
|
232
266
|
projectRoot: string,
|
|
233
|
-
config: ExportConfig
|
|
267
|
+
config: ExportConfig,
|
|
234
268
|
): Promise<void> {
|
|
235
269
|
const distDir = resolve(projectRoot, 'dist');
|
|
236
270
|
const standard = config.export?.standard || 'web';
|
|
@@ -239,47 +273,23 @@ export async function runExport(
|
|
|
239
273
|
const zipName = `${slug}-${version}.zip`;
|
|
240
274
|
const zipPath = resolve(projectRoot, zipName);
|
|
241
275
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
console.log(`✓ Web export: dist/ (${formatSize(totalSize)})`);
|
|
251
|
-
break;
|
|
252
|
-
}
|
|
276
|
+
if (standard === 'web') {
|
|
277
|
+
const files = collectFiles(distDir);
|
|
278
|
+
let totalSize = 0;
|
|
279
|
+
for (const f of files) totalSize += statSync(resolve(distDir, f)).size;
|
|
280
|
+
console.log(`✓ Web export: dist/ (${formatSize(totalSize)})`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
253
283
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
writeFileSync(resolve(distDir, 'imsmanifest.xml'), manifest, 'utf-8');
|
|
257
|
-
cleanOldZips(projectRoot, slug);
|
|
258
|
-
const zipSize = await createZip(distDir, zipPath);
|
|
259
|
-
console.log(
|
|
260
|
-
`✓ SCORM 1.2 export: ${zipName} (${formatSize(zipSize)})`
|
|
261
|
-
);
|
|
262
|
-
break;
|
|
263
|
-
}
|
|
284
|
+
const spec = PACKAGED_EXPORTS[standard as keyof typeof PACKAGED_EXPORTS];
|
|
285
|
+
if (!spec) return; // unknown standard — the validator rejects these upstream
|
|
264
286
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
break;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
case 'cmi5': {
|
|
277
|
-
const xml = generateCMI5Xml(config);
|
|
278
|
-
writeFileSync(resolve(distDir, 'cmi5.xml'), xml, 'utf-8');
|
|
279
|
-
cleanOldZips(projectRoot, slug);
|
|
280
|
-
const zipSize = await createZip(distDir, zipPath);
|
|
281
|
-
console.log(`✓ CMI5 export: ${zipName} (${formatSize(zipSize)})`);
|
|
282
|
-
break;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
287
|
+
writeFileSync(
|
|
288
|
+
resolve(distDir, spec.manifestFile),
|
|
289
|
+
spec.generate(config, distDir),
|
|
290
|
+
'utf-8',
|
|
291
|
+
);
|
|
292
|
+
cleanOldZips(projectRoot, slug);
|
|
293
|
+
const zipSize = await createZip(distDir, zipPath);
|
|
294
|
+
console.log(`✓ ${spec.label} export: ${zipName} (${formatSize(zipSize)})`);
|
|
285
295
|
}
|