@pie-players/pie-section-player
v0.3.29
Published
Web component for rendering QTI 3.0 assessment sections with passages and items
Readme
@pie-players/pie-section-player
Section rendering package with layout custom elements:
pie-section-player-splitpanepie-section-player-vertical
The package no longer exposes the legacy pie-section-player layout orchestration API.
Install
npm install @pie-players/pie-section-playerRuntime boundary and migration
- Browser-only package:
@pie-players/pie-section-playerregisters custom elements and is intended for browser/DOM hosts, not plain Node runtime imports. - Node-import-safe packages (for server/runtime utilities) are documented in
docs/setup/library-packaging-strategy.md. - Migration direction: prefer the stable default entry for side-effect registration:
import "@pie-players/pie-section-player";If hosts need explicit registration control, keep using documented component
entrypoints under @pie-players/pie-section-player/components/*.
Standalone browser variants for this package are intentionally deferred; the
current supported contract is the default bundler entrypoints under dist.
Usage
Import the custom-element registration entrypoint in consumers:
import '@pie-players/pie-section-player/components/section-player-splitpane-element';
import '@pie-players/pie-section-player/components/section-player-vertical-element';
import '@pie-players/pie-section-player/components/section-player-item-card-element';
import '@pie-players/pie-section-player/components/section-player-passage-card-element';Render in HTML/Svelte/JSX:
<pie-section-player-splitpane></pie-section-player-splitpane>Set complex values (runtime, section, env) as JS properties.
Runtime Inputs
Both layout elements support:
runtime(object): primary coordinator/tools/player runtime bundlesection(object): assessment section payloadenv(object): optional top-level override for{ mode, role }debug(boolean-like): verbose debug logging control ("true"enables,"false"/"0"disables)toolbar-position(string):top|right|bottom|left|nonenarrow-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.show-toolbar(boolean-like): acceptstrue/falseand common string forms ("true","false","1","0","yes","no")- Host extension props (JS properties only):
toolRegistry,sectionHostButtons,itemHostButtons,passageHostButtons,hooks
When viewport width is within the collapsed range (~1100px and below), inline section
toolbar positions (left/right) normalize to top so controls remain horizontally
laid out and easier to access.
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.
API direction: CE defaults first, JS customization for advanced cases
The intended usage model is:
- CE props for default/standard flows (roughly 90% use cases):
assessment-id,section,section-id,attempt-id,debugshow-toolbar,toolbar-position,narrow-layout-breakpoint,enabled-tools,item-toolbar-tools,passage-toolbar-tools
- JS API for advanced customization:
- Get the controller handle via
getSectionController()orwaitForSectionController() - Listen to
section-controller-ready - Apply custom policy/gating in host code (for example, domain-specific
canNextbased on controller events likesection-items-complete-changed) - Compose forward/backward eligibility in host code using
selectNavigation()+ host state; there is intentionally no separate parallel CE gating API for this - Inject custom toolbar tooling with
toolRegistryand optional host button arrays (sectionHostButtons,itemHostButtons,passageHostButtons) - Register host callbacks via
hooks(for examplehooks.cardTitleFormatter)
- Get the controller handle via
Example:
const host = document.querySelector("pie-section-player-splitpane") as any;
host.hooks = {
cardTitleFormatter: (context: any) => {
if (context.kind === "item") {
return `Question ${context.itemIndex + 1}: ${context.item?.name || context.defaultTitle}`;
}
return context.passage?.name || context.defaultTitle;
},
};Advanced CE props are still supported as escape hatches (runtime, coordinator, createSectionController, etc.), but hosts should prefer JS/controller composition for non-standard behavior.
Navigation signals
item-selected: item-level navigation change within the current section in theSectionControllerbroadcast stream (itemIndex,currentItemId,totalItems).section-navigation-change: section-level navigation/selection change in theSectionControllerbroadcast stream (previousSectionId,currentSectionId,reason).
Runtime precedence is explicit:
runtimevalues are primary for runtime fields (assessmentId,playerType,player,lazyInit,tools,accessibility,coordinator,createSectionController,isolation,env).- Top-level runtime-like props remain compatibility inputs and are merged with
runtimevalues. Forplayer, top-level values are merged first, thenruntime.playeroverrides. NestedloaderOptionsandloaderConfigare also merged with the same precedence. - Toolbar placement overrides (
enabled-tools,item-toolbar-tools,passage-toolbar-tools) are normalized on top of the runtime tools config. - Tool configuration validation is canonical in toolkit initialization (
pie-assessment-toolkit), including toolbar overlays. Useruntime.toolConfigStrictness(off|warn|error) to control warning-only vs fail-fast behavior. - TTS provider config must use
tools.providers.textToSpeech(canonical).tools.providers.ttsis rejected by validation. - Host tool overrides are additive:
toolRegistryoverrides the default toolbar registry when provided- host buttons are appended per toolbar scope via
sectionHostButtons,itemHostButtons,passageHostButtons
Debug logging can be controlled per section-player host:
- Enable:
<pie-section-player-splitpane debug="true"> - Disable:
<pie-section-player-splitpane debug="false">(ordebug="0")
You can also disable globally via window.PIE_DEBUG = false.
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.
Data flow and stability guarantees
Section-player follows a unidirectional flow model:
- Inputs flow downward (
runtime,section,env, toolbar options) into base/toolkit/layout/card render paths. - State updates flow upward as events (
runtime-*,session-changed, controller change events) and are reconciled by runtime owners. - Layout/card components should not create competing sources of truth for composition/session.
Stability guarantees
For non-structural updates, section-player guarantees behavior stability:
- Item/passage shell identity remains stable (no remount churn for response-only updates).
- Pane-local scroll position remains stable in splitpane and vertical layouts.
Non-structural updates include:
- response/session updates
- tool toggles/config updates
- runtime config changes that do not alter composition identity
Structural composition changes (new/removed/reordered entities) may legitimately re-render/remount affected nodes.
Custom layout authoring
For section layout authors, pie-section-player-shell is the primary abstraction:
- Use
pie-section-player-shellto place the section toolbar around your layout body. - Keep your custom layout logic focused on passages/items and layout UI.
- Treat
pie-section-player-baseas internal runtime plumbing that wraps the shell. - Use
pie-section-player-item-cardandpie-section-player-passage-cardas reusable card primitives. - Prefer shared context for cross-cutting card render plumbing (resolved player tag/action) over repeated prop drilling.
Minimal pattern for package layout components:
<pie-section-player-base runtime={effectiveRuntime} {section} section-id={sectionId} attempt-id={attemptId}>
<pie-section-player-shell
show-toolbar={showToolbar}
toolbar-position={toolbarPosition}
enabled-tools={enabledTools}
toolRegistry={toolRegistry}
sectionHostButtons={sectionHostButtons}
>
<!-- layout-specific body -->
<pie-section-player-passage-card
passage={passage}
playerParams={passagePlayerParams}
passageToolbarTools={passageToolbarTools}
toolRegistry={toolRegistry}
hostButtons={passageHostButtons}
></pie-section-player-passage-card>
<pie-section-player-item-card
item={item}
canonicalItemId={canonicalItemId}
playerParams={itemPlayerParams}
itemToolbarTools={itemToolbarTools}
toolRegistry={toolRegistry}
hostButtons={itemHostButtons}
></pie-section-player-item-card>
</pie-section-player-shell>
</pie-section-player-base>JS API example for advanced host policy
const host = document.querySelector("pie-section-player-splitpane") as any;
const controller = await host.waitForSectionController?.(5000);
let sectionComplete = false;
const unsubscribe = controller?.subscribe?.((event: any) => {
if (event?.type === "section-items-complete-changed") {
sectionComplete = event.complete === true;
}
});
function canAdvance() {
const nav = host.selectNavigation?.();
return Boolean(nav?.canNext && sectionComplete);
}If you already have a ToolkitCoordinator from toolkit-ready, prefer helper subscriptions for host logic:
const unsubscribeItem = coordinator.subscribeItemEvents({
sectionId,
attemptId,
listener: (event: any) => {
// item-scoped stream
},
});
const unsubscribeSection = coordinator.subscribeSectionLifecycleEvents({
sectionId,
attemptId,
listener: (event: any) => {
// section-loading-complete / section-items-complete-changed / section-error / section-navigation-change
},
});Use subscribeSectionEvents(...) only for advanced mixed filtering requirements.
Item-level observability configuration
Item-level resource observability is configured on the embedded pie-item-player via
loaderConfig. In section-player integrations, pass this through runtime.player.loaderConfig.
import { ConsoleInstrumentationProvider } from "@pie-players/pie-players-shared";
const provider = new ConsoleInstrumentationProvider({ useColors: true });
await provider.initialize({ debug: true });
sectionPlayerEl.runtime = {
playerType: "esm",
player: {
loaderConfig: {
trackPageActions: true,
instrumentationProvider: provider,
maxResourceRetries: 3,
resourceRetryDelay: 500,
},
loaderOptions: {
esmCdnUrl: "https://cdn.jsdelivr.net/npm",
},
},
};Important:
loaderOptionscontrols bundle loading.loaderConfigcontrols runtime resource monitoring.- Custom providers (functions/instances) must be passed as JS properties (
runtimeobject), not serialized string attributes.
Instrumentation ownership and semantics
Section-player instrumentation is provider-agnostic and uses the shared
InstrumentationProvider contract.
- Canonical provider path:
runtime.player.loaderConfig.instrumentationProvider - With
trackPageActions: true, missing/undefinedproviders use the default New Relic provider path. instrumentationProvider: nullexplicitly disables instrumentation.- Invalid provider objects are ignored (optional debug warning), also no-op.
- Existing
item-playerbehavior is preserved. - For local debug overlays, compose providers (for example
NewRelicInstrumentationProvider+DebugPanelInstrumentationProvider) throughCompositeInstrumentationProvider. - Toolkit telemetry forwarding uses the same provider path, so tool/backend operational events are visible alongside section events when toolkit is mounted.
Section-player owned canonical stream:
pie-section-readiness-changepie-section-interaction-readypie-section-readypie-section-controller-readypie-section-session-changedpie-section-composition-changedpie-section-runtime-error
Framework boundary stream (from toolkit, re-emitted through section-player wrappers):
framework-error(canonical)runtime-error(compatibility signal)
If toolkit is mounted, toolkit lifecycle events are emitted on a separate
pie-toolkit-* stream. This separation avoids semantic overlap; bridge dedupe
is a defensive safety net only.
Toolkit tool/backend operational stream:
pie-tool-init-start|success|errorpie-tool-backend-call-start|success|errorpie-tool-library-load-start|success|error
Item session management
Section session data can be managed either through persistence hooks or directly through the controller API.
const host = document.querySelector("pie-section-player-splitpane") as any;
const controller = await host.waitForSectionController?.(5000);
// Read current section session snapshot.
const currentSession = controller?.getSession?.();
// Replace section session state (resume from backend snapshot).
await controller?.applySession?.({
currentItemIndex: 0,
visitedItemIdentifiers: ["q1"],
itemSessions: {
q1: {
itemIdentifier: "q1",
pieSessionId: "q1-session",
session: { id: "q1-session", data: [{ id: "choice", value: "a" }] }
}
}
}, { mode: "replace" });
// Update a single item session directly.
await controller?.updateItemSession?.("q1", {
session: { id: "q1-session", data: [{ id: "choice", value: "b" }] },
complete: true,
});The same controller snapshot is what the persistence strategy saves/loads.
When a controller is reused for the same sectionId/attemptId, updateInput() refreshes composition input while preserving in-memory section session data.
Exports
Published exports are intentionally minimal:
@pie-players/pie-section-player@pie-players/pie-section-player/components/section-player-splitpane-element@pie-players/pie-section-player/components/section-player-vertical-element@pie-players/pie-section-player/components/section-player-kernel-host-element@pie-players/pie-section-player/components/section-player-shell-element@pie-players/pie-section-player/components/section-player-item-card-element@pie-players/pie-section-player/components/section-player-passage-card-element@pie-players/pie-section-player/contracts/layout-contract@pie-players/pie-section-player/contracts/public-events@pie-players/pie-section-player/contracts/runtime-host-contract@pie-players/pie-section-player/contracts/layout-parity-metadata@pie-players/pie-section-player/contracts/host-hooks@pie-players/pie-section-player/policies
Development
bun run --cwd packages/section-player dev
bun run --cwd packages/section-player check
bun run --cwd packages/section-player build