@intellectif/lk-react 1.0.1 → 2.1.0
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/dist/{chunk-5KW7TFX3.js → chunk-3X7MYAFT.js} +110 -29
- package/dist/chunk-3X7MYAFT.js.map +1 -0
- package/dist/chunk-7BT6OXAR.cjs +225 -0
- package/dist/chunk-7BT6OXAR.cjs.map +1 -0
- package/dist/{chunk-IZNMQSQP.js → chunk-E4MNA2IQ.js} +17 -3
- package/dist/chunk-E4MNA2IQ.js.map +1 -0
- package/dist/{chunk-TVK3SVV6.cjs → chunk-IYLQSNLE.cjs} +17 -3
- package/dist/chunk-IYLQSNLE.cjs.map +1 -0
- package/dist/{chunk-6OBRYL4C.cjs → chunk-QHOV2A6M.cjs} +11 -2
- package/dist/chunk-QHOV2A6M.cjs.map +1 -0
- package/dist/{chunk-RI53A36N.cjs → chunk-STLRKR2I.cjs} +18 -5
- package/dist/chunk-STLRKR2I.cjs.map +1 -0
- package/dist/{chunk-ZPD7BGCF.js → chunk-UASXUWHH.js} +18 -5
- package/dist/chunk-UASXUWHH.js.map +1 -0
- package/dist/{chunk-L3EV5LIX.js → chunk-UIGIYYGY.js} +11 -3
- package/dist/chunk-UIGIYYGY.js.map +1 -0
- package/dist/{chunk-O3GWWJFS.cjs → chunk-UXO6KYSZ.cjs} +112 -31
- package/dist/chunk-UXO6KYSZ.cjs.map +1 -0
- package/dist/chunk-W6BH2YMZ.cjs +259 -0
- package/dist/chunk-W6BH2YMZ.cjs.map +1 -0
- package/dist/chunk-XTJ22JAP.js +223 -0
- package/dist/chunk-XTJ22JAP.js.map +1 -0
- package/dist/chunk-ZVFWJTT4.js +257 -0
- package/dist/chunk-ZVFWJTT4.js.map +1 -0
- package/dist/components/ActivitySequence.cjs +5 -5
- package/dist/components/ActivitySequence.js +4 -4
- package/dist/components/FillInTheBlanks.cjs +3 -3
- package/dist/components/FillInTheBlanks.d.cts +7 -1
- package/dist/components/FillInTheBlanks.d.ts +7 -1
- package/dist/components/FillInTheBlanks.js +2 -2
- package/dist/components/MultipleChoice.cjs +3 -3
- package/dist/components/MultipleChoice.d.cts +14 -3
- package/dist/components/MultipleChoice.d.ts +14 -3
- package/dist/components/MultipleChoice.js +2 -2
- package/dist/components/WrittenResponse.cjs +14 -0
- package/dist/components/WrittenResponse.cjs.map +1 -0
- package/dist/components/WrittenResponse.d.cts +96 -0
- package/dist/components/WrittenResponse.d.ts +96 -0
- package/dist/components/WrittenResponse.js +5 -0
- package/dist/components/WrittenResponse.js.map +1 -0
- package/dist/hooks/useXAPI.cjs +2 -2
- package/dist/hooks/useXAPI.js +1 -1
- package/dist/index.cjs +19 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +13 -5
- package/dist/index.js.map +1 -1
- package/dist/theme/skin.css +85 -0
- package/dist/types-CNmjqD6M.d.cts +108 -0
- package/dist/types-CNmjqD6M.d.ts +108 -0
- package/package.json +13 -4
- package/dist/chunk-5KW7TFX3.js.map +0 -1
- package/dist/chunk-6OBRYL4C.cjs.map +0 -1
- package/dist/chunk-IZNMQSQP.js.map +0 -1
- package/dist/chunk-L3EV5LIX.js.map +0 -1
- package/dist/chunk-O3GWWJFS.cjs.map +0 -1
- package/dist/chunk-QBA6UXMS.cjs +0 -160
- package/dist/chunk-QBA6UXMS.cjs.map +0 -1
- package/dist/chunk-RI53A36N.cjs.map +0 -1
- package/dist/chunk-TVK3SVV6.cjs.map +0 -1
- package/dist/chunk-W2L66WHW.js +0 -158
- package/dist/chunk-W2L66WHW.js.map +0 -1
- package/dist/chunk-ZPD7BGCF.js.map +0 -1
- package/dist/types-iknDq5tf.d.cts +0 -17
- package/dist/types-iknDq5tf.d.ts +0 -17
|
@@ -2,6 +2,19 @@ import { useRef, useCallback } from 'react';
|
|
|
2
2
|
|
|
3
3
|
// src/hooks/useXAPI.ts
|
|
4
4
|
var BACKOFF_MS = [1e3, 2e3, 4e3];
|
|
5
|
+
function applyIdentity(statement, cfg) {
|
|
6
|
+
const isAnonymous = statement.actor?.account?.name === "anonymous" && statement.actor.account.homePage === "https://github.com/intellectif/learning-kit";
|
|
7
|
+
const isSdkObjectId = statement.object?.objectType === "Activity" && typeof statement.object.id === "string" && statement.object.id.startsWith("urn:learning-kit:activity:");
|
|
8
|
+
if (!isAnonymous && !isSdkObjectId) {
|
|
9
|
+
return statement;
|
|
10
|
+
}
|
|
11
|
+
const mappedId = isSdkObjectId && cfg.activityId ? typeof cfg.activityId === "function" ? cfg.activityId(statement.object.id) : cfg.activityId : void 0;
|
|
12
|
+
return {
|
|
13
|
+
...statement,
|
|
14
|
+
...isAnonymous && cfg.actor !== void 0 ? { actor: cfg.actor } : {},
|
|
15
|
+
...mappedId !== void 0 ? { object: { ...statement.object, id: mappedId } } : {}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
5
18
|
function toBase64Utf8(input) {
|
|
6
19
|
const bytes = new TextEncoder().encode(input);
|
|
7
20
|
let binary = "";
|
|
@@ -17,8 +30,9 @@ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
17
30
|
function useXAPI(config) {
|
|
18
31
|
const configRef = useRef(config);
|
|
19
32
|
configRef.current = config;
|
|
20
|
-
const sendStatement = useCallback(async (
|
|
33
|
+
const sendStatement = useCallback(async (rawStatement) => {
|
|
21
34
|
const cfg = configRef.current;
|
|
35
|
+
const statement = applyIdentity(rawStatement, cfg);
|
|
22
36
|
const maxAttempts = 1 + BACKOFF_MS.length;
|
|
23
37
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
24
38
|
let statusCode;
|
|
@@ -61,5 +75,5 @@ function useXAPI(config) {
|
|
|
61
75
|
}
|
|
62
76
|
|
|
63
77
|
export { useXAPI };
|
|
64
|
-
//# sourceMappingURL=chunk-
|
|
65
|
-
//# sourceMappingURL=chunk-
|
|
78
|
+
//# sourceMappingURL=chunk-E4MNA2IQ.js.map
|
|
79
|
+
//# sourceMappingURL=chunk-E4MNA2IQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/useXAPI.ts"],"names":[],"mappings":"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","file":"chunk-E4MNA2IQ.js","sourcesContent":["'use client';\n\nimport type { XAPIConfig, XAPIStatement } from '@intellectif/lk-core';\nimport { useCallback, useRef } from 'react';\n\n/** Backoff before retry 1 / 2 / 3 (Req 10.4). */\nconst BACKOFF_MS = [1000, 2000, 4000] as const;\n\n/**\n * Applies the configured learner identity to a statement built by an activity\n * component. Components emit an anonymous placeholder actor and a\n * `urn:learning-kit:activity:*` object id (they cannot know identity — Req\n * 3.1); this hook is the documented place where `XAPIConfig.actor` and\n * `XAPIConfig.activityId` take over. Conservative on purpose: the actor is\n * replaced only when the statement still carries the SDK's anonymous\n * placeholder, and the object id only when it is still the SDK URN — a\n * consumer that already rewrote either keeps its own values.\n */\nfunction applyIdentity(statement: XAPIStatement, cfg: XAPIConfig): XAPIStatement {\n const isAnonymous =\n statement.actor?.account?.name === 'anonymous' &&\n statement.actor.account.homePage === 'https://github.com/intellectif/learning-kit';\n const isSdkObjectId =\n statement.object?.objectType === 'Activity' &&\n typeof statement.object.id === 'string' &&\n statement.object.id.startsWith('urn:learning-kit:activity:');\n\n if (!isAnonymous && !isSdkObjectId) {\n return statement;\n }\n const mappedId =\n isSdkObjectId && cfg.activityId\n ? typeof cfg.activityId === 'function'\n ? cfg.activityId(statement.object.id)\n : cfg.activityId\n : undefined;\n return {\n ...statement,\n ...(isAnonymous && cfg.actor !== undefined ? { actor: cfg.actor } : {}),\n ...(mappedId !== undefined ? { object: { ...statement.object, id: mappedId } } : {}),\n };\n}\n\n/** UTF-8-safe base64 (`btoa` is Latin1-only and breaks on non-ASCII creds). */\nfunction toBase64Utf8(input: string): string {\n const bytes = new TextEncoder().encode(input);\n let binary = '';\n for (const byte of bytes) {\n binary += String.fromCharCode(byte);\n }\n return btoa(binary);\n}\n\nfunction authHeader(auth: XAPIConfig['auth']): string {\n return auth.type === 'basic'\n ? `Basic ${toBase64Utf8(`${auth.username}:${auth.password}`)}`\n : `Bearer ${auth.token}`;\n}\n\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\nexport interface UseXAPIResult {\n /**\n * Sends a statement to the configured LRS. **Never rejects** — xAPI is\n * fire-and-forget telemetry that must not break the learner's activity\n * flow. Network errors and 5xx responses are retried (backoff 1s/2s/4s,\n * up to 3 retries); a 4xx fails immediately. Any final failure is delivered\n * to `config.onError`.\n */\n sendStatement: (statement: XAPIStatement) => Promise<void>;\n}\n\nexport function useXAPI(config: XAPIConfig): UseXAPIResult {\n // Keep the latest config without changing `sendStatement`'s identity when\n // callers pass an inline config object.\n const configRef = useRef(config);\n configRef.current = config;\n\n const sendStatement = useCallback(async (rawStatement: XAPIStatement): Promise<void> => {\n const cfg = configRef.current;\n const statement = applyIdentity(rawStatement, cfg);\n const maxAttempts = 1 + BACKOFF_MS.length;\n\n for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {\n let statusCode: number | undefined;\n let message = 'Request failed';\n\n try {\n const response = await fetch(cfg.endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Experience-API-Version': '1.0.3',\n Authorization: authHeader(cfg.auth),\n },\n body: JSON.stringify(statement),\n });\n\n if (response.ok) {\n return;\n }\n\n statusCode = response.status;\n message = `LRS responded with ${response.status}`;\n\n // Only network errors and 5xx are retryable (Req 10.4). A 4xx is a\n // client error — fail immediately rather than waste retries.\n if (response.status < 500) {\n cfg.onError?.({ statement, attempt, statusCode, message });\n return;\n }\n } catch (error) {\n message = error instanceof Error ? error.message : 'Network error';\n }\n\n if (attempt === maxAttempts) {\n cfg.onError?.({\n statement,\n attempt,\n ...(statusCode !== undefined ? { statusCode } : {}),\n message,\n });\n return;\n }\n\n await sleep(BACKOFF_MS[attempt - 1]);\n }\n }, []);\n\n return { sendStatement };\n}\n"]}
|
|
@@ -4,6 +4,19 @@ var react = require('react');
|
|
|
4
4
|
|
|
5
5
|
// src/hooks/useXAPI.ts
|
|
6
6
|
var BACKOFF_MS = [1e3, 2e3, 4e3];
|
|
7
|
+
function applyIdentity(statement, cfg) {
|
|
8
|
+
const isAnonymous = statement.actor?.account?.name === "anonymous" && statement.actor.account.homePage === "https://github.com/intellectif/learning-kit";
|
|
9
|
+
const isSdkObjectId = statement.object?.objectType === "Activity" && typeof statement.object.id === "string" && statement.object.id.startsWith("urn:learning-kit:activity:");
|
|
10
|
+
if (!isAnonymous && !isSdkObjectId) {
|
|
11
|
+
return statement;
|
|
12
|
+
}
|
|
13
|
+
const mappedId = isSdkObjectId && cfg.activityId ? typeof cfg.activityId === "function" ? cfg.activityId(statement.object.id) : cfg.activityId : void 0;
|
|
14
|
+
return {
|
|
15
|
+
...statement,
|
|
16
|
+
...isAnonymous && cfg.actor !== void 0 ? { actor: cfg.actor } : {},
|
|
17
|
+
...mappedId !== void 0 ? { object: { ...statement.object, id: mappedId } } : {}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
7
20
|
function toBase64Utf8(input) {
|
|
8
21
|
const bytes = new TextEncoder().encode(input);
|
|
9
22
|
let binary = "";
|
|
@@ -19,8 +32,9 @@ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
19
32
|
function useXAPI(config) {
|
|
20
33
|
const configRef = react.useRef(config);
|
|
21
34
|
configRef.current = config;
|
|
22
|
-
const sendStatement = react.useCallback(async (
|
|
35
|
+
const sendStatement = react.useCallback(async (rawStatement) => {
|
|
23
36
|
const cfg = configRef.current;
|
|
37
|
+
const statement = applyIdentity(rawStatement, cfg);
|
|
24
38
|
const maxAttempts = 1 + BACKOFF_MS.length;
|
|
25
39
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
26
40
|
let statusCode;
|
|
@@ -63,5 +77,5 @@ function useXAPI(config) {
|
|
|
63
77
|
}
|
|
64
78
|
|
|
65
79
|
exports.useXAPI = useXAPI;
|
|
66
|
-
//# sourceMappingURL=chunk-
|
|
67
|
-
//# sourceMappingURL=chunk-
|
|
80
|
+
//# sourceMappingURL=chunk-IYLQSNLE.cjs.map
|
|
81
|
+
//# sourceMappingURL=chunk-IYLQSNLE.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/useXAPI.ts"],"names":["useRef","useCallback"],"mappings":"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","file":"chunk-IYLQSNLE.cjs","sourcesContent":["'use client';\n\nimport type { XAPIConfig, XAPIStatement } from '@intellectif/lk-core';\nimport { useCallback, useRef } from 'react';\n\n/** Backoff before retry 1 / 2 / 3 (Req 10.4). */\nconst BACKOFF_MS = [1000, 2000, 4000] as const;\n\n/**\n * Applies the configured learner identity to a statement built by an activity\n * component. Components emit an anonymous placeholder actor and a\n * `urn:learning-kit:activity:*` object id (they cannot know identity — Req\n * 3.1); this hook is the documented place where `XAPIConfig.actor` and\n * `XAPIConfig.activityId` take over. Conservative on purpose: the actor is\n * replaced only when the statement still carries the SDK's anonymous\n * placeholder, and the object id only when it is still the SDK URN — a\n * consumer that already rewrote either keeps its own values.\n */\nfunction applyIdentity(statement: XAPIStatement, cfg: XAPIConfig): XAPIStatement {\n const isAnonymous =\n statement.actor?.account?.name === 'anonymous' &&\n statement.actor.account.homePage === 'https://github.com/intellectif/learning-kit';\n const isSdkObjectId =\n statement.object?.objectType === 'Activity' &&\n typeof statement.object.id === 'string' &&\n statement.object.id.startsWith('urn:learning-kit:activity:');\n\n if (!isAnonymous && !isSdkObjectId) {\n return statement;\n }\n const mappedId =\n isSdkObjectId && cfg.activityId\n ? typeof cfg.activityId === 'function'\n ? cfg.activityId(statement.object.id)\n : cfg.activityId\n : undefined;\n return {\n ...statement,\n ...(isAnonymous && cfg.actor !== undefined ? { actor: cfg.actor } : {}),\n ...(mappedId !== undefined ? { object: { ...statement.object, id: mappedId } } : {}),\n };\n}\n\n/** UTF-8-safe base64 (`btoa` is Latin1-only and breaks on non-ASCII creds). */\nfunction toBase64Utf8(input: string): string {\n const bytes = new TextEncoder().encode(input);\n let binary = '';\n for (const byte of bytes) {\n binary += String.fromCharCode(byte);\n }\n return btoa(binary);\n}\n\nfunction authHeader(auth: XAPIConfig['auth']): string {\n return auth.type === 'basic'\n ? `Basic ${toBase64Utf8(`${auth.username}:${auth.password}`)}`\n : `Bearer ${auth.token}`;\n}\n\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\nexport interface UseXAPIResult {\n /**\n * Sends a statement to the configured LRS. **Never rejects** — xAPI is\n * fire-and-forget telemetry that must not break the learner's activity\n * flow. Network errors and 5xx responses are retried (backoff 1s/2s/4s,\n * up to 3 retries); a 4xx fails immediately. Any final failure is delivered\n * to `config.onError`.\n */\n sendStatement: (statement: XAPIStatement) => Promise<void>;\n}\n\nexport function useXAPI(config: XAPIConfig): UseXAPIResult {\n // Keep the latest config without changing `sendStatement`'s identity when\n // callers pass an inline config object.\n const configRef = useRef(config);\n configRef.current = config;\n\n const sendStatement = useCallback(async (rawStatement: XAPIStatement): Promise<void> => {\n const cfg = configRef.current;\n const statement = applyIdentity(rawStatement, cfg);\n const maxAttempts = 1 + BACKOFF_MS.length;\n\n for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {\n let statusCode: number | undefined;\n let message = 'Request failed';\n\n try {\n const response = await fetch(cfg.endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Experience-API-Version': '1.0.3',\n Authorization: authHeader(cfg.auth),\n },\n body: JSON.stringify(statement),\n });\n\n if (response.ok) {\n return;\n }\n\n statusCode = response.status;\n message = `LRS responded with ${response.status}`;\n\n // Only network errors and 5xx are retryable (Req 10.4). A 4xx is a\n // client error — fail immediately rather than waste retries.\n if (response.status < 500) {\n cfg.onError?.({ statement, attempt, statusCode, message });\n return;\n }\n } catch (error) {\n message = error instanceof Error ? error.message : 'Network error';\n }\n\n if (attempt === maxAttempts) {\n cfg.onError?.({\n statement,\n attempt,\n ...(statusCode !== undefined ? { statusCode } : {}),\n message,\n });\n return;\n }\n\n await sleep(BACKOFF_MS[attempt - 1]);\n }\n }, []);\n\n return { sendStatement };\n}\n"]}
|
|
@@ -40,6 +40,13 @@ var ANONYMOUS_ACTOR = {
|
|
|
40
40
|
function objectIdFor(id) {
|
|
41
41
|
return `urn:learning-kit:activity:${encodeURIComponent(id)}`;
|
|
42
42
|
}
|
|
43
|
+
function randomSessionId() {
|
|
44
|
+
const c = globalThis.crypto;
|
|
45
|
+
if (c?.randomUUID) {
|
|
46
|
+
return c.randomUUID();
|
|
47
|
+
}
|
|
48
|
+
return `s-${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
|
|
49
|
+
}
|
|
43
50
|
function ActivityMedia({ media }) {
|
|
44
51
|
const { type, url, alt, captionsUrl } = media;
|
|
45
52
|
if (type === "image") {
|
|
@@ -60,6 +67,7 @@ function ActivityMedia({ media }) {
|
|
|
60
67
|
title: alt ?? "Embedded media",
|
|
61
68
|
loading: "lazy",
|
|
62
69
|
referrerPolicy: "strict-origin-when-cross-origin",
|
|
70
|
+
sandbox: "allow-scripts allow-same-origin allow-presentation",
|
|
63
71
|
allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",
|
|
64
72
|
allowFullScreen: true
|
|
65
73
|
}
|
|
@@ -80,5 +88,6 @@ exports.ActivityMedia = ActivityMedia;
|
|
|
80
88
|
exports.FeedbackRegion = FeedbackRegion;
|
|
81
89
|
exports.isDevelopment = isDevelopment;
|
|
82
90
|
exports.objectIdFor = objectIdFor;
|
|
83
|
-
|
|
84
|
-
//# sourceMappingURL=chunk-
|
|
91
|
+
exports.randomSessionId = randomSessionId;
|
|
92
|
+
//# sourceMappingURL=chunk-QHOV2A6M.cjs.map
|
|
93
|
+
//# sourceMappingURL=chunk-QHOV2A6M.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/ActivityErrorBoundary.tsx","../src/components/_internal.ts","../src/components/shared/ActivityMedia.tsx","../src/components/shared/FeedbackRegion.tsx"],"names":["Component","jsxs","jsx"],"mappings":"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","file":"chunk-QHOV2A6M.cjs","sourcesContent":["'use client';\n\nimport { Component, type ReactNode } from 'react';\n\nfunction isProduction(): boolean {\n const g = globalThis as { process?: { env?: Record<string, string | undefined> } };\n return g.process?.env?.NODE_ENV === 'production';\n}\n\nexport interface ActivityErrorBoundaryProps {\n /** Shown in the production fallback when a render error occurs. */\n activityTitle?: string;\n children: ReactNode;\n}\n\ninterface ActivityErrorBoundaryState {\n error: Error | null;\n}\n\n/**\n * Catches render errors in an activity subtree. In development it renders the\n * full error stack to aid debugging; in production it renders an accessible,\n * generic fallback (optionally naming the activity) so a single broken\n * activity never crashes the host application.\n */\nexport class ActivityErrorBoundary extends Component<\n ActivityErrorBoundaryProps,\n ActivityErrorBoundaryState\n> {\n state: ActivityErrorBoundaryState = { error: null };\n\n static getDerivedStateFromError(error: Error): ActivityErrorBoundaryState {\n return { error };\n }\n\n render(): ReactNode {\n const { error } = this.state;\n if (!error) {\n return this.props.children;\n }\n\n if (!isProduction()) {\n return (\n <div role=\"alert\">\n <strong>Activity failed to render</strong>\n <pre>{error.stack ?? error.message}</pre>\n </div>\n );\n }\n\n return (\n <div role=\"alert\">\n {this.props.activityTitle\n ? `\"${this.props.activityTitle}\" could not be displayed.`\n : 'This activity could not be displayed.'}\n </div>\n );\n }\n}\n","import type { XAPIActor } from '@intellectif/lk-core';\n\n/** Dev = anything other than an explicit production NODE_ENV (browser-safe). */\nexport function isDevelopment(): boolean {\n const g = globalThis as { process?: { env?: Record<string, string | undefined> } };\n return g.process?.env?.NODE_ENV !== 'production';\n}\n\n/**\n * Activity components cannot know the learner's identity (Req 3.1 fixes the\n * prop set). They emit a structurally-valid statement with this anonymous\n * actor; real identity is applied by the useXAPI/LRS layer (XAPIConfig.actor).\n */\nexport const ANONYMOUS_ACTOR: XAPIActor = {\n objectType: 'Agent',\n account: { homePage: 'https://github.com/intellectif/learning-kit', name: 'anonymous' },\n};\n\n/** `data.id` is not an IRI; xAPI object ids must be. Wrap it as a URN. */\nexport function objectIdFor(id: string): string {\n return `urn:learning-kit:activity:${encodeURIComponent(id)}`;\n}\n\n/**\n * Random per-mount session id for the shuffle seed. `crypto.randomUUID` is\n * unavailable outside secure contexts (plain-http LAN/staging hosts), so a\n * Math.random fallback keeps the component from crashing there — the seed\n * only randomises presentation order, so cryptographic strength is not needed.\n */\nexport function randomSessionId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `s-${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;\n}\n","import type { ActivityMedia as ActivityMediaData } from '@intellectif/lk-core';\n\n/**\n * Presentational media block shown above a question or passage. URL-only:\n * the SDK does not host media (consumer responsibility). Accessibility:\n * images use `alt` (schema requires it non-empty); audio/video expose an\n * optional label and a captions `<track>` when `captionsUrl` is provided;\n * `embed` renders a responsive sandboxed iframe with `alt` as its required\n * accessible `title` (Req 14.5).\n */\nexport function ActivityMedia({ media }: { media: ActivityMediaData }): React.JSX.Element {\n const { type, url, alt, captionsUrl } = media;\n\n if (type === 'image') {\n return (\n <figure className=\"lk-media\">\n <img className=\"lk-media-el\" src={url} alt={alt ?? ''} />\n </figure>\n );\n }\n\n if (type === 'audio') {\n return (\n <figure className=\"lk-media\">\n {/* biome-ignore lint/a11y/useMediaCaption: captions are optional in the data contract — a <track> is rendered when captionsUrl is provided; absence is the author's documented choice (Req 14.5) */}\n <audio className=\"lk-media-el\" controls aria-label={alt || undefined}>\n <source src={url} />\n {captionsUrl ? <track kind=\"captions\" src={captionsUrl} default /> : null}\n </audio>\n </figure>\n );\n }\n\n if (type === 'embed') {\n return (\n <figure className=\"lk-media\">\n <div className=\"lk-media-embed\">\n <iframe\n className=\"lk-media-el\"\n src={url}\n title={alt ?? 'Embedded media'}\n loading=\"lazy\"\n referrerPolicy=\"strict-origin-when-cross-origin\"\n // The docblock always promised a sandboxed iframe; now it is one.\n // allow-scripts + allow-same-origin are required by provider\n // players (YouTube/Vimeo); top-navigation, forms, downloads and\n // popups stay blocked. Scheme allow-listing in MediaSchema is the\n // primary defense; this is depth.\n sandbox=\"allow-scripts allow-same-origin allow-presentation\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\"\n allowFullScreen\n />\n </div>\n </figure>\n );\n }\n\n return (\n <figure className=\"lk-media\">\n {/* biome-ignore lint/a11y/useMediaCaption: captions are optional in the data contract — a <track> is rendered when captionsUrl is provided; absence is the author's documented choice (Req 14.5) */}\n <video className=\"lk-media-el\" controls aria-label={alt || undefined}>\n <source src={url} />\n {captionsUrl ? <track kind=\"captions\" src={captionsUrl} default /> : null}\n </video>\n </figure>\n );\n}\n","import type { ReactNode } from 'react';\n\nexport interface FeedbackRegionProps {\n /** Identifier so activity inputs can reference this region via aria. */\n id?: string;\n className?: string;\n children?: ReactNode;\n}\n\n/**\n * A polite ARIA live region. Screen readers announce content injected here\n * (score, feedback, state changes) without moving focus. Pure/presentational\n * — safe to render in any environment.\n */\nexport function FeedbackRegion({ id, className, children }: FeedbackRegionProps) {\n return (\n <div id={id} className={className} aria-live=\"polite\" aria-atomic=\"true\">\n {children}\n </div>\n );\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var
|
|
3
|
+
var chunkW6BH2YMZ_cjs = require('./chunk-W6BH2YMZ.cjs');
|
|
4
|
+
var chunkUXO6KYSZ_cjs = require('./chunk-UXO6KYSZ.cjs');
|
|
5
5
|
var react = require('react');
|
|
6
6
|
var jsxRuntime = require('react/jsx-runtime');
|
|
7
7
|
|
|
@@ -20,6 +20,13 @@ function ActivitySequence({
|
|
|
20
20
|
);
|
|
21
21
|
const regionRef = react.useRef(null);
|
|
22
22
|
const mountedRef = react.useRef(false);
|
|
23
|
+
const setKey = JSON.stringify(activities.map((activity) => activity.id));
|
|
24
|
+
const [prevSetKey, setPrevSetKey] = react.useState(setKey);
|
|
25
|
+
if (prevSetKey !== setKey) {
|
|
26
|
+
setPrevSetKey(setKey);
|
|
27
|
+
setIndex(0);
|
|
28
|
+
setResults(activities.map(() => null));
|
|
29
|
+
}
|
|
23
30
|
react.useEffect(() => {
|
|
24
31
|
if (mountedRef.current) {
|
|
25
32
|
regionRef.current?.focus();
|
|
@@ -65,7 +72,13 @@ function ActivitySequence({
|
|
|
65
72
|
ref: regionRef,
|
|
66
73
|
tabIndex: -1,
|
|
67
74
|
"aria-label": `Question ${index + 1} of ${total}`,
|
|
68
|
-
children: current.type === "multiple-choice" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
75
|
+
children: current.type === "multiple-choice" ? /* @__PURE__ */ jsxRuntime.jsx(chunkW6BH2YMZ_cjs.MultipleChoice, { data: current, ...childProps }, `${index}:${current.id}`) : current.type === "fill-in-the-blanks" ? /* @__PURE__ */ jsxRuntime.jsx(chunkUXO6KYSZ_cjs.FillInTheBlanks, { data: current, ...childProps }, `${index}:${current.id}`) : (
|
|
76
|
+
// written-response completes without a score (deferred grading), so it
|
|
77
|
+
// cannot fill an ActivityResult slot; sequence support arrives with the
|
|
78
|
+
// widened results contract in v0.4. Render standalone <WrittenResponse>
|
|
79
|
+
// instead of putting it in a sequence.
|
|
80
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "lk-seq-unsupported", role: "note", children: "This activity type is not supported inside a question set yet." })
|
|
81
|
+
)
|
|
69
82
|
}
|
|
70
83
|
),
|
|
71
84
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lk-seq-nav", children: [
|
|
@@ -94,5 +107,5 @@ function ActivitySequence({
|
|
|
94
107
|
}
|
|
95
108
|
|
|
96
109
|
exports.ActivitySequence = ActivitySequence;
|
|
97
|
-
//# sourceMappingURL=chunk-
|
|
98
|
-
//# sourceMappingURL=chunk-
|
|
110
|
+
//# sourceMappingURL=chunk-STLRKR2I.cjs.map
|
|
111
|
+
//# sourceMappingURL=chunk-STLRKR2I.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/ActivitySequence/ActivitySequence.tsx"],"names":["useState","useRef","useEffect","jsx","jsxs","MultipleChoice","FillInTheBlanks"],"mappings":"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","file":"chunk-STLRKR2I.cjs","sourcesContent":["'use client';\n\nimport type {\n ActivityData,\n ActivityResult,\n InteractionEvent,\n ThemeTokens,\n} from '@intellectif/lk-core';\nimport { useEffect, useRef, useState } from 'react';\nimport { FillInTheBlanks } from '../FillInTheBlanks/index.js';\nimport { MultipleChoice } from '../MultipleChoice/index.js';\n\nexport interface ActivitySequenceProps {\n /** The ordered set of activities to present, one at a time. */\n activities: ActivityData[];\n /** Called whenever an individual activity is completed. */\n onActivityComplete?: (result: ActivityResult, index: number) => void;\n /** Called once every activity in the set has been completed. */\n onComplete?: (results: ActivityResult[]) => void;\n /** Forwarded to each activity. */\n onInteraction?: (event: InteractionEvent) => void;\n theme?: Partial<ThemeTokens>;\n locale?: string;\n disabled?: boolean;\n}\n\n/**\n * Presents a set of activities as an in-place pager: one question visible at a\n * time, navigated with Prev/Next (no page scrolling). Linear free navigation —\n * the learner may move back and forth; activities keep their own submit and\n * scoring. On navigation, focus moves to the question region and the position\n * is announced via an aria-live region.\n */\nexport function ActivitySequence({\n activities,\n onActivityComplete,\n onComplete,\n onInteraction,\n theme,\n locale,\n disabled,\n}: ActivitySequenceProps): React.JSX.Element {\n const [index, setIndex] = useState(0);\n const [results, setResults] = useState<(ActivityResult | null)[]>(() =>\n activities.map(() => null),\n );\n const regionRef = useRef<HTMLDivElement>(null);\n const mountedRef = useRef(false);\n\n // Reset pager position and results when the activity SET changes (B8):\n // without this, results from the previous set leak into the new one and can\n // fire onComplete with a mixed old/new array. Keyed on the set's CONTENT\n // identity (ordered activity ids), not the array reference — a parent that\n // re-creates a structurally identical array on every render (inline\n // literals, `.map()` in render) must not wipe in-progress answers.\n // Render-time state adjustment per React's \"adjusting state when a prop\n // changes\" pattern.\n // JSON.stringify (not join) so ids containing the separator cannot collide:\n // ['a','bc'] and ['ab','c'] must produce different keys.\n const setKey = JSON.stringify(activities.map((activity) => activity.id));\n const [prevSetKey, setPrevSetKey] = useState(setKey);\n if (prevSetKey !== setKey) {\n setPrevSetKey(setKey);\n setIndex(0);\n setResults(activities.map(() => null));\n }\n\n // After a navigation (not the initial mount) move focus to the question\n // region so screen-reader / keyboard users land on the new question.\n // biome-ignore lint/correctness/useExhaustiveDependencies: index is the intended trigger\n useEffect(() => {\n if (mountedRef.current) {\n regionRef.current?.focus();\n } else {\n mountedRef.current = true;\n }\n }, [index]);\n\n const total = activities.length;\n const current = activities[index];\n\n const go = (next: number): void => {\n setIndex(next);\n };\n\n const handleComplete = (result: ActivityResult): void => {\n const nextResults = results.slice();\n nextResults[index] = result;\n setResults(nextResults);\n onActivityComplete?.(result, index);\n if (nextResults.every((r): r is ActivityResult => r !== null)) {\n onComplete?.(nextResults);\n }\n };\n\n if (!current) {\n return <div className=\"lk-seq\" />;\n }\n\n const childProps = {\n onComplete: handleComplete,\n ...(onInteraction ? { onInteraction } : {}),\n ...(theme ? { theme } : {}),\n ...(locale ? { locale } : {}),\n ...(disabled ? { disabled } : {}),\n };\n\n return (\n <div className=\"lk-seq\" lang={locale}>\n <div className=\"lk-seq-progress\" aria-live=\"polite\" role=\"status\">\n Question {index + 1} of {total}\n </div>\n\n <section\n className=\"lk-seq-question\"\n ref={regionRef}\n tabIndex={-1}\n aria-label={`Question ${index + 1} of ${total}`}\n >\n {current.type === 'multiple-choice' ? (\n <MultipleChoice key={`${index}:${current.id}`} data={current} {...childProps} />\n ) : current.type === 'fill-in-the-blanks' ? (\n <FillInTheBlanks key={`${index}:${current.id}`} data={current} {...childProps} />\n ) : (\n // written-response completes without a score (deferred grading), so it\n // cannot fill an ActivityResult slot; sequence support arrives with the\n // widened results contract in v0.4. Render standalone <WrittenResponse>\n // instead of putting it in a sequence.\n <div className=\"lk-seq-unsupported\" role=\"note\">\n This activity type is not supported inside a question set yet.\n </div>\n )}\n </section>\n\n <div className=\"lk-seq-nav\">\n <button\n type=\"button\"\n className=\"lk-seq-prev\"\n onClick={() => go(index - 1)}\n disabled={index === 0}\n >\n Previous\n </button>\n <button\n type=\"button\"\n className=\"lk-seq-next\"\n onClick={() => go(index + 1)}\n disabled={index === total - 1}\n >\n Next\n </button>\n </div>\n </div>\n );\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { MultipleChoice } from './chunk-
|
|
2
|
-
import { FillInTheBlanks } from './chunk-
|
|
1
|
+
import { MultipleChoice } from './chunk-ZVFWJTT4.js';
|
|
2
|
+
import { FillInTheBlanks } from './chunk-3X7MYAFT.js';
|
|
3
3
|
import { useState, useRef, useEffect } from 'react';
|
|
4
4
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
5
5
|
|
|
@@ -18,6 +18,13 @@ function ActivitySequence({
|
|
|
18
18
|
);
|
|
19
19
|
const regionRef = useRef(null);
|
|
20
20
|
const mountedRef = useRef(false);
|
|
21
|
+
const setKey = JSON.stringify(activities.map((activity) => activity.id));
|
|
22
|
+
const [prevSetKey, setPrevSetKey] = useState(setKey);
|
|
23
|
+
if (prevSetKey !== setKey) {
|
|
24
|
+
setPrevSetKey(setKey);
|
|
25
|
+
setIndex(0);
|
|
26
|
+
setResults(activities.map(() => null));
|
|
27
|
+
}
|
|
21
28
|
useEffect(() => {
|
|
22
29
|
if (mountedRef.current) {
|
|
23
30
|
regionRef.current?.focus();
|
|
@@ -63,7 +70,13 @@ function ActivitySequence({
|
|
|
63
70
|
ref: regionRef,
|
|
64
71
|
tabIndex: -1,
|
|
65
72
|
"aria-label": `Question ${index + 1} of ${total}`,
|
|
66
|
-
children: current.type === "multiple-choice" ? /* @__PURE__ */ jsx(MultipleChoice, { data: current, ...childProps }) : /* @__PURE__ */ jsx(FillInTheBlanks, { data: current, ...childProps })
|
|
73
|
+
children: current.type === "multiple-choice" ? /* @__PURE__ */ jsx(MultipleChoice, { data: current, ...childProps }, `${index}:${current.id}`) : current.type === "fill-in-the-blanks" ? /* @__PURE__ */ jsx(FillInTheBlanks, { data: current, ...childProps }, `${index}:${current.id}`) : (
|
|
74
|
+
// written-response completes without a score (deferred grading), so it
|
|
75
|
+
// cannot fill an ActivityResult slot; sequence support arrives with the
|
|
76
|
+
// widened results contract in v0.4. Render standalone <WrittenResponse>
|
|
77
|
+
// instead of putting it in a sequence.
|
|
78
|
+
/* @__PURE__ */ jsx("div", { className: "lk-seq-unsupported", role: "note", children: "This activity type is not supported inside a question set yet." })
|
|
79
|
+
)
|
|
67
80
|
}
|
|
68
81
|
),
|
|
69
82
|
/* @__PURE__ */ jsxs("div", { className: "lk-seq-nav", children: [
|
|
@@ -92,5 +105,5 @@ function ActivitySequence({
|
|
|
92
105
|
}
|
|
93
106
|
|
|
94
107
|
export { ActivitySequence };
|
|
95
|
-
//# sourceMappingURL=chunk-
|
|
96
|
-
//# sourceMappingURL=chunk-
|
|
108
|
+
//# sourceMappingURL=chunk-UASXUWHH.js.map
|
|
109
|
+
//# sourceMappingURL=chunk-UASXUWHH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/ActivitySequence/ActivitySequence.tsx"],"names":[],"mappings":"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","file":"chunk-UASXUWHH.js","sourcesContent":["'use client';\n\nimport type {\n ActivityData,\n ActivityResult,\n InteractionEvent,\n ThemeTokens,\n} from '@intellectif/lk-core';\nimport { useEffect, useRef, useState } from 'react';\nimport { FillInTheBlanks } from '../FillInTheBlanks/index.js';\nimport { MultipleChoice } from '../MultipleChoice/index.js';\n\nexport interface ActivitySequenceProps {\n /** The ordered set of activities to present, one at a time. */\n activities: ActivityData[];\n /** Called whenever an individual activity is completed. */\n onActivityComplete?: (result: ActivityResult, index: number) => void;\n /** Called once every activity in the set has been completed. */\n onComplete?: (results: ActivityResult[]) => void;\n /** Forwarded to each activity. */\n onInteraction?: (event: InteractionEvent) => void;\n theme?: Partial<ThemeTokens>;\n locale?: string;\n disabled?: boolean;\n}\n\n/**\n * Presents a set of activities as an in-place pager: one question visible at a\n * time, navigated with Prev/Next (no page scrolling). Linear free navigation —\n * the learner may move back and forth; activities keep their own submit and\n * scoring. On navigation, focus moves to the question region and the position\n * is announced via an aria-live region.\n */\nexport function ActivitySequence({\n activities,\n onActivityComplete,\n onComplete,\n onInteraction,\n theme,\n locale,\n disabled,\n}: ActivitySequenceProps): React.JSX.Element {\n const [index, setIndex] = useState(0);\n const [results, setResults] = useState<(ActivityResult | null)[]>(() =>\n activities.map(() => null),\n );\n const regionRef = useRef<HTMLDivElement>(null);\n const mountedRef = useRef(false);\n\n // Reset pager position and results when the activity SET changes (B8):\n // without this, results from the previous set leak into the new one and can\n // fire onComplete with a mixed old/new array. Keyed on the set's CONTENT\n // identity (ordered activity ids), not the array reference — a parent that\n // re-creates a structurally identical array on every render (inline\n // literals, `.map()` in render) must not wipe in-progress answers.\n // Render-time state adjustment per React's \"adjusting state when a prop\n // changes\" pattern.\n // JSON.stringify (not join) so ids containing the separator cannot collide:\n // ['a','bc'] and ['ab','c'] must produce different keys.\n const setKey = JSON.stringify(activities.map((activity) => activity.id));\n const [prevSetKey, setPrevSetKey] = useState(setKey);\n if (prevSetKey !== setKey) {\n setPrevSetKey(setKey);\n setIndex(0);\n setResults(activities.map(() => null));\n }\n\n // After a navigation (not the initial mount) move focus to the question\n // region so screen-reader / keyboard users land on the new question.\n // biome-ignore lint/correctness/useExhaustiveDependencies: index is the intended trigger\n useEffect(() => {\n if (mountedRef.current) {\n regionRef.current?.focus();\n } else {\n mountedRef.current = true;\n }\n }, [index]);\n\n const total = activities.length;\n const current = activities[index];\n\n const go = (next: number): void => {\n setIndex(next);\n };\n\n const handleComplete = (result: ActivityResult): void => {\n const nextResults = results.slice();\n nextResults[index] = result;\n setResults(nextResults);\n onActivityComplete?.(result, index);\n if (nextResults.every((r): r is ActivityResult => r !== null)) {\n onComplete?.(nextResults);\n }\n };\n\n if (!current) {\n return <div className=\"lk-seq\" />;\n }\n\n const childProps = {\n onComplete: handleComplete,\n ...(onInteraction ? { onInteraction } : {}),\n ...(theme ? { theme } : {}),\n ...(locale ? { locale } : {}),\n ...(disabled ? { disabled } : {}),\n };\n\n return (\n <div className=\"lk-seq\" lang={locale}>\n <div className=\"lk-seq-progress\" aria-live=\"polite\" role=\"status\">\n Question {index + 1} of {total}\n </div>\n\n <section\n className=\"lk-seq-question\"\n ref={regionRef}\n tabIndex={-1}\n aria-label={`Question ${index + 1} of ${total}`}\n >\n {current.type === 'multiple-choice' ? (\n <MultipleChoice key={`${index}:${current.id}`} data={current} {...childProps} />\n ) : current.type === 'fill-in-the-blanks' ? (\n <FillInTheBlanks key={`${index}:${current.id}`} data={current} {...childProps} />\n ) : (\n // written-response completes without a score (deferred grading), so it\n // cannot fill an ActivityResult slot; sequence support arrives with the\n // widened results contract in v0.4. Render standalone <WrittenResponse>\n // instead of putting it in a sequence.\n <div className=\"lk-seq-unsupported\" role=\"note\">\n This activity type is not supported inside a question set yet.\n </div>\n )}\n </section>\n\n <div className=\"lk-seq-nav\">\n <button\n type=\"button\"\n className=\"lk-seq-prev\"\n onClick={() => go(index - 1)}\n disabled={index === 0}\n >\n Previous\n </button>\n <button\n type=\"button\"\n className=\"lk-seq-next\"\n onClick={() => go(index + 1)}\n disabled={index === total - 1}\n >\n Next\n </button>\n </div>\n </div>\n );\n}\n"]}
|
|
@@ -38,6 +38,13 @@ var ANONYMOUS_ACTOR = {
|
|
|
38
38
|
function objectIdFor(id) {
|
|
39
39
|
return `urn:learning-kit:activity:${encodeURIComponent(id)}`;
|
|
40
40
|
}
|
|
41
|
+
function randomSessionId() {
|
|
42
|
+
const c = globalThis.crypto;
|
|
43
|
+
if (c?.randomUUID) {
|
|
44
|
+
return c.randomUUID();
|
|
45
|
+
}
|
|
46
|
+
return `s-${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
|
|
47
|
+
}
|
|
41
48
|
function ActivityMedia({ media }) {
|
|
42
49
|
const { type, url, alt, captionsUrl } = media;
|
|
43
50
|
if (type === "image") {
|
|
@@ -58,6 +65,7 @@ function ActivityMedia({ media }) {
|
|
|
58
65
|
title: alt ?? "Embedded media",
|
|
59
66
|
loading: "lazy",
|
|
60
67
|
referrerPolicy: "strict-origin-when-cross-origin",
|
|
68
|
+
sandbox: "allow-scripts allow-same-origin allow-presentation",
|
|
61
69
|
allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",
|
|
62
70
|
allowFullScreen: true
|
|
63
71
|
}
|
|
@@ -72,6 +80,6 @@ function FeedbackRegion({ id, className, children }) {
|
|
|
72
80
|
return /* @__PURE__ */ jsx("div", { id, className, "aria-live": "polite", "aria-atomic": "true", children });
|
|
73
81
|
}
|
|
74
82
|
|
|
75
|
-
export { ANONYMOUS_ACTOR, ActivityErrorBoundary, ActivityMedia, FeedbackRegion, isDevelopment, objectIdFor };
|
|
76
|
-
//# sourceMappingURL=chunk-
|
|
77
|
-
//# sourceMappingURL=chunk-
|
|
83
|
+
export { ANONYMOUS_ACTOR, ActivityErrorBoundary, ActivityMedia, FeedbackRegion, isDevelopment, objectIdFor, randomSessionId };
|
|
84
|
+
//# sourceMappingURL=chunk-UIGIYYGY.js.map
|
|
85
|
+
//# sourceMappingURL=chunk-UIGIYYGY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/ActivityErrorBoundary.tsx","../src/components/_internal.ts","../src/components/shared/ActivityMedia.tsx","../src/components/shared/FeedbackRegion.tsx"],"names":["jsx","jsxs"],"mappings":"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","file":"chunk-UIGIYYGY.js","sourcesContent":["'use client';\n\nimport { Component, type ReactNode } from 'react';\n\nfunction isProduction(): boolean {\n const g = globalThis as { process?: { env?: Record<string, string | undefined> } };\n return g.process?.env?.NODE_ENV === 'production';\n}\n\nexport interface ActivityErrorBoundaryProps {\n /** Shown in the production fallback when a render error occurs. */\n activityTitle?: string;\n children: ReactNode;\n}\n\ninterface ActivityErrorBoundaryState {\n error: Error | null;\n}\n\n/**\n * Catches render errors in an activity subtree. In development it renders the\n * full error stack to aid debugging; in production it renders an accessible,\n * generic fallback (optionally naming the activity) so a single broken\n * activity never crashes the host application.\n */\nexport class ActivityErrorBoundary extends Component<\n ActivityErrorBoundaryProps,\n ActivityErrorBoundaryState\n> {\n state: ActivityErrorBoundaryState = { error: null };\n\n static getDerivedStateFromError(error: Error): ActivityErrorBoundaryState {\n return { error };\n }\n\n render(): ReactNode {\n const { error } = this.state;\n if (!error) {\n return this.props.children;\n }\n\n if (!isProduction()) {\n return (\n <div role=\"alert\">\n <strong>Activity failed to render</strong>\n <pre>{error.stack ?? error.message}</pre>\n </div>\n );\n }\n\n return (\n <div role=\"alert\">\n {this.props.activityTitle\n ? `\"${this.props.activityTitle}\" could not be displayed.`\n : 'This activity could not be displayed.'}\n </div>\n );\n }\n}\n","import type { XAPIActor } from '@intellectif/lk-core';\n\n/** Dev = anything other than an explicit production NODE_ENV (browser-safe). */\nexport function isDevelopment(): boolean {\n const g = globalThis as { process?: { env?: Record<string, string | undefined> } };\n return g.process?.env?.NODE_ENV !== 'production';\n}\n\n/**\n * Activity components cannot know the learner's identity (Req 3.1 fixes the\n * prop set). They emit a structurally-valid statement with this anonymous\n * actor; real identity is applied by the useXAPI/LRS layer (XAPIConfig.actor).\n */\nexport const ANONYMOUS_ACTOR: XAPIActor = {\n objectType: 'Agent',\n account: { homePage: 'https://github.com/intellectif/learning-kit', name: 'anonymous' },\n};\n\n/** `data.id` is not an IRI; xAPI object ids must be. Wrap it as a URN. */\nexport function objectIdFor(id: string): string {\n return `urn:learning-kit:activity:${encodeURIComponent(id)}`;\n}\n\n/**\n * Random per-mount session id for the shuffle seed. `crypto.randomUUID` is\n * unavailable outside secure contexts (plain-http LAN/staging hosts), so a\n * Math.random fallback keeps the component from crashing there — the seed\n * only randomises presentation order, so cryptographic strength is not needed.\n */\nexport function randomSessionId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `s-${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;\n}\n","import type { ActivityMedia as ActivityMediaData } from '@intellectif/lk-core';\n\n/**\n * Presentational media block shown above a question or passage. URL-only:\n * the SDK does not host media (consumer responsibility). Accessibility:\n * images use `alt` (schema requires it non-empty); audio/video expose an\n * optional label and a captions `<track>` when `captionsUrl` is provided;\n * `embed` renders a responsive sandboxed iframe with `alt` as its required\n * accessible `title` (Req 14.5).\n */\nexport function ActivityMedia({ media }: { media: ActivityMediaData }): React.JSX.Element {\n const { type, url, alt, captionsUrl } = media;\n\n if (type === 'image') {\n return (\n <figure className=\"lk-media\">\n <img className=\"lk-media-el\" src={url} alt={alt ?? ''} />\n </figure>\n );\n }\n\n if (type === 'audio') {\n return (\n <figure className=\"lk-media\">\n {/* biome-ignore lint/a11y/useMediaCaption: captions are optional in the data contract — a <track> is rendered when captionsUrl is provided; absence is the author's documented choice (Req 14.5) */}\n <audio className=\"lk-media-el\" controls aria-label={alt || undefined}>\n <source src={url} />\n {captionsUrl ? <track kind=\"captions\" src={captionsUrl} default /> : null}\n </audio>\n </figure>\n );\n }\n\n if (type === 'embed') {\n return (\n <figure className=\"lk-media\">\n <div className=\"lk-media-embed\">\n <iframe\n className=\"lk-media-el\"\n src={url}\n title={alt ?? 'Embedded media'}\n loading=\"lazy\"\n referrerPolicy=\"strict-origin-when-cross-origin\"\n // The docblock always promised a sandboxed iframe; now it is one.\n // allow-scripts + allow-same-origin are required by provider\n // players (YouTube/Vimeo); top-navigation, forms, downloads and\n // popups stay blocked. Scheme allow-listing in MediaSchema is the\n // primary defense; this is depth.\n sandbox=\"allow-scripts allow-same-origin allow-presentation\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\"\n allowFullScreen\n />\n </div>\n </figure>\n );\n }\n\n return (\n <figure className=\"lk-media\">\n {/* biome-ignore lint/a11y/useMediaCaption: captions are optional in the data contract — a <track> is rendered when captionsUrl is provided; absence is the author's documented choice (Req 14.5) */}\n <video className=\"lk-media-el\" controls aria-label={alt || undefined}>\n <source src={url} />\n {captionsUrl ? <track kind=\"captions\" src={captionsUrl} default /> : null}\n </video>\n </figure>\n );\n}\n","import type { ReactNode } from 'react';\n\nexport interface FeedbackRegionProps {\n /** Identifier so activity inputs can reference this region via aria. */\n id?: string;\n className?: string;\n children?: ReactNode;\n}\n\n/**\n * A polite ARIA live region. Screen readers announce content injected here\n * (score, feedback, state changes) without moving focus. Pure/presentational\n * — safe to render in any environment.\n */\nexport function FeedbackRegion({ id, className, children }: FeedbackRegionProps) {\n return (\n <div id={id} className={className} aria-live=\"polite\" aria-atomic=\"true\">\n {children}\n </div>\n );\n}\n"]}
|