🐼 Pandatone
2026.08.30
A unix inspired color tool
Pandatone is a color palette API. It serves colors and palettes
programmatically so my other design tools can reach them — a generative image
tool asks for the season’s palette, a CAD script asks for the brand red. I
pick and manage them through the UI: the library as swatches, palettes as
strips, lookup by hex, RGB or CMYK, exports to .ase and CSS custom
properties.
Two inspirations
Unix tool philosophy: this is the first of a set of small design tools, each doing one thing over a clean interface. Nothing in Pandatone renders an image or lays out a page. It holds colors and answers questions about them, and the next tool along asks.
And the Swiss International Typographic Style: near-monochrome, so the swatches are the only color on screen. Archivo, one variable subset shipped with the app rather than pulled from a CDN. Every signal that matters carries weight as well as an accent, so nothing rests on color alone — which feels like the least a color tool owes you.
The API
Everything is under /api/v1. Collections are bare arrays, no envelope. A
token in the header, never the session cookie — accept the cookie and any page
on the internet could drive the API from a signed-in browser.
curl -H "Authorization: Bearer $PANDATONE_TOKEN" \
https://pandatone.example.com/api/v1/palettes?tag=active
A color comes back as:
{
"id": 12,
"name": "signal-red",
"hex": "#E30613",
"rgb": { "r": 227, "g": 6, "b": 19 },
"cmyk": { "c": 0.0, "m": 97.4, "y": 91.6, "k": 11.0 },
"source_space": "rgb",
"tags": ["brand", "primary"]
}
That shape, key order included, is pinned by a contract test. It’s versioned
from the first commit, because the whole point is that other tools depend on
it: if the contract test fails, something downstream is already broken, and
the fix is a v2 rather than an edit to v1.
Exports ride the same routes by extension. .ase for a design tool, each
color going out in the space it was authored in; .css for a stylesheet,
custom properties on :root in the palette’s order. There’s no export of the
whole library — a palette is the unit with a name and an order, which is what
both formats are for.
Colors & Palettes
Pandatone stores two things.
Colors are first-class, not children of a palette. One brand blue used in ten palettes is one row joined to ten palettes. That is what makes the reverse lookup possible — paste a hex and get back every palette holding it.
Which forces a rule I like more the longer I use it: a color is its value.
No two colors may render the same hex. Otherwise the reverse lookup would be
an arbitrary choice between two rows that look identical. Near-duplicates are
a question rather than a rule: inside a redmean distance of 32 — #FFFFFF
against #FAFAF8 scores 17 — you get both swatches side by side and a “create
anyway” button.
Every color stores both spaces. source_space records which one was
authored; the other is redrawn on every write, so the two can’t drift. RGB
round-trips through CMYK losslessly, but many CMYK mixes collapse onto one
RGB triple — which is why the source space is recorded rather than inferred.
Palettes are collections of colors, held in an order. A palette is its set of colors, so no two palettes may hold exactly the same swatches — the order is not part of the identity. Duplicates get refused at the write, not cleaned up later.
What I haven’t built
Serious color science. Profiles, gamut mapping, perceptual uniformity — Pantone spends a fortune on that. CMYK is stored and served, labeled an approximate device conversion. No ICC profiles, no Lab, no spot colors. Leaving that out is what keeps the tool small and reliable.
Stack
Rails 8, omakase: Propshaft, importmap, Hotwire, SQLite, Minitest. No extra
gems, no framework CSS. System tests run through rack_test by default, so
the suite needs no browser and the app keeps working with JavaScript off; the
ones that need a real one — measured widths, the live preview, the clipboard —
skip until you point the driver at Chrome. 602 tests.