@pie-players/pie-section-player 0.3.73 → 0.3.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +395 -149
  2. package/dist/browser/assets/evaluation-worker-CORp9m0K.js +156459 -0
  3. package/dist/browser/chunks/ItemToolBar-jx38w99s-58defccc.js +1 -0
  4. package/dist/browser/chunks/PUA-tLuTiQ7--f2b27953.js +1 -0
  5. package/dist/browser/chunks/ToolCoordinator-cc2dc319.js +1 -0
  6. package/dist/browser/chunks/accents-BTUYmhn9-cafeb96e.js +1 -0
  7. package/dist/browser/chunks/accents-b-i-CngHZsLL-e0eb98fd.js +1 -0
  8. package/dist/browser/chunks/arabic-BzEHMVZD-30ac1ade.js +1 -0
  9. package/dist/browser/chunks/arrows-VjxlU-jv-2e17c192.js +1 -0
  10. package/dist/browser/chunks/base-dfd67c41.js +1 -0
  11. package/dist/browser/chunks/braille-BeWC_Ruq-a3bb8ef9.js +1 -0
  12. package/dist/browser/chunks/braille-d-Dk6J96GO-d832bfbf.js +1 -0
  13. package/dist/browser/chunks/browser-a3c32178.js +37 -0
  14. package/dist/browser/chunks/calculator-element-271c142f.js +13 -0
  15. package/dist/browser/chunks/calligraphic-Cew9cU3x-04cf0735.js +1 -0
  16. package/dist/browser/chunks/catalog-context-46d3e50e.js +1 -0
  17. package/dist/browser/chunks/catalog-owner-d602f784.js +1 -0
  18. package/dist/browser/chunks/cherokee-Cny3FYho-4da7d2f3.js +1 -0
  19. package/dist/browser/chunks/classPrivateFieldGet2-338e0179.js +1 -0
  20. package/dist/browser/chunks/cyrillic-CfCDulXU-5550df0b.js +1 -0
  21. package/dist/browser/chunks/cyrillic-ss-lU7bNGG9-12eb0188.js +1 -0
  22. package/dist/browser/chunks/defineProperty-a20a667d.js +1 -0
  23. package/dist/browser/chunks/devanagari-CDIz2h-r-18c38725.js +1 -0
  24. package/dist/browser/chunks/dist-41b39e7f.js +1 -0
  25. package/dist/browser/chunks/dist-59132a53.js +1 -0
  26. package/dist/browser/chunks/dist-60d7afef.js +1 -0
  27. package/dist/browser/chunks/dist-ca9b9622.js +1 -0
  28. package/dist/browser/chunks/double-struck-BpJEQk8y-500ce8dd.js +1 -0
  29. package/dist/browser/chunks/en-5ff22e17.js +1 -0
  30. package/dist/browser/chunks/es-a0ba6cb6.js +1 -0
  31. package/dist/browser/chunks/focus-trap-44913bf3.js +1 -0
  32. package/dist/browser/chunks/fraktur-rLEq9DEH-44cc14d8.js +1 -0
  33. package/dist/browser/chunks/greek-BlraJTyL-90ce8924.js +1 -0
  34. package/dist/browser/chunks/greek-ss-D-EN2PK0-91610739.js +1 -0
  35. package/dist/browser/chunks/hebrew-GiQZeybp-b583f555.js +1 -0
  36. package/dist/browser/chunks/latin-b-BhEnC7R2-d13e06ff.js +1 -0
  37. package/dist/browser/chunks/latin-bi-DqiIKv3U-1dec8705.js +1 -0
  38. package/dist/browser/chunks/latin-i-B1xtwaE1-21637466.js +1 -0
  39. package/dist/browser/chunks/latin-xfWFlLkD-5bcea5cf.js +1 -0
  40. package/dist/browser/chunks/logger-8b07fb72.js +1 -0
  41. package/dist/browser/chunks/marrows-DD1NgMQi-15b63755.js +1 -0
  42. package/dist/browser/chunks/math-DpWn1mbG-38eeb914.js +1 -0
  43. package/dist/browser/chunks/math-rendering-ready-9a0fd90a.js +1 -0
  44. package/dist/browser/chunks/mathjax-BKrHE4xs-3ca05bf6.js +232 -0
  45. package/dist/browser/chunks/module-0191e321.js +74 -0
  46. package/dist/browser/chunks/module-shim-58659a86.js +0 -0
  47. package/dist/browser/chunks/module-shim-b4470740.js +12 -0
  48. package/dist/browser/chunks/monospace-D116QD2a-98d484fd.js +1 -0
  49. package/dist/browser/chunks/monospace-ex-DcXpouKS-456b403c.js +1 -0
  50. package/dist/browser/chunks/monospace-l-Ct5wkMDs-f34a4a9e.js +1 -0
  51. package/dist/browser/chunks/mshapes-DCp86WSX-c0055952.js +1 -0
  52. package/dist/browser/chunks/nl-NL-364d61b9.js +1 -0
  53. package/dist/browser/chunks/nl-NL-8b7e6d4b.js +1 -0
  54. package/dist/browser/chunks/overlay-containment-edaaff13.js +1 -0
  55. package/dist/browser/chunks/overlay-placement-e8058232.js +1 -0
  56. package/dist/browser/chunks/phonetics-BwOCw2Y9-d2e3af59.js +1 -0
  57. package/dist/browser/chunks/phonetics-ss-CRtMn02f-579b6135.js +1 -0
  58. package/dist/browser/chunks/pie-api-client-687c319a.js +1 -0
  59. package/dist/browser/chunks/pointer-gesture-ce75f0a7.js +1 -0
  60. package/dist/browser/chunks/preload-helper-6ad28da3.js +1 -0
  61. package/dist/browser/chunks/provider-4c704118.js +1 -0
  62. package/dist/browser/chunks/rolldown-runtime-1287ba31.js +1 -0
  63. package/dist/browser/chunks/runtime-wcU7KOFU-ac86d7b8.js +8536 -0
  64. package/dist/browser/chunks/sanitize-svg-icon-b9082bba.js +3 -0
  65. package/dist/browser/chunks/sans-serif-BYPZKlE9-2efdbcd9.js +1 -0
  66. package/dist/browser/chunks/sans-serif-b-B0kRKiC--b5530dce.js +1 -0
  67. package/dist/browser/chunks/sans-serif-bi-DXWul6mu-5370dba4.js +1 -0
  68. package/dist/browser/chunks/sans-serif-ex-Boux1kMP-aa178f31.js +1 -0
  69. package/dist/browser/chunks/sans-serif-i-676RFaSO-2db54474.js +1 -0
  70. package/dist/browser/chunks/sans-serif-r-DrN9xdZt-0fc72eea.js +1 -0
  71. package/dist/browser/chunks/script-0ohehnXc-526904f4.js +1 -0
  72. package/dist/browser/chunks/shapes-BG47NodJ-d59b0eca.js +1 -0
  73. package/dist/browser/chunks/src-BcptuNS5-ed25d7fd.js +191 -0
  74. package/dist/browser/chunks/sre-0162a7a9.js +53 -0
  75. package/dist/browser/chunks/sre-engine-ace23548.js +1 -0
  76. package/dist/browser/chunks/sre-engine-jd8cc08x-42d270e5.js +1 -0
  77. package/dist/browser/chunks/sre-locales-112320af.js +1 -0
  78. package/dist/browser/chunks/symbols-BqnUxf_t-83087e87.js +1 -0
  79. package/dist/browser/chunks/symbols-b-i-eg9k5hmF-9c461ac2.js +1 -0
  80. package/dist/browser/chunks/tool-annotation-toolbar-997438b4.js +249 -0
  81. package/dist/browser/chunks/tool-answer-eliminator-35483abd.js +13 -0
  82. package/dist/browser/chunks/tool-color-scheme-b7de5cdf.js +26 -0
  83. package/dist/browser/chunks/tool-dictionary-cecff3b4.js +7 -0
  84. package/dist/browser/chunks/tool-graph-cb0338e8.js +11 -0
  85. package/dist/browser/chunks/tool-host-contract-b93b482c.js +1 -0
  86. package/dist/browser/chunks/tool-instance-id-2a92deb0.js +1 -0
  87. package/dist/browser/chunks/tool-line-reader-f8ef3c6a.js +62 -0
  88. package/dist/browser/chunks/tool-periodic-table-43b133b8.js +153 -0
  89. package/dist/browser/chunks/tool-picture-dictionary-33721e91.js +4 -0
  90. package/dist/browser/chunks/tool-protractor-23f77028.js +17 -0
  91. package/dist/browser/chunks/tool-ruler-1d9a03a6.js +15 -0
  92. package/dist/browser/chunks/tool-tts-inline-98c2ce7a.js +179 -0
  93. package/dist/browser/chunks/tts-runtime-config-a7ab1ad4.js +194 -0
  94. package/dist/browser/chunks/variants-2LRNhOUP-966fc49d.js +1 -0
  95. package/dist/browser/chunks/versioned-tag-54a76be9.js +1 -0
  96. package/dist/browser/pie-section-player.js +1727 -0
  97. package/dist/browser.d.ts +10 -0
  98. package/dist/chunks/ItemToolBar-jx38w99s-1287ba31.js +28 -0
  99. package/dist/chunks/module-shim-58659a86.js +2129 -0
  100. package/dist/chunks/{nl-NL-9312c316.js → nl-NL-364d61b9.js} +9 -26
  101. package/dist/chunks/sre-engine-jd8cc08x-42d270e5.js +8 -0
  102. package/dist/components/shared/composition.d.ts +3 -2
  103. package/dist/components/shared/framework-error-latch.d.ts +32 -0
  104. package/dist/components/shared/host-element.d.ts +10 -0
  105. package/dist/components/shared/kernel-host-default-body.d.ts +14 -0
  106. package/dist/components/shared/layout-host-methods.d.ts +385 -0
  107. package/dist/components/shared/missing-tool-providers.d.ts +16 -0
  108. package/dist/components/shared/player-preload.d.ts +14 -10
  109. package/dist/components/shared/section-player-backend-delivery.d.ts +1 -1
  110. package/dist/components/shared/section-player-card-context.d.ts +5 -1
  111. package/dist/components/shared/section-player-host-runtime.d.ts +32 -19
  112. package/dist/components/shared/section-player-layout-context.d.ts +77 -0
  113. package/dist/components/shared/section-player-own-event.d.ts +10 -0
  114. package/dist/components/shared/section-player-runtime-callbacks.d.ts +1 -1
  115. package/dist/components/shared/section-player-shell-layout.svelte.d.ts +3 -11
  116. package/dist/components/shared/section-player-view-state.d.ts +6 -1
  117. package/dist/components/shared/toolkit-ready-wait.d.ts +16 -0
  118. package/dist/contracts/host-hooks.d.ts +4 -12
  119. package/dist/contracts/host-hooks.js +0 -0
  120. package/dist/contracts/runtime-host-contract.d.ts +0 -5
  121. package/dist/contracts/runtime-host-contract.js +0 -0
  122. package/dist/controllers/SectionController.d.ts +14 -0
  123. package/dist/controllers/toolkit-section-contracts.d.ts +1 -17
  124. package/dist/controllers/types.d.ts +25 -11
  125. package/dist/item-section/index.d.ts +22 -0
  126. package/dist/item-section/index.js +37 -0
  127. package/dist/pie-section-player.d.ts +4 -6
  128. package/dist/pie-section-player.js +22006 -41076
  129. package/dist/policies/index.d.ts +5 -16
  130. package/dist/policies/index.js +17 -0
  131. package/dist/policies/types.d.ts +0 -4
  132. package/package.json +33 -68
  133. package/dist/chunks/module-a516757f.js +0 -73997
  134. package/dist/chunks/nl-NL-ef6dd604.js +0 -505
  135. package/dist/chunks/pie-api-client-22de9d9c.js +0 -168
  136. package/dist/chunks/rolldown-runtime-1287ba31.js +0 -20
  137. package/dist/chunks/sre-0162a7a9.js +0 -17637
  138. package/dist/components/item-shell-element.d.ts +0 -1
  139. package/dist/components/shared/shell-registration.d.ts +0 -55
  140. package/dist/contracts/layout-contract.d.ts +0 -17
  141. package/dist/contracts/layout-parity-metadata.d.ts +0 -4
  142. package/dist/contracts/public-events.d.ts +0 -93
  143. package/dist/contracts/stages.d.ts +0 -10
  144. package/dist/policies/guards.d.ts +0 -2
  145. package/dist/utils/z-index.d.ts +0 -5
package/README.md CHANGED
@@ -6,7 +6,9 @@ Section rendering package with layout custom elements:
6
6
  - `pie-section-player-vertical`
7
7
  - `pie-section-player-tabbed`
8
8
 
9
- Use the layout custom elements listed above for section rendering.
9
+ A host that arranges the section itself builds its layout from
10
+ `pie-section-player-kernel-host` and the two panes; see
11
+ [Custom layout authoring](#custom-layout-authoring).
10
12
 
11
13
  ## Install
12
14
 
@@ -33,11 +35,16 @@ opt-out.
33
35
  import "@pie-players/pie-section-player";
34
36
  ```
35
37
 
36
- If hosts need explicit registration control, keep using documented component
37
- entrypoints under `@pie-players/pie-section-player/components/*`.
38
+ The entrypoints under `@pie-players/pie-section-player/components/*` load one
39
+ element each, together with every element it renders: a layout entry also
40
+ registers the cards, panes and shell. They choose which layouts a host loads; the
41
+ tag names stay fixed.
38
42
 
39
- Standalone browser variants for this package are intentionally deferred; the
40
- current supported contract is the default bundler entrypoints under `dist`.
43
+ The entries are bundler-only: they import `@pie-players/pie-item-player`,
44
+ `@pie-players/pie-default-tool-loaders` and `speech-rule-engine`, with the
45
+ engine's JSON locale tables, by bare specifier and without import attributes.
46
+ Items render through the host's one `@pie-players/pie-item-player`, which this
47
+ package depends on at its own version.
41
48
 
42
49
  ## SectionController
43
50
 
@@ -59,17 +66,40 @@ the per-method contract.
59
66
 
60
67
  ```ts
61
68
  const host = document.querySelector("pie-section-player-splitpane") as any;
62
- const controller = await host.waitForSectionController?.(5000);
69
+ const controller = await host.waitForSectionController(5000);
63
70
  ```
64
71
 
65
72
  `waitForSectionController(timeoutMs)` resolves when the layout CE has wired
66
73
  its controller (the same anchor `pie-stage-change` reaches with
67
- `detail.stage === "engine-ready"`). Use `getSectionController()` if you've
68
- already passed the readiness anchor synchronously.
74
+ `detail.stage === "engine-ready"`), or with `null` once `timeoutMs` passes.
75
+ Use `getSectionController()` if you've already passed the readiness anchor
76
+ synchronously.
77
+
78
+ The layout elements define their host methods from the moment they are
79
+ created, so a host can call them before the element mounts. Until it mounts,
80
+ `waitForSectionController` waits, `getSectionController()` returns `null`, the
81
+ navigation methods return `false`, and `getSnapshot()` and the `select*` reads
82
+ return `null` (`pie-section-player-kernel-host` returns its bootstrapping
83
+ snapshot).
69
84
 
70
85
  ### Session lifecycle
71
86
 
72
- A typical host flow:
87
+ Set `session` with `section` to restore a section. The controller created for
88
+ that section applies it in replace mode in place of hydrating from the
89
+ persistence strategy, before the controller is published, so the first
90
+ composition, `engine-ready` and every reader see it. The strategy still receives
91
+ every `persist()`.
92
+
93
+ A later assignment applies through `applySession(value, { mode: "replace" })`
94
+ under the rules `<pie-item-player>` applies to its `session`: a value equal to
95
+ the current session is a no-op, so echoing `session-changed` back is safe, and an
96
+ item session with neither a response value nor a response field leaves one that
97
+ holds responses in place. `null` after creation is a no-op. A `section` change
98
+ without a `session` assignment hydrates the new controller from the strategy.
99
+ Reading `session` returns the assigned value until the controller is published,
100
+ and a fresh `getSession()` snapshot after.
101
+
102
+ Driving the controller directly:
73
103
 
74
104
  ```ts
75
105
  controller?.configureSessionPersistence?.({ context, strategy });
@@ -97,11 +127,25 @@ policy](#js-api-example-for-advanced-host-policy). Key event types:
97
127
 
98
128
  - `item-selected` — item navigation within the current section.
99
129
  - `item-session-data-changed` / `item-session-meta-changed` — per-item
100
- session updates the persistence layer should observe.
101
- - `content-loaded` — passage / item / rubric finished loading.
130
+ session updates the persistence layer should observe. `sectionId` names the
131
+ section. `elementId` names the reporting element when the change carries one,
132
+ and `complete` is then that element's own. A commit carries
133
+ `sessionCommitReason` (see [Commit at a section
134
+ boundary](#commit-at-a-section-boundary)).
135
+ - `item-complete-changed` — an item's completion flipped. An item is complete
136
+ when every element that has reported its completion is complete. A report
137
+ without `elementId`, and a restored session's item-level `complete`, set the
138
+ item's completion directly. A session restored through `applySession`
139
+ without `complete` is complete when it holds a response. A passage's own
140
+ `session-changed` stays inside its shell: a passage holds no response.
141
+ - `content-loaded` — passage / item / rubric finished loading. Carries
142
+ `contentKind`, `itemId`, and `canonicalItemId`.
102
143
  - `section-loading-complete` — every renderable in the section finished
103
144
  loading.
104
145
  - `section-items-complete-changed` — aggregate completion flip.
146
+ - `section-error` — an item player failed (`source: "item-player"`), or the
147
+ section runtime did (`source: "section-runtime"`), a rejected element warmup
148
+ included, which leaves the items unmounted.
105
149
  - `section-navigation-change` — the controller's section identity changed.
106
150
  - `formative-try-recorded` — a learner checked an answer.
107
151
  - `formative-reveal-changed` — the reveal state changed without a Try: a learner
@@ -119,6 +163,20 @@ policy](#js-api-example-for-advanced-host-policy). Key event types:
119
163
  - `timed-media-invalid` — authored `timedMedia` that cannot be delivered; the
120
164
  section renders without cue behavior.
121
165
 
166
+ The helpers' default event sets leave out `formative-try-recorded`,
167
+ `formative-reveal-changed`, `section-mastery-changed` and
168
+ `timed-media-audio-started`. Name them in `subscribeItemEvents({ eventTypes })`,
169
+ or subscribe through `subscribeSectionEvents`.
170
+
171
+ Item-scoped events carry both id forms, and both are always populated.
172
+ `itemId` is the bare `item.id` — the form the map returned by
173
+ `getItemSessionsByItemId()` is keyed by, and the form `applySession` expects.
174
+ `canonicalItemId` is that item's adapter identifier, and falls back to `itemId`
175
+ when the section was not built from an adapter or no adapter ref matches it.
176
+ Correlate with formative policy and `runtime.player.resolveBackend` by
177
+ `canonicalItemId`; reach the session by `itemId`. `content-loaded` and
178
+ `item-player-error` both carry the pair.
179
+
122
180
  ### Formative delivery
123
181
 
124
182
  Set `formative` on the section and the player renders a check-answer control per
@@ -145,12 +203,12 @@ projected over the section env — with `role: "instructor"` under
145
203
  `feedback: "solution"` — and the element draws the rest. Only that item's env
146
204
  changes; its neighbours stay editable. A retry withdraws the projection.
147
205
 
148
- Read the resolved state from `getFormativeProjection()`, or off
149
- `compositionModel.formative` in a custom layout. Drive it from a host through the
150
- same handle:
206
+ Read the resolved state from `getFormativeProjection()`, or from
207
+ `composition.formative` in a `composition-changed` event's detail. Drive it from a
208
+ host through the same handle:
151
209
 
152
210
  ```ts
153
- const controller = await host.waitForSectionController?.(5000);
211
+ const controller = await host.waitForSectionController(5000);
154
212
  // The learner's actions, budget-respecting.
155
213
  controller?.recordFormativeTry?.({ itemId, outcomes }); // outcomes from provideScore()
156
214
  controller?.retryFormativeItem?.({ itemId });
@@ -225,7 +283,7 @@ with its own player registers its own port instead, and that port outranks the
225
283
  card's discovery for as long as it is attached:
226
284
 
227
285
  ```ts
228
- const controller = await host.waitForSectionController?.(5000);
286
+ const controller = await host.waitForSectionController(5000);
229
287
  // No `renderableId`: a host is asserting its own port, where a renderable's adapter
230
288
  // has to name itself and is ignored unless it is the resolved stimulus.
231
289
  controller?.attachMediaTimeSource?.(myThirdPartyPort);
@@ -264,14 +322,10 @@ for the contract and the decision record.
264
322
 
265
323
  ## Usage
266
324
 
267
- Import the custom-element registration entrypoint in consumers:
325
+ Import the package root, which registers every section-player element:
268
326
 
269
327
  ```ts
270
- import '@pie-players/pie-section-player/components/section-player-splitpane-element';
271
- import '@pie-players/pie-section-player/components/section-player-vertical-element';
272
- import '@pie-players/pie-section-player/components/section-player-tabbed-element';
273
- import '@pie-players/pie-section-player/components/section-player-item-card-element';
274
- import '@pie-players/pie-section-player/components/section-player-passage-card-element';
328
+ import '@pie-players/pie-section-player';
275
329
  ```
276
330
 
277
331
  Render in HTML/Svelte/JSX:
@@ -280,29 +334,51 @@ Render in HTML/Svelte/JSX:
280
334
  <pie-section-player-splitpane></pie-section-player-splitpane>
281
335
  ```
282
336
 
283
- Set complex values (`runtime`, `section`, `env`) as JS properties.
337
+ Set complex values (`runtime`, `section`, `session`) as JS properties. `env` is a
338
+ `runtime` field (`runtime.env`); the layout elements have no `env` property.
339
+
340
+ Set `runtime` no later than `section`. When the player builds its own
341
+ coordinator, the section's arrival rebuilds that coordinator from the current
342
+ `runtime`, so both can be set a tick after the element mounts. Once the section
343
+ has initialized, a change to `runtime.tools`, `runtime.assessmentId`,
344
+ `runtime.accessibility`, `runtime.lazyInit`, `tool-config-strictness` or
345
+ `toolRegistry` is reported once in the console and does not reach that
346
+ coordinator; `runtime.tools.pnpEnforcement` still applies. Change a running
347
+ coordinator through the one `toolkit-ready` carries, with
348
+ `updateToolConfig(...)` or `updateToolsPlacement(...)`, or pass your own as
349
+ `runtime.coordinator`.
284
350
 
285
351
  ## Runtime Inputs
286
352
 
287
- Both layout elements support:
353
+ The layout elements (`pie-section-player-splitpane`,
354
+ `pie-section-player-vertical`, `pie-section-player-tabbed`) support:
288
355
 
289
356
  - `runtime` (object): primary coordinator/tools/player runtime bundle
290
357
  - `section` (object): assessment section payload
291
- - `env` (object): optional top-level override for `{ mode, role }`
358
+ - `session` (object, JS property only): the section's session, a `SectionControllerSessionState`; see [Session lifecycle](#session-lifecycle)
359
+ - `assessment` (object, JS property only): the `AssessmentEntity` whose `personalNeedsProfile` and `settings` tool policy reads, forwarded to the 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
292
360
  - `debug` (boolean-like): verbose debug logging control (`"true"` enables, `"false"`/`"0"` disables)
293
361
  - `toolbar-position` (string): `top|right|bottom|left|none`
294
362
  - `narrow-layout-breakpoint` (number, optional): viewport width in px below which the layout collapses (split pane: single column; vertical: toolbar moves to top). Clamped to 400–2000; default 1100.
295
363
  - `content-max-width-no-passage` (number, optional): max width in px when no passages exist. Clamped to 320–2200. Unset by default (layout uses available width).
296
364
  - `content-max-width-with-passage` (number, optional): max width in px when passages are present. Clamped to 320–2200. Unset by default (layout uses available width).
297
- - `split-pane-min-region-width` (number, optional): splitpane minimum pane width in px. Clamped to 160–1200. Unset by default (split bounds stay at 20–80). (Ignored by vertical layout; supported for API parity.)
298
- - `split-pane-collapse-strategy` (string, optional): splitpane stacked-mode strategy. Supported values: `tabbed` (default) and `vertical`. (Ignored by vertical/tabbed layouts; supported for API parity.)
365
+ - `split-pane-min-region-width` (number, optional): splitpane minimum pane width in px. Clamped to 160–1200. Unset by default (split bounds stay at 20–80). Splitpane only.
366
+ - `split-pane-collapse-strategy` (string, optional): splitpane stacked-mode strategy. Supported values: `tabbed` (default) and `vertical`. Splitpane only.
299
367
  - `base-heading-level` (number, optional): the heading level this player's card headings occupy, and the level every descendant's outline derives from. Clamped to 1–6; default 2. See [Heading structure](#heading-structure).
300
- - `show-toolbar` (boolean-like): accepts `true/false` and common string forms (`"true"`, `"false"`, `"1"`, `"0"`, `"yes"`, `"no"`)
368
+ - `show-toolbar` (boolean-like): accepts `true/false` and common string forms (`"true"`, `"false"`, `"1"`, `"0"`, `"yes"`, `"no"`); default `false`, so tools placed at `section` level render only when it is `true`
369
+ - `locale` (string, optional): BCP-47 locale for the player's own interface text. Unset renders `en-US`.
370
+ - `runtime.contentLanguage` (string, optional, `runtime` only): BCP-47 language of the content where its markup names none, which read-aloud speaks in and picks catalog cards by. A `lang` between the content and its card wins; unset reads `en-US`. `locale` never sets it.
371
+ - `nds-icons` (boolean): opt in to NDS icon buttons.
372
+ - `tool-config-strictness` (string, optional): `off|warn|error` for tool-config validation; default `error`.
373
+ - `split-pane-initial-passage-width` (number, optional): splitpane passage pane width in percent at mount. Clamped to 20–80; default 50. Splitpane only.
374
+ - `iife-bundle-host` (string, optional): bundle host for the IIFE element pre-warm when `runtime.player.loaderOptions.bundleHost` is unset.
301
375
  - Host extension props (JS properties only): `toolRegistry`, `sectionHostButtons`, `itemHostButtons`, `passageHostButtons`, `hooks`
302
376
 
303
- When viewport width is within the collapsed range (~1100px and below), splitpane and
304
- vertical layout hosts normalize section toolbar placement to `top`. This includes
305
- `left`, `right`, `bottom`, and `none` values.
377
+ When the viewport is no wider than `narrow-layout-breakpoint` (default 1100px),
378
+ splitpane and vertical layout hosts normalize section toolbar placement to `top`.
379
+ This includes `left`, `right`, `bottom`, and `none` values. Separately, the shell
380
+ moves a `left` or `right` toolbar to `top` at a fixed 1100px, so with a smaller
381
+ breakpoint side toolbars still move to the top from 1100px down.
306
382
 
307
383
  `hooks.cardTitleFormatter` remains active across responsive splitpane transitions (split -> stacked and stacked -> split), because title rendering is provided through shared card context rather than layout-specific state.
308
384
 
@@ -384,10 +460,7 @@ hooks for theming:
384
460
  - `pie-section-player-tab--active`
385
461
  - `pie-section-player-tab-panel`
386
462
 
387
- For theme compatibility with existing passage-label patterns, tabs also expose:
388
-
389
- - `data-pie-purpose="passage-label"` and alias class `passage-label`
390
- - `data-pie-purpose="item-label"` and alias class `item-label`
463
+ Tabs also carry `data-pie-purpose="passage-label"` and `data-pie-purpose="item-label"`.
391
464
 
392
465
  Tab colors, spacing, and track geometry can be themed via CSS variables:
393
466
  `--pie-section-player-tab-color`, `--pie-section-player-tab-background`,
@@ -503,14 +576,14 @@ mountable-but-unoccupied result, not an error.
503
576
  The intended usage model is:
504
577
 
505
578
  - **CE props for default/standard flows (roughly 90% use cases)**:
506
- - `assessment-id`, `section`, `section-id`, `attempt-id`, `debug`
579
+ - `section`, `section-id`, `attempt-id`, `debug`, `locale`, `nds-icons`, `tool-config-strictness`
507
580
  - `show-toolbar`, `toolbar-position`, `narrow-layout-breakpoint`
508
- - `content-max-width-no-passage`, `content-max-width-with-passage`, `split-pane-min-region-width`, `split-pane-collapse-strategy`
581
+ - `content-max-width-no-passage`, `content-max-width-with-passage`, and on splitpane `split-pane-min-region-width`, `split-pane-collapse-strategy`
509
582
  - **JS API for advanced customization**:
510
583
  - Get the controller handle via `getSectionController()` or `waitForSectionController()` (preferred)
511
584
  - Listen for `pie-stage-change` and filter on `detail.stage === "engine-ready"` for an event-driven entry point
512
585
  - Apply custom policy/gating in host code (for example, domain-specific `canNext` based on controller events like `section-items-complete-changed`)
513
- - Compose forward/backward eligibility in host code using `selectNavigation()` + host state; there is intentionally no separate parallel CE gating API for this
586
+ - Compose forward/backward eligibility in host code using `getSnapshot().navigation` + host state; there is intentionally no separate parallel CE gating API for this
514
587
  - Inject custom toolbar tooling with `toolRegistry` and optional host button arrays (`sectionHostButtons`, `itemHostButtons`, `passageHostButtons`)
515
588
  - Register host callbacks via `hooks` (for example `hooks.cardTitleFormatter`)
516
589
 
@@ -534,9 +607,9 @@ Advanced runtime configuration is supplied through the `runtime` object. Set pla
534
607
 
535
608
  Hosts can configure item-player backend delivery once at the section-player
536
609
  runtime level. Section-player derives a concrete `backend` prop for each
537
- embedded item player before it renders the item. This is intended for hosts such
538
- as Quiz Engine that need server-processed PIE models and server scoring without
539
- querying every nested `<pie-item-player>`.
610
+ embedded item player before it renders the item. This is intended for hosts
611
+ that need server-processed PIE models and server scoring without querying every
612
+ nested `<pie-item-player>`.
540
613
 
541
614
  ```ts
542
615
  import type {
@@ -577,6 +650,10 @@ each embedded item player. Static delivery fields such as `baseUrl`, `auth`,
577
650
  `endpoints`, `assignmentId`, and `autosave` are preserved. Use `assignmentId`
578
651
  for shared attempt/player identity.
579
652
 
653
+ An enabled `backend.delivery` also sets `hosted: true` on each embedded item
654
+ player unless `runtime.player.hosted` is set, so the item players load no
655
+ element controllers and render the models the server returns.
656
+
580
657
  `runtime.player.resolveBackend` is a section-player-reserved key. It is called
581
658
  with `{ itemId, canonicalItemId, item, itemIndex, itemSession, sectionId, env,
582
659
  baseBackend }` and is stripped before props reach `<pie-item-player>`. Use it
@@ -594,12 +671,73 @@ backend config is preserved.
594
671
  This backend delivery config is separate from the element-loader backend used
595
672
  for IIFE/ESM bundle preloading.
596
673
 
674
+ ### Preloaded elements
675
+
676
+ With `runtime.playerType: "preloaded"` the section player loads no element
677
+ code. Its pre-warm asserts that every tag the section's items and passages name
678
+ is registered, so the host registers the elements first:
679
+
680
+ ```ts
681
+ import { registerPreloadedElements } from "@pie-players/pie-item-player/preloaded";
682
+ import * as delivery from "@pie-element/multiple-choice/browser/delivery";
683
+ import * as controller from "@pie-element/multiple-choice/browser/controller";
684
+ import manifest from "../package.json"; // pins "@pie-element/multiple-choice" exactly
685
+
686
+ registerPreloadedElements(
687
+ [
688
+ {
689
+ tag: "pie-element-multiple-choice",
690
+ package: "@pie-element/multiple-choice",
691
+ version: manifest.dependencies["@pie-element/multiple-choice"],
692
+ element: delivery,
693
+ controller,
694
+ },
695
+ ],
696
+ { math: { assetRoot: "https://assets.example.com/npm" } },
697
+ );
698
+
699
+ sectionPlayer.runtime = { ...sectionPlayer.runtime, playerType: "preloaded" };
700
+ ```
701
+
702
+ - Register before the section player mounts. A tag missing at pre-warm leaves
703
+ the items unmounted and raises a non-recoverable `element-preload` framework
704
+ error.
705
+ - Install element packages with `npm install --save-exact`. npm otherwise saves
706
+ a caret range, which registration rejects as a `version`, and which a fresh
707
+ install can resolve to another release line: `^13.4.0-next.15` resolves to
708
+ the legacy `13.4.4`, which has no `./browser/*` modules.
709
+ - Install every pie-elements-ng package from one release, in one install from
710
+ the same dist-tag, and upgrade them together. Elements whose `./browser/*`
711
+ builds typeset on `window.MathJax` share the MathJax the first of them loads,
712
+ in the build and configuration of that element's release, so in a mixed set
713
+ an element can typeset with a MathJax it was not built for. Elements that
714
+ bundle their own MathJax share none
715
+ ([One MathJax version per page](../../docs/item-player/loading-strategies.md#one-mathjax-version-per-page)).
716
+ - Pass `math.assetRoot`, an npm root serving the fonts and speech the elements'
717
+ bundled MathJax loads, or `math.assetUrls`, each file's URL; without either,
718
+ elements on adapter 0.1.3 or later render without web fonts and speech
719
+ ([MathJax assets](../../docs/item-player/loading-strategies.md#mathjax-assets)).
720
+ - Register one version per package. The players align every authored version
721
+ of a package to the registered one, and registering a second version throws.
722
+ - Register each package's `controller` unless the item players are hosted
723
+ (`runtime.player.hosted`, or an enabled `runtime.player.backend.delivery`).
724
+ A player that is not hosted runs `model()` in the browser and warns for each
725
+ tag registered without one.
726
+ - Only `@pie-element/*` builds from pie-elements-ng publish
727
+ `./browser/delivery` and `./browser/controller`.
728
+ - Under TypeScript, the `package.json` import needs `resolveJsonModule`, and a
729
+ package version that ships no declarations for `./browser/*` needs a
730
+ `declare module` shim for those subpaths.
731
+
732
+ See [`strategy="preloaded"`](../../docs/item-player/loading-strategies.md#strategypreloaded)
733
+ for the registration contract.
734
+
597
735
  ### Host-owned focus
598
736
 
599
737
  Section-player does not move focus on behalf of host-level affordances such
600
738
  as "Skip to Main", nor does it make passage/question containers tab stops.
601
739
  Hosts own page chrome, skip links, landmarks, and any special focus placement.
602
- For example, Quiz Engine's Fixed Player shell can focus its own
740
+ For example, a host shell can focus its own
603
741
  `main#main-content`; the next Tab then follows the browser's natural order
604
742
  into the first actionable control rendered inside the section player.
605
743
 
@@ -615,11 +753,16 @@ sequential keyboard navigation.
615
753
  </main>
616
754
  ```
617
755
 
618
- **Wired policy toggles.** Each `SectionPlayerPolicies` field has a real
619
- runtime effect; nothing in this surface is decorative.
756
+ **Policy fields.** The layout accepts a partial `policies` object: every
757
+ unset field, a missing section included, takes its value from
758
+ `DEFAULT_SECTION_PLAYER_POLICIES`.
620
759
 
621
- - `readiness.mode` (`"progressive"` | `"strict"`) — drives readiness-event
622
- emission via `createReadinessDetail` in `SectionPlayerLayoutKernel`.
760
+ - `readiness.mode` (`"progressive"` | `"strict"`) — the mode the layout
761
+ kernel gates readiness by. Both modes
762
+ hold the `interactive` stage and `pie-loading-complete` until the section's
763
+ element pre-warm resolves and the item cards can mount. The kernel has no
764
+ later loading signal, so the two modes currently emit the same sequence.
765
+ Default: `"progressive"`.
623
766
  - `preload.enabled` — when `false`, `SectionItemsPane` short-circuits the
624
767
  section-level element warmup pipeline (`warmupSectionElements`). Items
625
768
  still mount and item-players register their own elements on demand. Use
@@ -631,10 +774,8 @@ runtime effect; nothing in this surface is decorative.
631
774
  shape of opt-out can still override `runtime.player.loaderConfig.instrumentationProvider`.
632
775
  Default: `true`.
633
776
 
634
- The exported `isPreloadEnabled(policies)` and `isTelemetryEnabled(policies)`
635
- helpers read these toggles with the documented default-true semantics, so
636
- host code that needs to mirror the same gate (e.g. when composing a custom
637
- layout host) can call them directly.
777
+ `resolveSectionPlayerPolicies(policies)` returns the filled-in object, for
778
+ host code that needs the same gates.
638
779
 
639
780
  ### Navigation signals
640
781
 
@@ -643,20 +784,20 @@ layout host) can call them directly.
643
784
 
644
785
  Runtime configuration is explicit:
645
786
 
646
- - `runtime` owns runtime fields (`assessmentId`, `playerType`, `player`, `lazyInit`, `tools`, `accessibility`, `coordinator`, `isolation`, `env`, `createSectionController`).
787
+ - `runtime` owns runtime fields (`assessmentId`, `playerType`, `player`, `lazyInit`, `tools`, `toolContextResolvers`, `accessibility`, `coordinator`, `createSectionController`, `isolation`, `env`, `contentLanguage` and the `on*` callbacks). `locale`, `nds-icons` and `tool-config-strictness` are attributes only.
647
788
  - Tool placement is configured through `runtime.tools.placement.section`, `runtime.tools.placement.item`, and `runtime.tools.placement.passage`.
648
- - 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.
789
+ - Tool configuration validation is canonical in toolkit initialization (`pie-assessment-toolkit`), including toolbar overlays. Use the `tool-config-strictness` attribute (`off` | `warn` | `error`) to control warning-only vs fail-fast behavior.
649
790
  - TTS provider config must use `tools.providers.textToSpeech` (canonical). `tools.providers.tts` is rejected by validation.
650
- - Host tool overrides are additive:
651
- - `toolRegistry` overrides the default toolbar registry when provided
791
+ - Host tool overrides:
792
+ - `toolRegistry` replaces the default toolbar registry when provided. Build it with `createPackagedToolRegistry({ toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS })` and register custom tools on it, since toolbars load each tool's element through the registry's loaders. A player that builds its own coordinator gives it this registry; a coordinator passed as `runtime.coordinator` keeps its own, which decides policy, so build that coordinator with the same registry
652
793
  - host buttons are appended per toolbar scope via `sectionHostButtons`, `itemHostButtons`, `passageHostButtons`
653
794
 
654
- Debug logging can be controlled per section-player host:
795
+ Debug logging is page-wide. A layout's `debug` attribute writes `window.PIE_DEBUG`, the flag every PIE logger on the page reads, so the last host to set it decides for all of them:
655
796
 
656
797
  - Enable: `<pie-section-player-splitpane debug="true">`
657
798
  - Disable: `<pie-section-player-splitpane debug="false">` (or `debug="0"`)
658
799
 
659
- You can also disable globally via `window.PIE_DEBUG = false`.
800
+ Without a `debug` attribute a layout follows `window.PIE_DEBUG`, which a host can set directly.
660
801
 
661
802
  See the progressive demo routes in `apps/section-demos/src/routes/(demos)` (for example `single-question/+page.svelte` and `session-hydrate-db/+page.svelte`) for end-to-end host integrations.
662
803
 
@@ -685,49 +826,138 @@ Structural composition changes (new/removed/reordered entities) may legitimately
685
826
 
686
827
  ## Custom layout authoring
687
828
 
688
- For section layout authors, `pie-section-player-shell` is the primary abstraction:
689
-
690
- - Use `pie-section-player-shell` to place the section toolbar around your layout body.
691
- - Keep your custom layout logic focused on passages/items and layout UI.
692
- - Treat `pie-section-player-base` as internal runtime plumbing that wraps the shell.
693
- - Use `pie-section-player-item-card` and `pie-section-player-passage-card` as reusable card primitives.
694
- - Prefer shared context for cross-cutting card render plumbing (resolved player tag/action) over repeated prop drilling.
695
-
696
- Minimal pattern for package layout components:
697
-
698
- ```svelte
699
- <pie-section-player-base runtime={effectiveRuntime} {section} section-id={sectionId} attempt-id={attemptId}>
700
- <pie-section-player-shell
701
- show-toolbar={showToolbar}
702
- toolbar-position={toolbarPosition}
703
- toolRegistry={toolRegistry}
704
- sectionHostButtons={sectionHostButtons}
705
- >
706
- <!-- layout-specific body -->
707
- <pie-section-player-passage-card
708
- passage={passage}
709
- playerParams={passagePlayerParams}
710
- passageToolbarTools={passageToolbarTools}
711
- toolRegistry={toolRegistry}
712
- hostButtons={passageHostButtons}
713
- ></pie-section-player-passage-card>
714
- <pie-section-player-item-card
715
- item={item}
716
- canonicalItemId={canonicalItemId}
717
- playerParams={itemPlayerParams}
718
- itemToolbarTools={itemToolbarTools}
719
- toolRegistry={toolRegistry}
720
- hostButtons={itemHostButtons}
721
- ></pie-section-player-item-card>
722
- </pie-section-player-shell>
723
- </pie-section-player-base>
829
+ A host builds its own section layout from `pie-section-player-kernel-host` and the
830
+ two panes. The kernel host runs the section: toolkit, section controller,
831
+ readiness, element pre-warm and the section toolbar. Its element children are the
832
+ layout, and the panes inside them render the section's items and passages. The
833
+ stock layouts are built from the same panes.
834
+
835
+ ```html
836
+ <!doctype html>
837
+ <html lang="en">
838
+ <head>
839
+ <meta charset="utf-8" />
840
+ <style>
841
+ body { margin: 0; }
842
+ pie-section-player-kernel-host { display: block; height: 100vh; }
843
+ .columns { display: grid; grid-template-columns: 3fr 2fr; height: 100%; }
844
+ .column { min-height: 0; overflow: auto; }
845
+ </style>
846
+ <script type="module">
847
+ import "https://cdn.jsdelivr.net/npm/@pie-players/pie-section-player@x.y.z/dist/browser/pie-section-player.js";
848
+
849
+ const player = document.querySelector("pie-section-player-kernel-host");
850
+ player.addEventListener("pie-loading-complete", () => {
851
+ console.log("ready", player.getSnapshot().navigation);
852
+ });
853
+ player.runtime = { env: { mode: "gather", role: "student" } };
854
+ player.section = {
855
+ identifier: "water-cycle",
856
+ rubricBlocks: [
857
+ {
858
+ identifier: "passage-1",
859
+ class: "stimulus",
860
+ view: ["candidate"],
861
+ passage: {
862
+ id: "passage-1",
863
+ config: {
864
+ markup: "<p>Water evaporates, condenses into clouds and falls as rain.</p>",
865
+ elements: {},
866
+ models: [],
867
+ },
868
+ },
869
+ },
870
+ ],
871
+ assessmentItemRefs: [
872
+ {
873
+ identifier: "item-1",
874
+ item: {
875
+ id: "item-1",
876
+ config: {
877
+ markup: "<p>Name the stage in which water vapour forms clouds.</p>",
878
+ elements: {},
879
+ models: [],
880
+ },
881
+ },
882
+ },
883
+ ],
884
+ };
885
+ </script>
886
+ </head>
887
+ <body>
888
+ <pie-section-player-kernel-host section-id="water-cycle" attempt-id="attempt-1">
889
+ <div class="columns">
890
+ <div class="column"><pie-section-player-items-pane></pie-section-player-items-pane></div>
891
+ <div class="column"><pie-section-player-passages-pane></pie-section-player-passages-pane></div>
892
+ </div>
893
+ </pie-section-player-kernel-host>
894
+ </body>
895
+ </html>
724
896
  ```
725
897
 
898
+ The example loads the [browser build](../../docs/setup/cdn_usage.md#section-player-browser-build).
899
+ A bundled host imports `@pie-players/pie-section-player`, which defines the
900
+ kernel host and the panes too. An item with PIE elements names them in
901
+ `config.elements` and carries their `config.models`, as in any section. The
902
+ `/custom-layout` route of `apps/section-demos`
903
+ ([source](../../apps/section-demos/src/routes/%28demos%29/custom-layout/+page.svelte))
904
+ builds the same two columns in Svelte.
905
+
906
+ ### Kernel host
907
+
908
+ - **Inputs and host methods.** Those of the layout elements in
909
+ [Runtime Inputs](#runtime-inputs), without the layout dimensions
910
+ (`narrow-layout-breakpoint`, `content-max-width-*`, `split-pane-*`).
911
+ - **DOM.** The open shadow root holds the toolkit and the section toolbar around
912
+ one default slot; `show-toolbar` and `toolbar-position` place the toolbar around
913
+ the layout. The children, the panes and the content they render stay in light
914
+ DOM, where page styles and the content stylesheet reach them. The element has
915
+ no styles of its own, so the host gives it `display` and a height.
916
+ - **Stock body.** With no element children it renders its own layout, the
917
+ passages pane above the items pane, and places the passages pane only for a
918
+ section with passages. The stock body leaves when the first element child
919
+ arrives and returns when the last one leaves. Text and comment children do not
920
+ count, so markup whitespace and a framework's placeholder comments leave it in
921
+ place.
922
+ - **Navigation and state.** The host methods (`navigateNext`, `navigatePrevious`,
923
+ `navigateTo`, `getSnapshot`, `getSectionController`, `waitForSectionController`)
924
+ and the events (`pie-stage-change`, `pie-loading-complete`,
925
+ `composition-changed`, `session-changed`, `framework-error`, `toolkit-ready`)
926
+ are those of the stock layouts. `detail.sourceCe` on its `pie-stage-change`
927
+ and `pie-loading-complete` events reads `pie-section-player-kernel-host`.
928
+
929
+ ### Panes
930
+
931
+ `<pie-section-player-items-pane>` renders the item cards of the current
932
+ composition, with their toolbars, and runs the element pre-warm;
933
+ `<pie-section-player-passages-pane>` renders the passage cards. A pane takes no
934
+ attributes or properties, and its pre-warm failures arrive as the section
935
+ player's `framework-error` and `element-preload-error` events. It reads the
936
+ section player it belongs to from a context the kernel publishes, so it renders
937
+ at any depth below the kernel host, and outside a section player it renders
938
+ nothing.
939
+
940
+ - **One pane of each kind renders**: the first connected. A second pane of a
941
+ kind renders nothing and takes over when the first disconnects; the player
942
+ reports the duplicate once in the console.
943
+ - **Readiness follows the rendering items pane.** `interactive` and
944
+ `pie-loading-complete` wait for its element pre-warm, and reports from any
945
+ other pane are ignored. A section with items and no items pane never reaches
946
+ either; the player reports it once in the console a task after
947
+ `section-ready`.
948
+ - **The passages pane is optional.** A section without passages needs none, and
949
+ readiness does not wait for it.
950
+ - **Scrolling belongs to the layout.** The items pane's scroll hint follows the
951
+ nearest ancestor with `overflow-y: auto` or `scroll`, so the layout gives each
952
+ pane's container that overflow and a bounded height. The stock layouts' pane backdrops and margins
953
+ stay with those layouts; the cards keep their tags and
954
+ [styling hooks](#card-header-styling-hooks).
955
+
726
956
  ### JS API example for advanced host policy
727
957
 
728
958
  ```ts
729
959
  const host = document.querySelector("pie-section-player-splitpane") as any;
730
- const controller = await host.waitForSectionController?.(5000);
960
+ const controller = await host.waitForSectionController(5000);
731
961
  let sectionComplete = false;
732
962
 
733
963
  const unsubscribe = controller?.subscribe?.((event: any) => {
@@ -737,7 +967,7 @@ const unsubscribe = controller?.subscribe?.((event: any) => {
737
967
  });
738
968
 
739
969
  function canAdvance() {
740
- const nav = host.selectNavigation?.();
970
+ const nav = host.getSnapshot?.()?.navigation;
741
971
  return Boolean(nav?.canNext && sectionComplete);
742
972
  }
743
973
  ```
@@ -758,7 +988,7 @@ const unsubscribeSection = coordinator.subscribeSectionLifecycleEvents({
758
988
  });
759
989
  ```
760
990
 
761
- 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.
991
+ Subscribe **after** the first `getOrCreateSectionController(...)` resolves (or after `toolkit-ready` once the section player has fully wired its controller — typically the safest anchor in host code is `toolkit-ready` followed by the first controller-resolve). Calling subscribe before the first `getOrCreateSectionController(...)` call throws; a listener added while a section is starting binds when that section becomes active.
762
992
 
763
993
  Use `subscribeSectionEvents(...)` only for advanced mixed filtering requirements.
764
994
 
@@ -810,52 +1040,41 @@ Section-player instrumentation is provider-agnostic and uses the shared
810
1040
  - Toolkit telemetry forwarding uses the same provider path, so tool/backend
811
1041
  operational events are visible alongside section events when toolkit is mounted.
812
1042
 
813
- Canonical lifecycle stream (engine-routed, dispatched on the outer layout CE):
1043
+ Canonical lifecycle stream (engine-routed, dispatched on the outer layout CE,
1044
+ bubbling and composed):
814
1045
 
815
1046
  - `pie-stage-change` — single typed transition stream covering
816
1047
  `composed` → `engine-ready` → `interactive` → `disposed`. Payload is a
817
- `StageChangeDetail`.
818
- - `pie-loading-complete` — fires once per cohort when every item has
819
- loaded (kernel-routed; gated on `interactive`).
1048
+ `StageChangeDetail`. A non-recoverable framework error before
1049
+ `interactive` emits the current stage `failed` and each stage it never
1050
+ reached `skipped`.
1051
+ - `pie-loading-complete` — fires once per cohort, when the section's element
1052
+ pre-warm resolves for the current composition and the item cards can mount
1053
+ (kernel-routed).
820
1054
  - `framework-error` — canonical error event for any failure crossing the
821
- framework boundary. Payload is a `FrameworkErrorModel`. The toolkit's
822
- package-internal `FrameworkErrorBus` and the `onFrameworkError`
823
- callback prop deliver each error exactly once regardless of wrapper
824
- depth. The `framework-error` *DOM event* on the outer layout CE
825
- also delivers each error exactly once: the kernel listener at
826
- `<pie-section-player-base>` intercepts the toolkit's bubbled emit
827
- and calls `event.stopPropagation()`, leaving only the canonical
828
- engine-bridge emit on the layout host. The single-emit contract is
829
- pinned by `tests/section-player-framework-error-dual-emit.test.ts`.
830
- Direct listeners attached to `<pie-assessment-toolkit>` itself
831
- still see the toolkit's own emit.
832
-
833
- Callback-prop mirrors with two-tier precedence (`runtime.<key>` wins over
834
- the top-level prop):
835
-
836
- - `onStageChange(detail)` — on every layout CE, `pie-section-player-base`,
837
- and `pie-assessment-toolkit`.
838
- - `onLoadingComplete(detail)` — on the kernel-backed layout CEs only
839
- (split-pane / vertical / tabbed / kernel-host).
840
- - `onFrameworkError(model)` — on every layout CE and
841
- `pie-section-player-base`. Fires exactly once per error regardless of
842
- wrapper depth (delivered through the package-internal
843
- `FrameworkErrorBus`). The `framework-error` DOM event on the layout
844
- CE host is also single-fire; consume either.
1055
+ framework boundary. Payload is a `FrameworkErrorModel`. The toolkit
1056
+ dispatches it once per error, bubbling and composed, so it reaches the
1057
+ layout CE and `document`; errors from a coordinator the host passes as
1058
+ `runtime.coordinator` arrive the same way.
1059
+ `tests/section-player-event-delivery.spec.ts` pins these counts.
1060
+
1061
+ Callbacks on `runtime`:
1062
+
1063
+ - `runtime.onStageChange(detail)` and `runtime.onLoadingComplete(detail)`.
1064
+ - `runtime.onFrameworkError(model)` fires once per error regardless of
1065
+ wrapper depth, like the `framework-error` DOM event; consume either.
845
1066
 
846
1067
  Section-player owned instrumentation stream:
847
1068
 
848
1069
  - `pie-section-stage-change`
849
1070
  - `pie-section-loading-complete`
850
- - `pie-section-session-changed`
851
- - `pie-section-composition-changed`
852
1071
  - `pie-section-framework-error`
1072
+ - `pie-section-element-preload-retry`
1073
+ - `pie-section-element-preload-error`
853
1074
 
854
1075
  Build consumers against these canonical lifecycle events:
855
1076
 
856
- - `readiness-change` → listen for `pie-stage-change`. The readiness
857
- payload is also available via `selectReadiness()` /
858
- `getSnapshot().readiness` on the layout CE.
1077
+ - `readiness-change` → listen for `pie-stage-change`.
859
1078
  - `interaction-ready` → `pie-stage-change` filtered on
860
1079
  `detail.stage === "interactive"`.
861
1080
  - `ready` → `pie-loading-complete`.
@@ -880,7 +1099,7 @@ Section session data can be managed either through persistence hooks or directly
880
1099
 
881
1100
  ```ts
882
1101
  const host = document.querySelector("pie-section-player-splitpane") as any;
883
- const controller = await host.waitForSectionController?.(5000);
1102
+ const controller = await host.waitForSectionController(5000);
884
1103
 
885
1104
  // Read current section session snapshot.
886
1105
  const currentSession = controller?.getSession?.();
@@ -905,9 +1124,34 @@ await controller?.updateItemSession?.("q1", {
905
1124
  });
906
1125
  ```
907
1126
 
908
- The same controller snapshot is what the persistence strategy saves/loads.
1127
+ The same controller snapshot is what the persistence strategy saves/loads, and
1128
+ what the layout's `session` property takes.
909
1129
  When a controller is reused for the same `sectionId`/`attemptId`, `updateInput()` refreshes composition input while preserving in-memory section session data.
910
1130
 
1131
+ ### One item as a section
1132
+
1133
+ `sectionFromItem` wraps one item config, in the shape `<pie-item-player config>`
1134
+ takes, and optionally its session, in the `section` and `session` the layouts
1135
+ take. It is exported from the package root and from
1136
+ `@pie-players/pie-section-player/item-section`, which defines no custom element
1137
+ and imports in Node.
1138
+
1139
+ ```ts
1140
+ import { sectionFromItem } from "@pie-players/pie-section-player/item-section";
1141
+
1142
+ const { section, session } = sectionFromItem(itemConfig, { session: itemSession });
1143
+ player.setAttribute("section-id", section.identifier);
1144
+ player.section = section;
1145
+ player.session = session;
1146
+ ```
1147
+
1148
+ The item ref's `identifier` and the item's `id` are the config's `id`, so every
1149
+ `itemId` the section reports is the id the host already holds. An advanced
1150
+ config's `passage` becomes the item's passage, and its `instructorResources` and
1151
+ `defaultExtraModels` stay on the item's config. The section carries no `baseId`
1152
+ or `version`; `options.sectionId` names the section, which defaults to the
1153
+ config's `id`.
1154
+
911
1155
  ### Commit at a section boundary
912
1156
 
913
1157
  A delivery element coalesces its `session-changed` dispatch, so a response the
@@ -917,13 +1161,23 @@ controller commits pending element sessions before item navigation, before
917
1161
  The player also commits when an item shell tears down and when the page goes
918
1162
  hidden.
919
1163
 
920
- The commit reaches the controller, and the controller's own
921
- `PIE_ITEM_SESSION_CHANGED_EVENT` is what reaches a host. A raw `session-changed`
922
- does not leave a section: `<pie-item-shell>` stops it and re-dispatches the
923
- normalized event, so a host listening on `document` for `session-changed`
924
- receives nothing from inside a section player, before this change or after it.
1164
+ A committed response travels like any other. A raw element `session-changed`
1165
+ does not leave its `<pie-item-scope>`, which re-dispatches it as the normalized
1166
+ `item-session-changed` (`PIE_ITEM_SESSION_CHANGED_EVENT`); the toolkit then
1167
+ publishes the section's canonical `session-changed`. Both bubble through the
1168
+ layout element to `document`, and a listener on either receives each dispatch
1169
+ once.
925
1170
  Host code persists from the controller's events or from its session snapshot.
926
1171
 
1172
+ Each of those events marks a commit with `sessionCommitReason`
1173
+ (`"teardown" | "navigate" | "page-hidden"`): `item-session-changed`, the
1174
+ toolkit's `session-changed`, and the controller's `item-session-data-changed`
1175
+ and `item-session-meta-changed`. A commit at a section swap or item navigation
1176
+ reports the item being left, which the host may already have moved past, so a
1177
+ handler that sets its current item or navigation state from these events leaves
1178
+ that state alone on a commit and persists the commit's session as usual. The
1179
+ controller's events also carry `sectionId`, the section the item belongs to.
1180
+
927
1181
  Navigation inside a section keeps every item mounted, so nothing is discarded
928
1182
  and the element's own debounce would complete on its own. The commit still runs
929
1183
  there because the response belongs to the item being left: a host that persists
@@ -941,8 +1195,9 @@ pending response at those boundaries.
941
1195
  ## Content trust boundary
942
1196
 
943
1197
  Section-player layouts embed `<pie-item-player>` elements for each item.
944
- Item and passage markup is sanitized by default via DOMPurify; see
945
- [pie-item-player README](./README.md#content-trust-boundary)
1198
+ Item and passage markup is sanitized by default via DOMPurify; see the
1199
+ [pie-item-player README](../item-player/README.md#content-trust-boundary)
1200
+ for the allow-list, the `trust-markup` opt-out and the `sanitizeMarkup`
946
1201
  property. Hosts can forward those settings through the section-player
947
1202
  `runtime.player` overrides — the runtime flattens these onto the embedded
948
1203
  `<pie-item-player>` instance, so any field not recognized by the kernel is
@@ -966,21 +1221,12 @@ produced by a trusted pipeline.
966
1221
  Published exports are intentionally minimal:
967
1222
 
968
1223
  - `@pie-players/pie-section-player`
969
- - `@pie-players/pie-section-player/components/section-player-splitpane-element`
970
- - `@pie-players/pie-section-player/components/section-player-vertical-element`
971
- - `@pie-players/pie-section-player/components/section-player-tabbed-element`
972
- - `@pie-players/pie-section-player/components/section-player-kernel-host-element`
973
- - `@pie-players/pie-section-player/components/section-player-shell-element`
974
- - `@pie-players/pie-section-player/components/section-player-item-card-element`
975
- - `@pie-players/pie-section-player/components/section-player-passage-card-element`
976
- - `@pie-players/pie-section-player/components/section-player-items-pane-element`
977
- - `@pie-players/pie-section-player/components/section-player-passages-pane-element`
978
- - `@pie-players/pie-section-player/contracts/layout-contract`
979
- - `@pie-players/pie-section-player/contracts/public-events`
1224
+ - `@pie-players/pie-section-player/browser`, the self-contained browser build ([CDN usage](../../docs/setup/cdn_usage.md#section-player-browser-build))
1225
+ - `@pie-players/pie-section-player/components/section-player-splitpane-element`, which resolves to the package root
980
1226
  - `@pie-players/pie-section-player/contracts/runtime-host-contract`
981
- - `@pie-players/pie-section-player/contracts/layout-parity-metadata`
982
1227
  - `@pie-players/pie-section-player/contracts/host-hooks`
983
1228
  - `@pie-players/pie-section-player/policies`
1229
+ - `@pie-players/pie-section-player/item-section`
984
1230
 
985
1231
  ## Development
986
1232