app-studio 0.6.59 → 0.7.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.
@@ -1072,7 +1072,8 @@ const ThemeProvider = _ref => {
1072
1072
  dark: darkOverride = {},
1073
1073
  light: lightOverride = {},
1074
1074
  children,
1075
- strict = false
1075
+ strict = false,
1076
+ targetWindow
1076
1077
  } = _ref;
1077
1078
  const [themeMode, setThemeMode] = React.useState(initialMode);
1078
1079
  const colorCache = React.useRef(new Map()).current;
@@ -1394,9 +1395,34 @@ const ThemeProvider = _ref => {
1394
1395
  themeMode,
1395
1396
  setThemeMode
1396
1397
  }), [getColor, getColorHex, getColorRGBA, getColorScheme, getContrastColor, mergedTheme, currentColors, themeMode]);
1398
+ // Generate CSS variables
1399
+ const cssVariables = React.useMemo(() => generateCSSVariables(mergedTheme, themeColors.light, themeColors.dark), [mergedTheme, themeColors]);
1400
+ // Inject CSS variables into target document (for iframe support)
1401
+ React.useEffect(() => {
1402
+ if (!targetWindow) return;
1403
+ const targetDoc = targetWindow.document;
1404
+ const styleId = 'app-studio-theme-vars';
1405
+ // Remove existing style tag if any
1406
+ const existing = targetDoc.getElementById(styleId);
1407
+ if (existing) {
1408
+ existing.remove();
1409
+ }
1410
+ // Create and inject new style tag
1411
+ const styleTag = targetDoc.createElement('style');
1412
+ styleTag.id = styleId;
1413
+ styleTag.textContent = cssVariables;
1414
+ targetDoc.head.appendChild(styleTag);
1415
+ // Cleanup on unmount
1416
+ return () => {
1417
+ const styleToRemove = targetDoc.getElementById(styleId);
1418
+ if (styleToRemove) {
1419
+ styleToRemove.remove();
1420
+ }
1421
+ };
1422
+ }, [targetWindow, cssVariables]);
1397
1423
  return /*#__PURE__*/React__default.createElement(ThemeContext.Provider, {
1398
1424
  value: contextValue
1399
- }, /*#__PURE__*/React__default.createElement("style", null, generateCSSVariables(mergedTheme, themeColors.light, themeColors.dark)), /*#__PURE__*/React__default.createElement("div", {
1425
+ }, !targetWindow && /*#__PURE__*/React__default.createElement("style", null, cssVariables), /*#__PURE__*/React__default.createElement("div", {
1400
1426
  "data-theme": themeMode,
1401
1427
  style: {
1402
1428
  backgroundColor: 'white',
@@ -1465,32 +1491,57 @@ const debounce = (func, wait) => {
1465
1491
  };
1466
1492
  // Helper to compute breakpoint from width
1467
1493
  const getBreakpointFromWidth = (width, breakpoints) => {
1468
- // console.log('[ResponsiveProvider] Computing breakpoint for width:', width);
1469
- // console.log('[ResponsiveProvider] Breakpoints config:', breakpoints);
1470
1494
  const sortedBreakpoints = Object.entries(breakpoints).sort((_ref, _ref2) => {
1471
1495
  let [, a] = _ref;
1472
1496
  let [, b] = _ref2;
1473
1497
  return b - a;
1474
1498
  }); // Sort descending by min value
1475
- // console.log('[ResponsiveProvider] Sorted breakpoints:', sortedBreakpoints);
1476
1499
  for (const [name, minWidth] of sortedBreakpoints) {
1477
1500
  if (width >= minWidth) {
1478
- // console.log(
1479
- // '[ResponsiveProvider] ✓ Match found:',
1480
- // name,
1481
- // 'for width',
1482
- // width,
1483
- // '>= minWidth',
1484
- // minWidth
1485
- // );
1486
1501
  return name;
1487
1502
  }
1488
1503
  }
1489
1504
  const fallback = sortedBreakpoints[sortedBreakpoints.length - 1]?.[0] || 'xs';
1490
- // console.log('[ResponsiveProvider] No match, using fallback:', fallback);
1491
1505
  return fallback;
1492
1506
  };
1493
- // Create the Context with default values
1507
+ // ============================================================================
1508
+ // SPLIT CONTEXTS FOR OPTIMIZED RE-RENDERS
1509
+ // ============================================================================
1510
+ // BreakpointContext - changes rarely (only on breakpoint threshold crossing)
1511
+ const defaultBreakpointConfig = {
1512
+ breakpoints: defaultBreakpointsConfig,
1513
+ devices: defaultDeviceConfig,
1514
+ mediaQueries: /*#__PURE__*/getMediaQueries(defaultBreakpointsConfig),
1515
+ currentBreakpoint: 'xs',
1516
+ currentDevice: 'mobile',
1517
+ orientation: 'portrait'
1518
+ };
1519
+ const BreakpointContext = /*#__PURE__*/React.createContext(defaultBreakpointConfig);
1520
+ // WindowDimensionsContext - changes often (on every resize)
1521
+ const defaultWindowDimensions = {
1522
+ width: 0,
1523
+ height: 0
1524
+ };
1525
+ const WindowDimensionsContext = /*#__PURE__*/React.createContext(defaultWindowDimensions);
1526
+ // ============================================================================
1527
+ // HOOKS FOR SPLIT CONTEXTS
1528
+ // ============================================================================
1529
+ /**
1530
+ * Hook to access breakpoint information only.
1531
+ * Components using this hook will NOT re-render on every resize,
1532
+ * only when the breakpoint actually changes.
1533
+ */
1534
+ const useBreakpointContext = () => React.useContext(BreakpointContext);
1535
+ /**
1536
+ * Hook to access window dimensions.
1537
+ * Components using this hook WILL re-render on every resize.
1538
+ * Use sparingly - prefer useBreakpointContext when possible.
1539
+ */
1540
+ const useWindowDimensionsContext = () => React.useContext(WindowDimensionsContext);
1541
+ // ============================================================================
1542
+ // LEGACY CONTEXT FOR BACKWARD COMPATIBILITY
1543
+ // ============================================================================
1544
+ // Create the combined Context with default values (for backward compatibility)
1494
1545
  const ResponsiveContext = /*#__PURE__*/React.createContext({
1495
1546
  breakpoints: defaultBreakpointsConfig,
1496
1547
  devices: defaultDeviceConfig,
@@ -1501,56 +1552,63 @@ const ResponsiveContext = /*#__PURE__*/React.createContext({
1501
1552
  currentDevice: 'mobile',
1502
1553
  orientation: 'portrait'
1503
1554
  });
1504
- // Custom Hook to Access the Responsive Context
1555
+ /**
1556
+ * Legacy hook for backward compatibility.
1557
+ * Prefer useBreakpointContext for better performance.
1558
+ * @deprecated Use useBreakpointContext instead for better performance
1559
+ */
1505
1560
  const useResponsiveContext = () => React.useContext(ResponsiveContext);
1506
1561
  const ResponsiveProvider = _ref3 => {
1507
1562
  let {
1508
1563
  breakpoints = defaultBreakpointsConfig,
1509
1564
  devices = defaultDeviceConfig,
1510
- children
1565
+ children,
1566
+ targetWindow
1511
1567
  } = _ref3;
1568
+ const win = targetWindow || (typeof window !== 'undefined' ? window : null);
1569
+ // Track current breakpoint - only updates when crossing thresholds
1512
1570
  const [screen, setScreen] = React.useState(() => {
1513
- // Initialize with correct breakpoint instead of hardcoded 'xs'
1514
- if (typeof window !== 'undefined') {
1515
- return getBreakpointFromWidth(window.innerWidth, breakpoints);
1571
+ if (win) {
1572
+ return getBreakpointFromWidth(win.innerWidth, breakpoints);
1516
1573
  }
1517
1574
  return 'xs';
1518
1575
  });
1576
+ // Track orientation - rarely changes
1519
1577
  const [orientation, setOrientation] = React.useState('portrait');
1520
- const [size, setSize] = React.useState({
1521
- width: typeof window !== 'undefined' ? window.innerWidth : 0,
1522
- height: typeof window !== 'undefined' ? window.innerHeight : 0
1578
+ // Track window dimensions - changes often
1579
+ const [dimensions, setDimensions] = React.useState({
1580
+ width: win?.innerWidth || 0,
1581
+ height: win?.innerHeight || 0
1523
1582
  });
1583
+ // Use ref to track previous breakpoint to avoid unnecessary state updates
1584
+ const prevBreakpointRef = React.useRef(screen);
1524
1585
  const mediaQueries = React.useMemo(() => getMediaQueries(breakpoints), [breakpoints]);
1525
1586
  React.useEffect(() => {
1526
- // console.log('[ResponsiveProvider] useEffect running - initial setup');
1587
+ if (!win) return;
1527
1588
  // Set initial screen size immediately based on window width
1528
- const initialScreen = getBreakpointFromWidth(window.innerWidth, breakpoints);
1529
- // console.log(
1530
- // '[ResponsiveProvider] Setting initial screen to:',
1531
- // initialScreen
1532
- // );
1589
+ const initialScreen = getBreakpointFromWidth(win.innerWidth, breakpoints);
1533
1590
  setScreen(initialScreen);
1591
+ prevBreakpointRef.current = initialScreen;
1534
1592
  const handleResize = () => {
1535
- const newWidth = window.innerWidth;
1536
- const newHeight = window.innerHeight;
1537
- // console.log('[ResponsiveProvider] Resize event - new dimensions:', {
1538
- // width: newWidth,
1539
- // height: newHeight,
1540
- // });
1541
- setSize({
1593
+ const newWidth = win.innerWidth;
1594
+ const newHeight = win.innerHeight;
1595
+ // Always update dimensions (WindowDimensionsContext will re-render)
1596
+ setDimensions({
1542
1597
  width: newWidth,
1543
1598
  height: newHeight
1544
1599
  });
1545
- // Update screen on resize
1600
+ // Only update breakpoint if it actually changed
1601
+ // This prevents BreakpointContext from causing unnecessary re-renders
1546
1602
  const newScreen = getBreakpointFromWidth(newWidth, breakpoints);
1547
- // console.log('[ResponsiveProvider] Setting screen to:', newScreen);
1548
- setScreen(newScreen);
1603
+ if (newScreen !== prevBreakpointRef.current) {
1604
+ prevBreakpointRef.current = newScreen;
1605
+ setScreen(newScreen);
1606
+ }
1549
1607
  };
1550
1608
  const debouncedResize = debounce(handleResize, 100);
1551
- window.addEventListener('resize', debouncedResize);
1609
+ win.addEventListener('resize', debouncedResize);
1552
1610
  // Set up orientation listener
1553
- const orientationMql = window.matchMedia('(orientation: landscape)');
1611
+ const orientationMql = win.matchMedia('(orientation: landscape)');
1554
1612
  const onOrientationChange = () => setOrientation(orientationMql.matches ? 'landscape' : 'portrait');
1555
1613
  if (orientationMql.addEventListener) {
1556
1614
  orientationMql.addEventListener('change', onOrientationChange);
@@ -1559,30 +1617,46 @@ const ResponsiveProvider = _ref3 => {
1559
1617
  }
1560
1618
  onOrientationChange();
1561
1619
  return () => {
1562
- window.removeEventListener('resize', debouncedResize);
1620
+ win.removeEventListener('resize', debouncedResize);
1563
1621
  if (orientationMql.removeEventListener) {
1564
1622
  orientationMql.removeEventListener('change', onOrientationChange);
1565
1623
  } else {
1566
1624
  orientationMql.removeListener(onOrientationChange);
1567
1625
  }
1568
1626
  };
1569
- }, [breakpoints]); // Removed mediaQueries dep since we now use direct width comparison
1570
- const value = React.useMemo(() => {
1571
- const contextValue = {
1572
- breakpoints,
1573
- devices,
1574
- mediaQueries,
1575
- currentWidth: size.width,
1576
- currentHeight: size.height,
1577
- currentBreakpoint: screen,
1578
- currentDevice: determineCurrentDevice(screen, devices),
1579
- orientation
1580
- };
1581
- return contextValue;
1582
- }, [breakpoints, devices, mediaQueries, size, screen, orientation]);
1583
- return /*#__PURE__*/React__default.createElement(ResponsiveContext.Provider, {
1584
- value: value
1585
- }, children);
1627
+ }, [breakpoints, win]);
1628
+ // Breakpoint context value - only updates when breakpoint/orientation changes
1629
+ const breakpointValue = React.useMemo(() => ({
1630
+ breakpoints,
1631
+ devices,
1632
+ mediaQueries,
1633
+ currentBreakpoint: screen,
1634
+ currentDevice: determineCurrentDevice(screen, devices),
1635
+ orientation
1636
+ }), [breakpoints, devices, mediaQueries, screen, orientation]);
1637
+ // Window dimensions context value - updates on every resize
1638
+ const dimensionsValue = React.useMemo(() => ({
1639
+ width: dimensions.width,
1640
+ height: dimensions.height
1641
+ }), [dimensions.width, dimensions.height]);
1642
+ // Combined legacy context value for backward compatibility
1643
+ const legacyValue = React.useMemo(() => ({
1644
+ breakpoints,
1645
+ devices,
1646
+ mediaQueries,
1647
+ currentWidth: dimensions.width,
1648
+ currentHeight: dimensions.height,
1649
+ currentBreakpoint: screen,
1650
+ currentDevice: determineCurrentDevice(screen, devices),
1651
+ orientation
1652
+ }), [breakpoints, devices, mediaQueries, dimensions, screen, orientation]);
1653
+ return /*#__PURE__*/React__default.createElement(BreakpointContext.Provider, {
1654
+ value: breakpointValue
1655
+ }, /*#__PURE__*/React__default.createElement(WindowDimensionsContext.Provider, {
1656
+ value: dimensionsValue
1657
+ }, /*#__PURE__*/React__default.createElement(ResponsiveContext.Provider, {
1658
+ value: legacyValue
1659
+ }, children)));
1586
1660
  };
1587
1661
 
1588
1662
  const Shadows = {
@@ -1733,7 +1807,9 @@ const cssProperties = /*#__PURE__*/new Set([
1733
1807
  // Borders
1734
1808
  'border', 'borderWidth', 'borderStyle', 'borderColor', 'borderRadius', 'borderTop', 'borderRight', 'borderBottom', 'borderLeft', 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomLeftRadius', 'borderBottomRightRadius',
1735
1809
  // Effects
1736
- 'boxShadow', 'textShadow', 'transform', 'transition', 'animation', 'filter', 'backdropFilter', 'mixBlendMode',
1810
+ 'boxShadow', 'textShadow', 'transform', 'transition', 'animation', 'animationName', 'animationDuration', 'animationTimingFunction', 'animationDelay', 'animationIterationCount', 'animationDirection', 'animationFillMode', 'animationPlayState',
1811
+ // Scroll-driven animation properties (CSS Scroll-Driven Animations spec)
1812
+ 'animationTimeline', 'animationRange', 'animationRangeStart', 'animationRangeEnd', 'scrollTimeline', 'scrollTimelineName', 'scrollTimelineAxis', 'viewTimeline', 'viewTimelineName', 'viewTimelineAxis', 'viewTimelineInset', 'filter', 'backdropFilter', 'mixBlendMode',
1737
1813
  // Layout
1738
1814
  'display', 'visibility', 'overflow', 'overflowX', 'overflowY', 'float', 'clear', 'objectFit', 'objectPosition',
1739
1815
  // Interactivity
@@ -1850,8 +1926,10 @@ const generateKeyframes = animation => {
1850
1926
  playState,
1851
1927
  timeline,
1852
1928
  range,
1853
- ...keyframesDef
1929
+ keyframes: explicitKeyframes,
1930
+ ...rest
1854
1931
  } = animation;
1932
+ const keyframesDef = explicitKeyframes || rest;
1855
1933
  // Générer une clé pour le cache basée sur les keyframes
1856
1934
  const animationConfigString = JSON.stringify(keyframesDef);
1857
1935
  if (keyframesCache.has(animationConfigString)) {
@@ -1878,14 +1956,14 @@ const generateKeyframes = animation => {
1878
1956
  const styles = keyframesDef[key];
1879
1957
  keyframesContent.push(`${cssKey} { ${styleObjectToCss(styles)} }`);
1880
1958
  });
1881
- const keyframes = `
1959
+ const keyframesCss = `
1882
1960
  @keyframes ${keyframesName} {
1883
1961
  ${keyframesContent.join('\n')}
1884
1962
  }
1885
1963
  `;
1886
1964
  return {
1887
1965
  keyframesName,
1888
- keyframes
1966
+ keyframes: keyframesCss
1889
1967
  };
1890
1968
  };
1891
1969
 
@@ -2138,16 +2216,28 @@ const AnimationUtils = {
2138
2216
  (manager || utilityClassManager).injectRule(keyframes);
2139
2217
  }
2140
2218
  result.names.push(keyframesName);
2141
- const durationMs = this.parseDuration(animation.duration || '0s');
2142
- const delayMs = this.parseDuration(animation.delay || '0s');
2143
- const totalDelayMs = cumulativeTime + delayMs;
2144
- cumulativeTime = totalDelayMs + durationMs;
2145
- result.durations.push(this.formatDuration(durationMs));
2219
+ // For scroll-driven animations (with timeline), use 'auto' duration
2220
+ // For time-based animations, parse the duration normally
2221
+ const hasTimeline = !!animation.timeline;
2222
+ if (hasTimeline) {
2223
+ // Scroll-driven animations should use 'auto' duration
2224
+ // unless explicitly specified
2225
+ result.durations.push(animation.duration || 'auto');
2226
+ // Don't accumulate time for scroll-driven animations
2227
+ result.delays.push(animation.delay || '0s');
2228
+ } else {
2229
+ const durationMs = this.parseDuration(animation.duration || '0s');
2230
+ const delayMs = this.parseDuration(animation.delay || '0s');
2231
+ const totalDelayMs = cumulativeTime + delayMs;
2232
+ cumulativeTime = totalDelayMs + durationMs;
2233
+ result.durations.push(this.formatDuration(durationMs));
2234
+ result.delays.push(this.formatDuration(totalDelayMs));
2235
+ }
2146
2236
  result.timingFunctions.push(animation.timingFunction || 'ease');
2147
- result.delays.push(this.formatDuration(totalDelayMs));
2148
2237
  result.iterationCounts.push(animation.iterationCount !== undefined ? `${animation.iterationCount}` : '1');
2149
2238
  result.directions.push(animation.direction || 'normal');
2150
- result.fillModes.push(animation.fillMode || 'none');
2239
+ // Default to 'both' fillMode for scroll-driven animations, 'none' for time-based
2240
+ result.fillModes.push(animation.fillMode || (hasTimeline ? 'both' : 'none'));
2151
2241
  result.playStates.push(animation.playState || 'running');
2152
2242
  result.timelines.push(animation.timeline || '');
2153
2243
  result.ranges.push(animation.range || '');
@@ -2876,6 +2966,50 @@ const AnalyticsProvider = _ref => {
2876
2966
  }, children);
2877
2967
  };
2878
2968
 
2969
+ /**
2970
+ * Computes a stable hash of style-relevant props.
2971
+ * This is used to determine if style extraction needs to be re-run.
2972
+ */
2973
+ function hashStyleProps(props) {
2974
+ // Build a deterministic string representation of style-relevant props
2975
+ const parts = [];
2976
+ const sortedKeys = Object.keys(props).sort();
2977
+ for (const key of sortedKeys) {
2978
+ // Skip non-style props that don't affect CSS generation
2979
+ if (key === 'children' || key === 'ref' || key === 'key') continue;
2980
+ // Include style-relevant props
2981
+ if (isStyleProp(key) || key.startsWith('_') || key === 'on' || key === 'media' || key === 'animate' || key === 'css' || key === 'shadow' || key === 'blend' || key === 'widthHeight' || key === 'paddingHorizontal' || key === 'paddingVertical' || key === 'marginHorizontal' || key === 'marginVertical') {
2982
+ const value = props[key];
2983
+ if (value !== undefined) {
2984
+ // Use JSON.stringify for consistent serialization
2985
+ parts.push(`${key}:${JSON.stringify(value)}`);
2986
+ }
2987
+ }
2988
+ }
2989
+ return hash(parts.join('|'));
2990
+ }
2991
+ /**
2992
+ * Custom hook that memoizes style extraction based on a stable hash of props.
2993
+ * Only recalculates when the hash of style-relevant props changes.
2994
+ */
2995
+ function useStableStyleMemo(propsToProcess, getColor, mediaQueries, devices, manager, theme) {
2996
+ const cacheRef = React.useRef(null);
2997
+ // Compute hash of current props
2998
+ const currentHash = React.useMemo(() => {
2999
+ // Include theme in hash to bust cache on theme changes
3000
+ const themeHash = theme ? JSON.stringify(theme) : '';
3001
+ return hashStyleProps(propsToProcess) + '|' + hash(themeHash);
3002
+ }, [propsToProcess, theme]);
3003
+ // Only recompute classes if hash changed
3004
+ if (!cacheRef.current || cacheRef.current.hash !== currentHash) {
3005
+ const classes = extractUtilityClasses(propsToProcess, getColor, mediaQueries, devices, manager);
3006
+ cacheRef.current = {
3007
+ hash: currentHash,
3008
+ classes
3009
+ };
3010
+ }
3011
+ return cacheRef.current.classes;
3012
+ }
2879
3013
  const Element = /*#__PURE__*/React__default.memo(/*#__PURE__*/React.forwardRef((_ref, ref) => {
2880
3014
  let {
2881
3015
  as = 'div',
@@ -2921,15 +3055,11 @@ const Element = /*#__PURE__*/React__default.memo(/*#__PURE__*/React.forwardRef((
2921
3055
  const {
2922
3056
  mediaQueries,
2923
3057
  devices
2924
- } = useResponsiveContext();
3058
+ } = useBreakpointContext();
2925
3059
  const {
2926
3060
  manager
2927
3061
  } = useStyleRegistry();
2928
3062
  const [isVisible, setIsVisible] = React.useState(false);
2929
- console.log({
2930
- mediaQueries,
2931
- devices
2932
- });
2933
3063
  React.useEffect(() => {
2934
3064
  if (!animateIn) {
2935
3065
  setIsVisible(true);
@@ -2970,15 +3100,16 @@ const Element = /*#__PURE__*/React__default.memo(/*#__PURE__*/React.forwardRef((
2970
3100
  }
2971
3101
  };
2972
3102
  }, [animateOut, manager]);
2973
- const utilityClasses = React.useMemo(() => {
2974
- const propsToProcess = {
3103
+ // Prepare props for processing (apply view/scroll timeline if needed)
3104
+ const propsToProcess = React.useMemo(() => {
3105
+ const processed = {
2975
3106
  ...rest,
2976
3107
  blend
2977
3108
  };
2978
3109
  // Apply view() timeline ONLY if animateOn='View' (not Both or Mount)
2979
- if (animateOn === 'View' && propsToProcess.animate) {
2980
- const animations = Array.isArray(propsToProcess.animate) ? propsToProcess.animate : [propsToProcess.animate];
2981
- propsToProcess.animate = animations.map(anim => {
3110
+ if (animateOn === 'View' && processed.animate) {
3111
+ const animations = Array.isArray(processed.animate) ? processed.animate : [processed.animate];
3112
+ processed.animate = animations.map(anim => {
2982
3113
  // Only add timeline if not already specified
2983
3114
  if (!anim.timeline) {
2984
3115
  return {
@@ -2992,9 +3123,9 @@ const Element = /*#__PURE__*/React__default.memo(/*#__PURE__*/React.forwardRef((
2992
3123
  });
2993
3124
  }
2994
3125
  // Apply scroll() timeline if animateOn='Scroll'
2995
- if (animateOn === 'Scroll' && propsToProcess.animate) {
2996
- const animations = Array.isArray(propsToProcess.animate) ? propsToProcess.animate : [propsToProcess.animate];
2997
- propsToProcess.animate = animations.map(anim => {
3126
+ if (animateOn === 'Scroll' && processed.animate) {
3127
+ const animations = Array.isArray(processed.animate) ? processed.animate : [processed.animate];
3128
+ processed.animate = animations.map(anim => {
2998
3129
  // Only add timeline if not already specified
2999
3130
  if (!anim.timeline) {
3000
3131
  return {
@@ -3006,10 +3137,10 @@ const Element = /*#__PURE__*/React__default.memo(/*#__PURE__*/React.forwardRef((
3006
3137
  return anim;
3007
3138
  });
3008
3139
  }
3009
- return extractUtilityClasses(propsToProcess, color => {
3010
- return getColor(color);
3011
- }, mediaQueries, devices, manager);
3012
- }, [rest, blend, animateOn, mediaQueries, devices, theme, manager]);
3140
+ return processed;
3141
+ }, [rest, blend, animateOn]);
3142
+ // Use hash-based memoization for style extraction
3143
+ const utilityClasses = useStableStyleMemo(propsToProcess, getColor, mediaQueries, devices, manager, theme);
3013
3144
  const newProps = {
3014
3145
  ref: setRef
3015
3146
  };
@@ -4921,20 +5052,23 @@ const WindowSizeContext = /*#__PURE__*/React.createContext({
4921
5052
  });
4922
5053
  const WindowSizeProvider = _ref => {
4923
5054
  let {
4924
- children
5055
+ children,
5056
+ targetWindow
4925
5057
  } = _ref;
5058
+ const win = targetWindow || (typeof window !== 'undefined' ? window : null);
4926
5059
  const [size, setSize] = React.useState({
4927
- width: window.innerWidth,
4928
- height: window.innerHeight
5060
+ width: win?.innerWidth || 0,
5061
+ height: win?.innerHeight || 0
4929
5062
  });
4930
5063
  React.useEffect(() => {
5064
+ if (!win) return;
4931
5065
  const handleResize = () => setSize({
4932
- width: window.innerWidth,
4933
- height: window.innerHeight
5066
+ width: win.innerWidth,
5067
+ height: win.innerHeight
4934
5068
  });
4935
- window.addEventListener('resize', handleResize);
4936
- return () => window.removeEventListener('resize', handleResize);
4937
- }, []);
5069
+ win.addEventListener('resize', handleResize);
5070
+ return () => win.removeEventListener('resize', handleResize);
5071
+ }, [win]);
4938
5072
  return /*#__PURE__*/React__default.createElement(WindowSizeContext.Provider, {
4939
5073
  value: size
4940
5074
  }, children);
@@ -4966,10 +5100,16 @@ function useActive() {
4966
5100
  return [ref, active];
4967
5101
  }
4968
5102
 
4969
- function useClickOutside() {
5103
+ function useClickOutside(options) {
4970
5104
  const [clickedOutside, setClickedOutside] = React.useState(false);
4971
5105
  const ref = React.useRef(null);
5106
+ const {
5107
+ targetWindow
5108
+ } = options || {};
4972
5109
  React.useEffect(() => {
5110
+ const win = targetWindow || (typeof window !== 'undefined' ? window : null);
5111
+ if (!win) return;
5112
+ const doc = win.document;
4973
5113
  const handleClick = e => {
4974
5114
  if (ref.current && !ref.current.contains(e.target)) {
4975
5115
  setClickedOutside(true);
@@ -4977,11 +5117,11 @@ function useClickOutside() {
4977
5117
  setClickedOutside(false);
4978
5118
  }
4979
5119
  };
4980
- document.addEventListener('mousedown', handleClick);
5120
+ doc.addEventListener('mousedown', handleClick);
4981
5121
  return () => {
4982
- document.removeEventListener('mousedown', handleClick);
5122
+ doc.removeEventListener('mousedown', handleClick);
4983
5123
  };
4984
- }, []);
5124
+ }, [targetWindow]);
4985
5125
  return [ref, clickedOutside];
4986
5126
  }
4987
5127
 
@@ -4998,7 +5138,8 @@ function useElementPosition(options) {
4998
5138
  throttleMs = 500,
4999
5139
  trackOnHover = true,
5000
5140
  trackOnScroll = false,
5001
- trackOnResize = false
5141
+ trackOnResize = false,
5142
+ targetWindow
5002
5143
  } = options;
5003
5144
  const elementRef = React.useRef(null);
5004
5145
  const [relation, setRelation] = React.useState(null);
@@ -5009,9 +5150,10 @@ function useElementPosition(options) {
5009
5150
  setRelation(currentRelation => currentRelation === null ? null : null);
5010
5151
  return;
5011
5152
  }
5153
+ const win = targetWindow || element.ownerDocument?.defaultView || window;
5012
5154
  const rect = element.getBoundingClientRect();
5013
- const viewportHeight = window.innerHeight;
5014
- const viewportWidth = window.innerWidth;
5155
+ const viewportHeight = win.innerHeight;
5156
+ const viewportWidth = win.innerWidth;
5015
5157
  // 1. Determine element's general position in viewport
5016
5158
  const elementCenterY = rect.top + rect.height / 2;
5017
5159
  const elementCenterX = rect.left + rect.width / 2;
@@ -5040,7 +5182,7 @@ function useElementPosition(options) {
5040
5182
  }
5041
5183
  return newRelation;
5042
5184
  });
5043
- }, []); // This callback is stable
5185
+ }, [targetWindow]); // This callback is stable
5044
5186
  const throttledUpdate = React.useCallback(() => {
5045
5187
  if (throttleTimerRef.current) {
5046
5188
  clearTimeout(throttleTimerRef.current);
@@ -5058,6 +5200,7 @@ function useElementPosition(options) {
5058
5200
  }
5059
5201
  const element = elementRef.current;
5060
5202
  if (!element) return;
5203
+ const win = targetWindow || element.ownerDocument?.defaultView || window;
5061
5204
  const handler = throttledUpdate;
5062
5205
  const immediateHandler = calculateRelation;
5063
5206
  // Add event listeners based on configuration
@@ -5073,18 +5216,18 @@ function useElementPosition(options) {
5073
5216
  }
5074
5217
  // Scroll events - throttled
5075
5218
  if (trackOnScroll) {
5076
- window.addEventListener('scroll', handler, {
5219
+ win.addEventListener('scroll', handler, {
5077
5220
  passive: true
5078
5221
  });
5079
5222
  cleanupFunctions.push(() => {
5080
- window.removeEventListener('scroll', handler);
5223
+ win.removeEventListener('scroll', handler);
5081
5224
  });
5082
5225
  }
5083
5226
  // Resize events - throttled
5084
5227
  if (trackOnResize) {
5085
- window.addEventListener('resize', handler);
5228
+ win.addEventListener('resize', handler);
5086
5229
  cleanupFunctions.push(() => {
5087
- window.removeEventListener('resize', handler);
5230
+ win.removeEventListener('resize', handler);
5088
5231
  });
5089
5232
  }
5090
5233
  return () => {
@@ -5093,7 +5236,7 @@ function useElementPosition(options) {
5093
5236
  }
5094
5237
  cleanupFunctions.forEach(cleanup => cleanup());
5095
5238
  };
5096
- }, [trackChanges, trackOnHover, trackOnScroll, trackOnResize, throttledUpdate, calculateRelation]);
5239
+ }, [trackChanges, trackOnHover, trackOnScroll, trackOnResize, throttledUpdate, calculateRelation, targetWindow]);
5097
5240
  const manualUpdateRelation = React.useCallback(() => {
5098
5241
  calculateRelation();
5099
5242
  }, [calculateRelation]);
@@ -5168,21 +5311,84 @@ const useMount = callback => {
5168
5311
  function useOnScreen(options) {
5169
5312
  const ref = React.useRef(null);
5170
5313
  const [isOnScreen, setOnScreen] = React.useState(false);
5314
+ const {
5315
+ targetWindow,
5316
+ ...observerOptions
5317
+ } = options || {};
5171
5318
  React.useEffect(() => {
5172
5319
  const node = ref.current;
5173
5320
  if (!node) return;
5321
+ // If targetWindow is provided and root is not explicitly set,
5322
+ // use the target window's document as the root
5323
+ const win = targetWindow || node.ownerDocument?.defaultView || window;
5324
+ const effectiveRoot = observerOptions.root !== undefined ? observerOptions.root : targetWindow ? win.document.documentElement : undefined;
5174
5325
  const observer = new IntersectionObserver(_ref => {
5175
5326
  let [entry] = _ref;
5176
5327
  setOnScreen(entry.isIntersecting);
5177
- }, options);
5328
+ }, {
5329
+ ...observerOptions,
5330
+ root: effectiveRoot
5331
+ });
5178
5332
  observer.observe(node);
5179
5333
  return () => {
5180
5334
  observer.disconnect();
5181
5335
  };
5182
- }, [options]);
5336
+ }, [targetWindow, options]);
5183
5337
  return [ref, isOnScreen];
5184
5338
  }
5185
5339
 
5340
+ /**
5341
+ * Optimized hook for components that only need breakpoint information.
5342
+ * This hook will NOT cause re-renders on every window resize,
5343
+ * only when the breakpoint actually changes.
5344
+ *
5345
+ * Use this hook instead of useResponsive for better performance
5346
+ * when you only need to check breakpoints or device type.
5347
+ *
5348
+ * @example
5349
+ * const { screen, on, is, orientation } = useBreakpoint();
5350
+ * if (on('mobile')) { ... }
5351
+ * if (is('xs')) { ... }
5352
+ */
5353
+ const useBreakpoint = () => {
5354
+ const context = useBreakpointContext();
5355
+ const {
5356
+ currentBreakpoint: screen,
5357
+ orientation,
5358
+ devices
5359
+ } = context;
5360
+ // Helper to check if current screen matches a breakpoint or device
5361
+ const on = s => devices[s] ? devices[s].includes(screen) : s === screen;
5362
+ return {
5363
+ ...context,
5364
+ screen,
5365
+ orientation,
5366
+ on,
5367
+ is: on
5368
+ };
5369
+ };
5370
+ /**
5371
+ * Hook for components that need exact window dimensions.
5372
+ * This hook WILL cause re-renders on every window resize.
5373
+ * Use sparingly - prefer useBreakpoint when possible.
5374
+ *
5375
+ * @example
5376
+ * const { width, height } = useWindowDimensions();
5377
+ */
5378
+ const useWindowDimensions = () => {
5379
+ return useWindowDimensionsContext();
5380
+ };
5381
+ /**
5382
+ * Combined hook that provides both breakpoint info and window dimensions.
5383
+ * This hook WILL cause re-renders on every window resize.
5384
+ *
5385
+ * For better performance, use:
5386
+ * - useBreakpoint() when you only need breakpoint/device info
5387
+ * - useWindowDimensions() when you only need exact dimensions
5388
+ *
5389
+ * @example
5390
+ * const { screen, on, currentWidth, currentHeight } = useResponsive();
5391
+ */
5186
5392
  const useResponsive = () => {
5187
5393
  const context = useResponsiveContext();
5188
5394
  const {
@@ -5198,7 +5404,6 @@ const useResponsive = () => {
5198
5404
  on,
5199
5405
  is: on
5200
5406
  };
5201
- // console.log('[useResponsive] Hook called, returning:', { screen, orientation, 'on(mobile)': on('mobile') });
5202
5407
  return result;
5203
5408
  };
5204
5409
 
@@ -5368,27 +5573,28 @@ const useScrollAnimation = function (ref, options) {
5368
5573
  };
5369
5574
  };
5370
5575
  // Enhanced useSmoothScroll with error handling
5371
- const useSmoothScroll = () => {
5576
+ const useSmoothScroll = targetWindow => {
5372
5577
  return React.useCallback(function (element, offset) {
5373
5578
  if (offset === void 0) {
5374
5579
  offset = 0;
5375
5580
  }
5376
5581
  if (!element) return;
5377
5582
  try {
5378
- const top = element.getBoundingClientRect().top + (window.scrollY || window.pageYOffset) - offset;
5379
- if ('scrollBehavior' in document.documentElement.style) {
5380
- window.scrollTo({
5583
+ const win = targetWindow || element.ownerDocument?.defaultView || window;
5584
+ const top = element.getBoundingClientRect().top + (win.scrollY || win.pageYOffset) - offset;
5585
+ if ('scrollBehavior' in win.document.documentElement.style) {
5586
+ win.scrollTo({
5381
5587
  top,
5382
5588
  behavior: 'smooth'
5383
5589
  });
5384
5590
  } else {
5385
5591
  // Fallback for browsers that don't support smooth scrolling
5386
- window.scrollTo(0, top);
5592
+ win.scrollTo(0, top);
5387
5593
  }
5388
5594
  } catch (error) {
5389
5595
  console.error('Error during smooth scroll:', error);
5390
5596
  }
5391
- }, []);
5597
+ }, [targetWindow]);
5392
5598
  };
5393
5599
  // Enhanced useInfiniteScroll with debouncing
5394
5600
  const useInfiniteScroll = function (callback, options) {
@@ -5432,7 +5638,7 @@ const useInfiniteScroll = function (callback, options) {
5432
5638
  sentinelRef: setSentinel
5433
5639
  };
5434
5640
  };
5435
- const useScrollDirection = function (threshold) {
5641
+ const useScrollDirection = function (threshold, targetWindow) {
5436
5642
  if (threshold === void 0) {
5437
5643
  threshold = 5;
5438
5644
  }
@@ -5441,12 +5647,15 @@ const useScrollDirection = function (threshold) {
5441
5647
  const lastDirection = React.useRef('up');
5442
5648
  const animationFrame = React.useRef();
5443
5649
  const ticking = React.useRef(false);
5444
- const updateDirection = () => {
5445
- const scrollY = window.scrollY || document.documentElement.scrollTop;
5650
+ const updateDirection = React.useCallback(() => {
5651
+ const win = targetWindow || (typeof window !== 'undefined' ? window : null);
5652
+ if (!win) return;
5653
+ const doc = win.document.documentElement;
5654
+ const scrollY = win.scrollY || doc.scrollTop;
5446
5655
  const direction = scrollY > lastScrollY.current ? 'down' : 'up';
5447
5656
  const scrollDelta = Math.abs(scrollY - lastScrollY.current);
5448
5657
  // Vérifier si on est au bas de la page
5449
- const isAtBottom = window.innerHeight + scrollY >= document.documentElement.scrollHeight - 1;
5658
+ const isAtBottom = win.innerHeight + scrollY >= doc.scrollHeight - 1;
5450
5659
  // Logique principale
5451
5660
  if (scrollDelta > threshold || direction === 'down' && isAtBottom) {
5452
5661
  if (direction !== lastDirection.current) {
@@ -5457,8 +5666,10 @@ const useScrollDirection = function (threshold) {
5457
5666
  // Mise à jour de la position avec un minimum de 0
5458
5667
  lastScrollY.current = Math.max(scrollY, 0);
5459
5668
  ticking.current = false;
5460
- };
5669
+ }, [threshold, targetWindow]);
5461
5670
  React.useEffect(() => {
5671
+ const win = targetWindow || (typeof window !== 'undefined' ? window : null);
5672
+ if (!win) return;
5462
5673
  const handleScroll = () => {
5463
5674
  if (!ticking.current) {
5464
5675
  animationFrame.current = requestAnimationFrame(() => {
@@ -5468,16 +5679,16 @@ const useScrollDirection = function (threshold) {
5468
5679
  ticking.current = true;
5469
5680
  }
5470
5681
  };
5471
- window.addEventListener('scroll', handleScroll, {
5682
+ win.addEventListener('scroll', handleScroll, {
5472
5683
  passive: true
5473
5684
  });
5474
5685
  return () => {
5475
- window.removeEventListener('scroll', handleScroll);
5686
+ win.removeEventListener('scroll', handleScroll);
5476
5687
  if (animationFrame.current) {
5477
5688
  cancelAnimationFrame(animationFrame.current);
5478
5689
  }
5479
5690
  };
5480
- }, [threshold]);
5691
+ }, [updateDirection, targetWindow]);
5481
5692
  return scrollDirection;
5482
5693
  };
5483
5694
 
@@ -5486,6 +5697,7 @@ const useWindowSize = () => React.useContext(WindowSizeContext);
5486
5697
  function useInView(options) {
5487
5698
  const {
5488
5699
  triggerOnce = false,
5700
+ targetWindow,
5489
5701
  ...observerOptions
5490
5702
  } = options || {};
5491
5703
  const ref = React.useRef(null);
@@ -5493,6 +5705,10 @@ function useInView(options) {
5493
5705
  React.useEffect(() => {
5494
5706
  const element = ref.current;
5495
5707
  if (!element) return;
5708
+ // If targetWindow is provided and root is not explicitly set,
5709
+ // use the target window's document as the root
5710
+ const win = targetWindow || element.ownerDocument?.defaultView || window;
5711
+ const effectiveRoot = observerOptions.root !== undefined ? observerOptions.root : targetWindow ? win.document.documentElement : undefined;
5496
5712
  const observer = new IntersectionObserver(_ref => {
5497
5713
  let [entry] = _ref;
5498
5714
  if (entry.isIntersecting) {
@@ -5505,18 +5721,121 @@ function useInView(options) {
5505
5721
  // Only update to false if not using triggerOnce
5506
5722
  setInView(false);
5507
5723
  }
5508
- }, observerOptions);
5724
+ }, {
5725
+ ...observerOptions,
5726
+ root: effectiveRoot
5727
+ });
5509
5728
  observer.observe(element);
5510
5729
  return () => {
5511
5730
  observer.disconnect();
5512
5731
  };
5513
- }, [triggerOnce, ...Object.values(observerOptions || {})]);
5732
+ }, [triggerOnce, targetWindow, ...Object.values(observerOptions || {})]);
5514
5733
  return {
5515
5734
  ref,
5516
5735
  inView
5517
5736
  };
5518
5737
  }
5519
5738
 
5739
+ /**
5740
+ * Hook to register an iframe's document with the style manager.
5741
+ * This ensures that all CSS utility classes are automatically injected into the iframe.
5742
+ *
5743
+ * @param iframeRef - Reference to the iframe element
5744
+ *
5745
+ * @example
5746
+ * ```tsx
5747
+ * const iframeRef = useRef<HTMLIFrameElement>(null);
5748
+ * useIframeStyles(iframeRef);
5749
+ *
5750
+ * return <iframe ref={iframeRef} src="/content" />;
5751
+ * ```
5752
+ */
5753
+ function useIframeStyles(iframeRef) {
5754
+ const {
5755
+ manager
5756
+ } = useStyleRegistry();
5757
+ const registeredDocRef = React.useRef(null);
5758
+ React.useEffect(() => {
5759
+ const iframe = iframeRef.current;
5760
+ if (!iframe) return;
5761
+ const registerDocument = () => {
5762
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
5763
+ if (!iframeDoc) return;
5764
+ // Only register if not already registered
5765
+ if (registeredDocRef.current !== iframeDoc) {
5766
+ manager.addDocument(iframeDoc);
5767
+ registeredDocRef.current = iframeDoc;
5768
+ }
5769
+ };
5770
+ // Try to register immediately if document is ready
5771
+ if (iframe.contentDocument) {
5772
+ registerDocument();
5773
+ }
5774
+ // Also register on load event (for external sources)
5775
+ iframe.addEventListener('load', registerDocument);
5776
+ return () => {
5777
+ iframe.removeEventListener('load', registerDocument);
5778
+ // Clean up when unmounting
5779
+ if (registeredDocRef.current) {
5780
+ manager.removeDocument(registeredDocRef.current);
5781
+ registeredDocRef.current = null;
5782
+ }
5783
+ };
5784
+ }, [manager, iframeRef]);
5785
+ }
5786
+ /**
5787
+ * Hook to get an iframe's window and document, and automatically register styles.
5788
+ * This is a convenience hook that combines iframe access with style registration.
5789
+ *
5790
+ * @param iframeRef - Reference to the iframe element
5791
+ * @returns Object containing iframeWindow, iframeDocument, and isLoaded flag
5792
+ *
5793
+ * @example
5794
+ * ```tsx
5795
+ * const iframeRef = useRef<HTMLIFrameElement>(null);
5796
+ * const { iframeWindow, iframeDocument, isLoaded } = useIframe(iframeRef);
5797
+ *
5798
+ * return (
5799
+ * <>
5800
+ * <iframe ref={iframeRef} src="/content" />
5801
+ * {isLoaded && <div>Iframe loaded!</div>}
5802
+ * </>
5803
+ * );
5804
+ * ```
5805
+ */
5806
+ function useIframe(iframeRef) {
5807
+ const [iframeWindow, setIframeWindow] = React.useState(null);
5808
+ const [iframeDocument, setIframeDocument] = React.useState(null);
5809
+ const [isLoaded, setIsLoaded] = React.useState(false);
5810
+ // Register styles
5811
+ useIframeStyles(iframeRef);
5812
+ React.useEffect(() => {
5813
+ const iframe = iframeRef.current;
5814
+ if (!iframe) return;
5815
+ const updateState = () => {
5816
+ const win = iframe.contentWindow;
5817
+ const doc = iframe.contentDocument || win?.document;
5818
+ if (win && doc) {
5819
+ setIframeWindow(win);
5820
+ setIframeDocument(doc);
5821
+ setIsLoaded(true);
5822
+ }
5823
+ };
5824
+ // Try immediately
5825
+ updateState();
5826
+ // Listen for load event
5827
+ iframe.addEventListener('load', updateState);
5828
+ return () => {
5829
+ iframe.removeEventListener('load', updateState);
5830
+ };
5831
+ }, [iframeRef]);
5832
+ return {
5833
+ iframeWindow,
5834
+ iframeDocument,
5835
+ isLoaded
5836
+ };
5837
+ }
5838
+
5520
5839
  /**
5521
5840
  * View Animation Utilities
5522
5841
  *
@@ -6091,6 +6410,7 @@ exports.AnalyticsContext = AnalyticsContext;
6091
6410
  exports.AnalyticsProvider = AnalyticsProvider;
6092
6411
  exports.Animation = Animation;
6093
6412
  exports.AnimationUtils = AnimationUtils;
6413
+ exports.BreakpointContext = BreakpointContext;
6094
6414
  exports.Button = Button;
6095
6415
  exports.Center = Center;
6096
6416
  exports.Div = Div;
@@ -6117,6 +6437,7 @@ exports.UtilityClassManager = UtilityClassManager;
6117
6437
  exports.Vertical = Vertical;
6118
6438
  exports.VerticalResponsive = VerticalResponsive;
6119
6439
  exports.View = View;
6440
+ exports.WindowDimensionsContext = WindowDimensionsContext;
6120
6441
  exports.WindowSizeContext = WindowSizeContext;
6121
6442
  exports.WindowSizeProvider = WindowSizeProvider;
6122
6443
  exports.animateOnView = animateOnView;
@@ -6156,10 +6477,14 @@ exports.slideRightOnView = slideRightOnView;
6156
6477
  exports.slideUpOnView = slideUpOnView;
6157
6478
  exports.useActive = useActive;
6158
6479
  exports.useAnalytics = useAnalytics;
6480
+ exports.useBreakpoint = useBreakpoint;
6481
+ exports.useBreakpointContext = useBreakpointContext;
6159
6482
  exports.useClickOutside = useClickOutside;
6160
6483
  exports.useElementPosition = useElementPosition;
6161
6484
  exports.useFocus = useFocus;
6162
6485
  exports.useHover = useHover;
6486
+ exports.useIframe = useIframe;
6487
+ exports.useIframeStyles = useIframeStyles;
6163
6488
  exports.useInView = useInView;
6164
6489
  exports.useInfiniteScroll = useInfiniteScroll;
6165
6490
  exports.useKeyPress = useKeyPress;
@@ -6174,6 +6499,8 @@ exports.useServerInsertedHTML = useServerInsertedHTML;
6174
6499
  exports.useSmoothScroll = useSmoothScroll;
6175
6500
  exports.useStyleRegistry = useStyleRegistry;
6176
6501
  exports.useTheme = useTheme;
6502
+ exports.useWindowDimensions = useWindowDimensions;
6503
+ exports.useWindowDimensionsContext = useWindowDimensionsContext;
6177
6504
  exports.useWindowSize = useWindowSize;
6178
6505
  exports.utilityClassManager = utilityClassManager;
6179
6506
  exports.viewAnimationPresets = viewAnimationPresets;