@oxyhq/bloom 0.40.0 → 0.42.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/lib/commonjs/theme/BloomThemeProvider.js +18 -7
- package/lib/commonjs/theme/BloomThemeProvider.js.map +1 -1
- package/lib/commonjs/theme/apply-dark-class.js +2 -2
- package/lib/commonjs/theme/apply-dark-class.js.map +1 -1
- package/lib/commonjs/theme/build-theme-from-seed.js +17 -5
- package/lib/commonjs/theme/build-theme-from-seed.js.map +1 -1
- package/lib/commonjs/theme/build-theme.js +9 -6
- package/lib/commonjs/theme/build-theme.js.map +1 -1
- package/lib/commonjs/theme/color-engine/index.js +8 -1
- package/lib/commonjs/theme/color-engine/index.js.map +1 -1
- package/lib/commonjs/theme/color-engine/scheme-variants.js +62 -15
- package/lib/commonjs/theme/color-engine/scheme-variants.js.map +1 -1
- package/lib/commonjs/theme/color-engine/seed-from-image.js +11 -1
- package/lib/commonjs/theme/color-engine/seed-from-image.js.map +1 -1
- package/lib/commonjs/theme/color-presets.js +13 -13
- package/lib/commonjs/theme/color-presets.js.map +1 -1
- package/lib/commonjs/theme/color-scope/seed-scope.js +4 -1
- package/lib/commonjs/theme/color-scope/seed-scope.js.map +1 -1
- package/lib/commonjs/theme/color-scope/style-builder.js +2 -2
- package/lib/commonjs/theme/color-scope/style-builder.js.map +1 -1
- package/lib/commonjs/theme/index.js.map +1 -1
- package/lib/commonjs/theme/native-root-vars.js +1 -1
- package/lib/commonjs/theme/native-root-vars.js.map +1 -1
- package/lib/commonjs/theme/native-root-vars.native.js +2 -2
- package/lib/commonjs/theme/native-root-vars.native.js.map +1 -1
- package/lib/commonjs/theme/native-root-vars.web.js +1 -1
- package/lib/commonjs/theme/native-root-vars.web.js.map +1 -1
- package/lib/commonjs/theme/preset-vars.js +16 -2
- package/lib/commonjs/theme/preset-vars.js.map +1 -1
- package/lib/commonjs/theme/token-registry.js +2 -2
- package/lib/commonjs/theme/token-registry.js.map +1 -1
- package/lib/module/theme/BloomThemeProvider.js +18 -7
- package/lib/module/theme/BloomThemeProvider.js.map +1 -1
- package/lib/module/theme/apply-dark-class.js +2 -2
- package/lib/module/theme/apply-dark-class.js.map +1 -1
- package/lib/module/theme/build-theme-from-seed.js +18 -5
- package/lib/module/theme/build-theme-from-seed.js.map +1 -1
- package/lib/module/theme/build-theme.js +9 -6
- package/lib/module/theme/build-theme.js.map +1 -1
- package/lib/module/theme/color-engine/index.js +8 -1
- package/lib/module/theme/color-engine/index.js.map +1 -1
- package/lib/module/theme/color-engine/scheme-variants.js +62 -15
- package/lib/module/theme/color-engine/scheme-variants.js.map +1 -1
- package/lib/module/theme/color-engine/seed-from-image.js +11 -1
- package/lib/module/theme/color-engine/seed-from-image.js.map +1 -1
- package/lib/module/theme/color-presets.js +13 -13
- package/lib/module/theme/color-presets.js.map +1 -1
- package/lib/module/theme/color-scope/seed-scope.js +4 -1
- package/lib/module/theme/color-scope/seed-scope.js.map +1 -1
- package/lib/module/theme/color-scope/style-builder.js +2 -2
- package/lib/module/theme/color-scope/style-builder.js.map +1 -1
- package/lib/module/theme/index.js.map +1 -1
- package/lib/module/theme/native-root-vars.js +1 -1
- package/lib/module/theme/native-root-vars.js.map +1 -1
- package/lib/module/theme/native-root-vars.native.js +2 -2
- package/lib/module/theme/native-root-vars.native.js.map +1 -1
- package/lib/module/theme/native-root-vars.web.js +1 -1
- package/lib/module/theme/native-root-vars.web.js.map +1 -1
- package/lib/module/theme/preset-vars.js +17 -2
- package/lib/module/theme/preset-vars.js.map +1 -1
- package/lib/module/theme/token-registry.js +2 -2
- package/lib/module/theme/token-registry.js.map +1 -1
- package/lib/typescript/commonjs/theme/BloomThemeProvider.d.ts +15 -1
- package/lib/typescript/commonjs/theme/BloomThemeProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/apply-dark-class.d.ts +2 -1
- package/lib/typescript/commonjs/theme/apply-dark-class.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/build-theme-from-seed.d.ts +13 -2
- package/lib/typescript/commonjs/theme/build-theme-from-seed.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/build-theme.d.ts +2 -1
- package/lib/typescript/commonjs/theme/build-theme.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-engine/index.d.ts +17 -1
- package/lib/typescript/commonjs/theme/color-engine/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-engine/scheme-variants.d.ts +34 -7
- package/lib/typescript/commonjs/theme/color-engine/scheme-variants.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-engine/seed-from-image.d.ts +11 -1
- package/lib/typescript/commonjs/theme/color-engine/seed-from-image.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-presets.d.ts +14 -0
- package/lib/typescript/commonjs/theme/color-presets.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-scope/seed-scope.d.ts +15 -2
- package/lib/typescript/commonjs/theme/color-scope/seed-scope.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-scope/style-builder.d.ts +2 -1
- package/lib/typescript/commonjs/theme/color-scope/style-builder.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/index.d.ts +3 -1
- package/lib/typescript/commonjs/theme/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/native-root-vars.d.ts +2 -1
- package/lib/typescript/commonjs/theme/native-root-vars.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/native-root-vars.native.d.ts +2 -1
- package/lib/typescript/commonjs/theme/native-root-vars.native.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/native-root-vars.web.d.ts +2 -1
- package/lib/typescript/commonjs/theme/native-root-vars.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/preset-vars.d.ts +12 -1
- package/lib/typescript/commonjs/theme/preset-vars.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/token-registry.d.ts +2 -1
- package/lib/typescript/commonjs/theme/token-registry.d.ts.map +1 -1
- package/lib/typescript/module/theme/BloomThemeProvider.d.ts +15 -1
- package/lib/typescript/module/theme/BloomThemeProvider.d.ts.map +1 -1
- package/lib/typescript/module/theme/apply-dark-class.d.ts +2 -1
- package/lib/typescript/module/theme/apply-dark-class.d.ts.map +1 -1
- package/lib/typescript/module/theme/build-theme-from-seed.d.ts +13 -2
- package/lib/typescript/module/theme/build-theme-from-seed.d.ts.map +1 -1
- package/lib/typescript/module/theme/build-theme.d.ts +2 -1
- package/lib/typescript/module/theme/build-theme.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-engine/index.d.ts +17 -1
- package/lib/typescript/module/theme/color-engine/index.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-engine/scheme-variants.d.ts +34 -7
- package/lib/typescript/module/theme/color-engine/scheme-variants.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-engine/seed-from-image.d.ts +11 -1
- package/lib/typescript/module/theme/color-engine/seed-from-image.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-presets.d.ts +14 -0
- package/lib/typescript/module/theme/color-presets.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-scope/seed-scope.d.ts +15 -2
- package/lib/typescript/module/theme/color-scope/seed-scope.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-scope/style-builder.d.ts +2 -1
- package/lib/typescript/module/theme/color-scope/style-builder.d.ts.map +1 -1
- package/lib/typescript/module/theme/index.d.ts +3 -1
- package/lib/typescript/module/theme/index.d.ts.map +1 -1
- package/lib/typescript/module/theme/native-root-vars.d.ts +2 -1
- package/lib/typescript/module/theme/native-root-vars.d.ts.map +1 -1
- package/lib/typescript/module/theme/native-root-vars.native.d.ts +2 -1
- package/lib/typescript/module/theme/native-root-vars.native.d.ts.map +1 -1
- package/lib/typescript/module/theme/native-root-vars.web.d.ts +2 -1
- package/lib/typescript/module/theme/native-root-vars.web.d.ts.map +1 -1
- package/lib/typescript/module/theme/preset-vars.d.ts +12 -1
- package/lib/typescript/module/theme/preset-vars.d.ts.map +1 -1
- package/lib/typescript/module/theme/token-registry.d.ts +2 -1
- package/lib/typescript/module/theme/token-registry.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/theme.test.ts +3 -0
- package/src/theme/BloomThemeProvider.tsx +38 -8
- package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +74 -74
- package/src/theme/__tests__/__snapshots__/visual-gallery.test.tsx.snap +68 -68
- package/src/theme/__tests__/explicit-accents.test.ts +129 -0
- package/src/theme/apply-dark-class.ts +7 -2
- package/src/theme/build-theme-from-seed.ts +25 -2
- package/src/theme/build-theme.ts +9 -3
- package/src/theme/color-engine/index.ts +27 -1
- package/src/theme/color-engine/scheme-variants.ts +131 -24
- package/src/theme/color-engine/seed-from-image.ts +11 -1
- package/src/theme/color-presets.ts +27 -13
- package/src/theme/color-scope/seed-scope.ts +18 -2
- package/src/theme/color-scope/style-builder.ts +3 -1
- package/src/theme/index.ts +3 -1
- package/src/theme/native-root-vars.native.ts +7 -2
- package/src/theme/native-root-vars.ts +6 -1
- package/src/theme/native-root-vars.web.ts +6 -1
- package/src/theme/preset-vars.ts +19 -0
- package/src/theme/token-registry.ts +3 -2
|
@@ -11,7 +11,7 @@ export { Hct } from './hct';
|
|
|
11
11
|
export { TonalPalette } from './tonal-palette';
|
|
12
12
|
export { DynamicScheme } from './dynamic-scheme';
|
|
13
13
|
export { buildScheme, schemeVibrant, schemeExpressive, schemeTonalSpot, schemeNeutral } from './scheme-variants';
|
|
14
|
-
export type { SchemeVariant } from './scheme-variants';
|
|
14
|
+
export type { SchemeVariant, AccentSources } from './scheme-variants';
|
|
15
15
|
export { Roles } from './color-roles';
|
|
16
16
|
export type { RoleName } from './color-roles';
|
|
17
17
|
export { ColorRole } from './color-role';
|
|
@@ -51,11 +51,27 @@ export interface GenerateOptions {
|
|
|
51
51
|
isDark?: boolean;
|
|
52
52
|
/** −1 (low) … 0 (normal) … 1 (high). */
|
|
53
53
|
contrastLevel?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Optional explicit secondary-accent seed, `#rrggbb`. When set, the secondary
|
|
56
|
+
* palette is pinned to THIS colour's hue + chroma (M3 role tones still apply)
|
|
57
|
+
* instead of the variant's derived hue-rotation of the primary seed. Omit for
|
|
58
|
+
* the current derived behaviour.
|
|
59
|
+
*/
|
|
60
|
+
secondarySeed?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Optional explicit tertiary-accent seed, `#rrggbb`. Same semantics as
|
|
63
|
+
* {@link GenerateOptions.secondarySeed} for the tertiary palette.
|
|
64
|
+
*/
|
|
65
|
+
tertiarySeed?: string;
|
|
54
66
|
}
|
|
55
67
|
|
|
56
68
|
/**
|
|
57
69
|
* Generate the full role set from ANY seed colour — the dynamic-theming entry
|
|
58
70
|
* point. Presets are just fixed seeds; a user-picked colour works identically.
|
|
71
|
+
*
|
|
72
|
+
* `secondarySeed` / `tertiarySeed` optionally PIN those accent families to real
|
|
73
|
+
* brand colours (e.g. a blue-primary brand with a yellow secondary); omitted, the
|
|
74
|
+
* accents are derived as the variant's hue-rotations — byte-identical to before.
|
|
59
75
|
*/
|
|
60
76
|
export function generateRoleColors(opts: GenerateOptions): RoleColors {
|
|
61
77
|
const scheme = buildScheme(
|
|
@@ -63,6 +79,16 @@ export function generateRoleColors(opts: GenerateOptions): RoleColors {
|
|
|
63
79
|
Hct.fromInt(argbFromHex(opts.seed)),
|
|
64
80
|
opts.isDark ?? false,
|
|
65
81
|
opts.contrastLevel ?? 0,
|
|
82
|
+
{
|
|
83
|
+
secondarySource:
|
|
84
|
+
opts.secondarySeed !== undefined
|
|
85
|
+
? Hct.fromInt(argbFromHex(opts.secondarySeed))
|
|
86
|
+
: undefined,
|
|
87
|
+
tertiarySource:
|
|
88
|
+
opts.tertiarySeed !== undefined
|
|
89
|
+
? Hct.fromInt(argbFromHex(opts.tertiarySeed))
|
|
90
|
+
: undefined,
|
|
91
|
+
},
|
|
66
92
|
);
|
|
67
93
|
const out = {} as RoleColors;
|
|
68
94
|
for (const name of Object.keys(Roles) as RoleName[]) {
|
|
@@ -13,7 +13,30 @@ import { sanitizeDegreesDouble } from './math-utils';
|
|
|
13
13
|
import { TonalPalette } from './tonal-palette';
|
|
14
14
|
import { Variant } from './variant';
|
|
15
15
|
|
|
16
|
-
export type SchemeVariant = 'vibrant' | 'expressive' | 'tonalSpot' | 'neutral';
|
|
16
|
+
export type SchemeVariant = 'vivid' | 'vibrant' | 'expressive' | 'tonalSpot' | 'neutral';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Optional explicit accent seeds for a scheme. When a source HCT is supplied, its
|
|
20
|
+
* palette is built from that colour itself (keeping the brand hue + chroma) instead
|
|
21
|
+
* of the derived hue-rotation — so a brand with a distinct secondary/tertiary accent
|
|
22
|
+
* (e.g. blue primary + yellow secondary) can PIN its real accent family. The M3 role
|
|
23
|
+
* tones still apply on top (a pinned yellow yields a yellow-hued secondary family:
|
|
24
|
+
* light container, dark on-colour) — that is correct M3 behaviour.
|
|
25
|
+
*/
|
|
26
|
+
export interface AccentSources {
|
|
27
|
+
secondarySource?: Hct;
|
|
28
|
+
tertiarySource?: Hct;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build the accent tonal palette for a role: from the pinned brand colour when a
|
|
33
|
+
* `source` is provided (preserving its hue + chroma), otherwise from the caller's
|
|
34
|
+
* derived rotation. Uses `TonalPalette.fromInt` so the palette keeps the exact
|
|
35
|
+
* brand key colour; the role tones apply the same way for both.
|
|
36
|
+
*/
|
|
37
|
+
function accentPalette(source: Hct | undefined, derived: () => TonalPalette): TonalPalette {
|
|
38
|
+
return source !== undefined ? TonalPalette.fromInt(source.toInt()) : derived();
|
|
39
|
+
}
|
|
17
40
|
|
|
18
41
|
const VIBRANT_HUES = [0.0, 41.0, 61.0, 101.0, 131.0, 181.0, 251.0, 301.0, 360.0];
|
|
19
42
|
const VIBRANT_SECONDARY_ROTATIONS = [18.0, 15.0, 10.0, 12.0, 15.0, 18.0, 15.0, 12.0, 12.0];
|
|
@@ -24,20 +47,29 @@ const EXPRESSIVE_SECONDARY_ROTATIONS = [45.0, 95.0, 45.0, 20.0, 45.0, 90.0, 45.0
|
|
|
24
47
|
const EXPRESSIVE_TERTIARY_ROTATIONS = [120.0, 120.0, 20.0, 45.0, 20.0, 15.0, 20.0, 120.0, 120.0];
|
|
25
48
|
|
|
26
49
|
/** Maxed-out colourfulness at each position of the primary palette. */
|
|
27
|
-
export function schemeVibrant(
|
|
50
|
+
export function schemeVibrant(
|
|
51
|
+
source: Hct,
|
|
52
|
+
isDark: boolean,
|
|
53
|
+
contrastLevel: number,
|
|
54
|
+
accents: AccentSources = {},
|
|
55
|
+
): DynamicScheme {
|
|
28
56
|
return new DynamicScheme({
|
|
29
57
|
sourceColorArgb: source.toInt(),
|
|
30
58
|
variant: Variant.VIBRANT,
|
|
31
59
|
contrastLevel,
|
|
32
60
|
isDark,
|
|
33
61
|
primaryPalette: TonalPalette.fromHueAndChroma(source.hue, 200.0),
|
|
34
|
-
secondaryPalette:
|
|
35
|
-
|
|
36
|
-
|
|
62
|
+
secondaryPalette: accentPalette(accents.secondarySource, () =>
|
|
63
|
+
TonalPalette.fromHueAndChroma(
|
|
64
|
+
DynamicScheme.getRotatedHue(source, VIBRANT_HUES, VIBRANT_SECONDARY_ROTATIONS),
|
|
65
|
+
24.0,
|
|
66
|
+
),
|
|
37
67
|
),
|
|
38
|
-
tertiaryPalette:
|
|
39
|
-
|
|
40
|
-
|
|
68
|
+
tertiaryPalette: accentPalette(accents.tertiarySource, () =>
|
|
69
|
+
TonalPalette.fromHueAndChroma(
|
|
70
|
+
DynamicScheme.getRotatedHue(source, VIBRANT_HUES, VIBRANT_TERTIARY_ROTATIONS),
|
|
71
|
+
32.0,
|
|
72
|
+
),
|
|
41
73
|
),
|
|
42
74
|
neutralPalette: TonalPalette.fromHueAndChroma(source.hue, 10.0),
|
|
43
75
|
neutralVariantPalette: TonalPalette.fromHueAndChroma(source.hue, 12.0),
|
|
@@ -45,20 +77,66 @@ export function schemeVibrant(source: Hct, isDark: boolean, contrastLevel: numbe
|
|
|
45
77
|
}
|
|
46
78
|
|
|
47
79
|
/** Playful, intentionally detached from the seed hue (primary rotated 240°). */
|
|
48
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Bloom's own brand-vivid scheme. Same vibrant base (max-chroma primary) but the
|
|
82
|
+
* secondary and tertiary accents keep MUCH more chroma than canonical M3 (which
|
|
83
|
+
* intentionally desaturates supporting accents). Neutrals stay low-chroma so
|
|
84
|
+
* surfaces read as clean tinted light/dark. This is what the presets use — the
|
|
85
|
+
* old hand-authored palette felt more vivid than pure M3, and this restores that
|
|
86
|
+
* across the whole primary/secondary/tertiary trio.
|
|
87
|
+
*/
|
|
88
|
+
export function schemeVivid(
|
|
89
|
+
source: Hct,
|
|
90
|
+
isDark: boolean,
|
|
91
|
+
contrastLevel: number,
|
|
92
|
+
accents: AccentSources = {},
|
|
93
|
+
): DynamicScheme {
|
|
94
|
+
return new DynamicScheme({
|
|
95
|
+
sourceColorArgb: source.toInt(),
|
|
96
|
+
variant: Variant.VIBRANT,
|
|
97
|
+
contrastLevel,
|
|
98
|
+
isDark,
|
|
99
|
+
primaryPalette: TonalPalette.fromHueAndChroma(source.hue, 200.0),
|
|
100
|
+
secondaryPalette: accentPalette(accents.secondarySource, () =>
|
|
101
|
+
TonalPalette.fromHueAndChroma(
|
|
102
|
+
DynamicScheme.getRotatedHue(source, VIBRANT_HUES, VIBRANT_SECONDARY_ROTATIONS),
|
|
103
|
+
64.0,
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
tertiaryPalette: accentPalette(accents.tertiarySource, () =>
|
|
107
|
+
TonalPalette.fromHueAndChroma(
|
|
108
|
+
DynamicScheme.getRotatedHue(source, VIBRANT_HUES, VIBRANT_TERTIARY_ROTATIONS),
|
|
109
|
+
72.0,
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
neutralPalette: TonalPalette.fromHueAndChroma(source.hue, 10.0),
|
|
113
|
+
neutralVariantPalette: TonalPalette.fromHueAndChroma(source.hue, 12.0),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function schemeExpressive(
|
|
118
|
+
source: Hct,
|
|
119
|
+
isDark: boolean,
|
|
120
|
+
contrastLevel: number,
|
|
121
|
+
accents: AccentSources = {},
|
|
122
|
+
): DynamicScheme {
|
|
49
123
|
return new DynamicScheme({
|
|
50
124
|
sourceColorArgb: source.toInt(),
|
|
51
125
|
variant: Variant.EXPRESSIVE,
|
|
52
126
|
contrastLevel,
|
|
53
127
|
isDark,
|
|
54
128
|
primaryPalette: TonalPalette.fromHueAndChroma(sanitizeDegreesDouble(source.hue + 240.0), 40.0),
|
|
55
|
-
secondaryPalette:
|
|
56
|
-
|
|
57
|
-
|
|
129
|
+
secondaryPalette: accentPalette(accents.secondarySource, () =>
|
|
130
|
+
TonalPalette.fromHueAndChroma(
|
|
131
|
+
DynamicScheme.getRotatedHue(source, EXPRESSIVE_HUES, EXPRESSIVE_SECONDARY_ROTATIONS),
|
|
132
|
+
24.0,
|
|
133
|
+
),
|
|
58
134
|
),
|
|
59
|
-
tertiaryPalette:
|
|
60
|
-
|
|
61
|
-
|
|
135
|
+
tertiaryPalette: accentPalette(accents.tertiarySource, () =>
|
|
136
|
+
TonalPalette.fromHueAndChroma(
|
|
137
|
+
DynamicScheme.getRotatedHue(source, EXPRESSIVE_HUES, EXPRESSIVE_TERTIARY_ROTATIONS),
|
|
138
|
+
32.0,
|
|
139
|
+
),
|
|
62
140
|
),
|
|
63
141
|
neutralPalette: TonalPalette.fromHueAndChroma(source.hue + 15, 8.0),
|
|
64
142
|
neutralVariantPalette: TonalPalette.fromHueAndChroma(source.hue + 15, 12.0),
|
|
@@ -66,48 +144,77 @@ export function schemeExpressive(source: Hct, isDark: boolean, contrastLevel: nu
|
|
|
66
144
|
}
|
|
67
145
|
|
|
68
146
|
/** The Material You default — a calm, balanced treatment of the seed. */
|
|
69
|
-
export function schemeTonalSpot(
|
|
147
|
+
export function schemeTonalSpot(
|
|
148
|
+
source: Hct,
|
|
149
|
+
isDark: boolean,
|
|
150
|
+
contrastLevel: number,
|
|
151
|
+
accents: AccentSources = {},
|
|
152
|
+
): DynamicScheme {
|
|
70
153
|
return new DynamicScheme({
|
|
71
154
|
sourceColorArgb: source.toInt(),
|
|
72
155
|
variant: Variant.TONAL_SPOT,
|
|
73
156
|
contrastLevel,
|
|
74
157
|
isDark,
|
|
75
158
|
primaryPalette: TonalPalette.fromHueAndChroma(source.hue, 36.0),
|
|
76
|
-
secondaryPalette:
|
|
77
|
-
|
|
159
|
+
secondaryPalette: accentPalette(accents.secondarySource, () =>
|
|
160
|
+
TonalPalette.fromHueAndChroma(source.hue, 16.0),
|
|
161
|
+
),
|
|
162
|
+
tertiaryPalette: accentPalette(accents.tertiarySource, () =>
|
|
163
|
+
TonalPalette.fromHueAndChroma(sanitizeDegreesDouble(source.hue + 60.0), 24.0),
|
|
164
|
+
),
|
|
78
165
|
neutralPalette: TonalPalette.fromHueAndChroma(source.hue, 6.0),
|
|
79
166
|
neutralVariantPalette: TonalPalette.fromHueAndChroma(source.hue, 8.0),
|
|
80
167
|
});
|
|
81
168
|
}
|
|
82
169
|
|
|
83
170
|
/** Near-greyscale — the seed hue is present only as a whisper. */
|
|
84
|
-
export function schemeNeutral(
|
|
171
|
+
export function schemeNeutral(
|
|
172
|
+
source: Hct,
|
|
173
|
+
isDark: boolean,
|
|
174
|
+
contrastLevel: number,
|
|
175
|
+
accents: AccentSources = {},
|
|
176
|
+
): DynamicScheme {
|
|
85
177
|
return new DynamicScheme({
|
|
86
178
|
sourceColorArgb: source.toInt(),
|
|
87
179
|
variant: Variant.NEUTRAL,
|
|
88
180
|
contrastLevel,
|
|
89
181
|
isDark,
|
|
90
182
|
primaryPalette: TonalPalette.fromHueAndChroma(source.hue, 12.0),
|
|
91
|
-
secondaryPalette:
|
|
92
|
-
|
|
183
|
+
secondaryPalette: accentPalette(accents.secondarySource, () =>
|
|
184
|
+
TonalPalette.fromHueAndChroma(source.hue, 8.0),
|
|
185
|
+
),
|
|
186
|
+
tertiaryPalette: accentPalette(accents.tertiarySource, () =>
|
|
187
|
+
TonalPalette.fromHueAndChroma(source.hue, 16.0),
|
|
188
|
+
),
|
|
93
189
|
neutralPalette: TonalPalette.fromHueAndChroma(source.hue, 2.0),
|
|
94
190
|
neutralVariantPalette: TonalPalette.fromHueAndChroma(source.hue, 2.0),
|
|
95
191
|
});
|
|
96
192
|
}
|
|
97
193
|
|
|
98
|
-
const BUILDERS: Record<
|
|
194
|
+
const BUILDERS: Record<
|
|
195
|
+
SchemeVariant,
|
|
196
|
+
(s: Hct, d: boolean, c: number, accents: AccentSources) => DynamicScheme
|
|
197
|
+
> = {
|
|
198
|
+
vivid: schemeVivid,
|
|
99
199
|
vibrant: schemeVibrant,
|
|
100
200
|
expressive: schemeExpressive,
|
|
101
201
|
tonalSpot: schemeTonalSpot,
|
|
102
202
|
neutral: schemeNeutral,
|
|
103
203
|
};
|
|
104
204
|
|
|
105
|
-
/**
|
|
205
|
+
/**
|
|
206
|
+
* Build a scheme by variant name.
|
|
207
|
+
*
|
|
208
|
+
* `accents` optionally pins the secondary and/or tertiary palette to an explicit
|
|
209
|
+
* brand colour (see {@link AccentSources}); omitted accents keep the variant's
|
|
210
|
+
* derived hue-rotation, so existing callers get byte-identical output.
|
|
211
|
+
*/
|
|
106
212
|
export function buildScheme(
|
|
107
213
|
variant: SchemeVariant,
|
|
108
214
|
source: Hct,
|
|
109
215
|
isDark: boolean,
|
|
110
216
|
contrastLevel = 0,
|
|
217
|
+
accents: AccentSources = {},
|
|
111
218
|
): DynamicScheme {
|
|
112
|
-
return BUILDERS[variant](source, isDark, contrastLevel);
|
|
219
|
+
return BUILDERS[variant](source, isDark, contrastLevel, accents);
|
|
113
220
|
}
|
|
@@ -51,7 +51,17 @@ export function quantizeImage(pixels: number[], maxColors = 128): Map<number, nu
|
|
|
51
51
|
return result;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Ranked seed candidates (best first) extracted from an image's pixels (ARGB).
|
|
56
|
+
*
|
|
57
|
+
* The top result (`seeds[0]`) is the primary seed for `generateRoleColors({ seed })`.
|
|
58
|
+
* To derive a richer, brand-accurate theme from artwork, a caller can additionally
|
|
59
|
+
* pin distinct accents from the ranked list: pass `hexFromArgb(seeds[1])` as
|
|
60
|
+
* `secondarySeed` and `hexFromArgb(seeds[2])` as `tertiarySeed` to
|
|
61
|
+
* `generateRoleColors` / `buildSeedScopeVars` / `buildThemeFromSeed` so the
|
|
62
|
+
* secondary and tertiary families reflect the artwork's real supporting colours
|
|
63
|
+
* instead of hue-rotations of the primary.
|
|
64
|
+
*/
|
|
55
65
|
export function seedsFromImagePixels(pixels: number[], options?: ScoreOptions): number[] {
|
|
56
66
|
return score(quantizeImage(pixels), options);
|
|
57
67
|
}
|
|
@@ -32,6 +32,20 @@ export interface AppColorPreset {
|
|
|
32
32
|
hex: string;
|
|
33
33
|
/** The tonal scheme variant the engine derives roles with. */
|
|
34
34
|
variant: SchemeVariant;
|
|
35
|
+
/**
|
|
36
|
+
* Optional explicit secondary-accent seed, `#rrggbb`. When set, the secondary
|
|
37
|
+
* palette is PINNED to this brand colour (hue + chroma preserved; M3 role tones
|
|
38
|
+
* still apply) instead of the derived hue-rotation of {@link AppColorPreset.hex}.
|
|
39
|
+
* Bloom's built-in presets do NOT set this, so their generated output is
|
|
40
|
+
* unchanged; a consumer brand with a distinct accent (blue primary + yellow
|
|
41
|
+
* secondary) sets it to pin its real accent family.
|
|
42
|
+
*/
|
|
43
|
+
secondaryHex?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Optional explicit tertiary-accent seed, `#rrggbb`. Same semantics as
|
|
46
|
+
* {@link AppColorPreset.secondaryHex} for the tertiary palette.
|
|
47
|
+
*/
|
|
48
|
+
tertiaryHex?: string;
|
|
35
49
|
}
|
|
36
50
|
|
|
37
51
|
/**
|
|
@@ -74,17 +88,17 @@ export function hexToAppColorName(hex: string): AppColorName {
|
|
|
74
88
|
* bold `vibrant` derivation to match the prior saturated palette.
|
|
75
89
|
*/
|
|
76
90
|
export const APP_COLOR_PRESETS: Record<AppColorName, AppColorPreset> = {
|
|
77
|
-
teal: { name: 'teal', hex: '#005c67', variant: '
|
|
78
|
-
blue: { name: 'blue', hex: '#1d9bf0', variant: '
|
|
79
|
-
green: { name: 'green', hex: '#10b981', variant: '
|
|
80
|
-
amber: { name: 'amber', hex: '#f59e0b', variant: '
|
|
81
|
-
yellow: { name: 'yellow', hex: '#ffc300', variant: '
|
|
82
|
-
red: { name: 'red', hex: '#ef4444', variant: '
|
|
83
|
-
purple: { name: 'purple', hex: '#8b5cf6', variant: '
|
|
84
|
-
pink: { name: 'pink', hex: '#ec4899', variant: '
|
|
85
|
-
sky: { name: 'sky', hex: '#0ea5e9', variant: '
|
|
86
|
-
orange: { name: 'orange', hex: '#f97316', variant: '
|
|
87
|
-
mint: { name: 'mint', hex: '#14b8a6', variant: '
|
|
88
|
-
oxy: { name: 'oxy', hex: '#c46ede', variant: '
|
|
89
|
-
faircoin: { name: 'faircoin', hex: '#9ffb50', variant: '
|
|
91
|
+
teal: { name: 'teal', hex: '#005c67', variant: 'vivid' },
|
|
92
|
+
blue: { name: 'blue', hex: '#1d9bf0', variant: 'vivid' },
|
|
93
|
+
green: { name: 'green', hex: '#10b981', variant: 'vivid' },
|
|
94
|
+
amber: { name: 'amber', hex: '#f59e0b', variant: 'vivid' },
|
|
95
|
+
yellow: { name: 'yellow', hex: '#ffc300', variant: 'vivid' },
|
|
96
|
+
red: { name: 'red', hex: '#ef4444', variant: 'vivid' },
|
|
97
|
+
purple: { name: 'purple', hex: '#8b5cf6', variant: 'vivid' },
|
|
98
|
+
pink: { name: 'pink', hex: '#ec4899', variant: 'vivid' },
|
|
99
|
+
sky: { name: 'sky', hex: '#0ea5e9', variant: 'vivid' },
|
|
100
|
+
orange: { name: 'orange', hex: '#f97316', variant: 'vivid' },
|
|
101
|
+
mint: { name: 'mint', hex: '#14b8a6', variant: 'vivid' },
|
|
102
|
+
oxy: { name: 'oxy', hex: '#c46ede', variant: 'vivid' },
|
|
103
|
+
faircoin: { name: 'faircoin', hex: '#9ffb50', variant: 'vivid' },
|
|
90
104
|
};
|
|
@@ -65,12 +65,25 @@ export interface SeedScopeOptions {
|
|
|
65
65
|
seed: string;
|
|
66
66
|
mode: 'light' | 'dark';
|
|
67
67
|
/**
|
|
68
|
-
* The tonal scheme variant. Defaults to `'
|
|
69
|
-
* named Bloom preset uses, so a preset's seed reproduces that preset exactly
|
|
68
|
+
* The tonal scheme variant. Defaults to `'vivid'` — the same variant every
|
|
69
|
+
* named Bloom preset uses, so a preset's seed reproduces that preset exactly
|
|
70
|
+
* (and an artwork-extracted seed gets the same brand-vivid feel as the app).
|
|
70
71
|
*/
|
|
71
72
|
variant?: SchemeVariant;
|
|
72
73
|
/** −1 (low) … 0 (normal) … 1 (high). Defaults to 0. */
|
|
73
74
|
contrastLevel?: number;
|
|
75
|
+
/**
|
|
76
|
+
* Optional explicit secondary-accent seed, `#rrggbb`. Pins the secondary palette
|
|
77
|
+
* to this brand colour instead of the derived rotation (M3 role tones still
|
|
78
|
+
* apply). Handy for an artwork-extracted theme: pass the second-ranked seed from
|
|
79
|
+
* `seedsFromImagePixels` (`seeds[1]`) as a distinct secondary accent.
|
|
80
|
+
*/
|
|
81
|
+
secondarySeed?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Optional explicit tertiary-accent seed, `#rrggbb`. Same semantics; pass
|
|
84
|
+
* `seedsFromImagePixels`' `seeds[2]` for a third artwork-derived accent.
|
|
85
|
+
*/
|
|
86
|
+
tertiarySeed?: string;
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
/**
|
|
@@ -88,8 +101,11 @@ export interface SeedScopeOptions {
|
|
|
88
101
|
export function buildSeedScopeVars(options: SeedScopeOptions): Record<string, string> {
|
|
89
102
|
const roles = generateRoleColors({
|
|
90
103
|
seed: options.seed,
|
|
104
|
+
variant: options.variant ?? 'vivid',
|
|
91
105
|
isDark: options.mode === 'dark',
|
|
92
106
|
contrastLevel: options.contrastLevel ?? 0,
|
|
107
|
+
secondarySeed: options.secondarySeed,
|
|
108
|
+
tertiarySeed: options.tertiarySeed,
|
|
93
109
|
});
|
|
94
110
|
const tokens = roleColorsToPresetTokens(roles);
|
|
95
111
|
const vars: Record<string, string> = { ...tokens };
|
|
@@ -3,6 +3,7 @@ import { Platform, type StyleProp, type ViewStyle } from 'react-native';
|
|
|
3
3
|
|
|
4
4
|
import { CANONICAL_TOKENS, getResolvedTokens } from '../token-registry';
|
|
5
5
|
import type { AppColorName } from '../color-presets';
|
|
6
|
+
import type { ExplicitAccents } from '../preset-vars';
|
|
6
7
|
import { lazyRequire } from '../../utils/lazy-require';
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -36,8 +37,9 @@ const getNativeWindVars = lazyRequire<NativeWindVarsModule>('nativewind');
|
|
|
36
37
|
export function buildScopeVars(
|
|
37
38
|
colorPreset: AppColorName,
|
|
38
39
|
mode: 'light' | 'dark',
|
|
40
|
+
accents?: ExplicitAccents,
|
|
39
41
|
): Record<string, string> {
|
|
40
|
-
const tokens = getResolvedTokens(colorPreset, mode);
|
|
42
|
+
const tokens = getResolvedTokens(colorPreset, mode, accents);
|
|
41
43
|
const vars: Record<string, string> = { ...tokens };
|
|
42
44
|
|
|
43
45
|
for (const token of CANONICAL_TOKENS) {
|
package/src/theme/index.ts
CHANGED
|
@@ -24,9 +24,10 @@ export {
|
|
|
24
24
|
greenFromArgb,
|
|
25
25
|
blueFromArgb,
|
|
26
26
|
} from './color-engine';
|
|
27
|
-
export type { RoleColors, GenerateOptions, RoleName, SchemeVariant } from './color-engine';
|
|
27
|
+
export type { RoleColors, GenerateOptions, RoleName, SchemeVariant, AccentSources } from './color-engine';
|
|
28
28
|
export { buildTheme, STATUS_COLORS } from './build-theme';
|
|
29
29
|
export { buildThemeFromSeed, buildColorsFromSeed } from './build-theme-from-seed';
|
|
30
|
+
export type { SeedAccents } from './build-theme-from-seed';
|
|
30
31
|
export { THEME_GRADIENTS } from './gradients';
|
|
31
32
|
export { useTheme, useThemeColor, useBloomTheme } from './use-theme';
|
|
32
33
|
export { useNavigationTheme } from './use-navigation-theme';
|
|
@@ -43,6 +44,7 @@ export {
|
|
|
43
44
|
export { parseRgb, withAlpha } from './color-utils';
|
|
44
45
|
export type { RgbChannels } from './color-utils';
|
|
45
46
|
export { getPresetVars, applyPresetVarsToDocument } from './preset-vars';
|
|
47
|
+
export type { ExplicitAccents } from './preset-vars';
|
|
46
48
|
export { applyDarkClass } from './apply-dark-class';
|
|
47
49
|
export { setColorSchemeSafe } from './set-color-scheme-safe';
|
|
48
50
|
export { initCssInteropDarkMode } from './init-css-interop';
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { rootVariables } from 'react-native-css/native-internal';
|
|
18
18
|
|
|
19
19
|
import { type AppColorName } from './color-presets';
|
|
20
|
+
import type { ExplicitAccents } from './preset-vars';
|
|
20
21
|
import { getResolvedTokens } from './token-registry';
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -121,10 +122,14 @@ import { getResolvedTokens } from './token-registry';
|
|
|
121
122
|
* Defensive guard: if a host ships a react-native-css build that doesn't expose
|
|
122
123
|
* `rootVariables` as a callable (it always does on @3), we bail rather than throw.
|
|
123
124
|
*/
|
|
124
|
-
export function applyNativeRootVars(
|
|
125
|
+
export function applyNativeRootVars(
|
|
126
|
+
colorPreset: AppColorName,
|
|
127
|
+
mode: 'light' | 'dark',
|
|
128
|
+
accents?: ExplicitAccents,
|
|
129
|
+
): void {
|
|
125
130
|
if (typeof rootVariables !== 'function') return;
|
|
126
131
|
|
|
127
|
-
const vars = getResolvedTokens(colorPreset, mode);
|
|
132
|
+
const vars = getResolvedTokens(colorPreset, mode, accents);
|
|
128
133
|
for (const [key, value] of Object.entries(vars)) {
|
|
129
134
|
// The family is keyed by the bare name; `getResolvedTokens` keys include `--`.
|
|
130
135
|
const name = key.startsWith('--') ? key.slice(2) : key;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AppColorName } from './color-presets';
|
|
2
|
+
import type { ExplicitAccents } from './preset-vars';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Platform-neutral DEFAULT variant of `applyNativeRootVars` — a no-op.
|
|
@@ -30,7 +31,11 @@ import { type AppColorName } from './color-presets';
|
|
|
30
31
|
* The signature is intentionally identical to those variants so the import site in
|
|
31
32
|
* `BloomThemeProvider` is platform-agnostic.
|
|
32
33
|
*/
|
|
33
|
-
export function applyNativeRootVars(
|
|
34
|
+
export function applyNativeRootVars(
|
|
35
|
+
_colorPreset: AppColorName,
|
|
36
|
+
_mode: 'light' | 'dark',
|
|
37
|
+
_accents?: ExplicitAccents,
|
|
38
|
+
): void {
|
|
34
39
|
// Intentionally empty — the native variant (`native-root-vars.native.ts`) does
|
|
35
40
|
// the real work; this neutral default and the web fork are inert.
|
|
36
41
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AppColorName } from './color-presets';
|
|
2
|
+
import type { ExplicitAccents } from './preset-vars';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Web fork of `applyNativeRootVars` — a no-op.
|
|
@@ -17,6 +18,10 @@ import { type AppColorName } from './color-presets';
|
|
|
17
18
|
* The signature is intentionally identical to the plain variant so the import
|
|
18
19
|
* site in `BloomThemeProvider` is platform-agnostic.
|
|
19
20
|
*/
|
|
20
|
-
export function applyNativeRootVars(
|
|
21
|
+
export function applyNativeRootVars(
|
|
22
|
+
_colorPreset: AppColorName,
|
|
23
|
+
_mode: 'light' | 'dark',
|
|
24
|
+
_accents?: ExplicitAccents,
|
|
25
|
+
): void {
|
|
21
26
|
// Intentionally empty — web applies preset vars to `document.documentElement`.
|
|
22
27
|
}
|
package/src/theme/preset-vars.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { APP_COLOR_PRESETS, type AppColorName, type PresetTokens } from './color-presets';
|
|
2
2
|
import { generateRoleColors, type RoleColors } from './color-engine';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Optional explicit accent-colour overrides (`#rrggbb`) that PIN the secondary
|
|
6
|
+
* and/or tertiary palette app-wide, regardless of the active preset's own seed.
|
|
7
|
+
* Threaded from `BloomThemeProvider`'s `secondaryColor` / `tertiaryColor` props
|
|
8
|
+
* down through the whole token pipeline. When both are omitted, the preset's own
|
|
9
|
+
* (usually derived) accents are used — output is unchanged.
|
|
10
|
+
*/
|
|
11
|
+
export interface ExplicitAccents {
|
|
12
|
+
secondaryHex?: string;
|
|
13
|
+
tertiaryHex?: string;
|
|
14
|
+
}
|
|
3
15
|
// Circular at the module graph level (`token-registry` imports `getPresetVars`
|
|
4
16
|
// from here), but safe: `getResolvedTokens` is only referenced inside the
|
|
5
17
|
// `applyPresetVarsToDocument` function body — i.e. at call time, after both
|
|
@@ -26,12 +38,19 @@ import { getResolvedTokens } from './token-registry';
|
|
|
26
38
|
export function getPresetVars(
|
|
27
39
|
colorName: AppColorName,
|
|
28
40
|
mode: 'light' | 'dark',
|
|
41
|
+
accents?: ExplicitAccents,
|
|
29
42
|
): PresetTokens {
|
|
30
43
|
const preset = APP_COLOR_PRESETS[colorName];
|
|
31
44
|
const r: RoleColors = generateRoleColors({
|
|
32
45
|
seed: preset.hex,
|
|
33
46
|
variant: preset.variant,
|
|
34
47
|
isDark: mode === 'dark',
|
|
48
|
+
// Pinned accents: an explicit app-wide override (from the provider) wins,
|
|
49
|
+
// else the preset's own declared accent, else undefined → derived rotation.
|
|
50
|
+
// The built-in presets declare none, so with no override their generated
|
|
51
|
+
// tokens stay byte-identical.
|
|
52
|
+
secondarySeed: accents?.secondaryHex ?? preset.secondaryHex,
|
|
53
|
+
tertiarySeed: accents?.tertiaryHex ?? preset.tertiaryHex,
|
|
35
54
|
});
|
|
36
55
|
|
|
37
56
|
return {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { APP_COLOR_NAMES, type AppColorName } from './color-presets';
|
|
2
|
-
import { getPresetVars } from './preset-vars';
|
|
2
|
+
import { getPresetVars, type ExplicitAccents } from './preset-vars';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The canonical set of Bloom theme token names — WITHOUT the leading `--`.
|
|
@@ -149,8 +149,9 @@ export function hslToSrgb(triple: string): string {
|
|
|
149
149
|
export function getResolvedTokens(
|
|
150
150
|
preset: AppColorName,
|
|
151
151
|
mode: 'light' | 'dark',
|
|
152
|
+
accents?: ExplicitAccents,
|
|
152
153
|
): Record<string, string> {
|
|
153
|
-
const raw = getPresetVars(preset, mode);
|
|
154
|
+
const raw = getPresetVars(preset, mode, accents);
|
|
154
155
|
const resolved: Record<string, string> = {};
|
|
155
156
|
for (const [key, value] of Object.entries(raw)) {
|
|
156
157
|
resolved[key] = RAW_HSL_TRIPLE.test(value) ? hslToSrgb(value) : value;
|