@intellectif/lk-react 2.1.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.
@@ -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"]}
@@ -1,8 +1,9 @@
1
1
  'use strict';
2
2
 
3
- var chunkSTLRKR2I_cjs = require('../chunk-STLRKR2I.cjs');
3
+ var chunkHO3AC3WQ_cjs = require('../chunk-HO3AC3WQ.cjs');
4
4
  require('../chunk-W6BH2YMZ.cjs');
5
5
  require('../chunk-UXO6KYSZ.cjs');
6
+ require('../chunk-7MW7HGOA.cjs');
6
7
  require('../chunk-QHOV2A6M.cjs');
7
8
  require('../chunk-HD2AIQRD.cjs');
8
9
 
@@ -10,7 +11,7 @@ require('../chunk-HD2AIQRD.cjs');
10
11
 
11
12
  Object.defineProperty(exports, "ActivitySequence", {
12
13
  enumerable: true,
13
- get: function () { return chunkSTLRKR2I_cjs.ActivitySequence; }
14
+ get: function () { return chunkHO3AC3WQ_cjs.ActivitySequence; }
14
15
  });
15
16
  //# sourceMappingURL=ActivitySequence.cjs.map
16
17
  //# sourceMappingURL=ActivitySequence.cjs.map
@@ -1,14 +1,62 @@
1
1
  import { ActivityData, ActivityResult, InteractionEvent, ThemeTokens } from '@intellectif/lk-core';
2
+ import { ComponentType } from 'react';
3
+ import { A as ActivityProps, R as RenderMode } from '../types-CNmjqD6M.cjs';
4
+ import { a as WrittenResponseSubmission } from '../WrittenResponse-B50EjVR0.cjs';
2
5
 
6
+ /**
7
+ * What one finished slot in a sequence produced. Two kinds, because two kinds
8
+ * of activity exist: those the SDK scores at submit time, and those a grader
9
+ * scores later. Collapsing them would mean inventing a score for ungraded
10
+ * work — the exact defect the deferred outcome exists to prevent.
11
+ */
12
+ type SequenceItemOutcome = {
13
+ kind: 'scored';
14
+ index: number;
15
+ activityId: string;
16
+ result: ActivityResult;
17
+ } | {
18
+ kind: 'submitted';
19
+ index: number;
20
+ activityId: string;
21
+ /** Ungraded submission; the grade arrives asynchronously. */
22
+ submission: WrittenResponseSubmission;
23
+ };
24
+ /**
25
+ * A component that can render one activity inside a sequence. Register one per
26
+ * activity `type` to put a consumer-defined type on screen — the React half of
27
+ * the activity-type registry, matching `registerActivityType` in lk-core.
28
+ */
29
+ type ActivityRenderer = ComponentType<ActivityProps>;
3
30
  interface ActivitySequenceProps {
4
31
  /** The ordered set of activities to present, one at a time. */
5
32
  activities: ActivityData[];
6
- /** Called whenever an individual activity is completed. */
33
+ /**
34
+ * Renderers for activity types beyond the built-ins, keyed by `data.type`.
35
+ * A key matching a built-in overrides it, so a consumer can replace the
36
+ * bundled renderer without forking the sequencer.
37
+ */
38
+ renderers?: Readonly<Record<string, ActivityRenderer>>;
39
+ /** Called whenever an individual activity is scored at submit time. */
7
40
  onActivityComplete?: (result: ActivityResult, index: number) => void;
8
- /** Called once every activity in the set has been completed. */
41
+ /**
42
+ * Called once every activity has been completed, with the scored results.
43
+ *
44
+ * Fires only when EVERY item produced a score. A set containing a
45
+ * deferred-graded activity (a written response) can never satisfy that, so
46
+ * use `onFinished` for mixed sets — it is the general completion signal and
47
+ * reports both kinds of outcome.
48
+ */
9
49
  onComplete?: (results: ActivityResult[]) => void;
50
+ /**
51
+ * Called once every activity has been completed, whether it was scored at
52
+ * submit time or submitted for later grading. Use this for any set that
53
+ * mixes graded and deferred-graded activities.
54
+ */
55
+ onFinished?: (items: SequenceItemOutcome[]) => void;
10
56
  /** Forwarded to each activity. */
11
57
  onInteraction?: (event: InteractionEvent) => void;
58
+ /** Forwarded to each activity. `exam` and `review` disable local scoring. */
59
+ renderMode?: RenderMode;
12
60
  theme?: Partial<ThemeTokens>;
13
61
  locale?: string;
14
62
  disabled?: boolean;
@@ -20,6 +68,6 @@ interface ActivitySequenceProps {
20
68
  * scoring. On navigation, focus moves to the question region and the position
21
69
  * is announced via an aria-live region.
22
70
  */
23
- declare function ActivitySequence({ activities, onActivityComplete, onComplete, onInteraction, theme, locale, disabled, }: ActivitySequenceProps): React.JSX.Element;
71
+ declare function ActivitySequence({ activities, renderers, onActivityComplete, onComplete, onFinished, onInteraction, renderMode, theme, locale, disabled, }: ActivitySequenceProps): React.JSX.Element;
24
72
 
25
- export { ActivitySequence, type ActivitySequenceProps };
73
+ export { type ActivityRenderer, ActivitySequence, type ActivitySequenceProps, type SequenceItemOutcome };
@@ -1,14 +1,62 @@
1
1
  import { ActivityData, ActivityResult, InteractionEvent, ThemeTokens } from '@intellectif/lk-core';
2
+ import { ComponentType } from 'react';
3
+ import { A as ActivityProps, R as RenderMode } from '../types-CNmjqD6M.js';
4
+ import { a as WrittenResponseSubmission } from '../WrittenResponse-BHhP936i.js';
2
5
 
6
+ /**
7
+ * What one finished slot in a sequence produced. Two kinds, because two kinds
8
+ * of activity exist: those the SDK scores at submit time, and those a grader
9
+ * scores later. Collapsing them would mean inventing a score for ungraded
10
+ * work — the exact defect the deferred outcome exists to prevent.
11
+ */
12
+ type SequenceItemOutcome = {
13
+ kind: 'scored';
14
+ index: number;
15
+ activityId: string;
16
+ result: ActivityResult;
17
+ } | {
18
+ kind: 'submitted';
19
+ index: number;
20
+ activityId: string;
21
+ /** Ungraded submission; the grade arrives asynchronously. */
22
+ submission: WrittenResponseSubmission;
23
+ };
24
+ /**
25
+ * A component that can render one activity inside a sequence. Register one per
26
+ * activity `type` to put a consumer-defined type on screen — the React half of
27
+ * the activity-type registry, matching `registerActivityType` in lk-core.
28
+ */
29
+ type ActivityRenderer = ComponentType<ActivityProps>;
3
30
  interface ActivitySequenceProps {
4
31
  /** The ordered set of activities to present, one at a time. */
5
32
  activities: ActivityData[];
6
- /** Called whenever an individual activity is completed. */
33
+ /**
34
+ * Renderers for activity types beyond the built-ins, keyed by `data.type`.
35
+ * A key matching a built-in overrides it, so a consumer can replace the
36
+ * bundled renderer without forking the sequencer.
37
+ */
38
+ renderers?: Readonly<Record<string, ActivityRenderer>>;
39
+ /** Called whenever an individual activity is scored at submit time. */
7
40
  onActivityComplete?: (result: ActivityResult, index: number) => void;
8
- /** Called once every activity in the set has been completed. */
41
+ /**
42
+ * Called once every activity has been completed, with the scored results.
43
+ *
44
+ * Fires only when EVERY item produced a score. A set containing a
45
+ * deferred-graded activity (a written response) can never satisfy that, so
46
+ * use `onFinished` for mixed sets — it is the general completion signal and
47
+ * reports both kinds of outcome.
48
+ */
9
49
  onComplete?: (results: ActivityResult[]) => void;
50
+ /**
51
+ * Called once every activity has been completed, whether it was scored at
52
+ * submit time or submitted for later grading. Use this for any set that
53
+ * mixes graded and deferred-graded activities.
54
+ */
55
+ onFinished?: (items: SequenceItemOutcome[]) => void;
10
56
  /** Forwarded to each activity. */
11
57
  onInteraction?: (event: InteractionEvent) => void;
58
+ /** Forwarded to each activity. `exam` and `review` disable local scoring. */
59
+ renderMode?: RenderMode;
12
60
  theme?: Partial<ThemeTokens>;
13
61
  locale?: string;
14
62
  disabled?: boolean;
@@ -20,6 +68,6 @@ interface ActivitySequenceProps {
20
68
  * scoring. On navigation, focus moves to the question region and the position
21
69
  * is announced via an aria-live region.
22
70
  */
23
- declare function ActivitySequence({ activities, onActivityComplete, onComplete, onInteraction, theme, locale, disabled, }: ActivitySequenceProps): React.JSX.Element;
71
+ declare function ActivitySequence({ activities, renderers, onActivityComplete, onComplete, onFinished, onInteraction, renderMode, theme, locale, disabled, }: ActivitySequenceProps): React.JSX.Element;
24
72
 
25
- export { ActivitySequence, type ActivitySequenceProps };
73
+ export { type ActivityRenderer, ActivitySequence, type ActivitySequenceProps, type SequenceItemOutcome };
@@ -1,6 +1,7 @@
1
- export { ActivitySequence } from '../chunk-UASXUWHH.js';
1
+ export { ActivitySequence } from '../chunk-BGWXN44S.js';
2
2
  import '../chunk-ZVFWJTT4.js';
3
3
  import '../chunk-3X7MYAFT.js';
4
+ import '../chunk-AEQQ7VPF.js';
4
5
  import '../chunk-UIGIYYGY.js';
5
6
  import '../chunk-5HVUKXF3.js';
6
7
  //# sourceMappingURL=ActivitySequence.js.map
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunk7BT6OXAR_cjs = require('../chunk-7BT6OXAR.cjs');
3
+ var chunk7MW7HGOA_cjs = require('../chunk-7MW7HGOA.cjs');
4
4
  require('../chunk-QHOV2A6M.cjs');
5
5
  require('../chunk-HD2AIQRD.cjs');
6
6
 
@@ -8,7 +8,7 @@ require('../chunk-HD2AIQRD.cjs');
8
8
 
9
9
  Object.defineProperty(exports, "WrittenResponse", {
10
10
  enumerable: true,
11
- get: function () { return chunk7BT6OXAR_cjs.WrittenResponse; }
11
+ get: function () { return chunk7MW7HGOA_cjs.WrittenResponse; }
12
12
  });
13
13
  //# sourceMappingURL=WrittenResponse.cjs.map
14
14
  //# sourceMappingURL=WrittenResponse.cjs.map
@@ -1,89 +1,8 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { WrittenResponseData, XAPIStatement, LearnerResponse, ItemOutcome, InteractionEvent, ThemeTokens } from '@intellectif/lk-core';
3
- import { a as Renderable, R as RenderMode, H as HtmlSanitizer } from '../types-CNmjqD6M.cjs';
4
-
5
- /**
6
- * Payload delivered when the learner submits a written response. Grading is
7
- * DEFERRED (asynchronous AI/human grading), so there is no score here — and
8
- * deliberately no fake one: an ungraded submission must never be shown or
9
- * stored as a 0. The consumer persists `text` and grades it out-of-band.
10
- */
11
- interface WrittenResponseSubmission {
12
- /** The submitted text, verbatim. */
13
- text: string;
14
- /** Word count of `text`, recomputed with the canonical `countWords()`. */
15
- wordCount: number;
16
- /** Whether `wordCount` falls within the activity's `[minWords, maxWords]`. */
17
- withinWordBounds: boolean;
18
- /** Time in milliseconds from first interaction to submission. */
19
- timeSpent: number;
20
- /** The SUBMITTED-verb xAPI statement built for this submission (no score fields). */
21
- xapiStatement: XAPIStatement;
22
- }
23
- /**
24
- * Props for {@link WrittenResponse}. Mirrors `ActivityProps` except for the
25
- * completion callback: a deferred-grading activity completes with a
26
- * {@link WrittenResponseSubmission} (`onSubmitted`), not a scored
27
- * `ActivityResult` (`onComplete`) — deviation from Req 22.8 recorded in the
28
- * roadmap (§3.1): fabricating a score of 0 for ungraded work is the exact
29
- * bug this type exists to fix. Everything else — controlled value, render
30
- * mode, server outcome, rich text — follows the shared `ActivityProps`
31
- * contract verbatim.
32
- */
33
- interface WrittenResponseProps {
34
- /** Activity content. Accepts a `redact()` projection in `exam` mode. */
35
- data: Renderable<WrittenResponseData>;
36
- /**
37
- * Called on submit with the ungraded submission (text, recomputed word
38
- * count, bounds flag, timing, xAPI statement). Fires in `practice` and
39
- * `exam` mode, after `onSubmit`; never in `review`.
40
- *
41
- * Optional since v0.4 so a `review`-mode render (which has no submit
42
- * control) and an exam runner that only wants the raw response through
43
- * `onSubmit` do not have to pass a no-op. Supply it in `practice`/`exam`
44
- * unless `onSubmit` already captures everything you persist.
45
- */
46
- onSubmitted?: (submission: WrittenResponseSubmission) => void;
47
- /**
48
- * Called on submit with the raw learner response and no grade, BEFORE
49
- * `onSubmitted`. This is the only submit callback an exam runner needs:
50
- * the server grades the text and returns an {@link ItemOutcome} later.
51
- */
52
- onSubmit?: (response: LearnerResponse) => void;
53
- /**
54
- * Controlled value: the learner's current response. When present the
55
- * textarea renders from it (never from internal state) and every keystroke
56
- * goes out through `onChange` — restore an in-progress attempt, autosave a
57
- * delta, or drive a review render with it.
58
- */
59
- value?: LearnerResponse;
60
- /** Initial response for an uncontrolled component (ignored when `value` is set). */
61
- defaultValue?: LearnerResponse;
62
- /** Fires on every change to the learner's response. Required for a controlled component. */
63
- onChange?: (response: LearnerResponse) => void;
64
- /**
65
- * Presentation mode, default `practice`. `practice` and `exam` are nearly
66
- * identical here — a written response is never graded client-side, so
67
- * there is no local score to withhold; `exam` additionally never calls
68
- * `evaluate()` and renders happily from a `redact()` projection.
69
- * `review` is read-only and shows the server's `outcome`.
70
- */
71
- renderMode?: RenderMode;
72
- /**
73
- * Server-computed outcome, rendered by `review` mode: `deferred` shows a
74
- * "not graded yet" affordance, `scored` shows the returned grade and
75
- * feedback. Ignored in other modes — this component never manufactures an
76
- * outcome of its own.
77
- */
78
- outcome?: ItemOutcome;
79
- /** Renders `data.promptHtml` when provided. See `HtmlSanitizer`. */
80
- sanitizeHtml?: HtmlSanitizer;
81
- onInteraction?: (event: InteractionEvent) => void;
82
- /** Per-instance token overrides, applied as inline CSS vars on the root. */
83
- theme?: Partial<ThemeTokens>;
84
- locale?: string;
85
- disabled?: boolean;
86
- }
2
+ import { W as WrittenResponseProps } from '../WrittenResponse-B50EjVR0.cjs';
3
+ export { a as WrittenResponseSubmission } from '../WrittenResponse-B50EjVR0.cjs';
4
+ export { H as HtmlSanitizer, R as RenderMode, a as Renderable } from '../types-CNmjqD6M.cjs';
5
+ import '@intellectif/lk-core';
87
6
 
88
7
  /**
89
8
  * Public Written Response activity: a labelled textarea with a live word
@@ -93,4 +12,4 @@ interface WrittenResponseProps {
93
12
  */
94
13
  declare function WrittenResponse(props: WrittenResponseProps): react_jsx_runtime.JSX.Element;
95
14
 
96
- export { HtmlSanitizer, RenderMode, Renderable, WrittenResponse, type WrittenResponseProps, type WrittenResponseSubmission };
15
+ export { WrittenResponse, WrittenResponseProps };
@@ -1,89 +1,8 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { WrittenResponseData, XAPIStatement, LearnerResponse, ItemOutcome, InteractionEvent, ThemeTokens } from '@intellectif/lk-core';
3
- import { a as Renderable, R as RenderMode, H as HtmlSanitizer } from '../types-CNmjqD6M.js';
4
-
5
- /**
6
- * Payload delivered when the learner submits a written response. Grading is
7
- * DEFERRED (asynchronous AI/human grading), so there is no score here — and
8
- * deliberately no fake one: an ungraded submission must never be shown or
9
- * stored as a 0. The consumer persists `text` and grades it out-of-band.
10
- */
11
- interface WrittenResponseSubmission {
12
- /** The submitted text, verbatim. */
13
- text: string;
14
- /** Word count of `text`, recomputed with the canonical `countWords()`. */
15
- wordCount: number;
16
- /** Whether `wordCount` falls within the activity's `[minWords, maxWords]`. */
17
- withinWordBounds: boolean;
18
- /** Time in milliseconds from first interaction to submission. */
19
- timeSpent: number;
20
- /** The SUBMITTED-verb xAPI statement built for this submission (no score fields). */
21
- xapiStatement: XAPIStatement;
22
- }
23
- /**
24
- * Props for {@link WrittenResponse}. Mirrors `ActivityProps` except for the
25
- * completion callback: a deferred-grading activity completes with a
26
- * {@link WrittenResponseSubmission} (`onSubmitted`), not a scored
27
- * `ActivityResult` (`onComplete`) — deviation from Req 22.8 recorded in the
28
- * roadmap (§3.1): fabricating a score of 0 for ungraded work is the exact
29
- * bug this type exists to fix. Everything else — controlled value, render
30
- * mode, server outcome, rich text — follows the shared `ActivityProps`
31
- * contract verbatim.
32
- */
33
- interface WrittenResponseProps {
34
- /** Activity content. Accepts a `redact()` projection in `exam` mode. */
35
- data: Renderable<WrittenResponseData>;
36
- /**
37
- * Called on submit with the ungraded submission (text, recomputed word
38
- * count, bounds flag, timing, xAPI statement). Fires in `practice` and
39
- * `exam` mode, after `onSubmit`; never in `review`.
40
- *
41
- * Optional since v0.4 so a `review`-mode render (which has no submit
42
- * control) and an exam runner that only wants the raw response through
43
- * `onSubmit` do not have to pass a no-op. Supply it in `practice`/`exam`
44
- * unless `onSubmit` already captures everything you persist.
45
- */
46
- onSubmitted?: (submission: WrittenResponseSubmission) => void;
47
- /**
48
- * Called on submit with the raw learner response and no grade, BEFORE
49
- * `onSubmitted`. This is the only submit callback an exam runner needs:
50
- * the server grades the text and returns an {@link ItemOutcome} later.
51
- */
52
- onSubmit?: (response: LearnerResponse) => void;
53
- /**
54
- * Controlled value: the learner's current response. When present the
55
- * textarea renders from it (never from internal state) and every keystroke
56
- * goes out through `onChange` — restore an in-progress attempt, autosave a
57
- * delta, or drive a review render with it.
58
- */
59
- value?: LearnerResponse;
60
- /** Initial response for an uncontrolled component (ignored when `value` is set). */
61
- defaultValue?: LearnerResponse;
62
- /** Fires on every change to the learner's response. Required for a controlled component. */
63
- onChange?: (response: LearnerResponse) => void;
64
- /**
65
- * Presentation mode, default `practice`. `practice` and `exam` are nearly
66
- * identical here — a written response is never graded client-side, so
67
- * there is no local score to withhold; `exam` additionally never calls
68
- * `evaluate()` and renders happily from a `redact()` projection.
69
- * `review` is read-only and shows the server's `outcome`.
70
- */
71
- renderMode?: RenderMode;
72
- /**
73
- * Server-computed outcome, rendered by `review` mode: `deferred` shows a
74
- * "not graded yet" affordance, `scored` shows the returned grade and
75
- * feedback. Ignored in other modes — this component never manufactures an
76
- * outcome of its own.
77
- */
78
- outcome?: ItemOutcome;
79
- /** Renders `data.promptHtml` when provided. See `HtmlSanitizer`. */
80
- sanitizeHtml?: HtmlSanitizer;
81
- onInteraction?: (event: InteractionEvent) => void;
82
- /** Per-instance token overrides, applied as inline CSS vars on the root. */
83
- theme?: Partial<ThemeTokens>;
84
- locale?: string;
85
- disabled?: boolean;
86
- }
2
+ import { W as WrittenResponseProps } from '../WrittenResponse-BHhP936i.js';
3
+ export { a as WrittenResponseSubmission } from '../WrittenResponse-BHhP936i.js';
4
+ export { H as HtmlSanitizer, R as RenderMode, a as Renderable } from '../types-CNmjqD6M.js';
5
+ import '@intellectif/lk-core';
87
6
 
88
7
  /**
89
8
  * Public Written Response activity: a labelled textarea with a live word
@@ -93,4 +12,4 @@ interface WrittenResponseProps {
93
12
  */
94
13
  declare function WrittenResponse(props: WrittenResponseProps): react_jsx_runtime.JSX.Element;
95
14
 
96
- export { HtmlSanitizer, RenderMode, Renderable, WrittenResponse, type WrittenResponseProps, type WrittenResponseSubmission };
15
+ export { WrittenResponse, WrittenResponseProps };
@@ -1,4 +1,4 @@
1
- export { WrittenResponse } from '../chunk-XTJ22JAP.js';
1
+ export { WrittenResponse } from '../chunk-AEQQ7VPF.js';
2
2
  import '../chunk-UIGIYYGY.js';
3
3
  import '../chunk-5HVUKXF3.js';
4
4
  //# sourceMappingURL=WrittenResponse.js.map