@cloudgrid-io/ui 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/theme.css CHANGED
@@ -66,6 +66,10 @@
66
66
  --color-page: var(--cg-surface-page);
67
67
  --color-page-alt: var(--cg-surface-page-alt);
68
68
  --color-subtle: var(--cg-surface-subtle);
69
+ /* The neutral raised ground (#151). `subtle` is the lavender tint and `card`
70
+ is white, so before this there was no name for "one step off white, no
71
+ hue" and a raised panel had to flatten or take a cast. */
72
+ --color-raised: var(--cg-surface-raised);
69
73
  --color-control: var(--cg-surface-control);
70
74
  --color-panel: var(--cg-surface-panel);
71
75
  --color-code: var(--cg-surface-code);
@@ -137,6 +141,24 @@
137
141
  --color-expired-fill: var(--cg-expired-fill);
138
142
  --color-danger-fill: var(--cg-danger-fill);
139
143
 
144
+ /* The other two rungs of the chip (#149, #150). `-fill` is the ground, the
145
+ bare name is the dot, `-border` is the hairline and `-strong` is the word.
146
+ Mapped in the same pass as the tokens, for the reason the block above
147
+ gives: a token whose utility lands a release later is a token the first
148
+ consumer reaches for and does not find.
149
+
150
+ There is no --color-danger-border or --color-unhealthy-border, because
151
+ there is no such token — the same shape as --color-offline-fill, and the
152
+ `state` group's own note carries the reason. */
153
+ --color-online-border: var(--cg-online-border);
154
+ --color-online-strong: var(--cg-online-strong);
155
+ --color-expired-border: var(--cg-expired-border);
156
+ --color-expired-strong: var(--cg-expired-strong);
157
+ --color-pending-border: var(--cg-pending-border);
158
+ --color-pending-strong: var(--cg-pending-strong);
159
+ --color-unhealthy-strong: var(--cg-unhealthy-strong);
160
+ --color-danger-strong: var(--cg-danger-strong);
161
+
140
162
  /* ---- lines ---- */
141
163
  --color-hairline: var(--cg-border-hairline);
142
164
  --color-line: var(--cg-border-control);
@@ -52,7 +52,7 @@
52
52
  }
53
53
  },
54
54
  "state": {
55
- "$description": "state: what an entity IS, not what it means editorially.\n\nAdopted from the Console rather than invented, 2026-08-24 (#96). The Console\nhad already chosen and contrast-tested these — --color-live and\n--color-attention — while the brand carried no state colour at all beyond the\ndanger placeholder. Taking its values means the Console's appearance does not\nchange when it starts consuming this package, and the two stop disagreeing\nabout what 'live' looks like.\n\nEach carries per-surface values in surface-panel and surface-ink, for the same\nreason danger does: measured against all three grounds, no single value works.\nThe live green is 1.72:1 on the purple panel, and the brand's own lime is\n1.54:1 on white. A flat value would be invisible somewhere.\n\nNo --cg-offline-fill, deliberately. Measured, the pair does not work: the\nmuted neutral on a neutral tint is 2.90:1, under the 3:1 bar for a dot or a\nborder. Rather than invent a value to satisfy the ratio, an unplugged chip\nuses the existing surface-card ground with border-control, and --cg-offline\nis the dot on that ground, where it measures 3.28:1. Absence of state should\nnot need a colour of its own.\n\npending joined them on 2026-08-24 (#114) as an ALIAS of expired rather than a\nnew value. Same amber today, separate name, so the two can diverge later with\na one-line edit here and no change in any consumer. Aliases compile to var(),\nso the flip survives.\n\nunhealthy joined them on 2026-08-25 (#122) as a REAL value, not an alias, and\nit is the one tone here that was searched for rather than adopted. Founder\ndecision: expired is terminal, unhealthy is live-and-fixable, and sharing a\ntoken would mean retuning one silently retunes the other. It is a warmer,\nbrighter gold on the same ramp as the amber rather than a new hue family,\nbecause green/amber/red is the traffic light an operator already reads and\n'serving but degraded' is its middle rung. Its own note carries the numbers\nand the three families that were measured and rejected.",
55
+ "$description": "state: what an entity IS, not what it means editorially.\n\nAdopted from the Console rather than invented, 2026-08-24 (#96). The Console\nhad already chosen and contrast-tested these — --color-live and\n--color-attention — while the brand carried no state colour at all beyond the\ndanger placeholder. Taking its values means the Console's appearance does not\nchange when it starts consuming this package, and the two stop disagreeing\nabout what 'live' looks like.\n\nEach carries per-surface values in surface-panel and surface-ink, for the same\nreason danger does: measured against all three grounds, no single value works.\nThe live green is 1.72:1 on the purple panel, and the brand's own lime is\n1.54:1 on white. A flat value would be invisible somewhere.\n\nNo --cg-offline-fill, deliberately. Measured, the pair does not work: the\nmuted neutral on a neutral tint is 2.90:1, under the 3:1 bar for a dot or a\nborder. Rather than invent a value to satisfy the ratio, an unplugged chip\nuses the existing surface-card ground with border-control, and --cg-offline\nis the dot on that ground, where it measures 3.28:1. Absence of state should\nnot need a colour of its own.\n\npending joined them on 2026-08-24 (#114) as an ALIAS of expired rather than a\nnew value. Same amber today, separate name, so the two can diverge later with\na one-line edit here and no change in any consumer. Aliases compile to var(),\nso the flip survives.\n\nunhealthy joined them on 2026-08-25 (#122) as a REAL value, not an alias, and\nit is the one tone here that was searched for rather than adopted. Founder\ndecision: expired is terminal, unhealthy is live-and-fixable, and sharing a\ntoken would mean retuning one silently retunes the other. It is a warmer,\nbrighter gold on the same ramp as the amber rather than a new hue family,\nbecause green/amber/red is the traffic light an operator already reads and\n'serving but degraded' is its middle rung. Its own note carries the numbers\nand the three families that were measured and rejected.\n\nTHREE RUNGS PER STATUS, from 2026-09-04 (#149, #150). A chip is a fill, a\nhairline around it, a dot and a word, and until now the set carried only the\nfirst two of those four. -border is the hairline and -strong is the word; the\nbare name stays the dot it has always been.\n\n <status>-fill the chip ground\n <status>-border the hairline around it, a pale tint\n <status> the dot. An indicator, so 3:1, never text\n <status>-strong the word on the fill. Text, so 4.5:1\n\nThe values are the Console's, measured and shipped, not new ones: #149 asked\nfor the rung and console#579 had already been forced to leave two bands as\nliterals for the want of it. Only unhealthy-strong and danger-strong are\nderived, each for a stated reason in its own note.\n\nNo --cg-danger-border and no --cg-unhealthy-border. Neither has ever been drawn\nby any surface — StatusChip's error state has no edge and unhealthy has no band\nat all — so a value here would be invented to complete a grid rather than to\nserve a chip. Same call as --cg-offline-fill above, and the same remedy: add\nthem when something needs them, with the number that made the choice.\n\noffline has no -border and no -strong either, and for the reason it has no\nfill: an unplugged chip sits on the card ground with border-control, and its\nword is --cg-text-primary. Absence of state still does not need a colour.\n\nOn the panel and on ink, -border resolves to the surface's own hairline and\n-strong to its own primary text. A dark ground has no darker rung to reach for,\nand the light tints these scopes carry measure 3.03:1 to 3.60:1 on the panel's\nchip fill, which is a dot and not a word. Worth knowing before retuning: on ink\nthe tints DO clear 4.5:1 on the ink fill (5.48:1 to 8.92:1), so a designer who\nwants the word tinted there changes five entries in surface-ink.json. On the\npanel it is not available at any value — white is 4.31:1 on that fill and white\nis the lightest colour there is, so AA on a panel chip needs the FILL to move.",
56
56
  "online": {
57
57
  "$value": "#00a05c",
58
58
  "$type": "color",
@@ -149,6 +149,84 @@
149
149
  "css": "--cg-danger-fill"
150
150
  }
151
151
  }
152
+ },
153
+ "online-border": {
154
+ "$value": "#abefc6",
155
+ "$type": "color",
156
+ "$description": "The hairline around an online chip. From the Console's OK band, where it has\nshipped as #abefc6 on #ecfdf3 (console#579).\n\nIts own rung rather than the tone, and console#579 is why. Both systems carried\n{tone, fill} pairs and no third value, so converting the band meant sending the\nborder AND the word to --cg-online. Measured, that is not a repaint: the word\nfell from 7.55:1 to 3.06:1 and the border went from this pale tint to full\nsaturation. The band stayed a literal instead. This token is what lets it move.\n\n1.19:1 against --cg-online-fill. A hairline is not text and not an indicator, so\nthere is no floor it has to clear; what it has to do is stay pale, which the\ncontrast guardrail holds with a ceiling rather than a floor.",
157
+ "$extensions": {
158
+ "io.cloudgrid": {
159
+ "css": "--cg-online-border"
160
+ }
161
+ }
162
+ },
163
+ "expired-border": {
164
+ "$value": "#fedf89",
165
+ "$type": "color",
166
+ "$description": "The hairline around an expired or pending chip. From the Console's WARN band,\nwhere it has shipped as #fedf89 on #fffaeb (console#579).\n\n1.18:1 against --cg-expired-fill. Same rung and same reasoning as\n--cg-online-border: collapsing it onto --cg-expired measured 3.99:1, which is\nthe tone doing the border's job.",
167
+ "$extensions": {
168
+ "io.cloudgrid": {
169
+ "css": "--cg-expired-border"
170
+ }
171
+ }
172
+ },
173
+ "pending-border": {
174
+ "$value": "{state.expired-border}",
175
+ "$description": "The hairline around a pending chip, aliased to expired's for the same reason the\ntone and the fill are. Splitting pending from expired later means editing the\nfour pending entries in this file and nothing else.",
176
+ "$extensions": {
177
+ "io.cloudgrid": {
178
+ "css": "--cg-pending-border"
179
+ }
180
+ }
181
+ },
182
+ "online-strong": {
183
+ "$value": "#085d3a",
184
+ "$type": "color",
185
+ "$description": "The WORD on an online chip, and online as running text anywhere. From the\nConsole's OK band. 7.17:1 on --cg-online-fill and 7.96:1 on white.\n\nThis is the rung EntityStatus said the library lacked. --cg-online is 3.40:1 on\nwhite and 3.06:1 on its own fill, which is an indicator and not a foreground, so\nthe component paints the dot with the tone and the word with --cg-text-primary,\nand its own note says a state foreground is a brand decision rather than a\ncomponent's to take. This is that decision, made with a value the Console has\nalready shipped rather than a new one.",
186
+ "$extensions": {
187
+ "io.cloudgrid": {
188
+ "css": "--cg-online-strong"
189
+ }
190
+ }
191
+ },
192
+ "expired-strong": {
193
+ "$value": "#7a3d0a",
194
+ "$type": "color",
195
+ "$description": "The word on an expired chip, and expired as running text. From the Console's\nWARN band. 7.63:1 on --cg-expired-fill and 8.40:1 on white.",
196
+ "$extensions": {
197
+ "io.cloudgrid": {
198
+ "css": "--cg-expired-strong"
199
+ }
200
+ }
201
+ },
202
+ "pending-strong": {
203
+ "$value": "{state.expired-strong}",
204
+ "$description": "The word on a pending chip. Aliased to expired's, like every other pending rung.",
205
+ "$extensions": {
206
+ "io.cloudgrid": {
207
+ "css": "--cg-pending-strong"
208
+ }
209
+ }
210
+ },
211
+ "unhealthy-strong": {
212
+ "$value": "#5d5401",
213
+ "$type": "color",
214
+ "$description": "The word on an unhealthy chip. 7.10:1 on --cg-unhealthy-fill and 7.68:1 on\nwhite.\n\nDERIVED, not adopted. unhealthy is the one tone here the Console never had, so\nthere is no shipped literal to take. This is --cg-unhealthy darkened along its\nown hue and saturation until it reaches the band the two adopted words already\noccupy — 7.17:1 for online and 7.63:1 for expired on their own fills — rather\nthan stopped at the first value that clears 4.5:1, so the words read as one set\ninstead of three matched and one nearly failing.\n\nThe tone is not simply reused because it does not clear the bar: --cg-unhealthy\nmeasures 4.46:1 on this fill. Four hundredths under the floor is under it.\n\ndE2000 to its neighbours in this rung: 20.6 from expired-strong, 23.1 from\nonline-strong. The tightest pair in the whole rung is expired-strong against\ndanger-strong at 19.8, above the 16.5 floor this palette was measured against.",
215
+ "$extensions": {
216
+ "io.cloudgrid": {
217
+ "css": "--cg-unhealthy-strong"
218
+ }
219
+ }
220
+ },
221
+ "danger-strong": {
222
+ "$value": "#c42828",
223
+ "$type": "color",
224
+ "$description": "The word on a danger chip, and danger as running text. 5.02:1 on\n--cg-danger-fill and 5.70:1 on white.\n\n--cg-danger itself is #E53935, which is 4.23:1 on white and 3.72:1 on its own\nfill. It passes as a fill, a border or an icon, where the bar is 3:1, and fails\nas a sentence. The token is deliberately NOT moved to fix that: every fill,\nborder and icon already consuming it would repaint, and the error is that danger\nhad no word rung, not that its tone is wrong.\n\nThe Console's retired --color-error #d42b2b is where this starts and is not\nwhere it lands. It is 5.02:1 on white, but 4.42:1 on --cg-danger-fill — still\nunder the floor on the exact chip that needs it. This is that red darkened along\nits own hue and saturation until it clears 5:1 on the fill, which is the\nsmallest move that holds on both grounds.\n\nKept lighter than the other three words on purpose. It is already the closest\npair in the rung, dE2000 19.8 from expired-strong, and darkening it into their\n7:1 band closes that to 16.6 — a failed chip and an expiring one should not\nconverge.",
225
+ "$extensions": {
226
+ "io.cloudgrid": {
227
+ "css": "--cg-danger-strong"
228
+ }
229
+ }
152
230
  }
153
231
  },
154
232
  "brand-derived": {
@@ -210,6 +288,16 @@
210
288
  }
211
289
  }
212
290
  },
291
+ "neutral-50": {
292
+ "$value": "#f4f6f8",
293
+ "$type": "color",
294
+ "$description": "SOURCE: the Console's --color-canvas, the recessed ground behind a card.\n\nNot a pure grey. It carries a slight blue cast where the rest of this ramp is\nneutral, and that is recorded rather than corrected because the value is what\nlegacy raised panels already render and matching them is the point (#151).",
295
+ "$extensions": {
296
+ "io.cloudgrid": {
297
+ "css": "--cg-neutral-50"
298
+ }
299
+ }
300
+ },
213
301
  "neutral-100": {
214
302
  "$value": "#f1f1f1",
215
303
  "$type": "color",
@@ -304,6 +392,15 @@
304
392
  }
305
393
  }
306
394
  },
395
+ "surface-raised": {
396
+ "$value": "{neutrals.neutral-50}",
397
+ "$description": "A neutral ground one step off white, for a panel that should read as lifted off\nthe page rather than flat on it.\n\n#151: --cg-surface-card is #ffffff and --cg-surface-subtle is the lavender tint,\nso a raised panel had no value-preserving target and every consumer faced the\nsame three-way choice with no right answer — flatten it to white, tint it\nlavender, or pin a literal. This is the fourth answer, and it is the Console's\nown #f4f6f8 rather than a new value.\n\nDeclared at :root only, like --cg-surface-subtle and --cg-surface-control. The\nneutral grounds do not flip: inside a purple panel or on ink, depth is carried\nby --cg-surface-card and the hairline instead. Making it flip is one entry in\neach of the two surface sets.",
398
+ "$extensions": {
399
+ "io.cloudgrid": {
400
+ "css": "--cg-surface-raised"
401
+ }
402
+ }
403
+ },
307
404
  "surface-control": {
308
405
  "$value": "{neutrals.neutral-200}",
309
406
  "$extensions": {
@@ -1164,23 +1261,13 @@
1164
1261
  "volt": {
1165
1262
  "$value": "#b6ff2e",
1166
1263
  "$type": "color",
1167
- "$description": "The live/current INDICATOR — the dot in its halo, and the brand mark. Never\ndecorative, never a fill.\n\nMEASURED 1.21:1 on white, which is below every bar there is. It works on the\npanel (4.82:1) and on ink (13.13:1); on the page ground it reads only because\nit is paired with the ring and, in a pill, with volt-ink. Recorded rather than\nsilently corrected: darkening it would change the Console's appearance, which\nis a design decision and not a contrast fix.\n\nNote it is a SECOND lime beside --cg-secondary (#b6de68), 36 dE away. Which one\nthe brand owns is the open question here.\n\nIt is also a second LIVE. state.online (#00a05c) is the same idea in the state\nscale, and EntityStatus (#116) already paints the live dot with it. They are not\ninterchangeable at any bar — online is 3.40:1 on white, volt is 1.21:1 — so a\nsurface must not swap one for the other on the assumption that both mean\n'serving'. Which name the brand keeps for live belongs to the same open question,\nand until it is answered volt is the Console's brand mark and online is the\nentity's state.",
1264
+ "$description": "The live/current INDICATOR — the dot in its halo, and the brand mark. Never\ndecorative, never a fill.\n\nMEASURED 1.21:1 on white, which is below every bar there is. It works on the\npanel (4.82:1) and on ink (13.13:1); on the page ground it reads only because\nit is paired with volt-ink in a pill. Recorded rather than\nsilently corrected: darkening it would change the Console's appearance, which\nis a design decision and not a contrast fix.\n\nNote it is a SECOND lime beside --cg-secondary (#b6de68), 36 dE away. Which one\nthe brand owns is the open question here.\n\nIt is also a second LIVE. state.online (#00a05c) is the same idea in the state\nscale, and EntityStatus (#116) already paints the live dot with it. They are not\ninterchangeable at any bar — online is 3.40:1 on white, volt is 1.21:1 — so a\nsurface must not swap one for the other on the assumption that both mean\n'serving'. Which name the brand keeps for live belongs to the same open question,\nand until it is answered volt is the Console's brand mark and online is the\nentity's state.",
1168
1265
  "$extensions": {
1169
1266
  "io.cloudgrid": {
1170
1267
  "css": "--cg-volt"
1171
1268
  }
1172
1269
  }
1173
1270
  },
1174
- "volt-ring": {
1175
- "$value": "rgba(182, 255, 46, 0.32)",
1176
- "$type": "color",
1177
- "$description": "The halo behind a volt dot. Its job is to make the dot findable on a light ground, which is what carries the 1.21:1 above.",
1178
- "$extensions": {
1179
- "io.cloudgrid": {
1180
- "css": "--cg-volt-ring"
1181
- }
1182
- }
1183
- },
1184
1271
  "volt-ink": {
1185
1272
  "$value": "#243d00",
1186
1273
  "$type": "color",
@@ -134,7 +134,7 @@
134
134
  }
135
135
  },
136
136
  "state": {
137
- "$description": "state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground.",
137
+ "$description": "state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground.\n\nThe -border and -strong rungs (#149, #150) are the exception to \"measured per surface\": they resolve to this surface's own hairline and its own primary text rather than to a tint of the tone. A dark ground has no darker rung, and the tints above are tuned to be dots — on the panel's chip fill they measure 3.03:1 to 3.60:1, which is an indicator and not a word. On ink they DO clear 4.5:1 on the ink fill (5.48:1 to 8.92:1), so tinting the word there is available and is five edits in this file; on the panel it is not available at any value, because white measures 4.31:1 on that fill and white is the lightest colour there is.",
138
138
  "online": {
139
139
  "$value": "#5FD99A",
140
140
  "$type": "color",
@@ -232,6 +232,70 @@
232
232
  "css": "--cg-danger-fill"
233
233
  }
234
234
  }
235
+ },
236
+ "online-border": {
237
+ "$value": "{semantic-lines.border-hairline}",
238
+ "$extensions": {
239
+ "io.cloudgrid": {
240
+ "css": "--cg-online-border"
241
+ }
242
+ }
243
+ },
244
+ "expired-border": {
245
+ "$value": "{semantic-lines.border-hairline}",
246
+ "$extensions": {
247
+ "io.cloudgrid": {
248
+ "css": "--cg-expired-border"
249
+ }
250
+ }
251
+ },
252
+ "pending-border": {
253
+ "$value": "{state.expired-border}",
254
+ "$extensions": {
255
+ "io.cloudgrid": {
256
+ "css": "--cg-pending-border"
257
+ }
258
+ }
259
+ },
260
+ "online-strong": {
261
+ "$value": "{semantic-text.text-primary}",
262
+ "$extensions": {
263
+ "io.cloudgrid": {
264
+ "css": "--cg-online-strong"
265
+ }
266
+ }
267
+ },
268
+ "expired-strong": {
269
+ "$value": "{semantic-text.text-primary}",
270
+ "$extensions": {
271
+ "io.cloudgrid": {
272
+ "css": "--cg-expired-strong"
273
+ }
274
+ }
275
+ },
276
+ "pending-strong": {
277
+ "$value": "{state.expired-strong}",
278
+ "$extensions": {
279
+ "io.cloudgrid": {
280
+ "css": "--cg-pending-strong"
281
+ }
282
+ }
283
+ },
284
+ "unhealthy-strong": {
285
+ "$value": "{semantic-text.text-primary}",
286
+ "$extensions": {
287
+ "io.cloudgrid": {
288
+ "css": "--cg-unhealthy-strong"
289
+ }
290
+ }
291
+ },
292
+ "danger-strong": {
293
+ "$value": "{semantic-text.text-primary}",
294
+ "$extensions": {
295
+ "io.cloudgrid": {
296
+ "css": "--cg-danger-strong"
297
+ }
298
+ }
235
299
  }
236
300
  },
237
301
  "indicator": {
@@ -118,7 +118,7 @@
118
118
  }
119
119
  },
120
120
  "state": {
121
- "$description": "state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground.",
121
+ "$description": "state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground.\n\nThe -border and -strong rungs (#149, #150) are the exception to \"measured per surface\": they resolve to this surface's own hairline and its own primary text rather than to a tint of the tone. A dark ground has no darker rung, and the tints above are tuned to be dots — on the panel's chip fill they measure 3.03:1 to 3.60:1, which is an indicator and not a word. On ink they DO clear 4.5:1 on the ink fill (5.48:1 to 8.92:1), so tinting the word there is available and is five edits in this file; on the panel it is not available at any value, because white measures 4.31:1 on that fill and white is the lightest colour there is.",
122
122
  "online": {
123
123
  "$value": "#C8F5DC",
124
124
  "$type": "color",
@@ -216,6 +216,70 @@
216
216
  "css": "--cg-danger-fill"
217
217
  }
218
218
  }
219
+ },
220
+ "online-border": {
221
+ "$value": "{semantic-lines.border-hairline}",
222
+ "$extensions": {
223
+ "io.cloudgrid": {
224
+ "css": "--cg-online-border"
225
+ }
226
+ }
227
+ },
228
+ "expired-border": {
229
+ "$value": "{semantic-lines.border-hairline}",
230
+ "$extensions": {
231
+ "io.cloudgrid": {
232
+ "css": "--cg-expired-border"
233
+ }
234
+ }
235
+ },
236
+ "pending-border": {
237
+ "$value": "{state.expired-border}",
238
+ "$extensions": {
239
+ "io.cloudgrid": {
240
+ "css": "--cg-pending-border"
241
+ }
242
+ }
243
+ },
244
+ "online-strong": {
245
+ "$value": "{semantic-text.text-primary}",
246
+ "$extensions": {
247
+ "io.cloudgrid": {
248
+ "css": "--cg-online-strong"
249
+ }
250
+ }
251
+ },
252
+ "expired-strong": {
253
+ "$value": "{semantic-text.text-primary}",
254
+ "$extensions": {
255
+ "io.cloudgrid": {
256
+ "css": "--cg-expired-strong"
257
+ }
258
+ }
259
+ },
260
+ "pending-strong": {
261
+ "$value": "{state.expired-strong}",
262
+ "$extensions": {
263
+ "io.cloudgrid": {
264
+ "css": "--cg-pending-strong"
265
+ }
266
+ }
267
+ },
268
+ "unhealthy-strong": {
269
+ "$value": "{semantic-text.text-primary}",
270
+ "$extensions": {
271
+ "io.cloudgrid": {
272
+ "css": "--cg-unhealthy-strong"
273
+ }
274
+ }
275
+ },
276
+ "danger-strong": {
277
+ "$value": "{semantic-text.text-primary}",
278
+ "$extensions": {
279
+ "io.cloudgrid": {
280
+ "css": "--cg-danger-strong"
281
+ }
282
+ }
219
283
  }
220
284
  },
221
285
  "indicator": {
package/dist/tokens.css CHANGED
@@ -107,7 +107,41 @@
107
107
  brighter gold on the same ramp as the amber rather than a new hue family,
108
108
  because green/amber/red is the traffic light an operator already reads and
109
109
  'serving but degraded' is its middle rung. Its own note carries the numbers
110
- and the three families that were measured and rejected. */
110
+ and the three families that were measured and rejected.
111
+
112
+ THREE RUNGS PER STATUS, from 2026-09-04 (#149, #150). A chip is a fill, a
113
+ hairline around it, a dot and a word, and until now the set carried only the
114
+ first two of those four. -border is the hairline and -strong is the word; the
115
+ bare name stays the dot it has always been.
116
+
117
+ <status>-fill the chip ground
118
+ <status>-border the hairline around it, a pale tint
119
+ <status> the dot. An indicator, so 3:1, never text
120
+ <status>-strong the word on the fill. Text, so 4.5:1
121
+
122
+ The values are the Console's, measured and shipped, not new ones: #149 asked
123
+ for the rung and console#579 had already been forced to leave two bands as
124
+ literals for the want of it. Only unhealthy-strong and danger-strong are
125
+ derived, each for a stated reason in its own note.
126
+
127
+ No --cg-danger-border and no --cg-unhealthy-border. Neither has ever been drawn
128
+ by any surface — StatusChip's error state has no edge and unhealthy has no band
129
+ at all — so a value here would be invented to complete a grid rather than to
130
+ serve a chip. Same call as --cg-offline-fill above, and the same remedy: add
131
+ them when something needs them, with the number that made the choice.
132
+
133
+ offline has no -border and no -strong either, and for the reason it has no
134
+ fill: an unplugged chip sits on the card ground with border-control, and its
135
+ word is --cg-text-primary. Absence of state still does not need a colour.
136
+
137
+ On the panel and on ink, -border resolves to the surface's own hairline and
138
+ -strong to its own primary text. A dark ground has no darker rung to reach for,
139
+ and the light tints these scopes carry measure 3.03:1 to 3.60:1 on the panel's
140
+ chip fill, which is a dot and not a word. Worth knowing before retuning: on ink
141
+ the tints DO clear 4.5:1 on the ink fill (5.48:1 to 8.92:1), so a designer who
142
+ wants the word tinted there changes five entries in surface-ink.json. On the
143
+ panel it is not available at any value — white is 4.31:1 on that fill and white
144
+ is the lightest colour there is, so AA on a panel chip needs the FILL to move. */
111
145
  /*
112
146
  Serving. From the Console's --color-live. 3.40:1 on white — clears the 3:1 bar for an indicator, which is all this is ever used as. Never body text on white. */
113
147
  --cg-online: #00a05c;
@@ -162,6 +196,90 @@
162
196
  /*
163
197
  From the Console's --color-error-fill. --cg-danger measures 3.72:1 on it. danger had no fill before; a chip needs one. */
164
198
  --cg-danger-fill: #fceded;
199
+ /*
200
+ The hairline around an online chip. From the Console's OK band, where it has
201
+ shipped as #abefc6 on #ecfdf3 (console#579).
202
+
203
+ Its own rung rather than the tone, and console#579 is why. Both systems carried
204
+ {tone, fill} pairs and no third value, so converting the band meant sending the
205
+ border AND the word to --cg-online. Measured, that is not a repaint: the word
206
+ fell from 7.55:1 to 3.06:1 and the border went from this pale tint to full
207
+ saturation. The band stayed a literal instead. This token is what lets it move.
208
+
209
+ 1.19:1 against --cg-online-fill. A hairline is not text and not an indicator, so
210
+ there is no floor it has to clear; what it has to do is stay pale, which the
211
+ contrast guardrail holds with a ceiling rather than a floor. */
212
+ --cg-online-border: #abefc6;
213
+ /*
214
+ The hairline around an expired or pending chip. From the Console's WARN band,
215
+ where it has shipped as #fedf89 on #fffaeb (console#579).
216
+
217
+ 1.18:1 against --cg-expired-fill. Same rung and same reasoning as
218
+ --cg-online-border: collapsing it onto --cg-expired measured 3.99:1, which is
219
+ the tone doing the border's job. */
220
+ --cg-expired-border: #fedf89;
221
+ /*
222
+ The hairline around a pending chip, aliased to expired's for the same reason the
223
+ tone and the fill are. Splitting pending from expired later means editing the
224
+ four pending entries in this file and nothing else. */
225
+ --cg-pending-border: var(--cg-expired-border);
226
+ /*
227
+ The WORD on an online chip, and online as running text anywhere. From the
228
+ Console's OK band. 7.17:1 on --cg-online-fill and 7.96:1 on white.
229
+
230
+ This is the rung EntityStatus said the library lacked. --cg-online is 3.40:1 on
231
+ white and 3.06:1 on its own fill, which is an indicator and not a foreground, so
232
+ the component paints the dot with the tone and the word with --cg-text-primary,
233
+ and its own note says a state foreground is a brand decision rather than a
234
+ component's to take. This is that decision, made with a value the Console has
235
+ already shipped rather than a new one. */
236
+ --cg-online-strong: #085d3a;
237
+ /*
238
+ The word on an expired chip, and expired as running text. From the Console's
239
+ WARN band. 7.63:1 on --cg-expired-fill and 8.40:1 on white. */
240
+ --cg-expired-strong: #7a3d0a;
241
+ /*
242
+ The word on a pending chip. Aliased to expired's, like every other pending rung. */
243
+ --cg-pending-strong: var(--cg-expired-strong);
244
+ /*
245
+ The word on an unhealthy chip. 7.10:1 on --cg-unhealthy-fill and 7.68:1 on
246
+ white.
247
+
248
+ DERIVED, not adopted. unhealthy is the one tone here the Console never had, so
249
+ there is no shipped literal to take. This is --cg-unhealthy darkened along its
250
+ own hue and saturation until it reaches the band the two adopted words already
251
+ occupy — 7.17:1 for online and 7.63:1 for expired on their own fills — rather
252
+ than stopped at the first value that clears 4.5:1, so the words read as one set
253
+ instead of three matched and one nearly failing.
254
+
255
+ The tone is not simply reused because it does not clear the bar: --cg-unhealthy
256
+ measures 4.46:1 on this fill. Four hundredths under the floor is under it.
257
+
258
+ dE2000 to its neighbours in this rung: 20.6 from expired-strong, 23.1 from
259
+ online-strong. The tightest pair in the whole rung is expired-strong against
260
+ danger-strong at 19.8, above the 16.5 floor this palette was measured against. */
261
+ --cg-unhealthy-strong: #5d5401;
262
+ /*
263
+ The word on a danger chip, and danger as running text. 5.02:1 on
264
+ --cg-danger-fill and 5.70:1 on white.
265
+
266
+ --cg-danger itself is #E53935, which is 4.23:1 on white and 3.72:1 on its own
267
+ fill. It passes as a fill, a border or an icon, where the bar is 3:1, and fails
268
+ as a sentence. The token is deliberately NOT moved to fix that: every fill,
269
+ border and icon already consuming it would repaint, and the error is that danger
270
+ had no word rung, not that its tone is wrong.
271
+
272
+ The Console's retired --color-error #d42b2b is where this starts and is not
273
+ where it lands. It is 5.02:1 on white, but 4.42:1 on --cg-danger-fill — still
274
+ under the floor on the exact chip that needs it. This is that red darkened along
275
+ its own hue and saturation until it clears 5:1 on the fill, which is the
276
+ smallest move that holds on both grounds.
277
+
278
+ Kept lighter than the other three words on purpose. It is already the closest
279
+ pair in the rung, dE2000 19.8 from expired-strong, and darkening it into their
280
+ 7:1 band closes that to 16.6 — a failed chip and an expiring one should not
281
+ converge. */
282
+ --cg-danger-strong: #c42828;
165
283
 
166
284
  /* ---- brand-derived ----
167
285
  brand derived (interaction only) */
@@ -174,6 +292,13 @@
174
292
  --cg-neutral-0: #ffffff;
175
293
  /* SOURCE: alternate page wash */
176
294
  --cg-lavender-50: #f2f1f7;
295
+ /*
296
+ SOURCE: the Console's --color-canvas, the recessed ground behind a card.
297
+
298
+ Not a pure grey. It carries a slight blue cast where the rest of this ramp is
299
+ neutral, and that is recorded rather than corrected because the value is what
300
+ legacy raised panels already render and matching them is the point (#151). */
301
+ --cg-neutral-50: #f4f6f8;
177
302
  /* SOURCE: hairline / card border */
178
303
  --cg-neutral-100: #f1f1f1;
179
304
  /* SOURCE: control border, inactive pill fill */
@@ -192,6 +317,21 @@
192
317
  --cg-surface-page-alt: var(--cg-lavender-50);
193
318
  --cg-surface-card: var(--cg-neutral-0);
194
319
  --cg-surface-subtle: var(--cg-lavender-50);
320
+ /*
321
+ A neutral ground one step off white, for a panel that should read as lifted off
322
+ the page rather than flat on it.
323
+
324
+ #151: --cg-surface-card is #ffffff and --cg-surface-subtle is the lavender tint,
325
+ so a raised panel had no value-preserving target and every consumer faced the
326
+ same three-way choice with no right answer — flatten it to white, tint it
327
+ lavender, or pin a literal. This is the fourth answer, and it is the Console's
328
+ own #f4f6f8 rather than a new value.
329
+
330
+ Declared at :root only, like --cg-surface-subtle and --cg-surface-control. The
331
+ neutral grounds do not flip: inside a purple panel or on ink, depth is carried
332
+ by --cg-surface-card and the hairline instead. Making it flip is one entry in
333
+ each of the two surface sets. */
334
+ --cg-surface-raised: var(--cg-neutral-50);
195
335
  --cg-surface-control: var(--cg-neutral-200);
196
336
  --cg-surface-panel: var(--cg-primary);
197
337
  --cg-surface-code: var(--cg-ink);
@@ -353,7 +493,7 @@
353
493
 
354
494
  MEASURED 1.21:1 on white, which is below every bar there is. It works on the
355
495
  panel (4.82:1) and on ink (13.13:1); on the page ground it reads only because
356
- it is paired with the ring and, in a pill, with volt-ink. Recorded rather than
496
+ it is paired with volt-ink in a pill. Recorded rather than
357
497
  silently corrected: darkening it would change the Console's appearance, which
358
498
  is a design decision and not a contrast fix.
359
499
 
@@ -368,9 +508,6 @@
368
508
  and until it is answered volt is the Console's brand mark and online is the
369
509
  entity's state. */
370
510
  --cg-volt: #b6ff2e;
371
- /*
372
- The halo behind a volt dot. Its job is to make the dot findable on a light ground, which is what carries the 1.21:1 above. */
373
- --cg-volt-ring: rgba(182, 255, 46, 0.32);
374
511
  /*
375
512
  Text and glyphs ON volt. 9.95:1 against it — the one part of the volt family that
376
513
  is comfortably legible.
@@ -423,7 +560,9 @@
423
560
  --cg-danger: #FFDAD6;
424
561
 
425
562
  /* ---- state ----
426
- state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground. */
563
+ state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground.
564
+
565
+ The -border and -strong rungs (#149, #150) are the exception to "measured per surface": they resolve to this surface's own hairline and its own primary text rather than to a tint of the tone. A dark ground has no darker rung, and the tints above are tuned to be dots — on the panel's chip fill they measure 3.03:1 to 3.60:1, which is an indicator and not a word. On ink they DO clear 4.5:1 on the ink fill (5.48:1 to 8.92:1), so tinting the word there is available and is five edits in this file; on the panel it is not available at any value, because white measures 4.31:1 on that fill and white is the lightest colour there is. */
427
566
  /*
428
567
  4.88:1 on the purple. The source green is 1.72:1 here — invisible, same class as the danger override. */
429
568
  --cg-online: #C8F5DC;
@@ -458,6 +597,14 @@
458
597
  /*
459
598
  Translucent white rather than a tint; the panel foreground measures 3.35:1 on it. */
460
599
  --cg-danger-fill: rgba(255, 255, 255, 0.16);
600
+ --cg-online-border: var(--cg-border-hairline);
601
+ --cg-expired-border: var(--cg-border-hairline);
602
+ --cg-pending-border: var(--cg-expired-border);
603
+ --cg-online-strong: var(--cg-text-primary);
604
+ --cg-expired-strong: var(--cg-text-primary);
605
+ --cg-pending-strong: var(--cg-expired-strong);
606
+ --cg-unhealthy-strong: var(--cg-text-primary);
607
+ --cg-danger-strong: var(--cg-text-primary);
461
608
 
462
609
  /* ---- indicator ----
463
610
  indicator + chrome: only hover flips. A dark wash is invisible on a dark ground. */
@@ -496,7 +643,9 @@
496
643
  --cg-danger: #FF8A80;
497
644
 
498
645
  /* ---- state ----
499
- state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground. */
646
+ state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground.
647
+
648
+ The -border and -strong rungs (#149, #150) are the exception to "measured per surface": they resolve to this surface's own hairline and its own primary text rather than to a tint of the tone. A dark ground has no darker rung, and the tints above are tuned to be dots — on the panel's chip fill they measure 3.03:1 to 3.60:1, which is an indicator and not a word. On ink they DO clear 4.5:1 on the ink fill (5.48:1 to 8.92:1), so tinting the word there is available and is five edits in this file; on the panel it is not available at any value, because white measures 4.31:1 on that fill and white is the lightest colour there is. */
500
649
  /*
501
650
  9.00:1 on #222. Ink has headroom, so this stays a recognisable green rather than washing to mint. */
502
651
  --cg-online: #5FD99A;
@@ -523,6 +672,14 @@
523
672
  --cg-unhealthy-fill: rgba(255, 255, 255, 0.08);
524
673
  /* Translucent white; the ink foreground measures 5.45:1 on it. */
525
674
  --cg-danger-fill: rgba(255, 255, 255, 0.08);
675
+ --cg-online-border: var(--cg-border-hairline);
676
+ --cg-expired-border: var(--cg-border-hairline);
677
+ --cg-pending-border: var(--cg-expired-border);
678
+ --cg-online-strong: var(--cg-text-primary);
679
+ --cg-expired-strong: var(--cg-text-primary);
680
+ --cg-pending-strong: var(--cg-expired-strong);
681
+ --cg-unhealthy-strong: var(--cg-text-primary);
682
+ --cg-danger-strong: var(--cg-text-primary);
526
683
 
527
684
  /* ---- indicator ----
528
685
  indicator + chrome: only hover flips. A dark wash is invisible on a dark ground. */
package/dist/tokens.json CHANGED
@@ -18,12 +18,21 @@
18
18
  "--cg-pending-fill": "var(--cg-expired-fill)",
19
19
  "--cg-unhealthy-fill": "#fdf6df",
20
20
  "--cg-danger-fill": "#fceded",
21
+ "--cg-online-border": "#abefc6",
22
+ "--cg-expired-border": "#fedf89",
23
+ "--cg-pending-border": "var(--cg-expired-border)",
24
+ "--cg-online-strong": "#085d3a",
25
+ "--cg-expired-strong": "#7a3d0a",
26
+ "--cg-pending-strong": "var(--cg-expired-strong)",
27
+ "--cg-unhealthy-strong": "#5d5401",
28
+ "--cg-danger-strong": "#c42828",
21
29
  "--cg-primary-press": "#4b39db",
22
30
  "--cg-primary-tint": "#eeecfe",
23
31
  "--cg-secondary-press": "#a6cf57",
24
32
  "--cg-tertiary-press": "#4dd8e0",
25
33
  "--cg-neutral-0": "#ffffff",
26
34
  "--cg-lavender-50": "#f2f1f7",
35
+ "--cg-neutral-50": "#f4f6f8",
27
36
  "--cg-neutral-100": "#f1f1f1",
28
37
  "--cg-neutral-200": "#e3e3e3",
29
38
  "--cg-neutral-400": "#acacac",
@@ -34,6 +43,7 @@
34
43
  "--cg-surface-page-alt": "var(--cg-lavender-50)",
35
44
  "--cg-surface-card": "var(--cg-neutral-0)",
36
45
  "--cg-surface-subtle": "var(--cg-lavender-50)",
46
+ "--cg-surface-raised": "var(--cg-neutral-50)",
37
47
  "--cg-surface-control": "var(--cg-neutral-200)",
38
48
  "--cg-surface-panel": "var(--cg-primary)",
39
49
  "--cg-surface-code": "var(--cg-ink)",
@@ -127,7 +137,6 @@
127
137
  "--cg-lift-hover": "translateY(-1px)",
128
138
  "--cg-press-scale": "0.985",
129
139
  "--cg-volt": "#b6ff2e",
130
- "--cg-volt-ring": "rgba(182, 255, 46, 0.32)",
131
140
  "--cg-volt-ink": "#243d00",
132
141
  "--cg-scrim": "rgba(34, 34, 34, 0.4)",
133
142
  "--cg-hover": "rgba(34, 34, 34, 0.045)"
@@ -155,6 +164,14 @@
155
164
  "--cg-pending-fill": "var(--cg-expired-fill)",
156
165
  "--cg-unhealthy-fill": "rgba(255, 255, 255, 0.16)",
157
166
  "--cg-danger-fill": "rgba(255, 255, 255, 0.16)",
167
+ "--cg-online-border": "var(--cg-border-hairline)",
168
+ "--cg-expired-border": "var(--cg-border-hairline)",
169
+ "--cg-pending-border": "var(--cg-expired-border)",
170
+ "--cg-online-strong": "var(--cg-text-primary)",
171
+ "--cg-expired-strong": "var(--cg-text-primary)",
172
+ "--cg-pending-strong": "var(--cg-expired-strong)",
173
+ "--cg-unhealthy-strong": "var(--cg-text-primary)",
174
+ "--cg-danger-strong": "var(--cg-text-primary)",
158
175
  "--cg-hover": "rgba(255, 255, 255, 0.10)"
159
176
  },
160
177
  "ink": {
@@ -182,6 +199,14 @@
182
199
  "--cg-pending-fill": "var(--cg-expired-fill)",
183
200
  "--cg-unhealthy-fill": "rgba(255, 255, 255, 0.08)",
184
201
  "--cg-danger-fill": "rgba(255, 255, 255, 0.08)",
202
+ "--cg-online-border": "var(--cg-border-hairline)",
203
+ "--cg-expired-border": "var(--cg-border-hairline)",
204
+ "--cg-pending-border": "var(--cg-expired-border)",
205
+ "--cg-online-strong": "var(--cg-text-primary)",
206
+ "--cg-expired-strong": "var(--cg-text-primary)",
207
+ "--cg-pending-strong": "var(--cg-expired-strong)",
208
+ "--cg-unhealthy-strong": "var(--cg-text-primary)",
209
+ "--cg-danger-strong": "var(--cg-text-primary)",
185
210
  "--cg-hover": "rgba(255, 255, 255, 0.06)"
186
211
  }
187
212
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudgrid-io/ui",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "CloudGrid design system: React components, brand design tokens, Tailwind theme, self-hosted fonts.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/",