@bsuite/theme 0.10.3 → 0.10.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/react/BrandingProvider.d.ts +44 -1
- package/dist/react/BrandingProvider.d.ts.map +1 -1
- package/dist/react/BrandingProvider.js +14 -48
- package/package.json +1 -1
- package/src/css/braden.css +14 -4
- package/src/css/utilities.css +41 -0
- package/src/css/vars.css +36 -7
- package/src/preset-v4.css +25 -0
|
@@ -1,5 +1,48 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* BrandingProvider — runtime enterprise white-labelling
|
|
3
|
+
* Version 0.4.1
|
|
4
|
+
*
|
|
5
|
+
* On mount:
|
|
6
|
+
* 1. Calls supabase.rpc('branding_json_for_tenant') using the user's authed session.
|
|
7
|
+
* 2. Validates each colour value is a well-formed oklch() string.
|
|
8
|
+
* 3. Applies each key as a CSS custom property on document.documentElement.
|
|
9
|
+
* 4. Caches the result in localStorage under BRANDING_STORAGE_KEY for FOUC prevention.
|
|
10
|
+
* 5. Subscribes to tenant row changes via Supabase Realtime so branding updates
|
|
11
|
+
* propagate without a page reload (within ~1 second).
|
|
12
|
+
*
|
|
13
|
+
* Security / brand policies:
|
|
14
|
+
* - Only oklch() values are accepted for colour fields — hex/rgb/hsl are rejected.
|
|
15
|
+
* - --role-error and --role-destructive are NOT overridable by tenants.
|
|
16
|
+
* Colourblind policy (purple error) is a system-level requirement, not a brand pref.
|
|
17
|
+
* - brand="braden" short-circuits all Supabase calls — Braden corporate site uses
|
|
18
|
+
* static CSS tokens, not runtime tenant overrides.
|
|
19
|
+
* - SEC-002 / SEC-003 (added 0.4.1) — tenant-controlled URL fields
|
|
20
|
+
* (`logo_url`, `logo_light_url`, `logo_dark_url`, `mark_url`,
|
|
21
|
+
* `favicon_url`) and `font_stack` are routed through `branding-sanitize`
|
|
22
|
+
* at the DOM-apply sink. Dangerous schemes (`javascript:`, `data:`,
|
|
23
|
+
* `vbscript:`, `blob:`, `file:`) are rejected; the URL is parsed and
|
|
24
|
+
* re-serialised so breakout chars are percent-encoded; the `url()`
|
|
25
|
+
* token is emitted in the safe quoted form with `"` and `\` escaped;
|
|
26
|
+
* font values carrying CSS-breakout tokens (`< > ( ) { } ; @ \ /* *\/`)
|
|
27
|
+
* are rejected and the var is cleared so the default font applies.
|
|
28
|
+
*
|
|
29
|
+
* Environment flags:
|
|
30
|
+
* - VITE_ENABLE_BRANDING_OVERRIDE (default: 'true') — set 'false' as kill switch.
|
|
31
|
+
*
|
|
32
|
+
* Usage:
|
|
33
|
+
* <ThemeProvider>
|
|
34
|
+
* <BrandingProvider supabaseClient={supabase}>
|
|
35
|
+
* {children}
|
|
36
|
+
* </BrandingProvider>
|
|
37
|
+
* </ThemeProvider>
|
|
38
|
+
*
|
|
39
|
+
* For Braden corporate (skips all RPC/Realtime):
|
|
40
|
+
* <BrandingProvider brand="braden" supabaseClient={supabase}>
|
|
41
|
+
* {children}
|
|
42
|
+
* </BrandingProvider>
|
|
43
|
+
*/
|
|
2
44
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
45
|
+
import type { ReactNode } from 'react';
|
|
3
46
|
export declare const BRANDING_STORAGE_KEY = "bsuite_tenant_branding";
|
|
4
47
|
export declare const BRANDING_OVERRIDE_FLAG = "VITE_ENABLE_BRANDING_OVERRIDE";
|
|
5
48
|
/** Shape returned by the branding_json_for_tenant RPC */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BrandingProvider.d.ts","sourceRoot":"","sources":["../../src/react/BrandingProvider.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"BrandingProvider.d.ts","sourceRoot":"","sources":["../../src/react/BrandingProvider.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAQtC,eAAO,MAAM,oBAAoB,2BAA2B,CAAA;AAC5D,eAAO,MAAM,sBAAsB,kCAAkC,CAAA;AAErE,yDAAyD;AACzD,MAAM,WAAW,cAAc;IAC7B,wDAAwD;IACxD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,oCAAoC;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,+BAA+B;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,8BAA8B;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kBAAkB;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,oDAAoD;IACpD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC3B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAA;IAC/B,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7B;AAED,eAAO,MAAM,eAAe,2DAA6D,CAAA;AAEzF,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAE9C,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,SAAS,CAAA;IACnB,2DAA2D;IAC3D,cAAc,EAAE,cAAc,CAAA;IAC9B;;;;OAIG;IACH,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC;AAyLD,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,cAAc,EACd,KAAgB,EAChB,qBAA4B,GAC7B,EAAE,qBAAqB,+BA4GvB"}
|
|
@@ -1,47 +1,4 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* BrandingProvider — runtime enterprise white-labelling
|
|
4
|
-
* Version 0.4.1
|
|
5
|
-
*
|
|
6
|
-
* On mount:
|
|
7
|
-
* 1. Calls supabase.rpc('branding_json_for_tenant') using the user's authed session.
|
|
8
|
-
* 2. Validates each colour value is a well-formed oklch() string.
|
|
9
|
-
* 3. Applies each key as a CSS custom property on document.documentElement.
|
|
10
|
-
* 4. Caches the result in localStorage under BRANDING_STORAGE_KEY for FOUC prevention.
|
|
11
|
-
* 5. Subscribes to tenant row changes via Supabase Realtime so branding updates
|
|
12
|
-
* propagate without a page reload (within ~1 second).
|
|
13
|
-
*
|
|
14
|
-
* Security / brand policies:
|
|
15
|
-
* - Only oklch() values are accepted for colour fields — hex/rgb/hsl are rejected.
|
|
16
|
-
* - --role-error and --role-destructive are NOT overridable by tenants.
|
|
17
|
-
* Colourblind policy (purple error) is a system-level requirement, not a brand pref.
|
|
18
|
-
* - brand="braden" short-circuits all Supabase calls — Braden corporate site uses
|
|
19
|
-
* static CSS tokens, not runtime tenant overrides.
|
|
20
|
-
* - SEC-002 / SEC-003 (added 0.4.1) — tenant-controlled URL fields
|
|
21
|
-
* (`logo_url`, `logo_light_url`, `logo_dark_url`, `mark_url`,
|
|
22
|
-
* `favicon_url`) and `font_stack` are routed through `branding-sanitize`
|
|
23
|
-
* at the DOM-apply sink. Dangerous schemes (`javascript:`, `data:`,
|
|
24
|
-
* `vbscript:`, `blob:`, `file:`) are rejected; the URL is parsed and
|
|
25
|
-
* re-serialised so breakout chars are percent-encoded; the `url()`
|
|
26
|
-
* token is emitted in the safe quoted form with `"` and `\` escaped;
|
|
27
|
-
* font values carrying CSS-breakout tokens (`< > ( ) { } ; @ \ /* *\/`)
|
|
28
|
-
* are rejected and the var is cleared so the default font applies.
|
|
29
|
-
*
|
|
30
|
-
* Environment flags:
|
|
31
|
-
* - VITE_ENABLE_BRANDING_OVERRIDE (default: 'true') — set 'false' as kill switch.
|
|
32
|
-
*
|
|
33
|
-
* Usage:
|
|
34
|
-
* <ThemeProvider>
|
|
35
|
-
* <BrandingProvider supabaseClient={supabase}>
|
|
36
|
-
* {children}
|
|
37
|
-
* </BrandingProvider>
|
|
38
|
-
* </ThemeProvider>
|
|
39
|
-
*
|
|
40
|
-
* For Braden corporate (skips all RPC/Realtime):
|
|
41
|
-
* <BrandingProvider brand="braden" supabaseClient={supabase}>
|
|
42
|
-
* {children}
|
|
43
|
-
* </BrandingProvider>
|
|
44
|
-
*/
|
|
45
2
|
import { createContext, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
46
3
|
import { sanitizeBrandingUrl, sanitizeFontFamily, toCssUrl, } from './branding-sanitize.js';
|
|
47
4
|
export const BRANDING_STORAGE_KEY = 'bsuite_tenant_branding';
|
|
@@ -169,8 +126,17 @@ function warnIfProtectedKeyAttempted(branding) {
|
|
|
169
126
|
}
|
|
170
127
|
}
|
|
171
128
|
function isDevEnvironment() {
|
|
172
|
-
|
|
173
|
-
|
|
129
|
+
// SECURITY: direct static key access only — assigning `import.meta.env` to a
|
|
130
|
+
// variable makes consumer bundlers (Vite) inline the ENTIRE env object into
|
|
131
|
+
// every client bundle, leaking every VITE_* secret. try/catch keeps this
|
|
132
|
+
// safe in non-Vite runtimes where import.meta.env does not exist.
|
|
133
|
+
try {
|
|
134
|
+
return (import.meta.env.DEV === true ||
|
|
135
|
+
import.meta.env.MODE === 'development');
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
174
140
|
}
|
|
175
141
|
function persistBranding(branding) {
|
|
176
142
|
try {
|
|
@@ -197,10 +163,10 @@ function loadPersistedBranding() {
|
|
|
197
163
|
}
|
|
198
164
|
}
|
|
199
165
|
function isBrandingEnabled() {
|
|
166
|
+
// SECURITY: direct static key access only (see isDevEnvironment) — reading
|
|
167
|
+
// `import.meta.env` wholesale leaks every VITE_* secret into client bundles.
|
|
200
168
|
try {
|
|
201
|
-
|
|
202
|
-
const flag = import.meta.env;
|
|
203
|
-
const value = flag?.VITE_ENABLE_BRANDING_OVERRIDE ?? flag?.['VITE_ENABLE_BRANDING_OVERRIDE'];
|
|
169
|
+
const value = import.meta.env.VITE_ENABLE_BRANDING_OVERRIDE;
|
|
204
170
|
if (value === 'false' || value === '0')
|
|
205
171
|
return false;
|
|
206
172
|
}
|
package/package.json
CHANGED
package/src/css/braden.css
CHANGED
|
@@ -78,7 +78,14 @@
|
|
|
78
78
|
============================================================ */
|
|
79
79
|
--role-text-heading: var(--role-h1);
|
|
80
80
|
--role-text-body: oklch(0.22 0.020 250);
|
|
81
|
-
|
|
81
|
+
/* -base is what a LIGHT panel nested inside a dark section resets to. A
|
|
82
|
+
dark brand fill re-scopes the surface tokens for everything inside it,
|
|
83
|
+
which is right until the section contains a white card — then the card
|
|
84
|
+
inherits on-dark type and renders light-on-white. The reset cannot
|
|
85
|
+
hardcode a value because the correct one differs per theme, so it reads
|
|
86
|
+
this alias instead. */
|
|
87
|
+
--role-text-secondary-base: oklch(0.38 0.018 250);
|
|
88
|
+
--role-text-secondary: var(--role-text-secondary-base);
|
|
82
89
|
--role-text-muted: oklch(0.52 0.018 250);
|
|
83
90
|
--role-text-subtle: oklch(0.60 0.012 250);
|
|
84
91
|
--role-text-disabled: oklch(0.72 0.010 250);
|
|
@@ -176,7 +183,8 @@
|
|
|
176
183
|
A saturated fill colour is not a legible text colour.
|
|
177
184
|
============================================================ */
|
|
178
185
|
--role-primary-text: oklch(0.566 0.170 17.6);
|
|
179
|
-
--role-accent-text:
|
|
186
|
+
--role-accent-text-base: oklch(0.544 0.092 89.9);
|
|
187
|
+
--role-accent-text: var(--role-accent-text-base);
|
|
180
188
|
--role-error-text: oklch(0.562 0.200 270);
|
|
181
189
|
--role-warning-text: oklch(0.544 0.092 89.9);
|
|
182
190
|
--role-success-text: oklch(0.515 0.130 198);
|
|
@@ -260,7 +268,8 @@
|
|
|
260
268
|
--role-h5: oklch(0.80 0.040 228);
|
|
261
269
|
--role-h6: oklch(0.75 0.048 220);
|
|
262
270
|
--role-text-body: oklch(0.93 0.014 250);
|
|
263
|
-
--role-text-secondary: oklch(0.80 0.016 250);
|
|
271
|
+
--role-text-secondary-base: oklch(0.80 0.016 250);
|
|
272
|
+
--role-text-secondary: var(--role-text-secondary-base);
|
|
264
273
|
--role-text-muted: oklch(0.66 0.018 250);
|
|
265
274
|
--role-text-subtle: oklch(0.55 0.015 250);
|
|
266
275
|
--role-text-disabled: oklch(0.43 0.010 250);
|
|
@@ -278,7 +287,8 @@
|
|
|
278
287
|
--role-kbd-bg: oklch(0.250 0.020 250);
|
|
279
288
|
--role-kbd-border: oklch(0.400 0.022 250);
|
|
280
289
|
--role-primary-text: oklch(0.704 0.170 17.6);
|
|
281
|
-
--role-accent-text:
|
|
290
|
+
--role-accent-text-base: oklch(0.683 0.092 89.9);
|
|
291
|
+
--role-accent-text: var(--role-accent-text-base);
|
|
282
292
|
--role-error-text: oklch(0.711 0.200 270);
|
|
283
293
|
--role-warning-text: oklch(0.683 0.092 89.9);
|
|
284
294
|
--role-success-text: oklch(0.662 0.130 198);
|
package/src/css/utilities.css
CHANGED
|
@@ -101,3 +101,44 @@
|
|
|
101
101
|
transition-duration: 0.01ms !important;
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
+
|
|
105
|
+
/* ============================================================
|
|
106
|
+
GRADIENT TEXT — the display-heading treatment
|
|
107
|
+
============================================================
|
|
108
|
+
Operator ruling 2026-08-04: a page TITLE carries the accent gradient, on
|
|
109
|
+
authenticated surfaces too. That widens the `.bsuite-accent-gradient` note
|
|
110
|
+
above, which restricted the gradient to marketing/pre-auth heroes — the ban
|
|
111
|
+
it was really protecting is on FUNCTIONAL text (labels, body copy, values,
|
|
112
|
+
anything you read to make a decision), and a page title is none of those.
|
|
113
|
+
|
|
114
|
+
Uses --gradient-heading, NOT --gradient-accent. The raw accent gradient ends
|
|
115
|
+
in cyan at 1.76:1 on the light background, which would leave half the word
|
|
116
|
+
unreadable; --gradient-heading is built from the AA-verified *-text variants
|
|
117
|
+
and is correct in both modes. See the token for the measurements.
|
|
118
|
+
|
|
119
|
+
background-clip: text requires `color: transparent`, so a gradient that fails
|
|
120
|
+
to resolve produces INVISIBLE text rather than an ugly one. Both stops
|
|
121
|
+
therefore come from tokens the package itself defines. */
|
|
122
|
+
.text-gradient-accent {
|
|
123
|
+
background-image: var(--gradient-heading);
|
|
124
|
+
-webkit-background-clip: text;
|
|
125
|
+
background-clip: text;
|
|
126
|
+
color: transparent;
|
|
127
|
+
/* SHRINK-WRAP, and this is the whole reason the effect is visible.
|
|
128
|
+
background-clip: text paints the gradient across the ELEMENT BOX, not the
|
|
129
|
+
glyphs. A block-level h1 spans its container, so a short word samples only
|
|
130
|
+
the first ~15% of the gradient and the far stop never reaches the screen —
|
|
131
|
+
it renders as a flat colour, which is indistinguishable from the bug this
|
|
132
|
+
replaces. fit-content makes the box hug the text so the gradient spans the
|
|
133
|
+
letters. */
|
|
134
|
+
width: fit-content;
|
|
135
|
+
/* A gradient heading must still be selectable and searchable. background-clip
|
|
136
|
+
keeps the real glyphs, so text selection, find-in-page and screen readers
|
|
137
|
+
are unaffected — do not reach for an SVG or an image here. */
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* Forced-colours mode drops background-image entirely, which would leave
|
|
141
|
+
transparent glyphs on screen. Hand the text back to the system palette. */
|
|
142
|
+
@media (forced-colors: active) {
|
|
143
|
+
.text-gradient-accent { background-image: none; color: CanvasText; }
|
|
144
|
+
}
|
package/src/css/vars.css
CHANGED
|
@@ -200,15 +200,30 @@
|
|
|
200
200
|
|
|
201
201
|
/* AA-safe text renderings of the saturated roles on light surfaces.
|
|
202
202
|
A saturated fill colour is not a legible text colour; these are the
|
|
203
|
-
text variants and are the ONLY correct token for coloured type.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
203
|
+
text variants and are the ONLY correct token for coloured type.
|
|
204
|
+
|
|
205
|
+
VERIFIED AGAINST THE TINTED PILL, NOT JUST THE PAGE BACKGROUND. These
|
|
206
|
+
previously sat at ~4.70:1 measured against --role-bg-body, which is the
|
|
207
|
+
right floor for a bare coloured word on the page and the wrong one for
|
|
208
|
+
where they are actually used most: `bg-success/10 text-success-text`, a
|
|
209
|
+
status pill captioned on a 10% wash of its own hue. That wash pulls the
|
|
210
|
+
backdrop toward the text, and every one of these measured BELOW 4.5:1 in
|
|
211
|
+
that setting — error worst at 3.64:1 live. A token can be AA in the
|
|
212
|
+
context it was measured in and fail in the context it ships in.
|
|
213
|
+
|
|
214
|
+
Each is now dropped in lightness until it clears 4.5:1 on its own /10
|
|
215
|
+
tint with margin. Darkening only ever RAISES contrast against a light
|
|
216
|
+
backdrop, so the original page-background figures improve rather than
|
|
217
|
+
regress; both are quoted below. Dark mode is untouched — its variants sit
|
|
218
|
+
at L 0.67-0.79 and already measure clean on tint (7:1+ AAA). */
|
|
219
|
+
--role-primary-text: oklch(0.499 0.215 262.9); /* 4.96:1 tint · 5.66:1 body ✓ AA */
|
|
220
|
+
--role-accent-text: oklch(0.468 0.132 191.7); /* 4.96:1 tint · 5.32:1 body ✓ AA */
|
|
221
|
+
--role-error-text: oklch(0.482 0.230 25); /* 4.96:1 tint · 5.83:1 body ✓ AA */
|
|
222
|
+
--role-warning-text: oklch(0.509 0.150 75); /* 4.96:1 tint · 5.24:1 body ✓ AA */
|
|
223
|
+
--role-success-text: oklch(0.463 0.130 195); /* 4.97:1 tint · 5.46:1 body ✓ AA */
|
|
209
224
|
/* Pair for --role-info. Every other semantic role had a -text token; info
|
|
210
225
|
was the only gap, which forced apps to invent a local copy (R80.4). */
|
|
211
|
-
--role-info-text: oklch(0.
|
|
226
|
+
--role-info-text: oklch(0.468 0.120 192); /* 4.96:1 tint · 5.44:1 body ✓ AA */
|
|
212
227
|
|
|
213
228
|
/* Text roles — six-tier scale (W3 §3.1 contract). Every tier below is
|
|
214
229
|
measured against --role-bg-body (the conservative floor); all tiers
|
|
@@ -317,6 +332,20 @@
|
|
|
317
332
|
============================================================ */
|
|
318
333
|
--gradient-accent: linear-gradient(135deg, var(--role-primary), var(--role-accent));
|
|
319
334
|
|
|
335
|
+
/* HEADING gradient — the same two hues, but built from the *-text role
|
|
336
|
+
variants rather than the raw ones, and that difference is load-bearing.
|
|
337
|
+
Measured against the mode backgrounds:
|
|
338
|
+
|
|
339
|
+
--role-accent (cyan) on the LIGHT bg ......... 1.76:1 unreadable
|
|
340
|
+
--role-accent-text on the LIGHT bg ......... 4.70:1 AA
|
|
341
|
+
|
|
342
|
+
So painting a heading with --gradient-accent verbatim leaves the cyan half
|
|
343
|
+
of the word illegible in light mode. The *-text variants are AA-verified in
|
|
344
|
+
BOTH modes and already flip under .dark, so one token is correct in both:
|
|
345
|
+
light 4.71:1 -> 4.70:1 dark 7.01:1 -> 9.78:1
|
|
346
|
+
No new colour values, so the palette whitelist is unaffected. */
|
|
347
|
+
--gradient-heading: linear-gradient(135deg, var(--role-primary-text), var(--role-accent-text));
|
|
348
|
+
|
|
320
349
|
/* ============================================================
|
|
321
350
|
GLOW + SHADOW MULTIPLIERS
|
|
322
351
|
============================================================ */
|
package/src/preset-v4.css
CHANGED
|
@@ -169,6 +169,31 @@
|
|
|
169
169
|
--color-text-on-success: var(--text-on-success);
|
|
170
170
|
--color-text-on-warning: var(--text-on-warning);
|
|
171
171
|
|
|
172
|
+
/* The two BASE inverse tones, exposed because the semantic roles above do
|
|
173
|
+
not cover every coloured fill in the system. The stage ramp is the case
|
|
174
|
+
that forced this: --stage-1..6 is a FIXED light-to-dark scale that does
|
|
175
|
+
NOT flip with the theme, so a chip painted `bg-stage-1` (L 0.86) needs
|
|
176
|
+
dark type in BOTH modes. Captioning it `text-foreground` — which DOES
|
|
177
|
+
flip — measured 1.34:1 in dark mode on conduit's candidate list, the
|
|
178
|
+
worst reading anywhere in the estate.
|
|
179
|
+
|
|
180
|
+
Read the name as describing the TEXT, not the fill: --text-on-dark-fill
|
|
181
|
+
is the DARK tone, and it is what goes on a LIGHT fill (see
|
|
182
|
+
--text-on-accent, cyan at L 0.77, which uses it).
|
|
183
|
+
|
|
184
|
+
For the stage ramp specifically, the crossover is between 4 and 5:
|
|
185
|
+
|
|
186
|
+
stage-1 L 0.86 light 1.52:1 dark 11.49:1
|
|
187
|
+
stage-2 L 0.78 light 1.93:1 dark 9.01:1
|
|
188
|
+
stage-3 L 0.70 light 2.55:1 dark 6.84:1
|
|
189
|
+
stage-4 L 0.62 light 3.48:1 dark 5.01:1 <- still takes DARK
|
|
190
|
+
stage-5 L 0.54 light 4.85:1 dark 3.59:1 <- flips to LIGHT
|
|
191
|
+
stage-6 L 0.46 light 6.86:1 dark 2.54:1
|
|
192
|
+
|
|
193
|
+
stage-4 is the trap: it reads as "mid, so white type is fine" and is not. */
|
|
194
|
+
--color-text-on-light-fill: var(--text-on-light-fill);
|
|
195
|
+
--color-text-on-dark-fill: var(--text-on-dark-fill);
|
|
196
|
+
|
|
172
197
|
/* ============================================================
|
|
173
198
|
TYPE — the contract's stack, bridged to Tailwind's font-* utilities.
|
|
174
199
|
`font-sans` and `font-mono` are what components actually write, so
|