This is a modern implementation of the PIE (Platform Independent Elements) specification, built with TypeScript, ESM, and contemporary tooling. The project currently syncs React-based elements from the upstream pie-elements repository while providing modern ESM packaging, Vite builds, and improved developer experience.
Current Status: Early development (v0.1.0)
- 28 React elements synced from upstream
- Core infrastructure and build tooling established
- Element and lib packages are publish-enabled (versioned/released via Changesets + GitHub Actions)
- Future plans include native Svelte 5 implementations and public npm releases
This is a completely new project, not a refactor of the original pie-elements. The legacy pie-elements project remains available for backwards compatibility, while this project provides a modern foundation for future development.
Key reasons for a new project:
- Backwards compatibility: Legacy consumers can continue using pie-elements without disruption
- ESM enablement: The PIE team's work on library updates (React, MUI) and migration from Slate to Tiptap now allows full ESM adoption
- Modern tooling: Clean slate enables use of Vite, Bun, and contemporary build tools
- Unified player architecture: ESM makes it possible to have a single player for all views (delivery, authoring, print)
This project differs fundamentally from the legacy pie-elements in eight key ways:
- Framework Flexibility: Legacy was React-only; this project is framework-agnostic via web components
- Module System: CommonJS → ESM-first with browser-managed dependencies
- Build Tooling: Bespoke tools (webpack, pie-shared-lib-builder) → Standard tools (Vite, Bun, Turbo)
- Bundle Strategy: IIFE with all dependencies → ESM with on-demand loading
- Player Architecture: Separate players per view → Unified player for all views
- Directory Organization: Asymmetric structure → Symmetric peer folders
- CI/CD: CircleCI → GitHub Actions
- Monorepo: pie-lib was separate → now integrated in packages/lib-react/
Legacy approach: React-only implementation. Every element, controller, and UI component was tightly coupled to React.
Modern approach: Framework-agnostic via web components and the PIE controller interface.
- Controllers are pure TypeScript business logic (completely framework-independent)
- UI implementations can use any framework (React, Svelte, Vue, Angular) as long as they produce web components
- Element Player loads elements via custom element registry, regardless of underlying framework
- Current State: All 28 elements use React; architecture supports future multi-framework implementations
This architectural flexibility will allow choosing the right framework for each use case (e.g., Svelte for smaller bundles, React for ecosystem compatibility).
Legacy approach: CommonJS modules with webpack and pie-shared-lib-builder creating IIFE bundles that include ALL dependencies.
Modern approach: ESM-only with modern build tools.
- Every package has
"type": "module"in package.json - Vite for fast builds with Hot Module Replacement (HMR)
- Bun as package manager (3-5x faster than npm)
- Turbo for monorepo orchestration with intelligent caching
- External dependencies: Not bundled, loaded by browser on-demand
Benefits of ESM:
- Better browser caching (shared dependencies across elements)
- Smaller initial bundle sizes (tree-shaking)
- Faster development (native ESM in browsers, instant HMR)
- Standards-based (no custom module system)
This was enabled by the PIE team's work on upstream library updates (React 18, MUI 7, Tiptap editor).
Legacy approach: Separate players and packages for different views:
- One player for delivery (student/teacher interaction)
- Separate authoring interface
- Separate package (
@pie-framework/pie-print) for print views
Modern approach: Two-level player architecture with clear separation of concerns.
Element-Level Player (this repository):
<pie-element-player>- Unified delivery, authoring, and print views- Package:
@pie-element/element-player - Use for: Element development, testing, documentation, and optional composable embedding
- Positioning: Not the default production orchestration path; production usage typically relies on the standard players in upstream
pie-elementsandpie-players
Item-Level Players (pie-players repository):
- Interactive players (esm-player, fixed-player, etc.) - Multi-element assessments
<pie-print>- Print views for production- Package:
@pie-player/print - Use for: Production applications, complete assessment items
Timed Media Section Context:
Future video-stimulus work in this repository should treat the media component as shared stimulus only. Cue-to-question orchestration, child item sessions, playback policy, and aggregate section completion belong to the timed-media section architecture documented in the pie-players repository (docs/architecture/timed-media-section.md).
Cross-element shared contracts such as host event projections, section score rollups, media/evidence metadata, accessibility handoff patterns, and standards-adapter hooks belong in pie-players shared architecture rather than individual element APIs; see docs/architecture/shared-contracts-p0.md in that repository.
Element structure: Each element has symmetric peer folders:
delivery/- Student/teacher interactionauthor/- Configuration interfacecontroller/- Business logicprint/- Print view (custom element)
Why ESM enables this: Dependencies are loaded on-demand, so we can have specialized players without worrying about bundle sizes. Element-level players load single elements via import maps. Item-level players dynamically load and orchestrate multiple element types from CDN.
Print architecture key insight: Print components are self-contained. Each element's print export handles its own transformations (preparePrintModel), role-based visibility, and rendering. Players simply load and orchestrate them - element-level for development, item-level for production.
View-based architecture: ESM's module system enables a powerful pattern - UI variants through subpaths. Instead of duplicating entire elements for different use cases (mobile, accessibility, branding, complexity levels), elements can provide multiple UI implementations that share the same controller logic. The ESM player loads the appropriate view on-demand with automatic fallback support.
Legacy approach: Asymmetric structure
- Root of package = student view
config/directory for authoring- Print was a separate package
Modern approach: Symmetric peer folders at src/ level
src/
├── delivery/ # Student/teacher interaction
├── author/ # Configuration interface
├── controller/ # Business logic
└── print/ # Print view (optional)
Benefits:
- Clear separation of concerns
- Each view is a peer (no implicit hierarchy)
- Easy to add new views (
listview/,mini/, etc.) as additional peers - Predictable package structure across all elements
The symmetric peer-folder structure combined with ESM's module system enables a powerful pattern: multiple UI implementations sharing a single controller.
packages/multiple-choice/
├── delivery/ # Standard UI
├── delivery-mobile/ # Touch-optimized UI (larger tap targets)
├── delivery-a11y/ # Accessibility-optimized UI
├── delivery-simple/ # Simplified UI for younger students
├── delivery-branded/ # Custom district branding
├── author/ # Configuration UI
├── controller/ # Shared business logic (scoring, validation)
└── print/ # Print view
// Load mobile-optimized view with automatic fallback
await esmLoader.load(config, document, {
view: 'delivery-mobile',
fallback: 'delivery',
loadControllers: true
});
// Load accessibility-optimized view
await esmLoader.load(config, document, {
view: 'delivery-a11y',
fallback: 'delivery',
loadControllers: true
});✅ Single source of truth - Controller logic (scoring, validation, outcomes) maintained in one place ✅ Consistent behavior - All variants use the same business logic, ensuring consistent assessment results ✅ Easy maintenance - Bug fixes and feature updates benefit all UI variants simultaneously ✅ Reduced duplication - No need to copy-paste controller code across multiple element packages ✅ Flexible deployment - Districts/users can choose their preferred UI without forking elements ✅ Graceful fallback - If a specialized view doesn't exist, automatically fall back to standard view
- Device optimization - Mobile vs desktop vs tablet layouts
- Accessibility - Screen reader optimized, high contrast, simplified visuals
- Age/grade adaptation - Simplified UI for younger students, advanced for older
- Branding - District-specific themes without forking elements
- Language/cultural adaptation - RTL layouts, culturally appropriate imagery
- Performance - Lightweight variants for low-bandwidth environments
In the old system, supporting these variations required either:
- ❌ Duplicating entire elements (e.g.,
multiple-choice-mobile,multiple-choice-a11y) - ❌ Complex conditional rendering in a single monolithic component
- ❌ Custom build configurations per variant
Each variant is simply a peer folder with its own implementation, loaded on-demand via ESM subpath imports. The ESM player handles view selection, fallback logic, and lazy loading automatically.
Legacy approach: Bespoke tooling
- Custom
pie-clifor element management pie-shared-lib-builderfor webpack configuration- Custom build scripts and conventions
Modern approach: Standard, widely-adopted tools
- Vite: Industry-standard build tool with excellent ESM support
- Bun: Modern package manager and runtime
- Turbo: Proven monorepo orchestration
- TypeScript: First-class throughout
- Biome: Modern linting and formatting
- Project CLI (
tools/cli): Built with oclif, has natural access to workspace code, used for upstream sync operations (not build tooling)
Developer experience benefits:
- Extensive documentation and community support for standard tools
- IDE integration works out of the box
- Fewer bespoke concepts to learn (build tooling is standard Vite)
- Can leverage ecosystem tooling (plugins, extensions)
- CLI uses oclif framework for robust command structure
Legacy approach: Per-element demos in docs/demo/ folders, generated by pie CLI tooling (pie install, pie clean commands). Each element had its own config.js, index.html, and generate.js.
Modern approach: Single unified demo app (apps/element-demo) built with SvelteKit.
Key differences:
- Legacy: Tool-generated demo per element, separate HTML files, required pie CLI commands
- Modern: One app demos ALL elements, SvelteKit routing, auto-discovery of elements
- Framework-agnostic: The modern demo loads elements built with ANY framework (React, Svelte, future Vue/Angular)
- Live editing: Vite HMR with instant updates vs static generated HTML
- Internal dev tool: Not published, purely for development
The demo system works by dynamically loading elements via the unified player, regardless of their underlying framework implementation.
Legacy approach:
pie-libwas a separate repository- Required coordinating releases across repos
- Version drift between pie-elements and pie-lib
Modern approach:
@pie-lib/*packages pulled into monorepo underpackages/lib-react/- Single source of truth for all PIE code
- Workspace references (
"workspace:*") ensure consistency
This project currently uses independent package versioning. Each publishable
@pie-element/* and @pie-lib/* package carries its own npm version, and release
automation publishes only the explicitly selected packages plus any dependents
that Changesets must bump.
Local development still uses workspace references ("workspace:*") so packages
are linked consistently inside the monorepo. Those workspace references are a
development and build-time mechanism, not a promise that every npm package shares
one version number.
Why independent versions here?
- Compatibility with existing consumers - Legacy PIE consumers already depend on independently versioned element packages.
- Selective publishing - A fix to one element should not publish dozens of unchanged packages unless dependency propagation requires it.
- Migration safety - Synced packages can preserve upstream package versions
during the migration from
../pie-elementsand../pie-lib. - Clear release blast radius - Changesets records which packages changed and why, while avoiding a false impression that every package changed.
Coordinated release waves:
When a group of independent package releases belongs to the same product wave,
use a release label/tag such as pie-elements-ng-YYYY.MM.DD. The label provides
human-readable coordination across packages without forcing lockstep npm
versions.
Publishing strategy:
- Element packages (
@pie-element/*) are versioned and published by CI when selected for release. - Library packages (
@pie-lib/*) are also versioned/publishable when their public surface changes or when Changesets dependency propagation requires it. - Default bump policy is
patchunless a user explicitly requestsminorormajor. - Manual publishing must use the repository publish scripts so package selection and token handling remain explicit.
This differs from earlier design notes that proposed workspace-wide lockstep versions. Release labels provide coordination; package versions remain independent.
Legacy approach: CircleCI for continuous integration and deployment.
Modern approach: GitHub Actions for all CI/CD workflows.
- Integration: Native GitHub integration (no external service)
- Transparency: Workflows visible in repository
- Ecosystem: Can use marketplace actions
- Modern: Industry-standard CI/CD platform
This project maintains compatibility with the existing PIE ecosystem by syncing element implementations from upstream repositories.
-
pie-elements →
packages/elements-react/- 28 React element implementations synced from upstream
- Controllers (business logic)
- UI components (delivery, authoring, print modes)
-
pie-lib →
packages/lib-react/- Shared UI libraries (config-ui, render-ui, etc.)
- In some cases, we have full replacements for legacy pie-lib packages
- Leverage existing work - Reuse production-tested elements from upstream
- Maintain compatibility - Ensure consistency with existing PIE consumers
- Modernize existing code - Transform to ESM, TypeScript, and modern tooling
- Stable baseline - Synced React elements provide production-ready implementations
The CLI tool (tools/cli) handles synchronization:
bun cli upstream:syncProcess:
- Analyze - Scan upstream packages for ESM compatibility
- Copy - Extract controller and UI code from upstream
- Transform - Convert to modern format:
.js→.tsconversions.jsx→.tsxconversions- Import rewrites (
lodash→lodash-es, package path updates) - Inline constants and utilities
- Generate - Create ESM-compatible configs:
package.jsonwith proper exportsvite.config.tsfor buildstsconfig.jsonfor TypeScript
- Commit - Transformed source is committed to this repo
What Gets Committed:
- Transformed source files (
src/) - ~1000 files - Generated configs (
package.json,vite.config.ts) - Demo configs (
docs/demo/config.mjs)
What's Gitignored:
- Build artifacts (
dist/,node_modules/) - Generated demo metadata (has timestamps, machine-specific paths)
See upstream-sync-commit-guide.md for details.
Upstream sync is complete - the React elements and libraries we have today are the stable baseline. Any future element implementations (Svelte, Angular, Vue, etc.) will be developed natively in this repository, not synced from upstream.
Framework Flexibility:
Multiple framework implementations can coexist. Consumers choose based on their needs.
The architecture consists of three main layers:
Application Layer: A single unified demo app (apps/element-demo) built with SvelteKit that can demonstrate ALL elements regardless of their underlying framework. This is an internal development tool, not published to npm.
Element Layer: Elements can be implemented in any framework (currently React and Svelte), as long as they export web components and implement the PIE controller interface. The unified Element Player can load elements from any framework implementation.
Foundation Layer: Shared libraries (@pie-lib/* in packages/lib-react/), core PIE interfaces (packages/core), and framework-specific utilities coexist to support element development.
Key organizational decisions:
- @pie-lib integration: The
@pie-lib/*packages (formerly a separate repository) are now inpackages/lib-react/for better version management and coordination - Versioning: Changesets-managed coordinated versioning and publishing for
@pie-element/*and@pie-lib/*packages - Workspaces: Bun workspaces with
"workspace:*"references ensure consistency across the monorepo
Each element follows a consistent, symmetric structure where student UI, authoring UI, and business logic are peer folders at the same level:
Symmetric peer organization (contrast with legacy):
Legacy structure (asymmetric):
- Root of package = student view
config/directory for authoring- Print was a separate package
Modern structure (symmetric):
src/delivery/- student/teacher interactionsrc/author/- configuration interfacesrc/controller/- business logicsrc/print/- print view (optional)
Future views like mini/, listview/, or others can be added as additional peer folders. This symmetric organization makes the package structure predictable and enables the unified player to load any view on demand.
Location: src/controller/index.ts
Responsibility: Framework-agnostic business logic
Key Functions:
model()- Transform question model for renderingoutcome()- Calculate score and feedback
export async function model(
question: MultipleChoiceModel,
session: SessionData | null,
env: PieEnvironment,
): Promise<ViewModel> {
// Transform based on mode (gather/view/evaluate/authoring)
// Apply role-based permissions (student/instructor)
// Return view model for rendering
}
export async function outcome(
question: MultipleChoiceModel,
session: SessionData,
env: PieEnvironment,
): Promise<PieOutcome> {
// Calculate score
// Generate feedback
// Return outcome
}Why Separate: Controllers are framework-agnostic business logic that can be shared or ported across implementations.
Location: src/delivery/index.tsx (React) or src/delivery/{Element}.svelte (Svelte)
Responsibility: Render question and handle user interaction
Modes Handled:
gather- Interactive, user can answerview- Read-only, show question without interactionevaluate- Show score, feedback, correct answers
<script lang="ts">
let { model, session = $bindable(), env } = $props<Props>();
const isDisabled = $derived(
env.mode === 'view' || env.mode === 'evaluate'
);
</script>
<div class="pie-multiple-choice">
<Prompt prompt={model.prompt} />
{#if env.mode === 'gather'}
<!-- Interactive choices -->
{:else if env.mode === 'evaluate'}
<!-- Show correctness and feedback -->
{:else}
<!-- View mode: static display -->
{/if}
</div>Location: src/authoring/index.ts (React) or src/authoring/{Element}Config.svelte (Svelte)
Responsibility: Author/configure question settings
Features:
- Rich text editor for prompts
- Choice management (add/remove/edit)
- Configuration options (scoring, feedback, etc.)
- Preview of changes
<script lang="ts">
let { model = $bindable() } = $props<Props>();
function updatePrompt(html: string) {
model = { ...model, prompt: html };
}
</script>
<RichTextEditor
value={model.prompt}
onChange={updatePrompt}
/>The PIE controller is the core contract that all elements implement:
export interface PieController {
/**
* Transform question model for rendering
* @param question - Element configuration
* @param session - User's current answer/state
* @param env - Rendering environment (mode, role)
* @returns View model for component rendering
*/
model(
question: PieModel,
session: SessionData | null,
env: PieEnvironment,
): Promise<ViewModel>;
/**
* Calculate outcome based on session
* @param question - Element configuration
* @param session - User's answer
* @param env - Evaluation environment
* @returns Score, feedback, and correctness
*/
outcome(
question: PieModel,
session: SessionData,
env: PieEnvironment,
): Promise<PieOutcome>;
}export interface PieEnvironment {
mode: "gather" | "view" | "evaluate" | "authoring" | "print";
role: "student" | "instructor";
}Modes:
gather- Student answering questionview- Read-only display (no interaction)evaluate- Show score, feedback, correct answersauthoring- Authoring/configuration interfaceprint- Static rendering for paper/PDF
Roles:
student- Learner interacting with assessmentinstructor- Teacher/author viewing or configuring
export interface PieModel {
id: string;
element: string;
prompt?: string;
// Element-specific properties
}export interface PieOutcome {
score: number; // 0.0 to 1.0
correct?: boolean; // Binary correct/incorrect
feedback?: string; // Feedback message
rationale?: string; // Explanation
// Element-specific outcome data
}Session represents the user's current answer/state for an element.
interface Session {
value?: unknown; // Element-specific answer data
// Additional session metadata
}Flow:
- User interacts with element
- Component updates
sessionvia$bindable() - Parent receives
onSessionChangeevent - Session persisted (by consumer application)
- On page reload, session passed back to element
Model represents the question configuration (authored content).
Immutable: Models should not change during student interaction. Changes only happen in authoring mode.
Svelte 5 runes provide fine-grained reactivity:
<script lang="ts">
// Props (from parent)
let { model, session = $bindable(), env } = $props<Props>();
// Local state
let localState = $state(0);
// Derived/computed values
const isDisabled = $derived(env.mode !== 'gather');
// Side effects
$effect(() => {
console.log('Session changed:', session);
});
</script>Core: No dependencies (pure interfaces)
Shared: Depends on core
Lib-UI: Depends on core, may use shared utilities
Elements: Depend on core, lib-ui, shared
Apps: Depend on elements
- Core defines interfaces only
- Shared utilities are stateless
- Elements don't depend on each other
- Apps are top-level consumers
Benefits:
- Native Web Components support
- Smaller bundle size (~3KB overhead vs 40KB+ for React)
- True reactivity without virtual DOM
- Runes API provides clear, predictable state management
- Excellent TypeScript support
Trade-offs:
- Smaller ecosystem than React
- Less community resources
- Learning curve for React developers
Benefits:
- Type safety catches errors early
- Better IDE support (autocomplete, refactoring)
- Self-documenting code
- Easier refactoring
- Improved maintainability
Benefits:
- Fast package installation (3-5x faster than npm)
- Built-in test runner
- Native TypeScript support
- All-in-one tool (package manager + bundler + runner)
Trade-offs:
- Newer, less proven than npm/yarn
- Some compatibility issues (using Vitest instead of Bun test for now)
Benefits:
- Extremely fast HMR (Hot Module Replacement)
- Native ESM support
- Excellent Svelte integration
- Modern, optimized builds
Benefits:
- Fast monorepo builds (caching, parallelization)
- Task orchestration
- Smart dependency graph execution
- Unit Tests - Controller logic, utilities
- Component Tests - Svelte components with Testing Library
- Integration Tests - Multi-package workflows
- E2E Tests - Full browser automation with Playwright
- Accessibility Tests - axe-core scans
- Evaluation Tests - YAML-driven comprehensive scenarios
See CLAUDE.md for the full testing strategy.
IMPORTANT: This is an internal development tool, NOT published to npm. It exists solely for testing and demonstrating elements during development.
Location: apps/element-demo/
Purpose: A single unified demo application that can demonstrate ALL PIE elements, regardless of which framework they're built with (React, Svelte, or future frameworks).
The demo uses the unified Element Player to dynamically load elements via custom element registration, making it framework-agnostic.
Features:
- Interactive element testing (gather, view, evaluate modes)
- Live model/session editing
- Controller testing
- Math rendering preview
- Accessibility testing
Architecture:
apps/element-demo/
├── src/
│ ├── routes/
│ │ └── [element]/ # Dynamic route for any element
│ │ ├── +page.svelte # Main player page
│ │ └── +layout.ts # Data loader
│ └── lib/
│ ├── element-player/ # Player implementation
│ ├── elements/
│ │ └── registry.ts # GENERATED: Element metadata
│ ├── data/
│ │ └── sample-configs/ # GENERATED: Sample configs
│ └── element-imports.ts # GENERATED: Import map
Generated Files (gitignored):
These files are automatically generated and should NOT be committed:
-
registry.ts- Element metadata (has timestamps)- Generated by scanning
packages/elements-react/ - Created during
upstream:syncandpredevscript
- Generated by scanning
-
element-imports.ts- Import map- Maps element names to absolute
/@fs/paths for Vite - Generated by
predevscript beforebun run dev
- Maps element names to absolute
-
sample-configs/- Demo data- Copied from
packages/elements-react/*/docs/demo/ - Redundant (source is already in element packages)
- Copied from
Running the Demo:
cd apps/element-demo
bun run dev # Runs predev script, then starts dev serverThe predev script automatically regenerates all required files before starting the server.
URL Format:
http://localhost:5173/[element-name]
Examples:
http://localhost:5173/multiple-choice
http://localhost:5173/hotspot
Contrast with legacy:
Legacy (webpack + IIFE):
- Bundled ALL dependencies into single IIFE file
- Large bundle sizes (~1-2MB per element)
- No tree-shaking across elements
- Required custom bundle service (pie-shared-lib-builder)
Modern (ESM + Vite):
- External dependencies loaded by browser
- Small element bundles (~15-40KB per element)
- Browser caches shared dependencies
- Tree-shaking works naturally
- Standards-based module loading
This ESM-first approach is what enables the unified player architecture - the browser manages dependency loading, so we don't need separate bundles for each view.
bun run dev- Watches for file changes
- Rebuilds affected packages
- Hot module replacement in the demos app
- Fast feedback loop
bun run build- Turbo orchestrates build order (respects dependencies)
- Each package builds with Vite
- TypeScript compilation
- Bundle optimization (tree-shaking, minification)
- Output to
dist/directories
ESM (ES Modules) format for modern bundlers:
// dist/index.js
export { default as MultipleChoice } from "./MultipleChoice.js";
export { model, outcome } from "./controller.js";IIFE (Immediately Invoked Function Expression) format for CDN deployment:
// dist/index.iife.js
(function () {
// Self-contained bundle with all dependencies (React, etc.)
// Auto-registers custom element: <multiple-choice-pie>
// Size: ~1.2MB / 400KB gzipped
})();Key difference from original pie-elements: The original @pie-element/* packages do NOT include IIFE builds. They only publish CommonJS (lib/index.js) and ESM source (src/index.js). This project adds IIFE builds to enable zero-config CDN deployment via <script> tags.
Types: TypeScript definitions generated:
// dist/index.d.ts
export declare const MultipleChoice: Component<Props>;All elements must meet WCAG 2.2 Level AA standards:
- Perceivable: Text alternatives, color contrast, adaptable layouts
- Operable: Keyboard navigation, sufficient time, seizure prevention
- Understandable: Readable text, predictable behavior, input assistance
- Robust: Compatible with assistive technologies
Every element must pass:
- Automated scans with axe-core
- Keyboard navigation testing
- Screen reader testing (manual)
- Touch target size validation (44x44px minimum)
- Color contrast checks
- Element (Svelte): < 15KB gzipped
- Element (React): < 40KB gzipped (includes React runtime)
- Shared libraries: < 30KB gzipped each
- Total for typical assessment: < 200KB gzipped
- Tree-shaking: Only include used code
- Code splitting: Load elements on-demand
- Dynamic imports: Lazy-load heavy dependencies (MathLive, etc.)
- Svelte compilation: No runtime overhead
- Shared dependencies: Deduplicate common libraries
- First Contentful Paint (FCP) < 1s
- Time to Interactive (TTI) < 3s
- Lighthouse score > 90
- DOMPurify sanitizes all HTML content
- Never use
{@html}without sanitization - Validate user input on both client and server
Elements should work with strict CSP:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
Note: 'unsafe-inline' for styles is required for Svelte scoped styles.
Current Strategy: Publishing is CI-driven via GitHub Actions and Changesets:
- Developer creates changeset:
bun run changeset - PR merged to
master(stable) ordevelop(prerelease) - GitHub Action creates "Version Packages" PR
- Maintainer merges Version PR
- Packages automatically published to npm
Dist-tag routing:
master-> release channelstable-> npm taglatestdevelop-> release channelnext-> npm tagnext- optional beta workflows -> release channel
beta-> npm tagbeta
Branch/channel mismatches are guarded in CI and publish scripts.
If a publish fails after merge, maintainers can manually rerun the Release workflow
with release_intent=publish and force_publish=true to recover without creating a
new version-bump commit.
See PUBLISHING.md for policy details, backfill runbook, and verification commands.
This project uses independent package versioning. Each publishable
@pie-element/* and @pie-lib/* package has its own npm version, while release
labels can group a coordinated release wave across packages.
Current Status: @pie-element/* and @pie-lib/* packages are publish-enabled and released through the Changesets CI flow.
See section 8 above for the current policy.
Semantic Versioning (SemVer):
Each changed package follows semantic versioning independently:
- Major: Breaking changes in that package
- Minor: Backward-compatible new features in that package
- Patch: Bug fixes in that package
How versioning works:
- Developer makes changes to any package(s)
- Run
bun run changesetto describe the change - Changesets determines the appropriate version bump for selected packages
- Only selected packages and dependency-propagated packages are published
- A release label may be added when several independent packages ship together
Example release:
$ bun run changeset
# Select "minor" for a new feature in multiple-choice
# PR merged
# Only affected packages are bumped/published.
@pie-element/multiple-choice: 1.6.0Current Reality: Package releases are coordinated through the CI release workflow.
Workspace builds and contract checks ensure compatible package surfaces before
publish. External consumers install the specific @pie-element/* packages they
need.
Packages can be loaded from CDN:
<script type="module">
import { MultipleChoice } from "https://esm.sh/@pie-element/multiple-choice";
</script>See CLAUDE.md for the step-by-step guide and element conventions.
Key requirements:
- Implement PIE controller interface
- Create student component
- Create authoring component
- Add comprehensive tests
- Document usage
Elements use CSS variables for theming:
:root {
--color-primary: #3b82f6;
--color-base-100: #ffffff;
--color-base-content: #1f2937;
}Consumers can override variables to match their brand.
Planned extension points:
- Custom validators
- Custom feedback generators
- Custom rendering plugins
- Third-party integrations
Rule: Session flows from element → player only, never back.
<!-- ❌ Wrong: bidirectional creates infinite loops -->
let { session = $bindable({}) } = $props();
$effect(() => { element.session = session; }); // Triggers loop
<!-- ✅ Correct: read-only, observe via events -->
let { session = {} } = $props();
let internalSession = $state(session);
function handleSessionChange(event) {
internalSession = event.detail.session;
dispatch('session-changed', event.detail);
}Why: Elements own their session state (user responses). Players observe changes via events. Pushing session back to elements creates loops: update → effect → element fires event → update → repeat.
Use $bindable only for true bidirectional flow:
- ✅ UI controls:
mode,playerRole,splitRatio - ✅ Settings:
partialScoring,addCorrectResponse - ❌ Session state (element owns it)
- ❌ Derived values (use
$derivedinstead)
-
Multi-Framework Support
- Add native implementations in additional frameworks as needed
- Svelte 5 elements for smaller bundles (~15KB vs ~40KB)
- Angular elements for Angular-native consumers
- Vue elements for Vue-native consumers
- All developed directly in this repository
-
Web Components as Distribution Format
- Framework-agnostic custom elements
- Standard browser APIs
- Universal compatibility across frameworks
- Can wrap any framework implementation
-
Plugin Architecture
- Extensible validation
- Custom scoring algorithms
- Custom rendering plugins
- Third-party integrations
-
Edge Runtime Support
- Deno, Cloudflare Workers compatibility
- Serverless controller execution
- Distributed scoring
Document Version: 1.0 Last Updated: 2025-01-07 Status: Living Document











