Skip to Content

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 / hardcodedToken (composable ctx)Token (static ctx)
OterColors.Primary, PrimaryColor, #6366F1Oter.colors.primaryDarkOterColors.primary
OterColors.PrimaryDark, PrimaryVariantColor, #4F46E5Oter.colors.primaryHoverDarkOterColors.primaryHover
OterColors.Secondary/PrimaryLight, #818CF8Oter.colors.primaryLightDarkOterColors.primaryLight
OterColors.Background, #0F172AOter.colors.backgroundDarkOterColors.background
OterColors.Surface, #1E293BOter.colors.surfaceDarkOterColors.surface
OterColors.SurfaceVariant, #334155Oter.colors.surfaceHoverDarkOterColors.surfaceHover
OterColors.OnBackground/OnSurface, #F8FAFCOter.colors.textPrimaryDarkOterColors.textPrimary
OterColors.OnSurfaceVariant, #94A3B8Oter.colors.textSecondaryDarkOterColors.textSecondary
OterColors.OnSurfaceDisabled, #64748BOter.colors.textTertiaryDarkOterColors.textTertiary
OterColors.Border/Divider, #334155Oter.colors.borderDarkOterColors.border
OterColors.BorderLight, #475569Oter.colors.borderLightDarkOterColors.borderLight
OterColors.Danger/Error/DangerColor, #EF4444Oter.colors.dangerDarkOterColors.danger
OterColors.Warning/WarningColor, #F59E0BOter.colors.warningDarkOterColors.warning
OterColors.Info/InfoColor, #3B82F6Oter.colors.infoDarkOterColors.info
OterColors.Success/SuccessColor, #22C55EOter.colors.successDarkOterColors.success
MaterialTheme.colors.primaryOter.colors.primary
MaterialTheme.colors.primaryVariantOter.colors.primaryHover
MaterialTheme.colors.secondaryOter.colors.primaryLight
MaterialTheme.colors.backgroundOter.colors.background
MaterialTheme.colors.surfaceOter.colors.surface
MaterialTheme.colors.errorOter.colors.danger
MaterialTheme.colors.onBackground/onSurfaceOter.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 to OterColorScheme.
  • ButtonType.ktDarkOterColors (static ctx).
  • ScheduledTransactionForm.outlinedFieldColors()Oter.colors.
  • MaterialTheme.colorsOter.colors — DONE across all platforms (171 commonMain+desktop + 7 android/iOS/jvm). 0 MaterialTheme.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 the Oter accessor; compiles clean.
  • Legacy retirement — DONE. SharedComponents.kt was 100% dead code → deleted. ui/DesignSystem.kt deleted. OterColors/OterGradients stripped from ui/Color.kt (domain flats kept), OterDimensions from ui/Dimension.kt (picker dims kept), OterShapes from ui/Shape.kt (Material Shapes kept). Finance OterDesignSystem.* refs → Oter.radius/Oter.elevation. Dead anchor constants removed from desktop ui/theme/Colors.kt. Zero OterDesignSystem/OterColors/OterGradients/OterDimensions/OterShapes references remain. Oter.* is now the single design system. Compiles clean on JVM/iOS/desktop (the only build error is an unrelated in-progress AuthViewModel.refreshUserTier visibility edit).

Layout token reference (new)

GroupAccessorTokens
Border widthOter.borderthin 1 · medium 2 · thick 3
Icon sizeOter.iconSizexs 12 · sm 16 · md 20 · lg 24 · xl 32 · xxl 40 · huge 48
Component sizeOter.sizecontrolSm 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: compileAndroidMain currently fails on an unrelated in-progress edit in api/HttpClientConfig.kt (configureAuth refactor) — 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)

#SourceWhat it isLocation
1Figma variablesThe live design file’s variable collectionsFile LV4kWrBFAePEdv6kathVEZ (LifeCommander)
2Code tokensCompose Multiplatform Oter.* tokensshared/.../ui/tokens/
3Prototype specDesignSystem.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) + Code

DECISION 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 OterPalette object (raw colors) that OterColorScheme references, mirroring Figma exactly. Recommended for traceability.

2. Color

2.1 Primitives (Figma Primitives collection — single mode)

FamilyTokens (Figma name → hex)
Indigoindigo/400 #818CF8 · indigo/500 #6366F1 · indigo/600 #4F46E5 · indigo/700 #4338CA · indigo/800 #3730A3
Purple/Violetpurple/500 #A855F7 · purple/800 #6B21A8 · violet/500 #8B5CF6
Slateslate/50 #F8FAFC · 100 #F1F5F9 · 200 #E2E8F0 · 300 #CBD5E1 · 400 #94A3B8 · 500 #64748B · 600 #475569 · 700 #334155 · 800 #1E293B · 900 #0F172A · 950 #020617
Graygray/500 #6B7280
Redred/400 #F87171 · red/500 #EF4444 · red/600 #DC2626 · red/700 #B91C1C
Greengreen/500 #22C55E · green/600 #16A34A · green/700 #15803D
Amberamber/400 #FBBF24 · amber/500 #F59E0B · amber/600 #D97706 · amber/700 #B45309
Blueblue/400 #60A5FA · blue/500 #3B82F6 · blue/600 #2563EB · blue/700 #1D4ED8
Yellow/Pinkyellow/500 #EAB308 · pink/500 #EC4899
Neutralwhite #FFFFFF
Brand alphaindigo/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 semanticCode fieldPrototype CSS varDark →Light →In code?
brand/primaryprimary--primary-colorindigo/500indigo/600
brand/primary-hoverprimaryHover--primary-hoverindigo/600indigo/800
brand/primary-lightprimaryLight--primary-lightindigo/400indigo/500
brand/tint-15primaryTint15--primary-tint-15indigo @15%indigo @15%
brand/tint-20primaryTint20--primary-tint-20indigo @20%indigo @20%
surface/backgroundbackground--background-colorslate/900slate/50
surface/surfacesurface--surface-colorslate/800white
surface/hoversurfaceHover--surface-hoverslate/700slate/100
border/defaultborder--border-colorslate/700slate/200
border/lightborderLight--border-lightslate/600slate/300
text/primarytextPrimary--text-primaryslate/50slate/900
text/secondarytextSecondary--text-secondaryslate/400slate/500
text/tertiarytextTertiary--text-tertiaryslate/500slate/400
status/successsuccess--success-colorgreen/500green/600
status/success-hoversuccessHover--success-hovergreen/600green/700
status/warningwarning--warning-coloramber/500amber/600
status/warning-hoverwarningHover--warning-hoveramber/600amber/700
status/dangerdanger--danger-colorred/500red/600
status/danger-hoverdangerHover--danger-hoverred/600red/700
status/infoinfo--info-colorblue/500blue/600
status/info-hoverinfoHover--info-hoverblue/600blue/700
status/danger-lightdangerLightred/400red/600
status/warning-lightwarningLightamber/400amber/600
status/info-lightinfoLightblue/400blue/600
tag/yellowyellow/500yellow/500missing
tag/pinkpink/500pink/500missing
tag/purplepurple/500purple/500missing

Note *-light flips 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)

FigmaCode fieldValueIn code?
eisenhower/urgency-urgenturgencyUrgentred/500 #EF4444
eisenhower/urgency-moderateurgencyModerateamber/500 #F59E0B
eisenhower/urgency-noturgencyNotgray/500 #6B7280
eisenhower/importance-highimportanceHighblue/500 #3B82F6
eisenhower/importance-mediumimportanceMediumviolet/500 #8B5CF6
eisenhower/importance-lowimportanceLowgray/500 #6B7280

2.4 Auth gradient

linear-gradient(to bottom right, #4338CA, #6B21A8) (indigo-700 → purple-800).

  • Code: authGradientStart / authGradientEnd + authGradient Brush. ✅
  • 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 []):

PrimitiveBaseAlpha
red/alpha-18, red/alpha-35#EF444418% / 35%
amber/alpha-18, amber/alpha-35#F59E0B18% / 35%
blue/alpha-18, blue/alpha-35#3B82F618% / 35%
gray/alpha-18, gray/alpha-35#6B728018% / 35%

Tier 2 — new semantic tokens (Color collection, same value Dark/Light, aliasing the primitives):

Semantic tokenAliasesScopeQuadBadge use
eisenhower/urgency-urgent-subtle / -outlinered alpha-18 / 35fill / strokeDo
eisenhower/urgency-moderate-subtle / -outlineamber alpha-18 / 35fill / strokeDelegate
eisenhower/importance-high-subtle / -outlineblue alpha-18 / 35fill / strokeSchedule
eisenhower/urgency-not-subtle / -outlinegray alpha-18 / 35fill / strokeEliminate

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 on status/danger/status/warning — rebound for a consistent namespace and theme-independent behaviour).
  • (c) Canonical alphas locked to 18% fill / 35% outline. (The old QuadrantButton 0.1–0.18 / 0.5 values should be migrated to these.)
  • (d) Naming chosen: -subtle (fill) / -outline (stroke), extending the brand/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)

TokenValue
xs4
sm8
md16
lg24
xl32
xxl64

No conflicts. (Note: there is intentionally no 12px spacing step; 12 exists only in Radius.)

5. Radius (Figma Radius ✅ matches code OterRadius)

TokenValue
sm6
md8
lg12
xl16
full9999

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?

TokenSizeWeightLine height
h1287001.2 (−0.01em)
h2247001.25 (−0.005em)
h3207001.3
h4187001.35
h5167001.4
h6147001.4
body164001.5
bodySm144001.5
caption124001.4
button166001.0
mono144001.5

8. Summary of open decisions

#DecisionRecommendation
1Mirror Figma primitives in code?Yes — add internal OterPalette for traceability
2Translucent fill/outline tokens (QuadBadge) — add, scope, alphas, namingDONEeisenhower/*-subtle (18%) + *-outline (35%) created & bound in Figma; mirror in code next
3Add status/*-light + tag/* to codeYes — dangerLight/warningLight/infoLight, tagYellow/Pink/Purple
4Elevation/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 to OterColorScheme
  • tagYellow, tagPink, tagPurple
  • Eisenhower *-subtle (18%) and *-outline (35%) — created in Figma and mirrored in OterColorScheme (urgencyUrgent/Moderate/Not + importanceHigh, each Subtle/Outline).
  • OterQuadBadge composable + QuadBadgeType enum — ui/components/primitives/OterBadges.kt, token-driven, compiles on JVM.
  • (optional) OterPalette raw-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.