Documentation menu
Documentation / Blocks & themes / theme-schoolroom
Theme Schoolroom
npm install @wabbit/tome-theme-schoolroomFirst install from our registry?
Get a registry token from your credentials page. You need a purchase that includes this package, or a Craft Library membership.
Add the registry and your token to the
.npmrcat the root of your project, with your token in place ofYOUR_TOKEN:@wabbit:registry=https://npm.wabbit.com/ //npm.wabbit.com/:_authToken=YOUR_TOKEN
Schoolroom, a Tome theme for people who teach for a living and want their school to look like a school, not a funnel: the marketing site and the learner app in one look. The outcome is the first screen, the syllabus is typeset, the lesson player keeps a margin, and the transcript, certificate and checkout are the same quiet page. Literata and Lexend, a graphite-and-highlighter default (Margin), four palettes plus derived opposite modes (Night School opens dark), self-hosted OFL fonts, activated by data-tome-theme="schoolroom".
Overview
Schoolroom is a Tome theme for people who teach for a living and want their school to look like a school, not a funnel. It covers both halves of a course business in one look: the marketing site (an outcome-first hero, a typeset syllabus and course index, a teacher's letter, skill paths, the buy box) and the learner app (the lesson player with its margin, the dashboard, the certificate and its public verification page, checkout). Its stance is restraint: the outcome is the first screen, color is something a student leaves on the page (a progress line, a highlighted row), and there is no ring, streak, leaderboard, ticket edge or seal anywhere.
This package is a theme (a ThemeManifest of kind: 'theme'). It owns no blocks. It declares the whole token surface blocks read, assigns the roles @wabbit/tome-lms-ui leaves open for a theme, self-hosts its two OFL font families, and restyles blocks, chrome and the learner UI through their public hooks. The blocks it is composed from are lms-pack's enrollment-cta, instructor-card, lesson-list, course-card, course-index and skill-path-rows; outcome-hero, section-header, text-block and media-feature from extras; term-ledger from dossier-pack; gallery, split-view and media from agency-essentials; and the marketing-starter testimonial, faq and cta.
Layer: ui · Family / tier: themes / addon · Versioning: independent.
Peer dependencies
| Peer | Range | Required |
|---|---|---|
@wabbit/tome-ui | >=0.22.0 <1.0.0 | yes |
@wabbit/tome-core | >=1.25.0 <2.0.0 | no (optional) |
@wabbit/tome-lms-ui | >=0.12.0 <1.0.0 | no (optional) |
@wabbit/tome-lms | >=0.26.0 <1.0.0 | no (optional) |
@wabbit/tome-ui is a CSS-contract peer. Its tokens.css re-declares the var()-aliased Layer 2 under [data-tome-theme] and defines the fixed-band ink family this theme sets, and it ships the platform scroll reveal Schoolroom turns on. @wabbit/tome-core is needed only for the @wabbit/tome-theme-schoolroom/register subpath and the ThemeManifest type. @wabbit/tome-lms-ui and @wabbit/tome-lms matter to a site that runs the learner app: the theme styles lms-ui's hooks and assigns the roles it leaves unassigned, and the blocks hydrate from tome-lms.
What is in this release
0.1.0 shipped the whole theme: the manifest, the tokens, the self-hosted fonts, the lms-ui role map and the three treatments, one stylesheet per area (treatments/marketing.css for the blocks and the chrome, treatments/player.css for the lesson player, treatments/learner.css for the dashboard, the certificate and the verification page). styledBlocks lists the seventeen blocks the marketing treatment restyles. (0.1.0's own notes said the treatments would follow in the next release; they did not, they were in it. This 0.1.1 corrects the notes and changes no CSS.)
- Four palettes, eight modes: Margin (graphite and a highlighter yellow; the default and the bare selector), Lab (ink-pen violet), Atelier (magenta) and Night School (chalk blue; the one dark-led palette). Margin, Lab and Atelier are designed light with a derived dark mode. Night School is designed dark, opens dark and derives its light mode. See Palettes.
- Status colors as tokens: passed, needs work, locked and expired are the same set in every palette per mode, so "passed" means one thing across a school's palette choices. See Status tokens.
- The lms-ui role map: the soft tints and the expired, needs-work, revoked and locked roles lms-ui documents as unassigned, assigned per palette in
tokens/lms.css. - Width: a marketing stage and a learner app track, with the app track published through the platform token
--tome-app-max-widthso lms-ui's shells follow it. See Width. - The treatments: the marketing treatment restyles the seventeen blocks in
SCHOOLROOM_STYLED_BLOCKS(lms-pack's enrollment-cta, instructor-card, lesson-list, course-card, course-index and skill-path-rows; extras' outcome-hero, section-header, text-block and media-feature; dossier-pack's term-ledger; agency-essentials' gallery, split-view and media; marketing-starter's testimonial, faq and cta) and Navbar 1's grouped Courses panel; the player treatment styles lms-ui's lesson player, quiz and assignment renderers through their public hooks; the learner treatment styles the dashboard pieces, the flat rail, the document certificate and the verification page. - Companion packs. Schoolroom owns no blocks; the demos are composed from these packs' blocks (
SCHOOLROOM_RECOMMENDED_BLOCKS): lms-pack, extras, dossier-pack, agency-essentials and marketing-starter.SCHOOLROOM_PLANNED_STYLED_BLOCKSis the same seventeen slugs, kept as the list the treatment is tested against.
Usage
Import the stylesheet once at the app root, after tome-ui's tokens (and after lms-ui's tokens for a learner app), and activate the theme on <html>:
// app/layout.tsx
import '@wabbit/tome-ui/tokens.css'
import '@wabbit/tome-lms-ui/tokens'
import '@wabbit/tome-theme-schoolroom/styles.css'
import '@wabbit/tome-theme-schoolroom/fonts.css'<html data-tome-theme="schoolroom"> <!-- Margin, light -->
<html data-tome-theme="schoolroom" data-tome-palette="lab"> <!-- Lab, light -->
<html data-tome-theme="schoolroom" data-tome-palette="atelier" data-theme="dark"> <!-- Atelier, derived dark -->
<html data-tome-theme="schoolroom" data-tome-palette="night" data-theme="dark"> <!-- Night School, as designed -->With no data-tome-palette the theme renders Margin. data-theme="dark" gives Margin, Lab and Atelier their derived dark mode. Night School's manifest entry carries defaultMode: 'dark', so the admin palette picker and themeConfigHtmlAttributes() open it dark; data-theme="light" is its derived light mode.
To have the admin ThemeSwitcher, createThemeConfig() and other registry readers list it, register it on the server before building the Payload config:
import '@wabbit/tome-theme-schoolroom/register'The accent fills the action; the wash marks where you have been
The action fill and the progress line are the palette's accent (--sr-accent, on --sr-accent-ink). In Margin and in Night School's light mode that fill is the ink itself. The highlight wash (--sr-wash) marks the student's current row and is decorative, never a boundary. Every state also carries a word or an icon and a 3px left rule, so none depends on color alone.
Status tokens
Passed, needs work and expired are judged colors, the same in all four palettes per mode. Each has a text weight and a soft tint behind it; locked is the muted ink on the palette tint (no new color), and info is the ink on the tint.
| State | Text (light / dark) | Soft tint (light / dark) | lms-ui role |
|---|---|---|---|
| Passed | #5B6D48 / #3ADD9E | #E2F6E6 / #172C1D | --lms-color-success-text, -success-soft |
| Needs work | #8B5B49 / #D8B501 | #FFEDD7 / #31240E | --lms-color-needs-work, -warning-text, -warning-soft |
| Expired | #C0234B / #FD717C | #FFE9E6 / #381F1B | --lms-color-expired, -error-text, -error-soft |
| Locked | the muted ink | the palette tint | --lms-color-locked |
| Info | the ink | the palette tint | --lms-color-info-text, -info-soft |
lms-ui reads its colors as var(--lms-color-x, <today's literal>), so with no theme every component renders exactly as before; this theme assigns the roles in tokens/lms.css as var() references to its own palette tokens. Load the stylesheet after lms-ui's tokens: lms-ui declares --lms-color-locked at :root, and the later declaration wins at equal specificity.
Fonts
Two families ship inside the package as latin-subset woff2 files, each with its SIL OFL 1.1 licence: Literata (display and reading: headings at 500, the lesson body, the teacher's letter and, in its true italic, the margin voice; variable opsz 7 to 72 and wght 400 to 700, roman and italic; fonts/OFL-Literata.md) and Lexend (the interface: UI, meta and running copy; variable wght 400 to 600; fonts/OFL-Lexend.md). The subset covers Basic Latin, Latin-1 and Latin Extended-A, so names and Spanish render. There is no third-party font request by default. Three ways to deliver them:
import '@wabbit/tome-theme-schoolroom/fonts.css': the bundler resolves the files from this package.- Your own host: copy the files from
@wabbit/tome-theme-schoolroom/fonts/*and injectgetSchoolroomFontStyles({ src }). next/font/local, using the files fromgetSchoolroomFontFaces().
import { getSchoolroomFontPreloads } from '@wabbit/tome-theme-schoolroom/fonts'
// in <head>: preload the roman display face and the interface face
{getSchoolroomFontPreloads({ src: '/fonts/schoolroom/' }).map((l) => <link key={l.href} {...l} />)}Sources, recorded in scripts/subset-fonts.py (the build never runs it; a rebuild is byte-identical) and checked by SHA-256, all from google/fonts at commit 6157dfaa640ca5738b4d1a7070fe389c3036d7fc:
| Face | Source | SHA-256 |
|---|---|---|
| Literata | ofl/literata/Literata[opsz,wght].ttf | b41138c9373112f32abb589cc22e8674b06ed4048b0c513be922bdd26f274440 |
| Literata Italic | ofl/literata/Literata-Italic[opsz,wght].ttf | d483dfaeba9cbf4ce71d32a52ee65df82f7e35b15fff8d1011cdb242d1fcd465 |
| Lexend | ofl/lexend/Lexend[wght].ttf | 3add53e641fbc81da64da4bb254285e2831b52b029527bc0714e2b9610832ee6 |
Numeric columns use Literata; Lexend has no tabular figures. Lexend's GSUB carries neither tnum nor pnum. Any cell that holds a duration, count, score, price or total in a column emits data-figure, and the theme sets those cells in Literata with font-variant-numeric: tabular-nums lining-nums. The recipe checks that Literata keeps tnum, lnum and pnum and that its ten tabular figures share one advance at every tested weight and optical size, and the test suite re-checks the shipped files. Lexend's OFL.txt declares the Reserved Font Name "RevReading Lexend"; the subset keeps the family name "Lexend", so no rename applies. Code falls back to the system monospace stack (the manifest has no mono slot).
Width
Marketing bands cap their content at the stage, learner pages at the app track, and prose stays at the 600px measure:
| Token | Value | At 1440 | At 1800 | At 1920 |
|---|---|---|---|---|
--sr-stage | clamp(1120px, 80vw, 1560px) | 1152px | 1440px | 1536px |
--sr-app | min(1760px, 100vw - 2 * var(--sr-gutter)) | 1332px | 1692px | 1760px |
--sr-measure | 600px | 600px | 600px | 600px |
The app track belongs to the shell, so the theme sets the platform token --tome-app-max-width to --sr-app and @wabbit/tome-lms-ui reads it on LearnerShell and CourseShell (its default is none, so other sites are unchanged).
Public API
| Export | Subpath | Purpose |
|---|---|---|
schoolroomThemeManifest (ThemeManifest) | ., @wabbit/tome-theme-schoolroom/manifest | The theme's manifest: name schoolroom, kind: 'theme', tier addon, four palettes (Night School with defaultMode: 'dark'), fonts and one pairing, recommended blocks and chrome (Navbar 1, footer 7), styledBlocks, requires.tomeUi. |
SCHOOLROOM_PALETTE_IDS, SchoolroomPaletteId | ., @wabbit/tome-theme-schoolroom/manifest | ['margin', 'lab', 'atelier', 'night'], the data-tome-palette values. |
SCHOOLROOM_PALETTE_DEFAULT_MODES | ., @wabbit/tome-theme-schoolroom/manifest | The mode each palette opens in: light for three, dark for Night School. |
SCHOOLROOM_STYLED_BLOCKS, SCHOOLROOM_PLANNED_STYLED_BLOCKS, SCHOOLROOM_RECOMMENDED_BLOCKS, SCHOOLROOM_PENDING_CATALOG_BLOCKS | ., @wabbit/tome-theme-schoolroom/manifest | The block slugs the theme restyles today, the slugs its treatments will restyle, the slugs its demos are composed from, and the ones whose pack has not yet registered them in the block catalog. |
ThemeManifest (type) | . | Re-exported from @wabbit/tome-core/registry/theme. |
getSchoolroomFontFaces(), getSchoolroomFontStyles({ src }), getSchoolroomFontPreloads({ src }) | ., @wabbit/tome-theme-schoolroom/fonts | Font descriptors, @font-face CSS for a custom host, and preload links. |
SCHOOLROOM_FONT_STACKS, SCHOOLROOM_FONT_LICENSES, SCHOOLROOM_LATIN_UNICODE_RANGE, SCHOOLROOM_DISPLAY_WEIGHTS, SCHOOLROOM_UI_WEIGHTS | ., @wabbit/tome-theme-schoolroom/fonts | The CSS stacks the tokens use, the licence files, the subset range, and the weights used. |
SchoolroomFontFace, SchoolroomFontPreloadDescriptor, SchoolroomFontStylesOptions (types) | ., @wabbit/tome-theme-schoolroom/fonts | The descriptor shapes. |
| side effect | @wabbit/tome-theme-schoolroom/register | Registers schoolroomThemeManifest in tome-core's theme registry. |
| stylesheet | @wabbit/tome-theme-schoolroom/styles.css | Tokens and treatments. The file that does everything visual. |
| stylesheet | @wabbit/tome-theme-schoolroom/fonts.css | The @font-face rules. Optional. |
| font files | @wabbit/tome-theme-schoolroom/fonts/* | The woff2 files and their OFL licences, for self-hosting. |
import { schoolroomThemeManifest } from '@wabbit/tome-theme-schoolroom/manifest'
schoolroomThemeManifest.palettes.map((p) => p.label)
// ['Margin', 'Lab', 'Atelier', 'Night School']Tokens
The theme sets Layer 1 (the names tome-ui's tokens.css reads: --background, --foreground, --card, --primary, --accent, --muted, --border, --input, --ring, the feedback, chart and muted2 names) and lets tome-ui's generated re-resolution derive the aliased Layer 2. It sets the Layer-2 names that have no Layer-1 input behind them:
--tome-color-accent-text, the feedback*-textweights, and--tome-color-surface-tint- the fixed-band inks (
--tome-color-surface-solid-darkand its family,--tome-color-surface-inverseand its family) - the four
--tome-type-*families, each paired with its--font-*slot - the platform token
--tome-app-max-width
The colour roles Schoolroom adds are private --sr-* tokens: the ground, surface, tint and wash (-ground, -surface, -tint, -wash), the ink and muted ink (-ink, -muted), the hair and the input line (-hair, -line), the action (-accent, -accent-ink, -accent-text, -hover, -focus), the closing band (-band, -band-ink, -band-muted, -band-accent, -band-accent-ink, -band-hair) and the status set (-success-text, -success-soft, -warning-text, -warning-soft, -error-text, -error-soft, -info-text, -info-soft). The size tokens (--sr-stage, --sr-app, --sr-gutter, --sr-measure, --sr-space-*, --sr-r-*, --sr-dur-* and the type scale) are palette-independent.
Selector ladder (specificity, never load order):
| Selector | Specificity | Holds |
|---|---|---|
[data-tome-theme="schoolroom"] | 0,1,0 | Margin light, type, radius, rhythm, width, motion, the lms-ui role map |
[data-tome-theme="schoolroom"][data-theme="dark"] | 0,2,0 | Margin dark |
[data-tome-theme="schoolroom"][data-tome-palette="p"] | 0,2,0 | palette light |
[data-tome-theme="schoolroom"][data-tome-palette="p"][data-theme="dark"] | 0,3,0 | palette dark |
The palette files are generated by scripts/generate-tokens.mjs from scripts/palette-roles.json, the output of the palette script (scripts/palettes.mjs --fix), and copied, never retyped.
Shape and rhythm. Fields, buttons and tags are 10px; media and the demo card are 20px; pills are for status only; progress is square-ended; rules are 1px in the palette's hair color; the tap target is 44px. The approved mockups were authored on a 16px root and tome-ui sets html { font-size: 18px }, so every rem value is converted (x 8/9) and the vw terms are kept.
Motion. One reveal (a 10px rise over 560ms, staggered 90ms) through tome-ui's platform scroll reveal, waiting for fonts; hover and press at 180ms with a 0.985 press; the progress step at 560ms. Under prefers-reduced-motion every duration resolves to 0s and the travel to 0px, and nothing is hidden without script.
Palettes
| Palette | Ground / surface / tint | Ink / muted | Action fill (light / dark) | Highlight wash (opening mode) |
|---|---|---|---|---|
| Margin (default) | #FAF8F3 / #FFFFFF / #F0EDE4 | #1D1F23 / #4F525A | graphite #1D1F23 / #F4F1EA | #F4E06A |
| Lab | #F5F7F8 / #FFFFFF / #EAE8F3 | #140020 / #3D405D | ink-pen violet #5B3CC4 / #8783FC | #DDD6F7 |
| Atelier | #F8F3EC / #FFFDF9 / #F0E7DC | #2A2220 / #5A4F4A | magenta #A91C72 / #F98CCE | #F6D3DF |
| Night School (opens dark) | dark #18201E / #1F2826 / #222C2A | #E8EDE6 / #AEB8B2 | chalk blue #96DBFF (light: the ink #18201E) | #2C4350 (dark) |
Every judged role (ink, muted, hair, line, the action, the accent text, the band and its accents, and the status text colors) clears an OKLab distance of 0.05 from every palette shipped before it, in both modes. Grounds, surfaces, tints, washes and the on-fill colors are reported for information only. Margin's graphite is within 0.004 of Studio Gallery's sign and Commons Civic's ink, its paper ground within 0.001 of Table Linen's, and Night School's light ink and band within 0.007 of Shelf Stone's: neutral near-twins, accepted under the rule and recorded here so a later audit does not rediscover them.
Contrast
All ratios are WCAG 2.x. Text must reach at least 4.5. Marks, field outlines, the focus ring and the progress fill must reach at least 3. The test suite re-measures every pair in all eight palette modes from the shipped values, the band and status pairs included, and pins the step-0 table. Atelier light's progress fill against its track is the tightest pass at 3.01.
Server / client posture
The package is pure CSS plus constants. It has no React components and no 'use client' files, so it is inert under React Server Components. It adds nothing to a client bundle beyond the stylesheets the consumer imports. @wabbit/tome-theme-schoolroom/register is a server-side import. The outcome hero's toggle, the nav panel and the lesson player's islands are the packages' own, not this package's.
Tests
pnpm --filter @wabbit/tome-theme-schoolroom testThe vitest suites cover:
- the theme conformance check in theme mode (Layer-1 surface,
*-textliterals, radius and font slots, the full dark redeclaration, palette coverage for all four palettes in both modes, scoping, no module class names, font pairing) and in platform mode - manifest validation, its agreement with
package.json(the version is read from the newest CHANGELOG heading, or 0.0.0 before the first release), registration, Night School as the only palette withdefaultMode, and the catalog check for every recommended and planned block - every palette defining the full required set in both modes, every value agreeing with the palette script's output, the status set being identical across palettes, and the lms-ui role map assigning only roles lms-ui documents
- a contrast test that re-measures every text pair at 4.5 and every mark or boundary at 3 in all eight modes and pins the step-0 table
- the A4.1 font pairing, the weight tokens, every rule setting both its family and its weight, the self-hosted faces with their licences,
tnum,lnum,pnumand the axes read out of the woff2 files, Lexend having notnum,tnumacross the weight and optical-size range (needs Python with fontTools; setSCHOOLROOM_FONT_PYTHONto run it), and the subset range - hook coverage: every hook in the contract is classified as emitted by a package on this branch, not emitted under its contracted name, or owned by the starter, and a treatment may only target a hook that exists
- the measure and the width: every running-text hook resolves to 600px, nothing widens it, and the stage and app track equal 1152, 1440 and 1536px and 1332, 1692 and 1760px at 1440, 1800 and 1920
- no colour literal outside
src/tokens/, no comment that swallows a rule, no class selector, no hand-coloured shadow, no scrim or gradient over a photograph, the shape tokens, and every treatment rule scoped to the theme - the motion tokens, the reveal switch and the reduced-motion path
License
Commercial, see LICENSE.md. Published to the private registry at https://npm.wabbit.com/.
Exports
| Import path |
|---|
@wabbit/tome-theme-schoolroom |
@wabbit/tome-theme-schoolroom/manifest |
@wabbit/tome-theme-schoolroom/register |
@wabbit/tome-theme-schoolroom/fonts |
@wabbit/tome-theme-schoolroom/styles.css |
@wabbit/tome-theme-schoolroom/fonts.css |
@wabbit/tome-theme-schoolroom/fonts/* |
Changelog
0.1.xMinor
First release of Schoolroom, a Tome theme for people who teach for a living and want their school to look like a school, not a funnel.
This first cut is the foundation: four palettes, each light and dark (Margin, the default, in graphite with a highlighter yellow; Lab in ink-pen violet; Atelier in magenta; Night School in chalk blue, which opens dark), the passed, needs-work and expired colors as tokens that mean the same thing in every palette, and a roomy marketing width with a learner-app width the lesson player and dashboard follow. Type is Literata for headings, reading and every column of figures, and Lexend for the interface; both are self-hosted.
It also assigns the colors the learner UI leaves open for a theme, so passed rows, locked lessons and expired certificates take the palette's own colors.
The block, chrome and learner-app treatments follow in the next release.
2 fixes (0.1.1 – 0.1.2)
- 0.1.2
Fixes the offset gallery: the two small photographs now sit beside the large one instead of in a sliver over it.
The pack lays the offset out by named grid areas from 48rem; the theme placed the photographs by lines but left the pack's areas in place, so from 768px the small photographs fell into an implicit column a few pixels wide over the large one. The theme now clears
grid-template-areas, resets the pack's area placement, and places the photographs through the parent so its rules out-rank the pack's:nth-childplacement.A test pins both.
- 0.1.1
Corrects the package notes: 0.1.0 shipped the three treatments, not only the foundation.
The README's "What is in this release" and the 0.1.0 changelog entry said the block, chrome and learner-app treatments would follow in the next release. They were in 0.1.0:
styledBlockslists the seventeen restyled blocks, and the marketing, player and learner stylesheets ship in the package.This release changes the notes and no CSS.