@pie-players/pie-section-player 0.3.67 → 0.3.69

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.
@@ -75,11 +75,15 @@ export declare function resolveSectionPlayerRuntimeState(args: RuntimeInputs): {
75
75
  };
76
76
  };
77
77
  /**
78
- * Fold a layout element's `nds-icons` convenience attribute into the
79
- * `runtime` config handed to the kernel. Only injects when the attribute
80
- * is explicitly set, so an unset attribute never clobbers a
81
- * `runtime.ndsIcons` the host provided; when both are present,
82
- * `runtime.ndsIcons` wins (spread last). Returns the input unchanged when
83
- * the attribute is unset.
78
+ * Fold a layout element's presentation convenience attributes — `nds-icons` and
79
+ * `locale` — into the `runtime` config handed to the kernel.
80
+ *
81
+ * Only injects an attribute that is explicitly set, so an unset one never
82
+ * clobbers a value the host put on `runtime`; when both are present, `runtime`
83
+ * wins (spread last). Returns the input unchanged when neither is set, which
84
+ * keeps `kernelRuntime` referentially stable for a host that supplies neither.
84
85
  */
85
- export declare function mergeNdsIconsIntoRuntime(runtime: RuntimeConfig | null, ndsIcons: boolean | undefined): RuntimeConfig | null;
86
+ export declare function mergeLayoutAttrsIntoRuntime(runtime: RuntimeConfig | null, attrs: {
87
+ ndsIcons?: boolean;
88
+ locale?: string;
89
+ }): RuntimeConfig | null;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The layout plumbing every section-player custom-element shell needs.
3
+ *
4
+ * The three shells — vertical, tabbed, split-pane — differ in how they arrange
5
+ * panes and in nothing else at this level: each clamps the same host-configurable
6
+ * pixel values, finds its own host element through the same root-node walk, and
7
+ * watches the same narrow-layout media query. That was copied three times, so a
8
+ * changed clamp bound reached whichever shells someone remembered.
9
+ *
10
+ * What cannot be shared, and is not this module's failure: the `props` map inside
11
+ * `<svelte:options customElement={{…}}>`. The Svelte compiler rejects anything but
12
+ * a statically analyzable object literal there — `svelte_options_invalid_customelement_props`
13
+ * — so that block stays duplicated per shell by construction. The command
14
+ * forwarders are the same story: the compiler exposes a component's
15
+ * `export function` declarations as custom-element methods, so they must be
16
+ * declared in each `.svelte` file, though their bodies are one line each.
17
+ */
18
+ export declare const DEFAULT_NARROW_BREAKPOINT_PX = 1100;
19
+ /**
20
+ * A host-supplied pixel value, clamped into range. `undefined` for anything the
21
+ * host left unset or wrote as a non-number, so a caller can fall back rather than
22
+ * render at zero.
23
+ */
24
+ export declare function resolveConfiguredPx(value: unknown, min: number, max: number): number | undefined;
25
+ /** The narrow-layout breakpoint, clamped, defaulting when the host set nothing. */
26
+ export declare function clampNarrowBreakpoint(value: unknown): number;
27
+ /**
28
+ * The two content-width caps, clamped.
29
+ *
30
+ * `withPassage` is floored at `noPassage`: a passage adds a column, so the
31
+ * two-column cap narrower than the one-column cap would shrink the content on
32
+ * the wider layout.
33
+ */
34
+ export declare function resolveContentMaxWidths(contentMaxWidthNoPassage: unknown, contentMaxWidthWithPassage: unknown): {
35
+ noPassagePx: number | undefined;
36
+ withPassagePx: number | undefined;
37
+ };
38
+ /**
39
+ * The custom element hosting this shell, reached from the shell's anchor node.
40
+ *
41
+ * Through `getRootNode` so it works whether the shell renders into a shadow root
42
+ * or into light DOM, which these shells do (`shadow: "none"`, so item-player and
43
+ * runtime styles can cascade into rendered item content).
44
+ */
45
+ export declare function getShellHostElement(anchor: HTMLElement | null): HTMLElement | null;
46
+ /**
47
+ * Track whether the viewport is below the shell's narrow-layout breakpoint.
48
+ *
49
+ * Call from a component's init; the effect it registers is torn down with that
50
+ * component. `getBreakpoint` is read reactively, so a host changing the
51
+ * breakpoint attribute re-subscribes to the new query.
52
+ */
53
+ export declare function createNarrowLayoutWatch(getBreakpoint: () => number): {
54
+ readonly isNarrow: boolean;
55
+ };
@@ -1,4 +1,5 @@
1
1
  import { ItemEntity } from '@pie-players/pie-players-shared/types';
2
+ import { FormativeItemView } from '@pie-players/pie-players-shared/formative';
2
3
  import { SectionCompositionModel } from '../../controllers/types.js';
3
4
  import { PlayerElementParams } from './player-action.js';
4
5
  /**
@@ -52,6 +53,39 @@ export declare function getPassagePlayerParams(args: {
52
53
  playerStrategy: string;
53
54
  baseHeadingLevel?: HeadingLevel;
54
55
  }): PlayerElementParams;
56
+ /**
57
+ * The formative view for one item, or `null` when the section does not deliver
58
+ * formatively.
59
+ *
60
+ * Derived here rather than in a component so the card, the env projection, and
61
+ * any future consumer read one predicate. The projection arrives on the
62
+ * composition model, which the runtime republishes on every controller event, so
63
+ * this is recomputed from fresh state on each render pass.
64
+ */
65
+ export declare function getFormativeItemView(args: {
66
+ compositionModel: SectionCompositionModel;
67
+ canonicalItemId: string;
68
+ }): FormativeItemView | null;
69
+ /**
70
+ * What a card needs to know about the cue timeline, or `null` when the section is
71
+ * not timed media.
72
+ *
73
+ * Derived here rather than in a component for the same reason the formative view
74
+ * is: two derivations of one predicate can disagree about whether an item is on
75
+ * screen.
76
+ */
77
+ export interface TimedMediaItemView {
78
+ /** Awaiting its cue. Kept mounted; see `SectionItemsPane`. */
79
+ pending: boolean;
80
+ /** This item is what a currently held gate is waiting for. */
81
+ isGateTarget: boolean;
82
+ /** The gate is enforced rather than advisory — playback is really stopped. */
83
+ gateEnforced: boolean;
84
+ }
85
+ export declare function getTimedMediaItemView(args: {
86
+ compositionModel: SectionCompositionModel;
87
+ canonicalItemId: string;
88
+ }): TimedMediaItemView | null;
55
89
  export declare function getItemPlayerParams(args: {
56
90
  item: ItemEntity;
57
91
  compositionModel: SectionCompositionModel;
@@ -61,6 +95,7 @@ export declare function getItemPlayerParams(args: {
61
95
  playerStrategy: string;
62
96
  itemIndex?: number;
63
97
  baseHeadingLevel?: HeadingLevel;
98
+ formativeView?: FormativeItemView | null;
64
99
  }): PlayerElementParams;
65
100
  export declare function getCanonicalItemId(args: {
66
101
  compositionModel: SectionCompositionModel;
@@ -0,0 +1,20 @@
1
+ import { I18nProvider, InterpolationValues, MessageKeyInput, PluralOptions, TextDirection } from '@pie-players/pie-players-shared/i18n/types';
2
+ export interface InterfaceI18n {
3
+ /** The resolved provider: the published one, or the English-only default. */
4
+ readonly provider: I18nProvider;
5
+ /** Active locale tag, for stamping `lang`. */
6
+ readonly locale: string;
7
+ /** Writing direction, for stamping `dir`. */
8
+ readonly direction: TextDirection;
9
+ t(key: MessageKeyInput, values?: InterpolationValues): string;
10
+ plural(key: MessageKeyInput, options: PluralOptions): string;
11
+ }
12
+ /**
13
+ * Resolve the interface-locale provider for a component.
14
+ *
15
+ * @param getHost Returns the element to make the context request from. Any
16
+ * element inside the player works: requests bubble to the toolkit's provider.
17
+ * Returning `undefined` before the element is bound is expected, and the
18
+ * connection is made when it appears.
19
+ */
20
+ export declare function useInterfaceI18n(getHost: () => HTMLElement | null | undefined): InterfaceI18n;
@@ -1,4 +1,6 @@
1
1
  import { TestAttemptSession } from '@pie-players/pie-assessment-toolkit';
2
+ import { FormativeSectionProjection } from '@pie-players/pie-players-shared/formative';
3
+ import { MediaTimeSource, TimedMediaSectionProjection } from '@pie-players/pie-players-shared/timed-media';
2
4
  import { SectionControllerHandle, SectionSessionPersistenceConfig, SectionControllerRuntimeState, SectionControllerSessionState } from './toolkit-section-contracts.js';
3
5
  import { ItemEntity } from '@pie-players/pie-players-shared';
4
6
  import { NavigationResult, SectionControllerChangeListener, SectionCompositionModel, SectionNavigationState, SectionViewModel, SessionChangedResult } from './types.js';
@@ -21,6 +23,19 @@ export declare class SectionController implements SectionControllerHandle {
21
23
  private nextApplyRevision;
22
24
  private lastReplayedApplyRevision;
23
25
  private pendingApplyReplay;
26
+ private formativePolicies;
27
+ private formativeStates;
28
+ private formativeEnabled;
29
+ private lastMasterySignature;
30
+ private timedMediaData;
31
+ private timedMediaState;
32
+ private mediaTimeSource;
33
+ private mediaTimeSourceOrigin;
34
+ private unsubscribeMediaTimeSource;
35
+ private lastTimedMediaSignature;
36
+ private lastTimedMediaInvalidSignature;
37
+ private reportedDegradationKeys;
38
+ private mediaAttachWatch;
24
39
  private emitChange;
25
40
  subscribe(listener: SectionControllerChangeListener): () => void;
26
41
  initialize(input?: unknown): Promise<void>;
@@ -49,7 +64,166 @@ export declare class SectionController implements SectionControllerHandle {
49
64
  * This is intentionally compact and not a full runtime diagnostics view.
50
65
  */
51
66
  getSession(): SectionControllerSessionState | null;
67
+ private rebuildFormativePolicies;
68
+ private computeMastery;
69
+ getFormativeProjection(): FormativeSectionProjection | null;
70
+ /**
71
+ * Record one Try from element outcomes.
72
+ *
73
+ * Correctness derivation lives here rather than at the call site so one
74
+ * aggregation policy applies to every caller. The reducer is a no-op when the
75
+ * item cannot currently be checked, which is what makes a double submit safe
76
+ * — a second click landing before the composition republishes is dropped
77
+ * rather than spending a Try.
78
+ */
79
+ recordFormativeTry(args: {
80
+ itemId: string;
81
+ outcomes?: unknown[];
82
+ }): void;
83
+ /** Dismiss a reveal and reopen the item, withdrawing its env projection. */
84
+ retryFormativeItem(args: {
85
+ itemId: string;
86
+ }): void;
87
+ /**
88
+ * Reveal on host authority — a teacher-driven "show the answer". Spends no
89
+ * Try and ignores the Try budget.
90
+ */
91
+ revealFormativeItem(args: {
92
+ itemId: string;
93
+ feedback: "correctness" | "solution";
94
+ }): void;
95
+ /** Withdraw a reveal on host authority, Try budget notwithstanding. */
96
+ hideFormativeItem(args: {
97
+ itemId: string;
98
+ }): void;
99
+ /**
100
+ * Store a reveal transition and announce it. Shared by the three callers so
101
+ * one event describes every reveal change a Try did not cause.
102
+ */
103
+ private commitFormativeReveal;
104
+ /**
105
+ * Emit only on a rollup change, matching how
106
+ * `section-items-complete-changed` gates on the aggregate rather than on
107
+ * every intermediate count.
108
+ */
109
+ private emitMasteryIfChanged;
52
110
  private getSectionItemIdentifiers;
111
+ private rebuildTimedMedia;
112
+ /**
113
+ * Report malformed authored data once per distinct failure.
114
+ *
115
+ * `initialize` runs again on every same-cohort `updateInput`, and a PnP toggle
116
+ * must not re-report a section's authoring defect each time.
117
+ */
118
+ private reportTimedMediaInvalid;
119
+ /**
120
+ * The live projection, uncloned and for internal readers only.
121
+ *
122
+ * Resolving is cheap and runs on every `timeupdate`; deep-cloning it there was
123
+ * not. `delivery` is threaded in where the caller already has it, because
124
+ * assembling it walks every item in the section.
125
+ */
126
+ private resolveTimedMediaView;
127
+ /** The public boundary, so this is where the copy is made. */
128
+ getTimedMediaProjection(): TimedMediaSectionProjection | null;
129
+ /**
130
+ * The delivery state cue gates read, assembled from state this controller
131
+ * already keeps: completion answers `"responded"`, and the last Try's
132
+ * correctness answers the correctness conditions. A gate on `"responded"`
133
+ * therefore works in a section that does not deliver formatively at all.
134
+ */
135
+ private getTimedMediaDeliveryState;
136
+ /**
137
+ * Bind the Media Time Source. Called by the stimulus card with a native adapter,
138
+ * and directly by a host supplying its own port — which is what lets a host
139
+ * deliver timed media without shipping a PIE element.
140
+ */
141
+ attachMediaTimeSource(source: MediaTimeSource, options?: {
142
+ origin?: "native-adapter" | "host";
143
+ renderableId?: string;
144
+ }): void;
145
+ detachMediaTimeSource(options?: {
146
+ origin?: "native-adapter" | "host";
147
+ }): void;
148
+ /**
149
+ * Watch for a stimulus that resolves but never exposes a time source.
150
+ *
151
+ * Armed once the section's content has loaded, and only while no source is
152
+ * attached — which is already the common case, because the stimulus card
153
+ * registers its adapter as soon as the media element appears in its subtree.
154
+ */
155
+ private armMediaAttachWatch;
156
+ private clearMediaAttachWatch;
157
+ /**
158
+ * The stimulus resolved to a renderable that exposes no media, so no cue can
159
+ * ever fire and every cued item would stay hidden for the whole session.
160
+ *
161
+ * Handled exactly as malformed `timedMedia` is: the timeline is dropped, the
162
+ * section delivers as an ordinary section with every item visible, and the
163
+ * defect is reported non-recoverably. Same outcome, same posture — refusing to
164
+ * deliver would turn one authoring slip into a blank section, and leaving the
165
+ * timeline in place would keep the items hidden behind cues nothing can fire.
166
+ */
167
+ private reportStimulusExposesNoTimeSource;
168
+ /**
169
+ * Move a freshly attached source up to the restored position, forward only so it
170
+ * never fights a learner who has already scrubbed — the same rule the signing
171
+ * region applies when it seeks a fragment on `loadedmetadata`.
172
+ */
173
+ private syncMediaPositionFromState;
174
+ /**
175
+ * One step of the cue reduction: fold the input into cue state, drive the port
176
+ * with whatever the reduction asked for, and emit only when cue state moved.
177
+ */
178
+ private advanceTimedMedia;
179
+ /**
180
+ * Silence media audio so another audio owner can speak.
181
+ *
182
+ * The handoff rule the contract's accessibility requirements name, reduced to
183
+ * the half the section can carry out: the section owns the port, so it can stop
184
+ * media audio, and it owns no read-aloud, so it decides nothing about speech.
185
+ * Whoever holds both — the toolkit — calls this and yields the other way on
186
+ * `timed-media-audio-started`.
187
+ *
188
+ * Returns whether media audio is now silent, which is `false` only for a source
189
+ * that is playing and reports no `canPause`. The overlap then stands: refusing
190
+ * to speak would withhold an accommodation to protect a policy the port already
191
+ * said it cannot keep, so enforcement degrades here exactly as it does for a
192
+ * gate — advisory, and reported at attach.
193
+ *
194
+ * No pause is issued for a source already paused, so calling this repeatedly
195
+ * costs nothing and never fights a learner.
196
+ */
197
+ pauseMediaForCompetingAudio(): boolean;
198
+ /** Announce media audio only where playback actually stood. */
199
+ private announceMediaAudioStarted;
200
+ /**
201
+ * Carry out an effect only where the port reports the capability for it.
202
+ *
203
+ * Skipping silently is the failure this contract names: the skip is already
204
+ * reported once per attach by `reportTimedMediaDegradations`, and the projection
205
+ * carries `enforcement: "advisory"` for as long as the gap lasts.
206
+ */
207
+ private applyTimedMediaEffects;
208
+ /**
209
+ * Report a capability gap once per attached source.
210
+ *
211
+ * Enforcement rather than the whole projection: the degradation list is all this
212
+ * needs, and resolving a projection to read one field of it also paid for a gate
213
+ * view and two id sets.
214
+ */
215
+ private reportTimedMediaDegradations;
216
+ /**
217
+ * Emit on cue state, never on the clock.
218
+ *
219
+ * `timedMediaProjectionSignature` is shared with the toolkit's composition
220
+ * revision key rather than restated here: two encodings of "what changes what a
221
+ * layout renders" drift silently, and the failure is an emit the toolkit
222
+ * coalesces away or a republish this never announced.
223
+ */
224
+ private emitTimedMediaIfChanged;
225
+ /** Re-evaluate gates after the state a gate condition reads has changed. */
226
+ private notifyTimedMediaDeliveryChanged;
53
227
  /**
54
228
  * Runtime/debugger shape scoped to the current section.
55
229
  * Use this for widgets that need a section-scoped live snapshot (debug panels, diagnostics).
@@ -1,6 +1,8 @@
1
1
  import { TestAttemptSession } from '@pie-players/pie-assessment-toolkit';
2
2
  import { ItemSessionUpdateIntent, AssessmentItemRef, AssessmentSection, ItemEntity, PassageEntity, RubricBlock } from '@pie-players/pie-players-shared';
3
3
  import { ConfigContainerEntity } from '@pie-players/pie-players-shared/types';
4
+ import { FormativeFeedbackReveal, FormativeItemPolicy, FormativeMasteryRollup, FormativeSectionProjection, FormativeSectionSlice, FormativeTryOutcome } from '@pie-players/pie-players-shared/formative';
5
+ import { TimedMediaDegradation, TimedMediaSectionProjection, TimedMediaSectionSessionSlice, TimedMediaValidationError } from '@pie-players/pie-players-shared/timed-media';
4
6
  export type SectionView = "candidate" | "scorer" | "author" | "proctor" | "testConstructor" | "tutor";
5
7
  export interface SectionContentModel {
6
8
  passages: PassageEntity[];
@@ -14,7 +16,24 @@ export interface SectionContentModel {
14
16
  id?: string;
15
17
  identifier?: string;
16
18
  };
19
+ /**
20
+ * The item ref's formative override, carried through because this
21
+ * projection is where the canonical identifier is decided. Resolving
22
+ * policy from the raw `assessmentItemRefs` instead would mean recomputing
23
+ * that identifier, and two implementations of one mapping.
24
+ */
25
+ formative?: FormativeItemPolicy;
17
26
  }>;
27
+ /**
28
+ * How an authored `timedMedia.stimulusRef` resolves to a renderable.
29
+ *
30
+ * Keyed by every spelling a section may reference the stimulus under — the
31
+ * rubric block identifier, the authored passage id, and the normalized id —
32
+ * mapping to the normalized passage id the renderables carry. Empty for a
33
+ * section with no stimulus rubric block, which is what makes an unresolvable
34
+ * `stimulusRef` detectable rather than silently cue-less.
35
+ */
36
+ stimulusRenderableIdsByRef: Record<string, string>;
18
37
  }
19
38
  export type SectionRenderableFlavor = "item" | "passage" | "rubric";
20
39
  export interface SectionRenderable {
@@ -35,6 +54,8 @@ export interface SectionSessionState {
35
54
  currentItemIndex?: number;
36
55
  visitedItemIdentifiers?: string[];
37
56
  itemSessions: Record<string, unknown>;
57
+ formative?: FormativeSectionSlice;
58
+ timedMedia?: TimedMediaSectionSessionSlice;
38
59
  }
39
60
  export interface SectionViewModel extends SectionContentModel {
40
61
  currentItemIndex: number;
@@ -70,6 +91,26 @@ export interface SectionCompositionModel {
70
91
  itemSessionsByItemId: Record<string, unknown>;
71
92
  testAttemptSession: TestAttemptSession | null;
72
93
  itemViewModels: SectionCanonicalItemViewModel[];
94
+ /**
95
+ * Formative delivery projection, keyed by canonical item id. Absent reads
96
+ * exactly as `null`: no item in the section delivers formatively, which is
97
+ * every section authored without a `formative` policy. Optional so anything
98
+ * else assembling this model — a host layout, a test double — omits the
99
+ * field instead of declaring a feature it does not use.
100
+ *
101
+ * Layouts read it from here rather than calling the controller: the runtime
102
+ * republishes the composition model on every controller event, so a recorded
103
+ * Try reaches the cards through the channel that already exists.
104
+ */
105
+ formative?: FormativeSectionProjection | null;
106
+ /**
107
+ * Timed-media projection: cues, enforcement, which items a cue has revealed,
108
+ * and the gate holding playback. `null` for every section that is not timed
109
+ * media, and for one whose `timedMedia` failed validation — a layout that reads
110
+ * `null` renders exactly as it does today, which is what keeps the addition
111
+ * invisible to existing content.
112
+ */
113
+ timedMedia?: TimedMediaSectionProjection | null;
73
114
  }
74
115
  export interface SectionCanonicalItemViewModel {
75
116
  item: ItemEntity;
@@ -144,7 +185,7 @@ export interface SectionNavigationState {
144
185
  }
145
186
  export type SectionContentKind = "item" | "passage" | "rubric" | "unknown";
146
187
  interface SectionControllerEventBase {
147
- type: "item-session-data-changed" | "item-session-meta-changed" | "item-selected" | "section-navigation-change" | "section-session-applied" | "content-loaded" | "item-player-error" | "item-complete-changed" | "section-loading-complete" | "section-items-complete-changed" | "section-error";
188
+ type: "item-session-data-changed" | "item-session-meta-changed" | "item-selected" | "section-navigation-change" | "section-session-applied" | "content-loaded" | "item-player-error" | "item-complete-changed" | "section-loading-complete" | "section-items-complete-changed" | "section-error" | "formative-try-recorded" | "formative-reveal-changed" | "section-mastery-changed" | "timed-media-cue-changed" | "timed-media-audio-started" | "timed-media-policy-degraded" | "timed-media-invalid";
148
189
  timestamp: number;
149
190
  }
150
191
  interface ItemScopedControllerEventBase extends SectionControllerEventBase {
@@ -229,6 +270,91 @@ export interface SectionErrorEvent extends ItemScopedControllerEventBase {
229
270
  canonicalItemId?: string;
230
271
  contentKind?: SectionContentKind;
231
272
  }
232
- export type SectionControllerChangeEvent = ItemSessionDataChangedEvent | ItemSessionMetaChangedEvent | ItemSelectedEvent | SectionNavigationChangeEvent | SectionSessionAppliedEvent | ContentLoadedEvent | ItemPlayerErrorEvent | ItemCompleteChangedEvent | SectionLoadingCompleteEvent | SectionItemsCompleteChangedEvent | SectionErrorEvent;
273
+ export interface FormativeTryRecordedEvent extends ItemScopedControllerEventBase {
274
+ type: "formative-try-recorded";
275
+ itemId: string;
276
+ canonicalItemId: string;
277
+ tryCount: number;
278
+ outcome: FormativeTryOutcome;
279
+ revealed: boolean;
280
+ }
281
+ /**
282
+ * The reveal state changed without a Try being recorded: a learner dismissed
283
+ * feedback to edit again, or a host forced a reveal or withdrew one. A Try that
284
+ * reveals reports through `formative-try-recorded`.
285
+ */
286
+ export interface FormativeRevealChangedEvent extends ItemScopedControllerEventBase {
287
+ type: "formative-reveal-changed";
288
+ itemId: string;
289
+ canonicalItemId: string;
290
+ revealed: boolean;
291
+ /** Reveal level in force, when a host overrode the policy's. */
292
+ feedback?: FormativeFeedbackReveal;
293
+ /** Tries already spent; none of these transitions consume one. */
294
+ tryCount: number;
295
+ source: "learner" | "host";
296
+ }
297
+ export interface SectionMasteryChangedEvent extends ItemScopedControllerEventBase {
298
+ type: "section-mastery-changed";
299
+ mastery: FormativeMasteryRollup;
300
+ }
301
+ /**
302
+ * Cue state changed: a cue activated, a gate released, or aggregate completion
303
+ * flipped.
304
+ *
305
+ * Media position is deliberately not an event. `timeupdate` fires about four
306
+ * times a second and changes nothing a layout renders — the media element draws
307
+ * its own clock — so position lives in the session slice and surfaces here only
308
+ * through the cue transitions it caused. Emitting per tick would republish the
309
+ * composition four times a second for no visible change.
310
+ */
311
+ export interface TimedMediaCueChangedEvent extends ItemScopedControllerEventBase {
312
+ type: "timed-media-cue-changed";
313
+ /** The cue that activated in this transition, where one did. */
314
+ activatedCueIdentifier?: string;
315
+ /** The gate that released in this transition, where one did. */
316
+ releasedCueIdentifier?: string;
317
+ activeCueIdentifier?: string;
318
+ visitedCueIdentifiers: string[];
319
+ completedCueIdentifiers: string[];
320
+ revealedItemIds: string[];
321
+ gateCueIdentifier: string | null;
322
+ mediaCompleted: boolean;
323
+ aggregateComplete: boolean;
324
+ }
325
+ /**
326
+ * Media audio is now running, so any other audio owner must yield.
327
+ *
328
+ * The section owns no audio but the port, and no policy over read-aloud, so this
329
+ * announces rather than decides: the toolkit holds both capabilities and arbitrates
330
+ * between them. Emitted only where playback actually stood — a gate that re-paused
331
+ * on the same `play` produced no audio, and silencing read-aloud for it would take
332
+ * an accommodation away for nothing.
333
+ *
334
+ * Carries no payload. "Media audio started" is the whole fact; position and cue
335
+ * state travel on `timed-media-cue-changed`.
336
+ */
337
+ export interface TimedMediaAudioStartedEvent extends ItemScopedControllerEventBase {
338
+ type: "timed-media-audio-started";
339
+ }
340
+ /**
341
+ * A playback policy the attached media time source cannot carry out. Cues still
342
+ * fire and state is still recorded; enforcement is what degrades, and it says so
343
+ * rather than appearing to hold.
344
+ */
345
+ export interface TimedMediaPolicyDegradedEvent extends ItemScopedControllerEventBase {
346
+ type: "timed-media-policy-degraded";
347
+ degradations: TimedMediaDegradation[];
348
+ }
349
+ /**
350
+ * Authored `timedMedia` this section cannot deliver. The section still renders as
351
+ * an ordinary section with every item visible; cues that silently never fire is
352
+ * the outcome this reports instead of producing.
353
+ */
354
+ export interface TimedMediaInvalidEvent extends ItemScopedControllerEventBase {
355
+ type: "timed-media-invalid";
356
+ errors: TimedMediaValidationError[];
357
+ }
358
+ export type SectionControllerChangeEvent = ItemSessionDataChangedEvent | ItemSessionMetaChangedEvent | ItemSelectedEvent | SectionNavigationChangeEvent | SectionSessionAppliedEvent | ContentLoadedEvent | ItemPlayerErrorEvent | ItemCompleteChangedEvent | SectionLoadingCompleteEvent | SectionItemsCompleteChangedEvent | SectionErrorEvent | FormativeTryRecordedEvent | FormativeRevealChangedEvent | SectionMasteryChangedEvent | TimedMediaCueChangedEvent | TimedMediaAudioStartedEvent | TimedMediaPolicyDegradedEvent | TimedMediaInvalidEvent;
233
359
  export type SectionControllerChangeListener = (event: SectionControllerChangeEvent) => void;
234
360
  export {};