@oxyhq/bloom 0.39.0 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/theme/build-theme-from-seed.js +90 -0
- package/lib/commonjs/theme/build-theme-from-seed.js.map +1 -0
- package/lib/commonjs/theme/color-engine/scheme-variants.js +23 -0
- package/lib/commonjs/theme/color-engine/scheme-variants.js.map +1 -1
- package/lib/commonjs/theme/color-presets.js +13 -13
- package/lib/commonjs/theme/color-presets.js.map +1 -1
- package/lib/commonjs/theme/color-scope/seed-scope.js +1 -0
- package/lib/commonjs/theme/color-scope/seed-scope.js.map +1 -1
- package/lib/commonjs/theme/index.js +94 -0
- package/lib/commonjs/theme/index.js.map +1 -1
- package/lib/commonjs/theme/seed-scope/index.js +87 -0
- package/lib/commonjs/theme/seed-scope/index.js.map +1 -0
- package/lib/commonjs/theme/seed-scope/index.web.js +86 -0
- package/lib/commonjs/theme/seed-scope/index.web.js.map +1 -0
- package/lib/module/theme/build-theme-from-seed.js +85 -0
- package/lib/module/theme/build-theme-from-seed.js.map +1 -0
- package/lib/module/theme/color-engine/scheme-variants.js +22 -0
- package/lib/module/theme/color-engine/scheme-variants.js.map +1 -1
- package/lib/module/theme/color-presets.js +13 -13
- package/lib/module/theme/color-presets.js.map +1 -1
- package/lib/module/theme/color-scope/seed-scope.js +1 -0
- package/lib/module/theme/color-scope/seed-scope.js.map +1 -1
- package/lib/module/theme/index.js +7 -0
- package/lib/module/theme/index.js.map +1 -1
- package/lib/module/theme/seed-scope/index.js +82 -0
- package/lib/module/theme/seed-scope/index.js.map +1 -0
- package/lib/module/theme/seed-scope/index.web.js +81 -0
- package/lib/module/theme/seed-scope/index.web.js.map +1 -0
- package/lib/typescript/commonjs/theme/build-theme-from-seed.d.ts +23 -0
- package/lib/typescript/commonjs/theme/build-theme-from-seed.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/color-engine/scheme-variants.d.ts +10 -1
- package/lib/typescript/commonjs/theme/color-engine/scheme-variants.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-scope/seed-scope.d.ts +3 -2
- package/lib/typescript/commonjs/theme/color-scope/seed-scope.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/index.d.ts +7 -0
- package/lib/typescript/commonjs/theme/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/seed-scope/index.d.ts +37 -0
- package/lib/typescript/commonjs/theme/seed-scope/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme/seed-scope/index.web.d.ts +35 -0
- package/lib/typescript/commonjs/theme/seed-scope/index.web.d.ts.map +1 -0
- package/lib/typescript/module/theme/build-theme-from-seed.d.ts +23 -0
- package/lib/typescript/module/theme/build-theme-from-seed.d.ts.map +1 -0
- package/lib/typescript/module/theme/color-engine/scheme-variants.d.ts +10 -1
- package/lib/typescript/module/theme/color-engine/scheme-variants.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-scope/seed-scope.d.ts +3 -2
- package/lib/typescript/module/theme/color-scope/seed-scope.d.ts.map +1 -1
- package/lib/typescript/module/theme/index.d.ts +7 -0
- package/lib/typescript/module/theme/index.d.ts.map +1 -1
- package/lib/typescript/module/theme/seed-scope/index.d.ts +37 -0
- package/lib/typescript/module/theme/seed-scope/index.d.ts.map +1 -0
- package/lib/typescript/module/theme/seed-scope/index.web.d.ts +35 -0
- package/lib/typescript/module/theme/seed-scope/index.web.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +74 -74
- package/src/theme/__tests__/__snapshots__/visual-gallery.test.tsx.snap +68 -68
- package/src/theme/__tests__/build-theme-from-seed-parity.test.ts +33 -0
- package/src/theme/build-theme-from-seed.ts +99 -0
- package/src/theme/color-engine/scheme-variants.ts +30 -1
- package/src/theme/color-presets.ts +13 -13
- package/src/theme/color-scope/seed-scope.ts +4 -2
- package/src/theme/index.ts +21 -0
- package/src/theme/seed-scope/index.tsx +100 -0
- package/src/theme/seed-scope/index.web.tsx +110 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React, { Children, cloneElement, isValidElement, useContext, useMemo } from 'react';
|
|
2
|
+
import { View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { BloomThemeContext, type BloomThemeContextValue } from '../BloomThemeProvider';
|
|
5
|
+
import { buildSeedScopeVars } from '../color-scope/seed-scope';
|
|
6
|
+
import { getVariableContextProvider } from '../color-scope/style-builder';
|
|
7
|
+
import { buildThemeFromSeed } from '../build-theme-from-seed';
|
|
8
|
+
import type { SchemeVariant } from '../color-engine';
|
|
9
|
+
|
|
10
|
+
export interface BloomSeedScopeProps {
|
|
11
|
+
/**
|
|
12
|
+
* The seed colour (`#rrggbb`) to derive this subtree's palette from. When
|
|
13
|
+
* `undefined`, the scope is a no-op and children inherit the parent scope
|
|
14
|
+
* unchanged — pass `undefined` to fall back to the app preset (e.g. on
|
|
15
|
+
* mouse-leave/blur when restoring the default theme).
|
|
16
|
+
*/
|
|
17
|
+
seed: string | undefined;
|
|
18
|
+
/** Tonal scheme variant. Defaults to `'vibrant'` (matches Bloom presets). */
|
|
19
|
+
variant?: SchemeVariant;
|
|
20
|
+
/** −1 (low) … 0 (normal) … 1 (high). Defaults to 0. */
|
|
21
|
+
contrastLevel?: number;
|
|
22
|
+
/**
|
|
23
|
+
* When `true`, do not render a wrapping `<View>`. The single child is cloned
|
|
24
|
+
* with the caller's `style` merged into its `style` prop (Radix-style).
|
|
25
|
+
*/
|
|
26
|
+
asChild?: boolean;
|
|
27
|
+
/** Additional style applied to the wrapping `<View>` (or merged into the cloned child with `asChild`). */
|
|
28
|
+
style?: StyleProp<ViewStyle>;
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface StyleableProps {
|
|
33
|
+
style?: StyleProp<ViewStyle>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Native `<BloomSeedScope>`: scopes a subtree to an ARBITRARY seed colour's
|
|
38
|
+
* palette (the dynamic counterpart of `<BloomColorScope>`, which takes a named
|
|
39
|
+
* preset). Feeds the seed through the colour engine and (1) provides the
|
|
40
|
+
* resulting canonical CSS vars to descendants via react-native-css's
|
|
41
|
+
* `VariableContext` — so `bg-background` / `bg-card` / `var(--primary)` resolve
|
|
42
|
+
* against the seed inside this subtree — and (2) publishes a matching JS `theme`
|
|
43
|
+
* on `BloomThemeContext`, so `useTheme()`/`useBloomTheme()` consumers inside the
|
|
44
|
+
* subtree also re-theme. The `colorPreset` field is left as the parent's (a seed
|
|
45
|
+
* is not a named preset).
|
|
46
|
+
*/
|
|
47
|
+
export function BloomSeedScope({
|
|
48
|
+
seed,
|
|
49
|
+
variant,
|
|
50
|
+
contrastLevel,
|
|
51
|
+
asChild = false,
|
|
52
|
+
style,
|
|
53
|
+
children,
|
|
54
|
+
}: BloomSeedScopeProps) {
|
|
55
|
+
// Hooks run unconditionally; conditional no-op/throw is applied afterwards.
|
|
56
|
+
const parent = useContext(BloomThemeContext);
|
|
57
|
+
const resolvedMode = parent?.theme.mode ?? 'light';
|
|
58
|
+
|
|
59
|
+
const nativeVars = useMemo<Record<string, string> | null>(
|
|
60
|
+
() => (seed ? buildSeedScopeVars({ seed, mode: resolvedMode, variant, contrastLevel }) : null),
|
|
61
|
+
[seed, resolvedMode, variant, contrastLevel],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const contextValue = useMemo<BloomThemeContextValue | null>(() => {
|
|
65
|
+
if (!parent || !seed) return null;
|
|
66
|
+
const theme = buildThemeFromSeed(seed, resolvedMode, variant, contrastLevel);
|
|
67
|
+
return { ...parent, theme };
|
|
68
|
+
}, [parent, seed, resolvedMode, variant, contrastLevel]);
|
|
69
|
+
|
|
70
|
+
if (!parent) {
|
|
71
|
+
throw new Error('BloomSeedScope must be used within a <BloomThemeProvider>');
|
|
72
|
+
}
|
|
73
|
+
// `seed` undefined => no-op; children inherit the parent scope.
|
|
74
|
+
if (!nativeVars || !contextValue) return <>{children}</>;
|
|
75
|
+
|
|
76
|
+
const VariableProvider = getVariableContextProvider();
|
|
77
|
+
|
|
78
|
+
let content: React.ReactNode;
|
|
79
|
+
if (asChild) {
|
|
80
|
+
const child = Children.only(children);
|
|
81
|
+
if (!isValidElement<StyleableProps>(child)) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
'BloomSeedScope with `asChild` requires a single React element child that accepts a `style` prop.',
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const childStyle = child.props.style;
|
|
87
|
+
const mergedStyle: StyleProp<ViewStyle> = [style, childStyle];
|
|
88
|
+
content = cloneElement(child, { style: mergedStyle });
|
|
89
|
+
} else {
|
|
90
|
+
content = <View style={[{ flex: 1 }, style]}>{children}</View>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const scoped = VariableProvider ? (
|
|
94
|
+
<VariableProvider value={nativeVars}>{content}</VariableProvider>
|
|
95
|
+
) : (
|
|
96
|
+
content
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return <BloomThemeContext.Provider value={contextValue}>{scoped}</BloomThemeContext.Provider>;
|
|
100
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import React, { Children, cloneElement, isValidElement, useContext, useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { BloomThemeContext, type BloomThemeContextValue } from '../BloomThemeProvider';
|
|
4
|
+
import { buildSeedScopeVars } from '../color-scope/seed-scope';
|
|
5
|
+
import { buildThemeFromSeed } from '../build-theme-from-seed';
|
|
6
|
+
import type { SchemeVariant } from '../color-engine';
|
|
7
|
+
|
|
8
|
+
export interface BloomSeedScopeProps {
|
|
9
|
+
/**
|
|
10
|
+
* The seed colour (`#rrggbb`) to derive this subtree's palette from. When
|
|
11
|
+
* `undefined`, the scope is a no-op and children inherit the parent scope
|
|
12
|
+
* unchanged — pass `undefined` to fall back to the app preset (e.g. on
|
|
13
|
+
* mouse-leave/blur when restoring the default theme).
|
|
14
|
+
*/
|
|
15
|
+
seed: string | undefined;
|
|
16
|
+
/** Tonal scheme variant. Defaults to `'vibrant'` (matches Bloom presets). */
|
|
17
|
+
variant?: SchemeVariant;
|
|
18
|
+
/** −1 (low) … 0 (normal) … 1 (high). Defaults to 0. */
|
|
19
|
+
contrastLevel?: number;
|
|
20
|
+
/**
|
|
21
|
+
* When `true`, do not render a wrapping `<div>`. The single child is cloned
|
|
22
|
+
* with the scope's CSS vars merged into its `style` prop (Radix-style).
|
|
23
|
+
*/
|
|
24
|
+
asChild?: boolean;
|
|
25
|
+
/** Additional style applied to the wrapping `<div>` (or merged into the cloned child with `asChild`). */
|
|
26
|
+
style?: React.CSSProperties;
|
|
27
|
+
children: React.ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* On web the single `asChild` child is frequently a react-native-web component
|
|
32
|
+
* whose `style` prop is a style ARRAY (or a numeric registered-style id), not a
|
|
33
|
+
* plain object. Spreading such an array into an object literal would copy its
|
|
34
|
+
* numeric indices as keys and crash RNW; merge as an array instead.
|
|
35
|
+
*/
|
|
36
|
+
type WebStyle =
|
|
37
|
+
| React.CSSProperties
|
|
38
|
+
| number
|
|
39
|
+
| null
|
|
40
|
+
| undefined
|
|
41
|
+
| false
|
|
42
|
+
| ReadonlyArray<WebStyle>;
|
|
43
|
+
|
|
44
|
+
interface StyleableProps {
|
|
45
|
+
style?: WebStyle;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Web `<BloomSeedScope>`: scopes a subtree to an ARBITRARY seed colour's palette
|
|
50
|
+
* (the dynamic counterpart of `<BloomColorScope>`). It (1) writes every canonical
|
|
51
|
+
* `--x` token plus the Tailwind v4 `--color-x` aliases (already resolved to
|
|
52
|
+
* `rgb(...)`) onto the wrapping element's inline `style`, so NativeWind classes
|
|
53
|
+
* inside resolve against the seed palette instead of the document root, and
|
|
54
|
+
* (2) publishes a matching JS `theme` on `BloomThemeContext` so
|
|
55
|
+
* `useTheme()`/`useBloomTheme()` consumers inside the subtree re-theme too. The
|
|
56
|
+
* `colorPreset` field is left as the parent's (a seed is not a named preset).
|
|
57
|
+
*/
|
|
58
|
+
export function BloomSeedScope({
|
|
59
|
+
seed,
|
|
60
|
+
variant,
|
|
61
|
+
contrastLevel,
|
|
62
|
+
asChild = false,
|
|
63
|
+
style,
|
|
64
|
+
children,
|
|
65
|
+
}: BloomSeedScopeProps) {
|
|
66
|
+
const parent = useContext(BloomThemeContext);
|
|
67
|
+
const resolvedMode = parent?.theme.mode ?? 'light';
|
|
68
|
+
|
|
69
|
+
const varsStyle = useMemo<React.CSSProperties | null>(
|
|
70
|
+
() =>
|
|
71
|
+
seed
|
|
72
|
+
? (buildSeedScopeVars({
|
|
73
|
+
seed,
|
|
74
|
+
mode: resolvedMode,
|
|
75
|
+
variant,
|
|
76
|
+
contrastLevel,
|
|
77
|
+
}) as React.CSSProperties)
|
|
78
|
+
: null,
|
|
79
|
+
[seed, resolvedMode, variant, contrastLevel],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const contextValue = useMemo<BloomThemeContextValue | null>(() => {
|
|
83
|
+
if (!parent || !seed) return null;
|
|
84
|
+
const theme = buildThemeFromSeed(seed, resolvedMode, variant, contrastLevel);
|
|
85
|
+
return { ...parent, theme };
|
|
86
|
+
}, [parent, seed, resolvedMode, variant, contrastLevel]);
|
|
87
|
+
|
|
88
|
+
if (!parent) {
|
|
89
|
+
throw new Error('BloomSeedScope must be used within a <BloomThemeProvider>');
|
|
90
|
+
}
|
|
91
|
+
if (!varsStyle || !contextValue) return <>{children}</>;
|
|
92
|
+
|
|
93
|
+
let content: React.ReactNode;
|
|
94
|
+
if (asChild) {
|
|
95
|
+
const child = Children.only(children);
|
|
96
|
+
if (!isValidElement<StyleableProps>(child)) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
'BloomSeedScope with `asChild` requires a single React element child that accepts a `style` prop.',
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const childStyle = child.props.style;
|
|
102
|
+
const mergedStyle: WebStyle = [varsStyle, style, childStyle];
|
|
103
|
+
content = cloneElement(child, { style: mergedStyle });
|
|
104
|
+
} else {
|
|
105
|
+
const mergedStyle: React.CSSProperties = { ...varsStyle, ...style };
|
|
106
|
+
content = <div style={mergedStyle}>{children}</div>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return <BloomThemeContext.Provider value={contextValue}>{content}</BloomThemeContext.Provider>;
|
|
110
|
+
}
|