@rookcom/monarch-tokens 0.0.0-canary-20261103232247
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +8 -0
- package/dist/appearance.d.ts +40 -0
- package/dist/appearance.d.ts.map +1 -0
- package/dist/appearance.js +46 -0
- package/dist/appearance.js.map +1 -0
- package/dist/brand.d.ts +95 -0
- package/dist/brand.d.ts.map +1 -0
- package/dist/brand.js +107 -0
- package/dist/brand.js.map +1 -0
- package/dist/color.d.ts +42 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +121 -0
- package/dist/color.js.map +1 -0
- package/dist/contrast-report.json +1285 -0
- package/dist/css/base.css +31 -0
- package/dist/css/compat.css +149 -0
- package/dist/css/monarch.css +429 -0
- package/dist/css/products/app.css +85 -0
- package/dist/css/products/atlas.css +85 -0
- package/dist/css/products/base.css +85 -0
- package/dist/css/products/calendar.css +85 -0
- package/dist/css/products/chat.css +85 -0
- package/dist/css/products/dossier.css +85 -0
- package/dist/css/products/dots.css +85 -0
- package/dist/css/products/friends.css +85 -0
- package/dist/css/products/gtm.css +85 -0
- package/dist/css/products/id.css +85 -0
- package/dist/css/products/intellect.css +85 -0
- package/dist/css/products/links.css +85 -0
- package/dist/css/products/people.css +85 -0
- package/dist/css/products/pulse.css +85 -0
- package/dist/css/products/ranker.css +85 -0
- package/dist/css/products/recruit.css +85 -0
- package/dist/css/products/signal.css +85 -0
- package/dist/css/products/stride.css +85 -0
- package/dist/css/products/tickets.css +85 -0
- package/dist/css.d.ts +49 -0
- package/dist/css.d.ts.map +1 -0
- package/dist/css.js +258 -0
- package/dist/css.js.map +1 -0
- package/dist/generated/products.d.ts +106 -0
- package/dist/generated/products.d.ts.map +1 -0
- package/dist/generated/products.js +25 -0
- package/dist/generated/products.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/primitives.d.ts +190 -0
- package/dist/primitives.d.ts.map +1 -0
- package/dist/primitives.js +210 -0
- package/dist/primitives.js.map +1 -0
- package/dist/tailwind.d.ts +275 -0
- package/dist/tailwind.d.ts.map +1 -0
- package/dist/tailwind.js +182 -0
- package/dist/tailwind.js.map +1 -0
- package/dist/themes.d.ts +74 -0
- package/dist/themes.d.ts.map +1 -0
- package/dist/themes.js +301 -0
- package/dist/themes.js.map +1 -0
- package/dist/tokens.json +2455 -0
- package/package.json +62 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @rookcom/monarch-tokens
|
|
2
|
+
|
|
3
|
+
## 0.0.0-canary-20261103232247
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 31753b0: Brand theme engine: `deriveTheme(hex)` derives a full accent token set (every intent role plus `--mn-color-text-link` and `--mn-color-focus-ring`) for the light, dark and both high-contrast themes from one brand colour, with the same contrast guarantees as product accents (4.5:1, 7:1 in high contrast). Pure, no DOM. `brandThemeCss(theme, { selector })` renders it as CSS that follows the Monarch theme selectors, `parseHexColor()` validates input, and invalid colours fall back instead of throwing. `compat: true` adds the legacy `--primary` variables.
|
|
8
|
+
|
|
9
|
+
## 0.2.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Rook Developers is out of Monarch's scope (owner decision), so its product accent is no longer generated: `css/products/developers.css` is removed. No Rook app imports it.
|
|
14
|
+
|
|
15
|
+
## 0.2.2
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- Legacy bridge: `--primary` now uses the accent's readable `strong` colour wherever it also works as a fill with the same text colour, so `text-primary` meets 4.5:1 on every surface (16 of 20 products). Dark-text brands (gold, orange) keep their fill. The Tailwind preset is now assignable to Tailwind 3's `Config` without a cast.
|
|
20
|
+
|
|
21
|
+
## 0.2.1
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- Accent and status `strong` colours are now guaranteed readable (4.5:1, 7:1 in high contrast) on `bg-muted` as well, where tables, selected rows and toolbars sit; several brands' strong text is slightly darker as a result. `appearanceScript()` honours Rook's `data-rook-theme-routes`, so public routes stay light when an app adopts it.
|
|
26
|
+
|
|
27
|
+
## 0.2.0
|
|
28
|
+
|
|
29
|
+
### Minor Changes
|
|
30
|
+
|
|
31
|
+
- Packages move to the public npm registry as `@rookcom/monarch-*` (were `@rookhq/monarch-*` on GitHub Packages). Apps install with plain `npm ci`; no registry token is needed. Releases are published from CI with npm trusted publishing and provenance.
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- a2d52a3: Tokens: contrast is now measured on 8-bit colour as browsers paint it, so no pair passes in maths and fails on screen (Rook Blue `info` was 4.48:1 in the browser).
|
|
36
|
+
|
|
37
|
+
Conformance: recognises the new components and patterns as Monarch primitives; `monarch-ignore-next-line` skips comment-only lines so it can sit next to `eslint-disable-next-line`; 4-digit numbers like `Order #1042` are no longer reported as colours.
|
package/README.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# @rookcom/monarch-tokens
|
|
2
|
+
|
|
3
|
+
Framework-independent Rook design tokens. Exports: `./css` (all themes),
|
|
4
|
+
`./css/base`, `./css/compat` (legacy shadcn names, deprecated),
|
|
5
|
+
`./css/products/<id>`, `./tailwind` (Tailwind 3 preset), `./appearance`
|
|
6
|
+
(pre-paint boot script), `./tokens.json`, `./contrast-report.json`, and a TS
|
|
7
|
+
API (`themes`, `resolveTheme`, `contrast`, `brand`, `products`).
|
|
8
|
+
Docs: ../../docs/guides/theming.md
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving the viewer's appearance into the attributes Monarch CSS reads.
|
|
3
|
+
*
|
|
4
|
+
* Themes switch on attributes only (see css.ts). This module turns a stored
|
|
5
|
+
* preference plus the OS settings into those attributes. `appearanceScript`
|
|
6
|
+
* returns an inline script for <head> that runs before first paint, so there is
|
|
7
|
+
* no flash of the wrong theme.
|
|
8
|
+
*/
|
|
9
|
+
export type ThemePreference = 'light' | 'dark' | 'system';
|
|
10
|
+
export type ContrastPreference = 'standard' | 'more' | 'system';
|
|
11
|
+
export interface Appearance {
|
|
12
|
+
readonly theme: 'light' | 'dark';
|
|
13
|
+
readonly contrast: 'standard' | 'more';
|
|
14
|
+
}
|
|
15
|
+
export interface Environment {
|
|
16
|
+
readonly prefersDark: boolean;
|
|
17
|
+
readonly prefersMoreContrast: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function resolveAppearance(theme: ThemePreference, contrast: ContrastPreference, env: Environment): Appearance;
|
|
20
|
+
/** Applies an appearance to an element (normally <html>). */
|
|
21
|
+
export declare function applyAppearance(el: HTMLElement, a: Appearance): void;
|
|
22
|
+
export interface AppearanceScriptOptions {
|
|
23
|
+
/** Product ID from the Monarch registry, set as data-mn-product. */
|
|
24
|
+
readonly product?: string;
|
|
25
|
+
/** Cookie holding light|dark|system. Rook apps use `rook_theme`. */
|
|
26
|
+
readonly themeCookie?: string;
|
|
27
|
+
/** Cookie holding standard|more|system. */
|
|
28
|
+
readonly contrastCookie?: string;
|
|
29
|
+
readonly defaultTheme?: ThemePreference;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Inline script source for <head>. Options are validated and JSON-encoded, so
|
|
33
|
+
* the output is safe to inject with dangerouslySetInnerHTML.
|
|
34
|
+
*
|
|
35
|
+
* Honours the Rook convention `<html data-rook-theme-routes="regex">`: dark
|
|
36
|
+
* applies only on matching (signed-in) routes; public pages stay light. It is a
|
|
37
|
+
* drop-in replacement for each app's ROOK_THEME_BOOT_SCRIPT.
|
|
38
|
+
*/
|
|
39
|
+
export declare function appearanceScript(options?: AppearanceScriptOptions): string;
|
|
40
|
+
//# sourceMappingURL=appearance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appearance.d.ts","sourceRoot":"","sources":["../src/appearance.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;AAC1D,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEhE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,CAAC;CACxC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,mBAAmB,EAAE,OAAO,CAAC;CACvC;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,eAAe,EACtB,QAAQ,EAAE,kBAAkB,EAC5B,GAAG,EAAE,WAAW,GACf,UAAU,CAKZ;AAED,6DAA6D;AAC7D,wBAAgB,eAAe,CAAC,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE,UAAU,GAAG,IAAI,CAKpE;AAED,MAAM,WAAW,uBAAuB;IACtC,oEAAoE;IACpE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,oEAAoE;IACpE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,2CAA2C;IAC3C,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,YAAY,CAAC,EAAE,eAAe,CAAC;CACzC;AAKD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,uBAA4B,GAAG,MAAM,CAiB9E"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving the viewer's appearance into the attributes Monarch CSS reads.
|
|
3
|
+
*
|
|
4
|
+
* Themes switch on attributes only (see css.ts). This module turns a stored
|
|
5
|
+
* preference plus the OS settings into those attributes. `appearanceScript`
|
|
6
|
+
* returns an inline script for <head> that runs before first paint, so there is
|
|
7
|
+
* no flash of the wrong theme.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveAppearance(theme, contrast, env) {
|
|
10
|
+
return {
|
|
11
|
+
theme: theme === 'system' ? (env.prefersDark ? 'dark' : 'light') : theme,
|
|
12
|
+
contrast: contrast === 'system' ? (env.prefersMoreContrast ? 'more' : 'standard') : contrast,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** Applies an appearance to an element (normally <html>). */
|
|
16
|
+
export function applyAppearance(el, a) {
|
|
17
|
+
el.setAttribute('data-mn-theme', a.theme);
|
|
18
|
+
el.setAttribute('data-mn-contrast', a.contrast);
|
|
19
|
+
// `.dark` is kept because every Rook app's Tailwind `dark:` variant uses it.
|
|
20
|
+
el.classList.toggle('dark', a.theme === 'dark');
|
|
21
|
+
}
|
|
22
|
+
const ID = /^[a-z][a-z0-9-]{1,31}$/;
|
|
23
|
+
const COOKIE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
24
|
+
/**
|
|
25
|
+
* Inline script source for <head>. Options are validated and JSON-encoded, so
|
|
26
|
+
* the output is safe to inject with dangerouslySetInnerHTML.
|
|
27
|
+
*
|
|
28
|
+
* Honours the Rook convention `<html data-rook-theme-routes="regex">`: dark
|
|
29
|
+
* applies only on matching (signed-in) routes; public pages stay light. It is a
|
|
30
|
+
* drop-in replacement for each app's ROOK_THEME_BOOT_SCRIPT.
|
|
31
|
+
*/
|
|
32
|
+
export function appearanceScript(options = {}) {
|
|
33
|
+
const { product, themeCookie = 'rook_theme', contrastCookie = 'mn_contrast', defaultTheme = 'light', } = options;
|
|
34
|
+
if (product !== undefined && !ID.test(product))
|
|
35
|
+
throw new Error(`Invalid product id: ${product}`);
|
|
36
|
+
if (!COOKIE.test(themeCookie) || !COOKIE.test(contrastCookie))
|
|
37
|
+
throw new Error('Invalid cookie name');
|
|
38
|
+
const cfg = JSON.stringify({
|
|
39
|
+
p: product ?? null,
|
|
40
|
+
t: themeCookie,
|
|
41
|
+
c: contrastCookie,
|
|
42
|
+
d: defaultTheme,
|
|
43
|
+
});
|
|
44
|
+
return `(function(){try{var o=${cfg};var e=document.documentElement;function k(n){var m=document.cookie.match(new RegExp('(?:^|; )'+n+'=([^;]*)'));return m?decodeURIComponent(m[1]):null}var t=k(o.t)||o.d,c=k(o.c)||'system',q=function(s){return window.matchMedia&&window.matchMedia(s).matches};if(t==='system')t=q('(prefers-color-scheme: dark)')?'dark':'light';if(t!=='dark')t='light';var r=e.getAttribute('data-rook-theme-routes');if(r&&!new RegExp(r).test(location.pathname))t='light';if(c==='system')c=q('(prefers-contrast: more)')?'more':'standard';if(c!=='more')c='standard';e.setAttribute('data-mn-theme',t);e.setAttribute('data-mn-contrast',c);e.classList.toggle('dark',t==='dark');if(o.p)e.setAttribute('data-mn-product',o.p)}catch(_){}})();`;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=appearance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appearance.js","sourceRoot":"","sources":["../src/appearance.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAeH,MAAM,UAAU,iBAAiB,CAC/B,KAAsB,EACtB,QAA4B,EAC5B,GAAgB;IAEhB,OAAO;QACL,KAAK,EAAE,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK;QACxE,QAAQ,EAAE,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ;KAC7F,CAAC;AACJ,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,eAAe,CAAC,EAAe,EAAE,CAAa;IAC5D,EAAE,CAAC,YAAY,CAAC,eAAe,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;IAC1C,EAAE,CAAC,YAAY,CAAC,kBAAkB,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;IAChD,6EAA6E;IAC7E,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC;AAClD,CAAC;AAYD,MAAM,EAAE,GAAG,wBAAwB,CAAC;AACpC,MAAM,MAAM,GAAG,uBAAuB,CAAC;AAEvC;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAAmC,EAAE;IACpE,MAAM,EACJ,OAAO,EACP,WAAW,GAAG,YAAY,EAC1B,cAAc,GAAG,aAAa,EAC9B,YAAY,GAAG,OAAO,GACvB,GAAG,OAAO,CAAC;IACZ,IAAI,OAAO,KAAK,SAAS,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,OAAO,EAAE,CAAC,CAAC;IAClG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC;QAC3D,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;IACzC,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC;QACzB,CAAC,EAAE,OAAO,IAAI,IAAI;QAClB,CAAC,EAAE,WAAW;QACd,CAAC,EAAE,cAAc;QACjB,CAAC,EAAE,YAAY;KAChB,CAAC,CAAC;IACH,OAAO,yBAAyB,GAAG,0sBAA0sB,CAAC;AAChvB,CAAC"}
|
package/dist/brand.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand theme engine: one brand colour in, a complete accent token set out.
|
|
3
|
+
*
|
|
4
|
+
* Products that let a customer pick a colour (booking pages, support portals,
|
|
5
|
+
* community sites, careers and status pages) previously each carried their own
|
|
6
|
+
* contrast maths. `deriveTheme` replaces those copies: it runs the same
|
|
7
|
+
* `deriveIntent` that builds every Monarch product accent, for all four
|
|
8
|
+
* themes, so a tenant colour gets exactly the guarantees a Rook product
|
|
9
|
+
* colour gets (ADR 0003):
|
|
10
|
+
*
|
|
11
|
+
* - `on-solid` on `solid`, `solid-hover` and `solid-active`: ≥ 4.5:1 (7:1 in
|
|
12
|
+
* the high-contrast themes);
|
|
13
|
+
* - `strong` (links, icons, focus rings) on canvas, surface, subtle, muted and
|
|
14
|
+
* the accent's own subtle tints: ≥ 4.5:1 (7:1 high contrast);
|
|
15
|
+
* - the fill stays distinguishable from the canvas (≥ 1.5:1, 4.5:1 high contrast).
|
|
16
|
+
*
|
|
17
|
+
* Everything here is a pure function of its arguments: no DOM, no globals, no
|
|
18
|
+
* caching. It runs the same on the server, in a worker or in the browser.
|
|
19
|
+
*/
|
|
20
|
+
import { type Hsl } from './color.js';
|
|
21
|
+
import { type IntentReport, type IntentScale, type ThemeId } from './themes.js';
|
|
22
|
+
/** CSS custom properties, ready for a `style` object or a stylesheet block. */
|
|
23
|
+
export type BrandThemeVars = Readonly<Record<`--${string}`, string>>;
|
|
24
|
+
export interface BrandThemeMode {
|
|
25
|
+
/** The derived accent roles for this theme (HSL). */
|
|
26
|
+
readonly accent: IntentScale;
|
|
27
|
+
/** What `deriveIntent` changed to meet contrast, in plain language, and the measured ratios. */
|
|
28
|
+
readonly report: IntentReport;
|
|
29
|
+
/** `--mn-color-accent-*`, `--mn-color-text-link` and `--mn-color-focus-ring` for this theme. */
|
|
30
|
+
readonly vars: BrandThemeVars;
|
|
31
|
+
}
|
|
32
|
+
export interface BrandTheme {
|
|
33
|
+
/** The value that was passed in, unchanged. */
|
|
34
|
+
readonly input: string;
|
|
35
|
+
/** The colour the theme was derived from, as lowercase `#rrggbb`. */
|
|
36
|
+
readonly brand: string;
|
|
37
|
+
/** False when `input` was not a hex colour and the fallback was used instead. */
|
|
38
|
+
readonly valid: boolean;
|
|
39
|
+
/** One accent set per Monarch theme. */
|
|
40
|
+
readonly modes: Readonly<Record<ThemeId, BrandThemeMode>>;
|
|
41
|
+
}
|
|
42
|
+
export interface DeriveThemeOptions {
|
|
43
|
+
/**
|
|
44
|
+
* Used when the brand colour is missing or not a hex colour, so a bad value
|
|
45
|
+
* saved by a customer never breaks the page. Default: the Monarch default
|
|
46
|
+
* accent (Rook Red).
|
|
47
|
+
*/
|
|
48
|
+
readonly fallback?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Also emit the legacy shadcn-style variables (`--primary`,
|
|
51
|
+
* `--primary-foreground`, `--ring`, `--sidebar-primary*`) for apps still on
|
|
52
|
+
* the compat bridge. Default false.
|
|
53
|
+
*/
|
|
54
|
+
readonly compat?: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Parses `#rgb`, `#rrggbb`, `rgb` or `rrggbb` (any case, surrounding spaces
|
|
58
|
+
* allowed). Returns null for anything else, including named colours and
|
|
59
|
+
* `#rrggbbaa`: a brand colour must be opaque for contrast to mean anything.
|
|
60
|
+
*/
|
|
61
|
+
export declare function parseHexColor(input: string | null | undefined): Hsl | null;
|
|
62
|
+
/**
|
|
63
|
+
* Derives a full accent token set from one brand colour, for the light, dark
|
|
64
|
+
* and both high-contrast themes. Pure: the same input always gives the same
|
|
65
|
+
* output, and nothing touches the DOM.
|
|
66
|
+
*
|
|
67
|
+
* ```ts
|
|
68
|
+
* const theme = deriveTheme(workspace.brandColor);
|
|
69
|
+
* // Inline, for a page that is always light:
|
|
70
|
+
* <div style={theme.modes.light.vars}>…</div>
|
|
71
|
+
* // Or a stylesheet that follows the user's theme:
|
|
72
|
+
* <style>{brandThemeCss(theme, { selector: '.booking-page' })}</style>
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export declare function deriveTheme(brand: string, options?: DeriveThemeOptions): BrandTheme;
|
|
76
|
+
export interface BrandThemeCssOptions {
|
|
77
|
+
/**
|
|
78
|
+
* The element the theme applies to. Default `:root` (the whole page). Use a
|
|
79
|
+
* class or attribute to theme one region, such as a booking widget.
|
|
80
|
+
*/
|
|
81
|
+
readonly selector?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Wrap the rules in `@layer monarch.tokens` so they sit with the other
|
|
84
|
+
* Monarch tokens and any unlayered app rule still wins. Default true.
|
|
85
|
+
*/
|
|
86
|
+
readonly layer?: boolean;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The theme as CSS text: one block per theme, keyed to the same selectors as
|
|
90
|
+
* the Monarch stylesheets (`.dark` / `data-mn-theme="dark"` and
|
|
91
|
+
* `data-mn-contrast="more"` on an ancestor or on the element itself), so it
|
|
92
|
+
* follows the user's theme with no script. Pure string output.
|
|
93
|
+
*/
|
|
94
|
+
export declare function brandThemeCss(theme: BrandTheme, options?: BrandThemeCssOptions): string;
|
|
95
|
+
//# sourceMappingURL=brand.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brand.d.ts","sourceRoot":"","sources":["../src/brand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAqC,KAAK,GAAG,EAAE,MAAM,YAAY,CAAC;AAGzE,OAAO,EAKL,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,OAAO,EACb,MAAM,aAAa,CAAC;AAErB,+EAA+E;AAC/E,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;AAErE,MAAM,WAAW,cAAc;IAC7B,qDAAqD;IACrD,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,gGAAgG;IAChG,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,gGAAgG;IAChG,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;CAC/B;AAED,MAAM,WAAW,UAAU;IACzB,+CAA+C;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iFAAiF;IACjF,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,wCAAwC;IACxC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC;CAC3D;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,GAAG,GAAG,IAAI,CAM1E;AA4BD;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,UAAU,CAWvF;AAED,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAiB3F"}
|
package/dist/brand.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand theme engine: one brand colour in, a complete accent token set out.
|
|
3
|
+
*
|
|
4
|
+
* Products that let a customer pick a colour (booking pages, support portals,
|
|
5
|
+
* community sites, careers and status pages) previously each carried their own
|
|
6
|
+
* contrast maths. `deriveTheme` replaces those copies: it runs the same
|
|
7
|
+
* `deriveIntent` that builds every Monarch product accent, for all four
|
|
8
|
+
* themes, so a tenant colour gets exactly the guarantees a Rook product
|
|
9
|
+
* colour gets (ADR 0003):
|
|
10
|
+
*
|
|
11
|
+
* - `on-solid` on `solid`, `solid-hover` and `solid-active`: ≥ 4.5:1 (7:1 in
|
|
12
|
+
* the high-contrast themes);
|
|
13
|
+
* - `strong` (links, icons, focus rings) on canvas, surface, subtle, muted and
|
|
14
|
+
* the accent's own subtle tints: ≥ 4.5:1 (7:1 high contrast);
|
|
15
|
+
* - the fill stays distinguishable from the canvas (≥ 1.5:1, 4.5:1 high contrast).
|
|
16
|
+
*
|
|
17
|
+
* Everything here is a pure function of its arguments: no DOM, no globals, no
|
|
18
|
+
* caching. It runs the same on the server, in a worker or in the browser.
|
|
19
|
+
*/
|
|
20
|
+
import { fromHex, toChannels, toCss, toHex } from './color.js';
|
|
21
|
+
import { compatPrimary, INTENT_ROLES, themeSelectors, LAYER } from './css.js';
|
|
22
|
+
import { defaultAccent } from './primitives.js';
|
|
23
|
+
import { THEME_IDS, deriveIntent, resolveTheme, themes, } from './themes.js';
|
|
24
|
+
/**
|
|
25
|
+
* Parses `#rgb`, `#rrggbb`, `rgb` or `rrggbb` (any case, surrounding spaces
|
|
26
|
+
* allowed). Returns null for anything else, including named colours and
|
|
27
|
+
* `#rrggbbaa`: a brand colour must be opaque for contrast to mean anything.
|
|
28
|
+
*/
|
|
29
|
+
export function parseHexColor(input) {
|
|
30
|
+
if (typeof input !== 'string')
|
|
31
|
+
return null;
|
|
32
|
+
const v = input.trim().replace(/^#/, '');
|
|
33
|
+
const six = /^[0-9a-f]{3}$/i.test(v) ? v.replace(/./g, (c) => c + c) : v;
|
|
34
|
+
if (!/^[0-9a-f]{6}$/i.test(six))
|
|
35
|
+
return null;
|
|
36
|
+
return fromHex(six);
|
|
37
|
+
}
|
|
38
|
+
function accentVars(theme, base, accent, compat) {
|
|
39
|
+
const vars = {};
|
|
40
|
+
for (const role of INTENT_ROLES)
|
|
41
|
+
vars[`--mn-color-accent-${role}`] = toCss(accent[role]);
|
|
42
|
+
// These two are declared on :root as var(--mn-color-accent-strong). A custom
|
|
43
|
+
// property's var() is resolved where it is declared, so re-theming a region
|
|
44
|
+
// must restate them, or links and focus rings keep the page accent.
|
|
45
|
+
vars['--mn-color-text-link'] = toCss(accent.strong);
|
|
46
|
+
vars['--mn-color-focus-ring'] =
|
|
47
|
+
themes[theme].contrast === 'more' ? 'var(--mn-color-text-default)' : toCss(accent.strong);
|
|
48
|
+
if (compat) {
|
|
49
|
+
const p = compatPrimary(resolveTheme(themes[theme], base));
|
|
50
|
+
vars['--primary'] = toChannels(p.fill);
|
|
51
|
+
vars['--primary-foreground'] = toChannels(p.on);
|
|
52
|
+
vars['--ring'] = toChannels(accent.strong);
|
|
53
|
+
vars['--sidebar-primary'] = toChannels(p.fill);
|
|
54
|
+
vars['--sidebar-primary-foreground'] = toChannels(p.on);
|
|
55
|
+
vars['--sidebar-ring'] = toChannels(accent.strong);
|
|
56
|
+
}
|
|
57
|
+
return vars;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Derives a full accent token set from one brand colour, for the light, dark
|
|
61
|
+
* and both high-contrast themes. Pure: the same input always gives the same
|
|
62
|
+
* output, and nothing touches the DOM.
|
|
63
|
+
*
|
|
64
|
+
* ```ts
|
|
65
|
+
* const theme = deriveTheme(workspace.brandColor);
|
|
66
|
+
* // Inline, for a page that is always light:
|
|
67
|
+
* <div style={theme.modes.light.vars}>…</div>
|
|
68
|
+
* // Or a stylesheet that follows the user's theme:
|
|
69
|
+
* <style>{brandThemeCss(theme, { selector: '.booking-page' })}</style>
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export function deriveTheme(brand, options = {}) {
|
|
73
|
+
const parsed = parseHexColor(brand);
|
|
74
|
+
const fallback = parseHexColor(options.fallback) ?? defaultAccent;
|
|
75
|
+
const base = parsed ?? fallback;
|
|
76
|
+
const compat = options.compat ?? false;
|
|
77
|
+
const modes = {};
|
|
78
|
+
for (const id of THEME_IDS) {
|
|
79
|
+
const { scale, report } = deriveIntent(base, themes[id]);
|
|
80
|
+
modes[id] = { accent: scale, report, vars: accentVars(id, base, scale, compat) };
|
|
81
|
+
}
|
|
82
|
+
return { input: brand, brand: toHex(base), valid: parsed !== null, modes };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The theme as CSS text: one block per theme, keyed to the same selectors as
|
|
86
|
+
* the Monarch stylesheets (`.dark` / `data-mn-theme="dark"` and
|
|
87
|
+
* `data-mn-contrast="more"` on an ancestor or on the element itself), so it
|
|
88
|
+
* follows the user's theme with no script. Pure string output.
|
|
89
|
+
*/
|
|
90
|
+
export function brandThemeCss(theme, options = {}) {
|
|
91
|
+
const target = options.selector?.trim() || ':root';
|
|
92
|
+
const selectorsFor = (id) => id === 'light'
|
|
93
|
+
? [target]
|
|
94
|
+
: themeSelectors(id).flatMap((q) => [`${q}${target}`, `${q} ${target}`]);
|
|
95
|
+
const indent = options.layer === false ? '' : ' ';
|
|
96
|
+
const blocks = THEME_IDS.map((id) => {
|
|
97
|
+
const body = Object.entries(theme.modes[id].vars)
|
|
98
|
+
.map(([k, v]) => `${indent} ${k}: ${v};`)
|
|
99
|
+
.join('\n');
|
|
100
|
+
return `${indent}${selectorsFor(id).join(`,\n${indent}`)} {\n${body}\n${indent}}`;
|
|
101
|
+
});
|
|
102
|
+
const comment = `/* Monarch brand theme ${theme.brand}${theme.valid ? '' : ' (fallback)'} */`;
|
|
103
|
+
return options.layer === false
|
|
104
|
+
? `${comment}\n${blocks.join('\n\n')}\n`
|
|
105
|
+
: `${comment}\n@layer ${LAYER} {\n${blocks.join('\n\n')}\n}\n`;
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=brand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brand.js","sourceRoot":"","sources":["../src/brand.ts"],"names":[],"mappings":"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"}
|
package/dist/color.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Colour math for Monarch tokens.
|
|
3
|
+
*
|
|
4
|
+
* All colours are authored in HSL because every Rook app already stores its
|
|
5
|
+
* tokens as HSL channels (`354 71% 52%`). Contrast is computed with the WCAG 2.x
|
|
6
|
+
* relative-luminance formula, which is what WCAG 2.2 AA is measured against.
|
|
7
|
+
*/
|
|
8
|
+
export interface Hsl {
|
|
9
|
+
/** Hue, 0–360. */
|
|
10
|
+
readonly h: number;
|
|
11
|
+
/** Saturation, 0–100. */
|
|
12
|
+
readonly s: number;
|
|
13
|
+
/** Lightness, 0–100. */
|
|
14
|
+
readonly l: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function hsl(h: number, s: number, l: number): Hsl;
|
|
17
|
+
export declare function clamp(value: number, min: number, max: number): number;
|
|
18
|
+
/** `354 71% 52%`, the channel format shadcn-style `hsl(var(--x) / a)` expects. */
|
|
19
|
+
export declare function toChannels(c: Hsl): string;
|
|
20
|
+
/** `hsl(354 71% 52%)`. */
|
|
21
|
+
export declare function toCss(c: Hsl): string;
|
|
22
|
+
/** sRGB in 0–1. */
|
|
23
|
+
export declare function toRgb(c: Hsl): [number, number, number];
|
|
24
|
+
export declare function toHex(c: Hsl): string;
|
|
25
|
+
/** Parses `#rrggbb` into HSL. Used for registry entries authored as hex. */
|
|
26
|
+
export declare function fromHex(hex: string): Hsl;
|
|
27
|
+
export declare function relativeLuminance(c: Hsl): number;
|
|
28
|
+
/** WCAG contrast ratio, 1–21. */
|
|
29
|
+
export declare function contrast(a: Hsl, b: Hsl): number;
|
|
30
|
+
/**
|
|
31
|
+
* Moves lightness in `direction` until `color` reaches `minRatio` against every
|
|
32
|
+
* colour in `against`. Hue and saturation are preserved so the brand stays
|
|
33
|
+
* recognisable. Throws if no lightness satisfies the target, so an impossible
|
|
34
|
+
* theme fails the build instead of shipping silently.
|
|
35
|
+
*/
|
|
36
|
+
export declare function ensureContrast(color: Hsl, against: readonly Hsl[], minRatio: number, direction: 'darken' | 'lighten'): Hsl;
|
|
37
|
+
/** Picks whichever candidate foreground has the higher contrast on `bg`. */
|
|
38
|
+
export declare function bestForeground(bg: Hsl, candidates: readonly Hsl[]): {
|
|
39
|
+
color: Hsl;
|
|
40
|
+
ratio: number;
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=color.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"color.d.ts","sourceRoot":"","sources":["../src/color.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,GAAG;IAClB,kBAAkB;IAClB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,yBAAyB;IACzB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,wBAAwB;IACxB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,GAAG,CAExD;AAED,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAErE;AAOD,kFAAkF;AAClF,wBAAgB,UAAU,CAAC,CAAC,EAAE,GAAG,GAAG,MAAM,CAEzC;AAED,0BAA0B;AAC1B,wBAAgB,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,MAAM,CAEpC;AAED,mBAAmB;AACnB,wBAAgB,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAOtD;AAED,wBAAgB,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,MAAM,CAWpC;AAED,4EAA4E;AAC5E,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAoBxC;AAWD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,GAAG,GAAG,MAAM,CAShD;AAED,iCAAiC;AACjC,wBAAgB,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,GAAG,MAAM,CAK/C;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,GAAG,EACV,OAAO,EAAE,SAAS,GAAG,EAAE,EACvB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,QAAQ,GAAG,SAAS,GAC9B,GAAG,CAYL;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,EAAE,EAAE,GAAG,EAAE,UAAU,EAAE,SAAS,GAAG,EAAE,GAAG;IAAE,KAAK,EAAE,GAAG,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAQjG"}
|
package/dist/color.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Colour math for Monarch tokens.
|
|
3
|
+
*
|
|
4
|
+
* All colours are authored in HSL because every Rook app already stores its
|
|
5
|
+
* tokens as HSL channels (`354 71% 52%`). Contrast is computed with the WCAG 2.x
|
|
6
|
+
* relative-luminance formula, which is what WCAG 2.2 AA is measured against.
|
|
7
|
+
*/
|
|
8
|
+
export function hsl(h, s, l) {
|
|
9
|
+
return { h: ((h % 360) + 360) % 360, s: clamp(s, 0, 100), l: clamp(l, 0, 100) };
|
|
10
|
+
}
|
|
11
|
+
export function clamp(value, min, max) {
|
|
12
|
+
return Math.min(max, Math.max(min, value));
|
|
13
|
+
}
|
|
14
|
+
/** Rounds to 1 decimal so generated CSS is stable and readable. */
|
|
15
|
+
function round1(n) {
|
|
16
|
+
return Math.round(n * 10) / 10;
|
|
17
|
+
}
|
|
18
|
+
/** `354 71% 52%`, the channel format shadcn-style `hsl(var(--x) / a)` expects. */
|
|
19
|
+
export function toChannels(c) {
|
|
20
|
+
return `${round1(c.h)} ${round1(c.s)}% ${round1(c.l)}%`;
|
|
21
|
+
}
|
|
22
|
+
/** `hsl(354 71% 52%)`. */
|
|
23
|
+
export function toCss(c) {
|
|
24
|
+
return `hsl(${toChannels(c)})`;
|
|
25
|
+
}
|
|
26
|
+
/** sRGB in 0–1. */
|
|
27
|
+
export function toRgb(c) {
|
|
28
|
+
const s = c.s / 100;
|
|
29
|
+
const l = c.l / 100;
|
|
30
|
+
const k = (n) => (n + c.h / 30) % 12;
|
|
31
|
+
const a = s * Math.min(l, 1 - l);
|
|
32
|
+
const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
|
33
|
+
return [f(0), f(8), f(4)];
|
|
34
|
+
}
|
|
35
|
+
export function toHex(c) {
|
|
36
|
+
return ('#' +
|
|
37
|
+
toRgb(c)
|
|
38
|
+
.map((v) => Math.round(v * 255)
|
|
39
|
+
.toString(16)
|
|
40
|
+
.padStart(2, '0'))
|
|
41
|
+
.join(''));
|
|
42
|
+
}
|
|
43
|
+
/** Parses `#rrggbb` into HSL. Used for registry entries authored as hex. */
|
|
44
|
+
export function fromHex(hex) {
|
|
45
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
46
|
+
if (!m?.[1])
|
|
47
|
+
throw new Error(`Invalid hex colour: ${hex}`);
|
|
48
|
+
const n = parseInt(m[1], 16);
|
|
49
|
+
const r = ((n >> 16) & 255) / 255;
|
|
50
|
+
const g = ((n >> 8) & 255) / 255;
|
|
51
|
+
const b = (n & 255) / 255;
|
|
52
|
+
const max = Math.max(r, g, b);
|
|
53
|
+
const min = Math.min(r, g, b);
|
|
54
|
+
const l = (max + min) / 2;
|
|
55
|
+
const d = max - min;
|
|
56
|
+
let h = 0;
|
|
57
|
+
let s = 0;
|
|
58
|
+
if (d !== 0) {
|
|
59
|
+
s = d / (1 - Math.abs(2 * l - 1));
|
|
60
|
+
if (max === r)
|
|
61
|
+
h = 60 * (((g - b) / d) % 6);
|
|
62
|
+
else if (max === g)
|
|
63
|
+
h = 60 * ((b - r) / d + 2);
|
|
64
|
+
else
|
|
65
|
+
h = 60 * ((r - g) / d + 4);
|
|
66
|
+
}
|
|
67
|
+
return hsl(h, s * 100, l * 100);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Browsers paint 8-bit channels, so contrast is measured on the quantised
|
|
71
|
+
* colour. Otherwise a pair can pass at 4.504:1 in floating point and fail at
|
|
72
|
+
* 4.48:1 on screen, which is what axe measures.
|
|
73
|
+
*/
|
|
74
|
+
function quantise(v) {
|
|
75
|
+
return Math.round(v * 255) / 255;
|
|
76
|
+
}
|
|
77
|
+
export function relativeLuminance(c) {
|
|
78
|
+
const [r, g, b] = toRgb(c)
|
|
79
|
+
.map(quantise)
|
|
80
|
+
.map((v) => (v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4)));
|
|
81
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
82
|
+
}
|
|
83
|
+
/** WCAG contrast ratio, 1–21. */
|
|
84
|
+
export function contrast(a, b) {
|
|
85
|
+
const la = relativeLuminance(a);
|
|
86
|
+
const lb = relativeLuminance(b);
|
|
87
|
+
const [hi, lo] = la > lb ? [la, lb] : [lb, la];
|
|
88
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Moves lightness in `direction` until `color` reaches `minRatio` against every
|
|
92
|
+
* colour in `against`. Hue and saturation are preserved so the brand stays
|
|
93
|
+
* recognisable. Throws if no lightness satisfies the target, so an impossible
|
|
94
|
+
* theme fails the build instead of shipping silently.
|
|
95
|
+
*/
|
|
96
|
+
export function ensureContrast(color, against, minRatio, direction) {
|
|
97
|
+
const step = direction === 'darken' ? -0.5 : 0.5;
|
|
98
|
+
let current = color;
|
|
99
|
+
for (let i = 0; i < 220; i++) {
|
|
100
|
+
if (against.every((bg) => contrast(current, bg) >= minRatio))
|
|
101
|
+
return current;
|
|
102
|
+
const next = hsl(current.h, current.s, current.l + step);
|
|
103
|
+
if (next.l === current.l)
|
|
104
|
+
break;
|
|
105
|
+
current = next;
|
|
106
|
+
}
|
|
107
|
+
throw new Error(`Cannot reach ${minRatio}:1 for ${toChannels(color)} against ${against.map(toChannels).join(', ')}`);
|
|
108
|
+
}
|
|
109
|
+
/** Picks whichever candidate foreground has the higher contrast on `bg`. */
|
|
110
|
+
export function bestForeground(bg, candidates) {
|
|
111
|
+
let best;
|
|
112
|
+
for (const c of candidates) {
|
|
113
|
+
const ratio = contrast(c, bg);
|
|
114
|
+
if (!best || ratio > best.ratio)
|
|
115
|
+
best = { color: c, ratio };
|
|
116
|
+
}
|
|
117
|
+
if (!best)
|
|
118
|
+
throw new Error('bestForeground needs at least one candidate');
|
|
119
|
+
return best;
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=color.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"color.js","sourceRoot":"","sources":["../src/color.ts"],"names":[],"mappings":"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"}
|