Documentation menu

Documentation / Learning engine / lms-ui

Lms Ui

Package
@wabbit/tome-lms-ui
Version
v0.12.1
npm install @wabbit/tome-lms-ui
First install from our registry?
  1. Get a registry token from your credentials page. You need a purchase that includes this package, or a Craft Library membership.

  2. Add the registry and your token to the .npmrc at the root of your project, with your token in place of YOUR_TOKEN:

    @wabbit:registry=https://npm.wabbit.com/
    //npm.wabbit.com/:_authToken=YOUR_TOKEN
Learning engineStable

React render layer for the Tome LMS — lesson player (progress bar, course shell, curriculum sidebar, quiz/assignment renderers, notes panel) and the LearnerShell app shell; depends on @wabbit/tome-lms.

Overview

The React render layer for the Tome LMS: the lesson player (progress bar, course shell, curriculum sidebar, quiz/assignment renderers, prev/next nav, notes panel) and the LearnerShell app-shell (sidebar, nav groups, header, user menu, active-quest widgets). app-adjacent layer per root ARCHITECTURE.md — depends on @wabbit/tome-lms. Its styles read @wabbit/tome-ui's Layer-2 tokens through var(--tome-*), so @wabbit/tome-ui is not a peer.

Peer dependencies

PeerRange
@wabbit/tome-lms>=0.12.0 <1.0.0
next>=15.0.0
payload>=3.67.0
react>=19.0.0
lucide-react>=0.460.0

None of the peers above are marked optional in peerDependenciesMeta (there is no peerDependenciesMeta block) — all five are required. `@wabbit/tome-lms` is a required peer, not a regular dependency — verified against peerDependencies in package.json. qrcode (and server-only) genuinely are regular dependencies, not peers.

60-second quickstart

// A lesson page (client component tree under the course shell)
import { CourseShell, LessonContent, ProgressBar } from '@wabbit/tome-lms-ui'
import '@wabbit/tome-lms-ui/base'

// Default three-column layout (curriculum sidebar + current lesson + an
// OnThisPage rail):
<CourseShell course={course} courseItems={courseItems} enrollment={enrollment} />

// Or a custom composition — children replace the default layout and read the
// current lesson from the shell's context:
<CourseShell course={course} courseItems={courseItems} enrollment={enrollment}>
  <ProgressBar value={progressPct} />
  <LessonContent bodyContent={lessonHtml} />
</CourseShell>

courseItems (the course's flat CourseItem list) is required. LessonContent takes no lesson prop — it renders the shell's current lesson; pass pre-rendered body content (a React node) via bodyContent. ProgressBar's value is a percentage, rounded and clamped to 0–100.

What the default composition renders. CourseShellLayout (and CourseShell without children, which renders it) fills the right rail with OnThisPage while the current lesson is accessible; pass onThisPageSlot to replace it or onThisPageSlot={null} to leave it empty. LessonContent's footer carries MarkCompleteButton, PrevNextNav and SuggestedNextPanel (which renders nothing without applicable suggestedNext entries). For a lesson the viewer cannot access, LessonContent renders the gate components instead of the body: PrerequisiteGate (title at h2) when the reason is 'prerequisite' and the shell's unmetPrerequisites list is non-empty, otherwise VisibilityGate, which shows ExpiredEnrollmentNotice for an expired enrollment and a locked or preview panel with EnrollmentCTA for the rest. No lesson body is passed into the gate, because VisibilityGate's preview mask is not styled yet and a body placed there would be readable in the DOM. Replace the whole gate with slotOverrides.gate. ExpiredEnrollmentNotice and PrerequisiteGate take headingLevel (2 or 3, default 3) so they fit the heading outline they are placed in.

CourseShell is a Client Component and never writes to Payload itself. Load course, courseItems, enrollment and completedLessonIds on the server, and pass server actions for the writes you support — onLessonComplete, onQuizSubmit / onQuizAttemptStart / onQuizAttemptSubmit, onAssignmentSubmit, onNoteCreate / onNoteUpdate / onNoteDelete, onProfileUpdate. Every callback is optional; leave one out and nothing persists for that action. Components import their own stylesheets, so the @wabbit/tome-lms-ui/base import above (plus @wabbit/tome-lms-ui/tokens if your site does not already load the @wabbit/tome-ui tokens) is the only CSS you add by hand.

// payload.config.ts — LearnerShell collection wiring.
// withLearnerShell(options?) returns a function of your raw config; apply it
// BEFORE buildConfig (buildConfig returns a Promise, not a config object).
import { buildConfig } from 'payload'
import { withLearnerShell } from '@wabbit/tome-lms-ui/payload'

export default buildConfig(withLearnerShell()({ ...baseConfig }))
// Server Component / route handler — active-quest data for the shell
import { getActiveEnrollments, getActiveQuestData } from '@wabbit/tome-lms-ui/server'

const enrollments = await getActiveEnrollments(payload, memberId)
const quest = await getActiveQuestData({ payload, memberId, preferredEnrollmentId })

Public API

40+ components are exported from the root barrel; the load-bearing groups:

GroupExamplesSubpath
Lesson playerCourseShell, CourseShellRoot, CourseShellLayout, CurriculumSidebar, LessonContent, ProgressBar, MarkCompleteButton, PrevNextNav, NotesPanel, OnThisPage, LessonVideo, CourseShellProvider, useCourseShellroot
Phases + progressionPhaseIndicator, PhaseTransitionScreen, SuggestedNextPanel, QuestMaproot
Utilities + videoflattenCourseItems, computePrevNext, tocFromLexical, createVideoAdapter, Html5VideoPlayer, html5VideoAdapter, PrintOnMount (certificate print helper)root
GatingVisibilityGate, deriveAccessState, resolveCtaState, EnrollmentCTA, PrerequisiteGate, ExpiredEnrollmentNoticeroot
RenderersQuizRenderer, AssignmentRenderer, defaultBlockRenderers (VideoBlock/QuizBlock/AssignmentBlock/CodeBlock/NoteBlock)root
Catalog + landingCourseCatalogList, CourseCard, AcademyCard, AcademyDirectory, CourseLandingHero, CourseLandingOutline, CourseLandingInstructorSection, CourseLandingReviewsSection, LeaveReviewForm, CourseLandingRelatedSectionroot
DashboardDashboardEnrollmentList, EnrollmentSummaryCard, DashboardGradeTable, CourseGradeDetail, DashboardCertificateList, CertificateCard, CertificateVerificationView, CertificateTemplate, DashboardNotesIndex, DashboardLeaderboardTable, StudentProfileEditor (@deprecated — see below), StudentProfileFormroot
LearnerShellLearnerShell, LearnerSidebar, LearnerNavGroup, LearnerNavItem, LearnerNavSubItem, LearnerHeader, LearnerUserMenu, LearnerInsetCard, ActiveQuestSection, ActiveQuestPickerroot
Nav resolutionresolveLearnerNav, DOMAIN_ORDER, DOMAIN_LABEL, DOMAIN_DEFAULT_OPEN, LEARNER_UI_PREFS_SLUGroot (re-exported from @wabbit/tome-lms-ui/nav)
HooksuseCurrentLesson, useCoursePrerequisites, usePhaseState, useCompletionTracking, useAsyncAction, useTagList, useLearnerPrefs (useLearnerPrefs is on both the root and @wabbit/tome-lms-ui/hooks/usePrefs)root / @wabbit/tome-lms-ui/hooks/usePrefs
Server datagetActiveEnrollments(payload, memberId), getActiveQuestData({ payload, memberId, preferredEnrollmentId? })@wabbit/tome-lms-ui/server
Payload configbuildLearnerUiPrefsCollection, learnerUiPrefsCollection, withLearnerShell(options?) → (config) => config (adds the prefs collection once; a no-op if you already added it)@wabbit/tome-lms-ui/payload
CSS@wabbit/tome-lms-ui/tokens, @wabbit/tome-lms-ui/base, and one stylesheet per component under @wabbit/tome-lms-ui/components/* (for example @wabbit/tome-lms-ui/components/progress-bar, @wabbit/tome-lms-ui/components/learner-nav-group/state). These subpaths resolve to .css files, not JavaScript, and the components already import them — reach for them only when styling a component you render outside its normal parentCSS

Server / client posture — the exemplary split, canonized

This package is the reference pattern for a Tome UI package that mixes render components with data access — new packages with a similar shape should copy this structure rather than reinvent one:

  • Root barrel (src/index.ts) — the render-component surface. 38 of the 65 non-test .tsx files declare 'use client' (including CourseShell and CourseShellRoot); the rest are server-renderable presentational components with no interactivity. The barrel's own header comment states the rule explicitly: "Server-only modules ship from `@wabbit/tome-lms-ui/server` and `@wabbit/tome-lms-ui/payload` subpaths and intentionally do NOT surface here."
  • `@wabbit/tome-lms-ui/server` subpath — guarded with import 'server-only' at the top of the barrel, so accidental client-bundling fails at build time rather than leaking data-layer code. Exports getActiveEnrollments/getActiveQuestData — pure data fetchers, zero React.
  • `@wabbit/tome-lms-ui/payload` subpath — Payload config helpers (withLearnerShell, buildLearnerUiPrefsCollection, learnerUiPrefsCollection). They are meant for config time on the server, but the subpath carries no server-only guard: it is plain, React-free config code, so the Payload CLI can load it. Its header comment is explicit about the split rationale: "Sites import `withLearnerShell()` from here at config time. The hook lives separately at `@wabbit/tome-lms-ui/hooks/usePrefs` (client-only)."
  • `@wabbit/tome-lms-ui/nav` subpath — deliberately carries no client/server directive at all. Its header comment states why: "No client/server directive — all values here are server-safe pure data and pure functions, importable from either side of the boundary." resolveLearnerNav and the domain constants run identically in a Server Component computing initial nav state and a Client Component re-deriving it after a client-side mutation.
  • `@wabbit/tome-lms-ui/hooks/usePrefs` — the one client-only hook broken out to its own subpath specifically so it doesn't have to live inside the server-guarded @wabbit/tome-lms-ui/payload module.

The pattern in one sentence: render components on the root barrel, data-access behind a `server-only` guard on a named subpath, pure isomorphic logic on its own subpath with no directive at all, and the one client-only hook that doesn't fit elsewhere gets a dedicated subpath.

Testing

pnpm --filter @wabbit/tome-lms-ui test runs the Vitest suite: co-located accessibility tests (*.a11y.test.tsx, via vitest-axe) and contract pin tests next to the components they cover.

Extending

New lesson-player or dashboard components follow the root-barrel pattern (co-located .tsx + its stylesheet — usually a plain index.css the component imports itself, exposed as a @wabbit/tome-lms-ui/components/* CSS subpath — exported from src/index.ts, 'use client' unless genuinely presentational). New server data helpers go in src/server/ behind the server-only import. The LearnerShell's five nav domains are a fixed manifest, not a free-form list — adding a sixth domain is a deliberate contract change, not a config tweak; read @wabbit/tome-lms-ui/nav's header comment before adding one.

Design history

  • Lesson player: course shell, curriculum sidebar, quiz/assignment renderers, prev/next nav, notes panel.
  • LearnerShell: sidebar app-shell with nav groups, header, user menu, and active-quest widgets, added afterward as the consumer-facing wrapper around the lesson player.
  • Editorial course-shell variant: an alternate visual treatment of the same course-shell contract.
  • docs/claude-gotchas.md → Payload / Typing / Layer Design section — "LMS server-write mutations: the promotion trigger already fired — and the factoring is inner-layer-only" is directly relevant to anyone adding a new server data helper here

Decisions that shaped this package

See docs/specs-map.md in this repo for the full package→spec routing table.

  • The LearnerShell's five nav domains (Adventure/Library/Progress/Community/Account) are fixed; only items slot in — architecturally mirrors @wabbit/tome-admin's sidebar manifest pattern but tuned for the consumer-side learner role; adding a sixth domain requires a deliberate contract change, not a config change.
  • The sidebar's Active Quest shows only the current module/chain, not the full curriculum map — the Compass page stays the canonical full-visibility surface (including locked content); the sidebar is a resume affordance, not a duplicate map.
  • `StudentProfileEditor` is deprecated in favor of folding its hardcoded fields (goals/experience/interests/archetype/custom) into `StudentProfileForm`'s schema vocabulary — the two components owned overlapping "edit student profile" contracts with no doc distinguishing them; a new useTagList extraction made the schema-driven merge cheap, so one surface wins instead of two (code: src/components/StudentProfileEditor/StudentProfileEditor.tsx carries the @deprecated tag pointing at StudentProfileForm, confirmed present).
  • Server-only data access is split onto named subpaths (`@wabbit/tome-lms-ui/server`, `@wabbit/tome-lms-ui/payload`) rather than mixed into the root render-component barrel — @wabbit/tome-lms-ui/server is guarded by import 'server-only' so accidental client-bundling of data-layer code fails at build time instead of leaking; @wabbit/tome-lms-ui/nav carries no directive at all because its contents are genuinely isomorphic pure data/functions.

Exports

Import path
@wabbit/tome-lms-ui
@wabbit/tome-lms-ui/tokens
@wabbit/tome-lms-ui/base
@wabbit/tome-lms-ui/components/progress-bar
@wabbit/tome-lms-ui/components/course-shell
@wabbit/tome-lms-ui/components/curriculum-sidebar
@wabbit/tome-lms-ui/components/lesson-content
@wabbit/tome-lms-ui/components/visibility-gate
@wabbit/tome-lms-ui/components/enrollment-cta
@wabbit/tome-lms-ui/components/mark-complete-button
@wabbit/tome-lms-ui/components/prev-next-nav
@wabbit/tome-lms-ui/components/notes-panel
@wabbit/tome-lms-ui/components/on-this-page
@wabbit/tome-lms-ui/components/quiz-renderer
@wabbit/tome-lms-ui/components/assignment-renderer
@wabbit/tome-lms-ui/components/prerequisite-gate
@wabbit/tome-lms-ui/components/expired-enrollment-notice
@wabbit/tome-lms-ui/components/phase-indicator
@wabbit/tome-lms-ui/components/phase-transition-screen
@wabbit/tome-lms-ui/components/student-profile-form
@wabbit/tome-lms-ui/components/suggested-next-panel
@wabbit/tome-lms-ui/components/quest-map
@wabbit/tome-lms-ui/components/lesson-video
@wabbit/tome-lms-ui/components/course-catalog-list
@wabbit/tome-lms-ui/components/course-card
@wabbit/tome-lms-ui/components/academy-card
@wabbit/tome-lms-ui/components/academy-directory
@wabbit/tome-lms-ui/components/course-landing-hero
@wabbit/tome-lms-ui/components/course-landing-outline
@wabbit/tome-lms-ui/components/course-landing-instructor-section
@wabbit/tome-lms-ui/components/course-landing-reviews-section
@wabbit/tome-lms-ui/components/leave-review-form
@wabbit/tome-lms-ui/components/course-landing-related-section
@wabbit/tome-lms-ui/components/dashboard-enrollment-list
@wabbit/tome-lms-ui/components/enrollment-summary-card
@wabbit/tome-lms-ui/components/dashboard-grade-table
@wabbit/tome-lms-ui/components/course-grade-detail
@wabbit/tome-lms-ui/components/dashboard-certificate-list
@wabbit/tome-lms-ui/components/certificate-card
@wabbit/tome-lms-ui/components/certificate-verification-view
@wabbit/tome-lms-ui/components/certificate-template
@wabbit/tome-lms-ui/components/certificate-pass
@wabbit/tome-lms-ui/components/certificate-visibility-toggle
@wabbit/tome-lms-ui/components/verification-lookup-form
@wabbit/tome-lms-ui/components/dashboard-overview
@wabbit/tome-lms-ui/components/course-workspace
@wabbit/tome-lms-ui/components/dashboard-replies
@wabbit/tome-lms-ui/components/dashboard-notes-index
@wabbit/tome-lms-ui/components/dashboard-leaderboard-table
@wabbit/tome-lms-ui/components/student-profile-editor
@wabbit/tome-lms-ui/components/learner-shell
@wabbit/tome-lms-ui/components/learner-sidebar
@wabbit/tome-lms-ui/components/learner-nav-group
@wabbit/tome-lms-ui/components/learner-nav-group/state
@wabbit/tome-lms-ui/components/learner-nav-item
@wabbit/tome-lms-ui/components/learner-nav-sub-item
@wabbit/tome-lms-ui/components/learner-header
@wabbit/tome-lms-ui/components/learner-user-menu
@wabbit/tome-lms-ui/components/learner-inset-card
@wabbit/tome-lms-ui/components/active-quest-section
@wabbit/tome-lms-ui/components/active-quest-picker
@wabbit/tome-lms-ui/server
@wabbit/tome-lms-ui/nav
@wabbit/tome-lms-ui/payload
@wabbit/tome-lms-ui/hooks/usePrefs

Changelog

0.12.xMinor

  • Fixes lesson navigation on nested curricula and adds opt-in labels, heading levels, prerequisite locks and a server-renderable notes index to the learner UI.

    Additive, with default output unchanged (the default-DOM pins still pass):

    • CertificateCard takes labels (issued, expires, viewCertificate, verifyCertificate, download); DashboardCertificateList threads them through a new cardLabels prop. Exports DEFAULT_CERTIFICATE_CARD_LABELS and CertificateCardLabels.
    • PrerequisiteGate takes labels (title, fallback). Exports DEFAULT_PREREQUISITE_GATE_LABELS.
    • DashboardReplies takes headingLevel (2 to 6, default 4). The "earlier" group heading renders one level above it.
    • CertificateTemplate (layout="sheet") takes recipientHeadingLevel (1 to 6, default 1).
    • CurriculumSidebar takes lockedLessonIds; with extendedHooks, a listed lesson reads data-lesson-state="locked", prints the locked word and gets data-prerequisite-locked="true". The row stays clickable so the learner reaches the prerequisite gate.
    • AssignmentRenderer takes showInstructions (default false) and renders authored instructions as paragraphs. AssignmentBlock now passes allowResubmit and showInstructions through, from its props or the block's own fields, and createAssignmentBlock(options) builds a configured renderer for blockRenderers.
    • DashboardNotesIndex no longer needs a client boundary: it attaches handlers only when a callback prop is passed. New props: courseOptions, noteHref (string template with {id}, or a function), filterAction (plain GET form) with filterSubmitLabel, searchParam, courseParam, and pageHref with pageParam. NoteSummaryLike gains an optional excerpt, printed under the row when present.

    Correctness fixes:

    • PrevNextNav and flattenCourseItems now walk nested topic.items (and topicItem, moduleItem, or an item's own items) as well as the flat parent shape, so previous and next render for the nested shape CurriculumSidebar uses. Flat input gives the same output as before.
    • CertificateVerificationView now fills {issuer} in the expired text and the not-found texts; a literal "{issuer}" no longer prints.
    • MarkCompleteButton now reads the lesson's lessonType (falling back to the legacy type), so Mark complete is disabled on quiz and assignment lessons as intended. Output changes for those lessons: the button is now disabled and shows the auto-complete hint.
    • BREAKING: CourseShell now merges blockRenderers over defaultBlockRenderers (your entries win) instead of replacing the map, as the prop's docs say. Migration: a consumer that passed a partial map and relied on the missing block types rendering nothing must now pass a renderer for them, or accept the default.
  • The learner UI is themeable: colors are tokens, strings take labels, and the player gains new pieces.

    The learner UI is now themeable: raw colors are tokens, fixed strings take labels, and the player gains a lesson eyebrow, progress counts, a buy state and a practice quiz mode.

    • Every raw color in the stylesheets now reads a token with today's value as its fallback, so nothing changes until a theme sets the token. New roles (--lms-color-success-text, -soft, --lms-color-on-fill, --lms-shadow-card and others) are documented in tokens.css and deliberately unassigned; no-literal-colors guards the rule.
    • QuizRenderer, AssignmentRenderer, NotesPanel, LessonContent, LessonVideo, ProgressBar, CourseCatalogList, CertificatePass and the dashboard lists take a labels prop and export DEFAULT_*_LABELS equal to today's copy. CourseShell takes labels for the components inside the player, and extendedHooks for extra data-lms-* hooks (status badges, data-figure) on existing elements; both are off by default.
    • ProgressBar accepts counts and labelFormat; LessonContent accepts eyebrow; EnrollmentCTA gains a buy state with href and price, and works as a link without script.
    • QuizRenderer accepts mode="practice" (or a mode key on the block): no pass or fail verdict, score or threshold, answers shown as text with their reasons, and "Clear and try again". Attempts are still recorded.
    • AssignmentRenderer shows a reviewed date when the submission has reviewedAt, and allowResubmit brings the form back after a revision request.
    • CourseShell and LearnerShell read --lms-app-max-width (default none, which follows --tome-app-max-width); CourseShellLayout accepts mobileDrawers for closable curriculum and notes drawers on phones.
  • The learner app gains dashboard views, a flat rail and a document-style certificate, all opt-in.

    The learner app gains its dashboard pieces, a flat rail and a certificate that reads as a document. Everything is opt-in: with default props the output is unchanged (default-DOM pins guard LearnerShell, CertificateTemplate classic and sheet, and CertificateVerificationView).

    • New Server Components DashboardOverview (Continue, since the last visit, one quiet line for the other course, a welcome for a new student), CourseWorkspace (course list beside the selected course, with a native GET picker on phones) and DashboardReplies ("New" means newer than the previous visit you pass in; nothing is stored). They take plain data, render with no script, and accept no streak, point or stat tile.
    • DashboardGradeTable gains layout="by-course" with groups: each item shows a status word (Passed, Needs another go, Submitted, Reviewed, Not taken yet), a score and plain Retake and Read why links. Row handlers attach only when onRowClick is given. DashboardCertificateList gains stillToEarn.
    • LearnerShell gains layout="rail" with rail: one nav of plain links, aria-current="page" on the current route, counts only above zero. flattenLearnerNav in @wabbit/tome-lms-ui/nav builds rail items from the manifests.
    • CertificateTemplate (sheet layout) takes labels, sheet.facts (a sentence built from computed course facts, with formatCourseMinutes for durations) and sheet.proportion="a4-landscape" (container-scaled frame that prints as one landscape page through a named @page; the classic layout and its portrait page rule are untouched). CertificateVerificationView takes labels and courseFacts ("What the course covered", never on not-found).
  • Adds styling hooks to the dashboard, replies and curriculum components for themes.

    Hooks the Schoolroom treatments needed. New components (unreleased SR1-k pieces) gain new attributes; the existing CurriculumSidebar changes only behind extendedHooks, so wabbit.com's default DOM is unchanged (the default-DOM pins pass).

    • DashboardOverview: the Continue lesson title is wrapped in span[data-lms-element="continue-lesson-title"], separate from continue-minutes, so a theme can wash the title alone. The p[data-lms-element="continue-lesson"] and its text stay as SR1-k states.
    • DashboardOverview: a since line flagged showNew also carries data-is-new="true". data-new="true" stays on every since line, the attribute contract SR1-k states; data-is-new is the hook for "actually new".
    • DashboardReplies: ReplyLike.reviewerImage ({ url, alt? }, optional) prints a small portrait first on the reply line as img[data-lms-element="reply-portrait"] (empty alt beside the printed name, decorative). Omitted, the markup is unchanged.
    • CurriculumSidebar: with the shell's extendedHooks, each lesson row carries data-lesson-state (active, locked, done, available, in that precedence) and a locked row prints the word in span[data-lms-element="lesson-row-locked"] (default "Locked", relabelled through the new labels prop or the shell's labels.curriculum). New CurriculumSidebarLabels and DEFAULT_CURRICULUM_SIDEBAR_LABELS.
1 fix (0.12.1)
  • 0.12.1
    The changelog, README and license text now use American spellings.

    No code or behavior changes.

0.11.xMinor

  • New certificate pass, sheet layout and four-state verification page components; the old verificationResult prop still works and is deprecated.
    • CertificatePass: dark pass card with a server-rendered QR of at least 176px, grouped verification code, four facts, share / open-verify / optional download actions and a link card to the full certificate.
    • CertificateVisibilityToggle: the holder's per-certificate "show my name on the public verify page" switch, driven by a consumer-supplied server action.
    • VerificationLookupForm (plain GET code lookup) and PrintButton.
    • CertificateTemplate gains layout="sheet" with a sheet details prop (holder, course, credential, hours, signatory, what it certifies, record).
    • CertificateVerificationView takes a verification prop shaped like getPublicCertificateVerification and renders verdict, record, "About this check" and a lookup form for the valid, expired, revoked and not-found states. The previous verificationResult prop still works for one minor and is deprecated.
    • DashboardCertificateList accepts hrefFor to link each card to its single-certificate route; QRVerificationPayload gains sizePx.
    • New CSS exports: ./components/certificate-pass, ./components/certificate-visibility-toggle, ./components/verification-lookup-form.
2 fixes (0.11.1 – 0.11.2)
  • 0.11.2

    README: subpath and file mentions are written package-qualified (for example @wabbit/tome-x/render) or as code text, so crawlers no longer resolve them as site URLs.

  • 0.11.1
    DashboardCertificateList gains hrefBase, a plain-string link base a Server Component can pass; hrefFor (a function) only works from a Client Component.
    • hrefBase="/my/certificates" links each card to /my/certificates/{id}. hrefFor still wins when both are set.
    • Passing hrefFor from a Server Component throws in Next.js ("Functions cannot be passed directly to Client Components"), because the list is a Client Component.
    • CertificatePass draws its dark card with tome-ui's surface-solid-dark / on-solid-dark pair. It used on-inverse, which is dark text for the white surface-inverse, so the card's text rendered dark on dark.

0.10.xMinor

  • CourseShell gains an additive, backward-compatible two-step submit contract for quizzes and assignments.
    • onQuizAttemptStart?: (lessonId, quizBlockId) => Promise<{ attemptId, cooldownExpiry?, attemptsRemaining? }> and onQuizAttemptSubmit?: (attemptId, answers) => Promise<QuizResult> let a site wire the platform's two-step startQuizAttempt → submitQuizAttempt mutation flow (@wabbit/tome-lms/server) into QuizRenderer. When both are provided, QuizRenderer starts an attempt on mount (passing the quiz block's own id), surfaces a disabled/cooldown-expiry submit state when the start response is locked out, and dispatches submit through the attempt id instead of the lesson id.
    • onAssignmentSubmitWithBlock?: (lessonId, assignmentBlockId, submission) => Promise<void> lets AssignmentRenderer carry the authored assignment block's own id through to sites whose mutation layer needs it, when the block has an id.
    • Both are purely additive: onQuizSubmit/onAssignmentSubmit keep working byte-identically when the new props are absent, and the two-step quiz path only activates when BOTH onQuizAttemptStart and onQuizAttemptSubmit are wired (partial wiring falls back to the single-call contract).
11 fixes (0.10.1 – 0.10.6)
  • 0.10.6
    Published packages now contain compiled JavaScript and type declarations under a one-line license banner, and no longer include source maps.

    What you install: one compiled .js (ESM) and .cjs (CommonJS) file per source module, its .d.ts / .d.cts declarations, and the stylesheets, fonts and other assets a package already shipped. Every JavaScript module opens with a comment naming the package and its license: /*! @wabbit/<package> — © Wabbit, LLC. Wabbit Tome Commercial License (see LICENSE.md). Not for redistribution. */.

    The .map files and the sourceMappingURL comments that pointed at them are gone, which roughly halves the size of each tarball.

    Debugging: the code is still unbundled and unminified, one readable file per module, so a stack trace points at real code with real names. Line numbers in a stack trace are one higher than before, because of the banner line.

    A 'use client' directive stays the first statement of its module (the banner is a comment above it), so React Server Component boundaries are unchanged.

    No API change, no runtime behavior change, and nothing to do on upgrade.

    In @wabbit/tome-blocks-gallery, the source snapshots extractGallerySource writes from an installed pack leave out the license banner line, so a component or config snapshot starts at the code and a paid block's preview shows its first 15 lines of real code.

  • 0.10.5

    CSS files are now copied to dist/ by a post-build script instead of a tsup onSuccess hook; no behavior change, and the published dist/ is identical.

  • 0.10.4
    The default course player now composes the on-this-page rail, suggested-next panel and gate components that already ship.

    The default course-player composition now uses the components that already ship: CourseShellLayout (and CourseShell without children, which now renders it) fills the on-this-page rail with OnThisPage while the lesson is accessible (onThisPageSlot={null} leaves it empty); LessonContent's footer renders SuggestedNextPanel; a gated lesson renders PrerequisiteGate or VisibilityGate (with EnrollmentCTA) instead of an inline message, with a new slotOverrides.gate to replace it; and VisibilityGate's expired branch renders ExpiredEnrollmentNotice instead of a placeholder. ExpiredEnrollmentNotice and PrerequisiteGate gain an optional headingLevel (default 3).

    All prop changes are additive.

  • 0.10.4

    Drops the unused @wabbit/tome-ui peer dependency; nothing in the package imported it.

  • 0.10.4
    Counts and dates now render with a pinned en-US locale and UTC, so server and browser markup match and hydration no longer mismatches.

    Covers student, enrollment and leaderboard counts and the enrollment, due, expiry and quiz-cooldown dates; the cooldown time now names its zone (UTC). Internal: collection slugs are typed through one package-internal helper instead of inline casts.

  • 0.10.3
    dist now raw-Node loadable: relative specifiers get explicit extensions post-build.

    build gains && node ../../scripts/fix-dist-extensions.mjs --strict as its last step, joining the 13 packages that already ran it. tsup builds bundle: false and emits relative specifiers exactly as the TypeScript source wrote them — extensionless — which bundlers resolve and raw Node does not (ESM ERR_MODULE_NOT_FOUND; CJS worse, require('./x') finds the ESM .js twin and Node 22+ require(esm) then dies on that file's own extensionless import). Every consumer outside a bundler hit this: the payload CLI under plain node, generate:types, generate:importmap, ops scripts, codegen tools.

    No source changes, no API changes, and bundler consumers are unaffected — extensioned relative specifiers are universally resolvable.

    Two supporting changes made the wiring possible, both in repo scripts rather than package source. fix-dist-extensions.mjs now skips bundler-asset specifiers (.css, .module.css, .scss, fonts, images, shaders) by explicit extension allowlist instead of reporting them as unresolvable — that single gap is why the 13 prior adopters were exactly the 13 packages that ship no CSS, since --strict exited 1 on any package with a relative stylesheet import.

    Dotted MODULE names (./config.meta, ./x.variants, ./y.demo) are deliberately NOT treated as assets and still get .js/.cjs appended. assert-node-loadable.mjs gained the matching carve-outs so the new repo-wide CI gate reports real defects only: a resolution failure whose path lands under node_modules is a peer SKIP (next@15 has no exports map, so next/image fails as an absolute path), and a bundler-asset load failure is an environmental SKIP (CJS surfaces it as SyntaxError: Unexpected token '.' raised from inside the stylesheet).

    Verified before/after on four packages built one at a time: print 8 FAIL → 0, readout 22 FAIL → 0, ai 3 FAIL → 0, gamification 2 FAIL → 0 (its failure was the other signature — a directory import missing /index). cop was already clean on a fresh build, so the audit's "27 of 46 fail" figure includes at least one package whose local dist was merely stale.

  • 0.10.3
    Normalize the react peer range from ^19.0.0 to >=19.0.0.

    Not a floor raise — lms-ui already required React 19. This is the last of the five competing peer shapes collapsing onto the one the platform now uses everywhere (ruled 2026-09-01), so a reader comparing manifests across packages sees one vocabulary instead of five.

    The practical difference is that a future React 20 consumer is no longer excluded by punctuation alone.

  • 0.10.3
    Manifest metadata: homepage, bugs, engines.

    All 46 publishable manifests were missing the three fields a consumer sees before any code (2026-09-01 sale-readiness audit §6). Metadata only — no source, no build, no runtime change.

    • homepage deep-links to that package README on GitHub (.../tree/main/packages/<dir>#readme). Without it a registry page links to the monorepo root and the reader has to guess which of 46 folders they want.
    • bugs.url points at the repo issue tracker, so a paying customer has a place to report a defect that is not email.
    • engines.node is >=22, matching the root engines and .nvmrc set the same day. This is a real floor, not decoration: CI on Node 20 could not expand the glob the block packs use for node --test, and a package installed on Node 20 fails at a runtime the installer cannot connect back to the version.

    The forcing function ships with the change: scripts/assert-manifest-metadata.mjs (root pnpm assert:manifest-metadata, wired into platform-discipline.yml beside assert:license-metadata) fails when any publishable manifest lacks description, repository.directory matching its own folder, homepage, bugs, engines.node equal to the repo floor, license, files or sideEffects. It reported 138 violations before this change and 0 after.

  • 0.10.3
    README fixes surfaced by the extended assert:readme-contract gate (2026-09-01 sale-readiness audit, Tier 2), each verified against the package's own manifest or source.
    • blocks-core — the ./categories and ./types entry points are now named in the Public API section; both were published but undocumented.
    • core — added /access/orgScoped, /access/vendorScoped, /infra/health and /infra/env-scaffold to the additional-subpaths table, and noted that /auth/collections/roles has a real /auth/collections/Roles case alias in the exports map.
    • crowdfund — CROWDFUND_LAYER_VERSION is also published standalone at ./version; the row now says so.
    • dispatch — the eight per-block ./blocks/* config subpaths and all eight ./components/* component subpaths are enumerated instead of one "etc." row.
    • forms — the peer table now lists @wabbit/tome-core, @wabbit/tome-ui and typescript, which are declared peerDependencies but appeared only in prose (or not at all).
    • lms-ui — StudentProfileEditor is flagged @deprecated in the component table, matching the tag its source already carries.
  • 0.10.2
    Fixed a defect where QuizRenderer and AssignmentRenderer never read the authored fields off blocks stored in the nested { blockType, blockData: {...} } shape — the shape EDU's real lesson rows use, with no id on the outer block object.

    asQuizBlock/asAssignmentBlock returned the block AS-IS and every field read (quiz.questions, quiz.maxAttempts, quiz.showCorrectAnswers, quiz.passingScore, assignment.submissionType, assignment.points, assignment.choices, …) came off the OUTER object, which only carries blockType/blockData — so questions was always undefined, no quiz question ever rendered (students saw only a bare Submit button), and every submit graded 0/n; assignment blocks silently collapsed to the default text submission type regardless of what was authored.

    Both narrowing functions now unwrap block.blockData when present, falling back to the block itself when it's absent — the pre-existing flat shape ({ id, title, questions, ... }, still used by every other consumer and by this package's own pre-existing tests) keeps working byte-for-byte. readBlockId on both renderers now also falls back through block.blockData?.id and block.blockID before giving up — EDU's real rows currently carry no id anywhere, so it still returns undefined for them, and the two-step quiz-attempt-start / richer-assignment-submit dispatch paths fall back exactly as they did before (onQuizAttemptStart receives '' via its existing ?? '' guard; onAssignmentSubmitWithBlock is skipped in favor of onAssignmentSubmit since it only fires when an id is truthy) — no d.ts contract change was needed for either.

    LessonContent's block dispatcher already routed this shape to both renderers correctly (its blockType lookup handles the 'quizBlock'/'quiz' and 'assignmentBlock'/'assignment' key spellings) — the renderers' own field reads were the only break. New tests cover both shapes side by side (QuizRenderer.blockdata-unwrap.test.tsx, AssignmentRenderer.blockdata-unwrap.test.tsx), including a nested fixture copied verbatim from the verified live EDU row shape, and were demonstrated red against the unfixed source before the fix was applied.

  • 0.10.1
    Re-exported QuizAttemptStartResult from the package root.

    It was already part of CourseShellRoot's public onQuizAttemptStart signature but had no supported import path — consumers had to mirror its shape structurally instead of importing it.

0.9.xMinor

  • CourseCatalogList: the course-card CTA no longer prefers --pc-_ brand tokens or falls back to the safety-yellow literal; it now rides --lms-color-primary-cta → --tome-color-primary (neutral).

    Industrial-lineage consumers get the previous look via @wabbit/tome-blocks-industrial-theme, which defines the --lms-color-_ hooks inside its theme scope.

9 fixes (0.9.1 – 0.9.8)
  • 0.9.8
    Packaging hygiene — @wabbit/tome-lms and @wabbit/tome-ui move from hard dependencies to peerDependencies (+ workspace:* devDependency twins).

    No source change, no behavior change.

    @wabbit/tome-lms-ui was the ONLY layer: app package in the monorepo declaring @wabbit/* packages as runtime dependencies. Every sibling — tome-admin, tome-admin-pro, tome-chrome, tome-dispatch, tome-longform, tome-readout — declares its engines as required peers with a workspace:* devDependency twin, and carries no peerDependenciesMeta entry for them.

    This package now matches that convention exactly.

    Both were declared workspace:^, which pnpm rewrites at pack time — so the published tarball carried a hard ^0.x runtime dependency on the LMS engine. A consumer mounting tome-lms-ui always mounts tome-lms itself (they are one licensable family, learning), so npm was installing and version-resolving a second copy of an engine the consumer already supplies.

    The peer declaration lets the consumer's copy satisfy it.

    Ranges follow the sibling convention (>=MIN <NEXTMAJOR): @wabbit/tome-ui at >=0.9.0 <1.0.0, matching tome-chrome/tome-dispatch/tome-longform/tome-readout/tome-admin verbatim. @wabbit/tome-lms at >=0.12.0 <1.0.0 — deliberately holding the floor the published tarball already carried rather than raising it to the current 0.14.0, so this change alters the KIND of dependency without narrowing the version contract consumers already rely on.

    (tome-sc declares the same engine at >=0.9.0 <1.0.0; the higher floor here is the conservative choice, not a conflict.)

    Both peers are REQUIRED (no peerDependenciesMeta entry), which is the honest declaration and matches the sibling packages:

    • @wabbit/tome-lms is statically value-imported in exactly one place — src/server/active-quest.ts imports getActiveEnrollments/getLessonChain from @wabbit/tome-lms/server, reachable via the ./server subpath. Everything else referencing the engine is a comment or a deliberately re-declared type (see src/types.ts, which re-declares CertificationAwardData specifically so consumers "don't have to depend directly on @wabbit/tome-lms types just for the shell"). An optional peer would be dishonest for ./server.
    • @wabbit/tome-ui has ZERO imports anywhere in src/ — its only references are prose comments and three CSS files that consume --tome-color-* Layer-2 tokens shipped by @wabbit/tome-ui/tokens. The dependency is real but ambient: the consumer must load those tokens for this package to render correctly. A peer declares that requirement without forcing a runtime install of a module nothing imports, which is precisely what the sibling packages do.

    The workspace:* devDependency twins are required, not optional bookkeeping: assert:declared-imports fails a statically-imported internal package that is peer-only ("add a workspace:\* devDependency so pnpm topology orders the build"), because peers are invisible to pnpm's build ordering on cold checkouts. pnpm assert:declared-imports is green after the change.

    Guarded going forward by a new assert:app-layer-peers check (repo tooling — no changeset of its own, since it ships no package), wired into platform-discipline CI pre-build alongside the other manifest asserts. It fails any layer: app package that declares a @wabbit/* engine in dependencies, and carries an empty, stale-entry-failing allowlist so a future exception has to be argued rather than assumed.

    NOT published here; publishing waits on the irreversible-publish preflight checklist.

  • 0.9.7

    Updated dependencies [57b7a43]

  • 0.9.7

    Updated dependencies [fa9c30b]

  • 0.9.7
    Updated dependencies [fc30bf6]
    • @wabbit/tome-lms@0.14.0
  • 0.9.6
    Updated dependencies
    • @wabbit/tome-lms@0.13.0
  • 0.9.5
    Updated dependencies [47a8d78]
    • @wabbit/tome-lms@0.12.0
  • 0.9.4
    Updated dependencies [0a070e0]
    • @wabbit/tome-ui@0.11.0
  • 0.9.3
    Updated dependencies [68465b3]
    • @wabbit/tome-lms@0.11.0
  • 0.9.1
    Updated dependencies
    • @wabbit/tome-ui@0.10.0

0.8.xMinor

  • StudentProfileForm gains a tag-list field type (built on useTagList) and an exported studentProfileEditorSchema(availableArchetypes?) reproducing StudentProfileEditor's field set through the schema vocabulary; Editor is @deprecated with the exact migration recipe in its tag.

    One honest gap, not smoothed over: Editor's free-form customFields key-value rows have no schema equivalent — a key-value-list field type gets built when a second consumer needs one (trigger named in both docblocks); consumers relying on customFields keep using Editor until then.

  • Every package now declares an explicit sideEffects field (38 added; motion/engine/forms already correct).

    Registration-bearing modules (render files' registerRenderer, blocks/*/index.ts defineBlock self-registration, widget register.ts files, productHooks, permission self-registrations, print templates, chrome built-in variants) are listed so bundlers can tree-shake everything else WITHOUT dropping import-time registrations — previously the field was unset, which blocked cross-module tree-shaking through the barrels entirely. Never blanket false on a package with registration or CSS.

  • Small verified fixes: agency-essentials Contact gains its missing 'use client' (it calls the rich-text adapter hook; direct RSC import crashed). chrome NavGuard now dev-warns when its capability gate fails to load while a requiredCapability is set (the fail-open contract itself is unchanged and now documented). blocks-core BLOCK_CATALOG.ts corrupted entries corrected from real block meta (content-two-column, content-with-corner-notch, signal-ship-card names/descriptions; gallery variants filled) + drift-risk header.

    Stale docstrings fixed (chrome HeaderLogo, blocks-gallery registry header, lms-ui payload JSDoc import path). blocks meta-package backcompat suite now asserts the RENDER registry resolves renderers (previously only descriptor registration was tested — a dropped render import shipped silently).

  • Re-render and cleanup fixes: chrome's HeaderClient dead theme state + unreachable effect deleted; Navbar6/7 body-scroll-lock now saves and restores the pre-existing overflow value (LearnerSidebar pattern) instead of clobbering to ''; Navbar7's scroll listener is rAF-throttled. marketing-starter's Testimonial derives the clamped slide index during render instead of an effect. forms' FieldRenderer is wrapped in React.memo (call-site props verified stable), cutting whole-step re-render work per keystroke in multi-field forms. lms-ui's useLearnerPrefs gains optional initialPrefs server-seeding (non-breaking) + in-flight dedup with TTL for the unseeded path.

  • DRY adoption sweep (the audit's "adoption, not extraction" rule): crm/deals capability presets delegate to core's sessionHasCapabilityOrLegacyAdmin; new core buildOwnershipWhere/ownershipOrBypass (via ./access) adopted by core's vendorScoped, catalog's vendor-scoping, and org's ownOrScoped (public APIs unchanged); slugField() adopted at 7 sites where semantics matched exactly (core lms collections + createMemberCollection — replacing a third independent slugify), with ~25 sites honestly skipped for named semantic divergences (auto-regenerate-on-clear vs allow-empty, collection-level hook pattern) now listed as core-enhancement candidates; new formatDisplayDate in blocks-core utilities (UTC-pinned, hydration-safe) adopted at 5 verified-identical sites; lms-ui consolidates its two certificate date formatters locally; useMediaQuery/useIsMobile published from tome-ui and adopted by AppShell + admin's SidebarProvider; gamification's awardPoints now uses the authoritative getPointsBalance (fixes a divergent 1000-row scan cap vs the correct 10000).

  • Updated dependencies [6bc419c]

  • Updated dependencies [36e537a]

  • Updated dependencies [36e537a]

  • Updated dependencies [a93f478]

  • Updated dependencies [aef2725]

  • Updated dependencies [aef2725]
    • @wabbit/tome-lms@0.10.0
    • @wabbit/tome-ui@0.9.9
2 fixes (0.8.1)
  • 0.8.1
    Two fixes from the wabbit EDU Phase 5 prod dogfood (2026-07-18).
    • lms-ui: CurriculumSidebar now derives per-row effective access via deriveAccessState (enrollment/tier-aware) instead of disabling every locked-visibility row — enrolled members can navigate locked lessons from the rail, matching what the content pane already grants. CurriculumTree gains an optional resolveLessonAccess prop; without it the visibility-tier fallback (anon/landing behavior) is unchanged.
    • lms: the course-completion badge check moved out of onLessonCompletion (it fired per LESSON, awarding course-completion badges on a student's first completed lesson) into a new awardCourseCompletionBadges CourseEnrollment afterChange hook guarded on the status transition into completed — the same guard autoAwardCertification uses. Per-lesson points (and the points-threshold badge cascade inside awardPoints) are unchanged.
  • 0.8.1
    Updated dependencies [1173d00]
    • @wabbit/tome-lms@0.10.1

0.7.xMinor

  • Tokenize longform + LMS typography to the platform --tome-text-* scale

    tome-longform — every hard-coded font-size rem/px literal across the 15 block CSS modules (Accordion, AnchorSection, Aside, AuthorAside, Callout, ChapterDivider, CrossLink, DataTable, ImageGrid, KeyFacts, SeriesNav, Spoiler, TabbedContent + DropCap's custom prop left as-is) now reads the platform type token with the original literal preserved as the fallback — e.g. font-size: var(--tome-text-sm, 0.875rem). em-based sizes are left untouched (intentional relative sizing).

    A Tome-themed consumer (one that loads @wabbit/tome-ui/tokens) now gets longform type that tracks the platform scale; a consumer with no Tome tokens renders identically to before (fallback = the prior literal). Non-breaking, but rendered sizes shift in Tome-themed consumers — re-verify longform visually after upgrading.

    SeriesNav — .partLink (which sits on the TOP_BANNER --block-accent-bg surface) now leads its color with --block-accent-text before falling back to --tome-color-muted-foreground. When --block-accent-text is unset this is identical to the prior rule (non-regressive); when a consumer sets an accent background they now have a paired on-accent text hook, closing the cream-on-pastel contrast gap by contract rather than by a hard-coded value (mirrors the existing .seriesLabel / .sidebarLabel pairing).

    tome-lms-ui — tokens.css now bridges the --lms-font-size-* scale to --tome-text-* (mirroring the existing color-token bridge), so LMS _content_ typography tracks the platform scale instead of being a parallel fixed scale. The previously-undefined xs / base / xl / 2xl names (used across components with inline fallbacks) are now defined.

    Fallbacks equal the dominant observed intended size, so a non-Tome consumer is non-breaking. CertificateTemplate literals are tokenized onto this scale.

    Dense sidebar/nav chrome keeps its sub-14px px literals — that is intentional UI density, not reading prose, and was deliberately left untokenized.

13 fixes (0.7.1 – 0.7.11)
  • 0.7.11

    @wabbit/tome-lms@0.9.3

  • 0.7.10
    Updated dependencies [ec4b7bc]
    • @wabbit/tome-ui@0.9.8
  • 0.7.9
    Admin label polish + formatted commerce money columns: explicit labels for CRM collections ("CRM Accounts…"), Admin/Learner UI Preferences, and better-auth generated collections ("Auth Accounts", "Two-Factor Credentials", OAuth/JWKS casing) via the plugin's customizeCollection hook; nav SYSTEM_LABEL_OVERRIDES map (payload-kv → "Payload KV") applied at resolver + pinned-section label sites; Orders.total / Payments.amount / Prices.amount virtual afterRead fields format integer cents against the row currency ("4900" → "$49.00") in list views with no client components (zero generate:importmap coupling).
    • @wabbit/tome-lms@0.9.2
  • 0.7.8
    Docs-manifest emitter pipeline (W3 ship-readiness).

    @wabbit/tome-blocks-core now ships a standalone Node ESM CLI at scripts/emit-docs-manifests.mjs that emits per-package documentation manifests (index.json, packages/<slug>.json, changelog.json) by reading what packages already carry — READMEs, the payload-free <pkg>/meta block-usage barrels, package.json exports maps, and CHANGELOG.md. It is the docs-pipeline sibling of the gallery source extractor and is consumed by host sites at prebuild: node node_modules/@wabbit/tome-blocks-core/scripts/emit-docs-manifests.mjs --output-dir <dir> --scope <scope.json>.

    To let the emitter import block metadata uniformly without dragging Payload config into a build script, the ./meta payload-free subpath (BlockMetaEntry[]) is extended to the remaining offered blocks packs — agency-essentials, catalog-pack, lms-pack, org-pack, and signal-theme — mirroring the existing editorial-pack / marketing-starter / content-writer / extras barrels. Each block's BlockMeta was relocated verbatim into a payload-free sibling meta module and re-imported by its block config; no meta values changed.

    Every supported-core package additionally adds CHANGELOG.md to its published files array so the next publish cascade ships changelogs the emitter can read from installed tarballs at prebuild.

  • 0.7.8

    Updated dependencies [bed3f90]

  • 0.7.8
    Updated dependencies [850d51c]
    • @wabbit/tome-ui@0.9.7
    • @wabbit/tome-lms@0.9.1
  • 0.7.7
    Updated dependencies
    • @wabbit/tome-ui@0.9.6
  • 0.7.6
    Updated dependencies
    • @wabbit/tome-ui@0.9.5
  • 0.7.5
    Updated dependencies [03865f0]
    • @wabbit/tome-lms@0.9.0
  • 0.7.4
    Updated dependencies [d2d0b0d]
    • @wabbit/tome-lms@0.8.0
  • 0.7.3
    Updated dependencies [d5d81ce]
    • @wabbit/tome-lms@0.7.0
  • 0.7.2

    @wabbit/tome-lms@0.6.2

  • 0.7.1
    Updated dependencies [84a047a]
    • @wabbit/tome-ui@0.9.3

0.6.xMinor

  • CourseShell now derives real lesson completion + access state.

    Two changes, both surfacing through <CourseShell>:

    1. Completion state (`completedLessonIds`). MarkCompleteButton's "Completed" pill and CurriculumSidebar's per-lesson completion markers never flipped — both derived completion from enrollment.completedLessons, a field that does not exist on the CourseEnrollment schema (the canonical source is the global lesson-completions collection).

    Fix:

    • New optional completedLessonIds?: string[] prop on <CourseShell> (CourseShellRootProps) and a required completedLessonIds: ReadonlySet<string> on CourseShellContextValue. Resolve it server-side via @wabbit/tome-lms#getCompletedLessonIds and pass it in.
    • CourseShellRoot merges the server-authoritative set with an in-session optimistic overlay, so the pill/marker flips immediately on mark-complete and reconciles (prunes) once the server revalidation re-hydrates the prop.
    • MarkCompleteButton + CurriculumSidebar now read the context set; the dead extractCompletedLessonIds helper (read the non-existent field) is removed.
    • Back-compat: omit the prop and completion markers simply never flip — the prior behavior, no errors.

    2. Access state (`canAccessCurrentLesson`) — BEHAVIOR CHANGE. CourseShellRoot shipped a Wave-2a stub hardcoding canAccessCurrentLesson = true, so VisibilityGate/PrerequisiteGate never actually gated.

    It now calls the existing pure deriveAccessState() (visibility + enrollment + tier + expiry precedence, RSC-safe, mirrors @wabbit/tome-lms/guards#canAccessLesson). Prerequisite gating stays disabled (empty list) until a resolver feeds the shell — additive, tracked separately.

    Consumers that relied on the stub's "everything accessible" behavior will now see locked/preview lessons actually gate. Verify any server-side paywall (e.g. body-stripping) still composes correctly with the now-live client gate.

  • Updated dependencies [56fc8d5]
    • @wabbit/tome-lms@0.6.0
1 fix (0.6.1)
  • 0.6.1
    Updated dependencies [8947ff1]
    • @wabbit/tome-ui@0.9.2
    • @wabbit/tome-lms@0.6.1

0.4.x

3 fixes (0.4.6 – 0.4.8)
  • 0.4.8
    Updated dependencies [0b2a1d6]
    • @wabbit/tome-ui@0.8.3
  • 0.4.7
    Updated dependencies [4225e9f]
    • @wabbit/tome-ui@0.8.2
  • 0.4.6
    Updated dependencies [1d90b24]
    • @wabbit/tome-ui@0.6.1

0.3.xMinor

  • lms-ui — CSS Modules → plain CSS rename (minor, consumer-visible). packages/lms-ui/src/components/*/index.module.css renamed to index.css across 30+ components; exports map in package.json updated to match (./components/* now point to index.css under dist/).

    Consumers switch from import styles from './index.module.css' to side-effect import './index.css'. The previous layout was rejected by Next.js because the CSS used attribute-based global selectors ([data-layout="three-column"]), which CSS Modules flag as non-pure.

    This rename unblocks cross-package CSS @import from consumer barrels (e.g. a consumer's src/styles/tome-lms-ui.css). Also bumps the build script to add NODE_OPTIONS=--max-old-space-size=8192 (DTS was OOM'ing against the peer type graph) and adds cross-env as a devDep.

    admin — Payload 3.x entrypoint alignment + Turbopack cmdk fix (patch). DefaultCommandRegistrar was split: the Payload-aware variant lives in a new PayloadDefaultCommandRegistrar.tsx with a static ESM import of @payloadcms/ui. Root cause: Next 15 Turbopack's CJS-of-ESM interop returned useConfig as not-a-function under the prior require('@payloadcms/ui') lazy-load path.

    Edit/List/Nav entrypoints now render <DefaultEditView> and siblings with DocumentViewClientProps, matching Payload 3.x's full-replacement slot contract (the prior HOC shape assumed children that Payload never delivered). No public API surface changes.

0.2.xMinor

  • Initial publish to npm.wabbit.com — first registry release for the 5 non-block-pack packages.

    Companion to the existing sprint-3-blocks-split changeset (which handles the 11 linked block packages). Together these two changesets bring all 8 publish-pipeline-Phase-2-remediated packages to a coherent first-release cohort:

    • @wabbit/tome-core 0.1.0 → 0.2.0
    • @wabbit/tome-ui 0.2.0 → 0.3.0
    • @wabbit/tome-motion 0.1.0 → 0.2.0
    • @wabbit/tome-lms 0.1.0 → 0.2.0
    • @wabbit/tome-lms-ui 0.1.0 → 0.2.0
    • @wabbit/tome-blocks-core 0.1.0 → 0.2.0 (via sprint-3)
    • @wabbit/tome-blocks-extras 0.1.0 → 0.2.0 (via sprint-3)
    • @wabbit/tome-blocks-marketing-starter 0.1.0 → 0.2.0 (via sprint-3)

    All 8 packages ship with metadata, dist/ output, exports map verified by P6 scratch-consumer smoke (35/35 resolutions), 'use client' + 'server-only' directives preserved through tsup bundle:false. Verdaccio v0 live since 2026-04-18 at npm.wabbit.com.

  • Updated dependencies
    • @wabbit/tome-ui@0.3.0
    • @wabbit/tome-lms@0.2.0

Full changelog across all packages