@oxyhq/bloom 0.8.0 → 0.8.2

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.
Files changed (32) hide show
  1. package/lib/commonjs/menu/index.web.js +7 -2
  2. package/lib/commonjs/menu/index.web.js.map +1 -1
  3. package/lib/commonjs/theme/build-theme.js +44 -18
  4. package/lib/commonjs/theme/build-theme.js.map +1 -1
  5. package/lib/module/menu/index.web.js +7 -2
  6. package/lib/module/menu/index.web.js.map +1 -1
  7. package/lib/module/theme/build-theme.js +45 -19
  8. package/lib/module/theme/build-theme.js.map +1 -1
  9. package/lib/typescript/commonjs/menu/index.web.d.ts +1 -1
  10. package/lib/typescript/commonjs/menu/index.web.d.ts.map +1 -1
  11. package/lib/typescript/commonjs/menu/types.d.ts +1 -0
  12. package/lib/typescript/commonjs/menu/types.d.ts.map +1 -1
  13. package/lib/typescript/commonjs/theme/build-theme.d.ts +1 -1
  14. package/lib/typescript/commonjs/theme/build-theme.d.ts.map +1 -1
  15. package/lib/typescript/commonjs/theme/types.d.ts +3 -3
  16. package/lib/typescript/commonjs/theme/types.d.ts.map +1 -1
  17. package/lib/typescript/module/menu/index.web.d.ts +1 -1
  18. package/lib/typescript/module/menu/index.web.d.ts.map +1 -1
  19. package/lib/typescript/module/menu/types.d.ts +1 -0
  20. package/lib/typescript/module/menu/types.d.ts.map +1 -1
  21. package/lib/typescript/module/theme/build-theme.d.ts +1 -1
  22. package/lib/typescript/module/theme/build-theme.d.ts.map +1 -1
  23. package/lib/typescript/module/theme/types.d.ts +3 -3
  24. package/lib/typescript/module/theme/types.d.ts.map +1 -1
  25. package/package.json +1 -1
  26. package/src/menu/index.web.tsx +7 -2
  27. package/src/menu/types.ts +1 -0
  28. package/src/theme/__tests__/__snapshots__/visual-gallery.test.tsx.snap +259 -259
  29. package/src/theme/__tests__/runtime-contract.test.ts +17 -3
  30. package/src/theme/__tests__/theme-colors-parity.test.ts +115 -0
  31. package/src/theme/build-theme.ts +45 -20
  32. 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('mislabeled aliases are fixed: secondary !== primary', () => {
47
- const { colors } = buildTheme('oxy', 'light');
48
- expect(colors.secondary).not.toBe(colors.primary);
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
+ });
@@ -1,8 +1,7 @@
1
1
  import { Platform } from 'react-native';
2
- import type { AppColorName } from './color-presets';
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. Subtle/alpha mixes derive from the
24
- * resolved rgb via `parseRgbString` + `srgbToRgbString`.
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
- // Re-emit a resolved token at a given alpha (sRGB rgb-with-alpha).
36
- const mix = (k: string, a: number): string => srgbToRgbString(parseRgbString(g(k)), a);
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
- // Corrected aliases (see types.ts): `primaryLight` is the preset accent
53
- // tint, `primaryDark` is the focus-ring shadeNOT the surface/background.
54
- primaryLight: g('accent'),
55
- primaryDark: g('ring'),
77
+ // Legacy aliases (see types.ts): `primaryLight` is the page surface tint,
78
+ // `primaryDark` is the page backgroundretained for downstream consumers.
79
+ primaryLight: g('surface'),
80
+ primaryDark: g('background'),
56
81
 
57
- // Corrected: `secondary` is the preset's secondary surface, NOT a primary mirror.
58
- secondary: g('secondary'),
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: mix('primary', isDark ? 0.16 : 0.12),
67
- primarySubtleForeground: g('primary'),
68
- negative: g('destructive'),
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: mix('destructive', isDark ? 0.16 : 0.12),
71
- negativeSubtleForeground: g('destructive'),
72
- contrast50: mix('foreground', 0.5),
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('card'),
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
  };
@@ -14,12 +14,12 @@ export interface ThemeColors {
14
14
 
15
15
  primary: string;
16
16
  primaryForeground: string;
17
- /** Preset accent tint (`--accent`) a soft brand-tinted surface, NOT the page surface. */
17
+ /** Legacy alias: the page surface tint (`--surface`), NOT a brand accent. */
18
18
  primaryLight: string;
19
- /** Focus-ring shade (`--ring`) — the preset's emphasized ring/border color, NOT the page background. */
19
+ /** Legacy alias: the page background (`--background`), NOT the focus ring. */
20
20
  primaryDark: string;
21
21
 
22
- /** Preset secondary surface (`--secondary`) a muted companion surface, NOT a mirror of `primary`. */
22
+ /** Legacy alias: mirrors `primary` (`--primary`) for backwards compatibility. */
23
23
  secondary: string;
24
24
 
25
25
  tint: string;