@rokkit/core 1.0.4 → 1.1.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.
- package/dist/color-space.d.ts +52 -0
- package/dist/constants.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/named-tokens.d.ts +55 -0
- package/dist/theme.d.ts +63 -7
- package/dist/utils.d.ts +7 -32
- package/package.json +2 -2
- package/src/color-space.js +308 -0
- package/src/constants.js +22 -1
- package/src/index.js +2 -0
- package/src/named-tokens.ts +161 -0
- package/src/theme.ts +228 -47
- package/src/utils.js +10 -81
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract base class for color space adapters.
|
|
3
|
+
*/
|
|
4
|
+
export class ColorSpace {
|
|
5
|
+
/**
|
|
6
|
+
* Factory method to create a ColorSpace adapter by name.
|
|
7
|
+
* @param {string} space - 'rgb', 'hsl', or 'oklch'
|
|
8
|
+
* @returns {ColorSpace}
|
|
9
|
+
*/
|
|
10
|
+
static create(space: string): ColorSpace;
|
|
11
|
+
/** @returns {string} */
|
|
12
|
+
get name(): string;
|
|
13
|
+
/** @returns {string} CSS color-mix interpolation space */
|
|
14
|
+
get mixSpace(): string;
|
|
15
|
+
/** @returns {string} CSS function name */
|
|
16
|
+
get fn(): string;
|
|
17
|
+
/**
|
|
18
|
+
* Convert a hex color to bare channels in this space.
|
|
19
|
+
* @param {string} hex - #rrggbb
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
fromHex(_hex: any): string;
|
|
23
|
+
/**
|
|
24
|
+
* Wrap any input into a complete CSS color value.
|
|
25
|
+
* - hex → convert and wrap
|
|
26
|
+
* - bare channels → wrap in fn()
|
|
27
|
+
* - already wrapped → pass through
|
|
28
|
+
* - non-string / null / undefined → pass through
|
|
29
|
+
* - named colors → pass through
|
|
30
|
+
* @param {*} value
|
|
31
|
+
* @returns {*}
|
|
32
|
+
*/
|
|
33
|
+
wrap(_value: any): any;
|
|
34
|
+
/**
|
|
35
|
+
* Produce a color-mix expression for theme CSS variables with alpha support.
|
|
36
|
+
* @param {string} varName - CSS variable name (e.g. '--color-primary-500')
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
themeColor(varName: string): string;
|
|
40
|
+
}
|
|
41
|
+
export class RgbColorSpace extends ColorSpace {
|
|
42
|
+
fromHex(hex: any): string;
|
|
43
|
+
wrap(value: any): any;
|
|
44
|
+
}
|
|
45
|
+
export class HslColorSpace extends ColorSpace {
|
|
46
|
+
fromHex(hex: any): string;
|
|
47
|
+
wrap(value: any): any;
|
|
48
|
+
}
|
|
49
|
+
export class OklchColorSpace extends ColorSpace {
|
|
50
|
+
fromHex(hex: any): string;
|
|
51
|
+
wrap(value: any): any;
|
|
52
|
+
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -54,6 +54,7 @@ export namespace DEFAULT_KEYMAP {
|
|
|
54
54
|
}
|
|
55
55
|
export namespace DEFAULT_THEME_MAPPING {
|
|
56
56
|
let surface: string;
|
|
57
|
+
let ink: string;
|
|
57
58
|
let primary: string;
|
|
58
59
|
let secondary: string;
|
|
59
60
|
let tertiary: string;
|
|
@@ -77,5 +78,6 @@ export namespace TONE_MAP {
|
|
|
77
78
|
let z9: number;
|
|
78
79
|
let z10: number;
|
|
79
80
|
}
|
|
81
|
+
export const INVERTED_ROLES: Set<string>;
|
|
80
82
|
export const DEFAULT_STATE_ICONS: any;
|
|
81
83
|
export { defaultColors, syntaxColors, shades, defaultPalette } from "./colors/index";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export * from "./types.js";
|
|
2
2
|
export * from "./constants.js";
|
|
3
|
+
export * from "./named-tokens.ts";
|
|
3
4
|
export * from "./utils.js";
|
|
4
5
|
export * from "./nested.js";
|
|
5
6
|
export * from "./string.js";
|
|
6
7
|
export * from "./theme.js";
|
|
8
|
+
export { ColorSpace } from "./color-space.js";
|
|
7
9
|
export { FieldMapper } from "./field-mapper.js";
|
|
8
10
|
export { createEmitter } from "./events.js";
|
|
9
11
|
export { getLineTypes } from "./connector.js";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 24 canonical named tokens that constitute the "core" emit vocabulary.
|
|
3
|
+
* Order is significant for tests and for predictable CSS-var output ordering.
|
|
4
|
+
*/
|
|
5
|
+
export declare const NAMED_TOKENS: readonly ["paper", "paper-soft", "paper-mute", "paper-edge", "ink", "ink-mute", "ink-soft", "ink-faint", "primary", "on-primary", "accent", "accent-soft", "success", "success-soft", "warning", "warning-soft", "danger", "danger-soft", "error", "error-soft", "info", "info-soft", "focus-ring", "shadow-tint"];
|
|
6
|
+
export type NamedToken = (typeof NAMED_TOKENS)[number];
|
|
7
|
+
export type SkinRole = 'surface' | 'ink' | 'primary' | 'accent' | 'success' | 'warning' | 'danger' | 'error' | 'info';
|
|
8
|
+
export type ZSlot = 'z0' | 'z1' | 'z2' | 'z3' | 'z4' | 'z5' | 'z6' | 'z7' | 'z8' | 'z9' | 'z10';
|
|
9
|
+
export declare const Z_SLOTS: ZSlot[];
|
|
10
|
+
/**
|
|
11
|
+
* Maps each named token to the palette shade index that backs it.
|
|
12
|
+
* 'derived' indicates the token is computed from another palette (e.g., on-primary
|
|
13
|
+
* picks the paper shade of the surface palette by default for white-on-primary).
|
|
14
|
+
*
|
|
15
|
+
* paper: 50 (lightest surface tone, canvas)
|
|
16
|
+
* paper-soft: 100 (card background)
|
|
17
|
+
* paper-mute: 200 (subdued panel)
|
|
18
|
+
* paper-edge: 400 (hairline border tone)
|
|
19
|
+
* ink: 900 (primary text)
|
|
20
|
+
* ink-mute: 700 (secondary text)
|
|
21
|
+
* ink-soft: 500 (placeholder)
|
|
22
|
+
* ink-faint: 300 (disabled)
|
|
23
|
+
* primary: 500 (CTA fill)
|
|
24
|
+
* on-primary: derived — surface.50 default (white-on-primary)
|
|
25
|
+
* accent / status: 500 (solid)
|
|
26
|
+
* *-soft: 100 (tinted-bg companion)
|
|
27
|
+
* focus-ring: 500 of accent
|
|
28
|
+
* shadow-tint: 900 of ink
|
|
29
|
+
*/
|
|
30
|
+
export declare const NAMED_TOKEN_SHADE_MAP: Record<NamedToken, number | 'derived'>;
|
|
31
|
+
/**
|
|
32
|
+
* Maps each named token to the skin role whose palette it draws from.
|
|
33
|
+
*/
|
|
34
|
+
export declare const NAMED_TOKEN_ROLE_MAP: Record<NamedToken, SkinRole>;
|
|
35
|
+
/**
|
|
36
|
+
* Surface z-slot → named slot. Used to emit back-compat aliases like
|
|
37
|
+
* `--color-surface-z1: var(--paper-soft);` in core mode.
|
|
38
|
+
*
|
|
39
|
+
* z2/z3 collapse to paper-mute, z5/z6 to ink-soft, z7/z8 to ink-mute,
|
|
40
|
+
* z9/z10 to ink. This collapse is the contract: 4 surface tones in core mode.
|
|
41
|
+
*/
|
|
42
|
+
export declare const Z_COLLAPSE_MAP_SURFACE: Record<ZSlot, NamedToken>;
|
|
43
|
+
/**
|
|
44
|
+
* Ink role uses the inverted z-scale (z0 = darkest = primary text).
|
|
45
|
+
* In core mode, the same 4+4 named ladder is reused; this table just inverts.
|
|
46
|
+
*/
|
|
47
|
+
export declare const Z_COLLAPSE_MAP_INK: Record<ZSlot, NamedToken>;
|
|
48
|
+
/**
|
|
49
|
+
* Returns true when the role has a `-soft` companion in the named vocabulary.
|
|
50
|
+
* Used to decide whether the role's tinted-background slot collapses to
|
|
51
|
+
* `var(--{role}-soft)` or to `var(--{role})`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function hasSoftCompanion(role: string): boolean;
|
|
54
|
+
export declare function shadeForNamedToken(name: string): number | 'derived' | undefined;
|
|
55
|
+
export declare function roleForNamedToken(name: string): SkinRole | undefined;
|
package/dist/theme.d.ts
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Generate shades for a color using css
|
|
2
|
+
* Generate shades for a color using css variable and a ColorSpace adapter.
|
|
3
3
|
*
|
|
4
4
|
* @param {string} name
|
|
5
|
-
* @param {string}
|
|
6
|
-
* @returns
|
|
5
|
+
* @param {string|import('./color-space').ColorSpace} space - color space name or adapter instance
|
|
6
|
+
* @returns {Record<string|number, string>}
|
|
7
7
|
*/
|
|
8
|
-
export declare function shadesOf(name: any,
|
|
9
|
-
DEFAULT:
|
|
8
|
+
export declare function shadesOf(name: any, space?: string): {
|
|
9
|
+
DEFAULT: string;
|
|
10
10
|
};
|
|
11
11
|
/**
|
|
12
12
|
* Constructs and returns the light and dark theme variants based on provided color mapping and color definitions.
|
|
13
13
|
*
|
|
14
14
|
* @param {Object} [mapping=DEFAULT_THEME_MAPPING] - An object mapping variant names to color property names.
|
|
15
15
|
* @param {Object} [colors=defaultColors] - The object containing default color definitions.
|
|
16
|
-
* @param {
|
|
16
|
+
* @param {string} [colorSpace] - Color space name for CSS variable values.
|
|
17
17
|
* @returns {Array<Array>} An array containing two arrays, one for the light theme variant and another for the dark theme.
|
|
18
18
|
*/
|
|
19
19
|
export declare function themeRules(mapping: {
|
|
20
20
|
surface: string;
|
|
21
|
+
ink: string;
|
|
21
22
|
primary: string;
|
|
22
23
|
secondary: string;
|
|
23
24
|
tertiary: string;
|
|
@@ -51,12 +52,13 @@ export declare class Theme {
|
|
|
51
52
|
#private;
|
|
52
53
|
/**
|
|
53
54
|
*
|
|
54
|
-
* @param {import('./types.js').ColorTheme & { colorSpace?:
|
|
55
|
+
* @param {import('./types.js').ColorTheme & { colorSpace?: string }} param0
|
|
55
56
|
*/
|
|
56
57
|
constructor({ colors, mapping, colorSpace }?: {
|
|
57
58
|
colors?: any;
|
|
58
59
|
mapping?: {
|
|
59
60
|
surface: string;
|
|
61
|
+
ink: string;
|
|
60
62
|
primary: string;
|
|
61
63
|
secondary: string;
|
|
62
64
|
tertiary: string;
|
|
@@ -78,6 +80,60 @@ export declare class Theme {
|
|
|
78
80
|
mapVariant(color: any, variant: any): {};
|
|
79
81
|
getColorRules(mapping?: null): {};
|
|
80
82
|
getPalette(mapping?: null): {};
|
|
83
|
+
/**
|
|
84
|
+
* Returns the named-token layer as a map of `--{name}: <resolved value>` entries.
|
|
85
|
+
*
|
|
86
|
+
* Palette values are inlined — there is no `var(--color-{role}-{shade})` indirection.
|
|
87
|
+
*
|
|
88
|
+
* `on-primary` is derived: it picks the paper shade (50) of the surface palette so the
|
|
89
|
+
* default contrast pair is "primary fill + white-ish text." Skins that need a different
|
|
90
|
+
* pair should override via custom tokens.
|
|
91
|
+
*
|
|
92
|
+
* `*-soft` variants resolve to shade 100 of their role's palette.
|
|
93
|
+
* `shadow-tint` resolves to shade 900 of the ink palette.
|
|
94
|
+
*
|
|
95
|
+
* @param {'light' | 'dark'} _mode — accepted for symmetry with future
|
|
96
|
+
* dark-aware derivations. Today the Theme is constructed with a mode-resolved
|
|
97
|
+
* mapping, so this parameter is informational.
|
|
98
|
+
* @param perRoleModes — optional map of role → 'core' | 'extended'. When a token's
|
|
99
|
+
* role is 'extended', the value is a `var(--color-{role}-{shade})` alias instead
|
|
100
|
+
* of the inlined palette value. Defaults to 'core' for all unspecified roles
|
|
101
|
+
* (and for the whole map when undefined).
|
|
102
|
+
*/
|
|
103
|
+
getNamedTokens(_mode?: string, perRoleModes?: Record<string, 'core' | 'extended'>): Record<string, string>;
|
|
104
|
+
/**
|
|
105
|
+
* Returns z-alias CSS-var assignments for back-compat in core mode.
|
|
106
|
+
* `--color-{role}-z{n}` → `var(--{namedSlot})`.
|
|
107
|
+
*
|
|
108
|
+
* - surface: uses Z_COLLAPSE_MAP_SURFACE (z2/z3 → paper-mute, z9/z10 → ink, etc.)
|
|
109
|
+
* - ink: uses Z_COLLAPSE_MAP_INK (inverted scale)
|
|
110
|
+
* - primary/accent/status: tint-vs-solid 2-state collapse.
|
|
111
|
+
* z0–z2 → role-soft (if it exists) else role; z3+ → role.
|
|
112
|
+
*
|
|
113
|
+
* This is the back-compat layer: existing `@apply bg-surface-z3` etc. resolves
|
|
114
|
+
* through these aliases until consumers migrate to the named vocabulary.
|
|
115
|
+
*/
|
|
116
|
+
/** Accepts SkinRole and any custom skin roles beyond the SkinRole literal union. */
|
|
117
|
+
getZAliasesForCore(role: string): Record<string, string>;
|
|
118
|
+
/**
|
|
119
|
+
* Returns named tokens as CSS-var aliases pointing at the palette layer.
|
|
120
|
+
* Used in extended mode where the preset emits the full --color-{role}-{shade}
|
|
121
|
+
* palette; named tokens become thin syntactic aliases over the palette.
|
|
122
|
+
*
|
|
123
|
+
* `on-primary` aliases to `var(--color-surface-50)` (derived: paper of surface,
|
|
124
|
+
* matching the core-mode resolution).
|
|
125
|
+
*/
|
|
126
|
+
getZAliasesForExtended(): Record<string, string>;
|
|
127
|
+
/**
|
|
128
|
+
* Emit the full --color-{role}-{shade} palette CSS vars for a single role.
|
|
129
|
+
* Mirrors what getPalette does for all roles, but scoped to one.
|
|
130
|
+
*/
|
|
131
|
+
getPaletteForRole(role: string): Record<string, string>;
|
|
132
|
+
/**
|
|
133
|
+
* Emit z-aliases for one role in extended mode — they point at the palette vars,
|
|
134
|
+
* using TONE_MAP for surface/non-inverted roles and the inverted map for ink.
|
|
135
|
+
*/
|
|
136
|
+
getZAliasesForRoleExtended(role: string): Record<string, string>;
|
|
81
137
|
getShortcuts(name: any): (string[] | (RegExp | (([, variant, end]: [any, any, any]) => string))[] | (RegExp | (([, end]: [any, any]) => string))[])[];
|
|
82
138
|
/**
|
|
83
139
|
* Generates CSS custom property declarations for the semantic z0–z10 tone scale.
|
package/dist/utils.d.ts
CHANGED
|
@@ -101,40 +101,16 @@ export function hex2oklch(hex: string): string;
|
|
|
101
101
|
*/
|
|
102
102
|
export function oklch2hex(L: number, C: number, H: number): string;
|
|
103
103
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* @returns {string} space-separated HSL components for CSS `hsl(H S% L% / alpha)`
|
|
107
|
-
*/
|
|
108
|
-
export function hex2hsl(hex: string): string;
|
|
109
|
-
/** @typedef {'rgb' | 'hsl' | 'oklch'} ColorSpace */
|
|
110
|
-
/**
|
|
111
|
-
* Convert a hex color to the component format for a given color space.
|
|
112
|
-
* - rgb: "r,g,b" → for `rgba(var(--x), alpha)`
|
|
113
|
-
* - hsl: "H S% L%" → for `hsl(var(--x) / alpha)`
|
|
114
|
-
* - oklch: "L C H" → for `oklch(var(--x) / alpha)`
|
|
104
|
+
* Detects whether an icon value is a CSS class or a literal character
|
|
105
|
+
* (kanji, emoji, single letter, etc.).
|
|
115
106
|
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
|
|
120
|
-
export function hexToComponents(hex: string, space: ColorSpace): string;
|
|
121
|
-
/**
|
|
122
|
-
* Convert a CSS color value to components for use in CSS variables.
|
|
123
|
-
*
|
|
124
|
-
* - Hex (#rrggbb): converted to space-appropriate components via hexToComponents.
|
|
125
|
-
* - oklch(...) strings: inner components extracted (e.g. "0.58 0.15 35") so the
|
|
126
|
-
* value is compatible with the oklch(var(--x) / alpha) wrapper pattern.
|
|
127
|
-
* - All other formats: returned as-is (no opacity-modifier support).
|
|
107
|
+
* Anything more than one grapheme is treated as a class — including bare
|
|
108
|
+
* semantic names like `file-svelte` which UnoCSS expands to the configured
|
|
109
|
+
* icon collection via `iconShortcuts(DEFAULT_ICONS, …)`. Single characters
|
|
110
|
+
* (kanji, emoji, single letters) render as literal text.
|
|
128
111
|
*
|
|
129
|
-
* @param {unknown} value
|
|
130
|
-
* @param {ColorSpace} [space]
|
|
131
|
-
* @returns {unknown}
|
|
132
|
-
*/
|
|
133
|
-
export function colorToRgb(value: unknown, space?: ColorSpace): unknown;
|
|
134
|
-
/**
|
|
135
|
-
* Detects whether an icon value is a CSS class (i-*) or literal text (kanji, emoji, etc.)
|
|
136
112
|
* @param {string | null | undefined} icon
|
|
137
|
-
* @returns {boolean} true if icon
|
|
113
|
+
* @returns {boolean} true if icon should render as a CSS class
|
|
138
114
|
*/
|
|
139
115
|
export function isIconClass(icon: string | null | undefined): boolean;
|
|
140
116
|
/**
|
|
@@ -144,4 +120,3 @@ export function isIconClass(icon: string | null | undefined): boolean;
|
|
|
144
120
|
* @returns {string|null} - Returns the original string if it's an image URL or image data, otherwise null
|
|
145
121
|
*/
|
|
146
122
|
export function getImage(str: string): string | null;
|
|
147
|
-
export type ColorSpace = "rgb" | "hsl" | "oklch";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rokkit/core",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
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.0
|
|
49
|
+
"@rokkit/icons": "1.1.0"
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ColorSpace adapter strategy pattern.
|
|
3
|
+
*
|
|
4
|
+
* Each adapter knows how to convert hex colors to its own representation,
|
|
5
|
+
* wrap bare channels into a complete CSS color function, and produce
|
|
6
|
+
* `color-mix()` expressions for theme variables with alpha support.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ── Private math helpers (copied from utils.js — not imported) ──────────
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Convert sRGB component (0-255) to linear RGB.
|
|
13
|
+
* @param {number} c - sRGB value 0-255
|
|
14
|
+
* @returns {number}
|
|
15
|
+
*/
|
|
16
|
+
function srgbToLinear(c) {
|
|
17
|
+
const s = c / 255
|
|
18
|
+
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Linear sRGB → XYZ D65
|
|
22
|
+
const SRGB_TO_XYZ = [
|
|
23
|
+
[0.4123907993, 0.3575843394, 0.1804807884],
|
|
24
|
+
[0.2126390059, 0.7151686788, 0.0721923154],
|
|
25
|
+
[0.0193308187, 0.1191947798, 0.9505321522]
|
|
26
|
+
]
|
|
27
|
+
// XYZ D65 → LMS
|
|
28
|
+
const XYZ_TO_LMS = [
|
|
29
|
+
[0.8189330101, 0.3618667424, -0.1288597137],
|
|
30
|
+
[0.0329845436, 0.9293118715, 0.0361456387],
|
|
31
|
+
[0.0482003018, 0.2643662691, 0.633851707]
|
|
32
|
+
]
|
|
33
|
+
// LMS^(1/3) → OKLab
|
|
34
|
+
const LMS3_TO_OKLAB = [
|
|
35
|
+
[0.2104542553, 0.793617785, -0.0040720468],
|
|
36
|
+
[1.9779984951, -2.428592205, 0.4505937099],
|
|
37
|
+
[0.0259040371, 0.7827717662, -0.808675766]
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
/** @param {number[][]} m @param {number[]} v */
|
|
41
|
+
function matMul(m, v) {
|
|
42
|
+
return m.map((row) => row[0] * v[0] + row[1] * v[1] + row[2] * v[2])
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Hex parsing helper ──────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse a hex string (#rrggbb) into [r, g, b] integers.
|
|
49
|
+
* @param {string} hex
|
|
50
|
+
* @returns {number[]}
|
|
51
|
+
*/
|
|
52
|
+
function parseHex(hex) {
|
|
53
|
+
return hex.match(/\w\w/g).map((x) => parseInt(x, 16))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── Hex → HSL conversion ───────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Convert hex to HSL bare channels: "H S% L%"
|
|
60
|
+
* @param {string} hex
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
function hexToHsl(hex) {
|
|
64
|
+
const [r, g, b] = parseHex(hex)
|
|
65
|
+
const rn = r / 255
|
|
66
|
+
const gn = g / 255
|
|
67
|
+
const bn = b / 255
|
|
68
|
+
const max = Math.max(rn, gn, bn)
|
|
69
|
+
const min = Math.min(rn, gn, bn)
|
|
70
|
+
const l = (max + min) / 2
|
|
71
|
+
|
|
72
|
+
if (max === min) return `0 0% ${Math.round(l * 100)}%`
|
|
73
|
+
|
|
74
|
+
const d = max - min
|
|
75
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
|
76
|
+
|
|
77
|
+
let h = 0
|
|
78
|
+
if (max === rn) h = ((gn - bn) / d + (gn < bn ? 6 : 0)) / 6
|
|
79
|
+
else if (max === gn) h = ((bn - rn) / d + 2) / 6
|
|
80
|
+
else h = ((rn - gn) / d + 4) / 6
|
|
81
|
+
|
|
82
|
+
return `${Math.round(h * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Hex → OKLCH conversion ─────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Convert hex to OKLCH bare channels: "L C H"
|
|
89
|
+
* @param {string} hex
|
|
90
|
+
* @returns {string}
|
|
91
|
+
*/
|
|
92
|
+
function hexToOklch(hex) {
|
|
93
|
+
const [r, g, b] = parseHex(hex)
|
|
94
|
+
const lin = [srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)]
|
|
95
|
+
|
|
96
|
+
const xyz = matMul(SRGB_TO_XYZ, lin)
|
|
97
|
+
const lms = matMul(XYZ_TO_LMS, xyz)
|
|
98
|
+
const lms3 = [Math.cbrt(lms[0]), Math.cbrt(lms[1]), Math.cbrt(lms[2])]
|
|
99
|
+
const [L, a, bk] = matMul(LMS3_TO_OKLAB, lms3)
|
|
100
|
+
|
|
101
|
+
const C = Math.sqrt(a * a + bk * bk)
|
|
102
|
+
let H = (Math.atan2(bk, a) * 180) / Math.PI
|
|
103
|
+
if (H < 0) H += 360
|
|
104
|
+
|
|
105
|
+
const Lr = Math.round(L * 1000000) / 1000000
|
|
106
|
+
const Cr = Math.round(C * 1000000) / 1000000
|
|
107
|
+
const Hr = Math.round(H * 10000) / 10000
|
|
108
|
+
|
|
109
|
+
return `${Lr} ${Cr} ${Hr}`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Hex regex ───────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
const HEX_RE = /^#[0-9a-fA-F]{6}$/
|
|
115
|
+
|
|
116
|
+
// ── Base class ──────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Abstract base class for color space adapters.
|
|
120
|
+
*/
|
|
121
|
+
export class ColorSpace {
|
|
122
|
+
/** @returns {string} */
|
|
123
|
+
get name() {
|
|
124
|
+
throw new Error('Subclass must implement name')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** @returns {string} CSS color-mix interpolation space */
|
|
128
|
+
get mixSpace() {
|
|
129
|
+
throw new Error('Subclass must implement mixSpace')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** @returns {string} CSS function name */
|
|
133
|
+
get fn() {
|
|
134
|
+
throw new Error('Subclass must implement fn')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Convert a hex color to bare channels in this space.
|
|
139
|
+
* @param {string} hex - #rrggbb
|
|
140
|
+
* @returns {string}
|
|
141
|
+
*/
|
|
142
|
+
fromHex(_hex) {
|
|
143
|
+
throw new Error('Subclass must implement fromHex')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Wrap any input into a complete CSS color value.
|
|
148
|
+
* - hex → convert and wrap
|
|
149
|
+
* - bare channels → wrap in fn()
|
|
150
|
+
* - already wrapped → pass through
|
|
151
|
+
* - non-string / null / undefined → pass through
|
|
152
|
+
* - named colors → pass through
|
|
153
|
+
* @param {*} value
|
|
154
|
+
* @returns {*}
|
|
155
|
+
*/
|
|
156
|
+
wrap(_value) {
|
|
157
|
+
throw new Error('Subclass must implement wrap')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Produce a color-mix expression for theme CSS variables with alpha support.
|
|
162
|
+
* @param {string} varName - CSS variable name (e.g. '--color-primary-500')
|
|
163
|
+
* @returns {string}
|
|
164
|
+
*/
|
|
165
|
+
themeColor(varName) {
|
|
166
|
+
return `color-mix(in ${this.mixSpace}, var(${varName}) calc(<alpha-value> * 100%), transparent)`
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Factory method to create a ColorSpace adapter by name.
|
|
171
|
+
* @param {string} space - 'rgb', 'hsl', or 'oklch'
|
|
172
|
+
* @returns {ColorSpace}
|
|
173
|
+
*/
|
|
174
|
+
static create(space) {
|
|
175
|
+
switch (space) {
|
|
176
|
+
case 'rgb':
|
|
177
|
+
return new RgbColorSpace()
|
|
178
|
+
case 'hsl':
|
|
179
|
+
return new HslColorSpace()
|
|
180
|
+
case 'oklch':
|
|
181
|
+
return new OklchColorSpace()
|
|
182
|
+
default:
|
|
183
|
+
throw new Error(`Unknown color space: ${space}`)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── RGB adapter ─────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
export class RgbColorSpace extends ColorSpace {
|
|
191
|
+
get name() {
|
|
192
|
+
return 'rgb'
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
get mixSpace() {
|
|
196
|
+
return 'srgb'
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
get fn() {
|
|
200
|
+
return 'rgb'
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fromHex(hex) {
|
|
204
|
+
const [r, g, b] = parseHex(hex)
|
|
205
|
+
return `${r}, ${g}, ${b}`
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
wrap(value) {
|
|
209
|
+
if (typeof value !== 'string') return value
|
|
210
|
+
|
|
211
|
+
// Already wrapped
|
|
212
|
+
if (value.startsWith('rgb(') || value.startsWith('rgba(')) return value
|
|
213
|
+
|
|
214
|
+
// Hex
|
|
215
|
+
if (HEX_RE.test(value)) return `rgb(${this.fromHex(value)})`
|
|
216
|
+
|
|
217
|
+
// Bare channels: comma-separated or space-separated digits
|
|
218
|
+
// Comma-separated: "255,0,0" or "255, 0, 0"
|
|
219
|
+
if (/^\d+\s*,\s*\d+\s*,\s*\d+$/.test(value)) {
|
|
220
|
+
const parts = value.split(',').map((s) => s.trim())
|
|
221
|
+
return `rgb(${parts.join(', ')})`
|
|
222
|
+
}
|
|
223
|
+
// Space-separated: "255 0 0"
|
|
224
|
+
if (/^\d+\s+\d+\s+\d+$/.test(value)) {
|
|
225
|
+
const parts = value.trim().split(/\s+/)
|
|
226
|
+
return `rgb(${parts.join(', ')})`
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Named color or other — pass through
|
|
230
|
+
return value
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── HSL adapter ─────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
export class HslColorSpace extends ColorSpace {
|
|
237
|
+
get name() {
|
|
238
|
+
return 'hsl'
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
get mixSpace() {
|
|
242
|
+
return 'srgb'
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
get fn() {
|
|
246
|
+
return 'hsl'
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
fromHex(hex) {
|
|
250
|
+
return hexToHsl(hex)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
wrap(value) {
|
|
254
|
+
if (typeof value !== 'string') return value
|
|
255
|
+
|
|
256
|
+
// Already wrapped
|
|
257
|
+
if (value.startsWith('hsl(') || value.startsWith('hsla(')) return value
|
|
258
|
+
|
|
259
|
+
// Hex
|
|
260
|
+
if (HEX_RE.test(value)) return `hsl(${this.fromHex(value)})`
|
|
261
|
+
|
|
262
|
+
// Bare channels: "H S% L%" pattern (number, space, number%, space, number%)
|
|
263
|
+
if (/^\d+\s+\d+%\s+\d+%$/.test(value)) {
|
|
264
|
+
return `hsl(${value})`
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Named color or other — pass through
|
|
268
|
+
return value
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── OKLCH adapter ───────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
export class OklchColorSpace extends ColorSpace {
|
|
275
|
+
get name() {
|
|
276
|
+
return 'oklch'
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
get mixSpace() {
|
|
280
|
+
return 'oklch'
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
get fn() {
|
|
284
|
+
return 'oklch'
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fromHex(hex) {
|
|
288
|
+
return hexToOklch(hex)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
wrap(value) {
|
|
292
|
+
if (typeof value !== 'string') return value
|
|
293
|
+
|
|
294
|
+
// Already wrapped
|
|
295
|
+
if (value.startsWith('oklch(')) return value
|
|
296
|
+
|
|
297
|
+
// Hex
|
|
298
|
+
if (HEX_RE.test(value)) return `oklch(${this.fromHex(value)})`
|
|
299
|
+
|
|
300
|
+
// Bare channels: three space-separated numbers (may contain decimals)
|
|
301
|
+
if (/^[\d.]+\s+[\d.]+\s+[\d.]+$/.test(value)) {
|
|
302
|
+
return `oklch(${value})`
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Named color or other — pass through
|
|
306
|
+
return value
|
|
307
|
+
}
|
|
308
|
+
}
|
package/src/constants.js
CHANGED
|
@@ -79,6 +79,7 @@ export const DEFAULT_ICONS = [
|
|
|
79
79
|
'action-close',
|
|
80
80
|
'action-copy',
|
|
81
81
|
'action-copysuccess',
|
|
82
|
+
'action-download',
|
|
82
83
|
'action-check',
|
|
83
84
|
'action-pin',
|
|
84
85
|
'action-save',
|
|
@@ -87,6 +88,20 @@ export const DEFAULT_ICONS = [
|
|
|
87
88
|
'node-closed',
|
|
88
89
|
'folder-opened',
|
|
89
90
|
'folder-closed',
|
|
91
|
+
'doc-default',
|
|
92
|
+
'doc-svelte',
|
|
93
|
+
'doc-js',
|
|
94
|
+
'doc-ts',
|
|
95
|
+
'doc-css',
|
|
96
|
+
'doc-html',
|
|
97
|
+
'doc-json',
|
|
98
|
+
'doc-md',
|
|
99
|
+
'doc-text',
|
|
100
|
+
'doc-pdf',
|
|
101
|
+
'doc-archive',
|
|
102
|
+
'media-image',
|
|
103
|
+
'media-video',
|
|
104
|
+
'media-audio',
|
|
90
105
|
'selector-opened',
|
|
91
106
|
'selector-closed',
|
|
92
107
|
'menu-opened',
|
|
@@ -133,7 +148,9 @@ export const DEFAULT_ICONS = [
|
|
|
133
148
|
'align-vertical-bottom',
|
|
134
149
|
'align-vertical-middle',
|
|
135
150
|
'view-code',
|
|
136
|
-
'view-chart'
|
|
151
|
+
'view-chart',
|
|
152
|
+
'view-preview',
|
|
153
|
+
'view-off'
|
|
137
154
|
]
|
|
138
155
|
|
|
139
156
|
export const DEFAULT_OPTIONS = {
|
|
@@ -154,6 +171,7 @@ export const DEFAULT_KEYMAP = {
|
|
|
154
171
|
|
|
155
172
|
export const DEFAULT_THEME_MAPPING = {
|
|
156
173
|
surface: 'slate',
|
|
174
|
+
ink: 'slate',
|
|
157
175
|
primary: 'orange',
|
|
158
176
|
secondary: 'pink',
|
|
159
177
|
tertiary: 'violet',
|
|
@@ -178,6 +196,9 @@ export const TONE_MAP = {
|
|
|
178
196
|
z9: 900,
|
|
179
197
|
z10: 950
|
|
180
198
|
}
|
|
199
|
+
|
|
200
|
+
export const INVERTED_ROLES = new Set(['ink'])
|
|
201
|
+
|
|
181
202
|
/**
|
|
182
203
|
* Splits an icon name into its group and key components.
|
|
183
204
|
* @param {string} name - The icon name to split.
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export * from './types.js'
|
|
3
3
|
// skipcq: JS-E1004 - Needed for exposing all constants
|
|
4
4
|
export * from './constants.js'
|
|
5
|
+
export * from './named-tokens.ts'
|
|
5
6
|
// skipcq: JS-E1004 - Needed for exposing all functions
|
|
6
7
|
export * from './utils.js'
|
|
7
8
|
// skipcq: JS-E1004 - Needed for exposing all functions
|
|
@@ -10,6 +11,7 @@ export * from './nested.js'
|
|
|
10
11
|
export * from './string.js'
|
|
11
12
|
// skipcq: JS-E1004 - Needed for exposing all functions
|
|
12
13
|
export * from './theme.js'
|
|
14
|
+
export { ColorSpace } from './color-space.js'
|
|
13
15
|
export { FieldMapper } from './field-mapper.js'
|
|
14
16
|
export { getItemAtIndex, getIndexForItem } from './mapped-items.js'
|
|
15
17
|
export { createEmitter } from './events.js'
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 24 canonical named tokens that constitute the "core" emit vocabulary.
|
|
3
|
+
* Order is significant for tests and for predictable CSS-var output ordering.
|
|
4
|
+
*/
|
|
5
|
+
export const NAMED_TOKENS = [
|
|
6
|
+
'paper', 'paper-soft', 'paper-mute', 'paper-edge',
|
|
7
|
+
'ink', 'ink-mute', 'ink-soft', 'ink-faint',
|
|
8
|
+
'primary', 'on-primary',
|
|
9
|
+
'accent', 'accent-soft',
|
|
10
|
+
'success', 'success-soft',
|
|
11
|
+
'warning', 'warning-soft',
|
|
12
|
+
'danger', 'danger-soft',
|
|
13
|
+
'error', 'error-soft',
|
|
14
|
+
'info', 'info-soft',
|
|
15
|
+
'focus-ring', 'shadow-tint'
|
|
16
|
+
] as const
|
|
17
|
+
|
|
18
|
+
export type NamedToken = (typeof NAMED_TOKENS)[number]
|
|
19
|
+
|
|
20
|
+
export type SkinRole =
|
|
21
|
+
| 'surface' | 'ink' | 'primary' | 'accent'
|
|
22
|
+
| 'success' | 'warning' | 'danger' | 'error' | 'info'
|
|
23
|
+
|
|
24
|
+
export type ZSlot = 'z0' | 'z1' | 'z2' | 'z3' | 'z4' | 'z5' | 'z6' | 'z7' | 'z8' | 'z9' | 'z10'
|
|
25
|
+
|
|
26
|
+
export const Z_SLOTS: ZSlot[] = ['z0', 'z1', 'z2', 'z3', 'z4', 'z5', 'z6', 'z7', 'z8', 'z9', 'z10']
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Maps each named token to the palette shade index that backs it.
|
|
30
|
+
* 'derived' indicates the token is computed from another palette (e.g., on-primary
|
|
31
|
+
* picks the paper shade of the surface palette by default for white-on-primary).
|
|
32
|
+
*
|
|
33
|
+
* paper: 50 (lightest surface tone, canvas)
|
|
34
|
+
* paper-soft: 100 (card background)
|
|
35
|
+
* paper-mute: 200 (subdued panel)
|
|
36
|
+
* paper-edge: 400 (hairline border tone)
|
|
37
|
+
* ink: 900 (primary text)
|
|
38
|
+
* ink-mute: 700 (secondary text)
|
|
39
|
+
* ink-soft: 500 (placeholder)
|
|
40
|
+
* ink-faint: 300 (disabled)
|
|
41
|
+
* primary: 500 (CTA fill)
|
|
42
|
+
* on-primary: derived — surface.50 default (white-on-primary)
|
|
43
|
+
* accent / status: 500 (solid)
|
|
44
|
+
* *-soft: 100 (tinted-bg companion)
|
|
45
|
+
* focus-ring: 500 of accent
|
|
46
|
+
* shadow-tint: 900 of ink
|
|
47
|
+
*/
|
|
48
|
+
export const NAMED_TOKEN_SHADE_MAP: Record<NamedToken, number | 'derived'> = {
|
|
49
|
+
paper: 50,
|
|
50
|
+
'paper-soft': 100,
|
|
51
|
+
'paper-mute': 200,
|
|
52
|
+
'paper-edge': 400,
|
|
53
|
+
ink: 900,
|
|
54
|
+
'ink-mute': 700,
|
|
55
|
+
'ink-soft': 500,
|
|
56
|
+
'ink-faint': 300,
|
|
57
|
+
primary: 500,
|
|
58
|
+
'on-primary': 'derived',
|
|
59
|
+
accent: 500,
|
|
60
|
+
'accent-soft': 100,
|
|
61
|
+
success: 500,
|
|
62
|
+
'success-soft': 100,
|
|
63
|
+
warning: 500,
|
|
64
|
+
'warning-soft': 100,
|
|
65
|
+
danger: 500,
|
|
66
|
+
'danger-soft': 100,
|
|
67
|
+
error: 500,
|
|
68
|
+
'error-soft': 100,
|
|
69
|
+
info: 500,
|
|
70
|
+
'info-soft': 100,
|
|
71
|
+
'focus-ring': 500,
|
|
72
|
+
'shadow-tint': 900
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Maps each named token to the skin role whose palette it draws from.
|
|
77
|
+
*/
|
|
78
|
+
export const NAMED_TOKEN_ROLE_MAP: Record<NamedToken, SkinRole> = {
|
|
79
|
+
paper: 'surface',
|
|
80
|
+
'paper-soft': 'surface',
|
|
81
|
+
'paper-mute': 'surface',
|
|
82
|
+
'paper-edge': 'surface',
|
|
83
|
+
ink: 'ink',
|
|
84
|
+
'ink-mute': 'ink',
|
|
85
|
+
'ink-soft': 'ink',
|
|
86
|
+
'ink-faint': 'ink',
|
|
87
|
+
primary: 'primary',
|
|
88
|
+
'on-primary': 'primary',
|
|
89
|
+
accent: 'accent',
|
|
90
|
+
'accent-soft': 'accent',
|
|
91
|
+
success: 'success',
|
|
92
|
+
'success-soft': 'success',
|
|
93
|
+
warning: 'warning',
|
|
94
|
+
'warning-soft': 'warning',
|
|
95
|
+
danger: 'danger',
|
|
96
|
+
'danger-soft': 'danger',
|
|
97
|
+
error: 'error',
|
|
98
|
+
'error-soft': 'error',
|
|
99
|
+
info: 'info',
|
|
100
|
+
'info-soft': 'info',
|
|
101
|
+
'focus-ring': 'accent',
|
|
102
|
+
'shadow-tint': 'ink'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Surface z-slot → named slot. Used to emit back-compat aliases like
|
|
107
|
+
* `--color-surface-z1: var(--paper-soft);` in core mode.
|
|
108
|
+
*
|
|
109
|
+
* z2/z3 collapse to paper-mute, z5/z6 to ink-soft, z7/z8 to ink-mute,
|
|
110
|
+
* z9/z10 to ink. This collapse is the contract: 4 surface tones in core mode.
|
|
111
|
+
*/
|
|
112
|
+
export const Z_COLLAPSE_MAP_SURFACE: Record<ZSlot, NamedToken> = {
|
|
113
|
+
z0: 'paper',
|
|
114
|
+
z1: 'paper-soft',
|
|
115
|
+
z2: 'paper-mute',
|
|
116
|
+
z3: 'paper-mute',
|
|
117
|
+
z4: 'paper-edge',
|
|
118
|
+
z5: 'ink-soft',
|
|
119
|
+
z6: 'ink-soft',
|
|
120
|
+
z7: 'ink-mute',
|
|
121
|
+
z8: 'ink-mute',
|
|
122
|
+
z9: 'ink',
|
|
123
|
+
z10: 'ink'
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Ink role uses the inverted z-scale (z0 = darkest = primary text).
|
|
128
|
+
* In core mode, the same 4+4 named ladder is reused; this table just inverts.
|
|
129
|
+
*/
|
|
130
|
+
export const Z_COLLAPSE_MAP_INK: Record<ZSlot, NamedToken> = {
|
|
131
|
+
z0: 'ink',
|
|
132
|
+
z1: 'ink-mute',
|
|
133
|
+
z2: 'ink-mute',
|
|
134
|
+
z3: 'ink-soft',
|
|
135
|
+
z4: 'ink-soft',
|
|
136
|
+
z5: 'paper-edge',
|
|
137
|
+
z6: 'paper-edge',
|
|
138
|
+
z7: 'paper-mute',
|
|
139
|
+
z8: 'paper-mute',
|
|
140
|
+
z9: 'paper-soft',
|
|
141
|
+
z10: 'paper'
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const SOFT_TOKENS: Set<string> = new Set(NAMED_TOKENS.filter((t) => t.endsWith('-soft')))
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Returns true when the role has a `-soft` companion in the named vocabulary.
|
|
148
|
+
* Used to decide whether the role's tinted-background slot collapses to
|
|
149
|
+
* `var(--{role}-soft)` or to `var(--{role})`.
|
|
150
|
+
*/
|
|
151
|
+
export function hasSoftCompanion(role: string): boolean {
|
|
152
|
+
return SOFT_TOKENS.has(`${role}-soft`)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function shadeForNamedToken(name: string): number | 'derived' | undefined {
|
|
156
|
+
return NAMED_TOKEN_SHADE_MAP[name as NamedToken]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function roleForNamedToken(name: string): SkinRole | undefined {
|
|
160
|
+
return NAMED_TOKEN_ROLE_MAP[name as NamedToken]
|
|
161
|
+
}
|
package/src/theme.ts
CHANGED
|
@@ -1,44 +1,35 @@
|
|
|
1
1
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
2
2
|
// @ts-nocheck
|
|
3
|
-
import { DEFAULT_THEME_MAPPING, defaultColors, TONE_MAP } from './constants'
|
|
3
|
+
import { DEFAULT_THEME_MAPPING, defaultColors, TONE_MAP, INVERTED_ROLES } from './constants'
|
|
4
4
|
import { shades } from './colors/index'
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* CSS function wrappers for each color space, used by mapVariant().
|
|
16
|
-
* rgb uses legacy rgba() comma syntax for broad compatibility.
|
|
17
|
-
* hsl/oklch use modern space-separated syntax with / for alpha.
|
|
18
|
-
*/
|
|
19
|
-
const COLOR_SPACE_WRAPPERS = {
|
|
20
|
-
rgb: (varRef) => `rgba(${varRef},<alpha-value>)`,
|
|
21
|
-
hsl: (varRef) => `hsl(${varRef} / <alpha-value>)`,
|
|
22
|
-
oklch: (varRef) => `oklch(${varRef} / <alpha-value>)`
|
|
23
|
-
}
|
|
5
|
+
import { ColorSpace } from './color-space'
|
|
6
|
+
import {
|
|
7
|
+
NAMED_TOKENS,
|
|
8
|
+
NAMED_TOKEN_SHADE_MAP,
|
|
9
|
+
NAMED_TOKEN_ROLE_MAP,
|
|
10
|
+
Z_COLLAPSE_MAP_SURFACE,
|
|
11
|
+
Z_COLLAPSE_MAP_INK,
|
|
12
|
+
Z_SLOTS,
|
|
13
|
+
hasSoftCompanion
|
|
14
|
+
} from './named-tokens'
|
|
24
15
|
|
|
25
16
|
/**
|
|
26
|
-
* Generate shades for a color using css
|
|
17
|
+
* Generate shades for a color using css variable and a ColorSpace adapter.
|
|
27
18
|
*
|
|
28
19
|
* @param {string} name
|
|
29
|
-
* @param {string}
|
|
30
|
-
* @returns
|
|
20
|
+
* @param {string|import('./color-space').ColorSpace} space - color space name or adapter instance
|
|
21
|
+
* @returns {Record<string|number, string>}
|
|
31
22
|
*/
|
|
32
|
-
export function shadesOf(name,
|
|
33
|
-
const
|
|
23
|
+
export function shadesOf(name, space = 'rgb') {
|
|
24
|
+
const adapter = typeof space === 'string' ? ColorSpace.create(space) : space
|
|
34
25
|
|
|
35
26
|
return shades.reduce(
|
|
36
27
|
(result, shade) => ({
|
|
37
28
|
...result,
|
|
38
|
-
[shade]:
|
|
29
|
+
[shade]: adapter.themeColor(`--color-${name}-${shade}`)
|
|
39
30
|
}),
|
|
40
31
|
{
|
|
41
|
-
DEFAULT:
|
|
32
|
+
DEFAULT: adapter.themeColor(`--color-${name}-500`)
|
|
42
33
|
}
|
|
43
34
|
)
|
|
44
35
|
}
|
|
@@ -49,14 +40,14 @@ export function shadesOf(name, modifier = 'none') {
|
|
|
49
40
|
* @param {string} variant - The name of the variant to generate rules for.
|
|
50
41
|
* @param {Object} colors - The object containing color definitions.
|
|
51
42
|
* @param {Object} mapping - An object that maps variant names to color property names.
|
|
52
|
-
* @param {import('./
|
|
43
|
+
* @param {import('./color-space').ColorSpace} adapter - ColorSpace adapter instance.
|
|
53
44
|
* @returns {import('./types').ShadeMappings} An array containing the color rules for both light and dark modes.
|
|
54
45
|
*/
|
|
55
|
-
function generateColorRules(variant, colors, mapping,
|
|
46
|
+
function generateColorRules(variant, colors, mapping, adapter) {
|
|
56
47
|
return ['DEFAULT', ...shades].flatMap((shade) => [
|
|
57
48
|
{
|
|
58
49
|
key: shade === 'DEFAULT' ? `--color-${variant}` : `--color-${variant}-${shade}`,
|
|
59
|
-
value:
|
|
50
|
+
value: adapter.wrap(colors[mapping[variant]][`${shade}`])
|
|
60
51
|
}
|
|
61
52
|
])
|
|
62
53
|
}
|
|
@@ -66,15 +57,16 @@ function generateColorRules(variant, colors, mapping, colorSpace) {
|
|
|
66
57
|
*
|
|
67
58
|
* @param {Object} [mapping=DEFAULT_THEME_MAPPING] - An object mapping variant names to color property names.
|
|
68
59
|
* @param {Object} [colors=defaultColors] - The object containing default color definitions.
|
|
69
|
-
* @param {
|
|
60
|
+
* @param {string} [colorSpace] - Color space name for CSS variable values.
|
|
70
61
|
* @returns {Array<Array>} An array containing two arrays, one for the light theme variant and another for the dark theme.
|
|
71
62
|
*/
|
|
72
63
|
export function themeRules(mapping = DEFAULT_THEME_MAPPING, colors = defaultColors, colorSpace) {
|
|
73
64
|
mapping = { ...DEFAULT_THEME_MAPPING, ...mapping }
|
|
74
65
|
colors = { ...defaultColors, ...colors }
|
|
66
|
+
const adapter = ColorSpace.create(colorSpace || 'rgb')
|
|
75
67
|
const variants = Object.keys(mapping)
|
|
76
68
|
const rules = variants
|
|
77
|
-
.flatMap((variant) => generateColorRules(variant, colors, mapping,
|
|
69
|
+
.flatMap((variant) => generateColorRules(variant, colors, mapping, adapter))
|
|
78
70
|
.reduce((acc, { key, value }) => ({ ...acc, [key]: value }), {})
|
|
79
71
|
|
|
80
72
|
return rules
|
|
@@ -126,9 +118,11 @@ function toneShortcuts({ prefix, name, toneName, lightValue, darkValue }) {
|
|
|
126
118
|
* @returns {Array} Array of shortcut definitions
|
|
127
119
|
*/
|
|
128
120
|
export function semanticShortcuts(name) {
|
|
121
|
+
const inverted = INVERTED_ROLES.has(name)
|
|
129
122
|
const shortcuts = []
|
|
130
|
-
for (const [toneName,
|
|
131
|
-
const
|
|
123
|
+
for (const [toneName, toneValue] of Object.entries(TONE_MAP)) {
|
|
124
|
+
const lightValue = inverted ? 1000 - toneValue : toneValue
|
|
125
|
+
const darkValue = inverted ? toneValue : 1000 - toneValue
|
|
132
126
|
for (const prefix of SEMANTIC_PREFIXES) {
|
|
133
127
|
shortcuts.push(...toneShortcuts({ prefix, name, toneName, lightValue, darkValue }))
|
|
134
128
|
}
|
|
@@ -163,6 +157,7 @@ export function contrastShortcuts(name) {
|
|
|
163
157
|
* If a semantic color is null, it inherits from another semantic color.
|
|
164
158
|
*/
|
|
165
159
|
const COLOR_FALLBACKS = {
|
|
160
|
+
ink: 'surface',
|
|
166
161
|
tertiary: 'primary',
|
|
167
162
|
secondary: 'primary',
|
|
168
163
|
accent: 'primary',
|
|
@@ -190,16 +185,16 @@ function resolveColors(mapping) {
|
|
|
190
185
|
export class Theme {
|
|
191
186
|
#colors
|
|
192
187
|
#mapping
|
|
193
|
-
#
|
|
188
|
+
#adapter
|
|
194
189
|
|
|
195
190
|
/**
|
|
196
191
|
*
|
|
197
|
-
* @param {import('./types.js').ColorTheme & { colorSpace?:
|
|
192
|
+
* @param {import('./types.js').ColorTheme & { colorSpace?: string }} param0
|
|
198
193
|
*/
|
|
199
194
|
constructor({ colors = defaultColors, mapping = DEFAULT_THEME_MAPPING, colorSpace = 'rgb' } = {}) {
|
|
200
195
|
this.#colors = { ...defaultColors, ...colors }
|
|
201
196
|
this.#mapping = resolveColors({ ...DEFAULT_THEME_MAPPING, ...mapping })
|
|
202
|
-
this.#
|
|
197
|
+
this.#adapter = ColorSpace.create(colorSpace)
|
|
203
198
|
}
|
|
204
199
|
|
|
205
200
|
get colors() {
|
|
@@ -217,21 +212,20 @@ export class Theme {
|
|
|
217
212
|
}
|
|
218
213
|
|
|
219
214
|
get colorSpace() {
|
|
220
|
-
return this.#
|
|
215
|
+
return this.#adapter.name
|
|
221
216
|
}
|
|
222
217
|
set colorSpace(colorSpace) {
|
|
223
|
-
this.#
|
|
218
|
+
this.#adapter = ColorSpace.create(colorSpace)
|
|
224
219
|
}
|
|
225
220
|
|
|
226
221
|
mapVariant(color, variant) {
|
|
227
|
-
const wrap = COLOR_SPACE_WRAPPERS[this.#colorSpace] || COLOR_SPACE_WRAPPERS.rgb
|
|
228
222
|
return Object.keys(color).reduce(
|
|
229
223
|
(acc, key) => ({
|
|
230
224
|
...acc,
|
|
231
225
|
[key]:
|
|
232
226
|
key === 'DEFAULT'
|
|
233
|
-
?
|
|
234
|
-
:
|
|
227
|
+
? this.#adapter.themeColor(`--color-${variant}`)
|
|
228
|
+
: this.#adapter.themeColor(`--color-${variant}-${key}`)
|
|
235
229
|
}),
|
|
236
230
|
{}
|
|
237
231
|
)
|
|
@@ -253,11 +247,196 @@ export class Theme {
|
|
|
253
247
|
const useColors = { ...defaultColors, ...this.#colors }
|
|
254
248
|
const variants = Object.keys(useMapping)
|
|
255
249
|
const rules = variants
|
|
256
|
-
.flatMap((variant) => generateColorRules(variant, useColors, useMapping, this.#
|
|
250
|
+
.flatMap((variant) => generateColorRules(variant, useColors, useMapping, this.#adapter))
|
|
257
251
|
.reduce((acc, { key, value }) => ({ ...acc, [key]: value }), {})
|
|
258
252
|
return rules
|
|
259
253
|
}
|
|
260
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Returns the named-token layer as a map of `--{name}: <resolved value>` entries.
|
|
257
|
+
*
|
|
258
|
+
* Palette values are inlined — there is no `var(--color-{role}-{shade})` indirection.
|
|
259
|
+
*
|
|
260
|
+
* `on-primary` is derived: it picks the paper shade (50) of the surface palette so the
|
|
261
|
+
* default contrast pair is "primary fill + white-ish text." Skins that need a different
|
|
262
|
+
* pair should override via custom tokens.
|
|
263
|
+
*
|
|
264
|
+
* `*-soft` variants resolve to shade 100 of their role's palette.
|
|
265
|
+
* `shadow-tint` resolves to shade 900 of the ink palette.
|
|
266
|
+
*
|
|
267
|
+
* @param {'light' | 'dark'} _mode — accepted for symmetry with future
|
|
268
|
+
* dark-aware derivations. Today the Theme is constructed with a mode-resolved
|
|
269
|
+
* mapping, so this parameter is informational.
|
|
270
|
+
* @param perRoleModes — optional map of role → 'core' | 'extended'. When a token's
|
|
271
|
+
* role is 'extended', the value is a `var(--color-{role}-{shade})` alias instead
|
|
272
|
+
* of the inlined palette value. Defaults to 'core' for all unspecified roles
|
|
273
|
+
* (and for the whole map when undefined).
|
|
274
|
+
*/
|
|
275
|
+
getNamedTokens(_mode = 'light', perRoleModes?: Record<string, 'core' | 'extended'>) {
|
|
276
|
+
const colors = { ...defaultColors, ...this.#colors }
|
|
277
|
+
const result: Record<string, string> = {}
|
|
278
|
+
for (const name of NAMED_TOKENS) {
|
|
279
|
+
const value = this.#resolveNamedToken(name, colors, perRoleModes)
|
|
280
|
+
if (value !== undefined) result[`--${name}`] = value
|
|
281
|
+
}
|
|
282
|
+
return result
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Resolves a single named token to its CSS value, respecting per-role mode.
|
|
287
|
+
* Returns undefined when the role's palette is missing.
|
|
288
|
+
*/
|
|
289
|
+
#resolveNamedToken(name: string, colors: Record<string, Record<string, string>>, perRoleModes?: Record<string, 'core' | 'extended'>): string | undefined {
|
|
290
|
+
const role = NAMED_TOKEN_ROLE_MAP[name]
|
|
291
|
+
const shadeOrDerived = NAMED_TOKEN_SHADE_MAP[name]
|
|
292
|
+
const paletteName = this.#mapping[role]
|
|
293
|
+
if (!paletteName || !colors[paletteName]) return undefined
|
|
294
|
+
if (shadeOrDerived === 'derived') return this.#resolveDerivedToken(name, colors, perRoleModes)
|
|
295
|
+
return this.#resolveShadeToken(role, shadeOrDerived as number, colors[paletteName], perRoleModes)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Resolves a shade-based named token value in either core (inline) or extended (alias) mode.
|
|
300
|
+
*/
|
|
301
|
+
#resolveShadeToken(role: string, shade: number, palette: Record<string, string>, perRoleModes?: Record<string, 'core' | 'extended'>): string | undefined {
|
|
302
|
+
const roleMode = perRoleModes?.[role] ?? 'core'
|
|
303
|
+
if (roleMode === 'extended') return `var(--color-${role}-${shade})`
|
|
304
|
+
const raw = palette[String(shade)]
|
|
305
|
+
return raw !== undefined ? this.#adapter.wrap(raw) : undefined
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Resolves a 'derived' named token. Today only `on-primary` is derived — it picks
|
|
310
|
+
* shade 50 from the surface palette for the default white-on-primary contrast pair.
|
|
311
|
+
*/
|
|
312
|
+
#resolveDerivedToken(name: string, colors: Record<string, Record<string, string>>, perRoleModes?: Record<string, 'core' | 'extended'>): string | undefined {
|
|
313
|
+
if (name === 'on-primary') {
|
|
314
|
+
const surfacePaletteName = this.#mapping['surface']
|
|
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'])
|
|
322
|
+
}
|
|
323
|
+
return undefined
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Generate z-aliases for surface or ink role using the provided map.
|
|
328
|
+
*/
|
|
329
|
+
#getZAliasesFromMap(role: 'surface' | 'ink', map: Record<ZSlot, NamedToken>): Record<string, string> {
|
|
330
|
+
const result: Record<string, string> = {}
|
|
331
|
+
// Bare `--color-{role}` alias — points at the role's primary named
|
|
332
|
+
// slot. Lets preset-wind3 color utilities (bg-surface, from-surface,
|
|
333
|
+
// to-surface, etc.) resolve in core mode without forcing the full
|
|
334
|
+
// palette emit. Surface → paper, Ink → ink.
|
|
335
|
+
result[`--color-${role}`] = role === 'surface' ? 'var(--paper)' : 'var(--ink)'
|
|
336
|
+
for (const z of Z_SLOTS) {
|
|
337
|
+
result[`--color-${role}-${z}`] = `var(--${map[z]})`
|
|
338
|
+
}
|
|
339
|
+
return result
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Generate z-aliases for other roles using tint-vs-solid 2-state collapse.
|
|
344
|
+
*/
|
|
345
|
+
#getZAliasesOther(role: string): Record<string, string> {
|
|
346
|
+
const result: Record<string, string> = {}
|
|
347
|
+
const hasSoft = hasSoftCompanion(role)
|
|
348
|
+
const softTarget = hasSoft ? `${role}-soft` : role
|
|
349
|
+
// Bare `--color-{role}` alias points at the named token so
|
|
350
|
+
// preset-wind3 color utilities (from-{role}, to-{role}, divide-{role},
|
|
351
|
+
// etc.) resolve in core mode — they read `--color-{role}` and we'd
|
|
352
|
+
// otherwise leave it undefined since core mode skips the palette emit.
|
|
353
|
+
result[`--color-${role}`] = `var(--${role})`
|
|
354
|
+
for (const z of Z_SLOTS) {
|
|
355
|
+
const zNum = parseInt(z.slice(1), 10)
|
|
356
|
+
const target = zNum <= 2 ? softTarget : role
|
|
357
|
+
result[`--color-${role}-${z}`] = `var(--${target})`
|
|
358
|
+
}
|
|
359
|
+
return result
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Returns z-alias CSS-var assignments for back-compat in core mode.
|
|
364
|
+
* `--color-{role}-z{n}` → `var(--{namedSlot})`.
|
|
365
|
+
*
|
|
366
|
+
* - surface: uses Z_COLLAPSE_MAP_SURFACE (z2/z3 → paper-mute, z9/z10 → ink, etc.)
|
|
367
|
+
* - ink: uses Z_COLLAPSE_MAP_INK (inverted scale)
|
|
368
|
+
* - primary/accent/status: tint-vs-solid 2-state collapse.
|
|
369
|
+
* z0–z2 → role-soft (if it exists) else role; z3+ → role.
|
|
370
|
+
*
|
|
371
|
+
* This is the back-compat layer: existing `@apply bg-surface-z3` etc. resolves
|
|
372
|
+
* through these aliases until consumers migrate to the named vocabulary.
|
|
373
|
+
*/
|
|
374
|
+
/** Accepts SkinRole and any custom skin roles beyond the SkinRole literal union. */
|
|
375
|
+
getZAliasesForCore(role: string): Record<string, string> {
|
|
376
|
+
if (role === 'surface') {
|
|
377
|
+
return this.#getZAliasesFromMap('surface', Z_COLLAPSE_MAP_SURFACE)
|
|
378
|
+
}
|
|
379
|
+
if (role === 'ink') {
|
|
380
|
+
return this.#getZAliasesFromMap('ink', Z_COLLAPSE_MAP_INK)
|
|
381
|
+
}
|
|
382
|
+
return this.#getZAliasesOther(role)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Returns named tokens as CSS-var aliases pointing at the palette layer.
|
|
387
|
+
* Used in extended mode where the preset emits the full --color-{role}-{shade}
|
|
388
|
+
* palette; named tokens become thin syntactic aliases over the palette.
|
|
389
|
+
*
|
|
390
|
+
* `on-primary` aliases to `var(--color-surface-50)` (derived: paper of surface,
|
|
391
|
+
* matching the core-mode resolution).
|
|
392
|
+
*/
|
|
393
|
+
getZAliasesForExtended(): Record<string, string> {
|
|
394
|
+
const result: Record<string, string> = {}
|
|
395
|
+
for (const name of NAMED_TOKENS) {
|
|
396
|
+
const role = NAMED_TOKEN_ROLE_MAP[name]
|
|
397
|
+
const shadeOrDerived = NAMED_TOKEN_SHADE_MAP[name]
|
|
398
|
+
if (shadeOrDerived === 'derived') {
|
|
399
|
+
// on-primary → surface.50
|
|
400
|
+
result[`--${name}`] = `var(--color-surface-50)`
|
|
401
|
+
continue
|
|
402
|
+
}
|
|
403
|
+
result[`--${name}`] = `var(--color-${role}-${shadeOrDerived})`
|
|
404
|
+
}
|
|
405
|
+
return result
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Emit the full --color-{role}-{shade} palette CSS vars for a single role.
|
|
410
|
+
* Mirrors what getPalette does for all roles, but scoped to one.
|
|
411
|
+
*/
|
|
412
|
+
getPaletteForRole(role: string): Record<string, string> {
|
|
413
|
+
const colors = { ...defaultColors, ...this.#colors }
|
|
414
|
+
const paletteName = this.#mapping[role]
|
|
415
|
+
if (!paletteName || !colors[paletteName]) return {}
|
|
416
|
+
const palette = colors[paletteName]
|
|
417
|
+
const result: Record<string, string> = {}
|
|
418
|
+
for (const shade of shades) {
|
|
419
|
+
if (palette[shade] !== undefined) {
|
|
420
|
+
result[`--color-${role}-${shade}`] = this.#adapter.wrap(palette[shade])
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return result
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Emit z-aliases for one role in extended mode — they point at the palette vars,
|
|
428
|
+
* using TONE_MAP for surface/non-inverted roles and the inverted map for ink.
|
|
429
|
+
*/
|
|
430
|
+
getZAliasesForRoleExtended(role: string): Record<string, string> {
|
|
431
|
+
const result: Record<string, string> = {}
|
|
432
|
+
const isInverted = INVERTED_ROLES.has(role)
|
|
433
|
+
for (const [zone, light] of Object.entries(TONE_MAP)) {
|
|
434
|
+
const shade = isInverted ? 1000 - light : light
|
|
435
|
+
result[`--color-${role}-${zone}`] = `var(--color-${role}-${shade})`
|
|
436
|
+
}
|
|
437
|
+
return result
|
|
438
|
+
}
|
|
439
|
+
|
|
261
440
|
getShortcuts(name) {
|
|
262
441
|
return [...semanticShortcuts(name), ...contrastShortcuts(name)]
|
|
263
442
|
}
|
|
@@ -276,15 +455,17 @@ export class Theme {
|
|
|
276
455
|
const names = Object.keys(this.#mapping)
|
|
277
456
|
|
|
278
457
|
const lightLines = names.flatMap((name) =>
|
|
279
|
-
Object.entries(TONE_MAP).map(
|
|
280
|
-
(
|
|
281
|
-
|
|
458
|
+
Object.entries(TONE_MAP).map(([zone, light]) => {
|
|
459
|
+
const value = INVERTED_ROLES.has(name) ? 1000 - light : light
|
|
460
|
+
return ` --color-${name}-${zone}: var(--color-${name}-${value});`
|
|
461
|
+
})
|
|
282
462
|
)
|
|
283
463
|
|
|
284
464
|
const darkLines = names.flatMap((name) =>
|
|
285
465
|
Object.entries(TONE_MAP).map(([zone, light]) => {
|
|
286
466
|
const dark = 1000 - light
|
|
287
|
-
|
|
467
|
+
const value = INVERTED_ROLES.has(name) ? light : dark
|
|
468
|
+
return ` --color-${name}-${zone}: var(--color-${name}-${value});`
|
|
288
469
|
})
|
|
289
470
|
)
|
|
290
471
|
|
package/src/utils.js
CHANGED
|
@@ -318,93 +318,22 @@ export function oklch2hex(L, C, H) {
|
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
/**
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
* @returns {string} space-separated HSL components for CSS `hsl(H S% L% / alpha)`
|
|
324
|
-
*/
|
|
325
|
-
export function hex2hsl(hex) {
|
|
326
|
-
const [r, g, b] = hex.match(/\w\w/g).map((x) => parseInt(x, 16))
|
|
327
|
-
|
|
328
|
-
const rn = r / 255
|
|
329
|
-
const gn = g / 255
|
|
330
|
-
const bn = b / 255
|
|
331
|
-
const max = Math.max(rn, gn, bn)
|
|
332
|
-
const min = Math.min(rn, gn, bn)
|
|
333
|
-
const l = (max + min) / 2
|
|
334
|
-
|
|
335
|
-
if (max === min) return `0 0% ${Math.round(l * 100)}%`
|
|
336
|
-
|
|
337
|
-
const d = max - min
|
|
338
|
-
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
|
339
|
-
|
|
340
|
-
let h = 0
|
|
341
|
-
if (max === rn) h = ((gn - bn) / d + (gn < bn ? 6 : 0)) / 6
|
|
342
|
-
else if (max === gn) h = ((bn - rn) / d + 2) / 6
|
|
343
|
-
else h = ((rn - gn) / d + 4) / 6
|
|
344
|
-
|
|
345
|
-
const Hr = Math.round(h * 360)
|
|
346
|
-
const Sr = Math.round(s * 100)
|
|
347
|
-
const Lr = Math.round(l * 100)
|
|
348
|
-
|
|
349
|
-
return `${Hr} ${Sr}% ${Lr}%`
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/** @typedef {'rgb' | 'hsl' | 'oklch'} ColorSpace */
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Convert a hex color to the component format for a given color space.
|
|
356
|
-
* - rgb: "r,g,b" → for `rgba(var(--x), alpha)`
|
|
357
|
-
* - hsl: "H S% L%" → for `hsl(var(--x) / alpha)`
|
|
358
|
-
* - oklch: "L C H" → for `oklch(var(--x) / alpha)`
|
|
321
|
+
* Detects whether an icon value is a CSS class or a literal character
|
|
322
|
+
* (kanji, emoji, single letter, etc.).
|
|
359
323
|
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
|
|
364
|
-
export function hexToComponents(hex, space) {
|
|
365
|
-
switch (space) {
|
|
366
|
-
case 'hsl':
|
|
367
|
-
return hex2hsl(hex)
|
|
368
|
-
case 'oklch':
|
|
369
|
-
return hex2oklch(hex)
|
|
370
|
-
case 'rgb':
|
|
371
|
-
default:
|
|
372
|
-
return hex2rgb(hex)
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Convert a CSS color value to components for use in CSS variables.
|
|
378
|
-
*
|
|
379
|
-
* - Hex (#rrggbb): converted to space-appropriate components via hexToComponents.
|
|
380
|
-
* - oklch(...) strings: inner components extracted (e.g. "0.58 0.15 35") so the
|
|
381
|
-
* value is compatible with the oklch(var(--x) / alpha) wrapper pattern.
|
|
382
|
-
* - All other formats: returned as-is (no opacity-modifier support).
|
|
324
|
+
* Anything more than one grapheme is treated as a class — including bare
|
|
325
|
+
* semantic names like `file-svelte` which UnoCSS expands to the configured
|
|
326
|
+
* icon collection via `iconShortcuts(DEFAULT_ICONS, …)`. Single characters
|
|
327
|
+
* (kanji, emoji, single letters) render as literal text.
|
|
383
328
|
*
|
|
384
|
-
* @param {unknown} value
|
|
385
|
-
* @param {ColorSpace} [space]
|
|
386
|
-
* @returns {unknown}
|
|
387
|
-
*/
|
|
388
|
-
export function colorToRgb(value, space) {
|
|
389
|
-
if (typeof value !== 'string') return value
|
|
390
|
-
if (/^#[0-9a-fA-F]{6}$/.test(value)) {
|
|
391
|
-
return space ? hexToComponents(value, space) : hex2rgb(value)
|
|
392
|
-
}
|
|
393
|
-
// oklch(...) — extract inner components so the var can be used in oklch(var(--x) / alpha)
|
|
394
|
-
const oklchMatch = value.match(/^oklch\(\s*([^)]+?)\s*\)$/)
|
|
395
|
-
if (oklchMatch) return oklchMatch[1]
|
|
396
|
-
return value
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Detects whether an icon value is a CSS class (i-*) or literal text (kanji, emoji, etc.)
|
|
402
329
|
* @param {string | null | undefined} icon
|
|
403
|
-
* @returns {boolean} true if icon
|
|
330
|
+
* @returns {boolean} true if icon should render as a CSS class
|
|
404
331
|
*/
|
|
405
332
|
export function isIconClass(icon) {
|
|
406
333
|
if (!icon || typeof icon !== 'string') return false
|
|
407
|
-
|
|
334
|
+
// Spread to count graphemes, not UTF-16 code units, so multi-codepoint
|
|
335
|
+
// emoji (e.g. 👨👩👧👦) and single CJK characters don't get misread.
|
|
336
|
+
return [...icon].length > 1
|
|
408
337
|
}
|
|
409
338
|
|
|
410
339
|
/**
|