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.
Files changed (75) 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 +2 -1
  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 +85 -10
  12. package/dist/plugin/index.js.map +1 -1
  13. package/dist/{validation-D9DXlqNP.js → validation-B-xTvM9B.js} +342 -18
  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 +17 -3
  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 +16 -6
  23. package/src/components/Image.svelte +12 -3
  24. package/src/components/LockedBanner.svelte +2 -1
  25. package/src/components/Matching.svelte +48 -19
  26. package/src/components/MediaTracks.svelte +21 -0
  27. package/src/components/MultipleChoice.svelte +33 -13
  28. package/src/components/Quiz.svelte +61 -20
  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 +21 -18
  34. package/src/components/util.ts +3 -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 +4 -1
  41. package/src/plugin/export.ts +42 -14
  42. package/src/plugin/index.ts +216 -44
  43. package/src/plugin/manifest.ts +62 -22
  44. package/src/plugin/validation.ts +736 -122
  45. package/src/runtime/App.svelte +119 -48
  46. package/src/runtime/LoadingBar.svelte +12 -3
  47. package/src/runtime/Sidebar.svelte +24 -8
  48. package/src/runtime/access.ts +15 -3
  49. package/src/runtime/adapters/cmi5.ts +55 -33
  50. package/src/runtime/adapters/index.ts +22 -10
  51. package/src/runtime/adapters/retry.ts +25 -20
  52. package/src/runtime/adapters/scorm-base.ts +19 -15
  53. package/src/runtime/adapters/scorm12.ts +7 -8
  54. package/src/runtime/adapters/scorm2004.ts +11 -14
  55. package/src/runtime/adapters/web.ts +1 -1
  56. package/src/runtime/hooks.svelte.ts +152 -326
  57. package/src/runtime/interaction-format.ts +30 -12
  58. package/src/runtime/interaction.ts +44 -11
  59. package/src/runtime/navigation.svelte.ts +27 -11
  60. package/src/runtime/persistence.ts +2 -2
  61. package/src/runtime/progress.svelte.ts +13 -9
  62. package/src/runtime/quiz-engine.svelte.ts +361 -0
  63. package/src/runtime/quiz-policy.ts +9 -3
  64. package/src/runtime/types.ts +24 -2
  65. package/src/runtime/xapi/agent-rules.ts +4 -1
  66. package/src/runtime/xapi/client.ts +5 -5
  67. package/src/runtime/xapi/derive-actor.ts +2 -2
  68. package/src/runtime/xapi/publisher.ts +32 -29
  69. package/src/runtime/xapi/setup.ts +18 -15
  70. package/src/runtime/xapi/validation.ts +15 -6
  71. package/src/virtual.d.ts +4 -1
  72. package/styles/base.css +32 -11
  73. package/styles/layout.css +39 -18
  74. package/styles/theme.css +15 -3
  75. 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);
@@ -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,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 { unlinkSync(resolve(projectRoot, f)); } catch {}
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
- { manifestFile: string; label: string; generate: (config: ExportConfig, distDir: string) => string }
242
+ {
243
+ manifestFile: string;
244
+ label: string;
245
+ generate: (config: ExportConfig, distDir: string) => string;
246
+ }
235
247
  > = {
236
- scorm12: { manifestFile: 'imsmanifest.xml', label: 'SCORM 1.2', generate: generateSCORM12Manifest },
237
- scorm2004: { manifestFile: 'imsmanifest.xml', label: 'SCORM 2004', generate: generateSCORM2004Manifest },
238
- cmi5: { manifestFile: 'cmi5.xml', label: 'CMI5', generate: (config) => generateCMI5Xml(config) },
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(resolve(distDir, spec.manifestFile), spec.generate(config, distDir), 'utf-8');
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)})`);