Buenos Aires · 00:28 GMT−3

Case 02 · Cactus · Design system

One system, three platforms

Cactus runs on three front-ends built at different times by different people. A shared library already existed. Nobody could trust it, so nobody used it. This is the audit, and the system that came out of it.

Company

Role

Senior product designer, system owner

Team

1 designer · 8 developers

Surfaces

POS (Flutter) · Portal, Order and Pay (Next.js)

Status

In progress · v0.1

The finding

The system existed. Nobody could trust it, so nobody used it.

So the first deliverable wasn’t components. It was the evidence: what the code actually does today, measured file by file.

My contribution

Audited the POS codebase (906 Dart files) and the shared widgets library.

Defined the token model and the scope of v0.1.

Planned the rollout with engineering, one platform at a time, starting with Order and Pay.

Not mine: [confirm] implementation in each codebase, by engineering.

The problem

Three products, two frameworks, one designer, eight developers, and no shared source of truth between design and code.

The POS runs on Flutter for tablet and mobile. The account portal and Order and Pay run on Next.js. Hundreds of screens had their values written by hand. The Figma library never matched the code, and the move from Sketch to Figma lost the styles that were there.

Every new screen was a fresh decision about grey, radius and spacing, made by whoever was building it. Nobody was doing anything wrong. There was just nothing to be right against.

The audit · POS app

What the code actually does.

Measured across the Flutter POS app, v1.10.9. These are the baseline every later number gets compared to.

199

distinct hex colours

927 uses across 275 files.

30

near-identical light greys

#EEEEEE alone appears 190 times.

32

font sizes

Including 19.5. Weight is written nine different ways.

47

corner radii

From 1.5 to 110.

53 / 70

spacing values off the 4-pt grid

Counting values up to 64.

108

button widgets

58 public, 50 private, six naming prefixes.

Thirty light greys, all in use. Shown on white, at the values in the code.

ΔE < 5 · perceptually the same

EEEEEE

F2F2F2

E6E6E6

FCFCFC

F3E7E9

EDEDED

FAFAFA

F3F3F3

F8F8F8

F6F6F6

F1F1F1

F9F9F9

E7E7E7

F5F5F5

FBFBFB

F4F4F4

F3F4F8

FEF8F9

E1E1E1

F1F4F9

F6F9FF

F8F9FC

F5F4F5

F7F7F7

EAEAEA

E5E7EB

F3F2F4

F8F9FA

FDF4F4

F3F7F7

The audit · shared library

The library existed. It disagreed with itself.

99 colour tokens holding 89 distinct values, nine groups of exact duplicates, and names that meant different things in different places.

Same name, different value

WARNING

library #FAAD14

app #FFE58F

MAGENTA

library #DD5AC9

app #B60EDA

COLOR_ERROR_BG

comment #FAFDFE

value #FFF1F0

Different names, same value

BLACK = pureBlack

NATURAL_BLACK = offBlack

DIALOG_BLACK = shade1Black

WHITE = pureWhite

FRESH_GREEN = MINT_CREAM

CHERRY_PIE = cherryPie

CAROUSEL_PINK = CLASSIC_ROSE

hi2Grey = hi3Grey

cherryCream = cBABAFF = c00AEEF

A type scale already existed: seven named sizes, from DISPLAY 25 to CAPTION 12.

Text set with the scale

Text set with the scale

~70

Text set by hand

Text set by hand

1,811

Decisions

Four calls that shaped v0.1.

Each one is a bet against the way the last library failed.

01

Source of truth

Chose

Tokens in one file, generated into Flutter and Tailwind.

Rejected

A Figma-only library, with each codebase keeping its own constants.

Why

A library that doesn’t match the code is exactly what already existed, and nobody used it.

02

Structure before values

Chose

Map today’s values to semantic roles and collapse duplicates first.

Rejected

A new palette at the same time.

Why

Changing structure and values together turns every diff into a visual regression nobody can review.

03

Scope of v0.1

Chose

Eight components, starting with the button.

Rejected

A complete library before anyone adopts it.

Why

108 button widgets are what a library nobody adopts looks like. It multiplies, it doesn’t consolidate.

04

Where to start

Chose

Order and Pay: the smallest surface, Next.js, customer-facing.

Rejected

The POS first, because it’s the biggest.

Why

Prove it in one release on a surface we can measure, then carry it to the portal and the POS.

Proposed · v0.1

One name, three outputs.

A primitive holds the value. A semantic token says what it’s for. Each platform reads the same file.

Semantic token

Value

Flutter

Tailwind

surface/base

#FAFAFA

CTokens.surfaceBase

bg-surface-base

surface/subtle

#F2F2F2

CTokens.surfaceSubtle

bg-surface-subtle

border/default

#E0E0E0

CTokens.borderDefault

border-default

text/primary

#161515

CTokens.textPrimary

text-primary

state/warning

#FAAD14

CTokens.stateWarning

bg-state-warning

Rollout

One platform at a time.

01 · Pilot · in progress

Order and Pay

Next.js. Tokens and the first components replace hand-written values on the guest flow.

02 · Next

Account portal

Next.js with its own Tailwind library. The tokens become its theme.

03 · After

POS app

Flutter, tablet and mobile. Generated constants replace CSAColors and CactusColors.

How I’ll know

Baselines I can measure in code.

Targets are what v0.1 is meant to move. They’re proposals until the pilot ships.

Measure

Today

Target

Screen files using colours without tokens

156 of 306

0 new

Primary buttons in parallel

3

1

Font sizes in use

32

7-step scale

Distinct corner radii

47

[target]

Screens reading the theme

1 call to Theme.of

All

What I took from it

A library nobody trusts adds a third option.

Evidence before components.

Numbers from the codebase made the problem concrete for engineering in a way a redesign couldn’t.

Consolidation is a migration.

Change the structure first and the values later, so every step can be reviewed.

One designer can’t police consistency.

With eight developers and three platforms, the tokens have to do it.

Trust is the adoption metric.

A component gets used when people believe it matches production. Everything else follows.

If your product got complicated, let’s talk.

© 2026 Marcos Di Julio

Designed and built in Framer