@idealyst/theme 1.2.29 → 1.2.31

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.
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Web-specific animation utilities
3
+ *
4
+ * These utilities generate CSS transition and animation strings
5
+ * from animation tokens for use in web stylesheets.
6
+ */
7
+
8
+ import { durations, easings, presets } from './tokens';
9
+ import type { DurationKey, EasingKey, PresetKey } from './tokens';
10
+ import type {
11
+ Duration,
12
+ Keyframes,
13
+ KeyframeAnimationConfig,
14
+ AnimationIterations,
15
+ AnimationDirection,
16
+ AnimationFillMode,
17
+ } from './types';
18
+
19
+ /**
20
+ * Resolve a duration value to milliseconds
21
+ */
22
+ export function resolveDuration(duration: Duration): number {
23
+ if (typeof duration === 'number') {
24
+ return duration;
25
+ }
26
+ return durations[duration];
27
+ }
28
+
29
+ /**
30
+ * Resolve an easing key to CSS string
31
+ */
32
+ export function resolveEasing(easing: EasingKey): string {
33
+ const easingConfig = easings[easing];
34
+ // Spring configs don't have CSS equivalent, fall back to ease-out
35
+ if ('damping' in easingConfig) {
36
+ return 'ease-out';
37
+ }
38
+ return easingConfig.css;
39
+ }
40
+
41
+ /**
42
+ * Generate a CSS transition string from tokens
43
+ *
44
+ * @param properties - CSS property or array of properties to transition
45
+ * @param duration - Duration token key or milliseconds
46
+ * @param easing - Easing token key
47
+ * @returns CSS transition string
48
+ *
49
+ * @example
50
+ * import { cssTransition } from '@idealyst/theme/animation';
51
+ *
52
+ * // Single property
53
+ * cssTransition('opacity', 'normal', 'easeOut')
54
+ * // => "opacity 200ms ease-out"
55
+ *
56
+ * // Multiple properties
57
+ * cssTransition(['opacity', 'transform'], 'fast', 'standard')
58
+ * // => "opacity 100ms cubic-bezier(0.4, 0, 0.2, 1), transform 100ms cubic-bezier(0.4, 0, 0.2, 1)"
59
+ *
60
+ * // With numeric duration
61
+ * cssTransition('background-color', 150, 'ease')
62
+ * // => "background-color 150ms ease"
63
+ */
64
+ export function cssTransition(
65
+ properties: string | string[],
66
+ duration: Duration = 'normal',
67
+ easing: EasingKey = 'ease'
68
+ ): string {
69
+ const props = Array.isArray(properties) ? properties : [properties];
70
+ const durationMs = resolveDuration(duration);
71
+ const easingValue = resolveEasing(easing);
72
+
73
+ return props.map((prop) => `${prop} ${durationMs}ms ${easingValue}`).join(', ');
74
+ }
75
+
76
+ /**
77
+ * Generate a CSS transition string from a preset
78
+ *
79
+ * @param presetName - Preset key from presets
80
+ * @param properties - Optional CSS properties (defaults to 'all')
81
+ * @returns CSS transition string
82
+ *
83
+ * @example
84
+ * import { cssPreset } from '@idealyst/theme/animation';
85
+ *
86
+ * cssPreset('hover')
87
+ * // => "all 100ms ease"
88
+ *
89
+ * cssPreset('fadeIn', 'opacity')
90
+ * // => "opacity 200ms cubic-bezier(0, 0, 0.2, 1)"
91
+ */
92
+ export function cssPreset(presetName: PresetKey, properties: string | string[] = 'all'): string {
93
+ const preset = presets[presetName];
94
+ return cssTransition(properties, preset.duration, preset.easing);
95
+ }
96
+
97
+ /**
98
+ * Generate CSS @keyframes string
99
+ *
100
+ * @param name - Unique name for the keyframes
101
+ * @param frames - Keyframe definitions
102
+ * @returns CSS @keyframes string
103
+ *
104
+ * @example
105
+ * import { cssKeyframes } from '@idealyst/theme/animation';
106
+ *
107
+ * const fadeIn = cssKeyframes('fadeIn', {
108
+ * '0%': { opacity: 0 },
109
+ * '100%': { opacity: 1 },
110
+ * });
111
+ * // => "@keyframes fadeIn { 0% { opacity: 0; } 100% { opacity: 1; } }"
112
+ */
113
+ export function cssKeyframes(name: string, frames: Keyframes): string {
114
+ const frameStrings = Object.entries(frames)
115
+ .map(([key, styles]) => {
116
+ const styleString = Object.entries(styles || {})
117
+ .map(([prop, value]) => {
118
+ // Convert camelCase to kebab-case
119
+ const kebabProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
120
+ return `${kebabProp}: ${value}`;
121
+ })
122
+ .join('; ');
123
+ return `${key} { ${styleString}; }`;
124
+ })
125
+ .join(' ');
126
+
127
+ return `@keyframes ${name} { ${frameStrings} }`;
128
+ }
129
+
130
+ /**
131
+ * Generate CSS animation shorthand string
132
+ *
133
+ * @param name - Animation name (must match @keyframes name)
134
+ * @param config - Animation configuration
135
+ * @returns CSS animation shorthand string
136
+ *
137
+ * @example
138
+ * import { cssAnimation } from '@idealyst/theme/animation';
139
+ *
140
+ * cssAnimation('fadeIn', { duration: 'normal', easing: 'easeOut' })
141
+ * // => "fadeIn 200ms ease-out"
142
+ *
143
+ * cssAnimation('spin', { duration: 'loop', easing: 'linear', iterations: 'infinite' })
144
+ * // => "spin 1000ms linear infinite"
145
+ */
146
+ export function cssAnimation(name: string, config: KeyframeAnimationConfig = {}): string {
147
+ const {
148
+ duration = 'normal',
149
+ easing = 'ease',
150
+ delay = 0,
151
+ iterations = 1,
152
+ direction = 'normal',
153
+ fillMode = 'none',
154
+ } = config;
155
+
156
+ const durationMs = resolveDuration(duration);
157
+ const easingValue = resolveEasing(easing);
158
+
159
+ const parts = [
160
+ name,
161
+ `${durationMs}ms`,
162
+ easingValue,
163
+ delay > 0 ? `${delay}ms` : null,
164
+ iterations !== 1 ? iterations : null,
165
+ direction !== 'normal' ? direction : null,
166
+ fillMode !== 'none' ? fillMode : null,
167
+ ].filter(Boolean);
168
+
169
+ return parts.join(' ');
170
+ }
171
+
172
+ /**
173
+ * CSS @property registration for animatable custom properties
174
+ *
175
+ * This CSS should be injected once into the document head to enable
176
+ * smooth gradient animations using CSS custom properties.
177
+ *
178
+ * @example
179
+ * // Inject in your app's entry point
180
+ * import { gradientPropertyCSS, injectGradientCSS } from '@idealyst/theme/animation';
181
+ *
182
+ * // Option 1: Manual injection
183
+ * const style = document.createElement('style');
184
+ * style.textContent = gradientPropertyCSS;
185
+ * document.head.appendChild(style);
186
+ *
187
+ * // Option 2: Use helper
188
+ * injectGradientCSS();
189
+ */
190
+ export const gradientPropertyCSS = `
191
+ @property --gradient-angle {
192
+ syntax: '<angle>';
193
+ initial-value: 0deg;
194
+ inherits: false;
195
+ }
196
+
197
+ @property --gradient-position {
198
+ syntax: '<percentage>';
199
+ initial-value: 0%;
200
+ inherits: false;
201
+ }
202
+
203
+ @keyframes spin-gradient {
204
+ from { --gradient-angle: 0deg; }
205
+ to { --gradient-angle: 360deg; }
206
+ }
207
+
208
+ @keyframes pulse-gradient {
209
+ 0%, 100% { opacity: 1; }
210
+ 50% { opacity: 0.5; }
211
+ }
212
+
213
+ @keyframes wave-gradient {
214
+ from { --gradient-position: -100%; }
215
+ to { --gradient-position: 200%; }
216
+ }
217
+ `;
218
+
219
+ let gradientCSSInjected = false;
220
+
221
+ /**
222
+ * Inject gradient CSS into document head (idempotent)
223
+ *
224
+ * Call this once in your app to enable gradient border animations.
225
+ * Safe to call multiple times - will only inject once.
226
+ */
227
+ export function injectGradientCSS(): void {
228
+ if (typeof document === 'undefined' || gradientCSSInjected) {
229
+ return;
230
+ }
231
+
232
+ const style = document.createElement('style');
233
+ style.id = 'idealyst-gradient-animations';
234
+ style.textContent = gradientPropertyCSS;
235
+ document.head.appendChild(style);
236
+ gradientCSSInjected = true;
237
+ }
238
+
239
+ /**
240
+ * Generate conic gradient border spinner styles
241
+ *
242
+ * @param colors - Array of gradient colors
243
+ * @param borderWidth - Border thickness in pixels
244
+ * @param duration - Animation duration
245
+ * @returns Style object for animated gradient border
246
+ *
247
+ * @example
248
+ * import { conicSpinnerStyle } from '@idealyst/theme/animation';
249
+ *
250
+ * const spinnerStyle = conicSpinnerStyle(
251
+ * ['#3b82f6', '#8b5cf6', '#ec4899'],
252
+ * 2,
253
+ * 2000
254
+ * );
255
+ */
256
+ export function conicSpinnerStyle(
257
+ colors: string[],
258
+ borderWidth: number = 2,
259
+ duration: Duration = 2000
260
+ ): Record<string, string | number> {
261
+ const durationMs = resolveDuration(duration);
262
+ const colorString = colors.join(', ');
263
+
264
+ return {
265
+ background: `conic-gradient(from var(--gradient-angle), ${colorString})`,
266
+ animation: `spin-gradient ${durationMs}ms linear infinite`,
267
+ // Mask technique for border-only effect
268
+ WebkitMask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
269
+ WebkitMaskComposite: 'xor',
270
+ maskComposite: 'exclude',
271
+ padding: borderWidth,
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Generate pulse gradient styles
277
+ *
278
+ * @param colors - Array of gradient colors
279
+ * @param duration - Animation duration
280
+ * @returns Style object for pulsing gradient
281
+ */
282
+ export function pulseGradientStyle(
283
+ colors: string[],
284
+ duration: Duration = 'slowest'
285
+ ): Record<string, string | number> {
286
+ const durationMs = resolveDuration(duration);
287
+ const colorString = colors.join(', ');
288
+
289
+ return {
290
+ background: `linear-gradient(135deg, ${colorString})`,
291
+ animation: `pulse-gradient ${durationMs}ms ease-in-out infinite`,
292
+ };
293
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Animation type definitions for @idealyst/theme
3
+ */
4
+
5
+ import type { durations, easings } from './tokens';
6
+
7
+ // Duration types
8
+ export type DurationKey = keyof typeof durations;
9
+ export type Duration = DurationKey | number;
10
+
11
+ // Easing types
12
+ export type EasingKey = keyof typeof easings;
13
+ export type BezierEasing = readonly [number, number, number, number];
14
+ export type SpringConfig = {
15
+ readonly damping: number;
16
+ readonly stiffness: number;
17
+ readonly mass: number;
18
+ };
19
+
20
+ export type CSSEasing = {
21
+ readonly css: string;
22
+ readonly bezier: BezierEasing;
23
+ };
24
+
25
+ export type Easing = CSSEasing | SpringConfig;
26
+
27
+ // Animation configuration
28
+ export interface AnimationConfig {
29
+ duration?: Duration;
30
+ easing?: EasingKey;
31
+ delay?: number;
32
+ }
33
+
34
+ // Transition configuration for CSS
35
+ export interface TransitionConfig extends AnimationConfig {
36
+ properties?: string | string[];
37
+ }
38
+
39
+ // Keyframe types
40
+ export type KeyframeStyles = Record<string, React.CSSProperties>;
41
+ export type KeyframePercentage = `${number}%` | 'from' | 'to';
42
+ export type Keyframes = Partial<Record<KeyframePercentage, React.CSSProperties>>;
43
+
44
+ // Animation iteration
45
+ export type AnimationIterations = number | 'infinite';
46
+ export type AnimationDirection = 'normal' | 'reverse' | 'alternate' | 'alternate-reverse';
47
+ export type AnimationFillMode = 'none' | 'forwards' | 'backwards' | 'both';
48
+
49
+ // Full animation configuration
50
+ export interface KeyframeAnimationConfig extends AnimationConfig {
51
+ iterations?: AnimationIterations;
52
+ direction?: AnimationDirection;
53
+ fillMode?: AnimationFillMode;
54
+ }
55
+
56
+ // Spring types for native
57
+ export type SpringType = 'spring' | 'springStiff' | 'springBouncy';
58
+
59
+ // Timing config for Reanimated
60
+ export interface TimingAnimationConfig {
61
+ duration: number;
62
+ easing: BezierEasing;
63
+ }
64
+
65
+ // Platform-specific options
66
+ export interface PlatformAnimationOptions {
67
+ web?: Partial<TransitionConfig> & { transition?: string };
68
+ native?: Partial<AnimationConfig> & Partial<SpringConfig>;
69
+ }
70
+
71
+ // Gradient animation types
72
+ export type GradientAnimation = 'spin' | 'pulse' | 'wave';
73
+
74
+ export interface GradientBorderConfig {
75
+ colors: string[];
76
+ borderWidth?: number;
77
+ borderRadius?: number;
78
+ duration?: Duration;
79
+ animation?: GradientAnimation;
80
+ }
@@ -210,6 +210,14 @@ function expandIterators(t, callback, themeParam, keys, verbose, expandedVariant
210
210
  }
211
211
  }
212
212
 
213
+ // Look for compoundVariants: [ { type: 'filled', selected: true, styles: { ... } }, ... ]
214
+ if (t.isObjectProperty(node) && t.isIdentifier(node.key, { name: 'compoundVariants' })) {
215
+ if (t.isArrayExpression(node.value)) {
216
+ const expanded = expandCompoundVariantsArray(t, node.value, themeParam, keys, verbose, expandedVariants);
217
+ return t.objectProperty(node.key, expanded);
218
+ }
219
+ }
220
+
213
221
  if (t.isObjectExpression(node)) {
214
222
  return t.objectExpression(
215
223
  node.properties.map(prop => processNode(prop, depth + 1))
@@ -352,6 +360,112 @@ function expandVariantsObject(t, variantsObj, themeParam, keys, verbose, expande
352
360
  return t.objectExpression(newProperties);
353
361
  }
354
362
 
363
+ /**
364
+ * Expand $iterator patterns in compoundVariants arrays.
365
+ *
366
+ * Input:
367
+ * compoundVariants: [
368
+ * { type: 'filled', selected: true, styles: { backgroundColor: theme.$intents.contrast } }
369
+ * ]
370
+ *
371
+ * Output (for intents = ['primary', 'success', 'danger', ...]):
372
+ * compoundVariants: [
373
+ * { type: 'filled', selected: true, intent: 'primary', styles: { backgroundColor: theme.intents.primary.contrast } },
374
+ * { type: 'filled', selected: true, intent: 'success', styles: { backgroundColor: theme.intents.success.contrast } },
375
+ * { type: 'filled', selected: true, intent: 'danger', styles: { backgroundColor: theme.intents.danger.contrast } },
376
+ * ...
377
+ * ]
378
+ */
379
+ function expandCompoundVariantsArray(t, arrayNode, themeParam, keys, verbose, expandedVariants) {
380
+ const newElements = [];
381
+
382
+ verbose(` expandCompoundVariantsArray: processing ${arrayNode.elements?.length || 0} compound variants`);
383
+
384
+ for (const element of arrayNode.elements) {
385
+ if (!t.isObjectExpression(element)) {
386
+ newElements.push(element);
387
+ continue;
388
+ }
389
+
390
+ // Find the 'styles' property in this compound variant entry
391
+ let stylesProperty = null;
392
+ const otherProperties = [];
393
+
394
+ for (const prop of element.properties) {
395
+ if (t.isObjectProperty(prop)) {
396
+ const keyName = t.isIdentifier(prop.key) ? prop.key.name :
397
+ t.isStringLiteral(prop.key) ? prop.key.value : null;
398
+ if (keyName === 'styles') {
399
+ stylesProperty = prop;
400
+ } else {
401
+ otherProperties.push(prop);
402
+ }
403
+ } else {
404
+ otherProperties.push(prop);
405
+ }
406
+ }
407
+
408
+ if (!stylesProperty) {
409
+ // No styles property, keep as-is
410
+ newElements.push(element);
411
+ continue;
412
+ }
413
+
414
+ // Check if the styles object contains $iterator patterns
415
+ const iteratorInfo = findIteratorPattern(t, stylesProperty.value, themeParam);
416
+
417
+ if (!iteratorInfo) {
418
+ // No $iterator pattern, keep as-is
419
+ newElements.push(element);
420
+ continue;
421
+ }
422
+
423
+ verbose(` Found $iterator in compoundVariant styles: ${iteratorInfo.type}`);
424
+
425
+ // Get keys to expand
426
+ let keysToExpand = [];
427
+ let variantKeyName = 'intent'; // Default for intents
428
+
429
+ if (iteratorInfo.type === 'intents') {
430
+ keysToExpand = keys?.intents || [];
431
+ variantKeyName = 'intent';
432
+ } else if (iteratorInfo.type === 'typography') {
433
+ keysToExpand = keys?.typography || [];
434
+ variantKeyName = 'typography';
435
+ } else if (iteratorInfo.type === 'sizes' && iteratorInfo.componentName) {
436
+ keysToExpand = keys?.sizes?.[iteratorInfo.componentName] || [];
437
+ variantKeyName = 'size';
438
+ }
439
+
440
+ if (keysToExpand.length === 0) {
441
+ // No keys to expand, keep as-is
442
+ newElements.push(element);
443
+ continue;
444
+ }
445
+
446
+ verbose(` Expanding compoundVariant for ${keysToExpand.length} ${variantKeyName} keys`);
447
+
448
+ // Expand this compound variant for each key
449
+ for (const key of keysToExpand) {
450
+ // Replace $iterator refs in the styles
451
+ const expandedStyles = replaceIteratorRefs(t, stylesProperty.value, themeParam, iteratorInfo, key);
452
+
453
+ // Create new compound variant entry with the variant key added as a condition
454
+ const newProps = [
455
+ ...otherProperties.map(p => t.cloneDeep(p)),
456
+ t.objectProperty(t.identifier(variantKeyName), t.stringLiteral(key)),
457
+ t.objectProperty(t.identifier('styles'), expandedStyles),
458
+ ];
459
+
460
+ newElements.push(t.objectExpression(newProps));
461
+ }
462
+
463
+ expandedVariants.push({ variant: 'compoundVariants', iterator: iteratorInfo.type });
464
+ }
465
+
466
+ return t.arrayExpression(newElements);
467
+ }
468
+
355
469
  function findIteratorPattern(t, node, themeParam, debugLog = () => {}) {
356
470
  let result = null;
357
471
 
@@ -578,8 +692,6 @@ module.exports = function idealystStylesPlugin({ types: t }) {
578
692
  }
579
693
  }
580
694
 
581
- // Plugin initialization logged in debug mode only
582
-
583
695
  return {
584
696
  name: 'idealyst-styles',
585
697