The design system
kiln's UI grew a real palette early — Theme.swift's adaptive paper/espresso
faces — but everything else was inline magic numbers: font sizes from 9 to 14
repeated across hundreds of call sites, ad-hoc paddings, corner radii written as
bare 3 and 8. This page covers the move toward naming those values, a shared
component family, and screenshot tests that guard how the components look.
The work is deliberately additive. The raw .font(.system(size:)) calls and
literal paddings still compile, so views adopt the scale at their own pace — the
same low-churn philosophy .swiftformat follows.
Tokens
Three scales live in Sources/kiln/DesignSystem/Tokens.swift, next to (not
replacing) Theme's colours:
Typography— semantic type roles (label,caption,footnote,callout,body,headline) plusdisplay(_:)for the Instrument Serif face andcode(_:)re-exporting JetBrains Mono. A size or weight change for a role happens once.Space— a 4-pt spacing scale (xxs2 →xxl32). Round to the nearest step when adopting; keep a literal where nothing fits rather than bending the layout.Radius— corner radii (sm3 for inline code chips,md8 for cards,lg12 for popovers).
Colours stay in Theme. Tokens and palette are the two halves of the same
language.
Themes
Theme ships a family of named palettes rather than one fixed pair of faces.
Each theme is a Palette (Sources/kiln/DesignSystem/Palette.swift) — the full
set of role colours, one hex per token, plus the syntax-highlighting hues — and
the app picks one light theme and one dark theme:
- Light: Cream (the original warm paper) and Kiln (its eponymous twin), Cloud (a cool monochrome white), VS Code (Light+), Lime White (a green-tinged white), Blush (a pink tinge), Claudius (Claude's brand — ivory paper, slate ink, a coral accent), and Porcelain (hand-painted delftware — cobalt blue ink and accents on speckled cream stoneware).
- Dark: Coffee (the original espresso) and Kiln Dark (its eponymous twin), Terracotta (warm clay), Gunmetal (a near-monochrome graphite, the Cursor-style black), Navy (deep blue), Willow (a muted forest), VS Code (Dark+), Claudius (Claude's brand — a near-black cinder canvas, ivory ink, the signature Claude coral), and Porcelain (a delftware night — cream ink and a cobalt glow on a deep blue-black canvas).
Both are chosen in Settings ▸ General. The mechanism keeps the single-seam
design intact: every Theme.xxx token is a dynamic colour whose closure looks
up the active palette in ThemeStore at resolve time, so all ~700 call sites
(and the highlighter, via TokenKind) recolour without changes. Tokens are
computed properties, not stored lets, so a SwiftUI body re-read sees a theme
switch; ThemeStore.refresh() nudges each window's appearance after a change so
AppKit surfaces re-resolve too, without tearing down view state. Adding a theme
is one Palette and one enum case.
The pill family
The editor's marginalia chips — story chips, the imports and collapse folds —
all shared one recipe: a faint tint fill inside a capsule, ringed by a slightly
stronger tint hairline. That recipe was copy-pasted into each chip. It now lives
once, in Sources/kiln/DesignSystem/Pill.swift:
.pill(_ tint:fill:stroke:padding:)— the view modifier the existing chips call, so the whole family tunes from one place.Pill { … }— a standalone tinted pill for new affordances and the gallery.
StoryChipView, ImportsChip, and CollapseChip are migrated as the worked
examples; pixels are unchanged.
The component gallery
Sources/kiln/DesignSystem/Gallery.swift is kiln's "storybook" (#86): a
registry of named, pure, self-contained component examples (ComponentStory).
Because the examples carry no environment or live state, the same registry feeds
three surfaces:
- the
KILN_SNAPSHOTPNG gallery —Gallery.sheet()renders every story on one sheet ascomponents.png; - the screenshot tests below;
- an in-app catalog surface, planned as a follow-up (#86).
A new component is documented and regression-tested in one edit: add a story.
Screenshot tests
Tests/kilnTests/ComponentSnapshotTests.swift renders each gallery story to a
reference PNG under both faces (paper and espresso) and diffs it on every run,
via swift-snapshot-testing
through its Swift Testing integration. An accidental restyle of a shared
component then shows up as a failing image diff.
Rendering goes through a real, appearance-tagged offscreen NSHostingView — the
technique Snapshot.swift already uses — so the adaptive Theme NSColors
resolve against a live AppKit appearance before capture.
The suite is off by default (these tests are macOS-only, and reference images
don't exist until recorded on a Mac). Opt in with KILN_SNAPSHOT_TESTS:
# First time — record references, then commit the PNGs:
SNAPSHOT_TESTING_RECORD=all KILN_SNAPSHOT_TESTS=1 swift test \
--filter ComponentSnapshotTests
# Thereafter — guard the diff:
KILN_SNAPSHOT_TESTS=1 swift test
The token scales themselves are covered by ordinary unit tests in
DesignSystemTests.swift, which run in every swift test — they pin the scale
values and ordering and check the gallery stays a well-formed registry.