@@ -5,6 +5,207 @@ All notable changes to `@codebar-ag/storybook`.
55The format follows [ Keep a Changelog] ( https://keepachangelog.com/en/1.1.0/ ) and
66this 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
10211A categorical (non-severity) colour vocabulary, added as a ** parallel** set to
0 commit comments