app-studio 0.6.58 → 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.
@@ -900,7 +900,8 @@ const ThemeContext = /*#__PURE__*/React.createContext({
900
900
  getColor: () => '',
901
901
  getColorHex: () => '',
902
902
  getColorRGBA: () => '',
903
- getColorScheme: () => 'light',
903
+ getColorScheme: () => undefined,
904
+ getContrastColor: () => 'black',
904
905
  theme: {},
905
906
  colors: {
906
907
  main: defaultLightColors,
@@ -1071,7 +1072,8 @@ const ThemeProvider = _ref => {
1071
1072
  dark: darkOverride = {},
1072
1073
  light: lightOverride = {},
1073
1074
  children,
1074
- strict = false
1075
+ strict = false,
1076
+ targetWindow
1075
1077
  } = _ref;
1076
1078
  const [themeMode, setThemeMode] = React.useState(initialMode);
1077
1079
  const colorCache = React.useRef(new Map()).current;
@@ -1292,23 +1294,135 @@ const ThemeProvider = _ref => {
1292
1294
  if (!override) colorCache.set(cacheKey, rgba);
1293
1295
  return rgba;
1294
1296
  }, [themeMode, colorCache, resolveColorTokenForMode]);
1295
- const getColorScheme = React.useCallback(override => {
1296
- return override?.themeMode ?? themeMode;
1297
- }, [themeMode]);
1297
+ const getColorScheme = React.useCallback((name, override) => {
1298
+ if (!name || typeof name !== 'string') return undefined;
1299
+ const effectiveMode = override?.themeMode ?? themeMode;
1300
+ const effectiveTheme = override?.theme ? deepMerge(mergedTheme, override.theme) : mergedTheme;
1301
+ // Resolve theme.* tokens to get the underlying color token
1302
+ let colorToken = name;
1303
+ if (name.startsWith(THEME_PREFIX)) {
1304
+ const themeKey = name.substring(THEME_PREFIX.length);
1305
+ const themeValue = effectiveTheme[themeKey];
1306
+ if (typeof themeValue === 'string') {
1307
+ colorToken = themeValue;
1308
+ }
1309
+ }
1310
+ // Handle light.* or dark.* prefixes
1311
+ if (colorToken.startsWith('light.') || colorToken.startsWith('dark.')) {
1312
+ const prefixLength = colorToken.startsWith('light.') ? 6 : 5;
1313
+ colorToken = `${COLOR_PREFIX}${colorToken.substring(prefixLength)}`;
1314
+ }
1315
+ // Extract color scheme from color.* tokens (e.g., color.blue.500 -> 'blue')
1316
+ if (colorToken.startsWith(COLOR_PREFIX)) {
1317
+ const keys = colorToken.substring(COLOR_PREFIX.length).split('.');
1318
+ if (keys.length >= 1) {
1319
+ return keys[0]; // Return the color scheme name (e.g., 'blue', 'pink')
1320
+ }
1321
+ }
1322
+ // Handle hex or rgba colors by finding the closest match in the palette
1323
+ const normalizedInput = normalizeToHex(colorToken).toLowerCase();
1324
+ if (normalizedInput.startsWith('#')) {
1325
+ const colorsToUse = themeColors[effectiveMode];
1326
+ const palette = deepMerge(colorsToUse.palette, override?.colors?.palette || {});
1327
+ const main = deepMerge(colorsToUse.main, override?.colors?.main || {});
1328
+ // First check main colors for exact match
1329
+ for (const [colorName, colorValue] of Object.entries(main)) {
1330
+ if (typeof colorValue === 'string') {
1331
+ const normalizedPalette = normalizeToHex(colorValue).toLowerCase();
1332
+ if (normalizedPalette === normalizedInput) {
1333
+ return colorName;
1334
+ }
1335
+ }
1336
+ }
1337
+ // Then check palette colors for exact match
1338
+ for (const [colorName, shades] of Object.entries(palette)) {
1339
+ if (typeof shades === 'object' && shades !== null) {
1340
+ for (const [, shadeValue] of Object.entries(shades)) {
1341
+ if (typeof shadeValue === 'string') {
1342
+ const normalizedPalette = normalizeToHex(shadeValue).toLowerCase();
1343
+ if (normalizedPalette === normalizedInput) {
1344
+ return colorName;
1345
+ }
1346
+ }
1347
+ }
1348
+ }
1349
+ }
1350
+ }
1351
+ return undefined;
1352
+ }, [mergedTheme, themeMode, themeColors]);
1353
+ const getContrastColor = React.useCallback((name, override) => {
1354
+ if (!name || typeof name !== 'string') return 'black';
1355
+ const effectiveMode = override?.themeMode ?? themeMode;
1356
+ // First resolve the color to a hex value
1357
+ let hexColor;
1358
+ // Check if it's already a hex or rgb color
1359
+ if (name.startsWith('#') || name.startsWith('rgb')) {
1360
+ hexColor = normalizeToHex(name);
1361
+ } else {
1362
+ // Resolve the token to get the actual color value
1363
+ const resolved = resolveColorTokenForMode(name, effectiveMode, override);
1364
+ hexColor = normalizeToHex(resolved);
1365
+ }
1366
+ // If we couldn't get a valid hex, default to black
1367
+ if (!hexColor.startsWith('#') || hexColor.length < 7) {
1368
+ return 'black';
1369
+ }
1370
+ // Extract RGB values
1371
+ const hex = hexColor.slice(1);
1372
+ const r = parseInt(hex.slice(0, 2), 16);
1373
+ const g = parseInt(hex.slice(2, 4), 16);
1374
+ const b = parseInt(hex.slice(4, 6), 16);
1375
+ // Calculate relative luminance using the sRGB formula
1376
+ // https://www.w3.org/TR/WCAG20/#relativeluminancedef
1377
+ const toLinear = c => {
1378
+ const sRGB = c / 255;
1379
+ return sRGB <= 0.03928 ? sRGB / 12.92 : Math.pow((sRGB + 0.055) / 1.055, 2.4);
1380
+ };
1381
+ const luminance = 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
1382
+ // Use threshold of 0.179 (WCAG recommendation)
1383
+ // Return white for dark colors, black for light colors
1384
+ return luminance > 0.179 ? 'black' : 'white';
1385
+ }, [themeMode, resolveColorTokenForMode]);
1298
1386
  // --- Memoize Context Value ---
1299
1387
  const contextValue = React.useMemo(() => ({
1300
1388
  getColor,
1301
1389
  getColorHex,
1302
1390
  getColorRGBA,
1303
1391
  getColorScheme,
1392
+ getContrastColor,
1304
1393
  theme: mergedTheme,
1305
1394
  colors: currentColors,
1306
1395
  themeMode,
1307
1396
  setThemeMode
1308
- }), [getColor, getColorHex, getColorRGBA, getColorScheme, mergedTheme, currentColors, themeMode]);
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]);
1309
1423
  return /*#__PURE__*/React__default.createElement(ThemeContext.Provider, {
1310
1424
  value: contextValue
1311
- }, /*#__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", {
1312
1426
  "data-theme": themeMode,
1313
1427
  style: {
1314
1428
  backgroundColor: 'white',
@@ -1377,32 +1491,57 @@ const debounce = (func, wait) => {
1377
1491
  };
1378
1492
  // Helper to compute breakpoint from width
1379
1493
  const getBreakpointFromWidth = (width, breakpoints) => {
1380
- // console.log('[ResponsiveProvider] Computing breakpoint for width:', width);
1381
- // console.log('[ResponsiveProvider] Breakpoints config:', breakpoints);
1382
1494
  const sortedBreakpoints = Object.entries(breakpoints).sort((_ref, _ref2) => {
1383
1495
  let [, a] = _ref;
1384
1496
  let [, b] = _ref2;
1385
1497
  return b - a;
1386
1498
  }); // Sort descending by min value
1387
- // console.log('[ResponsiveProvider] Sorted breakpoints:', sortedBreakpoints);
1388
1499
  for (const [name, minWidth] of sortedBreakpoints) {
1389
1500
  if (width >= minWidth) {
1390
- // console.log(
1391
- // '[ResponsiveProvider] ✓ Match found:',
1392
- // name,
1393
- // 'for width',
1394
- // width,
1395
- // '>= minWidth',
1396
- // minWidth
1397
- // );
1398
1501
  return name;
1399
1502
  }
1400
1503
  }
1401
1504
  const fallback = sortedBreakpoints[sortedBreakpoints.length - 1]?.[0] || 'xs';
1402
- // console.log('[ResponsiveProvider] No match, using fallback:', fallback);
1403
1505
  return fallback;
1404
1506
  };
1405
- // 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)
1406
1545
  const ResponsiveContext = /*#__PURE__*/React.createContext({
1407
1546
  breakpoints: defaultBreakpointsConfig,
1408
1547
  devices: defaultDeviceConfig,
@@ -1413,56 +1552,63 @@ const ResponsiveContext = /*#__PURE__*/React.createContext({
1413
1552
  currentDevice: 'mobile',
1414
1553
  orientation: 'portrait'
1415
1554
  });
1416
- // 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
+ */
1417
1560
  const useResponsiveContext = () => React.useContext(ResponsiveContext);
1418
1561
  const ResponsiveProvider = _ref3 => {
1419
1562
  let {
1420
1563
  breakpoints = defaultBreakpointsConfig,
1421
1564
  devices = defaultDeviceConfig,
1422
- children
1565
+ children,
1566
+ targetWindow
1423
1567
  } = _ref3;
1568
+ const win = targetWindow || (typeof window !== 'undefined' ? window : null);
1569
+ // Track current breakpoint - only updates when crossing thresholds
1424
1570
  const [screen, setScreen] = React.useState(() => {
1425
- // Initialize with correct breakpoint instead of hardcoded 'xs'
1426
- if (typeof window !== 'undefined') {
1427
- return getBreakpointFromWidth(window.innerWidth, breakpoints);
1571
+ if (win) {
1572
+ return getBreakpointFromWidth(win.innerWidth, breakpoints);
1428
1573
  }
1429
1574
  return 'xs';
1430
1575
  });
1576
+ // Track orientation - rarely changes
1431
1577
  const [orientation, setOrientation] = React.useState('portrait');
1432
- const [size, setSize] = React.useState({
1433
- width: typeof window !== 'undefined' ? window.innerWidth : 0,
1434
- 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
1435
1582
  });
1583
+ // Use ref to track previous breakpoint to avoid unnecessary state updates
1584
+ const prevBreakpointRef = React.useRef(screen);
1436
1585
  const mediaQueries = React.useMemo(() => getMediaQueries(breakpoints), [breakpoints]);
1437
1586
  React.useEffect(() => {
1438
- // console.log('[ResponsiveProvider] useEffect running - initial setup');
1587
+ if (!win) return;
1439
1588
  // Set initial screen size immediately based on window width
1440
- const initialScreen = getBreakpointFromWidth(window.innerWidth, breakpoints);
1441
- // console.log(
1442
- // '[ResponsiveProvider] Setting initial screen to:',
1443
- // initialScreen
1444
- // );
1589
+ const initialScreen = getBreakpointFromWidth(win.innerWidth, breakpoints);
1445
1590
  setScreen(initialScreen);
1591
+ prevBreakpointRef.current = initialScreen;
1446
1592
  const handleResize = () => {
1447
- const newWidth = window.innerWidth;
1448
- const newHeight = window.innerHeight;
1449
- // console.log('[ResponsiveProvider] Resize event - new dimensions:', {
1450
- // width: newWidth,
1451
- // height: newHeight,
1452
- // });
1453
- setSize({
1593
+ const newWidth = win.innerWidth;
1594
+ const newHeight = win.innerHeight;
1595
+ // Always update dimensions (WindowDimensionsContext will re-render)
1596
+ setDimensions({
1454
1597
  width: newWidth,
1455
1598
  height: newHeight
1456
1599
  });
1457
- // Update screen on resize
1600
+ // Only update breakpoint if it actually changed
1601
+ // This prevents BreakpointContext from causing unnecessary re-renders
1458
1602
  const newScreen = getBreakpointFromWidth(newWidth, breakpoints);
1459
- // console.log('[ResponsiveProvider] Setting screen to:', newScreen);
1460
- setScreen(newScreen);
1603
+ if (newScreen !== prevBreakpointRef.current) {
1604
+ prevBreakpointRef.current = newScreen;
1605
+ setScreen(newScreen);
1606
+ }
1461
1607
  };
1462
1608
  const debouncedResize = debounce(handleResize, 100);
1463
- window.addEventListener('resize', debouncedResize);
1609
+ win.addEventListener('resize', debouncedResize);
1464
1610
  // Set up orientation listener
1465
- const orientationMql = window.matchMedia('(orientation: landscape)');
1611
+ const orientationMql = win.matchMedia('(orientation: landscape)');
1466
1612
  const onOrientationChange = () => setOrientation(orientationMql.matches ? 'landscape' : 'portrait');
1467
1613
  if (orientationMql.addEventListener) {
1468
1614
  orientationMql.addEventListener('change', onOrientationChange);
@@ -1471,30 +1617,46 @@ const ResponsiveProvider = _ref3 => {
1471
1617
  }
1472
1618
  onOrientationChange();
1473
1619
  return () => {
1474
- window.removeEventListener('resize', debouncedResize);
1620
+ win.removeEventListener('resize', debouncedResize);
1475
1621
  if (orientationMql.removeEventListener) {
1476
1622
  orientationMql.removeEventListener('change', onOrientationChange);
1477
1623
  } else {
1478
1624
  orientationMql.removeListener(onOrientationChange);
1479
1625
  }
1480
1626
  };
1481
- }, [breakpoints]); // Removed mediaQueries dep since we now use direct width comparison
1482
- const value = React.useMemo(() => {
1483
- const contextValue = {
1484
- breakpoints,
1485
- devices,
1486
- mediaQueries,
1487
- currentWidth: size.width,
1488
- currentHeight: size.height,
1489
- currentBreakpoint: screen,
1490
- currentDevice: determineCurrentDevice(screen, devices),
1491
- orientation
1492
- };
1493
- return contextValue;
1494
- }, [breakpoints, devices, mediaQueries, size, screen, orientation]);
1495
- return /*#__PURE__*/React__default.createElement(ResponsiveContext.Provider, {
1496
- value: value
1497
- }, 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)));
1498
1660
  };
1499
1661
 
1500
1662
  const Shadows = {
@@ -2788,6 +2950,50 @@ const AnalyticsProvider = _ref => {
2788
2950
  }, children);
2789
2951
  };
2790
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
+ }
2791
2997
  const Element = /*#__PURE__*/React__default.memo(/*#__PURE__*/React.forwardRef((_ref, ref) => {
2792
2998
  let {
2793
2999
  as = 'div',
@@ -2833,15 +3039,11 @@ const Element = /*#__PURE__*/React__default.memo(/*#__PURE__*/React.forwardRef((
2833
3039
  const {
2834
3040
  mediaQueries,
2835
3041
  devices
2836
- } = useResponsiveContext();
3042
+ } = useBreakpointContext();
2837
3043
  const {
2838
3044
  manager
2839
3045
  } = useStyleRegistry();
2840
3046
  const [isVisible, setIsVisible] = React.useState(false);
2841
- console.log({
2842
- mediaQueries,
2843
- devices
2844
- });
2845
3047
  React.useEffect(() => {
2846
3048
  if (!animateIn) {
2847
3049
  setIsVisible(true);
@@ -2882,15 +3084,16 @@ const Element = /*#__PURE__*/React__default.memo(/*#__PURE__*/React.forwardRef((
2882
3084
  }
2883
3085
  };
2884
3086
  }, [animateOut, manager]);
2885
- const utilityClasses = React.useMemo(() => {
2886
- const propsToProcess = {
3087
+ // Prepare props for processing (apply view/scroll timeline if needed)
3088
+ const propsToProcess = React.useMemo(() => {
3089
+ const processed = {
2887
3090
  ...rest,
2888
3091
  blend
2889
3092
  };
2890
3093
  // Apply view() timeline ONLY if animateOn='View' (not Both or Mount)
2891
- if (animateOn === 'View' && propsToProcess.animate) {
2892
- const animations = Array.isArray(propsToProcess.animate) ? propsToProcess.animate : [propsToProcess.animate];
2893
- 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 => {
2894
3097
  // Only add timeline if not already specified
2895
3098
  if (!anim.timeline) {
2896
3099
  return {
@@ -2904,9 +3107,9 @@ const Element = /*#__PURE__*/React__default.memo(/*#__PURE__*/React.forwardRef((
2904
3107
  });
2905
3108
  }
2906
3109
  // Apply scroll() timeline if animateOn='Scroll'
2907
- if (animateOn === 'Scroll' && propsToProcess.animate) {
2908
- const animations = Array.isArray(propsToProcess.animate) ? propsToProcess.animate : [propsToProcess.animate];
2909
- 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 => {
2910
3113
  // Only add timeline if not already specified
2911
3114
  if (!anim.timeline) {
2912
3115
  return {
@@ -2918,10 +3121,10 @@ const Element = /*#__PURE__*/React__default.memo(/*#__PURE__*/React.forwardRef((
2918
3121
  return anim;
2919
3122
  });
2920
3123
  }
2921
- return extractUtilityClasses(propsToProcess, color => {
2922
- return getColor(color);
2923
- }, mediaQueries, devices, manager);
2924
- }, [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);
2925
3128
  const newProps = {
2926
3129
  ref: setRef
2927
3130
  };
@@ -4833,20 +5036,23 @@ const WindowSizeContext = /*#__PURE__*/React.createContext({
4833
5036
  });
4834
5037
  const WindowSizeProvider = _ref => {
4835
5038
  let {
4836
- children
5039
+ children,
5040
+ targetWindow
4837
5041
  } = _ref;
5042
+ const win = targetWindow || (typeof window !== 'undefined' ? window : null);
4838
5043
  const [size, setSize] = React.useState({
4839
- width: window.innerWidth,
4840
- height: window.innerHeight
5044
+ width: win?.innerWidth || 0,
5045
+ height: win?.innerHeight || 0
4841
5046
  });
4842
5047
  React.useEffect(() => {
5048
+ if (!win) return;
4843
5049
  const handleResize = () => setSize({
4844
- width: window.innerWidth,
4845
- height: window.innerHeight
5050
+ width: win.innerWidth,
5051
+ height: win.innerHeight
4846
5052
  });
4847
- window.addEventListener('resize', handleResize);
4848
- return () => window.removeEventListener('resize', handleResize);
4849
- }, []);
5053
+ win.addEventListener('resize', handleResize);
5054
+ return () => win.removeEventListener('resize', handleResize);
5055
+ }, [win]);
4850
5056
  return /*#__PURE__*/React__default.createElement(WindowSizeContext.Provider, {
4851
5057
  value: size
4852
5058
  }, children);
@@ -4878,10 +5084,16 @@ function useActive() {
4878
5084
  return [ref, active];
4879
5085
  }
4880
5086
 
4881
- function useClickOutside() {
5087
+ function useClickOutside(options) {
4882
5088
  const [clickedOutside, setClickedOutside] = React.useState(false);
4883
5089
  const ref = React.useRef(null);
5090
+ const {
5091
+ targetWindow
5092
+ } = options || {};
4884
5093
  React.useEffect(() => {
5094
+ const win = targetWindow || (typeof window !== 'undefined' ? window : null);
5095
+ if (!win) return;
5096
+ const doc = win.document;
4885
5097
  const handleClick = e => {
4886
5098
  if (ref.current && !ref.current.contains(e.target)) {
4887
5099
  setClickedOutside(true);
@@ -4889,11 +5101,11 @@ function useClickOutside() {
4889
5101
  setClickedOutside(false);
4890
5102
  }
4891
5103
  };
4892
- document.addEventListener('mousedown', handleClick);
5104
+ doc.addEventListener('mousedown', handleClick);
4893
5105
  return () => {
4894
- document.removeEventListener('mousedown', handleClick);
5106
+ doc.removeEventListener('mousedown', handleClick);
4895
5107
  };
4896
- }, []);
5108
+ }, [targetWindow]);
4897
5109
  return [ref, clickedOutside];
4898
5110
  }
4899
5111
 
@@ -4910,7 +5122,8 @@ function useElementPosition(options) {
4910
5122
  throttleMs = 500,
4911
5123
  trackOnHover = true,
4912
5124
  trackOnScroll = false,
4913
- trackOnResize = false
5125
+ trackOnResize = false,
5126
+ targetWindow
4914
5127
  } = options;
4915
5128
  const elementRef = React.useRef(null);
4916
5129
  const [relation, setRelation] = React.useState(null);
@@ -4921,9 +5134,10 @@ function useElementPosition(options) {
4921
5134
  setRelation(currentRelation => currentRelation === null ? null : null);
4922
5135
  return;
4923
5136
  }
5137
+ const win = targetWindow || element.ownerDocument?.defaultView || window;
4924
5138
  const rect = element.getBoundingClientRect();
4925
- const viewportHeight = window.innerHeight;
4926
- const viewportWidth = window.innerWidth;
5139
+ const viewportHeight = win.innerHeight;
5140
+ const viewportWidth = win.innerWidth;
4927
5141
  // 1. Determine element's general position in viewport
4928
5142
  const elementCenterY = rect.top + rect.height / 2;
4929
5143
  const elementCenterX = rect.left + rect.width / 2;
@@ -4952,7 +5166,7 @@ function useElementPosition(options) {
4952
5166
  }
4953
5167
  return newRelation;
4954
5168
  });
4955
- }, []); // This callback is stable
5169
+ }, [targetWindow]); // This callback is stable
4956
5170
  const throttledUpdate = React.useCallback(() => {
4957
5171
  if (throttleTimerRef.current) {
4958
5172
  clearTimeout(throttleTimerRef.current);
@@ -4970,6 +5184,7 @@ function useElementPosition(options) {
4970
5184
  }
4971
5185
  const element = elementRef.current;
4972
5186
  if (!element) return;
5187
+ const win = targetWindow || element.ownerDocument?.defaultView || window;
4973
5188
  const handler = throttledUpdate;
4974
5189
  const immediateHandler = calculateRelation;
4975
5190
  // Add event listeners based on configuration
@@ -4985,18 +5200,18 @@ function useElementPosition(options) {
4985
5200
  }
4986
5201
  // Scroll events - throttled
4987
5202
  if (trackOnScroll) {
4988
- window.addEventListener('scroll', handler, {
5203
+ win.addEventListener('scroll', handler, {
4989
5204
  passive: true
4990
5205
  });
4991
5206
  cleanupFunctions.push(() => {
4992
- window.removeEventListener('scroll', handler);
5207
+ win.removeEventListener('scroll', handler);
4993
5208
  });
4994
5209
  }
4995
5210
  // Resize events - throttled
4996
5211
  if (trackOnResize) {
4997
- window.addEventListener('resize', handler);
5212
+ win.addEventListener('resize', handler);
4998
5213
  cleanupFunctions.push(() => {
4999
- window.removeEventListener('resize', handler);
5214
+ win.removeEventListener('resize', handler);
5000
5215
  });
5001
5216
  }
5002
5217
  return () => {
@@ -5005,7 +5220,7 @@ function useElementPosition(options) {
5005
5220
  }
5006
5221
  cleanupFunctions.forEach(cleanup => cleanup());
5007
5222
  };
5008
- }, [trackChanges, trackOnHover, trackOnScroll, trackOnResize, throttledUpdate, calculateRelation]);
5223
+ }, [trackChanges, trackOnHover, trackOnScroll, trackOnResize, throttledUpdate, calculateRelation, targetWindow]);
5009
5224
  const manualUpdateRelation = React.useCallback(() => {
5010
5225
  calculateRelation();
5011
5226
  }, [calculateRelation]);
@@ -5080,21 +5295,84 @@ const useMount = callback => {
5080
5295
  function useOnScreen(options) {
5081
5296
  const ref = React.useRef(null);
5082
5297
  const [isOnScreen, setOnScreen] = React.useState(false);
5298
+ const {
5299
+ targetWindow,
5300
+ ...observerOptions
5301
+ } = options || {};
5083
5302
  React.useEffect(() => {
5084
5303
  const node = ref.current;
5085
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;
5086
5309
  const observer = new IntersectionObserver(_ref => {
5087
5310
  let [entry] = _ref;
5088
5311
  setOnScreen(entry.isIntersecting);
5089
- }, options);
5312
+ }, {
5313
+ ...observerOptions,
5314
+ root: effectiveRoot
5315
+ });
5090
5316
  observer.observe(node);
5091
5317
  return () => {
5092
5318
  observer.disconnect();
5093
5319
  };
5094
- }, [options]);
5320
+ }, [targetWindow, options]);
5095
5321
  return [ref, isOnScreen];
5096
5322
  }
5097
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
+ */
5098
5376
  const useResponsive = () => {
5099
5377
  const context = useResponsiveContext();
5100
5378
  const {
@@ -5110,7 +5388,6 @@ const useResponsive = () => {
5110
5388
  on,
5111
5389
  is: on
5112
5390
  };
5113
- // console.log('[useResponsive] Hook called, returning:', { screen, orientation, 'on(mobile)': on('mobile') });
5114
5391
  return result;
5115
5392
  };
5116
5393
 
@@ -5280,27 +5557,28 @@ const useScrollAnimation = function (ref, options) {
5280
5557
  };
5281
5558
  };
5282
5559
  // Enhanced useSmoothScroll with error handling
5283
- const useSmoothScroll = () => {
5560
+ const useSmoothScroll = targetWindow => {
5284
5561
  return React.useCallback(function (element, offset) {
5285
5562
  if (offset === void 0) {
5286
5563
  offset = 0;
5287
5564
  }
5288
5565
  if (!element) return;
5289
5566
  try {
5290
- const top = element.getBoundingClientRect().top + (window.scrollY || window.pageYOffset) - offset;
5291
- if ('scrollBehavior' in document.documentElement.style) {
5292
- 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({
5293
5571
  top,
5294
5572
  behavior: 'smooth'
5295
5573
  });
5296
5574
  } else {
5297
5575
  // Fallback for browsers that don't support smooth scrolling
5298
- window.scrollTo(0, top);
5576
+ win.scrollTo(0, top);
5299
5577
  }
5300
5578
  } catch (error) {
5301
5579
  console.error('Error during smooth scroll:', error);
5302
5580
  }
5303
- }, []);
5581
+ }, [targetWindow]);
5304
5582
  };
5305
5583
  // Enhanced useInfiniteScroll with debouncing
5306
5584
  const useInfiniteScroll = function (callback, options) {
@@ -5344,7 +5622,7 @@ const useInfiniteScroll = function (callback, options) {
5344
5622
  sentinelRef: setSentinel
5345
5623
  };
5346
5624
  };
5347
- const useScrollDirection = function (threshold) {
5625
+ const useScrollDirection = function (threshold, targetWindow) {
5348
5626
  if (threshold === void 0) {
5349
5627
  threshold = 5;
5350
5628
  }
@@ -5353,12 +5631,15 @@ const useScrollDirection = function (threshold) {
5353
5631
  const lastDirection = React.useRef('up');
5354
5632
  const animationFrame = React.useRef();
5355
5633
  const ticking = React.useRef(false);
5356
- const updateDirection = () => {
5357
- 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;
5358
5639
  const direction = scrollY > lastScrollY.current ? 'down' : 'up';
5359
5640
  const scrollDelta = Math.abs(scrollY - lastScrollY.current);
5360
5641
  // Vérifier si on est au bas de la page
5361
- const isAtBottom = window.innerHeight + scrollY >= document.documentElement.scrollHeight - 1;
5642
+ const isAtBottom = win.innerHeight + scrollY >= doc.scrollHeight - 1;
5362
5643
  // Logique principale
5363
5644
  if (scrollDelta > threshold || direction === 'down' && isAtBottom) {
5364
5645
  if (direction !== lastDirection.current) {
@@ -5369,8 +5650,10 @@ const useScrollDirection = function (threshold) {
5369
5650
  // Mise à jour de la position avec un minimum de 0
5370
5651
  lastScrollY.current = Math.max(scrollY, 0);
5371
5652
  ticking.current = false;
5372
- };
5653
+ }, [threshold, targetWindow]);
5373
5654
  React.useEffect(() => {
5655
+ const win = targetWindow || (typeof window !== 'undefined' ? window : null);
5656
+ if (!win) return;
5374
5657
  const handleScroll = () => {
5375
5658
  if (!ticking.current) {
5376
5659
  animationFrame.current = requestAnimationFrame(() => {
@@ -5380,16 +5663,16 @@ const useScrollDirection = function (threshold) {
5380
5663
  ticking.current = true;
5381
5664
  }
5382
5665
  };
5383
- window.addEventListener('scroll', handleScroll, {
5666
+ win.addEventListener('scroll', handleScroll, {
5384
5667
  passive: true
5385
5668
  });
5386
5669
  return () => {
5387
- window.removeEventListener('scroll', handleScroll);
5670
+ win.removeEventListener('scroll', handleScroll);
5388
5671
  if (animationFrame.current) {
5389
5672
  cancelAnimationFrame(animationFrame.current);
5390
5673
  }
5391
5674
  };
5392
- }, [threshold]);
5675
+ }, [updateDirection, targetWindow]);
5393
5676
  return scrollDirection;
5394
5677
  };
5395
5678
 
@@ -5398,6 +5681,7 @@ const useWindowSize = () => React.useContext(WindowSizeContext);
5398
5681
  function useInView(options) {
5399
5682
  const {
5400
5683
  triggerOnce = false,
5684
+ targetWindow,
5401
5685
  ...observerOptions
5402
5686
  } = options || {};
5403
5687
  const ref = React.useRef(null);
@@ -5405,6 +5689,10 @@ function useInView(options) {
5405
5689
  React.useEffect(() => {
5406
5690
  const element = ref.current;
5407
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;
5408
5696
  const observer = new IntersectionObserver(_ref => {
5409
5697
  let [entry] = _ref;
5410
5698
  if (entry.isIntersecting) {
@@ -5417,18 +5705,121 @@ function useInView(options) {
5417
5705
  // Only update to false if not using triggerOnce
5418
5706
  setInView(false);
5419
5707
  }
5420
- }, observerOptions);
5708
+ }, {
5709
+ ...observerOptions,
5710
+ root: effectiveRoot
5711
+ });
5421
5712
  observer.observe(element);
5422
5713
  return () => {
5423
5714
  observer.disconnect();
5424
5715
  };
5425
- }, [triggerOnce, ...Object.values(observerOptions || {})]);
5716
+ }, [triggerOnce, targetWindow, ...Object.values(observerOptions || {})]);
5426
5717
  return {
5427
5718
  ref,
5428
5719
  inView
5429
5720
  };
5430
5721
  }
5431
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
+
5432
5823
  /**
5433
5824
  * View Animation Utilities
5434
5825
  *
@@ -6003,6 +6394,7 @@ exports.AnalyticsContext = AnalyticsContext;
6003
6394
  exports.AnalyticsProvider = AnalyticsProvider;
6004
6395
  exports.Animation = Animation;
6005
6396
  exports.AnimationUtils = AnimationUtils;
6397
+ exports.BreakpointContext = BreakpointContext;
6006
6398
  exports.Button = Button;
6007
6399
  exports.Center = Center;
6008
6400
  exports.Div = Div;
@@ -6029,6 +6421,7 @@ exports.UtilityClassManager = UtilityClassManager;
6029
6421
  exports.Vertical = Vertical;
6030
6422
  exports.VerticalResponsive = VerticalResponsive;
6031
6423
  exports.View = View;
6424
+ exports.WindowDimensionsContext = WindowDimensionsContext;
6032
6425
  exports.WindowSizeContext = WindowSizeContext;
6033
6426
  exports.WindowSizeProvider = WindowSizeProvider;
6034
6427
  exports.animateOnView = animateOnView;
@@ -6068,10 +6461,14 @@ exports.slideRightOnView = slideRightOnView;
6068
6461
  exports.slideUpOnView = slideUpOnView;
6069
6462
  exports.useActive = useActive;
6070
6463
  exports.useAnalytics = useAnalytics;
6464
+ exports.useBreakpoint = useBreakpoint;
6465
+ exports.useBreakpointContext = useBreakpointContext;
6071
6466
  exports.useClickOutside = useClickOutside;
6072
6467
  exports.useElementPosition = useElementPosition;
6073
6468
  exports.useFocus = useFocus;
6074
6469
  exports.useHover = useHover;
6470
+ exports.useIframe = useIframe;
6471
+ exports.useIframeStyles = useIframeStyles;
6075
6472
  exports.useInView = useInView;
6076
6473
  exports.useInfiniteScroll = useInfiniteScroll;
6077
6474
  exports.useKeyPress = useKeyPress;
@@ -6086,6 +6483,8 @@ exports.useServerInsertedHTML = useServerInsertedHTML;
6086
6483
  exports.useSmoothScroll = useSmoothScroll;
6087
6484
  exports.useStyleRegistry = useStyleRegistry;
6088
6485
  exports.useTheme = useTheme;
6486
+ exports.useWindowDimensions = useWindowDimensions;
6487
+ exports.useWindowDimensionsContext = useWindowDimensionsContext;
6089
6488
  exports.useWindowSize = useWindowSize;
6090
6489
  exports.utilityClassManager = utilityClassManager;
6091
6490
  exports.viewAnimationPresets = viewAnimationPresets;