@idealyst/theme 1.2.80 → 1.2.82

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.80",
3
+ "version": "1.2.82",
4
4
  "description": "Theming system for Idealyst Framework",
5
5
  "readme": "README.md",
6
6
  "main": "src/index.ts",
@@ -63,7 +63,7 @@
63
63
  "publish:npm": "npm publish"
64
64
  },
65
65
  "dependencies": {
66
- "@idealyst/tooling": "^1.2.80"
66
+ "@idealyst/tooling": "^1.2.82"
67
67
  },
68
68
  "peerDependencies": {
69
69
  "react-native-unistyles": ">=3.0.0"
@@ -44,6 +44,8 @@ export type {
44
44
  export {
45
45
  resolveDuration,
46
46
  resolveEasing,
47
+ resolveEasingWithDuration,
48
+ getSpringInfo,
47
49
  cssTransition,
48
50
  cssPreset,
49
51
  cssKeyframes,
@@ -54,6 +56,17 @@ export {
54
56
  pulseGradientStyle,
55
57
  } from './transitions';
56
58
 
59
+ // Export spring approximation utilities
60
+ export {
61
+ approximateSpring,
62
+ getSpringApproximation,
63
+ springApproximations,
64
+ isLinearEasingSupported,
65
+ getLinearFallback,
66
+ usesLinearEasing,
67
+ type SpringApproximation,
68
+ } from './springApproximation';
69
+
57
70
  // Export native utilities (these are also exported from index.native.ts)
58
71
  // Including here allows type-checking in monorepo without platform resolution
59
72
  export {
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Spring-to-CSS Approximation Utilities
3
+ *
4
+ * These utilities convert spring physics parameters to CSS-compatible
5
+ * bezier curves and durations, enabling performant CSS-only animations
6
+ * on web that approximate native spring behavior.
7
+ *
8
+ * CSS cannot truly replicate spring physics (damping, stiffness, mass, velocity),
9
+ * but we can create close approximations using cubic-bezier curves and the
10
+ * CSS linear() function for more accurate representations.
11
+ */
12
+
13
+ import type { SpringConfig, BezierEasing } from './types';
14
+ import { easings } from './tokens';
15
+
16
+ /**
17
+ * Result of converting a spring config to CSS-compatible values
18
+ */
19
+ export interface SpringApproximation {
20
+ /** CSS easing string (cubic-bezier or linear()) */
21
+ css: string;
22
+ /** Bezier curve values (for fallback or simpler scenarios) */
23
+ bezier: BezierEasing;
24
+ /** Calculated duration in milliseconds */
25
+ duration: number;
26
+ /** Whether this spring has significant overshoot */
27
+ hasOvershoot: boolean;
28
+ /** Description of the approximation quality */
29
+ quality: 'good' | 'approximate' | 'limited';
30
+ }
31
+
32
+ /**
33
+ * Pre-computed approximations for built-in spring types
34
+ *
35
+ * These are hand-tuned to feel as close as possible to the
36
+ * native spring implementations.
37
+ */
38
+ export const springApproximations: Record<string, SpringApproximation> = {
39
+ /**
40
+ * spring: { damping: 15, stiffness: 200, mass: 1 }
41
+ * Medium bounce, good for toggles and state changes
42
+ * Has noticeable overshoot (~10-15%)
43
+ */
44
+ spring: {
45
+ css: 'cubic-bezier(0.34, 1.4, 0.64, 1)',
46
+ bezier: [0.34, 1.4, 0.64, 1] as BezierEasing,
47
+ duration: 350,
48
+ hasOvershoot: true,
49
+ quality: 'approximate',
50
+ },
51
+
52
+ /**
53
+ * springStiff: { damping: 40, stiffness: 200, mass: 1 }
54
+ * Snappy with minimal overshoot, good for switches
55
+ * Very little bounce, CSS can approximate well
56
+ */
57
+ springStiff: {
58
+ css: 'cubic-bezier(0.22, 0.68, 0, 1)',
59
+ bezier: [0.22, 0.68, 0, 1] as BezierEasing,
60
+ duration: 200,
61
+ hasOvershoot: false,
62
+ quality: 'good',
63
+ },
64
+
65
+ /**
66
+ * springBouncy: { damping: 10, stiffness: 180, mass: 1 }
67
+ * Very bouncy, playful feel
68
+ * Significant overshoot that CSS bezier can't fully replicate
69
+ */
70
+ springBouncy: {
71
+ // Using linear() for better overshoot representation
72
+ // This samples the spring curve at key points
73
+ css: `linear(
74
+ 0, 0.178 9.09%, 0.638 18.18%, 0.935 27.27%,
75
+ 1.105 36.36%, 1.13 40.91%, 1.114 45.45%,
76
+ 1.059 54.55%, 1.012 63.64%, 0.992 72.73%,
77
+ 0.997 81.82%, 1.001 90.91%, 1
78
+ )`,
79
+ bezier: [0.34, 1.56, 0.64, 1] as BezierEasing,
80
+ duration: 500,
81
+ hasOvershoot: true,
82
+ quality: 'approximate',
83
+ },
84
+ };
85
+
86
+ /**
87
+ * Approximate damping ratio from spring config
88
+ *
89
+ * ζ = damping / (2 * sqrt(stiffness * mass))
90
+ * - ζ < 1: Underdamped (oscillates/overshoots)
91
+ * - ζ = 1: Critically damped (fastest without overshoot)
92
+ * - ζ > 1: Overdamped (slow, no overshoot)
93
+ */
94
+ function calculateDampingRatio(config: SpringConfig): number {
95
+ const { damping, stiffness, mass } = config;
96
+ return damping / (2 * Math.sqrt(stiffness * mass));
97
+ }
98
+
99
+ /**
100
+ * Calculate approximate duration for spring to settle
101
+ *
102
+ * Uses the formula: duration ≈ (4 * mass) / damping
103
+ * With adjustments for stiffness and a minimum floor
104
+ */
105
+ function calculateSpringDuration(config: SpringConfig): number {
106
+ const { damping, stiffness, mass } = config;
107
+
108
+ // Natural frequency
109
+ const omega0 = Math.sqrt(stiffness / mass);
110
+
111
+ // Settling time approximation (to 2% of final value)
112
+ // For underdamped: t ≈ 4 / (damping_ratio * omega0)
113
+ // For overdamped: longer settling time
114
+ const dampingRatio = calculateDampingRatio(config);
115
+
116
+ let duration: number;
117
+ if (dampingRatio < 1) {
118
+ // Underdamped - oscillates before settling
119
+ duration = (4 / (dampingRatio * omega0)) * 1000;
120
+ } else {
121
+ // Critically damped or overdamped
122
+ duration = ((4 * mass) / damping) * 1000;
123
+ }
124
+
125
+ // Clamp to reasonable range
126
+ return Math.max(150, Math.min(800, Math.round(duration)));
127
+ }
128
+
129
+ /**
130
+ * Generate bezier approximation for a spring config
131
+ *
132
+ * This creates a cubic-bezier that approximates the spring's
133
+ * velocity profile. For underdamped springs, we simulate
134
+ * overshoot by pushing control points beyond [0,1].
135
+ */
136
+ function generateBezierApproximation(config: SpringConfig): BezierEasing {
137
+ const dampingRatio = calculateDampingRatio(config);
138
+
139
+ if (dampingRatio >= 1) {
140
+ // Critically damped or overdamped - no overshoot
141
+ // Use a deceleration curve
142
+ const decelFactor = Math.min(1, dampingRatio - 0.5);
143
+ return [0.22, 0.68 * decelFactor, 0, 1] as BezierEasing;
144
+ }
145
+
146
+ // Underdamped - has overshoot
147
+ // Calculate overshoot amount (0-1 scale, where 0.3 = 30% overshoot)
148
+ const overshoot = Math.exp((-dampingRatio * Math.PI) / Math.sqrt(1 - dampingRatio * dampingRatio));
149
+
150
+ // Map overshoot to bezier control point
151
+ // Y2 > 1 creates overshoot effect
152
+ const y2 = 1 + Math.min(0.6, overshoot * 0.8);
153
+
154
+ return [0.34, y2, 0.64, 1] as BezierEasing;
155
+ }
156
+
157
+ /**
158
+ * Generate CSS linear() easing for springs with significant bounce
159
+ *
160
+ * linear() allows us to define arbitrary easing curves by sampling
161
+ * points along the spring's motion. This gives better accuracy for
162
+ * bouncy springs than cubic-bezier.
163
+ *
164
+ * @see https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function#linear_easing_function
165
+ */
166
+ function generateLinearEasing(config: SpringConfig, samples: number = 12): string {
167
+ const { damping, stiffness, mass } = config;
168
+ const omega0 = Math.sqrt(stiffness / mass);
169
+ const dampingRatio = calculateDampingRatio(config);
170
+ const duration = calculateSpringDuration(config);
171
+
172
+ // Sample the spring curve
173
+ const points: string[] = [];
174
+
175
+ for (let i = 0; i <= samples; i++) {
176
+ const t = i / samples;
177
+ const time = (t * duration) / 1000; // Convert to seconds
178
+
179
+ let value: number;
180
+ if (dampingRatio < 1) {
181
+ // Underdamped spring equation
182
+ const dampedFreq = omega0 * Math.sqrt(1 - dampingRatio * dampingRatio);
183
+ const envelope = Math.exp(-dampingRatio * omega0 * time);
184
+ value = 1 - envelope * Math.cos(dampedFreq * time);
185
+ } else {
186
+ // Critically damped or overdamped
187
+ value = 1 - Math.exp(-omega0 * time) * (1 + omega0 * time);
188
+ }
189
+
190
+ // Format point (value with optional percentage)
191
+ const percent = Math.round(t * 10000) / 100;
192
+ if (i === 0) {
193
+ points.push('0');
194
+ } else if (i === samples) {
195
+ points.push('1');
196
+ } else {
197
+ points.push(`${value.toFixed(3)} ${percent}%`);
198
+ }
199
+ }
200
+
201
+ return `linear(${points.join(', ')})`;
202
+ }
203
+
204
+ /**
205
+ * Convert a spring configuration to CSS-compatible values
206
+ *
207
+ * Analyzes the spring parameters and generates the best possible
208
+ * CSS approximation, choosing between cubic-bezier and linear()
209
+ * based on the spring's characteristics.
210
+ *
211
+ * @param config - Spring configuration (damping, stiffness, mass)
212
+ * @returns CSS easing string, bezier fallback, and calculated duration
213
+ *
214
+ * @example
215
+ * import { approximateSpring } from '@idealyst/theme/animation';
216
+ *
217
+ * const result = approximateSpring({ damping: 15, stiffness: 200, mass: 1 });
218
+ * // => {
219
+ * // css: 'cubic-bezier(0.34, 1.4, 0.64, 1)',
220
+ * // bezier: [0.34, 1.4, 0.64, 1],
221
+ * // duration: 350,
222
+ * // hasOvershoot: true,
223
+ * // quality: 'approximate'
224
+ * // }
225
+ */
226
+ export function approximateSpring(config: SpringConfig): SpringApproximation {
227
+ const dampingRatio = calculateDampingRatio(config);
228
+ const duration = calculateSpringDuration(config);
229
+ const bezier = generateBezierApproximation(config);
230
+ const hasOvershoot = dampingRatio < 1;
231
+
232
+ // Determine quality and best CSS representation
233
+ let css: string;
234
+ let quality: SpringApproximation['quality'];
235
+
236
+ if (dampingRatio >= 0.9) {
237
+ // Nearly critically damped - bezier is accurate
238
+ css = `cubic-bezier(${bezier.join(', ')})`;
239
+ quality = 'good';
240
+ } else if (dampingRatio >= 0.5) {
241
+ // Moderately underdamped - bezier approximates well
242
+ css = `cubic-bezier(${bezier.join(', ')})`;
243
+ quality = 'approximate';
244
+ } else {
245
+ // Heavily underdamped (very bouncy) - use linear() for accuracy
246
+ css = generateLinearEasing(config);
247
+ quality = 'limited';
248
+ }
249
+
250
+ return {
251
+ css,
252
+ bezier,
253
+ duration,
254
+ hasOvershoot,
255
+ quality,
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Get spring approximation by name or config
261
+ *
262
+ * For named springs (spring, springStiff, springBouncy), returns
263
+ * pre-computed approximations. For custom configs, calculates
264
+ * the approximation on demand.
265
+ *
266
+ * @param spring - Spring name or configuration object
267
+ * @returns CSS-compatible spring approximation
268
+ *
269
+ * @example
270
+ * // Named spring
271
+ * const stiff = getSpringApproximation('springStiff');
272
+ *
273
+ * // Custom config
274
+ * const custom = getSpringApproximation({ damping: 20, stiffness: 300, mass: 1 });
275
+ */
276
+ export function getSpringApproximation(
277
+ spring: keyof typeof springApproximations | SpringConfig
278
+ ): SpringApproximation {
279
+ if (typeof spring === 'string') {
280
+ return springApproximations[spring] ?? springApproximations.spring;
281
+ }
282
+ return approximateSpring(spring);
283
+ }
284
+
285
+ /**
286
+ * Check if a CSS easing uses the linear() function
287
+ *
288
+ * Useful for feature detection since linear() has limited browser support.
289
+ * Falls back to bezier approximation if needed.
290
+ */
291
+ export function usesLinearEasing(css: string): boolean {
292
+ return css.startsWith('linear(');
293
+ }
294
+
295
+ /**
296
+ * Get fallback CSS for browsers without linear() support
297
+ *
298
+ * @param approximation - Spring approximation result
299
+ * @returns CSS cubic-bezier string
300
+ */
301
+ export function getLinearFallback(approximation: SpringApproximation): string {
302
+ return `cubic-bezier(${approximation.bezier.join(', ')})`;
303
+ }
304
+
305
+ /**
306
+ * Check if linear() easing is supported in the current environment
307
+ *
308
+ * linear() was added in Chrome 113, Firefox 112, Safari 17.2
309
+ */
310
+ export function isLinearEasingSupported(): boolean {
311
+ if (typeof CSS === 'undefined' || typeof CSS.supports !== 'function') {
312
+ return false;
313
+ }
314
+ return CSS.supports('animation-timing-function', 'linear(0, 1)');
315
+ }
@@ -15,6 +15,13 @@ import type {
15
15
  AnimationDirection,
16
16
  AnimationFillMode,
17
17
  } from './types';
18
+ import {
19
+ springApproximations,
20
+ getSpringApproximation,
21
+ isLinearEasingSupported,
22
+ getLinearFallback,
23
+ type SpringApproximation,
24
+ } from './springApproximation';
18
25
 
19
26
  /**
20
27
  * Resolve a duration value to milliseconds
@@ -28,16 +35,98 @@ export function resolveDuration(duration: Duration): number {
28
35
 
29
36
  /**
30
37
  * Resolve an easing key to CSS string
38
+ *
39
+ * For spring easings, returns an approximated CSS easing that
40
+ * tries to match the spring's feel as closely as possible.
31
41
  */
32
42
  export function resolveEasing(easing: EasingKey): string {
33
43
  const easingConfig = easings[easing];
34
- // Spring configs don't have CSS equivalent, fall back to ease-out
44
+
45
+ // Handle spring configs with smart approximation
35
46
  if ('damping' in easingConfig) {
36
- return 'ease-out';
47
+ const approx = getSpringApproximation(easing as keyof typeof springApproximations);
48
+
49
+ // Check if linear() is supported for bouncy springs
50
+ if (approx.css.startsWith('linear(') && !isLinearEasingSupported()) {
51
+ return getLinearFallback(approx);
52
+ }
53
+
54
+ return approx.css;
37
55
  }
56
+
38
57
  return easingConfig.css;
39
58
  }
40
59
 
60
+ /**
61
+ * Resolve an easing key to both CSS string and duration
62
+ *
63
+ * For spring easings, this returns the approximated CSS easing
64
+ * along with a calculated duration that matches the spring's
65
+ * settling time. This provides better spring approximation than
66
+ * using resolveEasing alone.
67
+ *
68
+ * @param easing - Easing token key
69
+ * @param baseDuration - Base duration (used for non-spring easings)
70
+ * @returns Object with CSS easing and recommended duration
71
+ *
72
+ * @example
73
+ * // Non-spring easing - uses provided duration
74
+ * resolveEasingWithDuration('easeOut', 200)
75
+ * // => { css: 'ease-out', duration: 200 }
76
+ *
77
+ * // Spring easing - calculates optimal duration
78
+ * resolveEasingWithDuration('spring', 200)
79
+ * // => { css: 'cubic-bezier(0.34, 1.4, 0.64, 1)', duration: 350 }
80
+ */
81
+ export function resolveEasingWithDuration(
82
+ easing: EasingKey,
83
+ baseDuration: number
84
+ ): { css: string; duration: number } {
85
+ const easingConfig = easings[easing];
86
+
87
+ // Handle spring configs with smart approximation
88
+ if ('damping' in easingConfig) {
89
+ const approx = getSpringApproximation(easing as keyof typeof springApproximations);
90
+
91
+ let css = approx.css;
92
+ if (approx.css.startsWith('linear(') && !isLinearEasingSupported()) {
93
+ css = getLinearFallback(approx);
94
+ }
95
+
96
+ return {
97
+ css,
98
+ duration: approx.duration,
99
+ };
100
+ }
101
+
102
+ return {
103
+ css: easingConfig.css,
104
+ duration: baseDuration,
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Check if an easing is a spring type
110
+ */
111
+ export function isSpringEasing(easing: EasingKey): boolean {
112
+ const easingConfig = easings[easing];
113
+ return 'damping' in easingConfig;
114
+ }
115
+
116
+ /**
117
+ * Get detailed spring approximation info for an easing
118
+ *
119
+ * Returns null for non-spring easings. For springs, returns
120
+ * the full approximation data including quality assessment.
121
+ */
122
+ export function getSpringInfo(easing: EasingKey): SpringApproximation | null {
123
+ const easingConfig = easings[easing];
124
+ if (!('damping' in easingConfig)) {
125
+ return null;
126
+ }
127
+ return getSpringApproximation(easing as keyof typeof springApproximations);
128
+ }
129
+
41
130
  /**
42
131
  * Generate a CSS transition string from tokens
43
132
  *
package/src/index.ts CHANGED
@@ -28,6 +28,9 @@ export * from './useResponsiveStyle';
28
28
  // Color scheme utilities
29
29
  export * from './colorScheme';
30
30
 
31
+ // Theme hook
32
+ export { useTheme } from './useTheme';
33
+
31
34
  // Style props hook (platform-specific via .native.ts)
32
35
  export { useStyleProps, type StyleProps } from './useStyleProps';
33
36
 
@@ -0,0 +1,46 @@
1
+ import { useUnistyles } from 'react-native-unistyles';
2
+ import type { Theme } from './theme';
3
+
4
+ /**
5
+ * Hook to access the current theme.
6
+ *
7
+ * Returns the current theme object with full TypeScript inference
8
+ * based on your registered theme type.
9
+ *
10
+ * @returns The current theme object
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { useTheme } from '@idealyst/theme';
15
+ *
16
+ * function MyComponent() {
17
+ * const theme = useTheme();
18
+ *
19
+ * return (
20
+ * <View style={{ backgroundColor: theme.colors.surface.primary }}>
21
+ * <Text style={{ color: theme.colors.text.primary }}>
22
+ * Hello
23
+ * </Text>
24
+ * </View>
25
+ * );
26
+ * }
27
+ * ```
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * // Access intent colors
32
+ * const theme = useTheme();
33
+ * const primaryColor = theme.intents.primary.primary;
34
+ * const successLight = theme.intents.success.light;
35
+ *
36
+ * // Access sizes
37
+ * const buttonPadding = theme.sizes.button.md.paddingHorizontal;
38
+ *
39
+ * // Access radii
40
+ * const borderRadius = theme.radii.md;
41
+ * ```
42
+ */
43
+ export function useTheme(): Theme {
44
+ const { theme } = useUnistyles();
45
+ return theme as Theme;
46
+ }