@rokkit/core 1.1.16 → 1.1.18
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/color-space.d.ts +11 -0
- package/dist/named-tokens.d.ts +3 -3
- package/dist/theme.d.ts +13 -6
- package/package.json +2 -2
- package/src/color-space.js +67 -0
- package/src/named-tokens.ts +3 -3
- package/src/theme.ts +57 -29
package/dist/color-space.d.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relative luminance (Y, 0–1) of a palette value in the given color space.
|
|
3
|
+
* Accepts hex (`#rrggbb`), bare RGB (`"r,g,b"` / `"r g b"`), and bare OKLCH
|
|
4
|
+
* (`"L C H"`). Returns null when the value can't be parsed — callers should
|
|
5
|
+
* treat null as "unknown" and fall back to a safe default.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} value
|
|
8
|
+
* @param {string} [space] — 'rgb' | 'oklch' | 'hsl'; disambiguates bare channels
|
|
9
|
+
* @returns {number | null}
|
|
10
|
+
*/
|
|
11
|
+
export function relativeLuminance(value: string, space?: string): number | null;
|
|
1
12
|
/**
|
|
2
13
|
* Abstract base class for color space adapters.
|
|
3
14
|
*/
|
package/dist/named-tokens.d.ts
CHANGED
|
@@ -9,8 +9,8 @@ export type ZSlot = 'z0' | 'z1' | 'z2' | 'z3' | 'z4' | 'z5' | 'z6' | 'z7' | 'z8'
|
|
|
9
9
|
export declare const Z_SLOTS: ZSlot[];
|
|
10
10
|
/**
|
|
11
11
|
* Maps each named token to the palette shade index that backs it.
|
|
12
|
-
* 'derived' indicates the token is computed
|
|
13
|
-
*
|
|
12
|
+
* 'derived' indicates the token is computed (e.g., on-primary is the auto
|
|
13
|
+
* on-color — near-black or near-white — picked by the primary fill's luminance).
|
|
14
14
|
*
|
|
15
15
|
* paper: 50 (lightest surface tone, canvas)
|
|
16
16
|
* paper-soft: 100 (card background)
|
|
@@ -21,7 +21,7 @@ export declare const Z_SLOTS: ZSlot[];
|
|
|
21
21
|
* ink-soft: 500 (placeholder)
|
|
22
22
|
* ink-faint: 300 (disabled)
|
|
23
23
|
* primary: 500 (CTA fill)
|
|
24
|
-
* on-primary: derived —
|
|
24
|
+
* on-primary: derived — auto on-color (near-black/white) for primary fill
|
|
25
25
|
* accent / status: 500 (solid)
|
|
26
26
|
* *-soft: 100 (tinted-bg companion)
|
|
27
27
|
* focus-ring: 500 of accent
|
package/dist/theme.d.ts
CHANGED
|
@@ -38,13 +38,20 @@ export declare function semanticShortcuts(name: any): (string[] | (RegExp | (([,
|
|
|
38
38
|
/**
|
|
39
39
|
* Generates "on-color" text shortcuts for readable text on colored backgrounds.
|
|
40
40
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
41
|
+
* The on-color is whichever of near-black / near-white best contrasts with the
|
|
42
|
+
* role's z5 fill (the principle: a 500-shade fill takes black OR white text —
|
|
43
|
+
* never a mid tint, which fails AA on bright fills like vermillion). The caller
|
|
44
|
+
* passes the resolved on-color hex (see `Theme.#onColorHex`); we emit it as an
|
|
45
|
+
* arbitrary-value utility so it stays a single CSS color, opacity-composable.
|
|
43
46
|
*
|
|
44
|
-
*
|
|
47
|
+
* - `text-on-{name}` → readable text on a z5+ fill
|
|
48
|
+
* - `text-on-{name}-muted` → same on-color (kept for back-compat callers)
|
|
49
|
+
*
|
|
50
|
+
* @param {string} name - Color name (e.g., 'primary', 'accent', 'danger')
|
|
51
|
+
* @param {string} [onColor='#fafafa'] - resolved on-color hex for this role
|
|
45
52
|
* @returns {Array} Array of shortcut definitions
|
|
46
53
|
*/
|
|
47
|
-
export declare function contrastShortcuts(name: any): (RegExp | (([, end]: [any, any]) => string))[][];
|
|
54
|
+
export declare function contrastShortcuts(name: any, onColor?: string): (RegExp | (([, end]: [any, any]) => string))[][];
|
|
48
55
|
/**
|
|
49
56
|
* Theme class for managing color palettes, mappings, and semantic shortcuts.
|
|
50
57
|
*/
|
|
@@ -120,8 +127,8 @@ export declare class Theme {
|
|
|
120
127
|
* Used in extended mode where the preset emits the full --color-{role}-{shade}
|
|
121
128
|
* palette; named tokens become thin syntactic aliases over the palette.
|
|
122
129
|
*
|
|
123
|
-
* `on-primary`
|
|
124
|
-
* matching the core-mode
|
|
130
|
+
* `on-primary` resolves to the auto on-color for the primary fill (near-black
|
|
131
|
+
* or near-white), matching the core-mode derivation.
|
|
125
132
|
*/
|
|
126
133
|
getZAliasesForExtended(): Record<string, string>;
|
|
127
134
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rokkit/core",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.18",
|
|
4
4
|
"description": "Contains core utility functions and classes that can be used in various components.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -46,6 +46,6 @@
|
|
|
46
46
|
"ramda": "^0.32.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@rokkit/icons": "1.1.
|
|
49
|
+
"@rokkit/icons": "1.1.18"
|
|
50
50
|
}
|
|
51
51
|
}
|
package/src/color-space.js
CHANGED
|
@@ -42,6 +42,73 @@ function matMul(m, v) {
|
|
|
42
42
|
return m.map((row) => row[0] * v[0] + row[1] * v[1] + row[2] * v[2])
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// Inverse OKLab matrices (for OKLCH → XYZ, to recover relative luminance Y).
|
|
46
|
+
const OKLAB_TO_LMS3 = [
|
|
47
|
+
[1, 0.3963377774, 0.2158037573],
|
|
48
|
+
[1, -0.1055613458, -0.0638541728],
|
|
49
|
+
[1, -0.0894841775, -1.291485548]
|
|
50
|
+
]
|
|
51
|
+
const LMS_TO_XYZ = [
|
|
52
|
+
[1.2270138511, -0.5577999807, 0.281256149],
|
|
53
|
+
[-0.0405801784, 1.1122568696, -0.0716766787],
|
|
54
|
+
[-0.0763812845, -0.4214819784, 1.5861632204]
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Relative luminance (Y, 0–1 per WCAG) of an sRGB triplet.
|
|
59
|
+
* @param {number} r @param {number} g @param {number} b — 0–255
|
|
60
|
+
* @returns {number}
|
|
61
|
+
*/
|
|
62
|
+
function rgbLuminance(r, g, b) {
|
|
63
|
+
const lin = [srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)]
|
|
64
|
+
return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Relative luminance (Y) of an OKLCH color. Y is the XYZ Y component, so we
|
|
69
|
+
* only need OKLab → LMS → XYZ — no full sRGB round-trip.
|
|
70
|
+
* @param {number} L @param {number} C @param {number} H — H in degrees
|
|
71
|
+
* @returns {number}
|
|
72
|
+
*/
|
|
73
|
+
function oklchLuminance(L, C, H) {
|
|
74
|
+
const hr = (H * Math.PI) / 180
|
|
75
|
+
const lab = [L, C * Math.cos(hr), C * Math.sin(hr)]
|
|
76
|
+
const lms = matMul(OKLAB_TO_LMS3, lab).map((v) => v ** 3)
|
|
77
|
+
return matMul(LMS_TO_XYZ, lms)[1]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Relative luminance (Y, 0–1) of a palette value in the given color space.
|
|
82
|
+
* Accepts hex (`#rrggbb`), bare RGB (`"r,g,b"` / `"r g b"`), and bare OKLCH
|
|
83
|
+
* (`"L C H"`). Returns null when the value can't be parsed — callers should
|
|
84
|
+
* treat null as "unknown" and fall back to a safe default.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} value
|
|
87
|
+
* @param {string} [space] — 'rgb' | 'oklch' | 'hsl'; disambiguates bare channels
|
|
88
|
+
* @returns {number | null}
|
|
89
|
+
*/
|
|
90
|
+
export function relativeLuminance(value, space = 'rgb') {
|
|
91
|
+
if (typeof value !== 'string') return null
|
|
92
|
+
const v = value.trim()
|
|
93
|
+
if (HEX_RE.test(v)) {
|
|
94
|
+
const [r, g, b] = parseHex(v)
|
|
95
|
+
return rgbLuminance(r, g, b)
|
|
96
|
+
}
|
|
97
|
+
if (space === 'oklch' && /^[\d.]+\s+[\d.]+\s+[\d.]+$/.test(v)) {
|
|
98
|
+
const [L, C, H] = v.split(/\s+/).map(Number)
|
|
99
|
+
return oklchLuminance(L, C, H)
|
|
100
|
+
}
|
|
101
|
+
if (/^\d+\s*,\s*\d+\s*,\s*\d+$/.test(v)) {
|
|
102
|
+
const [r, g, b] = v.split(',').map((s) => Number(s.trim()))
|
|
103
|
+
return rgbLuminance(r, g, b)
|
|
104
|
+
}
|
|
105
|
+
if (/^\d+\s+\d+\s+\d+$/.test(v)) {
|
|
106
|
+
const [r, g, b] = v.trim().split(/\s+/).map(Number)
|
|
107
|
+
return rgbLuminance(r, g, b)
|
|
108
|
+
}
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
|
|
45
112
|
// ── Hex parsing helper ──────────────────────────────────────────────────
|
|
46
113
|
|
|
47
114
|
/**
|
package/src/named-tokens.ts
CHANGED
|
@@ -27,8 +27,8 @@ export const Z_SLOTS: ZSlot[] = ['z0', 'z1', 'z2', 'z3', 'z4', 'z5', 'z6', 'z7',
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Maps each named token to the palette shade index that backs it.
|
|
30
|
-
* 'derived' indicates the token is computed
|
|
31
|
-
*
|
|
30
|
+
* 'derived' indicates the token is computed (e.g., on-primary is the auto
|
|
31
|
+
* on-color — near-black or near-white — picked by the primary fill's luminance).
|
|
32
32
|
*
|
|
33
33
|
* paper: 50 (lightest surface tone, canvas)
|
|
34
34
|
* paper-soft: 100 (card background)
|
|
@@ -39,7 +39,7 @@ export const Z_SLOTS: ZSlot[] = ['z0', 'z1', 'z2', 'z3', 'z4', 'z5', 'z6', 'z7',
|
|
|
39
39
|
* ink-soft: 500 (placeholder)
|
|
40
40
|
* ink-faint: 300 (disabled)
|
|
41
41
|
* primary: 500 (CTA fill)
|
|
42
|
-
* on-primary: derived —
|
|
42
|
+
* on-primary: derived — auto on-color (near-black/white) for primary fill
|
|
43
43
|
* accent / status: 500 (solid)
|
|
44
44
|
* *-soft: 100 (tinted-bg companion)
|
|
45
45
|
* focus-ring: 500 of accent
|
package/src/theme.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// @ts-nocheck
|
|
3
3
|
import { DEFAULT_THEME_MAPPING, defaultColors, TONE_MAP, INVERTED_ROLES } from './constants'
|
|
4
4
|
import { shades } from './colors/index'
|
|
5
|
-
import { ColorSpace } from './color-space'
|
|
5
|
+
import { ColorSpace, relativeLuminance } from './color-space'
|
|
6
6
|
import {
|
|
7
7
|
NAMED_TOKENS,
|
|
8
8
|
NAMED_TOKEN_SHADE_MAP,
|
|
@@ -133,25 +133,42 @@ export function semanticShortcuts(name) {
|
|
|
133
133
|
/**
|
|
134
134
|
* Generates "on-color" text shortcuts for readable text on colored backgrounds.
|
|
135
135
|
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
136
|
+
* The on-color is whichever of near-black / near-white best contrasts with the
|
|
137
|
+
* role's z5 fill (the principle: a 500-shade fill takes black OR white text —
|
|
138
|
+
* never a mid tint, which fails AA on bright fills like vermillion). The caller
|
|
139
|
+
* passes the resolved on-color hex (see `Theme.#onColorHex`); we emit it as an
|
|
140
|
+
* arbitrary-value utility so it stays a single CSS color, opacity-composable.
|
|
138
141
|
*
|
|
139
|
-
*
|
|
142
|
+
* - `text-on-{name}` → readable text on a z5+ fill
|
|
143
|
+
* - `text-on-{name}-muted` → same on-color (kept for back-compat callers)
|
|
144
|
+
*
|
|
145
|
+
* @param {string} name - Color name (e.g., 'primary', 'accent', 'danger')
|
|
146
|
+
* @param {string} [onColor='#fafafa'] - resolved on-color hex for this role
|
|
140
147
|
* @returns {Array} Array of shortcut definitions
|
|
141
148
|
*/
|
|
142
|
-
export function contrastShortcuts(name) {
|
|
149
|
+
export function contrastShortcuts(name, onColor = '#fafafa') {
|
|
143
150
|
return [
|
|
144
|
-
[
|
|
145
|
-
|
|
146
|
-
([, end]) => `text-${name}-50${end || ''} dark:text-${name}-50${end || ''}`
|
|
147
|
-
],
|
|
148
|
-
[
|
|
149
|
-
new RegExp(`^text-on-${name}-muted(\\/\\d+)?$`),
|
|
150
|
-
([, end]) => `text-${name}-100${end || ''} dark:text-${name}-200${end || ''}`
|
|
151
|
-
]
|
|
151
|
+
[new RegExp(`^text-on-${name}(\\/\\d+)?$`), ([, end]) => `text-[${onColor}]${end || ''}`],
|
|
152
|
+
[new RegExp(`^text-on-${name}-muted(\\/\\d+)?$`), ([, end]) => `text-[${onColor}]${end || ''}`]
|
|
152
153
|
]
|
|
153
154
|
}
|
|
154
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Near-black / near-white on-color endpoints. Not pure #000/#fff — a hair of
|
|
158
|
+
* lift reads softer on a saturated fill while still clearing AA (near-black is
|
|
159
|
+
* dark enough that even a mid-bright vermillion 500 reaches ~5:1).
|
|
160
|
+
*/
|
|
161
|
+
const ON_COLOR_DARK = '#161616'
|
|
162
|
+
const ON_COLOR_LIGHT = '#fafafa'
|
|
163
|
+
/**
|
|
164
|
+
* Fill luminance where ON_COLOR_DARK and ON_COLOR_LIGHT give equal WCAG contrast:
|
|
165
|
+
* solve (Yf+0.05)² = (Y_dark+0.05)(Y_light+0.05) with Y_dark≈0.0074, Y_light≈0.956
|
|
166
|
+
* → Yf ≈ 0.190. Fills at/above this take dark text; below take light text. (Truly
|
|
167
|
+
* mid-luminance fills near this point clear neither pair at AA — that's the
|
|
168
|
+
* palette author's choice to make, per "pick a 500 where black or white works".)
|
|
169
|
+
*/
|
|
170
|
+
const ON_COLOR_Y_CROSSOVER = 0.19
|
|
171
|
+
|
|
155
172
|
/**
|
|
156
173
|
* Fallback chain for nullable color mappings.
|
|
157
174
|
* If a semantic color is null, it inherits from another semantic color.
|
|
@@ -306,23 +323,32 @@ export class Theme {
|
|
|
306
323
|
}
|
|
307
324
|
|
|
308
325
|
/**
|
|
309
|
-
* Resolves a 'derived' named token. Today only `on-primary` is derived — it
|
|
310
|
-
*
|
|
326
|
+
* Resolves a 'derived' named token. Today only `on-primary` is derived — it's
|
|
327
|
+
* the auto on-color (near-black or near-white) for the primary fill, so the
|
|
328
|
+
* default pair always clears AA whether primary-500 is bright (→ black) or
|
|
329
|
+
* dark (→ white). Skins still override `on-primary` for a bespoke pair.
|
|
311
330
|
*/
|
|
312
|
-
#resolveDerivedToken(name: string, colors: Record<string, Record<string, string>>,
|
|
331
|
+
#resolveDerivedToken(name: string, colors: Record<string, Record<string, string>>, _perRoleModes?: Record<string, 'core' | 'extended'>): string | undefined {
|
|
313
332
|
if (name === 'on-primary') {
|
|
314
|
-
|
|
315
|
-
const surfacePalette = colors[surfacePaletteName]
|
|
316
|
-
if (!surfacePalette?.['50']) return undefined
|
|
317
|
-
const surfaceMode = perRoleModes?.['surface'] ?? 'core'
|
|
318
|
-
if (surfaceMode === 'extended') {
|
|
319
|
-
return `var(--color-surface-50)`
|
|
320
|
-
}
|
|
321
|
-
return this.#adapter.wrap(surfacePalette['50'])
|
|
333
|
+
return this.#adapter.wrap(this.#onColorHex('primary', colors))
|
|
322
334
|
}
|
|
323
335
|
return undefined
|
|
324
336
|
}
|
|
325
337
|
|
|
338
|
+
/**
|
|
339
|
+
* Picks the readable on-color (near-black / near-white hex) for a role's z5
|
|
340
|
+
* fill by its relative luminance. Bright fills (vermillion, teal, amber) take
|
|
341
|
+
* dark text; dark fills (violet, indigo) take light text. Falls back to light
|
|
342
|
+
* when the fill can't be measured (matches the historical default).
|
|
343
|
+
*/
|
|
344
|
+
#onColorHex(role: string, colors: Record<string, Record<string, string>>): string {
|
|
345
|
+
const palette = colors[this.#mapping[role]]
|
|
346
|
+
const fill = palette?.['500']
|
|
347
|
+
const y = fill !== undefined ? relativeLuminance(fill, this.#adapter.name) : null
|
|
348
|
+
if (y === null) return ON_COLOR_LIGHT
|
|
349
|
+
return y >= ON_COLOR_Y_CROSSOVER ? ON_COLOR_DARK : ON_COLOR_LIGHT
|
|
350
|
+
}
|
|
351
|
+
|
|
326
352
|
/**
|
|
327
353
|
* Generate z-aliases for surface or ink role using the provided map.
|
|
328
354
|
*/
|
|
@@ -387,17 +413,18 @@ export class Theme {
|
|
|
387
413
|
* Used in extended mode where the preset emits the full --color-{role}-{shade}
|
|
388
414
|
* palette; named tokens become thin syntactic aliases over the palette.
|
|
389
415
|
*
|
|
390
|
-
* `on-primary`
|
|
391
|
-
* matching the core-mode
|
|
416
|
+
* `on-primary` resolves to the auto on-color for the primary fill (near-black
|
|
417
|
+
* or near-white), matching the core-mode derivation.
|
|
392
418
|
*/
|
|
393
419
|
getZAliasesForExtended(): Record<string, string> {
|
|
420
|
+
const colors = { ...defaultColors, ...this.#colors }
|
|
394
421
|
const result: Record<string, string> = {}
|
|
395
422
|
for (const name of NAMED_TOKENS) {
|
|
396
423
|
const role = NAMED_TOKEN_ROLE_MAP[name]
|
|
397
424
|
const shadeOrDerived = NAMED_TOKEN_SHADE_MAP[name]
|
|
398
425
|
if (shadeOrDerived === 'derived') {
|
|
399
|
-
// on-primary →
|
|
400
|
-
result[`--${name}`] =
|
|
426
|
+
// on-primary → auto on-color (black/white) for the primary fill
|
|
427
|
+
result[`--${name}`] = this.#adapter.wrap(this.#onColorHex('primary', colors))
|
|
401
428
|
continue
|
|
402
429
|
}
|
|
403
430
|
result[`--${name}`] = `var(--color-${role}-${shadeOrDerived})`
|
|
@@ -438,7 +465,8 @@ export class Theme {
|
|
|
438
465
|
}
|
|
439
466
|
|
|
440
467
|
getShortcuts(name) {
|
|
441
|
-
|
|
468
|
+
const colors = { ...defaultColors, ...this.#colors }
|
|
469
|
+
return [...semanticShortcuts(name), ...contrastShortcuts(name, this.#onColorHex(name, colors))]
|
|
442
470
|
}
|
|
443
471
|
|
|
444
472
|
/**
|