Skip to content

feat(roots): iOS/Swift token export + thoughtstream brand (River Mist)#44

Merged
mattmaynes merged 2 commits into
mainfrom
feat/roots-ios-swift-tokens
Jul 13, 2026
Merged

feat(roots): iOS/Swift token export + thoughtstream brand (River Mist)#44
mattmaynes merged 2 commits into
mainfrom
feat/roots-ios-swift-tokens

Conversation

@mattmaynes

Copy link
Copy Markdown
Contributor

Adds two things to @rogueoak/roots, per spec 0032:

1. thoughtstream brand (River Mist)

The first REAL Canopy brand (vs the sunset demo), authored as DTCG token files at
packages/roots/examples/thoughtstream/, mirroring examples/sunset/ structure and role set.

River Mist is a calm slate-teal water palette:

  • slate (primary): slate-600 #42666f light / slate-300 #8fb6bb dark
  • mist (neutral): mist-50 #f3f6f6 (bg light), mist-900 #1e3238 (text light), mist-950 #0e1618 (bg dark)
  • current (secondary teal), tide (accent aqua), plus cool-tuned status ramps

It maps all 39 Canopy semantic roles in light + dark and builds AA-clean through
buildBrand(): every role/state pair meets WCAG AA in both themes, each dark override differs from
its light value. (The surface primitive is named canvas to dodge a DTCG group/token path collision
with the color.surface role.)

2. iOS / Swift token export

swift.mjs / buildSwift() (exported at @rogueoak/roots/swift, bin roots-swift) generates one
Tokens.swift for a brand from the same config roots-brand reads - the native Swift target the
architecture always anticipated ("just another Style Dictionary platform - no token rewrite"). It
reuses the core transforms rather than re-resolving tokens by hand:

  • Colours: two throwaway Style Dictionary instances resolve the brand's light + dark semantics to
    concrete hex (outputReferences: false), emitted as one Color(light:dark:) per role that adapts
    to the color scheme via a generated dynamic-UIColor initializer.
  • Non-colours: a third instance resolves Canopy's OWN spacing / radius / type scale (shared
    across brands) to CGFloat points + Font helpers. rem -> points at 16pt/rem; a raw px pill
    (radius.full) is kept as-is.

Output is three caseless enums (CanopyColor / CanopySpacing / CanopyRadius / CanopyFont - the
idiomatic Swift namespace with no instances) with a "Do not edit - generated by Style Dictionary"
header. Generated on demand (not part of the web pnpm build), landing at
dist/<brand>/Tokens.swift.

Regenerate: npx roots-swift examples/thoughtstream/brand.config.json

Text("Thought Stream")
    .font(CanopyFont.sizeX2xlFont())
    .foregroundStyle(CanopyColor.text)
    .padding(CanopySpacing.x4)                 // 16pt
    .clipShape(RoundedRectangle(cornerRadius: CanopyRadius.md))  // 8pt

Verification

  • node cli.mjs examples/thoughtstream/brand.config.json builds AA-clean (39/39 roles mapped).
  • swiftc -typecheck passes on the generated Tokens.swift (real, compilable SwiftUI).
  • Vitest: thoughtstream builds AA-clean; the Swift emitter's structure is asserted (header, primary
    light+dark hex, spacing/radius/font constants, CLI). pnpm test + pnpm lint green in roots, and
    the full monorepo pnpm build / test / lint are green.
  • README + docs/overview/ (features, architecture) updated to match.

🤖 Generated with Claude Code

mattmaynes and others added 2 commits July 12, 2026 17:52
Author the first real Canopy brand (vs the sunset demo) as DTCG token files,
mirroring examples/sunset exactly: primitive ramps (slate primary, current
secondary, tide accent, mist neutral, plus cool-tuned status ramps), light and
dark semantic mappings over Canopy's role names, brand.config.json, README.

River Mist is a calm slate-teal water palette. Anchors: primary slate.600
#42666f (light) / slate.300 #8fb6bb (dark); neutral mist.50 #f3f6f6 (bg light),
mist.900 #1e3238 (text light), mist.950 #0e1618 (bg dark). It builds AA-clean
through buildBrand() - all 39 roles mapped, every role/state pair meeting WCAG
AA in light and dark, each dark override distinct from its light value.

The surface primitive is named `canvas` (not `surface`) to avoid a DTCG
group/token path collision with the `color.surface` semantic role.

Adds spec 0032.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LV2MquNjUZebAS8S8z4mnR
Add a code-generated Swift emitter so a native SwiftUI app consumes Canopy
tokens with no re-authoring - the Swift target the architecture always
anticipated ("just another Style Dictionary platform - no token rewrite").

swift.mjs / buildSwift() (exported at @rogueoak/roots/swift, bin roots-swift)
generates one Tokens.swift for a brand from the same config roots-brand reads:

- Colours: two throwaway Style Dictionary instances resolve the brand's light
  and dark semantics to concrete hex (outputReferences:false), emitted as one
  Color(light:dark:) per role that adapts to the color scheme via a generated
  dynamic-UIColor initializer.
- Non-colours: a third instance resolves Canopy's OWN spacing / radius / type
  scale (shared across brands) to CGFloat points + Font helpers. rem converts
  to points at 16pt/rem; a raw px pill (radius.full) is kept as-is.

Output is three caseless enums (CanopyColor / CanopySpacing / CanopyRadius /
CanopyFont - the idiomatic Swift namespace with no instances) with a
do-not-edit generated header. Generated on demand (not part of the web build),
landing at dist/<brand>/Tokens.swift. Verified: swiftc -typecheck passes.

Tests assert the thoughtstream brand builds AA-clean and the emitter's
structure (header, a role's light+dark hex, spacing/radius/font constants,
CLI). Adds the tokens/ dir to the package files (swift.mjs reads them at
runtime), documents the export in the README, and updates the overview docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LV2MquNjUZebAS8S8z4mnR
@mattmaynes mattmaynes merged commit e6d0dc4 into main Jul 13, 2026
1 check passed
@mattmaynes mattmaynes deleted the feat/roots-ios-swift-tokens branch July 13, 2026 00:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant