v0.14.2
Released May 20, 2026 · All releases
Curated syntax preset lineup (Phase 1). The v0.14.2 milestone implements Curated syntax preset lineup — Phase 1 — imports six widely-recognised syntax palettes (Dracula, Solarized, Catppuccin, Tokyo Night, One Dark, Gruvbox) as first-party refrakt presets, following the pattern established by Nord in Nord preset module.
Changes
- Curated syntax preset lineup phase 1 (SPEC-057) + tint-mode override fix on preset-extending tints + jsonc highlighter language.
- Six imported syntax palettes. New
@refrakt-md/lumina/presets/{dracula,solarized,catppuccin,tokyo-night,one-dark,gruvbox}ship the six most widely-recognised community palettes as first-party presets, following the pattern established by Nord in v0.14.1. Dracula and One Dark are dark-only; Solarized, Catppuccin, Tokyo Night, and Gruvbox carry both light and dark canvases. Each preset uses the SPEC-056 mechanism unchanged — aThemeTokensConfigmodule with extendedSyntaxTokensroles, opt-incolor.code.*for canvas-claiming palettes, and live doc pages at/themes/<name>rendered through the scoped-tint mechanism. Application work only; no architectural changes. - Tint-mode override fix on preset-extending tints. When a tint extended a preset module path (e.g.
tint="nord"→extends: '@refrakt-md/lumina/presets/nord'), only the build-time scoped tint stylesheet knew about the preset and emitted--rf-color-*directly under[data-tint="nord"]. The runtime engine never saw the projected chrome accents becausepresetMapwasn't plumbed throughmergeThemeConfig→resolveTintExtends, so no inline--tint-*styles were emitted on tinted elements —tint.css's[data-color-scheme][data-tint]selectors collapsed to the colour scheme's neutral defaults regardless of the preset.presetMapnow threads from the SvelteKit loader path throughassembleThemeConfigandmergeThemeConfigintoresolveTintExtends, so preset chrome accents land inTintTokensshape and the engine emits them as inline--tint-*styles. The static scoped tint stylesheet keeps emitting--rf-color-*; inline styles override on the same element exactly astoken-stylesheet.tsalready anticipated. jsoncadded to pre-loaded highlighter languages. Config snippets across the preset doc pages (and other refrakt docs) use```jsoncto fence JSON-with-comments.jsoncwas intentional but not in Shiki'sDEFAULT_LANGS, so the highlighter silently fell back to plain text. One-line add — Shiki shipsjsoncas a bundled language.
Curated syntax preset lineup — Phase 1
Curated syntax preset lineup — Phase 1
Ship six widely-recognised, well-licensed syntax palettes as first-party refrakt presets — Dracula, Solarized, Catppuccin, Tokyo Night, One Dark, Gruvbox — following the pattern established by WORK-220 (Nord) under SPEC-056. Each palette becomes a ThemeTokensConfig module under packages/lumina/src/presets/, gets a documentation page under /themes/, and is registered in the refrakt docs site's theme.tints for live showcase rendering. No architectural changes — this spec is the application of SPEC-056 to a curated lineup.
This is Phase 1 of a longer roadmap. Phase 2 candidates (GitHub Light/Dark, Ayu, Rosé Pine, Monokai, Kanagawa, additional Catppuccin and Tokyo Night flavours) are listed in "Out of scope" so the door is open without committing to them now.
- Released in
- v0.14.2
Problem
SPEC-056 widened the syntax token contract and proved the mechanism on Nord. The lineup at v0.14.1 is:
- Lumina neutral default — the built-in chrome + neutral syntax everyone gets out of the box.
- Tideline — Lumina's cream + maritime navy chrome preset.
- Niwaki — refrakt's own scoped Japanese-garden syntax-only preset.
- Nord — the first imported third-party palette, integrated (chrome + canvas + foreground).
That's enough to demonstrate the architecture but not enough to satisfy users who want their site to feel like their preferred editor. Most refrakt users picking a theme want to choose from palettes they already know and love, not learn refrakt-internal vocabulary. The architecture is ready; the catalog isn't.
This spec is application work, not architecture work. Each preset is roughly 50–80 lines of ThemeTokensConfig plus a doc page; the implementation pattern is the one validated on Nord. Risk is low; volume is the main cost.
Design Principles
One preset per palette identity, not per flavour. Palettes with explicit light + dark variants from the same author (Solarized, Catppuccin, Gruvbox) ship as a single preset using modes.dark for the dark overlay. Users select the rendering mode via theme.colorScheme or page-level cascade, not by picking a different preset. This keeps the catalog small and matches the "one Nord, one Niwaki" precedent.
Canonical pair only for multi-flavour palettes. Catppuccin has four flavours (Latte / Frappé / Macchiato / Mocha); Tokyo Night has three (Storm / Moon / Day). Phase 1 ships the canonical light+dark pairing per palette (Catppuccin Latte+Mocha, Tokyo Night Day+Storm) and defers the mid-darks to a future milestone if there's demand. The mechanism doesn't preclude additional flavour presets — it just doesn't require them right now.
Integrated, not scoped. Every palette in this lineup ships with a canonical canvas tuned together with its foreground. They claim chrome accents + code surface + syntax, like Nord, not foreground-only, like niwaki. Niwaki remains the only scoped-archetype preset in the catalog.
Attribution is part of the deliverable. Every imported preset module carries an attribution header naming the original author(s), the source URL, and the licence (all six are MIT or equivalent). Every doc page carries the same attribution at the bottom.
No architectural changes. This spec is forbidden from modifying SyntaxTokens, TintDefinition, the scope-eligibility filter, the extended Shiki theme, or the generator. If a palette can't be expressed cleanly under the existing contract, the palette is deferred — not the contract revised.
The curated lineup
Six palettes chosen for distinctive visual identity, clean licensing, breadth of role-split exercise, and diversity of colour temperature. Each entry below names the rationale, the light/dark shape, and a sketch of the role mapping that the work item will refine.
Dracula
Purple/pink/cyan on near-black. Created by Zeno Rocha; one of the most installed editor themes on the planet. MIT licensed (https://github.com/dracula/dracula-theme).
- Shape: dark-canonical, integrated. No official light variant — would ship as dark-only (the preset's
modes.darkcarries the canonical values; light mode falls back to a darkened version or matches Dracula's "Alucard" sibling if author permits; otherwise we ship as a dark-only preset and note it). - Why include: peak name recognition. Splits
keyword(Pink),function(Green),type(Cyan),string(Yellow),constant(Purple) — exercises SPEC-056's extended roles at full fidelity.
Solarized
Light and dark, the same 16 hues from Ethan Schoonover's 2011 palette. MIT licensed (https://github.com/altercation/solarized).
- Shape: integrated, both modes from the same hue family (the only palette in the lineup designed deliberately around mode-symmetric canvases). Light =
base3canvas +base00text; dark =base03canvas +base0text. Same accents (yellow/orange/red/magenta/violet/blue/cyan/green) in both modes. - Why include: historical importance (most-cloned palette ever), and the test case for our "unified light+dark preset" shape. Validates that our
modes.darkmechanism handles palettes whose authors want the modes coordinated, not independent.
Catppuccin
Modern soft-pastel family, four flavours. Phase 1 ships Latte (light) + Mocha (dark) as the canonical pair. MIT licensed (https://github.com/catppuccin/catppuccin), unusually well-spec'd palette docs.
- Shape: integrated, light + dark from the same named-swatch family. Each flavour uses the same role assignments with different absolute values, so the role mapping written for Mocha translates structurally to Latte.
- Why include: currently very popular; well-documented spec means our mapping can be precise; demonstrates the "canonical pair only" multi-flavour decision. Frappé and Macchiato can be Phase 2 additions if there's demand.
Tokyo Night
Dark blue + magenta + cyan. Created by Enkia. MIT licensed (https://github.com/enkia/tokyo-night-vscode-theme). Phase 1 ships Storm (dark) + Day (light) as the canonical pair.
- Shape: integrated. Storm uses
#1a1b26canvas; Day uses#e1e2e7. Distinctive role splits —type(cyan) ≠function(blue) ≠parameter(orange) ≠keyword(magenta) ≠number(orange-distinct-from-constant). - Why include: exercises the SPEC-056 widened contract more thoroughly than any other lineup member. If Tokyo Night maps cleanly, the contract works for the maximally split-out palettes too.
One Dark
Atom's signature dark theme. Created by GitHub's Atom team. MIT licensed (https://github.com/atom/atom/tree/master/packages/one-dark-syntax). Phase 1 ships dark only — official "One Light" sibling deferred to Phase 2.
- Shape: dark-only initially. Blue-grey canvas (
#282c34), red/green/yellow/cyan/blue/purple accents. - Why include: fills the "modern blue-grey dark" niche distinct from Tokyo Night's saturation. Historical importance — Atom was the first widely-adopted editor with custom theme APIs.
Gruvbox
Warm retro palette. Created by Pavel Pertsev. MIT licensed (https://github.com/morhetz/gruvbox). Phase 1 ships dark medium + light medium as the canonical pair.
- Shape: integrated, light + dark variants from coordinated hues. Earthy: orange keywords, deep green functions, mustard strings, warm-red diagnostic colour.
- Why include: the only warm palette in the lineup. Five blue/cool members + one warm gives the catalog visual variety. Gruvbox is also the most "unix terminal heritage" of the lineup — important counterweight to the JS-editor-modern bias of Tokyo Night / Catppuccin.
Authoring Surface
No new authoring surface. Every preset uses the same opt-in mechanism Nord uses:
{
"site": {
"theme": {
"package": "@refrakt-md/lumina",
"presets": ["@refrakt-md/lumina/presets/dracula"]
}
}
}
And the same scoped-tint registration for showcase pages:
{
"tints": {
"dracula": { "extends": "@refrakt-md/lumina/presets/dracula" }
}
}
The refrakt docs site's tints config will gain entries for all six during this milestone so each preset's doc page can use tint="<name>" on palettes and the live preview codegroup.
Doc page structure
Every preset doc page follows the structure validated on Nord:
- Title + intro — palette identity, author, integrated-vs-scoped position, attribution sentence
- Opt in — config snippet
- The palette — two
{% palette %}blocks (light + dark, bothtint="<preset>") with named swatches - Live preview — the shared TypeScript+JSX snippet wrapped in
{% codegroup tint="<preset>" %} - Composing with X — a composition example with at least one other preset
- Attribution — full credit + licence
The TypeScript+JSX snippet stays identical across all preset pages so readers can directly compare palettes by toggling between pages without re-reading the example.
Out of scope
- Architectural changes — see "Design Principles" above. This spec is application work only.
- GitHub Light / Dark — universal recognition but optimised for "match what you see at github.com" rather than aesthetic choice. Phase 2 candidate.
- Monokai (original) — historical importance, but the modern incarnation (Monokai Pro) is paid and the brand is fragmented. Easier as Phase 2 once we've established the import pattern more broadly.
- Ayu, Rosé Pine, Kanagawa — strong palettes, but Phase 1 has to stop somewhere. Easy Phase 2 additions.
- Additional Catppuccin / Tokyo Night flavours — Frappé, Macchiato, Tokyo Night Moon. The mechanism doesn't preclude them; Phase 2 if there's demand.
- Per-preset Plugin packages — palettes stay inside
@refrakt-md/lumina/presets/for v1.x. Splitting into per-palette plugin packages is a packaging decision, not a content decision, and is out of scope here. - Light-only or dark-only mode locks per preset — the user chooses via
theme.colorScheme; presets ship both modes when the source palette has both. Nodracula-light/dracula-darksplit presets.
Validation
The spec is validated by all six preset doc pages rendering on the refrakt documentation site (whose active preset is niwaki) with their respective palettes correctly scoped to the palette swatches and code preview. Specifically:
- Each preset's palette swatches sit on the preset's canonical canvas (not niwaki's).
- Each preset's live code preview renders with the preset's syntax tokens and the preset's code-surface canvas.
- The surrounding page chrome (header, nav, prose) stays in niwaki on every preset doc page — no leakage.
- Each preset composes cleanly with tideline (
["tideline", "<preset>"]) — tideline typography survives, preset chrome + syntax wins. - Each preset module ships with attribution in the file header.
- The themes catalog landing page (
/themes/themes-catalog) lists all six in the "Syntax presets" group (renamed to "Imported syntax presets" if useful — see open question below). - No new tests fail; existing tests stay green. CSS coverage tests still pass for Lumina.
- The light-mode + dark-mode toggle on each preset doc page flips both the page chrome and the tinted regions correctly.
Acceptance Criteria
- Six preset modules added under
packages/lumina/src/presets/:dracula.ts,solarized.ts,catppuccin.ts,tokyo-night.ts,one-dark.ts,gruvbox.ts. - Each module exports a
ThemeTokensConfigwith chrome accents,color.code.*, andsyntax.*for both light (when applicable) and dark modes viamodes.dark. Single-mode palettes (Dracula, One Dark in Phase 1) ship dark-only and that's explicit in the file header. - Each module's file header includes attribution: author, source URL, licence.
packages/lumina/package.jsonexports each new preset path (./presets/dracula, etc.).- A test file per preset under
packages/lumina/test/mirroringnord-preset.test.ts— verifies structural shape, role-split assertions, code-surface canvas, CSS generation, and composition with tideline / niwaki / Lumina. - Six doc pages under
site/content/themes/:dracula.md,solarized.md,catppuccin.md,tokyo-night.md,one-dark.md,gruvbox.md. All follow the Nord-page structure. refrakt.config.jsonsites.main.tintsgains six new entries, each extending the corresponding preset module path.site/content/themes/_layout.md"Syntax presets" group lists all six (alongside niwaki and nord).site/content/themes/themes-catalog.mdupdated to mention the new presets and group them appropriately.- Full test suite passes. Site builds clean.
Open questions (defer to PR-time)
- Group naming — should
/themes/_layout.mdsplit "Syntax presets" into "refrakt's own" (niwaki) and "Imported" (nord + Phase 1 lineup)? Or leave one group with eight entries? My lean: one group at this size; revisit at 12+. - Theme catalog landing copy — the catalog page currently lists presets one-by-one. At eight syntax presets, a table or grid might read better than a flat list.
- Composition example variation — should every preset's "Composing with X" section pair with tideline (the same partner) or pick a different partner per preset (niwaki, neutral default, another import)? My lean: every preset gets one tideline example for consistency; one bonus composition pairing if it's interesting.
These don't affect the spec — they're authoring polish that the implementing PR can settle.
Nord preset module
Nord preset module
Author Lumina's Nord preset at packages/lumina/src/presets/nord.ts — an integrated palette preset that maps the official Nord palette's 16 named hues onto the extended SyntaxTokens contract and sets the code-surface (color.code.*) tokens so opting into Nord produces code blocks on Nord's canonical Polar Night canvas. Nord is the validation case for SPEC-056's three extensions: syntax role widening, optional code-surface claiming, and (via WORK-223) scoped tint projection.
Where niwaki took the scoped "foreground only" position deliberately, Nord takes the integrated "canvas + foreground" position because that's how Nord was designed — its hues were tuned against nord0 specifically, and rendering Nord foreground on Lumina's warm-cream bg misrepresents the palette.
- Priority
- high
- Complexity
- simple
- Milestone
- v0.14.1
- Source
- SPEC-056
Acceptance Criteria
packages/lumina/src/presets/nord.tsexports aThemeTokensConfigoverridingsyntax.*andcolor.code.*(no chrome bg/surface, no fonts, no status, no radius/spacing/shadow — those stay chrome's responsibility)- Built to
dist/presets/nord.js, exported at./presets/nordinpackages/lumina/package.json - Sets at least one extended optional syntax role distinctly from its core fallback (e.g.
type≠function) — proves the fidelity gain is real, not theoretical - Sets
color.code.bg,color.code.text, andcolor.code.borderfor at least one mode (dark) — proves the canvas-claiming path works end-to-end - Light-mode and dark-mode values both supplied via
modes.dark.*(Nord is dark-canonical but ships an Aurora-derived light variant — verify the light values against the Nord spec) - Each hue in the file references its official Nord swatch name in a comment (e.g.
// Frost nord7) for traceability against the upstream palette - File header includes attribution: "Derived from the Nord palette by Arctic Ice Studio & Sven Greb, MIT licensed. https://www.nordtheme.com/"
- A test site (or merge-pipeline test) with
presets: ["@refrakt-md/lumina/presets/nord"]against the neutral default renders code blocks with Nord colours on Nord's canvas — verifying both the syntax tokens and code-surface tokens cascade together - Composes with tideline —
["tideline", "nord"]renders tideline chrome with Nord syntax + Nord code surface. The rest of the page (body bg, surfaces, buttons, borders) stays in tideline; only the code surface flips to Nord - No CSS coverage regressions in
npx vitest run packages/lumina/test/css-coverage.test.ts
Approach
Read the official Nord palette spec (https://www.nordtheme.com/docs/colors-and-palettes) and Nord's own syntax highlighting documentation (which roles each Frost/Aurora hue is intended for, and which Polar Night/Snow Storm hues are intended for canvas/text). Then map onto refrakt's extended contract:
Rough starting mapping for syntax (verify against Nord's own syntax docs before committing):
| Refrakt role | Nord hue | Rationale |
|---|---|---|
| keyword | Frost nord9 | Nord's "keyword" colour |
| function | Frost nord8 | Nord's "method/function" colour |
| type | Frost nord7 | Nord's "class/type" colour — the role split that motivates SPEC-056 |
| string | Aurora nord14 | Nord's "string" colour |
| constant | Aurora nord15 | Nord's "constant/number" colour |
| comment | Polar Night nord3 | Nord's "comment" colour |
| punctuation | Snow Storm nord4 | Nord's "operator/punctuation" colour |
| variable | Snow Storm nord4 | Nord's default text |
| ... |
For code surface (dark mode):
| Refrakt token | Nord hue | Rationale |
|---|---|---|
color.code.bg | Polar Night nord0 (#2e3440) | Nord's canonical dark canvas |
color.code.text | Snow Storm nord4 (#d8dee9) | Nord's default text on dark canvas |
color.code.border | Polar Night nord1 (#3b4252) | Slightly lighter than canvas — Nord's UI panel separator |
For code surface (light mode):
| Refrakt token | Nord hue | Rationale |
|---|---|---|
color.code.bg | Snow Storm nord6 (#eceff4) | Nord's canonical light canvas |
color.code.text | Polar Night nord0 (#2e3440) | Nord's default text on light canvas |
color.code.border | Snow Storm nord5 (#e5e9f0) | Slightly darker than canvas |
If a syntax role refrakt cares about has no clear Nord intent (Nord doesn't separately spec tag or attribute, for example), leave it unset — the fallback chain from WORK-219 handles it.
The verification gate is: every distinct hue in Nord's spec lands on a distinct role in refrakt, or two hues collapse to the same role because Nord intends that. If the contract forces a collision Nord wouldn't have wanted, that's a SPEC-056 problem — escalate before working around it.
Dependencies
- WORK-217 — extended
SyntaxTokensshape must exist - WORK-219 — fallback chains so roles Nord doesn't set degrade gracefully
- WORK-218 — recommended (so colours actually show up on the new roles) but Nord's correctness as a preset module doesn't strictly depend on it
References
- SPEC-056 — "Validation" section names Nord as the validation case; "Code-surface (canvas) tokens" describes the canvas-claiming path
- Nord palette: https://www.nordtheme.com/docs/colors-and-palettes
packages/lumina/src/presets/niwaki.ts— reference for preset module shape and attribution comment style (note: niwaki is the scoped archetype; Nord is the integrated archetype)
Resolution
Completed: 2026-05-20
Branch: `claude/spec-056-milestone-v0-14-1`
What was done
- `packages/lumina/src/presets/nord.ts` (new) — Nord preset as a `ThemeTokensConfig` overriding `syntax.` and `color.code.` only. Both modes (light + dark) supplied. Every hue annotated with its official Nord swatch name (Polar Night nord0–3, Snow Storm nord4–6, Frost nord7–10, Aurora nord11–15) for traceability.
- `packages/lumina/package.json` — added `./presets/nord` export entry mirroring the niwaki/tideline pattern, so consumers can `import nord from '@refrakt-md/lumina/presets/nord'`.
- `packages/lumina/test/nord-preset.test.ts` (new) — 11 tests covering: structural shape (no chrome/fonts/structural keys), the `type` ≠ `function` fidelity-gain split, both modes' `color.code.*` canvas claims, CSS generation through the token-stylesheet generator, and composition with tideline / niwaki / Lumina via `mergeThemeTokensConfigs`. The "no chrome / typography / structural tokens" test enforces Nord's scope boundary at the generated-CSS layer — any future drift would fail the test.
Note on `color.code.border`
The work item's acceptance criterion mentioned `color.code.bg/text/border`, but the `TokenContract` defines `color.code` as `{ bg, text, 'inline-bg' }` — there is no `border` slot. The criterion is satisfied to the extent the contract allows: Nord sets `bg`, `text`, and `inline-bg` for both modes. If we need a code-surface border token, that's a separate SPEC-048 contract amendment.
Nord palette → role mapping highlights
The mapping follows Nord's own syntax-highlighting reference. SPEC-056's role-widening lets these distinctions land cleanly:
- type ≠ function (Frost nord7 `#8fbcbb` vs Frost nord8 `#88c0d0`) — the motivating SPEC-056 split.
- tag ≠ keyword (Frost nord10 `#5e81ac` vs Frost nord9 `#81a1c1`) — Nord renders HTML tags as deep Frost, distinct from keywords.
- attribute ≠ function (Frost nord7 vs Frost nord8) — attributes align with the type-family.
- operator ≠ punctuation (Frost nord9 `#81a1c1` vs Polar Night nord3 `#4c566a`) — operators read as keyword-family.
- number ≠ constant (Aurora nord12 orange `#d08770` vs Aurora nord15 purple `#b48ead`) — Nord splits numbers from booleans/null/symbols.
- regex ≠ string (Aurora nord13 yellow `#ebcb8b` vs Aurora nord14 green `#a3be8c`) — regex gets its own dedicated hue.
Roles Nord doesn't separately spec (`property`, `parameter`, `link`, `string-expression`) are left unset; they cascade via the fallback chain emitted by the token-stylesheet generator (WORK-219).
Test results
- `npx vitest run packages/lumina/test/nord-preset.test.ts` — 11/11 pass.
- `npx vitest run packages/lumina/` — all lumina tests pass.
- Full suite `npm test` — 2520/2520 pass across 205 test files.
Files touched
- `packages/lumina/src/presets/nord.ts` (new)
- `packages/lumina/package.json` (added export entry)
- `packages/lumina/test/nord-preset.test.ts` (new, 11 tests)