@intellectif/lk-react 3.0.0 → 5.0.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/README.md +7 -2
- package/dist/{WrittenResponse-BHhP936i.d.ts → WrittenResponse-Dkg7FUt-.d.ts} +1 -1
- package/dist/{WrittenResponse-B50EjVR0.d.cts → WrittenResponse-DqQ0NjPK.d.cts} +1 -1
- package/dist/chunk-3B4ACY4K.cjs +249 -0
- package/dist/chunk-3B4ACY4K.cjs.map +1 -0
- package/dist/{chunk-3X7MYAFT.js → chunk-3DNJDP73.js} +11 -9
- package/dist/chunk-3DNJDP73.js.map +1 -0
- package/dist/chunk-4HZ7LQIH.cjs +40 -0
- package/dist/chunk-4HZ7LQIH.cjs.map +1 -0
- package/dist/{chunk-ZVFWJTT4.js → chunk-5SYL5CJ2.js} +10 -28
- package/dist/chunk-5SYL5CJ2.js.map +1 -0
- package/dist/{chunk-UXO6KYSZ.cjs → chunk-67RIIDZX.cjs} +17 -15
- package/dist/chunk-67RIIDZX.cjs.map +1 -0
- package/dist/{chunk-AEQQ7VPF.js → chunk-67WDTQMG.js} +9 -4
- package/dist/chunk-67WDTQMG.js.map +1 -0
- package/dist/{chunk-7MW7HGOA.cjs → chunk-6COTYW77.cjs} +15 -10
- package/dist/chunk-6COTYW77.cjs.map +1 -0
- package/dist/chunk-6F4X2NKL.js +54 -0
- package/dist/chunk-6F4X2NKL.js.map +1 -0
- package/dist/chunk-GB5URWM4.js +38 -0
- package/dist/chunk-GB5URWM4.js.map +1 -0
- package/dist/chunk-NBFTCGQ5.js +247 -0
- package/dist/chunk-NBFTCGQ5.js.map +1 -0
- package/dist/chunk-NEWGUDA5.cjs +49 -0
- package/dist/chunk-NEWGUDA5.cjs.map +1 -0
- package/dist/{chunk-QHOV2A6M.cjs → chunk-PGEG7UZJ.cjs} +2 -34
- package/dist/chunk-PGEG7UZJ.cjs.map +1 -0
- package/dist/chunk-UQ3BBEIT.js +47 -0
- package/dist/chunk-UQ3BBEIT.js.map +1 -0
- package/dist/{chunk-W6BH2YMZ.cjs → chunk-WCVD5LVG.cjs} +16 -34
- package/dist/chunk-WCVD5LVG.cjs.map +1 -0
- package/dist/components/ActivitySequence.cjs +8 -6
- package/dist/components/ActivitySequence.d.cts +87 -9
- package/dist/components/ActivitySequence.d.ts +87 -9
- package/dist/components/ActivitySequence.js +7 -5
- package/dist/components/FillInTheBlanks.cjs +4 -3
- package/dist/components/FillInTheBlanks.d.cts +1 -1
- package/dist/components/FillInTheBlanks.d.ts +1 -1
- package/dist/components/FillInTheBlanks.js +3 -2
- package/dist/components/MultipleChoice.cjs +4 -3
- package/dist/components/MultipleChoice.d.cts +1 -1
- package/dist/components/MultipleChoice.d.ts +1 -1
- package/dist/components/MultipleChoice.js +3 -2
- package/dist/components/StimulusPanel.cjs +13 -0
- package/dist/components/StimulusPanel.cjs.map +1 -0
- package/dist/components/StimulusPanel.d.cts +30 -0
- package/dist/components/StimulusPanel.d.ts +30 -0
- package/dist/components/StimulusPanel.js +4 -0
- package/dist/components/StimulusPanel.js.map +1 -0
- package/dist/components/WrittenResponse.cjs +4 -3
- package/dist/components/WrittenResponse.d.cts +3 -3
- package/dist/components/WrittenResponse.d.ts +3 -3
- package/dist/components/WrittenResponse.js +3 -2
- package/dist/index.cjs +36 -26
- 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 +12 -7
- package/dist/index.js.map +1 -1
- package/dist/theme/skin.css +49 -0
- package/dist/{types-CNmjqD6M.d.cts → types-CrURyd9-.d.cts} +51 -3
- package/dist/{types-CNmjqD6M.d.ts → types-CrURyd9-.d.ts} +51 -3
- package/package.json +15 -4
- package/dist/chunk-3X7MYAFT.js.map +0 -1
- package/dist/chunk-7MW7HGOA.cjs.map +0 -1
- package/dist/chunk-AEQQ7VPF.js.map +0 -1
- package/dist/chunk-BGWXN44S.js +0 -152
- package/dist/chunk-BGWXN44S.js.map +0 -1
- package/dist/chunk-HO3AC3WQ.cjs +0 -154
- package/dist/chunk-HO3AC3WQ.cjs.map +0 -1
- package/dist/chunk-QHOV2A6M.cjs.map +0 -1
- package/dist/chunk-UIGIYYGY.js +0 -85
- package/dist/chunk-UIGIYYGY.js.map +0 -1
- package/dist/chunk-UXO6KYSZ.cjs.map +0 -1
- package/dist/chunk-W6BH2YMZ.cjs.map +0 -1
- package/dist/chunk-ZVFWJTT4.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/shared/ActivityMedia.tsx"],"names":[],"mappings":";;;AAUO,SAAS,aAAA,CAAc,EAAE,KAAA,EAAM,EAAoD;AACxF,EAAA,MAAM,EAAE,IAAA,EAAM,GAAA,EAAK,GAAA,EAAK,aAAY,GAAI,KAAA;AAExC,EAAA,IAAI,SAAS,OAAA,EAAS;AACpB,IAAA,uBACE,GAAA,CAAC,QAAA,EAAA,EAAO,SAAA,EAAU,UAAA,EAChB,QAAA,kBAAA,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,aAAA,EAAc,GAAA,EAAK,GAAA,EAAK,GAAA,EAAK,GAAA,IAAO,IAAI,CAAA,EACzD,CAAA;AAAA,EAEJ;AAEA,EAAA,IAAI,SAAS,OAAA,EAAS;AACpB,IAAA,uBACE,GAAA,CAAC,QAAA,EAAA,EAAO,SAAA,EAAU,UAAA,EAEhB,QAAA,kBAAA,IAAA,CAAC,OAAA,EAAA,EAAM,SAAA,EAAU,aAAA,EAAc,QAAA,EAAQ,IAAA,EAAC,YAAA,EAAY,GAAA,IAAO,MAAA,EACzD,QAAA,EAAA;AAAA,sBAAA,GAAA,CAAC,QAAA,EAAA,EAAO,KAAK,GAAA,EAAK,CAAA;AAAA,MACjB,WAAA,uBAAe,OAAA,EAAA,EAAM,IAAA,EAAK,YAAW,GAAA,EAAK,WAAA,EAAa,OAAA,EAAO,IAAA,EAAC,CAAA,GAAK;AAAA,KAAA,EACvE,CAAA,EACF,CAAA;AAAA,EAEJ;AAEA,EAAA,IAAI,SAAS,OAAA,EAAS;AACpB,IAAA,2BACG,QAAA,EAAA,EAAO,SAAA,EAAU,YAChB,QAAA,kBAAA,GAAA,CAAC,KAAA,EAAA,EAAI,WAAU,gBAAA,EACb,QAAA,kBAAA,GAAA;AAAA,MAAC,QAAA;AAAA,MAAA;AAAA,QACC,SAAA,EAAU,aAAA;AAAA,QACV,GAAA,EAAK,GAAA;AAAA,QACL,OAAO,GAAA,IAAO,gBAAA;AAAA,QACd,OAAA,EAAQ,MAAA;AAAA,QACR,cAAA,EAAe,iCAAA;AAAA,QAMf,OAAA,EAAQ,oDAAA;AAAA,QACR,KAAA,EAAM,qGAAA;AAAA,QACN,eAAA,EAAe;AAAA;AAAA,OAEnB,CAAA,EACF,CAAA;AAAA,EAEJ;AAEA,EAAA,uBACE,GAAA,CAAC,QAAA,EAAA,EAAO,SAAA,EAAU,UAAA,EAEhB,QAAA,kBAAA,IAAA,CAAC,OAAA,EAAA,EAAM,SAAA,EAAU,aAAA,EAAc,QAAA,EAAQ,IAAA,EAAC,YAAA,EAAY,GAAA,IAAO,MAAA,EACzD,QAAA,EAAA;AAAA,oBAAA,GAAA,CAAC,QAAA,EAAA,EAAO,KAAK,GAAA,EAAK,CAAA;AAAA,IACjB,WAAA,uBAAe,OAAA,EAAA,EAAM,IAAA,EAAK,YAAW,GAAA,EAAK,WAAA,EAAa,OAAA,EAAO,IAAA,EAAC,CAAA,GAAK;AAAA,GAAA,EACvE,CAAA,EACF,CAAA;AAEJ","file":"chunk-GB5URWM4.js","sourcesContent":["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"]}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { MultipleChoice } from './chunk-5SYL5CJ2.js';
|
|
2
|
+
import { FillInTheBlanks } from './chunk-3DNJDP73.js';
|
|
3
|
+
import { WrittenResponse } from './chunk-67WDTQMG.js';
|
|
4
|
+
import { randomSessionId } from './chunk-6F4X2NKL.js';
|
|
5
|
+
import { StimulusPanel } from './chunk-UQ3BBEIT.js';
|
|
6
|
+
import { isItemGroup, flattenSequence } from '@intellectif/lk-core';
|
|
7
|
+
import { useRef, useMemo, useState, useEffect } from 'react';
|
|
8
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
9
|
+
|
|
10
|
+
function entryKeyOf(slot) {
|
|
11
|
+
return slot.slotId.split(".")[0] ?? slot.slotId;
|
|
12
|
+
}
|
|
13
|
+
function SequencePane({
|
|
14
|
+
className,
|
|
15
|
+
hidden,
|
|
16
|
+
children
|
|
17
|
+
}) {
|
|
18
|
+
const ref = useRef(null);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!hidden) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const node = ref.current;
|
|
24
|
+
if (node === null) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
for (const media of node.querySelectorAll("audio, video")) {
|
|
28
|
+
const element = media;
|
|
29
|
+
if (!element.paused) {
|
|
30
|
+
element.pause();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}, [hidden]);
|
|
34
|
+
return /* @__PURE__ */ jsx("div", { className, hidden, ref, children });
|
|
35
|
+
}
|
|
36
|
+
function ActivitySequence({
|
|
37
|
+
activities,
|
|
38
|
+
renderers,
|
|
39
|
+
onActivityComplete,
|
|
40
|
+
onComplete,
|
|
41
|
+
onFinished,
|
|
42
|
+
onSubmit,
|
|
43
|
+
onInteraction,
|
|
44
|
+
renderMode = "practice",
|
|
45
|
+
shuffle,
|
|
46
|
+
shuffleSeed,
|
|
47
|
+
sanitizeHtml,
|
|
48
|
+
theme,
|
|
49
|
+
locale,
|
|
50
|
+
disabled
|
|
51
|
+
}) {
|
|
52
|
+
const sessionIdRef = useRef(null);
|
|
53
|
+
const shuffleEntries = shuffle === "entries";
|
|
54
|
+
const needsSeed = shuffleEntries || activities.some((entry) => isItemGroup(entry) && entry.shuffle === "within-group");
|
|
55
|
+
if (needsSeed && shuffleSeed === void 0 && renderMode !== "practice") {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`ActivitySequence: renderMode "${renderMode}" requires a \`shuffleSeed\` when shuffling (shuffle="entries", or a group with shuffle: "within-group"). Pass the attempt id, so the server's flattenSequence(entries, { seed }) reproduces the order the learner saw.`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const seed = needsSeed ? (
|
|
61
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: sanctioned lazy ref initialization
|
|
62
|
+
shuffleSeed ?? (sessionIdRef.current ??= randomSessionId())
|
|
63
|
+
) : void 0;
|
|
64
|
+
const slots = useMemo(
|
|
65
|
+
() => flattenSequence(activities, { shuffleEntries, ...seed !== void 0 ? { seed } : {} }),
|
|
66
|
+
[activities, shuffleEntries, seed]
|
|
67
|
+
);
|
|
68
|
+
const [index, setIndex] = useState(0);
|
|
69
|
+
const outcomesRef = useRef(null);
|
|
70
|
+
if (outcomesRef.current === null) {
|
|
71
|
+
outcomesRef.current = slots.map(() => null);
|
|
72
|
+
}
|
|
73
|
+
const regionRef = useRef(null);
|
|
74
|
+
const mountedRef = useRef(false);
|
|
75
|
+
const setKey = JSON.stringify(slots.map((slot) => [slot.slotId, slot.activity.id]));
|
|
76
|
+
const [prevSetKey, setPrevSetKey] = useState(setKey);
|
|
77
|
+
if (prevSetKey !== setKey) {
|
|
78
|
+
setPrevSetKey(setKey);
|
|
79
|
+
setIndex(0);
|
|
80
|
+
outcomesRef.current = slots.map(() => null);
|
|
81
|
+
}
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (mountedRef.current) {
|
|
84
|
+
regionRef.current?.focus();
|
|
85
|
+
} else {
|
|
86
|
+
mountedRef.current = true;
|
|
87
|
+
}
|
|
88
|
+
}, [index]);
|
|
89
|
+
const stimulusMounts = useMemo(() => {
|
|
90
|
+
const mounts = /* @__PURE__ */ new Map();
|
|
91
|
+
for (const slot of slots) {
|
|
92
|
+
if (slot.group === void 0) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const entryKey = entryKeyOf(slot);
|
|
96
|
+
const existing = mounts.get(entryKey);
|
|
97
|
+
if (existing === void 0) {
|
|
98
|
+
mounts.set(entryKey, { entryKey, group: slot.group, first: slot.index, last: slot.index });
|
|
99
|
+
} else {
|
|
100
|
+
existing.last = slot.index;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return [...mounts.values()];
|
|
104
|
+
}, [slots]);
|
|
105
|
+
const total = slots.length;
|
|
106
|
+
const current = slots[index];
|
|
107
|
+
const go = (next) => {
|
|
108
|
+
setIndex(next);
|
|
109
|
+
};
|
|
110
|
+
const record = (outcome) => {
|
|
111
|
+
const current2 = outcomesRef.current;
|
|
112
|
+
if (current2 === null || current2[outcome.index] != null) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const next = current2.slice();
|
|
116
|
+
next[outcome.index] = outcome;
|
|
117
|
+
outcomesRef.current = next;
|
|
118
|
+
if (outcome.kind === "scored") {
|
|
119
|
+
onActivityComplete?.(outcome.result, outcome.index, outcome.slotId);
|
|
120
|
+
}
|
|
121
|
+
if (!next.every((item) => item !== null)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
onFinished?.(next);
|
|
125
|
+
const scored = [];
|
|
126
|
+
for (const item of next) {
|
|
127
|
+
if (item.kind !== "scored") {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
scored.push(item.result);
|
|
131
|
+
}
|
|
132
|
+
onComplete?.(scored);
|
|
133
|
+
};
|
|
134
|
+
if (!current) {
|
|
135
|
+
return /* @__PURE__ */ jsx("div", { className: "lk-seq" });
|
|
136
|
+
}
|
|
137
|
+
const forwarded = {
|
|
138
|
+
...onInteraction ? { onInteraction } : {},
|
|
139
|
+
renderMode,
|
|
140
|
+
...sanitizeHtml ? { sanitizeHtml } : {},
|
|
141
|
+
...theme ? { theme } : {},
|
|
142
|
+
...locale ? { locale } : {},
|
|
143
|
+
...disabled ? { disabled } : {}
|
|
144
|
+
};
|
|
145
|
+
const renderSlot = (slot) => {
|
|
146
|
+
const activity = slot.activity;
|
|
147
|
+
const { slotId, index: slotIndex } = slot;
|
|
148
|
+
const activityId = activity.id;
|
|
149
|
+
const childProps = {
|
|
150
|
+
onComplete: (result) => record({ kind: "scored", index: slotIndex, slotId, activityId, result }),
|
|
151
|
+
onSubmit: (response) => {
|
|
152
|
+
onSubmit?.(response, { slotId, index: slotIndex, activityId });
|
|
153
|
+
if (renderMode !== "practice" && activity.type !== "written-response") {
|
|
154
|
+
record({ kind: "responded", index: slotIndex, slotId, activityId, response });
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
...forwarded
|
|
158
|
+
};
|
|
159
|
+
const CustomRenderer = renderers?.[activity.type];
|
|
160
|
+
if (CustomRenderer) {
|
|
161
|
+
return /* @__PURE__ */ jsx(CustomRenderer, { data: activity, ...childProps });
|
|
162
|
+
}
|
|
163
|
+
if (activity.type === "multiple-choice") {
|
|
164
|
+
return /* @__PURE__ */ jsx(MultipleChoice, { data: activity, ...childProps });
|
|
165
|
+
}
|
|
166
|
+
if (activity.type === "fill-in-the-blanks") {
|
|
167
|
+
return /* @__PURE__ */ jsx(FillInTheBlanks, { data: activity, ...childProps });
|
|
168
|
+
}
|
|
169
|
+
if (activity.type === "written-response") {
|
|
170
|
+
return /* @__PURE__ */ jsx(
|
|
171
|
+
WrittenResponse,
|
|
172
|
+
{
|
|
173
|
+
data: activity,
|
|
174
|
+
onSubmitted: (submission) => record({ kind: "submitted", index: slotIndex, slotId, activityId, submission }),
|
|
175
|
+
onSubmit: childProps.onSubmit,
|
|
176
|
+
...forwarded
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return /* @__PURE__ */ jsx("div", { className: "lk-seq-unsupported", role: "note", children: "This activity type has no renderer. Supply one through the renderers prop." });
|
|
181
|
+
};
|
|
182
|
+
const currentEntryKey = entryKeyOf(current);
|
|
183
|
+
return /* @__PURE__ */ jsxs("div", { className: "lk-seq", lang: locale, children: [
|
|
184
|
+
/* @__PURE__ */ jsxs("div", { className: "lk-seq-progress", "aria-live": "polite", role: "status", children: [
|
|
185
|
+
"Question ",
|
|
186
|
+
index + 1,
|
|
187
|
+
" of ",
|
|
188
|
+
total
|
|
189
|
+
] }),
|
|
190
|
+
stimulusMounts.map(({ entryKey, group, first, last }) => /* @__PURE__ */ jsx(
|
|
191
|
+
SequencePane,
|
|
192
|
+
{
|
|
193
|
+
className: "lk-seq-stimulus",
|
|
194
|
+
hidden: entryKey !== currentEntryKey,
|
|
195
|
+
children: /* @__PURE__ */ jsx(
|
|
196
|
+
StimulusPanel,
|
|
197
|
+
{
|
|
198
|
+
stimulus: group.stimulus,
|
|
199
|
+
range: { first: first + 1, last: last + 1 },
|
|
200
|
+
...sanitizeHtml ? { sanitizeHtml } : {},
|
|
201
|
+
...locale ? { locale } : {}
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
},
|
|
205
|
+
`group-${entryKey}`
|
|
206
|
+
)),
|
|
207
|
+
/* @__PURE__ */ jsx(
|
|
208
|
+
"section",
|
|
209
|
+
{
|
|
210
|
+
className: "lk-seq-question",
|
|
211
|
+
ref: regionRef,
|
|
212
|
+
tabIndex: -1,
|
|
213
|
+
"aria-label": `Question ${index + 1} of ${total}`,
|
|
214
|
+
children: slots.map((slot) => {
|
|
215
|
+
const slotKey = `${setKey}::slot-${slot.slotId}-${slot.activity.id}`;
|
|
216
|
+
return /* @__PURE__ */ jsx(SequencePane, { className: "lk-seq-slot", hidden: slot.index !== index, children: renderSlot(slot) }, slotKey);
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
),
|
|
220
|
+
/* @__PURE__ */ jsxs("div", { className: "lk-seq-nav", children: [
|
|
221
|
+
/* @__PURE__ */ jsx(
|
|
222
|
+
"button",
|
|
223
|
+
{
|
|
224
|
+
type: "button",
|
|
225
|
+
className: "lk-seq-prev",
|
|
226
|
+
onClick: () => go(index - 1),
|
|
227
|
+
disabled: index === 0,
|
|
228
|
+
children: "Previous"
|
|
229
|
+
}
|
|
230
|
+
),
|
|
231
|
+
/* @__PURE__ */ jsx(
|
|
232
|
+
"button",
|
|
233
|
+
{
|
|
234
|
+
type: "button",
|
|
235
|
+
className: "lk-seq-next",
|
|
236
|
+
onClick: () => go(index + 1),
|
|
237
|
+
disabled: index === total - 1,
|
|
238
|
+
children: "Next"
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
] })
|
|
242
|
+
] });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export { ActivitySequence };
|
|
246
|
+
//# sourceMappingURL=chunk-NBFTCGQ5.js.map
|
|
247
|
+
//# sourceMappingURL=chunk-NBFTCGQ5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/ActivitySequence/ActivitySequence.tsx"],"names":["current"],"mappings":"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","file":"chunk-NBFTCGQ5.js","sourcesContent":["'use client';\n\nimport {\n type ActivityResult,\n flattenSequence,\n type InteractionEvent,\n isItemGroup,\n type LearnerResponse,\n type SequenceEntry,\n type SequenceSlot,\n type SequenceSlotGroup,\n type ThemeTokens,\n} from '@intellectif/lk-core';\nimport { type ComponentType, useEffect, useMemo, useRef, useState } from 'react';\nimport { randomSessionId } from '../_internal.js';\nimport { FillInTheBlanks } from '../FillInTheBlanks/index.js';\nimport { MultipleChoice } from '../MultipleChoice/index.js';\nimport { StimulusPanel } from '../StimulusPanel/index.js';\nimport type { ActivityProps, HtmlSanitizer, RenderableActivity, RenderMode } from '../types.js';\nimport { WrittenResponse, type WrittenResponseSubmission } from '../WrittenResponse/index.js';\n\n/**\n * What one finished slot in a sequence produced. Two kinds, because two kinds\n * of activity exist: those the SDK scores at submit time, and those a grader\n * scores later. Collapsing them would mean inventing a score for ungraded\n * work — the exact defect the deferred outcome exists to prevent.\n */\nexport type SequenceItemOutcome =\n | {\n kind: 'scored';\n /** Presented position. */\n index: number;\n /** Slot identity from `flattenSequence` — stable under shuffling; the `slotId` to score with. */\n slotId: string;\n activityId: string;\n result: ActivityResult;\n }\n | {\n kind: 'submitted';\n index: number;\n slotId: string;\n activityId: string;\n /** Ungraded submission; the grade arrives asynchronously. */\n submission: WrittenResponseSubmission;\n }\n | {\n kind: 'responded';\n index: number;\n slotId: string;\n activityId: string;\n /**\n * The raw answer, with no grade of any kind. This is what an `exam` slot\n * produces: the client is forbidden to score, so a response is all there\n * is until the server grades it.\n */\n response: LearnerResponse;\n };\n\n/**\n * A component that can render one activity inside a sequence. Register one per\n * activity `type` to put a consumer-defined type on screen — the React half of\n * the activity-type registry, matching `registerActivityType` in lk-core.\n */\nexport type ActivityRenderer = ComponentType<ActivityProps>;\n\nexport interface ActivitySequenceProps {\n /**\n * The ordered set of entries to present, one question at a time: loose\n * activities, and item groups — a shared passage, recording or image with\n * the questions that refer to it. A group's stimulus stays on screen\n * alongside each of its questions.\n *\n * Accepts `redact()` projections. For data straight off a server, pass it\n * through `asRenderableSequence()` — see that helper for why a cast is\n * unavoidable there — and set `renderMode=\"exam\"`.\n */\n activities: readonly SequenceEntry<RenderableActivity>[];\n /**\n * Renderers for activity types beyond the built-ins, keyed by `data.type`.\n * A key matching a built-in overrides it, so a consumer can replace the\n * bundled renderer without forking the sequencer.\n */\n renderers?: Readonly<Record<string, ActivityRenderer>>;\n /**\n * Called whenever an individual activity is scored at submit time — the\n * incremental persist hook.\n *\n * `slotId` is the identity to store the result against, NOT `index`.\n * `index` is where the question was PRESENTED, which is not stable: it moves\n * under shuffling, and it already differs from the slot identity whenever a\n * group is present (the second entry of a sequence is presented at index 1\n * but is slot `\"1.0\"` if it is a group's first question). `composeAssessmentScore`\n * scores by `slotId`, so persisting `index` cannot be reconciled with it.\n */\n onActivityComplete?: (result: ActivityResult, index: number, slotId: string) => void;\n /**\n * Called on every submit with the learner's raw response and the identity of\n * the slot that produced it — before any grading, in every render mode.\n *\n * **This is the only response channel an `exam` sequence has.** In `exam`\n * mode the components never grade, so `onActivityComplete` cannot fire and\n * `onComplete` never will; without this prop nothing a learner submitted\n * would reach the consumer at all. Persist `response` against `slotId`.\n */\n onSubmit?: (\n response: LearnerResponse,\n slot: { slotId: string; index: number; activityId: string },\n ) => void;\n /**\n * Called once every activity has been completed, with the scored results.\n *\n * Fires only when EVERY item produced a score. A set containing a\n * deferred-graded activity (a written response) can never satisfy that, so\n * use `onFinished` for mixed sets — it is the general completion signal and\n * reports both kinds of outcome. It also carries `slotId` per item, which\n * this callback's bare array cannot.\n */\n onComplete?: (results: ActivityResult[]) => void;\n /**\n * Called once every activity has been completed, whether it was scored at\n * submit time or submitted for later grading. Use this for any set that\n * mixes graded and deferred-graded activities.\n */\n onFinished?: (items: SequenceItemOutcome[]) => void;\n /** Forwarded to each activity. */\n onInteraction?: (event: InteractionEvent) => void;\n /** Forwarded to each activity. `exam` and `review` disable local scoring. */\n renderMode?: RenderMode;\n /**\n * `entries` shuffles the top-level entries; a group moves as one block, and\n * the order INSIDE a group follows the group's own `shuffle` setting.\n * Default `none`: authored order.\n */\n shuffle?: 'none' | 'entries';\n /**\n * Seed for every shuffle in this sequence (`shuffle: 'entries'` and any\n * group with `shuffle: 'within-group'`). Supply the attempt id, so the\n * server's `flattenSequence(entries, { seed })` derives the same order this\n * pager shows.\n *\n * **Required in `exam` and `review` mode** — omitting it throws. An order\n * nobody can reproduce cannot be reconciled with a recorded attempt, and\n * failing at render is the only way that mistake surfaces before a learner\n * sits the paper. In `practice` mode it stays optional: a random per-mount\n * seed is used, which is stable within the mount and deliberately not\n * reproducible. That fallback is also not SSR-safe (server and client would\n * invent different orders and hydration would mismatch), so supply a seed\n * for any server-rendered sequence regardless of mode.\n */\n shuffleSeed?: string;\n /** Renders author-supplied rich text; forwarded to the stimulus panel and every activity. */\n sanitizeHtml?: HtmlSanitizer;\n theme?: Partial<ThemeTokens>;\n locale?: string;\n disabled?: boolean;\n}\n\n/** One stimulus panel to keep mounted: the group, and the presented span of its questions. */\ninterface StimulusMount {\n /** Authored entry position — unique per group in the sequence even if two groups share an id. */\n entryKey: string;\n group: SequenceSlotGroup;\n first: number;\n last: number;\n}\n\n/** The authored-entry prefix of a slot id: `\"3\"` for `\"3\"` and for `\"3.1\"`. */\nfunction entryKeyOf(slot: SequenceSlot<RenderableActivity>): string {\n return slot.slotId.split('.')[0] ?? slot.slotId;\n}\n\n/**\n * A pane that is kept MOUNTED but shown only when it is the current one, and\n * that stops any media it contains on the way out.\n *\n * `hidden` alone is not enough. It resolves to `display: none`, and CSS does\n * not touch playback: an `<audio>` or `<video>` inside a hidden subtree keeps\n * playing to the end. (The HTML spec pauses a media element when it is\n * REMOVED from the document — a different thing, and the thing this pager\n * deliberately does not do, because unmounting is what used to destroy the\n * learner's answers.) So a recording started for one question would carry on\n * underneath the next, which for a listening paper means the passage plays\n * while the learner is somewhere else entirely.\n *\n * Pausing preserves `currentTime`: coming back to a group resumes where the\n * learner left off. Nothing auto-plays on the way in — starting audio the\n * learner did not ask for is its own defect.\n *\n * `embed` media (a provider iframe) cannot be paused this way: controlling a\n * third-party player needs its own JS API, which the SDK has no reliable\n * access to because the author supplies the embed URL. Use `audio`/`video`\n * media for anything that must stop when the learner navigates.\n */\nfunction SequencePane({\n className,\n hidden,\n children,\n}: {\n className: string;\n hidden: boolean;\n children: React.ReactNode;\n}): React.JSX.Element {\n const ref = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n if (!hidden) {\n return;\n }\n const node = ref.current;\n if (node === null) {\n return;\n }\n for (const media of node.querySelectorAll('audio, video')) {\n const element = media as HTMLMediaElement;\n if (!element.paused) {\n element.pause();\n }\n }\n }, [hidden]);\n\n return (\n <div className={className} hidden={hidden} ref={ref}>\n {children}\n </div>\n );\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 *\n * Item groups are flattened into consecutive questions; the group's stimulus\n * is mounted ONCE, beside the question region rather than inside it, and shown\n * alongside every question in the group — so a passage stays put, and a\n * recording keeps its position, as the learner moves between its questions.\n */\nexport function ActivitySequence({\n activities,\n renderers,\n onActivityComplete,\n onComplete,\n onFinished,\n onSubmit,\n onInteraction,\n renderMode = 'practice',\n shuffle,\n shuffleSeed,\n sanitizeHtml,\n theme,\n locale,\n disabled,\n}: ActivitySequenceProps): React.JSX.Element {\n const sessionIdRef = useRef<string | null>(null);\n\n // The presented order comes from lk-core, never computed here: the server\n // that records an attempt calls the same function with the same seed.\n const shuffleEntries = shuffle === 'entries';\n const needsSeed =\n shuffleEntries ||\n activities.some((entry) => isItemGroup(entry) && entry.shuffle === 'within-group');\n\n // An unreproducible order is a practice-only affordance. Under `exam` or\n // `review` the server has to be able to rebuild exactly what the learner\n // saw, so a missing seed is an error rather than something to paper over —\n // the same reason `flattenSequence` refuses to invent one.\n if (needsSeed && shuffleSeed === undefined && renderMode !== 'practice') {\n throw new Error(\n `ActivitySequence: renderMode \"${renderMode}\" requires a \\`shuffleSeed\\` when shuffling ` +\n '(shuffle=\"entries\", or a group with shuffle: \"within-group\"). Pass the attempt id, so ' +\n \"the server's flattenSequence(entries, { seed }) reproduces the order the learner saw.\",\n );\n }\n\n // Lazily created only when shuffling in practice without a caller seed (same\n // contract as MultipleChoice.shuffleSeed): a per-mount session seed, stable\n // across re-renders, never reproducible.\n const seed = needsSeed\n ? // biome-ignore lint/suspicious/noAssignInExpressions: sanctioned lazy ref initialization\n (shuffleSeed ?? (sessionIdRef.current ??= randomSessionId()))\n : undefined;\n const slots = useMemo(\n () => flattenSequence(activities, { shuffleEntries, ...(seed !== undefined ? { seed } : {}) }),\n [activities, shuffleEntries, seed],\n );\n\n const [index, setIndex] = useState(0);\n // Outcomes live in a ref, not state: nothing renders from them, and a ref is\n // written SYNCHRONOUSLY. Reading them from a `useState` closure meant two\n // slots completing in the same tick both saw the pre-update array — the\n // second overwrote the first, so one answer vanished and completion never\n // fired. Every slot is mounted at once here, so same-tick completions are\n // reachable (a custom renderer that completes from a mount effect).\n const outcomesRef = useRef<(SequenceItemOutcome | null)[] | null>(null);\n if (outcomesRef.current === null) {\n outcomesRef.current = slots.map(() => null);\n }\n const regionRef = useRef<HTMLDivElement>(null);\n const mountedRef = useRef(false);\n\n // Reset pager position and outcomes when the presented SET changes (B8):\n // without this, results from the previous set leak into the new one and can\n // fire completion with a mixed old/new array. Keyed on the set's CONTENT\n // identity (ordered slot and activity ids), not the array reference — a\n // parent that re-creates a structurally identical array on every render\n // (inline literals, .map() in render) must not wipe in-progress answers.\n // JSON.stringify (not join) so ids containing the separator cannot collide.\n const setKey = JSON.stringify(slots.map((slot) => [slot.slotId, slot.activity.id]));\n const [prevSetKey, setPrevSetKey] = useState(setKey);\n if (prevSetKey !== setKey) {\n setPrevSetKey(setKey);\n setIndex(0);\n outcomesRef.current = slots.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 // One panel per group, spanning the presented positions of its questions.\n const stimulusMounts = useMemo<StimulusMount[]>(() => {\n const mounts = new Map<string, StimulusMount>();\n for (const slot of slots) {\n if (slot.group === undefined) {\n continue;\n }\n const entryKey = entryKeyOf(slot);\n const existing = mounts.get(entryKey);\n if (existing === undefined) {\n mounts.set(entryKey, { entryKey, group: slot.group, first: slot.index, last: slot.index });\n } else {\n existing.last = slot.index;\n }\n }\n return [...mounts.values()];\n }, [slots]);\n\n const total = slots.length;\n const current = slots[index];\n\n const go = (next: number): void => {\n setIndex(next);\n };\n\n /**\n * Records a finished slot and fires completion callbacks when the set is\n * done. A slot that already holds an outcome is FINAL: re-recording it would\n * fire completion a second time for one attempt, so a consumer persisting on\n * `onFinished` would write the same attempt twice.\n */\n const record = (outcome: SequenceItemOutcome): void => {\n const current = outcomesRef.current;\n if (current === null || current[outcome.index] != null) {\n return;\n }\n const next = current.slice();\n next[outcome.index] = outcome;\n // Commit before firing anything: a second record() in the same tick must\n // see this one.\n outcomesRef.current = next;\n\n if (outcome.kind === 'scored') {\n onActivityComplete?.(outcome.result, outcome.index, outcome.slotId);\n }\n\n if (!next.every((item): item is SequenceItemOutcome => item !== null)) {\n return;\n }\n onFinished?.(next);\n\n // `onComplete` predates deferred grading and promises ActivityResult[].\n // Fire it only when every slot really was scored, rather than inventing a\n // result for work nobody has graded.\n const scored: ActivityResult[] = [];\n for (const item of next) {\n if (item.kind !== 'scored') {\n return;\n }\n scored.push(item.result);\n }\n onComplete?.(scored);\n };\n\n if (!current) {\n return <div className=\"lk-seq\" />;\n }\n\n const forwarded = {\n ...(onInteraction ? { onInteraction } : {}),\n renderMode,\n ...(sanitizeHtml ? { sanitizeHtml } : {}),\n ...(theme ? { theme } : {}),\n ...(locale ? { locale } : {}),\n ...(disabled ? { disabled } : {}),\n };\n\n const renderSlot = (slot: SequenceSlot<RenderableActivity>): React.JSX.Element => {\n const activity = slot.activity;\n const { slotId, index: slotIndex } = slot;\n const activityId = activity.id;\n const childProps = {\n onComplete: (result: ActivityResult) =>\n record({ kind: 'scored', index: slotIndex, slotId, activityId, result }),\n onSubmit: (response: LearnerResponse) => {\n onSubmit?.(response, { slotId, index: slotIndex, activityId });\n // Outside `practice` the components do not grade, so a raw response is\n // the ONLY outcome this slot will ever produce — record it, or the set\n // could never complete and `onFinished` would be dead in exam mode.\n // Written responses are excluded because they report something richer\n // through `onSubmitted` a moment later, and the first outcome wins.\n if (renderMode !== 'practice' && activity.type !== 'written-response') {\n record({ kind: 'responded', index: slotIndex, slotId, activityId, response });\n }\n },\n ...forwarded,\n };\n\n const CustomRenderer = renderers?.[activity.type];\n if (CustomRenderer) {\n return <CustomRenderer data={activity} {...childProps} />;\n }\n if (activity.type === 'multiple-choice') {\n return <MultipleChoice data={activity} {...childProps} />;\n }\n if (activity.type === 'fill-in-the-blanks') {\n return <FillInTheBlanks data={activity} {...childProps} />;\n }\n if (activity.type === 'written-response') {\n return (\n <WrittenResponse\n data={activity}\n onSubmitted={(submission) =>\n record({ kind: 'submitted', index: slotIndex, slotId, activityId, submission })\n }\n // Explicit rather than spread: WrittenResponse has no `onComplete`\n // (it never grades), so `childProps` does not fit it — but it must\n // still report the raw response like every other slot.\n onSubmit={childProps.onSubmit}\n {...forwarded}\n />\n );\n }\n return (\n <div className=\"lk-seq-unsupported\" role=\"note\">\n This activity type has no renderer. Supply one through the renderers prop.\n </div>\n );\n };\n\n const currentEntryKey = entryKeyOf(current);\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 {/*\n Each group's stimulus is mounted ONCE and only hidden while the current\n question is outside its group. That — not re-rendering it per slot — is\n what makes the passage the same DOM node throughout, and what lets a\n recording keep its position between questions of one group.\n\n It sits BESIDE the question region, not inside it. Inside, every\n navigation dropped focus onto a container whose first content was the\n whole passage: the region promised \"Question 3 of 5\" and then began\n with the passage, keyboard users tabbed through the stimulus (audio\n controls included) before reaching the question, and the passage was a\n landmark nested inside another landmark. As a sibling it stays a\n landmark the learner can jump back to, while focus lands on the\n question that was actually navigated to.\n */}\n {stimulusMounts.map(({ entryKey, group, first, last }) => (\n <SequencePane\n className=\"lk-seq-stimulus\"\n hidden={entryKey !== currentEntryKey}\n key={`group-${entryKey}`}\n >\n <StimulusPanel\n stimulus={group.stimulus}\n range={{ first: first + 1, last: last + 1 }}\n {...(sanitizeHtml ? { sanitizeHtml } : {})}\n {...(locale ? { locale } : {})}\n />\n </SequencePane>\n ))}\n\n <section\n className=\"lk-seq-question\"\n ref={regionRef}\n tabIndex={-1}\n aria-label={`Question ${index + 1} of ${total}`}\n >\n {/*\n Every slot stays MOUNTED; only the current one is visible. Rendering\n just the current activity unmounted the others, so navigating back to\n an answered question showed it blank and re-answerable — the learner\n silently lost their answer and a second submit fired completion\n again. `hidden` removes the inactive slots from the accessibility\n tree and from tab order, so only the current question is reachable.\n */}\n {slots.map((slot) => {\n // The slot id (authored position) plus the activity id: the same\n // activity may legitimately fill two slots, and each must keep its\n // own answer state; and a different activity in the same slot must\n // remount rather than inherit the previous one's state.\n //\n // `setKey` leads, so a set change remounts every child. Without it a\n // pure REORDER changed `setKey` — clearing the outcomes — while each\n // child kept its key and therefore its `completed` state. Those\n // slots could not be answered again and no outcome would ever refill\n // them, so the sequence could never finish. A reset has to reset\n // both halves or neither. A parent re-creating a structurally\n // identical array leaves `setKey` untouched, so answers still\n // survive that.\n const slotKey = `${setKey}::slot-${slot.slotId}-${slot.activity.id}`;\n return (\n <SequencePane className=\"lk-seq-slot\" hidden={slot.index !== index} key={slotKey}>\n {renderSlot(slot)}\n </SequencePane>\n );\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"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunk4HZ7LQIH_cjs = require('./chunk-4HZ7LQIH.cjs');
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
|
|
7
|
+
var KIND_LABEL = {
|
|
8
|
+
text: "Passage",
|
|
9
|
+
audio: "Recording",
|
|
10
|
+
video: "Video",
|
|
11
|
+
image: "Image",
|
|
12
|
+
mixed: "Material"
|
|
13
|
+
};
|
|
14
|
+
function StimulusPanel({
|
|
15
|
+
stimulus,
|
|
16
|
+
range,
|
|
17
|
+
sanitizeHtml,
|
|
18
|
+
locale
|
|
19
|
+
}) {
|
|
20
|
+
const titleId = react.useId();
|
|
21
|
+
const bodyHtml = react.useMemo(() => {
|
|
22
|
+
if (sanitizeHtml === void 0 || typeof stimulus.bodyHtml !== "string") {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return sanitizeHtml(stimulus.bodyHtml);
|
|
26
|
+
}, [stimulus.bodyHtml, sanitizeHtml]);
|
|
27
|
+
const rangeText = range === void 0 ? null : range.first === range.last ? `Question ${range.first}` : `Questions ${range.first}\u2013${range.last}`;
|
|
28
|
+
const naming = stimulus.title !== void 0 ? { "aria-labelledby": titleId } : { "aria-label": KIND_LABEL[stimulus.kind] };
|
|
29
|
+
const contentLang = stimulus.locale !== void 0 ? { lang: stimulus.locale } : {};
|
|
30
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "lk-stimulus", "data-kind": stimulus.kind, lang: locale, ...naming, children: [
|
|
31
|
+
stimulus.title !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-stimulus-title", id: titleId, ...contentLang, children: stimulus.title }) : null,
|
|
32
|
+
rangeText !== null ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-stimulus-range", children: rangeText }) : null,
|
|
33
|
+
stimulus.media !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(chunk4HZ7LQIH_cjs.ActivityMedia, { media: stimulus.media }) : null,
|
|
34
|
+
bodyHtml !== null ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
35
|
+
"div",
|
|
36
|
+
{
|
|
37
|
+
className: "lk-stimulus-body",
|
|
38
|
+
"data-format": "html",
|
|
39
|
+
...contentLang,
|
|
40
|
+
dangerouslySetInnerHTML: { __html: bodyHtml }
|
|
41
|
+
}
|
|
42
|
+
) : stimulus.body !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lk-stimulus-body", "data-format": "text", ...contentLang, children: stimulus.body }) : null,
|
|
43
|
+
stimulus.attribution !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-stimulus-attribution", ...contentLang, children: /* @__PURE__ */ jsxRuntime.jsx("cite", { children: stimulus.attribution }) }) : null
|
|
44
|
+
] });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
exports.StimulusPanel = StimulusPanel;
|
|
48
|
+
//# sourceMappingURL=chunk-NEWGUDA5.cjs.map
|
|
49
|
+
//# sourceMappingURL=chunk-NEWGUDA5.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/StimulusPanel/StimulusPanel.tsx"],"names":["useId","useMemo","jsxs","jsx","ActivityMedia"],"mappings":";;;;;;AAqBA,IAAM,UAAA,GAA+C;AAAA,EACnD,IAAA,EAAM,SAAA;AAAA,EACN,KAAA,EAAO,WAAA;AAAA,EACP,KAAA,EAAO,OAAA;AAAA,EACP,KAAA,EAAO,OAAA;AAAA,EACP,KAAA,EAAO;AACT,CAAA;AAWO,SAAS,aAAA,CAAc;AAAA,EAC5B,QAAA;AAAA,EACA,KAAA;AAAA,EACA,YAAA;AAAA,EACA;AACF,CAAA,EAA0C;AACxC,EAAA,MAAM,UAAUA,WAAA,EAAM;AAEtB,EAAA,MAAM,QAAA,GAAWC,cAAQ,MAAM;AAC7B,IAAA,IAAI,YAAA,KAAiB,MAAA,IAAa,OAAO,QAAA,CAAS,aAAa,QAAA,EAAU;AACvE,MAAA,OAAO,IAAA;AAAA,IACT;AACA,IAAA,OAAO,YAAA,CAAa,SAAS,QAAQ,CAAA;AAAA,EACvC,CAAA,EAAG,CAAC,QAAA,CAAS,QAAA,EAAU,YAAY,CAAC,CAAA;AAEpC,EAAA,MAAM,YACJ,KAAA,KAAU,MAAA,GACN,IAAA,GACA,KAAA,CAAM,UAAU,KAAA,CAAM,IAAA,GACpB,CAAA,SAAA,EAAY,KAAA,CAAM,KAAK,CAAA,CAAA,GACvB,CAAA,UAAA,EAAa,MAAM,KAAK,CAAA,MAAA,EAAI,MAAM,IAAI,CAAA,CAAA;AAE9C,EAAA,MAAM,MAAA,GACJ,QAAA,CAAS,KAAA,KAAU,MAAA,GACf,EAAE,iBAAA,EAAmB,OAAA,EAAQ,GAC7B,EAAE,YAAA,EAAc,UAAA,CAAW,QAAA,CAAS,IAAI,CAAA,EAAE;AAShD,EAAA,MAAM,WAAA,GAAc,SAAS,MAAA,KAAW,MAAA,GAAY,EAAE,IAAA,EAAM,QAAA,CAAS,MAAA,EAAO,GAAI,EAAC;AAEjF,EAAA,uBACEC,eAAA,CAAC,SAAA,EAAA,EAAQ,SAAA,EAAU,aAAA,EAAc,WAAA,EAAW,SAAS,IAAA,EAAM,IAAA,EAAM,MAAA,EAAS,GAAG,MAAA,EAC1E,QAAA,EAAA;AAAA,IAAA,QAAA,CAAS,KAAA,KAAU,MAAA,mBAClBC,cAAA,CAAC,GAAA,EAAA,EAAE,SAAA,EAAU,mBAAA,EAAoB,EAAA,EAAI,OAAA,EAAU,GAAG,WAAA,EAC/C,QAAA,EAAA,QAAA,CAAS,KAAA,EACZ,CAAA,GACE,IAAA;AAAA,IACH,cAAc,IAAA,mBAAOA,cAAA,CAAC,OAAE,SAAA,EAAU,mBAAA,EAAqB,qBAAU,CAAA,GAAO,IAAA;AAAA,IACxE,QAAA,CAAS,UAAU,MAAA,mBAAYA,cAAA,CAACC,mCAAc,KAAA,EAAO,QAAA,CAAS,OAAO,CAAA,GAAK,IAAA;AAAA,IAC1E,aAAa,IAAA,mBACZD,cAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,SAAA,EAAU,kBAAA;AAAA,QACV,aAAA,EAAY,MAAA;AAAA,QACX,GAAG,WAAA;AAAA,QAEJ,uBAAA,EAAyB,EAAE,MAAA,EAAQ,QAAA;AAAS;AAAA,KAC9C,GACE,QAAA,CAAS,IAAA,KAAS,MAAA,kCACnB,KAAA,EAAA,EAAI,SAAA,EAAU,kBAAA,EAAmB,aAAA,EAAY,MAAA,EAAQ,GAAG,WAAA,EACtD,QAAA,EAAA,QAAA,CAAS,MACZ,CAAA,GACE,IAAA;AAAA,IACH,QAAA,CAAS,WAAA,KAAgB,MAAA,mBACxBA,cAAA,CAAC,OAAE,SAAA,EAAU,yBAAA,EAA2B,GAAG,WAAA,EACzC,QAAA,kBAAAA,cAAA,CAAC,MAAA,EAAA,EAAM,QAAA,EAAA,QAAA,CAAS,WAAA,EAAY,GAC9B,CAAA,GACE;AAAA,GAAA,EACN,CAAA;AAEJ","file":"chunk-NEWGUDA5.cjs","sourcesContent":["'use client';\n\nimport type { Stimulus } from '@intellectif/lk-core';\nimport { useId, useMemo } from 'react';\nimport { ActivityMedia } from '../shared/ActivityMedia.js';\nimport type { HtmlSanitizer } from '../types.js';\n\nexport interface StimulusPanelProps {\n stimulus: Stimulus;\n /**\n * 1-based presented positions of the first and last question this material\n * serves, rendered as \"Questions 3–8\" so a learner knows how far the\n * passage carries.\n */\n range?: { first: number; last: number };\n /** Renders `stimulus.bodyHtml` when provided. See `HtmlSanitizer`. */\n sanitizeHtml?: HtmlSanitizer;\n locale?: string;\n}\n\n/** Accessible name of the region when the stimulus has no title of its own. */\nconst KIND_LABEL: Record<Stimulus['kind'], string> = {\n text: 'Passage',\n audio: 'Recording',\n video: 'Video',\n image: 'Image',\n mixed: 'Material',\n};\n\n/**\n * Presents a shared stimulus — the passage, recording or image an item group's\n * questions refer to. A landmark region named by its title (or by its kind),\n * so a screen-reader user can jump back to the material from any question.\n *\n * Rich text follows the SDK-wide rule: `bodyHtml` is rendered only through a\n * caller-supplied sanitiser, and the plain `body` is the fallback — which the\n * schema guarantees exists whenever `bodyHtml` does.\n */\nexport function StimulusPanel({\n stimulus,\n range,\n sanitizeHtml,\n locale,\n}: StimulusPanelProps): React.JSX.Element {\n const titleId = useId();\n\n const bodyHtml = useMemo(() => {\n if (sanitizeHtml === undefined || typeof stimulus.bodyHtml !== 'string') {\n return null;\n }\n return sanitizeHtml(stimulus.bodyHtml);\n }, [stimulus.bodyHtml, sanitizeHtml]);\n\n const rangeText =\n range === undefined\n ? null\n : range.first === range.last\n ? `Question ${range.first}`\n : `Questions ${range.first}–${range.last}`;\n\n const naming =\n stimulus.title !== undefined\n ? { 'aria-labelledby': titleId }\n : { 'aria-label': KIND_LABEL[stimulus.kind] };\n\n // The AUTHORED parts carry the stimulus language; the SDK's own chrome does\n // not. Declaring `stimulus.locale` on the whole region made a screen reader\n // read \"Recording\" and \"Questions 3–5\" — English strings this package ships\n // — in the passage's voice, so a Spanish listening panel announced its own\n // name and the only orientation text in it with Spanish phonetics\n // (WCAG 3.1.2 Language of Parts). Scope the declaration to the content it\n // actually describes.\n const contentLang = stimulus.locale !== undefined ? { lang: stimulus.locale } : {};\n\n return (\n <section className=\"lk-stimulus\" data-kind={stimulus.kind} lang={locale} {...naming}>\n {stimulus.title !== undefined ? (\n <p className=\"lk-stimulus-title\" id={titleId} {...contentLang}>\n {stimulus.title}\n </p>\n ) : null}\n {rangeText !== null ? <p className=\"lk-stimulus-range\">{rangeText}</p> : null}\n {stimulus.media !== undefined ? <ActivityMedia media={stimulus.media} /> : null}\n {bodyHtml !== null ? (\n <div\n className=\"lk-stimulus-body\"\n data-format=\"html\"\n {...contentLang}\n // biome-ignore lint/security/noDangerouslySetInnerHtml: rendered only through the caller-supplied HtmlSanitizer — the SDK never injects unsanitised markup\n dangerouslySetInnerHTML={{ __html: bodyHtml }}\n />\n ) : stimulus.body !== undefined ? (\n <div className=\"lk-stimulus-body\" data-format=\"text\" {...contentLang}>\n {stimulus.body}\n </div>\n ) : null}\n {stimulus.attribution !== undefined ? (\n <p className=\"lk-stimulus-attribution\" {...contentLang}>\n <cite>{stimulus.attribution}</cite>\n </p>\n ) : null}\n </section>\n );\n}\n"]}
|
|
@@ -47,47 +47,15 @@ function randomSessionId() {
|
|
|
47
47
|
}
|
|
48
48
|
return `s-${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
|
|
49
49
|
}
|
|
50
|
-
function ActivityMedia({ media }) {
|
|
51
|
-
const { type, url, alt, captionsUrl } = media;
|
|
52
|
-
if (type === "image") {
|
|
53
|
-
return /* @__PURE__ */ jsxRuntime.jsx("figure", { className: "lk-media", children: /* @__PURE__ */ jsxRuntime.jsx("img", { className: "lk-media-el", src: url, alt: alt ?? "" }) });
|
|
54
|
-
}
|
|
55
|
-
if (type === "audio") {
|
|
56
|
-
return /* @__PURE__ */ jsxRuntime.jsx("figure", { className: "lk-media", children: /* @__PURE__ */ jsxRuntime.jsxs("audio", { className: "lk-media-el", controls: true, "aria-label": alt || void 0, children: [
|
|
57
|
-
/* @__PURE__ */ jsxRuntime.jsx("source", { src: url }),
|
|
58
|
-
captionsUrl ? /* @__PURE__ */ jsxRuntime.jsx("track", { kind: "captions", src: captionsUrl, default: true }) : null
|
|
59
|
-
] }) });
|
|
60
|
-
}
|
|
61
|
-
if (type === "embed") {
|
|
62
|
-
return /* @__PURE__ */ jsxRuntime.jsx("figure", { className: "lk-media", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lk-media-embed", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
63
|
-
"iframe",
|
|
64
|
-
{
|
|
65
|
-
className: "lk-media-el",
|
|
66
|
-
src: url,
|
|
67
|
-
title: alt ?? "Embedded media",
|
|
68
|
-
loading: "lazy",
|
|
69
|
-
referrerPolicy: "strict-origin-when-cross-origin",
|
|
70
|
-
sandbox: "allow-scripts allow-same-origin allow-presentation",
|
|
71
|
-
allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",
|
|
72
|
-
allowFullScreen: true
|
|
73
|
-
}
|
|
74
|
-
) }) });
|
|
75
|
-
}
|
|
76
|
-
return /* @__PURE__ */ jsxRuntime.jsx("figure", { className: "lk-media", children: /* @__PURE__ */ jsxRuntime.jsxs("video", { className: "lk-media-el", controls: true, "aria-label": alt || void 0, children: [
|
|
77
|
-
/* @__PURE__ */ jsxRuntime.jsx("source", { src: url }),
|
|
78
|
-
captionsUrl ? /* @__PURE__ */ jsxRuntime.jsx("track", { kind: "captions", src: captionsUrl, default: true }) : null
|
|
79
|
-
] }) });
|
|
80
|
-
}
|
|
81
50
|
function FeedbackRegion({ id, className, children }) {
|
|
82
51
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { id, className, "aria-live": "polite", "aria-atomic": "true", children });
|
|
83
52
|
}
|
|
84
53
|
|
|
85
54
|
exports.ANONYMOUS_ACTOR = ANONYMOUS_ACTOR;
|
|
86
55
|
exports.ActivityErrorBoundary = ActivityErrorBoundary;
|
|
87
|
-
exports.ActivityMedia = ActivityMedia;
|
|
88
56
|
exports.FeedbackRegion = FeedbackRegion;
|
|
89
57
|
exports.isDevelopment = isDevelopment;
|
|
90
58
|
exports.objectIdFor = objectIdFor;
|
|
91
59
|
exports.randomSessionId = randomSessionId;
|
|
92
|
-
//# sourceMappingURL=chunk-
|
|
93
|
-
//# sourceMappingURL=chunk-
|
|
60
|
+
//# sourceMappingURL=chunk-PGEG7UZJ.cjs.map
|
|
61
|
+
//# sourceMappingURL=chunk-PGEG7UZJ.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/ActivityErrorBoundary.tsx","../src/components/_internal.ts","../src/components/shared/FeedbackRegion.tsx"],"names":["Component","jsxs","jsx"],"mappings":";;;;;;AAIA,SAAS,YAAA,GAAwB;AAC/B,EAAA,MAAM,CAAA,GAAI,UAAA;AACV,EAAA,OAAO,CAAA,CAAE,OAAA,EAAS,GAAA,EAAK,QAAA,KAAa,YAAA;AACtC;AAkBO,IAAM,qBAAA,GAAN,cAAoCA,eAAA,CAGzC;AAAA,EACA,KAAA,GAAoC,EAAE,KAAA,EAAO,IAAA,EAAK;AAAA,EAElD,OAAO,yBAAyB,KAAA,EAA0C;AACxE,IAAA,OAAO,EAAE,KAAA,EAAM;AAAA,EACjB;AAAA,EAEA,MAAA,GAAoB;AAClB,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,IAAA,CAAK,KAAA;AACvB,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAA,OAAO,KAAK,KAAA,CAAM,QAAA;AAAA,IACpB;AAEA,IAAA,IAAI,CAAC,cAAa,EAAG;AACnB,MAAA,uBACEC,eAAA,CAAC,KAAA,EAAA,EAAI,IAAA,EAAK,OAAA,EACR,QAAA,EAAA;AAAA,wBAAAC,cAAA,CAAC,YAAO,QAAA,EAAA,2BAAA,EAAyB,CAAA;AAAA,wBACjCA,cAAA,CAAC,KAAA,EAAA,EAAK,QAAA,EAAA,KAAA,CAAM,KAAA,IAAS,MAAM,OAAA,EAAQ;AAAA,OAAA,EACrC,CAAA;AAAA,IAEJ;AAEA,IAAA,uBACEA,cAAA,CAAC,KAAA,EAAA,EAAI,IAAA,EAAK,OAAA,EACP,QAAA,EAAA,IAAA,CAAK,KAAA,CAAM,aAAA,GACR,CAAA,CAAA,EAAI,IAAA,CAAK,KAAA,CAAM,aAAa,CAAA,yBAAA,CAAA,GAC5B,uCAAA,EACN,CAAA;AAAA,EAEJ;AACF;;;ACvDO,SAAS,aAAA,GAAyB;AACvC,EAAA,MAAM,CAAA,GAAI,UAAA;AACV,EAAA,OAAO,CAAA,CAAE,OAAA,EAAS,GAAA,EAAK,QAAA,KAAa,YAAA;AACtC;AAOO,IAAM,eAAA,GAA6B;AAAA,EACxC,UAAA,EAAY,OAAA;AAAA,EACZ,OAAA,EAAS,EAAE,QAAA,EAAU,6CAAA,EAA+C,MAAM,WAAA;AAC5E;AAGO,SAAS,YAAY,EAAA,EAAoB;AAC9C,EAAA,OAAO,CAAA,0BAAA,EAA6B,kBAAA,CAAmB,EAAE,CAAC,CAAA,CAAA;AAC5D;AAQO,SAAS,eAAA,GAA0B;AACxC,EAAA,MAAM,IAAK,UAAA,CAA0D,MAAA;AACrE,EAAA,IAAI,GAAG,UAAA,EAAY;AACjB,IAAA,OAAO,EAAE,UAAA,EAAW;AAAA,EACtB;AACA,EAAA,OAAO,KAAK,IAAA,CAAK,MAAA,GAAS,QAAA,CAAS,EAAE,EAAE,KAAA,CAAM,CAAC,CAAC,CAAA,EAAG,IAAA,CAAK,QAAO,CAAE,QAAA,CAAS,EAAE,CAAA,CAAE,KAAA,CAAM,CAAC,CAAC,CAAA,CAAA;AACvF;ACrBO,SAAS,cAAA,CAAe,EAAE,EAAA,EAAI,SAAA,EAAW,UAAS,EAAwB;AAC/E,EAAA,uBACEA,eAAC,KAAA,EAAA,EAAI,EAAA,EAAQ,WAAsB,WAAA,EAAU,QAAA,EAAS,aAAA,EAAY,MAAA,EAC/D,QAAA,EACH,CAAA;AAEJ","file":"chunk-PGEG7UZJ.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 { 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"]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ActivityMedia } from './chunk-GB5URWM4.js';
|
|
2
|
+
import { useId, useMemo } from 'react';
|
|
3
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
var KIND_LABEL = {
|
|
6
|
+
text: "Passage",
|
|
7
|
+
audio: "Recording",
|
|
8
|
+
video: "Video",
|
|
9
|
+
image: "Image",
|
|
10
|
+
mixed: "Material"
|
|
11
|
+
};
|
|
12
|
+
function StimulusPanel({
|
|
13
|
+
stimulus,
|
|
14
|
+
range,
|
|
15
|
+
sanitizeHtml,
|
|
16
|
+
locale
|
|
17
|
+
}) {
|
|
18
|
+
const titleId = useId();
|
|
19
|
+
const bodyHtml = useMemo(() => {
|
|
20
|
+
if (sanitizeHtml === void 0 || typeof stimulus.bodyHtml !== "string") {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return sanitizeHtml(stimulus.bodyHtml);
|
|
24
|
+
}, [stimulus.bodyHtml, sanitizeHtml]);
|
|
25
|
+
const rangeText = range === void 0 ? null : range.first === range.last ? `Question ${range.first}` : `Questions ${range.first}\u2013${range.last}`;
|
|
26
|
+
const naming = stimulus.title !== void 0 ? { "aria-labelledby": titleId } : { "aria-label": KIND_LABEL[stimulus.kind] };
|
|
27
|
+
const contentLang = stimulus.locale !== void 0 ? { lang: stimulus.locale } : {};
|
|
28
|
+
return /* @__PURE__ */ jsxs("section", { className: "lk-stimulus", "data-kind": stimulus.kind, lang: locale, ...naming, children: [
|
|
29
|
+
stimulus.title !== void 0 ? /* @__PURE__ */ jsx("p", { className: "lk-stimulus-title", id: titleId, ...contentLang, children: stimulus.title }) : null,
|
|
30
|
+
rangeText !== null ? /* @__PURE__ */ jsx("p", { className: "lk-stimulus-range", children: rangeText }) : null,
|
|
31
|
+
stimulus.media !== void 0 ? /* @__PURE__ */ jsx(ActivityMedia, { media: stimulus.media }) : null,
|
|
32
|
+
bodyHtml !== null ? /* @__PURE__ */ jsx(
|
|
33
|
+
"div",
|
|
34
|
+
{
|
|
35
|
+
className: "lk-stimulus-body",
|
|
36
|
+
"data-format": "html",
|
|
37
|
+
...contentLang,
|
|
38
|
+
dangerouslySetInnerHTML: { __html: bodyHtml }
|
|
39
|
+
}
|
|
40
|
+
) : stimulus.body !== void 0 ? /* @__PURE__ */ jsx("div", { className: "lk-stimulus-body", "data-format": "text", ...contentLang, children: stimulus.body }) : null,
|
|
41
|
+
stimulus.attribution !== void 0 ? /* @__PURE__ */ jsx("p", { className: "lk-stimulus-attribution", ...contentLang, children: /* @__PURE__ */ jsx("cite", { children: stimulus.attribution }) }) : null
|
|
42
|
+
] });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { StimulusPanel };
|
|
46
|
+
//# sourceMappingURL=chunk-UQ3BBEIT.js.map
|
|
47
|
+
//# sourceMappingURL=chunk-UQ3BBEIT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/StimulusPanel/StimulusPanel.tsx"],"names":[],"mappings":";;;;AAqBA,IAAM,UAAA,GAA+C;AAAA,EACnD,IAAA,EAAM,SAAA;AAAA,EACN,KAAA,EAAO,WAAA;AAAA,EACP,KAAA,EAAO,OAAA;AAAA,EACP,KAAA,EAAO,OAAA;AAAA,EACP,KAAA,EAAO;AACT,CAAA;AAWO,SAAS,aAAA,CAAc;AAAA,EAC5B,QAAA;AAAA,EACA,KAAA;AAAA,EACA,YAAA;AAAA,EACA;AACF,CAAA,EAA0C;AACxC,EAAA,MAAM,UAAU,KAAA,EAAM;AAEtB,EAAA,MAAM,QAAA,GAAW,QAAQ,MAAM;AAC7B,IAAA,IAAI,YAAA,KAAiB,MAAA,IAAa,OAAO,QAAA,CAAS,aAAa,QAAA,EAAU;AACvE,MAAA,OAAO,IAAA;AAAA,IACT;AACA,IAAA,OAAO,YAAA,CAAa,SAAS,QAAQ,CAAA;AAAA,EACvC,CAAA,EAAG,CAAC,QAAA,CAAS,QAAA,EAAU,YAAY,CAAC,CAAA;AAEpC,EAAA,MAAM,YACJ,KAAA,KAAU,MAAA,GACN,IAAA,GACA,KAAA,CAAM,UAAU,KAAA,CAAM,IAAA,GACpB,CAAA,SAAA,EAAY,KAAA,CAAM,KAAK,CAAA,CAAA,GACvB,CAAA,UAAA,EAAa,MAAM,KAAK,CAAA,MAAA,EAAI,MAAM,IAAI,CAAA,CAAA;AAE9C,EAAA,MAAM,MAAA,GACJ,QAAA,CAAS,KAAA,KAAU,MAAA,GACf,EAAE,iBAAA,EAAmB,OAAA,EAAQ,GAC7B,EAAE,YAAA,EAAc,UAAA,CAAW,QAAA,CAAS,IAAI,CAAA,EAAE;AAShD,EAAA,MAAM,WAAA,GAAc,SAAS,MAAA,KAAW,MAAA,GAAY,EAAE,IAAA,EAAM,QAAA,CAAS,MAAA,EAAO,GAAI,EAAC;AAEjF,EAAA,uBACE,IAAA,CAAC,SAAA,EAAA,EAAQ,SAAA,EAAU,aAAA,EAAc,WAAA,EAAW,SAAS,IAAA,EAAM,IAAA,EAAM,MAAA,EAAS,GAAG,MAAA,EAC1E,QAAA,EAAA;AAAA,IAAA,QAAA,CAAS,KAAA,KAAU,MAAA,mBAClB,GAAA,CAAC,GAAA,EAAA,EAAE,SAAA,EAAU,mBAAA,EAAoB,EAAA,EAAI,OAAA,EAAU,GAAG,WAAA,EAC/C,QAAA,EAAA,QAAA,CAAS,KAAA,EACZ,CAAA,GACE,IAAA;AAAA,IACH,cAAc,IAAA,mBAAO,GAAA,CAAC,OAAE,SAAA,EAAU,mBAAA,EAAqB,qBAAU,CAAA,GAAO,IAAA;AAAA,IACxE,QAAA,CAAS,UAAU,MAAA,mBAAY,GAAA,CAAC,iBAAc,KAAA,EAAO,QAAA,CAAS,OAAO,CAAA,GAAK,IAAA;AAAA,IAC1E,aAAa,IAAA,mBACZ,GAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,SAAA,EAAU,kBAAA;AAAA,QACV,aAAA,EAAY,MAAA;AAAA,QACX,GAAG,WAAA;AAAA,QAEJ,uBAAA,EAAyB,EAAE,MAAA,EAAQ,QAAA;AAAS;AAAA,KAC9C,GACE,QAAA,CAAS,IAAA,KAAS,MAAA,uBACnB,KAAA,EAAA,EAAI,SAAA,EAAU,kBAAA,EAAmB,aAAA,EAAY,MAAA,EAAQ,GAAG,WAAA,EACtD,QAAA,EAAA,QAAA,CAAS,MACZ,CAAA,GACE,IAAA;AAAA,IACH,QAAA,CAAS,WAAA,KAAgB,MAAA,mBACxB,GAAA,CAAC,OAAE,SAAA,EAAU,yBAAA,EAA2B,GAAG,WAAA,EACzC,QAAA,kBAAA,GAAA,CAAC,MAAA,EAAA,EAAM,QAAA,EAAA,QAAA,CAAS,WAAA,EAAY,GAC9B,CAAA,GACE;AAAA,GAAA,EACN,CAAA;AAEJ","file":"chunk-UQ3BBEIT.js","sourcesContent":["'use client';\n\nimport type { Stimulus } from '@intellectif/lk-core';\nimport { useId, useMemo } from 'react';\nimport { ActivityMedia } from '../shared/ActivityMedia.js';\nimport type { HtmlSanitizer } from '../types.js';\n\nexport interface StimulusPanelProps {\n stimulus: Stimulus;\n /**\n * 1-based presented positions of the first and last question this material\n * serves, rendered as \"Questions 3–8\" so a learner knows how far the\n * passage carries.\n */\n range?: { first: number; last: number };\n /** Renders `stimulus.bodyHtml` when provided. See `HtmlSanitizer`. */\n sanitizeHtml?: HtmlSanitizer;\n locale?: string;\n}\n\n/** Accessible name of the region when the stimulus has no title of its own. */\nconst KIND_LABEL: Record<Stimulus['kind'], string> = {\n text: 'Passage',\n audio: 'Recording',\n video: 'Video',\n image: 'Image',\n mixed: 'Material',\n};\n\n/**\n * Presents a shared stimulus — the passage, recording or image an item group's\n * questions refer to. A landmark region named by its title (or by its kind),\n * so a screen-reader user can jump back to the material from any question.\n *\n * Rich text follows the SDK-wide rule: `bodyHtml` is rendered only through a\n * caller-supplied sanitiser, and the plain `body` is the fallback — which the\n * schema guarantees exists whenever `bodyHtml` does.\n */\nexport function StimulusPanel({\n stimulus,\n range,\n sanitizeHtml,\n locale,\n}: StimulusPanelProps): React.JSX.Element {\n const titleId = useId();\n\n const bodyHtml = useMemo(() => {\n if (sanitizeHtml === undefined || typeof stimulus.bodyHtml !== 'string') {\n return null;\n }\n return sanitizeHtml(stimulus.bodyHtml);\n }, [stimulus.bodyHtml, sanitizeHtml]);\n\n const rangeText =\n range === undefined\n ? null\n : range.first === range.last\n ? `Question ${range.first}`\n : `Questions ${range.first}–${range.last}`;\n\n const naming =\n stimulus.title !== undefined\n ? { 'aria-labelledby': titleId }\n : { 'aria-label': KIND_LABEL[stimulus.kind] };\n\n // The AUTHORED parts carry the stimulus language; the SDK's own chrome does\n // not. Declaring `stimulus.locale` on the whole region made a screen reader\n // read \"Recording\" and \"Questions 3–5\" — English strings this package ships\n // — in the passage's voice, so a Spanish listening panel announced its own\n // name and the only orientation text in it with Spanish phonetics\n // (WCAG 3.1.2 Language of Parts). Scope the declaration to the content it\n // actually describes.\n const contentLang = stimulus.locale !== undefined ? { lang: stimulus.locale } : {};\n\n return (\n <section className=\"lk-stimulus\" data-kind={stimulus.kind} lang={locale} {...naming}>\n {stimulus.title !== undefined ? (\n <p className=\"lk-stimulus-title\" id={titleId} {...contentLang}>\n {stimulus.title}\n </p>\n ) : null}\n {rangeText !== null ? <p className=\"lk-stimulus-range\">{rangeText}</p> : null}\n {stimulus.media !== undefined ? <ActivityMedia media={stimulus.media} /> : null}\n {bodyHtml !== null ? (\n <div\n className=\"lk-stimulus-body\"\n data-format=\"html\"\n {...contentLang}\n // biome-ignore lint/security/noDangerouslySetInnerHtml: rendered only through the caller-supplied HtmlSanitizer — the SDK never injects unsanitised markup\n dangerouslySetInnerHTML={{ __html: bodyHtml }}\n />\n ) : stimulus.body !== undefined ? (\n <div className=\"lk-stimulus-body\" data-format=\"text\" {...contentLang}>\n {stimulus.body}\n </div>\n ) : null}\n {stimulus.attribution !== undefined ? (\n <p className=\"lk-stimulus-attribution\" {...contentLang}>\n <cite>{stimulus.attribution}</cite>\n </p>\n ) : null}\n </section>\n );\n}\n"]}
|
|
@@ -1,29 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkPGEG7UZJ_cjs = require('./chunk-PGEG7UZJ.cjs');
|
|
4
|
+
var chunk4HZ7LQIH_cjs = require('./chunk-4HZ7LQIH.cjs');
|
|
4
5
|
var chunkHD2AIQRD_cjs = require('./chunk-HD2AIQRD.cjs');
|
|
5
6
|
var lkCore = require('@intellectif/lk-core');
|
|
6
7
|
var react = require('react');
|
|
7
8
|
var jsxRuntime = require('react/jsx-runtime');
|
|
8
9
|
|
|
9
|
-
function hashSeed(input) {
|
|
10
|
-
let h = 2166136261 >>> 0;
|
|
11
|
-
for (let i = 0; i < input.length; i += 1) {
|
|
12
|
-
h ^= input.charCodeAt(i);
|
|
13
|
-
h = Math.imul(h, 16777619) >>> 0;
|
|
14
|
-
}
|
|
15
|
-
return h;
|
|
16
|
-
}
|
|
17
|
-
function seededShuffle(items, seed) {
|
|
18
|
-
const out = [...items];
|
|
19
|
-
let s = seed >>> 0 || 1;
|
|
20
|
-
for (let i = out.length - 1; i > 0; i -= 1) {
|
|
21
|
-
s = Math.imul(s, 1664525) + 1013904223 >>> 0;
|
|
22
|
-
const j = s % (i + 1);
|
|
23
|
-
[out[i], out[j]] = [out[j], out[i]];
|
|
24
|
-
}
|
|
25
|
-
return out;
|
|
26
|
-
}
|
|
27
10
|
function selectionOf(response) {
|
|
28
11
|
return response?.type === "multiple-choice" ? response.selectedOptionIds : [];
|
|
29
12
|
}
|
|
@@ -69,7 +52,7 @@ function MultipleChoice({
|
|
|
69
52
|
shuffleSeed
|
|
70
53
|
}) {
|
|
71
54
|
const devError = react.useMemo(() => {
|
|
72
|
-
if (!
|
|
55
|
+
if (!chunkPGEG7UZJ_cjs.isDevelopment()) {
|
|
73
56
|
return null;
|
|
74
57
|
}
|
|
75
58
|
if (data.redacted === true) {
|
|
@@ -98,8 +81,8 @@ function MultipleChoice({
|
|
|
98
81
|
if (!data.shuffle) {
|
|
99
82
|
return [...data.options];
|
|
100
83
|
}
|
|
101
|
-
const seedSource = shuffleSeed ?? (sessionIdRef.current ??=
|
|
102
|
-
return seededShuffle(data.options,
|
|
84
|
+
const seedSource = shuffleSeed ?? (sessionIdRef.current ??= chunkPGEG7UZJ_cjs.randomSessionId());
|
|
85
|
+
return lkCore.seededShuffle(data.options, `${seedSource}:${data.id}`);
|
|
103
86
|
}, [data, shuffleSeed]);
|
|
104
87
|
const reviewDetails = react.useMemo(() => {
|
|
105
88
|
if (renderMode !== "review" || outcome === void 0 || outcome.status !== "scored") {
|
|
@@ -172,15 +155,14 @@ function MultipleChoice({
|
|
|
172
155
|
complete();
|
|
173
156
|
const timeSpent = getTimeSpent();
|
|
174
157
|
const xapiStatement = lkCore.xAPIBuilder.buildAnsweredStatement({
|
|
175
|
-
actor:
|
|
158
|
+
actor: chunkPGEG7UZJ_cjs.ANONYMOUS_ACTOR,
|
|
176
159
|
object: {
|
|
177
|
-
id:
|
|
160
|
+
id: chunkPGEG7UZJ_cjs.objectIdFor(data.id),
|
|
178
161
|
name: { [data.locale ?? "en-US"]: data.title },
|
|
179
|
-
type
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
],
|
|
162
|
+
// Activity-type IRI, interaction type and correct-responses pattern
|
|
163
|
+
// come from the REGISTERED descriptor, so a consumer-registered type
|
|
164
|
+
// gets correct interop without touching this component.
|
|
165
|
+
...lkCore.xapiDefinitionFor(data),
|
|
184
166
|
choices: data.options.map((option) => ({
|
|
185
167
|
id: option.id,
|
|
186
168
|
description: { [data.locale ?? "en-US"]: option.text }
|
|
@@ -235,7 +217,7 @@ function MultipleChoice({
|
|
|
235
217
|
] }, option.id);
|
|
236
218
|
});
|
|
237
219
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lk-mc", lang: locale, style: theme, children: [
|
|
238
|
-
data.media ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
220
|
+
data.media ? /* @__PURE__ */ jsxRuntime.jsx(chunk4HZ7LQIH_cjs.ActivityMedia, { media: data.media }) : null,
|
|
239
221
|
/* @__PURE__ */ jsxRuntime.jsx("form", { onSubmit: handleSubmit, children: /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { disabled: inactive, children: [
|
|
240
222
|
questionHtml === null ? /* @__PURE__ */ jsxRuntime.jsx("legend", { id: questionId, children: data.question }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
241
223
|
"legend",
|
|
@@ -247,13 +229,13 @@ function MultipleChoice({
|
|
|
247
229
|
isSingle ? /* @__PURE__ */ jsxRuntime.jsx("div", { role: "radiogroup", "aria-labelledby": questionId, children: optionList }) : /* @__PURE__ */ jsxRuntime.jsx("div", { children: optionList }),
|
|
248
230
|
isReview ? null : /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", disabled: inactive, children: "Submit" })
|
|
249
231
|
] }) }),
|
|
250
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
232
|
+
/* @__PURE__ */ jsxRuntime.jsx(chunkPGEG7UZJ_cjs.FeedbackRegion, { id: `${data.id}-feedback`, children: isReview ? reviewAnnouncement(outcome) : summary })
|
|
251
233
|
] });
|
|
252
234
|
}
|
|
253
235
|
function MultipleChoice2(props) {
|
|
254
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
236
|
+
return /* @__PURE__ */ jsxRuntime.jsx(chunkPGEG7UZJ_cjs.ActivityErrorBoundary, { activityTitle: props.data?.title, children: /* @__PURE__ */ jsxRuntime.jsx(MultipleChoice, { ...props }) });
|
|
255
237
|
}
|
|
256
238
|
|
|
257
239
|
exports.MultipleChoice = MultipleChoice2;
|
|
258
|
-
//# sourceMappingURL=chunk-
|
|
259
|
-
//# sourceMappingURL=chunk-
|
|
240
|
+
//# sourceMappingURL=chunk-WCVD5LVG.cjs.map
|
|
241
|
+
//# sourceMappingURL=chunk-WCVD5LVG.cjs.map
|