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

create-svelte-scorm

v0.1.1

Published

Scaffold a Svelte SCORM e-learning project

Downloads

12,130

Readme

SCORM Template — Course Author Reference

To initialize a new template project from this repo, run:

pnpm create svelte-scorm my-project

or using npx:

npx create-svelte-scorm my-project

Table of Contents


Course Definition

Define your course in src/course.ts using three helper functions:

import { defineCourse, defineLesson, defineSlide } from '$core/player/types.js';
import CourseFrame from './course/layouts/CourseFrame.svelte';
import LessonFrame from './course/layouts/LessonFrame.svelte';

export const course = defineCourse({
	id: 'my-course',
	title: 'My Course',
	description: 'Optional description',
	masteryScore: 80, // optional, score to pass
	minScore: 0,
	maxScore: 100,
	sequencing: 'linear', // 'linear' (default) or 'free'
	storageMode: 'standard', // 'standard' (default) or 'chunked'
	layout: CourseFrame, // optional course-level layout
	loadingComponent: MyLoader, // optional custom loading screen
	lessons: [
		defineLesson({
			id: 'intro',
			title: 'Introduction',
			description: 'Optional',
			layout: LessonFrame, // optional lesson-level layout
			slides: [
				defineSlide({
					id: 'welcome',
					title: 'Welcome', // optional, falls back to id
					description: 'Optional slide description',
					component: () => import('./course/slides/welcome/WelcomeSlide.svelte')
				}),
				defineSlide({
					id: 'quiz',
					title: 'Quiz',
					completionMode: 'manual', // requires explicit markPassed()
					component: () => import('./course/slides/quiz/Quiz.svelte')
				})
			]
		})
	]
});

Types

| Type | Fields | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | | CourseDefinition | id, title, description?, masteryScore?, minScore, maxScore, sequencing?, storageMode?, layout?, loadingComponent?, lessons | | LessonDefinition | id, title, description?, layout?, slides | | SlideDefinition | id, title?, description?, completionMode?, component (lazy import) | | LayoutComponent | Component<{ children: Snippet }> — wraps child content |

Runtime: CourseSlide

Each slide gets enhanced at runtime with:

type CourseSlide = SlideDefinition & {
	index: number; // 0-based position in entire course
	total: number; // total slide count
	lessonId: string;
	lessonTitle: string;
	pathname: `/${string}`; // e.g. '/intro/welcome'
	completionMode: SlideCompletionMode; // resolved: 'auto' or 'manual'
};

Sequencing

Controls whether learners must complete slides in order.

defineCourse({
	sequencing: 'linear' // default — slides gate in order
	// sequencing: 'free', // all slides accessible at any time
});

How linear sequencing works

  • A slide is locked if the previous slide has not been passed
  • goto() silently refuses navigation to locked slides
  • goNext() refuses if the current slide is not passed
  • goPrevious() always works — backward navigation is unrestricted
  • The first slide is never locked

How free sequencing works

  • All slides are accessible at any time
  • No navigation restrictions

Checking lock state

coursePlayer.isLocked(slide); // true if the slide can't be navigated to yet
coursePlayer.getSlideStatus(slide); // 'passed' | 'failed' | 'incomplete' | 'not attempted'

Slide Completion

Each slide has a completionMode that determines how it gets marked as passed.

Completion modes

| Mode | Behavior | Use for | | ---------- | -------------------------------------------------- | ---------------------- | | 'auto' | Marked as passed automatically on visit (default) | Content/info slides | | 'manual' | Must call markPassed() explicitly from the slide | Quiz/assessment slides |

useSlideCompletion() hook

For manual-completion slides, use this hook inside your slide component:

import { useSlideCompletion } from '$core/player';

const completion = useSlideCompletion();

Returned handle

| Property / Method | Type | Description | | ---------------------------- | ---------------------- | ------------------------------------------------------------------- | | status | SlideObjectiveStatus | Reactive: 'passed', 'failed', 'incomplete', 'not attempted' | | isPassed | boolean | Reactive: slide is passed | | isFailed | boolean | Reactive: slide is failed | | score | number \| undefined | Reactive: slide score (0–100) | | markPassed() | void | Mark the slide as passed | | markFailed() | void | Mark the slide as failed | | markPassedWithScore(score) | void | Mark passed with a score (0–100) |

A slide that has been marked 'passed' is never downgraded to 'failed' or 'incomplete'.

Example: quiz slide with manual completion

For simple cases you can use useSlideCompletion() directly, but for quizzes with scoring, thresholds, and course score integration, use defineTest() instead (see Quiz & Test System).

<script lang="ts">
	import { defineMultiChoiceQuestion, defineTest } from '$core/quiz';

	const q1 = defineMultiChoiceQuestion({
		id: 'scorm-acronym',
		question: 'What does SCORM stand for?',
		options: [
			{ key: 'A', label: 'Shareable Content Object Reference Model' },
			{ key: 'B', label: 'Standard Course Object Resource Manager' }
		],
		correctAnswer: 'A'
	});

	// defineTest handles slide completion, scoring, and course score automatically
	const test = defineTest({
		id: 'my-quiz',
		questions: [q1],
		passThreshold: 1.0 // 100% to pass (default)
	});
</script>

Zero-config behavior

  • Content slides auto-complete on visit — no code needed
  • Quiz slides: add completionMode: 'manual' in course.ts, then use defineTest() in the component — it handles slide completion automatically
  • Default sequencing is 'linear' — no config needed for gated navigation

Course Player

import { coursePlayer } from '$core/player';

Properties

| Property | Type | Description | | --------------- | -------------------------- | -------------------------------------------- | | slides | CourseSlide[] | All slides in the course | | activeSlide | CourseSlide \| undefined | Currently displayed slide (reactive) | | canGoNext | boolean | Reactive: next slide exists and is reachable | | canGoPrevious | boolean | Reactive: previous slide exists | | firstPath | `/${string}` | Path to the first slide | | isNavigating | boolean | True while navigating (reactive) |

Methods

await coursePlayer.goto('/lesson-id/slide-id'); // navigate to path (refused if locked)
await coursePlayer.goNext(); // next slide (refused if canGoNext is false)
await coursePlayer.goPrevious(); // previous slide (refused if canGoPrevious is false)
coursePlayer.isLocked(slide); // is this slide locked by sequencing?
coursePlayer.getSlideStatus(slide); // 'passed' | 'failed' | 'incomplete' | 'not attempted'

Player Metrics

All metrics are reactive ($derived). Access via coursePlayer:

coursePlayer.course

| Field | Type | Description | | ----------------- | -------- | -------------------------------------------- | | slideNumber | number | Current slide (1-based) | | totalSlides | number | Total slides in course | | slidesCompleted | number | Slides with status 'passed' | | progress | number | Percentage 0–100 (based on slides completed) | | lessonNumber | number | Current lesson (1-based) | | totalLessons | number | Total lessons |

coursePlayer.lesson

| Field | Type | Description | | ----------------- | -------- | ------------------------------------------ | | id | string | Current lesson ID | | title | string | Current lesson title | | slideNumber | number | Slide position within lesson (1-based) | | totalSlides | number | Slides in current lesson | | slidesCompleted | number | Completed slides in current lesson | | progress | number | Lesson progress 0–100 (based on completed) |

coursePlayer.slide

| Field | Type | Description | | ----------- | -------- | -------------------------- | | id | string | Current slide ID | | title | string | Current slide title | | elapsedMs | number | Time on current slide (ms) |

coursePlayer.session

| Field | Type | Description | | ----------- | -------- | --------------------------------------- | | elapsedMs | number | Total session time (ms), ticks every 1s |

Usage

<p>Progress: {coursePlayer.course.progress}%</p>
<p>
	Slide {coursePlayer.course.slideNumber} of {coursePlayer.course.totalSlides}
</p>
<p>
	Completed: {coursePlayer.course.slidesCompleted} / {coursePlayer.course.totalSlides}
</p>
<p>Time on slide: {Math.round(coursePlayer.slide.elapsedMs / 1000)}s</p>

SCORM State

import { scormState } from '$core/scorm';

Properties

| Property | Type | Description | | ------------------ | ------------------------------ | ------------------------------------------- | | isConnected | boolean | SCORM API found | | isInitialized | boolean | Successfully initialized | | version | '1.2' \| '2004' \| undefined | Detected SCORM version | | mode | 'lms' \| 'dev' | Running against LMS or localStorage | | location | string | Current slide path (read/write, auto-saved) | | sessionStartTime | number | Timestamp when course started | | sessionElapsedMs | number | Elapsed time (read-only) |

Student Info

scormState.student.id; // learner ID from LMS
scormState.student.name; // learner name from LMS

Session Info

scormState.session.mode; // 'normal' | 'browse' | 'review'
scormState.session.credit; // 'credit' | 'no-credit'
scormState.session.entry; // 'ab-initio' | 'ab_initio' | 'resume' | ''

Methods

scormState.commit(); // force save to LMS
scormState.terminate(); // end session (called automatically on page unload)

Quiz & Test System

The quiz system provides reusable Question and Test abstractions. Course authors define questions, group them into a test, and everything else — slide completion, weighted scoring, pass thresholds, and course score aggregation — is handled automatically.

Question types

defineMultiChoiceQuestion()

import { defineMultiChoiceQuestion } from '$core/quiz';

const q = defineMultiChoiceQuestion({
	id: 'my-question',
	question: 'What does SCORM stand for?',
	options: [
		{ key: 'A', label: 'Shareable Content Object Reference Model' },
		{ key: 'B', label: 'Standard Course Object Resource Manager' },
		{ key: 'C', label: 'Synchronized Content Online Reference Module' },
		{ key: 'D', label: 'Simple Course Object Runtime Model' }
	],
	correctAnswer: 'A',
	weight: 2 // optional, default 1
});

defineTrueFalseQuestion()

import { defineTrueFalseQuestion } from '$core/quiz';

const q = defineTrueFalseQuestion({
	id: 'tf-question',
	question: 'SCORM 2004 supports sequencing and navigation.',
	correctAnswer: 'true', // "true" or "false"
	weight: 1 // optional, default 1
});

The Question interface

Both factories return an object conforming to Question. Future question types (fill-in, matching, etc.) implement the same interface.

| Property / Method | Type | Description | | ----------------- | ----------------------- | ------------------------------------------------- | | id | string | Short ID provided by the author | | fullId | string | Globally unique: q:{lessonId}:{slideId}:{id} | | question | string | Question text | | weight | number | Weight for scoring (default 1) | | selectedAnswer | string \| undefined | Writable reactive state — bind UI selections here | | isPassed | boolean | Reactive: answered correctly | | attempts | RecordedInteraction[] | All recorded attempts | | handleSubmit() | void | Submits selectedAnswer to SCORM |

Set the user's selection directly on the question:

<button onclick={() => (q.selectedAnswer = 'A')}>Option A</button>

defineTest()

Groups questions into a test with weighted scoring and a pass threshold. The test owns slide completion — you never call useSlideCompletion() yourself.

import { defineMultiChoiceQuestion, defineTest } from "$core/quiz";

const q1 = defineMultiChoiceQuestion({ id: "q1", ..., weight: 2 });
const q2 = defineMultiChoiceQuestion({ id: "q2", ... });

const test = defineTest({
  id: "knowledge-check",
  questions: [q1, q2],
  passThreshold: 0.5, // 50% to pass (0–1, default 1.0)
});

TestHandle properties and methods

| Property / Method | Type | Description | | ------------------- | ---------------- | -------------------------------------------------- | | id | string | Test ID | | fullId | string | Globally unique: test:{lessonId}:{slideId}:{id} | | questions | Question[] | All questions in the test | | score | number | Weighted score 0–100% (reactive) | | isPassed | boolean | Reactive: score >= passThreshold * 100 | | isSubmitted | boolean | Reactive: test has been submitted | | allAnswered | boolean | Reactive: every question has a selectedAnswer | | hasIncorrect | boolean | Reactive: submitted with at least one wrong answer | | passedCount | number | Number of correctly answered questions | | answeredCount | number | Number of questions with a selected answer | | questionResult(q) | QuestionResult | 'correct', 'incorrect', or undefined | | submit() | void | Submits all questions' selectedAnswers | | retry() | void | Clears submitted state and all selections |

What defineTest() handles automatically

  1. Slide completion — calls useSlideCompletion() internally; marks the slide as passed (with score) or sets the score on failure
  2. Weighted scoringscore = (earned weight / total weight) * 100
  3. Pass thresholdisPassed when score >= passThreshold * 100
  4. Session restore — if the test was already passed in a previous session, pre-fills selectedAnswer from persisted attempts
  5. Course score — all tests automatically contribute to scormState.score.raw via the test registry (scaled to the course's minScoremaxScore range)

Course score aggregation

Every defineTest() call registers the test with a global testRegistry. The registry:

  • Averages all test scores into a single course score percentage
  • Scales that percentage to the course's minScoremaxScore range and writes it to scormState.score.raw
  • Calls scormState.completion.setPassed() when all tests pass

No manual wiring needed. If your course has two quiz slides each with a defineTest(), the course score is the average of their two scores.

Full example

<script lang="ts">
	import { defineMultiChoiceQuestion, defineTest } from '$core/quiz';
	import { Button } from '$lib/components/ui/button/index.js';

	const q1 = defineMultiChoiceQuestion({
		id: 'scorm-acronym',
		question: 'What does SCORM stand for?',
		options: [
			{ key: 'A', label: 'Shareable Content Object Reference Model' },
			{ key: 'B', label: 'Standard Course Object Resource Manager' }
		],
		correctAnswer: 'A',
		weight: 2
	});

	const q2 = defineMultiChoiceQuestion({
		id: 'scorm-version',
		question: 'Which SCORM version introduced sequencing?',
		options: [
			{ key: 'A', label: 'SCORM 1.2' },
			{ key: 'B', label: 'SCORM 2004' }
		],
		correctAnswer: 'B'
	});

	const test = defineTest({
		id: 'knowledge-check',
		questions: [q1, q2],
		passThreshold: 0.5
	});
</script>

{#each test.questions as q, qi (q.id)}
	{@const result = test.questionResult(q)}
	<h2>{qi + 1}. {q.question}</h2>
	{#each q.options as option (option.key)}
		<button disabled={test.isSubmitted} onclick={() => (q.selectedAnswer = option.key)}>
			{option.key}. {option.label}
		</button>
	{/each}
	{#if result === 'correct'}<p>Correct!</p>{/if}
	{#if result === 'incorrect'}<p>Incorrect — try again.</p>{/if}
{/each}

<p>{test.answeredCount} of {test.questions.length} answered</p>

{#if test.isPassed}
	<p>Passed — {Math.round(test.score)}%</p>
{:else if test.hasIncorrect}
	<p>{test.passedCount} of {test.questions.length} correct</p>
	<Button onclick={() => test.retry()}>Retry</Button>
{:else}
	<Button disabled={!test.allAnswered} onclick={() => test.submit()}>Submit</Button>
{/if}

Creating a new question type

To add a new question type (e.g., fill-in-the-blank):

  1. Create src/_core/quiz/define-fill-in.svelte.ts
  2. Export a factory that returns an object conforming to Question
  3. Internally call scormState.store.recordInteraction({ type: "fill-in", ... })
  4. Re-export from src/_core/quiz/index.ts

The new type works with defineTest() immediately — no changes to the test or scoring system.


Course Score vs Slide Score

These are two separate concepts that serve different purposes.

Course score (scormState.score)

The course score is the overall grade reported to the LMS via cmi.core.score (SCORM 1.2) or cmi.score (SCORM 2004). This is what the LMS displays in its gradebook.

scormState.score.raw; // get/set — clamped to [min, max]
scormState.score.min; // read-only (set from course definition)
scormState.score.max; // read-only (set from course definition)
scormState.score.scaled; // read-only, SCORM 2004 only (auto-calculated)
  • Range defined by minScore / maxScore in defineCourse()
  • If you use defineTest(), the course score is set automatically via the test registry (see Quiz & Test System)
  • You can also set it manually for custom grading logic
  • The LMS uses this alongside masteryScore to determine pass/fail
scormState.score.raw = 85;
// SCORM 1.2:  raw = 85
// SCORM 2004: raw = 85, scaled = 0.85 (auto)

Slide score (useSlideCompletion().score)

The slide score is a per-slide value (0–100) stored in SCORM objectives. It is used internally for sequencing and progress tracking.

const completion = useSlideCompletion();
completion.markPassedWithScore(90); // stores score=90, min=0, max=100 on this slide's objective
completion.score; // 90
  • Always 0–100 range (fixed)
  • Stored per-slide via cmi.objectives
  • Not automatically aggregated into the course score
  • Used for per-slide tracking, not LMS gradebook reporting

When to use which

| Scenario | Use | | ------------------------------------------ | -------------------------------------------------------------- | | Quiz slides with automatic scoring | defineTest() — handles both slide and course score | | Setting the overall grade manually | scormState.score.raw = 85 | | Tracking how a learner scored on one slide | completion.markPassedWithScore(90) | | Custom grading from slide scores | Read slide scores, compute average, set scormState.score.raw |


Course Completion vs Slide Completion

These are also two separate concepts.

Course completion (scormState.completion)

The course completion is the overall status reported to the LMS via cmi.core.lesson_status (SCORM 1.2) or cmi.completion_status / cmi.success_status (SCORM 2004). This is what the LMS uses to mark the course as done.

scormState.completion.status; // 'completed' | 'incomplete' | 'not attempted' | 'unknown'
scormState.completion.success; // 'passed' | 'failed' | 'unknown'

| Method | Status | Success | | ----------------- | ---------- | ------------- | | setCompleted() | completed | (unchanged) | | setIncomplete() | incomplete | unknown | | setPassed() | completed | passed | | setFailed() | completed | failed |

  • You call these yourself when your course logic decides the learner is done
  • The LMS uses this to determine if the learner has finished

Slide completion (useSlideCompletion() / coursePlayer.getSlideStatus())

Slide completion is a per-slide status stored in SCORM objectives. It drives sequencing (which slides are locked/unlocked) and progress tracking.

// From inside a slide component:
const completion = useSlideCompletion();
completion.status; // 'passed' | 'failed' | 'incomplete' | 'not attempted'
completion.isPassed; // true if passed
completion.markPassed(); // mark this slide as passed

// From anywhere:
coursePlayer.getSlideStatus(slide); // same status values
coursePlayer.isLocked(slide); // true if previous slide not passed (linear mode)
  • Managed automatically for 'auto' slides (passed on visit)
  • Managed via useSlideCompletion() for 'manual' slides
  • Not automatically aggregated into course completion

When to use which

| Scenario | Use | | ------------------------------------------------------- | ------------------------------------------------------------------------------ | | Telling the LMS the learner finished the course | scormState.completion.setPassed() | | Gating navigation so slide 3 requires slide 2 to pass | Set completionMode: 'manual' + useSlideCompletion() | | Checking if all slides are done to decide course status | Read coursePlayer.course.slidesCompleted === coursePlayer.course.totalSlides |


Persistent Store

Key-value storage that persists in SCORM suspend_data.

const store = scormState.store;

String / Number / Boolean / Object

store.setString('theme', 'dark');
store.getString('theme'); // 'dark' | undefined

store.setNumber('fontSize', 16);
store.getNumber('fontSize'); // 16 | undefined

store.setBoolean('soundOn', true);
store.getBoolean('soundOn'); // true | undefined

store.setObject('prefs', { a: 1 });
store.getObject<{ a: number }>('prefs'); // { a: 1 } | undefined

store.has('theme'); // true
store.delete('theme');

All set operations auto-persist immediately.

Access All Variables

store.variables; // Record<string, unknown> — reactive

Interactions

Record learner responses to questions. Written to the SCORM cmi.interactions data model for LMS reporting.

store.recordInteraction({
	id: 'q:lesson1:slide1:question1',
	type: 'choice', // 'true-false' | 'choice' | 'fill-in' | 'matching' | etc.
	learnerResponse: 'A',
	correctResponse: 'B',
	result: 'incorrect', // 'correct' | 'incorrect' | 'unanticipated' | 'neutral'
	weighting: 1 // points
	// optional: latency, objectiveId, description (2004 only)
});

Reading History (current session only)

store.interactionHistory; // Record<string, RecordedInteraction[]>

// RecordedInteraction:
// { id, type, learnerResponse, correctResponse, result, weighting, timestamp }

Note: cmi.interactions are write-only on most SCORM 1.2 LMS. History is available within a session but not across sessions. Use objectives for cross-session state.


Objectives

Automatically created when you recordInteraction(). Persist across sessions via cmi.objectives.

store.isObjectivePassed('q:lesson1:slide1:question1'); // boolean
store.getObjective('q:lesson1:slide1:question1');
// { id, status: 'passed' | 'failed' | 'incomplete' | 'not attempted', score? }

How it works

  • When result === 'correct' -> objective status = 'passed'
  • When result === 'incorrect' -> objective status = 'failed'
  • A 'passed' objective is never downgraded to 'failed'
  • Objective IDs should use a q: prefix for quiz objectives: q:lessonId:slideId:questionId

Slide objectives

Slide completion state is also stored as objectives with IDs in the format slide:{lessonId}:{slideId}. These are managed automatically by the player — you don't need to interact with them directly.


Storage Modes

Set in defineCourse({ storageMode: ... }):

| Mode | How | Limits | Best for | | ---------------------- | ----------------------------------------------------- | ---------------------- | -------------------------------------- | | 'standard' (default) | Variables compressed in suspend_data | 4KB (1.2), 64KB (2004) | Most courses | | 'chunked' | Overflow data split across cmi.interactions records | Virtually unlimited | Large courses with lots of stored data |


App Structure

src/
  course.ts                    <- Define your course here
  App.svelte                   <- Root component (don't edit usually)
  _core/
    player/
      CourseShell.svelte       <- Handles init, routing, lifecycle (internal)
      player.svelte.ts         <- coursePlayer singleton
      player-metrics.svelte.ts <- Reactive metrics
      slide-completion.svelte.ts <- useSlideCompletion() hook
      slide-context.svelte.ts  <- Slide identity context (internal)
      router.svelte.ts         <- sv-router setup (internal)
      types.ts                 <- defineCourse/Lesson/Slide, type definitions
    scorm/
      state.svelte.ts          <- scormState singleton
      persistent-store.svelte.ts <- Key-value + interactions + objectives + slide tracking
      score-state.svelte.ts    <- Score management
      completion-state.svelte.ts <- Completion management
      storage/                 <- Storage engines (internal)
    quiz/
      types.ts                     <- Question, TestDefinition, TestHandle interfaces
      define-multi-choice.svelte.ts <- defineMultiChoiceQuestion factory
      define-true-false.svelte.ts  <- defineTrueFalseQuestion factory
      define-test.svelte.ts        <- defineTest factory
      test-registry.svelte.ts      <- Global test registry + course score sync
  course/
    layouts/
      CourseFrame.svelte       <- Course-level layout (sidebar + header)
      LessonFrame.svelte       <- Lesson-level layout (title + prev/next nav)
    slides/                    <- Your slide components go here
  lib/
    components/
      app-sidebar.svelte       <- Course navigation sidebar (lock/check icons)
      ui/                      <- shadcn-svelte components (button, sidebar, etc.)

Layouts

CourseFrame — wraps the entire course. Provides sidebar navigation, header, progress bar.

LessonFrame — wraps each lesson's slides. Shows lesson title, previous/next buttons.

Both accept a children snippet:

<script lang="ts">
	import type { Snippet } from 'svelte';
	let { children }: { children: Snippet } = $props();
</script>

<div class="my-layout">
	{@render children()}
</div>

Dev vs LMS Mode

| | Dev Mode | LMS Mode | | -------------- | --------------------------- | ------------------------------------ | | API | localStorage | SCORM API | | Detection | No SCORM API found | window.API or window.API_1484_11 | | Storage prefix | scorm-dev:{courseId}: | N/A | | Student info | Empty strings | From LMS | | Check | scormState.mode === 'dev' | scormState.mode === 'lms' |

Dev mode activates automatically when no LMS is detected (e.g., running vite dev). All data persists in the browser's localStorage.