@intellectif/lk-react 6.0.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +330 -0
- package/README.md +43 -12
- package/dist/{WrittenResponse-C3GIWErG.d.ts → WrittenResponse-BXHidIaK.d.cts} +2 -2
- package/dist/{WrittenResponse-BfpzfLlR.d.cts → WrittenResponse-DJYGuMKk.d.ts} +2 -2
- package/dist/{chunk-E4MNA2IQ.js → chunk-2TW6WLBC.js} +3 -2
- package/dist/chunk-2TW6WLBC.js.map +1 -0
- package/dist/chunk-4HZ7LQIH.cjs +1 -0
- package/dist/chunk-57E3E2H4.js +1 -0
- package/dist/chunk-6F4X2NKL.js +1 -0
- package/dist/chunk-6N43WDVG.js +1 -0
- package/dist/{chunk-CPF2JJG6.cjs → chunk-AMGK7DDM.cjs} +32 -2
- package/dist/chunk-AMGK7DDM.cjs.map +1 -0
- package/dist/{chunk-YXG3UJVG.cjs → chunk-BE7R2X3S.cjs} +3 -2
- package/dist/chunk-BE7R2X3S.cjs.map +1 -0
- package/dist/{chunk-B3FUUKZ2.js → chunk-DQNVAXG6.js} +32 -3
- package/dist/chunk-DQNVAXG6.js.map +1 -0
- package/dist/{chunk-PBVMEOWI.cjs → chunk-DUIWK272.cjs} +7 -6
- package/dist/chunk-DUIWK272.cjs.map +1 -0
- package/dist/chunk-GB5URWM4.js +1 -0
- package/dist/{chunk-OQGBAQL2.js → chunk-IUODGQXE.js} +5 -4
- package/dist/chunk-IUODGQXE.js.map +1 -0
- package/dist/{chunk-R3OZ6JHQ.cjs → chunk-MDYKYDRN.cjs} +3 -2
- package/dist/chunk-MDYKYDRN.cjs.map +1 -0
- package/dist/{chunk-BQHDQ33H.js → chunk-MDZMKC27.js} +3 -2
- package/dist/chunk-MDZMKC27.js.map +1 -0
- package/dist/chunk-NEWGUDA5.cjs +1 -0
- package/dist/chunk-NPY2F7F6.cjs +1 -0
- package/dist/chunk-PGEG7UZJ.cjs +1 -0
- package/dist/{chunk-IYLQSNLE.cjs → chunk-PNNUPUH4.cjs} +3 -2
- package/dist/chunk-PNNUPUH4.cjs.map +1 -0
- package/dist/chunk-UQ3BBEIT.js +1 -0
- package/dist/chunk-XOR4MPEN.cjs +1 -0
- package/dist/{chunk-5TUTJ4R3.js → chunk-ZLUMUJUQ.js} +3 -2
- package/dist/chunk-ZLUMUJUQ.js.map +1 -0
- package/dist/components/ActivitySequence.cjs +5 -4
- package/dist/components/ActivitySequence.d.cts +18 -10
- package/dist/components/ActivitySequence.d.ts +18 -10
- package/dist/components/ActivitySequence.js +4 -3
- package/dist/components/FillInTheBlanks.cjs +1 -0
- package/dist/components/FillInTheBlanks.d.cts +1 -1
- package/dist/components/FillInTheBlanks.d.ts +1 -1
- package/dist/components/FillInTheBlanks.js +1 -0
- package/dist/components/MultipleChoice.cjs +3 -2
- package/dist/components/MultipleChoice.d.cts +2 -2
- package/dist/components/MultipleChoice.d.ts +2 -2
- package/dist/components/MultipleChoice.js +2 -1
- package/dist/components/StimulusPanel.cjs +1 -0
- package/dist/components/StimulusPanel.d.cts +1 -1
- package/dist/components/StimulusPanel.d.ts +1 -1
- package/dist/components/StimulusPanel.js +1 -0
- package/dist/components/WrittenResponse.cjs +3 -2
- package/dist/components/WrittenResponse.d.cts +3 -3
- package/dist/components/WrittenResponse.d.ts +3 -3
- package/dist/components/WrittenResponse.js +2 -1
- package/dist/hooks/useActivityState.cjs +1 -0
- package/dist/hooks/useActivityState.js +1 -0
- package/dist/hooks/useXAPI.cjs +3 -2
- package/dist/hooks/useXAPI.d.cts +31 -0
- package/dist/hooks/useXAPI.d.ts +31 -0
- package/dist/hooks/useXAPI.js +2 -1
- package/dist/index.cjs +18 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/theme/ThemeProvider.cjs +10 -5
- package/dist/theme/ThemeProvider.d.cts +21 -1
- package/dist/theme/ThemeProvider.d.ts +21 -1
- package/dist/theme/ThemeProvider.js +2 -1
- package/dist/theme/skin.css +7 -7
- package/dist/{types-DlHmPLbD.d.cts → types-9iS1Vs2I.d.cts} +29 -13
- package/dist/{types-DlHmPLbD.d.ts → types-9iS1Vs2I.d.ts} +29 -13
- package/package.json +28 -5
- package/dist/chunk-5TUTJ4R3.js.map +0 -1
- package/dist/chunk-B3FUUKZ2.js.map +0 -1
- package/dist/chunk-BQHDQ33H.js.map +0 -1
- package/dist/chunk-CPF2JJG6.cjs.map +0 -1
- package/dist/chunk-E4MNA2IQ.js.map +0 -1
- package/dist/chunk-IYLQSNLE.cjs.map +0 -1
- package/dist/chunk-OQGBAQL2.js.map +0 -1
- package/dist/chunk-PBVMEOWI.cjs.map +0 -1
- package/dist/chunk-R3OZ6JHQ.cjs.map +0 -1
- package/dist/chunk-YXG3UJVG.cjs.map +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# @intellectif/lk-react
|
|
2
|
+
|
|
3
|
+
## 6.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 58f0651: Make the published package match what the published docs promise — and fix the three defects that checking it exposed.
|
|
8
|
+
|
|
9
|
+
Every claim on both npm pages and in the linked guides was checked against the **published tarballs** rather than the source, because the tarball is what an integrator actually installs. Three of the gaps were not documentation problems at all.
|
|
10
|
+
|
|
11
|
+
**`'use client'` reached none of the shipped files.** Every source file declares it, but bundling drops module-level directives ("Module level directives cause errors when bundled … was ignored"), so the directive survived into **0 of 38** emitted modules — while three separate documents advertised RSC compatibility and the limitations table said "SSR / RSC: fully supported". Importing a component into a React Server Component tree failed for a package that promised the opposite. A build banner does not survive the treeshake pass either, so the directive is now re-applied to every emitted module after the build.
|
|
12
|
+
|
|
13
|
+
Because this defect lives _only_ in the build output, no lint, typecheck or unit test could see it — so the fix ships with a gate that reads `dist/` rather than `src/`: `verify-dist` asserts every emitted module carries the directive, that stylesheets and declaration files do not, and that the built entries really export what the docs promise. It runs after `build` alongside `publint` and `attw`, and fails if the post-build step is ever removed or replaced with a banner.
|
|
14
|
+
|
|
15
|
+
**`createTailwindTheme` was never exported.** It is implemented, unit-tested, and documented in four places — both READMEs, the subpath table and the styling guide, which gives a copy-pasteable import — but `src/theme/tailwind.ts` was never re-exported, so it reached no published build and the documented import was a module-resolution error. It now ships from `@intellectif/lk-react/theme/ThemeProvider` and the barrel. A missing re-export is invisible to every other check in this repo — the source compiles, the function's own tests pass, the package builds — so a test now pins the whole public export surface, and `verify-dist` additionally loads the _built_ modules, which is the only way to catch a bundler or exports-map regression.
|
|
16
|
+
|
|
17
|
+
**The skin ignored the theme for Written Response.** Its block referenced `--lk-font-family`, `--lk-border-radius-md` and `--lk-color-on-primary`; none is a token. CSS treats an undefined `var()` as invalid at computed-value time, so those properties silently fell back to `unset` — the submit button drew the page's inherited text colour on a primary-coloured background, a real contrast risk, and the component followed neither the theme's font nor its radius. Corrected to `--lk-font-family-base`, `--lk-radius-base` and `--lk-color-surface`, with a test asserting the skin references only tokens `defaults.css` defines.
|
|
18
|
+
|
|
19
|
+
**Grade-correctness corrections in the guides.** The upgrade guide told integrators to give an unanswered slot an `unscorable` outcome. Executed against the shipped build, a three-question paper with one correct answer and two `unscorable` slots composes to `status: 'final'`, `score: 1`, `passed: true` — a final, passing 100% for a paper the learner barely started, which is the exact defect this SDK exists to prevent. `unscorable` means "a grade is never coming", so the slot leaves the denominator _and_ the result may go final; `deferred` is what holds it provisional. Both guides now say so, and point at `scoredItemsFromPlan`, which defaults to `deferred` for this reason. The authoring guide had contradicted itself on the same point in two sections.
|
|
20
|
+
|
|
21
|
+
The guides also claimed `roundGrade` "refuses" a non-finite input; it returns it unchanged (`gradeFromRubric` is the one that refuses), so `gte(NaN, …)` reads as an ordinary fail unless you guard. And `seededShuffle`'s uniform `version: 2` is documented as usable for new content, but no SDK ordering path accepts it — `flattenSequence`, `planAttempt`, within-group shuffling and `<MultipleChoice>` all use version 1 and expose no option — so the limitation is now stated instead of implied away.
|
|
22
|
+
|
|
23
|
+
**Corrections to what the npm pages describe.**
|
|
24
|
+
|
|
25
|
+
- `validateActivity` **throws** `UnknownActivityTypeError` for an unregistered type rather than returning `{ success: false }` — the case that arises exactly when a content bank carries consumer-registered types.
|
|
26
|
+
- `score()` throws `DeferredScoringError` for a deferred-graded type and `RedactedScoringError` for a `redact()` projection. Both sit on the documented exam path; `evaluate()` is what to call there. The Errors bullet listed two of the four exported error classes.
|
|
27
|
+
- The lk-core page's own "Running an exam" recipe called `redact()` on the entries it had just passed to `planAttempt`. Those entries may be item groups, and `item-group` is a reserved container rather than a registered activity type, so the documented exam path threw `UnknownActivityTypeError` for any paper containing a reading or listening group. It now branches on `isItemGroup()`.
|
|
28
|
+
- `redact()` takes a single activity. Mapping a group's items through it and shipping the container leaves `Stimulus.transcript` — a listening passage's author-only transcript, i.e. the answers — in the learner's payload. `redactItemGroup()` / `assertRedactedItemGroup()` are now documented as the container equivalent.
|
|
29
|
+
- `validateItemGroup` and `xapiDefinitionFor` appeared on neither page.
|
|
30
|
+
- Attempt plans, attempt state and content hashing were absent from the lk-core page entirely, so a reader built the plan → `ScoredItem` bridge by hand.
|
|
31
|
+
- The React page named two of the three activity components and none of `renderMode`, the controlled-component props, `shuffleSeed`, `asRenderable`, the sequence resume props, `onFinished` or the `renderers` registry.
|
|
32
|
+
- Both "Retry" rows said a new `data` reference resets the activity to idle. It resets to its **seeded** state, so on the resume path it restores the previous answer — and a new React `key` does the same, because the seeds are simply re-read on the fresh mount. A genuinely fresh attempt needs the new `key` _and_ the seed props dropped.
|
|
33
|
+
- `<WrittenResponse>` was covered by the claim that "every built-in component throws at render when handed redacted data" in `practice`. It does not: it never grades on the client, so it has no such guard and a redacted essay renders and stays answerable, running the practice submit path silently. Documented in the guides and in the `asRenderableSequence` doc comment.
|
|
34
|
+
- `shuffleSeed`'s hover text said it is "required in `exam` and `review` mode — omitting it throws". It is required only when the sequence actually shuffles. The guard also does not cover an activity's own `data.shuffle`, where `<MultipleChoice>` still invents a per-mount seed in every mode — so an unseeded item shuffle yields an exam order the server cannot rebuild, without complaint. Both are now stated where a reader will meet them.
|
|
35
|
+
- The release map's first row paired `lk-core@0.3.0` with `lk-react@2.1.0`. That pair does not install: 2.1.0 peers on `^0.3.1` (2.0.0 is the 0.3.0 partner), and 2.1.0 is the one minor in the table, because a `lk-core` patch does not force a major.
|
|
36
|
+
- The React page said 6.0.0 "changes no React API" ten lines after listing the three props it added; 5.0.0 is the peer-only major.
|
|
37
|
+
- `data-correct` was documented as landing on "options/blanks". It lands on `.lk-mc-option` but on the `input` _inside_ `.lk-fib-blank`, so a rule written against the documented selector never matches.
|
|
38
|
+
- The custom-activity-type walkthrough titled "end to end" omitted the `declare module` augmentation, without which its own snippets do not compile (TS2345 / TS2322) — registration is a runtime act only.
|
|
39
|
+
- `review` mode renders more of a `GradeRecord` than documented: the grade `feedback`, and a **learner-visible** notice when `requiresHumanReview` is true.
|
|
40
|
+
- `HtmlSanitizer`'s hover text — the doc comment every rich-text integrator reads — promised `passageHtml` rendering that `<FillInTheBlanks>` refuses by design.
|
|
41
|
+
- `MultipleChoiceData.shuffle` was documented as shuffling "per session"; order derives from `shuffleSeed` when given, and is reproducible only then.
|
|
42
|
+
- The grade record's `evidence`, `rationale`, `confidence`, `grader` provenance and `usage` were described as rendered by the SDK. They are stored; only per-criterion scores and inline corrections are rendered.
|
|
43
|
+
- Version labels were a release out of step in several places: `CriterionScore.maxScore` and the per-type redacted types landed in 0.6.0 (not 0.5), item groups in 0.5, attempt plans in 0.6 and attempt state in 0.7. The upgrade guide now leads with a `lk-core` ↔ `lk-react` release map, since a `lk-react` major is often only the peer-range bump.
|
|
44
|
+
- `useXAPI` — the first symbol in both quick starts — had no doc comment at all.
|
|
45
|
+
|
|
46
|
+
**Packaging.** Both packages gain `keywords`, `homepage` and `bugs` (npm showed none), a description matching the current surface rather than the 0.2-era one, `./package.json` in the exports map, and `CHANGELOG.md` in the tarball — which is where a reader lands when a major turns out to be a peer bump. `repository.url` now carries the `git+` prefix `publint` asks for.
|
|
47
|
+
|
|
48
|
+
### Patch Changes
|
|
49
|
+
|
|
50
|
+
- Updated dependencies [58f0651]
|
|
51
|
+
- @intellectif/lk-core@0.7.1
|
|
52
|
+
|
|
53
|
+
## 6.0.0
|
|
54
|
+
|
|
55
|
+
### Minor Changes
|
|
56
|
+
|
|
57
|
+
- 4d0bd5a: Resume and review — an interrupted attempt can be reopened where it was left.
|
|
58
|
+
|
|
59
|
+
An integrating application maintains a ~500-line exam renderer and a ~425-line review modal, and the reason is not styling: `ActivitySequence` could not resume an interrupted attempt and could not render a finished one. A learner at question 18 of 20 whose tab crashed came back to question 1 with all twenty blank, however faithfully the responses had been persisted — the pager's position and per-slot answers were the one part of an attempt a consumer could not restore.
|
|
60
|
+
|
|
61
|
+
**`AttemptState`, bound to its plan.** `serializeAttemptState(plan, { responses, submittedSlotIds, index, savedAt })` captures an attempt in progress; `restoreAttemptState(plan, state)` reopens it. The `planHash` check is the point: slot ids are short and stable by design, so a snapshot from a _different_ paper — last term's midterm, a sibling version, a copy-pasted attempt row — lines its answers up against the wrong questions and looks entirely plausible doing it. Comparing the paper's fingerprint makes that impossible rather than unlikely.
|
|
62
|
+
|
|
63
|
+
Validation happens on the way **in**, not on the way out: a response recorded against a slot the paper does not contain is a bug at the moment it is written, and finding it when a learner tries to resume is finding it far too late. An `index` that is not a position the plan has is refused for the same reason, and `restoreAttemptState` refuses an envelope `stateVersion` it does not understand rather than reinterpreting it under this version's rules and re-stamping it — which would destroy the evidence it had ever been anything else.
|
|
64
|
+
|
|
65
|
+
Snapshots are copied **deeply**. A one-level copy hands back the caller's same response objects, and every `LearnerResponse` shape is an object: a consumer whose reducer edits an answer in place — an Immer draft, a push onto a multi-select, `answers[blankId] = text` — would mutate every snapshot ever taken, so the stored answer retroactively became the new one, `diffResponses` saw nothing, and a delta autosave silently wrote nothing for a change the learner really made.
|
|
66
|
+
|
|
67
|
+
The SDK reads no clock — pass `savedAt` — so the function stays pure and reproducible in a test.
|
|
68
|
+
|
|
69
|
+
**`diffResponses(before, after)`** reports what moved between two snapshots, including an answer the learner **cleared**, which comparing the later snapshot alone cannot see. It compares through the canonical form, so a response that survived a JSON round-trip with its keys reordered is not reported as a change the learner never made — while a reordered _selection_ is, because that is what they picked.
|
|
70
|
+
|
|
71
|
+
**`ActivitySequence` gains five props.** `defaultIndex` reopens on the stored question, clamped to one the paper actually has — including when the value is not a number at all, which `Number(row.last_index)` produces from a NULL column and which previously survived every clamp and rendered an empty page. `onIndexChange` reports every position the pager lands on, not only learner clicks: a clamp it had to apply and the reset a set change performs are positions a consumer must persist too, and reporting only clicks left storage disagreeing with the screen. `responses` seeds each slot's saved answer by `slotId` — as `defaultValue`, so a restored answer stays editable, since resume is not a freeze. `submittedSlotIds` reopens already-committed questions as committed; without it a summative resume unlocks everything the learner had submitted, and they can change and re-submit it. `outcomes` forwards the server's verdict per slot, which in `review` mode is the only thing that marks correctness; the client never scores, so a review render without it shows the answers and no verdict rather than inventing one.
|
|
72
|
+
|
|
73
|
+
The three seed props (`responses`, `submittedSlotIds`, `outcomes`) are keyed lookups guarded with `Object.hasOwn`, so a slot keyed `constructor` cannot resolve a function off the prototype chain into a question. They apply at mount and stop at the first set change — slot ids repeat across papers, so re-applying them after the entries changed dropped one paper's answers under another's questions.
|
|
74
|
+
|
|
75
|
+
**`defaultSubmitted` on every activity component**, and an optional initial state on `useActivityState`, are what make the submitted half restorable. `reset()` now takes the state to return to, because "reset" for a restored item does not mean idle: the Req-3.7 data-change effect reset unconditionally, and in `FillInTheBlanks` and `WrittenResponse` — which had no mount identity guard — it ran right after the first paint and undid the seed it had just been given, so `defaultSubmitted` was a no-op for those types entirely. All three now guard the mount and reset to the _current_ seed.
|
|
76
|
+
|
|
77
|
+
**`SequenceItemOutcome` gains a `restored` arm.** A slot the learner had already submitted mounts locked and will not submit again, so leaving its outcome null meant `onFinished` waited forever on something that could never arrive — a resumed attempt could never signal completion, however many of the remaining questions were answered. Restored slots are seeded at mount, firing no callback, so a later submit of the last outstanding slot completes the set. An attempt that was already complete announces nothing.
|
|
78
|
+
|
|
79
|
+
**The sequence's `shuffleSeed` now reaches the option shuffle.** It was forwarded to `flattenSequence` for question order but never to `MultipleChoice`, which invented a fresh per-mount order — so a resumed or reviewed item showed the learner's answers against a different arrangement than the one they sat. An appeal about "the second option" was about a different option.
|
|
80
|
+
|
|
81
|
+
**`MultipleChoice` now resets to `defaultValue` rather than to empty** when its `data` prop changes identity, matching what `FillInTheBlanks` already did. Clearing looked safer, but `data` identity is a poor proxy for "different question": a parent building entries in render — `activities={raw.map(redact)}`, the documented exam pattern — hands over new objects every render, and clearing wiped every _restored_ answer on the first unrelated re-render.
|
|
82
|
+
|
|
83
|
+
### Patch Changes
|
|
84
|
+
|
|
85
|
+
- Updated dependencies [4d0bd5a]
|
|
86
|
+
- @intellectif/lk-core@0.7.0
|
|
87
|
+
|
|
88
|
+
## 5.0.0
|
|
89
|
+
|
|
90
|
+
### Patch Changes
|
|
91
|
+
|
|
92
|
+
- 07672b3: Make the grading surface reachable, and correct what the docs promise.
|
|
93
|
+
|
|
94
|
+
An evidence sweep of a production integration found it pinned to `lk-core@^0.3.0` — so none of the v0.4 grading work was callable there — while two of the exact defects that work exists to prevent were live in its gradebook: a language model computing the weighted total of record for every essay, and an ungraded essay recorded as a hard zero that flowed into the learner's pass/fail. The features were shipped and correct. The obstacles were on this side.
|
|
95
|
+
|
|
96
|
+
**`CriterionScore.maxScore` (new).** `gradeFromRubric` required every criterion score to be pre-scaled to `[0,1]` and _rejected_ anything else. Real graders work out of 100, or out of 9 for a CEFR band, or out of a per-criterion points total — so the rejection sent integrators back to letting the model produce the weighted total itself, which is precisely the arithmetic this function exists to take away from it. Declare what each score is out of and the SDK normalises before weighting; a rubric may mix scales. Omitted, it defaults to `1` and the previous arithmetic is byte-identical. A `maxScore` that is zero, negative or non-finite is reported `unscorable` rather than divided by, and the out-of-range message now names `maxScore` as the remedy instead of telling the caller to normalise by hand.
|
|
97
|
+
|
|
98
|
+
**Per-type redacted types (new).** `redact()` returns `RedactedActivityData`, which proves a payload is learner-safe but is index-signature typed and says nothing about its shape — right for the assertion, useless for anything that has to render or transport the result. Every integrator re-declared those interfaces by hand and they drifted. `RedactedMultipleChoiceData`, `RedactedFillInTheBlanksData`, `RedactedWrittenResponseData`, the `RedactedActivity` union, `RedactedStimulus` and the option/blank shapes are now derived from the strict schemas with `z.infer`, so the type and the validator cannot disagree, with tests pinning them to what `redact()` actually produces.
|
|
99
|
+
|
|
100
|
+
**`<WrittenResponse>` normalises each criterion it displays.** `gradeFromRubric` stores the grader's judgements verbatim, in the units the grader used, so a grade stays auditable years later — which means anything _displaying_ a criterion has to normalise it, exactly as the overall score is already normalised against its own `maxScore`. The review renderer printed `score * 100`, so the moment a criterion could legitimately be `82 / 100` it would have read "8200%". Caught before release; the same review path is now tested end to end through `gradeFromRubric` with mixed native scales rather than pre-scaled `[0,1]` fixtures.
|
|
101
|
+
|
|
102
|
+
**`docs/upgrading.md` (new)**, leading with the two grade defects and what to call instead, and covering the `passed: boolean | null` and list-vs-count adjustments that adopting `composeAssessmentScore` requires.
|
|
103
|
+
|
|
104
|
+
**A documentation-truth pass**, treated as a correctness deliverable:
|
|
105
|
+
|
|
106
|
+
- The root README stated `redact()` strips **rubrics**. It does not, deliberately — a rubric tells the learner what they are assessed on — so an integrator trusting the README would ship rubrics to an exam client believing they were stripped. An in-source docblock contradicted the policy three lines below it.
|
|
107
|
+
- Both READMEs denied any resume capability ("no `initialResponse` prop → cannot re-hydrate a prior attempt") months after `value`/`defaultValue`/`onChange` shipped in lk-react 2.1.0.
|
|
108
|
+
- The published `.d.ts` told every IDE that `questionHtml` and `promptHtml` are "not rendered by the SDK yet". Both render, through a caller-supplied sanitiser. (`passageHtml` genuinely is not rendered; that JSDoc was correct and stands.)
|
|
109
|
+
- The authoring guide said the scoring engine never returns feedback and capped multiple choice at 10 options; `score()` has selected feedback on `passed` since 0.3.0 and the schema allows 26.
|
|
110
|
+
- `lk-react`'s README omitted `<WrittenResponse>` entirely, and `lk-core`'s advertised roughly its 0.2.x surface on a package published at 0.4.0 — no `evaluate`, registry, `redact`, `GradeRecord` or composition.
|
|
111
|
+
|
|
112
|
+
**`@intellectif/lk-server` is deleted.** It was a private, empty placeholder for its whole life, with no thesis anyone could state. An empty package with no purpose is a liability, not an option held open.
|
|
113
|
+
|
|
114
|
+
- Updated dependencies [07672b3]
|
|
115
|
+
- Updated dependencies [e5857b2]
|
|
116
|
+
- @intellectif/lk-core@0.6.0
|
|
117
|
+
|
|
118
|
+
## 4.0.0
|
|
119
|
+
|
|
120
|
+
### Minor Changes
|
|
121
|
+
|
|
122
|
+
- 2d962aa: Shared stimulus and item groups — the reading/listening-comprehension container.
|
|
123
|
+
|
|
124
|
+
**The gap was a container, not an item type.** Real comprehension content is one passage or recording serving several questions; with nowhere to put the passage, it ended up hidden behind a toggle or pasted into every item. `Stimulus` and `ItemGroup` are that container, modelled as **content** so they drop into a sequence, a lesson quiz or an exam section alike. `kind` is enforced (a `text` stimulus needs a body, an `audio` one needs audio media, a `video` one accepts an embed), `bodyHtml` requires the plain `body` it falls back to, and `transcript` is the one author-only field. `validateItemGroup` validates the container, the stimulus and every item against its registered schema, reporting an unregistered item type rather than throwing — an author fixing a six-item group wants every problem listed.
|
|
125
|
+
|
|
126
|
+
**Ordering lives in lk-core, so the server and the client agree.** `flattenSequence(entries, { shuffleEntries?, seed? })` turns activities and groups into presented slots. Groups are **shuffle-atomic** — a section shuffle moves a group as one block and never interleaves two passages' questions — and only a group that declares `shuffle: 'within-group'` reorders its items. A seed is **required** whenever anything shuffles, and an **empty group is refused**: contributing no slots would delete a whole section from a sequence that still looked well-formed, and `composeAssessmentScore` would then report a `final` grade over the survivors. Each slot's `slotId` comes from the _authored_ position, so it is unique and stable under shuffling.
|
|
127
|
+
|
|
128
|
+
**Redaction.** `redactItemGroup` / `assertRedactedItemGroup` extend the fail-closed model to the container: the transcript goes, the passage stays, every item is proven learner-safe against its own type's schema, and failures are reported at `items.<index>`. `item-group` is reserved in the registry.
|
|
129
|
+
|
|
130
|
+
**React.** `ActivitySequence` accepts `SequenceEntry[]`, flattens groups into consecutive questions, and mounts each group's stimulus **once**, kept beside every question in the group. New `shuffle`, `shuffleSeed`, `sanitizeHtml` and `onSubmit` props; new `<StimulusPanel>`, also exported for custom runners.
|
|
131
|
+
|
|
132
|
+
***
|
|
133
|
+
|
|
134
|
+
The rest of this entry is what a review of the above turned up. Several are older defects the group work only made visible.
|
|
135
|
+
|
|
136
|
+
**`ActivitySequence` had no exam response channel at all.** In `exam` mode the components deliberately never grade: they emit the raw answer through `onSubmit` and return before `onComplete`. The pager forwarded `onComplete` and nothing else, so an exam runner received _nothing_ — no response, no completion, for any question. `onSubmit(response, { slotId, index, activityId })` is new and fires in every mode before grading, and an exam slot now records a `responded` outcome so `onFinished` reports the finished set instead of never firing. (`SequenceItemOutcome` gains that third arm, so an exhaustive `switch` over it needs a new case.)
|
|
137
|
+
|
|
138
|
+
**Hiding a pane did not stop its media.** The pager keeps every question and stimulus mounted so answers survive back-navigation, and toggles visibility with `hidden` — which is `display: none`, and CSS does not touch playback. A recording started for one question carried on underneath the next, its controls removed from the page and from the accessibility tree, with no way to stop it but navigating back. Panes now pause their `<audio>`/`<video>` as they hide, preserving `currentTime`: a recording keeps its position between questions of its own group, stops on the way out, and never auto-resumes. A provider `embed` cannot be controlled this way and is documented as such. This affected per-question media too, since before item groups existed.
|
|
139
|
+
|
|
140
|
+
**Navigation put focus on the passage, not the question.** The stimulus was rendered inside the region labelled "Question 3 of 5", ahead of the question, so every Next dropped focus onto a landmark that began with the whole passage, keyboard users tabbed through it (audio controls included) before reaching the question, and a region sat nested inside a region. The stimulus is now a sibling of the question region: same visual order, still a landmark to jump back to, focus lands on the question.
|
|
141
|
+
|
|
142
|
+
**`onActivityComplete` could not identify what it had completed.** It reported the _presented_ index, which moves under shuffling and already differs from the slot identity once a group is present — while `composeAssessmentScore` keys on `slotId`, so persisted rows could not be reconciled with the grade. It now receives `slotId` as a third argument (existing two-parameter handlers keep compiling).
|
|
143
|
+
|
|
144
|
+
**A reordering set change could leave a sequence unfinishable.** Changing the presented order cleared the recorded outcomes but left the children mounted under unchanged keys — and therefore still `completed`. Those slots could not be answered again and nothing would refill them, so the set could never finish. A reset now resets both halves.
|
|
145
|
+
|
|
146
|
+
**Same-tick completions could be lost.** Outcomes were read from a `useState` closure, so two slots completing in one tick both saw the pre-update array and the second overwrote the first. They are held in a ref, written synchronously.
|
|
147
|
+
|
|
148
|
+
**Redacted groups did not type-check into the pager**, and the obvious fix does not work: `Renderable<T>` is built on `Omit`, which does not distribute over a union, so `Renderable<ActivityData>` collapses to the fields every activity shares and `.type` stops narrowing — widening the prop to it would have broken the pager's own dispatch and every custom renderer. `RenderableActivity` distributes properly (and fixes `ActivityProps`, where custom renderers could not narrow `data` either), and `asRenderableSequence` is the single documented crossing for `RedactedActivityData`, whose all-`unknown` fields satisfy no widening at all.
|
|
149
|
+
|
|
150
|
+
**`FillInTheBlanks` accepted redacted data in `practice` mode.** It would then call `score()` from the submit handler — where React error boundaries cannot reach — throwing after the learner had typed their answers. It now fails at render like `MultipleChoice` already did.
|
|
151
|
+
|
|
152
|
+
**`seededShuffle` gains an opt-in `version`.** Version 1 takes the Fisher–Yates index from its generator's low bits, which in an LCG are strongly correlated: on a four-option item only 12 of 24 orders are reachable _for any seed_, and the last authored option lands first 8% of the time against 42% second — a real fairness defect on an exam. Version 2 draws from the high bits and reaches every order uniformly. **Version 1 remains the default**, because the permutations are a wire contract: an attempt may be stored with only its seed, and a review render must reproduce what the learner saw, so changing the default is a package major. Use `{ version: 2 }` for new content.
|
|
153
|
+
|
|
154
|
+
**A stimulus no longer lends its language to the SDK's own text.** `lang` was set on the whole panel, so a screen reader read this package's English strings — the region's name, "Questions 3–5" — in the passage's voice (WCAG 3.1.2). It is now declared on the authored parts only.
|
|
155
|
+
|
|
156
|
+
### Patch Changes
|
|
157
|
+
|
|
158
|
+
- 339a5e0: Hardening: a typecheck gate, descriptor-driven xAPI interop, and an opt-in rounded item threshold.
|
|
159
|
+
|
|
160
|
+
**The test suites did not compile, and nothing noticed.** `tsc` only ever ran over `src/` as part of the build, so type errors in test files accumulated unseen — and a released defect (`redact()` being uncallable with the SDK's own exported interfaces) survived precisely because the suite that should have caught it was not type-checked. Both packages now have a `typecheck` script covering the full program, tests included, wired into `turbo` and CI. Getting to zero surfaced real problems rather than noise: matcher types that were never in the program (a `vitest-axe` augmentation the package does not ship), a fetch mock whose untyped parameters made `mock.calls[i][1]` an empty tuple so header assertions only compiled behind a cast, and a Node-only `Buffer` in a browser package's test.
|
|
161
|
+
|
|
162
|
+
**`xapiDefinitionFor(data)` (new).** `ActivityTypeDescriptor.interop` declares the activity-type IRI, the cmi.interaction type and how to derive `correctResponsesPattern` — and had **no reader anywhere**. Every renderer rebuilt the same strings inline, which is the same "declared but never used" defect this SDK has fixed elsewhere. The built-in components now read the descriptor, so a consumer-registered activity type gets correct xAPI interop with no component changes. Returns `{}` for an unregistered type, so it is always safe to spread.
|
|
163
|
+
|
|
164
|
+
**`computePassThreshold(data, score, rounding?)` — new optional third argument.** Item-level pass/fail still compared raw floats with `>=`, so an item displayed as "70%" could be recorded as a fail at 69.6 — exactly the disagreement `RoundingPolicy` eliminates for assessment totals. Passing a policy compares both sides rounded, via `gte`. It is **opt-in**: enabling it by default would change item-level pass/fail for any score inside the rounding band, and this SDK does not alter historical grades without an explicit decision. Absent, the comparison is bit-for-bit what it has always been.
|
|
165
|
+
|
|
166
|
+
- Updated dependencies [339a5e0]
|
|
167
|
+
- Updated dependencies [2d962aa]
|
|
168
|
+
- @intellectif/lk-core@0.5.0
|
|
169
|
+
|
|
170
|
+
## 3.0.0
|
|
171
|
+
|
|
172
|
+
### Minor Changes
|
|
173
|
+
|
|
174
|
+
- d08a518: The return trip for deferred grading: a grade that arrives later is now a first-class SDK value.
|
|
175
|
+
|
|
176
|
+
`evaluate()` could already say a submission was `deferred` — graded later by an AI or a human — but there was no type for the grade that comes **back**, so every consumer invented one and mirrored it by hand into their frontend. That gap is closed.
|
|
177
|
+
|
|
178
|
+
**New in `lk-core`:**
|
|
179
|
+
|
|
180
|
+
- **`GradeRecord`** — a scaled score, pass state and narrative feedback, plus the parts a rubric grader actually produces: `criteria` (per-criterion score or ordinal `band`, with comments and the weight applied), `corrections` (anchored in the learner's own text, with optional character offsets), `evidence`, `rationale`, `confidence`, `requiresHumanReview`, `grader` provenance (`kind`, `model`, `promptHash`) and token/cost `usage`. The shapes are the intersection of two independent production graders that converged on the same envelope.
|
|
181
|
+
- **`ItemOutcome` gains a `graded` arm** carrying the record, with `score` / `maxScore` / `passed` / `feedback` mirrored onto the outcome for uniform reads. `scored` continues to mean "the SDK computed this deterministically"; `graded` means "a grader returned it".
|
|
182
|
+
- **`gradeFromRubric(criteria, activity?, options?)`** — the weighted total as a **pure function of the grader's judgements**. A grader is asked for judgement, not mental arithmetic: if the model also returns the total, the grade becomes unverifiable and irreproducible, since two runs can disagree for identical criterion scores. Weights are normalised by their sum, so they need not add to 1. Criteria marked `notApplicable`, and band-only criteria with no numeric score, are excluded from **both** numerator and denominator. When nothing scoreable remains it returns `{ unscorable: true, reason }` — never a zero.
|
|
183
|
+
|
|
184
|
+
It also refuses out-of-contract input rather than turning it into a grade: a criterion whose score is `NaN` or infinite is **rejected, not silently dropped** (dropping it would regrade the learner on fewer criteria, with different effective weights, and nobody would know), and a score outside the scaled `[0,1]` range — a grader reporting raw points such as 4-out-of-5 — is rejected by name instead of being clamped, because silently rescaling someone's grader is worse than telling them it is out of contract. Float noise a hair outside the range is clamped.
|
|
185
|
+
|
|
186
|
+
- **`outcomeFromGrade(grade)`** and **`hasGrade(outcome)`**. Use `hasGrade` instead of testing `status === 'scored'`, which silently misses asynchronously graded work.
|
|
187
|
+
- **`XAPIVerb.SCORED`** for "a grade now exists", distinct from `answered` (which asserts the grade existed at submission time — here the learner acted earlier and the grade arrived later, often from a different actor).
|
|
188
|
+
|
|
189
|
+
**New in `lk-react`:** `<WrittenResponse renderMode="review" outcome={…} />` renders a returned grade — the percentage and pass state, narrative feedback, every criterion with its score or band and comment, inline corrections shown as `<del>`/`<ins>` pairs with explanations, and an awaiting-review affordance when `requiresHumanReview` is set. `notApplicable` criteria render as such rather than as zeros, and a `deferred` outcome still renders "not graded yet" rather than 0%.
|
|
190
|
+
|
|
191
|
+
Additive: `ItemOutcome` gained a union member, so an exhaustive `switch` over outcome statuses will need the new `graded` case.
|
|
192
|
+
|
|
193
|
+
- 2b1acbf: `ActivitySequence` gains a renderer registry and can finally run mixed sets containing a written response.
|
|
194
|
+
|
|
195
|
+
**Fixes a real defect:** a set mixing graded activities with a written response could **never complete**. The essay slot rendered a dead-end "not supported" note, so `onComplete` never fired — and any "section submitted / persist attempt / award XP" logic hanging off it silently never ran. Written responses are now dispatched to the real `<WrittenResponse>`.
|
|
196
|
+
|
|
197
|
+
**New `onFinished(items)`** — the general completion signal, reporting a `SequenceItemOutcome` per slot:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
type SequenceItemOutcome =
|
|
201
|
+
| {
|
|
202
|
+
kind: "scored";
|
|
203
|
+
index: number;
|
|
204
|
+
activityId: string;
|
|
205
|
+
result: ActivityResult;
|
|
206
|
+
}
|
|
207
|
+
| {
|
|
208
|
+
kind: "submitted";
|
|
209
|
+
index: number;
|
|
210
|
+
activityId: string;
|
|
211
|
+
submission: WrittenResponseSubmission;
|
|
212
|
+
};
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Two kinds, because two kinds of activity exist: those the SDK scores at submit time, and those a grader scores later. Collapsing them would mean inventing a score for ungraded work.
|
|
216
|
+
|
|
217
|
+
`onComplete` is unchanged and still promises `ActivityResult[]`, so it now fires **only when every slot was scored** — it cannot fire for a set containing a written response, because there is no honest `ActivityResult` to supply. Existing all-graded sequences behave exactly as before; use `onFinished` for mixed sets.
|
|
218
|
+
|
|
219
|
+
**New `renderers` prop — the React half of the activity-type registry.** `registerActivityType` already let a consumer define a custom type in `lk-core`; there was no way to put it on screen, so the type system was open in core and closed in React (original Requirement 15.4–15.6). Now:
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
<ActivitySequence activities={items} renderers={{ matching: MatchingItem }} />
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Renderers are keyed by `data.type` and receive the standard `ActivityProps`. A key matching a built-in **overrides** it, so a consumer can replace the bundled multiple-choice renderer without forking the sequencer. An unknown type with no registered renderer still degrades to an accessible note rather than crashing.
|
|
226
|
+
|
|
227
|
+
**New `renderMode` prop** on the sequence, forwarded to every child, so a whole set can be rendered in `exam` or `review` mode in one place.
|
|
228
|
+
|
|
229
|
+
### Patch Changes
|
|
230
|
+
|
|
231
|
+
- 0d62bca: Fix package resolution for CommonJS TypeScript consumers, and gate it in CI so it cannot regress.
|
|
232
|
+
|
|
233
|
+
**A CommonJS TypeScript project could not import these packages at all.** Both `exports` maps declared a single `types` condition pointing at the ESM `.d.ts`, which was then served to `require` as well. TypeScript under `node16`/`nodenext` resolution reported the declaration as ESM and failed the import with `TS1479` ("the current file is a CommonJS module whose imports will produce require calls"). `are-the-types-wrong` flagged every entrypoint as "Masquerading as ESM". The `types` condition is now nested inside each format, so `require` resolves the `.d.cts` declaration that was always being built:
|
|
234
|
+
|
|
235
|
+
```jsonc
|
|
236
|
+
".": {
|
|
237
|
+
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
|
|
238
|
+
"require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
This is a resolution fix only — no build change, no runtime change, no API change. ESM and bundler resolution were already correct and are unaffected. It matters most for server-side use: a Node grading service or queue worker compiled as CommonJS can now `import { evaluate, redact } from '@intellectif/lk-core'` directly, instead of working around it with `await import()` or switching `moduleResolution`.
|
|
243
|
+
|
|
244
|
+
**Raised the `zod` floor to `^3.25.1` (lk-core).** The previous range allowed `zod@3.25.0`, which declares a `./v4` export whose target files are absent from the published tarball. Since the schema layer imports `zod/v4`, any install that resolved exactly 3.25.0 failed with `MODULE_NOT_FOUND` in both ESM and CJS. No supported version is dropped — 3.25.0 was never functional here.
|
|
245
|
+
|
|
246
|
+
**New CI gate:** `publint` and `are-the-types-wrong` now run on every build (`pnpm check-packaging`). `node10` resolution is deliberately ignored — that is TypeScript before 4.7, and this SDK requires React 19.
|
|
247
|
+
|
|
248
|
+
- Updated dependencies [b96969b]
|
|
249
|
+
- Updated dependencies [d08a518]
|
|
250
|
+
- Updated dependencies [0d62bca]
|
|
251
|
+
- @intellectif/lk-core@0.4.0
|
|
252
|
+
|
|
253
|
+
## 2.1.0
|
|
254
|
+
|
|
255
|
+
### Minor Changes
|
|
256
|
+
|
|
257
|
+
- 69da998: Controlled components, render modes, redacted rendering and rich text — the release that lets an exam runner stop forking the SDK's components.
|
|
258
|
+
|
|
259
|
+
**Fixed: `shuffleSeed` was missing from the published types.** It worked at runtime but the public `MultipleChoice` wrapper was typed with the base `ActivityProps`, so the declaration dropped it and TypeScript consumers could not pass it without a cast. The wrapper is now typed with `MultipleChoiceProps`.
|
|
260
|
+
|
|
261
|
+
**Controlled components.** `value`, `defaultValue` and `onChange` on every activity, following the React convention: pass `value` + `onChange` to own the learner's answer — restore an in-progress attempt, autosave a delta, or drive a review. `defaultValue` seeds an uncontrolled mount. Passing neither reproduces the previous behaviour exactly.
|
|
262
|
+
|
|
263
|
+
**`renderMode: 'practice' | 'exam' | 'review'`** (named `renderMode`, not `mode`, because `MultipleChoiceData.mode` already means single/multi select):
|
|
264
|
+
|
|
265
|
+
- `practice` — the default and unchanged: the component scores locally, reveals correctness and feedback, emits xAPI, and calls `onComplete`.
|
|
266
|
+
- `exam` — the component **never** scores, **never** reads or reveals an answer-key field, and does not call `onComplete` or emit xAPI (a `correctResponsesPattern` is the answer key). Submitting calls the new `onSubmit(response)` with the raw response so the server can grade it.
|
|
267
|
+
- `review` — read-only, with no submit control. Correctness is marked **only** from a server-supplied `outcome: ItemOutcome`; the component never grades. A `deferred` outcome renders "not graded yet" rather than 0%, so an ungraded essay is never shown as a failure.
|
|
268
|
+
|
|
269
|
+
**Components render redacted data.** `data` now accepts a `redact()` projection, so the same component serves practice and exam. Use the exported `asRenderable<TData>(redacted)` helper to bridge `RedactedActivityData` into the `data` prop without a cast at your call site. Wiring a redacted item into `practice` mode throws immediately with an explanatory error rather than failing later inside the submit handler.
|
|
270
|
+
|
|
271
|
+
**Rich text, opt-in and fail-safe.** When you pass `sanitizeHtml`, `questionHtml` (multiple-choice) and `promptHtml` (written-response) render as HTML; without it, components render the escaped plain-text field as before. **The SDK ships no sanitiser on purpose** — that would be both a dependency and a false promise — so it can never inject HTML it was not explicitly handed a sanitiser for.
|
|
272
|
+
|
|
273
|
+
_Known limitation:_ the fill-in-the-blanks `passageHtml` is deliberately **not** rendered. The passage is the container of the `{{id}}` inputs, so honouring it would mean slicing sanitised HTML at each placeholder and re-parsing the fragments — which both destroys the authored block structure and voids the sanitiser's guarantee (a placeholder inside an attribute splits mid-attribute). Rendering rich passages safely needs a structured passage format, planned for a later release; until then FIB renders the plain passage.
|
|
274
|
+
|
|
275
|
+
New exports: `RenderMode`, `Renderable`, `HtmlSanitizer`, `asRenderable`, `MultipleChoiceProps`.
|
|
276
|
+
|
|
277
|
+
### Patch Changes
|
|
278
|
+
|
|
279
|
+
- Updated dependencies [69da998]
|
|
280
|
+
- @intellectif/lk-core@0.3.1
|
|
281
|
+
|
|
282
|
+
## 2.0.0
|
|
283
|
+
|
|
284
|
+
### Minor Changes
|
|
285
|
+
|
|
286
|
+
- a98f128: v1.1.0 — written-response component plus verified bug fixes. Additive API; two behavioral fixes are flagged below.
|
|
287
|
+
|
|
288
|
+
**New: `<WrittenResponse>`** (`@intellectif/lk-react/components/WrittenResponse`): labelled textarea with a live word counter and bounds messaging for asynchronously graded free writing. Completes via `onSubmitted(submission)` — `text`, `wordCount` (recomputed with `countWords`), `withinWordBounds`, `timeSpent`, and a SUBMITTED-verb xAPI statement with no score fields. Deliberately not `onComplete`: an ungraded submission never fabricates a score. Not yet dispatched inside `ActivitySequence` (its results contract is score-based; widening lands in v0.4) — a written-response item in a sequence renders an unsupported-type note.
|
|
289
|
+
|
|
290
|
+
**New: `shuffleSeed` prop on `MultipleChoice`.** Supply a seed (e.g. the attempt id) to make the shuffled order reproducible server-side, stable across reloads, and SSR-hydration-safe. Without it, behavior is unchanged (random per-mount order).
|
|
291
|
+
|
|
292
|
+
**Fixed:**
|
|
293
|
+
|
|
294
|
+
- `ActivitySequence` now resets its position and results when the activity **set** changes (B8): previously results leaked across sets, `onComplete` could fire with a mixed old/new results array, a shorter new set stranded the learner on an empty view, and the unkeyed child kept stale answer state. The child is now keyed per slot. The reset keys on the ordered activity **ids**, not the array reference — parents that rebuild a structurally identical array on every render (inline literals, `.map()` in render) keep their in-progress state.
|
|
295
|
+
- `crypto.randomUUID` is no longer called unconditionally on every `MultipleChoice` mount — it crashed on non-HTTPS origins (LAN/staging) even with shuffle off. It is now lazy (shuffle-without-seed only) with a non-crypto fallback.
|
|
296
|
+
- The embed `<iframe>` now actually has the `sandbox` attribute its documentation always claimed (`allow-scripts allow-same-origin allow-presentation`); combined with lk-core's new URL scheme allow-list this closes a stored-XSS vector.
|
|
297
|
+
- **Behavioral fix — `useXAPI` now applies the configured identity**: `XAPIConfig.actor` replaces the SDK's anonymous placeholder actor, and `XAPIConfig.activityId` replaces the SDK's `urn:learning-kit:activity:*` object id, before sending. This implements the contract that was documented but never wired (both config fields were dead). Conservative: statements whose actor/object id were already rewritten by the app are left untouched. If you relied on the anonymous actor reaching your LRS while also passing `actor` in config, statements will now carry the configured actor. A hook instance is per-activity by contract; for pages sending **several activities through one hook**, `activityId` now also accepts a mapper — `(sdkObjectId) => string` — so each question keeps a distinct IRI instead of collapsing onto one.
|
|
298
|
+
- xAPI statements now include `definition.type` + `interactionType` (+ `correctResponsesPattern`/`choices` for multiple-choice), and name language maps use the activity's `locale` instead of hardcoding `en-US`.
|
|
299
|
+
- Overall feedback is now taken from `ScoringResult.feedback` (selected by lk-core on `passed` — same visible behavior, one source of truth).
|
|
300
|
+
- The skin honors `prefers-reduced-motion`, and ships `.lk-wr*` styles for the new component.
|
|
301
|
+
|
|
302
|
+
**Packaging:** `engines.node >= 20`, LICENSE now ships in the tarball.
|
|
303
|
+
|
|
304
|
+
### Patch Changes
|
|
305
|
+
|
|
306
|
+
- Updated dependencies [a98f128]
|
|
307
|
+
- @intellectif/lk-core@0.3.0
|
|
308
|
+
|
|
309
|
+
## 1.0.1
|
|
310
|
+
|
|
311
|
+
### Patch Changes
|
|
312
|
+
|
|
313
|
+
- ed4a77d: Add a per-package README so visitors to npmjs.com see a focused overview, install command, quick-start example, subpath-export map, and links to the full documentation. No runtime change.
|
|
314
|
+
- Updated dependencies [ed4a77d]
|
|
315
|
+
- @intellectif/lk-core@0.2.1
|
|
316
|
+
|
|
317
|
+
## 1.0.0
|
|
318
|
+
|
|
319
|
+
### Minor Changes
|
|
320
|
+
|
|
321
|
+
- 9e24763: V1 feature set: Multiple Choice & Fill-in-the-Blanks activities, scoring engine,
|
|
322
|
+
xAPI builder + `useXAPI`, `useActivityState`, `ThemeProvider` with an optional
|
|
323
|
+
token-driven skin and `createTailwindTheme`, `ActivitySequence` question-set
|
|
324
|
+
pager, optional per-activity `media` (image/audio/video/embed) and authored
|
|
325
|
+
overall `feedback`, full WCAG 2.2 AA accessibility, and project documentation.
|
|
326
|
+
|
|
327
|
+
### Patch Changes
|
|
328
|
+
|
|
329
|
+
- Updated dependencies [9e24763]
|
|
330
|
+
- @intellectif/lk-core@0.2.0
|
package/README.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# @intellectif/lk-react
|
|
2
2
|
|
|
3
|
-
React 19 components, hooks, and theming for [learning-kit](https://github.com/intellectif/learning-kit): accessible
|
|
3
|
+
React 19 components, hooks, and theming for [learning-kit](https://github.com/intellectif/learning-kit): accessible activity components (Multiple Choice, Fill-in-the-Blanks, Written Response), a resumable in-place question-set pager with **exam** and **review** modes, a CSS-variable theming system with an optional skin, and an xAPI delivery hook.
|
|
4
4
|
|
|
5
|
-
A lightweight, composable, bring-your-own-backend alternative to H5P
|
|
5
|
+
A lightweight, composable, bring-your-own-backend alternative to H5P — and unlike a
|
|
6
|
+
practice-quiz widget, it is built to render a **summative** paper: in `exam` mode the
|
|
7
|
+
components never score, never reveal correctness, and are safe to hand a `redact()`
|
|
8
|
+
projection that carries no answer key.
|
|
6
9
|
|
|
7
10
|
```bash
|
|
8
11
|
pnpm add @intellectif/lk-react @intellectif/lk-core react react-dom
|
|
@@ -62,11 +65,18 @@ export function Demo() {
|
|
|
62
65
|
|
|
63
66
|
## Activities & features
|
|
64
67
|
|
|
65
|
-
- **`<MultipleChoice>`** — single / multi select, all-or-nothing or partial scoring,
|
|
68
|
+
- **`<MultipleChoice>`** — single / multi select, all-or-nothing or partial scoring, per-option `feedback`, and a deterministic option shuffle seeded by `shuffleSeed` (without one the order is stable for the life of the mount only, and is not reproducible afterwards).
|
|
66
69
|
- **`<FillInTheBlanks>`** — `{{id}}` placeholders, case/whitespace options, per-blank `hint` (Show/Hide toggle as an icon), per-blank `feedback` shown inline on submit with a learner-controlled **Hide/Show feedback** toggle, optional `showCorrectAnswers`.
|
|
67
70
|
- **`<WrittenResponse>`** — free-text writing with a live word counter and bounds messaging, graded **asynchronously**: it emits an ungraded submission (never a fake zero) and a SUBMITTED-verb xAPI statement, and renders a returned `GradeRecord` in `review` mode with per-criterion scores and inline corrections.
|
|
68
|
-
- **`<ActivitySequence>`** — in-place "question set" pager (Previous/Next, "Question X of N", no scrolling, focus-managed). Accepts item groups and keeps their stimulus beside every question; `onSubmit` reports every raw answer with its `slotId`, which is the only response channel an `exam` sequence has.
|
|
71
|
+
- **`<ActivitySequence>`** — in-place "question set" pager (Previous/Next, "Question X of N", no scrolling, focus-managed). Accepts item groups and keeps their stimulus beside every question; `onSubmit` reports every raw answer with its `slotId`, which is the only response channel an `exam` sequence has. `onFinished` is the completion signal for a set that mixes scored and deferred-graded items, where `onComplete` can never fire.
|
|
72
|
+
- **Resume and review a whole attempt** — `<ActivitySequence>` takes `defaultIndex`, `responses` and `submittedSlotIds` (read at mount; remount with a `key` to show a different attempt), so an interrupted paper reopens on the right question with the right answers and already-committed questions still committed. `onIndexChange` reports every position the pager lands on, including a clamp it had to apply. Pass `renderMode="review"` plus server-computed `outcomes` to render a finished attempt read-only. Pairs with `serializeAttemptState` / `restoreAttemptState` in `lk-core`.
|
|
73
|
+
- **`renderMode`** — `practice` (default: the component scores locally and reveals correctness), `exam` (never scores, never reveals; submit emits the raw response for the server to grade), `review` (read-only, marks correctness only from an `outcome` you supply). This is the single switch that takes grading off the client.
|
|
74
|
+
- **Controlled or uncontrolled** — every activity component follows the React convention: `defaultValue` to seed, `value` + `onChange` to own the answer outright, `defaultSubmitted` to mount an already-committed question as committed.
|
|
75
|
+
- **`asRenderable()` / `asRenderableSequence()`** — the one documented bridge from a server's `redact()` projection to the `data` prop, so `as unknown as` stays out of your code.
|
|
76
|
+
- **`shuffleSeed`** — one seed drives question order *and* each item's option order, so a review render reproduces exactly the arrangement the learner sat. Shuffling in `exam` / `review` mode **requires** it.
|
|
69
77
|
- **`<StimulusPanel>`** — a shared passage / recording / image (an item group's stimulus) as a landmark region; the sequence uses it, and a custom runner can too.
|
|
78
|
+
- **Custom activity types** — `renderers={{ 'my-type': MyRenderer }}` on the sequence; a key matching a built-in overrides it, so you can replace a bundled renderer without forking the sequencer.
|
|
79
|
+
- **Rich text, opt-in** — `sanitizeHtml` renders author-supplied `questionHtml` / `promptHtml`. The SDK ships **no** sanitiser and injects no HTML without one; without it the escaped plain-text field is used. (`FillInTheBlanks` ignores `passageHtml` by design — its passage hosts the answer inputs.)
|
|
70
80
|
- **Media per question** — optional `image` / `audio` / `video` / `embed` (YouTube/Vimeo iframe) above the question; alt-text required for `image`/`embed` (WCAG).
|
|
71
81
|
- **Activity-level overall feedback** — `{ correct, incorrect }` shown after submit (h5p "Overall Feedback" parity).
|
|
72
82
|
- **`useXAPI(config)`** — fire-and-forget LRS delivery with retry/backoff for 5xx/network (1 s / 2 s / 4 s), immediate fail on 4xx, never throws.
|
|
@@ -92,22 +102,43 @@ export function Demo() {
|
|
|
92
102
|
| `@intellectif/lk-react/theme/defaults.css` | Tokens (required) |
|
|
93
103
|
| `@intellectif/lk-react/theme/skin.css` | Optional polished skin |
|
|
94
104
|
|
|
95
|
-
ESM + CJS + `.d.ts` for every entry
|
|
105
|
+
ESM + CJS + `.d.ts` for every JS entry (the two `.css` entries are plain stylesheets).
|
|
106
|
+
Tree-shakeable. Node >= 20, React `^19`.
|
|
96
107
|
|
|
97
|
-
|
|
108
|
+
`asRenderable`, `asRenderableSequence`, `RenderMode`, `ActivityProps` and the other
|
|
109
|
+
shared types are exported from the **barrel** (`@intellectif/lk-react`); they have no
|
|
110
|
+
subpath of their own.
|
|
111
|
+
|
|
112
|
+
## Capabilities & limitations
|
|
98
113
|
|
|
99
114
|
| Concern | Behavior |
|
|
100
115
|
|---|---|
|
|
101
|
-
| **Scoring** | Pure & deterministic
|
|
102
|
-
| **
|
|
103
|
-
| **
|
|
104
|
-
| **
|
|
116
|
+
| **Scoring** | Pure & deterministic, in `lk-core`; per-activity `all-or-nothing` and `partial`. Weighted totals across items and sections are `composeAssessmentScore`; per-option weighting inside one activity is not implemented. |
|
|
117
|
+
| **Grading on the client** | `practice` only. `exam` and `review` never score and never reveal correctness — pass a `redact()` projection and grade server-side. |
|
|
118
|
+
| **Feedback** | Per-item (MC per-option, FIB per-blank) shown inline on submit with Hide/Show toggle, + activity-level overall. Score-band feedback is not implemented. |
|
|
119
|
+
| **Retry** | Pass a new `data` reference or change the React `key` → the activity resets to **its seeded state**, not to blank. With `defaultValue` / `defaultSubmitted` set, a new `key` and a new `data` reference are indistinguishable: both return the restored answer, still locked as submitted. For a genuinely fresh attempt, change the `key` **and stop passing the seeds**. No built-in button — retry *policy* is yours. |
|
|
120
|
+
| **Persistence / resume** | Storage is yours — capture `onSubmit` / `onChange` / `onInteraction` / `onIndexChange` and persist as you wish. Re-hydration is supported at **both** levels: `defaultValue` / `value` + `onChange` on a component (since 2.1.0), and `defaultIndex` / `responses` / `submittedSlotIds` on `<ActivitySequence>` (since 6.0.0). |
|
|
121
|
+
| **Rich text** | Rendered only when you pass `sanitizeHtml`; the SDK bundles no sanitiser and injects no HTML without one. `FillInTheBlanks` deliberately **ignores** `passageHtml` — the passage hosts the answer inputs, so it is built from `passage` plus the blanks (dev-mode warning if you pass it). |
|
|
122
|
+
| **i18n** | `locale` sets the `lang` attribute on the rendered region and names xAPI statements. UI strings are English and not yet overridable, and there is no RTL-specific styling. |
|
|
123
|
+
| **Media** | `<audio>` / `<video>` are paused when the pager navigates away, preserving `currentTime` so a group resumes where the learner left it; nothing ever auto-plays. A provider `embed` (iframe) **cannot** be paused this way — controlling a third-party player needs its own JS API. Use `audio` / `video` for anything that must stop when the learner navigates. |
|
|
105
124
|
| **Authoring / content storage / CDN / auth** | Consumer responsibility — typed schemas + `validateActivity` + JSON Schema export are provided for you to build authoring on. |
|
|
106
|
-
| **SSR / RSC** | Fully supported
|
|
125
|
+
| **SSR / RSC** | Fully supported; every component carries `'use client'`. |
|
|
126
|
+
|
|
127
|
+
### Versioning note
|
|
128
|
+
|
|
129
|
+
`@intellectif/lk-core` is a **peer** dependency, so widening its range is a breaking
|
|
130
|
+
change for installs and forces a `lk-react` major. Some majors here — **5.0.0**
|
|
131
|
+
is the clearest — are exactly that and change no React API. Others do: 6.0.0
|
|
132
|
+
added five `<ActivitySequence>` props, a `restored` arm to `SequenceItemOutcome`
|
|
133
|
+
that an exhaustive `switch` must handle, and a changed reset target for
|
|
134
|
+
`<MultipleChoice>`. Always read the
|
|
135
|
+
[CHANGELOG](https://github.com/intellectif/learning-kit/blob/main/packages/lk-react/CHANGELOG.md)
|
|
136
|
+
before assuming a migration is needed.
|
|
107
137
|
|
|
108
138
|
## Documentation
|
|
109
139
|
|
|
110
|
-
- [Upgrading](https://github.com/intellectif/learning-kit/blob/main/docs/upgrading.md) — start here
|
|
140
|
+
- [Upgrading](https://github.com/intellectif/learning-kit/blob/main/docs/upgrading.md) — start here on any upgrade from 2.x, 3.x, 4.x or 5.x.
|
|
141
|
+
- [Changelog](https://github.com/intellectif/learning-kit/blob/main/packages/lk-react/CHANGELOG.md) — every release, with the reasoning.
|
|
111
142
|
- [Authoring & content storage](https://github.com/intellectif/learning-kit/blob/main/docs/authoring.md)
|
|
112
143
|
- [Styling](https://github.com/intellectif/learning-kit/blob/main/docs/styling.md) — tokens, the skin, overrides, dark mode, Tailwind.
|
|
113
144
|
- [Project README](https://github.com/intellectif/learning-kit#readme) — full picture & monorepo layout.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { XAPIStatement, WrittenResponseData, LearnerResponse, ItemOutcome, InteractionEvent, ThemeTokens } from '@intellectif/lk-core';
|
|
2
|
-
import { a as Renderable, R as RenderMode, H as HtmlSanitizer } from './types-
|
|
2
|
+
import { a as Renderable, R as RenderMode, H as HtmlSanitizer } from './types-9iS1Vs2I.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Payload delivered when the learner submits a written response. Grading is
|
|
@@ -37,7 +37,7 @@ interface WrittenResponseProps {
|
|
|
37
37
|
* count, bounds flag, timing, xAPI statement). Fires in `practice` and
|
|
38
38
|
* `exam` mode, after `onSubmit`; never in `review`.
|
|
39
39
|
*
|
|
40
|
-
* Optional since
|
|
40
|
+
* Optional since lk-react 2.1.0 so a `review`-mode render (which has no submit
|
|
41
41
|
* control) and an exam runner that only wants the raw response through
|
|
42
42
|
* `onSubmit` do not have to pass a no-op. Supply it in `practice`/`exam`
|
|
43
43
|
* unless `onSubmit` already captures everything you persist.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { XAPIStatement, WrittenResponseData, LearnerResponse, ItemOutcome, InteractionEvent, ThemeTokens } from '@intellectif/lk-core';
|
|
2
|
-
import { a as Renderable, R as RenderMode, H as HtmlSanitizer } from './types-
|
|
2
|
+
import { a as Renderable, R as RenderMode, H as HtmlSanitizer } from './types-9iS1Vs2I.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Payload delivered when the learner submits a written response. Grading is
|
|
@@ -37,7 +37,7 @@ interface WrittenResponseProps {
|
|
|
37
37
|
* count, bounds flag, timing, xAPI statement). Fires in `practice` and
|
|
38
38
|
* `exam` mode, after `onSubmit`; never in `review`.
|
|
39
39
|
*
|
|
40
|
-
* Optional since
|
|
40
|
+
* Optional since lk-react 2.1.0 so a `review`-mode render (which has no submit
|
|
41
41
|
* control) and an exam runner that only wants the raw response through
|
|
42
42
|
* `onSubmit` do not have to pass a no-op. Supply it in `practice`/`exam`
|
|
43
43
|
* unless `onSubmit` already captures everything you persist.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { useRef, useCallback } from 'react';
|
|
2
3
|
|
|
3
4
|
// src/hooks/useXAPI.ts
|
|
@@ -75,5 +76,5 @@ function useXAPI(config) {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
export { useXAPI };
|
|
78
|
-
//# sourceMappingURL=chunk-
|
|
79
|
-
//# sourceMappingURL=chunk-
|
|
79
|
+
//# sourceMappingURL=chunk-2TW6WLBC.js.map
|
|
80
|
+
//# sourceMappingURL=chunk-2TW6WLBC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/useXAPI.ts"],"names":[],"mappings":";;;AAMA,IAAM,UAAA,GAAa,CAAC,GAAA,EAAM,GAAA,EAAM,GAAI,CAAA;AAYpC,SAAS,aAAA,CAAc,WAA0B,GAAA,EAAgC;AAC/E,EAAA,MAAM,WAAA,GACJ,UAAU,KAAA,EAAO,OAAA,EAAS,SAAS,WAAA,IACnC,SAAA,CAAU,KAAA,CAAM,OAAA,CAAQ,QAAA,KAAa,6CAAA;AACvC,EAAA,MAAM,aAAA,GACJ,SAAA,CAAU,MAAA,EAAQ,UAAA,KAAe,cACjC,OAAO,SAAA,CAAU,MAAA,CAAO,EAAA,KAAO,QAAA,IAC/B,SAAA,CAAU,MAAA,CAAO,EAAA,CAAG,WAAW,4BAA4B,CAAA;AAE7D,EAAA,IAAI,CAAC,WAAA,IAAe,CAAC,aAAA,EAAe;AAClC,IAAA,OAAO,SAAA;AAAA,EACT;AACA,EAAA,MAAM,QAAA,GACJ,aAAA,IAAiB,GAAA,CAAI,UAAA,GACjB,OAAO,GAAA,CAAI,UAAA,KAAe,UAAA,GACxB,GAAA,CAAI,WAAW,SAAA,CAAU,MAAA,CAAO,EAAE,CAAA,GAClC,IAAI,UAAA,GACN,MAAA;AACN,EAAA,OAAO;AAAA,IACL,GAAG,SAAA;AAAA,IACH,GAAI,WAAA,IAAe,GAAA,CAAI,KAAA,KAAU,MAAA,GAAY,EAAE,KAAA,EAAO,GAAA,CAAI,KAAA,EAAM,GAAI,EAAC;AAAA,IACrE,GAAI,QAAA,KAAa,MAAA,GAAY,EAAE,MAAA,EAAQ,EAAE,GAAG,SAAA,CAAU,MAAA,EAAQ,EAAA,EAAI,QAAA,EAAS,KAAM;AAAC,GACpF;AACF;AAGA,SAAS,aAAa,KAAA,EAAuB;AAC3C,EAAA,MAAM,KAAA,GAAQ,IAAI,WAAA,EAAY,CAAE,OAAO,KAAK,CAAA;AAC5C,EAAA,IAAI,MAAA,GAAS,EAAA;AACb,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,MAAA,IAAU,MAAA,CAAO,aAAa,IAAI,CAAA;AAAA,EACpC;AACA,EAAA,OAAO,KAAK,MAAM,CAAA;AACpB;AAEA,SAAS,WAAW,IAAA,EAAkC;AACpD,EAAA,OAAO,KAAK,IAAA,KAAS,OAAA,GACjB,CAAA,MAAA,EAAS,YAAA,CAAa,GAAG,IAAA,CAAK,QAAQ,CAAA,CAAA,EAAI,IAAA,CAAK,QAAQ,CAAA,CAAE,CAAC,CAAA,CAAA,GAC1D,CAAA,OAAA,EAAU,KAAK,KAAK,CAAA,CAAA;AAC1B;AAEA,IAAM,KAAA,GAAQ,CAAC,EAAA,KAA8B,IAAI,OAAA,CAAQ,CAAC,OAAA,KAAY,UAAA,CAAW,OAAA,EAAS,EAAE,CAAC,CAAA;AA4CtF,SAAS,QAAQ,MAAA,EAAmC;AAGzD,EAAA,MAAM,SAAA,GAAY,OAAO,MAAM,CAAA;AAC/B,EAAA,SAAA,CAAU,OAAA,GAAU,MAAA;AAEpB,EAAA,MAAM,aAAA,GAAgB,WAAA,CAAY,OAAO,YAAA,KAA+C;AACtF,IAAA,MAAM,MAAM,SAAA,CAAU,OAAA;AACtB,IAAA,MAAM,SAAA,GAAY,aAAA,CAAc,YAAA,EAAc,GAAG,CAAA;AACjD,IAAA,MAAM,WAAA,GAAc,IAAI,UAAA,CAAW,MAAA;AAEnC,IAAA,KAAA,IAAS,OAAA,GAAU,CAAA,EAAG,OAAA,IAAW,WAAA,EAAa,WAAW,CAAA,EAAG;AAC1D,MAAA,IAAI,UAAA;AACJ,MAAA,IAAI,OAAA,GAAU,gBAAA;AAEd,MAAA,IAAI;AACF,QAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,CAAI,QAAA,EAAU;AAAA,UACzC,MAAA,EAAQ,MAAA;AAAA,UACR,OAAA,EAAS;AAAA,YACP,cAAA,EAAgB,kBAAA;AAAA,YAChB,0BAAA,EAA4B,OAAA;AAAA,YAC5B,aAAA,EAAe,UAAA,CAAW,GAAA,CAAI,IAAI;AAAA,WACpC;AAAA,UACA,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,SAAS;AAAA,SAC/B,CAAA;AAED,QAAA,IAAI,SAAS,EAAA,EAAI;AACf,UAAA;AAAA,QACF;AAEA,QAAA,UAAA,GAAa,QAAA,CAAS,MAAA;AACtB,QAAA,OAAA,GAAU,CAAA,mBAAA,EAAsB,SAAS,MAAM,CAAA,CAAA;AAI/C,QAAA,IAAI,QAAA,CAAS,SAAS,GAAA,EAAK;AACzB,UAAA,GAAA,CAAI,UAAU,EAAE,SAAA,EAAW,OAAA,EAAS,UAAA,EAAY,SAAS,CAAA;AACzD,UAAA;AAAA,QACF;AAAA,MACF,SAAS,KAAA,EAAO;AACd,QAAA,OAAA,GAAU,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,eAAA;AAAA,MACrD;AAEA,MAAA,IAAI,YAAY,WAAA,EAAa;AAC3B,QAAA,GAAA,CAAI,OAAA,GAAU;AAAA,UACZ,SAAA;AAAA,UACA,OAAA;AAAA,UACA,GAAI,UAAA,KAAe,MAAA,GAAY,EAAE,UAAA,KAAe,EAAC;AAAA,UACjD;AAAA,SACD,CAAA;AACD,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,KAAA,CAAM,UAAA,CAAW,OAAA,GAAU,CAAC,CAAC,CAAA;AAAA,IACrC;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,OAAO,EAAE,aAAA,EAAc;AACzB","file":"chunk-2TW6WLBC.js","sourcesContent":["'use client';\n\nimport type { XAPIConfig, XAPIStatement } from '@intellectif/lk-core';\nimport { useCallback, useRef } from 'react';\n\n/** Backoff before retry 1 / 2 / 3 (Req 10.4). */\nconst BACKOFF_MS = [1000, 2000, 4000] as const;\n\n/**\n * Applies the configured learner identity to a statement built by an activity\n * component. Components emit an anonymous placeholder actor and a\n * `urn:learning-kit:activity:*` object id (they cannot know identity — Req\n * 3.1); this hook is the documented place where `XAPIConfig.actor` and\n * `XAPIConfig.activityId` take over. Conservative on purpose: the actor is\n * replaced only when the statement still carries the SDK's anonymous\n * placeholder, and the object id only when it is still the SDK URN — a\n * consumer that already rewrote either keeps its own values.\n */\nfunction applyIdentity(statement: XAPIStatement, cfg: XAPIConfig): XAPIStatement {\n const isAnonymous =\n statement.actor?.account?.name === 'anonymous' &&\n statement.actor.account.homePage === 'https://github.com/intellectif/learning-kit';\n const isSdkObjectId =\n statement.object?.objectType === 'Activity' &&\n typeof statement.object.id === 'string' &&\n statement.object.id.startsWith('urn:learning-kit:activity:');\n\n if (!isAnonymous && !isSdkObjectId) {\n return statement;\n }\n const mappedId =\n isSdkObjectId && cfg.activityId\n ? typeof cfg.activityId === 'function'\n ? cfg.activityId(statement.object.id)\n : cfg.activityId\n : undefined;\n return {\n ...statement,\n ...(isAnonymous && cfg.actor !== undefined ? { actor: cfg.actor } : {}),\n ...(mappedId !== undefined ? { object: { ...statement.object, id: mappedId } } : {}),\n };\n}\n\n/** UTF-8-safe base64 (`btoa` is Latin1-only and breaks on non-ASCII creds). */\nfunction toBase64Utf8(input: string): string {\n const bytes = new TextEncoder().encode(input);\n let binary = '';\n for (const byte of bytes) {\n binary += String.fromCharCode(byte);\n }\n return btoa(binary);\n}\n\nfunction authHeader(auth: XAPIConfig['auth']): string {\n return auth.type === 'basic'\n ? `Basic ${toBase64Utf8(`${auth.username}:${auth.password}`)}`\n : `Bearer ${auth.token}`;\n}\n\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\nexport interface UseXAPIResult {\n /**\n * Sends a statement to the configured LRS. **Never rejects** — xAPI is\n * fire-and-forget telemetry that must not break the learner's activity\n * flow. Network errors and 5xx responses are retried (backoff 1s/2s/4s,\n * up to 3 retries); a 4xx fails immediately. Any final failure is delivered\n * to `config.onError`.\n */\n sendStatement: (statement: XAPIStatement) => Promise<void>;\n}\n\n/**\n * Delivers xAPI statements to a Learning Record Store, fire-and-forget.\n *\n * Telemetry must never break the learner's attempt, so\n * {@link UseXAPIResult.sendStatement} **never rejects**: network errors and 5xx\n * responses are retried (backoff 1s / 2s / 4s, up to 3 retries), a 4xx fails\n * immediately, and any final failure is reported to `config.onError` instead of\n * throwing. See `XAPIConfig` in `@intellectif/lk-core` for the options.\n *\n * The hook is also where learner identity is applied. Activity components\n * cannot know who the learner is, so they emit an anonymous placeholder actor\n * and a `urn:learning-kit:activity:*` object id; this hook substitutes\n * `config.actor` and `config.activityId` on the way out — and only while those\n * placeholders are still in place, so a statement you rewrote yourself is left\n * alone.\n *\n * `sendStatement` keeps a stable identity across renders even when `config` is\n * an inline object, so it is safe in a dependency array.\n *\n * ```tsx\n * const { sendStatement } = useXAPI({\n * endpoint: 'https://your-lrs.example/xapi/statements',\n * auth: { type: 'bearer', token: 'YOUR_TOKEN' },\n * activityId: 'https://your-app.example/quiz/capital-jp',\n * actor: { objectType: 'Agent', mbox: 'mailto:learner@example.com' },\n * onError: (e) => console.error('xAPI send failed', e),\n * });\n *\n * <MultipleChoice data={quiz} onComplete={(r) => void sendStatement(r.xapiStatement)} />\n * ```\n */\nexport function useXAPI(config: XAPIConfig): UseXAPIResult {\n // Keep the latest config without changing `sendStatement`'s identity when\n // callers pass an inline config object.\n const configRef = useRef(config);\n configRef.current = config;\n\n const sendStatement = useCallback(async (rawStatement: XAPIStatement): Promise<void> => {\n const cfg = configRef.current;\n const statement = applyIdentity(rawStatement, cfg);\n const maxAttempts = 1 + BACKOFF_MS.length;\n\n for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {\n let statusCode: number | undefined;\n let message = 'Request failed';\n\n try {\n const response = await fetch(cfg.endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Experience-API-Version': '1.0.3',\n Authorization: authHeader(cfg.auth),\n },\n body: JSON.stringify(statement),\n });\n\n if (response.ok) {\n return;\n }\n\n statusCode = response.status;\n message = `LRS responded with ${response.status}`;\n\n // Only network errors and 5xx are retryable (Req 10.4). A 4xx is a\n // client error — fail immediately rather than waste retries.\n if (response.status < 500) {\n cfg.onError?.({ statement, attempt, statusCode, message });\n return;\n }\n } catch (error) {\n message = error instanceof Error ? error.message : 'Network error';\n }\n\n if (attempt === maxAttempts) {\n cfg.onError?.({\n statement,\n attempt,\n ...(statusCode !== undefined ? { statusCode } : {}),\n message,\n });\n return;\n }\n\n await sleep(BACKOFF_MS[attempt - 1]);\n }\n }, []);\n\n return { sendStatement };\n}\n"]}
|
package/dist/chunk-4HZ7LQIH.cjs
CHANGED
package/dist/chunk-57E3E2H4.js
CHANGED
package/dist/chunk-6F4X2NKL.js
CHANGED
package/dist/chunk-6N43WDVG.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
'use strict';
|
|
2
3
|
|
|
3
4
|
var react = require('react');
|
|
@@ -57,6 +58,34 @@ var defaultTheme = {
|
|
|
57
58
|
"--lk-transition-fast": "150ms ease",
|
|
58
59
|
"--lk-transition-base": "250ms ease"
|
|
59
60
|
};
|
|
61
|
+
|
|
62
|
+
// src/theme/tailwind.ts
|
|
63
|
+
var BUCKETS = [
|
|
64
|
+
["--lk-color-", "colors"],
|
|
65
|
+
["--lk-spacing-", "spacing"],
|
|
66
|
+
["--lk-radius-", "borderRadius"],
|
|
67
|
+
["--lk-font-family-", "fontFamily"],
|
|
68
|
+
["--lk-font-size-", "fontSize"]
|
|
69
|
+
];
|
|
70
|
+
function createTailwindTheme(theme) {
|
|
71
|
+
const merged = { ...defaultTheme, ...theme };
|
|
72
|
+
const extension = {
|
|
73
|
+
colors: {},
|
|
74
|
+
spacing: {},
|
|
75
|
+
borderRadius: {},
|
|
76
|
+
fontFamily: {},
|
|
77
|
+
fontSize: {}
|
|
78
|
+
};
|
|
79
|
+
for (const [tokenName, fallback] of Object.entries(merged)) {
|
|
80
|
+
for (const [prefix, bucket] of BUCKETS) {
|
|
81
|
+
if (tokenName.startsWith(prefix)) {
|
|
82
|
+
extension[bucket][`lk-${tokenName.slice(prefix.length)}`] = `var(${tokenName}, ${fallback})`;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return extension;
|
|
88
|
+
}
|
|
60
89
|
var ThemeContext = react.createContext(defaultTheme);
|
|
61
90
|
var DARK_QUERY = "(prefers-color-scheme: dark)";
|
|
62
91
|
function subscribePrefersDark(onChange) {
|
|
@@ -89,8 +118,9 @@ function useTheme() {
|
|
|
89
118
|
}
|
|
90
119
|
|
|
91
120
|
exports.ThemeProvider = ThemeProvider;
|
|
121
|
+
exports.createTailwindTheme = createTailwindTheme;
|
|
92
122
|
exports.darkTheme = darkTheme;
|
|
93
123
|
exports.defaultTheme = defaultTheme;
|
|
94
124
|
exports.useTheme = useTheme;
|
|
95
|
-
//# sourceMappingURL=chunk-
|
|
96
|
-
//# sourceMappingURL=chunk-
|
|
125
|
+
//# sourceMappingURL=chunk-AMGK7DDM.cjs.map
|
|
126
|
+
//# sourceMappingURL=chunk-AMGK7DDM.cjs.map
|