ADR-0013 — Helios design system strategy
**Status.** Accepted.
Status. Accepted. Date. 2026-05-19. Supersedes. Partially supersedes ADR-0008 (frontend design system) by adding a shared-token layer above it; ADR-0008's pnpm + Tailwind decisions still stand. Builds on. ADR-0012 (docs platform) — formalises the Phase-2 Fumadocs migration as the target documentation runtime.
Context
Five surfaces compete for visual coherence:
- Telegram bot — text + inline keyboards (HTML rendered by Telegram).
- Mini App — React 18 inside Telegram WebApp.
- Portal — public web at
helios.mom, OIDC login, marketing. - Docs — currently mkdocs-material with a heavy CSS override at
docs.helios.mom. - Owner / admin panel — embedded into the Mini App today; eventually a separate surface.
Without a shared source of truth, each surface drifts. We have already seen:
- The bot uses
◉ ◇ ⌬symbols that nobody picked deliberately; they arrived in templates ad-hoc. - The Mini App declares its own colour palette under
apps/miniapp/src/theme/tokens.ts. - The docs override declares another palette under
docs/stylesheets/helios.css. - The portal shares the Mini App bundle but renders a different layout with no formal handoff.
Each is internally consistent but they don't agree with each other.
The product is launch-ready (phase 70). Drift will compound from here.
Decision
Introduce shared-design/ as the single source of truth for design
tokens, iconography, and bot emoji contract. Every surface consumes it.
Architecture
shared-design/
├── README.md — how to consume the package
├── MASTER.md — the rules (philosophy, recipes, anti-patterns)
├── BOT_EMOJI.md — bot-specific contract (custom-emoji IDs + fallbacks)
├── tokens.ts — typed tokens; imported into every TS surface
├── tokens.css — same tokens emitted as CSS custom properties
├── tailwind-preset.ts — Tailwind theme mapping (future, post-Fumadocs spike)
└── icons/ — curated Iconify SVGs (Lucide outline + Phosphor filled)
How each surface consumes
| Surface | Mechanism |
|---|---|
| Mini App / Portal | apps/miniapp/src/theme/tokens.ts re-exports from shared-design/tokens.ts; apps/miniapp/index.html loads shared-design/tokens.css before any other styles. |
| Fumadocs (apps/docs-site, Phase 2 spike, Phase 3 cutover) | next.config.ts aliases @helios/tokens to shared-design/; app/layout.tsx imports tokens.css. |
| mkdocs (transitional) | docs/stylesheets/helios.css symlinks tokens.css and only uses tokens; the surface-specific glass overrides remain in helios.css. |
| Telegram bot | reads emoji-pack IDs from core.branding.bot_emoji_pack(), which resolves from core/settings/parameters.py (single key: ui.symbol_pack). Bot has no CSS; the contract is the symbol-set + mapping. |
Versioning
v0.1 ships with this ADR. Tokens follow append-only-safe semver:
- Patch — colour tweak inside an existing token (re-shade brand-500 by 1 %).
- Minor — add a new token (introduce
radius-2xl). - Major — rename or remove a token; requires migration notes in
shared-design/CHANGELOG.mdand a per-surface checklist.
Decisions, in detail
D1. Tokens are colocated, not packaged
shared-design/ lives at the repo root, not under packages/. It is
not a published npm package. Rationale:
- We are a monorepo with a single deployment unit. Publishing tokens to npm would add a release ceremony for zero benefit.
- TS imports use a path alias (
@helios/tokens) configured per app. - CSS imports happen at the document level — one
<link>, no bundler trickery.
If we ever decide to open-source the design system as a standalone package, the existing directory layout is a no-cost port.
D2. Tailwind preset, but Tailwind is not mandatory
tailwind-preset.ts is provided so any future Tailwind app gets the
tokens for free. mkdocs and the bot do not run Tailwind — they
consume tokens.css and the emoji pack respectively. The system is
framework-agnostic by design.
D3. Dark mode is the canonical theme
The default :root block in tokens.css declares the light values
because that's the CSS convention (most projects ship light-first). But
every surface sets data-theme="dark" on <html> by default. The user
can toggle to light; Mini App syncs with Telegram.WebApp.colorScheme.
Why dark-first as canonical:
- Operator audience overlaps heavily with terminal users.
- Glass surfaces look substantively richer on dark (light glass = ghostly).
- Brand neighbours (Linear, Vercel, Raycast, GitHub) ship dark-first.
D4. No emoji glyphs on the web; bot uses custom-emoji IDs
Hard rule. Every icon on a web surface is an SVG from
shared-design/icons/. Source set: Lucide outline (default), Phosphor
filled (states / badges). One stroke width across the product.
The bot is the only place emoji appear, and only via two channels:
- Custom-emoji mode —
<tg-emoji emoji-id="…">FALLBACK</tg-emoji>. The Telegram client renders the operator's owned emoji (e.g. uploaded to a Premium emoji pack). If the client is not Premium the fallback ASCII glyph renders. - Plain-symbol mode — just the ASCII glyph from
ui.symbol_pack(◉ ◇ ⌬ ▸ └ …). Used when the client has no Premium subscription and no custom-emoji mapping is configured.
Resolution chain: per-client override → tier default → global default. Configurable at runtime via the parameters registry — no redeploy.
D5. Fumadocs is the documentation target
ADR-0012 deferred this. We commit to it now:
- The mkdocs site at
docs.helios.momis transitional. It works today (phase 70 ships an iOS-style glass override). - Phase 2 (this ADR): spike Fumadocs in
apps/docs-site/. Render the homepage + one nested page + the ADR index. Validate the visual parity with the mkdocs version. Commit the spike. - Phase 3 (next sprint): port all
.mdto.mdx. Replacemkdocs buildwithpnpm build(Next.js static export). Repoint nginx root fordocs.helios.mom. Delete mkdocs config + dependencies.
Why Fumadocs specifically: ADR-0012 §"Why Fumadocs specifically" — the short version is MDX + Tailwind + shadcn + Orama search out of the box, which means we share the stack with the rest of the front-end.
D6. Telegram bot UI gets a real spec
The bot was the only surface without a design spec. BOT_EMOJI.md
formalises the missing layer:
- Three "shapes" of message (menu, profile, notification).
- Tree-style listings ≤ 3 levels.
- One leading symbol per row, never per button.
- Pack-driven; tier-aware; client-overridable.
Implementation lands in a follow-up commit: see task #75 in the project tracker.
Consequences
Positive
- Drift stops compounding. Tokens are the diff surface, not 50 scattered hex literals.
- Future surfaces (CLI, status page, custom dashboards) cost almost
nothing to launch — they
import shared-design/tokensand inherit brand parity. - The mkdocs → Fumadocs migration has a clear destination instead of an open-ended "we should switch eventually".
- The bot stops looking like an afterthought.
Negative
- One more directory in the repo root.
- Tokens require discipline to maintain — adding a colour should always
go through
shared-design/tokens.ts, not a one-off inapps/miniapp/src/some-component.tsx. A pre-commit lint rule should refuse raw hex literals in component files (follow-up; seescripts/no_raw_hex_check.pyin the backlog). - Brief duplicated work during the mkdocs → Fumadocs window: docs
authors edit
.mdfiles but the future build path is.mdx. We accept this for one sprint.
Alternatives considered
- Style-Dictionary — generates platform-specific outputs from a
single JSON. Powerful but premature for our scope; the file we
generate (
tokens.css) is ~150 lines and writing it by hand is faster than tooling it. - Vanilla-extract — typed CSS-in-TS. Strong but couples us to a specific bundler; mkdocs would still need a static CSS export anyway.
- Linaria — same family, same reason against.
- CSS-only (no
tokens.ts) — would force React surfaces to consume tokens by string literal (var(--helios-…)) only, losing type-checking. Rejected: TypeScript catches typos.
Rollout
- This commit —
shared-design/package, ADR-0013, MASTER.md, tokens.ts, tokens.css, icon set, BOT_EMOJI.md. - Next commit — apply tokens to
apps/miniapp/src/theme/tokens.tsanddocs/stylesheets/helios.css. Zero visual regression intended — token values match the existing palette. - Spike commit —
apps/docs-site/with one or two pages running on Fumadocs. - Bot UI commit — emoji pack expansion, parameter wiring, template
refactor. Visible in
@HeliosWatchbotwithin a sprint. - Full migration commit — Fumadocs replaces mkdocs at
docs.helios.mom. mkdocs config removed.
Verification
- Lint rule (
scripts/no_raw_hex_check.py, follow-up) refuses#[0-9a-fA-F]{3,8}inapps/,docs/,core/branding.pyoutside the test fixtures. Tokens must be imported. - Existing test suite stays green (no behavioural change in v0.1).
make docsbuilds cleanly with the new CSS.- Mini App
pnpm buildsucceeds with the new tokens import.