app-studio 0.6.59 → 0.6.60

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 = {
@@ -2876,6 +2950,50 @@ const AnalyticsProvider = _ref => {
2876
2950
  }, children);
2877
2951
  };
2878
2952
 
2953
+ /**
2954
+ * Computes a stable hash of style-relevant props.
2955
+ * This is used to determine if style extraction needs to be re-run.
2956
+ */
2957
+ function hashStyleProps(props) {
2958
+ // Build a deterministic string representation of style-relevant props
2959
+ const parts = [];
2960
+ const sortedKeys = Object.keys(props).sort();
2961
+ for (const key of sortedKeys) {
2962
+ // Skip non-style props that don't affect CSS generation
2963
+ if (key === 'children' || key === 'ref' || key === 'key') continue;
2964
+ // Include style-relevant props
2965
+ 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') {
2966
+ const value = props[key];
2967
+ if (value !== undefined) {
2968
+ // Use JSON.stringify for consistent serialization
2969
+ parts.push(`${key}:${JSON.stringify(value)}`);
2970
+ }
2971
+ }
2972
+ }
2973
+ return hash(parts.join('|'));
2974
+ }
2975
+ /**
2976
+ * Custom hook that memoizes style extraction based on a stable hash of props.
2977
+ * Only recalculates when the hash of style-relevant props changes.
2978
+ */
2979
+ function useStableStyleMemo(propsToProcess, getColor, mediaQueries, devices, manager, theme) {
2980
+ const cacheRef = React.useRef(null);
2981
+ // Compute hash of current props
2982
+ const currentHash = React.useMemo(() => {
2983
+ // Include theme in hash to bust cache on theme changes
2984
+ const themeHash = theme ? JSON.stringify(theme) : '';
2985
+ return hashStyleProps(propsToProcess) + '|' + hash(themeHash);
2986
+ }, [propsToProcess, theme]);
2987
+ // Only recompute classes if hash changed
2988
+ if (!cacheRef.current || cacheRef.current.hash !== currentHash) {
2989
+ const classes = extractUtilityClasses(propsToProcess, getColor, mediaQueries, devices, manager);
2990
+ cacheRef.current = {
2991
+ hash: currentHash,
2992
+ classes
2993
+ };
2994
+ }
2995
+ return cacheRef.current.classes;
2996
+ }
2879
2997
  const Element = /*#__PURE__*/React__default.memo(/*#__PURE__*/React.forwardRef((_ref, ref) => {
2880
2998
  let {
2881
2999
  as = 'div',
@@ -2921,15 +3039,11 @@ const Element = /*#__PURE__*/React__default.memo(/*#__PURE__*/React.forwardRef((
2921
3039
  const {
2922
3040
  mediaQueries,
2923
3041
  devices
2924
- } = useResponsiveContext();
3042
+ } = useBreakpointContext();
2925
3043
  const {
2926
3044
  manager
2927
3045
  } = useStyleRegistry();
2928
3046
  const [isVisible, setIsVisible] = React.useState(false);
2929
- console.log({
2930
- mediaQueries,
2931
- devices
2932
- });
2933
3047
  React.useEffect(() => {
2934
3048
  if (!animateIn) {
2935
3049
  setIsVisible(true);
@@ -2970,15 +3084,16 @@ const Element = /*#__PURE__*/React__default.memo(/*#__PURE__*/React.forwardRef((
2970
3084
  }
2971
3085
  };
2972
3086
  }, [animateOut, manager]);
2973
- const utilityClasses = React.useMemo(() => {
2974
- const propsToProcess = {
3087
+ // Prepare props for processing (apply view/scroll timeline if needed)
3088
+ const propsToProcess = React.useMemo(() => {
3089
+ const processed = {
2975
3090
  ...rest,
2976
3091
  blend
2977
3092
  };
2978
3093
  // 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 => {
3094
+ if (animateOn === 'View' && processed.animate) {
3095
+ const animations = Array.isArray(processed.animate) ? processed.animate : [processed.animate];
3096
+ processed.animate = animations.map(anim => {
2982
3097
  // Only add timeline if not already specified
2983
3098
  if (!anim.timeline) {
2984
3099
  return {
@@ -2992,9 +3107,9 @@ const Element = /*#__PURE__*/React__default.memo(/*#__PURE__*/React.forwardRef((
2992
3107
  });
2993
3108
  }
2994
3109
  // 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 => {
3110
+ if (animateOn === 'Scroll' && processed.animate) {
3111
+ const animations = Array.isArray(processed.animate) ? processed.animate : [processed.animate];
3112
+ processed.animate = animations.map(anim => {
2998
3113
  // Only add timeline if not already specified
2999
3114
  if (!anim.timeline) {
3000
3115
  return {
@@ -3006,10 +3121,10 @@ const Element = /*#__PURE__*/React__default.memo(/*#__PURE__*/React.forwardRef((
3006
3121
  return anim;
3007
3122
  });
3008
3123
  }
3009
- return extractUtilityClasses(propsToProcess, color => {
3010
- return getColor(color);
3011
- }, mediaQueries, devices, manager);
3012
- }, [rest, blend, animateOn, mediaQueries, devices, theme, manager]);
3124
+ return processed;
3125
+ }, [rest, blend, animateOn]);
3126
+ // Use hash-based memoization for style extraction
3127
+ const utilityClasses = useStableStyleMemo(propsToProcess, getColor, mediaQueries, devices, manager, theme);
3013
3128
  const newProps = {
3014
3129
  ref: setRef
3015
3130
  };
@@ -4921,20 +5036,23 @@ const WindowSizeContext = /*#__PURE__*/React.createContext({
4921
5036
  });
4922
5037
  const WindowSizeProvider = _ref => {
4923
5038
  let {
4924
- children
5039
+ children,
5040
+ targetWindow
4925
5041
  } = _ref;
5042
+ const win = targetWindow || (typeof window !== 'undefined' ? window : null);
4926
5043
  const [size, setSize] = React.useState({
4927
- width: window.innerWidth,
4928
- height: window.innerHeight
5044
+ width: win?.innerWidth || 0,
5045
+ height: win?.innerHeight || 0
4929
5046
  });
4930
5047
  React.useEffect(() => {
5048
+ if (!win) return;
4931
5049
  const handleResize = () => setSize({
4932
- width: window.innerWidth,
4933
- height: window.innerHeight
5050
+ width: win.innerWidth,
5051
+ height: win.innerHeight
4934
5052
  });
4935
- window.addEventListener('resize', handleResize);
4936
- return () => window.removeEventListener('resize', handleResize);
4937
- }, []);
5053
+ win.addEventListener('resize', handleResize);
5054
+ return () => win.removeEventListener('resize', handleResize);
5055
+ }, [win]);
4938
5056
  return /*#__PURE__*/React__default.createElement(WindowSizeContext.Provider, {
4939
5057
  value: size
4940
5058
  }, children);
@@ -4966,10 +5084,16 @@ function useActive() {
4966
5084
  return [ref, active];
4967
5085
  }
4968
5086
 
4969
- function useClickOutside() {
5087
+ function useClickOutside(options) {
4970
5088
  const [clickedOutside, setClickedOutside] = React.useState(false);
4971
5089
  const ref = React.useRef(null);
5090
+ const {
5091
+ targetWindow
5092
+ } = options || {};
4972
5093
  React.useEffect(() => {
5094
+ const win = targetWindow || (typeof window !== 'undefined' ? window : null);
5095
+ if (!win) return;
5096
+ const doc = win.document;
4973
5097
  const handleClick = e => {
4974
5098
  if (ref.current && !ref.current.contains(e.target)) {
4975
5099
  setClickedOutside(true);
@@ -4977,11 +5101,11 @@ function useClickOutside() {
4977
5101
  setClickedOutside(false);
4978
5102
  }
4979
5103
  };
4980
- document.addEventListener('mousedown', handleClick);
5104
+ doc.addEventListener('mousedown', handleClick);
4981
5105
  return () => {
4982
- document.removeEventListener('mousedown', handleClick);
5106
+ doc.removeEventListener('mousedown', handleClick);
4983
5107
  };
4984
- }, []);
5108
+ }, [targetWindow]);
4985
5109
  return [ref, clickedOutside];
4986
5110
  }
4987
5111
 
@@ -4998,7 +5122,8 @@ function useElementPosition(options) {
4998
5122
  throttleMs = 500,
4999
5123
  trackOnHover = true,
5000
5124
  trackOnScroll = false,
5001
- trackOnResize = false
5125
+ trackOnResize = false,
5126
+ targetWindow
5002
5127
  } = options;
5003
5128
  const elementRef = React.useRef(null);
5004
5129
  const [relation, setRelation] = React.useState(null);
@@ -5009,9 +5134,10 @@ function useElementPosition(options) {
5009
5134
  setRelation(currentRelation => currentRelation === null ? null : null);
5010
5135
  return;
5011
5136
  }
5137
+ const win = targetWindow || element.ownerDocument?.defaultView || window;
5012
5138
  const rect = element.getBoundingClientRect();
5013
- const viewportHeight = window.innerHeight;
5014
- const viewportWidth = window.innerWidth;
5139
+ const viewportHeight = win.innerHeight;
5140
+ const viewportWidth = win.innerWidth;
5015
5141
  // 1. Determine element's general position in viewport
5016
5142
  const elementCenterY = rect.top + rect.height / 2;
5017
5143
  const elementCenterX = rect.left + rect.width / 2;
@@ -5040,7 +5166,7 @@ function useElementPosition(options) {
5040
5166
  }
5041
5167
  return newRelation;
5042
5168
  });
5043
- }, []); // This callback is stable
5169
+ }, [targetWindow]); // This callback is stable
5044
5170
  const throttledUpdate = React.useCallback(() => {
5045
5171
  if (throttleTimerRef.current) {
5046
5172
  clearTimeout(throttleTimerRef.current);
@@ -5058,6 +5184,7 @@ function useElementPosition(options) {
5058
5184
  }
5059
5185
  const element = elementRef.current;
5060
5186
  if (!element) return;
5187
+ const win = targetWindow || element.ownerDocument?.defaultView || window;
5061
5188
  const handler = throttledUpdate;
5062
5189
  const immediateHandler = calculateRelation;
5063
5190
  // Add event listeners based on configuration
@@ -5073,18 +5200,18 @@ function useElementPosition(options) {
5073
5200
  }
5074
5201
  // Scroll events - throttled
5075
5202
  if (trackOnScroll) {
5076
- window.addEventListener('scroll', handler, {
5203
+ win.addEventListener('scroll', handler, {
5077
5204
  passive: true
5078
5205
  });
5079
5206
  cleanupFunctions.push(() => {
5080
- window.removeEventListener('scroll', handler);
5207
+ win.removeEventListener('scroll', handler);
5081
5208
  });
5082
5209
  }
5083
5210
  // Resize events - throttled
5084
5211
  if (trackOnResize) {
5085
- window.addEventListener('resize', handler);
5212
+ win.addEventListener('resize', handler);
5086
5213
  cleanupFunctions.push(() => {
5087
- window.removeEventListener('resize', handler);
5214
+ win.removeEventListener('resize', handler);
5088
5215
  });
5089
5216
  }
5090
5217
  return () => {
@@ -5093,7 +5220,7 @@ function useElementPosition(options) {
5093
5220
  }
5094
5221
  cleanupFunctions.forEach(cleanup => cleanup());
5095
5222
  };
5096
- }, [trackChanges, trackOnHover, trackOnScroll, trackOnResize, throttledUpdate, calculateRelation]);
5223
+ }, [trackChanges, trackOnHover, trackOnScroll, trackOnResize, throttledUpdate, calculateRelation, targetWindow]);
5097
5224
  const manualUpdateRelation = React.useCallback(() => {
5098
5225
  calculateRelation();
5099
5226
  }, [calculateRelation]);
@@ -5168,21 +5295,84 @@ const useMount = callback => {
5168
5295
  function useOnScreen(options) {
5169
5296
  const ref = React.useRef(null);
5170
5297
  const [isOnScreen, setOnScreen] = React.useState(false);
5298
+ const {
5299
+ targetWindow,
5300
+ ...observerOptions
5301
+ } = options || {};
5171
5302
  React.useEffect(() => {
5172
5303
  const node = ref.current;
5173
5304
  if (!node) return;
5305
+ // If targetWindow is provided and root is not explicitly set,
5306
+ // use the target window's document as the root
5307
+ const win = targetWindow || node.ownerDocument?.defaultView || window;
5308
+ const effectiveRoot = observerOptions.root !== undefined ? observerOptions.root : targetWindow ? win.document.documentElement : undefined;
5174
5309
  const observer = new IntersectionObserver(_ref => {
5175
5310
  let [entry] = _ref;
5176
5311
  setOnScreen(entry.isIntersecting);
5177
- }, options);
5312
+ }, {
5313
+ ...observerOptions,
5314
+ root: effectiveRoot
5315
+ });
5178
5316
  observer.observe(node);
5179
5317
  return () => {
5180
5318
  observer.disconnect();
5181
5319
  };
5182
- }, [options]);
5320
+ }, [targetWindow, options]);
5183
5321
  return [ref, isOnScreen];
5184
5322
  }
5185
5323
 
5324
+ /**
5325
+ * Optimized hook for components that only need breakpoint information.
5326
+ * This hook will NOT cause re-renders on every window resize,
5327
+ * only when the breakpoint actually changes.
5328
+ *
5329
+ * Use this hook instead of useResponsive for better performance
5330
+ * when you only need to check breakpoints or device type.
5331
+ *
5332
+ * @example
5333
+ * const { screen, on, is, orientation } = useBreakpoint();
5334
+ * if (on('mobile')) { ... }
5335
+ * if (is('xs')) { ... }
5336
+ */
5337
+ const useBreakpoint = () => {
5338
+ const context = useBreakpointContext();
5339
+ const {
5340
+ currentBreakpoint: screen,
5341
+ orientation,
5342
+ devices
5343
+ } = context;
5344
+ // Helper to check if current screen matches a breakpoint or device
5345
+ const on = s => devices[s] ? devices[s].includes(screen) : s === screen;
5346
+ return {
5347
+ ...context,
5348
+ screen,
5349
+ orientation,
5350
+ on,
5351
+ is: on
5352
+ };
5353
+ };
5354
+ /**
5355
+ * Hook for components that need exact window dimensions.
5356
+ * This hook WILL cause re-renders on every window resize.
5357
+ * Use sparingly - prefer useBreakpoint when possible.
5358
+ *
5359
+ * @example
5360
+ * const { width, height } = useWindowDimensions();
5361
+ */
5362
+ const useWindowDimensions = () => {
5363
+ return useWindowDimensionsContext();
5364
+ };
5365
+ /**
5366
+ * Combined hook that provides both breakpoint info and window dimensions.
5367
+ * This hook WILL cause re-renders on every window resize.
5368
+ *
5369
+ * For better performance, use:
5370
+ * - useBreakpoint() when you only need breakpoint/device info
5371
+ * - useWindowDimensions() when you only need exact dimensions
5372
+ *
5373
+ * @example
5374
+ * const { screen, on, currentWidth, currentHeight } = useResponsive();
5375
+ */
5186
5376
  const useResponsive = () => {
5187
5377
  const context = useResponsiveContext();
5188
5378
  const {
@@ -5198,7 +5388,6 @@ const useResponsive = () => {
5198
5388
  on,
5199
5389
  is: on
5200
5390
  };
5201
- // console.log('[useResponsive] Hook called, returning:', { screen, orientation, 'on(mobile)': on('mobile') });
5202
5391
  return result;
5203
5392
  };
5204
5393
 
@@ -5368,27 +5557,28 @@ const useScrollAnimation = function (ref, options) {
5368
5557
  };
5369
5558
  };
5370
5559
  // Enhanced useSmoothScroll with error handling
5371
- const useSmoothScroll = () => {
5560
+ const useSmoothScroll = targetWindow => {
5372
5561
  return React.useCallback(function (element, offset) {
5373
5562
  if (offset === void 0) {
5374
5563
  offset = 0;
5375
5564
  }
5376
5565
  if (!element) return;
5377
5566
  try {
5378
- const top = element.getBoundingClientRect().top + (window.scrollY || window.pageYOffset) - offset;
5379
- if ('scrollBehavior' in document.documentElement.style) {
5380
- window.scrollTo({
5567
+ const win = targetWindow || element.ownerDocument?.defaultView || window;
5568
+ const top = element.getBoundingClientRect().top + (win.scrollY || win.pageYOffset) - offset;
5569
+ if ('scrollBehavior' in win.document.documentElement.style) {
5570
+ win.scrollTo({
5381
5571
  top,
5382
5572
  behavior: 'smooth'
5383
5573
  });
5384
5574
  } else {
5385
5575
  // Fallback for browsers that don't support smooth scrolling
5386
- window.scrollTo(0, top);
5576
+ win.scrollTo(0, top);
5387
5577
  }
5388
5578
  } catch (error) {
5389
5579
  console.error('Error during smooth scroll:', error);
5390
5580
  }
5391
- }, []);
5581
+ }, [targetWindow]);
5392
5582
  };
5393
5583
  // Enhanced useInfiniteScroll with debouncing
5394
5584
  const useInfiniteScroll = function (callback, options) {
@@ -5432,7 +5622,7 @@ const useInfiniteScroll = function (callback, options) {
5432
5622
  sentinelRef: setSentinel
5433
5623
  };
5434
5624
  };
5435
- const useScrollDirection = function (threshold) {
5625
+ const useScrollDirection = function (threshold, targetWindow) {
5436
5626
  if (threshold === void 0) {
5437
5627
  threshold = 5;
5438
5628
  }
@@ -5441,12 +5631,15 @@ const useScrollDirection = function (threshold) {
5441
5631
  const lastDirection = React.useRef('up');
5442
5632
  const animationFrame = React.useRef();
5443
5633
  const ticking = React.useRef(false);
5444
- const updateDirection = () => {
5445
- const scrollY = window.scrollY || document.documentElement.scrollTop;
5634
+ const updateDirection = React.useCallback(() => {
5635
+ const win = targetWindow || (typeof window !== 'undefined' ? window : null);
5636
+ if (!win) return;
5637
+ const doc = win.document.documentElement;
5638
+ const scrollY = win.scrollY || doc.scrollTop;
5446
5639
  const direction = scrollY > lastScrollY.current ? 'down' : 'up';
5447
5640
  const scrollDelta = Math.abs(scrollY - lastScrollY.current);
5448
5641
  // Vérifier si on est au bas de la page
5449
- const isAtBottom = window.innerHeight + scrollY >= document.documentElement.scrollHeight - 1;
5642
+ const isAtBottom = win.innerHeight + scrollY >= doc.scrollHeight - 1;
5450
5643
  // Logique principale
5451
5644
  if (scrollDelta > threshold || direction === 'down' && isAtBottom) {
5452
5645
  if (direction !== lastDirection.current) {
@@ -5457,8 +5650,10 @@ const useScrollDirection = function (threshold) {
5457
5650
  // Mise à jour de la position avec un minimum de 0
5458
5651
  lastScrollY.current = Math.max(scrollY, 0);
5459
5652
  ticking.current = false;
5460
- };
5653
+ }, [threshold, targetWindow]);
5461
5654
  React.useEffect(() => {
5655
+ const win = targetWindow || (typeof window !== 'undefined' ? window : null);
5656
+ if (!win) return;
5462
5657
  const handleScroll = () => {
5463
5658
  if (!ticking.current) {
5464
5659
  animationFrame.current = requestAnimationFrame(() => {
@@ -5468,16 +5663,16 @@ const useScrollDirection = function (threshold) {
5468
5663
  ticking.current = true;
5469
5664
  }
5470
5665
  };
5471
- window.addEventListener('scroll', handleScroll, {
5666
+ win.addEventListener('scroll', handleScroll, {
5472
5667
  passive: true
5473
5668
  });
5474
5669
  return () => {
5475
- window.removeEventListener('scroll', handleScroll);
5670
+ win.removeEventListener('scroll', handleScroll);
5476
5671
  if (animationFrame.current) {
5477
5672
  cancelAnimationFrame(animationFrame.current);
5478
5673
  }
5479
5674
  };
5480
- }, [threshold]);
5675
+ }, [updateDirection, targetWindow]);
5481
5676
  return scrollDirection;
5482
5677
  };
5483
5678
 
@@ -5486,6 +5681,7 @@ const useWindowSize = () => React.useContext(WindowSizeContext);
5486
5681
  function useInView(options) {
5487
5682
  const {
5488
5683
  triggerOnce = false,
5684
+ targetWindow,
5489
5685
  ...observerOptions
5490
5686
  } = options || {};
5491
5687
  const ref = React.useRef(null);
@@ -5493,6 +5689,10 @@ function useInView(options) {
5493
5689
  React.useEffect(() => {
5494
5690
  const element = ref.current;
5495
5691
  if (!element) return;
5692
+ // If targetWindow is provided and root is not explicitly set,
5693
+ // use the target window's document as the root
5694
+ const win = targetWindow || element.ownerDocument?.defaultView || window;
5695
+ const effectiveRoot = observerOptions.root !== undefined ? observerOptions.root : targetWindow ? win.document.documentElement : undefined;
5496
5696
  const observer = new IntersectionObserver(_ref => {
5497
5697
  let [entry] = _ref;
5498
5698
  if (entry.isIntersecting) {
@@ -5505,18 +5705,121 @@ function useInView(options) {
5505
5705
  // Only update to false if not using triggerOnce
5506
5706
  setInView(false);
5507
5707
  }
5508
- }, observerOptions);
5708
+ }, {
5709
+ ...observerOptions,
5710
+ root: effectiveRoot
5711
+ });
5509
5712
  observer.observe(element);
5510
5713
  return () => {
5511
5714
  observer.disconnect();
5512
5715
  };
5513
- }, [triggerOnce, ...Object.values(observerOptions || {})]);
5716
+ }, [triggerOnce, targetWindow, ...Object.values(observerOptions || {})]);
5514
5717
  return {
5515
5718
  ref,
5516
5719
  inView
5517
5720
  };
5518
5721
  }
5519
5722
 
5723
+ /**
5724
+ * Hook to register an iframe's document with the style manager.
5725
+ * This ensures that all CSS utility classes are automatically injected into the iframe.
5726
+ *
5727
+ * @param iframeRef - Reference to the iframe element
5728
+ *
5729
+ * @example
5730
+ * ```tsx
5731
+ * const iframeRef = useRef<HTMLIFrameElement>(null);
5732
+ * useIframeStyles(iframeRef);
5733
+ *
5734
+ * return <iframe ref={iframeRef} src="/content" />;
5735
+ * ```
5736
+ */
5737
+ function useIframeStyles(iframeRef) {
5738
+ const {
5739
+ manager
5740
+ } = useStyleRegistry();
5741
+ const registeredDocRef = React.useRef(null);
5742
+ React.useEffect(() => {
5743
+ const iframe = iframeRef.current;
5744
+ if (!iframe) return;
5745
+ const registerDocument = () => {
5746
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
5747
+ if (!iframeDoc) return;
5748
+ // Only register if not already registered
5749
+ if (registeredDocRef.current !== iframeDoc) {
5750
+ manager.addDocument(iframeDoc);
5751
+ registeredDocRef.current = iframeDoc;
5752
+ }
5753
+ };
5754
+ // Try to register immediately if document is ready
5755
+ if (iframe.contentDocument) {
5756
+ registerDocument();
5757
+ }
5758
+ // Also register on load event (for external sources)
5759
+ iframe.addEventListener('load', registerDocument);
5760
+ return () => {
5761
+ iframe.removeEventListener('load', registerDocument);
5762
+ // Clean up when unmounting
5763
+ if (registeredDocRef.current) {
5764
+ manager.removeDocument(registeredDocRef.current);
5765
+ registeredDocRef.current = null;
5766
+ }
5767
+ };
5768
+ }, [manager, iframeRef]);
5769
+ }
5770
+ /**
5771
+ * Hook to get an iframe's window and document, and automatically register styles.
5772
+ * This is a convenience hook that combines iframe access with style registration.
5773
+ *
5774
+ * @param iframeRef - Reference to the iframe element
5775
+ * @returns Object containing iframeWindow, iframeDocument, and isLoaded flag
5776
+ *
5777
+ * @example
5778
+ * ```tsx
5779
+ * const iframeRef = useRef<HTMLIFrameElement>(null);
5780
+ * const { iframeWindow, iframeDocument, isLoaded } = useIframe(iframeRef);
5781
+ *
5782
+ * return (
5783
+ * <>
5784
+ * <iframe ref={iframeRef} src="/content" />
5785
+ * {isLoaded && <div>Iframe loaded!</div>}
5786
+ * </>
5787
+ * );
5788
+ * ```
5789
+ */
5790
+ function useIframe(iframeRef) {
5791
+ const [iframeWindow, setIframeWindow] = React.useState(null);
5792
+ const [iframeDocument, setIframeDocument] = React.useState(null);
5793
+ const [isLoaded, setIsLoaded] = React.useState(false);
5794
+ // Register styles
5795
+ useIframeStyles(iframeRef);
5796
+ React.useEffect(() => {
5797
+ const iframe = iframeRef.current;
5798
+ if (!iframe) return;
5799
+ const updateState = () => {
5800
+ const win = iframe.contentWindow;
5801
+ const doc = iframe.contentDocument || win?.document;
5802
+ if (win && doc) {
5803
+ setIframeWindow(win);
5804
+ setIframeDocument(doc);
5805
+ setIsLoaded(true);
5806
+ }
5807
+ };
5808
+ // Try immediately
5809
+ updateState();
5810
+ // Listen for load event
5811
+ iframe.addEventListener('load', updateState);
5812
+ return () => {
5813
+ iframe.removeEventListener('load', updateState);
5814
+ };
5815
+ }, [iframeRef]);
5816
+ return {
5817
+ iframeWindow,
5818
+ iframeDocument,
5819
+ isLoaded
5820
+ };
5821
+ }
5822
+
5520
5823
  /**
5521
5824
  * View Animation Utilities
5522
5825
  *
@@ -6091,6 +6394,7 @@ exports.AnalyticsContext = AnalyticsContext;
6091
6394
  exports.AnalyticsProvider = AnalyticsProvider;
6092
6395
  exports.Animation = Animation;
6093
6396
  exports.AnimationUtils = AnimationUtils;
6397
+ exports.BreakpointContext = BreakpointContext;
6094
6398
  exports.Button = Button;
6095
6399
  exports.Center = Center;
6096
6400
  exports.Div = Div;
@@ -6117,6 +6421,7 @@ exports.UtilityClassManager = UtilityClassManager;
6117
6421
  exports.Vertical = Vertical;
6118
6422
  exports.VerticalResponsive = VerticalResponsive;
6119
6423
  exports.View = View;
6424
+ exports.WindowDimensionsContext = WindowDimensionsContext;
6120
6425
  exports.WindowSizeContext = WindowSizeContext;
6121
6426
  exports.WindowSizeProvider = WindowSizeProvider;
6122
6427
  exports.animateOnView = animateOnView;
@@ -6156,10 +6461,14 @@ exports.slideRightOnView = slideRightOnView;
6156
6461
  exports.slideUpOnView = slideUpOnView;
6157
6462
  exports.useActive = useActive;
6158
6463
  exports.useAnalytics = useAnalytics;
6464
+ exports.useBreakpoint = useBreakpoint;
6465
+ exports.useBreakpointContext = useBreakpointContext;
6159
6466
  exports.useClickOutside = useClickOutside;
6160
6467
  exports.useElementPosition = useElementPosition;
6161
6468
  exports.useFocus = useFocus;
6162
6469
  exports.useHover = useHover;
6470
+ exports.useIframe = useIframe;
6471
+ exports.useIframeStyles = useIframeStyles;
6163
6472
  exports.useInView = useInView;
6164
6473
  exports.useInfiniteScroll = useInfiniteScroll;
6165
6474
  exports.useKeyPress = useKeyPress;
@@ -6174,6 +6483,8 @@ exports.useServerInsertedHTML = useServerInsertedHTML;
6174
6483
  exports.useSmoothScroll = useSmoothScroll;
6175
6484
  exports.useStyleRegistry = useStyleRegistry;
6176
6485
  exports.useTheme = useTheme;
6486
+ exports.useWindowDimensions = useWindowDimensions;
6487
+ exports.useWindowDimensionsContext = useWindowDimensionsContext;
6177
6488
  exports.useWindowSize = useWindowSize;
6178
6489
  exports.utilityClassManager = utilityClassManager;
6179
6490
  exports.viewAnimationPresets = viewAnimationPresets;