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
|
@@ -127,7 +127,7 @@ export class XAPIPublisher {
|
|
|
127
127
|
}
|
|
128
128
|
if (!/^https?:\/\//i.test(opts.endpoint)) {
|
|
129
129
|
throw new XAPIConfigError(
|
|
130
|
-
'XAPIPublisher: endpoint must be an absolute http(s) URL'
|
|
130
|
+
'XAPIPublisher: endpoint must be an absolute http(s) URL',
|
|
131
131
|
);
|
|
132
132
|
}
|
|
133
133
|
if (!opts.activityId) {
|
|
@@ -173,7 +173,7 @@ export class XAPIPublisher {
|
|
|
173
173
|
resolved = await this.#actorValue();
|
|
174
174
|
} catch (err) {
|
|
175
175
|
throw new XAPIConfigError(
|
|
176
|
-
`xapi.actor resolver threw: ${err instanceof Error ? err.message : String(err)}
|
|
176
|
+
`xapi.actor resolver threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
177
177
|
);
|
|
178
178
|
}
|
|
179
179
|
const err = validateAgent(resolved);
|
|
@@ -199,7 +199,7 @@ export class XAPIPublisher {
|
|
|
199
199
|
getActor(): XAPIAgent {
|
|
200
200
|
if (!this.#cachedActor) {
|
|
201
201
|
throw new XAPIConfigError(
|
|
202
|
-
'XAPIPublisher.getActor() called before init() resolved. Await publisher.init() before reading the actor.'
|
|
202
|
+
'XAPIPublisher.getActor() called before init() resolved. Await publisher.init() before reading the actor.',
|
|
203
203
|
);
|
|
204
204
|
}
|
|
205
205
|
return this.#cachedActor;
|
|
@@ -231,7 +231,7 @@ export class XAPIPublisher {
|
|
|
231
231
|
buildStatement(partial: PartialStatement, opts?: { id?: string }): Statement {
|
|
232
232
|
if (!this.#cachedActor) {
|
|
233
233
|
throw new XAPIConfigError(
|
|
234
|
-
'XAPIPublisher.buildStatement() called before init() resolved.'
|
|
234
|
+
'XAPIPublisher.buildStatement() called before init() resolved.',
|
|
235
235
|
);
|
|
236
236
|
}
|
|
237
237
|
const userCtx = partial.context ?? {};
|
|
@@ -274,7 +274,8 @@ export class XAPIPublisher {
|
|
|
274
274
|
timestamp: new Date().toISOString(),
|
|
275
275
|
};
|
|
276
276
|
if (partial.result !== undefined) statement.result = partial.result;
|
|
277
|
-
if (partial.attachments !== undefined)
|
|
277
|
+
if (partial.attachments !== undefined)
|
|
278
|
+
statement.attachments = partial.attachments;
|
|
278
279
|
return statement;
|
|
279
280
|
}
|
|
280
281
|
|
|
@@ -289,7 +290,7 @@ export class XAPIPublisher {
|
|
|
289
290
|
*/
|
|
290
291
|
sendStatement(
|
|
291
292
|
partial: PartialStatement,
|
|
292
|
-
options?: SendStatementOptions & { id?: string }
|
|
293
|
+
options?: SendStatementOptions & { id?: string },
|
|
293
294
|
): Promise<SendStatementResult> {
|
|
294
295
|
if (this.#unavailableReason) {
|
|
295
296
|
return Promise.reject(this.#unavailableReason());
|
|
@@ -306,8 +307,8 @@ export class XAPIPublisher {
|
|
|
306
307
|
if (!this.#cachedActor) {
|
|
307
308
|
return Promise.reject(
|
|
308
309
|
new XAPIConfigError(
|
|
309
|
-
'XAPIPublisher.sendStatement() called before init() resolved.'
|
|
310
|
-
)
|
|
310
|
+
'XAPIPublisher.sendStatement() called before init() resolved.',
|
|
311
|
+
),
|
|
311
312
|
);
|
|
312
313
|
}
|
|
313
314
|
const statement = this.buildStatement(partial, { id: options?.id });
|
|
@@ -326,7 +327,7 @@ export class XAPIPublisher {
|
|
|
326
327
|
*/
|
|
327
328
|
enqueueBuilt(
|
|
328
329
|
statementOrBatch: Statement | Statement[],
|
|
329
|
-
options?: SendStatementOptions
|
|
330
|
+
options?: SendStatementOptions,
|
|
330
331
|
): Promise<DestinationOutcome> {
|
|
331
332
|
if (this.#unavailableReason) {
|
|
332
333
|
return Promise.reject(this.#unavailableReason());
|
|
@@ -336,7 +337,7 @@ export class XAPIPublisher {
|
|
|
336
337
|
endpoint: this.#endpoint,
|
|
337
338
|
ok: false,
|
|
338
339
|
error: new XAPIConfigError(
|
|
339
|
-
`XAPIPublisher queue saturated (${this.#queueDepth} in-flight); refusing further sends until the LRS catches up
|
|
340
|
+
`XAPIPublisher queue saturated (${this.#queueDepth} in-flight); refusing further sends until the LRS catches up.`,
|
|
340
341
|
),
|
|
341
342
|
});
|
|
342
343
|
}
|
|
@@ -345,7 +346,7 @@ export class XAPIPublisher {
|
|
|
345
346
|
console.warn(
|
|
346
347
|
`Tessera: xAPI publisher queue depth ${this.#queueDepth} (>= ${QUEUE_DEPTH_WARN}). ` +
|
|
347
348
|
`Each pending statement is retained in the promise chain's closure until it drains; ` +
|
|
348
|
-
`consider rate-limiting authoring sends or batching before sendStatement
|
|
349
|
+
`consider rate-limiting authoring sends or batching before sendStatement.`,
|
|
349
350
|
);
|
|
350
351
|
}
|
|
351
352
|
this.#queueDepth++;
|
|
@@ -367,7 +368,7 @@ export class XAPIPublisher {
|
|
|
367
368
|
status: outcome.status,
|
|
368
369
|
error: outcome.error,
|
|
369
370
|
});
|
|
370
|
-
})
|
|
371
|
+
}),
|
|
371
372
|
);
|
|
372
373
|
return outcomePromise;
|
|
373
374
|
}
|
|
@@ -385,7 +386,9 @@ export class XAPIPublisher {
|
|
|
385
386
|
resolveTask = r;
|
|
386
387
|
});
|
|
387
388
|
this.#queue = this.#queue.then(() =>
|
|
388
|
-
fn()
|
|
389
|
+
fn()
|
|
390
|
+
.catch(() => {})
|
|
391
|
+
.then(() => resolveTask()),
|
|
389
392
|
);
|
|
390
393
|
return taskPromise;
|
|
391
394
|
}
|
|
@@ -413,7 +416,7 @@ export class XAPIPublisher {
|
|
|
413
416
|
|
|
414
417
|
#sendWithRetry(
|
|
415
418
|
statementOrBatch: Statement | Statement[],
|
|
416
|
-
options?: SendStatementOptions
|
|
419
|
+
options?: SendStatementOptions,
|
|
417
420
|
): Promise<SendOutcome> {
|
|
418
421
|
const body = JSON.stringify(statementOrBatch);
|
|
419
422
|
const retry = options?.retry !== false; // default: retry enabled
|
|
@@ -432,7 +435,7 @@ export class XAPIPublisher {
|
|
|
432
435
|
`Tessera: xAPI ${count}-statement batch is ${body.length} bytes, ` +
|
|
433
436
|
`over the 64 KiB keepalive cap. The browser may silently drop this ` +
|
|
434
437
|
`request during unload. Reduce per-statement size or split sends ` +
|
|
435
|
-
`before terminate
|
|
438
|
+
`before terminate.`,
|
|
436
439
|
);
|
|
437
440
|
}
|
|
438
441
|
return this.#sendOnce(body, keepalive).then((outcome) => {
|
|
@@ -447,9 +450,9 @@ export class XAPIPublisher {
|
|
|
447
450
|
return outcome;
|
|
448
451
|
}
|
|
449
452
|
if (isFinal) return outcome;
|
|
450
|
-
return new Promise<void>((r) =>
|
|
451
|
-
|
|
452
|
-
)
|
|
453
|
+
return new Promise<void>((r) => setTimeout(r, backoffMs(n))).then(() =>
|
|
454
|
+
attempt(n + 1),
|
|
455
|
+
);
|
|
453
456
|
});
|
|
454
457
|
};
|
|
455
458
|
|
|
@@ -461,7 +464,7 @@ export class XAPIPublisher {
|
|
|
461
464
|
return Promise.resolve({
|
|
462
465
|
ok: false,
|
|
463
466
|
error: new XAPIConfigError(
|
|
464
|
-
'xapi.auth was rejected by the LRS twice in a row; refusing further sends for the publisher lifetime. Reload the runtime to retry.'
|
|
467
|
+
'xapi.auth was rejected by the LRS twice in a row; refusing further sends for the publisher lifetime. Reload the runtime to retry.',
|
|
465
468
|
),
|
|
466
469
|
});
|
|
467
470
|
}
|
|
@@ -482,7 +485,7 @@ export class XAPIPublisher {
|
|
|
482
485
|
#fetchWithToken(
|
|
483
486
|
token: string,
|
|
484
487
|
body: string,
|
|
485
|
-
keepalive: boolean
|
|
488
|
+
keepalive: boolean,
|
|
486
489
|
): Promise<SendOutcome> {
|
|
487
490
|
const headers = new Headers();
|
|
488
491
|
if (token) headers.set('Authorization', `Basic ${token}`);
|
|
@@ -504,7 +507,7 @@ export class XAPIPublisher {
|
|
|
504
507
|
#handleResponse(
|
|
505
508
|
resp: Response,
|
|
506
509
|
body: string,
|
|
507
|
-
keepalive: boolean
|
|
510
|
+
keepalive: boolean,
|
|
508
511
|
): Promise<SendOutcome> | SendOutcome {
|
|
509
512
|
if (resp.ok || resp.status === 409) {
|
|
510
513
|
return { ok: true, status: resp.status };
|
|
@@ -540,7 +543,7 @@ export class XAPIPublisher {
|
|
|
540
543
|
ok: false,
|
|
541
544
|
status: 401,
|
|
542
545
|
error: new Error(
|
|
543
|
-
'LRS rejected re-resolved auth (consecutive 401s); auth resolver marked dead'
|
|
546
|
+
'LRS rejected re-resolved auth (consecutive 401s); auth resolver marked dead',
|
|
544
547
|
),
|
|
545
548
|
};
|
|
546
549
|
}
|
|
@@ -555,7 +558,7 @@ export class XAPIPublisher {
|
|
|
555
558
|
ok: false,
|
|
556
559
|
status: 401,
|
|
557
560
|
error: err instanceof Error ? err : new Error(String(err)),
|
|
558
|
-
})
|
|
561
|
+
}),
|
|
559
562
|
);
|
|
560
563
|
}
|
|
561
564
|
// Append the LRS body to the error message so callers see the
|
|
@@ -573,14 +576,14 @@ export class XAPIPublisher {
|
|
|
573
576
|
ok: false,
|
|
574
577
|
status: resp.status,
|
|
575
578
|
error: new Error(
|
|
576
|
-
`LRS responded ${resp.status}${respBody ? `: ${respBody.slice(0, 500)}` : ''}
|
|
579
|
+
`LRS responded ${resp.status}${respBody ? `: ${respBody.slice(0, 500)}` : ''}`,
|
|
577
580
|
),
|
|
578
581
|
}),
|
|
579
582
|
(): SendOutcome => ({
|
|
580
583
|
ok: false,
|
|
581
584
|
status: resp.status,
|
|
582
585
|
error: new Error(`LRS responded ${resp.status}`),
|
|
583
|
-
})
|
|
586
|
+
}),
|
|
584
587
|
);
|
|
585
588
|
}
|
|
586
589
|
|
|
@@ -593,7 +596,8 @@ export class XAPIPublisher {
|
|
|
593
596
|
// where the LMS fetch URL produced no token); otherwise revalidate.
|
|
594
597
|
if (this.#authValue.length > 0) {
|
|
595
598
|
const err = validateAuthCredential(this.#authValue);
|
|
596
|
-
if (err)
|
|
599
|
+
if (err)
|
|
600
|
+
return Promise.reject(new XAPIConfigError(`xapi.auth: ${err}`));
|
|
597
601
|
}
|
|
598
602
|
this.#cachedAuth = this.#authValue;
|
|
599
603
|
return Promise.resolve(this.#cachedAuth);
|
|
@@ -603,7 +607,7 @@ export class XAPIPublisher {
|
|
|
603
607
|
.then((resolved) => {
|
|
604
608
|
if (typeof resolved !== 'string' || !resolved) {
|
|
605
609
|
throw new XAPIConfigError(
|
|
606
|
-
'xapi.auth resolver must return a non-empty string'
|
|
610
|
+
'xapi.auth resolver must return a non-empty string',
|
|
607
611
|
);
|
|
608
612
|
}
|
|
609
613
|
const err = validateAuthCredential(resolved);
|
|
@@ -614,9 +618,8 @@ export class XAPIPublisher {
|
|
|
614
618
|
.catch((err) => {
|
|
615
619
|
if (err instanceof XAPIConfigError) throw err;
|
|
616
620
|
throw new XAPIConfigError(
|
|
617
|
-
`xapi.auth resolver threw: ${err instanceof Error ? err.message : String(err)}
|
|
621
|
+
`xapi.auth resolver threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
618
622
|
);
|
|
619
623
|
});
|
|
620
624
|
}
|
|
621
625
|
}
|
|
622
|
-
|
|
@@ -34,8 +34,8 @@ class XAPIDevFallbackError extends Error {
|
|
|
34
34
|
"Tessera xAPI: xapi.endpoint is 'lms' but no cmi5 launch parameters " +
|
|
35
35
|
'(fetch / endpoint / activityId / actor) were present on the URL. ' +
|
|
36
36
|
'Either launch this course from a real LMS / SCORM Cloud, or ' +
|
|
37
|
-
|
|
38
|
-
'local LRS (e.g. http://localhost:8080/data/xAPI/) for dev work.'
|
|
37
|
+
'temporarily change xapi.endpoint to an explicit URL pointed at a ' +
|
|
38
|
+
'local LRS (e.g. http://localhost:8080/data/xAPI/) for dev work.',
|
|
39
39
|
);
|
|
40
40
|
this.name = 'XAPIDevFallbackError';
|
|
41
41
|
}
|
|
@@ -72,14 +72,14 @@ class XAPISCORMDevFallbackError extends Error {
|
|
|
72
72
|
'destination. Either supply xapi.actor explicitly in course.config.js, or launch from ' +
|
|
73
73
|
'a real LMS / SCORM Cloud where ' +
|
|
74
74
|
(standard === 'scorm12' ? 'cmi.core.student_id' : 'cmi.learner_id') +
|
|
75
|
-
' is populated.'
|
|
75
|
+
' is populated.',
|
|
76
76
|
);
|
|
77
77
|
this.name = 'XAPISCORMDevFallbackError';
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
function makeSCORMDevFallbackPublisher(
|
|
82
|
-
standard: 'scorm12' | 'scorm2004'
|
|
82
|
+
standard: 'scorm12' | 'scorm2004',
|
|
83
83
|
): XAPIPublisher {
|
|
84
84
|
return makeRejectingPublisher(() => new XAPISCORMDevFallbackError(standard));
|
|
85
85
|
}
|
|
@@ -93,13 +93,13 @@ function makeSCORMDevFallbackPublisher(
|
|
|
93
93
|
function resolveDestination(
|
|
94
94
|
entry: XAPIConfig,
|
|
95
95
|
config: CourseConfig,
|
|
96
|
-
adapter: PersistenceAdapter | null
|
|
96
|
+
adapter: PersistenceAdapter | null,
|
|
97
97
|
): DestinationSource | null {
|
|
98
98
|
if (entry.endpoint === 'lms') {
|
|
99
99
|
if (config.export?.standard !== 'cmi5') {
|
|
100
100
|
// Build-time validator should reject this; defense in depth at runtime.
|
|
101
101
|
console.warn(
|
|
102
|
-
"Tessera xAPI: ignoring xapi entry with endpoint: 'lms' under non-cmi5 export."
|
|
102
|
+
"Tessera xAPI: ignoring xapi entry with endpoint: 'lms' under non-cmi5 export.",
|
|
103
103
|
);
|
|
104
104
|
return null;
|
|
105
105
|
}
|
|
@@ -121,14 +121,17 @@ function resolveDestination(
|
|
|
121
121
|
(actorOrResolver as { __scormDevFallback?: 'scorm12' | 'scorm2004' })
|
|
122
122
|
.__scormDevFallback
|
|
123
123
|
) {
|
|
124
|
-
const std = (
|
|
125
|
-
|
|
124
|
+
const std = (
|
|
125
|
+
actorOrResolver as { __scormDevFallback: 'scorm12' | 'scorm2004' }
|
|
126
|
+
).__scormDevFallback;
|
|
126
127
|
return { kind: 'explicit', publisher: makeSCORMDevFallbackPublisher(std) };
|
|
127
128
|
}
|
|
128
129
|
const publisher = new XAPIPublisher({
|
|
129
130
|
endpoint: explicit.endpoint,
|
|
130
131
|
auth: explicit.auth,
|
|
131
|
-
actor: actorOrResolver as
|
|
132
|
+
actor: actorOrResolver as
|
|
133
|
+
| XAPIAgent
|
|
134
|
+
| (() => XAPIAgent | Promise<XAPIAgent>),
|
|
132
135
|
activityId: explicit.activityId,
|
|
133
136
|
registration: explicit.registration,
|
|
134
137
|
});
|
|
@@ -145,7 +148,7 @@ function resolveDestination(
|
|
|
145
148
|
function resolveExplicitActor(
|
|
146
149
|
explicit: XAPIExplicitConfig,
|
|
147
150
|
config: CourseConfig,
|
|
148
|
-
adapter: PersistenceAdapter | null
|
|
151
|
+
adapter: PersistenceAdapter | null,
|
|
149
152
|
):
|
|
150
153
|
| XAPIAgent
|
|
151
154
|
| (() => XAPIAgent | Promise<XAPIAgent>)
|
|
@@ -170,7 +173,7 @@ function resolveExplicitActor(
|
|
|
170
173
|
return synthesizeSCORM12Actor(
|
|
171
174
|
adapter.getAPI(),
|
|
172
175
|
explicit.activityId,
|
|
173
|
-
explicit.actorAccountHomePage
|
|
176
|
+
explicit.actorAccountHomePage,
|
|
174
177
|
);
|
|
175
178
|
}
|
|
176
179
|
// Adapter is the WebAdapter dev fallback. Mirror the cmi5 'lms'
|
|
@@ -184,14 +187,14 @@ function resolveExplicitActor(
|
|
|
184
187
|
return synthesizeSCORM2004Actor(
|
|
185
188
|
adapter.getAPI(),
|
|
186
189
|
explicit.activityId,
|
|
187
|
-
explicit.actorAccountHomePage
|
|
190
|
+
explicit.actorAccountHomePage,
|
|
188
191
|
);
|
|
189
192
|
}
|
|
190
193
|
return { __scormDevFallback: 'scorm2004' };
|
|
191
194
|
}
|
|
192
195
|
// Web export with no actor — build-time validator should have errored.
|
|
193
196
|
console.warn(
|
|
194
|
-
'Tessera xAPI: explicit destination has no actor and no derivation source — skipping.'
|
|
197
|
+
'Tessera xAPI: explicit destination has no actor and no derivation source — skipping.',
|
|
195
198
|
);
|
|
196
199
|
return null;
|
|
197
200
|
}
|
|
@@ -206,7 +209,7 @@ function resolveExplicitActor(
|
|
|
206
209
|
*/
|
|
207
210
|
export async function buildXAPIClient(
|
|
208
211
|
config: CourseConfig,
|
|
209
|
-
adapter: PersistenceAdapter | null
|
|
212
|
+
adapter: PersistenceAdapter | null,
|
|
210
213
|
): Promise<XAPIClient | null> {
|
|
211
214
|
const raw = config.xapi;
|
|
212
215
|
if (raw === undefined || raw === null) return null;
|
|
@@ -233,7 +236,7 @@ export async function buildXAPIClient(
|
|
|
233
236
|
} catch (err) {
|
|
234
237
|
console.warn(
|
|
235
238
|
'Tessera xAPI: failed to initialize an explicit destination — skipping.',
|
|
236
|
-
err
|
|
239
|
+
err,
|
|
237
240
|
);
|
|
238
241
|
}
|
|
239
242
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { PartialStatement } from './types.js';
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
validateAgent,
|
|
4
|
+
validateAuthCredential,
|
|
5
|
+
joinFieldError,
|
|
6
|
+
} from './agent-rules.js';
|
|
3
7
|
|
|
4
8
|
/** Thrown for runtime-validation failures (auth/actor resolver misuse). */
|
|
5
9
|
export class XAPIConfigError extends Error {
|
|
@@ -32,7 +36,7 @@ export function validatePartialStatement(partial: PartialStatement): void {
|
|
|
32
36
|
if (!partial || typeof partial !== 'object') {
|
|
33
37
|
throw new XAPIStatementError(
|
|
34
38
|
'sendStatement: partial statement must be an object',
|
|
35
|
-
partial
|
|
39
|
+
partial,
|
|
36
40
|
);
|
|
37
41
|
}
|
|
38
42
|
if (
|
|
@@ -43,7 +47,7 @@ export function validatePartialStatement(partial: PartialStatement): void {
|
|
|
43
47
|
) {
|
|
44
48
|
throw new XAPIStatementError(
|
|
45
49
|
'sendStatement: verb.id is required and must be a non-empty string',
|
|
46
|
-
partial
|
|
50
|
+
partial,
|
|
47
51
|
);
|
|
48
52
|
}
|
|
49
53
|
if (partial.object !== undefined) {
|
|
@@ -55,16 +59,21 @@ export function validatePartialStatement(partial: PartialStatement): void {
|
|
|
55
59
|
) {
|
|
56
60
|
throw new XAPIStatementError(
|
|
57
61
|
'sendStatement: object.id must be a non-empty string when object is supplied',
|
|
58
|
-
partial
|
|
62
|
+
partial,
|
|
59
63
|
);
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
const scaled = partial.result?.score?.scaled;
|
|
63
67
|
if (scaled !== undefined) {
|
|
64
|
-
if (
|
|
68
|
+
if (
|
|
69
|
+
typeof scaled !== 'number' ||
|
|
70
|
+
!Number.isFinite(scaled) ||
|
|
71
|
+
scaled < -1 ||
|
|
72
|
+
scaled > 1
|
|
73
|
+
) {
|
|
65
74
|
throw new XAPIStatementError(
|
|
66
75
|
`sendStatement: result.score.scaled must be a number in [-1, 1], got ${scaled}`,
|
|
67
|
-
partial
|
|
76
|
+
partial,
|
|
68
77
|
);
|
|
69
78
|
}
|
|
70
79
|
}
|
package/src/virtual.d.ts
CHANGED
|
@@ -14,7 +14,10 @@ declare module 'virtual:tessera-xapi-setup' {
|
|
|
14
14
|
import type { CourseConfig } from 'tessera-learn/runtime/types.js';
|
|
15
15
|
import type { PersistenceAdapter } from 'tessera-learn/runtime/persistence.js';
|
|
16
16
|
import type { XAPIClient } from 'tessera-learn/runtime/xapi/client.js';
|
|
17
|
-
export function buildXAPIClient(
|
|
17
|
+
export function buildXAPIClient(
|
|
18
|
+
config: CourseConfig,
|
|
19
|
+
adapter: PersistenceAdapter,
|
|
20
|
+
): Promise<XAPIClient | null>;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
interface ImportMetaEnv {
|
package/styles/base.css
CHANGED
|
@@ -47,7 +47,12 @@ object {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/* ---- Typography ---- */
|
|
50
|
-
h1,
|
|
50
|
+
h1,
|
|
51
|
+
h2,
|
|
52
|
+
h3,
|
|
53
|
+
h4,
|
|
54
|
+
h5,
|
|
55
|
+
h6 {
|
|
51
56
|
font-weight: 700;
|
|
52
57
|
line-height: 1.25;
|
|
53
58
|
color: var(--tessera-text);
|
|
@@ -55,12 +60,24 @@ h1, h2, h3, h4, h5, h6 {
|
|
|
55
60
|
margin-bottom: var(--tessera-spacing-md);
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
h1 {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
h1 {
|
|
64
|
+
font-size: 2rem;
|
|
65
|
+
}
|
|
66
|
+
h2 {
|
|
67
|
+
font-size: 1.625rem;
|
|
68
|
+
}
|
|
69
|
+
h3 {
|
|
70
|
+
font-size: 1.375rem;
|
|
71
|
+
}
|
|
72
|
+
h4 {
|
|
73
|
+
font-size: 1.125rem;
|
|
74
|
+
}
|
|
75
|
+
h5 {
|
|
76
|
+
font-size: 1rem;
|
|
77
|
+
}
|
|
78
|
+
h6 {
|
|
79
|
+
font-size: 0.875rem;
|
|
80
|
+
}
|
|
64
81
|
|
|
65
82
|
h1:first-child,
|
|
66
83
|
h2:first-child,
|
|
@@ -90,16 +107,19 @@ a:focus-visible {
|
|
|
90
107
|
border-radius: 2px;
|
|
91
108
|
}
|
|
92
109
|
|
|
93
|
-
strong,
|
|
110
|
+
strong,
|
|
111
|
+
b {
|
|
94
112
|
font-weight: 700;
|
|
95
113
|
}
|
|
96
114
|
|
|
97
|
-
em,
|
|
115
|
+
em,
|
|
116
|
+
i {
|
|
98
117
|
font-style: italic;
|
|
99
118
|
}
|
|
100
119
|
|
|
101
120
|
/* ---- Lists ---- */
|
|
102
|
-
ul,
|
|
121
|
+
ul,
|
|
122
|
+
ol {
|
|
103
123
|
margin-bottom: var(--tessera-spacing-md);
|
|
104
124
|
padding-left: var(--tessera-spacing-xl);
|
|
105
125
|
}
|
|
@@ -170,7 +190,8 @@ table {
|
|
|
170
190
|
font-size: 0.9375rem;
|
|
171
191
|
}
|
|
172
192
|
|
|
173
|
-
th,
|
|
193
|
+
th,
|
|
194
|
+
td {
|
|
174
195
|
padding: var(--tessera-spacing-sm) var(--tessera-spacing-md);
|
|
175
196
|
border: 1px solid var(--tessera-border);
|
|
176
197
|
text-align: left;
|
package/styles/layout.css
CHANGED
|
@@ -104,8 +104,9 @@
|
|
|
104
104
|
background: none;
|
|
105
105
|
cursor: pointer;
|
|
106
106
|
text-align: left;
|
|
107
|
-
transition:
|
|
108
|
-
|
|
107
|
+
transition:
|
|
108
|
+
background-color var(--tessera-transition-fast),
|
|
109
|
+
color var(--tessera-transition-fast);
|
|
109
110
|
border-left: 3px solid transparent;
|
|
110
111
|
}
|
|
111
112
|
|
|
@@ -114,19 +115,19 @@
|
|
|
114
115
|
color: var(--tessera-text);
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
.tessera-nav-page[aria-current=
|
|
118
|
+
.tessera-nav-page[aria-current='page'] {
|
|
118
119
|
background-color: var(--tessera-primary-light);
|
|
119
120
|
color: var(--tessera-primary-dark);
|
|
120
121
|
font-weight: 600;
|
|
121
122
|
border-left-color: var(--tessera-primary);
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
.tessera-nav-page[aria-disabled=
|
|
125
|
+
.tessera-nav-page[aria-disabled='true'] {
|
|
125
126
|
opacity: 0.45;
|
|
126
127
|
cursor: not-allowed;
|
|
127
128
|
}
|
|
128
129
|
|
|
129
|
-
.tessera-nav-page[aria-disabled=
|
|
130
|
+
.tessera-nav-page[aria-disabled='true']:hover {
|
|
130
131
|
background-color: transparent;
|
|
131
132
|
color: var(--tessera-text-light);
|
|
132
133
|
}
|
|
@@ -176,8 +177,9 @@
|
|
|
176
177
|
border: 1px solid var(--tessera-primary);
|
|
177
178
|
border-radius: 6px;
|
|
178
179
|
cursor: pointer;
|
|
179
|
-
transition:
|
|
180
|
-
|
|
180
|
+
transition:
|
|
181
|
+
background-color var(--tessera-transition-fast),
|
|
182
|
+
color var(--tessera-transition-fast);
|
|
181
183
|
}
|
|
182
184
|
|
|
183
185
|
.tessera-page-nav-btn:hover:not(:disabled) {
|
|
@@ -250,8 +252,9 @@
|
|
|
250
252
|
height: 2px;
|
|
251
253
|
background-color: var(--tessera-text);
|
|
252
254
|
border-radius: 1px;
|
|
253
|
-
transition:
|
|
254
|
-
|
|
255
|
+
transition:
|
|
256
|
+
transform var(--tessera-transition-fast),
|
|
257
|
+
opacity var(--tessera-transition-fast);
|
|
255
258
|
}
|
|
256
259
|
|
|
257
260
|
.tessera-hamburger-lines {
|
|
@@ -260,15 +263,21 @@
|
|
|
260
263
|
gap: 5px;
|
|
261
264
|
}
|
|
262
265
|
|
|
263
|
-
.tessera-hamburger[aria-expanded=
|
|
266
|
+
.tessera-hamburger[aria-expanded='true']
|
|
267
|
+
.tessera-hamburger-lines
|
|
268
|
+
.tessera-hamburger-line:nth-child(1) {
|
|
264
269
|
transform: translateY(7px) rotate(45deg);
|
|
265
270
|
}
|
|
266
271
|
|
|
267
|
-
.tessera-hamburger[aria-expanded=
|
|
272
|
+
.tessera-hamburger[aria-expanded='true']
|
|
273
|
+
.tessera-hamburger-lines
|
|
274
|
+
.tessera-hamburger-line:nth-child(2) {
|
|
268
275
|
opacity: 0;
|
|
269
276
|
}
|
|
270
277
|
|
|
271
|
-
.tessera-hamburger[aria-expanded=
|
|
278
|
+
.tessera-hamburger[aria-expanded='true']
|
|
279
|
+
.tessera-hamburger-lines
|
|
280
|
+
.tessera-hamburger-line:nth-child(3) {
|
|
272
281
|
transform: translateY(-7px) rotate(-45deg);
|
|
273
282
|
}
|
|
274
283
|
|
|
@@ -334,7 +343,9 @@
|
|
|
334
343
|
cursor: pointer;
|
|
335
344
|
min-height: 44px;
|
|
336
345
|
font-family: var(--tessera-font-family);
|
|
337
|
-
transition:
|
|
346
|
+
transition:
|
|
347
|
+
background-color var(--tessera-transition-fast),
|
|
348
|
+
opacity var(--tessera-transition-fast);
|
|
338
349
|
}
|
|
339
350
|
|
|
340
351
|
.tessera-btn-primary:hover:not(:disabled) {
|
|
@@ -401,7 +412,9 @@
|
|
|
401
412
|
}
|
|
402
413
|
|
|
403
414
|
.tessera-content {
|
|
404
|
-
padding-top: calc(
|
|
415
|
+
padding-top: calc(
|
|
416
|
+
var(--tessera-spacing-xl) + 44px + var(--tessera-spacing-md)
|
|
417
|
+
);
|
|
405
418
|
}
|
|
406
419
|
}
|
|
407
420
|
|
|
@@ -409,7 +422,9 @@
|
|
|
409
422
|
@media (max-width: 640px) {
|
|
410
423
|
.tessera-content {
|
|
411
424
|
padding: var(--tessera-spacing-md);
|
|
412
|
-
padding-top: calc(
|
|
425
|
+
padding-top: calc(
|
|
426
|
+
var(--tessera-spacing-md) + 44px + var(--tessera-spacing-md)
|
|
427
|
+
);
|
|
413
428
|
}
|
|
414
429
|
|
|
415
430
|
.tessera-page-nav {
|
|
@@ -423,7 +438,13 @@
|
|
|
423
438
|
font-size: 1rem;
|
|
424
439
|
}
|
|
425
440
|
|
|
426
|
-
h1 {
|
|
427
|
-
|
|
428
|
-
|
|
441
|
+
h1 {
|
|
442
|
+
font-size: 1.625rem;
|
|
443
|
+
}
|
|
444
|
+
h2 {
|
|
445
|
+
font-size: 1.375rem;
|
|
446
|
+
}
|
|
447
|
+
h3 {
|
|
448
|
+
font-size: 1.125rem;
|
|
449
|
+
}
|
|
429
450
|
}
|
package/styles/theme.css
CHANGED
|
@@ -9,11 +9,23 @@
|
|
|
9
9
|
--tessera-bg-secondary: #f9fafb;
|
|
10
10
|
--tessera-border: #e5e7eb;
|
|
11
11
|
--tessera-success: #16a34a;
|
|
12
|
-
--tessera-success-bg: color-mix(
|
|
13
|
-
|
|
12
|
+
--tessera-success-bg: color-mix(
|
|
13
|
+
in srgb,
|
|
14
|
+
var(--tessera-success) 8%,
|
|
15
|
+
transparent
|
|
16
|
+
);
|
|
17
|
+
--tessera-success-border: color-mix(
|
|
18
|
+
in srgb,
|
|
19
|
+
var(--tessera-success) 25%,
|
|
20
|
+
transparent
|
|
21
|
+
);
|
|
14
22
|
--tessera-error: #dc2626;
|
|
15
23
|
--tessera-error-bg: color-mix(in srgb, var(--tessera-error) 8%, transparent);
|
|
16
|
-
--tessera-error-border: color-mix(
|
|
24
|
+
--tessera-error-border: color-mix(
|
|
25
|
+
in srgb,
|
|
26
|
+
var(--tessera-error) 25%,
|
|
27
|
+
transparent
|
|
28
|
+
);
|
|
17
29
|
--tessera-warning: #d97706;
|
|
18
30
|
|
|
19
31
|
/* Typography */
|