Helios

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:

  1. Telegram bot — text + inline keyboards (HTML rendered by Telegram).
  2. Mini App — React 18 inside Telegram WebApp.
  3. Portal — public web at helios.mom, OIDC login, marketing.
  4. Docs — currently mkdocs-material with a heavy CSS override at docs.helios.mom.
  5. 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

SurfaceMechanism
Mini App / Portalapps/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 botreads 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.md and 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:

  1. 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.
  2. 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.mom is 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 .md to .mdx. Replace mkdocs build with pnpm build (Next.js static export). Repoint nginx root for docs.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/tokens and 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 in apps/miniapp/src/some-component.tsx. A pre-commit lint rule should refuse raw hex literals in component files (follow-up; see scripts/no_raw_hex_check.py in the backlog).
  • Brief duplicated work during the mkdocs → Fumadocs window: docs authors edit .md files 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

  1. This commitshared-design/ package, ADR-0013, MASTER.md, tokens.ts, tokens.css, icon set, BOT_EMOJI.md.
  2. Next commit — apply tokens to apps/miniapp/src/theme/tokens.ts and docs/stylesheets/helios.css. Zero visual regression intended — token values match the existing palette.
  3. Spike commitapps/docs-site/ with one or two pages running on Fumadocs.
  4. Bot UI commit — emoji pack expansion, parameter wiring, template refactor. Visible in @HeliosWatchbot within a sprint.
  5. 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} in apps/, docs/, core/branding.py outside the test fixtures. Tokens must be imported.
  • Existing test suite stays green (no behavioural change in v0.1).
  • make docs builds cleanly with the new CSS.
  • Mini App pnpm build succeeds with the new tokens import.