@oxyhq/bloom 0.8.0 → 0.8.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/build-theme.js +44 -18
- package/lib/commonjs/theme/build-theme.js.map +1 -1
- package/lib/module/theme/build-theme.js +45 -19
- package/lib/module/theme/build-theme.js.map +1 -1
- package/lib/typescript/commonjs/theme/build-theme.d.ts +1 -1
- package/lib/typescript/commonjs/theme/build-theme.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/types.d.ts +3 -3
- package/lib/typescript/commonjs/theme/types.d.ts.map +1 -1
- package/lib/typescript/module/theme/build-theme.d.ts +1 -1
- package/lib/typescript/module/theme/build-theme.d.ts.map +1 -1
- package/lib/typescript/module/theme/types.d.ts +3 -3
- package/lib/typescript/module/theme/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/theme/__tests__/__snapshots__/visual-gallery.test.tsx.snap +259 -259
- package/src/theme/__tests__/runtime-contract.test.ts +17 -3
- package/src/theme/__tests__/theme-colors-parity.test.ts +115 -0
- package/src/theme/build-theme.ts +45 -20
- package/src/theme/types.ts +3 -3
|
@@ -7,6 +7,7 @@ import { Platform } from 'react-native';
|
|
|
7
7
|
|
|
8
8
|
import { buildTheme } from '../build-theme';
|
|
9
9
|
import { applyColorPresetVars } from '../apply-dark-class';
|
|
10
|
+
import { getResolvedTokens } from '../token-registry';
|
|
10
11
|
|
|
11
12
|
// `@react-native/normalize-colors` is the exact parser React Native (native) and
|
|
12
13
|
// react-native-web use behind `StyleSheet`/`processColor`. If it returns `null`
|
|
@@ -43,7 +44,20 @@ describe('web var(--primary) resolves to a real color (not a bare triple)', () =
|
|
|
43
44
|
});
|
|
44
45
|
});
|
|
45
46
|
|
|
46
|
-
it('
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
it('restored legacy alias invariants hold', () => {
|
|
48
|
+
// Pre-0.8.0 semantics, restored in 0.8.1: `secondary` mirrors `primary`,
|
|
49
|
+
// `card` and `primaryLight` are the page surface, `primaryDark` is the page
|
|
50
|
+
// background. All read straight from the same resolved rgb token map, so the
|
|
51
|
+
// alias values are byte-identical to their source tokens.
|
|
52
|
+
for (const preset of ['oxy', 'blue'] as const) {
|
|
53
|
+
for (const mode of ['light', 'dark'] as const) {
|
|
54
|
+
const { colors } = buildTheme(preset, mode);
|
|
55
|
+
const t = getResolvedTokens(preset, mode);
|
|
56
|
+
expect(colors.secondary).toBe(colors.primary);
|
|
57
|
+
expect(colors.secondary).toBe(t['--primary']);
|
|
58
|
+
expect(colors.card).toBe(t['--surface']);
|
|
59
|
+
expect(colors.primaryLight).toBe(t['--surface']);
|
|
60
|
+
expect(colors.primaryDark).toBe(t['--background']);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
49
63
|
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import normalizeColor from '@react-native/normalize-colors';
|
|
6
|
+
|
|
7
|
+
import { buildTheme } from '../build-theme';
|
|
8
|
+
import type { AppColorName } from '../color-presets';
|
|
9
|
+
import type { ThemeColors } from '../types';
|
|
10
|
+
|
|
11
|
+
// Parity guard for the 0.8.1 restoration of the pre-0.8.0 `theme.colors`
|
|
12
|
+
// derived/alias values. 0.8.0 changed several derived/alias mappings, visibly
|
|
13
|
+
// altering dark (and light) mode for consuming components. 0.8.1 restores the
|
|
14
|
+
// EXACT pre-0.8.0 colors while keeping the single rgb pipeline (the old logic
|
|
15
|
+
// emitted `hsl(h,s%,l%)` strings; the new logic emits the `rgb(...)` of the same
|
|
16
|
+
// color). These oracles are the VERBATIM pre-0.8.0 outputs (commit 00a4cf3:
|
|
17
|
+
// `git show 00a4cf3:src/theme/build-theme.ts`). We assert visual equality via
|
|
18
|
+
// ΔE on the parsed sRGB — `hsl(...)` and the matching `rgb(...)` parse to the
|
|
19
|
+
// same bytes under the RN/RNW/browser `normalizeColor`, so ΔE is 0.
|
|
20
|
+
|
|
21
|
+
type Rgb = { r: number; g: number; b: number };
|
|
22
|
+
|
|
23
|
+
function parse(color: string): Rgb {
|
|
24
|
+
const n = normalizeColor(color);
|
|
25
|
+
if (n === null || n === undefined) {
|
|
26
|
+
throw new Error(`unparseable color: ${color}`);
|
|
27
|
+
}
|
|
28
|
+
// normalizeColor returns 0xRRGGBBAA.
|
|
29
|
+
return { r: (n >>> 24) & 0xff, g: (n >>> 16) & 0xff, b: (n >>> 8) & 0xff };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function deltaE(a: Rgb, b: Rgb): number {
|
|
33
|
+
return Math.sqrt((a.r - b.r) ** 2 + (a.g - b.g) ** 2 + (a.b - b.b) ** 2) / Math.sqrt(3);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The pre-0.8.0 `theme.colors` for a (preset, mode) pair, expressed in the EXACT
|
|
38
|
+
* source forms commit 00a4cf3 produced — token-backed values as `hsl(H, S%, L%)`
|
|
39
|
+
* (from the preset's raw triples) and computed tints via `hsl(hue, s%, l%)`.
|
|
40
|
+
*
|
|
41
|
+
* We only assert the fields the restoration touches plus the load-bearing
|
|
42
|
+
* aliases (primaryLight/primaryDark/secondary/card). Status colors and
|
|
43
|
+
* shadow/overlay were never changed, so they are intentionally omitted.
|
|
44
|
+
*/
|
|
45
|
+
type Oracle = Partial<Record<keyof ThemeColors, string>>;
|
|
46
|
+
|
|
47
|
+
const ORACLES: Array<{ preset: AppColorName; mode: 'light' | 'dark'; old: Oracle }> = [
|
|
48
|
+
{
|
|
49
|
+
// blue: primary 205 87% 53%, surface dark 205 20% 18%, background dark 205 50% 5%, destructive hue 0.
|
|
50
|
+
preset: 'blue',
|
|
51
|
+
mode: 'dark',
|
|
52
|
+
old: {
|
|
53
|
+
secondary: 'hsl(205, 87%, 53%)', // == primary
|
|
54
|
+
primary: 'hsl(205, 87%, 53%)',
|
|
55
|
+
card: 'hsl(205, 20%, 18%)', // == surface
|
|
56
|
+
primaryLight: 'hsl(205, 20%, 18%)', // == surface
|
|
57
|
+
primaryDark: 'hsl(205, 50%, 5%)', // == background
|
|
58
|
+
primarySubtle: 'hsl(205, 50%, 10%)',
|
|
59
|
+
primarySubtleForeground: 'hsl(205, 70%, 65%)',
|
|
60
|
+
negative: 'hsl(0, 84%, 45%)',
|
|
61
|
+
negativeForeground: '#FFFFFF',
|
|
62
|
+
negativeSubtle: 'hsl(0, 50%, 10%)',
|
|
63
|
+
negativeSubtleForeground: 'hsl(0, 70%, 65%)',
|
|
64
|
+
contrast50: 'hsl(205, 15%, 12%)',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
// oxy: primary 277 66% 56%, surface dark 277 20% 18%, background dark 277 50% 5%, destructive hue 0.
|
|
69
|
+
preset: 'oxy',
|
|
70
|
+
mode: 'dark',
|
|
71
|
+
old: {
|
|
72
|
+
secondary: 'hsl(277, 66%, 56%)',
|
|
73
|
+
primary: 'hsl(277, 66%, 56%)',
|
|
74
|
+
card: 'hsl(277, 20%, 18%)',
|
|
75
|
+
primaryLight: 'hsl(277, 20%, 18%)',
|
|
76
|
+
primaryDark: 'hsl(277, 50%, 5%)',
|
|
77
|
+
primarySubtle: 'hsl(277, 50%, 10%)',
|
|
78
|
+
primarySubtleForeground: 'hsl(277, 70%, 65%)',
|
|
79
|
+
negative: 'hsl(0, 84%, 45%)',
|
|
80
|
+
negativeForeground: '#FFFFFF',
|
|
81
|
+
negativeSubtle: 'hsl(0, 50%, 10%)',
|
|
82
|
+
negativeSubtleForeground: 'hsl(0, 70%, 65%)',
|
|
83
|
+
contrast50: 'hsl(277, 15%, 12%)',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
// blue light: surface 205 58% 94%, background 205 55% 96%.
|
|
88
|
+
preset: 'blue',
|
|
89
|
+
mode: 'light',
|
|
90
|
+
old: {
|
|
91
|
+
secondary: 'hsl(205, 87%, 53%)',
|
|
92
|
+
primary: 'hsl(205, 87%, 53%)',
|
|
93
|
+
card: 'hsl(205, 58%, 94%)',
|
|
94
|
+
primaryLight: 'hsl(205, 58%, 94%)',
|
|
95
|
+
primaryDark: 'hsl(205, 55%, 96%)',
|
|
96
|
+
primarySubtle: 'hsl(205, 70%, 93%)',
|
|
97
|
+
primarySubtleForeground: 'hsl(205, 90%, 25%)',
|
|
98
|
+
negative: 'hsl(0, 84%, 45%)',
|
|
99
|
+
negativeForeground: '#FFFFFF',
|
|
100
|
+
negativeSubtle: 'hsl(0, 90%, 95%)',
|
|
101
|
+
negativeSubtleForeground: 'hsl(0, 80%, 40%)',
|
|
102
|
+
contrast50: 'hsl(205, 10%, 93%)',
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
describe.each(ORACLES)('theme.colors parity with pre-0.8.0 ($preset/$mode)', ({ preset, mode, old }) => {
|
|
108
|
+
const { colors } = buildTheme(preset, mode);
|
|
109
|
+
|
|
110
|
+
it.each(Object.entries(old))('%s matches the pre-0.8.0 color (ΔE ≤ 1)', (key, oldColor) => {
|
|
111
|
+
const newColor = colors[key as keyof ThemeColors];
|
|
112
|
+
const dE = deltaE(parse(oldColor), parse(newColor));
|
|
113
|
+
expect(dE).toBeLessThanOrEqual(1);
|
|
114
|
+
});
|
|
115
|
+
});
|
package/src/theme/build-theme.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Platform } from 'react-native';
|
|
2
|
-
import type
|
|
2
|
+
import { APP_COLOR_PRESETS, type AppColorName } from './color-presets';
|
|
3
3
|
import { getAdaptiveColors } from './adaptive-colors';
|
|
4
|
-
import { getResolvedTokens } from './token-registry';
|
|
5
|
-
import { parseRgbString, srgbToRgbString } from './color-space';
|
|
4
|
+
import { getResolvedTokens, hslToSrgb } from './token-registry';
|
|
6
5
|
import type { Theme, ThemeColors } from './types';
|
|
7
6
|
|
|
8
7
|
/**
|
|
@@ -16,12 +15,30 @@ export const STATUS_COLORS = {
|
|
|
16
15
|
info: '#3B82F6',
|
|
17
16
|
} as const;
|
|
18
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Extract the integer hue from a shadcn-style raw HSL triple (`'H S% L%'`,
|
|
20
|
+
* optionally with an alpha tail). Used to seed the computed brand tints below
|
|
21
|
+
* from the preset's primary/destructive hue, exactly as pre-0.8.0.
|
|
22
|
+
*/
|
|
23
|
+
function extractHue(triple: string): number {
|
|
24
|
+
const first = (triple.split(/\s+/)[0] ?? '0').replace(/deg$/i, '');
|
|
25
|
+
const hue = parseInt(first, 10);
|
|
26
|
+
return Number.isFinite(hue) ? hue : 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
/**
|
|
20
30
|
* Build the JS `theme.colors` object from the SAME canonical rgb token source
|
|
21
31
|
* the web/native CSS-var writes use (`getResolvedTokens`). JS styles and the
|
|
22
32
|
* `var(--x)` document tokens therefore share one rgb pipeline — no second,
|
|
23
|
-
* drift-prone HSL conversion lives here.
|
|
24
|
-
*
|
|
33
|
+
* drift-prone HSL conversion lives here.
|
|
34
|
+
*
|
|
35
|
+
* Token-backed values read straight from the resolved rgb map via `g(...)`.
|
|
36
|
+
* The brand tints (primarySubtle/negative/contrast50/...) reproduce the EXACT
|
|
37
|
+
* pre-0.8.0 colors — computed from the preset's primary/destructive HUE with
|
|
38
|
+
* the historical `hsl(h, s%, l%)` math — but are emitted as `rgb(...)` by
|
|
39
|
+
* routing the triple through the registry's `hslToSrgb`, so the single-format
|
|
40
|
+
* pipeline is preserved. (`hsl(h,s%,l%)` and the `rgb(...)` of the same color
|
|
41
|
+
* are byte-identical under the RN/RNW/browser parser.)
|
|
25
42
|
*/
|
|
26
43
|
function buildColorsFromPreset(
|
|
27
44
|
preset: AppColorName,
|
|
@@ -32,8 +49,16 @@ function buildColorsFromPreset(
|
|
|
32
49
|
|
|
33
50
|
// Read a resolved `rgb(...)` token by its bare name (no leading `--`).
|
|
34
51
|
const g = (k: string): string => t[`--${k}`] ?? 'rgb(0 0 0)';
|
|
35
|
-
|
|
36
|
-
|
|
52
|
+
|
|
53
|
+
// Convert a computed `H S% L%` triple to the canonical `rgb(...)` via the
|
|
54
|
+
// registry's single HSL→sRGB conversion (matches the old `hsl(h,s%,l%)`).
|
|
55
|
+
const hslRgb = (h: number, s: number, l: number): string => hslToSrgb(`${h} ${s}% ${l}%`);
|
|
56
|
+
|
|
57
|
+
// Brand hues seed the computed tints from the preset's raw HSL triples,
|
|
58
|
+
// exactly as the pre-0.8.0 logic did.
|
|
59
|
+
const presetTokens = isDark ? APP_COLOR_PRESETS[preset].dark : APP_COLOR_PRESETS[preset].light;
|
|
60
|
+
const pHue = extractHue(presetTokens['--primary'] ?? '0 0% 50%');
|
|
61
|
+
const dHue = extractHue(presetTokens['--destructive'] ?? '0 0% 0%');
|
|
37
62
|
|
|
38
63
|
return {
|
|
39
64
|
background: g('background'),
|
|
@@ -49,13 +74,13 @@ function buildColorsFromPreset(
|
|
|
49
74
|
|
|
50
75
|
primary: g('primary'),
|
|
51
76
|
primaryForeground: g('primary-foreground'),
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
primaryLight: g('
|
|
55
|
-
primaryDark: g('
|
|
77
|
+
// Legacy aliases (see types.ts): `primaryLight` is the page surface tint,
|
|
78
|
+
// `primaryDark` is the page background — retained for downstream consumers.
|
|
79
|
+
primaryLight: g('surface'),
|
|
80
|
+
primaryDark: g('background'),
|
|
56
81
|
|
|
57
|
-
//
|
|
58
|
-
secondary: g('
|
|
82
|
+
// `secondary` historically mirrors `primary`. Retained for compatibility.
|
|
83
|
+
secondary: g('primary'),
|
|
59
84
|
|
|
60
85
|
tint: g('primary'),
|
|
61
86
|
icon: g('muted-foreground'),
|
|
@@ -63,15 +88,15 @@ function buildColorsFromPreset(
|
|
|
63
88
|
|
|
64
89
|
...STATUS_COLORS,
|
|
65
90
|
|
|
66
|
-
primarySubtle:
|
|
67
|
-
primarySubtleForeground:
|
|
68
|
-
negative:
|
|
91
|
+
primarySubtle: isDark ? hslRgb(pHue, 50, 10) : hslRgb(pHue, 70, 93),
|
|
92
|
+
primarySubtleForeground: isDark ? hslRgb(pHue, 70, 65) : hslRgb(pHue, 90, 25),
|
|
93
|
+
negative: hslRgb(dHue, 84, 45),
|
|
69
94
|
negativeForeground: '#FFFFFF',
|
|
70
|
-
negativeSubtle:
|
|
71
|
-
negativeSubtleForeground:
|
|
72
|
-
contrast50:
|
|
95
|
+
negativeSubtle: isDark ? hslRgb(dHue, 50, 10) : hslRgb(dHue, 90, 95),
|
|
96
|
+
negativeSubtleForeground: isDark ? hslRgb(dHue, 70, 65) : hslRgb(dHue, 80, 40),
|
|
97
|
+
contrast50: isDark ? hslRgb(pHue, 15, 12) : hslRgb(pHue, 10, 93),
|
|
73
98
|
|
|
74
|
-
card: g('
|
|
99
|
+
card: g('surface'),
|
|
75
100
|
shadow: isDark ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.1)',
|
|
76
101
|
overlay: 'rgba(0, 0, 0, 0.5)',
|
|
77
102
|
};
|
package/src/theme/types.ts
CHANGED
|
@@ -14,12 +14,12 @@ export interface ThemeColors {
|
|
|
14
14
|
|
|
15
15
|
primary: string;
|
|
16
16
|
primaryForeground: string;
|
|
17
|
-
/**
|
|
17
|
+
/** Legacy alias: the page surface tint (`--surface`), NOT a brand accent. */
|
|
18
18
|
primaryLight: string;
|
|
19
|
-
/**
|
|
19
|
+
/** Legacy alias: the page background (`--background`), NOT the focus ring. */
|
|
20
20
|
primaryDark: string;
|
|
21
21
|
|
|
22
|
-
/**
|
|
22
|
+
/** Legacy alias: mirrors `primary` (`--primary`) for backwards compatibility. */
|
|
23
23
|
secondary: string;
|
|
24
24
|
|
|
25
25
|
tint: string;
|