@intellectif/lk-react 2.0.0 → 3.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.
Files changed (58) hide show
  1. package/dist/WrittenResponse-B50EjVR0.d.cts +87 -0
  2. package/dist/WrittenResponse-BHhP936i.d.ts +87 -0
  3. package/dist/{chunk-32WWQOG3.js → chunk-3X7MYAFT.js} +99 -26
  4. package/dist/chunk-3X7MYAFT.js.map +1 -0
  5. package/dist/chunk-7MW7HGOA.cjs +258 -0
  6. package/dist/chunk-7MW7HGOA.cjs.map +1 -0
  7. package/dist/chunk-AEQQ7VPF.js +256 -0
  8. package/dist/chunk-AEQQ7VPF.js.map +1 -0
  9. package/dist/chunk-BGWXN44S.js +152 -0
  10. package/dist/chunk-BGWXN44S.js.map +1 -0
  11. package/dist/chunk-HO3AC3WQ.cjs +154 -0
  12. package/dist/chunk-HO3AC3WQ.cjs.map +1 -0
  13. package/dist/{chunk-73XCWYKD.cjs → chunk-UXO6KYSZ.cjs} +97 -24
  14. package/dist/chunk-UXO6KYSZ.cjs.map +1 -0
  15. package/dist/{chunk-MKH5THDR.cjs → chunk-W6BH2YMZ.cjs} +119 -36
  16. package/dist/chunk-W6BH2YMZ.cjs.map +1 -0
  17. package/dist/{chunk-G5ZEWJRA.js → chunk-ZVFWJTT4.js} +119 -36
  18. package/dist/chunk-ZVFWJTT4.js.map +1 -0
  19. package/dist/components/ActivitySequence.cjs +5 -4
  20. package/dist/components/ActivitySequence.d.cts +52 -4
  21. package/dist/components/ActivitySequence.d.ts +52 -4
  22. package/dist/components/ActivitySequence.js +4 -3
  23. package/dist/components/FillInTheBlanks.cjs +2 -2
  24. package/dist/components/FillInTheBlanks.d.cts +7 -1
  25. package/dist/components/FillInTheBlanks.d.ts +7 -1
  26. package/dist/components/FillInTheBlanks.js +1 -1
  27. package/dist/components/MultipleChoice.cjs +2 -2
  28. package/dist/components/MultipleChoice.d.cts +14 -3
  29. package/dist/components/MultipleChoice.d.ts +14 -3
  30. package/dist/components/MultipleChoice.js +1 -1
  31. package/dist/components/WrittenResponse.cjs +2 -2
  32. package/dist/components/WrittenResponse.d.cts +5 -38
  33. package/dist/components/WrittenResponse.d.ts +5 -38
  34. package/dist/components/WrittenResponse.js +1 -1
  35. package/dist/index.cjs +13 -9
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.d.cts +6 -5
  38. package/dist/index.d.ts +6 -5
  39. package/dist/index.js +11 -4
  40. package/dist/index.js.map +1 -1
  41. package/dist/theme/skin.css +75 -0
  42. package/dist/types-CNmjqD6M.d.cts +108 -0
  43. package/dist/types-CNmjqD6M.d.ts +108 -0
  44. package/package.json +70 -28
  45. package/dist/chunk-32WWQOG3.js.map +0 -1
  46. package/dist/chunk-73XCWYKD.cjs.map +0 -1
  47. package/dist/chunk-G5ZEWJRA.js.map +0 -1
  48. package/dist/chunk-KJNQLAB5.cjs +0 -158
  49. package/dist/chunk-KJNQLAB5.cjs.map +0 -1
  50. package/dist/chunk-MKH5THDR.cjs.map +0 -1
  51. package/dist/chunk-OS7MO2OW.js +0 -109
  52. package/dist/chunk-OS7MO2OW.js.map +0 -1
  53. package/dist/chunk-SH7BRP4M.cjs +0 -111
  54. package/dist/chunk-SH7BRP4M.cjs.map +0 -1
  55. package/dist/chunk-V5EN3ESV.js +0 -156
  56. package/dist/chunk-V5EN3ESV.js.map +0 -1
  57. package/dist/types-iknDq5tf.d.cts +0 -17
  58. package/dist/types-iknDq5tf.d.ts +0 -17
@@ -0,0 +1,152 @@
1
+ import { MultipleChoice } from './chunk-ZVFWJTT4.js';
2
+ import { FillInTheBlanks } from './chunk-3X7MYAFT.js';
3
+ import { WrittenResponse } from './chunk-AEQQ7VPF.js';
4
+ import { useState, useRef, useEffect } from 'react';
5
+ import { jsx, jsxs } from 'react/jsx-runtime';
6
+
7
+ function ActivitySequence({
8
+ activities,
9
+ renderers,
10
+ onActivityComplete,
11
+ onComplete,
12
+ onFinished,
13
+ onInteraction,
14
+ renderMode,
15
+ theme,
16
+ locale,
17
+ disabled
18
+ }) {
19
+ const [index, setIndex] = useState(0);
20
+ const [outcomes, setOutcomes] = useState(
21
+ () => activities.map(() => null)
22
+ );
23
+ const regionRef = useRef(null);
24
+ const mountedRef = useRef(false);
25
+ const setKey = JSON.stringify(activities.map((activity) => activity.id));
26
+ const [prevSetKey, setPrevSetKey] = useState(setKey);
27
+ if (prevSetKey !== setKey) {
28
+ setPrevSetKey(setKey);
29
+ setIndex(0);
30
+ setOutcomes(activities.map(() => null));
31
+ }
32
+ useEffect(() => {
33
+ if (mountedRef.current) {
34
+ regionRef.current?.focus();
35
+ } else {
36
+ mountedRef.current = true;
37
+ }
38
+ }, [index]);
39
+ const total = activities.length;
40
+ const current = activities[index];
41
+ const go = (next) => {
42
+ setIndex(next);
43
+ };
44
+ const record = (outcome) => {
45
+ if (outcomes[outcome.index] != null) {
46
+ return;
47
+ }
48
+ const next = outcomes.slice();
49
+ next[outcome.index] = outcome;
50
+ setOutcomes(next);
51
+ if (outcome.kind === "scored") {
52
+ onActivityComplete?.(outcome.result, outcome.index);
53
+ }
54
+ if (!next.every((item) => item !== null)) {
55
+ return;
56
+ }
57
+ onFinished?.(next);
58
+ const scored = [];
59
+ for (const item of next) {
60
+ if (item.kind !== "scored") {
61
+ return;
62
+ }
63
+ scored.push(item.result);
64
+ }
65
+ onComplete?.(scored);
66
+ };
67
+ if (!current) {
68
+ return /* @__PURE__ */ jsx("div", { className: "lk-seq" });
69
+ }
70
+ const forwarded = {
71
+ ...onInteraction ? { onInteraction } : {},
72
+ ...renderMode ? { renderMode } : {},
73
+ ...theme ? { theme } : {},
74
+ ...locale ? { locale } : {},
75
+ ...disabled ? { disabled } : {}
76
+ };
77
+ const renderSlot = (activity, slotIndex) => {
78
+ const activityId = activity.id;
79
+ const childProps = {
80
+ onComplete: (result) => record({ kind: "scored", index: slotIndex, activityId, result }),
81
+ ...forwarded
82
+ };
83
+ const CustomRenderer = renderers?.[activity.type];
84
+ if (CustomRenderer) {
85
+ return /* @__PURE__ */ jsx(CustomRenderer, { data: activity, ...childProps });
86
+ }
87
+ if (activity.type === "multiple-choice") {
88
+ return /* @__PURE__ */ jsx(MultipleChoice, { data: activity, ...childProps });
89
+ }
90
+ if (activity.type === "fill-in-the-blanks") {
91
+ return /* @__PURE__ */ jsx(FillInTheBlanks, { data: activity, ...childProps });
92
+ }
93
+ if (activity.type === "written-response") {
94
+ return /* @__PURE__ */ jsx(
95
+ WrittenResponse,
96
+ {
97
+ data: activity,
98
+ onSubmitted: (submission) => record({ kind: "submitted", index: slotIndex, activityId, submission }),
99
+ ...forwarded
100
+ }
101
+ );
102
+ }
103
+ return /* @__PURE__ */ jsx("div", { className: "lk-seq-unsupported", role: "note", children: "This activity type has no renderer. Supply one through the renderers prop." });
104
+ };
105
+ return /* @__PURE__ */ jsxs("div", { className: "lk-seq", lang: locale, children: [
106
+ /* @__PURE__ */ jsxs("div", { className: "lk-seq-progress", "aria-live": "polite", role: "status", children: [
107
+ "Question ",
108
+ index + 1,
109
+ " of ",
110
+ total
111
+ ] }),
112
+ /* @__PURE__ */ jsx(
113
+ "section",
114
+ {
115
+ className: "lk-seq-question",
116
+ ref: regionRef,
117
+ tabIndex: -1,
118
+ "aria-label": `Question ${index + 1} of ${total}`,
119
+ children: activities.map((activity, slotIndex) => {
120
+ const slotKey = `slot-${slotIndex}-${activity.id}`;
121
+ return /* @__PURE__ */ jsx("div", { className: "lk-seq-slot", hidden: slotIndex !== index, children: renderSlot(activity, slotIndex) }, slotKey);
122
+ })
123
+ }
124
+ ),
125
+ /* @__PURE__ */ jsxs("div", { className: "lk-seq-nav", children: [
126
+ /* @__PURE__ */ jsx(
127
+ "button",
128
+ {
129
+ type: "button",
130
+ className: "lk-seq-prev",
131
+ onClick: () => go(index - 1),
132
+ disabled: index === 0,
133
+ children: "Previous"
134
+ }
135
+ ),
136
+ /* @__PURE__ */ jsx(
137
+ "button",
138
+ {
139
+ type: "button",
140
+ className: "lk-seq-next",
141
+ onClick: () => go(index + 1),
142
+ disabled: index === total - 1,
143
+ children: "Next"
144
+ }
145
+ )
146
+ ] })
147
+ ] });
148
+ }
149
+
150
+ export { ActivitySequence };
151
+ //# sourceMappingURL=chunk-BGWXN44S.js.map
152
+ //# sourceMappingURL=chunk-BGWXN44S.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/ActivitySequence/ActivitySequence.tsx"],"names":[],"mappings":"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","file":"chunk-BGWXN44S.js","sourcesContent":["'use client';\n\nimport type {\n ActivityData,\n ActivityResult,\n InteractionEvent,\n ThemeTokens,\n} from '@intellectif/lk-core';\nimport { type ComponentType, useEffect, useRef, useState } from 'react';\nimport { FillInTheBlanks } from '../FillInTheBlanks/index.js';\nimport { MultipleChoice } from '../MultipleChoice/index.js';\nimport type { ActivityProps, 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 index: number;\n activityId: string;\n result: ActivityResult;\n }\n | {\n kind: 'submitted';\n index: number;\n activityId: string;\n /** Ungraded submission; the grade arrives asynchronously. */\n submission: WrittenResponseSubmission;\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 /** The ordered set of activities to present, one at a time. */\n activities: ActivityData[];\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 /** Called whenever an individual activity is scored at submit time. */\n onActivityComplete?: (result: ActivityResult, index: number) => 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.\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 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 renderers,\n onActivityComplete,\n onComplete,\n onFinished,\n onInteraction,\n renderMode,\n theme,\n locale,\n disabled,\n}: ActivitySequenceProps): React.JSX.Element {\n const [index, setIndex] = useState(0);\n const [outcomes, setOutcomes] = useState<(SequenceItemOutcome | null)[]>(() =>\n activities.map(() => null),\n );\n const regionRef = useRef<HTMLDivElement>(null);\n const mountedRef = useRef(false);\n\n // Reset pager position and outcomes when the activity 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 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 // JSON.stringify (not join) so ids containing the separator cannot collide.\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 setOutcomes(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 /**\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 if (outcomes[outcome.index] != null) {\n return;\n }\n const next = outcomes.slice();\n next[outcome.index] = outcome;\n setOutcomes(next);\n\n if (outcome.kind === 'scored') {\n onActivityComplete?.(outcome.result, outcome.index);\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 ? { renderMode } : {}),\n ...(theme ? { theme } : {}),\n ...(locale ? { locale } : {}),\n ...(disabled ? { disabled } : {}),\n };\n\n const renderSlot = (activity: ActivityData, slotIndex: number): React.JSX.Element => {\n const activityId = activity.id;\n const childProps = {\n onComplete: (result: ActivityResult) =>\n record({ kind: 'scored', index: slotIndex, activityId, result }),\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, activityId, submission })\n }\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 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 {/*\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 {activities.map((activity, slotIndex) => {\n // Slot POSITION is part of the key on purpose: the same activity may\n // legitimately appear in two slots, and each must keep its own\n // answer state. Computed here rather than inline so the intent is\n // explicit rather than an accidental index-as-key.\n const slotKey = `slot-${slotIndex}-${activity.id}`;\n return (\n <div className=\"lk-seq-slot\" hidden={slotIndex !== index} key={slotKey}>\n {renderSlot(activity, slotIndex)}\n </div>\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,154 @@
1
+ 'use strict';
2
+
3
+ var chunkW6BH2YMZ_cjs = require('./chunk-W6BH2YMZ.cjs');
4
+ var chunkUXO6KYSZ_cjs = require('./chunk-UXO6KYSZ.cjs');
5
+ var chunk7MW7HGOA_cjs = require('./chunk-7MW7HGOA.cjs');
6
+ var react = require('react');
7
+ var jsxRuntime = require('react/jsx-runtime');
8
+
9
+ function ActivitySequence({
10
+ activities,
11
+ renderers,
12
+ onActivityComplete,
13
+ onComplete,
14
+ onFinished,
15
+ onInteraction,
16
+ renderMode,
17
+ theme,
18
+ locale,
19
+ disabled
20
+ }) {
21
+ const [index, setIndex] = react.useState(0);
22
+ const [outcomes, setOutcomes] = react.useState(
23
+ () => activities.map(() => null)
24
+ );
25
+ const regionRef = react.useRef(null);
26
+ const mountedRef = react.useRef(false);
27
+ const setKey = JSON.stringify(activities.map((activity) => activity.id));
28
+ const [prevSetKey, setPrevSetKey] = react.useState(setKey);
29
+ if (prevSetKey !== setKey) {
30
+ setPrevSetKey(setKey);
31
+ setIndex(0);
32
+ setOutcomes(activities.map(() => null));
33
+ }
34
+ react.useEffect(() => {
35
+ if (mountedRef.current) {
36
+ regionRef.current?.focus();
37
+ } else {
38
+ mountedRef.current = true;
39
+ }
40
+ }, [index]);
41
+ const total = activities.length;
42
+ const current = activities[index];
43
+ const go = (next) => {
44
+ setIndex(next);
45
+ };
46
+ const record = (outcome) => {
47
+ if (outcomes[outcome.index] != null) {
48
+ return;
49
+ }
50
+ const next = outcomes.slice();
51
+ next[outcome.index] = outcome;
52
+ setOutcomes(next);
53
+ if (outcome.kind === "scored") {
54
+ onActivityComplete?.(outcome.result, outcome.index);
55
+ }
56
+ if (!next.every((item) => item !== null)) {
57
+ return;
58
+ }
59
+ onFinished?.(next);
60
+ const scored = [];
61
+ for (const item of next) {
62
+ if (item.kind !== "scored") {
63
+ return;
64
+ }
65
+ scored.push(item.result);
66
+ }
67
+ onComplete?.(scored);
68
+ };
69
+ if (!current) {
70
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lk-seq" });
71
+ }
72
+ const forwarded = {
73
+ ...onInteraction ? { onInteraction } : {},
74
+ ...renderMode ? { renderMode } : {},
75
+ ...theme ? { theme } : {},
76
+ ...locale ? { locale } : {},
77
+ ...disabled ? { disabled } : {}
78
+ };
79
+ const renderSlot = (activity, slotIndex) => {
80
+ const activityId = activity.id;
81
+ const childProps = {
82
+ onComplete: (result) => record({ kind: "scored", index: slotIndex, activityId, result }),
83
+ ...forwarded
84
+ };
85
+ const CustomRenderer = renderers?.[activity.type];
86
+ if (CustomRenderer) {
87
+ return /* @__PURE__ */ jsxRuntime.jsx(CustomRenderer, { data: activity, ...childProps });
88
+ }
89
+ if (activity.type === "multiple-choice") {
90
+ return /* @__PURE__ */ jsxRuntime.jsx(chunkW6BH2YMZ_cjs.MultipleChoice, { data: activity, ...childProps });
91
+ }
92
+ if (activity.type === "fill-in-the-blanks") {
93
+ return /* @__PURE__ */ jsxRuntime.jsx(chunkUXO6KYSZ_cjs.FillInTheBlanks, { data: activity, ...childProps });
94
+ }
95
+ if (activity.type === "written-response") {
96
+ return /* @__PURE__ */ jsxRuntime.jsx(
97
+ chunk7MW7HGOA_cjs.WrittenResponse,
98
+ {
99
+ data: activity,
100
+ onSubmitted: (submission) => record({ kind: "submitted", index: slotIndex, activityId, submission }),
101
+ ...forwarded
102
+ }
103
+ );
104
+ }
105
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lk-seq-unsupported", role: "note", children: "This activity type has no renderer. Supply one through the renderers prop." });
106
+ };
107
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lk-seq", lang: locale, children: [
108
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lk-seq-progress", "aria-live": "polite", role: "status", children: [
109
+ "Question ",
110
+ index + 1,
111
+ " of ",
112
+ total
113
+ ] }),
114
+ /* @__PURE__ */ jsxRuntime.jsx(
115
+ "section",
116
+ {
117
+ className: "lk-seq-question",
118
+ ref: regionRef,
119
+ tabIndex: -1,
120
+ "aria-label": `Question ${index + 1} of ${total}`,
121
+ children: activities.map((activity, slotIndex) => {
122
+ const slotKey = `slot-${slotIndex}-${activity.id}`;
123
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lk-seq-slot", hidden: slotIndex !== index, children: renderSlot(activity, slotIndex) }, slotKey);
124
+ })
125
+ }
126
+ ),
127
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lk-seq-nav", children: [
128
+ /* @__PURE__ */ jsxRuntime.jsx(
129
+ "button",
130
+ {
131
+ type: "button",
132
+ className: "lk-seq-prev",
133
+ onClick: () => go(index - 1),
134
+ disabled: index === 0,
135
+ children: "Previous"
136
+ }
137
+ ),
138
+ /* @__PURE__ */ jsxRuntime.jsx(
139
+ "button",
140
+ {
141
+ type: "button",
142
+ className: "lk-seq-next",
143
+ onClick: () => go(index + 1),
144
+ disabled: index === total - 1,
145
+ children: "Next"
146
+ }
147
+ )
148
+ ] })
149
+ ] });
150
+ }
151
+
152
+ exports.ActivitySequence = ActivitySequence;
153
+ //# sourceMappingURL=chunk-HO3AC3WQ.cjs.map
154
+ //# sourceMappingURL=chunk-HO3AC3WQ.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/ActivitySequence/ActivitySequence.tsx"],"names":["useState","useRef","useEffect","jsx","MultipleChoice","FillInTheBlanks","WrittenResponse","jsxs"],"mappings":"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","file":"chunk-HO3AC3WQ.cjs","sourcesContent":["'use client';\n\nimport type {\n ActivityData,\n ActivityResult,\n InteractionEvent,\n ThemeTokens,\n} from '@intellectif/lk-core';\nimport { type ComponentType, useEffect, useRef, useState } from 'react';\nimport { FillInTheBlanks } from '../FillInTheBlanks/index.js';\nimport { MultipleChoice } from '../MultipleChoice/index.js';\nimport type { ActivityProps, 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 index: number;\n activityId: string;\n result: ActivityResult;\n }\n | {\n kind: 'submitted';\n index: number;\n activityId: string;\n /** Ungraded submission; the grade arrives asynchronously. */\n submission: WrittenResponseSubmission;\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 /** The ordered set of activities to present, one at a time. */\n activities: ActivityData[];\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 /** Called whenever an individual activity is scored at submit time. */\n onActivityComplete?: (result: ActivityResult, index: number) => 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.\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 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 renderers,\n onActivityComplete,\n onComplete,\n onFinished,\n onInteraction,\n renderMode,\n theme,\n locale,\n disabled,\n}: ActivitySequenceProps): React.JSX.Element {\n const [index, setIndex] = useState(0);\n const [outcomes, setOutcomes] = useState<(SequenceItemOutcome | null)[]>(() =>\n activities.map(() => null),\n );\n const regionRef = useRef<HTMLDivElement>(null);\n const mountedRef = useRef(false);\n\n // Reset pager position and outcomes when the activity 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 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 // JSON.stringify (not join) so ids containing the separator cannot collide.\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 setOutcomes(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 /**\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 if (outcomes[outcome.index] != null) {\n return;\n }\n const next = outcomes.slice();\n next[outcome.index] = outcome;\n setOutcomes(next);\n\n if (outcome.kind === 'scored') {\n onActivityComplete?.(outcome.result, outcome.index);\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 ? { renderMode } : {}),\n ...(theme ? { theme } : {}),\n ...(locale ? { locale } : {}),\n ...(disabled ? { disabled } : {}),\n };\n\n const renderSlot = (activity: ActivityData, slotIndex: number): React.JSX.Element => {\n const activityId = activity.id;\n const childProps = {\n onComplete: (result: ActivityResult) =>\n record({ kind: 'scored', index: slotIndex, activityId, result }),\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, activityId, submission })\n }\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 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 {/*\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 {activities.map((activity, slotIndex) => {\n // Slot POSITION is part of the key on purpose: the same activity may\n // legitimately appear in two slots, and each must keep its own\n // answer state. Computed here rather than inline so the intent is\n // explicit rather than an accidental index-as-key.\n const slotKey = `slot-${slotIndex}-${activity.id}`;\n return (\n <div className=\"lk-seq-slot\" hidden={slotIndex !== index} key={slotKey}>\n {renderSlot(activity, slotIndex)}\n </div>\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"]}
@@ -27,55 +27,106 @@ function parsePassage(passage) {
27
27
  }
28
28
  return segments;
29
29
  }
30
+ var NO_ANSWERS = {};
31
+ function answersOf(response) {
32
+ return response !== void 0 && response.type === "fill-in-the-blanks" ? response.answers : NO_ANSWERS;
33
+ }
34
+ function correctnessByItem(details) {
35
+ const map = /* @__PURE__ */ new Map();
36
+ for (const detail of details) {
37
+ map.set(
38
+ detail.itemId,
39
+ detail.outcome !== void 0 ? detail.outcome === "correct" : detail.correct
40
+ );
41
+ }
42
+ return map;
43
+ }
30
44
  function FillInTheBlanks({
31
45
  data,
32
46
  onComplete,
47
+ onSubmit,
48
+ onChange,
33
49
  onInteraction,
50
+ value,
51
+ defaultValue,
52
+ renderMode = "practice",
53
+ outcome,
54
+ sanitizeHtml,
34
55
  theme,
35
56
  locale,
36
57
  disabled,
37
58
  showCorrectAnswers
38
59
  }) {
60
+ const isExam = renderMode === "exam";
61
+ const isReview = renderMode === "review";
39
62
  const devError = react.useMemo(() => {
40
63
  if (!chunkQHOV2A6M_cjs.isDevelopment()) {
41
64
  return null;
42
65
  }
66
+ if (data.redacted === true) {
67
+ try {
68
+ lkCore.assertRedacted(data);
69
+ return null;
70
+ } catch (error) {
71
+ return error instanceof Error ? error : new Error(String(error));
72
+ }
73
+ }
43
74
  const result2 = lkCore.validateActivity("fill-in-the-blanks", data);
44
75
  return result2.success ? null : new lkCore.ActivitySchemaError("fill-in-the-blanks", result2.errors);
45
76
  }, [data]);
46
77
  const { state, start, complete, getTimeSpent, reset } = chunkHD2AIQRD_cjs.useActivityState();
47
- const [answers, setAnswers] = react.useState({});
78
+ const isControlled = value !== void 0;
79
+ const [internalAnswers, setInternalAnswers] = react.useState(
80
+ () => answersOf(defaultValue)
81
+ );
48
82
  const [revealed, setRevealed] = react.useState(() => /* @__PURE__ */ new Set());
49
83
  const [result, setResult] = react.useState(null);
50
84
  const [summary, setSummary] = react.useState(null);
51
85
  const [feedbackHidden, setFeedbackHidden] = react.useState(false);
86
+ const answers = isControlled ? answersOf(value) : internalAnswers;
87
+ const defaultValueRef = react.useRef(defaultValue);
52
88
  react.useEffect(() => {
53
- setAnswers({});
89
+ defaultValueRef.current = defaultValue;
90
+ }, [defaultValue]);
91
+ react.useEffect(() => {
92
+ setInternalAnswers(answersOf(defaultValueRef.current));
54
93
  setRevealed(/* @__PURE__ */ new Set());
55
94
  setResult(null);
56
95
  setSummary(null);
57
96
  setFeedbackHidden(false);
58
97
  reset();
59
98
  }, [data, reset]);
99
+ react.useEffect(() => {
100
+ if (chunkQHOV2A6M_cjs.isDevelopment() && sanitizeHtml !== void 0 && data.passageHtml !== void 0) {
101
+ console.warn(
102
+ "[lk-react] FillInTheBlanks ignores `passageHtml`: the passage hosts the answer inputs, so rich HTML cannot be split at {{placeholders}} without voiding the sanitiser. Rendering the plain `passage` instead."
103
+ );
104
+ }
105
+ }, [sanitizeHtml, data.passageHtml]);
60
106
  const segments = react.useMemo(() => parsePassage(data.passage), [data.passage]);
61
107
  const blankById = react.useMemo(() => new Map(data.blanks.map((b) => [b.id, b])), [data.blanks]);
62
108
  if (devError) {
63
109
  throw devError;
64
110
  }
65
111
  const submitted = state === "completed";
66
- const inactive = disabled === true || submitted;
112
+ const inactive = disabled === true || submitted || isReview;
113
+ const revealing = isReview ? outcome?.status === "scored" : !isExam && submitted;
67
114
  const fireInteraction = (type, payload) => {
68
115
  onInteraction?.({ type, activityId: data.id, timestamp: Date.now(), payload });
69
116
  };
70
- const handleChange = (blankId, value) => {
117
+ const handleChange = (blankId, text) => {
71
118
  if (inactive) {
72
119
  return;
73
120
  }
74
121
  if (state === "idle") {
75
122
  start();
76
123
  }
77
- setAnswers((prev) => ({ ...prev, [blankId]: value }));
78
- fireInteraction("blank-filled", { blankId, value });
124
+ const next = { ...answers, [blankId]: text };
125
+ if (!isControlled) {
126
+ setInternalAnswers(next);
127
+ }
128
+ onChange?.({ type: "fill-in-the-blanks", answers: next });
129
+ fireInteraction("blank-filled", { blankId, value: text });
79
130
  };
80
131
  const toggleHint = (blankId) => {
81
132
  if (inactive) {
@@ -103,6 +154,13 @@ function FillInTheBlanks({
103
154
  return;
104
155
  }
105
156
  const response = { type: "fill-in-the-blanks", answers };
157
+ onSubmit?.(response);
158
+ if (isExam) {
159
+ complete();
160
+ setSummary("Answer submitted.");
161
+ fireInteraction("submitted", { answers });
162
+ return;
163
+ }
106
164
  const scoringResult = lkCore.score("fill-in-the-blanks", data, response);
107
165
  complete();
108
166
  const timeSpent = getTimeSpent();
@@ -122,7 +180,7 @@ function FillInTheBlanks({
122
180
  response: JSON.stringify(answers)
123
181
  });
124
182
  setResult(scoringResult);
125
- onComplete({
183
+ onComplete?.({
126
184
  score: scoringResult.score,
127
185
  maxScore: scoringResult.maxScore,
128
186
  passed: scoringResult.passed,
@@ -136,18 +194,30 @@ function FillInTheBlanks({
136
194
  fireInteraction("submitted", { answers, score: scoringResult.score });
137
195
  };
138
196
  const correctByBlank = react.useMemo(() => {
139
- const map = /* @__PURE__ */ new Map();
140
- for (const detail of result?.details ?? []) {
141
- map.set(detail.itemId, detail.correct);
197
+ if (isExam) {
198
+ return /* @__PURE__ */ new Map();
199
+ }
200
+ if (isReview) {
201
+ return outcome?.status === "scored" ? correctnessByItem(outcome.details) : /* @__PURE__ */ new Map();
202
+ }
203
+ return correctnessByItem(result?.details ?? []);
204
+ }, [isExam, isReview, outcome, result]);
205
+ const reviewSummary = react.useMemo(() => {
206
+ if (!isReview || outcome === void 0) {
207
+ return null;
208
+ }
209
+ if (outcome.status === "scored") {
210
+ return `Score ${Math.round(outcome.score * 100)}%. ${outcome.passed ? "Passed." : "Not passed."}${outcome.feedback ? ` ${outcome.feedback}` : ""}`;
142
211
  }
143
- return map;
144
- }, [result]);
212
+ return outcome.status === "deferred" ? "Not graded yet." : "No grade available.";
213
+ }, [isReview, outcome]);
145
214
  return /* @__PURE__ */ jsxRuntime.jsxs(
146
215
  "form",
147
216
  {
148
217
  className: "lk-fib",
149
218
  "aria-label": data.title,
150
219
  lang: locale,
220
+ "data-render-mode": renderMode,
151
221
  style: theme,
152
222
  onSubmit: handleSubmit,
153
223
  children: [
@@ -161,10 +231,13 @@ function FillInTheBlanks({
161
231
  if (!blank) {
162
232
  return null;
163
233
  }
164
- const isCorrect = correctByBlank.get(seg.id) === true;
234
+ const acceptedAnswers = isExam ? void 0 : blank.acceptedAnswers;
235
+ const blankFeedback = isExam ? void 0 : blank.feedback;
236
+ const correctness = correctByBlank.get(seg.id);
165
237
  const hintId = `${data.id}-hint-${seg.id}`;
166
- if (submitted && showCorrectAnswers && !isCorrect) {
167
- return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lk-fib-answer", "data-correct": "false", children: blank.acceptedAnswers[0] }, seg.id);
238
+ const correctAnswer = acceptedAnswers?.[0];
239
+ if (revealing && showCorrectAnswers && correctness === false && correctAnswer) {
240
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lk-fib-answer", "data-correct": "false", children: correctAnswer }, seg.id);
168
241
  }
169
242
  return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "lk-fib-blank", children: [
170
243
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -176,7 +249,7 @@ function FillInTheBlanks({
176
249
  value: answers[seg.id] ?? "",
177
250
  disabled: inactive,
178
251
  "aria-disabled": inactive || void 0,
179
- "data-correct": submitted ? String(isCorrect) : void 0,
252
+ "data-correct": revealing && correctness !== void 0 ? String(correctness) : void 0,
180
253
  onChange: (e) => handleChange(seg.id, e.target.value)
181
254
  }
182
255
  ),
@@ -213,20 +286,20 @@ function FillInTheBlanks({
213
286
  ),
214
287
  /* @__PURE__ */ jsxRuntime.jsx("span", { id: hintId, "aria-live": "polite", children: revealed.has(seg.id) ? blank.hint : "" })
215
288
  ] }) : null,
216
- submitted && !feedbackHidden && blank.feedback ? /* @__PURE__ */ jsxRuntime.jsx(
289
+ revealing && !feedbackHidden && blankFeedback ? /* @__PURE__ */ jsxRuntime.jsx(
217
290
  "span",
218
291
  {
219
292
  className: "lk-fib-blank-feedback",
220
293
  role: "note",
221
- "data-correct": String(isCorrect),
222
- children: blank.feedback
294
+ "data-correct": correctness !== void 0 ? String(correctness) : void 0,
295
+ children: blankFeedback
223
296
  }
224
297
  ) : null
225
298
  ] }, seg.id);
226
299
  }) }),
227
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", disabled: inactive, children: "Check answers" })
300
+ isReview ? null : /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", disabled: inactive, children: isExam ? "Submit answers" : "Check answers" })
228
301
  ] }),
229
- submitted && data.blanks.some((b) => b.feedback) ? /* @__PURE__ */ jsxRuntime.jsx(
302
+ revealing && data.blanks.some((b) => b.feedback) ? /* @__PURE__ */ jsxRuntime.jsx(
230
303
  "button",
231
304
  {
232
305
  type: "button",
@@ -236,7 +309,7 @@ function FillInTheBlanks({
236
309
  children: feedbackHidden ? "Show feedback" : "Hide feedback"
237
310
  }
238
311
  ) : null,
239
- /* @__PURE__ */ jsxRuntime.jsx(chunkQHOV2A6M_cjs.FeedbackRegion, { id: `${data.id}-feedback`, children: summary })
312
+ /* @__PURE__ */ jsxRuntime.jsx(chunkQHOV2A6M_cjs.FeedbackRegion, { id: `${data.id}-feedback`, children: isReview ? reviewSummary : summary })
240
313
  ]
241
314
  }
242
315
  );
@@ -246,5 +319,5 @@ function FillInTheBlanks2(props) {
246
319
  }
247
320
 
248
321
  exports.FillInTheBlanks = FillInTheBlanks2;
249
- //# sourceMappingURL=chunk-73XCWYKD.cjs.map
250
- //# sourceMappingURL=chunk-73XCWYKD.cjs.map
322
+ //# sourceMappingURL=chunk-UXO6KYSZ.cjs.map
323
+ //# sourceMappingURL=chunk-UXO6KYSZ.cjs.map