@pie-players/pie-section-player 0.3.75 → 1.0.0-beta.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/README.md +845 -964
- package/custom-elements.json +3922 -0
- package/dist/browser/assets/{evaluation-worker-CORp9m0K.js → evaluation-worker-0xRDVTJx.js} +82215 -73234
- package/dist/browser/chunks/ToolCoordinator-0a6c4088.js +1 -0
- package/dist/browser/chunks/attribute-coercion-b93b482c.js +1 -0
- package/dist/browser/chunks/calculator-element-271c142f.js +8 -9
- package/dist/browser/chunks/catalog-context-46d3e50e.js +1 -1
- package/dist/browser/chunks/classPrivateFieldGet2-338e0179.js +1 -1
- package/dist/browser/chunks/classPrivateMethodInitSpec-563cd0a5.js +1 -0
- package/dist/browser/chunks/custom-element-define-2a92deb0.js +1 -0
- package/dist/browser/chunks/dist-41b39e7f.js +1 -1
- package/dist/browser/chunks/dist-ca9b9622.js +1 -1
- package/dist/browser/chunks/focus-trap-44913bf3.js +1 -1
- package/dist/browser/chunks/math-rendering-ready-9a0fd90a.js +1 -1
- package/dist/browser/chunks/mathjax-BKrHE4xs-3ca05bf6.js +1 -1
- package/dist/browser/chunks/nl-NL-364d61b9.js +1 -1
- package/dist/browser/chunks/nl-NL-8b7e6d4b.js +1 -1
- package/dist/browser/chunks/pie-api-client-687c319a.js +1 -1
- package/dist/browser/chunks/provider-4c704118.js +1 -1
- package/dist/browser/chunks/runtime-YFqnU6ic-d5c0cb49.js +8605 -0
- package/dist/browser/chunks/{src-BcptuNS5-ed25d7fd.js → src-D_1goyPT-8cca0246.js} +81 -82
- package/dist/browser/chunks/{sre-engine-jd8cc08x-42d270e5.js → sre-engine-bj997w4b-891a4cbe.js} +1 -1
- package/dist/browser/chunks/term-lookup-632cf6b1.js +1 -0
- package/dist/browser/chunks/tool-annotation-toolbar-997438b4.js +5 -5
- package/dist/browser/chunks/tool-answer-eliminator-35483abd.js +6 -6
- package/dist/browser/chunks/tool-color-scheme-b7de5cdf.js +5 -5
- package/dist/browser/chunks/tool-dictionary-cecff3b4.js +3 -3
- package/dist/browser/chunks/tool-graph-cb0338e8.js +6 -6
- package/dist/browser/chunks/tool-host-contract-fae1c415.js +1 -0
- package/dist/browser/chunks/tool-instance-id-76ac45e2.js +1 -0
- package/dist/browser/chunks/tool-line-reader-f8ef3c6a.js +3 -3
- package/dist/browser/chunks/tool-periodic-table-43b133b8.js +13 -16
- package/dist/browser/chunks/tool-picture-dictionary-33721e91.js +3 -3
- package/dist/browser/chunks/tool-protractor-23f77028.js +6 -6
- package/dist/browser/chunks/tool-ruler-1d9a03a6.js +12 -9
- package/dist/browser/chunks/tool-tts-inline-98c2ce7a.js +24 -30
- package/dist/browser/chunks/toolbar-registration-helpers-a7ab1ad4.js +194 -0
- package/dist/browser/pie-section-player.js +61 -65
- package/dist/chunks/nl-NL-364d61b9.js +34 -6
- package/dist/chunks/{sre-engine-jd8cc08x-42d270e5.js → sre-engine-bj997w4b-891a4cbe.js} +1 -1
- package/dist/component-definitions.d.ts +0 -2
- package/dist/components/shared/card-media-region.d.ts +3 -4
- package/dist/components/shared/framework-error-latch.d.ts +4 -2
- package/dist/components/shared/missing-tool-providers.d.ts +0 -6
- package/dist/components/shared/player-preload.d.ts +4 -9
- package/dist/components/shared/section-player-card-context.d.ts +0 -3
- package/dist/components/shared/section-player-host-runtime.d.ts +0 -2
- package/dist/components/shared/section-player-view-state.d.ts +0 -1
- package/dist/controllers/SectionController.d.ts +20 -6
- package/dist/pie-section-player.js +10987 -11045
- package/package.json +11 -9
- package/dist/browser/chunks/ToolCoordinator-cc2dc319.js +0 -1
- package/dist/browser/chunks/runtime-wcU7KOFU-ac86d7b8.js +0 -8536
- package/dist/browser/chunks/tool-host-contract-b93b482c.js +0 -1
- package/dist/browser/chunks/tool-instance-id-2a92deb0.js +0 -1
- package/dist/browser/chunks/tts-runtime-config-a7ab1ad4.js +0 -194
- /package/dist/browser/chunks/{ItemToolBar-jx38w99s-58defccc.js → ItemToolBar-66hcwmfw-3bcb82a3.js} +0 -0
- /package/dist/chunks/{ItemToolBar-jx38w99s-1287ba31.js → ItemToolBar-66hcwmfw-1287ba31.js} +0 -0
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# @pie-players/pie-section-player
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
[
|
|
3
|
+
The section player renders one assessment section, its items and passages, as a
|
|
4
|
+
custom element. This README is the API reference for hosts that embed it: the
|
|
5
|
+
elements, their inputs, host methods and events, and the package exports. The
|
|
6
|
+
[section player integration guide](../../docs/section-player/integration-guide.md)
|
|
7
|
+
walks through an integration end to end,
|
|
8
|
+
[Custom Section Layouts](../../docs/section-player/custom-layouts.md) covers
|
|
9
|
+
host-built layouts, [Formative Delivery](../../docs/section-player/formative-delivery.md)
|
|
10
|
+
covers practice sections, and the package's
|
|
11
|
+
[architecture notes](./ARCHITECTURE.md) cover its internals.
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
|
@@ -16,71 +16,331 @@ A host that arranges the section itself builds its layout from
|
|
|
16
16
|
npm install @pie-players/pie-section-player
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
```ts
|
|
20
|
+
import "@pie-players/pie-section-player";
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Importing the package root registers every section player element. The root
|
|
24
|
+
entry is bundler-only: it imports `@pie-players/pie-item-player`,
|
|
25
|
+
`@pie-players/pie-default-tool-loaders` and `speech-rule-engine`, with the
|
|
26
|
+
engine's JSON locale tables, by bare specifier and without import attributes.
|
|
27
|
+
Items render through the host's one `@pie-players/pie-item-player`, which this
|
|
28
|
+
package depends on at its own version. A host without a bundler loads the
|
|
29
|
+
self-contained `./browser` build
|
|
30
|
+
([Loading from a CDN](../../docs/install/cdn.md#section-player-browser-build)).
|
|
31
|
+
|
|
32
|
+
The package is browser-only. `@pie-players/pie-section-player/item-section` is
|
|
33
|
+
its one Node-safe entry; the other Node-safe packages are listed in the
|
|
34
|
+
[library packaging strategy](../../docs/setup/library-packaging-strategy.md).
|
|
35
|
+
|
|
19
36
|
No stylesheet import is needed. Authored content depends on shared classes
|
|
20
37
|
(passage markup, the legacy `kds-*` families, answer-eliminator styles) from
|
|
21
|
-
`@pie-players/pie-theme/components.css`, and
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
38
|
+
`@pie-players/pie-theme/components.css`, and the item player installs that
|
|
39
|
+
stylesheet itself. [Content styles](../item-player/README.md#content-styles)
|
|
40
|
+
describes the host-ownership opt-out.
|
|
41
|
+
|
|
42
|
+
## Elements
|
|
43
|
+
|
|
44
|
+
| Element | Role |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `pie-section-player-splitpane` | Passages and items side by side, with a resizable divider. Below the narrow breakpoint it collapses to tabs or a vertical stack |
|
|
47
|
+
| `pie-section-player-vertical` | The passages above the items |
|
|
48
|
+
| `pie-section-player-tabbed` | A Passage tab and a Questions tab when the section has passages; the items alone otherwise |
|
|
49
|
+
| `pie-section-player-kernel-host` | A section player without a layout of its own; its children are the layout ([Custom layout authoring](#custom-layout-authoring)) |
|
|
50
|
+
| `pie-section-player-items-pane`, `pie-section-player-passages-pane` | The panes a custom layout places inside the kernel host |
|
|
51
|
+
| `pie-section-player-item-card`, `pie-section-player-passage-card` | The cards the panes render, which hosts style ([Styling](#styling)) |
|
|
25
52
|
|
|
26
|
-
|
|
53
|
+
The layouts also render `pie-section-player-base`, `pie-section-player-shell`
|
|
54
|
+
and `pie-passage-shell`. Those are internal and not part of the API.
|
|
27
55
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
```html
|
|
59
|
+
<pie-section-player-splitpane section-id="practice-set" attempt-id="attempt-1"></pie-section-player-splitpane>
|
|
60
|
+
```
|
|
33
61
|
|
|
34
62
|
```ts
|
|
35
63
|
import "@pie-players/pie-section-player";
|
|
64
|
+
import type {
|
|
65
|
+
SectionPlayerRuntimeConfig,
|
|
66
|
+
SectionPlayerRuntimeHostContract,
|
|
67
|
+
} from "@pie-players/pie-section-player";
|
|
68
|
+
import type { AssessmentSection } from "@pie-players/pie-players-shared/types";
|
|
69
|
+
|
|
70
|
+
declare const section: AssessmentSection;
|
|
71
|
+
|
|
72
|
+
const runtime: SectionPlayerRuntimeConfig = {
|
|
73
|
+
assessmentId: "practice-1",
|
|
74
|
+
playerType: "iife",
|
|
75
|
+
env: { mode: "gather", role: "student" },
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const player = document.querySelector<HTMLElement & SectionPlayerRuntimeHostContract>(
|
|
79
|
+
"pie-section-player-splitpane",
|
|
80
|
+
);
|
|
81
|
+
if (player) {
|
|
82
|
+
player.runtime = runtime;
|
|
83
|
+
player.section = section;
|
|
84
|
+
}
|
|
36
85
|
```
|
|
37
86
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
tag names stay fixed.
|
|
87
|
+
Strings and numbers are attributes; objects (`runtime`, `section`, `session` and
|
|
88
|
+
the rest of [Inputs](#inputs)) are JavaScript properties. `env` is a `runtime`
|
|
89
|
+
field: the layouts have no `env` property.
|
|
42
90
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
91
|
+
Set `runtime` no later than `section`. When the player builds its own
|
|
92
|
+
coordinator, the section's arrival rebuilds that coordinator from the current
|
|
93
|
+
`runtime`, so both can be set a tick after the element mounts. Once the section
|
|
94
|
+
has initialized, a change to `runtime.tools`, `runtime.assessmentId`,
|
|
95
|
+
`runtime.accessibility`, `runtime.lazyInit`, `tool-config-strictness` or
|
|
96
|
+
`toolRegistry` is reported once in the console and does not reach that
|
|
97
|
+
coordinator; `runtime.tools.pnpEnforcement` still applies. Change a running
|
|
98
|
+
coordinator through the one `toolkit-ready` carries, with
|
|
99
|
+
`updateToolConfig(...)` or `updateToolsPlacement(...)`, or pass your own as
|
|
100
|
+
`runtime.coordinator`.
|
|
48
101
|
|
|
49
|
-
|
|
102
|
+
Attributes cover standard delivery. Host policy runs in host code over the
|
|
103
|
+
JavaScript surface: the [host methods](#host-methods), the
|
|
104
|
+
[section controller](#section-controller) and its [events](#events). The player
|
|
105
|
+
has no gating API of its own; a host composes forward and backward eligibility
|
|
106
|
+
from `getSnapshot().navigation` and controller events. Updates that leave the
|
|
107
|
+
composition unchanged (responses, tool toggles, runtime changes that keep the
|
|
108
|
+
same items) keep every item and passage mounted and keep each pane's scroll
|
|
109
|
+
position ([flow invariants](./ARCHITECTURE.md#unidirectional-flow-invariants)).
|
|
110
|
+
|
|
111
|
+
The `single-question` and `session-hydrate-db` routes of `apps/section-demos`
|
|
112
|
+
([single question](../../apps/section-demos/src/routes/%28demos%29/single-question/+page.svelte),
|
|
113
|
+
[session hydration](../../apps/section-demos/src/routes/%28demos%29/session-hydrate-db/+page.svelte))
|
|
114
|
+
are complete host integrations.
|
|
115
|
+
|
|
116
|
+
## Inputs
|
|
117
|
+
|
|
118
|
+
The three layouts take these inputs. `pie-section-player-kernel-host` takes all
|
|
119
|
+
of them except the [layout dimensions](#layout-dimensions). Numeric attributes
|
|
120
|
+
are clamped to their range.
|
|
121
|
+
|
|
122
|
+
| Input | Form | Default | Effect |
|
|
123
|
+
| --- | --- | --- | --- |
|
|
124
|
+
| `runtime` | property | — | Player strategy, tools, accessibility, coordinator, env and callbacks ([Runtime configuration](#runtime-configuration)) |
|
|
125
|
+
| `section` | property | — | The `AssessmentSection` to deliver |
|
|
126
|
+
| `session` | property | — | The section's session, a `SectionControllerSessionState` ([Session lifecycle](#session-lifecycle)) |
|
|
127
|
+
| `assessment` | property | — | The `AssessmentEntity` whose `personalNeedsProfile` and `settings` tool policy reads, forwarded to a coordinator the player builds. A coordinator passed in `runtime` is the host's to bind with `updateAssessment`. A section's own `personalNeedsProfile` is not read, and the player warns once when it finds one |
|
|
128
|
+
| `policies` | property | `DEFAULT_SECTION_PLAYER_POLICIES` | A partial `SectionPlayerPolicies` for readiness, element pre-warm and telemetry ([Policies](#policies)) |
|
|
129
|
+
| `hooks` | property | — | Host callbacks, a `SectionPlayerHostHooks` ([Card titles](#card-titles)) |
|
|
130
|
+
| `toolRegistry` | property | `createPackagedToolRegistry()` | The registry the toolbars load tools from ([Tools](#tools)) |
|
|
131
|
+
| `sectionHostButtons`, `itemHostButtons`, `passageHostButtons` | property | — | Host buttons appended to the section, item and passage toolbars |
|
|
132
|
+
| `section-id` | attribute | `section.identifier`, then `section-<assessmentId>` | The section's id for its controller and persistence |
|
|
133
|
+
| `attempt-id` | attribute | — | The attempt the session belongs to. Without one the default persistence neither reads nor writes |
|
|
134
|
+
| `locale` | attribute | `en-US` | BCP-47 locale of the player's own interface text. It never sets `runtime.contentLanguage` |
|
|
135
|
+
| `base-heading-level` | attribute | `2` (1–6) | The level of the card headings, from which every descendant's outline derives ([Heading structure](#heading-structure)) |
|
|
136
|
+
| `show-toolbar` | attribute | `false` | Renders the section toolbar, which section-level tools need. Accepts `true`/`false`, `1`/`0` and `yes`/`no` |
|
|
137
|
+
| `toolbar-position` | attribute | `right` | `top`, `right`, `bottom`, `left` or `none` |
|
|
138
|
+
| `tool-config-strictness` | attribute | `error` | `off`, `warn` or `error` for tool configuration validation |
|
|
139
|
+
| `nds-icons` | attribute | — | Renders toolbar buttons as NDS icon buttons (below) |
|
|
140
|
+
| `iife-bundle-host` | attribute | — | The bundle host for the IIFE element pre-warm when `runtime.player.loaderOptions.bundleHost` is unset |
|
|
141
|
+
| `debug` | attribute | — | Debug logging (`true` enables, `false` or `0` disables; [Debug logging](#debug-logging)) |
|
|
142
|
+
| `narrow-layout-breakpoint` | attribute | `1100` (400–2000) | The viewport width in px at or below which the layout narrows |
|
|
143
|
+
| `content-max-width-no-passage` | attribute | unset (320–2200) | Maximum content width in px for a section without passages |
|
|
144
|
+
| `content-max-width-with-passage` | attribute | unset (320–2200) | Maximum content width in px for a section with passages |
|
|
145
|
+
| `split-pane-initial-passage-width` | attribute | `50` (20–80) | Splitpane only: the passage pane's width in percent at mount |
|
|
146
|
+
| `split-pane-min-region-width` | attribute | unset (160–1200) | Splitpane only: the minimum pane width in px. Unset, the divider stays between 20% and 80% |
|
|
147
|
+
| `split-pane-collapse-strategy` | attribute | `tabbed` | Splitpane only: `tabbed` or `vertical` below the breakpoint |
|
|
148
|
+
|
|
149
|
+
`nds-icons` opts in to the Renaissance Next Design System (NDS) icon buttons,
|
|
150
|
+
which render in Font Awesome and Roboto. The players add Font Awesome Free from
|
|
151
|
+
jsDelivr unless the page links a Font Awesome stylesheet, and the buttons add
|
|
152
|
+
Roboto from `ui.renaissance.com` unless the page links a stylesheet whose URL
|
|
153
|
+
contains `Roboto`. That CDN serves the font files to Renaissance origins only,
|
|
154
|
+
so a page elsewhere links its own Roboto. A page that links Font Awesome Pro
|
|
155
|
+
keeps the design's Light and Regular weights; without Pro every glyph renders in
|
|
156
|
+
Solid.
|
|
157
|
+
|
|
158
|
+
### Layout dimensions
|
|
159
|
+
|
|
160
|
+
At or below `narrow-layout-breakpoint` the splitpane renders its
|
|
161
|
+
`split-pane-collapse-strategy` view, and every layout moves the section toolbar
|
|
162
|
+
to `top`, whatever `toolbar-position` says, `none` included. The shell
|
|
163
|
+
separately moves a `left` or `right` toolbar to `top` at a fixed 1100px, so with
|
|
164
|
+
a smaller breakpoint side toolbars still move to the top from 1100px down.
|
|
165
|
+
|
|
166
|
+
The vertical and tabbed layouts take the two max-width attributes too. When
|
|
167
|
+
both are set, the with-passage cap resolves to the greater of the two after
|
|
168
|
+
clamping, so a section with passages is never narrower than one without.
|
|
50
169
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
for the end-to-end walkthrough.
|
|
170
|
+
```html
|
|
171
|
+
<pie-section-player-splitpane
|
|
172
|
+
content-max-width-no-passage="800"
|
|
173
|
+
content-max-width-with-passage="1200"
|
|
174
|
+
split-pane-min-region-width="280"
|
|
175
|
+
split-pane-collapse-strategy="vertical"
|
|
176
|
+
></pie-section-player-splitpane>
|
|
177
|
+
```
|
|
60
178
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
179
|
+
### Runtime configuration
|
|
180
|
+
|
|
181
|
+
`runtime` is a `SectionPlayerRuntimeConfig`. `locale`, `nds-icons` and
|
|
182
|
+
`tool-config-strictness` are attributes only.
|
|
183
|
+
|
|
184
|
+
| Key | Default | Effect |
|
|
185
|
+
| --- | --- | --- |
|
|
186
|
+
| `assessmentId` | — | The assessment the section belongs to; part of every session key |
|
|
187
|
+
| `playerType` | `"iife"` | The item player strategy: `"iife"`, `"esm"` or `"preloaded"` ([Preloaded elements](#preloaded-elements)) |
|
|
188
|
+
| `player` | — | Item player settings: `hosted`, `loaderOptions`, `loaderConfig`, `backend` and `resolveBackend` ([Backend delivery](#backend-delivery-for-embedded-items)). Every other field is passed to each embedded item player as a property |
|
|
189
|
+
| `env` | `{ mode: "gather", role: "student" }` | The section env every item renders in |
|
|
190
|
+
| `contentLanguage` | `en-US` | BCP-47 language of content whose markup names none; read-aloud speaks in it and catalog cards are picked by it. A `lang` between the content and its card wins |
|
|
191
|
+
| `tools` | — | Tool placement, providers and policy ([Tools](#tools)) |
|
|
192
|
+
| `toolContextResolvers` | — | Per-tool resolvers that run after the policy gates: they can hide a tool that survived them or attach render parameters, and cannot re-enable a removed one |
|
|
193
|
+
| `accessibility` | — | `{ catalogs, language }`: accessibility catalogs and their language |
|
|
194
|
+
| `lazyInit` | `false` | Starts text to speech at first use. `waitUntilReady()` then settles without it unless policy grants the tool |
|
|
195
|
+
| `coordinator` | — | A `ToolkitCoordinator` the host built. Unset, the player builds one |
|
|
196
|
+
| `isolation` | `"inherit"` | Without `coordinator`, `"inherit"` shares an enclosing toolkit's coordinator when there is one; `"force"` keeps the player's own |
|
|
197
|
+
| `createSectionController` | — | A factory for the section controller ([Custom section controllers](#custom-section-controllers)) |
|
|
198
|
+
| `onStageChange`, `onLoadingComplete`, `onFrameworkError` | — | Callbacks with each `pie-stage-change` detail, the `pie-loading-complete` detail and each framework error model ([Instrumentation](#instrumentation)) |
|
|
199
|
+
|
|
200
|
+
### Tools
|
|
201
|
+
|
|
202
|
+
Tool placement is empty by default. `runtime.tools.placement` places tools by
|
|
203
|
+
scope (`section`, `item`, `passage`), and
|
|
204
|
+
`SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT` from
|
|
205
|
+
`@pie-players/pie-default-tool-loaders` is the packaged opt-in. Section-level
|
|
206
|
+
tools render only when `show-toolbar` is set. The
|
|
207
|
+
[canonical tool ids](../../docs/tools-and-accomodations/tool_provider_system.md#canonical-tool-ids)
|
|
208
|
+
are listed with the tool provider system. Tool configuration is validated when
|
|
209
|
+
the toolkit initializes, toolbar overlays included, at the
|
|
210
|
+
`tool-config-strictness` level. The text-to-speech provider is configured under
|
|
211
|
+
`tools.providers.textToSpeech`; validation rejects `tools.providers.tts`.
|
|
64
212
|
|
|
65
|
-
|
|
213
|
+
```ts
|
|
214
|
+
import {
|
|
215
|
+
SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT,
|
|
216
|
+
createPackagedToolRegistry,
|
|
217
|
+
} from "@pie-players/pie-default-tool-loaders";
|
|
218
|
+
import type { SectionPlayerRuntimeHostContract } from "@pie-players/pie-section-player";
|
|
219
|
+
|
|
220
|
+
const player = document.querySelector<HTMLElement & SectionPlayerRuntimeHostContract>(
|
|
221
|
+
"pie-section-player-splitpane",
|
|
222
|
+
);
|
|
223
|
+
if (player) {
|
|
224
|
+
player.toolRegistry = createPackagedToolRegistry();
|
|
225
|
+
player.runtime = {
|
|
226
|
+
assessmentId: "practice-1",
|
|
227
|
+
tools: { placement: SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT },
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`toolRegistry` replaces the packaged registry. Build it with
|
|
233
|
+
`createPackagedToolRegistry()` and register custom tools on it, since the
|
|
234
|
+
toolbars load each tool's element through the registry's loaders. A coordinator
|
|
235
|
+
the player builds takes this registry. A coordinator passed as
|
|
236
|
+
`runtime.coordinator` keeps the registry it was constructed with, which decides
|
|
237
|
+
policy; constructed without one, it takes the player's.
|
|
238
|
+
|
|
239
|
+
### Policies
|
|
240
|
+
|
|
241
|
+
`policies` is a partial `SectionPlayerPolicies`. Every unset field takes its
|
|
242
|
+
value from `DEFAULT_SECTION_PLAYER_POLICIES`, exported from the package root and
|
|
243
|
+
from `@pie-players/pie-section-player/policies`.
|
|
244
|
+
|
|
245
|
+
| Field | Default | Effect |
|
|
246
|
+
| --- | --- | --- |
|
|
247
|
+
| `readiness.mode` | `"progressive"` | `"progressive"` or `"strict"`. Both hold the `interactive` stage and `pie-loading-complete` until the element pre-warm resolves, and the two currently emit the same sequence |
|
|
248
|
+
| `preload.enabled` | `true` | `false` skips the section's element pre-warm, the step that loads every element the items name before they mount. Items still mount, and each item player registers its own elements. For a host that owns element registration end to end |
|
|
249
|
+
| `telemetry.enabled` | `true` | `false` stops the `pie-section-*` instrumentation stream ([Instrumentation](#instrumentation)) |
|
|
250
|
+
|
|
251
|
+
`resolveSectionPlayerPolicies(policies)` from
|
|
252
|
+
`@pie-players/pie-section-player/policies` returns the filled-in object, for
|
|
253
|
+
host code that applies the same gates.
|
|
254
|
+
|
|
255
|
+
### Card titles
|
|
256
|
+
|
|
257
|
+
`hooks.cardTitleFormatter` sets the heading of each item and passage card:
|
|
66
258
|
|
|
67
259
|
```ts
|
|
68
|
-
|
|
69
|
-
|
|
260
|
+
import type { SectionPlayerHostHooks } from "@pie-players/pie-section-player";
|
|
261
|
+
|
|
262
|
+
const hooks: SectionPlayerHostHooks = {
|
|
263
|
+
cardTitleFormatter: (context) =>
|
|
264
|
+
context.kind === "item"
|
|
265
|
+
? `Question ${context.itemIndex + 1}: ${context.item.name || context.defaultTitle}`
|
|
266
|
+
: context.passage.name || context.defaultTitle,
|
|
267
|
+
};
|
|
70
268
|
```
|
|
71
269
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
270
|
+
An item context carries `item`, `itemIndex`, `itemCount`, `canonicalItemId` and
|
|
271
|
+
`defaultTitle`, the localized "Question N" ("Question" for a one-item section);
|
|
272
|
+
a passage context carries `passage` and `defaultTitle`. A formatter that returns
|
|
273
|
+
anything but a non-empty string, or throws, gets `defaultTitle`. The formatter
|
|
274
|
+
stays active across the splitpane's narrow and wide transitions.
|
|
275
|
+
|
|
276
|
+
### Debug logging
|
|
277
|
+
|
|
278
|
+
Debug logging is page-wide. A layout's `debug` attribute writes
|
|
279
|
+
`window.PIE_DEBUG`, the flag every PIE logger on the page reads, so the last
|
|
280
|
+
host to set it decides for all of them. Without a `debug` attribute a layout
|
|
281
|
+
follows `window.PIE_DEBUG`, which a host can set directly.
|
|
77
282
|
|
|
78
|
-
|
|
79
|
-
|
|
283
|
+
## Host methods
|
|
284
|
+
|
|
285
|
+
The layouts and the kernel host implement `SectionPlayerRuntimeHostContract`:
|
|
286
|
+
|
|
287
|
+
| Method | Returns | Effect |
|
|
288
|
+
| --- | --- | --- |
|
|
289
|
+
| `getSnapshot()` | `SectionPlayerSnapshot` | `{ composition: { itemsCount, passagesCount }, navigation: { currentIndex, totalItems, canNext, canPrevious, currentItemId } }` |
|
|
290
|
+
| `navigateTo(index)` | `boolean` | Moves to the item at a zero-based index; `false` when it cannot |
|
|
291
|
+
| `navigateNext()`, `navigatePrevious()` | `boolean` | Moves one item; `false` when it cannot |
|
|
292
|
+
| `getSectionController()` | `SectionControllerHandle \| null` | The current section's controller |
|
|
293
|
+
| `waitForSectionController(timeoutMs = 5000)` | `Promise<SectionControllerHandle \| null>` | Resolves at once when the controller is resolvable, else at the next `toolkit-ready`, else at the timeout with the current lookup, which may be `null` |
|
|
294
|
+
|
|
295
|
+
The methods exist from the moment the element is created. Until it mounts,
|
|
80
296
|
`waitForSectionController` waits, `getSectionController()` returns `null`, the
|
|
81
|
-
navigation methods return `false`, and `getSnapshot()`
|
|
82
|
-
|
|
83
|
-
|
|
297
|
+
navigation methods return `false`, and `getSnapshot()` returns `null` on the
|
|
298
|
+
stock layouts and the bootstrap snapshot (zero counts, no navigation) on the
|
|
299
|
+
kernel host.
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
import type { SectionPlayerRuntimeHostContract } from "@pie-players/pie-section-player";
|
|
303
|
+
|
|
304
|
+
const player = document.querySelector<HTMLElement & SectionPlayerRuntimeHostContract>(
|
|
305
|
+
"pie-section-player-splitpane",
|
|
306
|
+
);
|
|
307
|
+
const controller = await player?.waitForSectionController(5000);
|
|
308
|
+
|
|
309
|
+
let sectionComplete = false;
|
|
310
|
+
const unsubscribe = controller?.subscribe?.((event) => {
|
|
311
|
+
if (event.type === "section-items-complete-changed") {
|
|
312
|
+
sectionComplete = event.complete;
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
function canAdvance(): boolean {
|
|
317
|
+
return Boolean(player?.getSnapshot()?.navigation.canNext && sectionComplete);
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Section controller
|
|
322
|
+
|
|
323
|
+
The section controller is the section's domain authority: it owns in-section
|
|
324
|
+
navigation, the canonical aggregation of item sessions and the persistence
|
|
325
|
+
snapshot. The layouts are transport adapters around it
|
|
326
|
+
([controller ownership](./ARCHITECTURE.md#controller-ownership)). Its handle,
|
|
327
|
+
`SectionControllerHandle` from `@pie-players/pie-assessment-toolkit`, declares
|
|
328
|
+
every member optional, and the interface's JSDoc carries the per-method
|
|
329
|
+
contract.
|
|
330
|
+
|
|
331
|
+
| Member | Effect |
|
|
332
|
+
| --- | --- |
|
|
333
|
+
| `getSession()` | The section session, a `SectionControllerSessionState`: `currentItemIndex`, `visitedItemIdentifiers`, `itemSessions`, and `formative` and `timedMedia` when the section uses them |
|
|
334
|
+
| `applySession(session, { mode })` | Applies a section session. `"replace"` (the default) replaces it; `"merge"` overlays it item by item. Item entries may be canonical entries or raw item sessions |
|
|
335
|
+
| `updateItemSession(itemId, detail)` | Writes one item's session synchronously and emits `item-session-data-changed` or `item-session-meta-changed` |
|
|
336
|
+
| `hydrate()` | Loads the session from the persistence strategy and applies it in replace mode; a no-op without a strategy or a stored session |
|
|
337
|
+
| `persist()` | Saves the session through the strategy, at the host's cadence; a no-op without one |
|
|
338
|
+
| `configureSessionPersistence({ context, strategy })` | Sets the persistence strategy |
|
|
339
|
+
| `subscribe(listener)` | Delivers every controller event to `listener` until the returned function is called or the controller is disposed |
|
|
340
|
+
| `getRuntimeState()` | Introspection only; persist `getSession()` |
|
|
341
|
+
| `updateInput(input)` | Refreshes the composition for the same cohort, the `(sectionId, attemptId)` pair, and keeps the in-memory session |
|
|
342
|
+
| Formative members | `recordFormativeTry`, `retryFormativeItem`, `revealFormativeItem`, `hideFormativeItem`, `getFormativeProjection` ([Formative delivery](#formative-delivery)) |
|
|
343
|
+
| Timed-media members | `attachMediaTimeSource`, `detachMediaTimeSource`, `pauseMediaForCompetingAudio`, `getTimedMediaProjection` ([Timed media](#timed-media)) |
|
|
84
344
|
|
|
85
345
|
### Session lifecycle
|
|
86
346
|
|
|
@@ -105,129 +365,232 @@ Driving the controller directly:
|
|
|
105
365
|
controller?.configureSessionPersistence?.({ context, strategy });
|
|
106
366
|
await controller?.hydrate?.();
|
|
107
367
|
const unsubscribe = controller?.subscribe?.(handleEvent);
|
|
108
|
-
//
|
|
368
|
+
// Later, on save or unload:
|
|
109
369
|
await controller?.persist?.();
|
|
110
370
|
unsubscribe?.();
|
|
111
371
|
```
|
|
112
372
|
|
|
113
|
-
|
|
114
|
-
detail)` are the direct read/write surfaces and exchange the same
|
|
115
|
-
`SectionControllerSessionState` shape the persistence strategy load/save
|
|
116
|
-
methods receive. See [Item session management](#item-session-management) for
|
|
117
|
-
worked examples.
|
|
118
|
-
|
|
119
|
-
### Event stream
|
|
120
|
-
|
|
121
|
-
The controller's typed event stream (`SectionControllerEvent` discriminated
|
|
122
|
-
union) is the single source of truth for in-section change. Hosts usually
|
|
123
|
-
subscribe through `ToolkitCoordinator.subscribeItemEvents` /
|
|
124
|
-
`subscribeSectionLifecycleEvents` (cohort-aware filtering, survives
|
|
125
|
-
navigation) — see [JS API example for advanced host
|
|
126
|
-
policy](#js-api-example-for-advanced-host-policy). Key event types:
|
|
127
|
-
|
|
128
|
-
- `item-selected` — item navigation within the current section.
|
|
129
|
-
- `item-session-data-changed` / `item-session-meta-changed` — per-item
|
|
130
|
-
session updates the persistence layer should observe. `sectionId` names the
|
|
131
|
-
section. `elementId` names the reporting element when the change carries one,
|
|
132
|
-
and `complete` is then that element's own. A commit carries
|
|
133
|
-
`sessionCommitReason` (see [Commit at a section
|
|
134
|
-
boundary](#commit-at-a-section-boundary)).
|
|
135
|
-
- `item-complete-changed` — an item's completion flipped. An item is complete
|
|
136
|
-
when every element that has reported its completion is complete. A report
|
|
137
|
-
without `elementId`, and a restored session's item-level `complete`, set the
|
|
138
|
-
item's completion directly. A session restored through `applySession`
|
|
139
|
-
without `complete` is complete when it holds a response. A passage's own
|
|
140
|
-
`session-changed` stays inside its shell: a passage holds no response.
|
|
141
|
-
- `content-loaded` — passage / item / rubric finished loading. Carries
|
|
142
|
-
`contentKind`, `itemId`, and `canonicalItemId`.
|
|
143
|
-
- `section-loading-complete` — every renderable in the section finished
|
|
144
|
-
loading.
|
|
145
|
-
- `section-items-complete-changed` — aggregate completion flip.
|
|
146
|
-
- `section-error` — an item player failed (`source: "item-player"`), or the
|
|
147
|
-
section runtime did (`source: "section-runtime"`), a rejected element warmup
|
|
148
|
-
included, which leaves the items unmounted.
|
|
149
|
-
- `section-navigation-change` — the controller's section identity changed.
|
|
150
|
-
- `formative-try-recorded` — a learner checked an answer.
|
|
151
|
-
- `formative-reveal-changed` — the reveal state changed without a Try: a learner
|
|
152
|
-
dismissed feedback, or a host forced or withdrew a reveal (`source` says which).
|
|
153
|
-
- `section-mastery-changed` — the mastery rollup changed. Emitted on change
|
|
154
|
-
only, like `section-items-complete-changed`.
|
|
155
|
-
- `timed-media-cue-changed` — a cue activated, a gate released, or aggregate
|
|
156
|
-
completion flipped. Not emitted for media position: `timeupdate` fires about four
|
|
157
|
-
times a second and moves nothing a layout renders.
|
|
158
|
-
- `timed-media-audio-started` — media audio is running, so read-aloud must yield.
|
|
159
|
-
Emitted only where playback actually stood; a gate that re-paused on the same
|
|
160
|
-
`play` produced no audio.
|
|
161
|
-
- `timed-media-policy-degraded` — the attached media time source cannot carry out a
|
|
162
|
-
playback policy, so it is advisory from here.
|
|
163
|
-
- `timed-media-invalid` — authored `timedMedia` that cannot be delivered; the
|
|
164
|
-
section renders without cue behavior.
|
|
165
|
-
|
|
166
|
-
The helpers' default event sets leave out `formative-try-recorded`,
|
|
167
|
-
`formative-reveal-changed`, `section-mastery-changed` and
|
|
168
|
-
`timed-media-audio-started`. Name them in `subscribeItemEvents({ eventTypes })`,
|
|
169
|
-
or subscribe through `subscribeSectionEvents`.
|
|
373
|
+
### Item session management
|
|
170
374
|
|
|
171
|
-
|
|
172
|
-
`
|
|
173
|
-
`
|
|
174
|
-
`canonicalItemId` is that item's adapter identifier, and falls back to `itemId`
|
|
175
|
-
when the section was not built from an adapter or no adapter ref matches it.
|
|
176
|
-
Correlate with formative policy and `runtime.player.resolveBackend` by
|
|
177
|
-
`canonicalItemId`; reach the session by `itemId`. `content-loaded` and
|
|
178
|
-
`item-player-error` both carry the pair.
|
|
375
|
+
`getSession()`, `applySession()` and `updateItemSession()` exchange the same
|
|
376
|
+
`SectionControllerSessionState` the persistence strategy loads and saves and the
|
|
377
|
+
`session` property takes.
|
|
179
378
|
|
|
180
|
-
|
|
379
|
+
```ts
|
|
380
|
+
import type { SectionPlayerRuntimeHostContract } from "@pie-players/pie-section-player";
|
|
181
381
|
|
|
182
|
-
|
|
183
|
-
|
|
382
|
+
const player = document.querySelector<HTMLElement & SectionPlayerRuntimeHostContract>(
|
|
383
|
+
"pie-section-player-splitpane",
|
|
384
|
+
);
|
|
385
|
+
const controller = await player?.waitForSectionController(5000);
|
|
184
386
|
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
387
|
+
// Read the current section session.
|
|
388
|
+
const currentSession = controller?.getSession?.();
|
|
389
|
+
|
|
390
|
+
// Replace the section session, as when resuming from a stored snapshot.
|
|
391
|
+
await controller?.applySession?.(
|
|
392
|
+
{
|
|
393
|
+
currentItemIndex: 0,
|
|
394
|
+
visitedItemIdentifiers: ["q1"],
|
|
395
|
+
itemSessions: {
|
|
396
|
+
q1: {
|
|
397
|
+
itemIdentifier: "q1",
|
|
398
|
+
pieSessionId: "q1-session",
|
|
399
|
+
session: { id: "q1-session", data: [{ id: "choice", value: "a" }] },
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
{ mode: "replace" },
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// Update one item's session.
|
|
407
|
+
controller?.updateItemSession?.("q1", {
|
|
408
|
+
session: { id: "q1-session", data: [{ id: "choice", value: "b" }] },
|
|
409
|
+
complete: true,
|
|
410
|
+
});
|
|
196
411
|
```
|
|
197
412
|
|
|
198
|
-
|
|
199
|
-
|
|
413
|
+
### Commit at a section boundary
|
|
414
|
+
|
|
415
|
+
A delivery element coalesces its `session-changed` dispatch, so a response the
|
|
416
|
+
learner has finished entering can still be pending when the section moves. The
|
|
417
|
+
controller commits pending element sessions before item navigation, before
|
|
418
|
+
`updateInput()` snapshots the session for a section swap, and before `persist()`.
|
|
419
|
+
The player also commits when an item shell tears down and when the page goes
|
|
420
|
+
hidden.
|
|
421
|
+
|
|
422
|
+
A committed response travels like any other. A raw element `session-changed`
|
|
423
|
+
does not leave its `<pie-item-scope>`, which re-dispatches it as the normalized
|
|
424
|
+
`item-session-changed` (`PIE_ITEM_SESSION_CHANGED_EVENT`); the toolkit then
|
|
425
|
+
publishes the section's canonical `session-changed`. Both bubble through the
|
|
426
|
+
layout element to `document`, and a listener on either receives each dispatch
|
|
427
|
+
once. Host code persists from the controller's events or from its session
|
|
428
|
+
snapshot.
|
|
429
|
+
|
|
430
|
+
Each of those events marks a commit with `sessionCommitReason`
|
|
431
|
+
(`"teardown" | "navigate" | "page-hidden"`): `item-session-changed`, the
|
|
432
|
+
toolkit's `session-changed`, and the controller's `item-session-data-changed`
|
|
433
|
+
and `item-session-meta-changed`. A commit at a section swap or item navigation
|
|
434
|
+
reports the item being left, which the host may already have moved past, so a
|
|
435
|
+
handler that sets its current item or navigation state from these events leaves
|
|
436
|
+
that state alone on a commit and persists the commit's session as usual. The
|
|
437
|
+
controller's events also carry `sectionId`, the section the item belongs to.
|
|
200
438
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
439
|
+
Navigation inside a section keeps every item mounted, so nothing is discarded
|
|
440
|
+
and the element's own debounce would complete on its own. The commit still runs
|
|
441
|
+
there because the response belongs to the item being left: a host that persists
|
|
442
|
+
on the navigation event, or a `persist()` that follows it, would otherwise
|
|
443
|
+
snapshot a session the learner had already changed.
|
|
205
444
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
445
|
+
The section controller is DOM-free, so the player supplies the commit through
|
|
446
|
+
the handle's `setPendingSessionCommit()`. The player registers it on the
|
|
447
|
+
controllers it creates and on one the toolkit has already built. A host-built
|
|
448
|
+
controller that leaves the method unimplemented gets no commit, and a pending
|
|
449
|
+
response is lost at those boundaries.
|
|
450
|
+
|
|
451
|
+
### Custom section controllers
|
|
452
|
+
|
|
453
|
+
`runtime.createSectionController` is a factory the player calls for each
|
|
454
|
+
section's controller. A coordinator's `hooks.createSectionController` takes
|
|
455
|
+
precedence over it; without either, the player constructs a `SectionController`.
|
|
456
|
+
The player builds its own coordinator without hooks, so a host that lets it do
|
|
457
|
+
so supplies a controller through `runtime.createSectionController`.
|
|
458
|
+
|
|
459
|
+
## Events
|
|
460
|
+
|
|
461
|
+
### DOM events
|
|
462
|
+
|
|
463
|
+
Every event bubbles and is composed, so it reaches `document`, and a listener on
|
|
464
|
+
the layout element receives each one. The layout element dispatches
|
|
465
|
+
`pie-stage-change`, `pie-loading-complete` and the `element-preload-*` events.
|
|
466
|
+
The toolkit dispatches the runtime, readiness, composition, `session-changed`
|
|
467
|
+
and `framework-error` events on the inner `pie-section-player-base`, and they
|
|
468
|
+
bubble out through the layout element. `item-session-changed` starts at the
|
|
469
|
+
item's `<pie-item-scope>`.
|
|
470
|
+
|
|
471
|
+
| Event | Detail | When |
|
|
472
|
+
| --- | --- | --- |
|
|
473
|
+
| `runtime-owned`, `runtime-inherited` | `runtimeId`, `parentRuntimeId` | The player's toolkit owns its coordinator, or shares an enclosing one |
|
|
474
|
+
| `runtime-ready` | `runtimeId`, `coordinator`, `ownership` | Once per coordinator, before the first section starts: the earliest point a host holds the coordinator |
|
|
475
|
+
| `toolkit-ready` | `runtimeId`, `assessmentId`, `sectionId`, `itemPlayer`, `coordinator` | For each section, a microtask after its controller resolves and its composition goes out |
|
|
476
|
+
| `section-ready` | `sectionId`, `attemptId`, `controller` (or `null`) | After that section's `toolkit-ready` and its `engine-ready` stage |
|
|
477
|
+
| `composition-changed` | `composition`, `version` | The composition changed; `composition.formative` carries the formative projection |
|
|
478
|
+
| `session-changed` | The normalized item session, plus `itemId`, `canonicalItemId` and `sourceRuntimeId` | An item's session changed |
|
|
479
|
+
| `item-session-changed` | The normalized item session | Re-dispatched by `<pie-item-scope>` from an element's `session-changed` |
|
|
480
|
+
| `pie-stage-change` | `stage`, `status`, `runtimeId`, `sectionId`, `attemptId`, `timestamp`, `sourceCe` | A lifecycle stage was entered: `composed`, `engine-ready`, `interactive`, `disposed` |
|
|
481
|
+
| `pie-loading-complete` | `runtimeId`, `sectionId`, `attemptId`, `itemCount`, `timestamp`, `sourceCe` | Once per cohort, after `section-ready` and the element pre-warm, before the items load |
|
|
482
|
+
| `framework-error` | A `FrameworkErrorModel` | Any failure crossing the framework boundary, once per error |
|
|
483
|
+
| `element-preload-retry`, `element-preload-error` | `assessmentId`, `sectionId`, `attemptId` | The element pre-warm retried or failed |
|
|
484
|
+
|
|
485
|
+
A first section starts in this order: `pie-stage-change` `composed`,
|
|
486
|
+
`runtime-ready`, `toolkit-ready`, `section-ready`, `pie-stage-change`
|
|
487
|
+
`engine-ready`, then `pie-stage-change` `interactive` and `pie-loading-complete`
|
|
488
|
+
once the element pre-warm resolves. The items load after that; the controller's
|
|
489
|
+
`section-loading-complete` marks every one loaded, and a session applied before
|
|
490
|
+
then is announced again as `section-session-applied` with `replay: true`.
|
|
491
|
+
|
|
492
|
+
A non-recoverable framework error before `interactive` ends the stage chain:
|
|
493
|
+
the first stage the section did not reach is emitted as `failed`, and any after
|
|
494
|
+
it as `skipped`. A pre-warm failure also arrives as an `element-preload`
|
|
495
|
+
framework error.
|
|
496
|
+
|
|
497
|
+
### Controller events
|
|
498
|
+
|
|
499
|
+
`subscribe()` on the handle, and the coordinator's subscription helpers,
|
|
500
|
+
deliver `SectionControllerEvent`, a union discriminated by `type`. Every event
|
|
501
|
+
carries `timestamp`, and every one except `section-navigation-change` carries
|
|
502
|
+
`currentItemIndex`.
|
|
503
|
+
|
|
504
|
+
| Event | Payload | Emitted when |
|
|
505
|
+
| --- | --- | --- |
|
|
506
|
+
| `item-selected` | `previousItemId`, `currentItemId`, `itemIndex`, `totalItems` | Item navigation within the section |
|
|
507
|
+
| `item-session-data-changed` | `itemId`, `canonicalItemId`, `session`, `intent`, `complete`, `component`, `elementId`, `sectionId`, `sessionCommitReason` | An item's response changed |
|
|
508
|
+
| `item-session-meta-changed` | As above, without `session` and `intent` | An item's session metadata changed |
|
|
509
|
+
| `item-complete-changed` | `itemId`, `canonicalItemId`, `complete`, `previousComplete` | An item's completion flipped |
|
|
510
|
+
| `content-loaded` | `contentKind` (`item`, `passage`, `rubric`, `unknown`), `itemId`, `canonicalItemId`, `detail` | A passage, item or rubric finished loading |
|
|
511
|
+
| `item-player-error` | `contentKind`, `itemId`, `canonicalItemId`, `error` | An item player failed |
|
|
512
|
+
| `section-loading-complete` | `totalRegistered`, `totalLoaded` | Every renderable in the section finished loading |
|
|
513
|
+
| `section-items-complete-changed` | `complete`, `completedCount`, `totalItems` | The section's aggregate completion flipped |
|
|
514
|
+
| `section-session-applied` | `mode`, `itemSessionCount`, `replay` | A session was applied; `replay: true` after `section-loading-complete` |
|
|
515
|
+
| `section-navigation-change` | `previousSectionId`, `currentSectionId`, `attemptId`, `reason` (`input-change`, `runtime-transition`) | The controller's section identity changed |
|
|
516
|
+
| `section-error` | `source`, `error`, `itemId`, `canonicalItemId`, `contentKind` | An item player failed (`source: "item-player"`), or the section runtime did (`"section-runtime"`), a rejected element pre-warm included, which leaves the items unmounted |
|
|
517
|
+
| `formative-try-recorded` | `itemId`, `canonicalItemId`, `tryCount`, `outcome`, `revealed` | A Try was recorded |
|
|
518
|
+
| `formative-reveal-changed` | `itemId`, `canonicalItemId`, `revealed`, `feedback`, `tryCount`, `source` | The reveal changed without a Try |
|
|
519
|
+
| `section-mastery-changed` | `mastery` | The mastery rollup changed |
|
|
520
|
+
| `timed-media-cue-changed` | Active, visited and completed cues, `revealedItemIds`, `gateCueIdentifier`, `mediaCompleted`, `aggregateComplete` | A cue activated, a gate released, or aggregate completion flipped |
|
|
521
|
+
| `timed-media-audio-started` | — | Media audio is running, so read-aloud yields |
|
|
522
|
+
| `timed-media-policy-degraded` | `degradations` | The media time source cannot carry out a playback policy |
|
|
523
|
+
| `timed-media-invalid` | `errors` | The authored `timedMedia` cannot be delivered |
|
|
524
|
+
|
|
525
|
+
An item is complete when every element that has reported its completion is
|
|
526
|
+
complete. A report without `elementId`, and a restored session's item-level
|
|
527
|
+
`complete`, set the item's completion directly. A session restored through
|
|
528
|
+
`applySession` without `complete` is complete when it holds a response. A
|
|
529
|
+
passage's own `session-changed` stays inside its shell: a passage holds no
|
|
530
|
+
response.
|
|
531
|
+
|
|
532
|
+
`timed-media-cue-changed` is not emitted for media position: `timeupdate` fires
|
|
533
|
+
about four times a second and moves nothing a layout renders.
|
|
534
|
+
`timed-media-audio-started` is emitted only where playback actually started; a
|
|
535
|
+
gate that re-paused on the same `play` produced no audio.
|
|
536
|
+
|
|
537
|
+
### Subscriptions
|
|
538
|
+
|
|
539
|
+
The coordinator's helpers follow the active cohort, the `(sectionId, attemptId)`
|
|
540
|
+
pair, across section changes, so one subscription survives navigation; the
|
|
541
|
+
handle's `subscribe()` ends with its controller.
|
|
209
542
|
|
|
210
543
|
```ts
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
544
|
+
const unsubscribeItems = coordinator.subscribeItemEvents({
|
|
545
|
+
listener: (event) => {
|
|
546
|
+
// item-selected, item-session-*, item-complete-changed, content-loaded, item-player-error
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
const unsubscribeSection = coordinator.subscribeSectionLifecycleEvents({
|
|
551
|
+
listener: (event) => {
|
|
552
|
+
// section-navigation-change, section-session-applied, section-loading-complete,
|
|
553
|
+
// section-items-complete-changed, section-error, timed-media-* (except audio-started)
|
|
554
|
+
},
|
|
555
|
+
});
|
|
219
556
|
```
|
|
220
557
|
|
|
221
|
-
|
|
222
|
-
`
|
|
223
|
-
|
|
558
|
+
The defaults leave out `formative-try-recorded`, `formative-reveal-changed`,
|
|
559
|
+
`section-mastery-changed` and `timed-media-audio-started`. Name them in
|
|
560
|
+
`subscribeItemEvents({ eventTypes })`, or subscribe through
|
|
561
|
+
`subscribeSectionEvents({ listener, eventTypes, itemIds })`.
|
|
224
562
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
563
|
+
A helper called before the coordinator's first section has been requested
|
|
564
|
+
throws ("requires an active section cohort"). `runtime-ready` fires before that
|
|
565
|
+
request, so a host subscribes from `toolkit-ready` or later. A listener added
|
|
566
|
+
while a section is starting binds when it becomes active.
|
|
229
567
|
|
|
230
|
-
|
|
568
|
+
Item-scoped events carry both id forms, and both are always populated.
|
|
569
|
+
`itemId` is the bare `item.id`, the form `applySession` expects.
|
|
570
|
+
`canonicalItemId` is that item's adapter identifier, and falls back to `itemId`
|
|
571
|
+
when the section was not built from an adapter or no adapter ref matches it.
|
|
572
|
+
Correlate with formative policy and `runtime.player.resolveBackend` by
|
|
573
|
+
`canonicalItemId`; reach the session by `itemId`.
|
|
574
|
+
|
|
575
|
+
## Formative delivery
|
|
576
|
+
|
|
577
|
+
A section with `formative` set delivers as practice: each formative item gets a
|
|
578
|
+
check-answer control, records Tries against `maxTries`, and reveals feedback by
|
|
579
|
+
`feedback` and `revealOn` (`"on-try"`, or `"on-final-try"`, which resolves to
|
|
580
|
+
`"on-try"` under `maxTries: "unlimited"`). An item ref changes the section's
|
|
581
|
+
policy field by field. A revealed item gets the env projection, `mode:
|
|
582
|
+
"evaluate"` (with `role: "instructor"` under `feedback: "solution"`) over the
|
|
583
|
+
section env for that item alone, and the element draws the feedback. A section
|
|
584
|
+
in which no item is enabled delivers unchanged, with no control, state or env
|
|
585
|
+
projection. Scoring a Try needs an unhosted item player
|
|
586
|
+
([player requirements](../../docs/section-player/formative-delivery.md#player-requirements)):
|
|
587
|
+
in a hosted player `provideScore()` returns empty slots and every Try records
|
|
588
|
+
`unknown`. [Formative Delivery](../../docs/section-player/formative-delivery.md)
|
|
589
|
+
covers the policy, the controller methods and events, mastery and persistence;
|
|
590
|
+
the [formative delivery contract](../../docs/prds/formative-delivery-contract.md)
|
|
591
|
+
holds the specification and its QTI 3 mapping.
|
|
592
|
+
|
|
593
|
+
## Timed media
|
|
231
594
|
|
|
232
595
|
Set `sectionType: "timed-media"` and a `timedMedia` block, and the section's cue
|
|
233
596
|
timeline decides when its items are delivered:
|
|
@@ -244,8 +607,8 @@ const section: AssessmentSection = {
|
|
|
244
607
|
class: "stimulus",
|
|
245
608
|
view: ["candidate"],
|
|
246
609
|
// An ordinary passage. Its config mounts the media element — a PIE element,
|
|
247
|
-
// or authored `<video>` markup
|
|
248
|
-
// carry
|
|
610
|
+
// or authored `<video>` markup, whose `<track>` carries the captions — and
|
|
611
|
+
// its accessibility catalogs carry the transcript and signed alternates.
|
|
249
612
|
passage: videoPassage,
|
|
250
613
|
},
|
|
251
614
|
],
|
|
@@ -266,150 +629,226 @@ const section: AssessmentSection = {
|
|
|
266
629
|
};
|
|
267
630
|
```
|
|
268
631
|
|
|
269
|
-
Every item a gate names must satisfy its `releaseOn`. To split must-answer items
|
|
270
|
-
optional ones, author two cues at the same timestamp
|
|
271
|
-
reveal over the second. Both activate in the same pass, the reveal
|
|
272
|
-
and only the gate holds playback.
|
|
632
|
+
Every item a gate names must satisfy its `releaseOn`. To split must-answer items
|
|
633
|
+
from optional ones, author two cues at the same timestamp: a gate over the first
|
|
634
|
+
set and a reveal over the second. Both activate in the same pass, the reveal
|
|
635
|
+
completes at once, and only the gate holds playback.
|
|
273
636
|
|
|
274
|
-
|
|
275
|
-
cue behavior. An item no `reveal` or `gate` cue names is delivered normally
|
|
637
|
+
Without `sectionType`, delivery is unchanged: no projection, no session slice,
|
|
638
|
+
no cue behavior. An item no `reveal` or `gate` cue names is delivered normally,
|
|
276
639
|
including one a `metadata` cue names, since metadata records state and reveals
|
|
277
|
-
nothing. A cued item is mounted and hidden until its cue fires, so its session
|
|
278
|
-
shell registration survive a seek backwards.
|
|
640
|
+
nothing. A cued item is mounted and hidden until its cue fires, so its session
|
|
641
|
+
and shell registration survive a seek backwards.
|
|
279
642
|
|
|
280
|
-
The section reaches media only through a **Media Time Source**. The stimulus
|
|
281
|
-
finds the media element its passage mounted and registers a native adapter;
|
|
282
|
-
with its own player registers its own port
|
|
283
|
-
|
|
643
|
+
The section reaches media only through a **Media Time Source**. The stimulus
|
|
644
|
+
card finds the media element its passage mounted and registers a native adapter;
|
|
645
|
+
a host with its own player registers its own port, which outranks the card's
|
|
646
|
+
adapter for as long as it is attached:
|
|
284
647
|
|
|
285
648
|
```ts
|
|
286
649
|
const controller = await host.waitForSectionController(5000);
|
|
287
|
-
// No `renderableId`: a host
|
|
288
|
-
//
|
|
650
|
+
// No `renderableId`: a host asserts its own port. A renderable's adapter names
|
|
651
|
+
// itself, and is ignored unless it is the resolved stimulus.
|
|
289
652
|
controller?.attachMediaTimeSource?.(myThirdPartyPort);
|
|
290
653
|
controller?.detachMediaTimeSource?.();
|
|
291
654
|
controller?.getTimedMediaProjection?.(); // cues, gate, enforcement, revealed items
|
|
292
|
-
// One half of the read-aloud handoff
|
|
293
|
-
// overlap stands
|
|
655
|
+
// One half of the read-aloud handoff. `false` means the port cannot pause, and
|
|
656
|
+
// the overlap stands; the accommodation is not withheld.
|
|
294
657
|
controller?.pauseMediaForCompetingAudio?.();
|
|
295
658
|
```
|
|
296
659
|
|
|
297
|
-
Read-aloud and media audio never run at once, and the action the learner just
|
|
298
|
-
wins: starting read-aloud pauses media, starting media pauses read-aloud.
|
|
299
|
-
supplies both halves
|
|
300
|
-
toolkit arbitrates between them, because
|
|
301
|
-
the
|
|
660
|
+
Read-aloud and media audio never run at once, and the action the learner just
|
|
661
|
+
took wins: starting read-aloud pauses media, starting media pauses read-aloud.
|
|
662
|
+
The section supplies both halves, the method above and
|
|
663
|
+
`timed-media-audio-started`, and the toolkit arbitrates between them, because
|
|
664
|
+
only the toolkit holds both the text-to-speech service and the section. Neither
|
|
665
|
+
direction resumes what it silenced.
|
|
302
666
|
|
|
303
667
|
Where the port reports `canPause: false` or `canRestrictSeeking: false`, the
|
|
304
|
-
matching policy degrades to **advisory**: cues still fire, state is still
|
|
305
|
-
the projection says `enforcement: "advisory"`, and a recoverable
|
|
306
|
-
framework warning names the policy that
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
Three authoring mistakes fail loudly
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
Cue state persists inside `SectionControllerSessionState.timedMedia` and
|
|
318
|
-
with the rest of the snapshot, including the furthest position reached,
|
|
319
|
-
`allowSeekAhead: false` clamps against across a reload.
|
|
320
|
-
[
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
##
|
|
668
|
+
matching policy degrades to **advisory**: cues still fire, state is still
|
|
669
|
+
recorded, the projection says `enforcement: "advisory"`, and a recoverable
|
|
670
|
+
`timed-media` framework warning names the policy that can no longer be
|
|
671
|
+
enforced.
|
|
672
|
+
|
|
673
|
+
Three authoring mistakes fail loudly: a `stimulusRef` that resolves to no
|
|
674
|
+
renderable in the section; a gate on correctness over an item without unlimited
|
|
675
|
+
Tries, where a learner who spent a finite budget could never release playback
|
|
676
|
+
again; and a `stimulusRef` that resolves to a renderable which mounts no media,
|
|
677
|
+
reported once the section's content has loaded and no time source has attached.
|
|
678
|
+
Each reports a `timed-media` framework error, after which the section delivers
|
|
679
|
+
as an ordinary section with every item visible.
|
|
680
|
+
|
|
681
|
+
Cue state persists inside `SectionControllerSessionState.timedMedia` and
|
|
682
|
+
hydrates with the rest of the snapshot, including the furthest position reached,
|
|
683
|
+
which `allowSeekAhead: false` clamps against across a reload. The
|
|
684
|
+
[timed-media section contract](../../docs/prds/timed-media-section-contract.md)
|
|
685
|
+
holds the contract and its decision record.
|
|
686
|
+
|
|
687
|
+
## One item as a section
|
|
324
688
|
|
|
325
|
-
|
|
689
|
+
`sectionFromItem` wraps one item config, in the shape `<pie-item-player config>`
|
|
690
|
+
takes, and optionally its session, in the `section` and `session` the layouts
|
|
691
|
+
take. It is exported from the package root and from
|
|
692
|
+
`@pie-players/pie-section-player/item-section`, which defines no custom element
|
|
693
|
+
and imports in Node.js.
|
|
326
694
|
|
|
327
695
|
```ts
|
|
328
|
-
import
|
|
696
|
+
import { sectionFromItem } from "@pie-players/pie-section-player/item-section";
|
|
697
|
+
|
|
698
|
+
const { section, session } = sectionFromItem(itemConfig, { session: itemSession });
|
|
699
|
+
player.section = section;
|
|
700
|
+
player.session = session;
|
|
329
701
|
```
|
|
330
702
|
|
|
331
|
-
|
|
703
|
+
The item ref's `identifier` and the item's `id` are the config's `id`, so every
|
|
704
|
+
`itemId` the section reports is the id the host already holds. An advanced
|
|
705
|
+
config's `passage` becomes the item's passage, and its `instructorResources` and
|
|
706
|
+
`defaultExtraModels` stay on the item's config. The section carries no `baseId`
|
|
707
|
+
or `version`; `options.sectionId` names the section, which defaults to the
|
|
708
|
+
config's `id`, and the player takes it as the section id when `section-id` is
|
|
709
|
+
unset.
|
|
332
710
|
|
|
333
|
-
|
|
334
|
-
|
|
711
|
+
## Backend delivery for embedded items
|
|
712
|
+
|
|
713
|
+
A host configures item player backend delivery once, on the section player's
|
|
714
|
+
runtime, for a delivery backend that processes models and scores on the server.
|
|
715
|
+
The section player derives a `backend` property for each embedded item player
|
|
716
|
+
before it renders the item.
|
|
717
|
+
|
|
718
|
+
```ts
|
|
719
|
+
import type { SectionPlayerRuntimeConfig } from "@pie-players/pie-section-player";
|
|
720
|
+
|
|
721
|
+
const runtime: SectionPlayerRuntimeConfig = {
|
|
722
|
+
playerType: "iife",
|
|
723
|
+
env: { mode: "gather", role: "student" },
|
|
724
|
+
player: {
|
|
725
|
+
backend: {
|
|
726
|
+
delivery: {
|
|
727
|
+
enabled: true,
|
|
728
|
+
baseUrl: apiBaseUrl,
|
|
729
|
+
assignmentId,
|
|
730
|
+
autosave: { enabled: true, debounceMs: 250 },
|
|
731
|
+
},
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
sectionPlayer.runtime = runtime;
|
|
335
737
|
```
|
|
336
738
|
|
|
337
|
-
|
|
338
|
-
`
|
|
739
|
+
With `runtime.player.backend.delivery` enabled, each item player's `itemId` and
|
|
740
|
+
`sessionId` are its per-item delivery identity, derived from
|
|
741
|
+
`canonicalItemId || item.id` and the item session. Static delivery fields
|
|
742
|
+
(`baseUrl`, `auth`, `endpoints`, `assignmentId`, `autosave`) are kept. Set
|
|
743
|
+
`assignmentId` to the backend assignment the items are delivered under.
|
|
339
744
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
`
|
|
349
|
-
|
|
745
|
+
An enabled `backend.delivery` also sets `hosted: true` on each embedded item
|
|
746
|
+
player unless `runtime.player.hosted` is set, so the item players load no
|
|
747
|
+
element controllers and render the models the server returns. Those item players
|
|
748
|
+
cannot score in the browser, so formative Tries record `unknown`: formative
|
|
749
|
+
delivery needs an unhosted player.
|
|
750
|
+
|
|
751
|
+
`runtime.player.resolveBackend(context, baseBackend)` maps per-item identity
|
|
752
|
+
when the derivation does not fit. `context` carries `itemId`, `canonicalItemId`,
|
|
753
|
+
`item`, `itemIndex`, `itemSession`, `sectionId`, `env` and `baseBackend`; the
|
|
754
|
+
returned config merges over the derived backend, and a nullish return keeps it.
|
|
755
|
+
The resolver receives cloned backend objects, so a per-item change neither
|
|
756
|
+
mutates the shared runtime nor leaks across items, and the key is removed before
|
|
757
|
+
the properties reach `<pie-item-player>`.
|
|
758
|
+
|
|
759
|
+
The section player derives `backend` and calls nothing on the item players. An
|
|
760
|
+
embedded `<pie-item-player>` loads on its own when its derived
|
|
761
|
+
`backend.delivery` has a load signature, so every mounted item player issues one
|
|
762
|
+
backend load for its own item. Passage players receive the shared non-delivery
|
|
763
|
+
backend configuration and no item delivery configuration. This configuration is
|
|
764
|
+
separate from the element loader, which hands IIFE and ESM bundle loads to an
|
|
765
|
+
adapter (`preloaded` uses none). The item player's
|
|
766
|
+
[backend support](../../docs/item-player/backend-support.md) guide covers the
|
|
767
|
+
delivery backend contract.
|
|
768
|
+
|
|
769
|
+
### Section session persistence
|
|
770
|
+
|
|
771
|
+
Section state persists outside `runtime.player.backend`, which covers one
|
|
772
|
+
item's calls. A coordinator hook,
|
|
773
|
+
`ToolkitCoordinatorHooks.createSectionSessionPersistence`, returns the strategy
|
|
774
|
+
for each section controller, created once per `(assessmentId, sectionId,
|
|
775
|
+
attemptId)`; it loads and saves the `SectionControllerSessionState` that carries
|
|
776
|
+
navigation, item sessions, and formative and timed-media state.
|
|
350
777
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
- `show-toolbar` (boolean-like): accepts `true/false` and common string forms (`"true"`, `"false"`, `"1"`, `"0"`, `"yes"`, `"no"`); default `false`, so tools placed at `section` level render only when it is `true`
|
|
369
|
-
- `locale` (string, optional): BCP-47 locale for the player's own interface text. Unset renders `en-US`.
|
|
370
|
-
- `runtime.contentLanguage` (string, optional, `runtime` only): BCP-47 language of the content where its markup names none, which read-aloud speaks in and picks catalog cards by. A `lang` between the content and its card wins; unset reads `en-US`. `locale` never sets it.
|
|
371
|
-
- `nds-icons` (boolean): opt in to NDS icon buttons.
|
|
372
|
-
- `tool-config-strictness` (string, optional): `off|warn|error` for tool-config validation; default `error`.
|
|
373
|
-
- `split-pane-initial-passage-width` (number, optional): splitpane passage pane width in percent at mount. Clamped to 20–80; default 50. Splitpane only.
|
|
374
|
-
- `iife-bundle-host` (string, optional): bundle host for the IIFE element pre-warm when `runtime.player.loaderOptions.bundleHost` is unset.
|
|
375
|
-
- Host extension props (JS properties only): `toolRegistry`, `sectionHostButtons`, `itemHostButtons`, `passageHostButtons`, `hooks`
|
|
376
|
-
|
|
377
|
-
When the viewport is no wider than `narrow-layout-breakpoint` (default 1100px),
|
|
378
|
-
splitpane and vertical layout hosts normalize section toolbar placement to `top`.
|
|
379
|
-
This includes `left`, `right`, `bottom`, and `none` values. Separately, the shell
|
|
380
|
-
moves a `left` or `right` toolbar to `top` at a fixed 1100px, so with a smaller
|
|
381
|
-
breakpoint side toolbars still move to the top from 1100px down.
|
|
382
|
-
|
|
383
|
-
`hooks.cardTitleFormatter` remains active across responsive splitpane transitions (split -> stacked and stacked -> split), because title rendering is provided through shared card context rather than layout-specific state.
|
|
384
|
-
|
|
385
|
-
To opt into PIE-117 dimensions from a host, configure:
|
|
778
|
+
```ts
|
|
779
|
+
import { ToolkitCoordinator } from "@pie-players/pie-assessment-toolkit";
|
|
780
|
+
import type { SectionPlayerRuntimeConfig } from "@pie-players/pie-section-player";
|
|
781
|
+
|
|
782
|
+
const coordinator = new ToolkitCoordinator({
|
|
783
|
+
assessmentId: "practice-1",
|
|
784
|
+
hooks: {
|
|
785
|
+
createSectionSessionPersistence: () => ({
|
|
786
|
+
async loadSession({ key }) {
|
|
787
|
+
return await loadSectionSession(key.sectionId, key.attemptId);
|
|
788
|
+
},
|
|
789
|
+
async saveSession({ key }, session) {
|
|
790
|
+
await saveSectionSession(key.sectionId, key.attemptId, session);
|
|
791
|
+
},
|
|
792
|
+
}),
|
|
793
|
+
},
|
|
794
|
+
});
|
|
386
795
|
|
|
387
|
-
|
|
388
|
-
<pie-section-player-splitpane
|
|
389
|
-
content-max-width-no-passage="800"
|
|
390
|
-
content-max-width-with-passage="1200"
|
|
391
|
-
split-pane-min-region-width="280"
|
|
392
|
-
></pie-section-player-splitpane>
|
|
796
|
+
const runtime: SectionPlayerRuntimeConfig = { assessmentId: "practice-1", coordinator };
|
|
393
797
|
```
|
|
394
798
|
|
|
395
|
-
|
|
799
|
+
Without the hook, the strategy stores the session in `localStorage` under
|
|
800
|
+
`pie:section-controller:v1:{assessmentId}:{sectionId}:{attemptId}`, and only
|
|
801
|
+
when there is an attempt id. The coordinator persists a controller before it
|
|
802
|
+
disposes it. A player that builds its own coordinator installs no hooks, so a
|
|
803
|
+
host that lets it do so restores through the `session` property and saves from
|
|
804
|
+
the controller's events or `getSession()`, or sets a strategy on the handle with
|
|
805
|
+
`configureSessionPersistence()`.
|
|
396
806
|
|
|
397
|
-
|
|
807
|
+
The player does not load a section definition by identity: the host loads the
|
|
808
|
+
`AssessmentSection` and passes it in.
|
|
398
809
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
810
|
+
## Preloaded elements
|
|
811
|
+
|
|
812
|
+
With `runtime.playerType: "preloaded"` the section player loads no element
|
|
813
|
+
code. Its pre-warm asserts that every tag the section's items and passages name
|
|
814
|
+
is registered, so the host registers the elements first:
|
|
815
|
+
|
|
816
|
+
```ts
|
|
817
|
+
import { registerPreloadedElements } from "@pie-players/pie-item-player/preloaded";
|
|
818
|
+
import * as delivery from "@pie-element/multiple-choice/browser/delivery";
|
|
819
|
+
import * as controller from "@pie-element/multiple-choice/browser/controller";
|
|
820
|
+
import manifest from "../package.json"; // pins "@pie-element/multiple-choice" exactly
|
|
821
|
+
|
|
822
|
+
registerPreloadedElements(
|
|
823
|
+
[
|
|
824
|
+
{
|
|
825
|
+
tag: "pie-element-multiple-choice",
|
|
826
|
+
package: "@pie-element/multiple-choice",
|
|
827
|
+
version: manifest.dependencies["@pie-element/multiple-choice"],
|
|
828
|
+
element: delivery,
|
|
829
|
+
controller,
|
|
830
|
+
},
|
|
831
|
+
],
|
|
832
|
+
{ math: { assetRoot: "https://assets.example.com/npm" } },
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
sectionPlayer.runtime = { ...sectionPlayer.runtime, playerType: "preloaded" };
|
|
404
836
|
```
|
|
405
837
|
|
|
406
|
-
|
|
838
|
+
Register before the section player mounts: a tag missing at pre-warm leaves
|
|
839
|
+
the items unmounted and raises a non-recoverable `element-preload` framework
|
|
840
|
+
error. Item players count as hosted under `runtime.player.hosted` or an enabled
|
|
841
|
+
`runtime.player.backend.delivery`, and then need no `controller`. Version pins,
|
|
842
|
+
element sets, MathJax assets and TypeScript setup are in
|
|
843
|
+
[Registering elements from npm](../../docs/item-player/loading-strategies.md#registering-elements-from-npm).
|
|
844
|
+
|
|
845
|
+
## Styling
|
|
407
846
|
|
|
408
847
|
### Heading structure
|
|
409
848
|
|
|
410
849
|
The player publishes one number and every descendant derives its outline from it.
|
|
411
850
|
Set `base-heading-level` to the level the cards should occupy in the surrounding
|
|
412
|
-
page
|
|
851
|
+
page: 2 when the page has its own `<h1>` above the player, 3 when the player sits
|
|
413
852
|
under an `<h2>`, and so on:
|
|
414
853
|
|
|
415
854
|
```html
|
|
@@ -426,34 +865,30 @@ h2 Question 1 <- item card heading
|
|
|
426
865
|
h3 Part A <- authored data-heading content in the prompt
|
|
427
866
|
```
|
|
428
867
|
|
|
429
|
-
The two content kinds derive different levels from the same number
|
|
430
|
-
|
|
431
|
-
item
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
deeper, putting the passage's own title beneath it.
|
|
868
|
+
The two content kinds derive different levels from the same number. An item
|
|
869
|
+
card's heading is the item's heading, so the item player emits no screen-reader
|
|
870
|
+
item heading of its own: one at that level already exists, and a second would
|
|
871
|
+
read as its sibling. A passage card's heading is a group label, so the passage
|
|
872
|
+
player starts one level deeper, putting the passage's own title beneath it.
|
|
435
873
|
|
|
436
874
|
Authored `data-heading="headingN"` markup in passages and prompts becomes real
|
|
437
|
-
heading elements only when a level is published, which the player
|
|
438
|
-
|
|
439
|
-
[PIE-151](https://illuminate.atlassian.net/browse/PIE-151) therefore renders as
|
|
440
|
-
structure without any host configuration.
|
|
875
|
+
heading elements only when a level is published, which the player always does,
|
|
876
|
+
so authored heading markup renders as structure without host configuration.
|
|
441
877
|
|
|
442
|
-
A host that
|
|
443
|
-
|
|
878
|
+
A host that does not supply question headings of its own, and so needs the
|
|
879
|
+
element's screen-reader item heading, turns it on through the runtime:
|
|
444
880
|
|
|
445
881
|
```js
|
|
446
882
|
sectionPlayer.runtime = { player: { includeSrHeading: true } };
|
|
447
883
|
```
|
|
448
884
|
|
|
449
|
-
|
|
450
|
-
is
|
|
451
|
-
[`docs/architecture/composition-context.md`](../../docs/architecture/composition-context.md).
|
|
885
|
+
[Composition context](../../docs/architecture/composition-context.md) sets out
|
|
886
|
+
the pattern, and why the level is published to descendants.
|
|
452
887
|
|
|
453
888
|
### Tab styling hooks
|
|
454
889
|
|
|
455
|
-
`pie-section-player-tabbed` and splitpane `tabbed` collapse mode expose
|
|
456
|
-
hooks
|
|
890
|
+
`pie-section-player-tabbed` and the splitpane's `tabbed` collapse mode expose
|
|
891
|
+
these hooks:
|
|
457
892
|
|
|
458
893
|
- `pie-section-player-tabs`
|
|
459
894
|
- `pie-section-player-tab`
|
|
@@ -462,48 +897,43 @@ hooks for theming:
|
|
|
462
897
|
|
|
463
898
|
Tabs also carry `data-pie-purpose="passage-label"` and `data-pie-purpose="item-label"`.
|
|
464
899
|
|
|
465
|
-
Tab colors, spacing
|
|
900
|
+
Tab colors, spacing and track geometry are themed through
|
|
466
901
|
`--pie-section-player-tab-color`, `--pie-section-player-tab-background`,
|
|
467
902
|
`--pie-section-player-tab-active-color`,
|
|
468
903
|
`--pie-section-player-tab-active-background`,
|
|
469
904
|
`--pie-section-player-tab-gap`,
|
|
470
905
|
`--pie-section-player-tab-track-radius`,
|
|
471
|
-
`--pie-section-player-tab-track-padding
|
|
906
|
+
`--pie-section-player-tab-track-padding` and
|
|
472
907
|
`--pie-section-player-tab-padding-block`.
|
|
473
908
|
|
|
474
909
|
### Card header styling hooks
|
|
475
910
|
|
|
476
|
-
Passage and item cards share a
|
|
911
|
+
Passage and item cards share a header row
|
|
477
912
|
(`.pie-section-player-content-card-header`, with the card-specific aliases
|
|
478
|
-
`.pie-section-player-passage-header`
|
|
479
|
-
|
|
480
|
-
- Title and toolbar are centered vertically
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
- Card corners default to `8px
|
|
484
|
-
|
|
485
|
-
- The header fill is transparent by default.
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
-
|
|
489
|
-
card
|
|
490
|
-
`--pie-section-player-card-header-radius`.
|
|
913
|
+
`.pie-section-player-passage-header` and `.pie-section-player-item-header`).
|
|
914
|
+
|
|
915
|
+
- Title and toolbar are centered vertically. A host that needs another
|
|
916
|
+
alignment overrides the selector in its own stylesheet; there is no attribute
|
|
917
|
+
for it.
|
|
918
|
+
- Card corners default to `8px`; `--pie-section-player-card-radius` overrides
|
|
919
|
+
them.
|
|
920
|
+
- The header fill is transparent by default.
|
|
921
|
+
`--pie-section-player-card-header-background` sets a color; the framework
|
|
922
|
+
ships no brand palette.
|
|
923
|
+
- With a header fill, the header's top corners default to just inside the card
|
|
924
|
+
radius; `--pie-section-player-card-header-radius` overrides them.
|
|
491
925
|
- Under a dark theme the header takes
|
|
492
|
-
`--pie-section-player-card-header-background-dark
|
|
493
|
-
`--pie-section-player-card-header-background` when it is unset
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
picks up the same header fill without either side hardcoding the other's
|
|
504
|
-
token name.
|
|
505
|
-
|
|
506
|
-
Example (host CSS):
|
|
926
|
+
`--pie-section-player-card-header-background-dark`, falling back to
|
|
927
|
+
`--pie-section-player-card-header-background` when it is unset: the hook for
|
|
928
|
+
a brand tint that is legible on a light card and not on a dark one. A dark
|
|
929
|
+
theme is `[data-theme="dark"]` on an ancestor or `pie-theme[theme="dark"]`,
|
|
930
|
+
the selectors the theme package writes its dark tokens under. The `-dark`
|
|
931
|
+
suffix here means under a dark theme; on canonical tokens such as
|
|
932
|
+
`--pie-background-dark` it means a darker shade.
|
|
933
|
+
- `pie-section-player-passage-card` bridges `--pie-passage-header-background`
|
|
934
|
+
to `--pie-section-player-card-header-background`, so a passage player custom
|
|
935
|
+
element defined outside this package takes the same header fill without
|
|
936
|
+
either side hardcoding the other's token name.
|
|
507
937
|
|
|
508
938
|
```css
|
|
509
939
|
pie-section-player-passage-card,
|
|
@@ -515,19 +945,14 @@ pie-section-player-item-card {
|
|
|
515
945
|
}
|
|
516
946
|
```
|
|
517
947
|
|
|
518
|
-
When both max-width attributes are set, the with-passage cap resolves to the greater
|
|
519
|
-
of the two configured values (after clamp), so with-passage mode never ends up narrower
|
|
520
|
-
than no-passage mode.
|
|
521
|
-
|
|
522
948
|
### Split-pane backdrop
|
|
523
949
|
|
|
524
950
|
The split-pane layout paints a backdrop behind each scrollable pane, under the
|
|
525
951
|
passage and item cards. It reads the canonical `--pie-background-dark`, so it
|
|
526
|
-
follows the active theme and color scheme, and
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
`--pie-section-player-card-header-background`.
|
|
952
|
+
follows the active theme and color scheme, and it has no pane-specific hook: the
|
|
953
|
+
backdrop stays with the theme. The rule covers the items pane as well as the
|
|
954
|
+
passage pane, so no passage-header hook drives it. Card fills stay independent
|
|
955
|
+
through `--pie-section-player-card-header-background`.
|
|
531
956
|
|
|
532
957
|
### Content-card tool surfaces
|
|
533
958
|
|
|
@@ -536,215 +961,53 @@ Item and passage cards offer two content-scoped host surfaces:
|
|
|
536
961
|
- `content-lead` is a full-width stack before the authored player content.
|
|
537
962
|
- `content-media` is the resizable region beside that content.
|
|
538
963
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
resource gets no dead affordance.
|
|
964
|
+
Any capability that declares one of those names in `surfaces` may render there,
|
|
965
|
+
and this package names no capability. A surface becomes mountable only when
|
|
966
|
+
policy grants the capability and its own `requiresAuthoredContent` resolves, so
|
|
967
|
+
an item or passage without the authored resource gets no dead affordance.
|
|
544
968
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
969
|
+
The packaged audio transcript (`audioTranscriptRegistration` in
|
|
970
|
+
`@pie-players/pie-default-tool-loaders`) renders in `content-lead`.
|
|
971
|
+
`content-media` holds `@pie-players/pie-tool-sign-language`, a signed (ASL)
|
|
972
|
+
translation gated on the `signLanguage` PNP support; it is outside the packaged
|
|
973
|
+
set, so a deployment opts in by registering it on the tool registry it passes to
|
|
974
|
+
the player.
|
|
549
975
|
|
|
550
|
-
The `content-media`
|
|
551
|
-
keyboard-accessible divider (`role="separator"`; arrow keys,
|
|
552
|
-
`Escape` to cancel a drag). Below a card width of 560px the region
|
|
553
|
-
the content and the divider is withdrawn. Placement is fixed
|
|
554
|
-
|
|
976
|
+
The `content-media` region sits to the right of the content and is resizable
|
|
977
|
+
through a keyboard-accessible divider (`role="separator"`; arrow keys,
|
|
978
|
+
`Home`/`End`, `Escape` to cancel a drag). Below a card width of 560px the region
|
|
979
|
+
stacks under the content and the divider is withdrawn. Placement is fixed: there
|
|
980
|
+
is no orientation toggle and no free repositioning.
|
|
555
981
|
|
|
556
982
|
This package owns the region's share of the card width and nothing inside it. A
|
|
557
|
-
capability mounted here sizes its own content: signing legibility needs height
|
|
558
|
-
hands and face, so
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
All three section
|
|
565
|
-
`section-overlay`) share one
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
- Apply custom policy/gating in host code (for example, domain-specific `canNext` based on controller events like `section-items-complete-changed`)
|
|
586
|
-
- Compose forward/backward eligibility in host code using `getSnapshot().navigation` + host state; there is intentionally no separate parallel CE gating API for this
|
|
587
|
-
- Inject custom toolbar tooling with `toolRegistry` and optional host button arrays (`sectionHostButtons`, `itemHostButtons`, `passageHostButtons`)
|
|
588
|
-
- Register host callbacks via `hooks` (for example `hooks.cardTitleFormatter`)
|
|
589
|
-
|
|
590
|
-
Example:
|
|
591
|
-
|
|
592
|
-
```ts
|
|
593
|
-
const host = document.querySelector("pie-section-player-splitpane") as any;
|
|
594
|
-
host.hooks = {
|
|
595
|
-
cardTitleFormatter: (context: any) => {
|
|
596
|
-
if (context.kind === "item") {
|
|
597
|
-
return `Question ${context.itemIndex + 1}: ${context.item?.name || context.defaultTitle}`;
|
|
598
|
-
}
|
|
599
|
-
return context.passage?.name || context.defaultTitle;
|
|
600
|
-
},
|
|
601
|
-
};
|
|
602
|
-
```
|
|
603
|
-
|
|
604
|
-
Advanced runtime configuration is supplied through the `runtime` object. Set player config, tools, accessibility, coordinator, env, and `createSectionController` on `runtime.<key>`.
|
|
605
|
-
|
|
606
|
-
### Backend delivery for embedded items
|
|
607
|
-
|
|
608
|
-
Hosts can configure item-player backend delivery once at the section-player
|
|
609
|
-
runtime level. Section-player derives a concrete `backend` prop for each
|
|
610
|
-
embedded item player before it renders the item. This is intended for hosts
|
|
611
|
-
that need server-processed PIE models and server scoring without querying every
|
|
612
|
-
nested `<pie-item-player>`.
|
|
613
|
-
|
|
614
|
-
```ts
|
|
615
|
-
import type {
|
|
616
|
-
SectionPlayerRuntimeConfig,
|
|
617
|
-
} from "@pie-players/pie-section-player";
|
|
618
|
-
|
|
619
|
-
const runtime: SectionPlayerRuntimeConfig = {
|
|
620
|
-
playerType: "iife",
|
|
621
|
-
env: {
|
|
622
|
-
mode: "gather",
|
|
623
|
-
role: "student",
|
|
624
|
-
},
|
|
625
|
-
player: {
|
|
626
|
-
backend: {
|
|
627
|
-
delivery: {
|
|
628
|
-
enabled: true,
|
|
629
|
-
baseUrl: bffUrl,
|
|
630
|
-
assignmentId: playerSessionId,
|
|
631
|
-
endpoints: {
|
|
632
|
-
load: "/api/player/load",
|
|
633
|
-
saveSession: "/api/player/save",
|
|
634
|
-
model: "/api/player/model",
|
|
635
|
-
score: "/api/player/score",
|
|
636
|
-
},
|
|
637
|
-
autosave: { enabled: true, debounceMs: 250 },
|
|
638
|
-
},
|
|
639
|
-
},
|
|
640
|
-
},
|
|
641
|
-
};
|
|
642
|
-
|
|
643
|
-
sectionPlayer.runtime = runtime;
|
|
644
|
-
```
|
|
645
|
-
|
|
646
|
-
When `runtime.player.backend.delivery` is enabled, section-player treats
|
|
647
|
-
`itemId` and `sessionId` as per-item delivery identity. It derives them from
|
|
648
|
-
`canonicalItemId || item.id` and the item session before forwarding `backend` to
|
|
649
|
-
each embedded item player. Static delivery fields such as `baseUrl`, `auth`,
|
|
650
|
-
`endpoints`, `assignmentId`, and `autosave` are preserved. Use `assignmentId`
|
|
651
|
-
for shared attempt/player identity.
|
|
652
|
-
|
|
653
|
-
An enabled `backend.delivery` also sets `hosted: true` on each embedded item
|
|
654
|
-
player unless `runtime.player.hosted` is set, so the item players load no
|
|
655
|
-
element controllers and render the models the server returns.
|
|
656
|
-
|
|
657
|
-
`runtime.player.resolveBackend` is a section-player-reserved key. It is called
|
|
658
|
-
with `{ itemId, canonicalItemId, item, itemIndex, itemSession, sectionId, env,
|
|
659
|
-
baseBackend }` and is stripped before props reach `<pie-item-player>`. Use it
|
|
660
|
-
only when the backend needs custom per-item identity mapping. The resolver
|
|
661
|
-
receives cloned backend objects, so per-item identity changes do not mutate the
|
|
662
|
-
shared runtime configuration or leak across items.
|
|
663
|
-
|
|
664
|
-
Section-player only derives the concrete `backend` prop. It does not call
|
|
665
|
-
`loadFromBackend()` on nested item players. Embedded `<pie-item-player>` loads
|
|
666
|
-
automatically when its derived `backend.delivery` config has a load signature,
|
|
667
|
-
so every mounted item player issues one backend load for its own item. Passage
|
|
668
|
-
players do not receive item delivery backend config, but shared non-delivery
|
|
669
|
-
backend config is preserved.
|
|
670
|
-
|
|
671
|
-
This backend delivery config is separate from the element-loader backend used
|
|
672
|
-
for IIFE/ESM bundle preloading.
|
|
673
|
-
|
|
674
|
-
### Preloaded elements
|
|
675
|
-
|
|
676
|
-
With `runtime.playerType: "preloaded"` the section player loads no element
|
|
677
|
-
code. Its pre-warm asserts that every tag the section's items and passages name
|
|
678
|
-
is registered, so the host registers the elements first:
|
|
679
|
-
|
|
680
|
-
```ts
|
|
681
|
-
import { registerPreloadedElements } from "@pie-players/pie-item-player/preloaded";
|
|
682
|
-
import * as delivery from "@pie-element/multiple-choice/browser/delivery";
|
|
683
|
-
import * as controller from "@pie-element/multiple-choice/browser/controller";
|
|
684
|
-
import manifest from "../package.json"; // pins "@pie-element/multiple-choice" exactly
|
|
685
|
-
|
|
686
|
-
registerPreloadedElements(
|
|
687
|
-
[
|
|
688
|
-
{
|
|
689
|
-
tag: "pie-element-multiple-choice",
|
|
690
|
-
package: "@pie-element/multiple-choice",
|
|
691
|
-
version: manifest.dependencies["@pie-element/multiple-choice"],
|
|
692
|
-
element: delivery,
|
|
693
|
-
controller,
|
|
694
|
-
},
|
|
695
|
-
],
|
|
696
|
-
{ math: { assetRoot: "https://assets.example.com/npm" } },
|
|
697
|
-
);
|
|
698
|
-
|
|
699
|
-
sectionPlayer.runtime = { ...sectionPlayer.runtime, playerType: "preloaded" };
|
|
700
|
-
```
|
|
701
|
-
|
|
702
|
-
- Register before the section player mounts. A tag missing at pre-warm leaves
|
|
703
|
-
the items unmounted and raises a non-recoverable `element-preload` framework
|
|
704
|
-
error.
|
|
705
|
-
- Install element packages with `npm install --save-exact`. npm otherwise saves
|
|
706
|
-
a caret range, which registration rejects as a `version`, and which a fresh
|
|
707
|
-
install can resolve to another release line: `^13.4.0-next.15` resolves to
|
|
708
|
-
the legacy `13.4.4`, which has no `./browser/*` modules.
|
|
709
|
-
- Install every pie-elements-ng package from one release, in one install from
|
|
710
|
-
the same dist-tag, and upgrade them together. Elements whose `./browser/*`
|
|
711
|
-
builds typeset on `window.MathJax` share the MathJax the first of them loads,
|
|
712
|
-
in the build and configuration of that element's release, so in a mixed set
|
|
713
|
-
an element can typeset with a MathJax it was not built for. Elements that
|
|
714
|
-
bundle their own MathJax share none
|
|
715
|
-
([One MathJax version per page](../../docs/item-player/loading-strategies.md#one-mathjax-version-per-page)).
|
|
716
|
-
- Pass `math.assetRoot`, an npm root serving the fonts and speech the elements'
|
|
717
|
-
bundled MathJax loads, or `math.assetUrls`, each file's URL; without either,
|
|
718
|
-
elements on adapter 0.1.3 or later render without web fonts and speech
|
|
719
|
-
([MathJax assets](../../docs/item-player/loading-strategies.md#mathjax-assets)).
|
|
720
|
-
- Register one version per package. The players align every authored version
|
|
721
|
-
of a package to the registered one, and registering a second version throws.
|
|
722
|
-
- Register each package's `controller` unless the item players are hosted
|
|
723
|
-
(`runtime.player.hosted`, or an enabled `runtime.player.backend.delivery`).
|
|
724
|
-
A player that is not hosted runs `model()` in the browser and warns for each
|
|
725
|
-
tag registered without one.
|
|
726
|
-
- Only `@pie-element/*` builds from pie-elements-ng publish
|
|
727
|
-
`./browser/delivery` and `./browser/controller`.
|
|
728
|
-
- Under TypeScript, the `package.json` import needs `resolveJsonModule`, and a
|
|
729
|
-
package version that ships no declarations for `./browser/*` needs a
|
|
730
|
-
`declare module` shim for those subpaths.
|
|
731
|
-
|
|
732
|
-
See [`strategy="preloaded"`](../../docs/item-player/loading-strategies.md#strategypreloaded)
|
|
733
|
-
for the registration contract.
|
|
734
|
-
|
|
735
|
-
### Host-owned focus
|
|
736
|
-
|
|
737
|
-
Section-player does not move focus on behalf of host-level affordances such
|
|
738
|
-
as "Skip to Main", nor does it make passage/question containers tab stops.
|
|
739
|
-
Hosts own page chrome, skip links, landmarks, and any special focus placement.
|
|
740
|
-
For example, a host shell can focus its own
|
|
741
|
-
`main#main-content`; the next Tab then follows the browser's natural order
|
|
742
|
-
into the first actionable control rendered inside the section player.
|
|
743
|
-
|
|
744
|
-
The passage and item card custom elements are content/layout surfaces, not
|
|
745
|
-
public focus targets. Splitpane passage content remains scrollable through the
|
|
746
|
-
pane's native scroll behavior, but the passage pane itself is not inserted into
|
|
747
|
-
sequential keyboard navigation.
|
|
983
|
+
capability mounted here sizes its own content: signing legibility needs height
|
|
984
|
+
for hands and face, so the sign-language tool sizes by an aspect-ratio target
|
|
985
|
+
with a height floor. The `--pie-section-player-item-media-*` tokens that size it
|
|
986
|
+
belong to `@pie-players/pie-tool-sign-language` and are documented with their
|
|
987
|
+
defaults in [its README](../tool-sign-language/README.md); they keep the
|
|
988
|
+
`pie-section-player` prefix because hosts already set them by those names.
|
|
989
|
+
|
|
990
|
+
All three section player surfaces (`content-lead`, `content-media` and
|
|
991
|
+
`section-overlay`) share one tool surface host. It observes live `ToolRegistry`
|
|
992
|
+
mutations and policy and catalog changes, keeps registration order across lazy
|
|
993
|
+
loads, synchronizes an existing element when its context changes, and always
|
|
994
|
+
calls `destroy()` before removing it. Surface failures are isolated per
|
|
995
|
+
capability and emitted as recoverable `framework-error` warnings; they never
|
|
996
|
+
block readiness or remove another working capability. A `renderSurface()` result
|
|
997
|
+
of `null` means mountable and unoccupied.
|
|
998
|
+
|
|
999
|
+
## Host-owned focus
|
|
1000
|
+
|
|
1001
|
+
The section player does not move focus on behalf of host affordances such as
|
|
1002
|
+
"Skip to Main", and does not make passage or question containers tab stops.
|
|
1003
|
+
Hosts own page chrome, skip links, landmarks and special focus placement. A host
|
|
1004
|
+
shell can focus its own `main#main-content`, and the next Tab follows the
|
|
1005
|
+
browser's natural order into the first actionable control in the section
|
|
1006
|
+
player.
|
|
1007
|
+
|
|
1008
|
+
The passage and item cards are content surfaces and not focus targets. Splitpane
|
|
1009
|
+
passage content scrolls through the pane's native scrolling, and the passage
|
|
1010
|
+
pane itself is not in the sequential keyboard order.
|
|
748
1011
|
|
|
749
1012
|
```html
|
|
750
1013
|
<a href="#main-content" class="skip-link">Skip to Main</a>
|
|
@@ -753,251 +1016,13 @@ sequential keyboard navigation.
|
|
|
753
1016
|
</main>
|
|
754
1017
|
```
|
|
755
1018
|
|
|
756
|
-
|
|
757
|
-
unset field, a missing section included, takes its value from
|
|
758
|
-
`DEFAULT_SECTION_PLAYER_POLICIES`.
|
|
759
|
-
|
|
760
|
-
- `readiness.mode` (`"progressive"` | `"strict"`) — the mode the layout
|
|
761
|
-
kernel gates readiness by. Both modes
|
|
762
|
-
hold the `interactive` stage and `pie-loading-complete` until the section's
|
|
763
|
-
element pre-warm resolves and the item cards can mount. The kernel has no
|
|
764
|
-
later loading signal, so the two modes currently emit the same sequence.
|
|
765
|
-
Default: `"progressive"`.
|
|
766
|
-
- `preload.enabled` — when `false`, `SectionItemsPane` short-circuits the
|
|
767
|
-
section-level element warmup pipeline (`warmupSectionElements`). Items
|
|
768
|
-
still mount and item-players register their own elements on demand. Use
|
|
769
|
-
this to disable section pre-warm when the host already owns element
|
|
770
|
-
registration end-to-end. Default: `true`.
|
|
771
|
-
- `telemetry.enabled` — when `false`, the layout custom elements skip
|
|
772
|
-
`attachInstrumentationEventBridge` setup, so no `pie-section-*`
|
|
773
|
-
telemetry events flow through the bridge. Hosts that want a different
|
|
774
|
-
shape of opt-out can still override `runtime.player.loaderConfig.instrumentationProvider`.
|
|
775
|
-
Default: `true`.
|
|
776
|
-
|
|
777
|
-
`resolveSectionPlayerPolicies(policies)` returns the filled-in object, for
|
|
778
|
-
host code that needs the same gates.
|
|
779
|
-
|
|
780
|
-
### Navigation signals
|
|
781
|
-
|
|
782
|
-
- `item-selected`: item-level navigation change within the current section in the `SectionController` broadcast stream (`itemIndex`, `currentItemId`, `totalItems`).
|
|
783
|
-
- `section-navigation-change`: section-level navigation/selection change in the `SectionController` broadcast stream (`previousSectionId`, `currentSectionId`, `reason`).
|
|
784
|
-
|
|
785
|
-
Runtime configuration is explicit:
|
|
786
|
-
|
|
787
|
-
- `runtime` owns runtime fields (`assessmentId`, `playerType`, `player`, `lazyInit`, `tools`, `toolContextResolvers`, `accessibility`, `coordinator`, `createSectionController`, `isolation`, `env`, `contentLanguage` and the `on*` callbacks). `locale`, `nds-icons` and `tool-config-strictness` are attributes only.
|
|
788
|
-
- Tool placement is configured through `runtime.tools.placement.section`, `runtime.tools.placement.item`, and `runtime.tools.placement.passage`.
|
|
789
|
-
- Tool configuration validation is canonical in toolkit initialization (`pie-assessment-toolkit`), including toolbar overlays. Use the `tool-config-strictness` attribute (`off` | `warn` | `error`) to control warning-only vs fail-fast behavior.
|
|
790
|
-
- TTS provider config must use `tools.providers.textToSpeech` (canonical). `tools.providers.tts` is rejected by validation.
|
|
791
|
-
- Host tool overrides:
|
|
792
|
-
- `toolRegistry` replaces the default toolbar registry when provided. Build it with `createPackagedToolRegistry({ toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS })` and register custom tools on it, since toolbars load each tool's element through the registry's loaders. A player that builds its own coordinator gives it this registry; a coordinator passed as `runtime.coordinator` keeps its own, which decides policy, so build that coordinator with the same registry
|
|
793
|
-
- host buttons are appended per toolbar scope via `sectionHostButtons`, `itemHostButtons`, `passageHostButtons`
|
|
794
|
-
|
|
795
|
-
Debug logging is page-wide. A layout's `debug` attribute writes `window.PIE_DEBUG`, the flag every PIE logger on the page reads, so the last host to set it decides for all of them:
|
|
796
|
-
|
|
797
|
-
- Enable: `<pie-section-player-splitpane debug="true">`
|
|
798
|
-
- Disable: `<pie-section-player-splitpane debug="false">` (or `debug="0"`)
|
|
799
|
-
|
|
800
|
-
Without a `debug` attribute a layout follows `window.PIE_DEBUG`, which a host can set directly.
|
|
801
|
-
|
|
802
|
-
See the progressive demo routes in `apps/section-demos/src/routes/(demos)` (for example `single-question/+page.svelte` and `session-hydrate-db/+page.svelte`) for end-to-end host integrations.
|
|
803
|
-
|
|
804
|
-
## Data flow and stability guarantees
|
|
805
|
-
|
|
806
|
-
Section-player follows a unidirectional flow model:
|
|
807
|
-
|
|
808
|
-
- Inputs flow downward (`runtime`, `section`, `env`, toolbar options) into base/toolkit/layout/card render paths.
|
|
809
|
-
- State updates flow upward as events (`runtime-*`, `session-changed`, controller change events) and are reconciled by runtime owners.
|
|
810
|
-
- Layout/card components should not create competing sources of truth for composition/session.
|
|
811
|
-
|
|
812
|
-
### Stability guarantees
|
|
813
|
-
|
|
814
|
-
For non-structural updates, section-player guarantees behavior stability:
|
|
815
|
-
|
|
816
|
-
- Item/passage shell identity remains stable (no remount churn for response-only updates).
|
|
817
|
-
- Pane-local scroll position remains stable in splitpane and vertical layouts.
|
|
818
|
-
|
|
819
|
-
Non-structural updates include:
|
|
1019
|
+
## Instrumentation
|
|
820
1020
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
## Custom layout authoring
|
|
828
|
-
|
|
829
|
-
A host builds its own section layout from `pie-section-player-kernel-host` and the
|
|
830
|
-
two panes. The kernel host runs the section: toolkit, section controller,
|
|
831
|
-
readiness, element pre-warm and the section toolbar. Its element children are the
|
|
832
|
-
layout, and the panes inside them render the section's items and passages. The
|
|
833
|
-
stock layouts are built from the same panes.
|
|
834
|
-
|
|
835
|
-
```html
|
|
836
|
-
<!doctype html>
|
|
837
|
-
<html lang="en">
|
|
838
|
-
<head>
|
|
839
|
-
<meta charset="utf-8" />
|
|
840
|
-
<style>
|
|
841
|
-
body { margin: 0; }
|
|
842
|
-
pie-section-player-kernel-host { display: block; height: 100vh; }
|
|
843
|
-
.columns { display: grid; grid-template-columns: 3fr 2fr; height: 100%; }
|
|
844
|
-
.column { min-height: 0; overflow: auto; }
|
|
845
|
-
</style>
|
|
846
|
-
<script type="module">
|
|
847
|
-
import "https://cdn.jsdelivr.net/npm/@pie-players/pie-section-player@x.y.z/dist/browser/pie-section-player.js";
|
|
848
|
-
|
|
849
|
-
const player = document.querySelector("pie-section-player-kernel-host");
|
|
850
|
-
player.addEventListener("pie-loading-complete", () => {
|
|
851
|
-
console.log("ready", player.getSnapshot().navigation);
|
|
852
|
-
});
|
|
853
|
-
player.runtime = { env: { mode: "gather", role: "student" } };
|
|
854
|
-
player.section = {
|
|
855
|
-
identifier: "water-cycle",
|
|
856
|
-
rubricBlocks: [
|
|
857
|
-
{
|
|
858
|
-
identifier: "passage-1",
|
|
859
|
-
class: "stimulus",
|
|
860
|
-
view: ["candidate"],
|
|
861
|
-
passage: {
|
|
862
|
-
id: "passage-1",
|
|
863
|
-
config: {
|
|
864
|
-
markup: "<p>Water evaporates, condenses into clouds and falls as rain.</p>",
|
|
865
|
-
elements: {},
|
|
866
|
-
models: [],
|
|
867
|
-
},
|
|
868
|
-
},
|
|
869
|
-
},
|
|
870
|
-
],
|
|
871
|
-
assessmentItemRefs: [
|
|
872
|
-
{
|
|
873
|
-
identifier: "item-1",
|
|
874
|
-
item: {
|
|
875
|
-
id: "item-1",
|
|
876
|
-
config: {
|
|
877
|
-
markup: "<p>Name the stage in which water vapour forms clouds.</p>",
|
|
878
|
-
elements: {},
|
|
879
|
-
models: [],
|
|
880
|
-
},
|
|
881
|
-
},
|
|
882
|
-
},
|
|
883
|
-
],
|
|
884
|
-
};
|
|
885
|
-
</script>
|
|
886
|
-
</head>
|
|
887
|
-
<body>
|
|
888
|
-
<pie-section-player-kernel-host section-id="water-cycle" attempt-id="attempt-1">
|
|
889
|
-
<div class="columns">
|
|
890
|
-
<div class="column"><pie-section-player-items-pane></pie-section-player-items-pane></div>
|
|
891
|
-
<div class="column"><pie-section-player-passages-pane></pie-section-player-passages-pane></div>
|
|
892
|
-
</div>
|
|
893
|
-
</pie-section-player-kernel-host>
|
|
894
|
-
</body>
|
|
895
|
-
</html>
|
|
896
|
-
```
|
|
897
|
-
|
|
898
|
-
The example loads the [browser build](../../docs/setup/cdn_usage.md#section-player-browser-build).
|
|
899
|
-
A bundled host imports `@pie-players/pie-section-player`, which defines the
|
|
900
|
-
kernel host and the panes too. An item with PIE elements names them in
|
|
901
|
-
`config.elements` and carries their `config.models`, as in any section. The
|
|
902
|
-
`/custom-layout` route of `apps/section-demos`
|
|
903
|
-
([source](../../apps/section-demos/src/routes/%28demos%29/custom-layout/+page.svelte))
|
|
904
|
-
builds the same two columns in Svelte.
|
|
905
|
-
|
|
906
|
-
### Kernel host
|
|
907
|
-
|
|
908
|
-
- **Inputs and host methods.** Those of the layout elements in
|
|
909
|
-
[Runtime Inputs](#runtime-inputs), without the layout dimensions
|
|
910
|
-
(`narrow-layout-breakpoint`, `content-max-width-*`, `split-pane-*`).
|
|
911
|
-
- **DOM.** The open shadow root holds the toolkit and the section toolbar around
|
|
912
|
-
one default slot; `show-toolbar` and `toolbar-position` place the toolbar around
|
|
913
|
-
the layout. The children, the panes and the content they render stay in light
|
|
914
|
-
DOM, where page styles and the content stylesheet reach them. The element has
|
|
915
|
-
no styles of its own, so the host gives it `display` and a height.
|
|
916
|
-
- **Stock body.** With no element children it renders its own layout, the
|
|
917
|
-
passages pane above the items pane, and places the passages pane only for a
|
|
918
|
-
section with passages. The stock body leaves when the first element child
|
|
919
|
-
arrives and returns when the last one leaves. Text and comment children do not
|
|
920
|
-
count, so markup whitespace and a framework's placeholder comments leave it in
|
|
921
|
-
place.
|
|
922
|
-
- **Navigation and state.** The host methods (`navigateNext`, `navigatePrevious`,
|
|
923
|
-
`navigateTo`, `getSnapshot`, `getSectionController`, `waitForSectionController`)
|
|
924
|
-
and the events (`pie-stage-change`, `pie-loading-complete`,
|
|
925
|
-
`composition-changed`, `session-changed`, `framework-error`, `toolkit-ready`)
|
|
926
|
-
are those of the stock layouts. `detail.sourceCe` on its `pie-stage-change`
|
|
927
|
-
and `pie-loading-complete` events reads `pie-section-player-kernel-host`.
|
|
928
|
-
|
|
929
|
-
### Panes
|
|
930
|
-
|
|
931
|
-
`<pie-section-player-items-pane>` renders the item cards of the current
|
|
932
|
-
composition, with their toolbars, and runs the element pre-warm;
|
|
933
|
-
`<pie-section-player-passages-pane>` renders the passage cards. A pane takes no
|
|
934
|
-
attributes or properties, and its pre-warm failures arrive as the section
|
|
935
|
-
player's `framework-error` and `element-preload-error` events. It reads the
|
|
936
|
-
section player it belongs to from a context the kernel publishes, so it renders
|
|
937
|
-
at any depth below the kernel host, and outside a section player it renders
|
|
938
|
-
nothing.
|
|
939
|
-
|
|
940
|
-
- **One pane of each kind renders**: the first connected. A second pane of a
|
|
941
|
-
kind renders nothing and takes over when the first disconnects; the player
|
|
942
|
-
reports the duplicate once in the console.
|
|
943
|
-
- **Readiness follows the rendering items pane.** `interactive` and
|
|
944
|
-
`pie-loading-complete` wait for its element pre-warm, and reports from any
|
|
945
|
-
other pane are ignored. A section with items and no items pane never reaches
|
|
946
|
-
either; the player reports it once in the console a task after
|
|
947
|
-
`section-ready`.
|
|
948
|
-
- **The passages pane is optional.** A section without passages needs none, and
|
|
949
|
-
readiness does not wait for it.
|
|
950
|
-
- **Scrolling belongs to the layout.** The items pane's scroll hint follows the
|
|
951
|
-
nearest ancestor with `overflow-y: auto` or `scroll`, so the layout gives each
|
|
952
|
-
pane's container that overflow and a bounded height. The stock layouts' pane backdrops and margins
|
|
953
|
-
stay with those layouts; the cards keep their tags and
|
|
954
|
-
[styling hooks](#card-header-styling-hooks).
|
|
955
|
-
|
|
956
|
-
### JS API example for advanced host policy
|
|
957
|
-
|
|
958
|
-
```ts
|
|
959
|
-
const host = document.querySelector("pie-section-player-splitpane") as any;
|
|
960
|
-
const controller = await host.waitForSectionController(5000);
|
|
961
|
-
let sectionComplete = false;
|
|
962
|
-
|
|
963
|
-
const unsubscribe = controller?.subscribe?.((event: any) => {
|
|
964
|
-
if (event?.type === "section-items-complete-changed") {
|
|
965
|
-
sectionComplete = event.complete === true;
|
|
966
|
-
}
|
|
967
|
-
});
|
|
968
|
-
|
|
969
|
-
function canAdvance() {
|
|
970
|
-
const nav = host.getSnapshot?.()?.navigation;
|
|
971
|
-
return Boolean(nav?.canNext && sectionComplete);
|
|
972
|
-
}
|
|
973
|
-
```
|
|
974
|
-
|
|
975
|
-
If you already have a `ToolkitCoordinator`, prefer helper subscriptions for host logic. Subscriptions follow the toolkit's active section cohort automatically — a single subscribe call survives navigation:
|
|
976
|
-
|
|
977
|
-
```ts
|
|
978
|
-
const unsubscribeItem = coordinator.subscribeItemEvents({
|
|
979
|
-
listener: (event: any) => {
|
|
980
|
-
// item-scoped stream
|
|
981
|
-
},
|
|
982
|
-
});
|
|
983
|
-
|
|
984
|
-
const unsubscribeSection = coordinator.subscribeSectionLifecycleEvents({
|
|
985
|
-
listener: (event: any) => {
|
|
986
|
-
// section-loading-complete / section-items-complete-changed / section-error / section-navigation-change
|
|
987
|
-
},
|
|
988
|
-
});
|
|
989
|
-
```
|
|
990
|
-
|
|
991
|
-
Subscribe **after** the first `getOrCreateSectionController(...)` resolves (or after `toolkit-ready` once the section player has fully wired its controller — typically the safest anchor in host code is `toolkit-ready` followed by the first controller-resolve). Calling subscribe before the first `getOrCreateSectionController(...)` call throws; a listener added while a section is starting binds when that section becomes active.
|
|
992
|
-
|
|
993
|
-
Use `subscribeSectionEvents(...)` only for advanced mixed filtering requirements.
|
|
994
|
-
|
|
995
|
-
> **Upgrading from `<0.3.35`?** The `sectionId` / `attemptId` arguments on `subscribeItemEvents` / `subscribeSectionLifecycleEvents` / `subscribeSectionEvents` were dropped — subscriptions now follow the toolkit's active section cohort automatically and migrate across navigation. See the **"Migrating from `<0.3.35`"** section in [`@pie-players/pie-assessment-toolkit`](../assessment-toolkit/README.md#migrating-from-0335-breaking--pre-10) for the full upgrade recipe.
|
|
996
|
-
|
|
997
|
-
### Item-level observability configuration
|
|
998
|
-
|
|
999
|
-
Item-level resource observability is configured on the embedded `pie-item-player` via
|
|
1000
|
-
`loaderConfig`. In section-player integrations, pass this through `runtime.player.loaderConfig`.
|
|
1021
|
+
Instrumentation goes through the shared `InstrumentationProvider` contract, set
|
|
1022
|
+
at `runtime.player.loaderConfig.instrumentationProvider`. The embedded item
|
|
1023
|
+
players use the same `loaderConfig` for resource monitoring; `loaderOptions`
|
|
1024
|
+
controls bundle loading. A provider is a JavaScript object, so it is set on the
|
|
1025
|
+
`runtime` property.
|
|
1001
1026
|
|
|
1002
1027
|
```ts
|
|
1003
1028
|
import { ConsoleInstrumentationProvider } from "@pie-players/pie-players-shared";
|
|
@@ -1021,212 +1046,68 @@ sectionPlayerEl.runtime = {
|
|
|
1021
1046
|
};
|
|
1022
1047
|
```
|
|
1023
1048
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
`
|
|
1033
|
-
|
|
1034
|
-
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
Canonical lifecycle stream (engine-routed, dispatched on the outer layout CE,
|
|
1044
|
-
bubbling and composed):
|
|
1045
|
-
|
|
1046
|
-
- `pie-stage-change` — single typed transition stream covering
|
|
1047
|
-
`composed` → `engine-ready` → `interactive` → `disposed`. Payload is a
|
|
1048
|
-
`StageChangeDetail`. A non-recoverable framework error before
|
|
1049
|
-
`interactive` emits the current stage `failed` and each stage it never
|
|
1050
|
-
reached `skipped`.
|
|
1051
|
-
- `pie-loading-complete` — fires once per cohort, when the section's element
|
|
1052
|
-
pre-warm resolves for the current composition and the item cards can mount
|
|
1053
|
-
(kernel-routed).
|
|
1054
|
-
- `framework-error` — canonical error event for any failure crossing the
|
|
1055
|
-
framework boundary. Payload is a `FrameworkErrorModel`. The toolkit
|
|
1056
|
-
dispatches it once per error, bubbling and composed, so it reaches the
|
|
1057
|
-
layout CE and `document`; errors from a coordinator the host passes as
|
|
1058
|
-
`runtime.coordinator` arrive the same way.
|
|
1059
|
-
`tests/section-player-event-delivery.spec.ts` pins these counts.
|
|
1060
|
-
|
|
1061
|
-
Callbacks on `runtime`:
|
|
1062
|
-
|
|
1063
|
-
- `runtime.onStageChange(detail)` and `runtime.onLoadingComplete(detail)`.
|
|
1064
|
-
- `runtime.onFrameworkError(model)` fires once per error regardless of
|
|
1065
|
-
wrapper depth, like the `framework-error` DOM event; consume either.
|
|
1066
|
-
|
|
1067
|
-
Section-player owned instrumentation stream:
|
|
1068
|
-
|
|
1069
|
-
- `pie-section-stage-change`
|
|
1070
|
-
- `pie-section-loading-complete`
|
|
1071
|
-
- `pie-section-framework-error`
|
|
1072
|
-
- `pie-section-element-preload-retry`
|
|
1073
|
-
- `pie-section-element-preload-error`
|
|
1074
|
-
|
|
1075
|
-
Build consumers against these canonical lifecycle events:
|
|
1076
|
-
|
|
1077
|
-
- `readiness-change` → listen for `pie-stage-change`.
|
|
1078
|
-
- `interaction-ready` → `pie-stage-change` filtered on
|
|
1079
|
-
`detail.stage === "interactive"`.
|
|
1080
|
-
- `ready` → `pie-loading-complete`.
|
|
1081
|
-
- `section-controller-ready` → call
|
|
1082
|
-
`waitForSectionController(timeoutMs)` or `getSectionController()`
|
|
1083
|
-
on the layout CE, or filter `pie-stage-change` for
|
|
1084
|
-
`detail.stage === "engine-ready"`.
|
|
1085
|
-
|
|
1086
|
-
If toolkit is mounted, toolkit lifecycle events are emitted on a separate
|
|
1087
|
-
`pie-toolkit-*` stream. This separation avoids semantic overlap; bridge dedupe
|
|
1088
|
-
is a defensive safety net only.
|
|
1089
|
-
|
|
1090
|
-
Toolkit tool/backend operational stream:
|
|
1091
|
-
|
|
1092
|
-
- `pie-tool-init-start|success|error`
|
|
1093
|
-
- `pie-tool-backend-call-start|success|error`
|
|
1094
|
-
- `pie-tool-library-load-start|success|error`
|
|
1095
|
-
|
|
1096
|
-
### Item session management
|
|
1097
|
-
|
|
1098
|
-
Section session data can be managed either through persistence hooks or directly through the controller API.
|
|
1099
|
-
|
|
1100
|
-
```ts
|
|
1101
|
-
const host = document.querySelector("pie-section-player-splitpane") as any;
|
|
1102
|
-
const controller = await host.waitForSectionController(5000);
|
|
1103
|
-
|
|
1104
|
-
// Read current section session snapshot.
|
|
1105
|
-
const currentSession = controller?.getSession?.();
|
|
1106
|
-
|
|
1107
|
-
// Replace section session state (resume from backend snapshot).
|
|
1108
|
-
await controller?.applySession?.({
|
|
1109
|
-
currentItemIndex: 0,
|
|
1110
|
-
visitedItemIdentifiers: ["q1"],
|
|
1111
|
-
itemSessions: {
|
|
1112
|
-
q1: {
|
|
1113
|
-
itemIdentifier: "q1",
|
|
1114
|
-
pieSessionId: "q1-session",
|
|
1115
|
-
session: { id: "q1-session", data: [{ id: "choice", value: "a" }] }
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
}, { mode: "replace" });
|
|
1119
|
-
|
|
1120
|
-
// Update a single item session directly.
|
|
1121
|
-
await controller?.updateItemSession?.("q1", {
|
|
1122
|
-
session: { id: "q1-session", data: [{ id: "choice", value: "b" }] },
|
|
1123
|
-
complete: true,
|
|
1124
|
-
});
|
|
1125
|
-
```
|
|
1126
|
-
|
|
1127
|
-
The same controller snapshot is what the persistence strategy saves/loads, and
|
|
1128
|
-
what the layout's `session` property takes.
|
|
1129
|
-
When a controller is reused for the same `sectionId`/`attemptId`, `updateInput()` refreshes composition input while preserving in-memory section session data.
|
|
1130
|
-
|
|
1131
|
-
### One item as a section
|
|
1132
|
-
|
|
1133
|
-
`sectionFromItem` wraps one item config, in the shape `<pie-item-player config>`
|
|
1134
|
-
takes, and optionally its session, in the `section` and `session` the layouts
|
|
1135
|
-
take. It is exported from the package root and from
|
|
1136
|
-
`@pie-players/pie-section-player/item-section`, which defines no custom element
|
|
1137
|
-
and imports in Node.
|
|
1138
|
-
|
|
1139
|
-
```ts
|
|
1140
|
-
import { sectionFromItem } from "@pie-players/pie-section-player/item-section";
|
|
1141
|
-
|
|
1142
|
-
const { section, session } = sectionFromItem(itemConfig, { session: itemSession });
|
|
1143
|
-
player.setAttribute("section-id", section.identifier);
|
|
1144
|
-
player.section = section;
|
|
1145
|
-
player.session = session;
|
|
1146
|
-
```
|
|
1147
|
-
|
|
1148
|
-
The item ref's `identifier` and the item's `id` are the config's `id`, so every
|
|
1149
|
-
`itemId` the section reports is the id the host already holds. An advanced
|
|
1150
|
-
config's `passage` becomes the item's passage, and its `instructorResources` and
|
|
1151
|
-
`defaultExtraModels` stay on the item's config. The section carries no `baseId`
|
|
1152
|
-
or `version`; `options.sectionId` names the section, which defaults to the
|
|
1153
|
-
config's `id`.
|
|
1154
|
-
|
|
1155
|
-
### Commit at a section boundary
|
|
1049
|
+
[Instrumentation providers](../../docs/architecture/instrumentation-providers.md#provider-resolution)
|
|
1050
|
+
sets out how an unset, `null` or invalid provider resolves. Local debug overlays
|
|
1051
|
+
compose providers (for example `NewRelicInstrumentationProvider` and
|
|
1052
|
+
`DebugPanelInstrumentationProvider`) through `CompositeInstrumentationProvider`.
|
|
1053
|
+
|
|
1054
|
+
The provider receives the section player's own stream (`pie-section-stage-change`,
|
|
1055
|
+
`pie-section-loading-complete`, `pie-section-framework-error`,
|
|
1056
|
+
`pie-section-element-preload-retry`, `pie-section-element-preload-error`), which
|
|
1057
|
+
`policies.telemetry.enabled: false` stops, and the toolkit's separate
|
|
1058
|
+
`pie-toolkit-*` stream with its tool and backend
|
|
1059
|
+
[operational events](../../docs/architecture/instrumentation-providers.md#operational-events).
|
|
1060
|
+
The two streams do not overlap.
|
|
1061
|
+
|
|
1062
|
+
`runtime.onStageChange(detail)` and `runtime.onLoadingComplete(detail)` receive
|
|
1063
|
+
the `pie-stage-change` and `pie-loading-complete` details.
|
|
1064
|
+
`runtime.onFrameworkError(model)` fires once per error whatever the wrapper
|
|
1065
|
+
depth, like the `framework-error` DOM event; consume either. Errors from a
|
|
1066
|
+
coordinator passed as `runtime.coordinator` arrive the same way.
|
|
1156
1067
|
|
|
1157
|
-
|
|
1158
|
-
learner has finished entering can still be pending when the section moves. The
|
|
1159
|
-
controller commits pending element sessions before item navigation, before
|
|
1160
|
-
`updateInput()` snapshots the session for a section swap, and before `persist()`.
|
|
1161
|
-
The player also commits when an item shell tears down and when the page goes
|
|
1162
|
-
hidden.
|
|
1163
|
-
|
|
1164
|
-
A committed response travels like any other. A raw element `session-changed`
|
|
1165
|
-
does not leave its `<pie-item-scope>`, which re-dispatches it as the normalized
|
|
1166
|
-
`item-session-changed` (`PIE_ITEM_SESSION_CHANGED_EVENT`); the toolkit then
|
|
1167
|
-
publishes the section's canonical `session-changed`. Both bubble through the
|
|
1168
|
-
layout element to `document`, and a listener on either receives each dispatch
|
|
1169
|
-
once.
|
|
1170
|
-
Host code persists from the controller's events or from its session snapshot.
|
|
1171
|
-
|
|
1172
|
-
Each of those events marks a commit with `sessionCommitReason`
|
|
1173
|
-
(`"teardown" | "navigate" | "page-hidden"`): `item-session-changed`, the
|
|
1174
|
-
toolkit's `session-changed`, and the controller's `item-session-data-changed`
|
|
1175
|
-
and `item-session-meta-changed`. A commit at a section swap or item navigation
|
|
1176
|
-
reports the item being left, which the host may already have moved past, so a
|
|
1177
|
-
handler that sets its current item or navigation state from these events leaves
|
|
1178
|
-
that state alone on a commit and persists the commit's session as usual. The
|
|
1179
|
-
controller's events also carry `sectionId`, the section the item belongs to.
|
|
1180
|
-
|
|
1181
|
-
Navigation inside a section keeps every item mounted, so nothing is discarded
|
|
1182
|
-
and the element's own debounce would complete on its own. The commit still runs
|
|
1183
|
-
there because the response belongs to the item being left: a host that persists
|
|
1184
|
-
on the navigation event, or a `persist()` that follows it, would otherwise
|
|
1185
|
-
snapshot a session the learner had already changed.
|
|
1068
|
+
## Custom layout authoring
|
|
1186
1069
|
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
pending response at those boundaries.
|
|
1070
|
+
A host builds its own section layout from `pie-section-player-kernel-host` and
|
|
1071
|
+
the two panes. The kernel host runs the section: the toolkit, the section
|
|
1072
|
+
controller, readiness, the element pre-warm and the section toolbar. Its element
|
|
1073
|
+
children are the layout, and the panes inside them render the section's items
|
|
1074
|
+
and passages. [Custom Section Layouts](../../docs/section-player/custom-layouts.md)
|
|
1075
|
+
covers the kernel host, the panes and the rules a layout follows.
|
|
1194
1076
|
|
|
1195
1077
|
## Content trust boundary
|
|
1196
1078
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
[
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
`<pie-item-player>` instance, so any field not recognized by the kernel is
|
|
1204
|
-
passed straight through as a prop/attribute:
|
|
1079
|
+
The layouts embed a `<pie-item-player>` for each item. Item and passage markup
|
|
1080
|
+
is sanitized with DOMPurify by default; the
|
|
1081
|
+
[item player README](../item-player/README.md#content-trust-boundary) describes
|
|
1082
|
+
the allow-list, the `trust-markup` opt-out and the `sanitizeMarkup` property.
|
|
1083
|
+
Every `runtime.player` field the section player does not consume is set on each
|
|
1084
|
+
embedded item player as a property, so a host forwards those settings there:
|
|
1205
1085
|
|
|
1206
|
-
```
|
|
1086
|
+
```js
|
|
1207
1087
|
const runtime = {
|
|
1208
1088
|
playerType: "iife",
|
|
1209
1089
|
player: {
|
|
1210
|
-
trustMarkup: false, // default
|
|
1090
|
+
trustMarkup: false, // the default: DOMPurify stays on
|
|
1211
1091
|
// sanitizeMarkup: (html) => myCustomSanitize(html),
|
|
1212
1092
|
},
|
|
1213
1093
|
};
|
|
1214
1094
|
```
|
|
1215
1095
|
|
|
1216
|
-
Set `trustMarkup: true` only when
|
|
1217
|
-
|
|
1096
|
+
Set `trustMarkup: true` only when a trusted pipeline produces the section
|
|
1097
|
+
payload. The [security overview](../../docs/security/readme.md) covers the wider
|
|
1098
|
+
trust model.
|
|
1218
1099
|
|
|
1219
1100
|
## Exports
|
|
1220
1101
|
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1102
|
+
| Specifier | Contents |
|
|
1103
|
+
| --- | --- |
|
|
1104
|
+
| `@pie-players/pie-section-player` | Registers every element. Exports `sectionFromItem`, `DEFAULT_SECTION_PLAYER_POLICIES` and the types `SectionPlayerRuntimeConfig`, `SectionPlayerRuntimePlayerConfig`, `SectionPlayerRuntimeHostContract`, `SectionPlayerSnapshot`, `SectionPlayerNavigationSnapshot`, `SectionPlayerPolicies`, `SectionPlayerHostHooks`, the card title context types, `SectionPlayerBackendResolver` and `SectionPlayerBackendResolverContext` |
|
|
1105
|
+
| `@pie-players/pie-section-player/browser` | The self-contained browser build, which also exports `createPackagedToolRegistry` and `DEFAULT_TOOL_MODULE_LOADERS` ([Loading from a CDN](../../docs/install/cdn.md#section-player-browser-build)) |
|
|
1106
|
+
| `@pie-players/pie-section-player/components/section-player-splitpane-element` | An alias of the package root |
|
|
1107
|
+
| `@pie-players/pie-section-player/contracts/runtime-host-contract` | The host method types |
|
|
1108
|
+
| `@pie-players/pie-section-player/contracts/host-hooks` | `SectionPlayerHostHooks` |
|
|
1109
|
+
| `@pie-players/pie-section-player/policies` | `DEFAULT_SECTION_PLAYER_POLICIES`, `resolveSectionPlayerPolicies` and the policy types |
|
|
1110
|
+
| `@pie-players/pie-section-player/item-section` | `sectionFromItem`, Node-safe |
|
|
1230
1111
|
|
|
1231
1112
|
## Development
|
|
1232
1113
|
|