@oxyhq/bloom 0.7.5 → 0.7.7

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 (66) hide show
  1. package/lib/commonjs/theme/apply-dark-class.js +28 -10
  2. package/lib/commonjs/theme/apply-dark-class.js.map +1 -1
  3. package/lib/commonjs/theme/color-presets.js +10 -4
  4. package/lib/commonjs/theme/color-presets.js.map +1 -1
  5. package/lib/commonjs/theme/color-scope/index.web.js +19 -2
  6. package/lib/commonjs/theme/color-scope/index.web.js.map +1 -1
  7. package/lib/commonjs/theme/index.js +6 -0
  8. package/lib/commonjs/theme/index.js.map +1 -1
  9. package/lib/commonjs/theme/native-root-vars.js +32 -117
  10. package/lib/commonjs/theme/native-root-vars.js.map +1 -1
  11. package/lib/commonjs/theme/native-root-vars.native.js +127 -0
  12. package/lib/commonjs/theme/native-root-vars.native.js.map +1 -0
  13. package/lib/commonjs/theme/preset-vars.js +54 -1
  14. package/lib/commonjs/theme/preset-vars.js.map +1 -1
  15. package/lib/module/theme/apply-dark-class.js +24 -11
  16. package/lib/module/theme/apply-dark-class.js.map +1 -1
  17. package/lib/module/theme/color-presets.js +10 -4
  18. package/lib/module/theme/color-presets.js.map +1 -1
  19. package/lib/module/theme/color-scope/index.web.js +22 -3
  20. package/lib/module/theme/color-scope/index.web.js.map +1 -1
  21. package/lib/module/theme/index.js +1 -1
  22. package/lib/module/theme/index.js.map +1 -1
  23. package/lib/module/theme/native-root-vars.js +32 -116
  24. package/lib/module/theme/native-root-vars.js.map +1 -1
  25. package/lib/module/theme/native-root-vars.native.js +122 -0
  26. package/lib/module/theme/native-root-vars.native.js.map +1 -0
  27. package/lib/module/theme/preset-vars.js +53 -1
  28. package/lib/module/theme/preset-vars.js.map +1 -1
  29. package/lib/typescript/commonjs/theme/apply-dark-class.d.ts +18 -9
  30. package/lib/typescript/commonjs/theme/apply-dark-class.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/theme/color-presets.d.ts +10 -4
  32. package/lib/typescript/commonjs/theme/color-presets.d.ts.map +1 -1
  33. package/lib/typescript/commonjs/theme/color-scope/index.web.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/theme/index.d.ts +1 -1
  35. package/lib/typescript/commonjs/theme/index.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/theme/native-root-vars.d.ts +30 -90
  37. package/lib/typescript/commonjs/theme/native-root-vars.d.ts.map +1 -1
  38. package/lib/typescript/commonjs/theme/native-root-vars.native.d.ts +94 -0
  39. package/lib/typescript/commonjs/theme/native-root-vars.native.d.ts.map +1 -0
  40. package/lib/typescript/commonjs/theme/preset-vars.d.ts +30 -0
  41. package/lib/typescript/commonjs/theme/preset-vars.d.ts.map +1 -1
  42. package/lib/typescript/module/theme/apply-dark-class.d.ts +18 -9
  43. package/lib/typescript/module/theme/apply-dark-class.d.ts.map +1 -1
  44. package/lib/typescript/module/theme/color-presets.d.ts +10 -4
  45. package/lib/typescript/module/theme/color-presets.d.ts.map +1 -1
  46. package/lib/typescript/module/theme/color-scope/index.web.d.ts.map +1 -1
  47. package/lib/typescript/module/theme/index.d.ts +1 -1
  48. package/lib/typescript/module/theme/index.d.ts.map +1 -1
  49. package/lib/typescript/module/theme/native-root-vars.d.ts +30 -90
  50. package/lib/typescript/module/theme/native-root-vars.d.ts.map +1 -1
  51. package/lib/typescript/module/theme/native-root-vars.native.d.ts +94 -0
  52. package/lib/typescript/module/theme/native-root-vars.native.d.ts.map +1 -0
  53. package/lib/typescript/module/theme/preset-vars.d.ts +30 -0
  54. package/lib/typescript/module/theme/preset-vars.d.ts.map +1 -1
  55. package/package.json +1 -1
  56. package/src/__tests__/BloomColorScope.test.tsx +14 -0
  57. package/src/__tests__/apply-dark-class.test.ts +65 -0
  58. package/src/__tests__/native-root-vars.test.ts +30 -4
  59. package/src/__tests__/preset-vars.test.ts +47 -1
  60. package/src/theme/apply-dark-class.ts +24 -11
  61. package/src/theme/color-presets.ts +10 -4
  62. package/src/theme/color-scope/index.web.tsx +23 -2
  63. package/src/theme/index.ts +1 -0
  64. package/src/theme/native-root-vars.native.ts +123 -0
  65. package/src/theme/native-root-vars.ts +32 -118
  66. package/src/theme/preset-vars.ts +53 -1
@@ -1,6 +1,11 @@
1
1
  import { Platform } from 'react-native';
2
2
  import { APP_COLOR_PRESETS, type AppColorName } from './color-presets';
3
- import { getPresetVars } from './preset-vars';
3
+ import { getPresetVars, toWebColorValue } from './preset-vars';
4
+
5
+ // Re-exported so the web var-contract helper is reachable from this file (the
6
+ // home of the web write path). Defined in `preset-vars.ts` next to the related
7
+ // HSL parsing so native (which never imports this module) doesn't pull it in.
8
+ export { toWebColorValue } from './preset-vars';
4
9
 
5
10
  export function applyDarkClass(resolved: 'light' | 'dark') {
6
11
  if (Platform.OS === 'web' && typeof document !== 'undefined') {
@@ -10,16 +15,24 @@ export function applyDarkClass(resolved: 'light' | 'dark') {
10
15
 
11
16
  /**
12
17
  * Apply a color preset's CSS custom properties to the document root.
13
- * No-op on native — only affects web.
18
+ * No-op on native — only affects web (early-returns on `Platform.OS !== 'web'`).
19
+ *
20
+ * Web var contract (the form Tailwind v4 `@theme inline` compiles to)
21
+ * -------------------------------------------------------------------
22
+ * The shadcn/Tailwind-v4 web apps compile their color utilities to reference the
23
+ * BASE token directly (`.bg-background { background-color: var(--background) }`),
24
+ * so on web the base `--x` tokens MUST be FULL CSS colors. We therefore write
25
+ * them as `hsl(...)` (e.g. `--primary: hsl(185 100% 20%)`) — `var(--primary)`
26
+ * then resolves to a valid color. The resolved `--color-*` vars are written
27
+ * verbatim as `rgb(...)` (already full colors, used by native `color-mix` alpha
28
+ * utilities; harmless on web). Non-color tokens (`--radius`, etc.) pass through
29
+ * unchanged. See `toWebColorValue`.
14
30
  *
15
- * Writes both the raw HSL triples (e.g. `--primary: 185 100% 20%`) and the
16
- * resolved `--color-*` vars (`--color-primary: rgb(0 94 102)`) so both
17
- * shadcn-style `hsl(var(--primary))` plumbing and Tailwind v4 `@theme`
18
- * utilities resolve consistently. The resolved vars are emitted as `rgb(...)`
19
- * (not `hsl(...)`) so the alpha utilities (`bg-primary/10`) resolve on native —
20
- * see `getPresetVars` / `hslTripletToRgb`. Includes extended tokens (card,
21
- * chart-*, content-area, sidebar-*) so consumer apps don't need to synthesize
22
- * them.
31
+ * NATIVE writes the SAME tokens as RAW HSL triples (no `hsl()` wrapper) via
32
+ * `rootVariables` (`native-root-vars.native.ts`), consumed through bloom's
33
+ * native `global.css` `hsl(var(--x))` indirection that path is untouched here.
34
+ * Includes extended tokens (card, chart-*, content-area, sidebar-*) so consumer
35
+ * apps don't need to synthesize them.
23
36
  */
24
37
  export function applyColorPresetVars(preset: AppColorName, resolved: 'light' | 'dark') {
25
38
  if (Platform.OS !== 'web' || typeof document === 'undefined') return;
@@ -29,6 +42,6 @@ export function applyColorPresetVars(preset: AppColorName, resolved: 'light' | '
29
42
  const root = document.documentElement.style;
30
43
 
31
44
  for (const [key, value] of Object.entries(vars)) {
32
- root.setProperty(key, value);
45
+ root.setProperty(key, toWebColorValue(key, value));
33
46
  }
34
47
  }
@@ -7,10 +7,16 @@ export type AppColorName = 'teal' | 'blue' | 'green' | 'amber' | 'yellow' | 'red
7
7
  * names) — keep in mind the values are platform-agnostic **raw HSL triples**
8
8
  * (e.g. `'185 100% 20%'` or `'185 100% 20% / 0.5'`), not CSS-resolved colors.
9
9
  * The same map drives both:
10
- * - the web layer (written verbatim into `document.documentElement.style`
11
- * so Tailwind's `hsl(var(--primary))` plumbing picks them up), and
12
- * - the native layer (`buildTheme` resolves them into `hsl(...)` strings
13
- * consumable by React Native styles).
10
+ * - the web layer: the base `--x` tokens are wrapped to full `hsl(...)` colors
11
+ * (via `toWebColorValue`) before being written to `document.documentElement`
12
+ * / a `BloomColorScope` element, so `var(--x)` — the form Tailwind v4
13
+ * `@theme inline` compiles its color utilities to — resolves to a valid
14
+ * color directly; the resolved `--color-*` companions are written as
15
+ * `rgb(...)`, and
16
+ * - the native layer (`buildTheme` resolves the raw triples into `hsl(...)`
17
+ * strings consumable by React Native styles; `native-root-vars.native.ts`
18
+ * writes the raw triples through bloom's `hsl(var(--x))` `global.css`
19
+ * indirection).
14
20
  *
15
21
  * The `--` prefix is an implementation detail we will drop in a future major.
16
22
  */
@@ -3,8 +3,29 @@ import React, { Children, cloneElement, isValidElement, useContext, useMemo } fr
3
3
  import { BloomThemeContext, type BloomThemeContextValue } from '../BloomThemeProvider';
4
4
  import { buildTheme } from '../build-theme';
5
5
  import type { AppColorName } from '../color-presets';
6
+ import { toWebColorValue } from '../preset-vars';
6
7
  import { buildScopeVars } from './style-builder';
7
8
 
9
+ /**
10
+ * `buildScopeVars` returns the preset's tokens as platform-agnostic raw HSL
11
+ * triples (shared with the native write path). On web they are scoped onto an
12
+ * element's inline `style`, where Tailwind v4's compiled utilities read them as
13
+ * `var(--x)` directly — so the base tokens MUST be full CSS colors. Wrap each
14
+ * raw triple in `hsl(...)` via `toWebColorValue`; `--color-*` rgb vars and
15
+ * non-color tokens pass through. See `toWebColorValue` in `../preset-vars`.
16
+ */
17
+ function buildWebScopeVars(
18
+ colorPreset: AppColorName,
19
+ mode: 'light' | 'dark',
20
+ ): React.CSSProperties {
21
+ const raw = buildScopeVars(colorPreset, mode);
22
+ const out: Record<string, string> = {};
23
+ for (const [key, value] of Object.entries(raw)) {
24
+ out[key] = toWebColorValue(key, value);
25
+ }
26
+ return out as React.CSSProperties;
27
+ }
28
+
8
29
  export interface BloomColorScopeProps {
9
30
  /**
10
31
  * Preset to apply within this subtree. When `undefined`, the scope is a
@@ -63,7 +84,7 @@ export function BloomColorScope({
63
84
  }, [colorPreset, resolvedMode, parent]);
64
85
 
65
86
  const varsStyle = useMemo(
66
- () => buildScopeVars(colorPreset, resolvedMode) as React.CSSProperties,
87
+ () => buildWebScopeVars(colorPreset, resolvedMode),
67
88
  [colorPreset, resolvedMode],
68
89
  );
69
90
 
@@ -104,7 +125,7 @@ export function useColorScopeStyle(colorPreset: AppColorName): React.CSSProperti
104
125
  }
105
126
  const resolvedMode = parent.theme.mode;
106
127
  return useMemo(
107
- () => buildScopeVars(colorPreset, resolvedMode) as React.CSSProperties,
128
+ () => buildWebScopeVars(colorPreset, resolvedMode),
108
129
  [colorPreset, resolvedMode],
109
130
  );
110
131
  }
@@ -19,6 +19,7 @@ export {
19
19
  export {
20
20
  getPresetVars,
21
21
  applyPresetVarsToDocument,
22
+ toWebColorValue,
22
23
  } from './preset-vars';
23
24
  export type { PresetVarsOptions } from './preset-vars';
24
25
  export { applyDarkClass } from './apply-dark-class';
@@ -0,0 +1,123 @@
1
+ /// <reference path="../react-native-css.d.ts" />
2
+ // The triple-slash reference above pins Bloom's ambient declaration for
3
+ // `react-native-css/native-internal` (`src/react-native-css.d.ts`) into the
4
+ // compilation graph of EVERY program that compiles THIS file — including
5
+ // downstream consumers that resolve Bloom's `"react-native"` source export
6
+ // condition (`./src/theme/index.ts` → `BloomThemeProvider` → this file). Without
7
+ // it the ambient decl is only auto-loaded inside Bloom's own tsconfig
8
+ // (`include: ["src"]`); a consumer's tsconfig never includes
9
+ // `node_modules/@oxyhq/bloom/src/**`, and TypeScript does NOT auto-pick-up
10
+ // ambient `.d.ts` files that live under `node_modules`, so
11
+ // `react-native-css/native-internal` is unresolved (TS2307) unless the reference
12
+ // travels with the source. react-native-css is intentionally NOT a Bloom
13
+ // dependency — it arrives transitively via a host's NativeWind 5 install, or is
14
+ // absent entirely for NativeWind 4 consumers — so the ambient declaration is the
15
+ // only thing that can type this import, and it MUST reach the consumer. The
16
+ // directive ships raw in `src/` (Bloom publishes `files: ["src", "lib"]`).
17
+ import { rootVariables } from 'react-native-css/native-internal';
18
+
19
+ import { buildScopeVars } from './color-scope/style-builder';
20
+ import { type AppColorName } from './color-presets';
21
+
22
+ /**
23
+ * Publish the active color preset's CSS custom properties into react-native-css's
24
+ * GLOBAL root-variable family so they reach the ENTIRE native app tree — including
25
+ * content rendered outside the React subtree of `BloomThemeProvider`: expo-router
26
+ * navigator screens, `Portal`s, `Dialog`/`BottomSheet` overlays, and any other host
27
+ * that paints through a separate React root.
28
+ *
29
+ * Why a global write is required (native only)
30
+ * --------------------------------------------
31
+ * react-native-css resolves a `var(--x)` reference (its `varResolver`) in this order:
32
+ * 1. `name in inheritedVariables` — the React-context-inherited vars. This is what
33
+ * `VariableContextProvider` populates, but it is React-context-scoped, so it only
34
+ * reaches *direct descendants* of the provider in the React tree.
35
+ * 2. `inlineVariables[name]` — same-component inline vars.
36
+ * 3. `variables[name]` — resolved inherited vars.
37
+ * 4. `universalVariables(name)` — a GLOBAL observable family.
38
+ * 5. `rootVariables(name)` — a GLOBAL observable family.
39
+ * 6. the declared fallback.
40
+ *
41
+ * `BloomThemeProvider`'s `VariableContextProvider` only feeds path #1, so the navigator
42
+ * host and every portal/modal — which render outside that subtree — never see the
43
+ * preset vars and fall through to `rootVariables`/`universalVariables`. By default those
44
+ * families hold only react-native-css's own `__rn-css-rem`/`__rn-css-color` entries
45
+ * (Bloom's `global.css` defines the `hsl(var(--primary))` indirection but not the HSL
46
+ * triples themselves — those are injected at runtime), so the whole tree below the
47
+ * navigator renders monochrome. Writing the vars into `rootVariables` here is the
48
+ * exact same mechanism react-native-css uses for compiled `:root {}` / `@theme` vars
49
+ * (`StyleCollection.inject` does `rootVariables(name).set(valueArray)`), so portaled
50
+ * content resolves the palette identically regardless of React-tree position.
51
+ *
52
+ * The raw tokens are written as HSL triples (`primary: '205 87% 53%'`) and the
53
+ * resolved Tailwind v4 `--color-*` vars as sRGB `rgb(...)` (`color-primary:
54
+ * 'rgb(31 153 239)'`) — the latter so `color-mix`-based alpha utilities resolve
55
+ * on native (see `getPresetVars` / `hslTripletToRgb`).
56
+ *
57
+ * Why a STATIC `import`, not a runtime `require` (critical)
58
+ * ---------------------------------------------------------
59
+ * react-native-css@3.0.7 ships both a `dist/commonjs` and a `dist/module` build, and
60
+ * Metro bundles BOTH. In `native-internal/root.js` the variable families are plain
61
+ * module-local consts — NOT `globalThis`-guarded the way `StyleCollection` is — so the
62
+ * commonjs `root.js` and the module `root.js` each instantiate their OWN separate
63
+ * `rootVariables` family. The react-native-css renderer (`react-native-css/src/components/View.tsx`
64
+ * → `varResolver`) imports its `root.js` via ESM `import`, i.e. the MODULE build. A
65
+ * runtime `require('react-native-css/native-internal')` resolves the package's `require`
66
+ * export condition → the COMMONJS build → a DIFFERENT family the renderer never
67
+ * subscribes to, so the writes are a silent no-op on device. A static top-level
68
+ * `import` resolves the `import` condition → the MODULE build → the renderer's instance.
69
+ *
70
+ * This plain file is the NATIVE/default variant (Metro selects it on iOS/Android,
71
+ * which have no `.native` override here); the web variant lives in the sibling
72
+ * `native-root-vars.web.ts` no-op, which web bundlers pick over this one. So the
73
+ * static `react-native-css/native-internal` import below is never pulled into a web
74
+ * bundle — only into native. This mirrors Bloom's existing platform-split convention
75
+ * (`color-scope/index.tsx` + `color-scope/index.web.tsx`, `FontLoader.native.tsx`).
76
+ *
77
+ * Keying contract (verified against react-native-css@3.0.7)
78
+ * ---------------------------------------------------------
79
+ * The `rootVariables` family is keyed by the BARE variable name, WITHOUT the leading
80
+ * `--`. The compiler strips it uniformly: a `:root { --primary: ... }` declaration is
81
+ * stored via `rule.v.push([property.slice(2), value])` then routed into
82
+ * `shared.rootVariables[name]`, and `@property --x` goes through
83
+ * `name.startsWith("--") ? name.slice(2) : name`. The lookup side matches: a `var(--x)`
84
+ * reference compiles to `[{}, "var", ident.slice(2)]`, so `varResolver` looks up the
85
+ * bare name. `buildScopeVars` returns keys WITH the `--` prefix, so we strip it before
86
+ * writing each entry.
87
+ *
88
+ * Value shape
89
+ * -----------
90
+ * Each entry is a `VariableValue[]` — an array of `[value, mediaCondition?]` tuples.
91
+ * Bloom's preset vars are unconditional, so each is a single-element `[[value]]`, e.g.
92
+ * `rootVariables('primary').set([['205 87% 53%']])`. This is the same shape the compiler
93
+ * emits for unconditional `:root` vars.
94
+ *
95
+ * This is native-only and additive: the `VariableContextProvider` wrapper stays (it is
96
+ * the correct, scoped mechanism for `BloomColorScope` subtree overrides and harmlessly
97
+ * covers direct descendants). The web fork (`native-root-vars.web.ts`) no-ops — web
98
+ * writes the same vars to `document.documentElement` via `applyColorPresetVars`.
99
+ */
100
+
101
+ /**
102
+ * Write the preset's CSS vars to react-native-css's global `rootVariables` family
103
+ * so the whole native tree (navigator screens + portals/modals/bottom-sheets)
104
+ * resolves the active palette.
105
+ *
106
+ * Each `.set` notifies its own observers synchronously (react-native-css only
107
+ * batches notifications inside its own `StyleCollection.inject`; outside it,
108
+ * `observableBatch.current` is unset so every `.set` runs observers immediately),
109
+ * so sequential writes correctly trigger a re-render when the preset/mode changes.
110
+ *
111
+ * Defensive guard: if a host ships a react-native-css build that doesn't expose
112
+ * `rootVariables` as a callable (it always does on @3), we bail rather than throw.
113
+ */
114
+ export function applyNativeRootVars(colorPreset: AppColorName, mode: 'light' | 'dark'): void {
115
+ if (typeof rootVariables !== 'function') return;
116
+
117
+ const vars = buildScopeVars(colorPreset, mode);
118
+ for (const [key, value] of Object.entries(vars)) {
119
+ // The family is keyed by the bare name; `buildScopeVars` keys include `--`.
120
+ const name = key.startsWith('--') ? key.slice(2) : key;
121
+ rootVariables(name).set([[value]]);
122
+ }
123
+ }
@@ -1,123 +1,37 @@
1
- /// <reference path="../react-native-css.d.ts" />
2
- // The triple-slash reference above pins Bloom's ambient declaration for
3
- // `react-native-css/native-internal` (`src/react-native-css.d.ts`) into the
4
- // compilation graph of EVERY program that compiles THIS file — including
5
- // downstream consumers that resolve Bloom's `"react-native"` source export
6
- // condition (`./src/theme/index.ts` → `BloomThemeProvider` → this file). Without
7
- // it the ambient decl is only auto-loaded inside Bloom's own tsconfig
8
- // (`include: ["src"]`); a consumer's tsconfig never includes
9
- // `node_modules/@oxyhq/bloom/src/**`, and TypeScript does NOT auto-pick-up
10
- // ambient `.d.ts` files that live under `node_modules`, so
11
- // `react-native-css/native-internal` is unresolved (TS2307) unless the reference
12
- // travels with the source. react-native-css is intentionally NOT a Bloom
13
- // dependency — it arrives transitively via a host's NativeWind 5 install, or is
14
- // absent entirely for NativeWind 4 consumers — so the ambient declaration is the
15
- // only thing that can type this import, and it MUST reach the consumer. The
16
- // directive ships raw in `src/` (Bloom publishes `files: ["src", "lib"]`).
17
- import { rootVariables } from 'react-native-css/native-internal';
18
-
19
- import { buildScopeVars } from './color-scope/style-builder';
20
1
  import { type AppColorName } from './color-presets';
21
2
 
22
3
  /**
23
- * Publish the active color preset's CSS custom properties into react-native-css's
24
- * GLOBAL root-variable family so they reach the ENTIRE native app tree — including
25
- * content rendered outside the React subtree of `BloomThemeProvider`: expo-router
26
- * navigator screens, `Portal`s, `Dialog`/`BottomSheet` overlays, and any other host
27
- * that paints through a separate React root.
28
- *
29
- * Why a global write is required (native only)
30
- * --------------------------------------------
31
- * react-native-css resolves a `var(--x)` reference (its `varResolver`) in this order:
32
- * 1. `name in inheritedVariables` — the React-context-inherited vars. This is what
33
- * `VariableContextProvider` populates, but it is React-context-scoped, so it only
34
- * reaches *direct descendants* of the provider in the React tree.
35
- * 2. `inlineVariables[name]` — same-component inline vars.
36
- * 3. `variables[name]` — resolved inherited vars.
37
- * 4. `universalVariables(name)` a GLOBAL observable family.
38
- * 5. `rootVariables(name)` — a GLOBAL observable family.
39
- * 6. the declared fallback.
40
- *
41
- * `BloomThemeProvider`'s `VariableContextProvider` only feeds path #1, so the navigator
42
- * host and every portal/modal which render outside that subtree — never see the
43
- * preset vars and fall through to `rootVariables`/`universalVariables`. By default those
44
- * families hold only react-native-css's own `__rn-css-rem`/`__rn-css-color` entries
45
- * (Bloom's `global.css` defines the `hsl(var(--primary))` indirection but not the HSL
46
- * triples themselves those are injected at runtime), so the whole tree below the
47
- * navigator renders monochrome. Writing the vars into `rootVariables` here is the
48
- * exact same mechanism react-native-css uses for compiled `:root {}` / `@theme` vars
49
- * (`StyleCollection.inject` does `rootVariables(name).set(valueArray)`), so portaled
50
- * content resolves the palette identically regardless of React-tree position.
51
- *
52
- * The raw tokens are written as HSL triples (`primary: '205 87% 53%'`) and the
53
- * resolved Tailwind v4 `--color-*` vars as sRGB `rgb(...)` (`color-primary:
54
- * 'rgb(31 153 239)'`) — the latter so `color-mix`-based alpha utilities resolve
55
- * on native (see `getPresetVars` / `hslTripletToRgb`).
56
- *
57
- * Why a STATIC `import`, not a runtime `require` (critical)
58
- * ---------------------------------------------------------
59
- * react-native-css@3.0.7 ships both a `dist/commonjs` and a `dist/module` build, and
60
- * Metro bundles BOTH. In `native-internal/root.js` the variable families are plain
61
- * module-local consts — NOT `globalThis`-guarded the way `StyleCollection` is — so the
62
- * commonjs `root.js` and the module `root.js` each instantiate their OWN separate
63
- * `rootVariables` family. The react-native-css renderer (`react-native-css/src/components/View.tsx`
64
- * → `varResolver`) imports its `root.js` via ESM `import`, i.e. the MODULE build. A
65
- * runtime `require('react-native-css/native-internal')` resolves the package's `require`
66
- * export condition → the COMMONJS build → a DIFFERENT family the renderer never
67
- * subscribes to, so the writes are a silent no-op on device. A static top-level
68
- * `import` resolves the `import` condition → the MODULE build → the renderer's instance.
69
- *
70
- * This plain file is the NATIVE/default variant (Metro selects it on iOS/Android,
71
- * which have no `.native` override here); the web variant lives in the sibling
72
- * `native-root-vars.web.ts` no-op, which web bundlers pick over this one. So the
73
- * static `react-native-css/native-internal` import below is never pulled into a web
74
- * bundle — only into native. This mirrors Bloom's existing platform-split convention
75
- * (`color-scope/index.tsx` + `color-scope/index.web.tsx`, `FontLoader.native.tsx`).
76
- *
77
- * Keying contract (verified against react-native-css@3.0.7)
78
- * ---------------------------------------------------------
79
- * The `rootVariables` family is keyed by the BARE variable name, WITHOUT the leading
80
- * `--`. The compiler strips it uniformly: a `:root { --primary: ... }` declaration is
81
- * stored via `rule.v.push([property.slice(2), value])` then routed into
82
- * `shared.rootVariables[name]`, and `@property --x` goes through
83
- * `name.startsWith("--") ? name.slice(2) : name`. The lookup side matches: a `var(--x)`
84
- * reference compiles to `[{}, "var", ident.slice(2)]`, so `varResolver` looks up the
85
- * bare name. `buildScopeVars` returns keys WITH the `--` prefix, so we strip it before
86
- * writing each entry.
87
- *
88
- * Value shape
89
- * -----------
90
- * Each entry is a `VariableValue[]` — an array of `[value, mediaCondition?]` tuples.
91
- * Bloom's preset vars are unconditional, so each is a single-element `[[value]]`, e.g.
92
- * `rootVariables('primary').set([['205 87% 53%']])`. This is the same shape the compiler
93
- * emits for unconditional `:root` vars.
94
- *
95
- * This is native-only and additive: the `VariableContextProvider` wrapper stays (it is
96
- * the correct, scoped mechanism for `BloomColorScope` subtree overrides and harmlessly
97
- * covers direct descendants). The web fork (`native-root-vars.web.ts`) no-ops — web
98
- * writes the same vars to `document.documentElement` via `applyColorPresetVars`.
4
+ * Platform-neutral DEFAULT variant of `applyNativeRootVars` a no-op.
5
+ *
6
+ * This is the file resolved by every bundler / type-checker that does NOT do
7
+ * React Native's platform-extension resolution:
8
+ * - a consumer's `tsc` (resolving Bloom's `"react-native"` -> `./src/...` source
9
+ * export condition: `theme/index.ts` -> `BloomThemeProvider` -> this import);
10
+ * - non-Metro web bundlers (Vite/Rolldown, webpack) resolving the package
11
+ * `exports` `import`/`require` conditions to `lib/.../native-root-vars.js`.
12
+ *
13
+ * It deliberately does NOT import `react-native-css/native-internal`. That subpath
14
+ * is native-only: it has no web build, and react-native-css is not even a Bloom
15
+ * dependency. Statically importing it from a file a web bundler resolves breaks
16
+ * module resolution outright (the original report: `Rolldown failed to resolve
17
+ * import "react-native-css/native-internal" from .../native-root-vars.js`).
18
+ * Importing it from a file a consumer's `tsc` resolves breaks type-check (TS2307).
19
+ *
20
+ * Confining the real implementation (and that import) to the `.native` sibling —
21
+ * `native-root-vars.native.ts`, which ONLY Metro selects — keeps both web bundling
22
+ * AND downstream `tsc` green, while native still publishes the preset vars into
23
+ * react-native-css's GLOBAL `rootVariables` family so the whole native tree
24
+ * (navigator screens + portals/modals) resolves the palette.
25
+ *
26
+ * The other variants:
27
+ * - `native-root-vars.native.ts`iOS/Android (Metro), the real implementation;
28
+ * - `native-root-vars.web.ts` web, also a no-op (web writes the same vars to
29
+ * `document.documentElement` via `applyColorPresetVars` instead).
30
+ *
31
+ * The signature is intentionally identical to those variants so the import site in
32
+ * `BloomThemeProvider` is platform-agnostic.
99
33
  */
100
-
101
- /**
102
- * Write the preset's CSS vars to react-native-css's global `rootVariables` family
103
- * so the whole native tree (navigator screens + portals/modals/bottom-sheets)
104
- * resolves the active palette.
105
- *
106
- * Each `.set` notifies its own observers synchronously (react-native-css only
107
- * batches notifications inside its own `StyleCollection.inject`; outside it,
108
- * `observableBatch.current` is unset so every `.set` runs observers immediately),
109
- * so sequential writes correctly trigger a re-render when the preset/mode changes.
110
- *
111
- * Defensive guard: if a host ships a react-native-css build that doesn't expose
112
- * `rootVariables` as a callable (it always does on @3), we bail rather than throw.
113
- */
114
- export function applyNativeRootVars(colorPreset: AppColorName, mode: 'light' | 'dark'): void {
115
- if (typeof rootVariables !== 'function') return;
116
-
117
- const vars = buildScopeVars(colorPreset, mode);
118
- for (const [key, value] of Object.entries(vars)) {
119
- // The family is keyed by the bare name; `buildScopeVars` keys include `--`.
120
- const name = key.startsWith('--') ? key.slice(2) : key;
121
- rootVariables(name).set([[value]]);
122
- }
34
+ export function applyNativeRootVars(_colorPreset: AppColorName, _mode: 'light' | 'dark'): void {
35
+ // Intentionally empty — the native variant (`native-root-vars.native.ts`) does
36
+ // the real work; this neutral default and the web fork are inert.
123
37
  }
@@ -83,6 +83,53 @@ export function hslTripletToRgb(triplet: string): string {
83
83
  return `rgb(${red} ${green} ${blue})`;
84
84
  }
85
85
 
86
+ /**
87
+ * Prefix that marks a Tailwind v4 resolved color var (`--color-primary`). These
88
+ * are already emitted as full `rgb(...)` colors by `getPresetVars` and must be
89
+ * written to web verbatim — wrapping them would corrupt the value.
90
+ */
91
+ const RESOLVED_COLOR_VAR_PREFIX = '--color-';
92
+
93
+ /**
94
+ * Matches a shadcn-style raw HSL triple — `H S% L%`, optionally with an alpha
95
+ * tail `H S% L% / A`. The value starts with a digit (or a leading `-`) and
96
+ * carries at least one `%` (the saturation/lightness units), which together
97
+ * distinguish a bare triple from an already-resolved CSS color (`rgb(...)`,
98
+ * `hsl(...)`, `#fff`, named colors) or a unitless/length token (`--radius`).
99
+ * Mirrors the triple shape parsed by `hslTripletToRgb`.
100
+ */
101
+ const RAW_HSL_TRIPLE = /^-?\d[\d.]*\s+[\d.]+%/;
102
+
103
+ /**
104
+ * Transform a single preset var into the value to write to a WEB element
105
+ * (`document.documentElement` or a `BloomColorScope` subtree element).
106
+ *
107
+ * The web apps (Vite + Tailwind v4 + shadcn) compile their `@theme inline`
108
+ * block so the color utilities reference the BASE token directly — e.g.
109
+ * `.bg-background { background-color: var(--background) }`. So on web the base
110
+ * `--x` tokens MUST resolve to a COMPLETE CSS color; a bare HSL triple
111
+ * (`185 50% 5%`) is invalid as a `background-color` value and renders
112
+ * transparent (the production incident this fixes). We therefore wrap raw
113
+ * triples in `hsl(...)`, preserving the exact preset color math (and any `/ A`
114
+ * alpha tail, valid in modern `hsl(H S% L% / A)` syntax) with zero rounding
115
+ * drift.
116
+ *
117
+ * Values that are already full colors pass through unchanged:
118
+ * - `--color-*` resolved vars (already `rgb(...)` from `getPresetVars`),
119
+ * - any value not shaped like a raw HSL triple (`--radius: 0.5rem`,
120
+ * pre-resolved `rgb(...)`/`hsl(...)`, etc.).
121
+ *
122
+ * NATIVE never calls this — `getPresetVars` returns raw triples verbatim and
123
+ * `native-root-vars.native.ts` writes them through bloom's `hsl(var(--x))`
124
+ * native `global.css` indirection. Pure function — no side effects — so it is
125
+ * unit-testable in isolation.
126
+ */
127
+ export function toWebColorValue(key: string, value: string): string {
128
+ if (key.startsWith(RESOLVED_COLOR_VAR_PREFIX)) return value;
129
+ if (!RAW_HSL_TRIPLE.test(value)) return value;
130
+ return `hsl(${value})`;
131
+ }
132
+
86
133
  const RESOLVED_COLOR_MAP: Record<string, string> = {
87
134
  '--background': '--color-background',
88
135
  '--foreground': '--color-foreground',
@@ -183,6 +230,11 @@ export function getPresetVars(
183
230
  * on native. `BloomThemeProvider` already writes the base preset vars on web;
184
231
  * call this only when an app needs the extended (card/chart/sidebar) tokens on
185
232
  * the document root.
233
+ *
234
+ * The base raw-HSL-triple tokens are wrapped to full `hsl(...)` colors via
235
+ * `toWebColorValue` so `var(--x)` (the form Tailwind v4 `@theme inline`
236
+ * compiles to) resolves directly on web; `--color-*` rgb vars pass through. See
237
+ * `toWebColorValue` for the web var contract.
186
238
  */
187
239
  export function applyPresetVarsToDocument(
188
240
  colorName: AppColorName,
@@ -194,6 +246,6 @@ export function applyPresetVarsToDocument(
194
246
  const vars = getPresetVars(colorName, mode, options);
195
247
  const root = document.documentElement.style;
196
248
  for (const [key, value] of Object.entries(vars)) {
197
- root.setProperty(key, value);
249
+ root.setProperty(key, toWebColorValue(key, value));
198
250
  }
199
251
  }