Soft Pro Design System

Token Audit

Archaeological pass over spoedmarktplaats-mobile/src/{screens,components,navigation}. Goal: turn the de-facto style vocabulary already hiding in screens into a declarative token system. No UI or component code was modified. The output of this audit lives in tokens.ts and theme.ts (same folder).

1What was found

Every #RRGGBB hex literal, every StyleSheet magic number, every fontSize that appears in src/screens and src/components was tabulated and clustered.

1aColors

Total distinct hex literals across screens + components: ~60. Top of the frequency table:

CountHexRole found in codeMapped to
51#FFFcard / sheet / input backgroundsurface.primary / fixed.white
31#000overlay / iconfixed.black / overlay.*
22#E3FF00accent / chartreuse highlightsbrand.accent
21#1A1814body text, primary buttonstext.primary / brand.primary
14#FCFAF7screen backgroundsurface.background
11#6B6359caption / metadata texttext.secondary
9#3B9C4Fdeal status, secondary greenfeedback.success.soft
7#F8FFB3accent-tinted calloutsbrand.accentSoft / surface.tinted
7#F8F4ECdark-mode text / light cardspaper100 primitive
7#F2EEE8secondary surface (chips etc.)surface.secondary
7#16A34Averified badge / successfeedback.success.default
6#FF5F1FProjectversterking brandbrand.flame
6#FBBF24boost / warning softfeedback.warning.soft
6#DC2626destructive / errorfeedback.error.default
6#15130Fdark-mode backgrounddark900 primitive

The de-facto Soft Pro palette

ink
#1A1814
paper
#FCFAF7
paper.200
#F2EEE8
paper.400
#D8D2CA
warm.600
#6B6359
warm.500
#A39B8E
chartreuse
#E3FF00
chartreuse.soft
#F8FFB3
flame
#FF5F1F
success
#16A34A
warning
#F59E0B
error
#DC2626

Collapses made

Where multiple hexes were doing the same job:

1bSpacing

Distinct integer literals on padding* / margin* / gap: 27 values. After clustering onto an 8pt grid:

Step (px)HitsToken
242space.hairline
463space.xs
854space.sm
1251space.md
1634space.lg
2016space.xl
2413space['2xl']
323space['3xl']
481space['4xl']

Off-grid drift

The semantic aliases inset.* and gap.* layer over these, so screens can express intent (inset.card) rather than step (space.lg).

1cFont size

22 distinct sizes (11 → 80). Clustered into a clean ramp:

TokenpxHitsReplaces drift
micro1144tab labels
caption1244small captions
captionLg1354dominant caption variant
label1440field labels, button copy
body1618body
bodyLg179emphasized body
subheading183small section header
heading2211card title
title262screen heading
display323hero number
hero401landing hero

Outliers collapsed: 9→11, 10 (8×)→11, 15 (33×)→14, 20→18, 24→22, 28 (5×)→26, 30 (2×)→32, 34 (2×)→32.

1dBorder radius

15 distinct values; 999 (pill) is its own thing. Clustered to:

TokenpxHitsNotes
xs416
sm84chips prefer pill
md1212controls
lg1621cards
xl2040hero cards — biggest bucket
2xl240sheet/modal
pill99910chips / avatars

Outliers collapsed: 3→4, 6→4, 10 (19×)→12, 13 (8×)→12, 14 (37×)→16, 17 (4×)→16, 18 (4×)→20, 22 (8×)→20, 28 (4×)→24.

1eBorder width

TokenpxHits
hairline1110
thin1.512
thick215

1fFont weight

'600' (102) · '700' (80) · '500' (29) · '400' (5). Exposed as semibold | bold | medium | regular.

1gSizes (controls, icons, avatars)

The width/height audit revealed a clear control + avatar ladder:

1hElevation

Audit found 4 hits of shadow* properties across the entire mobile src tree. Soft Pro is intentionally flat; borders carry hierarchy. The token set keeps elevation.none as the default and offers sheet / modal for the two cases where lift is genuinely needed.

2What was already correct

The existing src/theme/themes.ts already had a coherent light/dark split and most of the brand semantics. The audit kept its names where they made sense:

The gap was that screens weren't using these tokens consistently — they fell back to hardcoded hex and ad-hoc spacing. The new tokens file makes the existing palette explicit and adds the missing buckets (feedback.success.soft, surface.tinted, border.strong, etc.) the screens were already inventing inline.

3How dark mode is derived

Dark mode is built in theme.ts, not duplicated in tokens. Each semantic key in darkColor re-maps to the same palette primitives (paper, ink, warm, chartreuse) but inverted:

The brand accent (chartreuse500) and flame (flame500) intentionally stay identical across modes — they're the brand signal.

4Public API

import { useTheme } from './theme';

function MyScreen() {
  const t = useTheme();
  return (
    <View
      style={{
        backgroundColor: t.color.surface.primary,
        padding: t.inset.card,
        borderRadius: t.cornerRadius.card,
        borderWidth: t.borderWidth.hairline,
        borderColor: t.color.border.subtle,
      }}
    >
      <Text style={[t.textStyle.heading, { color: t.color.text.primary }]}>
        Hello
      </Text>
    </View>
  );
}

5What this audit did NOT do

Per the brief:
  • No screens or components were edited. Migrating screens to the new tokens is a separate pass.
  • No UI behavior was changed. This is purely additive.
  • The legacy src/theme/{themes,theme,ThemeContext}.ts files are untouched — they continue to work for existing imports.

A future migration pass would replace inline hex / magic numbers in screens with useTheme() references, screen-by-screen.