@pie-players/pie-section-player 0.3.74 → 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 +862 -866
- package/custom-elements.json +3922 -0
- package/dist/browser/assets/{evaluation-worker-DkU_wGh3.js → evaluation-worker-0xRDVTJx.js} +98934 -88735
- package/dist/browser/chunks/PUA-tLuTiQ7--f2b27953.js +1 -1
- package/dist/browser/chunks/ToolCoordinator-0a6c4088.js +1 -0
- package/dist/browser/chunks/accents-BTUYmhn9-cafeb96e.js +1 -1
- package/dist/browser/chunks/accents-b-i-CngHZsLL-e0eb98fd.js +1 -1
- package/dist/browser/chunks/arabic-BzEHMVZD-30ac1ade.js +1 -1
- package/dist/browser/chunks/arrows-VjxlU-jv-2e17c192.js +1 -1
- package/dist/browser/chunks/attribute-coercion-b93b482c.js +1 -0
- package/dist/browser/chunks/braille-BeWC_Ruq-a3bb8ef9.js +1 -1
- package/dist/browser/chunks/braille-d-Dk6J96GO-d832bfbf.js +1 -1
- package/dist/browser/chunks/browser-a3c32178.js +17 -16
- package/dist/browser/chunks/calculator-element-271c142f.js +11 -8
- package/dist/browser/chunks/calligraphic-Cew9cU3x-04cf0735.js +1 -1
- package/dist/browser/chunks/catalog-context-46d3e50e.js +1 -0
- package/dist/browser/chunks/catalog-owner-d602f784.js +1 -0
- package/dist/browser/chunks/cherokee-Cny3FYho-4da7d2f3.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/cyrillic-CfCDulXU-5550df0b.js +1 -1
- package/dist/browser/chunks/cyrillic-ss-lU7bNGG9-12eb0188.js +1 -1
- package/dist/browser/chunks/devanagari-CDIz2h-r-18c38725.js +1 -1
- package/dist/browser/chunks/dist-41b39e7f.js +1 -1
- package/dist/browser/chunks/dist-59132a53.js +1 -1
- package/dist/browser/chunks/dist-60d7afef.js +1 -1
- package/dist/browser/chunks/dist-ca9b9622.js +1 -1
- package/dist/browser/chunks/double-struck-BpJEQk8y-500ce8dd.js +1 -1
- package/dist/browser/chunks/focus-trap-44913bf3.js +1 -1
- package/dist/browser/chunks/fraktur-rLEq9DEH-44cc14d8.js +1 -1
- package/dist/browser/chunks/greek-BlraJTyL-90ce8924.js +1 -1
- package/dist/browser/chunks/greek-ss-D-EN2PK0-91610739.js +1 -1
- package/dist/browser/chunks/hebrew-GiQZeybp-b583f555.js +1 -1
- package/dist/browser/chunks/latin-b-BhEnC7R2-d13e06ff.js +1 -1
- package/dist/browser/chunks/latin-bi-DqiIKv3U-1dec8705.js +1 -1
- package/dist/browser/chunks/latin-i-B1xtwaE1-21637466.js +1 -1
- package/dist/browser/chunks/latin-xfWFlLkD-5bcea5cf.js +1 -1
- package/dist/browser/chunks/logger-8b07fb72.js +1 -1
- package/dist/browser/chunks/marrows-DD1NgMQi-15b63755.js +1 -1
- package/dist/browser/chunks/math-DpWn1mbG-38eeb914.js +1 -1
- package/dist/browser/chunks/math-rendering-ready-9a0fd90a.js +1 -1
- package/dist/browser/chunks/{mathjax-D2JHb_R_-64780dc5.js → mathjax-BKrHE4xs-3ca05bf6.js} +41 -41
- package/dist/browser/chunks/monospace-D116QD2a-98d484fd.js +1 -1
- package/dist/browser/chunks/monospace-ex-DcXpouKS-456b403c.js +1 -1
- package/dist/browser/chunks/monospace-l-Ct5wkMDs-f34a4a9e.js +1 -1
- package/dist/browser/chunks/mshapes-DCp86WSX-c0055952.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/phonetics-BwOCw2Y9-d2e3af59.js +1 -1
- package/dist/browser/chunks/phonetics-ss-CRtMn02f-579b6135.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/sans-serif-BYPZKlE9-2efdbcd9.js +1 -1
- package/dist/browser/chunks/sans-serif-b-B0kRKiC--b5530dce.js +1 -1
- package/dist/browser/chunks/sans-serif-bi-DXWul6mu-5370dba4.js +1 -1
- package/dist/browser/chunks/sans-serif-ex-Boux1kMP-aa178f31.js +1 -1
- package/dist/browser/chunks/sans-serif-i-676RFaSO-2db54474.js +1 -1
- package/dist/browser/chunks/sans-serif-r-DrN9xdZt-0fc72eea.js +1 -1
- package/dist/browser/chunks/script-0ohehnXc-526904f4.js +1 -1
- package/dist/browser/chunks/shapes-BG47NodJ-d59b0eca.js +1 -1
- package/dist/browser/chunks/{src-BcptuNS5-ed25d7fd.js → src-D_1goyPT-8cca0246.js} +81 -82
- package/dist/browser/chunks/{sre-engine-wf9s43gg-f09289c5.js → sre-0162a7a9.js} +18 -18
- package/dist/browser/chunks/sre-engine-ace23548.js +1 -0
- package/dist/browser/chunks/sre-engine-bj997w4b-891a4cbe.js +1 -0
- package/dist/browser/chunks/sre-locales-112320af.js +1 -0
- package/dist/browser/chunks/symbols-BqnUxf_t-83087e87.js +1 -1
- package/dist/browser/chunks/symbols-b-i-eg9k5hmF-9c461ac2.js +1 -1
- package/dist/browser/chunks/term-lookup-632cf6b1.js +1 -0
- package/dist/browser/chunks/tool-annotation-toolbar-997438b4.js +7 -6
- 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 +27 -33
- package/dist/browser/chunks/toolbar-registration-helpers-a7ab1ad4.js +194 -0
- package/dist/browser/chunks/variants-2LRNhOUP-966fc49d.js +1 -1
- package/dist/browser/pie-section-player.js +210 -100
- package/dist/chunks/nl-NL-364d61b9.js +35 -30
- package/dist/chunks/{sre-engine-wf9s43gg-f09289c5.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/composition.d.ts +3 -1
- package/dist/components/shared/framework-error-latch.d.ts +34 -0
- package/dist/components/shared/host-element.d.ts +10 -0
- package/dist/components/shared/kernel-host-default-body.d.ts +14 -0
- package/dist/components/shared/layout-host-methods.d.ts +12 -27
- package/dist/components/shared/missing-tool-providers.d.ts +6 -12
- package/dist/components/shared/player-preload.d.ts +4 -9
- package/dist/components/shared/section-player-backend-delivery.d.ts +1 -1
- package/dist/components/shared/section-player-card-context.d.ts +5 -4
- package/dist/components/shared/section-player-host-runtime.d.ts +32 -21
- package/dist/components/shared/section-player-layout-context.d.ts +77 -0
- package/dist/components/shared/section-player-own-event.d.ts +10 -0
- package/dist/components/shared/section-player-runtime-callbacks.d.ts +1 -1
- package/dist/components/shared/section-player-shell-layout.svelte.d.ts +3 -11
- package/dist/components/shared/section-player-view-state.d.ts +6 -2
- package/dist/components/shared/toolkit-ready-wait.d.ts +16 -0
- package/dist/contracts/host-hooks.d.ts +0 -7
- package/dist/contracts/runtime-host-contract.d.ts +0 -5
- package/dist/controllers/SectionController.d.ts +21 -6
- package/dist/controllers/types.d.ts +8 -1
- package/dist/item-section/index.d.ts +22 -0
- package/dist/item-section/index.js +37 -0
- package/dist/pie-section-player.d.ts +4 -6
- package/dist/pie-section-player.js +24965 -25463
- package/dist/policies/index.d.ts +1 -19
- package/dist/policies/index.js +6 -14
- package/dist/policies/types.d.ts +0 -4
- package/package.json +17 -55
- package/dist/browser/chunks/ToolCoordinator-cc2dc319.js +0 -1
- package/dist/browser/chunks/nds-icon-button-d602f784.js +0 -194
- package/dist/browser/chunks/pie-tool-calculator-cortex-0626bc24.js +0 -9
- package/dist/browser/chunks/pie-tool-calculator-geogebra-561236cb.js +0 -9
- package/dist/browser/chunks/runtime-C7H1jS4E-64772f5e.js +0 -8413
- package/dist/browser/chunks/tool-host-contract-b93b482c.js +0 -1
- package/dist/components/item-shell-element.d.ts +0 -1
- package/dist/components/shared/shell-registration.d.ts +0 -55
- package/dist/contracts/layout-contract.d.ts +0 -17
- package/dist/contracts/layout-contract.js +0 -0
- package/dist/contracts/layout-parity-metadata.d.ts +0 -4
- package/dist/contracts/layout-parity-metadata.js +0 -87
- package/dist/contracts/public-events.d.ts +0 -92
- package/dist/contracts/public-events.js +0 -12
- package/dist/policies/guards.d.ts +0 -2
- /package/dist/browser/chunks/{ItemToolBar-wr2myaah-2ce4b643.js → ItemToolBar-66hcwmfw-3bcb82a3.js} +0 -0
- /package/dist/chunks/{ItemToolBar-wr2myaah-1287ba31.js → ItemToolBar-66hcwmfw-1287ba31.js} +0 -0
package/README.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# @pie-players/pie-section-player
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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.
|
|
10
12
|
|
|
11
13
|
## Install
|
|
12
14
|
|
|
@@ -14,201 +16,581 @@ Use the layout custom elements listed above for section rendering.
|
|
|
14
16
|
npm install @pie-players/pie-section-player
|
|
15
17
|
```
|
|
16
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
|
+
|
|
17
36
|
No stylesheet import is needed. Authored content depends on shared classes
|
|
18
37
|
(passage markup, the legacy `kds-*` families, answer-eliminator styles) from
|
|
19
|
-
`@pie-players/pie-theme/components.css`, and
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
opt-out.
|
|
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.
|
|
23
41
|
|
|
24
|
-
##
|
|
42
|
+
## Elements
|
|
25
43
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
-
|
|
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)) |
|
|
52
|
+
|
|
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.
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
```html
|
|
59
|
+
<pie-section-player-splitpane section-id="practice-set" attempt-id="attempt-1"></pie-section-player-splitpane>
|
|
60
|
+
```
|
|
31
61
|
|
|
32
62
|
```ts
|
|
33
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
|
+
}
|
|
34
85
|
```
|
|
35
86
|
|
|
36
|
-
|
|
37
|
-
|
|
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.
|
|
38
90
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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`.
|
|
44
101
|
|
|
45
|
-
|
|
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.
|
|
46
169
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
```
|
|
178
|
+
|
|
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`.
|
|
212
|
+
|
|
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)) |
|
|
56
250
|
|
|
57
|
-
|
|
58
|
-
`@pie-players/pie-
|
|
59
|
-
the
|
|
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.
|
|
60
254
|
|
|
61
|
-
###
|
|
255
|
+
### Card titles
|
|
256
|
+
|
|
257
|
+
`hooks.cardTitleFormatter` sets the heading of each item and passage card:
|
|
62
258
|
|
|
63
259
|
```ts
|
|
64
|
-
|
|
65
|
-
|
|
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
|
+
};
|
|
66
268
|
```
|
|
67
269
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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.
|
|
73
282
|
|
|
74
|
-
|
|
75
|
-
|
|
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,
|
|
76
296
|
`waitForSectionController` waits, `getSectionController()` returns `null`, the
|
|
77
|
-
navigation methods return `false`, and `getSnapshot()`
|
|
78
|
-
|
|
79
|
-
|
|
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)) |
|
|
80
344
|
|
|
81
345
|
### Session lifecycle
|
|
82
346
|
|
|
83
|
-
|
|
347
|
+
Set `session` with `section` to restore a section. The controller created for
|
|
348
|
+
that section applies it in replace mode in place of hydrating from the
|
|
349
|
+
persistence strategy, before the controller is published, so the first
|
|
350
|
+
composition, `engine-ready` and every reader see it. The strategy still receives
|
|
351
|
+
every `persist()`.
|
|
352
|
+
|
|
353
|
+
A later assignment applies through `applySession(value, { mode: "replace" })`
|
|
354
|
+
under the rules `<pie-item-player>` applies to its `session`: a value equal to
|
|
355
|
+
the current session is a no-op, so echoing `session-changed` back is safe, and an
|
|
356
|
+
item session with neither a response value nor a response field leaves one that
|
|
357
|
+
holds responses in place. `null` after creation is a no-op. A `section` change
|
|
358
|
+
without a `session` assignment hydrates the new controller from the strategy.
|
|
359
|
+
Reading `session` returns the assigned value until the controller is published,
|
|
360
|
+
and a fresh `getSession()` snapshot after.
|
|
361
|
+
|
|
362
|
+
Driving the controller directly:
|
|
84
363
|
|
|
85
364
|
```ts
|
|
86
365
|
controller?.configureSessionPersistence?.({ context, strategy });
|
|
87
366
|
await controller?.hydrate?.();
|
|
88
367
|
const unsubscribe = controller?.subscribe?.(handleEvent);
|
|
89
|
-
//
|
|
368
|
+
// Later, on save or unload:
|
|
90
369
|
await controller?.persist?.();
|
|
91
370
|
unsubscribe?.();
|
|
92
371
|
```
|
|
93
372
|
|
|
94
|
-
|
|
95
|
-
detail)` are the direct read/write surfaces and exchange the same
|
|
96
|
-
`SectionControllerSessionState` shape the persistence strategy load/save
|
|
97
|
-
methods receive. See [Item session management](#item-session-management) for
|
|
98
|
-
worked examples.
|
|
99
|
-
|
|
100
|
-
### Event stream
|
|
101
|
-
|
|
102
|
-
The controller's typed event stream (`SectionControllerEvent` discriminated
|
|
103
|
-
union) is the single source of truth for in-section change. Hosts usually
|
|
104
|
-
subscribe through `ToolkitCoordinator.subscribeItemEvents` /
|
|
105
|
-
`subscribeSectionLifecycleEvents` (cohort-aware filtering, survives
|
|
106
|
-
navigation) — see [JS API example for advanced host
|
|
107
|
-
policy](#js-api-example-for-advanced-host-policy). Key event types:
|
|
108
|
-
|
|
109
|
-
- `item-selected` — item navigation within the current section.
|
|
110
|
-
- `item-session-data-changed` / `item-session-meta-changed` — per-item
|
|
111
|
-
session updates the persistence layer should observe. `sectionId` names the
|
|
112
|
-
section. `elementId` names the reporting element when the change carries one,
|
|
113
|
-
and `complete` is then that element's own. A commit carries
|
|
114
|
-
`sessionCommitReason` (see [Commit at a section
|
|
115
|
-
boundary](#commit-at-a-section-boundary)).
|
|
116
|
-
- `item-complete-changed` — an item's completion flipped. An item is complete
|
|
117
|
-
when every element that has reported its completion is complete. A report
|
|
118
|
-
without `elementId`, and a restored session's item-level `complete`, set the
|
|
119
|
-
item's completion directly. A session restored through `applySession`
|
|
120
|
-
without `complete` is complete when it holds a response. A passage's own
|
|
121
|
-
`session-changed` stays inside its shell: a passage holds no response.
|
|
122
|
-
- `content-loaded` — passage / item / rubric finished loading. Carries
|
|
123
|
-
`contentKind`, `itemId`, and `canonicalItemId`.
|
|
124
|
-
- `section-loading-complete` — every renderable in the section finished
|
|
125
|
-
loading.
|
|
126
|
-
- `section-items-complete-changed` — aggregate completion flip.
|
|
127
|
-
- `section-error` — an item player failed (`source: "item-player"`), or the
|
|
128
|
-
section runtime did (`source: "section-runtime"`), a rejected element warmup
|
|
129
|
-
included, which leaves the items unmounted.
|
|
130
|
-
- `section-navigation-change` — the controller's section identity changed.
|
|
131
|
-
- `formative-try-recorded` — a learner checked an answer.
|
|
132
|
-
- `formative-reveal-changed` — the reveal state changed without a Try: a learner
|
|
133
|
-
dismissed feedback, or a host forced or withdrew a reveal (`source` says which).
|
|
134
|
-
- `section-mastery-changed` — the mastery rollup changed. Emitted on change
|
|
135
|
-
only, like `section-items-complete-changed`.
|
|
136
|
-
- `timed-media-cue-changed` — a cue activated, a gate released, or aggregate
|
|
137
|
-
completion flipped. Not emitted for media position: `timeupdate` fires about four
|
|
138
|
-
times a second and moves nothing a layout renders.
|
|
139
|
-
- `timed-media-audio-started` — media audio is running, so read-aloud must yield.
|
|
140
|
-
Emitted only where playback actually stood; a gate that re-paused on the same
|
|
141
|
-
`play` produced no audio.
|
|
142
|
-
- `timed-media-policy-degraded` — the attached media time source cannot carry out a
|
|
143
|
-
playback policy, so it is advisory from here.
|
|
144
|
-
- `timed-media-invalid` — authored `timedMedia` that cannot be delivered; the
|
|
145
|
-
section renders without cue behavior.
|
|
146
|
-
|
|
147
|
-
The helpers' default event sets leave out `formative-try-recorded`,
|
|
148
|
-
`formative-reveal-changed`, `section-mastery-changed` and
|
|
149
|
-
`timed-media-audio-started`. Name them in `subscribeItemEvents({ eventTypes })`,
|
|
150
|
-
or subscribe through `subscribeSectionEvents`.
|
|
373
|
+
### Item session management
|
|
151
374
|
|
|
152
|
-
|
|
153
|
-
`
|
|
154
|
-
`
|
|
155
|
-
`canonicalItemId` is that item's adapter identifier, and falls back to `itemId`
|
|
156
|
-
when the section was not built from an adapter or no adapter ref matches it.
|
|
157
|
-
Correlate with formative policy and `runtime.player.resolveBackend` by
|
|
158
|
-
`canonicalItemId`; reach the session by `itemId`. `content-loaded` and
|
|
159
|
-
`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.
|
|
160
378
|
|
|
161
|
-
|
|
379
|
+
```ts
|
|
380
|
+
import type { SectionPlayerRuntimeHostContract } from "@pie-players/pie-section-player";
|
|
162
381
|
|
|
163
|
-
|
|
164
|
-
|
|
382
|
+
const player = document.querySelector<HTMLElement & SectionPlayerRuntimeHostContract>(
|
|
383
|
+
"pie-section-player-splitpane",
|
|
384
|
+
);
|
|
385
|
+
const controller = await player?.waitForSectionController(5000);
|
|
165
386
|
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
});
|
|
177
411
|
```
|
|
178
412
|
|
|
179
|
-
|
|
180
|
-
|
|
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.
|
|
181
438
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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.
|
|
186
444
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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.
|
|
190
542
|
|
|
191
543
|
```ts
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
+
});
|
|
200
556
|
```
|
|
201
557
|
|
|
202
|
-
|
|
203
|
-
`
|
|
204
|
-
|
|
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 })`.
|
|
205
562
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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.
|
|
210
567
|
|
|
211
|
-
|
|
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
|
|
212
594
|
|
|
213
595
|
Set `sectionType: "timed-media"` and a `timedMedia` block, and the section's cue
|
|
214
596
|
timeline decides when its items are delivered:
|
|
@@ -225,8 +607,8 @@ const section: AssessmentSection = {
|
|
|
225
607
|
class: "stimulus",
|
|
226
608
|
view: ["candidate"],
|
|
227
609
|
// An ordinary passage. Its config mounts the media element — a PIE element,
|
|
228
|
-
// or authored `<video>` markup
|
|
229
|
-
// carry
|
|
610
|
+
// or authored `<video>` markup, whose `<track>` carries the captions — and
|
|
611
|
+
// its accessibility catalogs carry the transcript and signed alternates.
|
|
230
612
|
passage: videoPassage,
|
|
231
613
|
},
|
|
232
614
|
],
|
|
@@ -247,149 +629,226 @@ const section: AssessmentSection = {
|
|
|
247
629
|
};
|
|
248
630
|
```
|
|
249
631
|
|
|
250
|
-
Every item a gate names must satisfy its `releaseOn`. To split must-answer items
|
|
251
|
-
optional ones, author two cues at the same timestamp
|
|
252
|
-
reveal over the second. Both activate in the same pass, the reveal
|
|
253
|
-
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.
|
|
254
636
|
|
|
255
|
-
|
|
256
|
-
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,
|
|
257
639
|
including one a `metadata` cue names, since metadata records state and reveals
|
|
258
|
-
nothing. A cued item is mounted and hidden until its cue fires, so its session
|
|
259
|
-
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.
|
|
260
642
|
|
|
261
|
-
The section reaches media only through a **Media Time Source**. The stimulus
|
|
262
|
-
finds the media element its passage mounted and registers a native adapter;
|
|
263
|
-
with its own player registers its own port
|
|
264
|
-
|
|
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:
|
|
265
647
|
|
|
266
648
|
```ts
|
|
267
649
|
const controller = await host.waitForSectionController(5000);
|
|
268
|
-
// No `renderableId`: a host
|
|
269
|
-
//
|
|
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.
|
|
270
652
|
controller?.attachMediaTimeSource?.(myThirdPartyPort);
|
|
271
653
|
controller?.detachMediaTimeSource?.();
|
|
272
654
|
controller?.getTimedMediaProjection?.(); // cues, gate, enforcement, revealed items
|
|
273
|
-
// One half of the read-aloud handoff
|
|
274
|
-
// 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.
|
|
275
657
|
controller?.pauseMediaForCompetingAudio?.();
|
|
276
658
|
```
|
|
277
659
|
|
|
278
|
-
Read-aloud and media audio never run at once, and the action the learner just
|
|
279
|
-
wins: starting read-aloud pauses media, starting media pauses read-aloud.
|
|
280
|
-
supplies both halves
|
|
281
|
-
toolkit arbitrates between them, because
|
|
282
|
-
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.
|
|
283
666
|
|
|
284
667
|
Where the port reports `canPause: false` or `canRestrictSeeking: false`, the
|
|
285
|
-
matching policy degrades to **advisory**: cues still fire, state is still
|
|
286
|
-
the projection says `enforcement: "advisory"`, and a recoverable
|
|
287
|
-
framework warning names the policy that
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
Three authoring mistakes fail loudly
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
Cue state persists inside `SectionControllerSessionState.timedMedia` and
|
|
299
|
-
with the rest of the snapshot, including the furthest position reached,
|
|
300
|
-
`allowSeekAhead: false` clamps against across a reload.
|
|
301
|
-
[
|
|
302
|
-
|
|
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
|
|
688
|
+
|
|
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.
|
|
303
694
|
|
|
304
|
-
|
|
695
|
+
```ts
|
|
696
|
+
import { sectionFromItem } from "@pie-players/pie-section-player/item-section";
|
|
305
697
|
|
|
306
|
-
|
|
698
|
+
const { section, session } = sectionFromItem(itemConfig, { session: itemSession });
|
|
699
|
+
player.section = section;
|
|
700
|
+
player.session = session;
|
|
701
|
+
```
|
|
702
|
+
|
|
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.
|
|
710
|
+
|
|
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.
|
|
307
717
|
|
|
308
718
|
```ts
|
|
309
|
-
import
|
|
310
|
-
import '@pie-players/pie-section-player/components/section-player-vertical-element';
|
|
311
|
-
import '@pie-players/pie-section-player/components/section-player-tabbed-element';
|
|
312
|
-
import '@pie-players/pie-section-player/components/section-player-item-card-element';
|
|
313
|
-
import '@pie-players/pie-section-player/components/section-player-passage-card-element';
|
|
314
|
-
```
|
|
719
|
+
import type { SectionPlayerRuntimeConfig } from "@pie-players/pie-section-player";
|
|
315
720
|
|
|
316
|
-
|
|
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
|
+
};
|
|
317
735
|
|
|
318
|
-
|
|
319
|
-
<pie-section-player-splitpane></pie-section-player-splitpane>
|
|
736
|
+
sectionPlayer.runtime = runtime;
|
|
320
737
|
```
|
|
321
738
|
|
|
322
|
-
|
|
323
|
-
`
|
|
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.
|
|
324
744
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
`
|
|
334
|
-
|
|
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.
|
|
335
777
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
- `nds-icons` (boolean): opt in to NDS icon buttons. Mirrored onto `runtime.ndsIcons`, which wins when both are set.
|
|
354
|
-
- `tool-config-strictness` (string, optional): `off|warn|error` for tool-config validation; default `error`. `runtime.toolConfigStrictness` wins when both are set.
|
|
355
|
-
- `split-pane-initial-passage-width` (number, optional): splitpane passage pane width in percent at mount. Clamped to 20–80; default 50.
|
|
356
|
-
- `iife-bundle-host` (string, optional): bundle host for the IIFE element pre-warm when `runtime.player.loaderOptions.bundleHost` is unset.
|
|
357
|
-
- Host extension props (JS properties only): `toolRegistry`, `sectionHostButtons`, `itemHostButtons`, `passageHostButtons`, `hooks`
|
|
358
|
-
|
|
359
|
-
When viewport width is within the collapsed range (~1100px and below), splitpane and
|
|
360
|
-
vertical layout hosts normalize section toolbar placement to `top`. This includes
|
|
361
|
-
`left`, `right`, `bottom`, and `none` values.
|
|
362
|
-
|
|
363
|
-
`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.
|
|
364
|
-
|
|
365
|
-
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
|
+
});
|
|
366
795
|
|
|
367
|
-
|
|
368
|
-
<pie-section-player-splitpane
|
|
369
|
-
content-max-width-no-passage="800"
|
|
370
|
-
content-max-width-with-passage="1200"
|
|
371
|
-
split-pane-min-region-width="280"
|
|
372
|
-
></pie-section-player-splitpane>
|
|
796
|
+
const runtime: SectionPlayerRuntimeConfig = { assessmentId: "practice-1", coordinator };
|
|
373
797
|
```
|
|
374
798
|
|
|
375
|
-
|
|
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()`.
|
|
376
806
|
|
|
377
|
-
|
|
807
|
+
The player does not load a section definition by identity: the host loads the
|
|
808
|
+
`AssessmentSection` and passes it in.
|
|
378
809
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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" };
|
|
384
836
|
```
|
|
385
837
|
|
|
386
|
-
|
|
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
|
|
387
846
|
|
|
388
847
|
### Heading structure
|
|
389
848
|
|
|
390
849
|
The player publishes one number and every descendant derives its outline from it.
|
|
391
850
|
Set `base-heading-level` to the level the cards should occupy in the surrounding
|
|
392
|
-
page
|
|
851
|
+
page: 2 when the page has its own `<h1>` above the player, 3 when the player sits
|
|
393
852
|
under an `<h2>`, and so on:
|
|
394
853
|
|
|
395
854
|
```html
|
|
@@ -406,87 +865,75 @@ h2 Question 1 <- item card heading
|
|
|
406
865
|
h3 Part A <- authored data-heading content in the prompt
|
|
407
866
|
```
|
|
408
867
|
|
|
409
|
-
The two content kinds derive different levels from the same number
|
|
410
|
-
|
|
411
|
-
item
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
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.
|
|
415
873
|
|
|
416
874
|
Authored `data-heading="headingN"` markup in passages and prompts becomes real
|
|
417
|
-
heading elements only when a level is published, which the player
|
|
418
|
-
|
|
419
|
-
[PIE-151](https://illuminate.atlassian.net/browse/PIE-151) therefore renders as
|
|
420
|
-
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.
|
|
421
877
|
|
|
422
|
-
A host that
|
|
423
|
-
|
|
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:
|
|
424
880
|
|
|
425
881
|
```js
|
|
426
882
|
sectionPlayer.runtime = { player: { includeSrHeading: true } };
|
|
427
883
|
```
|
|
428
884
|
|
|
429
|
-
|
|
430
|
-
is
|
|
431
|
-
[`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.
|
|
432
887
|
|
|
433
888
|
### Tab styling hooks
|
|
434
889
|
|
|
435
|
-
`pie-section-player-tabbed` and splitpane `tabbed` collapse mode expose
|
|
436
|
-
hooks
|
|
890
|
+
`pie-section-player-tabbed` and the splitpane's `tabbed` collapse mode expose
|
|
891
|
+
these hooks:
|
|
437
892
|
|
|
438
893
|
- `pie-section-player-tabs`
|
|
439
894
|
- `pie-section-player-tab`
|
|
440
895
|
- `pie-section-player-tab--active`
|
|
441
896
|
- `pie-section-player-tab-panel`
|
|
442
897
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
- `data-pie-purpose="passage-label"` and alias class `passage-label`
|
|
446
|
-
- `data-pie-purpose="item-label"` and alias class `item-label`
|
|
898
|
+
Tabs also carry `data-pie-purpose="passage-label"` and `data-pie-purpose="item-label"`.
|
|
447
899
|
|
|
448
|
-
Tab colors, spacing
|
|
900
|
+
Tab colors, spacing and track geometry are themed through
|
|
449
901
|
`--pie-section-player-tab-color`, `--pie-section-player-tab-background`,
|
|
450
902
|
`--pie-section-player-tab-active-color`,
|
|
451
903
|
`--pie-section-player-tab-active-background`,
|
|
452
904
|
`--pie-section-player-tab-gap`,
|
|
453
905
|
`--pie-section-player-tab-track-radius`,
|
|
454
|
-
`--pie-section-player-tab-track-padding
|
|
906
|
+
`--pie-section-player-tab-track-padding` and
|
|
455
907
|
`--pie-section-player-tab-padding-block`.
|
|
456
908
|
|
|
457
909
|
### Card header styling hooks
|
|
458
910
|
|
|
459
|
-
Passage and item cards share a
|
|
911
|
+
Passage and item cards share a header row
|
|
460
912
|
(`.pie-section-player-content-card-header`, with the card-specific aliases
|
|
461
|
-
`.pie-section-player-passage-header`
|
|
462
|
-
|
|
463
|
-
- Title and toolbar are centered vertically
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
- Card corners default to `8px
|
|
467
|
-
|
|
468
|
-
- The header fill is transparent by default.
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
-
|
|
472
|
-
card
|
|
473
|
-
`--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.
|
|
474
925
|
- Under a dark theme the header takes
|
|
475
|
-
`--pie-section-player-card-header-background-dark
|
|
476
|
-
`--pie-section-player-card-header-background` when it is unset
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
picks up the same header fill without either side hardcoding the other's
|
|
487
|
-
token name.
|
|
488
|
-
|
|
489
|
-
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.
|
|
490
937
|
|
|
491
938
|
```css
|
|
492
939
|
pie-section-player-passage-card,
|
|
@@ -498,19 +945,14 @@ pie-section-player-item-card {
|
|
|
498
945
|
}
|
|
499
946
|
```
|
|
500
947
|
|
|
501
|
-
When both max-width attributes are set, the with-passage cap resolves to the greater
|
|
502
|
-
of the two configured values (after clamp), so with-passage mode never ends up narrower
|
|
503
|
-
than no-passage mode.
|
|
504
|
-
|
|
505
948
|
### Split-pane backdrop
|
|
506
949
|
|
|
507
950
|
The split-pane layout paints a backdrop behind each scrollable pane, under the
|
|
508
951
|
passage and item cards. It reads the canonical `--pie-background-dark`, so it
|
|
509
|
-
follows the active theme and color scheme, and
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
`--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`.
|
|
514
956
|
|
|
515
957
|
### Content-card tool surfaces
|
|
516
958
|
|
|
@@ -519,208 +961,53 @@ Item and passage cards offer two content-scoped host surfaces:
|
|
|
519
961
|
- `content-lead` is a full-width stack before the authored player content.
|
|
520
962
|
- `content-media` is the resizable region beside that content.
|
|
521
963
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
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.
|
|
527
968
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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.
|
|
532
975
|
|
|
533
|
-
The `content-media`
|
|
534
|
-
keyboard-accessible divider (`role="separator"`; arrow keys,
|
|
535
|
-
`Escape` to cancel a drag). Below a card width of 560px the region
|
|
536
|
-
the content and the divider is withdrawn. Placement is fixed
|
|
537
|
-
|
|
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.
|
|
538
981
|
|
|
539
982
|
This package owns the region's share of the card width and nothing inside it. A
|
|
540
|
-
capability mounted here sizes its own content: signing legibility needs height
|
|
541
|
-
hands and face, so
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
All three section
|
|
548
|
-
`section-overlay`) share one
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
- Apply custom policy/gating in host code (for example, domain-specific `canNext` based on controller events like `section-items-complete-changed`)
|
|
569
|
-
- Compose forward/backward eligibility in host code using `selectNavigation()` + host state; there is intentionally no separate parallel CE gating API for this
|
|
570
|
-
- Inject custom toolbar tooling with `toolRegistry` and optional host button arrays (`sectionHostButtons`, `itemHostButtons`, `passageHostButtons`)
|
|
571
|
-
- Register host callbacks via `hooks` (for example `hooks.cardTitleFormatter`)
|
|
572
|
-
|
|
573
|
-
Example:
|
|
574
|
-
|
|
575
|
-
```ts
|
|
576
|
-
const host = document.querySelector("pie-section-player-splitpane") as any;
|
|
577
|
-
host.hooks = {
|
|
578
|
-
cardTitleFormatter: (context: any) => {
|
|
579
|
-
if (context.kind === "item") {
|
|
580
|
-
return `Question ${context.itemIndex + 1}: ${context.item?.name || context.defaultTitle}`;
|
|
581
|
-
}
|
|
582
|
-
return context.passage?.name || context.defaultTitle;
|
|
583
|
-
},
|
|
584
|
-
};
|
|
585
|
-
```
|
|
586
|
-
|
|
587
|
-
Advanced runtime configuration is supplied through the `runtime` object. Set player config, tools, accessibility, coordinator, env, and `createSectionController` on `runtime.<key>`.
|
|
588
|
-
|
|
589
|
-
### Backend delivery for embedded items
|
|
590
|
-
|
|
591
|
-
Hosts can configure item-player backend delivery once at the section-player
|
|
592
|
-
runtime level. Section-player derives a concrete `backend` prop for each
|
|
593
|
-
embedded item player before it renders the item. This is intended for hosts
|
|
594
|
-
that need server-processed PIE models and server scoring without querying every
|
|
595
|
-
nested `<pie-item-player>`.
|
|
596
|
-
|
|
597
|
-
```ts
|
|
598
|
-
import type {
|
|
599
|
-
SectionPlayerRuntimeConfig,
|
|
600
|
-
} from "@pie-players/pie-section-player";
|
|
601
|
-
|
|
602
|
-
const runtime: SectionPlayerRuntimeConfig = {
|
|
603
|
-
playerType: "iife",
|
|
604
|
-
env: {
|
|
605
|
-
mode: "gather",
|
|
606
|
-
role: "student",
|
|
607
|
-
},
|
|
608
|
-
player: {
|
|
609
|
-
backend: {
|
|
610
|
-
delivery: {
|
|
611
|
-
enabled: true,
|
|
612
|
-
baseUrl: bffUrl,
|
|
613
|
-
assignmentId: playerSessionId,
|
|
614
|
-
endpoints: {
|
|
615
|
-
load: "/api/player/load",
|
|
616
|
-
saveSession: "/api/player/save",
|
|
617
|
-
model: "/api/player/model",
|
|
618
|
-
score: "/api/player/score",
|
|
619
|
-
},
|
|
620
|
-
autosave: { enabled: true, debounceMs: 250 },
|
|
621
|
-
},
|
|
622
|
-
},
|
|
623
|
-
},
|
|
624
|
-
};
|
|
625
|
-
|
|
626
|
-
sectionPlayer.runtime = runtime;
|
|
627
|
-
```
|
|
628
|
-
|
|
629
|
-
When `runtime.player.backend.delivery` is enabled, section-player treats
|
|
630
|
-
`itemId` and `sessionId` as per-item delivery identity. It derives them from
|
|
631
|
-
`canonicalItemId || item.id` and the item session before forwarding `backend` to
|
|
632
|
-
each embedded item player. Static delivery fields such as `baseUrl`, `auth`,
|
|
633
|
-
`endpoints`, `assignmentId`, and `autosave` are preserved. Use `assignmentId`
|
|
634
|
-
for shared attempt/player identity.
|
|
635
|
-
|
|
636
|
-
An enabled `backend.delivery` also sets `hosted: true` on each embedded item
|
|
637
|
-
player unless `runtime.player.hosted` is set, so the item players load no
|
|
638
|
-
element controllers and render the models the server returns.
|
|
639
|
-
|
|
640
|
-
`runtime.player.resolveBackend` is a section-player-reserved key. It is called
|
|
641
|
-
with `{ itemId, canonicalItemId, item, itemIndex, itemSession, sectionId, env,
|
|
642
|
-
baseBackend }` and is stripped before props reach `<pie-item-player>`. Use it
|
|
643
|
-
only when the backend needs custom per-item identity mapping. The resolver
|
|
644
|
-
receives cloned backend objects, so per-item identity changes do not mutate the
|
|
645
|
-
shared runtime configuration or leak across items.
|
|
646
|
-
|
|
647
|
-
Section-player only derives the concrete `backend` prop. It does not call
|
|
648
|
-
`loadFromBackend()` on nested item players. Embedded `<pie-item-player>` loads
|
|
649
|
-
automatically when its derived `backend.delivery` config has a load signature,
|
|
650
|
-
so every mounted item player issues one backend load for its own item. Passage
|
|
651
|
-
players do not receive item delivery backend config, but shared non-delivery
|
|
652
|
-
backend config is preserved.
|
|
653
|
-
|
|
654
|
-
This backend delivery config is separate from the element-loader backend used
|
|
655
|
-
for IIFE/ESM bundle preloading.
|
|
656
|
-
|
|
657
|
-
### Preloaded elements
|
|
658
|
-
|
|
659
|
-
With `runtime.playerType: "preloaded"` the section player loads no element
|
|
660
|
-
code. Its pre-warm asserts that every tag the section's items and passages name
|
|
661
|
-
is registered, so the host registers the elements first:
|
|
662
|
-
|
|
663
|
-
```ts
|
|
664
|
-
import { registerPreloadedElements } from "@pie-players/pie-item-player/preloaded";
|
|
665
|
-
import * as delivery from "@pie-element/multiple-choice/browser/delivery";
|
|
666
|
-
import * as controller from "@pie-element/multiple-choice/browser/controller";
|
|
667
|
-
import manifest from "../package.json"; // pins "@pie-element/multiple-choice" exactly
|
|
668
|
-
|
|
669
|
-
registerPreloadedElements([
|
|
670
|
-
{
|
|
671
|
-
tag: "pie-element-multiple-choice",
|
|
672
|
-
package: "@pie-element/multiple-choice",
|
|
673
|
-
version: manifest.dependencies["@pie-element/multiple-choice"],
|
|
674
|
-
element: delivery,
|
|
675
|
-
controller,
|
|
676
|
-
},
|
|
677
|
-
]);
|
|
678
|
-
|
|
679
|
-
sectionPlayer.runtime = { ...sectionPlayer.runtime, playerType: "preloaded" };
|
|
680
|
-
```
|
|
681
|
-
|
|
682
|
-
- Register before the section player mounts. A tag missing at pre-warm leaves
|
|
683
|
-
the items unmounted and raises a non-recoverable `element-preload` framework
|
|
684
|
-
error.
|
|
685
|
-
- Install element packages with `npm install --save-exact`. npm otherwise saves
|
|
686
|
-
a caret range, which registration rejects as a `version`, and which a fresh
|
|
687
|
-
install can resolve to another release line: `^13.4.0-next.15` resolves to
|
|
688
|
-
the legacy `13.4.4`, which has no `./browser/*` modules.
|
|
689
|
-
- Install every pie-elements-ng package from one release, in one install from
|
|
690
|
-
the same dist-tag, and upgrade them together. Elements whose `./browser/*`
|
|
691
|
-
builds typeset on `window.MathJax` share the MathJax the first of them loads,
|
|
692
|
-
in the build and configuration of that element's release, so in a mixed set
|
|
693
|
-
an element can typeset with a MathJax it was not built for. Elements that
|
|
694
|
-
bundle their own MathJax share none
|
|
695
|
-
([One MathJax version per page](../../docs/item-player/loading-strategies.md#one-mathjax-version-per-page)).
|
|
696
|
-
- Register one version per package. The players align every authored version
|
|
697
|
-
of a package to the registered one, and registering a second version throws.
|
|
698
|
-
- Register each package's `controller` unless the item players are hosted
|
|
699
|
-
(`runtime.player.hosted`, or an enabled `runtime.player.backend.delivery`).
|
|
700
|
-
A player that is not hosted runs `model()` in the browser and warns for each
|
|
701
|
-
tag registered without one.
|
|
702
|
-
- Only `@pie-element/*` builds from pie-elements-ng publish
|
|
703
|
-
`./browser/delivery` and `./browser/controller`.
|
|
704
|
-
- Under TypeScript, the `package.json` import needs `resolveJsonModule`, and a
|
|
705
|
-
package version that ships no declarations for `./browser/*` needs a
|
|
706
|
-
`declare module` shim for those subpaths.
|
|
707
|
-
|
|
708
|
-
See [`strategy="preloaded"`](../../docs/item-player/loading-strategies.md#strategypreloaded)
|
|
709
|
-
for the registration contract.
|
|
710
|
-
|
|
711
|
-
### Host-owned focus
|
|
712
|
-
|
|
713
|
-
Section-player does not move focus on behalf of host-level affordances such
|
|
714
|
-
as "Skip to Main", nor does it make passage/question containers tab stops.
|
|
715
|
-
Hosts own page chrome, skip links, landmarks, and any special focus placement.
|
|
716
|
-
For example, a host shell can focus its own
|
|
717
|
-
`main#main-content`; the next Tab then follows the browser's natural order
|
|
718
|
-
into the first actionable control rendered inside the section player.
|
|
719
|
-
|
|
720
|
-
The passage and item card custom elements are content/layout surfaces, not
|
|
721
|
-
public focus targets. Splitpane passage content remains scrollable through the
|
|
722
|
-
pane's native scroll behavior, but the passage pane itself is not inserted into
|
|
723
|
-
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.
|
|
724
1011
|
|
|
725
1012
|
```html
|
|
726
1013
|
<a href="#main-content" class="skip-link">Skip to Main</a>
|
|
@@ -729,164 +1016,13 @@ sequential keyboard navigation.
|
|
|
729
1016
|
</main>
|
|
730
1017
|
```
|
|
731
1018
|
|
|
732
|
-
|
|
733
|
-
unset field, a missing section included, takes its value from
|
|
734
|
-
`DEFAULT_SECTION_PLAYER_POLICIES`.
|
|
735
|
-
|
|
736
|
-
- `readiness.mode` (`"progressive"` | `"strict"`) — the mode
|
|
737
|
-
`SectionPlayerLayoutKernel` passes to `createReadinessDetail`. Both modes
|
|
738
|
-
hold the `interactive` stage and `pie-loading-complete` until the section's
|
|
739
|
-
element pre-warm resolves and the item cards can mount. The kernel has no
|
|
740
|
-
later loading signal, so the two modes currently emit the same sequence.
|
|
741
|
-
Default: `"progressive"`.
|
|
742
|
-
- `preload.enabled` — when `false`, `SectionItemsPane` short-circuits the
|
|
743
|
-
section-level element warmup pipeline (`warmupSectionElements`). Items
|
|
744
|
-
still mount and item-players register their own elements on demand. Use
|
|
745
|
-
this to disable section pre-warm when the host already owns element
|
|
746
|
-
registration end-to-end. Default: `true`.
|
|
747
|
-
- `telemetry.enabled` — when `false`, the layout custom elements skip
|
|
748
|
-
`attachInstrumentationEventBridge` setup, so no `pie-section-*`
|
|
749
|
-
telemetry events flow through the bridge. Hosts that want a different
|
|
750
|
-
shape of opt-out can still override `runtime.player.loaderConfig.instrumentationProvider`.
|
|
751
|
-
Default: `true`.
|
|
752
|
-
|
|
753
|
-
The exported `isPreloadEnabled(policies)` and `isTelemetryEnabled(policies)`
|
|
754
|
-
helpers read these toggles with the documented default-true semantics, so
|
|
755
|
-
host code that needs to mirror the same gate (e.g. when composing a custom
|
|
756
|
-
layout host) can call them directly.
|
|
757
|
-
|
|
758
|
-
### Navigation signals
|
|
759
|
-
|
|
760
|
-
- `item-selected`: item-level navigation change within the current section in the `SectionController` broadcast stream (`itemIndex`, `currentItemId`, `totalItems`).
|
|
761
|
-
- `section-navigation-change`: section-level navigation/selection change in the `SectionController` broadcast stream (`previousSectionId`, `currentSectionId`, `reason`).
|
|
762
|
-
|
|
763
|
-
Runtime configuration is explicit:
|
|
764
|
-
|
|
765
|
-
- `runtime` owns runtime fields (`assessmentId`, `playerType`, `player`, `lazyInit`, `tools`, `accessibility`, `coordinator`, `isolation`, `env`, `createSectionController`).
|
|
766
|
-
- Tool placement is configured through `runtime.tools.placement.section`, `runtime.tools.placement.item`, and `runtime.tools.placement.passage`.
|
|
767
|
-
- Tool configuration validation is canonical in toolkit initialization (`pie-assessment-toolkit`), including toolbar overlays. Use `runtime.toolConfigStrictness` (`off` | `warn` | `error`) to control warning-only vs fail-fast behavior.
|
|
768
|
-
- TTS provider config must use `tools.providers.textToSpeech` (canonical). `tools.providers.tts` is rejected by validation.
|
|
769
|
-
- Host tool overrides are additive:
|
|
770
|
-
- `toolRegistry` overrides the default toolbar registry when provided
|
|
771
|
-
- host buttons are appended per toolbar scope via `sectionHostButtons`, `itemHostButtons`, `passageHostButtons`
|
|
772
|
-
|
|
773
|
-
Debug logging can be controlled per section-player host:
|
|
774
|
-
|
|
775
|
-
- Enable: `<pie-section-player-splitpane debug="true">`
|
|
776
|
-
- Disable: `<pie-section-player-splitpane debug="false">` (or `debug="0"`)
|
|
777
|
-
|
|
778
|
-
You can also disable globally via `window.PIE_DEBUG = false`.
|
|
779
|
-
|
|
780
|
-
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.
|
|
781
|
-
|
|
782
|
-
## Data flow and stability guarantees
|
|
783
|
-
|
|
784
|
-
Section-player follows a unidirectional flow model:
|
|
785
|
-
|
|
786
|
-
- Inputs flow downward (`runtime`, `section`, `env`, toolbar options) into base/toolkit/layout/card render paths.
|
|
787
|
-
- State updates flow upward as events (`runtime-*`, `session-changed`, controller change events) and are reconciled by runtime owners.
|
|
788
|
-
- Layout/card components should not create competing sources of truth for composition/session.
|
|
789
|
-
|
|
790
|
-
### Stability guarantees
|
|
791
|
-
|
|
792
|
-
For non-structural updates, section-player guarantees behavior stability:
|
|
793
|
-
|
|
794
|
-
- Item/passage shell identity remains stable (no remount churn for response-only updates).
|
|
795
|
-
- Pane-local scroll position remains stable in splitpane and vertical layouts.
|
|
796
|
-
|
|
797
|
-
Non-structural updates include:
|
|
798
|
-
|
|
799
|
-
- response/session updates
|
|
800
|
-
- tool toggles/config updates
|
|
801
|
-
- runtime config changes that do not alter composition identity
|
|
802
|
-
|
|
803
|
-
Structural composition changes (new/removed/reordered entities) may legitimately re-render/remount affected nodes.
|
|
804
|
-
|
|
805
|
-
## Custom layout authoring
|
|
806
|
-
|
|
807
|
-
For section layout authors, `pie-section-player-shell` is the primary abstraction:
|
|
808
|
-
|
|
809
|
-
- Use `pie-section-player-shell` to place the section toolbar around your layout body.
|
|
810
|
-
- Keep your custom layout logic focused on passages/items and layout UI.
|
|
811
|
-
- Treat `pie-section-player-base` as internal runtime plumbing that wraps the shell.
|
|
812
|
-
- Use `pie-section-player-item-card` and `pie-section-player-passage-card` as reusable card primitives.
|
|
813
|
-
- Prefer shared context for cross-cutting card render plumbing (resolved player tag/action) over repeated prop drilling.
|
|
814
|
-
|
|
815
|
-
Minimal pattern for package layout components:
|
|
816
|
-
|
|
817
|
-
```svelte
|
|
818
|
-
<pie-section-player-base runtime={effectiveRuntime} {section} section-id={sectionId} attempt-id={attemptId}>
|
|
819
|
-
<pie-section-player-shell
|
|
820
|
-
show-toolbar={showToolbar}
|
|
821
|
-
toolbar-position={toolbarPosition}
|
|
822
|
-
toolRegistry={toolRegistry}
|
|
823
|
-
sectionHostButtons={sectionHostButtons}
|
|
824
|
-
>
|
|
825
|
-
<!-- layout-specific body -->
|
|
826
|
-
<pie-section-player-passage-card
|
|
827
|
-
passage={passage}
|
|
828
|
-
playerParams={passagePlayerParams}
|
|
829
|
-
passageToolbarTools={passageToolbarTools}
|
|
830
|
-
toolRegistry={toolRegistry}
|
|
831
|
-
hostButtons={passageHostButtons}
|
|
832
|
-
></pie-section-player-passage-card>
|
|
833
|
-
<pie-section-player-item-card
|
|
834
|
-
item={item}
|
|
835
|
-
canonicalItemId={canonicalItemId}
|
|
836
|
-
playerParams={itemPlayerParams}
|
|
837
|
-
itemToolbarTools={itemToolbarTools}
|
|
838
|
-
toolRegistry={toolRegistry}
|
|
839
|
-
hostButtons={itemHostButtons}
|
|
840
|
-
></pie-section-player-item-card>
|
|
841
|
-
</pie-section-player-shell>
|
|
842
|
-
</pie-section-player-base>
|
|
843
|
-
```
|
|
844
|
-
|
|
845
|
-
### JS API example for advanced host policy
|
|
846
|
-
|
|
847
|
-
```ts
|
|
848
|
-
const host = document.querySelector("pie-section-player-splitpane") as any;
|
|
849
|
-
const controller = await host.waitForSectionController(5000);
|
|
850
|
-
let sectionComplete = false;
|
|
851
|
-
|
|
852
|
-
const unsubscribe = controller?.subscribe?.((event: any) => {
|
|
853
|
-
if (event?.type === "section-items-complete-changed") {
|
|
854
|
-
sectionComplete = event.complete === true;
|
|
855
|
-
}
|
|
856
|
-
});
|
|
857
|
-
|
|
858
|
-
function canAdvance() {
|
|
859
|
-
const nav = host.selectNavigation?.();
|
|
860
|
-
return Boolean(nav?.canNext && sectionComplete);
|
|
861
|
-
}
|
|
862
|
-
```
|
|
863
|
-
|
|
864
|
-
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:
|
|
865
|
-
|
|
866
|
-
```ts
|
|
867
|
-
const unsubscribeItem = coordinator.subscribeItemEvents({
|
|
868
|
-
listener: (event: any) => {
|
|
869
|
-
// item-scoped stream
|
|
870
|
-
},
|
|
871
|
-
});
|
|
872
|
-
|
|
873
|
-
const unsubscribeSection = coordinator.subscribeSectionLifecycleEvents({
|
|
874
|
-
listener: (event: any) => {
|
|
875
|
-
// section-loading-complete / section-items-complete-changed / section-error / section-navigation-change
|
|
876
|
-
},
|
|
877
|
-
});
|
|
878
|
-
```
|
|
879
|
-
|
|
880
|
-
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 any cohort exists throws.
|
|
881
|
-
|
|
882
|
-
Use `subscribeSectionEvents(...)` only for advanced mixed filtering requirements.
|
|
883
|
-
|
|
884
|
-
> **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.
|
|
1019
|
+
## Instrumentation
|
|
885
1020
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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.
|
|
890
1026
|
|
|
891
1027
|
```ts
|
|
892
1028
|
import { ConsoleInstrumentationProvider } from "@pie-players/pie-players-shared";
|
|
@@ -910,208 +1046,68 @@ sectionPlayerEl.runtime = {
|
|
|
910
1046
|
};
|
|
911
1047
|
```
|
|
912
1048
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
`
|
|
922
|
-
|
|
923
|
-
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
Canonical lifecycle stream (engine-routed, dispatched on the outer layout CE):
|
|
933
|
-
|
|
934
|
-
- `pie-stage-change` — single typed transition stream covering
|
|
935
|
-
`composed` → `engine-ready` → `interactive` → `disposed`. Payload is a
|
|
936
|
-
`StageChangeDetail`.
|
|
937
|
-
- `pie-loading-complete` — fires once per cohort, when the section's element
|
|
938
|
-
pre-warm resolves for the current composition and the item cards can mount
|
|
939
|
-
(kernel-routed).
|
|
940
|
-
- `framework-error` — canonical error event for any failure crossing the
|
|
941
|
-
framework boundary. Payload is a `FrameworkErrorModel`. The toolkit's
|
|
942
|
-
package-internal `FrameworkErrorBus` and the `onFrameworkError`
|
|
943
|
-
callback prop deliver each error exactly once regardless of wrapper
|
|
944
|
-
depth, including errors from a coordinator the host passes as
|
|
945
|
-
`runtime.coordinator`. The `framework-error` *DOM event* on the outer
|
|
946
|
-
layout CE also delivers each error exactly once: the kernel listener at
|
|
947
|
-
`<pie-section-player-base>` stops the toolkit's bubbled emit, and the
|
|
948
|
-
engine dispatches the error on the layout host without bubbling, so it
|
|
949
|
-
does not reach `document`. `tests/section-player-event-delivery.spec.ts`
|
|
950
|
-
pins these counts. Direct listeners attached to `<pie-assessment-toolkit>`
|
|
951
|
-
itself still see the toolkit's own emit.
|
|
952
|
-
|
|
953
|
-
Callback-prop mirrors with two-tier precedence (`runtime.<key>` wins over
|
|
954
|
-
the top-level prop):
|
|
955
|
-
|
|
956
|
-
- `onStageChange(detail)` — on every layout CE, `pie-section-player-base`,
|
|
957
|
-
and `pie-assessment-toolkit`.
|
|
958
|
-
- `onLoadingComplete(detail)` — on the kernel-backed layout CEs only
|
|
959
|
-
(split-pane / vertical / tabbed / kernel-host).
|
|
960
|
-
- `onFrameworkError(model)` — on every layout CE and
|
|
961
|
-
`pie-section-player-base`. Fires exactly once per error regardless of
|
|
962
|
-
wrapper depth (delivered through the package-internal
|
|
963
|
-
`FrameworkErrorBus`). The `framework-error` DOM event on the layout
|
|
964
|
-
CE host is also single-fire; consume either.
|
|
965
|
-
|
|
966
|
-
Section-player owned instrumentation stream:
|
|
967
|
-
|
|
968
|
-
- `pie-section-stage-change`
|
|
969
|
-
- `pie-section-loading-complete`
|
|
970
|
-
- `pie-section-framework-error`
|
|
971
|
-
- `pie-section-element-preload-retry`
|
|
972
|
-
- `pie-section-element-preload-error`
|
|
973
|
-
|
|
974
|
-
Build consumers against these canonical lifecycle events:
|
|
975
|
-
|
|
976
|
-
- `readiness-change` → listen for `pie-stage-change`. The readiness
|
|
977
|
-
payload is also available via `selectReadiness()` /
|
|
978
|
-
`getSnapshot().readiness` on the layout CE.
|
|
979
|
-
- `interaction-ready` → `pie-stage-change` filtered on
|
|
980
|
-
`detail.stage === "interactive"`.
|
|
981
|
-
- `ready` → `pie-loading-complete`.
|
|
982
|
-
- `section-controller-ready` → call
|
|
983
|
-
`waitForSectionController(timeoutMs)` or `getSectionController()`
|
|
984
|
-
on the layout CE, or filter `pie-stage-change` for
|
|
985
|
-
`detail.stage === "engine-ready"`.
|
|
986
|
-
|
|
987
|
-
If toolkit is mounted, toolkit lifecycle events are emitted on a separate
|
|
988
|
-
`pie-toolkit-*` stream. This separation avoids semantic overlap; bridge dedupe
|
|
989
|
-
is a defensive safety net only.
|
|
990
|
-
|
|
991
|
-
Toolkit tool/backend operational stream:
|
|
992
|
-
|
|
993
|
-
- `pie-tool-init-start|success|error`
|
|
994
|
-
- `pie-tool-backend-call-start|success|error`
|
|
995
|
-
- `pie-tool-library-load-start|success|error`
|
|
996
|
-
|
|
997
|
-
### Item session management
|
|
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.
|
|
998
1067
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
```ts
|
|
1002
|
-
const host = document.querySelector("pie-section-player-splitpane") as any;
|
|
1003
|
-
const controller = await host.waitForSectionController(5000);
|
|
1004
|
-
|
|
1005
|
-
// Read current section session snapshot.
|
|
1006
|
-
const currentSession = controller?.getSession?.();
|
|
1007
|
-
|
|
1008
|
-
// Replace section session state (resume from backend snapshot).
|
|
1009
|
-
await controller?.applySession?.({
|
|
1010
|
-
currentItemIndex: 0,
|
|
1011
|
-
visitedItemIdentifiers: ["q1"],
|
|
1012
|
-
itemSessions: {
|
|
1013
|
-
q1: {
|
|
1014
|
-
itemIdentifier: "q1",
|
|
1015
|
-
pieSessionId: "q1-session",
|
|
1016
|
-
session: { id: "q1-session", data: [{ id: "choice", value: "a" }] }
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
}, { mode: "replace" });
|
|
1020
|
-
|
|
1021
|
-
// Update a single item session directly.
|
|
1022
|
-
await controller?.updateItemSession?.("q1", {
|
|
1023
|
-
session: { id: "q1-session", data: [{ id: "choice", value: "b" }] },
|
|
1024
|
-
complete: true,
|
|
1025
|
-
});
|
|
1026
|
-
```
|
|
1027
|
-
|
|
1028
|
-
The same controller snapshot is what the persistence strategy saves/loads.
|
|
1029
|
-
When a controller is reused for the same `sectionId`/`attemptId`, `updateInput()` refreshes composition input while preserving in-memory section session data.
|
|
1030
|
-
|
|
1031
|
-
### Commit at a section boundary
|
|
1032
|
-
|
|
1033
|
-
A delivery element coalesces its `session-changed` dispatch, so a response the
|
|
1034
|
-
learner has finished entering can still be pending when the section moves. The
|
|
1035
|
-
controller commits pending element sessions before item navigation, before
|
|
1036
|
-
`updateInput()` snapshots the session for a section swap, and before `persist()`.
|
|
1037
|
-
The player also commits when an item shell tears down and when the page goes
|
|
1038
|
-
hidden.
|
|
1039
|
-
|
|
1040
|
-
A committed response travels like any other. A raw element `session-changed`
|
|
1041
|
-
does not leave its `<pie-item-shell>`, which re-dispatches it as the normalized
|
|
1042
|
-
`item-session-changed` (`PIE_ITEM_SESSION_CHANGED_EVENT`); the toolkit then
|
|
1043
|
-
publishes the section's canonical `session-changed`. Both bubble through the
|
|
1044
|
-
layout element to `document`, and a listener on either receives each dispatch
|
|
1045
|
-
once.
|
|
1046
|
-
Host code persists from the controller's events or from its session snapshot.
|
|
1047
|
-
|
|
1048
|
-
Each of those events marks a commit with `sessionCommitReason`
|
|
1049
|
-
(`"teardown" | "navigate" | "page-hidden"`): `item-session-changed`, the
|
|
1050
|
-
toolkit's `session-changed`, and the controller's `item-session-data-changed`
|
|
1051
|
-
and `item-session-meta-changed`. A commit at a section swap or item navigation
|
|
1052
|
-
reports the item being left, which the host may already have moved past, so a
|
|
1053
|
-
handler that sets its current item or navigation state from these events leaves
|
|
1054
|
-
that state alone on a commit and persists the commit's session as usual. The
|
|
1055
|
-
controller's events also carry `sectionId`, the section the item belongs to.
|
|
1056
|
-
|
|
1057
|
-
Navigation inside a section keeps every item mounted, so nothing is discarded
|
|
1058
|
-
and the element's own debounce would complete on its own. The commit still runs
|
|
1059
|
-
there because the response belongs to the item being left: a host that persists
|
|
1060
|
-
on the navigation event, or a `persist()` that follows it, would otherwise
|
|
1061
|
-
snapshot a session the learner had already changed.
|
|
1068
|
+
## Custom layout authoring
|
|
1062
1069
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
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.
|
|
1070
1076
|
|
|
1071
1077
|
## Content trust boundary
|
|
1072
1078
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
[
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
`<pie-item-player>` instance, so any field not recognized by the kernel is
|
|
1080
|
-
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:
|
|
1081
1085
|
|
|
1082
|
-
```
|
|
1086
|
+
```js
|
|
1083
1087
|
const runtime = {
|
|
1084
1088
|
playerType: "iife",
|
|
1085
1089
|
player: {
|
|
1086
|
-
trustMarkup: false, // default
|
|
1090
|
+
trustMarkup: false, // the default: DOMPurify stays on
|
|
1087
1091
|
// sanitizeMarkup: (html) => myCustomSanitize(html),
|
|
1088
1092
|
},
|
|
1089
1093
|
};
|
|
1090
1094
|
```
|
|
1091
1095
|
|
|
1092
|
-
Set `trustMarkup: true` only when
|
|
1093
|
-
|
|
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.
|
|
1094
1099
|
|
|
1095
1100
|
## Exports
|
|
1096
1101
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
- `@pie-players/pie-section-player/components/section-player-passage-card-element`
|
|
1107
|
-
- `@pie-players/pie-section-player/components/section-player-items-pane-element`
|
|
1108
|
-
- `@pie-players/pie-section-player/components/section-player-passages-pane-element`
|
|
1109
|
-
- `@pie-players/pie-section-player/contracts/layout-contract`
|
|
1110
|
-
- `@pie-players/pie-section-player/contracts/public-events`
|
|
1111
|
-
- `@pie-players/pie-section-player/contracts/runtime-host-contract`
|
|
1112
|
-
- `@pie-players/pie-section-player/contracts/layout-parity-metadata`
|
|
1113
|
-
- `@pie-players/pie-section-player/contracts/host-hooks`
|
|
1114
|
-
- `@pie-players/pie-section-player/policies`
|
|
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 |
|
|
1115
1111
|
|
|
1116
1112
|
## Development
|
|
1117
1113
|
|