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.
Files changed (79) hide show
  1. package/README.md +1 -0
  2. package/dist/audit-BBJpQGqb.js +204 -0
  3. package/dist/audit-BBJpQGqb.js.map +1 -0
  4. package/dist/plugin/a11y-cli.d.ts +1 -0
  5. package/dist/plugin/a11y-cli.js +36 -0
  6. package/dist/plugin/a11y-cli.js.map +1 -0
  7. package/dist/plugin/cli.js +6 -3
  8. package/dist/plugin/cli.js.map +1 -1
  9. package/dist/plugin/index.d.ts +16 -1
  10. package/dist/plugin/index.d.ts.map +1 -1
  11. package/dist/plugin/index.js +171 -140
  12. package/dist/plugin/index.js.map +1 -1
  13. package/dist/{validation-BxWAMMnJ.js → validation-B-xTvM9B.js} +417 -81
  14. package/dist/validation-B-xTvM9B.js.map +1 -0
  15. package/package.json +17 -2
  16. package/src/components/Accordion.svelte +3 -1
  17. package/src/components/AccordionItem.svelte +1 -5
  18. package/src/components/Audio.svelte +22 -5
  19. package/src/components/Callout.svelte +5 -1
  20. package/src/components/Carousel.svelte +24 -8
  21. package/src/components/DefaultLayout.svelte +41 -12
  22. package/src/components/FillInTheBlank.svelte +75 -103
  23. package/src/components/Image.svelte +14 -10
  24. package/src/components/LockedBanner.svelte +5 -5
  25. package/src/components/Matching.svelte +48 -19
  26. package/src/components/MediaTracks.svelte +21 -0
  27. package/src/components/MultipleChoice.svelte +81 -102
  28. package/src/components/Quiz.svelte +63 -21
  29. package/src/components/ResultIcon.svelte +20 -4
  30. package/src/components/RevealModal.svelte +25 -22
  31. package/src/components/Sorting.svelte +61 -26
  32. package/src/components/Transcript.svelte +37 -0
  33. package/src/components/Video.svelte +25 -20
  34. package/src/components/util.ts +4 -1
  35. package/src/components/video-embed.ts +25 -0
  36. package/src/index.ts +2 -7
  37. package/src/plugin/a11y/audit.ts +299 -0
  38. package/src/plugin/a11y/contrast.ts +67 -0
  39. package/src/plugin/a11y-cli.ts +35 -0
  40. package/src/plugin/cli.ts +6 -8
  41. package/src/plugin/export.ts +60 -50
  42. package/src/plugin/index.ts +244 -101
  43. package/src/plugin/layout.ts +6 -51
  44. package/src/plugin/manifest.ts +90 -24
  45. package/src/plugin/override-plugin.ts +68 -0
  46. package/src/plugin/quiz.ts +9 -54
  47. package/src/plugin/validation.ts +768 -183
  48. package/src/runtime/App.svelte +128 -64
  49. package/src/runtime/LoadingBar.svelte +12 -3
  50. package/src/runtime/Sidebar.svelte +24 -8
  51. package/src/runtime/access.ts +15 -3
  52. package/src/runtime/adapters/cmi5.ts +68 -116
  53. package/src/runtime/adapters/format.ts +67 -0
  54. package/src/runtime/adapters/index.ts +45 -34
  55. package/src/runtime/adapters/retry.ts +25 -84
  56. package/src/runtime/adapters/scorm-base.ts +19 -15
  57. package/src/runtime/adapters/scorm12.ts +8 -9
  58. package/src/runtime/adapters/scorm2004.ts +22 -30
  59. package/src/runtime/adapters/web.ts +1 -1
  60. package/src/runtime/hooks.svelte.ts +152 -328
  61. package/src/runtime/interaction-format.ts +30 -12
  62. package/src/runtime/interaction.ts +44 -11
  63. package/src/runtime/navigation.svelte.ts +29 -40
  64. package/src/runtime/persistence.ts +2 -2
  65. package/src/runtime/progress.svelte.ts +22 -9
  66. package/src/runtime/quiz-engine.svelte.ts +361 -0
  67. package/src/runtime/quiz-policy.ts +28 -179
  68. package/src/runtime/types.ts +24 -2
  69. package/src/runtime/xapi/agent-rules.ts +11 -3
  70. package/src/runtime/xapi/client.ts +5 -5
  71. package/src/runtime/xapi/derive-actor.ts +2 -2
  72. package/src/runtime/xapi/publisher.ts +33 -40
  73. package/src/runtime/xapi/setup.ts +18 -15
  74. package/src/runtime/xapi/validation.ts +15 -6
  75. package/src/virtual.d.ts +4 -1
  76. package/styles/base.css +32 -11
  77. package/styles/layout.css +39 -18
  78. package/styles/theme.css +15 -3
  79. 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
- for (const warning of warnings) {
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);
@@ -1,4 +1,10 @@
1
- import { existsSync, readdirSync, statSync, writeFileSync, unlinkSync } from 'node:fs';
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 { unlinkSync(resolve(projectRoot, f)); } catch {}
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
- switch (standard) {
243
- case 'web': {
244
- // Compute dist size
245
- const files = collectFiles(distDir);
246
- let totalSize = 0;
247
- for (const f of files) {
248
- totalSize += statSync(resolve(distDir, f)).size;
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
- case 'scorm12': {
255
- const manifest = generateSCORM12Manifest(config, distDir);
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
- case 'scorm2004': {
266
- const manifest = generateSCORM2004Manifest(config, distDir);
267
- writeFileSync(resolve(distDir, 'imsmanifest.xml'), manifest, 'utf-8');
268
- cleanOldZips(projectRoot, slug);
269
- const zipSize = await createZip(distDir, zipPath);
270
- console.log(
271
- `✓ SCORM 2004 export: ${zipName} (${formatSize(zipSize)})`
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
  }