@idealyst/theme 1.2.61 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/theme",
3
- "version": "1.2.61",
3
+ "version": "1.2.62",
4
4
  "description": "Theming system for Idealyst Framework",
5
5
  "readme": "README.md",
6
6
  "main": "src/index.ts",
package/src/index.ts CHANGED
@@ -28,6 +28,9 @@ export * from './useResponsiveStyle';
28
28
  // Style props hook (platform-specific via .native.ts)
29
29
  export { useStyleProps, type StyleProps } from './useStyleProps';
30
30
 
31
+ // Shadow utility (platform-specific via .native.ts)
32
+ export { shadow, type ShadowOptions, type ShadowStyle } from './shadow';
33
+
31
34
  // Animation tokens and utilities
32
35
  // Note: Use '@idealyst/theme/animation' for full animation API
33
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
+ }