Skip to main content

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) plus display(_:) for the Instrument Serif face and code(_:) re-exporting JetBrains Mono. A size or weight change for a role happens once.
  • Space — a 4-pt spacing scale (xxs 2 → xxl 32). Round to the nearest step when adopting; keep a literal where nothing fits rather than bending the layout.
  • Radius — corner radii (sm 3 for inline code chips, md 8 for cards, lg 12 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.

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_SNAPSHOT PNG gallery — Gallery.sheet() renders every story on one sheet as components.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.