Skip to content

Commit 3014102

Browse files
authored
Merge pull request #24 from codebar-ag/release/v1.16.0
v1.16.0 — a non-colour channel for categorical axes
2 parents 2a317b0 + 15b867a commit 3014102

12 files changed

Lines changed: 897 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,207 @@ All notable changes to `@codebar-ag/storybook`.
55
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
66
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## v1.16.0
9+
10+
The non-colour channel v1.15.0 said would be needed. Additive and non-breaking:
11+
nothing existing changes shape, colour or markup — `Tone` is still five values,
12+
`Category` is still three, and no token moved.
13+
14+
### Added
15+
16+
- **`KindMark` — a kind label whose primary channel is geometry, not colour.**
17+
18+
v1.15.0 shipped three categorical colours and documented three as a *measured
19+
ceiling*: inside the one arc of the hue wheel that clears AA-as-text on three
20+
grounds, holds enough chroma to read as a hue, and stays clear of every
21+
severity token, three is the largest mutually separable set. That conclusion
22+
has held up. What it left open was what to do about an axis with more than
23+
three members, and there was a real one waiting: a consuming app's flow-graph
24+
diagram with **15 node kinds**, drawn on 12 raw Tailwind ramp hues because
25+
there was nothing else to draw them with. Several of those hues sat on the
26+
severity ramp — a node whose *kind* was "data source" rendered in the amber
27+
the UI uses for "something is wrong". That was reported as a bug and it was
28+
one.
29+
30+
It is not fixed by a fourth colour, and the proof is inside this kit's own
31+
tokens rather than in an argument. Desaturated, `--color-cat-indigo` and
32+
`--color-cat-magenta` measure **L\* 40.7 and 40.6**. They are the same grey.
33+
Print the graph in black and white and the entire categorical vocabulary
34+
collapses to one grey and one dark grey — so any design where colour is the
35+
distinction has already failed a test nobody was running.
36+
37+
`KindMark` runs that test on purpose. It splits the distinction across three
38+
channels of decreasing coarseness and increasing certainty:
39+
40+
| channel | values | needs | survives greyscale |
41+
| --- | --- | --- | --- |
42+
| silhouette | 4 | ≥12px | yes — it is geometry |
43+
| glyph | the icon registry | ≥12px of glyph, so ≥32px of mark | yes |
44+
| label | unbounded | reading | yes |
45+
46+
Colour rides on top of the silhouette — one colour per shape, so it is purely
47+
redundant. Delete it and nothing is lost. That is the difference between a
48+
reinforcement channel and a signalling one, and it is the whole design.
49+
50+
Props: `shape` and `label` required, `icon` and `category` optional, `size` of
51+
`sm | md | lg`. **`label` cannot be suppressed.** Four silhouettes cannot name
52+
fifteen kinds and the component is not allowed to imply otherwise; an omitted
53+
`icon` renders the silhouette alone, which is a real state the legend uses.
54+
55+
- **`KindLegend` — the four-row key, and only ever four rows.**
56+
57+
A silhouette means "this box is the same sort of thing as that one", which is
58+
a claim about the set, not about any one mark — so no individual card can
59+
convey it and it needs a legend. The glyph does not: every mark carries its
60+
kind's name, so a reader can always just read it. The legend therefore lists
61+
**families, not kinds**: four rows for fifteen kinds, and still four for
62+
fifty. A legend that grew with the axis would be the concession that the
63+
encoding had stopped working.
64+
65+
- **Three icons — `database`, `chat`, `chip`.** Not decoration: the app above
66+
had 15 kinds on 11 glyphs, so four pairs shared one — `code` for both node
67+
and schema, `document` for both data source and prompt, `cloud` for both
68+
provider and AI provider, `link` for both gateway and gateway endpoint. Each
69+
of those pairs measures **DSSIM 0**, the only score that means "the same
70+
picture". A design that promotes the glyph to a primary channel has to supply
71+
enough glyphs for it to be one.
72+
73+
### How the shape channel was measured
74+
75+
Colour has ΔE. A shape channel needs its own measured equivalent or "it is
76+
distinguishable" is just an assertion, so: **DSSIM = (1 − SSIM) × 100**
77+
(structural similarity, Wang et al. 2004), greyscale, computed on real Chromium
78+
rasters of the real marks at real pixel sizes — antialiasing, stroke joins and
79+
all. SSIM's 11×11 Gaussian window is itself a coarse low-pass, which is a fair
80+
model of what "at a glance" means.
81+
82+
The floor is **DSSIM ≥ 30**, and it is calibrated against controls rather than
83+
picked, because a number in a new metric means nothing on its own:
84+
85+
| control | DSSIM | what it establishes |
86+
| --- | --- | --- |
87+
| `circle` ↔ regular octagon | 13.4 | at mark size an octagon *is* a circle |
88+
| `square` ↔ same square, bigger radius | 25.4 | a corner radius is not a shape |
89+
| `eye``eye-slash` (this kit's registry) | 26.3 | a pair users demonstrably confuse |
90+
| any glyph ↔ itself | 0 | identity |
91+
92+
Everything at or under ~26 is a pair we can independently confirm is confusable,
93+
so 30 is the first honest floor above them.
94+
95+
One limitation, stated rather than hidden: DSSIM is structural, so it *over*-scores
96+
pairs that differ only by rotation — `chevron-up``chevron-down` scores 81.2
97+
though people confuse them constantly. It is therefore a rejection gate, not a
98+
certificate: below the floor is proof of confusability, above it is not proof of
99+
distinctness, and glyph choices still need a human to look at them.
100+
101+
**The four silhouettes** — worst pair `square``circle`, **59.0** at 32px with
102+
a 1.5px stroke: better than twice the floor and 4.4× the octagon control. What
103+
was rejected and why:
104+
105+
| rejected | DSSIM | why |
106+
| --- | --- | --- |
107+
| octagon | 13.4 vs `circle` | at mark sizes it is a circle |
108+
| squircle | 25.4 vs `square` | a corner radius is not a shape |
109+
| pentagon | 53.2 vs `hexagon` | clears the floor, but falls with size |
110+
| triangle | 59.0 | separable, and **still unusable** — its largest centred inscribed square is 30% of the mark box, so it cannot hold a glyph. A silhouette that cannot host the second channel is not a member of this set. |
111+
112+
Interior fit is the constraint nobody expects: square 77%, circle 63%, hexagon
113+
54%, diamond 45%, triangle 30%. The diamond is the binding one and it is why the
114+
glyph is drawn at 12 of the mark's 32 units rather than larger.
115+
116+
**Size.** The silhouette is the coarse channel and long outlives the glyph.
117+
Worst cross-shape pair, outline only:
118+
119+
| mark | 40px | 32px | 24px | 16px | 12px | 8px |
120+
| --- | --- | --- | --- | --- | --- | --- |
121+
| DSSIM | 55.2 | 59.0 | 68.4 | 55.5 | 37.8 | 25.2 |
122+
123+
So the family reads down to 12px and is gone by 8px, where it lands exactly on
124+
the squircle control. The glyph needs far more room, which is the point of
125+
having two channels: at low zoom a graph keeps telling you *what sort* of node
126+
each box is long after it has stopped telling you which one.
127+
128+
**Glyph**, for the 15-kind set, at the glyph size each mark size produces —
129+
worst pair *within one family*, which is the binding case since the silhouette
130+
separates the rest:
131+
132+
| size | mark | glyph | worst within-family pair | all 15 glyphs |
133+
| --- | --- | --- | --- | --- |
134+
| `lg` | 40px | 15px | 55.2 | 49.9 |
135+
| `md` | 32px | 12px | 49.6 | 47.9 |
136+
| `sm` | 24px | 9px | 34.8 | 34.8 |
137+
138+
`sm` sits just above the floor; it is documented for dense rows where the label
139+
is carrying the load anyway.
140+
141+
**Stroke weight** is a shape channel's version of "is this token dark enough",
142+
and it binds on contrast rather than on separation — separation barely moves
143+
between 0.75px and 2.5px. Measured on `--color-bg`, taking the darkest pixel the
144+
renderer actually paints:
145+
146+
| stroke | `cat-indigo` | `accent` |
147+
| --- | --- | --- |
148+
| 0.5px | 2.17:1 — **under** WCAG 1.4.11's 3:1 non-text floor | 3.18:1 |
149+
| 0.75px | 3.46:1 | 6.94:1 |
150+
| 1px | 5.58:1 | 15.53:1 |
151+
| ≥1.25px | 5.87:1 — its nominal contrast | 16.55:1 |
152+
153+
A sub-pixel stroke is rendered as partial coverage and composites toward the
154+
background, so it never reaches the colour it was specified in. **1.25px is the
155+
minimum**; the silhouette ships at 1.5px and the glyph at 1.4px, both held
156+
constant in *device* pixels across sizes rather than scaled, because the
157+
constraint is absolute rather than relative.
158+
159+
**Colour, held to v1.15.0's gates.** All four family colours as text:
160+
161+
| family colour | on white | on `bg` | on `surface-2` | on own soft |
162+
| --- | --- | --- | --- | --- |
163+
| `cat-indigo` `#4f46e5` | 6.29:1 | 5.87:1 | 5.67:1 | 5.62:1 |
164+
| `cat-purple` `#581c87` | 10.88:1 | 10.16:1 | 9.81:1 | 10.14:1 |
165+
| `cat-magenta` `#a21caf` | 6.32:1 | 5.91:1 | 5.71:1 | 5.89:1 |
166+
| `accent` `#18181b` | 17.72:1 | 16.55:1 | 15.99:1 | 15.99:1 |
167+
168+
Distance to the severity ramp, OKLab ΔE×100, normal / worst under simulated
169+
protanopia and deuteranopia (Machado-Oliveira-Fernandes 2009, severity 1.0):
170+
indigo↔`success` 31.2 / 27.3, purple↔`success` 27.6 / 19.3, magenta↔`success`
171+
34.1 / 17.9, accent↔`danger` 28.4 / 16.1. Every pair clears ≥15 normal and ≥8
172+
CVD, so no kind mark can be read as a status. The `accent` family is not a
173+
fourth category — it is the *absence* of one, which is how four families fit
174+
inside a three-category vocabulary, and why the two ceilings coincide instead of
175+
fighting.
176+
177+
### Where this stops scaling, stated rather than discovered later
178+
179+
**Four families.** There is no fifth silhouette: the table above is the whole
180+
search, not a sample of it. A fifth family cannot be encoded, only spelled out
181+
in the label.
182+
183+
Inside a family the glyph is the channel, and it is roomier than any real axis:
184+
a greedy max-min sweep of the whole 36-glyph registry at 16px keeps every pair
185+
above 70.0 out to a set of **17 glyphs**, and is still at 59.8 at 20. But that
186+
is not the honest limit either. The honest limit is what a reader has to *learn*
187+
— four silhouettes, taught by `KindLegend` — because the glyph never has to be
188+
learned at all: the kind's name is printed on the mark.
189+
190+
And the number that keeps the design honest rather than flattering it: taking
191+
the complete marks for all 15 kinds in greyscale, the **worst** of the 105 pairs
192+
scores 6.7 (`ai_model``mcp_server`) against a median of 82.6. Two kinds in
193+
the same family, differing only by their glyph, genuinely do look alike at a
194+
glance — that is what a family *is*. The mark alone does not separate fifteen
195+
kinds and never claimed to. The label is why it does not have to, and that is
196+
why `label` is required and cannot be turned off.
197+
198+
### Notes
199+
200+
- Not changed, but re-confirmed while measuring: `warning` and `danger` remain
201+
ΔE 6.7 apart to normal vision and 2.9 under simulated red-green colour
202+
blindness (v1.15.0's note). Both always carry text, so meaning is never
203+
colour-alone, and separating them is still a major-release decision.
204+
- The consuming app's adoption is a separate change: `KindMark` replaces a
205+
15-kind, 12-hue map that this release exists to retire. The comment in that
206+
map naming the ceiling should be rewritten, not deleted — the ceiling moved
207+
from three to four, it did not go away.
208+
8209
## v1.15.0
9210

10211
A categorical (non-severity) colour vocabulary, added as a **parallel** set to

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@codebar-ag/storybook",
3-
"version": "1.15.0",
3+
"version": "1.16.0",
44
"description": "codebar-ag DocuHub — shared Vue 3 + Tailwind v4 design-system atoms and tokens, documented in Storybook.",
55
"license": "MIT",
66
"author": "codebar Solutions AG",

0 commit comments

Comments
 (0)