@intellectif/lk-react 6.1.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +2 -0
  3. package/dist/{WrittenResponse-BXHidIaK.d.cts → WrittenResponse-BJL4dGsR.d.ts} +5 -1
  4. package/dist/{WrittenResponse-DJYGuMKk.d.ts → WrittenResponse-zWT-Go1g.d.cts} +5 -1
  5. package/dist/chunk-6ZKVJAO6.js +540 -0
  6. package/dist/chunk-6ZKVJAO6.js.map +1 -0
  7. package/dist/chunk-A572N5G7.cjs +542 -0
  8. package/dist/chunk-A572N5G7.cjs.map +1 -0
  9. package/dist/{chunk-UQ3BBEIT.js → chunk-BG456EM3.js} +21 -5
  10. package/dist/chunk-BG456EM3.js.map +1 -0
  11. package/dist/{chunk-BE7R2X3S.cjs → chunk-BNESCXJ5.cjs} +16 -4
  12. package/dist/chunk-BNESCXJ5.cjs.map +1 -0
  13. package/dist/{chunk-MDZMKC27.js → chunk-CMZGT26S.js} +21 -4
  14. package/dist/chunk-CMZGT26S.js.map +1 -0
  15. package/dist/{chunk-57E3E2H4.js → chunk-J23XVR2B.js} +16 -4
  16. package/dist/chunk-J23XVR2B.js.map +1 -0
  17. package/dist/{chunk-DUIWK272.cjs → chunk-J4LP4CLY.cjs} +109 -29
  18. package/dist/chunk-J4LP4CLY.cjs.map +1 -0
  19. package/dist/{chunk-NEWGUDA5.cjs → chunk-KK4WGW2V.cjs} +21 -5
  20. package/dist/chunk-KK4WGW2V.cjs.map +1 -0
  21. package/dist/{chunk-MDYKYDRN.cjs → chunk-KPI335JJ.cjs} +21 -4
  22. package/dist/chunk-KPI335JJ.cjs.map +1 -0
  23. package/dist/{chunk-ZLUMUJUQ.js → chunk-MB532SXM.js} +16 -4
  24. package/dist/chunk-MB532SXM.js.map +1 -0
  25. package/dist/{chunk-NPY2F7F6.cjs → chunk-VXOOGQ6W.cjs} +16 -4
  26. package/dist/chunk-VXOOGQ6W.cjs.map +1 -0
  27. package/dist/{chunk-IUODGQXE.js → chunk-ZPVMB3LH.js} +107 -27
  28. package/dist/chunk-ZPVMB3LH.js.map +1 -0
  29. package/dist/components/ActivitySequence.cjs +7 -7
  30. package/dist/components/ActivitySequence.d.cts +13 -15
  31. package/dist/components/ActivitySequence.d.ts +13 -15
  32. package/dist/components/ActivitySequence.js +6 -6
  33. package/dist/components/FillInTheBlanks.cjs +3 -3
  34. package/dist/components/FillInTheBlanks.d.cts +1 -1
  35. package/dist/components/FillInTheBlanks.d.ts +1 -1
  36. package/dist/components/FillInTheBlanks.js +2 -2
  37. package/dist/components/MultipleChoice.cjs +3 -3
  38. package/dist/components/MultipleChoice.d.cts +1 -1
  39. package/dist/components/MultipleChoice.d.ts +1 -1
  40. package/dist/components/MultipleChoice.js +2 -2
  41. package/dist/components/StimulusPanel.cjs +3 -3
  42. package/dist/components/StimulusPanel.d.cts +19 -3
  43. package/dist/components/StimulusPanel.d.ts +19 -3
  44. package/dist/components/StimulusPanel.js +2 -2
  45. package/dist/components/WrittenResponse.cjs +3 -3
  46. package/dist/components/WrittenResponse.d.cts +3 -3
  47. package/dist/components/WrittenResponse.d.ts +3 -3
  48. package/dist/components/WrittenResponse.js +2 -2
  49. package/dist/index.cjs +11 -11
  50. package/dist/index.cjs.map +1 -1
  51. package/dist/index.d.cts +2 -2
  52. package/dist/index.d.ts +2 -2
  53. package/dist/index.js +6 -6
  54. package/dist/index.js.map +1 -1
  55. package/dist/theme/skin.css +204 -1
  56. package/dist/{types-9iS1Vs2I.d.ts → types-eXX4hzSj.d.cts} +128 -2
  57. package/dist/{types-9iS1Vs2I.d.cts → types-eXX4hzSj.d.ts} +128 -2
  58. package/package.json +3 -3
  59. package/dist/chunk-4HZ7LQIH.cjs +0 -41
  60. package/dist/chunk-4HZ7LQIH.cjs.map +0 -1
  61. package/dist/chunk-57E3E2H4.js.map +0 -1
  62. package/dist/chunk-BE7R2X3S.cjs.map +0 -1
  63. package/dist/chunk-DUIWK272.cjs.map +0 -1
  64. package/dist/chunk-GB5URWM4.js +0 -39
  65. package/dist/chunk-GB5URWM4.js.map +0 -1
  66. package/dist/chunk-IUODGQXE.js.map +0 -1
  67. package/dist/chunk-MDYKYDRN.cjs.map +0 -1
  68. package/dist/chunk-MDZMKC27.js.map +0 -1
  69. package/dist/chunk-NEWGUDA5.cjs.map +0 -1
  70. package/dist/chunk-NPY2F7F6.cjs.map +0 -1
  71. package/dist/chunk-UQ3BBEIT.js.map +0 -1
  72. package/dist/chunk-ZLUMUJUQ.js.map +0 -1
@@ -1,7 +1,7 @@
1
1
  import { SequenceEntry, ActivityResult, LearnerResponse, InteractionEvent, ItemOutcome, ThemeTokens } from '@intellectif/lk-core';
2
2
  import { ComponentType } from 'react';
3
- import { A as ActivityProps, b as RenderableActivity, R as RenderMode, H as HtmlSanitizer } from '../types-9iS1Vs2I.js';
4
- import { a as WrittenResponseSubmission } from '../WrittenResponse-DJYGuMKk.js';
3
+ import { A as ActivityProps, c as RenderableActivity, R as RenderMode, S as SequenceMediaBudget, H as HtmlSanitizer } from '../types-eXX4hzSj.js';
4
+ import { a as WrittenResponseSubmission } from '../WrittenResponse-BJL4dGsR.js';
5
5
 
6
6
  /**
7
7
  * What one finished slot in a sequence produced. Two kinds, because two kinds
@@ -117,6 +117,16 @@ interface ActivitySequenceProps {
117
117
  onInteraction?: (event: InteractionEvent) => void;
118
118
  /** Forwarded to each activity. `exam` and `review` disable local scoring. */
119
119
  renderMode?: RenderMode;
120
+ /**
121
+ * Binds every budgeted recording in this sequence to a play ledger the
122
+ * consumer persists.
123
+ *
124
+ * The SDK refuses a play; it does not remember one. `plays` seeds the counts
125
+ * at mount (pass `restoreMediaPlayLedger(plan, stored).entries`), and
126
+ * `onPlayConsumed` is how a play becomes durable — see
127
+ * {@link SequenceMediaBudget}.
128
+ */
129
+ mediaBudget?: SequenceMediaBudget;
120
130
  /**
121
131
  * `entries` shuffles the top-level entries; a group moves as one block, and
122
132
  * the order INSIDE a group follows the group's own `shuffle` setting.
@@ -188,18 +198,6 @@ interface ActivitySequenceProps {
188
198
  locale?: string;
189
199
  disabled?: boolean;
190
200
  }
191
- /**
192
- * Presents a set of activities as an in-place pager: one question visible at a
193
- * time, navigated with Prev/Next (no page scrolling). Linear free navigation —
194
- * the learner may move back and forth; activities keep their own submit and
195
- * scoring. On navigation, focus moves to the question region and the position
196
- * is announced via an aria-live region.
197
- *
198
- * Item groups are flattened into consecutive questions; the group's stimulus
199
- * is mounted ONCE, beside the question region rather than inside it, and shown
200
- * alongside every question in the group — so a passage stays put, and a
201
- * recording keeps its position, as the learner moves between its questions.
202
- */
203
- declare function ActivitySequence({ activities, renderers, onActivityComplete, onComplete, onFinished, onSubmit, onInteraction, renderMode, shuffle, shuffleSeed, defaultIndex, onIndexChange, responses, submittedSlotIds, outcomes, sanitizeHtml, theme, locale, disabled, }: ActivitySequenceProps): React.JSX.Element;
201
+ declare function ActivitySequence({ activities, renderers, onActivityComplete, onComplete, onFinished, onSubmit, onInteraction, renderMode, shuffle, shuffleSeed, defaultIndex, onIndexChange, responses, submittedSlotIds, outcomes, sanitizeHtml, theme, locale, disabled, mediaBudget, }: ActivitySequenceProps): React.JSX.Element;
204
202
 
205
203
  export { type ActivityRenderer, ActivitySequence, type ActivitySequenceProps, type SequenceItemOutcome };
@@ -1,11 +1,11 @@
1
1
  'use client';
2
- export { ActivitySequence } from '../chunk-IUODGQXE.js';
3
- import '../chunk-ZLUMUJUQ.js';
4
- import '../chunk-57E3E2H4.js';
5
- import '../chunk-MDZMKC27.js';
2
+ export { ActivitySequence } from '../chunk-ZPVMB3LH.js';
3
+ import '../chunk-MB532SXM.js';
4
+ import '../chunk-J23XVR2B.js';
5
+ import '../chunk-CMZGT26S.js';
6
6
  import '../chunk-6F4X2NKL.js';
7
- import '../chunk-UQ3BBEIT.js';
8
- import '../chunk-GB5URWM4.js';
7
+ import '../chunk-BG456EM3.js';
8
+ import '../chunk-6ZKVJAO6.js';
9
9
  import '../chunk-6N43WDVG.js';
10
10
  //# sourceMappingURL=ActivitySequence.js.map
11
11
  //# sourceMappingURL=ActivitySequence.js.map
@@ -1,16 +1,16 @@
1
1
  'use client';
2
2
  'use strict';
3
3
 
4
- var chunkNPY2F7F6_cjs = require('../chunk-NPY2F7F6.cjs');
4
+ var chunkVXOOGQ6W_cjs = require('../chunk-VXOOGQ6W.cjs');
5
5
  require('../chunk-PGEG7UZJ.cjs');
6
- require('../chunk-4HZ7LQIH.cjs');
6
+ require('../chunk-A572N5G7.cjs');
7
7
  require('../chunk-XOR4MPEN.cjs');
8
8
 
9
9
 
10
10
 
11
11
  Object.defineProperty(exports, "FillInTheBlanks", {
12
12
  enumerable: true,
13
- get: function () { return chunkNPY2F7F6_cjs.FillInTheBlanks; }
13
+ get: function () { return chunkVXOOGQ6W_cjs.FillInTheBlanks; }
14
14
  });
15
15
  //# sourceMappingURL=FillInTheBlanks.cjs.map
16
16
  //# sourceMappingURL=FillInTheBlanks.cjs.map
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { FillInTheBlanksData } from '@intellectif/lk-core';
3
- import { A as ActivityProps } from '../types-9iS1Vs2I.cjs';
3
+ import { A as ActivityProps } from '../types-eXX4hzSj.cjs';
4
4
 
5
5
  interface FillInTheBlanksProps extends ActivityProps<FillInTheBlanksData> {
6
6
  /**
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { FillInTheBlanksData } from '@intellectif/lk-core';
3
- import { A as ActivityProps } from '../types-9iS1Vs2I.js';
3
+ import { A as ActivityProps } from '../types-eXX4hzSj.js';
4
4
 
5
5
  interface FillInTheBlanksProps extends ActivityProps<FillInTheBlanksData> {
6
6
  /**
@@ -1,7 +1,7 @@
1
1
  'use client';
2
- export { FillInTheBlanks } from '../chunk-57E3E2H4.js';
2
+ export { FillInTheBlanks } from '../chunk-J23XVR2B.js';
3
3
  import '../chunk-6F4X2NKL.js';
4
- import '../chunk-GB5URWM4.js';
4
+ import '../chunk-6ZKVJAO6.js';
5
5
  import '../chunk-6N43WDVG.js';
6
6
  //# sourceMappingURL=FillInTheBlanks.js.map
7
7
  //# sourceMappingURL=FillInTheBlanks.js.map
@@ -1,16 +1,16 @@
1
1
  'use client';
2
2
  'use strict';
3
3
 
4
- var chunkBE7R2X3S_cjs = require('../chunk-BE7R2X3S.cjs');
4
+ var chunkBNESCXJ5_cjs = require('../chunk-BNESCXJ5.cjs');
5
5
  require('../chunk-PGEG7UZJ.cjs');
6
- require('../chunk-4HZ7LQIH.cjs');
6
+ require('../chunk-A572N5G7.cjs');
7
7
  require('../chunk-XOR4MPEN.cjs');
8
8
 
9
9
 
10
10
 
11
11
  Object.defineProperty(exports, "MultipleChoice", {
12
12
  enumerable: true,
13
- get: function () { return chunkBE7R2X3S_cjs.MultipleChoice; }
13
+ get: function () { return chunkBNESCXJ5_cjs.MultipleChoice; }
14
14
  });
15
15
  //# sourceMappingURL=MultipleChoice.cjs.map
16
16
  //# sourceMappingURL=MultipleChoice.cjs.map
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { MultipleChoiceData } from '@intellectif/lk-core';
3
- import { A as ActivityProps } from '../types-9iS1Vs2I.cjs';
3
+ import { A as ActivityProps } from '../types-eXX4hzSj.cjs';
4
4
 
5
5
  interface MultipleChoiceProps extends ActivityProps<MultipleChoiceData> {
6
6
  /**
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { MultipleChoiceData } from '@intellectif/lk-core';
3
- import { A as ActivityProps } from '../types-9iS1Vs2I.js';
3
+ import { A as ActivityProps } from '../types-eXX4hzSj.js';
4
4
 
5
5
  interface MultipleChoiceProps extends ActivityProps<MultipleChoiceData> {
6
6
  /**
@@ -1,7 +1,7 @@
1
1
  'use client';
2
- export { MultipleChoice } from '../chunk-ZLUMUJUQ.js';
2
+ export { MultipleChoice } from '../chunk-MB532SXM.js';
3
3
  import '../chunk-6F4X2NKL.js';
4
- import '../chunk-GB5URWM4.js';
4
+ import '../chunk-6ZKVJAO6.js';
5
5
  import '../chunk-6N43WDVG.js';
6
6
  //# sourceMappingURL=MultipleChoice.js.map
7
7
  //# sourceMappingURL=MultipleChoice.js.map
@@ -1,14 +1,14 @@
1
1
  'use client';
2
2
  'use strict';
3
3
 
4
- var chunkNEWGUDA5_cjs = require('../chunk-NEWGUDA5.cjs');
5
- require('../chunk-4HZ7LQIH.cjs');
4
+ var chunkKK4WGW2V_cjs = require('../chunk-KK4WGW2V.cjs');
5
+ require('../chunk-A572N5G7.cjs');
6
6
 
7
7
 
8
8
 
9
9
  Object.defineProperty(exports, "StimulusPanel", {
10
10
  enumerable: true,
11
- get: function () { return chunkNEWGUDA5_cjs.StimulusPanel; }
11
+ get: function () { return chunkKK4WGW2V_cjs.StimulusPanel; }
12
12
  });
13
13
  //# sourceMappingURL=StimulusPanel.cjs.map
14
14
  //# sourceMappingURL=StimulusPanel.cjs.map
@@ -1,5 +1,5 @@
1
- import { Stimulus } from '@intellectif/lk-core';
2
- import { H as HtmlSanitizer } from '../types-9iS1Vs2I.cjs';
1
+ import { Stimulus, InteractionEvent } from '@intellectif/lk-core';
2
+ import { H as HtmlSanitizer, R as RenderMode, M as MediaBudgetBinding, a as MediaTransportStrings } from '../types-eXX4hzSj.cjs';
3
3
 
4
4
  interface StimulusPanelProps {
5
5
  stimulus: Stimulus;
@@ -15,6 +15,22 @@ interface StimulusPanelProps {
15
15
  /** Renders `stimulus.bodyHtml` when provided. See `HtmlSanitizer`. */
16
16
  sanitizeHtml?: HtmlSanitizer;
17
17
  locale?: string;
18
+ /**
19
+ * Forwarded to the stimulus's own media. `review` keeps the browser's control
20
+ * bar even under an enforcing policy — a graded paper cannot be changed by
21
+ * listening again.
22
+ */
23
+ renderMode?: RenderMode;
24
+ /**
25
+ * Binds this group's recording to a play budget the consumer persists. One
26
+ * recording serves every question in the group, so the key is derived from
27
+ * the ENTRY rather than the slot — see `stimulusMediaKey()` in lk-core.
28
+ */
29
+ mediaBudget?: MediaBudgetBinding;
30
+ /** Translations for the audio transport chrome. */
31
+ mediaStrings?: Partial<MediaTransportStrings>;
32
+ onInteraction?: (event: InteractionEvent) => void;
33
+ disabled?: boolean;
18
34
  }
19
35
  /**
20
36
  * Presents a shared stimulus — the passage, recording or image an item group's
@@ -25,6 +41,6 @@ interface StimulusPanelProps {
25
41
  * caller-supplied sanitiser, and the plain `body` is the fallback — which the
26
42
  * schema guarantees exists whenever `bodyHtml` does.
27
43
  */
28
- declare function StimulusPanel({ stimulus, range, sanitizeHtml, locale, }: StimulusPanelProps): React.JSX.Element;
44
+ declare function StimulusPanel({ stimulus, range, sanitizeHtml, locale, renderMode, mediaBudget, mediaStrings, onInteraction, disabled, }: StimulusPanelProps): React.JSX.Element;
29
45
 
30
46
  export { StimulusPanel, type StimulusPanelProps };
@@ -1,5 +1,5 @@
1
- import { Stimulus } from '@intellectif/lk-core';
2
- import { H as HtmlSanitizer } from '../types-9iS1Vs2I.js';
1
+ import { Stimulus, InteractionEvent } from '@intellectif/lk-core';
2
+ import { H as HtmlSanitizer, R as RenderMode, M as MediaBudgetBinding, a as MediaTransportStrings } from '../types-eXX4hzSj.js';
3
3
 
4
4
  interface StimulusPanelProps {
5
5
  stimulus: Stimulus;
@@ -15,6 +15,22 @@ interface StimulusPanelProps {
15
15
  /** Renders `stimulus.bodyHtml` when provided. See `HtmlSanitizer`. */
16
16
  sanitizeHtml?: HtmlSanitizer;
17
17
  locale?: string;
18
+ /**
19
+ * Forwarded to the stimulus's own media. `review` keeps the browser's control
20
+ * bar even under an enforcing policy — a graded paper cannot be changed by
21
+ * listening again.
22
+ */
23
+ renderMode?: RenderMode;
24
+ /**
25
+ * Binds this group's recording to a play budget the consumer persists. One
26
+ * recording serves every question in the group, so the key is derived from
27
+ * the ENTRY rather than the slot — see `stimulusMediaKey()` in lk-core.
28
+ */
29
+ mediaBudget?: MediaBudgetBinding;
30
+ /** Translations for the audio transport chrome. */
31
+ mediaStrings?: Partial<MediaTransportStrings>;
32
+ onInteraction?: (event: InteractionEvent) => void;
33
+ disabled?: boolean;
18
34
  }
19
35
  /**
20
36
  * Presents a shared stimulus — the passage, recording or image an item group's
@@ -25,6 +41,6 @@ interface StimulusPanelProps {
25
41
  * caller-supplied sanitiser, and the plain `body` is the fallback — which the
26
42
  * schema guarantees exists whenever `bodyHtml` does.
27
43
  */
28
- declare function StimulusPanel({ stimulus, range, sanitizeHtml, locale, }: StimulusPanelProps): React.JSX.Element;
44
+ declare function StimulusPanel({ stimulus, range, sanitizeHtml, locale, renderMode, mediaBudget, mediaStrings, onInteraction, disabled, }: StimulusPanelProps): React.JSX.Element;
29
45
 
30
46
  export { StimulusPanel, type StimulusPanelProps };
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- export { StimulusPanel } from '../chunk-UQ3BBEIT.js';
3
- import '../chunk-GB5URWM4.js';
2
+ export { StimulusPanel } from '../chunk-BG456EM3.js';
3
+ import '../chunk-6ZKVJAO6.js';
4
4
  //# sourceMappingURL=StimulusPanel.js.map
5
5
  //# sourceMappingURL=StimulusPanel.js.map
@@ -1,16 +1,16 @@
1
1
  'use client';
2
2
  'use strict';
3
3
 
4
- var chunkMDYKYDRN_cjs = require('../chunk-MDYKYDRN.cjs');
4
+ var chunkKPI335JJ_cjs = require('../chunk-KPI335JJ.cjs');
5
5
  require('../chunk-PGEG7UZJ.cjs');
6
- require('../chunk-4HZ7LQIH.cjs');
6
+ require('../chunk-A572N5G7.cjs');
7
7
  require('../chunk-XOR4MPEN.cjs');
8
8
 
9
9
 
10
10
 
11
11
  Object.defineProperty(exports, "WrittenResponse", {
12
12
  enumerable: true,
13
- get: function () { return chunkMDYKYDRN_cjs.WrittenResponse; }
13
+ get: function () { return chunkKPI335JJ_cjs.WrittenResponse; }
14
14
  });
15
15
  //# sourceMappingURL=WrittenResponse.cjs.map
16
16
  //# sourceMappingURL=WrittenResponse.cjs.map
@@ -1,7 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { W as WrittenResponseProps } from '../WrittenResponse-BXHidIaK.cjs';
3
- export { a as WrittenResponseSubmission } from '../WrittenResponse-BXHidIaK.cjs';
4
- export { H as HtmlSanitizer, R as RenderMode, a as Renderable } from '../types-9iS1Vs2I.cjs';
2
+ import { W as WrittenResponseProps } from '../WrittenResponse-zWT-Go1g.cjs';
3
+ export { a as WrittenResponseSubmission } from '../WrittenResponse-zWT-Go1g.cjs';
4
+ export { H as HtmlSanitizer, R as RenderMode, b as Renderable } from '../types-eXX4hzSj.cjs';
5
5
  import '@intellectif/lk-core';
6
6
 
7
7
  /**
@@ -1,7 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { W as WrittenResponseProps } from '../WrittenResponse-DJYGuMKk.js';
3
- export { a as WrittenResponseSubmission } from '../WrittenResponse-DJYGuMKk.js';
4
- export { H as HtmlSanitizer, R as RenderMode, a as Renderable } from '../types-9iS1Vs2I.js';
2
+ import { W as WrittenResponseProps } from '../WrittenResponse-BJL4dGsR.js';
3
+ export { a as WrittenResponseSubmission } from '../WrittenResponse-BJL4dGsR.js';
4
+ export { H as HtmlSanitizer, R as RenderMode, b as Renderable } from '../types-eXX4hzSj.js';
5
5
  import '@intellectif/lk-core';
6
6
 
7
7
  /**
@@ -1,7 +1,7 @@
1
1
  'use client';
2
- export { WrittenResponse } from '../chunk-MDZMKC27.js';
2
+ export { WrittenResponse } from '../chunk-CMZGT26S.js';
3
3
  import '../chunk-6F4X2NKL.js';
4
- import '../chunk-GB5URWM4.js';
4
+ import '../chunk-6ZKVJAO6.js';
5
5
  import '../chunk-6N43WDVG.js';
6
6
  //# sourceMappingURL=WrittenResponse.js.map
7
7
  //# sourceMappingURL=WrittenResponse.js.map
package/dist/index.cjs CHANGED
@@ -2,13 +2,13 @@
2
2
  'use strict';
3
3
 
4
4
  var chunkAMGK7DDM_cjs = require('./chunk-AMGK7DDM.cjs');
5
- var chunkDUIWK272_cjs = require('./chunk-DUIWK272.cjs');
6
- var chunkBE7R2X3S_cjs = require('./chunk-BE7R2X3S.cjs');
7
- var chunkNPY2F7F6_cjs = require('./chunk-NPY2F7F6.cjs');
8
- var chunkMDYKYDRN_cjs = require('./chunk-MDYKYDRN.cjs');
5
+ var chunkJ4LP4CLY_cjs = require('./chunk-J4LP4CLY.cjs');
6
+ var chunkBNESCXJ5_cjs = require('./chunk-BNESCXJ5.cjs');
7
+ var chunkVXOOGQ6W_cjs = require('./chunk-VXOOGQ6W.cjs');
8
+ var chunkKPI335JJ_cjs = require('./chunk-KPI335JJ.cjs');
9
9
  require('./chunk-PGEG7UZJ.cjs');
10
- var chunkNEWGUDA5_cjs = require('./chunk-NEWGUDA5.cjs');
11
- require('./chunk-4HZ7LQIH.cjs');
10
+ var chunkKK4WGW2V_cjs = require('./chunk-KK4WGW2V.cjs');
11
+ require('./chunk-A572N5G7.cjs');
12
12
  var chunkXOR4MPEN_cjs = require('./chunk-XOR4MPEN.cjs');
13
13
  var chunkPNNUPUH4_cjs = require('./chunk-PNNUPUH4.cjs');
14
14
 
@@ -42,23 +42,23 @@ Object.defineProperty(exports, "useTheme", {
42
42
  });
43
43
  Object.defineProperty(exports, "ActivitySequence", {
44
44
  enumerable: true,
45
- get: function () { return chunkDUIWK272_cjs.ActivitySequence; }
45
+ get: function () { return chunkJ4LP4CLY_cjs.ActivitySequence; }
46
46
  });
47
47
  Object.defineProperty(exports, "MultipleChoice", {
48
48
  enumerable: true,
49
- get: function () { return chunkBE7R2X3S_cjs.MultipleChoice; }
49
+ get: function () { return chunkBNESCXJ5_cjs.MultipleChoice; }
50
50
  });
51
51
  Object.defineProperty(exports, "FillInTheBlanks", {
52
52
  enumerable: true,
53
- get: function () { return chunkNPY2F7F6_cjs.FillInTheBlanks; }
53
+ get: function () { return chunkVXOOGQ6W_cjs.FillInTheBlanks; }
54
54
  });
55
55
  Object.defineProperty(exports, "WrittenResponse", {
56
56
  enumerable: true,
57
- get: function () { return chunkMDYKYDRN_cjs.WrittenResponse; }
57
+ get: function () { return chunkKPI335JJ_cjs.WrittenResponse; }
58
58
  });
59
59
  Object.defineProperty(exports, "StimulusPanel", {
60
60
  enumerable: true,
61
- get: function () { return chunkNEWGUDA5_cjs.StimulusPanel; }
61
+ get: function () { return chunkKK4WGW2V_cjs.StimulusPanel; }
62
62
  });
63
63
  Object.defineProperty(exports, "useActivityState", {
64
64
  enumerable: true,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/types.ts"],"names":[],"mappings":";;;;;;;;;;;;;;AAgKO,SAAS,aACd,QAAA,EACmB;AACnB,EAAA,OAAO,QAAA;AACT;AAgCO,SAAS,qBACd,OAAA,EAC8C;AAC9C,EAAA,OAAO,OAAA;AACT","file":"index.cjs","sourcesContent":["import type {\n ActivityData,\n ActivityResult,\n InteractionEvent,\n ItemGroup,\n ItemOutcome,\n LearnerResponse,\n RedactedActivityData,\n SequenceEntry,\n ThemeTokens,\n} from '@intellectif/lk-core';\n\n/**\n * How an activity is being presented. This is the single switch that decides\n * whether the component may grade, reveal, or submit — the three things a\n * summative exam must take away from the client.\n *\n * - `practice` (default, and the only behaviour before `renderMode` existed):\n * the component owns the attempt. It\n * scores locally on submit, reveals correctness and feedback, and calls\n * `onComplete` with a full {@link ActivityResult}.\n * - `exam`: the component NEVER scores and NEVER reveals correctness. Submit\n * emits the raw learner response through `onSubmit`; the server grades it.\n * Safe to render redacted data, because nothing here needs the answer key.\n * - `review`: read-only. Renders the learner's submitted answer, and marks\n * correctness only from an `outcome` the caller supplies (which the server\n * computed). Nothing is submittable.\n */\nexport type RenderMode = 'practice' | 'exam' | 'review';\n\n/**\n * Makes the answer-key-bearing parts of an activity optional, so a component\n * can render either full activity data OR a `redact()` projection with the\n * same props. Fields the SDK classifies `answer-key` become optional here;\n * a component in `exam` mode must not read them at all.\n */\nexport type Renderable<TData> = Omit<TData, 'scoringStrategy'> & {\n scoringStrategy?: unknown;\n /** Present on a `redact()` projection. */\n redacted?: true;\n};\n\n/**\n * {@link Renderable} DISTRIBUTED over a union of activity types.\n *\n * This distinction is load-bearing, not cosmetic. `Renderable<T>` is built on\n * `Omit`, and `Omit` does not distribute: `Omit<A | B, K>` collapses to the\n * keys A and B have IN COMMON, so `Renderable<ActivityData>` is a single\n * object type carrying only the fields every activity shares. Narrowing it\n * dies with it — after `if (data.type === 'multiple-choice')` the compiler\n * still refuses `data.options`, because the union it would narrow to no\n * longer exists.\n *\n * The conditional below re-distributes, so `RenderableActivity` is a real\n * union of per-type renderables and `.type` narrows again. Anything that\n * accepts \"some renderable activity, I don't know which\" — a custom renderer,\n * a sequence entry — must use THIS, not `Renderable<ActivityData>`.\n */\nexport type RenderableActivity<TData extends ActivityData = ActivityData> = TData extends unknown\n ? Renderable<TData>\n : never;\n\n/**\n * Sanitiser for author-supplied rich text (`questionHtml`, `promptHtml`, and a\n * stimulus's `bodyHtml`). The SDK deliberately ships NO sanitiser — that would\n * add a dependency and, worse, a false promise. Rich text is rendered only when\n * you supply this function; without it the component falls back to the\n * plain-text field, which is always escaped. Fail-safe by construction: the SDK\n * never injects HTML it was not explicitly given a sanitiser for.\n *\n * `FillInTheBlanks.passageHtml` is the one exception, and is **never**\n * rendered: the passage hosts the answer inputs, so it cannot be split at the\n * `{{blank}}` placeholders without voiding the sanitiser. The plain `passage`\n * is always used, and passing `passageHtml` warns in development.\n */\nexport type HtmlSanitizer = (html: string) => string;\n\n/**\n * The prop contract shared by every activity component (Req 3.1). Defined\n * here (React-specific) rather than in lk-core, which is React-free.\n *\n * Controlled / uncontrolled follows the React convention: pass `value` +\n * `onChange` to own the learner's answer (restore an in-progress attempt,\n * autosave a delta, drive a review); pass `defaultValue` to seed an\n * uncontrolled component; pass neither to keep the pre-2.1.0 behaviour, where\n * the component owns the answer outright.\n */\nexport interface ActivityProps<TData extends ActivityData = ActivityData> {\n /** Activity content. Accepts a `redact()` projection in `exam` mode. */\n data: RenderableActivity<TData>;\n /**\n * Called when the component scored the attempt itself. Only ever fires in\n * `practice` mode — in `exam` mode the client does not grade, so there is\n * no `ActivityResult` to give you; use `onSubmit`.\n */\n onComplete?: (result: ActivityResult) => void;\n /**\n * Called on submit with the raw learner response and no grade. Fires in\n * every mode, before `onComplete`, so an exam runner can persist the\n * response and let the server score it.\n */\n onSubmit?: (response: LearnerResponse) => void;\n /** Controlled value: the learner's current response. */\n value?: LearnerResponse;\n /** Initial response for an uncontrolled component (ignored when `value` is set). */\n defaultValue?: LearnerResponse;\n /**\n * Mount the component as already submitted — read at mount only, like any\n * `default*` prop.\n *\n * Restoring an attempt without it reopens a question the learner had already\n * submitted as answerable, so on a summative paper they can change and\n * re-submit it. `AttemptState.submittedSlotIds` is what this consumes.\n */\n defaultSubmitted?: boolean;\n /** Fires on every change to the learner's response. Required for a controlled component. */\n onChange?: (response: LearnerResponse) => void;\n /** Presentation mode. Defaults to `practice`. */\n renderMode?: RenderMode;\n /**\n * Server-computed outcome, used by `review` mode to mark correctness\n * without the client ever scoring. Ignored in other modes.\n */\n outcome?: ItemOutcome;\n /** Renders author-supplied rich text when provided. See {@link HtmlSanitizer}. */\n sanitizeHtml?: HtmlSanitizer;\n onInteraction?: (event: InteractionEvent) => void;\n /** Per-instance token overrides, applied as inline CSS vars on the root. */\n theme?: Partial<ThemeTokens>;\n locale?: string;\n disabled?: boolean;\n}\n\n/**\n * Bridges a server-produced `redact()` projection into the `data` prop.\n *\n * `RedactedActivityData` is deliberately index-signature typed in lk-core — it\n * proves a payload is learner-safe, not what shape it has — so TypeScript\n * cannot know it still carries the public fields a renderer needs. This is the\n * SDK's single, documented crossing of that gap, so an exam runner does not\n * have to write `as unknown as` at every call site:\n *\n * ```tsx\n * <MultipleChoice\n * data={asRenderable<MultipleChoiceData>(redactedFromServer)}\n * renderMode=\"exam\"\n * onSubmit={persist}\n * />\n * ```\n *\n * Safe because `exam` mode reads only public fields; the answer-key fields the\n * type claims are exactly the ones the component is forbidden to touch there.\n *\n * Per-type redacted interfaces (`RedactedMultipleChoiceData`, …) ship from\n * lk-core since 0.6.0, but they are not yet *assignable* to `data`:\n * {@link Renderable} widens `scoringStrategy` and leaves nested answer-key\n * fields (`options[].isCorrect`, `blanks[].acceptedAnswers`) required, so a\n * real redacted payload still needs this bridge. Closing that gap is tracked\n * in the roadmap.\n */\nexport function asRenderable<TData extends ActivityData>(\n redacted: RedactedActivityData,\n): Renderable<TData> {\n return redacted as unknown as Renderable<TData>;\n}\n\n/**\n * The same bridge as {@link asRenderable}, for a whole sequence: activities\n * and item groups as a server hands them over, ready for `<ActivitySequence>`.\n *\n * Needed for the same reason and no other. `redactItemGroup` returns\n * `ItemGroup<RedactedActivityData>`, and `RedactedActivityData` is an\n * index-signature type whose fields are all `unknown` — so its `question` is\n * not a `string` and it satisfies no per-type renderable, however the prop is\n * widened. Widening alone cannot fix this; a crossing point is required, and\n * having exactly one keeps `as unknown as` out of consumer code.\n *\n * ```tsx\n * const entries = await fetchExam(); // redacted, server-side\n * <ActivitySequence\n * activities={asRenderableSequence(entries)}\n * renderMode=\"exam\" // REQUIRED: see below\n * shuffleSeed={attemptId}\n * onSubmit={persist}\n * />\n * ```\n *\n * Pass `renderMode=\"exam\"` (or `\"review\"`). Redacted data has no answer key,\n * and the default `practice` mode grades locally — so `<MultipleChoice>` and\n * `<FillInTheBlanks>` throw at render rather than fail at submit time.\n *\n * `<WrittenResponse>` is the exception: it never grades on the client and has\n * no such guard, so a redacted essay renders and stays answerable in\n * `practice`. A mis-wired essay item is therefore SILENT — set `renderMode`\n * explicitly rather than relying on the throw.\n */\nexport function asRenderableSequence(\n entries: readonly (RedactedActivityData | RedactedItemGroupData)[],\n): readonly SequenceEntry<RenderableActivity>[] {\n return entries as unknown as readonly SequenceEntry<RenderableActivity>[];\n}\n\n/** Structural shape of a `redactItemGroup()` projection, as it arrives from a server. */\ntype RedactedItemGroupData = ItemGroup<RedactedActivityData> & { redacted: true };\n"]}
1
+ {"version":3,"sources":["../src/components/types.ts"],"names":[],"mappings":";;;;;;;;;;;;;;AA2KO,SAAS,aACd,QAAA,EACmB;AACnB,EAAA,OAAO,QAAA;AACT;AAgCO,SAAS,qBACd,OAAA,EAC8C;AAC9C,EAAA,OAAO,OAAA;AACT","file":"index.cjs","sourcesContent":["import type {\n ActivityData,\n ActivityResult,\n InteractionEvent,\n ItemGroup,\n ItemOutcome,\n LearnerResponse,\n MediaPlayClaim,\n MediaPlayGrant,\n MediaPlayLedgerEntry,\n RedactedActivityData,\n SequenceEntry,\n ThemeTokens,\n} from '@intellectif/lk-core';\n\n/**\n * How an activity is being presented. This is the single switch that decides\n * whether the component may grade, reveal, or submit — the three things a\n * summative exam must take away from the client.\n *\n * - `practice` (default, and the only behaviour before `renderMode` existed):\n * the component owns the attempt. It\n * scores locally on submit, reveals correctness and feedback, and calls\n * `onComplete` with a full {@link ActivityResult}.\n * - `exam`: the component NEVER scores and NEVER reveals correctness. Submit\n * emits the raw learner response through `onSubmit`; the server grades it.\n * Safe to render redacted data, because nothing here needs the answer key.\n * - `review`: read-only. Renders the learner's submitted answer, and marks\n * correctness only from an `outcome` the caller supplies (which the server\n * computed). Nothing is submittable.\n */\nexport type RenderMode = 'practice' | 'exam' | 'review';\n\n/**\n * Makes the answer-key-bearing parts of an activity optional, so a component\n * can render either full activity data OR a `redact()` projection with the\n * same props. Fields the SDK classifies `answer-key` become optional here;\n * a component in `exam` mode must not read them at all.\n */\nexport type Renderable<TData> = Omit<TData, 'scoringStrategy'> & {\n scoringStrategy?: unknown;\n /** Present on a `redact()` projection. */\n redacted?: true;\n};\n\n/**\n * {@link Renderable} DISTRIBUTED over a union of activity types.\n *\n * This distinction is load-bearing, not cosmetic. `Renderable<T>` is built on\n * `Omit`, and `Omit` does not distribute: `Omit<A | B, K>` collapses to the\n * keys A and B have IN COMMON, so `Renderable<ActivityData>` is a single\n * object type carrying only the fields every activity shares. Narrowing it\n * dies with it — after `if (data.type === 'multiple-choice')` the compiler\n * still refuses `data.options`, because the union it would narrow to no\n * longer exists.\n *\n * The conditional below re-distributes, so `RenderableActivity` is a real\n * union of per-type renderables and `.type` narrows again. Anything that\n * accepts \"some renderable activity, I don't know which\" — a custom renderer,\n * a sequence entry — must use THIS, not `Renderable<ActivityData>`.\n */\nexport type RenderableActivity<TData extends ActivityData = ActivityData> = TData extends unknown\n ? Renderable<TData>\n : never;\n\n/**\n * Sanitiser for author-supplied rich text (`questionHtml`, `promptHtml`, and a\n * stimulus's `bodyHtml`). The SDK deliberately ships NO sanitiser — that would\n * add a dependency and, worse, a false promise. Rich text is rendered only when\n * you supply this function; without it the component falls back to the\n * plain-text field, which is always escaped. Fail-safe by construction: the SDK\n * never injects HTML it was not explicitly given a sanitiser for.\n *\n * `FillInTheBlanks.passageHtml` is the one exception, and is **never**\n * rendered: the passage hosts the answer inputs, so it cannot be split at the\n * `{{blank}}` placeholders without voiding the sanitiser. The plain `passage`\n * is always used, and passing `passageHtml` warns in development.\n */\nexport type HtmlSanitizer = (html: string) => string;\n\n/**\n * The prop contract shared by every activity component (Req 3.1). Defined\n * here (React-specific) rather than in lk-core, which is React-free.\n *\n * Controlled / uncontrolled follows the React convention: pass `value` +\n * `onChange` to own the learner's answer (restore an in-progress attempt,\n * autosave a delta, drive a review); pass `defaultValue` to seed an\n * uncontrolled component; pass neither to keep the pre-2.1.0 behaviour, where\n * the component owns the answer outright.\n */\nexport interface ActivityProps<TData extends ActivityData = ActivityData> {\n /** Activity content. Accepts a `redact()` projection in `exam` mode. */\n data: RenderableActivity<TData>;\n /**\n * Called when the component scored the attempt itself. Only ever fires in\n * `practice` mode — in `exam` mode the client does not grade, so there is\n * no `ActivityResult` to give you; use `onSubmit`.\n */\n onComplete?: (result: ActivityResult) => void;\n /**\n * Called on submit with the raw learner response and no grade. Fires in\n * every mode, before `onComplete`, so an exam runner can persist the\n * response and let the server score it.\n */\n onSubmit?: (response: LearnerResponse) => void;\n /** Controlled value: the learner's current response. */\n value?: LearnerResponse;\n /** Initial response for an uncontrolled component (ignored when `value` is set). */\n defaultValue?: LearnerResponse;\n /**\n * Mount the component as already submitted — read at mount only, like any\n * `default*` prop.\n *\n * Restoring an attempt without it reopens a question the learner had already\n * submitted as answerable, so on a summative paper they can change and\n * re-submit it. `AttemptState.submittedSlotIds` is what this consumes.\n */\n defaultSubmitted?: boolean;\n /** Fires on every change to the learner's response. Required for a controlled component. */\n onChange?: (response: LearnerResponse) => void;\n /** Presentation mode. Defaults to `practice`. */\n renderMode?: RenderMode;\n /**\n * Server-computed outcome, used by `review` mode to mark correctness\n * without the client ever scoring. Ignored in other modes.\n */\n outcome?: ItemOutcome;\n /** Renders author-supplied rich text when provided. See {@link HtmlSanitizer}. */\n sanitizeHtml?: HtmlSanitizer;\n /**\n * Binds this activity's own `data.media` to a play budget the consumer\n * persists. Usually supplied by `<ActivitySequence mediaBudget={…}>`; pass it\n * yourself when rendering an activity standalone.\n */\n mediaBudget?: MediaBudgetBinding;\n /** Translations for the audio transport chrome. See {@link MediaTransportStrings}. */\n mediaStrings?: Partial<MediaTransportStrings>;\n onInteraction?: (event: InteractionEvent) => void;\n /** Per-instance token overrides, applied as inline CSS vars on the root. */\n theme?: Partial<ThemeTokens>;\n locale?: string;\n disabled?: boolean;\n}\n\n/**\n * Bridges a server-produced `redact()` projection into the `data` prop.\n *\n * `RedactedActivityData` is deliberately index-signature typed in lk-core — it\n * proves a payload is learner-safe, not what shape it has — so TypeScript\n * cannot know it still carries the public fields a renderer needs. This is the\n * SDK's single, documented crossing of that gap, so an exam runner does not\n * have to write `as unknown as` at every call site:\n *\n * ```tsx\n * <MultipleChoice\n * data={asRenderable<MultipleChoiceData>(redactedFromServer)}\n * renderMode=\"exam\"\n * onSubmit={persist}\n * />\n * ```\n *\n * Safe because `exam` mode reads only public fields; the answer-key fields the\n * type claims are exactly the ones the component is forbidden to touch there.\n *\n * Per-type redacted interfaces (`RedactedMultipleChoiceData`, …) ship from\n * lk-core since 0.6.0, but they are not yet *assignable* to `data`:\n * {@link Renderable} widens `scoringStrategy` and leaves nested answer-key\n * fields (`options[].isCorrect`, `blanks[].acceptedAnswers`) required, so a\n * real redacted payload still needs this bridge. Closing that gap is tracked\n * in the roadmap.\n */\nexport function asRenderable<TData extends ActivityData>(\n redacted: RedactedActivityData,\n): Renderable<TData> {\n return redacted as unknown as Renderable<TData>;\n}\n\n/**\n * The same bridge as {@link asRenderable}, for a whole sequence: activities\n * and item groups as a server hands them over, ready for `<ActivitySequence>`.\n *\n * Needed for the same reason and no other. `redactItemGroup` returns\n * `ItemGroup<RedactedActivityData>`, and `RedactedActivityData` is an\n * index-signature type whose fields are all `unknown` — so its `question` is\n * not a `string` and it satisfies no per-type renderable, however the prop is\n * widened. Widening alone cannot fix this; a crossing point is required, and\n * having exactly one keeps `as unknown as` out of consumer code.\n *\n * ```tsx\n * const entries = await fetchExam(); // redacted, server-side\n * <ActivitySequence\n * activities={asRenderableSequence(entries)}\n * renderMode=\"exam\" // REQUIRED: see below\n * shuffleSeed={attemptId}\n * onSubmit={persist}\n * />\n * ```\n *\n * Pass `renderMode=\"exam\"` (or `\"review\"`). Redacted data has no answer key,\n * and the default `practice` mode grades locally — so `<MultipleChoice>` and\n * `<FillInTheBlanks>` throw at render rather than fail at submit time.\n *\n * `<WrittenResponse>` is the exception: it never grades on the client and has\n * no such guard, so a redacted essay renders and stays answerable in\n * `practice`. A mis-wired essay item is therefore SILENT — set `renderMode`\n * explicitly rather than relying on the throw.\n */\nexport function asRenderableSequence(\n entries: readonly (RedactedActivityData | RedactedItemGroupData)[],\n): readonly SequenceEntry<RenderableActivity>[] {\n return entries as unknown as readonly SequenceEntry<RenderableActivity>[];\n}\n\n/** Structural shape of a `redactItemGroup()` projection, as it arrives from a server. */\ntype RedactedItemGroupData = ItemGroup<RedactedActivityData> & { redacted: true };\n\n/**\n * Every English string the SDK's audio transport renders.\n *\n * Supply them to translate it. These are the highest-stakes strings on a\n * listening paper — \"No plays remaining\" decides whether a learner believes\n * they may try again — so shipping them as untranslatable English inside a\n * Spanish panel was not acceptable. Supplying any of them also sets `lang` on\n * the transport chrome, so a screen reader does not read the SDK's own words\n * with the passage's phonetics.\n */\nexport interface MediaTransportStrings {\n play: string;\n pause: string;\n preparing: string;\n mute: string;\n unmute: string;\n volume: string;\n speed: string;\n seek: string;\n /** e.g. `(1, 2) => '1 of 2 plays remaining'`. */\n playsRemaining: (remaining: number, max: number) => string;\n noPlaysRemaining: string;\n /** Shown before the LAST play is spent, so a stray press cannot cost it. */\n lastPlayConfirm: string;\n lastPlayStart: string;\n lastPlayCancel: string;\n seekBlocked: string;\n rateBlocked: string;\n playFailed: string;\n}\n\n/**\n * Binds ONE media block to a play budget the consumer persists.\n *\n * The SDK refuses a play; it does not remember one. Everything durable here is\n * the consuming application's — see {@link SequenceMediaBudget.onPlayConsumed}.\n */\nexport interface MediaBudgetBinding {\n /** From `slotMediaKey(slotId)` / `stimulusMediaKey(slotId)` in lk-core. */\n key: string;\n /** Plays already spent, and where playback stood. Read at mount only. */\n entry?: MediaPlayLedgerEntry;\n /** Defaults to `renderMode !== 'review'`. An explicit boolean wins either way. */\n enforced?: boolean;\n /** Slot context stamped onto the claim and the interaction event. */\n slotId: string;\n index: number;\n activityId?: string;\n onPlayConsumed?: (claim: MediaPlayClaim) => undefined | Promise<MediaPlayGrant | undefined>;\n onPlayRefunded?: (claim: MediaPlayClaim) => void;\n onPosition?: (key: string, seconds: number) => void;\n /** Translations for the transport chrome. */\n strings?: Partial<MediaTransportStrings>;\n}\n\n/**\n * The pager-level half of a play budget. One prop, because it is one concept.\n */\nexport interface SequenceMediaBudget {\n /**\n * `MediaPlayLedger.entries` goes straight in. An absent key means nothing\n * spent. Read at mount, like `responses`.\n */\n plays?: Readonly<Record<string, MediaPlayLedgerEntry>>;\n /**\n * Re-seed token. Change this string and the budgets re-seed from `plays`\n * WITHOUT remounting the pager — the invigilator path (\"the audio never\n * started, give her the play back\") that would otherwise cost the learner\n * their focus, their scroll position and an unsaved answer.\n */\n resumeKey?: string;\n /** Explicit override of the default (`renderMode !== 'review'`). */\n enforced?: boolean;\n /**\n * Called the instant a play is claimed, BEFORE any audio is audible.\n *\n * Two tiers, chosen by what you return:\n *\n * - **Return nothing (optimistic).** Playback starts immediately and the\n * count is only as durable as your write. **Do not debounce this, and do\n * not batch it with the answer autosave** — an eight-second debounce is\n * exactly long enough to start a third play and hard-reload. A `pagehide`\n * beacon is a backstop, not the mechanism. A crash between this call and\n * your write landing RETURNS the play to the learner; that is the honest\n * description of what you are buying.\n * - **Return a promise (confirmed).** Playback is held — the button reads\n * \"Preparing…\" and is `aria-busy` — until it settles. Resolve with\n * `{ playsUsed }` from an ATOMIC server write (`UPDATE … SET plays = plays\n * + 1 … RETURNING plays`, or a compare-and-set on\n * `claim.previousPlaysUsed`). A resolved count above `maxPlays` refuses the\n * play, which is how a second tab is caught: two mounts both seeded at 0\n * both claim 1, and only an atomic increment can tell them apart. A\n * rejection charges nothing and lets the learner retry. This is the only\n * tier in which \"consumed before audible\" is true of storage rather than\n * only of memory; use it for summative papers.\n *\n * Never settle the promise and the learner cannot play at all: settle it.\n */\n onPlayConsumed?: (claim: MediaPlayClaim) => undefined | Promise<MediaPlayGrant | undefined>;\n /**\n * Called when a charged play produced no audio — the element errored before\n * playback advanced past 0.25 s, an expired signed URL being the realistic\n * cause. Supply it to give the play back, decrementing with a compare-and-set\n * on `claim.playsUsed`. Omit it and the play stays spent: the SDK will not\n * decrement a ledger it has no channel to correct.\n */\n onPlayRefunded?: (claim: MediaPlayClaim) => void;\n /**\n * Position reports, so a refresh resumes the play the learner already paid\n * for instead of charging them again. Fires on pause, on end (with 0), and at\n * most once per whole second of playback.\n *\n * **This one you MAY throttle** — the granularity you persist is the\n * granularity of the replay a crash grants. Three seconds is sane; three\n * minutes is not.\n */\n onPosition?: (key: string, seconds: number) => void;\n /** Translations for the transport chrome. Defaults are English. */\n strings?: Partial<MediaTransportStrings>;\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -2,12 +2,12 @@ export { ActivityRenderer, ActivitySequence, ActivitySequenceProps, SequenceItem
2
2
  export { FillInTheBlanks, FillInTheBlanksProps } from './components/FillInTheBlanks.cjs';
3
3
  export { MultipleChoice, MultipleChoiceProps } from './components/MultipleChoice.cjs';
4
4
  export { StimulusPanel, StimulusPanelProps } from './components/StimulusPanel.cjs';
5
- export { A as ActivityProps, H as HtmlSanitizer, R as RenderMode, a as Renderable, b as RenderableActivity, c as asRenderable, d as asRenderableSequence } from './types-9iS1Vs2I.cjs';
5
+ export { A as ActivityProps, H as HtmlSanitizer, M as MediaBudgetBinding, a as MediaTransportStrings, R as RenderMode, b as Renderable, c as RenderableActivity, S as SequenceMediaBudget, d as asRenderable, e as asRenderableSequence } from './types-eXX4hzSj.cjs';
6
6
  export { WrittenResponse } from './components/WrittenResponse.cjs';
7
7
  export { ActivityState, UseActivityStateResult, useActivityState } from './hooks/useActivityState.cjs';
8
8
  export { UseXAPIResult, useXAPI } from './hooks/useXAPI.cjs';
9
9
  export { TailwindThemeExtension, ThemeProvider, ThemeProviderProps, createTailwindTheme, darkTheme, defaultTheme, useTheme } from './theme/ThemeProvider.cjs';
10
- export { W as WrittenResponseProps, a as WrittenResponseSubmission } from './WrittenResponse-BXHidIaK.cjs';
10
+ export { W as WrittenResponseProps, a as WrittenResponseSubmission } from './WrittenResponse-zWT-Go1g.cjs';
11
11
  import '@intellectif/lk-core';
12
12
  import 'react';
13
13
  import 'react/jsx-runtime';
package/dist/index.d.ts CHANGED
@@ -2,12 +2,12 @@ export { ActivityRenderer, ActivitySequence, ActivitySequenceProps, SequenceItem
2
2
  export { FillInTheBlanks, FillInTheBlanksProps } from './components/FillInTheBlanks.js';
3
3
  export { MultipleChoice, MultipleChoiceProps } from './components/MultipleChoice.js';
4
4
  export { StimulusPanel, StimulusPanelProps } from './components/StimulusPanel.js';
5
- export { A as ActivityProps, H as HtmlSanitizer, R as RenderMode, a as Renderable, b as RenderableActivity, c as asRenderable, d as asRenderableSequence } from './types-9iS1Vs2I.js';
5
+ export { A as ActivityProps, H as HtmlSanitizer, M as MediaBudgetBinding, a as MediaTransportStrings, R as RenderMode, b as Renderable, c as RenderableActivity, S as SequenceMediaBudget, d as asRenderable, e as asRenderableSequence } from './types-eXX4hzSj.js';
6
6
  export { WrittenResponse } from './components/WrittenResponse.js';
7
7
  export { ActivityState, UseActivityStateResult, useActivityState } from './hooks/useActivityState.js';
8
8
  export { UseXAPIResult, useXAPI } from './hooks/useXAPI.js';
9
9
  export { TailwindThemeExtension, ThemeProvider, ThemeProviderProps, createTailwindTheme, darkTheme, defaultTheme, useTheme } from './theme/ThemeProvider.js';
10
- export { W as WrittenResponseProps, a as WrittenResponseSubmission } from './WrittenResponse-DJYGuMKk.js';
10
+ export { W as WrittenResponseProps, a as WrittenResponseSubmission } from './WrittenResponse-BJL4dGsR.js';
11
11
  import '@intellectif/lk-core';
12
12
  import 'react';
13
13
  import 'react/jsx-runtime';
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  'use client';
2
2
  export { ThemeProvider, createTailwindTheme, darkTheme, defaultTheme, useTheme } from './chunk-DQNVAXG6.js';
3
- export { ActivitySequence } from './chunk-IUODGQXE.js';
4
- export { MultipleChoice } from './chunk-ZLUMUJUQ.js';
5
- export { FillInTheBlanks } from './chunk-57E3E2H4.js';
6
- export { WrittenResponse } from './chunk-MDZMKC27.js';
3
+ export { ActivitySequence } from './chunk-ZPVMB3LH.js';
4
+ export { MultipleChoice } from './chunk-MB532SXM.js';
5
+ export { FillInTheBlanks } from './chunk-J23XVR2B.js';
6
+ export { WrittenResponse } from './chunk-CMZGT26S.js';
7
7
  import './chunk-6F4X2NKL.js';
8
- export { StimulusPanel } from './chunk-UQ3BBEIT.js';
9
- import './chunk-GB5URWM4.js';
8
+ export { StimulusPanel } from './chunk-BG456EM3.js';
9
+ import './chunk-6ZKVJAO6.js';
10
10
  export { useActivityState } from './chunk-6N43WDVG.js';
11
11
  export { useXAPI } from './chunk-2TW6WLBC.js';
12
12
 
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/types.ts"],"names":[],"mappings":";;;;;;;;;;;;AAgKO,SAAS,aACd,QAAA,EACmB;AACnB,EAAA,OAAO,QAAA;AACT;AAgCO,SAAS,qBACd,OAAA,EAC8C;AAC9C,EAAA,OAAO,OAAA;AACT","file":"index.js","sourcesContent":["import type {\n ActivityData,\n ActivityResult,\n InteractionEvent,\n ItemGroup,\n ItemOutcome,\n LearnerResponse,\n RedactedActivityData,\n SequenceEntry,\n ThemeTokens,\n} from '@intellectif/lk-core';\n\n/**\n * How an activity is being presented. This is the single switch that decides\n * whether the component may grade, reveal, or submit — the three things a\n * summative exam must take away from the client.\n *\n * - `practice` (default, and the only behaviour before `renderMode` existed):\n * the component owns the attempt. It\n * scores locally on submit, reveals correctness and feedback, and calls\n * `onComplete` with a full {@link ActivityResult}.\n * - `exam`: the component NEVER scores and NEVER reveals correctness. Submit\n * emits the raw learner response through `onSubmit`; the server grades it.\n * Safe to render redacted data, because nothing here needs the answer key.\n * - `review`: read-only. Renders the learner's submitted answer, and marks\n * correctness only from an `outcome` the caller supplies (which the server\n * computed). Nothing is submittable.\n */\nexport type RenderMode = 'practice' | 'exam' | 'review';\n\n/**\n * Makes the answer-key-bearing parts of an activity optional, so a component\n * can render either full activity data OR a `redact()` projection with the\n * same props. Fields the SDK classifies `answer-key` become optional here;\n * a component in `exam` mode must not read them at all.\n */\nexport type Renderable<TData> = Omit<TData, 'scoringStrategy'> & {\n scoringStrategy?: unknown;\n /** Present on a `redact()` projection. */\n redacted?: true;\n};\n\n/**\n * {@link Renderable} DISTRIBUTED over a union of activity types.\n *\n * This distinction is load-bearing, not cosmetic. `Renderable<T>` is built on\n * `Omit`, and `Omit` does not distribute: `Omit<A | B, K>` collapses to the\n * keys A and B have IN COMMON, so `Renderable<ActivityData>` is a single\n * object type carrying only the fields every activity shares. Narrowing it\n * dies with it — after `if (data.type === 'multiple-choice')` the compiler\n * still refuses `data.options`, because the union it would narrow to no\n * longer exists.\n *\n * The conditional below re-distributes, so `RenderableActivity` is a real\n * union of per-type renderables and `.type` narrows again. Anything that\n * accepts \"some renderable activity, I don't know which\" — a custom renderer,\n * a sequence entry — must use THIS, not `Renderable<ActivityData>`.\n */\nexport type RenderableActivity<TData extends ActivityData = ActivityData> = TData extends unknown\n ? Renderable<TData>\n : never;\n\n/**\n * Sanitiser for author-supplied rich text (`questionHtml`, `promptHtml`, and a\n * stimulus's `bodyHtml`). The SDK deliberately ships NO sanitiser — that would\n * add a dependency and, worse, a false promise. Rich text is rendered only when\n * you supply this function; without it the component falls back to the\n * plain-text field, which is always escaped. Fail-safe by construction: the SDK\n * never injects HTML it was not explicitly given a sanitiser for.\n *\n * `FillInTheBlanks.passageHtml` is the one exception, and is **never**\n * rendered: the passage hosts the answer inputs, so it cannot be split at the\n * `{{blank}}` placeholders without voiding the sanitiser. The plain `passage`\n * is always used, and passing `passageHtml` warns in development.\n */\nexport type HtmlSanitizer = (html: string) => string;\n\n/**\n * The prop contract shared by every activity component (Req 3.1). Defined\n * here (React-specific) rather than in lk-core, which is React-free.\n *\n * Controlled / uncontrolled follows the React convention: pass `value` +\n * `onChange` to own the learner's answer (restore an in-progress attempt,\n * autosave a delta, drive a review); pass `defaultValue` to seed an\n * uncontrolled component; pass neither to keep the pre-2.1.0 behaviour, where\n * the component owns the answer outright.\n */\nexport interface ActivityProps<TData extends ActivityData = ActivityData> {\n /** Activity content. Accepts a `redact()` projection in `exam` mode. */\n data: RenderableActivity<TData>;\n /**\n * Called when the component scored the attempt itself. Only ever fires in\n * `practice` mode — in `exam` mode the client does not grade, so there is\n * no `ActivityResult` to give you; use `onSubmit`.\n */\n onComplete?: (result: ActivityResult) => void;\n /**\n * Called on submit with the raw learner response and no grade. Fires in\n * every mode, before `onComplete`, so an exam runner can persist the\n * response and let the server score it.\n */\n onSubmit?: (response: LearnerResponse) => void;\n /** Controlled value: the learner's current response. */\n value?: LearnerResponse;\n /** Initial response for an uncontrolled component (ignored when `value` is set). */\n defaultValue?: LearnerResponse;\n /**\n * Mount the component as already submitted — read at mount only, like any\n * `default*` prop.\n *\n * Restoring an attempt without it reopens a question the learner had already\n * submitted as answerable, so on a summative paper they can change and\n * re-submit it. `AttemptState.submittedSlotIds` is what this consumes.\n */\n defaultSubmitted?: boolean;\n /** Fires on every change to the learner's response. Required for a controlled component. */\n onChange?: (response: LearnerResponse) => void;\n /** Presentation mode. Defaults to `practice`. */\n renderMode?: RenderMode;\n /**\n * Server-computed outcome, used by `review` mode to mark correctness\n * without the client ever scoring. Ignored in other modes.\n */\n outcome?: ItemOutcome;\n /** Renders author-supplied rich text when provided. See {@link HtmlSanitizer}. */\n sanitizeHtml?: HtmlSanitizer;\n onInteraction?: (event: InteractionEvent) => void;\n /** Per-instance token overrides, applied as inline CSS vars on the root. */\n theme?: Partial<ThemeTokens>;\n locale?: string;\n disabled?: boolean;\n}\n\n/**\n * Bridges a server-produced `redact()` projection into the `data` prop.\n *\n * `RedactedActivityData` is deliberately index-signature typed in lk-core — it\n * proves a payload is learner-safe, not what shape it has — so TypeScript\n * cannot know it still carries the public fields a renderer needs. This is the\n * SDK's single, documented crossing of that gap, so an exam runner does not\n * have to write `as unknown as` at every call site:\n *\n * ```tsx\n * <MultipleChoice\n * data={asRenderable<MultipleChoiceData>(redactedFromServer)}\n * renderMode=\"exam\"\n * onSubmit={persist}\n * />\n * ```\n *\n * Safe because `exam` mode reads only public fields; the answer-key fields the\n * type claims are exactly the ones the component is forbidden to touch there.\n *\n * Per-type redacted interfaces (`RedactedMultipleChoiceData`, …) ship from\n * lk-core since 0.6.0, but they are not yet *assignable* to `data`:\n * {@link Renderable} widens `scoringStrategy` and leaves nested answer-key\n * fields (`options[].isCorrect`, `blanks[].acceptedAnswers`) required, so a\n * real redacted payload still needs this bridge. Closing that gap is tracked\n * in the roadmap.\n */\nexport function asRenderable<TData extends ActivityData>(\n redacted: RedactedActivityData,\n): Renderable<TData> {\n return redacted as unknown as Renderable<TData>;\n}\n\n/**\n * The same bridge as {@link asRenderable}, for a whole sequence: activities\n * and item groups as a server hands them over, ready for `<ActivitySequence>`.\n *\n * Needed for the same reason and no other. `redactItemGroup` returns\n * `ItemGroup<RedactedActivityData>`, and `RedactedActivityData` is an\n * index-signature type whose fields are all `unknown` — so its `question` is\n * not a `string` and it satisfies no per-type renderable, however the prop is\n * widened. Widening alone cannot fix this; a crossing point is required, and\n * having exactly one keeps `as unknown as` out of consumer code.\n *\n * ```tsx\n * const entries = await fetchExam(); // redacted, server-side\n * <ActivitySequence\n * activities={asRenderableSequence(entries)}\n * renderMode=\"exam\" // REQUIRED: see below\n * shuffleSeed={attemptId}\n * onSubmit={persist}\n * />\n * ```\n *\n * Pass `renderMode=\"exam\"` (or `\"review\"`). Redacted data has no answer key,\n * and the default `practice` mode grades locally — so `<MultipleChoice>` and\n * `<FillInTheBlanks>` throw at render rather than fail at submit time.\n *\n * `<WrittenResponse>` is the exception: it never grades on the client and has\n * no such guard, so a redacted essay renders and stays answerable in\n * `practice`. A mis-wired essay item is therefore SILENT — set `renderMode`\n * explicitly rather than relying on the throw.\n */\nexport function asRenderableSequence(\n entries: readonly (RedactedActivityData | RedactedItemGroupData)[],\n): readonly SequenceEntry<RenderableActivity>[] {\n return entries as unknown as readonly SequenceEntry<RenderableActivity>[];\n}\n\n/** Structural shape of a `redactItemGroup()` projection, as it arrives from a server. */\ntype RedactedItemGroupData = ItemGroup<RedactedActivityData> & { redacted: true };\n"]}
1
+ {"version":3,"sources":["../src/components/types.ts"],"names":[],"mappings":";;;;;;;;;;;;AA2KO,SAAS,aACd,QAAA,EACmB;AACnB,EAAA,OAAO,QAAA;AACT;AAgCO,SAAS,qBACd,OAAA,EAC8C;AAC9C,EAAA,OAAO,OAAA;AACT","file":"index.js","sourcesContent":["import type {\n ActivityData,\n ActivityResult,\n InteractionEvent,\n ItemGroup,\n ItemOutcome,\n LearnerResponse,\n MediaPlayClaim,\n MediaPlayGrant,\n MediaPlayLedgerEntry,\n RedactedActivityData,\n SequenceEntry,\n ThemeTokens,\n} from '@intellectif/lk-core';\n\n/**\n * How an activity is being presented. This is the single switch that decides\n * whether the component may grade, reveal, or submit — the three things a\n * summative exam must take away from the client.\n *\n * - `practice` (default, and the only behaviour before `renderMode` existed):\n * the component owns the attempt. It\n * scores locally on submit, reveals correctness and feedback, and calls\n * `onComplete` with a full {@link ActivityResult}.\n * - `exam`: the component NEVER scores and NEVER reveals correctness. Submit\n * emits the raw learner response through `onSubmit`; the server grades it.\n * Safe to render redacted data, because nothing here needs the answer key.\n * - `review`: read-only. Renders the learner's submitted answer, and marks\n * correctness only from an `outcome` the caller supplies (which the server\n * computed). Nothing is submittable.\n */\nexport type RenderMode = 'practice' | 'exam' | 'review';\n\n/**\n * Makes the answer-key-bearing parts of an activity optional, so a component\n * can render either full activity data OR a `redact()` projection with the\n * same props. Fields the SDK classifies `answer-key` become optional here;\n * a component in `exam` mode must not read them at all.\n */\nexport type Renderable<TData> = Omit<TData, 'scoringStrategy'> & {\n scoringStrategy?: unknown;\n /** Present on a `redact()` projection. */\n redacted?: true;\n};\n\n/**\n * {@link Renderable} DISTRIBUTED over a union of activity types.\n *\n * This distinction is load-bearing, not cosmetic. `Renderable<T>` is built on\n * `Omit`, and `Omit` does not distribute: `Omit<A | B, K>` collapses to the\n * keys A and B have IN COMMON, so `Renderable<ActivityData>` is a single\n * object type carrying only the fields every activity shares. Narrowing it\n * dies with it — after `if (data.type === 'multiple-choice')` the compiler\n * still refuses `data.options`, because the union it would narrow to no\n * longer exists.\n *\n * The conditional below re-distributes, so `RenderableActivity` is a real\n * union of per-type renderables and `.type` narrows again. Anything that\n * accepts \"some renderable activity, I don't know which\" — a custom renderer,\n * a sequence entry — must use THIS, not `Renderable<ActivityData>`.\n */\nexport type RenderableActivity<TData extends ActivityData = ActivityData> = TData extends unknown\n ? Renderable<TData>\n : never;\n\n/**\n * Sanitiser for author-supplied rich text (`questionHtml`, `promptHtml`, and a\n * stimulus's `bodyHtml`). The SDK deliberately ships NO sanitiser — that would\n * add a dependency and, worse, a false promise. Rich text is rendered only when\n * you supply this function; without it the component falls back to the\n * plain-text field, which is always escaped. Fail-safe by construction: the SDK\n * never injects HTML it was not explicitly given a sanitiser for.\n *\n * `FillInTheBlanks.passageHtml` is the one exception, and is **never**\n * rendered: the passage hosts the answer inputs, so it cannot be split at the\n * `{{blank}}` placeholders without voiding the sanitiser. The plain `passage`\n * is always used, and passing `passageHtml` warns in development.\n */\nexport type HtmlSanitizer = (html: string) => string;\n\n/**\n * The prop contract shared by every activity component (Req 3.1). Defined\n * here (React-specific) rather than in lk-core, which is React-free.\n *\n * Controlled / uncontrolled follows the React convention: pass `value` +\n * `onChange` to own the learner's answer (restore an in-progress attempt,\n * autosave a delta, drive a review); pass `defaultValue` to seed an\n * uncontrolled component; pass neither to keep the pre-2.1.0 behaviour, where\n * the component owns the answer outright.\n */\nexport interface ActivityProps<TData extends ActivityData = ActivityData> {\n /** Activity content. Accepts a `redact()` projection in `exam` mode. */\n data: RenderableActivity<TData>;\n /**\n * Called when the component scored the attempt itself. Only ever fires in\n * `practice` mode — in `exam` mode the client does not grade, so there is\n * no `ActivityResult` to give you; use `onSubmit`.\n */\n onComplete?: (result: ActivityResult) => void;\n /**\n * Called on submit with the raw learner response and no grade. Fires in\n * every mode, before `onComplete`, so an exam runner can persist the\n * response and let the server score it.\n */\n onSubmit?: (response: LearnerResponse) => void;\n /** Controlled value: the learner's current response. */\n value?: LearnerResponse;\n /** Initial response for an uncontrolled component (ignored when `value` is set). */\n defaultValue?: LearnerResponse;\n /**\n * Mount the component as already submitted — read at mount only, like any\n * `default*` prop.\n *\n * Restoring an attempt without it reopens a question the learner had already\n * submitted as answerable, so on a summative paper they can change and\n * re-submit it. `AttemptState.submittedSlotIds` is what this consumes.\n */\n defaultSubmitted?: boolean;\n /** Fires on every change to the learner's response. Required for a controlled component. */\n onChange?: (response: LearnerResponse) => void;\n /** Presentation mode. Defaults to `practice`. */\n renderMode?: RenderMode;\n /**\n * Server-computed outcome, used by `review` mode to mark correctness\n * without the client ever scoring. Ignored in other modes.\n */\n outcome?: ItemOutcome;\n /** Renders author-supplied rich text when provided. See {@link HtmlSanitizer}. */\n sanitizeHtml?: HtmlSanitizer;\n /**\n * Binds this activity's own `data.media` to a play budget the consumer\n * persists. Usually supplied by `<ActivitySequence mediaBudget={…}>`; pass it\n * yourself when rendering an activity standalone.\n */\n mediaBudget?: MediaBudgetBinding;\n /** Translations for the audio transport chrome. See {@link MediaTransportStrings}. */\n mediaStrings?: Partial<MediaTransportStrings>;\n onInteraction?: (event: InteractionEvent) => void;\n /** Per-instance token overrides, applied as inline CSS vars on the root. */\n theme?: Partial<ThemeTokens>;\n locale?: string;\n disabled?: boolean;\n}\n\n/**\n * Bridges a server-produced `redact()` projection into the `data` prop.\n *\n * `RedactedActivityData` is deliberately index-signature typed in lk-core — it\n * proves a payload is learner-safe, not what shape it has — so TypeScript\n * cannot know it still carries the public fields a renderer needs. This is the\n * SDK's single, documented crossing of that gap, so an exam runner does not\n * have to write `as unknown as` at every call site:\n *\n * ```tsx\n * <MultipleChoice\n * data={asRenderable<MultipleChoiceData>(redactedFromServer)}\n * renderMode=\"exam\"\n * onSubmit={persist}\n * />\n * ```\n *\n * Safe because `exam` mode reads only public fields; the answer-key fields the\n * type claims are exactly the ones the component is forbidden to touch there.\n *\n * Per-type redacted interfaces (`RedactedMultipleChoiceData`, …) ship from\n * lk-core since 0.6.0, but they are not yet *assignable* to `data`:\n * {@link Renderable} widens `scoringStrategy` and leaves nested answer-key\n * fields (`options[].isCorrect`, `blanks[].acceptedAnswers`) required, so a\n * real redacted payload still needs this bridge. Closing that gap is tracked\n * in the roadmap.\n */\nexport function asRenderable<TData extends ActivityData>(\n redacted: RedactedActivityData,\n): Renderable<TData> {\n return redacted as unknown as Renderable<TData>;\n}\n\n/**\n * The same bridge as {@link asRenderable}, for a whole sequence: activities\n * and item groups as a server hands them over, ready for `<ActivitySequence>`.\n *\n * Needed for the same reason and no other. `redactItemGroup` returns\n * `ItemGroup<RedactedActivityData>`, and `RedactedActivityData` is an\n * index-signature type whose fields are all `unknown` — so its `question` is\n * not a `string` and it satisfies no per-type renderable, however the prop is\n * widened. Widening alone cannot fix this; a crossing point is required, and\n * having exactly one keeps `as unknown as` out of consumer code.\n *\n * ```tsx\n * const entries = await fetchExam(); // redacted, server-side\n * <ActivitySequence\n * activities={asRenderableSequence(entries)}\n * renderMode=\"exam\" // REQUIRED: see below\n * shuffleSeed={attemptId}\n * onSubmit={persist}\n * />\n * ```\n *\n * Pass `renderMode=\"exam\"` (or `\"review\"`). Redacted data has no answer key,\n * and the default `practice` mode grades locally — so `<MultipleChoice>` and\n * `<FillInTheBlanks>` throw at render rather than fail at submit time.\n *\n * `<WrittenResponse>` is the exception: it never grades on the client and has\n * no such guard, so a redacted essay renders and stays answerable in\n * `practice`. A mis-wired essay item is therefore SILENT — set `renderMode`\n * explicitly rather than relying on the throw.\n */\nexport function asRenderableSequence(\n entries: readonly (RedactedActivityData | RedactedItemGroupData)[],\n): readonly SequenceEntry<RenderableActivity>[] {\n return entries as unknown as readonly SequenceEntry<RenderableActivity>[];\n}\n\n/** Structural shape of a `redactItemGroup()` projection, as it arrives from a server. */\ntype RedactedItemGroupData = ItemGroup<RedactedActivityData> & { redacted: true };\n\n/**\n * Every English string the SDK's audio transport renders.\n *\n * Supply them to translate it. These are the highest-stakes strings on a\n * listening paper — \"No plays remaining\" decides whether a learner believes\n * they may try again — so shipping them as untranslatable English inside a\n * Spanish panel was not acceptable. Supplying any of them also sets `lang` on\n * the transport chrome, so a screen reader does not read the SDK's own words\n * with the passage's phonetics.\n */\nexport interface MediaTransportStrings {\n play: string;\n pause: string;\n preparing: string;\n mute: string;\n unmute: string;\n volume: string;\n speed: string;\n seek: string;\n /** e.g. `(1, 2) => '1 of 2 plays remaining'`. */\n playsRemaining: (remaining: number, max: number) => string;\n noPlaysRemaining: string;\n /** Shown before the LAST play is spent, so a stray press cannot cost it. */\n lastPlayConfirm: string;\n lastPlayStart: string;\n lastPlayCancel: string;\n seekBlocked: string;\n rateBlocked: string;\n playFailed: string;\n}\n\n/**\n * Binds ONE media block to a play budget the consumer persists.\n *\n * The SDK refuses a play; it does not remember one. Everything durable here is\n * the consuming application's — see {@link SequenceMediaBudget.onPlayConsumed}.\n */\nexport interface MediaBudgetBinding {\n /** From `slotMediaKey(slotId)` / `stimulusMediaKey(slotId)` in lk-core. */\n key: string;\n /** Plays already spent, and where playback stood. Read at mount only. */\n entry?: MediaPlayLedgerEntry;\n /** Defaults to `renderMode !== 'review'`. An explicit boolean wins either way. */\n enforced?: boolean;\n /** Slot context stamped onto the claim and the interaction event. */\n slotId: string;\n index: number;\n activityId?: string;\n onPlayConsumed?: (claim: MediaPlayClaim) => undefined | Promise<MediaPlayGrant | undefined>;\n onPlayRefunded?: (claim: MediaPlayClaim) => void;\n onPosition?: (key: string, seconds: number) => void;\n /** Translations for the transport chrome. */\n strings?: Partial<MediaTransportStrings>;\n}\n\n/**\n * The pager-level half of a play budget. One prop, because it is one concept.\n */\nexport interface SequenceMediaBudget {\n /**\n * `MediaPlayLedger.entries` goes straight in. An absent key means nothing\n * spent. Read at mount, like `responses`.\n */\n plays?: Readonly<Record<string, MediaPlayLedgerEntry>>;\n /**\n * Re-seed token. Change this string and the budgets re-seed from `plays`\n * WITHOUT remounting the pager — the invigilator path (\"the audio never\n * started, give her the play back\") that would otherwise cost the learner\n * their focus, their scroll position and an unsaved answer.\n */\n resumeKey?: string;\n /** Explicit override of the default (`renderMode !== 'review'`). */\n enforced?: boolean;\n /**\n * Called the instant a play is claimed, BEFORE any audio is audible.\n *\n * Two tiers, chosen by what you return:\n *\n * - **Return nothing (optimistic).** Playback starts immediately and the\n * count is only as durable as your write. **Do not debounce this, and do\n * not batch it with the answer autosave** — an eight-second debounce is\n * exactly long enough to start a third play and hard-reload. A `pagehide`\n * beacon is a backstop, not the mechanism. A crash between this call and\n * your write landing RETURNS the play to the learner; that is the honest\n * description of what you are buying.\n * - **Return a promise (confirmed).** Playback is held — the button reads\n * \"Preparing…\" and is `aria-busy` — until it settles. Resolve with\n * `{ playsUsed }` from an ATOMIC server write (`UPDATE … SET plays = plays\n * + 1 … RETURNING plays`, or a compare-and-set on\n * `claim.previousPlaysUsed`). A resolved count above `maxPlays` refuses the\n * play, which is how a second tab is caught: two mounts both seeded at 0\n * both claim 1, and only an atomic increment can tell them apart. A\n * rejection charges nothing and lets the learner retry. This is the only\n * tier in which \"consumed before audible\" is true of storage rather than\n * only of memory; use it for summative papers.\n *\n * Never settle the promise and the learner cannot play at all: settle it.\n */\n onPlayConsumed?: (claim: MediaPlayClaim) => undefined | Promise<MediaPlayGrant | undefined>;\n /**\n * Called when a charged play produced no audio — the element errored before\n * playback advanced past 0.25 s, an expired signed URL being the realistic\n * cause. Supply it to give the play back, decrementing with a compare-and-set\n * on `claim.playsUsed`. Omit it and the play stays spent: the SDK will not\n * decrement a ledger it has no channel to correct.\n */\n onPlayRefunded?: (claim: MediaPlayClaim) => void;\n /**\n * Position reports, so a refresh resumes the play the learner already paid\n * for instead of charging them again. Fires on pause, on end (with 0), and at\n * most once per whole second of playback.\n *\n * **This one you MAY throttle** — the granularity you persist is the\n * granularity of the replay a crash grants. Three seconds is sane; three\n * minutes is not.\n */\n onPosition?: (key: string, seconds: number) => void;\n /** Translations for the transport chrome. Defaults are English. */\n strings?: Partial<MediaTransportStrings>;\n}\n"]}