Storybook — the live component library
Скрин запущенного Storybook (npm run storybook в Angular-репо): слева список компонентов, справа один компонент с его вариантами/контролами. 1 кадр достаточно; если хочешь — второй с токенами/цветами. Лучшее визуальное доказательство, что система работает в коде.
BIGVU runs on one design system across four platforms — Figma, web (Angular), iOS (SwiftUI), and Android. I owned it as the single source of truth: the token architecture, the component APIs, and the code that keeps the platforms in sync.
This was a code job, not just a Figma library. I worked in the platform repos — defining tokens, shaping component APIs, and writing component code so design and product don't drift apart.
bg-action-strong- Figma
- Backgrounds (Surfaces)/Primary Action/bg-action-strong
- Web
- var(--bg-action-strong)
- iOS
- BigvuColorsSemantic.bgActionStrong
- Androidmigrating
- @color/bg_action_strong
Before: four platforms drifting apart
The system existed to fix problems that compound on every multi-platform product:
- Design and code had drifted — the source of truth was unclear.
- Each platform team named the same token differently.
- New components were rebuilt by hand, a little different every time.
- Light/dark and cross-platform parity were hard to verify.
A token system, not just a page of styles
The core idea: match the structure to each kind of token. Each token type gets the number of layers it actually needs — no rigid three-layer model forced onto everything.
| Category | Layers | Why |
|---|---|---|
| Colour | 2 | primitives → semantic — the only category that shifts with light / dark |
| Spacing | 1 | one flat pixel-named scale |
| Radius | 1 | small role-named set |
| Typography | 1 | role-named scale |
| Alpha | 0 | composed inline from RGB fragments — not a token |
None of this lives only in people's heads. DESIGN.md is the single contract all four platforms follow — it sets out the token schema, how a token maps onto each platform, and the bad habits that keep the set from growing out of control.
And it's more than a document — the rules run as commands:
/new-token— adds a token in the right category and checks the name against the rules./audit-tokens— flags tokens used zero or one time, so they can be removed./check-drift— diffs the Figma exports against web, iOS, and Android; fails CI when they disagree./apply-tokens— applies semantic tokens to an unstyled Figma frame via the Figma MCP.
Colours get a semantic layer because the same role maps to different values in light and dark. Spacing, radius, and type don't — they stay flat. Alpha isn't a token at all. Every semantic token names a role, never a value — bg-action-strong, not blue-500— and components reference that layer directly, so there's no third per-component layer to maintain.
// _colors-semantic.scss — semantic names, never raw values
$semantic-light: (
'bg-action-strong': color('blue-500'),
'bg-action-strong-hover': color('blue-300'),
'bg-action-strong-active': color('blue-400'),
'bg-action-subtle': color('blue-50'),
'bg-action-critical': color('red-500'),
'border-default': color('gray-200'),
// …
);One source, four platforms
The Figma variables are the source of truth, exported as .tokens.json. A generator reads that export and writes each platform's native file — CSS custom properties on the web, a typed enum on iOS, XML resources on Android. Nobody hand-copies values.
// Spacing/Default.tokens.json — exported from Figma variables
{
"space-0": { "$type": "number", "$value": 0 },
"space-1": { "$type": "number", "$value": 4 },
"space-2": { "$type": "number", "$value": 8 },
// …
}// ColorsSemantic.swift — the iOS result, light/dark built in
public enum BigvuColorsSemantic {
public static var bgActionStrong: Color {
Color.adaptive(
light: BigvuColors.blue500,
dark: BigvuColors.blue600
)
}
public static var bgActionStrongHover: Color {
BigvuColors.blue300
}
}Terminal — generator & drift check
Скрин терминала: прогон генератора (строки «Generated …/button-colors.scss») и/или вывод check-drift. Снимается за минуту — конкретный пруф пайплайна и CI-дисциплины.
Anatomy of one component
Every component is built to the same recipe. Watch the button come together — one token category at a time, with the reason for each.
Start with dimensions — pulled from the size scale, not magic numbers. The md button is 40px tall, 12px padding, 10px radius. Every control on the same scale lines up.
// sizes.ts — dimensions from the shared scale
buttonSizes.md = {
height: '40px',
paddingX: '12px',
radius: '10px',
}Add type from the shared role scale: Inter, 16px, weight 600. The label now reads like the rest of the product.
// typography.ts — type from the role-named scale
buttonTypography.md = {
fontFamily: 'Inter, sans-serif',
fontWeight: 600,
fontSize: '16px',
lineHeight: 1.4,
}Colour by role, not value. background → bg-action-strong, text → fg-on-color. Naming the role (not “blue-500”) is what lets one token resolve correctly in light and dark.
// button-colors.scss — colour by role
'primary-bg': 'bg-action-strong', // → blue-500
'primary-fg': 'fg-on-color', // → whiteStates are part of the contract — and token-driven too. Hover and active step along the blue ramp; disabled swaps to the neutral surface. Hover the button.
// button-colors.scss — states, also tokens
'primary-bg-hover': 'bg-action-strong-hover', // blue-300
'primary-bg-active': 'bg-action-strong-active', // blue-400
'bg-disabled': 'bg-disabled', // gray-100Finally, variants. One component, four roles — each maps to its own semantic tokens. Same structure and states; only the colour roles change. Switch styles above.
// button.ts — one component, four roles
export type ButtonStyle =
'primary' | 'secondary' | 'tertiary' | 'ghost';Live previews re-created in React from the real BIGVU tokens. The production component is the Angular one below.
That's the whole component: a typed API, every dimension and colour from a token, states and variants baked in. Assembled in code, it's just this:
// button.ts — typed API, styling driven by tokens
export type ButtonSize = 'xl' | 'lg' | 'md' | 'sm' | 'xs';
export type ButtonStyle = 'primary' | 'secondary' | 'tertiary' | 'ghost';
export class Button {
@Input() size: ButtonSize = 'md';
@Input() variant: ButtonStyle = 'primary';
@Input() disabled = false;
@Input() loading = false;
@HostBinding('style')
get hostStyles(): Record<string, string> {
const sizeToken = buttonSizes[this.size];
return {
'--btn-height': sizeToken.height,
'--btn-padding-x': sizeToken.paddingX,
'--btn-radius': sizeToken.radius,
'--btn-icon-size': sizeToken.iconSize,
};
}
}And every one of the 40 components follows that same recipe. Same structure, four platforms.
bigvu-ui-angular/
src/lib/
tokens/
Light.tokens.json // Figma export — source of truth
_colors.scss // primitives
_colors-semantic.scss // semantic (light + dark)
sizes.ts typography.ts
components/ // 40 components
button/ input/ modal/ dropdown/ badge/ …
scripts/
generate-tokens.js // export → native token filesOne component, every platform
Один компонент (напр. Button) рядом на платформах: web (Storybook) + iOS (Example в симуляторе) + Android (эмулятор) — выглядят одинаково. Главный пруф «одна система, четыре платформы». Если симулятор/эмулятор недоступны — хотя бы web + iOS.
Built to be extended — by people and agents
Forty components across four platforms only stay consistent if the rules are enforced, not just remembered. So I gave each repo rules the tools can read and check automatically — and an AI agent can extend the system without breaking it.
- Web — a
/new-componentClaude Code command (and aCLAUDE.md): scaffold a component from a Figma URL, pulling the design via the Figma MCP and matching the existing token conventions. - iOS — Cursor rules (
SwiftUI_Rules.md) pin MVVM and Clean Architecture so generated views fit the codebase. - Android — a
firebender.jsonbinds the architecture rules to every.ktfile the agent touches.
The payoff: a new component scaffolds in minutes, already on-spec — typed API, tokens wired, states in place — instead of a hand-built one-off that drifts.
// firebender.json — AI agent rules for the Android repo
{
"rules": [
"Write clear, concise code comments",
{
"filePathMatches": "**/*.kt",
"rulesPaths": "firebender_docs/firebenderArchitectureRules.md"
}
]
}Scaffolding a component with the agent
Запись экрана (15–30 сек): запускаешь /new-component с Figma-ссылкой → агент читает дизайн через Figma MCP, изучает соседние компоненты и генерит файлы по конвенциям. Видно, как система расширяется «на спеке». Если видео тяжело — скрин редактора с командой и созданными файлами.
Honest about the edges
Not everything was finished — and the case shouldn't pretend it was. Android still runs a flat legacy colour list with no semantic layer; the plan, documented in the project DESIGN.md, is to regenerate light (values/) and dark (values-night/) from the same Figma exports the other platforms already consume.
And tokens have to earn their place:
- A token exists only if it is used in more than one place.
- No synonyms, no -2 suffixes, no per-component aliases that just forward a semantic token.
- No token ships without a consumer — unused tokens rot.
The code
The system spans four repos — the Figma variable exports plus the Angular, iOS, and Android libraries. The Angular library ships a Storybook — the fastest way to see it running as real, interactive components.
Storybook walkthrough
Короткая запись (8–20 сек, луп): скролл списка компонентов, открыть один, потыкать варианты/контролы (knobs), переключить light/dark. Живая интерактивная система — самый сильный визуал кейса.
Light / dark parity (по желанию)
Один экран Storybook в светлой и тёмной теме рядом — пруф семантических токенов. Не обязательно: тёмная тема уже видна в блоках кода.
What it adds up to
One design language, 40 components, four platforms kept in sync from a single source — the system I owned end to end. Beyond the architecture, it:
- Less guesswork at handoff — designers and engineers point at the same token.
- Gave every platform one shared set of names instead of four.
- Made token drift visible — and blocked — in CI.
- Made building a new component repeatable instead of a one-off.