@cloudgrid-io/ui 0.6.0 → 0.7.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.
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * ## The mapping is a table, and the table is the thing a designer edits
10
10
  *
11
- * Nine statuses, five tones. `ENTITY_STATUS_TONE` is the whole mapping and
11
+ * Ten statuses, six tones. `ENTITY_STATUS_TONE` is the whole mapping and
12
12
  * nothing else in this file branches on a status, so retuning what colour
13
13
  * `paused` is means editing one line. `ENTITY_STATUS_LABEL` is the same
14
14
  * arrangement for the words.
@@ -41,7 +41,7 @@
41
41
  *
42
42
  * ## Grounds
43
43
  *
44
- * Four tones sit on their own fill. `offline` sits on the card ground with a
44
+ * Five tones sit on their own fill. `offline` sits on the card ground with a
45
45
  * control border instead, which is #111's decision rather than this component's:
46
46
  * the muted neutral on a neutral tint measures 2.90:1, under the bar, so absence
47
47
  * of state deliberately has no fill of its own.
@@ -55,10 +55,10 @@
55
55
  * both tables below and typecheck says so, which is the only arrangement where
56
56
  * a half-added status cannot ship.
57
57
  */
58
- export declare const ENTITY_STATUS_ORDER: readonly ["live", "building", "charged", "draft", "paused", "stopped", "archived", "expired", "error"];
58
+ export declare const ENTITY_STATUS_ORDER: readonly ["live", "unhealthy", "building", "charged", "draft", "paused", "stopped", "archived", "expired", "error"];
59
59
  export type EntityStatusName = (typeof ENTITY_STATUS_ORDER)[number];
60
- /** The five families of the state palette. One token pair each. */
61
- export type EntityStatusTone = "online" | "pending" | "offline" | "expired" | "danger";
60
+ /** The six families of the state palette. One token pair each. */
61
+ export type EntityStatusTone = "online" | "unhealthy" | "pending" | "offline" | "expired" | "danger";
62
62
  /**
63
63
  * THE MAPPING. Edit this to change what a status looks like.
64
64
  *
@@ -80,6 +80,14 @@ export type EntityStatusTone = "online" | "pending" | "offline" | "expired" | "d
80
80
  * `live` is the library's green, explicitly not the console's lime: the brand
81
81
  * has already spent lime on decoration, so a lime "live" would not read as a
82
82
  * signal.
83
+ *
84
+ * `unhealthy` is its OWN tone, added 2026-08-25 (#122). Before it existed the
85
+ * console had nowhere to put `live_unhealthy` and folded it onto `live`, so a
86
+ * degraded entity rendered green and a broken app looked healthy unless you
87
+ * read the word. It is deliberately not `expired` and not `danger`: expired is
88
+ * terminal, danger is failed, and this one is still serving and still fixable.
89
+ * `--cg-unhealthy` is a real value rather than an alias, so retuning it cannot
90
+ * move either of them.
83
91
  */
84
92
  export declare const ENTITY_STATUS_TONE: Record<EntityStatusName, EntityStatusTone>;
85
93
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"entity-status.d.ts","sourceRoot":"","sources":["../../src/blocks/entity-status.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,wGAUtB,CAAA;AAEV,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEnE,mEAAmE;AACnE,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAA;AAEtF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,gBAAgB,EAAE,gBAAgB,CAUzE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAUhE,CAAA;AAkBD,iBAAS,YAAY,CAAC,EACpB,MAAM,EACN,KAAK,EACL,SAAS,EACT,GAAG,KAAK,EACT,EAAE,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,GAAG;IAClD,MAAM,EAAE,gBAAgB,CAAA;IACxB,sFAAsF;IACtF,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CACxB,+BAqCA;AAED,OAAO,EAAE,YAAY,EAAE,CAAA"}
1
+ {"version":3,"file":"entity-status.d.ts","sourceRoot":"","sources":["../../src/blocks/entity-status.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,qHAWtB,CAAA;AAEV,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEnE,kEAAkE;AAClE,MAAM,MAAM,gBAAgB,GACxB,QAAQ,GACR,WAAW,GACX,SAAS,GACT,SAAS,GACT,SAAS,GACT,QAAQ,CAAA;AAEZ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,gBAAgB,EAAE,gBAAgB,CAWzE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAehE,CAAA;AAmBD,iBAAS,YAAY,CAAC,EACpB,MAAM,EACN,KAAK,EACL,SAAS,EACT,GAAG,KAAK,EACT,EAAE,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,GAAG;IAClD,MAAM,EAAE,gBAAgB,CAAA;IACxB,sFAAsF;IACtF,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CACxB,+BAqCA;AAED,OAAO,EAAE,YAAY,EAAE,CAAA"}
@@ -10,7 +10,7 @@ import { cn } from "../lib/cloudgrid-cn.js";
10
10
  *
11
11
  * ## The mapping is a table, and the table is the thing a designer edits
12
12
  *
13
- * Nine statuses, five tones. `ENTITY_STATUS_TONE` is the whole mapping and
13
+ * Ten statuses, six tones. `ENTITY_STATUS_TONE` is the whole mapping and
14
14
  * nothing else in this file branches on a status, so retuning what colour
15
15
  * `paused` is means editing one line. `ENTITY_STATUS_LABEL` is the same
16
16
  * arrangement for the words.
@@ -43,7 +43,7 @@ import { cn } from "../lib/cloudgrid-cn.js";
43
43
  *
44
44
  * ## Grounds
45
45
  *
46
- * Four tones sit on their own fill. `offline` sits on the card ground with a
46
+ * Five tones sit on their own fill. `offline` sits on the card ground with a
47
47
  * control border instead, which is #111's decision rather than this component's:
48
48
  * the muted neutral on a neutral tint measures 2.90:1, under the bar, so absence
49
49
  * of state deliberately has no fill of its own.
@@ -59,6 +59,7 @@ import { cn } from "../lib/cloudgrid-cn.js";
59
59
  */
60
60
  export const ENTITY_STATUS_ORDER = [
61
61
  "live",
62
+ "unhealthy",
62
63
  "building",
63
64
  "charged",
64
65
  "draft",
@@ -89,9 +90,18 @@ export const ENTITY_STATUS_ORDER = [
89
90
  * `live` is the library's green, explicitly not the console's lime: the brand
90
91
  * has already spent lime on decoration, so a lime "live" would not read as a
91
92
  * signal.
93
+ *
94
+ * `unhealthy` is its OWN tone, added 2026-08-25 (#122). Before it existed the
95
+ * console had nowhere to put `live_unhealthy` and folded it onto `live`, so a
96
+ * degraded entity rendered green and a broken app looked healthy unless you
97
+ * read the word. It is deliberately not `expired` and not `danger`: expired is
98
+ * terminal, danger is failed, and this one is still serving and still fixable.
99
+ * `--cg-unhealthy` is a real value rather than an alias, so retuning it cannot
100
+ * move either of them.
92
101
  */
93
102
  export const ENTITY_STATUS_TONE = {
94
103
  live: "online",
104
+ unhealthy: "unhealthy",
95
105
  building: "pending",
96
106
  charged: "offline",
97
107
  draft: "offline",
@@ -108,6 +118,11 @@ export const ENTITY_STATUS_TONE = {
108
118
  */
109
119
  export const ENTITY_STATUS_LABEL = {
110
120
  live: "Live",
121
+ // Not "Live, degraded". The word is the whole meaning when the colour is
122
+ // unfamiliar, and a comma makes it read as a variant of Live rather than as
123
+ // its own state. A surface that knows more passes `label`, e.g. "Failing
124
+ // health checks".
125
+ unhealthy: "Unhealthy",
111
126
  building: "Building",
112
127
  charged: "Charged",
113
128
  draft: "Draft",
@@ -126,6 +141,7 @@ export const ENTITY_STATUS_LABEL = {
126
141
  */
127
142
  const TONE_STYLE = {
128
143
  online: { chip: "border-transparent bg-online-fill", dot: "bg-online" },
144
+ unhealthy: { chip: "border-transparent bg-unhealthy-fill", dot: "bg-unhealthy" },
129
145
  pending: { chip: "border-transparent bg-pending-fill", dot: "bg-pending" },
130
146
  expired: { chip: "border-transparent bg-expired-fill", dot: "bg-expired" },
131
147
  danger: { chip: "border-transparent bg-danger-fill", dot: "bg-danger" },
@@ -1 +1 @@
1
- {"version":3,"file":"entity-status.js","sourceRoot":"","sources":["../../src/blocks/entity-status.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,oBAAoB,CAAA;AAEvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,MAAM;IACN,UAAU;IACV,SAAS;IACT,OAAO;IACP,QAAQ;IACR,SAAS;IACT,UAAU;IACV,SAAS;IACT,OAAO;CACC,CAAA;AAOV;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAA+C;IAC5E,IAAI,EAAE,QAAQ;IACd,QAAQ,EAAE,SAAS;IACnB,OAAO,EAAE,SAAS;IAClB,KAAK,EAAE,SAAS;IAChB,MAAM,EAAE,SAAS;IACjB,OAAO,EAAE,SAAS;IAClB,QAAQ,EAAE,SAAS;IACnB,OAAO,EAAE,SAAS;IAClB,KAAK,EAAE,QAAQ;CAChB,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAqC;IACnE,IAAI,EAAE,MAAM;IACZ,QAAQ,EAAE,UAAU;IACpB,OAAO,EAAE,SAAS;IAClB,KAAK,EAAE,OAAO;IACd,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,SAAS;IAClB,QAAQ,EAAE,UAAU;IACpB,OAAO,EAAE,SAAS;IAClB,KAAK,EAAE,OAAO;CACf,CAAA;AAED;;;;;;GAMG;AACH,MAAM,UAAU,GAA4D;IAC1E,MAAM,EAAE,EAAE,IAAI,EAAE,mCAAmC,EAAE,GAAG,EAAE,WAAW,EAAE;IACvE,OAAO,EAAE,EAAE,IAAI,EAAE,oCAAoC,EAAE,GAAG,EAAE,YAAY,EAAE;IAC1E,OAAO,EAAE,EAAE,IAAI,EAAE,oCAAoC,EAAE,GAAG,EAAE,YAAY,EAAE;IAC1E,MAAM,EAAE,EAAE,IAAI,EAAE,mCAAmC,EAAE,GAAG,EAAE,WAAW,EAAE;IACvE,8DAA8D;IAC9D,OAAO,EAAE,EAAE,IAAI,EAAE,qBAAqB,EAAE,GAAG,EAAE,YAAY,EAAE;CAC5D,CAAA;AAED,SAAS,YAAY,CAAC,EACpB,MAAM,EACN,KAAK,EACL,SAAS,EACT,GAAG,KAAK,EAKT;IACC,MAAM,IAAI,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAA;IACvC,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,CAAA;IAE9B,OAAO,CACL,6BACY,eAAe,iBAIZ,MAAM,eACR,IAAI,EACf,SAAS,EAAE,EAAE,CACX,uIAAuI,EACvI,KAAK,CAAC,IAAI,EACV,SAAS,CACV,KACG,KAAK,aAET,8BAGc,MAAM,eACR,mBAAmB,EAC7B,SAAS,EAAE,EAAE,CACX,gCAAgC,EAChC,KAAK,CAAC,GAAG;gBACT,uEAAuE;gBACvE,uEAAuE;gBACvE,qEAAqE;gBACrE,8CAA8C;gBAC9C,MAAM,KAAK,UAAU,IAAI,2BAA2B,CACrD,GACD,EACD,KAAK,IAAI,mBAAmB,CAAC,MAAM,CAAC,IAChC,CACR,CAAA;AACH,CAAC;AAED,OAAO,EAAE,YAAY,EAAE,CAAA"}
1
+ {"version":3,"file":"entity-status.js","sourceRoot":"","sources":["../../src/blocks/entity-status.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,oBAAoB,CAAA;AAEvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,MAAM;IACN,WAAW;IACX,UAAU;IACV,SAAS;IACT,OAAO;IACP,QAAQ;IACR,SAAS;IACT,UAAU;IACV,SAAS;IACT,OAAO;CACC,CAAA;AAaV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAA+C;IAC5E,IAAI,EAAE,QAAQ;IACd,SAAS,EAAE,WAAW;IACtB,QAAQ,EAAE,SAAS;IACnB,OAAO,EAAE,SAAS;IAClB,KAAK,EAAE,SAAS;IAChB,MAAM,EAAE,SAAS;IACjB,OAAO,EAAE,SAAS;IAClB,QAAQ,EAAE,SAAS;IACnB,OAAO,EAAE,SAAS;IAClB,KAAK,EAAE,QAAQ;CAChB,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAqC;IACnE,IAAI,EAAE,MAAM;IACZ,yEAAyE;IACzE,4EAA4E;IAC5E,yEAAyE;IACzE,kBAAkB;IAClB,SAAS,EAAE,WAAW;IACtB,QAAQ,EAAE,UAAU;IACpB,OAAO,EAAE,SAAS;IAClB,KAAK,EAAE,OAAO;IACd,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,SAAS;IAClB,QAAQ,EAAE,UAAU;IACpB,OAAO,EAAE,SAAS;IAClB,KAAK,EAAE,OAAO;CACf,CAAA;AAED;;;;;;GAMG;AACH,MAAM,UAAU,GAA4D;IAC1E,MAAM,EAAE,EAAE,IAAI,EAAE,mCAAmC,EAAE,GAAG,EAAE,WAAW,EAAE;IACvE,SAAS,EAAE,EAAE,IAAI,EAAE,sCAAsC,EAAE,GAAG,EAAE,cAAc,EAAE;IAChF,OAAO,EAAE,EAAE,IAAI,EAAE,oCAAoC,EAAE,GAAG,EAAE,YAAY,EAAE;IAC1E,OAAO,EAAE,EAAE,IAAI,EAAE,oCAAoC,EAAE,GAAG,EAAE,YAAY,EAAE;IAC1E,MAAM,EAAE,EAAE,IAAI,EAAE,mCAAmC,EAAE,GAAG,EAAE,WAAW,EAAE;IACvE,8DAA8D;IAC9D,OAAO,EAAE,EAAE,IAAI,EAAE,qBAAqB,EAAE,GAAG,EAAE,YAAY,EAAE;CAC5D,CAAA;AAED,SAAS,YAAY,CAAC,EACpB,MAAM,EACN,KAAK,EACL,SAAS,EACT,GAAG,KAAK,EAKT;IACC,MAAM,IAAI,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAA;IACvC,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,CAAA;IAE9B,OAAO,CACL,6BACY,eAAe,iBAIZ,MAAM,eACR,IAAI,EACf,SAAS,EAAE,EAAE,CACX,uIAAuI,EACvI,KAAK,CAAC,IAAI,EACV,SAAS,CACV,KACG,KAAK,aAET,8BAGc,MAAM,eACR,mBAAmB,EAC7B,SAAS,EAAE,EAAE,CACX,gCAAgC,EAChC,KAAK,CAAC,GAAG;gBACT,uEAAuE;gBACvE,uEAAuE;gBACvE,qEAAqE;gBACrE,8CAA8C;gBAC9C,MAAM,KAAK,UAAU,IAAI,2BAA2B,CACrD,GACD,EACD,KAAK,IAAI,mBAAmB,CAAC,MAAM,CAAC,IAChC,CACR,CAAA;AACH,CAAC;AAED,OAAO,EAAE,YAAY,EAAE,CAAA"}
package/dist/theme.css CHANGED
@@ -98,7 +98,8 @@
98
98
  --color-danger: var(--cg-danger);
99
99
 
100
100
  /* ---- state ----
101
- What an entity IS: online, offline, pending, expired, and danger above.
101
+ What an entity IS: online, offline, pending, unhealthy, expired, and danger
102
+ above.
102
103
 
103
104
  #111 added the tokens and stopped there, so for one release the state
104
105
  palette was reachable only as raw var(--cg-online). That is the shape #30
@@ -117,12 +118,21 @@
117
118
  #111's note explains why, and it is a design decision rather than an
118
119
  omission: the muted neutral on a neutral tint measures 2.90:1, under the
119
120
  3:1 bar, so an unplugged chip uses the card ground and border-control
120
- instead. A utility here would have to invent the value it maps. */
121
+ instead. A utility here would have to invent the value it maps.
122
+
123
+ unhealthy (#122) arrives with its token rather than a release later, which
124
+ is the whole point of the guard that walks this block. #113 shipped the
125
+ `indicator` group with no bridge at all and nothing went red, because that
126
+ guard walks the `state` group and only that one. A token whose utility
127
+ lands in a later version is a token the first consumer reaches for and
128
+ does not find. */
121
129
  --color-online: var(--cg-online);
122
130
  --color-online-fill: var(--cg-online-fill);
123
131
  --color-offline: var(--cg-offline);
124
132
  --color-pending: var(--cg-pending);
125
133
  --color-pending-fill: var(--cg-pending-fill);
134
+ --color-unhealthy: var(--cg-unhealthy);
135
+ --color-unhealthy-fill: var(--cg-unhealthy-fill);
126
136
  --color-expired: var(--cg-expired);
127
137
  --color-expired-fill: var(--cg-expired-fill);
128
138
  --color-danger-fill: var(--cg-danger-fill);
@@ -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.",
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.",
56
56
  "online": {
57
57
  "$value": "#00a05c",
58
58
  "$type": "color",
@@ -91,6 +91,16 @@
91
91
  }
92
92
  }
93
93
  },
94
+ "unhealthy": {
95
+ "$value": "#7F7301",
96
+ "$type": "color",
97
+ "$description": "Serving, and degraded. The entity is up and answering; its health checks are not. 4.82:1 on white and 4.46:1 on its own fill — an indicator, like every tone here, never body text.\n\nA REAL value rather than an alias, which is the whole decision. Founder, 2026-08-25, issue #122: expired is terminal, unhealthy is live-and-fixable, and one token for both would mean a designer retuning one silently retunes the other. That is the same reasoning that gave pending its own NAME in #114 — this one also needs its own VALUE, because it must not read as either of the states it sits between.\n\nWHY A GOLD, measured rather than asserted. Green/amber/red is the traffic light an operator already reads, and this palette's amber is spent on two states that are both benign: pending is transitional, expired is terminal. Unhealthy is the warning rung, so it takes the warmer, brighter, more saturated seat on the same ramp rather than a different hue family.\n\nThe alternatives were searched, not guessed, over every hue at 5-degree steps against all four existing tones on all three surfaces:\n\n magenta h339 13.7 from danger on the panel, where danger is a pale pink.\n The one confusion that matters most, and a new palette worst.\n azure h235 the best normal-vision numbers of anything measured (21.5),\n and 1.2 from panel offline for a deuteranope — a degraded\n entity would be indistinguishable from a stopped one.\n chartreuse 10-14 from --cg-secondary and --cg-volt. It IS the brand lime,\n and #114 already ruled a lime cannot signal state.\n\ndE2000 on white: online 28.4, offline 26.6, expired/pending 22.3, danger 43.1. The tightest pair is expired, as expected for the neighbouring rung, and 22.3 is wider than the tightest pair this palette already ships on white (19.4, expired/danger).\n\nKNOWN, and recorded rather than tuned away: on ink the pair is 18.3, which is a new worst for that surface (was 25.3). It stays above the palette's global floor of 16.5, and buying more would mean walking into --cg-secondary (16.5 away) or --cg-volt (17.8). A light amber and a light gold have the least room on a dark ground; that is the cost of putting them on one ramp deliberately.",
98
+ "$extensions": {
99
+ "io.cloudgrid": {
100
+ "css": "--cg-unhealthy"
101
+ }
102
+ }
103
+ },
94
104
  "online-fill": {
95
105
  "$value": "#e6f7ef",
96
106
  "$type": "color",
@@ -120,6 +130,16 @@
120
130
  }
121
131
  }
122
132
  },
133
+ "unhealthy-fill": {
134
+ "$value": "#fdf6df",
135
+ "$type": "color",
136
+ "$description": "The chip ground for unhealthy. --cg-unhealthy measures 4.46:1 on it.\n\nA pale tint of its own foreground, in the same family as online-fill and expired-fill, so the five chips read as one set rather than as four tints and an exception. It is deliberately NOT expired-fill: a shared ground would put the two warm states on the same card and undo half of what the separate foreground buys.",
137
+ "$extensions": {
138
+ "io.cloudgrid": {
139
+ "css": "--cg-unhealthy-fill"
140
+ }
141
+ }
142
+ },
123
143
  "danger-fill": {
124
144
  "$value": "#fceded",
125
145
  "$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.",
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.",
138
138
  "online": {
139
139
  "$value": "#5FD99A",
140
140
  "$type": "color",
@@ -174,6 +174,16 @@
174
174
  }
175
175
  }
176
176
  },
177
+ "unhealthy": {
178
+ "$value": "#F1DC03",
179
+ "$type": "color",
180
+ "$description": "11.35:1 on #222, 8.88:1 on its own fill. Ink has headroom, so the gold stays a gold instead of washing to cream.\n\nThis is the tightest the tone gets anywhere: 18.3 from expired, which is a new worst pair for this surface (was 25.3, offline/danger) while staying above the palette's global floor of 16.5. Recorded rather than tuned away — a light amber and a light gold have the least room on a dark ground, and the next 2 points of separation would walk into --cg-secondary (16.5 away) or --cg-volt (17.8). The pair that matters most is unaffected: 31.3 from online.",
181
+ "$extensions": {
182
+ "io.cloudgrid": {
183
+ "css": "--cg-unhealthy"
184
+ }
185
+ }
186
+ },
177
187
  "online-fill": {
178
188
  "$value": "rgba(255, 255, 255, 0.08)",
179
189
  "$type": "color",
@@ -203,6 +213,16 @@
203
213
  }
204
214
  }
205
215
  },
216
+ "unhealthy-fill": {
217
+ "$value": "rgba(255, 255, 255, 0.08)",
218
+ "$type": "color",
219
+ "$description": "Translucent white; the ink foreground measures 11.42:1 on it.",
220
+ "$extensions": {
221
+ "io.cloudgrid": {
222
+ "css": "--cg-unhealthy-fill"
223
+ }
224
+ }
225
+ },
206
226
  "danger-fill": {
207
227
  "$value": "rgba(255, 255, 255, 0.08)",
208
228
  "$type": "color",
@@ -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.",
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.",
122
122
  "online": {
123
123
  "$value": "#C8F5DC",
124
124
  "$type": "color",
@@ -158,6 +158,16 @@
158
158
  }
159
159
  }
160
160
  },
161
+ "unhealthy": {
162
+ "$value": "#F7E20C",
163
+ "$type": "color",
164
+ "$description": "4.41:1 on the purple, 3.27:1 on its own fill. The source gold is 1.09:1 on this ground — invisible, same class as the danger override.\n\nThis is the surface that DECIDED the value. A dot on a panel chip has to clear 3:1 against rgba(255,255,255,0.16) over #5a47f0, whose relative luminance is 0.192; that forces the dot's own luminance above 0.677, i.e. Lab L >= 86, which is why every state value here is a near-white tint. Four already occupy that sliver at hues 29, 81, 159 and 297, and a fifth has to fit between them. The gold sits at hue 97, 20.0 from expired and 29.5 from online.\n\nIt is louder than its four siblings, deliberately: a warning that recedes is not doing its job, and chroma is the only axis left once lightness is pinned by the contrast floor.",
165
+ "$extensions": {
166
+ "io.cloudgrid": {
167
+ "css": "--cg-unhealthy"
168
+ }
169
+ }
170
+ },
161
171
  "online-fill": {
162
172
  "$value": "rgba(255, 255, 255, 0.16)",
163
173
  "$type": "color",
@@ -187,6 +197,16 @@
187
197
  }
188
198
  }
189
199
  },
200
+ "unhealthy-fill": {
201
+ "$value": "rgba(255, 255, 255, 0.16)",
202
+ "$type": "color",
203
+ "$description": "Translucent white rather than a tint, like every other chip ground here; the panel foreground measures 4.33:1 on it. Written out rather than aliased to online-fill for the same reason the others are: four tokens that happen to share a value are not one token.",
204
+ "$extensions": {
205
+ "io.cloudgrid": {
206
+ "css": "--cg-unhealthy-fill"
207
+ }
208
+ }
209
+ },
190
210
  "danger-fill": {
191
211
  "$value": "rgba(255, 255, 255, 0.16)",
192
212
  "$type": "color",
package/dist/tokens.css CHANGED
@@ -98,7 +98,16 @@
98
98
  pending joined them on 2026-08-24 (#114) as an ALIAS of expired rather than a
99
99
  new value. Same amber today, separate name, so the two can diverge later with
100
100
  a one-line edit here and no change in any consumer. Aliases compile to var(),
101
- so the flip survives. */
101
+ so the flip survives.
102
+
103
+ unhealthy joined them on 2026-08-25 (#122) as a REAL value, not an alias, and
104
+ it is the one tone here that was searched for rather than adopted. Founder
105
+ decision: expired is terminal, unhealthy is live-and-fixable, and sharing a
106
+ token would mean retuning one silently retunes the other. It is a warmer,
107
+ brighter gold on the same ramp as the amber rather than a new hue family,
108
+ because green/amber/red is the traffic light an operator already reads and
109
+ 'serving but degraded' is its middle rung. Its own note carries the numbers
110
+ and the three families that were measured and rejected. */
102
111
  /*
103
112
  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. */
104
113
  --cg-online: #00a05c;
@@ -115,6 +124,27 @@
115
124
 
116
125
  Founder decision 2026-08-24, issue #114: "building is like something in progress, something in between amber and yellow and orange." */
117
126
  --cg-pending: var(--cg-expired);
127
+ /*
128
+ Serving, and degraded. The entity is up and answering; its health checks are not. 4.82:1 on white and 4.46:1 on its own fill — an indicator, like every tone here, never body text.
129
+
130
+ A REAL value rather than an alias, which is the whole decision. Founder, 2026-08-25, issue #122: expired is terminal, unhealthy is live-and-fixable, and one token for both would mean a designer retuning one silently retunes the other. That is the same reasoning that gave pending its own NAME in #114 — this one also needs its own VALUE, because it must not read as either of the states it sits between.
131
+
132
+ WHY A GOLD, measured rather than asserted. Green/amber/red is the traffic light an operator already reads, and this palette's amber is spent on two states that are both benign: pending is transitional, expired is terminal. Unhealthy is the warning rung, so it takes the warmer, brighter, more saturated seat on the same ramp rather than a different hue family.
133
+
134
+ The alternatives were searched, not guessed, over every hue at 5-degree steps against all four existing tones on all three surfaces:
135
+
136
+ magenta h339 13.7 from danger on the panel, where danger is a pale pink.
137
+ The one confusion that matters most, and a new palette worst.
138
+ azure h235 the best normal-vision numbers of anything measured (21.5),
139
+ and 1.2 from panel offline for a deuteranope — a degraded
140
+ entity would be indistinguishable from a stopped one.
141
+ chartreuse 10-14 from --cg-secondary and --cg-volt. It IS the brand lime,
142
+ and #114 already ruled a lime cannot signal state.
143
+
144
+ dE2000 on white: online 28.4, offline 26.6, expired/pending 22.3, danger 43.1. The tightest pair is expired, as expected for the neighbouring rung, and 22.3 is wider than the tightest pair this palette already ships on white (19.4, expired/danger).
145
+
146
+ KNOWN, and recorded rather than tuned away: on ink the pair is 18.3, which is a new worst for that surface (was 25.3). It stays above the palette's global floor of 16.5, and buying more would mean walking into --cg-secondary (16.5 away) or --cg-volt (17.8). A light amber and a light gold have the least room on a dark ground; that is the cost of putting them on one ramp deliberately. */
147
+ --cg-unhealthy: #7F7301;
118
148
  /*
119
149
  From the Console's --color-live-fill. --cg-online measures 3.06:1 on it. */
120
150
  --cg-online-fill: #e6f7ef;
@@ -124,6 +154,11 @@
124
154
  /*
125
155
  The chip ground for pending, aliased to expired's for the same reason the foreground is. Splitting pending from expired later means editing these two entries and nothing else. */
126
156
  --cg-pending-fill: var(--cg-expired-fill);
157
+ /*
158
+ The chip ground for unhealthy. --cg-unhealthy measures 4.46:1 on it.
159
+
160
+ A pale tint of its own foreground, in the same family as online-fill and expired-fill, so the five chips read as one set rather than as four tints and an exception. It is deliberately NOT expired-fill: a shared ground would put the two warm states on the same card and undo half of what the separate foreground buys. */
161
+ --cg-unhealthy-fill: #fdf6df;
127
162
  /*
128
163
  From the Console's --color-error-fill. --cg-danger measures 3.72:1 on it. danger had no fill before; a chip needs one. */
129
164
  --cg-danger-fill: #fceded;
@@ -388,7 +423,7 @@
388
423
  --cg-danger: #FFDAD6;
389
424
 
390
425
  /* ---- state ----
391
- 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. */
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. */
392
427
  /*
393
428
  4.88:1 on the purple. The source green is 1.72:1 here — invisible, same class as the danger override. */
394
429
  --cg-online: #C8F5DC;
@@ -401,6 +436,13 @@
401
436
  /*
402
437
  Aliased to expired, which is 4.68:1 on the purple. Declared here as well as at :root because an alias is substituted where it is DECLARED — a :root-only pending would freeze to the white-surface amber and stop flipping, with no error. */
403
438
  --cg-pending: var(--cg-expired);
439
+ /*
440
+ 4.41:1 on the purple, 3.27:1 on its own fill. The source gold is 1.09:1 on this ground — invisible, same class as the danger override.
441
+
442
+ This is the surface that DECIDED the value. A dot on a panel chip has to clear 3:1 against rgba(255,255,255,0.16) over #5a47f0, whose relative luminance is 0.192; that forces the dot's own luminance above 0.677, i.e. Lab L >= 86, which is why every state value here is a near-white tint. Four already occupy that sliver at hues 29, 81, 159 and 297, and a fifth has to fit between them. The gold sits at hue 97, 20.0 from expired and 29.5 from online.
443
+
444
+ It is louder than its four siblings, deliberately: a warning that recedes is not doing its job, and chroma is the only axis left once lightness is pinned by the contrast floor. */
445
+ --cg-unhealthy: #F7E20C;
404
446
  /*
405
447
  Translucent white rather than a tint; the panel foreground measures 3.62:1 on it. */
406
448
  --cg-online-fill: rgba(255, 255, 255, 0.16);
@@ -410,6 +452,9 @@
410
452
  /*
411
453
  Aliased to expired-fill. Declared per scope for the same reason the foreground is. */
412
454
  --cg-pending-fill: var(--cg-expired-fill);
455
+ /*
456
+ Translucent white rather than a tint, like every other chip ground here; the panel foreground measures 4.33:1 on it. Written out rather than aliased to online-fill for the same reason the others are: four tokens that happen to share a value are not one token. */
457
+ --cg-unhealthy-fill: rgba(255, 255, 255, 0.16);
413
458
  /*
414
459
  Translucent white rather than a tint; the panel foreground measures 3.35:1 on it. */
415
460
  --cg-danger-fill: rgba(255, 255, 255, 0.16);
@@ -451,7 +496,7 @@
451
496
  --cg-danger: #FF8A80;
452
497
 
453
498
  /* ---- state ----
454
- 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. */
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. */
455
500
  /*
456
501
  9.00:1 on #222. Ink has headroom, so this stays a recognisable green rather than washing to mint. */
457
502
  --cg-online: #5FD99A;
@@ -462,6 +507,11 @@
462
507
  /*
463
508
  Aliased to expired, which is 8.63:1 on #222. Declared here as well as at :root because an alias is substituted where it is DECLARED — a :root-only pending would freeze to the white-surface amber and stop flipping, with no error. */
464
509
  --cg-pending: var(--cg-expired);
510
+ /*
511
+ 11.35:1 on #222, 8.88:1 on its own fill. Ink has headroom, so the gold stays a gold instead of washing to cream.
512
+
513
+ This is the tightest the tone gets anywhere: 18.3 from expired, which is a new worst pair for this surface (was 25.3, offline/danger) while staying above the palette's global floor of 16.5. Recorded rather than tuned away — a light amber and a light gold have the least room on a dark ground, and the next 2 points of separation would walk into --cg-secondary (16.5 away) or --cg-volt (17.8). The pair that matters most is unaffected: 31.3 from online. */
514
+ --cg-unhealthy: #F1DC03;
465
515
  /* Translucent white; the ink foreground measures 7.04:1 on it. */
466
516
  --cg-online-fill: rgba(255, 255, 255, 0.08);
467
517
  /* Translucent white; the ink foreground measures 6.76:1 on it. */
@@ -469,6 +519,8 @@
469
519
  /*
470
520
  Aliased to expired-fill. Declared per scope for the same reason the foreground is. */
471
521
  --cg-pending-fill: var(--cg-expired-fill);
522
+ /* Translucent white; the ink foreground measures 11.42:1 on it. */
523
+ --cg-unhealthy-fill: rgba(255, 255, 255, 0.08);
472
524
  /* Translucent white; the ink foreground measures 5.45:1 on it. */
473
525
  --cg-danger-fill: rgba(255, 255, 255, 0.08);
474
526
 
package/dist/tokens.json CHANGED
@@ -12,9 +12,11 @@
12
12
  "--cg-offline": "var(--cg-neutral-500)",
13
13
  "--cg-expired": "#b0662a",
14
14
  "--cg-pending": "var(--cg-expired)",
15
+ "--cg-unhealthy": "#7F7301",
15
16
  "--cg-online-fill": "#e6f7ef",
16
17
  "--cg-expired-fill": "#fdf3e0",
17
18
  "--cg-pending-fill": "var(--cg-expired-fill)",
19
+ "--cg-unhealthy-fill": "#fdf6df",
18
20
  "--cg-danger-fill": "#fceded",
19
21
  "--cg-primary-press": "#4b39db",
20
22
  "--cg-primary-tint": "#eeecfe",
@@ -147,9 +149,11 @@
147
149
  "--cg-offline": "#D9D4FB",
148
150
  "--cg-expired": "#FFE2B8",
149
151
  "--cg-pending": "var(--cg-expired)",
152
+ "--cg-unhealthy": "#F7E20C",
150
153
  "--cg-online-fill": "rgba(255, 255, 255, 0.16)",
151
154
  "--cg-expired-fill": "rgba(255, 255, 255, 0.16)",
152
155
  "--cg-pending-fill": "var(--cg-expired-fill)",
156
+ "--cg-unhealthy-fill": "rgba(255, 255, 255, 0.16)",
153
157
  "--cg-danger-fill": "rgba(255, 255, 255, 0.16)",
154
158
  "--cg-hover": "rgba(255, 255, 255, 0.10)"
155
159
  },
@@ -172,9 +176,11 @@
172
176
  "--cg-offline": "#9A9A9A",
173
177
  "--cg-expired": "#F0B45E",
174
178
  "--cg-pending": "var(--cg-expired)",
179
+ "--cg-unhealthy": "#F1DC03",
175
180
  "--cg-online-fill": "rgba(255, 255, 255, 0.08)",
176
181
  "--cg-expired-fill": "rgba(255, 255, 255, 0.08)",
177
182
  "--cg-pending-fill": "var(--cg-expired-fill)",
183
+ "--cg-unhealthy-fill": "rgba(255, 255, 255, 0.08)",
178
184
  "--cg-danger-fill": "rgba(255, 255, 255, 0.08)",
179
185
  "--cg-hover": "rgba(255, 255, 255, 0.06)"
180
186
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudgrid-io/ui",
3
- "version": "0.6.0",
3
+ "version": "0.7.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/",