@oxyhq/bloom 0.41.0 → 0.42.1
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 +42 -18
- 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.map +1 -1
- package/lib/commonjs/theme/color-scope/seed-scope.js +3 -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/seed-scope/index.js +11 -4
- package/lib/commonjs/theme/seed-scope/index.js.map +1 -1
- package/lib/commonjs/theme/seed-scope/index.web.js +11 -4
- package/lib/commonjs/theme/seed-scope/index.web.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 +43 -18
- 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.map +1 -1
- package/lib/module/theme/color-scope/seed-scope.js +3 -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/seed-scope/index.js +11 -4
- package/lib/module/theme/seed-scope/index.js.map +1 -1
- package/lib/module/theme/seed-scope/index.web.js +11 -4
- package/lib/module/theme/seed-scope/index.web.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 +25 -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 +12 -0
- 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/seed-scope/index.d.ts +9 -1
- package/lib/typescript/commonjs/theme/seed-scope/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/seed-scope/index.web.d.ts +9 -1
- package/lib/typescript/commonjs/theme/seed-scope/index.web.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 +25 -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 +12 -0
- 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/seed-scope/index.d.ts +9 -1
- package/lib/typescript/module/theme/seed-scope/index.d.ts.map +1 -1
- package/lib/typescript/module/theme/seed-scope/index.web.d.ts +9 -1
- package/lib/typescript/module/theme/seed-scope/index.web.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__/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 +108 -30
- package/src/theme/color-engine/seed-from-image.ts +11 -1
- package/src/theme/color-presets.ts +14 -0
- package/src/theme/color-scope/seed-scope.ts +14 -0
- 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/seed-scope/index.tsx +17 -4
- package/src/theme/seed-scope/index.web.tsx +15 -3
- package/src/theme/token-registry.ts +3 -2
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explicit secondary/tertiary accent pinning.
|
|
3
|
+
*
|
|
4
|
+
* A brand with a distinct accent (e.g. blue primary + yellow secondary) can PIN
|
|
5
|
+
* its real accent colours instead of the engine deriving secondary/tertiary as
|
|
6
|
+
* hue-rotations of the primary seed. These tests assert:
|
|
7
|
+
* 1. pinning shifts the secondary/tertiary family HUE toward the pinned colour
|
|
8
|
+
* (and away from the derived one), and
|
|
9
|
+
* 2. omitting the seeds is byte-identical to the pre-feature derived output.
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
argbFromHex,
|
|
13
|
+
generateRoleColors,
|
|
14
|
+
type RoleColors,
|
|
15
|
+
} from '../color-engine';
|
|
16
|
+
import { Hct } from '../color-engine/hct';
|
|
17
|
+
import { buildSeedScopeVars } from '../color-scope/seed-scope';
|
|
18
|
+
|
|
19
|
+
/** Hue (deg) of an `rgb(r g b)` engine output string. */
|
|
20
|
+
function hueOf(rgb: string): number {
|
|
21
|
+
const m = rgb.match(/rgb\((\d+) (\d+) (\d+)\)/);
|
|
22
|
+
if (!m) throw new Error(`expected "rgb(r g b)", got: ${rgb}`);
|
|
23
|
+
const argb = (0xff << 24) | (Number(m[1]) << 16) | (Number(m[2]) << 8) | Number(m[3]);
|
|
24
|
+
return Hct.fromInt(argb >>> 0).hue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Smallest absolute angular distance between two hues, in degrees (0–180). */
|
|
28
|
+
function hueDistance(a: number, b: number): number {
|
|
29
|
+
const d = Math.abs(a - b) % 360;
|
|
30
|
+
return d > 180 ? 360 - d : d;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const BLUE = '#1d9bf0';
|
|
34
|
+
const YELLOW = '#ffc300';
|
|
35
|
+
const PINK = '#ec4899';
|
|
36
|
+
|
|
37
|
+
describe('explicit accent pinning — generateRoleColors', () => {
|
|
38
|
+
const derived: RoleColors = generateRoleColors({ seed: BLUE, variant: 'vivid', isDark: false });
|
|
39
|
+
|
|
40
|
+
it('pins the secondary family to the supplied colour hue', () => {
|
|
41
|
+
const pinned = generateRoleColors({
|
|
42
|
+
seed: BLUE,
|
|
43
|
+
variant: 'vivid',
|
|
44
|
+
isDark: false,
|
|
45
|
+
secondarySeed: YELLOW,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const yellowHue = Hct.fromInt(argbFromHex(YELLOW)).hue;
|
|
49
|
+
// The pinned secondary family reads at the yellow hue…
|
|
50
|
+
expect(hueDistance(hueOf(pinned.secondary), yellowHue)).toBeLessThan(20);
|
|
51
|
+
// …and its container / on-colour roles share that hue family too.
|
|
52
|
+
expect(hueDistance(hueOf(pinned.secondaryContainer), yellowHue)).toBeLessThan(20);
|
|
53
|
+
expect(hueDistance(hueOf(pinned.onSecondaryContainer), yellowHue)).toBeLessThan(20);
|
|
54
|
+
// …which differs materially from the derived (blue-rotation) secondary.
|
|
55
|
+
expect(hueDistance(hueOf(pinned.secondary), hueOf(derived.secondary))).toBeGreaterThan(30);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('pins the tertiary family independently of the secondary', () => {
|
|
59
|
+
const pinned = generateRoleColors({
|
|
60
|
+
seed: BLUE,
|
|
61
|
+
variant: 'vivid',
|
|
62
|
+
isDark: false,
|
|
63
|
+
tertiarySeed: PINK,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const pinkHue = Hct.fromInt(argbFromHex(PINK)).hue;
|
|
67
|
+
expect(hueDistance(hueOf(pinned.tertiary), pinkHue)).toBeLessThan(20);
|
|
68
|
+
expect(hueDistance(hueOf(pinned.tertiary), hueOf(derived.tertiary))).toBeGreaterThan(20);
|
|
69
|
+
// Secondary is untouched when only tertiary is pinned.
|
|
70
|
+
expect(pinned.secondary).toBe(derived.secondary);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('pins both accents at once, leaving primary + neutrals unchanged', () => {
|
|
74
|
+
const pinned = generateRoleColors({
|
|
75
|
+
seed: BLUE,
|
|
76
|
+
variant: 'vivid',
|
|
77
|
+
isDark: false,
|
|
78
|
+
secondarySeed: YELLOW,
|
|
79
|
+
tertiarySeed: PINK,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(hueDistance(hueOf(pinned.secondary), Hct.fromInt(argbFromHex(YELLOW)).hue)).toBeLessThan(20);
|
|
83
|
+
expect(hueDistance(hueOf(pinned.tertiary), Hct.fromInt(argbFromHex(PINK)).hue)).toBeLessThan(20);
|
|
84
|
+
// Primary + surfaces are seed-driven and must not move.
|
|
85
|
+
expect(pinned.primary).toBe(derived.primary);
|
|
86
|
+
expect(pinned.background).toBe(derived.background);
|
|
87
|
+
expect(pinned.surfaceContainerLowest).toBe(derived.surfaceContainerLowest);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('is byte-identical to the derived output when no accents are supplied', () => {
|
|
91
|
+
const again = generateRoleColors({ seed: BLUE, variant: 'vivid', isDark: false });
|
|
92
|
+
expect(again).toEqual(derived);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('applies pinned accents in dark mode too', () => {
|
|
96
|
+
const pinnedDark = generateRoleColors({
|
|
97
|
+
seed: BLUE,
|
|
98
|
+
variant: 'vivid',
|
|
99
|
+
isDark: true,
|
|
100
|
+
secondarySeed: YELLOW,
|
|
101
|
+
});
|
|
102
|
+
expect(hueDistance(hueOf(pinnedDark.secondary), Hct.fromInt(argbFromHex(YELLOW)).hue)).toBeLessThan(20);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('explicit accent pinning — arbitrary-seed scope path', () => {
|
|
107
|
+
it('threads pinned accents through buildSeedScopeVars', () => {
|
|
108
|
+
const derivedVars = buildSeedScopeVars({ seed: BLUE, mode: 'light' });
|
|
109
|
+
const pinnedVars = buildSeedScopeVars({
|
|
110
|
+
seed: BLUE,
|
|
111
|
+
mode: 'light',
|
|
112
|
+
secondarySeed: YELLOW,
|
|
113
|
+
tertiarySeed: PINK,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const yellowHue = Hct.fromInt(argbFromHex(YELLOW)).hue;
|
|
117
|
+
const pinkHue = Hct.fromInt(argbFromHex(PINK)).hue;
|
|
118
|
+
|
|
119
|
+
expect(pinnedVars['--secondary']).toBeDefined();
|
|
120
|
+
expect(hueDistance(hueOf(pinnedVars['--secondary'] ?? ''), yellowHue)).toBeLessThan(20);
|
|
121
|
+
expect(hueDistance(hueOf(pinnedVars['--tertiary'] ?? ''), pinkHue)).toBeLessThan(20);
|
|
122
|
+
// The scoped `--color-*` alias tracks the pinned value too (web utility contract).
|
|
123
|
+
expect(pinnedVars['--color-secondary']).toBe(pinnedVars['--secondary']);
|
|
124
|
+
// Primary token is unchanged vs the derived scope.
|
|
125
|
+
expect(pinnedVars['--primary']).toBe(derivedVars['--primary']);
|
|
126
|
+
// Derived secondary differs from the pinned one.
|
|
127
|
+
expect(pinnedVars['--secondary']).not.toBe(derivedVars['--secondary']);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Platform } from 'react-native';
|
|
2
2
|
import { APP_COLOR_PRESETS, type AppColorName } from './color-presets';
|
|
3
|
+
import type { ExplicitAccents } from './preset-vars';
|
|
3
4
|
import { getResolvedTokens } from './token-registry';
|
|
4
5
|
|
|
5
6
|
export function applyDarkClass(resolved: 'light' | 'dark') {
|
|
@@ -25,11 +26,15 @@ export function applyDarkClass(resolved: 'light' | 'dark') {
|
|
|
25
26
|
* Includes extended tokens (card, chart-*, content-area, sidebar-*) so consumer
|
|
26
27
|
* apps don't need to synthesize them.
|
|
27
28
|
*/
|
|
28
|
-
export function applyColorPresetVars(
|
|
29
|
+
export function applyColorPresetVars(
|
|
30
|
+
preset: AppColorName,
|
|
31
|
+
resolved: 'light' | 'dark',
|
|
32
|
+
accents?: ExplicitAccents,
|
|
33
|
+
) {
|
|
29
34
|
if (Platform.OS !== 'web' || typeof document === 'undefined') return;
|
|
30
35
|
if (!APP_COLOR_PRESETS[preset]) return;
|
|
31
36
|
|
|
32
|
-
const vars = getResolvedTokens(preset, resolved);
|
|
37
|
+
const vars = getResolvedTokens(preset, resolved, accents);
|
|
33
38
|
const root = document.documentElement.style;
|
|
34
39
|
|
|
35
40
|
for (const [key, value] of Object.entries(vars)) {
|
|
@@ -4,6 +4,18 @@ import { STATUS_COLORS } from './build-theme';
|
|
|
4
4
|
import { THEME_GRADIENTS } from './gradients';
|
|
5
5
|
import type { Theme, ThemeColors } from './types';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Optional explicit accent seeds for the arbitrary-seed theme path. Pinning a
|
|
9
|
+
* `secondarySeed` / `tertiarySeed` (`#rrggbb`) forces that accent family to the
|
|
10
|
+
* given brand colour instead of the derived hue-rotation — mirroring
|
|
11
|
+
* {@link GenerateOptions.secondarySeed}. For an artwork-derived theme, pass the
|
|
12
|
+
* ranked `seedsFromImagePixels` results (`seeds[1]` / `seeds[2]`).
|
|
13
|
+
*/
|
|
14
|
+
export interface SeedAccents {
|
|
15
|
+
secondarySeed?: string;
|
|
16
|
+
tertiarySeed?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
7
19
|
/**
|
|
8
20
|
* Build the JS `theme.colors` object from an ARBITRARY seed colour — the dynamic
|
|
9
21
|
* counterpart of `buildTheme(preset, …)`.
|
|
@@ -22,8 +34,16 @@ export function buildColorsFromSeed(
|
|
|
22
34
|
resolved: 'light' | 'dark',
|
|
23
35
|
variant?: SchemeVariant,
|
|
24
36
|
contrastLevel?: number,
|
|
37
|
+
accents?: SeedAccents,
|
|
25
38
|
): ThemeColors {
|
|
26
|
-
const t = buildSeedScopeVars({
|
|
39
|
+
const t = buildSeedScopeVars({
|
|
40
|
+
seed,
|
|
41
|
+
mode: resolved,
|
|
42
|
+
variant,
|
|
43
|
+
contrastLevel,
|
|
44
|
+
secondarySeed: accents?.secondarySeed,
|
|
45
|
+
tertiarySeed: accents?.tertiarySeed,
|
|
46
|
+
});
|
|
27
47
|
const isDark = resolved === 'dark';
|
|
28
48
|
|
|
29
49
|
// Read a resolved `rgb(...)` token by its bare name (no leading `--`).
|
|
@@ -34,6 +54,8 @@ export function buildColorsFromSeed(
|
|
|
34
54
|
variant,
|
|
35
55
|
isDark,
|
|
36
56
|
contrastLevel,
|
|
57
|
+
secondarySeed: accents?.secondarySeed,
|
|
58
|
+
tertiarySeed: accents?.tertiarySeed,
|
|
37
59
|
});
|
|
38
60
|
|
|
39
61
|
return {
|
|
@@ -88,10 +110,11 @@ export function buildThemeFromSeed(
|
|
|
88
110
|
resolved: 'light' | 'dark',
|
|
89
111
|
variant?: SchemeVariant,
|
|
90
112
|
contrastLevel?: number,
|
|
113
|
+
accents?: SeedAccents,
|
|
91
114
|
): Theme {
|
|
92
115
|
return {
|
|
93
116
|
mode: resolved,
|
|
94
|
-
colors: buildColorsFromSeed(seed, resolved, variant, contrastLevel),
|
|
117
|
+
colors: buildColorsFromSeed(seed, resolved, variant, contrastLevel, accents),
|
|
95
118
|
gradients: THEME_GRADIENTS,
|
|
96
119
|
isDark: resolved === 'dark',
|
|
97
120
|
isLight: resolved === 'light',
|
package/src/theme/build-theme.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Platform } from 'react-native';
|
|
2
2
|
import { APP_COLOR_PRESETS, type AppColorName } from './color-presets';
|
|
3
3
|
import { generateRoleColors, type RoleColors } from './color-engine';
|
|
4
|
+
import type { ExplicitAccents } from './preset-vars';
|
|
4
5
|
import { getAdaptiveColors } from './adaptive-colors';
|
|
5
6
|
import { getResolvedTokens } from './token-registry';
|
|
6
7
|
import { THEME_GRADIENTS } from './gradients';
|
|
@@ -32,20 +33,24 @@ export const STATUS_COLORS = {
|
|
|
32
33
|
function buildColorsFromPreset(
|
|
33
34
|
preset: AppColorName,
|
|
34
35
|
resolved: 'light' | 'dark',
|
|
36
|
+
accents?: ExplicitAccents,
|
|
35
37
|
): ThemeColors {
|
|
36
|
-
const t = getResolvedTokens(preset, resolved);
|
|
38
|
+
const t = getResolvedTokens(preset, resolved, accents);
|
|
37
39
|
const isDark = resolved === 'dark';
|
|
38
40
|
|
|
39
41
|
// Read a resolved `rgb(...)` token by its bare name (no leading `--`).
|
|
40
42
|
const g = (k: string): string => t[`--${k}`] ?? 'rgb(0 0 0)';
|
|
41
43
|
|
|
42
44
|
// The full engine role set — for fields that map to roles Bloom's canonical
|
|
43
|
-
// token set does not surface (containers, error family).
|
|
45
|
+
// token set does not surface (containers, error family). An app-wide accent
|
|
46
|
+
// override wins over the preset's own declared accent (else derived).
|
|
44
47
|
const config = APP_COLOR_PRESETS[preset];
|
|
45
48
|
const r: RoleColors = generateRoleColors({
|
|
46
49
|
seed: config.hex,
|
|
47
50
|
variant: config.variant,
|
|
48
51
|
isDark,
|
|
52
|
+
secondarySeed: accents?.secondaryHex ?? config.secondaryHex,
|
|
53
|
+
tertiarySeed: accents?.tertiaryHex ?? config.tertiaryHex,
|
|
49
54
|
});
|
|
50
55
|
|
|
51
56
|
return {
|
|
@@ -105,9 +110,10 @@ export function buildTheme(
|
|
|
105
110
|
preset: AppColorName,
|
|
106
111
|
resolved: 'light' | 'dark',
|
|
107
112
|
isAdaptive: boolean = false,
|
|
113
|
+
accents?: ExplicitAccents,
|
|
108
114
|
): Theme {
|
|
109
115
|
const adaptive = isAdaptive && Platform.OS !== 'web' ? getAdaptiveColors() : undefined;
|
|
110
|
-
const colors = adaptive ?? buildColorsFromPreset(preset, resolved);
|
|
116
|
+
const colors = adaptive ?? buildColorsFromPreset(preset, resolved, accents);
|
|
111
117
|
|
|
112
118
|
return {
|
|
113
119
|
mode: resolved,
|
|
@@ -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[]) {
|
|
@@ -15,6 +15,29 @@ import { Variant } from './variant';
|
|
|
15
15
|
|
|
16
16
|
export type SchemeVariant = 'vivid' | 'vibrant' | 'expressive' | 'tonalSpot' | 'neutral';
|
|
17
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
|
+
}
|
|
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];
|
|
20
43
|
const VIBRANT_TERTIARY_ROTATIONS = [35.0, 30.0, 20.0, 25.0, 30.0, 35.0, 30.0, 25.0, 25.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),
|
|
@@ -53,40 +85,58 @@ export function schemeVibrant(source: Hct, isDark: boolean, contrastLevel: numbe
|
|
|
53
85
|
* old hand-authored palette felt more vivid than pure M3, and this restores that
|
|
54
86
|
* across the whole primary/secondary/tertiary trio.
|
|
55
87
|
*/
|
|
56
|
-
export function schemeVivid(
|
|
88
|
+
export function schemeVivid(
|
|
89
|
+
source: Hct,
|
|
90
|
+
isDark: boolean,
|
|
91
|
+
contrastLevel: number,
|
|
92
|
+
accents: AccentSources = {},
|
|
93
|
+
): DynamicScheme {
|
|
57
94
|
return new DynamicScheme({
|
|
58
95
|
sourceColorArgb: source.toInt(),
|
|
59
96
|
variant: Variant.VIBRANT,
|
|
60
97
|
contrastLevel,
|
|
61
98
|
isDark,
|
|
62
99
|
primaryPalette: TonalPalette.fromHueAndChroma(source.hue, 200.0),
|
|
63
|
-
secondaryPalette:
|
|
64
|
-
|
|
65
|
-
|
|
100
|
+
secondaryPalette: accentPalette(accents.secondarySource, () =>
|
|
101
|
+
TonalPalette.fromHueAndChroma(
|
|
102
|
+
DynamicScheme.getRotatedHue(source, VIBRANT_HUES, VIBRANT_SECONDARY_ROTATIONS),
|
|
103
|
+
64.0,
|
|
104
|
+
),
|
|
66
105
|
),
|
|
67
|
-
tertiaryPalette:
|
|
68
|
-
|
|
69
|
-
|
|
106
|
+
tertiaryPalette: accentPalette(accents.tertiarySource, () =>
|
|
107
|
+
TonalPalette.fromHueAndChroma(
|
|
108
|
+
DynamicScheme.getRotatedHue(source, VIBRANT_HUES, VIBRANT_TERTIARY_ROTATIONS),
|
|
109
|
+
72.0,
|
|
110
|
+
),
|
|
70
111
|
),
|
|
71
112
|
neutralPalette: TonalPalette.fromHueAndChroma(source.hue, 10.0),
|
|
72
113
|
neutralVariantPalette: TonalPalette.fromHueAndChroma(source.hue, 12.0),
|
|
73
114
|
});
|
|
74
115
|
}
|
|
75
116
|
|
|
76
|
-
export function schemeExpressive(
|
|
117
|
+
export function schemeExpressive(
|
|
118
|
+
source: Hct,
|
|
119
|
+
isDark: boolean,
|
|
120
|
+
contrastLevel: number,
|
|
121
|
+
accents: AccentSources = {},
|
|
122
|
+
): DynamicScheme {
|
|
77
123
|
return new DynamicScheme({
|
|
78
124
|
sourceColorArgb: source.toInt(),
|
|
79
125
|
variant: Variant.EXPRESSIVE,
|
|
80
126
|
contrastLevel,
|
|
81
127
|
isDark,
|
|
82
128
|
primaryPalette: TonalPalette.fromHueAndChroma(sanitizeDegreesDouble(source.hue + 240.0), 40.0),
|
|
83
|
-
secondaryPalette:
|
|
84
|
-
|
|
85
|
-
|
|
129
|
+
secondaryPalette: accentPalette(accents.secondarySource, () =>
|
|
130
|
+
TonalPalette.fromHueAndChroma(
|
|
131
|
+
DynamicScheme.getRotatedHue(source, EXPRESSIVE_HUES, EXPRESSIVE_SECONDARY_ROTATIONS),
|
|
132
|
+
24.0,
|
|
133
|
+
),
|
|
86
134
|
),
|
|
87
|
-
tertiaryPalette:
|
|
88
|
-
|
|
89
|
-
|
|
135
|
+
tertiaryPalette: accentPalette(accents.tertiarySource, () =>
|
|
136
|
+
TonalPalette.fromHueAndChroma(
|
|
137
|
+
DynamicScheme.getRotatedHue(source, EXPRESSIVE_HUES, EXPRESSIVE_TERTIARY_ROTATIONS),
|
|
138
|
+
32.0,
|
|
139
|
+
),
|
|
90
140
|
),
|
|
91
141
|
neutralPalette: TonalPalette.fromHueAndChroma(source.hue + 15, 8.0),
|
|
92
142
|
neutralVariantPalette: TonalPalette.fromHueAndChroma(source.hue + 15, 12.0),
|
|
@@ -94,36 +144,57 @@ export function schemeExpressive(source: Hct, isDark: boolean, contrastLevel: nu
|
|
|
94
144
|
}
|
|
95
145
|
|
|
96
146
|
/** The Material You default — a calm, balanced treatment of the seed. */
|
|
97
|
-
export function schemeTonalSpot(
|
|
147
|
+
export function schemeTonalSpot(
|
|
148
|
+
source: Hct,
|
|
149
|
+
isDark: boolean,
|
|
150
|
+
contrastLevel: number,
|
|
151
|
+
accents: AccentSources = {},
|
|
152
|
+
): DynamicScheme {
|
|
98
153
|
return new DynamicScheme({
|
|
99
154
|
sourceColorArgb: source.toInt(),
|
|
100
155
|
variant: Variant.TONAL_SPOT,
|
|
101
156
|
contrastLevel,
|
|
102
157
|
isDark,
|
|
103
158
|
primaryPalette: TonalPalette.fromHueAndChroma(source.hue, 36.0),
|
|
104
|
-
secondaryPalette:
|
|
105
|
-
|
|
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
|
+
),
|
|
106
165
|
neutralPalette: TonalPalette.fromHueAndChroma(source.hue, 6.0),
|
|
107
166
|
neutralVariantPalette: TonalPalette.fromHueAndChroma(source.hue, 8.0),
|
|
108
167
|
});
|
|
109
168
|
}
|
|
110
169
|
|
|
111
170
|
/** Near-greyscale — the seed hue is present only as a whisper. */
|
|
112
|
-
export function schemeNeutral(
|
|
171
|
+
export function schemeNeutral(
|
|
172
|
+
source: Hct,
|
|
173
|
+
isDark: boolean,
|
|
174
|
+
contrastLevel: number,
|
|
175
|
+
accents: AccentSources = {},
|
|
176
|
+
): DynamicScheme {
|
|
113
177
|
return new DynamicScheme({
|
|
114
178
|
sourceColorArgb: source.toInt(),
|
|
115
179
|
variant: Variant.NEUTRAL,
|
|
116
180
|
contrastLevel,
|
|
117
181
|
isDark,
|
|
118
182
|
primaryPalette: TonalPalette.fromHueAndChroma(source.hue, 12.0),
|
|
119
|
-
secondaryPalette:
|
|
120
|
-
|
|
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
|
+
),
|
|
121
189
|
neutralPalette: TonalPalette.fromHueAndChroma(source.hue, 2.0),
|
|
122
190
|
neutralVariantPalette: TonalPalette.fromHueAndChroma(source.hue, 2.0),
|
|
123
191
|
});
|
|
124
192
|
}
|
|
125
193
|
|
|
126
|
-
const BUILDERS: Record<
|
|
194
|
+
const BUILDERS: Record<
|
|
195
|
+
SchemeVariant,
|
|
196
|
+
(s: Hct, d: boolean, c: number, accents: AccentSources) => DynamicScheme
|
|
197
|
+
> = {
|
|
127
198
|
vivid: schemeVivid,
|
|
128
199
|
vibrant: schemeVibrant,
|
|
129
200
|
expressive: schemeExpressive,
|
|
@@ -131,12 +202,19 @@ const BUILDERS: Record<SchemeVariant, (s: Hct, d: boolean, c: number) => Dynamic
|
|
|
131
202
|
neutral: schemeNeutral,
|
|
132
203
|
};
|
|
133
204
|
|
|
134
|
-
/**
|
|
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
|
+
*/
|
|
135
212
|
export function buildScheme(
|
|
136
213
|
variant: SchemeVariant,
|
|
137
214
|
source: Hct,
|
|
138
215
|
isDark: boolean,
|
|
139
216
|
contrastLevel = 0,
|
|
217
|
+
accents: AccentSources = {},
|
|
140
218
|
): DynamicScheme {
|
|
141
|
-
return BUILDERS[variant](source, isDark, contrastLevel);
|
|
219
|
+
return BUILDERS[variant](source, isDark, contrastLevel, accents);
|
|
142
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
|
/**
|
|
@@ -72,6 +72,18 @@ export interface SeedScopeOptions {
|
|
|
72
72
|
variant?: SchemeVariant;
|
|
73
73
|
/** −1 (low) … 0 (normal) … 1 (high). Defaults to 0. */
|
|
74
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;
|
|
75
87
|
}
|
|
76
88
|
|
|
77
89
|
/**
|
|
@@ -92,6 +104,8 @@ export function buildSeedScopeVars(options: SeedScopeOptions): Record<string, st
|
|
|
92
104
|
variant: options.variant ?? 'vivid',
|
|
93
105
|
isDark: options.mode === 'dark',
|
|
94
106
|
contrastLevel: options.contrastLevel ?? 0,
|
|
107
|
+
secondarySeed: options.secondarySeed,
|
|
108
|
+
tertiarySeed: options.tertiarySeed,
|
|
95
109
|
});
|
|
96
110
|
const tokens = roleColorsToPresetTokens(roles);
|
|
97
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';
|