@idealyst/theme 1.2.60 → 1.2.62
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/package.json +1 -1
- package/src/index.ts +6 -0
- package/src/shadow.native.ts +141 -0
- package/src/shadow.ts +119 -0
- package/src/useStyleProps.native.ts +62 -0
- package/src/useStyleProps.ts +61 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -25,6 +25,12 @@ export * from './responsive';
|
|
|
25
25
|
export * from './breakpoints';
|
|
26
26
|
export * from './useResponsiveStyle';
|
|
27
27
|
|
|
28
|
+
// Style props hook (platform-specific via .native.ts)
|
|
29
|
+
export { useStyleProps, type StyleProps } from './useStyleProps';
|
|
30
|
+
|
|
31
|
+
// Shadow utility (platform-specific via .native.ts)
|
|
32
|
+
export { shadow, type ShadowOptions, type ShadowStyle } from './shadow';
|
|
33
|
+
|
|
28
34
|
// Animation tokens and utilities
|
|
29
35
|
// Note: Use '@idealyst/theme/animation' for full animation API
|
|
30
36
|
export { durations, easings, presets } from './animation/tokens';
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shadow - Native implementation
|
|
3
|
+
*
|
|
4
|
+
* Creates cross-platform shadow styles from a simple, unified API.
|
|
5
|
+
* All parameters work consistently across platforms.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { View } from '@idealyst/components';
|
|
10
|
+
* import { shadow } from '@idealyst/theme';
|
|
11
|
+
*
|
|
12
|
+
* <View style={shadow({ radius: 10, y: 4 })} />
|
|
13
|
+
* <View style={shadow({ radius: 20, y: 8, color: '#3b82f6', opacity: 0.3 })} />
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Platform } from 'react-native';
|
|
18
|
+
|
|
19
|
+
export interface ShadowOptions {
|
|
20
|
+
/** Shadow radius/size - controls blur and elevation (default: 10) */
|
|
21
|
+
radius?: number;
|
|
22
|
+
/** Horizontal offset in pixels (default: 0) */
|
|
23
|
+
x?: number;
|
|
24
|
+
/** Vertical offset in pixels (default: 4) */
|
|
25
|
+
y?: number;
|
|
26
|
+
/** Shadow color (default: '#000000') */
|
|
27
|
+
color?: string;
|
|
28
|
+
/** Shadow opacity 0-1 (default: 0.15) */
|
|
29
|
+
opacity?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ShadowStyle {
|
|
33
|
+
// Web
|
|
34
|
+
boxShadow?: string;
|
|
35
|
+
// iOS
|
|
36
|
+
shadowColor?: string;
|
|
37
|
+
shadowOffset?: { width: number; height: number };
|
|
38
|
+
shadowOpacity?: number;
|
|
39
|
+
shadowRadius?: number;
|
|
40
|
+
// Android
|
|
41
|
+
elevation?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Approximate Android elevation from shadow radius.
|
|
46
|
+
* Range is clamped to 0-24 (Android's max elevation).
|
|
47
|
+
*/
|
|
48
|
+
function radiusToElevation(radius: number): number {
|
|
49
|
+
// Map radius to elevation: radius 10 ≈ elevation 3-4
|
|
50
|
+
// This provides reasonable visual parity with iOS/web
|
|
51
|
+
const elevation = Math.round(radius / 3);
|
|
52
|
+
return Math.max(0, Math.min(24, elevation));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Parse color string to RGBA components.
|
|
57
|
+
* Supports: #RGB, #RRGGBB, #RRGGBBAA, rgb(), rgba()
|
|
58
|
+
*/
|
|
59
|
+
function parseColor(color: string): { r: number; g: number; b: number; a: number } {
|
|
60
|
+
// Default fallback
|
|
61
|
+
const fallback = { r: 0, g: 0, b: 0, a: 1 };
|
|
62
|
+
|
|
63
|
+
// Handle rgba(r, g, b, a) or rgb(r, g, b)
|
|
64
|
+
const rgbaMatch = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/i);
|
|
65
|
+
if (rgbaMatch) {
|
|
66
|
+
return {
|
|
67
|
+
r: parseInt(rgbaMatch[1], 10),
|
|
68
|
+
g: parseInt(rgbaMatch[2], 10),
|
|
69
|
+
b: parseInt(rgbaMatch[3], 10),
|
|
70
|
+
a: rgbaMatch[4] !== undefined ? parseFloat(rgbaMatch[4]) : 1,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Handle hex: #RGB, #RRGGBB, #RRGGBBAA
|
|
75
|
+
const hex = color.replace('#', '');
|
|
76
|
+
if (hex.length === 3) {
|
|
77
|
+
// #RGB -> #RRGGBB
|
|
78
|
+
return {
|
|
79
|
+
r: parseInt(hex[0] + hex[0], 16),
|
|
80
|
+
g: parseInt(hex[1] + hex[1], 16),
|
|
81
|
+
b: parseInt(hex[2] + hex[2], 16),
|
|
82
|
+
a: 1,
|
|
83
|
+
};
|
|
84
|
+
} else if (hex.length === 6) {
|
|
85
|
+
// #RRGGBB
|
|
86
|
+
return {
|
|
87
|
+
r: parseInt(hex.slice(0, 2), 16),
|
|
88
|
+
g: parseInt(hex.slice(2, 4), 16),
|
|
89
|
+
b: parseInt(hex.slice(4, 6), 16),
|
|
90
|
+
a: 1,
|
|
91
|
+
};
|
|
92
|
+
} else if (hex.length === 8) {
|
|
93
|
+
// #RRGGBBAA
|
|
94
|
+
return {
|
|
95
|
+
r: parseInt(hex.slice(0, 2), 16),
|
|
96
|
+
g: parseInt(hex.slice(2, 4), 16),
|
|
97
|
+
b: parseInt(hex.slice(4, 6), 16),
|
|
98
|
+
a: parseInt(hex.slice(6, 8), 16) / 255,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return fallback;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Creates a cross-platform shadow style object.
|
|
107
|
+
*
|
|
108
|
+
* @param options - Shadow configuration
|
|
109
|
+
* @returns Style object with platform-appropriate shadow properties
|
|
110
|
+
*/
|
|
111
|
+
export function shadow(options: ShadowOptions = {}): ShadowStyle {
|
|
112
|
+
const {
|
|
113
|
+
radius = 10,
|
|
114
|
+
x = 0,
|
|
115
|
+
y = 4,
|
|
116
|
+
color = '#000000',
|
|
117
|
+
opacity = 0.15,
|
|
118
|
+
} = options;
|
|
119
|
+
|
|
120
|
+
const parsed = parseColor(color);
|
|
121
|
+
// Multiply color's alpha with opacity parameter
|
|
122
|
+
const finalAlpha = parsed.a * opacity;
|
|
123
|
+
|
|
124
|
+
if (Platform.OS === 'android') {
|
|
125
|
+
// Android: elevation + shadowColor (limited control)
|
|
126
|
+
// Offset (x, y) is not supported - elevation controls everything
|
|
127
|
+
// Bake opacity into shadowColor as alpha channel
|
|
128
|
+
return {
|
|
129
|
+
elevation: radiusToElevation(radius),
|
|
130
|
+
shadowColor: `rgba(${parsed.r}, ${parsed.g}, ${parsed.b}, ${finalAlpha})`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// iOS: Full shadow control
|
|
135
|
+
return {
|
|
136
|
+
shadowColor: `rgb(${parsed.r}, ${parsed.g}, ${parsed.b})`,
|
|
137
|
+
shadowOffset: { width: x, height: y },
|
|
138
|
+
shadowOpacity: finalAlpha,
|
|
139
|
+
shadowRadius: radius / 2, // iOS shadowRadius is roughly half the CSS blur
|
|
140
|
+
};
|
|
141
|
+
}
|
package/src/shadow.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shadow - Web implementation
|
|
3
|
+
*
|
|
4
|
+
* Creates cross-platform shadow styles from a simple, unified API.
|
|
5
|
+
* All parameters work consistently across platforms.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { View } from '@idealyst/components';
|
|
10
|
+
* import { shadow } from '@idealyst/theme';
|
|
11
|
+
*
|
|
12
|
+
* <View style={shadow({ radius: 10, y: 4 })} />
|
|
13
|
+
* <View style={shadow({ radius: 20, y: 8, color: '#3b82f6', opacity: 0.3 })} />
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface ShadowOptions {
|
|
18
|
+
/** Shadow radius/size - controls blur and spread (default: 10) */
|
|
19
|
+
radius?: number;
|
|
20
|
+
/** Horizontal offset in pixels (default: 0) */
|
|
21
|
+
x?: number;
|
|
22
|
+
/** Vertical offset in pixels (default: 4) */
|
|
23
|
+
y?: number;
|
|
24
|
+
/** Shadow color (default: '#000000') */
|
|
25
|
+
color?: string;
|
|
26
|
+
/** Shadow opacity 0-1 (default: 0.15) */
|
|
27
|
+
opacity?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ShadowStyle {
|
|
31
|
+
// Web
|
|
32
|
+
boxShadow?: string;
|
|
33
|
+
// iOS
|
|
34
|
+
shadowColor?: string;
|
|
35
|
+
shadowOffset?: { width: number; height: number };
|
|
36
|
+
shadowOpacity?: number;
|
|
37
|
+
shadowRadius?: number;
|
|
38
|
+
// Android
|
|
39
|
+
elevation?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parse color string to RGBA components.
|
|
44
|
+
* Supports: #RGB, #RRGGBB, #RRGGBBAA, rgb(), rgba()
|
|
45
|
+
*/
|
|
46
|
+
function parseColor(color: string): { r: number; g: number; b: number; a: number } {
|
|
47
|
+
// Default fallback
|
|
48
|
+
const fallback = { r: 0, g: 0, b: 0, a: 1 };
|
|
49
|
+
|
|
50
|
+
// Handle rgba(r, g, b, a) or rgb(r, g, b)
|
|
51
|
+
const rgbaMatch = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/i);
|
|
52
|
+
if (rgbaMatch) {
|
|
53
|
+
return {
|
|
54
|
+
r: parseInt(rgbaMatch[1], 10),
|
|
55
|
+
g: parseInt(rgbaMatch[2], 10),
|
|
56
|
+
b: parseInt(rgbaMatch[3], 10),
|
|
57
|
+
a: rgbaMatch[4] !== undefined ? parseFloat(rgbaMatch[4]) : 1,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Handle hex: #RGB, #RRGGBB, #RRGGBBAA
|
|
62
|
+
const hex = color.replace('#', '');
|
|
63
|
+
if (hex.length === 3) {
|
|
64
|
+
// #RGB -> #RRGGBB
|
|
65
|
+
return {
|
|
66
|
+
r: parseInt(hex[0] + hex[0], 16),
|
|
67
|
+
g: parseInt(hex[1] + hex[1], 16),
|
|
68
|
+
b: parseInt(hex[2] + hex[2], 16),
|
|
69
|
+
a: 1,
|
|
70
|
+
};
|
|
71
|
+
} else if (hex.length === 6) {
|
|
72
|
+
// #RRGGBB
|
|
73
|
+
return {
|
|
74
|
+
r: parseInt(hex.slice(0, 2), 16),
|
|
75
|
+
g: parseInt(hex.slice(2, 4), 16),
|
|
76
|
+
b: parseInt(hex.slice(4, 6), 16),
|
|
77
|
+
a: 1,
|
|
78
|
+
};
|
|
79
|
+
} else if (hex.length === 8) {
|
|
80
|
+
// #RRGGBBAA
|
|
81
|
+
return {
|
|
82
|
+
r: parseInt(hex.slice(0, 2), 16),
|
|
83
|
+
g: parseInt(hex.slice(2, 4), 16),
|
|
84
|
+
b: parseInt(hex.slice(4, 6), 16),
|
|
85
|
+
a: parseInt(hex.slice(6, 8), 16) / 255,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return fallback;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Creates a cross-platform shadow style object.
|
|
94
|
+
*
|
|
95
|
+
* @param options - Shadow configuration
|
|
96
|
+
* @returns Style object with platform-appropriate shadow properties
|
|
97
|
+
*/
|
|
98
|
+
export function shadow(options: ShadowOptions = {}): ShadowStyle {
|
|
99
|
+
const {
|
|
100
|
+
radius = 10,
|
|
101
|
+
x = 0,
|
|
102
|
+
y = 4,
|
|
103
|
+
color = '#000000',
|
|
104
|
+
opacity = 0.15,
|
|
105
|
+
} = options;
|
|
106
|
+
|
|
107
|
+
const parsed = parseColor(color);
|
|
108
|
+
// Multiply color's alpha with opacity parameter
|
|
109
|
+
const finalAlpha = parsed.a * opacity;
|
|
110
|
+
|
|
111
|
+
// Derive blur and spread from radius for natural-looking shadows
|
|
112
|
+
// Blur is the main visual size, spread adds subtle expansion
|
|
113
|
+
const blur = radius;
|
|
114
|
+
const spread = Math.round(radius * 0.1); // 10% of radius for subtle spread
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
boxShadow: `${x}px ${y}px ${blur}px ${spread}px rgba(${parsed.r}, ${parsed.g}, ${parsed.b}, ${finalAlpha})`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useStyleProps - Native implementation
|
|
3
|
+
*
|
|
4
|
+
* Unified hook for applying styles across platforms.
|
|
5
|
+
* On native, combines Unistyles and additional styles into an array.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { View } from '@idealyst/components';
|
|
10
|
+
* import { useStyleProps } from '@idealyst/theme';
|
|
11
|
+
*
|
|
12
|
+
* function MyComponent({ style }: { style?: StyleProp<ViewStyle> }) {
|
|
13
|
+
* const styleProps = useStyleProps(
|
|
14
|
+
* (myStyles.container as any)({}), // Unistyles
|
|
15
|
+
* [style, { marginTop: 16 }] // Additional styles (array or single)
|
|
16
|
+
* );
|
|
17
|
+
*
|
|
18
|
+
* // Native: spreads { style: [unistyles, ...inlineStyles] }
|
|
19
|
+
* return <View {...styleProps}>Content</View>;
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { useMemo } from 'react';
|
|
25
|
+
import type { StyleProp, ViewStyle, TextStyle, ImageStyle } from 'react-native';
|
|
26
|
+
|
|
27
|
+
type AnyStyle = ViewStyle | TextStyle | ImageStyle;
|
|
28
|
+
type StyleInput = AnyStyle | StyleProp<AnyStyle> | undefined | null | false;
|
|
29
|
+
|
|
30
|
+
export interface StyleProps {
|
|
31
|
+
/** Style array to pass to React Native components */
|
|
32
|
+
style?: StyleInput[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Hook that returns style props for native components.
|
|
37
|
+
*
|
|
38
|
+
* @param unistyles - Unistyles style (from stylesheet)
|
|
39
|
+
* @param inlineStyles - Additional inline styles (single style or array)
|
|
40
|
+
* @returns Object with style array to spread onto element
|
|
41
|
+
*/
|
|
42
|
+
export function useStyleProps(
|
|
43
|
+
unistyles: StyleInput,
|
|
44
|
+
inlineStyles?: StyleInput | StyleInput[]
|
|
45
|
+
): StyleProps {
|
|
46
|
+
return useMemo(() => {
|
|
47
|
+
// Combine unistyles with inline styles into array
|
|
48
|
+
const styles: StyleInput[] = [unistyles];
|
|
49
|
+
|
|
50
|
+
if (inlineStyles) {
|
|
51
|
+
if (Array.isArray(inlineStyles)) {
|
|
52
|
+
for (const s of inlineStyles) {
|
|
53
|
+
styles.push(s as StyleInput);
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
styles.push(inlineStyles);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { style: styles };
|
|
61
|
+
}, [unistyles, inlineStyles]);
|
|
62
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useStyleProps - Web implementation
|
|
3
|
+
*
|
|
4
|
+
* Unified hook for applying styles across platforms.
|
|
5
|
+
* On web, wraps Unistyles with getWebProps to generate CSS class names,
|
|
6
|
+
* and passes additional styles for the style prop.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* import { View } from '@idealyst/components';
|
|
11
|
+
* import { useStyleProps } from '@idealyst/theme';
|
|
12
|
+
*
|
|
13
|
+
* function MyComponent({ style }: { style?: StyleProp<ViewStyle> }) {
|
|
14
|
+
* const styleProps = useStyleProps(
|
|
15
|
+
* (myStyles.container as any)({}), // Unistyles (goes through getWebProps)
|
|
16
|
+
* [style, { marginTop: 16 }] // Additional styles (array or single)
|
|
17
|
+
* );
|
|
18
|
+
*
|
|
19
|
+
* // Web: spreads { className, ref, style }
|
|
20
|
+
* return <View {...styleProps}>Content</View>;
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { useMemo } from 'react';
|
|
26
|
+
import type { StyleProp, ViewStyle, TextStyle, ImageStyle } from 'react-native';
|
|
27
|
+
import { getWebProps } from 'react-native-unistyles/web';
|
|
28
|
+
|
|
29
|
+
type AnyStyle = ViewStyle | TextStyle | ImageStyle | React.CSSProperties;
|
|
30
|
+
type StyleInput = AnyStyle | StyleProp<AnyStyle> | undefined | null | false;
|
|
31
|
+
|
|
32
|
+
export interface StyleProps {
|
|
33
|
+
/** CSS class name(s) for the element (from Unistyles) */
|
|
34
|
+
className?: string;
|
|
35
|
+
/** Ref to attach to the element (required for Unistyles on web) */
|
|
36
|
+
ref?: React.Ref<any>;
|
|
37
|
+
/** Additional inline styles */
|
|
38
|
+
style?: StyleInput | StyleInput[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Hook that returns style props for web components.
|
|
43
|
+
*
|
|
44
|
+
* @param unistyles - Unistyles style (from stylesheet), passed to getWebProps
|
|
45
|
+
* @param inlineStyles - Additional inline styles (single style or array)
|
|
46
|
+
* @returns Object with className, ref, and style to spread onto element
|
|
47
|
+
*/
|
|
48
|
+
export function useStyleProps(
|
|
49
|
+
unistyles: StyleInput,
|
|
50
|
+
inlineStyles?: StyleInput | StyleInput[]
|
|
51
|
+
): StyleProps {
|
|
52
|
+
return useMemo(() => {
|
|
53
|
+
// Process Unistyles through getWebProps
|
|
54
|
+
const webProps = getWebProps(unistyles as any);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
...webProps,
|
|
58
|
+
style: inlineStyles,
|
|
59
|
+
};
|
|
60
|
+
}, [unistyles, inlineStyles]);
|
|
61
|
+
}
|