Oter — Design System Tokens
Status: Draft for reconciliation · Last updated: 2026-06-03 Goal of this doc: consolidate every design token across our three sources so we can agree on the final token set the codebase will be built on. Where the sources disagree or a token is missing, it’s flagged as a DECISION below.
0. Centralization sweep — mapping table & progress (started 2026-06-04)
Goal: make Oter.* the single color system across all platforms. Two parallel
systems exist today: the new Oter.* tokens (good, ~4.4k usages) and a legacy
OterDesignSystem / OterColors / OterGradients (ui/Color.kt, ui/DesignSystem.kt).
Canonical replacement map (every sweep edit must follow this — no guessing):
| Legacy / hardcoded | Token (composable ctx) | Token (static ctx) |
|---|---|---|
OterColors.Primary, PrimaryColor, #6366F1 | Oter.colors.primary | DarkOterColors.primary |
OterColors.PrimaryDark, PrimaryVariantColor, #4F46E5 | Oter.colors.primaryHover | DarkOterColors.primaryHover |
OterColors.Secondary/PrimaryLight, #818CF8 | Oter.colors.primaryLight | DarkOterColors.primaryLight |
OterColors.Background, #0F172A | Oter.colors.background | DarkOterColors.background |
OterColors.Surface, #1E293B | Oter.colors.surface | DarkOterColors.surface |
OterColors.SurfaceVariant, #334155 | Oter.colors.surfaceHover | DarkOterColors.surfaceHover |
OterColors.OnBackground/OnSurface, #F8FAFC | Oter.colors.textPrimary | DarkOterColors.textPrimary |
OterColors.OnSurfaceVariant, #94A3B8 | Oter.colors.textSecondary | DarkOterColors.textSecondary |
OterColors.OnSurfaceDisabled, #64748B | Oter.colors.textTertiary | DarkOterColors.textTertiary |
OterColors.Border/Divider, #334155 | Oter.colors.border | DarkOterColors.border |
OterColors.BorderLight, #475569 | Oter.colors.borderLight | DarkOterColors.borderLight |
OterColors.Danger/Error/DangerColor, #EF4444 | Oter.colors.danger | DarkOterColors.danger |
OterColors.Warning/WarningColor, #F59E0B | Oter.colors.warning | DarkOterColors.warning |
OterColors.Info/InfoColor, #3B82F6 | Oter.colors.info | DarkOterColors.info |
OterColors.Success/SuccessColor, #22C55E | Oter.colors.success | DarkOterColors.success |
MaterialTheme.colors.primary | Oter.colors.primary | — |
MaterialTheme.colors.primaryVariant | Oter.colors.primaryHover | — |
MaterialTheme.colors.secondary | Oter.colors.primaryLight | — |
MaterialTheme.colors.background | Oter.colors.background | — |
MaterialTheme.colors.surface | Oter.colors.surface | — |
MaterialTheme.colors.error | Oter.colors.danger | — |
MaterialTheme.colors.onBackground/onSurface | Oter.colors.textPrimary | — |
Rules: (1) only replace a hardcoded Color(0x..) when its hex exactly matches a
token above — leave domain/macro colors (FatColor, CarbColor, category hexes that
aren’t tokens) alone. (2) Static/non-composable contexts (enums, top-level vals,
default args) use DarkOterColors.*. (3) Non-color legacy (OterDimensions,
OterShapes, ComponentPresets) is a separate track — out of scope for the color sweep.
Progress
- Tag tokens (
tagYellow/Pink/Purple) added toOterColorScheme. -
ButtonType.kt→DarkOterColors(static ctx). -
ScheduledTransactionForm.outlinedFieldColors()→Oter.colors. -
MaterialTheme.colors→Oter.colors— DONE across all platforms (171 commonMain+desktop + 7 android/iOS/jvm). 0MaterialTheme.colors.<field>left. Compiles green on metadata(common)/JVM/desktop/iOS. - Hardcoded
Color(0x..)sweep — N/A: the remaining token-hex literals are all intentional categorical palette/data colors (category/chart/meal/event coloring, commented “palette data — not theme chrome”) or token definitions. Converting them would be wrong. - Layout token layer added (
OterScales.kt) — closes the gap that blocked legacy retirement:Oter.border(thin/medium/thick = 1/2/3),Oter.iconSize(xs..huge = 12/16/20/24/32/40/48),Oter.size(control heights 36/48/56, appBar 56, touchMin 44, avatars 32→120, max-widths card/dialog/content). Wired into theOteraccessor; compiles clean. - Legacy retirement — DONE.
SharedComponents.ktwas 100% dead code → deleted.ui/DesignSystem.ktdeleted.OterColors/OterGradientsstripped fromui/Color.kt(domain flats kept),OterDimensionsfromui/Dimension.kt(picker dims kept),OterShapesfromui/Shape.kt(MaterialShapeskept). FinanceOterDesignSystem.*refs →Oter.radius/Oter.elevation. Dead anchor constants removed from desktopui/theme/Colors.kt. ZeroOterDesignSystem/OterColors/OterGradients/OterDimensions/OterShapesreferences remain.Oter.*is now the single design system. Compiles clean on JVM/iOS/desktop (the only build error is an unrelated in-progressAuthViewModel.refreshUserTiervisibility edit).
Layout token reference (new)
| Group | Accessor | Tokens |
|---|---|---|
| Border width | Oter.border | thin 1 · medium 2 · thick 3 |
| Icon size | Oter.iconSize | xs 12 · sm 16 · md 20 · lg 24 · xl 32 · xxl 40 · huge 48 |
| Component size | Oter.size | controlSm 36 · control 48 · controlLg 56 · appBar 56 · touchMin 44 · avatarSm 32 · avatarMd 40 · avatarLg 48 · avatarXl 64 · avatarHuge 80 · avatarProfile 120 · maxWidthCard 400 · maxWidthDialog 560 · maxWidthContent 1200 |
Android note:
compileAndroidMaincurrently fails on an unrelated in-progress edit inapi/HttpClientConfig.kt(configureAuthrefactor) — not part of this sweep. The color migration itself is validated on the other four targets.
Sources of truth (today there are three — that’s the problem)
| # | Source | What it is | Location |
|---|---|---|---|
| 1 | Figma variables | The live design file’s variable collections | File LV4kWrBFAePEdv6kathVEZ (LifeCommander) |
| 2 | Code tokens | Compose Multiplatform Oter.* tokens | shared/.../ui/tokens/ |
| 3 | Prototype spec | DesignSystem.md v1.4.2 (Claude Design export) | ~/Desktop/oter 2/project/DesignSystem.md |
The product is dark-first (indigo-on-slate); light theme inverts surfaces and keeps the brand hue. The end state we want: Figma variables and code tokens are 1:1, name-for-name. This doc is the gap analysis to get there.
1. Token architecture
Figma models tokens in three tiers; code currently has only the middle tier.
Tier 1 PRIMITIVES raw palette (indigo/500, slate/900, red/400…) ← Figma only
Tier 2 SEMANTIC themed aliases (brand/primary, status/danger…) ← Figma + Code
Tier 3 SCALES spacing / radius / elevation / motion / z ← Figma(partial) + CodeDECISION 1 — Do we mirror Tier 1 (primitives) in code?
Code jumps straight to semantic tokens. Figma has a 40-entry Primitives collection
that semantic tokens alias. Options:
- (a) Keep code semantic-only (current). Simpler, but the palette lives only in Figma.
- (b) Add an internal
OterPaletteobject (raw colors) thatOterColorSchemereferences, mirroring Figma exactly. Recommended for traceability.
2. Color
2.1 Primitives (Figma Primitives collection — single mode)
| Family | Tokens (Figma name → hex) |
|---|---|
| Indigo | indigo/400 #818CF8 · indigo/500 #6366F1 · indigo/600 #4F46E5 · indigo/700 #4338CA · indigo/800 #3730A3 |
| Purple/Violet | purple/500 #A855F7 · purple/800 #6B21A8 · violet/500 #8B5CF6 |
| Slate | slate/50 #F8FAFC · 100 #F1F5F9 · 200 #E2E8F0 · 300 #CBD5E1 · 400 #94A3B8 · 500 #64748B · 600 #475569 · 700 #334155 · 800 #1E293B · 900 #0F172A · 950 #020617 |
| Gray | gray/500 #6B7280 |
| Red | red/400 #F87171 · red/500 #EF4444 · red/600 #DC2626 · red/700 #B91C1C |
| Green | green/500 #22C55E · green/600 #16A34A · green/700 #15803D |
| Amber | amber/400 #FBBF24 · amber/500 #F59E0B · amber/600 #D97706 · amber/700 #B45309 |
| Blue | blue/400 #60A5FA · blue/500 #3B82F6 · blue/600 #2563EB · blue/700 #1D4ED8 |
| Yellow/Pink | yellow/500 #EAB308 · pink/500 #EC4899 |
| Neutral | white #FFFFFF |
| Brand alpha | indigo/alpha-15 #6366F1 @15% · indigo/alpha-20 #6366F1 @20% |
2.2 Semantic — themed (Figma Color collection, modes Dark / Light)
Each row = one semantic variable aliasing a primitive per mode. ✅ = present in code (OterColorScheme).
| Figma semantic | Code field | Prototype CSS var | Dark → | Light → | In code? |
|---|---|---|---|---|---|
brand/primary | primary | --primary-color | indigo/500 | indigo/600 | ✅ |
brand/primary-hover | primaryHover | --primary-hover | indigo/600 | indigo/800 | ✅ |
brand/primary-light | primaryLight | --primary-light | indigo/400 | indigo/500 | ✅ |
brand/tint-15 | primaryTint15 | --primary-tint-15 | indigo @15% | indigo @15% | ✅ |
brand/tint-20 | primaryTint20 | --primary-tint-20 | indigo @20% | indigo @20% | ✅ |
surface/background | background | --background-color | slate/900 | slate/50 | ✅ |
surface/surface | surface | --surface-color | slate/800 | white | ✅ |
surface/hover | surfaceHover | --surface-hover | slate/700 | slate/100 | ✅ |
border/default | border | --border-color | slate/700 | slate/200 | ✅ |
border/light | borderLight | --border-light | slate/600 | slate/300 | ✅ |
text/primary | textPrimary | --text-primary | slate/50 | slate/900 | ✅ |
text/secondary | textSecondary | --text-secondary | slate/400 | slate/500 | ✅ |
text/tertiary | textTertiary | --text-tertiary | slate/500 | slate/400 | ✅ |
status/success | success | --success-color | green/500 | green/600 | ✅ |
status/success-hover | successHover | --success-hover | green/600 | green/700 | ✅ |
status/warning | warning | --warning-color | amber/500 | amber/600 | ✅ |
status/warning-hover | warningHover | --warning-hover | amber/600 | amber/700 | ✅ |
status/danger | danger | --danger-color | red/500 | red/600 | ✅ |
status/danger-hover | dangerHover | --danger-hover | red/600 | red/700 | ✅ |
status/info | info | --info-color | blue/500 | blue/600 | ✅ |
status/info-hover | infoHover | --info-hover | blue/600 | blue/700 | ✅ |
status/danger-light | dangerLight | — | red/400 | red/600 | ✅ |
status/warning-light | warningLight | — | amber/400 | amber/600 | ✅ |
status/info-light | infoLight | — | blue/400 | blue/600 | ✅ |
tag/yellow | — | — | yellow/500 | yellow/500 | ❌ missing |
tag/pink | — | — | pink/500 | pink/500 | ❌ missing |
tag/purple | — | — | purple/500 | purple/500 | ❌ missing |
Note
*-lightflips meaning by theme: in dark it’s a lighter tint (red/400), in light it’s a darker shade (red/600) — i.e. “the legible accent tint for the current theme.”
2.3 Eisenhower (theme-independent in both Figma & code)
| Figma | Code field | Value | In code? |
|---|---|---|---|
eisenhower/urgency-urgent | urgencyUrgent | red/500 #EF4444 | ✅ |
eisenhower/urgency-moderate | urgencyModerate | amber/500 #F59E0B | ✅ |
eisenhower/urgency-not | urgencyNot | gray/500 #6B7280 | ✅ |
eisenhower/importance-high | importanceHigh | blue/500 #3B82F6 | ✅ |
eisenhower/importance-medium | importanceMedium | violet/500 #8B5CF6 | ✅ |
eisenhower/importance-low | importanceLow | gray/500 #6B7280 | ✅ |
2.4 Auth gradient
linear-gradient(to bottom right, #4338CA, #6B21A8) (indigo-700 → purple-800).
- Code:
authGradientStart/authGradientEnd+authGradientBrush. ✅ - Figma: primitives exist (
indigo/700,purple/800) but there is no gradient variable (Figma can’t store gradients as variables). Documented, not tokenized.
3. The translucent-fill problem (QuadBadge & friends) — DECISION 2: RESOLVED ✅
The Eisenhower badges use a translucent fill + soft stroke + solid light text:
fill = categoryColor @ 18% ← now a *-subtle token
stroke = categoryColor @ 35% ← now a *-outline token
text = <color>-light @ 100%Why paint-opacity failed (the original symptom): Figma hides the opacity field once a color variable is bound to a fill, and re-binding resets opacity to 100%. The QuadBadge Schedule fill silently reverted to solid twice because of this — paint opacity is not durable when a variable is bound.
Resolution — alpha is now baked into tokens. Implemented 2026-06-04:
Tier 1 — new primitive alpha colors (Primitives collection, scopes []):
| Primitive | Base | Alpha |
|---|---|---|
red/alpha-18, red/alpha-35 | #EF4444 | 18% / 35% |
amber/alpha-18, amber/alpha-35 | #F59E0B | 18% / 35% |
blue/alpha-18, blue/alpha-35 | #3B82F6 | 18% / 35% |
gray/alpha-18, gray/alpha-35 | #6B7280 | 18% / 35% |
Tier 2 — new semantic tokens (Color collection, same value Dark/Light, aliasing the primitives):
| Semantic token | Aliases | Scope | QuadBadge use |
|---|---|---|---|
eisenhower/urgency-urgent-subtle / -outline | red alpha-18 / 35 | fill / stroke | Do |
eisenhower/urgency-moderate-subtle / -outline | amber alpha-18 / 35 | fill / stroke | Delegate |
eisenhower/importance-high-subtle / -outline | blue alpha-18 / 35 | fill / stroke | Schedule |
eisenhower/urgency-not-subtle / -outline | gray alpha-18 / 35 | fill / stroke | Eliminate |
Decisions taken (defaults — adjust if needed):
- (a) Scoped to the 4 colors QuadBadge actually uses (not all 6 Eisenhower colors). Easy to extend.
- (b) All four badges now bind to the
eisenhower/*namespace (Do/Delegate were previously onstatus/danger/status/warning— rebound for a consistent namespace and theme-independent behaviour). - (c) Canonical alphas locked to 18% fill / 35% outline. (The old
QuadrantButton0.1–0.18 / 0.5 values should be migrated to these.) - (d) Naming chosen:
-subtle(fill) /-outline(stroke), extending thebrand/tint-*alpha precedent.
QuadBadge variant bindings are now 100% token-driven (paint opacity = 1.0; transparency comes from the variable), so the solid-fill regression cannot recur.
4. Spacing — 4px base (Figma Spacing ✅ matches code OterSpacing)
| Token | Value |
|---|---|
xs | 4 |
sm | 8 |
md | 16 |
lg | 24 |
xl | 32 |
xxl | 64 |
No conflicts. (Note: there is intentionally no 12px spacing step; 12 exists only in Radius.)
5. Radius (Figma Radius ✅ matches code OterRadius)
| Token | Value |
|---|---|
sm | 6 |
md | 8 |
lg | 12 |
xl | 16 |
full | 9999 |
QuadBadge uses sm (6). No conflicts.
6. Elevation / Motion / Z-index — code-only (not in Figma variables)
These live only in OterScales.kt (mirroring the prototype spec). Figma has no
variables for them. DECISION 3: leave as code-only, or also create Figma number
variables for parity? (Low priority — shadows/motion rarely bound in Figma.)
- Elevation: none 0 · sm 1 · md 4 · lg 10 · xl 20 (dp; maps from CSS box-shadow y-offsets)
- Motion: easing
cubic-bezier(.4,0,.2,1); fast 150ms · base 200ms · slow 300ms - Z-index: base 0 · dropdown 1000 · sticky 1020 · fixed 1030 · modalBackdrop 1040 · modal 1050 · toast 1060 · tooltip 1070
7. Typography — code-only (not in Figma variables)
Family: Lexend (mobile) / Geist (web/desktop), mono = Geist Mono. Defined in
OterTypeScale.kt. Figma has no type variables/styles captured yet — DECISION 4:
publish these as Figma text styles for parity?
| Token | Size | Weight | Line height |
|---|---|---|---|
h1 | 28 | 700 | 1.2 (−0.01em) |
h2 | 24 | 700 | 1.25 (−0.005em) |
h3 | 20 | 700 | 1.3 |
h4 | 18 | 700 | 1.35 |
h5 | 16 | 700 | 1.4 |
h6 | 14 | 700 | 1.4 |
body | 16 | 400 | 1.5 |
bodySm | 14 | 400 | 1.5 |
caption | 12 | 400 | 1.4 |
button | 16 | 600 | 1.0 |
mono | 14 | 400 | 1.5 |
8. Summary of open decisions
| # | Decision | Recommendation |
|---|---|---|
| 1 | Mirror Figma primitives in code? | Yes — add internal OterPalette for traceability |
| 2 | Translucent fill/outline tokens (QuadBadge) — add, scope, alphas, naming | ✅ DONE — eisenhower/*-subtle (18%) + *-outline (35%) created & bound in Figma; mirror in code next |
| 3 | Add status/*-light + tag/* to code | Yes — dangerLight/warningLight/infoLight, tagYellow/Pink/Purple |
| 4 | Elevation/motion/z-index & typography as Figma variables/styles? | Optional / low priority — keep code as source for now |
9. Token gaps to close in code (once decisions land)
-
dangerLight,warningLight,infoLight(theme-aware) — added toOterColorScheme -
tagYellow,tagPink,tagPurple - Eisenhower
*-subtle(18%) and*-outline(35%) — created in Figma and mirrored inOterColorScheme(urgencyUrgent/Moderate/Not+importanceHigh, eachSubtle/Outline). -
OterQuadBadgecomposable +QuadBadgeTypeenum —ui/components/primitives/OterBadges.kt, token-driven, compiles on JVM. - (optional)
OterPaletteraw-primitive layer — pending DECISION 1 - Migrate the legacy
QuadrantButton(EisenhowerQuadrantSelector.kt) off its hardcoded 0.1–0.18 / 0.5 alphas onto the new subtle/outline tokens.