@intellectif/lk-react 7.0.0 → 7.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 +109 -0
- package/README.md +5 -2
- package/dist/{WrittenResponse-BJL4dGsR.d.ts → WrittenResponse-BdD1-2tQ.d.ts} +3 -1
- package/dist/{WrittenResponse-zWT-Go1g.d.cts → WrittenResponse-Cb_Fg4fc.d.cts} +3 -1
- package/dist/{chunk-MB532SXM.js → chunk-365FW4BO.js} +24 -13
- package/dist/chunk-365FW4BO.js.map +1 -0
- package/dist/{chunk-J23XVR2B.js → chunk-4WLZONV6.js} +29 -14
- package/dist/chunk-4WLZONV6.js.map +1 -0
- package/dist/{chunk-6F4X2NKL.js → chunk-6DYGQKYD.js} +5 -5
- package/dist/chunk-6DYGQKYD.js.map +1 -0
- package/dist/chunk-7CQ7NUMX.js +113 -0
- package/dist/chunk-7CQ7NUMX.js.map +1 -0
- package/dist/{chunk-VXOOGQ6W.cjs → chunk-7RLLJHCI.cjs} +35 -20
- package/dist/chunk-7RLLJHCI.cjs.map +1 -0
- package/dist/{chunk-J4LP4CLY.cjs → chunk-B4OR4LPL.cjs} +26 -29
- package/dist/chunk-B4OR4LPL.cjs.map +1 -0
- package/dist/{chunk-A572N5G7.cjs → chunk-CKATY6WH.cjs} +16 -27
- package/dist/chunk-CKATY6WH.cjs.map +1 -0
- package/dist/{chunk-6ZKVJAO6.js → chunk-F3EBTOOH.js} +16 -27
- package/dist/chunk-F3EBTOOH.js.map +1 -0
- package/dist/{chunk-KPI335JJ.cjs → chunk-GMPSLHGL.cjs} +27 -29
- package/dist/chunk-GMPSLHGL.cjs.map +1 -0
- package/dist/{chunk-BG456EM3.js → chunk-GTX4IWIT.js} +10 -13
- package/dist/chunk-GTX4IWIT.js.map +1 -0
- package/dist/{chunk-ZPVMB3LH.js → chunk-MU5WELLJ.js} +21 -24
- package/dist/chunk-MU5WELLJ.js.map +1 -0
- package/dist/{chunk-KK4WGW2V.cjs → chunk-MVJQ3KVE.cjs} +11 -14
- package/dist/chunk-MVJQ3KVE.cjs.map +1 -0
- package/dist/{chunk-BNESCXJ5.cjs → chunk-RWR77BOV.cjs} +29 -18
- package/dist/chunk-RWR77BOV.cjs.map +1 -0
- package/dist/chunk-UGLMKKNA.cjs +120 -0
- package/dist/chunk-UGLMKKNA.cjs.map +1 -0
- package/dist/{chunk-PGEG7UZJ.cjs → chunk-YDSFFFR4.cjs} +5 -5
- package/dist/chunk-YDSFFFR4.cjs.map +1 -0
- package/dist/{chunk-CMZGT26S.js → chunk-ZCQXFIB3.js} +23 -25
- package/dist/chunk-ZCQXFIB3.js.map +1 -0
- package/dist/components/ActivitySequence.cjs +9 -8
- package/dist/components/ActivitySequence.d.cts +16 -3
- package/dist/components/ActivitySequence.d.ts +16 -3
- package/dist/components/ActivitySequence.js +8 -7
- package/dist/components/FillInTheBlanks.cjs +5 -4
- package/dist/components/FillInTheBlanks.d.cts +1 -1
- package/dist/components/FillInTheBlanks.d.ts +1 -1
- package/dist/components/FillInTheBlanks.js +4 -3
- package/dist/components/MultipleChoice.cjs +5 -4
- package/dist/components/MultipleChoice.d.cts +1 -1
- package/dist/components/MultipleChoice.d.ts +1 -1
- package/dist/components/MultipleChoice.js +4 -3
- package/dist/components/StimulusPanel.cjs +4 -3
- package/dist/components/StimulusPanel.d.cts +4 -2
- package/dist/components/StimulusPanel.d.ts +4 -2
- package/dist/components/StimulusPanel.js +3 -2
- package/dist/components/WrittenResponse.cjs +5 -4
- package/dist/components/WrittenResponse.d.cts +3 -3
- package/dist/components/WrittenResponse.d.ts +3 -3
- package/dist/components/WrittenResponse.js +4 -3
- package/dist/i18n/LkIntlProvider.cjs +33 -0
- package/dist/i18n/LkIntlProvider.cjs.map +1 -0
- package/dist/i18n/LkIntlProvider.d.cts +71 -0
- package/dist/i18n/LkIntlProvider.d.ts +71 -0
- package/dist/i18n/LkIntlProvider.js +4 -0
- package/dist/i18n/LkIntlProvider.js.map +1 -0
- package/dist/index.cjs +40 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +9 -8
- package/dist/index.js.map +1 -1
- package/dist/{types-eXX4hzSj.d.ts → strings-CfgmSQzU.d.cts} +163 -3
- package/dist/{types-eXX4hzSj.d.cts → strings-CfgmSQzU.d.ts} +163 -3
- package/dist/theme/skin.css +4 -4
- package/package.json +13 -3
- package/dist/chunk-6F4X2NKL.js.map +0 -1
- package/dist/chunk-6ZKVJAO6.js.map +0 -1
- package/dist/chunk-A572N5G7.cjs.map +0 -1
- package/dist/chunk-BG456EM3.js.map +0 -1
- package/dist/chunk-BNESCXJ5.cjs.map +0 -1
- package/dist/chunk-CMZGT26S.js.map +0 -1
- package/dist/chunk-J23XVR2B.js.map +0 -1
- package/dist/chunk-J4LP4CLY.cjs.map +0 -1
- package/dist/chunk-KK4WGW2V.cjs.map +0 -1
- package/dist/chunk-KPI335JJ.cjs.map +0 -1
- package/dist/chunk-MB532SXM.js.map +0 -1
- package/dist/chunk-PGEG7UZJ.cjs.map +0 -1
- package/dist/chunk-VXOOGQ6W.cjs.map +0 -1
- package/dist/chunk-ZPVMB3LH.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActivityData, ActivityResult, LearnerResponse, ItemOutcome, MediaPlayLedgerEntry, MediaPlayClaim, MediaPlayGrant, InteractionEvent, ThemeTokens, RedactedActivityData, ItemGroup, SequenceEntry } from '@intellectif/lk-core';
|
|
1
|
+
import { ActivityData, ActivityResult, LearnerResponse, ItemOutcome, MediaPlayLedgerEntry, MediaPlayClaim, MediaPlayGrant, InteractionEvent, ThemeTokens, RedactedActivityData, ItemGroup, SequenceEntry, Stimulus } from '@intellectif/lk-core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* How an activity is being presented. This is the single switch that decides
|
|
@@ -116,9 +116,26 @@ interface ActivityProps<TData extends ActivityData = ActivityData> {
|
|
|
116
116
|
mediaBudget?: MediaBudgetBinding;
|
|
117
117
|
/** Translations for the audio transport chrome. See {@link MediaTransportStrings}. */
|
|
118
118
|
mediaStrings?: Partial<MediaTransportStrings>;
|
|
119
|
+
/**
|
|
120
|
+
* Overrides the SDK's chrome text for this activity, layered on whatever
|
|
121
|
+
* `LkIntlProvider` supplies. `mediaStrings` still works and is merged after
|
|
122
|
+
* this, so an existing 0.8.x call site keeps behaving as it did.
|
|
123
|
+
*/
|
|
124
|
+
strings?: LkStringsOverride;
|
|
119
125
|
onInteraction?: (event: InteractionEvent) => void;
|
|
120
126
|
/** Per-instance token overrides, applied as inline CSS vars on the root. */
|
|
121
127
|
theme?: Partial<ThemeTokens>;
|
|
128
|
+
/**
|
|
129
|
+
* BCP 47 tag stamped as `lang` on this component's root. This is the
|
|
130
|
+
* INTERFACE language — the SDK's own chrome renders inside that element — so
|
|
131
|
+
* it should carry the same value you give `<LkIntlProvider locale>`. Passing
|
|
132
|
+
* a different one re-declares the language of every SDK string in this
|
|
133
|
+
* subtree without changing the words.
|
|
134
|
+
*
|
|
135
|
+
* It is NOT `data.locale`, which labels xAPI statements only. Authored
|
|
136
|
+
* content in another language belongs on `stimulus.locale`, which
|
|
137
|
+
* `<StimulusPanel>` puts on the passage alone.
|
|
138
|
+
*/
|
|
122
139
|
locale?: string;
|
|
123
140
|
disabled?: boolean;
|
|
124
141
|
}
|
|
@@ -186,7 +203,12 @@ type RedactedItemGroupData = ItemGroup<RedactedActivityData> & {
|
|
|
186
203
|
redacted: true;
|
|
187
204
|
};
|
|
188
205
|
/**
|
|
189
|
-
* Every
|
|
206
|
+
* Every word the SDK's audio transport renders.
|
|
207
|
+
*
|
|
208
|
+
* Words, not characters: the `m:ss / m:ss` clock is digits and punctuation and
|
|
209
|
+
* is formatted by the component, because it reads identically in every locale
|
|
210
|
+
* this SDK targets. The scrubber's SPOKEN value does have a word in it and does
|
|
211
|
+
* have a key ({@link MediaTransportStrings.timeValue}).
|
|
190
212
|
*
|
|
191
213
|
* Supply them to translate it. These are the highest-stakes strings on a
|
|
192
214
|
* listening paper — "No plays remaining" decides whether a learner believes
|
|
@@ -204,6 +226,12 @@ interface MediaTransportStrings {
|
|
|
204
226
|
volume: string;
|
|
205
227
|
speed: string;
|
|
206
228
|
seek: string;
|
|
229
|
+
/**
|
|
230
|
+
* Spoken value of the scrubber, e.g. `('1:05', '4:30') => '1:05 of 4:30'`.
|
|
231
|
+
* Takes ALREADY-FORMATTED `m:ss` strings: a translation should not have to
|
|
232
|
+
* reimplement the clock to change the word between them.
|
|
233
|
+
*/
|
|
234
|
+
timeValue: (elapsed: string, duration: string) => string;
|
|
207
235
|
/** e.g. `(1, 2) => '1 of 2 plays remaining'`. */
|
|
208
236
|
playsRemaining: (remaining: number, max: number) => string;
|
|
209
237
|
noPlaysRemaining: string;
|
|
@@ -304,4 +332,136 @@ interface SequenceMediaBudget {
|
|
|
304
332
|
strings?: Partial<MediaTransportStrings>;
|
|
305
333
|
}
|
|
306
334
|
|
|
307
|
-
|
|
335
|
+
/**
|
|
336
|
+
* Every string the SDK's own chrome renders.
|
|
337
|
+
*
|
|
338
|
+
* Until 0.8.1 these were literals, and `locale` set only the `lang` attribute —
|
|
339
|
+
* so a learner sitting a Spanish paper read a Spanish passage inside an English
|
|
340
|
+
* scaffold, and "No plays remaining" on a listening exam was untranslatable.
|
|
341
|
+
* This is the whole surface, in one place, so a consumer can replace it.
|
|
342
|
+
*
|
|
343
|
+
* **Interpolation and plurals are functions, not format strings.** A message
|
|
344
|
+
* like `"Question {n} of {total}"` needs a parser, and a parser needs plural
|
|
345
|
+
* rules for every locale the SDK does not know. A function hands both problems
|
|
346
|
+
* to the consumer, who has `Intl.PluralRules` and knows their own language:
|
|
347
|
+
*
|
|
348
|
+
* ```ts
|
|
349
|
+
* questionProgress: (index, total) => `Pregunta ${index} de ${total}`,
|
|
350
|
+
* wordCount: (n) => `${n} ${n === 1 ? 'palabra' : 'palabras'}`,
|
|
351
|
+
* ```
|
|
352
|
+
*
|
|
353
|
+
* It also lets a translation reorder its arguments, which a positional token
|
|
354
|
+
* cannot, and TypeScript checks the arity. No parser, no message catalogue
|
|
355
|
+
* format, no runtime dependency.
|
|
356
|
+
*
|
|
357
|
+
* **What is deliberately NOT here: thrown errors.** Every `throw` in this
|
|
358
|
+
* package is addressed to the developer who wired the component up, not to the
|
|
359
|
+
* learner. Translating them would make them unsearchable and would put the one
|
|
360
|
+
* audience who can act on them behind a translation. They stay English.
|
|
361
|
+
*/
|
|
362
|
+
interface LkStrings {
|
|
363
|
+
/** Multiple Choice submit button, and Written Response. */
|
|
364
|
+
submit: string;
|
|
365
|
+
/** Fill-in-the-Blanks in `practice`: the learner is checking their own work. */
|
|
366
|
+
checkAnswers: string;
|
|
367
|
+
/** Fill-in-the-Blanks in `exam`: nothing is checked on the client. */
|
|
368
|
+
submitAnswers: string;
|
|
369
|
+
/**
|
|
370
|
+
* The scored result, read by a screen reader on submit.
|
|
371
|
+
*
|
|
372
|
+
* `percent` is already rounded to a whole number. The authored overall
|
|
373
|
+
* feedback is appended SEPARATELY by the component, so a translation of this
|
|
374
|
+
* sentence never has to carry the author's words — the two were previously
|
|
375
|
+
* concatenated into one literal, which made the score sentence untranslatable
|
|
376
|
+
* without also touching authored content.
|
|
377
|
+
*/
|
|
378
|
+
scoreAnnouncement: (percent: number, passed: boolean) => string;
|
|
379
|
+
/** Prefix announced on submit, before the score sentence. */
|
|
380
|
+
answerSubmitted: string;
|
|
381
|
+
/** An answer submitted for grading that has none yet. Never rendered as 0%. */
|
|
382
|
+
notGradedYet: string;
|
|
383
|
+
/** An outcome that carries no grade and never will. */
|
|
384
|
+
noGradeAvailable: string;
|
|
385
|
+
showFeedback: string;
|
|
386
|
+
hideFeedback: string;
|
|
387
|
+
/** Accessible name of a blank's input. `ordinal` is 1-based. */
|
|
388
|
+
blankLabel: (ordinal: number) => string;
|
|
389
|
+
showHint: string;
|
|
390
|
+
hideHint: string;
|
|
391
|
+
/**
|
|
392
|
+
* Announced when an essay is handed in. Distinct from {@link LkStrings.awaitingGrade},
|
|
393
|
+
* which is the `review`-mode state of a submission made earlier: this one
|
|
394
|
+
* confirms the act, and is the only thing a learner hears at the moment they
|
|
395
|
+
* commit work that nothing on the client will grade.
|
|
396
|
+
*/
|
|
397
|
+
responseSubmitted: string;
|
|
398
|
+
/** Live word counter. Needs the count for languages whose plural rules differ. */
|
|
399
|
+
wordCount: (count: number) => string;
|
|
400
|
+
/** The authored bounds, e.g. "50–200 words". `min` is 0 when unbounded below. */
|
|
401
|
+
wordBounds: (min: number, max: number) => string;
|
|
402
|
+
/** A rubric criterion the grader marked not applicable. */
|
|
403
|
+
notApplicable: string;
|
|
404
|
+
/** A returned grade flagged for a human to look at — learner-visible by design. */
|
|
405
|
+
awaitingHumanReview: string;
|
|
406
|
+
/** Submitted, no grade back yet. */
|
|
407
|
+
awaitingGrade: string;
|
|
408
|
+
/** The grader could not produce a grade. */
|
|
409
|
+
couldNotBeGraded: string;
|
|
410
|
+
previous: string;
|
|
411
|
+
next: string;
|
|
412
|
+
/** Progress label, also the question region's accessible name. `index` is 1-based. */
|
|
413
|
+
questionProgress: (index: number, total: number) => string;
|
|
414
|
+
/** Shown when no renderer is registered for an activity type. */
|
|
415
|
+
unsupportedActivity: string;
|
|
416
|
+
/**
|
|
417
|
+
* Accessible name of a provider iframe whose author supplied no `alt`.
|
|
418
|
+
* `MediaSchema` requires `alt` on an embed, so validated content never
|
|
419
|
+
* reaches this — but `validateActivity` is opt-in, and a screen-reader
|
|
420
|
+
* learner meeting an unvalidated one should not meet it in English.
|
|
421
|
+
*/
|
|
422
|
+
embeddedMedia: string;
|
|
423
|
+
/**
|
|
424
|
+
* Accessible name of the stimulus region when the author gave it no title.
|
|
425
|
+
* Keyed by `Stimulus['kind']`, because "Recording" and "Passage" do not share
|
|
426
|
+
* a gender or an article in every language.
|
|
427
|
+
*/
|
|
428
|
+
stimulusKind: Record<Stimulus['kind'], string>;
|
|
429
|
+
/**
|
|
430
|
+
* How far the material carries, e.g. "Questions 3–8". Called with equal
|
|
431
|
+
* `first` and `last` for a single question, so a translation can choose its
|
|
432
|
+
* own singular rather than receive a pre-pluralised English one.
|
|
433
|
+
*/
|
|
434
|
+
stimulusRange: (first: number, last: number) => string;
|
|
435
|
+
/** Production fallback when an activity throws during render. */
|
|
436
|
+
activityFailed: string;
|
|
437
|
+
/** The same, when the activity's title is known. */
|
|
438
|
+
activityFailedNamed: (title: string) => string;
|
|
439
|
+
/** Production fallback when the activity has no title to name. */
|
|
440
|
+
activityFailedUnnamed: string;
|
|
441
|
+
/**
|
|
442
|
+
* The listening-paper transport. Shipped in 0.8.0 as its own `mediaStrings`
|
|
443
|
+
* prop, which still works — this is the same shape, reachable from the one
|
|
444
|
+
* provider so a consumer does not wire two mechanisms.
|
|
445
|
+
*/
|
|
446
|
+
media: MediaTransportStrings;
|
|
447
|
+
}
|
|
448
|
+
/** A partial override, nested one level for {@link LkStrings.media}. */
|
|
449
|
+
type LkStringsOverride = Partial<Omit<LkStrings, 'media' | 'stimulusKind'>> & {
|
|
450
|
+
media?: Partial<MediaTransportStrings>;
|
|
451
|
+
stimulusKind?: Partial<Record<Stimulus['kind'], string>>;
|
|
452
|
+
};
|
|
453
|
+
/**
|
|
454
|
+
* English defaults, byte-identical to what every component rendered before the
|
|
455
|
+
* strings surface existed. Supplying no override changes nothing on screen.
|
|
456
|
+
*/
|
|
457
|
+
declare const DEFAULT_STRINGS: LkStrings;
|
|
458
|
+
/**
|
|
459
|
+
* Layers an override onto a base dictionary.
|
|
460
|
+
*
|
|
461
|
+
* Two levels deep and no further, because the shape is two levels deep. A
|
|
462
|
+
* generic deep merge would silently accept a nested object where a function
|
|
463
|
+
* belongs and fail at render instead of at the call site.
|
|
464
|
+
*/
|
|
465
|
+
declare function mergeStrings(base: LkStrings, override?: LkStringsOverride): LkStrings;
|
|
466
|
+
|
|
467
|
+
export { type ActivityProps as A, DEFAULT_STRINGS as D, type HtmlSanitizer as H, type LkStrings as L, type MediaBudgetBinding as M, type RenderMode as R, type SequenceMediaBudget as S, type LkStringsOverride as a, type MediaTransportStrings as b, type Renderable as c, type RenderableActivity as d, asRenderable as e, asRenderableSequence as f, mergeStrings as m };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActivityData, ActivityResult, LearnerResponse, ItemOutcome, MediaPlayLedgerEntry, MediaPlayClaim, MediaPlayGrant, InteractionEvent, ThemeTokens, RedactedActivityData, ItemGroup, SequenceEntry } from '@intellectif/lk-core';
|
|
1
|
+
import { ActivityData, ActivityResult, LearnerResponse, ItemOutcome, MediaPlayLedgerEntry, MediaPlayClaim, MediaPlayGrant, InteractionEvent, ThemeTokens, RedactedActivityData, ItemGroup, SequenceEntry, Stimulus } from '@intellectif/lk-core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* How an activity is being presented. This is the single switch that decides
|
|
@@ -116,9 +116,26 @@ interface ActivityProps<TData extends ActivityData = ActivityData> {
|
|
|
116
116
|
mediaBudget?: MediaBudgetBinding;
|
|
117
117
|
/** Translations for the audio transport chrome. See {@link MediaTransportStrings}. */
|
|
118
118
|
mediaStrings?: Partial<MediaTransportStrings>;
|
|
119
|
+
/**
|
|
120
|
+
* Overrides the SDK's chrome text for this activity, layered on whatever
|
|
121
|
+
* `LkIntlProvider` supplies. `mediaStrings` still works and is merged after
|
|
122
|
+
* this, so an existing 0.8.x call site keeps behaving as it did.
|
|
123
|
+
*/
|
|
124
|
+
strings?: LkStringsOverride;
|
|
119
125
|
onInteraction?: (event: InteractionEvent) => void;
|
|
120
126
|
/** Per-instance token overrides, applied as inline CSS vars on the root. */
|
|
121
127
|
theme?: Partial<ThemeTokens>;
|
|
128
|
+
/**
|
|
129
|
+
* BCP 47 tag stamped as `lang` on this component's root. This is the
|
|
130
|
+
* INTERFACE language — the SDK's own chrome renders inside that element — so
|
|
131
|
+
* it should carry the same value you give `<LkIntlProvider locale>`. Passing
|
|
132
|
+
* a different one re-declares the language of every SDK string in this
|
|
133
|
+
* subtree without changing the words.
|
|
134
|
+
*
|
|
135
|
+
* It is NOT `data.locale`, which labels xAPI statements only. Authored
|
|
136
|
+
* content in another language belongs on `stimulus.locale`, which
|
|
137
|
+
* `<StimulusPanel>` puts on the passage alone.
|
|
138
|
+
*/
|
|
122
139
|
locale?: string;
|
|
123
140
|
disabled?: boolean;
|
|
124
141
|
}
|
|
@@ -186,7 +203,12 @@ type RedactedItemGroupData = ItemGroup<RedactedActivityData> & {
|
|
|
186
203
|
redacted: true;
|
|
187
204
|
};
|
|
188
205
|
/**
|
|
189
|
-
* Every
|
|
206
|
+
* Every word the SDK's audio transport renders.
|
|
207
|
+
*
|
|
208
|
+
* Words, not characters: the `m:ss / m:ss` clock is digits and punctuation and
|
|
209
|
+
* is formatted by the component, because it reads identically in every locale
|
|
210
|
+
* this SDK targets. The scrubber's SPOKEN value does have a word in it and does
|
|
211
|
+
* have a key ({@link MediaTransportStrings.timeValue}).
|
|
190
212
|
*
|
|
191
213
|
* Supply them to translate it. These are the highest-stakes strings on a
|
|
192
214
|
* listening paper — "No plays remaining" decides whether a learner believes
|
|
@@ -204,6 +226,12 @@ interface MediaTransportStrings {
|
|
|
204
226
|
volume: string;
|
|
205
227
|
speed: string;
|
|
206
228
|
seek: string;
|
|
229
|
+
/**
|
|
230
|
+
* Spoken value of the scrubber, e.g. `('1:05', '4:30') => '1:05 of 4:30'`.
|
|
231
|
+
* Takes ALREADY-FORMATTED `m:ss` strings: a translation should not have to
|
|
232
|
+
* reimplement the clock to change the word between them.
|
|
233
|
+
*/
|
|
234
|
+
timeValue: (elapsed: string, duration: string) => string;
|
|
207
235
|
/** e.g. `(1, 2) => '1 of 2 plays remaining'`. */
|
|
208
236
|
playsRemaining: (remaining: number, max: number) => string;
|
|
209
237
|
noPlaysRemaining: string;
|
|
@@ -304,4 +332,136 @@ interface SequenceMediaBudget {
|
|
|
304
332
|
strings?: Partial<MediaTransportStrings>;
|
|
305
333
|
}
|
|
306
334
|
|
|
307
|
-
|
|
335
|
+
/**
|
|
336
|
+
* Every string the SDK's own chrome renders.
|
|
337
|
+
*
|
|
338
|
+
* Until 0.8.1 these were literals, and `locale` set only the `lang` attribute —
|
|
339
|
+
* so a learner sitting a Spanish paper read a Spanish passage inside an English
|
|
340
|
+
* scaffold, and "No plays remaining" on a listening exam was untranslatable.
|
|
341
|
+
* This is the whole surface, in one place, so a consumer can replace it.
|
|
342
|
+
*
|
|
343
|
+
* **Interpolation and plurals are functions, not format strings.** A message
|
|
344
|
+
* like `"Question {n} of {total}"` needs a parser, and a parser needs plural
|
|
345
|
+
* rules for every locale the SDK does not know. A function hands both problems
|
|
346
|
+
* to the consumer, who has `Intl.PluralRules` and knows their own language:
|
|
347
|
+
*
|
|
348
|
+
* ```ts
|
|
349
|
+
* questionProgress: (index, total) => `Pregunta ${index} de ${total}`,
|
|
350
|
+
* wordCount: (n) => `${n} ${n === 1 ? 'palabra' : 'palabras'}`,
|
|
351
|
+
* ```
|
|
352
|
+
*
|
|
353
|
+
* It also lets a translation reorder its arguments, which a positional token
|
|
354
|
+
* cannot, and TypeScript checks the arity. No parser, no message catalogue
|
|
355
|
+
* format, no runtime dependency.
|
|
356
|
+
*
|
|
357
|
+
* **What is deliberately NOT here: thrown errors.** Every `throw` in this
|
|
358
|
+
* package is addressed to the developer who wired the component up, not to the
|
|
359
|
+
* learner. Translating them would make them unsearchable and would put the one
|
|
360
|
+
* audience who can act on them behind a translation. They stay English.
|
|
361
|
+
*/
|
|
362
|
+
interface LkStrings {
|
|
363
|
+
/** Multiple Choice submit button, and Written Response. */
|
|
364
|
+
submit: string;
|
|
365
|
+
/** Fill-in-the-Blanks in `practice`: the learner is checking their own work. */
|
|
366
|
+
checkAnswers: string;
|
|
367
|
+
/** Fill-in-the-Blanks in `exam`: nothing is checked on the client. */
|
|
368
|
+
submitAnswers: string;
|
|
369
|
+
/**
|
|
370
|
+
* The scored result, read by a screen reader on submit.
|
|
371
|
+
*
|
|
372
|
+
* `percent` is already rounded to a whole number. The authored overall
|
|
373
|
+
* feedback is appended SEPARATELY by the component, so a translation of this
|
|
374
|
+
* sentence never has to carry the author's words — the two were previously
|
|
375
|
+
* concatenated into one literal, which made the score sentence untranslatable
|
|
376
|
+
* without also touching authored content.
|
|
377
|
+
*/
|
|
378
|
+
scoreAnnouncement: (percent: number, passed: boolean) => string;
|
|
379
|
+
/** Prefix announced on submit, before the score sentence. */
|
|
380
|
+
answerSubmitted: string;
|
|
381
|
+
/** An answer submitted for grading that has none yet. Never rendered as 0%. */
|
|
382
|
+
notGradedYet: string;
|
|
383
|
+
/** An outcome that carries no grade and never will. */
|
|
384
|
+
noGradeAvailable: string;
|
|
385
|
+
showFeedback: string;
|
|
386
|
+
hideFeedback: string;
|
|
387
|
+
/** Accessible name of a blank's input. `ordinal` is 1-based. */
|
|
388
|
+
blankLabel: (ordinal: number) => string;
|
|
389
|
+
showHint: string;
|
|
390
|
+
hideHint: string;
|
|
391
|
+
/**
|
|
392
|
+
* Announced when an essay is handed in. Distinct from {@link LkStrings.awaitingGrade},
|
|
393
|
+
* which is the `review`-mode state of a submission made earlier: this one
|
|
394
|
+
* confirms the act, and is the only thing a learner hears at the moment they
|
|
395
|
+
* commit work that nothing on the client will grade.
|
|
396
|
+
*/
|
|
397
|
+
responseSubmitted: string;
|
|
398
|
+
/** Live word counter. Needs the count for languages whose plural rules differ. */
|
|
399
|
+
wordCount: (count: number) => string;
|
|
400
|
+
/** The authored bounds, e.g. "50–200 words". `min` is 0 when unbounded below. */
|
|
401
|
+
wordBounds: (min: number, max: number) => string;
|
|
402
|
+
/** A rubric criterion the grader marked not applicable. */
|
|
403
|
+
notApplicable: string;
|
|
404
|
+
/** A returned grade flagged for a human to look at — learner-visible by design. */
|
|
405
|
+
awaitingHumanReview: string;
|
|
406
|
+
/** Submitted, no grade back yet. */
|
|
407
|
+
awaitingGrade: string;
|
|
408
|
+
/** The grader could not produce a grade. */
|
|
409
|
+
couldNotBeGraded: string;
|
|
410
|
+
previous: string;
|
|
411
|
+
next: string;
|
|
412
|
+
/** Progress label, also the question region's accessible name. `index` is 1-based. */
|
|
413
|
+
questionProgress: (index: number, total: number) => string;
|
|
414
|
+
/** Shown when no renderer is registered for an activity type. */
|
|
415
|
+
unsupportedActivity: string;
|
|
416
|
+
/**
|
|
417
|
+
* Accessible name of a provider iframe whose author supplied no `alt`.
|
|
418
|
+
* `MediaSchema` requires `alt` on an embed, so validated content never
|
|
419
|
+
* reaches this — but `validateActivity` is opt-in, and a screen-reader
|
|
420
|
+
* learner meeting an unvalidated one should not meet it in English.
|
|
421
|
+
*/
|
|
422
|
+
embeddedMedia: string;
|
|
423
|
+
/**
|
|
424
|
+
* Accessible name of the stimulus region when the author gave it no title.
|
|
425
|
+
* Keyed by `Stimulus['kind']`, because "Recording" and "Passage" do not share
|
|
426
|
+
* a gender or an article in every language.
|
|
427
|
+
*/
|
|
428
|
+
stimulusKind: Record<Stimulus['kind'], string>;
|
|
429
|
+
/**
|
|
430
|
+
* How far the material carries, e.g. "Questions 3–8". Called with equal
|
|
431
|
+
* `first` and `last` for a single question, so a translation can choose its
|
|
432
|
+
* own singular rather than receive a pre-pluralised English one.
|
|
433
|
+
*/
|
|
434
|
+
stimulusRange: (first: number, last: number) => string;
|
|
435
|
+
/** Production fallback when an activity throws during render. */
|
|
436
|
+
activityFailed: string;
|
|
437
|
+
/** The same, when the activity's title is known. */
|
|
438
|
+
activityFailedNamed: (title: string) => string;
|
|
439
|
+
/** Production fallback when the activity has no title to name. */
|
|
440
|
+
activityFailedUnnamed: string;
|
|
441
|
+
/**
|
|
442
|
+
* The listening-paper transport. Shipped in 0.8.0 as its own `mediaStrings`
|
|
443
|
+
* prop, which still works — this is the same shape, reachable from the one
|
|
444
|
+
* provider so a consumer does not wire two mechanisms.
|
|
445
|
+
*/
|
|
446
|
+
media: MediaTransportStrings;
|
|
447
|
+
}
|
|
448
|
+
/** A partial override, nested one level for {@link LkStrings.media}. */
|
|
449
|
+
type LkStringsOverride = Partial<Omit<LkStrings, 'media' | 'stimulusKind'>> & {
|
|
450
|
+
media?: Partial<MediaTransportStrings>;
|
|
451
|
+
stimulusKind?: Partial<Record<Stimulus['kind'], string>>;
|
|
452
|
+
};
|
|
453
|
+
/**
|
|
454
|
+
* English defaults, byte-identical to what every component rendered before the
|
|
455
|
+
* strings surface existed. Supplying no override changes nothing on screen.
|
|
456
|
+
*/
|
|
457
|
+
declare const DEFAULT_STRINGS: LkStrings;
|
|
458
|
+
/**
|
|
459
|
+
* Layers an override onto a base dictionary.
|
|
460
|
+
*
|
|
461
|
+
* Two levels deep and no further, because the shape is two levels deep. A
|
|
462
|
+
* generic deep merge would silently accept a nested object where a function
|
|
463
|
+
* belongs and fail at render instead of at the call site.
|
|
464
|
+
*/
|
|
465
|
+
declare function mergeStrings(base: LkStrings, override?: LkStringsOverride): LkStrings;
|
|
466
|
+
|
|
467
|
+
export { type ActivityProps as A, DEFAULT_STRINGS as D, type HtmlSanitizer as H, type LkStrings as L, type MediaBudgetBinding as M, type RenderMode as R, type SequenceMediaBudget as S, type LkStringsOverride as a, type MediaTransportStrings as b, type Renderable as c, type RenderableActivity as d, asRenderable as e, asRenderableSequence as f, mergeStrings as m };
|
package/dist/theme/skin.css
CHANGED
|
@@ -260,7 +260,7 @@
|
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
.lk-mc-option-feedback {
|
|
263
|
-
margin-
|
|
263
|
+
margin-inline-start: auto;
|
|
264
264
|
font-size: var(--lk-font-size-sm);
|
|
265
265
|
color: var(--lk-color-text-muted);
|
|
266
266
|
}
|
|
@@ -338,7 +338,7 @@
|
|
|
338
338
|
|
|
339
339
|
/* Inline per-blank feedback shown after submission. */
|
|
340
340
|
.lk-fib-blank-feedback {
|
|
341
|
-
margin-
|
|
341
|
+
margin-inline-start: var(--lk-spacing-xs);
|
|
342
342
|
font-size: var(--lk-font-size-sm);
|
|
343
343
|
color: var(--lk-color-text-muted);
|
|
344
344
|
}
|
|
@@ -376,7 +376,7 @@
|
|
|
376
376
|
.lk-fib-blank [aria-live]:not(:empty) {
|
|
377
377
|
position: absolute;
|
|
378
378
|
bottom: calc(100% + 6px);
|
|
379
|
-
|
|
379
|
+
inset-inline-start: 0;
|
|
380
380
|
z-index: 1;
|
|
381
381
|
max-width: 16rem;
|
|
382
382
|
padding: var(--lk-spacing-xs) var(--lk-spacing-sm);
|
|
@@ -393,7 +393,7 @@
|
|
|
393
393
|
content: "";
|
|
394
394
|
position: absolute;
|
|
395
395
|
top: 100%;
|
|
396
|
-
|
|
396
|
+
inset-inline-start: var(--lk-spacing-sm);
|
|
397
397
|
border: 5px solid transparent;
|
|
398
398
|
border-top-color: var(--lk-color-text);
|
|
399
399
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intellectif/lk-react",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.1.0",
|
|
4
4
|
"description": "Accessible React 19 assessment components — multiple choice, fill-in-the-blanks, written response — with a resumable exam/review pager, theming, and xAPI delivery.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -113,6 +113,16 @@
|
|
|
113
113
|
"default": "./dist/hooks/useXAPI.cjs"
|
|
114
114
|
}
|
|
115
115
|
},
|
|
116
|
+
"./i18n/LkIntlProvider": {
|
|
117
|
+
"import": {
|
|
118
|
+
"types": "./dist/i18n/LkIntlProvider.d.ts",
|
|
119
|
+
"default": "./dist/i18n/LkIntlProvider.js"
|
|
120
|
+
},
|
|
121
|
+
"require": {
|
|
122
|
+
"types": "./dist/i18n/LkIntlProvider.d.cts",
|
|
123
|
+
"default": "./dist/i18n/LkIntlProvider.cjs"
|
|
124
|
+
}
|
|
125
|
+
},
|
|
116
126
|
"./theme/ThemeProvider": {
|
|
117
127
|
"import": {
|
|
118
128
|
"types": "./dist/theme/ThemeProvider.d.ts",
|
|
@@ -137,7 +147,7 @@
|
|
|
137
147
|
"peerDependencies": {
|
|
138
148
|
"react": "^19",
|
|
139
149
|
"react-dom": "^19",
|
|
140
|
-
"@intellectif/lk-core": "^0.8.
|
|
150
|
+
"@intellectif/lk-core": "^0.8.2"
|
|
141
151
|
},
|
|
142
152
|
"devDependencies": {
|
|
143
153
|
"@axe-core/react": "^4.10.0",
|
|
@@ -156,7 +166,7 @@
|
|
|
156
166
|
"tsup": "^8.3.0",
|
|
157
167
|
"vitest": "^4.1.6",
|
|
158
168
|
"vitest-axe": "^0.1.0",
|
|
159
|
-
"@intellectif/lk-core": "0.8.
|
|
169
|
+
"@intellectif/lk-core": "0.8.2"
|
|
160
170
|
},
|
|
161
171
|
"license": "MIT",
|
|
162
172
|
"engines": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/ActivityErrorBoundary.tsx","../src/components/_internal.ts","../src/components/shared/FeedbackRegion.tsx"],"names":["jsx"],"mappings":";;;;AAIA,SAAS,YAAA,GAAwB;AAC/B,EAAA,MAAM,CAAA,GAAI,UAAA;AACV,EAAA,OAAO,CAAA,CAAE,OAAA,EAAS,GAAA,EAAK,QAAA,KAAa,YAAA;AACtC;AAkBO,IAAM,qBAAA,GAAN,cAAoC,SAAA,CAGzC;AAAA,EACA,KAAA,GAAoC,EAAE,KAAA,EAAO,IAAA,EAAK;AAAA,EAElD,OAAO,yBAAyB,KAAA,EAA0C;AACxE,IAAA,OAAO,EAAE,KAAA,EAAM;AAAA,EACjB;AAAA,EAEA,MAAA,GAAoB;AAClB,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,IAAA,CAAK,KAAA;AACvB,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAA,OAAO,KAAK,KAAA,CAAM,QAAA;AAAA,IACpB;AAEA,IAAA,IAAI,CAAC,cAAa,EAAG;AACnB,MAAA,uBACE,IAAA,CAAC,KAAA,EAAA,EAAI,IAAA,EAAK,OAAA,EACR,QAAA,EAAA;AAAA,wBAAA,GAAA,CAAC,YAAO,QAAA,EAAA,2BAAA,EAAyB,CAAA;AAAA,wBACjC,GAAA,CAAC,KAAA,EAAA,EAAK,QAAA,EAAA,KAAA,CAAM,KAAA,IAAS,MAAM,OAAA,EAAQ;AAAA,OAAA,EACrC,CAAA;AAAA,IAEJ;AAEA,IAAA,uBACE,GAAA,CAAC,KAAA,EAAA,EAAI,IAAA,EAAK,OAAA,EACP,QAAA,EAAA,IAAA,CAAK,KAAA,CAAM,aAAA,GACR,CAAA,CAAA,EAAI,IAAA,CAAK,KAAA,CAAM,aAAa,CAAA,yBAAA,CAAA,GAC5B,uCAAA,EACN,CAAA;AAAA,EAEJ;AACF;;;ACvDO,SAAS,aAAA,GAAyB;AACvC,EAAA,MAAM,CAAA,GAAI,UAAA;AACV,EAAA,OAAO,CAAA,CAAE,OAAA,EAAS,GAAA,EAAK,QAAA,KAAa,YAAA;AACtC;AAOO,IAAM,eAAA,GAA6B;AAAA,EACxC,UAAA,EAAY,OAAA;AAAA,EACZ,OAAA,EAAS,EAAE,QAAA,EAAU,6CAAA,EAA+C,MAAM,WAAA;AAC5E;AAGO,SAAS,YAAY,EAAA,EAAoB;AAC9C,EAAA,OAAO,CAAA,0BAAA,EAA6B,kBAAA,CAAmB,EAAE,CAAC,CAAA,CAAA;AAC5D;AAQO,SAAS,eAAA,GAA0B;AACxC,EAAA,MAAM,IAAK,UAAA,CAA0D,MAAA;AACrE,EAAA,IAAI,GAAG,UAAA,EAAY;AACjB,IAAA,OAAO,EAAE,UAAA,EAAW;AAAA,EACtB;AACA,EAAA,OAAO,KAAK,IAAA,CAAK,MAAA,GAAS,QAAA,CAAS,EAAE,EAAE,KAAA,CAAM,CAAC,CAAC,CAAA,EAAG,IAAA,CAAK,QAAO,CAAE,QAAA,CAAS,EAAE,CAAA,CAAE,KAAA,CAAM,CAAC,CAAC,CAAA,CAAA;AACvF;ACrBO,SAAS,cAAA,CAAe,EAAE,EAAA,EAAI,SAAA,EAAW,UAAS,EAAwB;AAC/E,EAAA,uBACEA,IAAC,KAAA,EAAA,EAAI,EAAA,EAAQ,WAAsB,WAAA,EAAU,QAAA,EAAS,aAAA,EAAY,MAAA,EAC/D,QAAA,EACH,CAAA;AAEJ","file":"chunk-6F4X2NKL.js","sourcesContent":["'use client';\n\nimport { Component, type ReactNode } from 'react';\n\nfunction isProduction(): boolean {\n const g = globalThis as { process?: { env?: Record<string, string | undefined> } };\n return g.process?.env?.NODE_ENV === 'production';\n}\n\nexport interface ActivityErrorBoundaryProps {\n /** Shown in the production fallback when a render error occurs. */\n activityTitle?: string;\n children: ReactNode;\n}\n\ninterface ActivityErrorBoundaryState {\n error: Error | null;\n}\n\n/**\n * Catches render errors in an activity subtree. In development it renders the\n * full error stack to aid debugging; in production it renders an accessible,\n * generic fallback (optionally naming the activity) so a single broken\n * activity never crashes the host application.\n */\nexport class ActivityErrorBoundary extends Component<\n ActivityErrorBoundaryProps,\n ActivityErrorBoundaryState\n> {\n state: ActivityErrorBoundaryState = { error: null };\n\n static getDerivedStateFromError(error: Error): ActivityErrorBoundaryState {\n return { error };\n }\n\n render(): ReactNode {\n const { error } = this.state;\n if (!error) {\n return this.props.children;\n }\n\n if (!isProduction()) {\n return (\n <div role=\"alert\">\n <strong>Activity failed to render</strong>\n <pre>{error.stack ?? error.message}</pre>\n </div>\n );\n }\n\n return (\n <div role=\"alert\">\n {this.props.activityTitle\n ? `\"${this.props.activityTitle}\" could not be displayed.`\n : 'This activity could not be displayed.'}\n </div>\n );\n }\n}\n","import type { XAPIActor } from '@intellectif/lk-core';\n\n/** Dev = anything other than an explicit production NODE_ENV (browser-safe). */\nexport function isDevelopment(): boolean {\n const g = globalThis as { process?: { env?: Record<string, string | undefined> } };\n return g.process?.env?.NODE_ENV !== 'production';\n}\n\n/**\n * Activity components cannot know the learner's identity (Req 3.1 fixes the\n * prop set). They emit a structurally-valid statement with this anonymous\n * actor; real identity is applied by the useXAPI/LRS layer (XAPIConfig.actor).\n */\nexport const ANONYMOUS_ACTOR: XAPIActor = {\n objectType: 'Agent',\n account: { homePage: 'https://github.com/intellectif/learning-kit', name: 'anonymous' },\n};\n\n/** `data.id` is not an IRI; xAPI object ids must be. Wrap it as a URN. */\nexport function objectIdFor(id: string): string {\n return `urn:learning-kit:activity:${encodeURIComponent(id)}`;\n}\n\n/**\n * Random per-mount session id for the shuffle seed. `crypto.randomUUID` is\n * unavailable outside secure contexts (plain-http LAN/staging hosts), so a\n * Math.random fallback keeps the component from crashing there — the seed\n * only randomises presentation order, so cryptographic strength is not needed.\n */\nexport function randomSessionId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `s-${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;\n}\n","import type { ReactNode } from 'react';\n\nexport interface FeedbackRegionProps {\n /** Identifier so activity inputs can reference this region via aria. */\n id?: string;\n className?: string;\n children?: ReactNode;\n}\n\n/**\n * A polite ARIA live region. Screen readers announce content injected here\n * (score, feedback, state changes) without moving focus. Pure/presentational\n * — safe to render in any environment.\n */\nexport function FeedbackRegion({ id, className, children }: FeedbackRegionProps) {\n return (\n <div id={id} className={className} aria-live=\"polite\" aria-atomic=\"true\">\n {children}\n </div>\n );\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/shared/AudioTransport.tsx","../src/components/shared/ActivityMedia.tsx"],"names":["jsx","jsxs"],"mappings":"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","file":"chunk-6ZKVJAO6.js","sourcesContent":["'use client';\n\nimport type {\n ActivityMedia as ActivityMediaData,\n MediaPlayClaim,\n ResolvedPlaybackPolicy,\n} from '@intellectif/lk-core';\nimport { useCallback, useEffect, useId, useRef, useState } from 'react';\nimport type { MediaBudgetBinding, MediaTransportStrings } from '../types.js';\n\n/**\n * The SDK's own audio player, used whenever a policy has something to enforce.\n *\n * The browser's control bar cannot express a spent budget — its play button\n * stays enabled — and a control that looks operable and does nothing is a\n * WCAG 3.2.2 / 4.1.3 failure, so an enforcing policy replaces the bar rather\n * than lying with it. What replaces it is a SUPERSET minus what the policy\n * removes on purpose: play/pause, elapsed and total time, mute, volume, an\n * optional speed control, an optional scrubber, and a live plays-remaining\n * status. Volume and mute are never restricted by any policy — they affect no\n * assessment property, and a learner in a lab with a locked OS volume has no\n * other lever once the native bar is gone.\n */\n\nconst DEFAULT_STRINGS: MediaTransportStrings = {\n play: 'Play',\n pause: 'Pause',\n preparing: 'Preparing…',\n mute: 'Mute',\n unmute: 'Unmute',\n volume: 'Volume',\n speed: 'Playback speed',\n seek: 'Seek',\n playsRemaining: (remaining, max) =>\n `${remaining} of ${max} play${max === 1 ? '' : 's'} remaining`,\n noPlaysRemaining: 'No plays remaining',\n lastPlayConfirm: 'This is your last play. Start it now?',\n lastPlayStart: 'Start last play',\n lastPlayCancel: 'Not yet',\n seekBlocked: 'Rewinding is not available for this recording.',\n rateBlocked: 'Playback speed is fixed for this recording.',\n playFailed: 'The recording could not be started. Try again.',\n};\n\n/** A resume is a start within a quarter-second of where playback last stopped. */\nconst RESUME_EPSILON = 0.25;\n/** Below this, a seek is the browser settling rather than the learner moving. */\nconst SEEK_EPSILON = 0.35;\n/** A charged play that errors before this much audio played produced nothing. */\nconst NO_PROGRESS = 0.25;\n\nconst formatTime = (seconds: number): string => {\n if (!Number.isFinite(seconds) || seconds < 0) {\n return '0:00';\n }\n const whole = Math.floor(seconds);\n return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, '0')}`;\n};\n\nexport interface AudioTransportProps {\n media: ActivityMediaData;\n policy: ResolvedPlaybackPolicy;\n renderMode?: 'practice' | 'exam' | 'review';\n disabled?: boolean;\n locale?: string;\n mediaBudget?: MediaBudgetBinding;\n mediaStrings?: Partial<MediaTransportStrings>;\n onInteraction?: (event: {\n type: string;\n activityId: string;\n timestamp: number;\n payload: Record<string, unknown>;\n }) => void;\n}\n\nexport function AudioTransport({\n media,\n policy,\n renderMode = 'practice',\n disabled = false,\n locale,\n mediaBudget,\n mediaStrings,\n onInteraction,\n}: AudioTransportProps): React.JSX.Element {\n const elementRef = useRef<HTMLAudioElement>(null);\n const statusId = useId();\n\n const suppliedStrings = mediaBudget?.strings ?? mediaStrings;\n const s: MediaTransportStrings = { ...DEFAULT_STRINGS, ...mediaStrings, ...mediaBudget?.strings };\n\n const enforced = mediaBudget?.enforced ?? renderMode !== 'review';\n const budgeted = enforced && policy.maxPlays !== null && mediaBudget !== undefined;\n const maxPlays = policy.maxPlays ?? 0;\n\n // Written synchronously inside handlers and mirrored into state for\n // rendering, for the same reason `ActivitySequence` keeps `outcomesRef` in a\n // ref: two events in one tick would otherwise both read the pre-update value.\n const usedRef = useRef(mediaBudget?.entry?.plays ?? 0);\n const pendingRef = useRef(false);\n const resumeAtRef = useRef<number | null>(null);\n const highWaterRef = useRef(0);\n const chargeRef = useRef<{ claim: MediaPlayClaim; at: number } | null>(null);\n const seededRef = useRef(false);\n const lastPositionRef = useRef(-1);\n // Marks a `play()` this component itself issued after charging. The element's\n // `play` event is where the budget is enforced — so that a media key or a\n // scripted `play()` is charged too — which means our OWN start re-enters it.\n // In the confirmed tier `pendingRef` covered that; in the optimistic tier\n // nothing did, and `remaining` is still the pre-charge value in that closure,\n // so a single press was charged twice.\n const selfStartRef = useRef(false);\n\n const [used, setUsed] = useState(usedRef.current);\n const [playing, setPlaying] = useState(false);\n const [pending, setPending] = useState(false);\n const [elapsed, setElapsed] = useState(0);\n const [duration, setDuration] = useState(0);\n const [muted, setMuted] = useState(false);\n const [volume, setVolume] = useState(1);\n const [confirming, setConfirming] = useState(false);\n const [notice, setNotice] = useState('');\n // A repeated identical message must announce again, so the alert node is\n // REPLACED rather than mutated. Screen readers ignore an unchanged one.\n const [noticeNonce, setNoticeNonce] = useState(0);\n\n const remaining = Math.max(0, maxPlays - used);\n\n const announce = useCallback((message: string) => {\n setNotice(message);\n setNoticeNonce((n) => n + 1);\n }, []);\n\n const emit = useCallback(\n (type: string, playsUsed: number) => {\n if (!budgeted || mediaBudget === undefined || onInteraction === undefined) {\n return;\n }\n onInteraction({\n type,\n activityId: mediaBudget.activityId ?? mediaBudget.slotId,\n timestamp: Date.now(),\n payload: {\n mediaKey: mediaBudget.key,\n mediaType: 'audio',\n playsUsed,\n maxPlays,\n playsRemaining: Math.max(0, maxPlays - playsUsed),\n },\n });\n },\n [budgeted, mediaBudget, maxPlays, onInteraction],\n );\n\n const reportPosition = useCallback(\n (seconds: number) => {\n mediaBudget?.onPosition?.(mediaBudget.key, seconds);\n },\n [mediaBudget],\n );\n\n /** A start within a hair of where playback last stopped is a resume, not a play. */\n const isResume = useCallback((element: HTMLAudioElement): boolean => {\n const at = resumeAtRef.current;\n return at !== null && Math.abs(element.currentTime - at) < RESUME_EPSILON;\n }, []);\n\n const buildClaim = useCallback((): MediaPlayClaim | null => {\n if (mediaBudget === undefined) {\n return null;\n }\n return {\n key: mediaBudget.key,\n previousPlaysUsed: usedRef.current,\n playsUsed: usedRef.current + 1,\n maxPlays,\n playsRemaining: Math.max(0, maxPlays - (usedRef.current + 1)),\n slotId: mediaBudget.slotId,\n index: mediaBudget.index,\n ...(mediaBudget.activityId !== undefined ? { activityId: mediaBudget.activityId } : {}),\n };\n }, [mediaBudget, maxPlays]);\n\n const refuse = useCallback(() => {\n announce(s.noPlaysRemaining);\n emit('media-play-refused', usedRef.current);\n }, [announce, emit, s.noPlaysRemaining]);\n\n const startPlayback = useCallback(() => {\n const element = elementRef.current;\n if (element === null) {\n return;\n }\n highWaterRef.current = element.currentTime;\n selfStartRef.current = true;\n void element.play().catch(() => {\n selfStartRef.current = false;\n /* autoplay policies and races surface through the `error` handler */\n });\n }, []);\n\n const commit = useCallback(\n (claim: MediaPlayClaim) => {\n const element = elementRef.current;\n chargeRef.current = { claim, at: element?.currentTime ?? 0 };\n usedRef.current = claim.playsUsed;\n setUsed(claim.playsUsed);\n emit('media-play-consumed', claim.playsUsed);\n },\n [emit],\n );\n\n /**\n * Charges a play, then starts it.\n *\n * The confirmed tier holds playback until the consumer's atomic write\n * settles, which is the only construction that catches a second tab: two\n * mounts seeded at 0 both claim 1, and only the server can tell them apart.\n */\n const claimAndStart = useCallback(\n (element: HTMLAudioElement, alreadyPlaying: boolean) => {\n const claim = buildClaim();\n if (claim === null) {\n return;\n }\n const result = mediaBudget?.onPlayConsumed?.(claim);\n\n if (result !== undefined && typeof (result as Promise<unknown>).then === 'function') {\n if (alreadyPlaying) {\n element.pause();\n }\n pendingRef.current = true;\n setPending(true);\n void (result as Promise<{ playsUsed: number } | undefined>)\n .then((grant) => {\n const granted =\n grant !== undefined && Number.isInteger(grant.playsUsed)\n ? grant.playsUsed\n : claim.playsUsed;\n if (granted > maxPlays) {\n usedRef.current = granted;\n setUsed(granted);\n refuse();\n return;\n }\n commit({ ...claim, playsUsed: granted });\n startPlayback();\n })\n .catch(() => {\n // Nothing charged: the learner may retry.\n announce(s.playFailed);\n emit('media-play-errored', usedRef.current);\n })\n .finally(() => {\n pendingRef.current = false;\n setPending(false);\n });\n return;\n }\n\n commit(claim);\n if (!alreadyPlaying) {\n startPlayback();\n }\n },\n [\n buildClaim,\n mediaBudget,\n maxPlays,\n commit,\n startPlayback,\n refuse,\n announce,\n emit,\n s.playFailed,\n ],\n );\n\n const handleButton = useCallback(() => {\n const element = elementRef.current;\n if (element === null || pendingRef.current || disabled) {\n return;\n }\n if (!element.paused) {\n element.pause();\n return;\n }\n if (!budgeted || isResume(element)) {\n startPlayback();\n return;\n }\n if (remaining === 0) {\n refuse();\n return;\n }\n if (remaining === 1 && !confirming) {\n setConfirming(true);\n return;\n }\n setConfirming(false);\n claimAndStart(element, false);\n }, [disabled, budgeted, isResume, startPlayback, remaining, refuse, confirming, claimAndStart]);\n\n // Seed the restored position once metadata is known, BEFORE the seek guard\n // can see it — otherwise the guard reverts the very restore it was given.\n const handleLoadedMetadata = useCallback(() => {\n const element = elementRef.current;\n if (element === null) {\n return;\n }\n setDuration(Number.isFinite(element.duration) ? element.duration : 0);\n const at = mediaBudget?.entry?.at;\n if (!seededRef.current && typeof at === 'number' && Number.isFinite(at) && at > 0) {\n seededRef.current = true;\n highWaterRef.current = at;\n resumeAtRef.current = at;\n element.currentTime = at;\n setElapsed(at);\n }\n }, [mediaBudget]);\n\n /**\n * The budget lives on the ELEMENT's `play` event, not on the button, so a\n * hardware media key, a browser extension or a scripted `el.play()` is\n * charged too — and an exhausted budget stops playback inside the event,\n * before a sample is audible.\n */\n const handlePlay = useCallback(() => {\n const element = elementRef.current;\n if (element === null) {\n return;\n }\n if (disabled) {\n element.pause();\n return;\n }\n if (selfStartRef.current) {\n selfStartRef.current = false;\n setPlaying(true);\n return;\n }\n if (pendingRef.current) {\n setPlaying(true);\n return;\n }\n if (isResume(element)) {\n setPlaying(true);\n resumeAtRef.current = null;\n return;\n }\n if (!budgeted) {\n setPlaying(true);\n return;\n }\n if (remaining === 0) {\n element.pause();\n refuse();\n return;\n }\n setPlaying(true);\n claimAndStart(element, true);\n }, [disabled, isResume, budgeted, remaining, refuse, claimAndStart]);\n\n const handlePause = useCallback(() => {\n const element = elementRef.current;\n setPlaying(false);\n if (element === null) {\n return;\n }\n // Every pause records the position — learner, browser, or the pager's own\n // pause as a pane hides. Without this, paging through a six-question\n // listening group and back would exhaust the budget by navigation alone.\n resumeAtRef.current = element.currentTime;\n reportPosition(element.currentTime);\n }, [reportPosition]);\n\n const handleEnded = useCallback(() => {\n setPlaying(false);\n resumeAtRef.current = null;\n highWaterRef.current = 0;\n chargeRef.current = null;\n setElapsed(0);\n reportPosition(0);\n }, [reportPosition]);\n\n const handleTimeUpdate = useCallback(() => {\n const element = elementRef.current;\n if (element === null) {\n return;\n }\n setElapsed(element.currentTime);\n highWaterRef.current = Math.max(highWaterRef.current, element.currentTime);\n const whole = Math.floor(element.currentTime);\n if (whole !== lastPositionRef.current) {\n lastPositionRef.current = whole;\n reportPosition(element.currentTime);\n }\n }, [reportPosition]);\n\n const handleSeeking = useCallback(() => {\n const element = elementRef.current;\n if (element === null || policy.seek !== 'none') {\n return;\n }\n if (Math.abs(element.currentTime - highWaterRef.current) > SEEK_EPSILON) {\n element.currentTime = highWaterRef.current;\n announce(s.seekBlocked);\n }\n }, [policy.seek, announce, s.seekBlocked]);\n\n const handleRateChange = useCallback(() => {\n const element = elementRef.current;\n if (element === null || policy.rate !== 'fixed' || element.playbackRate === 1) {\n return;\n }\n element.playbackRate = 1;\n announce(s.rateBlocked);\n }, [policy.rate, announce, s.rateBlocked]);\n\n const handleError = useCallback(() => {\n const charge = chargeRef.current;\n const element = elementRef.current;\n // Ordering matters: an element can error with nothing charged (a bad URL\n // the learner never pressed play on), so the charge must be proven to\n // exist before its position is read.\n const progressed =\n charge !== null && element !== null && element.currentTime - charge.at >= NO_PROGRESS;\n if (charge !== null && !progressed) {\n if (mediaBudget?.onPlayRefunded !== undefined) {\n usedRef.current = charge.claim.previousPlaysUsed;\n setUsed(charge.claim.previousPlaysUsed);\n mediaBudget.onPlayRefunded(charge.claim);\n emit('media-play-refunded', charge.claim.previousPlaysUsed);\n } else {\n emit('media-play-errored', usedRef.current);\n }\n }\n chargeRef.current = null;\n setPlaying(false);\n announce(s.playFailed);\n }, [mediaBudget, emit, announce, s.playFailed]);\n\n // Keep the element's volume in step with the controls.\n useEffect(() => {\n const element = elementRef.current;\n if (element !== null) {\n element.volume = volume;\n element.muted = muted;\n }\n }, [volume, muted]);\n\n const exhausted = budgeted && remaining === 0;\n const buttonLabel = pending ? s.preparing : playing ? s.pause : s.play;\n\n return (\n <figure className=\"lk-media\" data-controls=\"minimal\">\n {/* biome-ignore lint/a11y/useMediaCaption: captions are optional in the data contract — a <track> is rendered when captionsUrl is provided; absence is the author's documented choice (Req 14.5) */}\n <audio\n ref={elementRef}\n className=\"lk-media-el\"\n preload=\"metadata\"\n aria-label={media.alt || undefined}\n onLoadedMetadata={handleLoadedMetadata}\n onPlay={handlePlay}\n onPause={handlePause}\n onEnded={handleEnded}\n onTimeUpdate={handleTimeUpdate}\n onSeeking={handleSeeking}\n onRateChange={handleRateChange}\n onError={handleError}\n >\n <source src={media.url} />\n {media.captionsUrl ? <track kind=\"captions\" src={media.captionsUrl} default /> : null}\n </audio>\n\n {/* A timeline runs with the audio, not with the script. Forcing LTR here\n fixes seek direction for RTL locales by construction; the surrounding\n text keeps the document direction. */}\n <div\n className=\"lk-media-transport\"\n dir=\"ltr\"\n {...(suppliedStrings !== undefined && locale !== undefined ? { lang: locale } : {})}\n >\n <button\n type=\"button\"\n className=\"lk-media-play\"\n onClick={handleButton}\n // NEVER `disabled`: a natively disabled button leaves the focus\n // order, so a learner who tabs into nothing is told nothing — and\n // disabling it mid-attempt would drop focus to <body>. Kept\n // focusable, it re-announces the refusal when pressed.\n aria-disabled={exhausted || disabled || undefined}\n aria-busy={pending || undefined}\n aria-label={exhausted ? `${s.play} — ${s.noPlaysRemaining}` : undefined}\n aria-describedby={budgeted ? statusId : undefined}\n >\n {buttonLabel}\n </button>\n\n <span className=\"lk-media-time\">\n {formatTime(elapsed)} / {formatTime(duration)}\n </span>\n\n {policy.seek === 'allow' ? (\n <input\n type=\"range\"\n className=\"lk-media-scrub\"\n min={0}\n max={duration || 0}\n step={0.1}\n value={Math.min(elapsed, duration || 0)}\n aria-label={s.seek}\n aria-valuetext={`${formatTime(elapsed)} of ${formatTime(duration)}`}\n onChange={(event) => {\n const element = elementRef.current;\n if (element !== null) {\n element.currentTime = Number(event.target.value);\n }\n }}\n />\n ) : null}\n\n <button\n type=\"button\"\n className=\"lk-media-mute\"\n aria-pressed={muted}\n onClick={() => setMuted((m) => !m)}\n >\n {muted ? s.unmute : s.mute}\n </button>\n\n <input\n type=\"range\"\n className=\"lk-media-volume\"\n min={0}\n max={1}\n step={0.05}\n value={volume}\n aria-label={s.volume}\n onChange={(event) => setVolume(Number(event.target.value))}\n />\n\n {policy.rate === 'allow' ? (\n <select\n className=\"lk-media-rate\"\n aria-label={s.speed}\n defaultValue=\"1\"\n onChange={(event) => {\n const element = elementRef.current;\n if (element !== null) {\n element.playbackRate = Number(event.target.value);\n }\n }}\n >\n {['0.75', '1', '1.25', '1.5'].map((rate) => (\n <option key={rate} value={rate}>\n {rate}×\n </option>\n ))}\n </select>\n ) : null}\n </div>\n\n {budgeted ? (\n <p className=\"lk-media-plays\" id={statusId} role=\"status\">\n {remaining > 0 ? s.playsRemaining(remaining, maxPlays) : s.noPlaysRemaining}\n </p>\n ) : null}\n\n {/* Separate from the polite status on purpose: a refusal is a direct\n response to a user action and must not be the update a screen reader\n drops when two polite regions compete. */}\n <p className=\"lk-media-notice\" role=\"alert\" key={noticeNonce}>\n {notice}\n </p>\n\n {confirming ? (\n <fieldset className=\"lk-media-confirm\">\n <legend>{s.lastPlayConfirm}</legend>\n <button\n type=\"button\"\n className=\"lk-media-confirm-start\"\n // biome-ignore lint/a11y/noAutofocus: focus must land on the confirm action the press opened, or a keyboard learner is stranded on a button whose meaning just changed\n autoFocus\n onClick={() => {\n const element = elementRef.current;\n setConfirming(false);\n if (element !== null) {\n claimAndStart(element, false);\n }\n }}\n >\n {s.lastPlayStart}\n </button>\n <button\n type=\"button\"\n className=\"lk-media-confirm-cancel\"\n onClick={() => setConfirming(false)}\n >\n {s.lastPlayCancel}\n </button>\n </fieldset>\n ) : null}\n </figure>\n );\n}\n","import type { ActivityMedia as ActivityMediaData, InteractionEvent } from '@intellectif/lk-core';\nimport { resolvePlaybackPolicy } from '@intellectif/lk-core';\nimport type { MediaBudgetBinding, MediaTransportStrings, RenderMode } from '../types.js';\nimport { AudioTransport } from './AudioTransport.js';\n\n/**\n * Presentational media block shown above a question or passage. URL-only:\n * the SDK does not host media (consumer responsibility). Accessibility:\n * images use `alt` (schema requires it non-empty); audio/video expose an\n * optional label and a captions `<track>` when `captionsUrl` is provided;\n * `embed` renders a responsive sandboxed iframe with `alt` as its required\n * accessible `title` (Req 14.5).\n *\n * Audio additionally carries a playback policy (`media.playback`). With no\n * policy the emitted element is character-identical to what every release\n * before 0.8.0 produced; with something to enforce, the browser's control bar\n * is replaced by {@link AudioTransport}.\n */\nexport interface ActivityMediaProps {\n media: ActivityMediaData;\n renderMode?: RenderMode;\n disabled?: boolean;\n locale?: string;\n mediaBudget?: MediaBudgetBinding;\n mediaStrings?: Partial<MediaTransportStrings>;\n onInteraction?: (event: InteractionEvent) => void;\n}\n\nexport function ActivityMedia(props: ActivityMediaProps): React.JSX.Element {\n const { media, renderMode = 'practice' } = props;\n\n if (media.type === 'image') {\n return (\n <figure className=\"lk-media\">\n <img className=\"lk-media-el\" src={media.url} alt={media.alt ?? ''} />\n </figure>\n );\n }\n\n if (media.type === 'audio') {\n const policy = resolvePlaybackPolicy(media);\n\n // REVIEW ALWAYS GETS THE NATIVE BAR. The paper is graded; a learner\n // reviewing it cannot change an answer by listening again, so taking away\n // their scrubber, their speed control and their browser-localized controls\n // buys nothing — and costs a learner with a processing disability the\n // ability to work out what they got wrong.\n if (policy.controls === 'minimal' && renderMode !== 'review') {\n return <AudioTransport {...props} media={media} policy={policy} />;\n }\n\n // A mis-wired budget must not render as an ordinary recording. Without a\n // binding nothing persists the count, so a refresh restores the full\n // budget while the page still says \"2 plays remaining\".\n if (policy.maxPlays !== null && renderMode === 'exam' && props.mediaBudget === undefined) {\n throw new Error(\n `ActivityMedia: audio ${JSON.stringify(media.url)} declares maxPlays and is rendered in ` +\n 'renderMode \"exam\" without a `mediaBudget` binding. Nothing would persist the count, so ' +\n 'a refresh restores the full budget while the page says plays remain. Render it through ' +\n '<ActivitySequence mediaBudget={…}> or pass `mediaBudget` yourself.',\n );\n }\n\n const hints = renderMode === 'review' ? [] : policy.nativeControlHints;\n return (\n <figure className=\"lk-media\">\n {/* biome-ignore lint/a11y/useMediaCaption: captions are optional in the data contract — a <track> is rendered when captionsUrl is provided; absence is the author's documented choice (Req 14.5) */}\n <audio\n className=\"lk-media-el\"\n controls\n aria-label={media.alt || undefined}\n {...(hints.length > 0\n ? {\n controlsList: hints\n .map((hint) => (hint === 'hide-download' ? 'nodownload' : 'noplaybackrate'))\n .join(' '),\n }\n : {})}\n >\n <source src={media.url} />\n {media.captionsUrl ? <track kind=\"captions\" src={media.captionsUrl} default /> : null}\n </audio>\n </figure>\n );\n }\n\n if (media.type === 'embed') {\n return (\n <figure className=\"lk-media\">\n <div className=\"lk-media-embed\">\n <iframe\n className=\"lk-media-el\"\n src={media.url}\n title={media.alt ?? 'Embedded media'}\n loading=\"lazy\"\n referrerPolicy=\"strict-origin-when-cross-origin\"\n // The docblock always promised a sandboxed iframe; now it is one.\n // allow-scripts + allow-same-origin are required by provider\n // players (YouTube/Vimeo); top-navigation, forms, downloads and\n // popups stay blocked. Scheme allow-listing in MediaSchema is the\n // primary defense; this is depth.\n sandbox=\"allow-scripts allow-same-origin allow-presentation\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\"\n allowFullScreen\n />\n </div>\n </figure>\n );\n }\n\n return (\n <figure className=\"lk-media\">\n {/* biome-ignore lint/a11y/useMediaCaption: captions are optional in the data contract — a <track> is rendered when captionsUrl is provided; absence is the author's documented choice (Req 14.5) */}\n <video className=\"lk-media-el\" controls aria-label={media.alt || undefined}>\n <source src={media.url} />\n {media.captionsUrl ? <track kind=\"captions\" src={media.captionsUrl} default /> : null}\n </video>\n </figure>\n );\n}\n"]}
|