npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pie-players/pie-section-player

v0.3.50

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-splitpane
  • pie-section-player-vertical
  • pie-section-player-tabbed

Use the layout custom elements listed above for section rendering.

Install

npm install @pie-players/pie-section-player

Runtime boundary and migration

  • Browser-only package: @pie-players/pie-section-player registers 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.

SectionController

SectionController is the domain authority inside a section player. It owns in-section navigation state, the canonical aggregation of per-item sessions, and the host-facing persistence snapshot. The layout custom elements (pie-section-player-splitpane / -vertical / -tabbed) are transport adapters around it. See docs/section-player/controller-boundaries.md for the rationale behind that split, and docs/section-player/client-architecture-tutorial.md for the end-to-end walkthrough.

The handle implements SectionControllerHandle from @pie-players/pie-assessment-toolkit; see the JSDoc on that interface for the per-method contract.

Obtaining the handle

const host = document.querySelector("pie-section-player-splitpane") as any;
const controller = await host.waitForSectionController?.(5000);

waitForSectionController(timeoutMs) resolves when the layout CE has wired its controller (the same anchor pie-stage-change reaches with detail.stage === "engine-ready"). Use getSectionController() if you've already passed the readiness anchor synchronously.

Session lifecycle

A typical host flow:

controller?.configureSessionPersistence?.({ context, strategy });
await controller?.hydrate?.();
const unsubscribe = controller?.subscribe?.(handleEvent);
// ...later, on save / unload:
await controller?.persist?.();
unsubscribe?.();

getSession() / applySession(session, { mode }) / updateItemSession(itemId, detail) are the direct read/write surfaces and exchange the same SectionControllerSessionState shape the persistence strategy load/save methods receive. See Item session management for worked examples.

Event stream

The controller's typed event stream (SectionControllerEvent discriminated union) is the single source of truth for in-section change. Hosts usually subscribe through ToolkitCoordinator.subscribeItemEvents / subscribeSectionLifecycleEvents (cohort-aware filtering, survives navigation) — see JS API example for advanced host policy. Key event types:

  • item-selected — item navigation within the current section.
  • item-session-data-changed / item-session-meta-changed — per-item session updates the persistence layer should observe.
  • content-loaded — passage / item / rubric finished loading.
  • section-loading-complete — every renderable in the section finished loading.
  • section-items-complete-changed — aggregate completion flip.
  • section-navigation-change — the controller's section identity changed.

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-tabbed-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 bundle
  • section (object): assessment section payload
  • env (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|none
  • 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.
  • 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).
  • 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).
  • 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.)
  • 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.)
  • show-toolbar (boolean-like): accepts true/false and 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), splitpane and vertical layout hosts normalize section toolbar placement to top. This includes left, right, bottom, and none values.

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.

To opt into PIE-117 dimensions from a host, configure:

<pie-section-player-splitpane
  content-max-width-no-passage="800"
  content-max-width-with-passage="1200"
  split-pane-min-region-width="280"
></pie-section-player-splitpane>

Use the same max-width attributes on pie-section-player-vertical when you want the same no-passage/with-passage width behavior in vertical mode.

To force splitpane stacked mode to use vertical rendering:

<pie-section-player-splitpane
  narrow-layout-breakpoint="1100"
  split-pane-collapse-strategy="vertical"
></pie-section-player-splitpane>

By default, splitpane stacked mode uses tabs. The dedicated pie-section-player-tabbed layout also always renders passage/items tabs when passages are present.

Tab styling hooks

pie-section-player-tabbed and splitpane tabbed collapse mode expose canonical pie-* hooks for theming:

  • pie-section-player-tabs
  • pie-section-player-tab
  • pie-section-player-tab--active
  • pie-section-player-tab-panel

For theme compatibility with existing passage-label patterns, tabs also expose:

  • data-pie-purpose="passage-label" and alias class passage-label
  • data-pie-purpose="item-label" and alias class item-label

Tab colors/spacing can be themed via CSS variables such as: --pie-section-player-tab-color, --pie-section-player-tab-active-color, --pie-section-player-tab-indicator-color, and --pie-section-player-tab-spacing.

Card header styling hooks

Passage and item cards share a common header row (.pie-section-player-content-card-header, with the card-specific aliases .pie-section-player-passage-header / .pie-section-player-item-header).

  • Title and toolbar are centered vertically by default. There is no prop or attribute for this — hosts needing a non-standard alignment should override the selector in their own stylesheet.
  • The header fill is transparent by default. Hosts/themes opt into a color via the --pie-section-player-card-header-background CSS variable; the framework does not ship a brand palette.

Example (host CSS):

pie-section-player-passage-card,
pie-section-player-item-card {
  --pie-section-player-card-header-background: #c9e5e6;
}

When both max-width attributes are set, the with-passage cap resolves to the greater of the two configured values (after clamp), so with-passage mode never ends up narrower than no-passage mode.

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, debug
    • show-toolbar, toolbar-position, narrow-layout-breakpoint
    • content-max-width-no-passage, content-max-width-with-passage, split-pane-min-region-width, split-pane-collapse-strategy
  • JS API for advanced customization:
    • Get the controller handle via getSectionController() or waitForSectionController() (preferred)
    • Listen for pie-stage-change and filter on detail.stage === "engine-ready" for an event-driven entry point
    • Apply custom policy/gating in host code (for example, domain-specific canNext based on controller events like section-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 toolRegistry and optional host button arrays (sectionHostButtons, itemHostButtons, passageHostButtons)
    • Register host callbacks via hooks (for example hooks.cardTitleFormatter)

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 runtime configuration is supplied through the runtime object. Set player config, tools, accessibility, coordinator, env, and createSectionController on runtime.<key>.

Host-owned focus

Section-player does not move focus on behalf of host-level affordances such as "Skip to Main", nor does it make passage/question containers tab stops. Hosts own page chrome, skip links, landmarks, and any special focus placement. For example, Quiz Engine's Fixed Player shell can focus its own main#main-content; the next Tab then follows the browser's natural order into the first actionable control rendered inside the section player.

The passage and item card custom elements are content/layout surfaces, not public focus targets. Splitpane passage content remains scrollable through the pane's native scroll behavior, but the passage pane itself is not inserted into sequential keyboard navigation.

<a href="#main-content" class="skip-link">Skip to Main</a>
<main id="main-content" tabindex="-1">
  <pie-section-player-splitpane></pie-section-player-splitpane>
</main>

Wired policy toggles. Each SectionPlayerPolicies field has a real runtime effect; nothing in this surface is decorative.

  • readiness.mode ("progressive" | "strict") — drives readiness-event emission via createReadinessDetail in SectionPlayerLayoutKernel.
  • preload.enabled — when false, SectionItemsPane short-circuits the section-level element warmup pipeline (warmupSectionElements). Items still mount and item-players register their own elements on demand. Use this to disable section pre-warm when the host already owns element registration end-to-end. Default: true.
  • telemetry.enabled — when false, the layout custom elements skip attachInstrumentationEventBridge setup, so no pie-section-* telemetry events flow through the bridge. Hosts that want a different shape of opt-out can still override runtime.player.loaderConfig.instrumentationProvider. Default: true.

The exported isPreloadEnabled(policies) and isTelemetryEnabled(policies) helpers read these toggles with the documented default-true semantics, so host code that needs to mirror the same gate (e.g. when composing a custom layout host) can call them directly.

Navigation signals

  • item-selected: item-level navigation change within the current section in the SectionController broadcast stream (itemIndex, currentItemId, totalItems).
  • section-navigation-change: section-level navigation/selection change in the SectionController broadcast stream (previousSectionId, currentSectionId, reason).

Runtime configuration is explicit:

  • runtime owns runtime fields (assessmentId, playerType, player, lazyInit, tools, accessibility, coordinator, isolation, env, createSectionController).
  • Tool placement is configured through runtime.tools.placement.section, runtime.tools.placement.item, and runtime.tools.placement.passage.
  • 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.
  • TTS provider config must use tools.providers.textToSpeech (canonical). tools.providers.tts is rejected by validation.
  • Host tool overrides are additive:
    • toolRegistry overrides 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"> (or debug="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-shell to place the section toolbar around your layout body.
  • Keep your custom layout logic focused on passages/items and layout UI.
  • Treat pie-section-player-base as internal runtime plumbing that wraps the shell.
  • Use pie-section-player-item-card and pie-section-player-passage-card as 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}
    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, prefer helper subscriptions for host logic. Subscriptions follow the toolkit's active section cohort automatically — a single subscribe call survives navigation:

const unsubscribeItem = coordinator.subscribeItemEvents({
  listener: (event: any) => {
    // item-scoped stream
  },
});

const unsubscribeSection = coordinator.subscribeSectionLifecycleEvents({
  listener: (event: any) => {
    // section-loading-complete / section-items-complete-changed / section-error / section-navigation-change
  },
});

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.

Use subscribeSectionEvents(...) only for advanced mixed filtering requirements.

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 for the full upgrade recipe.

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:

  • loaderOptions controls bundle loading. loaderConfig controls runtime resource monitoring.
  • Custom providers (functions/instances) must be passed as JS properties (runtime object), 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/undefined providers use the default New Relic provider path.
  • instrumentationProvider: null explicitly disables instrumentation.
  • Invalid provider objects are ignored (optional debug warning), also no-op.
  • Existing item-player behavior is preserved.
  • For local debug overlays, compose providers (for example NewRelicInstrumentationProvider + DebugPanelInstrumentationProvider) through CompositeInstrumentationProvider.
  • Toolkit telemetry forwarding uses the same provider path, so tool/backend operational events are visible alongside section events when toolkit is mounted.

Canonical lifecycle stream (engine-routed, dispatched on the outer layout CE):

  • pie-stage-change — single typed transition stream covering composedengine-readyinteractivedisposed. Payload is a StageChangeDetail.
  • pie-loading-complete — fires once per cohort when every item has loaded (kernel-routed; gated on interactive).
  • framework-error — canonical error event for any failure crossing the framework boundary. Payload is a FrameworkErrorModel. The toolkit's package-internal FrameworkErrorBus and the onFrameworkError callback prop deliver each error exactly once regardless of wrapper depth. The framework-error DOM event on the outer layout CE also delivers each error exactly once: the kernel listener at <pie-section-player-base> intercepts the toolkit's bubbled emit and calls event.stopPropagation(), leaving only the canonical engine-bridge emit on the layout host. The single-emit contract is pinned by tests/section-player-framework-error-dual-emit.test.ts. Direct listeners attached to <pie-assessment-toolkit> itself still see the toolkit's own emit.

Callback-prop mirrors with two-tier precedence (runtime.<key> wins over the top-level prop):

  • onStageChange(detail) — on every layout CE, pie-section-player-base, and pie-assessment-toolkit.
  • onLoadingComplete(detail) — on the kernel-backed layout CEs only (split-pane / vertical / tabbed / kernel-host).
  • onFrameworkError(model) — on every layout CE and pie-section-player-base. Fires exactly once per error regardless of wrapper depth (delivered through the package-internal FrameworkErrorBus). The framework-error DOM event on the layout CE host is also single-fire; consume either.

Section-player owned instrumentation stream:

  • pie-section-stage-change
  • pie-section-loading-complete
  • pie-section-session-changed
  • pie-section-composition-changed
  • pie-section-framework-error

Build consumers against these canonical lifecycle events:

  • readiness-change → listen for pie-stage-change. The readiness payload is also available via selectReadiness() / getSnapshot().readiness on the layout CE.
  • interaction-readypie-stage-change filtered on detail.stage === "interactive".
  • readypie-loading-complete.
  • section-controller-ready → call waitForSectionController(timeoutMs) or getSectionController() on the layout CE, or filter pie-stage-change for detail.stage === "engine-ready".

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|error
  • pie-tool-backend-call-start|success|error
  • pie-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.

Content trust boundary

Section-player layouts embed <pie-item-player> elements for each item. Item and passage markup is sanitized by default via DOMPurify; see pie-item-player README property. Hosts can forward those settings through the section-player runtime.player overrides — the runtime flattens these onto the embedded <pie-item-player> instance, so any field not recognized by the kernel is passed straight through as a prop/attribute:

const runtime = {
  playerType: "iife",
  player: {
    trustMarkup: false, // default, keeps DOMPurify on
    // sanitizeMarkup: (html) => myCustomSanitize(html),
  },
};

Set trustMarkup: true only when the section payload is guaranteed to be produced by a trusted pipeline.

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-tabbed-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/components/section-player-items-pane-element
  • @pie-players/pie-section-player/components/section-player-passages-pane-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