Documentation menu
Documentation / Blocks & themes / theme-commons
Theme Commons
npm install @wabbit/tome-theme-commonsFirst 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
Commons, a Tome theme for organizations that belong to their members: neighborhood associations, tenant and parent groups, co-ops, community halls, clubs and small nonprofits. The record is the design: a reference number leads every figure, a filed document backs every claim, the Running Total is one honest line over a flat bar, and the one dark band is the ask. Schibsted Grotesk and Inclusive Sans, a lantern-and-bottle-green default (Grove), four palettes plus derived opposite modes, self-hosted OFL fonts, activated by data-tome-theme="commons".
Overview
Commons is a Tome theme for organizations that belong to their members: neighborhood associations, tenant and parent groups, co-ops, community halls, clubs and small nonprofits. Its stance is "the record is the design". A reference number leads every figure, a filed document backs every claim, the Running Total is one honest line over a flat bar, and the page asks for money exactly once, in the one dark band. It never pleads: no pity imagery, no countdown, no pulsing, no pre-ticked box, and nothing set over a photograph.
This package is a theme (a ThemeManifest of kind: 'theme'). It owns no blocks. It declares the whole token surface blocks read, self-hosts its two OFL font families, and restyles blocks and chrome through their public hooks. The blocks it is composed from are campaign-pack's campaign-hero (variant portrait), campaign-count, give-form, membership-card, campaign-case, campaign-tiers and campaign-terms; division-card and event-list from org-pack; stat (variant sourced) and team-roster from agency-essentials; section-header, layoutGrid, text-block and content from extras; the content-writer archive; and the marketing-starter faq.
Layer: ui · Family / tier: themes / addon · Versioning: independent.
Peer dependencies
| Peer | Range | Required |
|---|---|---|
@wabbit/tome-ui | >=0.21.0 <1.0.0 | yes |
@wabbit/tome-core | >=1.25.0 <2.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 Commons turns on. @wabbit/tome-core is needed only for the ./register subpath and the ThemeManifest type.
What is in the first release
Wave 1 ships the foundation and the home page's first and last bands; Wave 2a adds most of the bands between them:
- Foundation: four palettes (Grove, Civic, Vesper and Rally) with their derived dark modes, the type and rhythm tokens, the self-hosted fonts, the base treatments, the record-line voice, the section opener (the masthead line), the motion tokens with their reduced-motion paths, and 44px hit areas on every control.
- Chrome: Navbar 1 with Give (the fill) and Join (the outline) always at the right.
- Home: the hero (
campaign-heroportrait: a portrait photograph from the left page edge, the mission with one phrase at 800, Give and Join, the Running Total); the Running Total (the line and bar, and the Ledger Strip); the sourced figures ledger (statsourced); and the close as the one dark band (an extraslayoutGridholding the Running Total, the Give form and the Card).
- Between the hero and the close (Wave 2a): the band rhythm and the tint (the opener's gap, the section rhythm, ground and tint alternating), programs (
division-cardentry, the photo's side alternating), the updates and the Record (archivefiled: a lead and a ruled list; a register with the record number, the category as a tint wash, the dates and a Download action), events (event-list: dated rows, capacity chips, "Add to calendar"), people (team-rosterpeople, with the statement tile on a hairline and no top bar) and governance (thelayoutGridthat holds the next board meeting), the FAQ with its "Still have a question?" card, the footer's tonal zone (Footer 7), and the phone menu (no scrim).
Wave 2b adds the campaign page's case, levels and terms, the checkout chrome, the thank-you page and the sign-up sheet. COMMONS_STYLED_BLOCKS lists every block with a treatment.
Usage
Import the stylesheet once at the app root, after tome-ui's tokens, and activate the theme on <html>:
// app/layout.tsx
import '@wabbit/tome-ui/tokens.css'
import '@wabbit/tome-theme-commons/styles.css'
import '@wabbit/tome-theme-commons/fonts.css'<html data-tome-theme="commons"> <!-- Grove, light -->
<html data-tome-theme="commons" data-tome-palette="civic"> <!-- Civic, light -->
<html data-tome-theme="commons" data-tome-palette="rally" data-theme="dark"> <!-- Rally, derived dark -->With no data-tome-palette the theme renders Grove. Every palette is a light design and data-theme="dark" gives it its derived dark mode, built from the palette's band values. Rally's closing band stays the red fill in both modes.
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-commons/register'The accent fills exactly two things
The accent fills the primary action (Give, or Join) and the Running Total's bar. Everywhere else it is punctuation: rules, the record number, the focus ring, hover underlines. Every accented state also changes shape or weight, because the mark against the ink is under 3:1 in every palette. In Grove, the default, the lantern amber fills only Give and the bar, its label is green, the Give button carries a 1.5px ink edge, and the mark, the ring and the record number are the ink itself (1.00:1), over a dark-glass track.
The Card has no accent top rule
Every Card has a neutral hairline all round. The recommended tier has a 2px ink border all round and a visually hidden "Recommended", never a coloured top edge, a badge or a bar.
The Running Total
The theme maps its roles onto campaign-pack's own properties (--campaign-bar-fill, --campaign-bar-track, --campaign-bar-height, --campaign-bar-radius, --campaign-dur-bar, --campaign-ease) and does not style the bar's inner element. On the closing band the fill is the band's action colour and the track the band line. The numerals count up once and the bar fills once, never past the real total; under reduced motion and without script the final values render at load.
Fonts
Two variable faces ship inside the package as latin-subset woff2 files, each with its SIL OFL 1.1 licence: Schibsted Grotesk (wght 400 to 900: the display, the record line and every numeral; fonts/OFL-Schibsted-Grotesk.md) and Inclusive Sans (wght 400 to 700: the body; fonts/OFL-Inclusive-Sans.md). There is no third-party font request by default. Three ways to deliver them:
import '@wabbit/tome-theme-commons/fonts.css': the bundler resolves the files from this package.- Your own host: copy the files from
@wabbit/tome-theme-commons/fonts/*and injectgetCommonsFontStyles({ src }). next/font/local, using the files fromgetCommonsFontFaces().
import { getCommonsFontPreloads } from '@wabbit/tome-theme-commons/fonts'
// in <head>: preload the two faces
{getCommonsFontPreloads({ src: '/fonts/commons/' }).map((l) => <link key={l.href} {...l} />)}Sources, recorded in scripts/subset-fonts.py (the build never runs it) and checked by SHA-256:
| Face | Source | Pinned at |
|---|---|---|
| Schibsted Grotesk | google/fonts ofl/schibstedgrotesk/SchibstedGrotesk[wght].ttf | commit 8b0a1d0f5983c89bc2b93f1b5fb55f9e252744b5, SHA-256 6ceeadf6be8e1fd7687011c7fa38ed0edd1abe967a0b73d97caec183552e823d |
| Inclusive Sans | google/fonts ofl/inclusivesans/InclusiveSans[wght].ttf | commit 207b497e875e3dc4b87f6a38fc2d1af535ddf387, SHA-256 52ae5fdb8eda45633f1e9817ebdb07d8075c89081d51204f16f78394608a5c4e |
Schibsted's default figures are proportional and its tabular comma takes a full figure width, so big numerals and record numbers ("No. 2026") are proportional and tnum is set only where figures stack in a column. The Schibsted subset keeps tnum, pnum and case; the ten tabular figures share one advance at every tested weight (the recipe checks its own output and the test suite re-checks the shipped file). Inclusive Sans has no tnum, so no figure column is ever set in it. Sofia Sans was the round-1 alternate; option C was picked in Schibsted, so the package ships Schibsted only. No italic is shipped: emphasis is weight.
Public API
| Export | Subpath | Purpose |
|---|---|---|
commonsThemeManifest (ThemeManifest) | ., ./manifest | The theme's manifest: name commons, kind: 'theme', tier addon, four palettes, fonts and one pairing, recommended blocks and chrome (Navbar 1, footer 7), styledBlocks, requires.tomeUi. |
COMMONS_PALETTE_IDS, CommonsPaletteId | ., ./manifest | ['grove', 'civic', 'vesper', 'rally'], the data-tome-palette values. |
COMMONS_PALETTE_DEFAULT_MODES | ., ./manifest | The mode each palette opens in: light for all four. |
COMMONS_STYLED_BLOCKS, COMMONS_RECOMMENDED_BLOCKS, COMMONS_PENDING_CATALOG_BLOCKS | ., ./manifest | The block slugs the theme restyles, the slugs its demo is composed from, and the ones campaign-pack has not yet registered in the block catalog. |
ThemeManifest (type) | . | Re-exported from @wabbit/tome-core/registry/theme. |
getCommonsFontFaces(), getCommonsFontStyles({ src }), getCommonsFontPreloads({ src }) | ., ./fonts | Font descriptors, @font-face CSS for a custom host, and preload links. |
COMMONS_FONT_STACKS, COMMONS_FONT_LICENSES, COMMONS_LATIN_UNICODE_RANGE, COMMONS_DISPLAY_WEIGHTS, COMMONS_UI_WEIGHTS | ., ./fonts | The CSS stacks the tokens use, the licence files, the subset range, and the weights used. |
CommonsFontFace, CommonsFontPreloadDescriptor, CommonsFontStylesOptions (types) | ., ./fonts | The descriptor shapes. |
| side effect | ./register | Registers commonsThemeManifest in tome-core's theme registry. |
| stylesheet | ./styles.css | Tokens and treatments. The file that does everything visual. |
| stylesheet | ./fonts.css | The @font-face rules. Optional. |
| font files | ./fonts/* | The woff2 files and their OFL licences, for self-hosting. |
import { commonsThemeManifest } from '@wabbit/tome-theme-commons/manifest'
commonsThemeManifest.palettes.map((p) => p.label)
// ['Grove', 'Civic', 'Vesper', 'Rally']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 colour roles Commons adds are private --commons-* tokens: the fill (-fill, -on-fill, -fill-edge), the mark and the record number (-mark, -accent-text), the bar's -track, the closing band (-band, -on-band, -on-band-muted, -accent-on-band, -band-action, -on-band-action, -band-input, -ring-on-band and the hairline -band-line), the Card (-card-ink, -card-muted, -card-accent-text, -card-rule), the tags and the pressed segment (-tag-bg, -seg-on), the footer zone (-footer-bg), the tints, and the photo tone.
Selector ladder (specificity, never load order):
| Selector | Specificity | Holds |
|---|---|---|
[data-tome-theme="commons"] | 0,1,0 | Grove light, type, radius, rhythm, lanes, motion |
[data-tome-theme="commons"][data-theme="dark"] | 0,2,0 | Grove dark |
[data-tome-theme="commons"][data-tome-palette="p"] | 0,2,0 | palette light |
[data-tome-theme="commons"][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 and tags are 3px, media and cards 6px, the Card alone 14px, actions are pills, bands are square. Rules are 1px, the record hairline 2px. The prose measure is 600px. The page lanes are three marginalia columns each side and one prose-pad column, four each side from 1760px.
Type scale. 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.
Palettes
| Palette | Ground / surface / tint | Ink / muted | Fill (Give, the bar) | Closing band |
|---|---|---|---|---|
| Grove (default) | #F2F1EA / #FAFAF6 / #E3E8DD | #0B4A30 / #3F5A4D | lantern #E69301, label #05442A, 1.5px ink edge | #08331F |
| Civic | #F8F6F1 / #FFFFFF / #ECEFF4 | #1C1E22 / #4F535B | cobalt #1E4FC9 | #1C1E22 |
| Vesper | #F1EEEA / #FBFAF8 / #E9E2E7 | #2A1533 / #564B5A | plum #6B3570 | #2B1F30 |
| Rally | #F4EEE2 / #FBF8F1 / #E9E2D3 | #0F0D0C / #4A4540 | poster red #9C1A0A | the red fill, in both modes |
Rally ships as a recorded near-match to Roster Chalk (accent 0.065, whole palette 0.086 in OKLab).
Contrast
All ratios are WCAG 2.x. Text must reach at least 4.5. Marks, field outlines, the focus ring, the Give button's boundary, the bar against its track and the closing band's field outline and ring must reach at least 3. The band line is a hairline with no floor; band ink on it (the band's tags) must reach 4.5. The test suite re-measures every pair in all eight palette modes from the shipped values, the band pairs included, and pins the brief's tightest ones: Grove's Give label on the lantern at 4.57 and its dark bar against its track at 3.10. The track is under 3:1 against the ground in every light palette but Grove, so the goal is always printed as a numeral beside the bar.
What it styles
| Hook | Treatment |
|---|---|
Section openers ([data-opener], [data-opener-meta], [data-opener-title], [data-opener-body], [data-opener-link], from section-header ruled-split) | A record-voice line under a 1px hairline in the mark, then the heading and one sentence in the left nine columns. |
Chrome header ([data-visible][data-nav-theme][data-overlay], button[data-mobile-menu-trigger="word"]) | The ground, the surface and hairline once scrolled, Give as the fill and Join as the outline, the word menu button. |
campaign-hero portrait ([data-campaign-hero-copy], -kicker, -title, -lede, -actions, -media, -image, -caption, -credit) | The photograph from the left page edge to line 9, the copy from line 10, the label and credit as a caption after the image; the compact band with the photo at the end. |
The Running Total ([data-campaign-line], [data-campaign-bar], [data-campaign-line-meta], [data-campaign-cells], [data-campaign-cell], [data-campaign-value], [data-campaign-rule]) | The line, the flat bar, the ruled Ledger Strip. |
stat sourced ([data-stat-label-cell], [data-stat-cells], [data-stat-cell], [data-stat-source], [data-stat-value]) | One ruled strip of four cells on the tint, each led by its record number and source link. |
The close (layoutGrid holding a give-form: [data-layout-grid-variant], [data-tome-grid-cell]) | The one dark band, painted and placed with --tome-block-column and explicit rows. |
give-form ([data-give-*]) and membership-card ([data-membership-*]) | The chips, the segmented control, the Give pill; the wallet-proportioned Card with a 2px ink border all round when recommended. |
[data-appearance] links and buttons, [data-campaign-cta] | The pills: Give is the fill, Join the outline, one underline hover. |
[data-compliance-label] (every block) | A tint-wash tag in a caption on the ground, never an overlay on a photograph. |
Focus (:focus-visible) | A 2px ring in the ring colour, 3px clear of the element. |
Treatments target only public hooks: data-block-type on the block wrapper, data-block-variant on the root, the blocks' documented data-* part attributes, and published component tokens. The positional reads are a cell's own row position in the ledgers and the first and last cell of the close.
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. ./register is a server-side import. The Running Total's motion is campaign-pack's island, not this package's.
Tests
pnpm --filter @wabbit/tome-theme-commons 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, and the pending-catalog tripwire for the campaign-pack blocks not yet registered - every palette defining the full required set in both modes, every value agreeing with the palette script's output, the band line present in all eight modes
- a contrast test that re-measures every text pair at 4.5 and every mark or boundary at 3 in all eight modes, the closing band's pairs and the bar against the band line included, and pins the brief's pairs
- 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,pnumand the axes read out of the woff2 files,tnumacross the weight range (needs Python with fontTools; setCOMMONS_FONT_PYTHONto run it), and the subset range - hook coverage: every hook this release styles has a selector, the hooks of later waves are not styled early, and the few that carry behaviour only are listed with the reason
- the measure: every running-text hook resolves to 600px and nothing widens it
- no colour literal outside
src/tokens/, no comment that swallows a rule, no class selector, no hand-coloured shadow, no accent bar and no accent top rule, the shape tokens, no scrim or overlay on a photograph, and every treatment rule scoped to the theme - the reveal switch and timing, the page lanes, the motion tokens, the hero's, the close's and the ledgers' geometry, and every reduced-motion path
License
Commercial, see LICENSE.md. Published to the private registry at https://npm.wabbit.com/.
Exports
| Import path |
|---|
@wabbit/tome-theme-commons |
@wabbit/tome-theme-commons/manifest |
@wabbit/tome-theme-commons/register |
@wabbit/tome-theme-commons/fonts |
@wabbit/tome-theme-commons/styles.css |
@wabbit/tome-theme-commons/fonts.css |
@wabbit/tome-theme-commons/fonts/* |
Changelog
0.1.xMinor
New theme package
@wabbit/tome-theme-commonsfor organizations that belong to their members: neighborhood associations, tenant and parent groups, co-ops, community halls, clubs and small nonprofits.It is a new package, so the minor bump is purely additive. The first release ships four palettes with their derived dark modes: Grove (the default; a bottle-glass ink, with a lantern amber that fills only Give and the Running Total bar), Civic, Vesper and Rally (whose closing band is the red fill in both modes).
Fonts are Schibsted Grotesk (display, the record line and every numeral) and Inclusive Sans (body), self-hosted as OFL subsets that keep
tnumandpnumon Schibsted. The first treatments cover:- the base and the record-line voice, with the pills (Give is the fill, Join the outline);
- the section opener's masthead line;
- the Navbar 1 header;
- the
campaign-heroportraithero, with its photograph from the page edge and its label and credit in a caption after the image; - the Running Total (the line, the flat bar and the Ledger Strip), with the bar mapped onto campaign-pack's own custom properties;
- the sourced figures ledger (
statsourced); - the close as the one dark band, with the Give form and the Card inside it;
- the band rhythm and the tint: the opener's gap and the section rhythm on the block wrappers, the events band and the Record on the tint, and tags on the tint stepping to the surface;
- programs (
division-cardentry: the photo's side alternating by grid placement, the lead line, the next date under a hairline, the caption after the image); - the updates and the Record (
archivefiled: a lead and a ruled list; a register with the record number, the category as a tint wash, the dates, the file size and a Download action, the latest filing led by its thumbnail), and the outline "read the full record" link after the register; - events (
event-list: dated ruled rows, capacity chips where there is capacity data and a 2px border on a full event, "Add to calendar"), and the next board meeting stacked in its governance cell; - people (
team-rosterpeople: 4:5 portraits in one tonal treatment with the label below the photo, the role in the record voice, the statement tile on a 1px hairline with no top bar) and the governance grid under it; - the FAQ with its "Still have a question?" card (the callout, on a neutral hairline), the footer's tonal zone (Footer 7), the phone menu (no scrim, the same Give and Join pills) and the 44px targets for all of them;
- the Give form root filling its wrapper outside the close.
Checkpoint A fixes against the approved round 2:
- the close lays out on the page grid: its layoutGrid gives up the extras size container (layout containment made
subgridcompute tonone), its text-blocks' body and links span their root, the close's Running Total line runs the 600px measure, the band re-points--tome-color-surfaceand--tome-color-on-surface(the give form's and the Card's inks), and the reply link has a 44px target; - the figures strip places its label cell and cells under the sourced root, out-ranking the pack's own wide-screen placement, so the four cells run from the reading line from 1024px;
- the give form's control wrappers dissolve into their fieldset's grid (the segmented control and the chips fill the form), an empty error region draws no frame, and the Other input loses its second outline;
- the Card leaves its tiers' and picker's display to the block (one tier under the picker, opening on Household) and keeps its price at the face's start;
- the home hero sits on the page ground and, below 1024px, its photo row leads (grid rows; the copy stays first in the reading order);
- the hero eyebrow's record number takes the record-number colour;
- the volunteer form's Send is the outline pill (the lantern fills only Give and the bar);
- a layoutGrid other than the close (Ways in, governance) stacks below 1024px.
The recommended Card tier is a 2px ink border all round. No Commons rule draws a coloured top edge, a badge or a bar on a card.
Wave 2b treatments, the campaign page and the single-task flows:
- the case for a campaign (
campaign-case: the reading column with marginalia, each note an ink hairline with its title in the record voice, the budget as ruled lines with its computed total in the heavy weight, the margins folding into one tinted region below 1024px); - giving levels (
campaign-tiersgiving-levels: columns of the page under an ink hairline, the price at the Card's numeral size, the action as an outline pill on the bottom line, no recommended level and no box, one column, two from 640px, the level count from 1024px); - the terms ledger (
campaign-termsledger: the keep statement with its one run at the heavy weight, five or six terms as ruled definition rows, the filing line in the record voice); - Ways in as three equal doors (the volunteer form, Join and Give: the ask in the record voice, the action on the bottom line, the form's repeated step legend visually hidden, the Send and the two links as outline pills, labels with 44px targets), the opt-in sign-up sheet (
data-commons-texture="signup-sheet"on<html>: faint ruled rows with numbers in the gutter behind the doors only, never on the dark band, no numbers under 640px) and "What we hold to" (extrascontenton the tint, the heading in the start lane and the prose in the reading column); - chrome's checkout mode: the brand header with a hairline-divided label and "Back to <name>", wrapping to two rows under 640px (the real bar is one nowrap row), the plain footer on the footer zone's tint, and the lockup (
data-commons-lockup, a mark and the name in a row), which Wave 1 left unstyled; - the Give page (the context, the Running Total and the form as a panel on the card surface, side by side from 1024px) and the Join page (the wide Card on the content lines, the block's repeated heading visually hidden), every single-task h1 at the heavy h2 step, and the thank-you pages' numbered rows.
The close's rules now exclude a layoutGrid that holds the page's own h1, so the Give page, which also holds a give form, is not painted as the dark band. The hook-coverage later-wave list is empty.
Contract hooks without a part hook (the Ways-in text blocks, the form intro,
content, the thank-you rows) are reached by position through a documented allow-list in the structural-read test.Checkpoint B fixes against round 2 and the approved Give flow:
- the Give page: a paragraph before the page's h1 is the record line (the record voice, in the ink), and a text block after the form is the reply line, under the context from 1024px and after the form on a phone, its link a 44px target; the give form's opt-in lines (the amount hint, the fee-cover total over the button, the monthly charge line under it, the larger-gift line in the error region, whose hidden state no longer draws the empty error frame);
- the Ledger Strip: the bar takes the lantern fill and the deep track (campaign-pack's strip now reads the bar properties Commons already set), its "Demo figures" tag steps to the surface on the tint, and under the campaign page's own hero the strip takes round 2's tighter rhythm;
- the case: the budget total in proportional figures (no tabular comma gap), and below 1024px the reading column starts at the content edge instead of centring;
- the giving levels sit on the tint with their opener (round 2's alternation), one gap under the opener, with each level's sentence at the small size;
- the campaign updates (no kicker, no lead, no downloads) set the heading and its sentence in the start lane and the list in the reading column from 1024px, and a row's author line follows the number after a middle dot;
- the header keeps Give and Join below 1024px: one row (the lockup, Give and Join, Menu) from 560px, and two rows under it (the lockup and Menu, then Give and Join sharing the width), placed by grid area;
- 44px targets for a text block's own link row ("Write to Tom") and a linked hero heading ("The roof and the boiler").