@cloudgrid-io/ui 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,140 @@
1
+ import * as React from "react";
2
+ import { Button } from "../ui/button.js";
3
+ /**
4
+ * The sign-in methods, in one place.
5
+ *
6
+ * ## Why this exists (#129)
7
+ *
8
+ * The console rendered its sign-in options in four separate places and they
9
+ * diverged: the invitation-accept screen offered ONLY Google, so an invitee
10
+ * without a Google account could not accept an invitation at all — no error, no
11
+ * fallback, on the one screen whose whole purpose is to let an invited person
12
+ * in. A fifth hand-written list would be the same mistake with a larger blast
13
+ * radius. So the list renders through one component, and adding a provider is
14
+ * one edit at the call site that owns the list rather than four that can
15
+ * silently disagree.
16
+ *
17
+ * ## Presentation only
18
+ *
19
+ * The same line #117 / #119 / #121 / #126 drew. This block holds no auth logic,
20
+ * no provider SDK, no redirect and no token; it renders the methods it is
21
+ * handed and never implies a provider that does not exist. Starting Google
22
+ * OAuth needs a session code the page mints — so that is a callback the page
23
+ * owns. The email option is a LINK, deliberately: middle-click and "open in new
24
+ * tab" work, and the target is visible in the status bar.
25
+ *
26
+ * ## The one thing it does own: `carry`
27
+ *
28
+ * Sign-in must not lose state that rides the URL — where to land afterwards
29
+ * (`return_to`), the onboarding path a NEW user should walk (`arrival`), and
30
+ * for the device flow the code being authorized (`code`) and the client that
31
+ * started it (`source`). Forgetting one is not a visual bug and shows up
32
+ * nowhere: sign-in succeeds and the person silently lands somewhere generic or
33
+ * walks the wrong flow. That has happened three times (`ai_tool`, `invite`,
34
+ * `watermark`).
35
+ *
36
+ * So the page declares what must survive ONCE, in `carry`, and the block
37
+ * applies it to every method uniformly: appended to each link method's `href`,
38
+ * handed to each action method's `onSelect`. The block never knows what the
39
+ * keys mean — the page wires, the block carries.
40
+ *
41
+ * One obligation stays with the caller, loudly: **validate `return_to` before
42
+ * handing it in.** The console keeps its `isSafePostAuthReturn` allowlist and
43
+ * this package cannot import it; an unvalidated `return_to` is an open
44
+ * redirect. Pass the value already validated with the same allowlist the
45
+ * post-auth resolver uses, so there is one thing to audit rather than two.
46
+ *
47
+ * ## Methods are groups
48
+ *
49
+ * `methods` is an array of GROUPS: buttons within a group stack tight, and the
50
+ * divider — "OR" unless `divider` says otherwise — renders between groups.
51
+ * `[[google], [email]]` is the console's current screen; a second OAuth
52
+ * provider joins the first group with no API change. The grouping is what lets
53
+ * the divider sit "between the providers and email" without the block knowing
54
+ * which method is which.
55
+ *
56
+ * ## Icons are handed in
57
+ *
58
+ * Every mark in this package is flattened to `currentColor` and the literal
59
+ * gate keeps it that way, so the four-colour Google G cannot live here without
60
+ * a brand decision. Consumers pass their own marks (`icon`), which the block
61
+ * hides from assistive tech: the accessible name is exactly `label`.
62
+ *
63
+ * @example
64
+ * ```tsx
65
+ * <SignInOptions
66
+ * methods={[
67
+ * [{ id: "google", label: "Continue with Google", icon: <GoogleIcon />, onSelect: startGoogle }],
68
+ * [{ id: "email", label: "Continue with Email", icon: <Mail />, href: "/auth/email" }],
69
+ * ]}
70
+ * carry={{ return_to: safeReturn, arrival, code, source }}
71
+ * renderLink={(p) => <Link {...p} />}
72
+ * />
73
+ * ```
74
+ */
75
+ interface SignInMethodBase {
76
+ /** Stable, never shown. Lands on the button as `data-method`. */
77
+ id: string;
78
+ /** The words on the button, and its exact accessible name. */
79
+ label: string;
80
+ /**
81
+ * The method's mark, ~20px inside a fixed 5-unit slot at the inline start.
82
+ * Hidden from assistive tech here, so any mark keeps the name = `label`.
83
+ */
84
+ icon?: React.ReactNode;
85
+ disabled?: boolean;
86
+ }
87
+ /**
88
+ * Exactly one of `href` (a link method: a real anchor, `carry` appended) or
89
+ * `onSelect` (an action method: the page owns what selecting it starts, and
90
+ * receives the carried entries). The union is enforced in the types because a
91
+ * method with both would hide which one wins, and a method with neither is a
92
+ * button that cannot sign anyone in — worse than an absent one.
93
+ */
94
+ export type SignInMethod = (SignInMethodBase & {
95
+ href: string;
96
+ onSelect?: never;
97
+ }) | (SignInMethodBase & {
98
+ href?: never;
99
+ onSelect: (carried: Readonly<Record<string, string>>) => void;
100
+ });
101
+ export interface SignInOptionsProps {
102
+ /** Groups of methods. The divider renders between groups, never inside one. */
103
+ methods: ReadonlyArray<readonly SignInMethod[]>;
104
+ /**
105
+ * What every method must carry through sign-in — `return_to`, `arrival`,
106
+ * device `code`, `source`, whatever the page cannot afford to lose. Entries
107
+ * that are `null`, `undefined` or empty are dropped, so query params can be
108
+ * passed straight through. Appended to link methods' `href`; handed to
109
+ * action methods' `onSelect`.
110
+ */
111
+ carry?: Readonly<Record<string, string | null | undefined>>;
112
+ /** Replaces the "OR" between groups. The rule itself stays. */
113
+ divider?: React.ReactNode;
114
+ /**
115
+ * Renders the anchor for link methods, for consumers with a framework link —
116
+ * `(p) => <Link {...p} />`. Defaults to a plain `<a>`, which works
117
+ * everywhere; the framework link is an optimisation, not a requirement.
118
+ */
119
+ renderLink?: (props: {
120
+ href: string;
121
+ }) => React.ReactElement;
122
+ className?: string;
123
+ }
124
+ /**
125
+ * One full-width method row: the library `Button` (`variant="outline"` — white
126
+ * fill, hairline border, ink label) sized up to the ~52px onboarding rhythm.
127
+ * The icon sits at the inline start, positioned absolute so the label stays
128
+ * centered rather than being pushed off-center.
129
+ *
130
+ * Exported so a surface composing its own screen (the console's styleguide
131
+ * pilot) renders the same row `SignInOptions` does, instead of keeping a
132
+ * private twin. Every remaining prop passes through to `Button`.
133
+ */
134
+ export declare function SignInMethodButton({ icon, label, className, ...props }: Omit<React.ComponentProps<typeof Button>, "children"> & {
135
+ icon?: React.ReactNode;
136
+ label: string;
137
+ }): React.JSX.Element;
138
+ export declare function SignInOptions({ methods, carry, divider, renderLink, className, }: SignInOptionsProps): React.JSX.Element;
139
+ export {};
140
+ //# sourceMappingURL=sign-in-options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sign-in-options.d.ts","sourceRoot":"","sources":["../../src/blocks/sign-in-options.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAI7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuEG;AAEH,UAAU,gBAAgB;IACxB,iEAAiE;IACjE,EAAE,EAAE,MAAM,CAAA;IACV,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAA;IACb;;;OAGG;IACH,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GACpB,CAAC,gBAAgB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC,GACvD,CAAC,gBAAgB,GAAG;IAClB,IAAI,CAAC,EAAE,KAAK,CAAA;IACZ,QAAQ,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,KAAK,IAAI,CAAA;CAC9D,CAAC,CAAA;AAEN,MAAM,WAAW,kBAAkB;IACjC,+EAA+E;IAC/E,OAAO,EAAE,aAAa,CAAC,SAAS,YAAY,EAAE,CAAC,CAAA;IAC/C;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC,CAAA;IAC3D,+DAA+D;IAC/D,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,KAAK,CAAC,YAAY,CAAA;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAuBD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,IAAI,EACJ,KAAK,EACL,SAAS,EACT,GAAG,KAAK,EACT,EAAE,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,OAAO,MAAM,CAAC,EAAE,UAAU,CAAC,GAAG;IACzD,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB,KAAK,EAAE,MAAM,CAAA;CACd,qBAsBA;AAmBD,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EACP,KAAK,EACL,OAAO,EACP,UAAU,EACV,SAAS,GACV,EAAE,kBAAkB,qBAoDpB"}
@@ -0,0 +1,57 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { Button } from "../ui/button.js";
5
+ import { Separator } from "../ui/separator.js";
6
+ import { cn } from "../lib/cloudgrid-cn.js";
7
+ /** `carry` with the empty entries dropped, in declaration order. */
8
+ function carriedEntries(carry) {
9
+ if (!carry)
10
+ return [];
11
+ return Object.entries(carry).filter((entry) => typeof entry[1] === "string" && entry[1] !== "");
12
+ }
13
+ /** `href` with the carried entries appended, hash kept at the end. */
14
+ function withCarried(href, entries) {
15
+ if (entries.length === 0)
16
+ return href;
17
+ const hashAt = href.indexOf("#");
18
+ const base = hashAt === -1 ? href : href.slice(0, hashAt);
19
+ const hash = hashAt === -1 ? "" : href.slice(hashAt);
20
+ const joined = new URLSearchParams(entries).toString();
21
+ return `${base}${base.includes("?") ? "&" : "?"}${joined}${hash}`;
22
+ }
23
+ /**
24
+ * One full-width method row: the library `Button` (`variant="outline"` — white
25
+ * fill, hairline border, ink label) sized up to the ~52px onboarding rhythm.
26
+ * The icon sits at the inline start, positioned absolute so the label stays
27
+ * centered rather than being pushed off-center.
28
+ *
29
+ * Exported so a surface composing its own screen (the console's styleguide
30
+ * pilot) renders the same row `SignInOptions` does, instead of keeping a
31
+ * private twin. Every remaining prop passes through to `Button`.
32
+ */
33
+ export function SignInMethodButton({ icon, label, className, ...props }) {
34
+ return (_jsxs(Button, { variant: "outline", "data-slot": "sign-in-method", className: cn("relative h-[52px] w-full justify-center rounded-xl text-body-md font-medium", className), ...props, children: [icon == null ? null : (_jsx("span", { "aria-hidden": "true", className: "absolute left-4 flex size-5 items-center justify-center [&_svg]:size-5", children: icon })), label] }));
35
+ }
36
+ /** The "OR" rule between method groups. Decorative, and hidden as such. */
37
+ function SignInOptionsDivider({ children }) {
38
+ return (_jsxs("div", { "data-slot": "sign-in-options-divider", "aria-hidden": "true", className: "my-5 flex items-center gap-3", children: [_jsx(Separator, { className: "flex-1" }), _jsx("span", { className: "text-caption font-medium text-faint", children: children ?? "OR" }), _jsx(Separator, { className: "flex-1" })] }));
39
+ }
40
+ export function SignInOptions({ methods, carry, divider, renderLink, className, }) {
41
+ const entries = carriedEntries(carry);
42
+ const carried = Object.fromEntries(entries);
43
+ const groups = methods.filter((group) => group.length > 0);
44
+ return (_jsx("div", { "data-slot": "sign-in-options", className: className, children: groups.map((group, index) => (
45
+ // Groups have no identity of their own — position is what they mean.
46
+ // eslint-disable-next-line react/no-array-index-key
47
+ _jsxs(React.Fragment, { children: [index > 0 && _jsx(SignInOptionsDivider, { children: divider }), _jsx("div", { className: "grid gap-3", children: group.map((method) => method.href !== undefined ? (
48
+ /*
49
+ A real anchor rather than an onClick. Base UI's Button warns
50
+ when the rendered element is not a <button>, hence
51
+ `nativeButton={false}`, and it stamps `role="button"` on a
52
+ non-native target — put back to `link`, because this one
53
+ really does navigate.
54
+ */
55
+ _jsx(SignInMethodButton, { "data-method": method.id, render: renderLink?.({ href: withCarried(method.href, entries) }) ?? (_jsx("a", { href: withCarried(method.href, entries) })), nativeButton: false, role: "link", icon: method.icon, label: method.label, disabled: method.disabled }, method.id)) : (_jsx(SignInMethodButton, { "data-method": method.id, icon: method.icon, label: method.label, disabled: method.disabled, onClick: () => method.onSelect(carried) }, method.id))) })] }, index))) }));
56
+ }
57
+ //# sourceMappingURL=sign-in-options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sign-in-options.js","sourceRoot":"","sources":["../../src/blocks/sign-in-options.tsx"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -42,6 +42,7 @@ export * from "./blocks/pricing-tier-cards.js";
42
42
  export * from "./blocks/pricing-tiers.js";
43
43
  export * from "./blocks/quote.js";
44
44
  export * from "./blocks/rich-text.js";
45
+ export * from "./blocks/sign-in-options.js";
45
46
  export * from "./blocks/stage-panel.js";
46
47
  export * from "./blocks/step-item.js";
47
48
  export * from "./blocks/tool-logo.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,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,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
@@ -59,6 +59,7 @@ export * from "./blocks/pricing-tier-cards.js";
59
59
  export * from "./blocks/pricing-tiers.js";
60
60
  export * from "./blocks/quote.js";
61
61
  export * from "./blocks/rich-text.js";
62
+ export * from "./blocks/sign-in-options.js";
62
63
  export * from "./blocks/stage-panel.js";
63
64
  export * from "./blocks/step-item.js";
64
65
  export * from "./blocks/tool-logo.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,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,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
@@ -66,6 +66,10 @@
66
66
  --color-page: var(--cg-surface-page);
67
67
  --color-page-alt: var(--cg-surface-page-alt);
68
68
  --color-subtle: var(--cg-surface-subtle);
69
+ /* The neutral raised ground (#151). `subtle` is the lavender tint and `card`
70
+ is white, so before this there was no name for "one step off white, no
71
+ hue" and a raised panel had to flatten or take a cast. */
72
+ --color-raised: var(--cg-surface-raised);
69
73
  --color-control: var(--cg-surface-control);
70
74
  --color-panel: var(--cg-surface-panel);
71
75
  --color-code: var(--cg-surface-code);
@@ -137,6 +141,24 @@
137
141
  --color-expired-fill: var(--cg-expired-fill);
138
142
  --color-danger-fill: var(--cg-danger-fill);
139
143
 
144
+ /* The other two rungs of the chip (#149, #150). `-fill` is the ground, the
145
+ bare name is the dot, `-border` is the hairline and `-strong` is the word.
146
+ Mapped in the same pass as the tokens, for the reason the block above
147
+ gives: a token whose utility lands a release later is a token the first
148
+ consumer reaches for and does not find.
149
+
150
+ There is no --color-danger-border or --color-unhealthy-border, because
151
+ there is no such token — the same shape as --color-offline-fill, and the
152
+ `state` group's own note carries the reason. */
153
+ --color-online-border: var(--cg-online-border);
154
+ --color-online-strong: var(--cg-online-strong);
155
+ --color-expired-border: var(--cg-expired-border);
156
+ --color-expired-strong: var(--cg-expired-strong);
157
+ --color-pending-border: var(--cg-pending-border);
158
+ --color-pending-strong: var(--cg-pending-strong);
159
+ --color-unhealthy-strong: var(--cg-unhealthy-strong);
160
+ --color-danger-strong: var(--cg-danger-strong);
161
+
140
162
  /* ---- lines ---- */
141
163
  --color-hairline: var(--cg-border-hairline);
142
164
  --color-line: var(--cg-border-control);
@@ -52,7 +52,7 @@
52
52
  }
53
53
  },
54
54
  "state": {
55
- "$description": "state: what an entity IS, not what it means editorially.\n\nAdopted from the Console rather than invented, 2026-08-24 (#96). The Console\nhad already chosen and contrast-tested these — --color-live and\n--color-attention — while the brand carried no state colour at all beyond the\ndanger placeholder. Taking its values means the Console's appearance does not\nchange when it starts consuming this package, and the two stop disagreeing\nabout what 'live' looks like.\n\nEach carries per-surface values in surface-panel and surface-ink, for the same\nreason danger does: measured against all three grounds, no single value works.\nThe live green is 1.72:1 on the purple panel, and the brand's own lime is\n1.54:1 on white. A flat value would be invisible somewhere.\n\nNo --cg-offline-fill, deliberately. Measured, the pair does not work: the\nmuted neutral on a neutral tint is 2.90:1, under the 3:1 bar for a dot or a\nborder. Rather than invent a value to satisfy the ratio, an unplugged chip\nuses the existing surface-card ground with border-control, and --cg-offline\nis the dot on that ground, where it measures 3.28:1. Absence of state should\nnot need a colour of its own.\n\npending joined them on 2026-08-24 (#114) as an ALIAS of expired rather than a\nnew value. Same amber today, separate name, so the two can diverge later with\na one-line edit here and no change in any consumer. Aliases compile to var(),\nso the flip survives.\n\nunhealthy joined them on 2026-08-25 (#122) as a REAL value, not an alias, and\nit is the one tone here that was searched for rather than adopted. Founder\ndecision: expired is terminal, unhealthy is live-and-fixable, and sharing a\ntoken would mean retuning one silently retunes the other. It is a warmer,\nbrighter gold on the same ramp as the amber rather than a new hue family,\nbecause green/amber/red is the traffic light an operator already reads and\n'serving but degraded' is its middle rung. Its own note carries the numbers\nand the three families that were measured and rejected.",
55
+ "$description": "state: what an entity IS, not what it means editorially.\n\nAdopted from the Console rather than invented, 2026-08-24 (#96). The Console\nhad already chosen and contrast-tested these — --color-live and\n--color-attention — while the brand carried no state colour at all beyond the\ndanger placeholder. Taking its values means the Console's appearance does not\nchange when it starts consuming this package, and the two stop disagreeing\nabout what 'live' looks like.\n\nEach carries per-surface values in surface-panel and surface-ink, for the same\nreason danger does: measured against all three grounds, no single value works.\nThe live green is 1.72:1 on the purple panel, and the brand's own lime is\n1.54:1 on white. A flat value would be invisible somewhere.\n\nNo --cg-offline-fill, deliberately. Measured, the pair does not work: the\nmuted neutral on a neutral tint is 2.90:1, under the 3:1 bar for a dot or a\nborder. Rather than invent a value to satisfy the ratio, an unplugged chip\nuses the existing surface-card ground with border-control, and --cg-offline\nis the dot on that ground, where it measures 3.28:1. Absence of state should\nnot need a colour of its own.\n\npending joined them on 2026-08-24 (#114) as an ALIAS of expired rather than a\nnew value. Same amber today, separate name, so the two can diverge later with\na one-line edit here and no change in any consumer. Aliases compile to var(),\nso the flip survives.\n\nunhealthy joined them on 2026-08-25 (#122) as a REAL value, not an alias, and\nit is the one tone here that was searched for rather than adopted. Founder\ndecision: expired is terminal, unhealthy is live-and-fixable, and sharing a\ntoken would mean retuning one silently retunes the other. It is a warmer,\nbrighter gold on the same ramp as the amber rather than a new hue family,\nbecause green/amber/red is the traffic light an operator already reads and\n'serving but degraded' is its middle rung. Its own note carries the numbers\nand the three families that were measured and rejected.\n\nTHREE RUNGS PER STATUS, from 2026-09-04 (#149, #150). A chip is a fill, a\nhairline around it, a dot and a word, and until now the set carried only the\nfirst two of those four. -border is the hairline and -strong is the word; the\nbare name stays the dot it has always been.\n\n <status>-fill the chip ground\n <status>-border the hairline around it, a pale tint\n <status> the dot. An indicator, so 3:1, never text\n <status>-strong the word on the fill. Text, so 4.5:1\n\nThe values are the Console's, measured and shipped, not new ones: #149 asked\nfor the rung and console#579 had already been forced to leave two bands as\nliterals for the want of it. Only unhealthy-strong and danger-strong are\nderived, each for a stated reason in its own note.\n\nNo --cg-danger-border and no --cg-unhealthy-border. Neither has ever been drawn\nby any surface — StatusChip's error state has no edge and unhealthy has no band\nat all — so a value here would be invented to complete a grid rather than to\nserve a chip. Same call as --cg-offline-fill above, and the same remedy: add\nthem when something needs them, with the number that made the choice.\n\noffline has no -border and no -strong either, and for the reason it has no\nfill: an unplugged chip sits on the card ground with border-control, and its\nword is --cg-text-primary. Absence of state still does not need a colour.\n\nOn the panel and on ink, -border resolves to the surface's own hairline and\n-strong to its own primary text. A dark ground has no darker rung to reach for,\nand the light tints these scopes carry measure 3.03:1 to 3.60:1 on the panel's\nchip fill, which is a dot and not a word. Worth knowing before retuning: on ink\nthe tints DO clear 4.5:1 on the ink fill (5.48:1 to 8.92:1), so a designer who\nwants the word tinted there changes five entries in surface-ink.json. On the\npanel it is not available at any value — white is 4.31:1 on that fill and white\nis the lightest colour there is, so AA on a panel chip needs the FILL to move.",
56
56
  "online": {
57
57
  "$value": "#00a05c",
58
58
  "$type": "color",
@@ -149,6 +149,84 @@
149
149
  "css": "--cg-danger-fill"
150
150
  }
151
151
  }
152
+ },
153
+ "online-border": {
154
+ "$value": "#abefc6",
155
+ "$type": "color",
156
+ "$description": "The hairline around an online chip. From the Console's OK band, where it has\nshipped as #abefc6 on #ecfdf3 (console#579).\n\nIts own rung rather than the tone, and console#579 is why. Both systems carried\n{tone, fill} pairs and no third value, so converting the band meant sending the\nborder AND the word to --cg-online. Measured, that is not a repaint: the word\nfell from 7.55:1 to 3.06:1 and the border went from this pale tint to full\nsaturation. The band stayed a literal instead. This token is what lets it move.\n\n1.19:1 against --cg-online-fill. A hairline is not text and not an indicator, so\nthere is no floor it has to clear; what it has to do is stay pale, which the\ncontrast guardrail holds with a ceiling rather than a floor.",
157
+ "$extensions": {
158
+ "io.cloudgrid": {
159
+ "css": "--cg-online-border"
160
+ }
161
+ }
162
+ },
163
+ "expired-border": {
164
+ "$value": "#fedf89",
165
+ "$type": "color",
166
+ "$description": "The hairline around an expired or pending chip. From the Console's WARN band,\nwhere it has shipped as #fedf89 on #fffaeb (console#579).\n\n1.18:1 against --cg-expired-fill. Same rung and same reasoning as\n--cg-online-border: collapsing it onto --cg-expired measured 3.99:1, which is\nthe tone doing the border's job.",
167
+ "$extensions": {
168
+ "io.cloudgrid": {
169
+ "css": "--cg-expired-border"
170
+ }
171
+ }
172
+ },
173
+ "pending-border": {
174
+ "$value": "{state.expired-border}",
175
+ "$description": "The hairline around a pending chip, aliased to expired's for the same reason the\ntone and the fill are. Splitting pending from expired later means editing the\nfour pending entries in this file and nothing else.",
176
+ "$extensions": {
177
+ "io.cloudgrid": {
178
+ "css": "--cg-pending-border"
179
+ }
180
+ }
181
+ },
182
+ "online-strong": {
183
+ "$value": "#085d3a",
184
+ "$type": "color",
185
+ "$description": "The WORD on an online chip, and online as running text anywhere. From the\nConsole's OK band. 7.17:1 on --cg-online-fill and 7.96:1 on white.\n\nThis is the rung EntityStatus said the library lacked. --cg-online is 3.40:1 on\nwhite and 3.06:1 on its own fill, which is an indicator and not a foreground, so\nthe component paints the dot with the tone and the word with --cg-text-primary,\nand its own note says a state foreground is a brand decision rather than a\ncomponent's to take. This is that decision, made with a value the Console has\nalready shipped rather than a new one.",
186
+ "$extensions": {
187
+ "io.cloudgrid": {
188
+ "css": "--cg-online-strong"
189
+ }
190
+ }
191
+ },
192
+ "expired-strong": {
193
+ "$value": "#7a3d0a",
194
+ "$type": "color",
195
+ "$description": "The word on an expired chip, and expired as running text. From the Console's\nWARN band. 7.63:1 on --cg-expired-fill and 8.40:1 on white.",
196
+ "$extensions": {
197
+ "io.cloudgrid": {
198
+ "css": "--cg-expired-strong"
199
+ }
200
+ }
201
+ },
202
+ "pending-strong": {
203
+ "$value": "{state.expired-strong}",
204
+ "$description": "The word on a pending chip. Aliased to expired's, like every other pending rung.",
205
+ "$extensions": {
206
+ "io.cloudgrid": {
207
+ "css": "--cg-pending-strong"
208
+ }
209
+ }
210
+ },
211
+ "unhealthy-strong": {
212
+ "$value": "#5d5401",
213
+ "$type": "color",
214
+ "$description": "The word on an unhealthy chip. 7.10:1 on --cg-unhealthy-fill and 7.68:1 on\nwhite.\n\nDERIVED, not adopted. unhealthy is the one tone here the Console never had, so\nthere is no shipped literal to take. This is --cg-unhealthy darkened along its\nown hue and saturation until it reaches the band the two adopted words already\noccupy — 7.17:1 for online and 7.63:1 for expired on their own fills — rather\nthan stopped at the first value that clears 4.5:1, so the words read as one set\ninstead of three matched and one nearly failing.\n\nThe tone is not simply reused because it does not clear the bar: --cg-unhealthy\nmeasures 4.46:1 on this fill. Four hundredths under the floor is under it.\n\ndE2000 to its neighbours in this rung: 20.6 from expired-strong, 23.1 from\nonline-strong. The tightest pair in the whole rung is expired-strong against\ndanger-strong at 19.8, above the 16.5 floor this palette was measured against.",
215
+ "$extensions": {
216
+ "io.cloudgrid": {
217
+ "css": "--cg-unhealthy-strong"
218
+ }
219
+ }
220
+ },
221
+ "danger-strong": {
222
+ "$value": "#c42828",
223
+ "$type": "color",
224
+ "$description": "The word on a danger chip, and danger as running text. 5.02:1 on\n--cg-danger-fill and 5.70:1 on white.\n\n--cg-danger itself is #E53935, which is 4.23:1 on white and 3.72:1 on its own\nfill. It passes as a fill, a border or an icon, where the bar is 3:1, and fails\nas a sentence. The token is deliberately NOT moved to fix that: every fill,\nborder and icon already consuming it would repaint, and the error is that danger\nhad no word rung, not that its tone is wrong.\n\nThe Console's retired --color-error #d42b2b is where this starts and is not\nwhere it lands. It is 5.02:1 on white, but 4.42:1 on --cg-danger-fill — still\nunder the floor on the exact chip that needs it. This is that red darkened along\nits own hue and saturation until it clears 5:1 on the fill, which is the\nsmallest move that holds on both grounds.\n\nKept lighter than the other three words on purpose. It is already the closest\npair in the rung, dE2000 19.8 from expired-strong, and darkening it into their\n7:1 band closes that to 16.6 — a failed chip and an expiring one should not\nconverge.",
225
+ "$extensions": {
226
+ "io.cloudgrid": {
227
+ "css": "--cg-danger-strong"
228
+ }
229
+ }
152
230
  }
153
231
  },
154
232
  "brand-derived": {
@@ -210,6 +288,16 @@
210
288
  }
211
289
  }
212
290
  },
291
+ "neutral-50": {
292
+ "$value": "#f4f6f8",
293
+ "$type": "color",
294
+ "$description": "SOURCE: the Console's --color-canvas, the recessed ground behind a card.\n\nNot a pure grey. It carries a slight blue cast where the rest of this ramp is\nneutral, and that is recorded rather than corrected because the value is what\nlegacy raised panels already render and matching them is the point (#151).",
295
+ "$extensions": {
296
+ "io.cloudgrid": {
297
+ "css": "--cg-neutral-50"
298
+ }
299
+ }
300
+ },
213
301
  "neutral-100": {
214
302
  "$value": "#f1f1f1",
215
303
  "$type": "color",
@@ -304,6 +392,15 @@
304
392
  }
305
393
  }
306
394
  },
395
+ "surface-raised": {
396
+ "$value": "{neutrals.neutral-50}",
397
+ "$description": "A neutral ground one step off white, for a panel that should read as lifted off\nthe page rather than flat on it.\n\n#151: --cg-surface-card is #ffffff and --cg-surface-subtle is the lavender tint,\nso a raised panel had no value-preserving target and every consumer faced the\nsame three-way choice with no right answer — flatten it to white, tint it\nlavender, or pin a literal. This is the fourth answer, and it is the Console's\nown #f4f6f8 rather than a new value.\n\nDeclared at :root only, like --cg-surface-subtle and --cg-surface-control. The\nneutral grounds do not flip: inside a purple panel or on ink, depth is carried\nby --cg-surface-card and the hairline instead. Making it flip is one entry in\neach of the two surface sets.",
398
+ "$extensions": {
399
+ "io.cloudgrid": {
400
+ "css": "--cg-surface-raised"
401
+ }
402
+ }
403
+ },
307
404
  "surface-control": {
308
405
  "$value": "{neutrals.neutral-200}",
309
406
  "$extensions": {
@@ -1164,23 +1261,13 @@
1164
1261
  "volt": {
1165
1262
  "$value": "#b6ff2e",
1166
1263
  "$type": "color",
1167
- "$description": "The live/current INDICATOR — the dot in its halo, and the brand mark. Never\ndecorative, never a fill.\n\nMEASURED 1.21:1 on white, which is below every bar there is. It works on the\npanel (4.82:1) and on ink (13.13:1); on the page ground it reads only because\nit is paired with the ring and, in a pill, with volt-ink. Recorded rather than\nsilently corrected: darkening it would change the Console's appearance, which\nis a design decision and not a contrast fix.\n\nNote it is a SECOND lime beside --cg-secondary (#b6de68), 36 dE away. Which one\nthe brand owns is the open question here.\n\nIt is also a second LIVE. state.online (#00a05c) is the same idea in the state\nscale, and EntityStatus (#116) already paints the live dot with it. They are not\ninterchangeable at any bar — online is 3.40:1 on white, volt is 1.21:1 — so a\nsurface must not swap one for the other on the assumption that both mean\n'serving'. Which name the brand keeps for live belongs to the same open question,\nand until it is answered volt is the Console's brand mark and online is the\nentity's state.",
1264
+ "$description": "The live/current INDICATOR — the dot in its halo, and the brand mark. Never\ndecorative, never a fill.\n\nMEASURED 1.21:1 on white, which is below every bar there is. It works on the\npanel (4.82:1) and on ink (13.13:1); on the page ground it reads only because\nit is paired with volt-ink in a pill. Recorded rather than\nsilently corrected: darkening it would change the Console's appearance, which\nis a design decision and not a contrast fix.\n\nNote it is a SECOND lime beside --cg-secondary (#b6de68), 36 dE away. Which one\nthe brand owns is the open question here.\n\nIt is also a second LIVE. state.online (#00a05c) is the same idea in the state\nscale, and EntityStatus (#116) already paints the live dot with it. They are not\ninterchangeable at any bar — online is 3.40:1 on white, volt is 1.21:1 — so a\nsurface must not swap one for the other on the assumption that both mean\n'serving'. Which name the brand keeps for live belongs to the same open question,\nand until it is answered volt is the Console's brand mark and online is the\nentity's state.",
1168
1265
  "$extensions": {
1169
1266
  "io.cloudgrid": {
1170
1267
  "css": "--cg-volt"
1171
1268
  }
1172
1269
  }
1173
1270
  },
1174
- "volt-ring": {
1175
- "$value": "rgba(182, 255, 46, 0.32)",
1176
- "$type": "color",
1177
- "$description": "The halo behind a volt dot. Its job is to make the dot findable on a light ground, which is what carries the 1.21:1 above.",
1178
- "$extensions": {
1179
- "io.cloudgrid": {
1180
- "css": "--cg-volt-ring"
1181
- }
1182
- }
1183
- },
1184
1271
  "volt-ink": {
1185
1272
  "$value": "#243d00",
1186
1273
  "$type": "color",
@@ -134,7 +134,7 @@
134
134
  }
135
135
  },
136
136
  "state": {
137
- "$description": "state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground.",
137
+ "$description": "state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground.\n\nThe -border and -strong rungs (#149, #150) are the exception to \"measured per surface\": they resolve to this surface's own hairline and its own primary text rather than to a tint of the tone. A dark ground has no darker rung, and the tints above are tuned to be dots — on the panel's chip fill they measure 3.03:1 to 3.60:1, which is an indicator and not a word. On ink they DO clear 4.5:1 on the ink fill (5.48:1 to 8.92:1), so tinting the word there is available and is five edits in this file; on the panel it is not available at any value, because white measures 4.31:1 on that fill and white is the lightest colour there is.",
138
138
  "online": {
139
139
  "$value": "#5FD99A",
140
140
  "$type": "color",
@@ -232,6 +232,70 @@
232
232
  "css": "--cg-danger-fill"
233
233
  }
234
234
  }
235
+ },
236
+ "online-border": {
237
+ "$value": "{semantic-lines.border-hairline}",
238
+ "$extensions": {
239
+ "io.cloudgrid": {
240
+ "css": "--cg-online-border"
241
+ }
242
+ }
243
+ },
244
+ "expired-border": {
245
+ "$value": "{semantic-lines.border-hairline}",
246
+ "$extensions": {
247
+ "io.cloudgrid": {
248
+ "css": "--cg-expired-border"
249
+ }
250
+ }
251
+ },
252
+ "pending-border": {
253
+ "$value": "{state.expired-border}",
254
+ "$extensions": {
255
+ "io.cloudgrid": {
256
+ "css": "--cg-pending-border"
257
+ }
258
+ }
259
+ },
260
+ "online-strong": {
261
+ "$value": "{semantic-text.text-primary}",
262
+ "$extensions": {
263
+ "io.cloudgrid": {
264
+ "css": "--cg-online-strong"
265
+ }
266
+ }
267
+ },
268
+ "expired-strong": {
269
+ "$value": "{semantic-text.text-primary}",
270
+ "$extensions": {
271
+ "io.cloudgrid": {
272
+ "css": "--cg-expired-strong"
273
+ }
274
+ }
275
+ },
276
+ "pending-strong": {
277
+ "$value": "{state.expired-strong}",
278
+ "$extensions": {
279
+ "io.cloudgrid": {
280
+ "css": "--cg-pending-strong"
281
+ }
282
+ }
283
+ },
284
+ "unhealthy-strong": {
285
+ "$value": "{semantic-text.text-primary}",
286
+ "$extensions": {
287
+ "io.cloudgrid": {
288
+ "css": "--cg-unhealthy-strong"
289
+ }
290
+ }
291
+ },
292
+ "danger-strong": {
293
+ "$value": "{semantic-text.text-primary}",
294
+ "$extensions": {
295
+ "io.cloudgrid": {
296
+ "css": "--cg-danger-strong"
297
+ }
298
+ }
235
299
  }
236
300
  },
237
301
  "indicator": {
@@ -118,7 +118,7 @@
118
118
  }
119
119
  },
120
120
  "state": {
121
- "$description": "state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground.",
121
+ "$description": "state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground.\n\nThe -border and -strong rungs (#149, #150) are the exception to \"measured per surface\": they resolve to this surface's own hairline and its own primary text rather than to a tint of the tone. A dark ground has no darker rung, and the tints above are tuned to be dots — on the panel's chip fill they measure 3.03:1 to 3.60:1, which is an indicator and not a word. On ink they DO clear 4.5:1 on the ink fill (5.48:1 to 8.92:1), so tinting the word there is available and is five edits in this file; on the panel it is not available at any value, because white measures 4.31:1 on that fill and white is the lightest colour there is.",
122
122
  "online": {
123
123
  "$value": "#C8F5DC",
124
124
  "$type": "color",
@@ -216,6 +216,70 @@
216
216
  "css": "--cg-danger-fill"
217
217
  }
218
218
  }
219
+ },
220
+ "online-border": {
221
+ "$value": "{semantic-lines.border-hairline}",
222
+ "$extensions": {
223
+ "io.cloudgrid": {
224
+ "css": "--cg-online-border"
225
+ }
226
+ }
227
+ },
228
+ "expired-border": {
229
+ "$value": "{semantic-lines.border-hairline}",
230
+ "$extensions": {
231
+ "io.cloudgrid": {
232
+ "css": "--cg-expired-border"
233
+ }
234
+ }
235
+ },
236
+ "pending-border": {
237
+ "$value": "{state.expired-border}",
238
+ "$extensions": {
239
+ "io.cloudgrid": {
240
+ "css": "--cg-pending-border"
241
+ }
242
+ }
243
+ },
244
+ "online-strong": {
245
+ "$value": "{semantic-text.text-primary}",
246
+ "$extensions": {
247
+ "io.cloudgrid": {
248
+ "css": "--cg-online-strong"
249
+ }
250
+ }
251
+ },
252
+ "expired-strong": {
253
+ "$value": "{semantic-text.text-primary}",
254
+ "$extensions": {
255
+ "io.cloudgrid": {
256
+ "css": "--cg-expired-strong"
257
+ }
258
+ }
259
+ },
260
+ "pending-strong": {
261
+ "$value": "{state.expired-strong}",
262
+ "$extensions": {
263
+ "io.cloudgrid": {
264
+ "css": "--cg-pending-strong"
265
+ }
266
+ }
267
+ },
268
+ "unhealthy-strong": {
269
+ "$value": "{semantic-text.text-primary}",
270
+ "$extensions": {
271
+ "io.cloudgrid": {
272
+ "css": "--cg-unhealthy-strong"
273
+ }
274
+ }
275
+ },
276
+ "danger-strong": {
277
+ "$value": "{semantic-text.text-primary}",
278
+ "$extensions": {
279
+ "io.cloudgrid": {
280
+ "css": "--cg-danger-strong"
281
+ }
282
+ }
219
283
  }
220
284
  },
221
285
  "indicator": {
package/dist/tokens.css CHANGED
@@ -107,7 +107,41 @@
107
107
  brighter gold on the same ramp as the amber rather than a new hue family,
108
108
  because green/amber/red is the traffic light an operator already reads and
109
109
  'serving but degraded' is its middle rung. Its own note carries the numbers
110
- and the three families that were measured and rejected. */
110
+ and the three families that were measured and rejected.
111
+
112
+ THREE RUNGS PER STATUS, from 2026-09-04 (#149, #150). A chip is a fill, a
113
+ hairline around it, a dot and a word, and until now the set carried only the
114
+ first two of those four. -border is the hairline and -strong is the word; the
115
+ bare name stays the dot it has always been.
116
+
117
+ <status>-fill the chip ground
118
+ <status>-border the hairline around it, a pale tint
119
+ <status> the dot. An indicator, so 3:1, never text
120
+ <status>-strong the word on the fill. Text, so 4.5:1
121
+
122
+ The values are the Console's, measured and shipped, not new ones: #149 asked
123
+ for the rung and console#579 had already been forced to leave two bands as
124
+ literals for the want of it. Only unhealthy-strong and danger-strong are
125
+ derived, each for a stated reason in its own note.
126
+
127
+ No --cg-danger-border and no --cg-unhealthy-border. Neither has ever been drawn
128
+ by any surface — StatusChip's error state has no edge and unhealthy has no band
129
+ at all — so a value here would be invented to complete a grid rather than to
130
+ serve a chip. Same call as --cg-offline-fill above, and the same remedy: add
131
+ them when something needs them, with the number that made the choice.
132
+
133
+ offline has no -border and no -strong either, and for the reason it has no
134
+ fill: an unplugged chip sits on the card ground with border-control, and its
135
+ word is --cg-text-primary. Absence of state still does not need a colour.
136
+
137
+ On the panel and on ink, -border resolves to the surface's own hairline and
138
+ -strong to its own primary text. A dark ground has no darker rung to reach for,
139
+ and the light tints these scopes carry measure 3.03:1 to 3.60:1 on the panel's
140
+ chip fill, which is a dot and not a word. Worth knowing before retuning: on ink
141
+ the tints DO clear 4.5:1 on the ink fill (5.48:1 to 8.92:1), so a designer who
142
+ wants the word tinted there changes five entries in surface-ink.json. On the
143
+ panel it is not available at any value — white is 4.31:1 on that fill and white
144
+ is the lightest colour there is, so AA on a panel chip needs the FILL to move. */
111
145
  /*
112
146
  Serving. From the Console's --color-live. 3.40:1 on white — clears the 3:1 bar for an indicator, which is all this is ever used as. Never body text on white. */
113
147
  --cg-online: #00a05c;
@@ -162,6 +196,90 @@
162
196
  /*
163
197
  From the Console's --color-error-fill. --cg-danger measures 3.72:1 on it. danger had no fill before; a chip needs one. */
164
198
  --cg-danger-fill: #fceded;
199
+ /*
200
+ The hairline around an online chip. From the Console's OK band, where it has
201
+ shipped as #abefc6 on #ecfdf3 (console#579).
202
+
203
+ Its own rung rather than the tone, and console#579 is why. Both systems carried
204
+ {tone, fill} pairs and no third value, so converting the band meant sending the
205
+ border AND the word to --cg-online. Measured, that is not a repaint: the word
206
+ fell from 7.55:1 to 3.06:1 and the border went from this pale tint to full
207
+ saturation. The band stayed a literal instead. This token is what lets it move.
208
+
209
+ 1.19:1 against --cg-online-fill. A hairline is not text and not an indicator, so
210
+ there is no floor it has to clear; what it has to do is stay pale, which the
211
+ contrast guardrail holds with a ceiling rather than a floor. */
212
+ --cg-online-border: #abefc6;
213
+ /*
214
+ The hairline around an expired or pending chip. From the Console's WARN band,
215
+ where it has shipped as #fedf89 on #fffaeb (console#579).
216
+
217
+ 1.18:1 against --cg-expired-fill. Same rung and same reasoning as
218
+ --cg-online-border: collapsing it onto --cg-expired measured 3.99:1, which is
219
+ the tone doing the border's job. */
220
+ --cg-expired-border: #fedf89;
221
+ /*
222
+ The hairline around a pending chip, aliased to expired's for the same reason the
223
+ tone and the fill are. Splitting pending from expired later means editing the
224
+ four pending entries in this file and nothing else. */
225
+ --cg-pending-border: var(--cg-expired-border);
226
+ /*
227
+ The WORD on an online chip, and online as running text anywhere. From the
228
+ Console's OK band. 7.17:1 on --cg-online-fill and 7.96:1 on white.
229
+
230
+ This is the rung EntityStatus said the library lacked. --cg-online is 3.40:1 on
231
+ white and 3.06:1 on its own fill, which is an indicator and not a foreground, so
232
+ the component paints the dot with the tone and the word with --cg-text-primary,
233
+ and its own note says a state foreground is a brand decision rather than a
234
+ component's to take. This is that decision, made with a value the Console has
235
+ already shipped rather than a new one. */
236
+ --cg-online-strong: #085d3a;
237
+ /*
238
+ The word on an expired chip, and expired as running text. From the Console's
239
+ WARN band. 7.63:1 on --cg-expired-fill and 8.40:1 on white. */
240
+ --cg-expired-strong: #7a3d0a;
241
+ /*
242
+ The word on a pending chip. Aliased to expired's, like every other pending rung. */
243
+ --cg-pending-strong: var(--cg-expired-strong);
244
+ /*
245
+ The word on an unhealthy chip. 7.10:1 on --cg-unhealthy-fill and 7.68:1 on
246
+ white.
247
+
248
+ DERIVED, not adopted. unhealthy is the one tone here the Console never had, so
249
+ there is no shipped literal to take. This is --cg-unhealthy darkened along its
250
+ own hue and saturation until it reaches the band the two adopted words already
251
+ occupy — 7.17:1 for online and 7.63:1 for expired on their own fills — rather
252
+ than stopped at the first value that clears 4.5:1, so the words read as one set
253
+ instead of three matched and one nearly failing.
254
+
255
+ The tone is not simply reused because it does not clear the bar: --cg-unhealthy
256
+ measures 4.46:1 on this fill. Four hundredths under the floor is under it.
257
+
258
+ dE2000 to its neighbours in this rung: 20.6 from expired-strong, 23.1 from
259
+ online-strong. The tightest pair in the whole rung is expired-strong against
260
+ danger-strong at 19.8, above the 16.5 floor this palette was measured against. */
261
+ --cg-unhealthy-strong: #5d5401;
262
+ /*
263
+ The word on a danger chip, and danger as running text. 5.02:1 on
264
+ --cg-danger-fill and 5.70:1 on white.
265
+
266
+ --cg-danger itself is #E53935, which is 4.23:1 on white and 3.72:1 on its own
267
+ fill. It passes as a fill, a border or an icon, where the bar is 3:1, and fails
268
+ as a sentence. The token is deliberately NOT moved to fix that: every fill,
269
+ border and icon already consuming it would repaint, and the error is that danger
270
+ had no word rung, not that its tone is wrong.
271
+
272
+ The Console's retired --color-error #d42b2b is where this starts and is not
273
+ where it lands. It is 5.02:1 on white, but 4.42:1 on --cg-danger-fill — still
274
+ under the floor on the exact chip that needs it. This is that red darkened along
275
+ its own hue and saturation until it clears 5:1 on the fill, which is the
276
+ smallest move that holds on both grounds.
277
+
278
+ Kept lighter than the other three words on purpose. It is already the closest
279
+ pair in the rung, dE2000 19.8 from expired-strong, and darkening it into their
280
+ 7:1 band closes that to 16.6 — a failed chip and an expiring one should not
281
+ converge. */
282
+ --cg-danger-strong: #c42828;
165
283
 
166
284
  /* ---- brand-derived ----
167
285
  brand derived (interaction only) */
@@ -174,6 +292,13 @@
174
292
  --cg-neutral-0: #ffffff;
175
293
  /* SOURCE: alternate page wash */
176
294
  --cg-lavender-50: #f2f1f7;
295
+ /*
296
+ SOURCE: the Console's --color-canvas, the recessed ground behind a card.
297
+
298
+ Not a pure grey. It carries a slight blue cast where the rest of this ramp is
299
+ neutral, and that is recorded rather than corrected because the value is what
300
+ legacy raised panels already render and matching them is the point (#151). */
301
+ --cg-neutral-50: #f4f6f8;
177
302
  /* SOURCE: hairline / card border */
178
303
  --cg-neutral-100: #f1f1f1;
179
304
  /* SOURCE: control border, inactive pill fill */
@@ -192,6 +317,21 @@
192
317
  --cg-surface-page-alt: var(--cg-lavender-50);
193
318
  --cg-surface-card: var(--cg-neutral-0);
194
319
  --cg-surface-subtle: var(--cg-lavender-50);
320
+ /*
321
+ A neutral ground one step off white, for a panel that should read as lifted off
322
+ the page rather than flat on it.
323
+
324
+ #151: --cg-surface-card is #ffffff and --cg-surface-subtle is the lavender tint,
325
+ so a raised panel had no value-preserving target and every consumer faced the
326
+ same three-way choice with no right answer — flatten it to white, tint it
327
+ lavender, or pin a literal. This is the fourth answer, and it is the Console's
328
+ own #f4f6f8 rather than a new value.
329
+
330
+ Declared at :root only, like --cg-surface-subtle and --cg-surface-control. The
331
+ neutral grounds do not flip: inside a purple panel or on ink, depth is carried
332
+ by --cg-surface-card and the hairline instead. Making it flip is one entry in
333
+ each of the two surface sets. */
334
+ --cg-surface-raised: var(--cg-neutral-50);
195
335
  --cg-surface-control: var(--cg-neutral-200);
196
336
  --cg-surface-panel: var(--cg-primary);
197
337
  --cg-surface-code: var(--cg-ink);
@@ -353,7 +493,7 @@
353
493
 
354
494
  MEASURED 1.21:1 on white, which is below every bar there is. It works on the
355
495
  panel (4.82:1) and on ink (13.13:1); on the page ground it reads only because
356
- it is paired with the ring and, in a pill, with volt-ink. Recorded rather than
496
+ it is paired with volt-ink in a pill. Recorded rather than
357
497
  silently corrected: darkening it would change the Console's appearance, which
358
498
  is a design decision and not a contrast fix.
359
499
 
@@ -368,9 +508,6 @@
368
508
  and until it is answered volt is the Console's brand mark and online is the
369
509
  entity's state. */
370
510
  --cg-volt: #b6ff2e;
371
- /*
372
- The halo behind a volt dot. Its job is to make the dot findable on a light ground, which is what carries the 1.21:1 above. */
373
- --cg-volt-ring: rgba(182, 255, 46, 0.32);
374
511
  /*
375
512
  Text and glyphs ON volt. 9.95:1 against it — the one part of the volt family that
376
513
  is comfortably legible.
@@ -423,7 +560,9 @@
423
560
  --cg-danger: #FFDAD6;
424
561
 
425
562
  /* ---- state ----
426
- state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground. */
563
+ state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground.
564
+
565
+ The -border and -strong rungs (#149, #150) are the exception to "measured per surface": they resolve to this surface's own hairline and its own primary text rather than to a tint of the tone. A dark ground has no darker rung, and the tints above are tuned to be dots — on the panel's chip fill they measure 3.03:1 to 3.60:1, which is an indicator and not a word. On ink they DO clear 4.5:1 on the ink fill (5.48:1 to 8.92:1), so tinting the word there is available and is five edits in this file; on the panel it is not available at any value, because white measures 4.31:1 on that fill and white is the lightest colour there is. */
427
566
  /*
428
567
  4.88:1 on the purple. The source green is 1.72:1 here — invisible, same class as the danger override. */
429
568
  --cg-online: #C8F5DC;
@@ -458,6 +597,14 @@
458
597
  /*
459
598
  Translucent white rather than a tint; the panel foreground measures 3.35:1 on it. */
460
599
  --cg-danger-fill: rgba(255, 255, 255, 0.16);
600
+ --cg-online-border: var(--cg-border-hairline);
601
+ --cg-expired-border: var(--cg-border-hairline);
602
+ --cg-pending-border: var(--cg-expired-border);
603
+ --cg-online-strong: var(--cg-text-primary);
604
+ --cg-expired-strong: var(--cg-text-primary);
605
+ --cg-pending-strong: var(--cg-expired-strong);
606
+ --cg-unhealthy-strong: var(--cg-text-primary);
607
+ --cg-danger-strong: var(--cg-text-primary);
461
608
 
462
609
  /* ---- indicator ----
463
610
  indicator + chrome: only hover flips. A dark wash is invisible on a dark ground. */
@@ -496,7 +643,9 @@
496
643
  --cg-danger: #FF8A80;
497
644
 
498
645
  /* ---- state ----
499
- state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground. */
646
+ state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground.
647
+
648
+ The -border and -strong rungs (#149, #150) are the exception to "measured per surface": they resolve to this surface's own hairline and its own primary text rather than to a tint of the tone. A dark ground has no darker rung, and the tints above are tuned to be dots — on the panel's chip fill they measure 3.03:1 to 3.60:1, which is an indicator and not a word. On ink they DO clear 4.5:1 on the ink fill (5.48:1 to 8.92:1), so tinting the word there is available and is five edits in this file; on the panel it is not available at any value, because white measures 4.31:1 on that fill and white is the lightest colour there is. */
500
649
  /*
501
650
  9.00:1 on #222. Ink has headroom, so this stays a recognisable green rather than washing to mint. */
502
651
  --cg-online: #5FD99A;
@@ -523,6 +672,14 @@
523
672
  --cg-unhealthy-fill: rgba(255, 255, 255, 0.08);
524
673
  /* Translucent white; the ink foreground measures 5.45:1 on it. */
525
674
  --cg-danger-fill: rgba(255, 255, 255, 0.08);
675
+ --cg-online-border: var(--cg-border-hairline);
676
+ --cg-expired-border: var(--cg-border-hairline);
677
+ --cg-pending-border: var(--cg-expired-border);
678
+ --cg-online-strong: var(--cg-text-primary);
679
+ --cg-expired-strong: var(--cg-text-primary);
680
+ --cg-pending-strong: var(--cg-expired-strong);
681
+ --cg-unhealthy-strong: var(--cg-text-primary);
682
+ --cg-danger-strong: var(--cg-text-primary);
526
683
 
527
684
  /* ---- indicator ----
528
685
  indicator + chrome: only hover flips. A dark wash is invisible on a dark ground. */
package/dist/tokens.json CHANGED
@@ -18,12 +18,21 @@
18
18
  "--cg-pending-fill": "var(--cg-expired-fill)",
19
19
  "--cg-unhealthy-fill": "#fdf6df",
20
20
  "--cg-danger-fill": "#fceded",
21
+ "--cg-online-border": "#abefc6",
22
+ "--cg-expired-border": "#fedf89",
23
+ "--cg-pending-border": "var(--cg-expired-border)",
24
+ "--cg-online-strong": "#085d3a",
25
+ "--cg-expired-strong": "#7a3d0a",
26
+ "--cg-pending-strong": "var(--cg-expired-strong)",
27
+ "--cg-unhealthy-strong": "#5d5401",
28
+ "--cg-danger-strong": "#c42828",
21
29
  "--cg-primary-press": "#4b39db",
22
30
  "--cg-primary-tint": "#eeecfe",
23
31
  "--cg-secondary-press": "#a6cf57",
24
32
  "--cg-tertiary-press": "#4dd8e0",
25
33
  "--cg-neutral-0": "#ffffff",
26
34
  "--cg-lavender-50": "#f2f1f7",
35
+ "--cg-neutral-50": "#f4f6f8",
27
36
  "--cg-neutral-100": "#f1f1f1",
28
37
  "--cg-neutral-200": "#e3e3e3",
29
38
  "--cg-neutral-400": "#acacac",
@@ -34,6 +43,7 @@
34
43
  "--cg-surface-page-alt": "var(--cg-lavender-50)",
35
44
  "--cg-surface-card": "var(--cg-neutral-0)",
36
45
  "--cg-surface-subtle": "var(--cg-lavender-50)",
46
+ "--cg-surface-raised": "var(--cg-neutral-50)",
37
47
  "--cg-surface-control": "var(--cg-neutral-200)",
38
48
  "--cg-surface-panel": "var(--cg-primary)",
39
49
  "--cg-surface-code": "var(--cg-ink)",
@@ -127,7 +137,6 @@
127
137
  "--cg-lift-hover": "translateY(-1px)",
128
138
  "--cg-press-scale": "0.985",
129
139
  "--cg-volt": "#b6ff2e",
130
- "--cg-volt-ring": "rgba(182, 255, 46, 0.32)",
131
140
  "--cg-volt-ink": "#243d00",
132
141
  "--cg-scrim": "rgba(34, 34, 34, 0.4)",
133
142
  "--cg-hover": "rgba(34, 34, 34, 0.045)"
@@ -155,6 +164,14 @@
155
164
  "--cg-pending-fill": "var(--cg-expired-fill)",
156
165
  "--cg-unhealthy-fill": "rgba(255, 255, 255, 0.16)",
157
166
  "--cg-danger-fill": "rgba(255, 255, 255, 0.16)",
167
+ "--cg-online-border": "var(--cg-border-hairline)",
168
+ "--cg-expired-border": "var(--cg-border-hairline)",
169
+ "--cg-pending-border": "var(--cg-expired-border)",
170
+ "--cg-online-strong": "var(--cg-text-primary)",
171
+ "--cg-expired-strong": "var(--cg-text-primary)",
172
+ "--cg-pending-strong": "var(--cg-expired-strong)",
173
+ "--cg-unhealthy-strong": "var(--cg-text-primary)",
174
+ "--cg-danger-strong": "var(--cg-text-primary)",
158
175
  "--cg-hover": "rgba(255, 255, 255, 0.10)"
159
176
  },
160
177
  "ink": {
@@ -182,6 +199,14 @@
182
199
  "--cg-pending-fill": "var(--cg-expired-fill)",
183
200
  "--cg-unhealthy-fill": "rgba(255, 255, 255, 0.08)",
184
201
  "--cg-danger-fill": "rgba(255, 255, 255, 0.08)",
202
+ "--cg-online-border": "var(--cg-border-hairline)",
203
+ "--cg-expired-border": "var(--cg-border-hairline)",
204
+ "--cg-pending-border": "var(--cg-expired-border)",
205
+ "--cg-online-strong": "var(--cg-text-primary)",
206
+ "--cg-expired-strong": "var(--cg-text-primary)",
207
+ "--cg-pending-strong": "var(--cg-expired-strong)",
208
+ "--cg-unhealthy-strong": "var(--cg-text-primary)",
209
+ "--cg-danger-strong": "var(--cg-text-primary)",
185
210
  "--cg-hover": "rgba(255, 255, 255, 0.06)"
186
211
  }
187
212
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudgrid-io/ui",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "CloudGrid design system: React components, brand design tokens, Tailwind theme, self-hosted fonts.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/",