@cloudgrid-io/ui 0.13.0 → 0.15.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.
@@ -0,0 +1,16 @@
1
+ export interface CelebrationProps {
2
+ /**
3
+ * Called once, when the burst has finished and the overlay is gone. Under
4
+ * reduced motion it is called immediately instead.
5
+ */
6
+ onDone?: () => void;
7
+ /** Milliseconds. Read once at mount, clamped. */
8
+ duration?: number;
9
+ /** Read once at mount, clamped. */
10
+ particleCount?: number;
11
+ /** Applied to the overlay canvas. */
12
+ className?: string;
13
+ }
14
+ declare function Celebration({ onDone, duration, particleCount, className }: CelebrationProps): null;
15
+ export { Celebration };
16
+ //# sourceMappingURL=celebration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"celebration.d.ts","sourceRoot":"","sources":["../../src/blocks/celebration.tsx"],"names":[],"mappings":"AAgIA,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mCAAmC;IACnC,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,qCAAqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAwCD,iBAAS,WAAW,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,SAAS,EAAE,EAAE,gBAAgB,QAqJpF;AAED,OAAO,EAAE,WAAW,EAAE,CAAA"}
@@ -0,0 +1,259 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ /**
4
+ * One burst of confetti, and then it is gone.
5
+ *
6
+ * ## Why this exists (#160)
7
+ *
8
+ * The onboarding welcome screen wanted a celebration. Written inline in the
9
+ * console it would be a canvas animation carrying its own hardcoded palette,
10
+ * living beside the screen that happens to use it first, and a second screen
11
+ * that wanted one would get a second copy. So it is a component, and its colours
12
+ * come out of the token layer like everything else here.
13
+ *
14
+ * ## What it does
15
+ *
16
+ * Mounts, paints a single burst over the viewport, removes itself, and calls
17
+ * `onDone`. It never loops, never fires twice, and never restarts because its
18
+ * parent re-rendered or handed it a new prop. Mount it to celebrate; unmount is
19
+ * not required, since it takes its own overlay away when the burst is over.
20
+ *
21
+ * ```tsx
22
+ * {justPlugged ? <Celebration onDone={() => setJustPlugged(false)} /> : null}
23
+ * ```
24
+ *
25
+ * ## Reduced motion
26
+ *
27
+ * `prefers-reduced-motion: reduce` renders nothing at all — no canvas is
28
+ * created — and `onDone` fires immediately, on the same tick as the mount. That
29
+ * second half is the important one: a caller may gate a screen transition on
30
+ * `onDone`, and it must not sit for 1.8s waiting for an animation that is never
31
+ * going to run. The celebration is decoration; the screen is the message.
32
+ *
33
+ * A **static accent** — the same burst painted once and held, rather than
34
+ * animated — is the obvious future alternative, and it would slot in here with
35
+ * no API change: same mount, same `onDone`, one `drawFrame` call instead of a
36
+ * loop. It is deliberately not shipped yet, because an accent that only
37
+ * reduced-motion users ever see is a second design and nobody has drawn it.
38
+ *
39
+ * ## Colour
40
+ *
41
+ * `--cg-primary`, `--cg-secondary` and `--cg-tertiary`, resolved off the
42
+ * document root once at mount. Canvas takes colour strings rather than CSS
43
+ * variables, so the read has to happen somewhere; doing it at mount keeps it off
44
+ * the per-frame path and means a re-theme moves the confetti with everything
45
+ * else. The ROOT is read deliberately, not the nearest surface: this overlay
46
+ * belongs to the viewport, not to whichever panel mounted it.
47
+ *
48
+ * If none of the three resolve — a consumer who never imported `tokens.css` —
49
+ * the fallback is the overlay's own inherited text colour. Never a hardcoded
50
+ * one: a brand effect that invents a brand is exactly what the token layer
51
+ * exists to prevent.
52
+ *
53
+ * ## What it costs
54
+ *
55
+ * Nothing at import time, no dependency beyond React, and no node in the
56
+ * caller's tree — the component renders `null` and the overlay hangs off
57
+ * `document.body`. That last part is not a shortcut. A `position: fixed`
58
+ * element is positioned against the nearest ancestor carrying a `transform`,
59
+ * `filter` or `will-change`, and an onboarding screen is exactly the kind of
60
+ * surface that animates its own container, so an overlay rendered in place
61
+ * would cover the viewport everywhere it was tested and cover a card where it
62
+ * shipped.
63
+ *
64
+ * ## Props
65
+ *
66
+ * - `onDone` — called once, when the burst is over. Not called if the component
67
+ * is unmounted mid-burst: the caller who unmounted it already knows.
68
+ * - `duration` — milliseconds, default {@link DEFAULT_DURATION_MS}, clamped to
69
+ * {@link MIN_DURATION_MS}–{@link MAX_DURATION_MS}. A celebration is a moment,
70
+ * not a state.
71
+ * - `particleCount` — default {@link DEFAULT_PARTICLE_COUNT}, capped at
72
+ * {@link MAX_PARTICLE_COUNT}.
73
+ * - `className` — lands on the overlay canvas. Its position, inset,
74
+ * pointer-events and stacking are inline and win over a utility, because those
75
+ * four are the contract rather than a default.
76
+ *
77
+ * `duration` and `particleCount` are read ONCE, at mount. Changing them later
78
+ * does nothing, deliberately: a burst that restarted when its parent re-rendered
79
+ * with a new number is the "repeats on re-render" defect wearing a plausible
80
+ * face. `onDone` is the exception and is always the current one.
81
+ */
82
+ /** A celebration is a moment. Long enough to register, short enough to leave. */
83
+ const DEFAULT_DURATION_MS = 1800;
84
+ const MIN_DURATION_MS = 300;
85
+ const MAX_DURATION_MS = 8000;
86
+ /** Conservative on purpose: this runs on the first screen a new person sees. */
87
+ const DEFAULT_PARTICLE_COUNT = 90;
88
+ const MAX_PARTICLE_COUNT = 400;
89
+ /** The brand ramp, in the order the burst cycles it. */
90
+ const RAMP = ["--cg-primary", "--cg-secondary", "--cg-tertiary"];
91
+ /**
92
+ * Above application chrome, below a browser's own UI. Inline rather than a
93
+ * token because it is a stacking decision, not a brand one.
94
+ */
95
+ const OVERLAY_Z_INDEX = 9999;
96
+ /** Per frame at 60fps, which is what `dt` normalises to. */
97
+ const GRAVITY = 0.4;
98
+ const DRAG = 0.99;
99
+ /** The share of the burst that plays at full opacity before it fades out. */
100
+ const FADE_FROM = 0.7;
101
+ /** Retina is worth paying for; a 3x phone is not, for 90 rectangles. */
102
+ const MAX_PIXEL_RATIO = 2;
103
+ const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
104
+ /**
105
+ * A cone of scraps thrown up from a little above the middle of the screen.
106
+ *
107
+ * Minted once per burst and kept, so a React strict-mode remount resumes the
108
+ * same confetti rather than throwing a second handful. See the effect.
109
+ */
110
+ function mint(count, width, height, palette) {
111
+ const originX = width / 2;
112
+ const originY = height * 0.45;
113
+ const particles = [];
114
+ for (let i = 0; i < count; i++) {
115
+ // Straight up, give or take a radian either way. Wide enough to read as a
116
+ // burst, narrow enough that most of it stays on the screen it decorates.
117
+ const angle = -Math.PI / 2 + (Math.random() - 0.5) * 2;
118
+ const speed = 6 + Math.random() * 8;
119
+ particles.push({
120
+ x: originX + (Math.random() - 0.5) * width * 0.1,
121
+ y: originY + (Math.random() - 0.5) * 20,
122
+ vx: Math.cos(angle) * speed,
123
+ vy: Math.sin(angle) * speed,
124
+ rot: Math.random() * Math.PI * 2,
125
+ spin: (Math.random() - 0.5) * 0.3,
126
+ w: 6 + Math.random() * 6,
127
+ h: 9 + Math.random() * 7,
128
+ wobble: Math.random() * Math.PI * 2,
129
+ wobbleRate: 0.08 + Math.random() * 0.1,
130
+ colour: palette[i % palette.length],
131
+ });
132
+ }
133
+ return particles;
134
+ }
135
+ function Celebration({ onDone, duration, particleCount, className }) {
136
+ // The callback is kept fresh; the numbers are not. See the props note above.
137
+ const onDoneRef = React.useRef(onDone);
138
+ React.useEffect(() => {
139
+ onDoneRef.current = onDone;
140
+ });
141
+ // useRef keeps its FIRST argument for the life of the component, which is
142
+ // exactly "read once at mount" without a second hook to say so.
143
+ const settings = React.useRef({ duration, particleCount, className });
144
+ /**
145
+ * The burst's own state, on a ref so it survives what React does to effects.
146
+ *
147
+ * Strict mode runs an effect, cleans it up, and runs it again. The naive guard
148
+ * — a boolean that makes the second run a no-op — is worse than none: the
149
+ * first run's cleanup has already cancelled the frame and removed the canvas,
150
+ * so it produces a component that animates nothing in development and
151
+ * correctly in production.
152
+ *
153
+ * One burst means one CLOCK, not one effect run. `startedAt` and `particles`
154
+ * are minted on the first run only, so a second run re-creates the canvas and
155
+ * resumes the same confetti at the elapsed time the clock says. `finished`
156
+ * short-circuits everything after, so `onDone` cannot fire twice.
157
+ */
158
+ const run = React.useRef({ startedAt: null, particles: null, finished: false });
159
+ React.useEffect(() => {
160
+ const state = run.current;
161
+ if (state.finished)
162
+ return;
163
+ const finish = () => {
164
+ if (state.finished)
165
+ return;
166
+ state.finished = true;
167
+ onDoneRef.current?.();
168
+ };
169
+ const reduced = typeof window.matchMedia === "function" &&
170
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches;
171
+ if (reduced) {
172
+ finish();
173
+ return;
174
+ }
175
+ const durationMs = clamp(settings.current.duration ?? DEFAULT_DURATION_MS, MIN_DURATION_MS, MAX_DURATION_MS);
176
+ const count = Math.round(clamp(settings.current.particleCount ?? DEFAULT_PARTICLE_COUNT, 0, MAX_PARTICLE_COUNT));
177
+ const canvas = document.createElement("canvas");
178
+ canvas.dataset.slot = "celebration";
179
+ canvas.setAttribute("aria-hidden", "true");
180
+ if (settings.current.className)
181
+ canvas.className = settings.current.className;
182
+ // width/height are not redundant beside inset: a <canvas> is a REPLACED
183
+ // element, and an absolutely positioned replaced box with auto width keeps
184
+ // its INTRINSIC size rather than stretching to its offsets.
185
+ canvas.style.cssText =
186
+ "position:fixed;inset:0;width:100%;height:100%;" +
187
+ `pointer-events:none;z-index:${OVERLAY_Z_INDEX}`;
188
+ document.body.appendChild(canvas);
189
+ const ctx = canvas.getContext("2d");
190
+ if (!ctx) {
191
+ canvas.remove();
192
+ finish();
193
+ return;
194
+ }
195
+ const width = window.innerWidth;
196
+ const height = window.innerHeight;
197
+ const ratio = Math.min(window.devicePixelRatio || 1, MAX_PIXEL_RATIO);
198
+ canvas.width = Math.round(width * ratio);
199
+ canvas.height = Math.round(height * ratio);
200
+ ctx.scale(ratio, ratio);
201
+ const rootStyle = getComputedStyle(document.documentElement);
202
+ const palette = RAMP.map((name) => rootStyle.getPropertyValue(name).trim()).filter(Boolean);
203
+ if (palette.length === 0)
204
+ palette.push(getComputedStyle(canvas).color);
205
+ if (state.particles === null)
206
+ state.particles = mint(count, width, height, palette);
207
+ if (state.startedAt === null)
208
+ state.startedAt = performance.now();
209
+ const particles = state.particles;
210
+ const startedAt = state.startedAt;
211
+ let last = performance.now();
212
+ let frame = 0;
213
+ const step = (now) => {
214
+ const progress = (now - startedAt) / durationMs;
215
+ if (progress >= 1) {
216
+ canvas.remove();
217
+ finish();
218
+ return;
219
+ }
220
+ // Normalised to a 60fps frame and capped, so a backgrounded tab resumes
221
+ // rather than teleporting every scrap off the bottom of the screen.
222
+ const dt = Math.min(now - last, 64) / (1000 / 60);
223
+ last = now;
224
+ ctx.clearRect(0, 0, width, height);
225
+ ctx.globalAlpha =
226
+ progress < FADE_FROM ? 1 : 1 - (progress - FADE_FROM) / (1 - FADE_FROM);
227
+ for (const p of particles) {
228
+ p.vy += GRAVITY * dt;
229
+ p.vx *= DRAG ** dt;
230
+ p.vy *= DRAG ** dt;
231
+ p.x += p.vx * dt;
232
+ p.y += p.vy * dt;
233
+ p.rot += p.spin * dt;
234
+ p.wobble += p.wobbleRate * dt;
235
+ ctx.save();
236
+ ctx.translate(p.x, p.y);
237
+ ctx.rotate(p.rot);
238
+ ctx.scale(1, Math.cos(p.wobble));
239
+ ctx.fillStyle = p.colour;
240
+ ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
241
+ ctx.restore();
242
+ }
243
+ frame = requestAnimationFrame(step);
244
+ };
245
+ frame = requestAnimationFrame(step);
246
+ return () => {
247
+ cancelAnimationFrame(frame);
248
+ canvas.remove();
249
+ };
250
+ // Mount only. Every prop this effect reads is read through a ref, precisely
251
+ // so that a changed prop cannot re-run it and start a second burst.
252
+ // eslint-disable-next-line react-hooks/exhaustive-deps
253
+ }, []);
254
+ // Nothing in the caller's tree, on the server or on the client. The overlay is
255
+ // the effect's, and it lives on document.body.
256
+ return null;
257
+ }
258
+ export { Celebration };
259
+ //# sourceMappingURL=celebration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"celebration.js","sourceRoot":"","sources":["../../src/blocks/celebration.tsx"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -23,6 +23,7 @@ export * from "./ui/textarea.js";
23
23
  export * from "./ui/tooltip.js";
24
24
  export * from "./blocks/agent-prompt.js";
25
25
  export * from "./blocks/card-grid.js";
26
+ export * from "./blocks/celebration.js";
26
27
  export * from "./blocks/compare-table.js";
27
28
  export * from "./blocks/creation-tile.js";
28
29
  export * from "./blocks/cta-banner.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiBA,cAAc,mBAAmB,CAAA;AACjC,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,uBAAuB,CAAA;AACrC,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,sBAAsB,CAAA;AACpC,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,mBAAmB,CAAA;AACjC,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA;AAG/B,cAAc,0BAA0B,CAAA;AACxC,cAAc,uBAAuB,CAAA;AACrC,cAAc,2BAA2B,CAAA;AACzC,cAAc,2BAA2B,CAAA;AACzC,cAAc,wBAAwB,CAAA;AACtC,cAAc,uBAAuB,CAAA;AACrC,cAAc,yBAAyB,CAAA;AACvC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,2BAA2B,CAAA;AACzC,cAAc,2BAA2B,CAAA;AACzC,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,yBAAyB,CAAA;AACvC,cAAc,0BAA0B,CAAA;AACxC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,yBAAyB,CAAA;AACvC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,2BAA2B,CAAA;AACzC,cAAc,mBAAmB,CAAA;AACjC,cAAc,uBAAuB,CAAA;AACrC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,yBAAyB,CAAA;AACvC,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,wBAAwB,CAAA;AACtC,cAAc,wBAAwB,CAAA;AAGtC,cAAc,uBAAuB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiBA,cAAc,mBAAmB,CAAA;AACjC,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,uBAAuB,CAAA;AACrC,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,sBAAsB,CAAA;AACpC,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,mBAAmB,CAAA;AACjC,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA;AAG/B,cAAc,0BAA0B,CAAA;AACxC,cAAc,uBAAuB,CAAA;AACrC,cAAc,yBAAyB,CAAA;AACvC,cAAc,2BAA2B,CAAA;AACzC,cAAc,2BAA2B,CAAA;AACzC,cAAc,wBAAwB,CAAA;AACtC,cAAc,uBAAuB,CAAA;AACrC,cAAc,yBAAyB,CAAA;AACvC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,2BAA2B,CAAA;AACzC,cAAc,2BAA2B,CAAA;AACzC,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,yBAAyB,CAAA;AACvC,cAAc,0BAA0B,CAAA;AACxC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,yBAAyB,CAAA;AACvC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,2BAA2B,CAAA;AACzC,cAAc,mBAAmB,CAAA;AACjC,cAAc,uBAAuB,CAAA;AACrC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,yBAAyB,CAAA;AACvC,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,wBAAwB,CAAA;AACtC,cAAc,wBAAwB,CAAA;AAGtC,cAAc,uBAAuB,CAAA"}
package/dist/index.js CHANGED
@@ -40,6 +40,7 @@ export * from "./ui/tooltip.js";
40
40
  // Composed blocks.
41
41
  export * from "./blocks/agent-prompt.js";
42
42
  export * from "./blocks/card-grid.js";
43
+ export * from "./blocks/celebration.js";
43
44
  export * from "./blocks/compare-table.js";
44
45
  export * from "./blocks/creation-tile.js";
45
46
  export * from "./blocks/cta-banner.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAC1C,EAAE;AACF,gFAAgF;AAChF,8EAA8E;AAC9E,oEAAoE;AACpE,EAAE;AACF,gFAAgF;AAChF,+EAA+E;AAC/E,yEAAyE;AACzE,gFAAgF;AAChF,4EAA4E;AAC5E,EAAE;AACF,kEAAkE;AAClE,8EAA8E;AAC9E,uEAAuE;AAEvE,cAAc;AACd,cAAc,mBAAmB,CAAA;AACjC,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,uBAAuB,CAAA;AACrC,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,sBAAsB,CAAA;AACpC,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,mBAAmB,CAAA;AACjC,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA;AAE/B,mBAAmB;AACnB,cAAc,0BAA0B,CAAA;AACxC,cAAc,uBAAuB,CAAA;AACrC,cAAc,2BAA2B,CAAA;AACzC,cAAc,2BAA2B,CAAA;AACzC,cAAc,wBAAwB,CAAA;AACtC,cAAc,uBAAuB,CAAA;AACrC,cAAc,yBAAyB,CAAA;AACvC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,2BAA2B,CAAA;AACzC,cAAc,2BAA2B,CAAA;AACzC,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,yBAAyB,CAAA;AACvC,cAAc,0BAA0B,CAAA;AACxC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,yBAAyB,CAAA;AACvC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,2BAA2B,CAAA;AACzC,cAAc,mBAAmB,CAAA;AACjC,cAAc,uBAAuB,CAAA;AACrC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,yBAAyB,CAAA;AACvC,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,wBAAwB,CAAA;AACtC,cAAc,wBAAwB,CAAA;AAEtC,0EAA0E;AAC1E,cAAc,uBAAuB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAC1C,EAAE;AACF,gFAAgF;AAChF,8EAA8E;AAC9E,oEAAoE;AACpE,EAAE;AACF,gFAAgF;AAChF,+EAA+E;AAC/E,yEAAyE;AACzE,gFAAgF;AAChF,4EAA4E;AAC5E,EAAE;AACF,kEAAkE;AAClE,8EAA8E;AAC9E,uEAAuE;AAEvE,cAAc;AACd,cAAc,mBAAmB,CAAA;AACjC,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,uBAAuB,CAAA;AACrC,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,sBAAsB,CAAA;AACpC,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,mBAAmB,CAAA;AACjC,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA;AAE/B,mBAAmB;AACnB,cAAc,0BAA0B,CAAA;AACxC,cAAc,uBAAuB,CAAA;AACrC,cAAc,yBAAyB,CAAA;AACvC,cAAc,2BAA2B,CAAA;AACzC,cAAc,2BAA2B,CAAA;AACzC,cAAc,wBAAwB,CAAA;AACtC,cAAc,uBAAuB,CAAA;AACrC,cAAc,yBAAyB,CAAA;AACvC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,2BAA2B,CAAA;AACzC,cAAc,2BAA2B,CAAA;AACzC,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,yBAAyB,CAAA;AACvC,cAAc,0BAA0B,CAAA;AACxC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,yBAAyB,CAAA;AACvC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,2BAA2B,CAAA;AACzC,cAAc,mBAAmB,CAAA;AACjC,cAAc,uBAAuB,CAAA;AACrC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,yBAAyB,CAAA;AACvC,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,wBAAwB,CAAA;AACtC,cAAc,wBAAwB,CAAA;AAEtC,0EAA0E;AAC1E,cAAc,uBAAuB,CAAA"}
package/dist/theme.css CHANGED
@@ -147,16 +147,18 @@
147
147
  gives: a token whose utility lands a release later is a token the first
148
148
  consumer reaches for and does not find.
149
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. */
150
+ The grid is whole as of #154: danger and unhealthy have borders now, so the
151
+ absence this comment used to record is gone. --color-offline-fill is still
152
+ the one deliberate hole, and still for #111's reason. */
153
153
  --color-online-border: var(--cg-online-border);
154
154
  --color-online-strong: var(--cg-online-strong);
155
155
  --color-expired-border: var(--cg-expired-border);
156
156
  --color-expired-strong: var(--cg-expired-strong);
157
157
  --color-pending-border: var(--cg-pending-border);
158
158
  --color-pending-strong: var(--cg-pending-strong);
159
+ --color-unhealthy-border: var(--cg-unhealthy-border);
159
160
  --color-unhealthy-strong: var(--cg-unhealthy-strong);
161
+ --color-danger-border: var(--cg-danger-border);
160
162
  --color-danger-strong: var(--cg-danger-strong);
161
163
 
162
164
  /* ---- lines ---- */
@@ -273,6 +275,50 @@
273
275
  --shadow-hover: var(--cg-shadow-hover);
274
276
  --shadow-panel: var(--cg-shadow-panel);
275
277
  --shadow-focus: var(--cg-ring-focus);
278
+ /* The live dot's halo (#154), paired with --cg-live-halo the way --shadow-focus
279
+ is paired with --cg-focus-ring. */
280
+ --shadow-live: var(--cg-ring-live);
281
+
282
+ /* Tailwind's OWN three rungs, pointed at the brand's (#155).
283
+
284
+ This block is not a rename and not a convenience. Until it existed, the
285
+ elevation of this package's own components was decided by the CONSUMER:
286
+ dropdown-menu is authored with `shadow-md` and `shadow-lg`, select and
287
+ combobox with `shadow-md`, tabs with `shadow-sm`, and theme.css declared
288
+ none of those three names. So in a consumer that declares nothing, every
289
+ library dropdown and select renders Tailwind stock — measured,
290
+ `0 10px 15px -3px #0000001a, 0 4px 6px -4px #0000001a` for shadow-lg — which
291
+ is far heavier and greyer than anything this brand ships. Nothing errors and
292
+ nothing looks broken; the components are just not on the brand.
293
+
294
+ The console was carrying three lines exactly like these in its own
295
+ globals.css to hold this package's components on this package's elevation.
296
+ A consumer paying to fix the library is the shape #149, #150 and #151 all
297
+ had, and the answer is the same: it belongs in the token file.
298
+
299
+ Forwarded, not repointed. The alternative was to rewrite the four component
300
+ class strings onto `shadow-card` / `shadow-raised`, which would have changed
301
+ the compiled class NAMES and so changed what the console renders today. This
302
+ way the compiled rules are byte-identical to what the console's own forwards
303
+ produce, and every other consumer gets the brand values without knowing this
304
+ was ever a problem. Proof in docs/test-results/.
305
+
306
+ The role mapping is the console's, unchanged: sm and md are the card
307
+ elevation, lg is the overlay lift. sm and md landing on one value is not an
308
+ oversight — this system contains one card elevation, and the console had
309
+ already collapsed its own three-tier ramp onto it.
310
+
311
+ xs, xl and 2xl are deliberately left on Tailwind stock. No component here
312
+ uses them, and forwarding a rung nothing consumes is a value invented to
313
+ complete a grid.
314
+
315
+ KNOWN: a forwarded rung loses Tailwind's --tw-shadow-color fallback, so
316
+ `shadow-red-500/20` stops tinting shadow-md. That is already true of
317
+ shadow-card and shadow-raised, and this palette has no colour a shadow would
318
+ be tinted with. */
319
+ --shadow-sm: var(--cg-shadow-card);
320
+ --shadow-md: var(--cg-shadow-card);
321
+ --shadow-lg: var(--cg-shadow-raised);
276
322
 
277
323
  /* ---- spacing ----
278
324
  --spacing sets the 4px base, so the numeric utilities (p-4, gap-6) line up
@@ -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.\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.",
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\nFOUR RUNGS PER STATUS as of 2026-09-05 (#154). --cg-danger-border and\n--cg-unhealthy-border were the two holes #152 left, on the reasoning above: at\nthe time nothing drew either one, so a value would have been invented to complete\na grid rather than to serve a chip. That note said what would change the answer —\n\"add them when something needs them, with the number that made the choice\" — and\nthis is it. Both are DERIVED rather than adopted, because neither has a shipped\nliteral anywhere, and each carries its number in its own note.\n\nThe derivation is one rule, and it is the two adopted borders' own measured\nposition rather than a recipe: a -border is its own tone's hue, at the lightness\nthat reproduces the 1.18-1.19:1 those two hold against their own fills, carrying\nthe mean of their chroma (OKLCH 0.0998) or the most sRGB has at that hue and\nlightness, whichever is smaller. Danger's hue hits the second clause and\nunhealthy's does not; both notes show the sweep.\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",
@@ -170,6 +170,26 @@
170
170
  }
171
171
  }
172
172
  },
173
+ "unhealthy-border": {
174
+ "$value": "#eee596",
175
+ "$type": "color",
176
+ "$description": "The hairline around an unhealthy chip. 1.19:1 against --cg-unhealthy-fill and\n1.28:1 on white.\n\nDERIVED, like unhealthy-strong and for the same reason: unhealthy is the one\ntone here the Console never had, so there is no shipped literal to take.\n\nThe rule is the two adopted borders' own position, measured rather than\ninvented. #abefc6 and #fedf89 are not mixes of their tone and their fill — the\nclosest sRGB mix reproduces them at dE2000 5.5 and 13.1 — but they sit in one\nnarrow band: OKLCH L 0.8958 and 0.9112, chroma 0.0886 and 0.1109, and 1.19:1\nand 1.18:1 against their own fills. So a new border is its own tone's hue at the\nlightness that reproduces that ratio, carrying the mean of their chroma (0.0998)\nor the most sRGB has at that hue and lightness, whichever is smaller. Here the\nhue has room: L 0.911, C 0.0998, H 102.17.\n\nKNOWN, measured before it was accepted rather than after: dE2000 6.3 from\nexpired-border, which is far under the 16.5 floor the tones in this palette were\nchosen against. It is not a defect, and the reason is that this rung already\nbehaves this way — expired-fill and unhealthy-fill ship 2.4 apart, so the border\nis MORE separated than the ground it surrounds. 16.5 is a floor for the rungs\nthat carry the signal, the dot and the word, which measure 22.3 and 20.6 for this\npair. At the pale end an amber and a gold are the same colour: swept across the\nwhole hue's usable chroma, the best separation available anywhere in the band is\n11.6, and buying it costs the pale tint that IS this rung's definition. Same\nfinding #122 recorded for the tones on a dark ground.\n\nNot an alias of expired-border, though the two are close enough that it would\nrender almost identically today. #122's whole decision was that unhealthy must\nnot be an alias of expired: a designer retuning one must not silently retune the\nother.",
177
+ "$extensions": {
178
+ "io.cloudgrid": {
179
+ "css": "--cg-unhealthy-border"
180
+ }
181
+ }
182
+ },
183
+ "danger-border": {
184
+ "$value": "#ffd3cd",
185
+ "$type": "color",
186
+ "$description": "The hairline around a danger chip. 1.19:1 against --cg-danger-fill and 1.35:1 on\nwhite.\n\nDERIVED by the rule unhealthy-border's note sets out, and the interesting part is\nwhere the rule runs out. --cg-danger is OKLCH H 27.03, and at the lightness the\nrule asks for, sRGB carries at most C 0.0506 on that hue — half the 0.0998 the\ntwo adopted borders carry. There is no paler red with more chroma; the gamut\nends. So this is the second clause of the rule doing its work: L 0.904 at the\nmost chroma available, which puts the red channel at 255.\n\nMeasured across the band, so the choice is visible:\n\n L 0.896 maxC 0.0551 #ffd0c9 1.22:1 on fill\n L 0.904 maxC 0.0506 #ffd3cd 1.19:1 on fill <- taken\n L 0.912 maxC 0.0461 #ffd7d2 1.16:1 on fill\n\ndE2000 to its neighbours in this rung: 38.6 from online-border, 27.3 from\nunhealthy-border, 25.0 from expired-border. It is the best separated of the four,\nwhich is the right way round — a failed chip is the one a reader must not\nmisread.\n\n#152 declined to add this, and correctly: at the time nothing drew it, and the\n`state` group's note says a value would then be invented to complete a grid\nrather than to serve a chip. That note also says what would change the answer —\n\"add them when something needs them, with the number that made the choice\" — and\nui#154 is that. The number is above.",
187
+ "$extensions": {
188
+ "io.cloudgrid": {
189
+ "css": "--cg-danger-border"
190
+ }
191
+ }
192
+ },
173
193
  "pending-border": {
174
194
  "$value": "{state.expired-border}",
175
195
  "$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.",
@@ -1179,6 +1199,16 @@
1179
1199
  "css": "--cg-ring-focus"
1180
1200
  }
1181
1201
  }
1202
+ },
1203
+ "ring-live": {
1204
+ "$value": "0 0 0 4px var(--cg-live-halo)",
1205
+ "$description": "The live dot's halo as a ready-made box-shadow, so a surface draws the mark with\none token instead of restating the geometry.\n\nSame pair as --cg-focus-ring and --cg-ring-focus above: the colour lives in the\nindicator group, the shadow that spends it lives here. 4px rather than the focus\nring's 3px because that is the spread the mark ships at — the Console writes\n`box-shadow: 0 0 0 4px var(--color-volt-ring)` at all seven call sites, so the\ngeometry was already one value repeated seven times and is now one token.\n\nA dot larger than 8px wants a larger halo; that is a call site's business, and it\nsets its own box-shadow with --cg-live-halo rather than this rung.",
1206
+ "$extensions": {
1207
+ "io.cloudgrid": {
1208
+ "raw": true,
1209
+ "css": "--cg-ring-live"
1210
+ }
1211
+ }
1182
1212
  }
1183
1213
  },
1184
1214
  "motion": {
@@ -1278,6 +1308,16 @@
1278
1308
  }
1279
1309
  }
1280
1310
  },
1311
+ "live-halo": {
1312
+ "$value": "color-mix(in srgb, var(--cg-volt) 32%, transparent)",
1313
+ "$description": "The soft ring around a live dot. The other half of the mark --cg-volt's note\ndescribes: \"the dot in its halo\".\n\nDERIVED FROM THE DOT, not written out, and that is the whole reason this exists\nrather than the value being pasted back. Computed it is the Console's own\nrgba(182, 255, 46, 0.32) — 32% of #b6ff2e over white is #e8ffbc, which is what\nships today — but written as a literal it would be a third place #b6ff2e is\nspelled out, and the next volt retune would leave an old-hue halo around a\nnew-hue dot with nothing anywhere going red.\n\nA ROLE name rather than --cg-volt-ring, which #149 purged as volt residue. The\npurge was right about this package (nothing here read it) and the name was the\nresidue: a token named for the hue cannot survive the hue being renamed, and\n--cg-volt's own note records that which lime the brand owns is still open. This\none is deliberately downstream of that question — whatever answers it, the halo\nfollows.\n\nWhat the purge missed is that the Console DOES read it, seven times: .cg-live-dot\nplus StatusChip, DiscoverRails, EngagementHeadline, MyStuffLoopStrip,\nViewsTrendChart and GridStatusBoard, each setting the dot and its box-shadow\ntogether. console#591 kept all three volt names rather than convert two thirds of\none mark, which is what ui#154 reports.\n\nNo contrast obligation of its own: it is a halo, not a foreground, and the dot it\nsurrounds is the signal. Declared at :root only, like --cg-volt itself, because a\nbrand mark is not a state and does not flip per surface.",
1314
+ "$extensions": {
1315
+ "io.cloudgrid": {
1316
+ "raw": true,
1317
+ "css": "--cg-live-halo"
1318
+ }
1319
+ }
1320
+ },
1281
1321
  "scrim": {
1282
1322
  "$value": "rgba(34, 34, 34, 0.4)",
1283
1323
  "$type": "color",
@@ -249,6 +249,24 @@
249
249
  }
250
250
  }
251
251
  },
252
+ "unhealthy-border": {
253
+ "$value": "{semantic-lines.border-hairline}",
254
+ "$description": "The hairline around an unhealthy chip on this surface. The surface's own hairline, for the reason online-border and expired-border take it: a dark ground has no paler rung of the tone to reach for, and the light tint this scope carries is a dot.",
255
+ "$extensions": {
256
+ "io.cloudgrid": {
257
+ "css": "--cg-unhealthy-border"
258
+ }
259
+ }
260
+ },
261
+ "danger-border": {
262
+ "$value": "{semantic-lines.border-hairline}",
263
+ "$description": "The hairline around a danger chip on this surface. Same rule as every other -border here: the surface's own hairline.",
264
+ "$extensions": {
265
+ "io.cloudgrid": {
266
+ "css": "--cg-danger-border"
267
+ }
268
+ }
269
+ },
252
270
  "pending-border": {
253
271
  "$value": "{state.expired-border}",
254
272
  "$extensions": {
@@ -233,6 +233,24 @@
233
233
  }
234
234
  }
235
235
  },
236
+ "unhealthy-border": {
237
+ "$value": "{semantic-lines.border-hairline}",
238
+ "$description": "The hairline around an unhealthy chip on this surface. The surface's own hairline, for the reason online-border and expired-border take it: a dark ground has no paler rung of the tone to reach for, and the light tint this scope carries is a dot.",
239
+ "$extensions": {
240
+ "io.cloudgrid": {
241
+ "css": "--cg-unhealthy-border"
242
+ }
243
+ }
244
+ },
245
+ "danger-border": {
246
+ "$value": "{semantic-lines.border-hairline}",
247
+ "$description": "The hairline around a danger chip on this surface. Same rule as every other -border here: the surface's own hairline.",
248
+ "$extensions": {
249
+ "io.cloudgrid": {
250
+ "css": "--cg-danger-border"
251
+ }
252
+ }
253
+ },
236
254
  "pending-border": {
237
255
  "$value": "{state.expired-border}",
238
256
  "$extensions": {
package/dist/tokens.css CHANGED
@@ -124,11 +124,20 @@
124
124
  literals for the want of it. Only unhealthy-strong and danger-strong are
125
125
  derived, each for a stated reason in its own note.
126
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.
127
+ FOUR RUNGS PER STATUS as of 2026-09-05 (#154). --cg-danger-border and
128
+ --cg-unhealthy-border were the two holes #152 left, on the reasoning above: at
129
+ the time nothing drew either one, so a value would have been invented to complete
130
+ a grid rather than to serve a chip. That note said what would change the answer
131
+ "add them when something needs them, with the number that made the choice" — and
132
+ this is it. Both are DERIVED rather than adopted, because neither has a shipped
133
+ literal anywhere, and each carries its number in its own note.
134
+
135
+ The derivation is one rule, and it is the two adopted borders' own measured
136
+ position rather than a recipe: a -border is its own tone's hue, at the lightness
137
+ that reproduces the 1.18-1.19:1 those two hold against their own fills, carrying
138
+ the mean of their chroma (OKLCH 0.0998) or the most sRGB has at that hue and
139
+ lightness, whichever is smaller. Danger's hue hits the second clause and
140
+ unhealthy's does not; both notes show the sweep.
132
141
 
133
142
  offline has no -border and no -strong either, and for the reason it has no
134
143
  fill: an unplugged chip sits on the card ground with border-control, and its
@@ -218,6 +227,66 @@
218
227
  --cg-online-border: collapsing it onto --cg-expired measured 3.99:1, which is
219
228
  the tone doing the border's job. */
220
229
  --cg-expired-border: #fedf89;
230
+ /*
231
+ The hairline around an unhealthy chip. 1.19:1 against --cg-unhealthy-fill and
232
+ 1.28:1 on white.
233
+
234
+ DERIVED, like unhealthy-strong and for the same reason: unhealthy is the one
235
+ tone here the Console never had, so there is no shipped literal to take.
236
+
237
+ The rule is the two adopted borders' own position, measured rather than
238
+ invented. #abefc6 and #fedf89 are not mixes of their tone and their fill — the
239
+ closest sRGB mix reproduces them at dE2000 5.5 and 13.1 — but they sit in one
240
+ narrow band: OKLCH L 0.8958 and 0.9112, chroma 0.0886 and 0.1109, and 1.19:1
241
+ and 1.18:1 against their own fills. So a new border is its own tone's hue at the
242
+ lightness that reproduces that ratio, carrying the mean of their chroma (0.0998)
243
+ or the most sRGB has at that hue and lightness, whichever is smaller. Here the
244
+ hue has room: L 0.911, C 0.0998, H 102.17.
245
+
246
+ KNOWN, measured before it was accepted rather than after: dE2000 6.3 from
247
+ expired-border, which is far under the 16.5 floor the tones in this palette were
248
+ chosen against. It is not a defect, and the reason is that this rung already
249
+ behaves this way — expired-fill and unhealthy-fill ship 2.4 apart, so the border
250
+ is MORE separated than the ground it surrounds. 16.5 is a floor for the rungs
251
+ that carry the signal, the dot and the word, which measure 22.3 and 20.6 for this
252
+ pair. At the pale end an amber and a gold are the same colour: swept across the
253
+ whole hue's usable chroma, the best separation available anywhere in the band is
254
+ 11.6, and buying it costs the pale tint that IS this rung's definition. Same
255
+ finding #122 recorded for the tones on a dark ground.
256
+
257
+ Not an alias of expired-border, though the two are close enough that it would
258
+ render almost identically today. #122's whole decision was that unhealthy must
259
+ not be an alias of expired: a designer retuning one must not silently retune the
260
+ other. */
261
+ --cg-unhealthy-border: #eee596;
262
+ /*
263
+ The hairline around a danger chip. 1.19:1 against --cg-danger-fill and 1.35:1 on
264
+ white.
265
+
266
+ DERIVED by the rule unhealthy-border's note sets out, and the interesting part is
267
+ where the rule runs out. --cg-danger is OKLCH H 27.03, and at the lightness the
268
+ rule asks for, sRGB carries at most C 0.0506 on that hue — half the 0.0998 the
269
+ two adopted borders carry. There is no paler red with more chroma; the gamut
270
+ ends. So this is the second clause of the rule doing its work: L 0.904 at the
271
+ most chroma available, which puts the red channel at 255.
272
+
273
+ Measured across the band, so the choice is visible:
274
+
275
+ L 0.896 maxC 0.0551 #ffd0c9 1.22:1 on fill
276
+ L 0.904 maxC 0.0506 #ffd3cd 1.19:1 on fill <- taken
277
+ L 0.912 maxC 0.0461 #ffd7d2 1.16:1 on fill
278
+
279
+ dE2000 to its neighbours in this rung: 38.6 from online-border, 27.3 from
280
+ unhealthy-border, 25.0 from expired-border. It is the best separated of the four,
281
+ which is the right way round — a failed chip is the one a reader must not
282
+ misread.
283
+
284
+ #152 declined to add this, and correctly: at the time nothing drew it, and the
285
+ `state` group's note says a value would then be invented to complete a grid
286
+ rather than to serve a chip. That note also says what would change the answer —
287
+ "add them when something needs them, with the number that made the choice" — and
288
+ ui#154 is that. The number is above. */
289
+ --cg-danger-border: #ffd3cd;
221
290
  /*
222
291
  The hairline around a pending chip, aliased to expired's for the same reason the
223
292
  tone and the fill are. Splitting pending from expired later means editing the
@@ -466,6 +535,19 @@
466
535
  --cg-shadow-hover: 0 8px 24px rgba(34, 34, 34, 0.08);
467
536
  --cg-shadow-panel: 0 10px 40px rgba(90, 71, 240, 0.18);
468
537
  --cg-ring-focus: 0 0 0 3px var(--cg-focus-ring);
538
+ /*
539
+ The live dot's halo as a ready-made box-shadow, so a surface draws the mark with
540
+ one token instead of restating the geometry.
541
+
542
+ Same pair as --cg-focus-ring and --cg-ring-focus above: the colour lives in the
543
+ indicator group, the shadow that spends it lives here. 4px rather than the focus
544
+ ring's 3px because that is the spread the mark ships at — the Console writes
545
+ `box-shadow: 0 0 0 4px var(--color-volt-ring)` at all seven call sites, so the
546
+ geometry was already one value repeated seven times and is now one token.
547
+
548
+ A dot larger than 8px wants a larger halo; that is a call site's business, and it
549
+ sets its own box-shadow with --cg-live-halo rather than this rung. */
550
+ --cg-ring-live: 0 0 0 4px var(--cg-live-halo);
469
551
 
470
552
  /* ---- motion ----
471
553
  motion: small and quick, nothing slides in */
@@ -519,6 +601,34 @@
519
601
  and belongs to volt. Adding a state -ink is still a brand decision and still
520
602
  open. */
521
603
  --cg-volt-ink: #243d00;
604
+ /*
605
+ The soft ring around a live dot. The other half of the mark --cg-volt's note
606
+ describes: "the dot in its halo".
607
+
608
+ DERIVED FROM THE DOT, not written out, and that is the whole reason this exists
609
+ rather than the value being pasted back. Computed it is the Console's own
610
+ rgba(182, 255, 46, 0.32) — 32% of #b6ff2e over white is #e8ffbc, which is what
611
+ ships today — but written as a literal it would be a third place #b6ff2e is
612
+ spelled out, and the next volt retune would leave an old-hue halo around a
613
+ new-hue dot with nothing anywhere going red.
614
+
615
+ A ROLE name rather than --cg-volt-ring, which #149 purged as volt residue. The
616
+ purge was right about this package (nothing here read it) and the name was the
617
+ residue: a token named for the hue cannot survive the hue being renamed, and
618
+ --cg-volt's own note records that which lime the brand owns is still open. This
619
+ one is deliberately downstream of that question — whatever answers it, the halo
620
+ follows.
621
+
622
+ What the purge missed is that the Console DOES read it, seven times: .cg-live-dot
623
+ plus StatusChip, DiscoverRails, EngagementHeadline, MyStuffLoopStrip,
624
+ ViewsTrendChart and GridStatusBoard, each setting the dot and its box-shadow
625
+ together. console#591 kept all three volt names rather than convert two thirds of
626
+ one mark, which is what ui#154 reports.
627
+
628
+ No contrast obligation of its own: it is a halo, not a foreground, and the dot it
629
+ surrounds is the signal. Declared at :root only, like --cg-volt itself, because a
630
+ brand mark is not a state and does not flip per surface. */
631
+ --cg-live-halo: color-mix(in srgb, var(--cg-volt) 32%, transparent);
522
632
  /*
523
633
  The wash behind a modal or overlay. Uses the BRAND ink (#222222) rather than the
524
634
  Console's (#16181d) — a scrim sits above content and belongs to the system, not
@@ -599,6 +709,12 @@
599
709
  --cg-danger-fill: rgba(255, 255, 255, 0.16);
600
710
  --cg-online-border: var(--cg-border-hairline);
601
711
  --cg-expired-border: var(--cg-border-hairline);
712
+ /*
713
+ The hairline around an unhealthy chip on this surface. The surface's own hairline, for the reason online-border and expired-border take it: a dark ground has no paler rung of the tone to reach for, and the light tint this scope carries is a dot. */
714
+ --cg-unhealthy-border: var(--cg-border-hairline);
715
+ /*
716
+ The hairline around a danger chip on this surface. Same rule as every other -border here: the surface's own hairline. */
717
+ --cg-danger-border: var(--cg-border-hairline);
602
718
  --cg-pending-border: var(--cg-expired-border);
603
719
  --cg-online-strong: var(--cg-text-primary);
604
720
  --cg-expired-strong: var(--cg-text-primary);
@@ -674,6 +790,12 @@
674
790
  --cg-danger-fill: rgba(255, 255, 255, 0.08);
675
791
  --cg-online-border: var(--cg-border-hairline);
676
792
  --cg-expired-border: var(--cg-border-hairline);
793
+ /*
794
+ The hairline around an unhealthy chip on this surface. The surface's own hairline, for the reason online-border and expired-border take it: a dark ground has no paler rung of the tone to reach for, and the light tint this scope carries is a dot. */
795
+ --cg-unhealthy-border: var(--cg-border-hairline);
796
+ /*
797
+ The hairline around a danger chip on this surface. Same rule as every other -border here: the surface's own hairline. */
798
+ --cg-danger-border: var(--cg-border-hairline);
677
799
  --cg-pending-border: var(--cg-expired-border);
678
800
  --cg-online-strong: var(--cg-text-primary);
679
801
  --cg-expired-strong: var(--cg-text-primary);
package/dist/tokens.json CHANGED
@@ -20,6 +20,8 @@
20
20
  "--cg-danger-fill": "#fceded",
21
21
  "--cg-online-border": "#abefc6",
22
22
  "--cg-expired-border": "#fedf89",
23
+ "--cg-unhealthy-border": "#eee596",
24
+ "--cg-danger-border": "#ffd3cd",
23
25
  "--cg-pending-border": "var(--cg-expired-border)",
24
26
  "--cg-online-strong": "#085d3a",
25
27
  "--cg-expired-strong": "#7a3d0a",
@@ -128,6 +130,7 @@
128
130
  "--cg-shadow-hover": "0 8px 24px rgba(34, 34, 34, 0.08)",
129
131
  "--cg-shadow-panel": "0 10px 40px rgba(90, 71, 240, 0.18)",
130
132
  "--cg-ring-focus": "0 0 0 3px var(--cg-focus-ring)",
133
+ "--cg-ring-live": "0 0 0 4px var(--cg-live-halo)",
131
134
  "--cg-duration-fast": "120ms",
132
135
  "--cg-duration": "180ms",
133
136
  "--cg-duration-slow": "280ms",
@@ -138,6 +141,7 @@
138
141
  "--cg-press-scale": "0.985",
139
142
  "--cg-volt": "#b6ff2e",
140
143
  "--cg-volt-ink": "#243d00",
144
+ "--cg-live-halo": "color-mix(in srgb, var(--cg-volt) 32%, transparent)",
141
145
  "--cg-scrim": "rgba(34, 34, 34, 0.4)",
142
146
  "--cg-hover": "rgba(34, 34, 34, 0.045)"
143
147
  },
@@ -166,6 +170,8 @@
166
170
  "--cg-danger-fill": "rgba(255, 255, 255, 0.16)",
167
171
  "--cg-online-border": "var(--cg-border-hairline)",
168
172
  "--cg-expired-border": "var(--cg-border-hairline)",
173
+ "--cg-unhealthy-border": "var(--cg-border-hairline)",
174
+ "--cg-danger-border": "var(--cg-border-hairline)",
169
175
  "--cg-pending-border": "var(--cg-expired-border)",
170
176
  "--cg-online-strong": "var(--cg-text-primary)",
171
177
  "--cg-expired-strong": "var(--cg-text-primary)",
@@ -201,6 +207,8 @@
201
207
  "--cg-danger-fill": "rgba(255, 255, 255, 0.08)",
202
208
  "--cg-online-border": "var(--cg-border-hairline)",
203
209
  "--cg-expired-border": "var(--cg-border-hairline)",
210
+ "--cg-unhealthy-border": "var(--cg-border-hairline)",
211
+ "--cg-danger-border": "var(--cg-border-hairline)",
204
212
  "--cg-pending-border": "var(--cg-expired-border)",
205
213
  "--cg-online-strong": "var(--cg-text-primary)",
206
214
  "--cg-expired-strong": "var(--cg-text-primary)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudgrid-io/ui",
3
- "version": "0.13.0",
3
+ "version": "0.15.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/",