@cntrl-site/sdk-nextjs 1.9.14 → 1.9.16

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.
Files changed (59) hide show
  1. package/lib/ItemGeometry/GenericGeometryController.js +43 -0
  2. package/lib/ItemGeometry/ItemGeometryContext.js +6 -0
  3. package/lib/ItemGeometry/ItemGeometryController.js +2 -0
  4. package/lib/ItemGeometry/ItemGeometryService.js +34 -0
  5. package/lib/ItemGeometry/RichTextGeometryController.js +135 -0
  6. package/lib/ItemGeometry/useItemGeometry.js +25 -0
  7. package/lib/components/Page.js +4 -1
  8. package/lib/components/items/CodeEmbedItem/CodeEmbedItem.js +2 -0
  9. package/lib/components/items/ComponentItem/ComponentItem.js +2 -0
  10. package/lib/components/items/CompoundItem/CompoundItem.js +17 -0
  11. package/lib/components/items/CustomItem/CustomItem.js +2 -0
  12. package/lib/components/items/EmbedVideoItem/VimeoEmbed.js +2 -0
  13. package/lib/components/items/EmbedVideoItem/YoutubeEmbed.js +2 -0
  14. package/lib/components/items/FileItem/ImageItem.js +2 -0
  15. package/lib/components/items/FileItem/VideoItem.js +2 -0
  16. package/lib/components/items/GroupItem/GroupItem.js +17 -0
  17. package/lib/components/items/Item.js +8 -0
  18. package/lib/components/items/RectangleItem/RectangleItem.js +2 -0
  19. package/lib/components/items/RichTextItem/RichTextItem.js +5 -1
  20. package/lib/components/items/useRichTextItemValues.js +2 -1
  21. package/lib/interactions/InteractionsRegistry.js +113 -12
  22. package/lib/interactions/ItemInteractionCtrl.js +1 -1
  23. package/lib/interactions/getTransition.js +1 -1
  24. package/lib/provider/InteractionsContext.js +4 -2
  25. package/lib/utils/CSSNumeric.js +19 -0
  26. package/lib/utils/RichTextConverter/RichTextConverter.js +4 -4
  27. package/lib/utils/domDfs.js +15 -0
  28. package/package.json +2 -2
  29. package/src/ItemGeometry/GenericGeometryController.ts +59 -0
  30. package/src/ItemGeometry/ItemGeometryContext.ts +4 -0
  31. package/src/ItemGeometry/ItemGeometryController.ts +22 -0
  32. package/src/ItemGeometry/ItemGeometryService.ts +34 -0
  33. package/src/ItemGeometry/RichTextGeometryController.ts +166 -0
  34. package/src/ItemGeometry/useItemGeometry.ts +25 -0
  35. package/src/components/Page.tsx +8 -3
  36. package/src/components/items/CodeEmbedItem/CodeEmbedItem.tsx +2 -0
  37. package/src/components/items/ComponentItem/ComponentItem.tsx +2 -0
  38. package/src/components/items/CompoundItem/CompoundItem.tsx +18 -1
  39. package/src/components/items/CustomItem/CustomItem.tsx +2 -0
  40. package/src/components/items/EmbedVideoItem/VimeoEmbed.tsx +2 -0
  41. package/src/components/items/EmbedVideoItem/YoutubeEmbed.tsx +2 -0
  42. package/src/components/items/FileItem/ImageItem.tsx +2 -0
  43. package/src/components/items/FileItem/VideoItem.tsx +2 -0
  44. package/src/components/items/GroupItem/GroupItem.tsx +18 -1
  45. package/src/components/items/Item.tsx +11 -3
  46. package/src/components/items/RectangleItem/RectangleItem.tsx +3 -1
  47. package/src/components/items/RichTextItem/RichTextItem.tsx +6 -1
  48. package/src/components/items/useRichTextItemValues.ts +2 -1
  49. package/src/interactions/InteractionsRegistry.ts +120 -15
  50. package/src/interactions/ItemInteractionCtrl.ts +2 -2
  51. package/src/interactions/getTransition.ts +1 -1
  52. package/src/interactions/types.ts +2 -2
  53. package/src/provider/InteractionsContext.tsx +4 -2
  54. package/src/utils/CSSNumeric.ts +19 -0
  55. package/src/utils/RichTextConverter/RichTextConverter.tsx +20 -4
  56. package/src/utils/domDfs.ts +11 -0
  57. package/cntrl-site-sdk-nextjs-1.9.14-2.tgz +0 -0
  58. /package/lib/{interactions → ItemGeometry}/CSSPropertyNameMap.js +0 -0
  59. /package/src/{interactions → ItemGeometry}/CSSPropertyNameMap.ts +0 -0
@@ -7,6 +7,7 @@ import { useRegisterResize } from '../../../common/useRegisterResize';
7
7
  import { useItemAngle } from '../useItemAngle';
8
8
  import { LinkWrapper } from '../LinkWrapper';
9
9
  import { useCodeEmbedItem } from './useCodeEmbedItem';
10
+ import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry';
10
11
 
11
12
  const stylesMap = {
12
13
  [AreaAnchor.TopLeft]: {},
@@ -31,6 +32,7 @@ export const CodeEmbedItem: FC<ItemProps<TCodeEmbedItem>> = ({ item, sectionId,
31
32
  const html = decodeBase64(item.commonParams.html);
32
33
  const [ref, setRef] = useState<HTMLDivElement | null>(null);
33
34
  useRegisterResize(ref, onResize);
35
+ useItemGeometry(item.id, ref);
34
36
  const pos = stylesMap[anchor];
35
37
  const layoutValues: Record<string, any>[] = [item.area, item.layoutParams];
36
38
  const stateParams = interactionCtrl?.getState<number>(['angle', 'blur', 'opacity']);
@@ -8,6 +8,7 @@ import { useItemAngle } from '../useItemAngle';
8
8
  import { useRegisterResize } from '../../../common/useRegisterResize';
9
9
  import { getStyleFromItemStateAndParams } from '../../../utils/getStyleFromItemStateAndParams';
10
10
  import { useLayoutContext } from '../../useLayoutContext';
11
+ import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry';
11
12
 
12
13
  export const ComponentItem: FC<ItemProps<TComponentItem>> = ({ item, sectionId, onResize, interactionCtrl }) => {
13
14
  const sdk = useCntrlContext();
@@ -19,6 +20,7 @@ export const ComponentItem: FC<ItemProps<TComponentItem>> = ({ item, sectionId,
19
20
  const [ref, setRef] = useState<HTMLDivElement | null>(null);
20
21
  const { opacity: itemOpacity, blur: itemBlur } = useComponentItem(item, sectionId);
21
22
  useRegisterResize(ref, onResize);
23
+ useItemGeometry(item.id, ref);
22
24
  const stateParams = interactionCtrl?.getState<number>(['opacity', 'angle', 'blur']);
23
25
  const angle = getStyleFromItemStateAndParams(stateParams?.styles?.angle, itemAngle);
24
26
  const opacity = getStyleFromItemStateAndParams(stateParams?.styles?.opacity, itemOpacity);
@@ -1,4 +1,4 @@
1
- import React, { FC, useEffect, useId, useState } from 'react';
1
+ import React, { FC, useContext, useEffect, useId, useState } from 'react';
2
2
  import { useItemAngle } from '../useItemAngle';
3
3
  import { useCntrlContext } from '../../../provider/useCntrlContext';
4
4
  import { useRegisterResize } from '../../../common/useRegisterResize';
@@ -9,16 +9,21 @@ import JSXStyle from 'styled-jsx/style';
9
9
  import { getLayoutStyles, CompoundItem as TCompoundItem } from '@cntrl-site/sdk';
10
10
  import { CompoundChild } from './CompoundChild';
11
11
  import { useCompoundItem } from './useCompoundItem';
12
+ import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry';
13
+ import { ItemGeometryContext } from '../../../ItemGeometry/ItemGeometryContext';
12
14
 
13
15
  export const CompoundItem: FC<ItemProps<TCompoundItem>> = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange }) => {
14
16
  const id = useId();
15
17
  const { items } = item;
18
+ const itemIds = items ? items.map(item => item.id) : [];
16
19
  const itemAngle = useItemAngle(item, sectionId);
17
20
  const { layouts } = useCntrlContext();
18
21
  const { opacity: itemOpacity } = useCompoundItem(item, sectionId);
19
22
  const layoutValues: Record<string, any>[] = [item.area, item.layoutParams];
20
23
  const [ref, setRef] = useState<HTMLDivElement | null>(null);
21
24
  useRegisterResize(ref, onResize);
25
+ useItemGeometry(item.id, ref);
26
+ const itemGeometryService = useContext(ItemGeometryContext);
22
27
  const stateParams = interactionCtrl?.getState<number>(['opacity', 'angle']);
23
28
  const angle = getStyleFromItemStateAndParams(stateParams?.styles?.angle, itemAngle);
24
29
  const opacity = getStyleFromItemStateAndParams(stateParams?.styles?.opacity, itemOpacity);
@@ -26,6 +31,18 @@ export const CompoundItem: FC<ItemProps<TCompoundItem>> = ({ item, sectionId, on
26
31
  useEffect(() => {
27
32
  onVisibilityChange?.(isInteractive);
28
33
  }, [isInteractive, onVisibilityChange]);
34
+ useEffect(() => {
35
+ // because on first render controllers not yet initialized
36
+ setTimeout(() => {
37
+ for (const id of itemIds) {
38
+ const hasItem = itemGeometryService.hasItem(id);
39
+ if (hasItem) {
40
+ const cntrl = itemGeometryService.getControllerById(id);
41
+ cntrl.setParentId(item.id);
42
+ }
43
+ }
44
+ }, 0);
45
+ }, [itemIds, item.id]);
29
46
  return (
30
47
  <LinkWrapper url={item.link?.url} target={item.link?.target}>
31
48
  <>
@@ -5,6 +5,7 @@ import { ItemProps } from '../Item';
5
5
  import JSXStyle from 'styled-jsx/style';
6
6
  import { useRegisterResize } from '../../../common/useRegisterResize';
7
7
  import { useItemAngle } from '../useItemAngle';
8
+ import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry';
8
9
 
9
10
  export const CustomItem: FC<ItemProps<TCustomItem>> = ({ item, onResize, sectionId, interactionCtrl }) => {
10
11
  const sdk = useCntrlContext();
@@ -14,6 +15,7 @@ export const CustomItem: FC<ItemProps<TCustomItem>> = ({ item, onResize, section
14
15
  const layoutValues: Record<string, any>[] = [item.area];
15
16
  const [ref, setRef] = useState<HTMLDivElement | null>(null);
16
17
  useRegisterResize(ref, onResize);
18
+ useItemGeometry(item.id, ref);
17
19
  const stateParams = interactionCtrl?.getState<number>(['angle']);
18
20
  const angle = stateParams?.styles?.angle ?? itemAngle;
19
21
  if (!component) return null;
@@ -12,6 +12,7 @@ import { getStyleFromItemStateAndParams } from '../../../utils/getStyleFromItemS
12
12
  import { useEmbedVideoItem } from './useEmbedVideoItem';
13
13
  import { LinkWrapper } from '../LinkWrapper';
14
14
  import { useLayoutContext } from '../../useLayoutContext';
15
+ import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry';
15
16
 
16
17
  export const VimeoEmbedItem: FC<ItemProps<TVimeoEmbedItem>> = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange }) => {
17
18
  const id = useId();
@@ -38,6 +39,7 @@ export const VimeoEmbedItem: FC<ItemProps<TVimeoEmbedItem>> = ({ item, sectionId
38
39
  const opacity = getStyleFromItemStateAndParams(wrapperStateParams?.styles?.opacity, itemOpacity);
39
40
  const radius = getStyleFromItemStateAndParams(frameStateParams?.styles?.radius, itemRadius);
40
41
  useRegisterResize(ref, onResize);
42
+ useItemGeometry(item.id, ref);
41
43
  const validUrl = useMemo(() => {
42
44
  if (!layoutParams) return url;
43
45
  const validURL = new URL(url);
@@ -12,6 +12,7 @@ import { YTPlayer } from '../../../utils/Youtube/YoutubeIframeApi';
12
12
  import { useRegisterResize } from '../../../common/useRegisterResize';
13
13
  import { getStyleFromItemStateAndParams } from '../../../utils/getStyleFromItemStateAndParams';
14
14
  import { useLayoutContext } from '../../useLayoutContext';
15
+ import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry';
15
16
 
16
17
  export const YoutubeEmbedItem: FC<ItemProps<TYoutubeEmbedItem>> = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange }) => {
17
18
  const id = useId();
@@ -38,6 +39,7 @@ export const YoutubeEmbedItem: FC<ItemProps<TYoutubeEmbedItem>> = ({ item, secti
38
39
  onVisibilityChange?.(isInteractive);
39
40
  }, [isInteractive, onVisibilityChange]);
40
41
  useRegisterResize(div, onResize);
42
+ useItemGeometry(item.id, div);
41
43
  useEffect(() => {
42
44
  const newUrl = new URL(url);
43
45
  const videoId = getYoutubeId(newUrl);
@@ -13,6 +13,7 @@ import { useLayoutContext } from '../../useLayoutContext';
13
13
  import { getStyleFromItemStateAndParams } from '../../../utils/getStyleFromItemStateAndParams';
14
14
  import { useItemFXData } from '../../../common/useItemFXData';
15
15
  import { getFill } from '../../../utils/getFill';
16
+ import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry';
16
17
 
17
18
  export const ImageItem: FC<ItemProps<TImageItem>> = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange }) => {
18
19
  const id = useId();
@@ -28,6 +29,7 @@ export const ImageItem: FC<ItemProps<TImageItem>> = ({ item, sectionId, onResize
28
29
  const itemAngle = useItemAngle(item, sectionId);
29
30
  const [wrapperRef, setWrapperRef] = useState<HTMLDivElement | null>(null);
30
31
  useRegisterResize(wrapperRef, onResize);
32
+ useItemGeometry(item.id, wrapperRef);
31
33
  const { url, hasGLEffect } = item.commonParams;
32
34
  const fxCanvas = useRef<HTMLCanvasElement | null>(null);
33
35
  const isInitialRef = useRef(true);
@@ -14,6 +14,7 @@ import { useVideoFx } from '../../../utils/effects/useVideoFx';
14
14
  import { useElementRect } from '../../../utils/useElementRect';
15
15
  import { useItemFXData } from '../../../common/useItemFXData';
16
16
  import { getFill } from '../../../utils/getFill';
17
+ import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry';
17
18
 
18
19
  export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange }) => {
19
20
  const id = useId();
@@ -74,6 +75,7 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
74
75
  height
75
76
  );
76
77
  useRegisterResize(ref, onResize);
78
+ useItemGeometry(item.id, ref);
77
79
  const inlineStyles = {
78
80
  ...(radius !== undefined ? { borderRadius: `${radius * 100}vw` } : {}),
79
81
  ...(strokeWidth !== undefined ? {
@@ -1,4 +1,4 @@
1
- import React, { FC, useEffect, useId, useState } from 'react';
1
+ import React, { FC, useContext, useEffect, useId, useState } from 'react';
2
2
  import { Item, ItemProps } from '../Item';
3
3
  import JSXStyle from 'styled-jsx/style';
4
4
  import { getLayoutStyles, GroupItem as TGroupItem } from '@cntrl-site/sdk';
@@ -9,16 +9,21 @@ import { useItemAngle } from '../useItemAngle';
9
9
  import { useGroupItem } from './useGroupItem';
10
10
  import { getStyleFromItemStateAndParams } from '../../../utils/getStyleFromItemStateAndParams';
11
11
  import { CompoundChild } from '../CompoundItem/CompoundChild';
12
+ import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry';
13
+ import { ItemGeometryContext } from '../../../ItemGeometry/ItemGeometryContext';
12
14
 
13
15
  export const GroupItem: FC<ItemProps<TGroupItem>> = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange, isInCompound }) => {
14
16
  const id = useId();
15
17
  const { items } = item;
18
+ const itemIds = items ? items.map(item => item.id) : [];
16
19
  const itemAngle = useItemAngle(item, sectionId);
17
20
  const { layouts } = useCntrlContext();
18
21
  const { opacity: itemOpacity, blur: itemBlur } = useGroupItem(item, sectionId);
19
22
  const layoutValues: Record<string, any>[] = [item.area, item.layoutParams];
20
23
  const [ref, setRef] = useState<HTMLDivElement | null>(null);
21
24
  useRegisterResize(ref, onResize);
25
+ useItemGeometry(item.id, ref);
26
+ const itemGeometryService = useContext(ItemGeometryContext);
22
27
  const stateParams = interactionCtrl?.getState<number>(['opacity', 'angle', 'blur']);
23
28
  const angle = getStyleFromItemStateAndParams(stateParams?.styles?.angle, itemAngle);
24
29
  const opacity = getStyleFromItemStateAndParams(stateParams?.styles?.opacity, itemOpacity);
@@ -27,6 +32,18 @@ export const GroupItem: FC<ItemProps<TGroupItem>> = ({ item, sectionId, onResize
27
32
  useEffect(() => {
28
33
  onVisibilityChange?.(isInteractive);
29
34
  }, [isInteractive, onVisibilityChange]);
35
+ useEffect(() => {
36
+ // because on first render controllers not yet initialized
37
+ setTimeout(() => {
38
+ for (const id of itemIds) {
39
+ const hasItem = itemGeometryService.hasItem(id);
40
+ if (hasItem) {
41
+ const cntrl = itemGeometryService.getControllerById(id);
42
+ cntrl.setParentId(item.id);
43
+ }
44
+ }
45
+ }, 0);
46
+ }, [itemIds, item.id]);
30
47
  return (
31
48
  <LinkWrapper url={item.link?.url} target={item.link?.target}>
32
49
  <>
@@ -35,6 +35,7 @@ import { parseSizing, useSizing } from './useSizing';
35
35
  import { useItemPointerEvents } from './useItemPointerEvents';
36
36
  import { useItemArea } from './useItemArea';
37
37
  import { useDraggable } from './useDraggable';
38
+ import { ItemGeometryContext } from '../../ItemGeometry/ItemGeometryContext';
38
39
 
39
40
  export interface ItemProps<I extends ItemAny> {
40
41
  item: I;
@@ -84,6 +85,13 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight, isP
84
85
  const [isDraggingActive, setIsDraggingActive] = useState(false);
85
86
  const wrapperStateProps = interactionCtrl?.getState<number>(['top', 'left']);
86
87
  const innerStateProps = interactionCtrl?.getState<number>(['width', 'height', 'scale']);
88
+ const geometryService = useContext(ItemGeometryContext);
89
+ const triggerZone = geometryService.getItemBoundary(item.id);
90
+ const hasItem = geometryService.hasItem(item.id);
91
+ const geometryController = useMemo(() => hasItem ? geometryService.getControllerById(item.id) : undefined, [hasItem]);
92
+ if (geometryController) {
93
+ geometryController.setScale(itemScale!);
94
+ }
87
95
  const { width, height, top, left } = useItemArea(item, sectionId, {
88
96
  top: wrapperStateProps?.styles?.top as number,
89
97
  left: wrapperStateProps?.styles?.left as number,
@@ -228,8 +236,8 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight, isP
228
236
  }
229
237
  .item-${item.id}-inner {
230
238
  width: ${sizingAxis.x === 'manual'
231
- ? `${area.width * 100}vw`
232
- : 'max-content'};
239
+ ? `${area.width * 100}vw`
240
+ : 'max-content'};
233
241
  height: ${sizingAxis.y === 'manual' ? `${area.height * 100}vw` : 'unset'};
234
242
  transform-origin: ${ScaleAnchorMap[scaleAnchor]};
235
243
  transform: scale(${area.scale});
@@ -245,7 +253,7 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight, isP
245
253
  left: ${area.left * 100}vw;
246
254
  }
247
255
  `);
248
- })}
256
+ })}
249
257
  `}
250
258
  </JSXStyle>
251
259
  </div>
@@ -1,4 +1,4 @@
1
- import { FC, useEffect, useId, useState } from 'react';
1
+ import { FC, useContext, useEffect, useId, useState } from 'react';
2
2
  import JSXStyle from 'styled-jsx/style';
3
3
  import { RectangleItem as TRectangleItem, getLayoutStyles, FillLayer } from '@cntrl-site/sdk';
4
4
  import { CntrlColor } from '@cntrl-site/color';
@@ -11,6 +11,7 @@ import { useRegisterResize } from '../../../common/useRegisterResize';
11
11
  import { getStyleFromItemStateAndParams } from '../../../utils/getStyleFromItemStateAndParams';
12
12
  import { getFill } from '../../../utils/getFill';
13
13
  import { areFillsVisible } from '../../../utils/areFillsVisible/areFillsVisible';
14
+ import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry';
14
15
 
15
16
  export const RectangleItem: FC<ItemProps<TRectangleItem>> = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange }) => {
16
17
  const id = useId();
@@ -32,6 +33,7 @@ export const RectangleItem: FC<ItemProps<TRectangleItem>> = ({ item, sectionId,
32
33
  const layoutValues: Record<string, any>[] = [item.area, item.layoutParams];
33
34
  const [ref, setRef] = useState<HTMLDivElement | null>(null);
34
35
  useRegisterResize(ref, onResize);
36
+ useItemGeometry(item.id, ref);
35
37
  const backdropBlur = getStyleFromItemStateAndParams(styles?.backdropBlur, itemBackdropBlur);
36
38
  const radius = getStyleFromItemStateAndParams(styles?.radius, itemRadius);
37
39
  const strokeWidth = getStyleFromItemStateAndParams(styles?.strokeWidth, itemStrokeWidth);
@@ -12,6 +12,8 @@ import { useExemplary } from '../../../common/useExemplary';
12
12
  import { useItemAngle } from '../useItemAngle';
13
13
  import { getStyleFromItemStateAndParams } from '../../../utils/getStyleFromItemStateAndParams';
14
14
  import { useCurrentLayout } from '../../../common/useCurrentLayout';
15
+ import { useItemGeometry } from '../../../ItemGeometry/useItemGeometry';
16
+ import { RichTextGeometryController } from '../../../ItemGeometry/RichTextGeometryController';
15
17
 
16
18
  export const RichTextItem: FC<ItemProps<TRichTextItem>> = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange }) => {
17
19
  const id = useId();
@@ -24,12 +26,15 @@ export const RichTextItem: FC<ItemProps<TRichTextItem>> = ({ item, sectionId, on
24
26
  letterSpacing: itemLetterSpacing,
25
27
  color: itemColor,
26
28
  fontSize,
27
- lineHeight
29
+ lineHeight,
30
+ xSizing
28
31
  } = useRichTextItemValues(item, sectionId);
29
32
  const layoutValues: Record<string, any>[] = [item.area, item.layoutParams];
30
33
  const exemplary = useExemplary();
31
34
  const { layoutId } = useCurrentLayout();
32
35
  useRegisterResize(ref, onResize);
36
+ const geometry = useItemGeometry(item.id, ref, RichTextGeometryController, { xSizing });
37
+ geometry?.setAngle(itemAngle!);
33
38
  const stateParams = interactionCtrl?.getState<number | string>(['angle', 'blur', 'letterSpacing', 'wordSpacing', 'color']);
34
39
  const stateStyles = stateParams?.styles ?? {};
35
40
  const transition = stateParams?.transition ?? 'none';
@@ -60,6 +60,7 @@ export const useRichTextItemValues = (item: RichTextItem, sectionId: string) =>
60
60
 
61
61
  const fontSize = layoutId ? item.layoutParams[layoutId].fontSize : undefined;
62
62
  const lineHeight = layoutId ? item.layoutParams[layoutId].lineHeight : undefined;
63
+ const xSizing = layoutId ? item.layoutParams[layoutId].sizing : undefined;
63
64
 
64
- return { blur, letterSpacing, wordSpacing, color, fontSize, lineHeight };
65
+ return { blur, letterSpacing, wordSpacing, color, fontSize, lineHeight, xSizing };
65
66
  };
@@ -7,18 +7,23 @@ import {
7
7
  ItemAny,
8
8
  } from '@cntrl-site/sdk';
9
9
  import { isItemType } from '../utils/isItemType';
10
+ import { InteractionItemScrollTrigger, InteractionScrollTrigger } from '@cntrl-site/sdk/lib/types/article/Interaction';
11
+ import { ItemGeometryService } from '../ItemGeometry/ItemGeometryService';
10
12
 
11
13
  export class InteractionsRegistry implements InteractionsRegistryPort {
12
14
  private ctrls: Map<ItemId, ItemInteractionCtrl> = new Map();
13
15
  private items: ItemAny[];
16
+ private itemGeometryService: ItemGeometryService;
14
17
  private interactions: Interaction[];
15
18
  private stateItemsIdsMap: StateItemsIdsMap;
16
19
  private interactionStateMap: InteractionStateMap;
17
20
  private itemsStages: ItemStages;
18
21
  private activeStateIdInteractionIdMap: Record<StateId, InteractionId>;
22
+ private itemScrollPositionAnimationMap: Map<string, number> = new Map();
19
23
 
20
- constructor(article: Article, layoutId: string) {
24
+ constructor(article: Article, layoutId: string, itemGeometryService: ItemGeometryService) {
21
25
  this.items = this.unpackItems(article);
26
+ this.itemGeometryService = itemGeometryService;
22
27
  const interactions = article.interactions[layoutId] ?? [];
23
28
  const activeStatesIds = interactions.reduce<StateId[]>((map, inter) => {
24
29
  const activeStateId = inter.states.find((state) => state.id !== inter.startStateId)?.id;
@@ -54,6 +59,7 @@ export class InteractionsRegistry implements InteractionsRegistryPort {
54
59
  this.interactionStateMap = interactionStateMap;
55
60
  const itemStages = this.getDefaultItemStages();
56
61
  this.itemsStages = itemStages;
62
+ this.itemScrollPositionAnimationMap = new Map();
57
63
  }
58
64
 
59
65
  register(itemId: ItemId, ctrl: ItemInteractionCtrl) {
@@ -96,28 +102,69 @@ export class InteractionsRegistry implements InteractionsRegistryPort {
96
102
  return itemStyles;
97
103
  }
98
104
 
99
- getItemAvailableTriggers(itemId: string): Set<InteractionItemTrigger['type']> {
100
- const available = new Set<InteractionItemTrigger['type']>();
105
+ getItemAvailableTriggers(itemId: string): Set<InteractionItemTrigger['triggerEvent']> {
106
+ const available = new Set<InteractionItemTrigger['triggerEvent']>();
101
107
  const activeStates = Object.values(this.interactionStateMap);
102
108
  for (const interaction of this.interactions) {
103
109
  const { triggers } = interaction;
104
110
  for (const trigger of triggers) {
105
- if (!('itemId' in trigger)) continue;
111
+ if (!('triggerEvent' in trigger)) continue;
106
112
  if (trigger.itemId !== itemId) continue;
107
113
  if (activeStates.includes(trigger.from)) {
108
- available.add(trigger.type);
114
+ available.add(trigger.triggerEvent);
109
115
  }
110
116
  }
111
117
  }
112
118
  return available;
113
119
  }
114
120
 
121
+ getTriggerPositionForItemScrollTrigger(trigger: InteractionItemScrollTrigger): number {
122
+ const itemRect = this.itemGeometryService.getItemBoundary(trigger.itemId);
123
+ let triggerPosition = 0;
124
+ switch (trigger.screenPosition) {
125
+ case 'top':
126
+ triggerPosition = itemRect.top;
127
+ break;
128
+ case 'center':
129
+ triggerPosition = itemRect.top - window.innerHeight / 2;
130
+ break;
131
+ case 'bottom':
132
+ triggerPosition = itemRect.top - window.innerHeight;
133
+ break;
134
+ default:
135
+ triggerPosition = itemRect.top;
136
+ break;
137
+ }
138
+ switch (trigger.itemPosition) {
139
+ case 'top':
140
+ triggerPosition += 0;
141
+ break;
142
+ case 'center':
143
+ triggerPosition += itemRect.height / 2;
144
+ break;
145
+ case 'bottom':
146
+ triggerPosition += itemRect.height;
147
+ break;
148
+ default:
149
+ triggerPosition += 0;
150
+ break;
151
+ }
152
+ triggerPosition += trigger.offset;
153
+ return triggerPosition;
154
+ }
155
+
156
+ isItemScrollTriggerOnStartScreen(trigger: InteractionItemScrollTrigger): boolean {
157
+ const triggerPosition = this.getTriggerPositionForItemScrollTrigger(trigger);
158
+ return triggerPosition < 0;
159
+ }
160
+
115
161
  notifyLoad() {
116
162
  const timestamp = Date.now();
117
163
  for (const interaction of this.interactions) {
118
164
  const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
119
165
  const matchingTrigger = interaction.triggers.find(trigger =>
120
- 'position' in trigger && trigger.position === 0 && trigger.from === currentStateId
166
+ ('position' in trigger && trigger.position === 0 && trigger.from === currentStateId)
167
+ || (trigger.type === 'item-scroll-position' && this.items.find((item) => item.id === trigger.itemId) && trigger.from === currentStateId && this.isItemScrollTriggerOnStartScreen(trigger))
121
168
  );
122
169
  if (!matchingTrigger) continue;
123
170
  const activeStateId = this.getActiveInteractionState(interaction.id);
@@ -155,22 +202,80 @@ export class InteractionsRegistry implements InteractionsRegistryPort {
155
202
  }
156
203
  }
157
204
 
205
+ scrollPositionNotifyScroll(trigger: InteractionScrollTrigger, position: number): boolean {
206
+ if (trigger.position === 0) return false;
207
+ const triggerPosition = trigger.position * window.innerWidth;
208
+ return triggerPosition < position;
209
+ }
210
+
211
+ scrollItemPositionNotifyScroll(trigger: InteractionItemScrollTrigger, position: number, interaction: Interaction): boolean {
212
+ let triggerPosition;
213
+ let isScrolledPastTrigger = false;
214
+ const itemRect = this.itemGeometryService.getItemBoundary(trigger.itemId);
215
+ if (!itemRect) return false;
216
+ switch (trigger.screenPosition) {
217
+ case 'top':
218
+ triggerPosition = 0;
219
+ break;
220
+ case 'center':
221
+ triggerPosition = window.innerHeight / 2;
222
+ break;
223
+ case 'bottom':
224
+ triggerPosition = window.innerHeight;
225
+ break;
226
+ default:
227
+ triggerPosition = 0;
228
+ break;
229
+ }
230
+ triggerPosition -= trigger.offset;
231
+ switch (trigger.itemPosition) {
232
+ case 'top':
233
+ isScrolledPastTrigger = itemRect.y < triggerPosition;
234
+ break;
235
+ case 'center':
236
+ isScrolledPastTrigger = itemRect.y + itemRect.height / 2 < triggerPosition;
237
+ break;
238
+ case 'bottom':
239
+ isScrolledPastTrigger = itemRect.bottom < triggerPosition;
240
+ break;
241
+ default:
242
+ isScrolledPastTrigger = itemRect.y < triggerPosition;
243
+ break;
244
+ }
245
+ if (isScrolledPastTrigger && !this.itemScrollPositionAnimationMap.get(interaction.id)) {
246
+ this.itemScrollPositionAnimationMap.set(interaction.id, position + itemRect.y - triggerPosition);
247
+ }
248
+ if (this.itemScrollPositionAnimationMap.get(interaction.id)! >= position) isScrolledPastTrigger = false;
249
+ return isScrolledPastTrigger;
250
+ }
251
+
158
252
  notifyScroll(position: number) {
159
253
  const timestamp = Date.now();
160
254
  for (const interaction of this.interactions) {
161
255
  const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
162
256
  const activeStateId = interaction.states.find((state) => state.id !== interaction.startStateId)?.id;
163
257
  const matchingTrigger = interaction.triggers.find((trigger) => {
164
- if (!('position' in trigger) || trigger.position === 0) return false;
165
- const triggerPosition = trigger.position * window.innerWidth;
166
- const isScrolledPastTrigger = triggerPosition < position;
258
+ if (trigger.type !== 'scroll-position' && trigger.type !== 'item-scroll-position') return false;
259
+ let isScrolledPastTrigger = false;
260
+ if (trigger.type === 'scroll-position') {
261
+ isScrolledPastTrigger = this.scrollPositionNotifyScroll(trigger, position);
262
+ }
263
+ if (trigger.type === 'item-scroll-position') {
264
+ isScrolledPastTrigger = this.scrollItemPositionNotifyScroll(trigger, position, interaction);
265
+ }
167
266
  if (!isScrolledPastTrigger && !trigger.isReverse) return false;
168
267
  const stateId = isScrolledPastTrigger ? trigger.from : trigger.to;
169
268
  return stateId === currentStateId;
170
269
  });
171
- if (!matchingTrigger || !('position' in matchingTrigger) || !activeStateId) continue;
172
- const triggerPosition = matchingTrigger.position * window.innerWidth;
173
- const isScrolledPastTrigger = triggerPosition < position;
270
+ if (!matchingTrigger || (matchingTrigger.type !== 'scroll-position' && matchingTrigger.type !== 'item-scroll-position') || !activeStateId) continue;
271
+ let isScrolledPastTrigger = false;
272
+ if (matchingTrigger.type === 'scroll-position') {
273
+ const triggerPosition = matchingTrigger.position * window.innerWidth;
274
+ isScrolledPastTrigger = triggerPosition < position;
275
+ }
276
+ if (matchingTrigger.type === 'item-scroll-position') {
277
+ isScrolledPastTrigger = this.itemScrollPositionAnimationMap.get(interaction.id)! < position; // scroll down
278
+ }
174
279
  const targetStateId = isScrolledPastTrigger ? matchingTrigger.to : matchingTrigger.from;
175
280
  this.setCurrentStateForInteraction(interaction.id, targetStateId);
176
281
  const transitioningItems = this.stateItemsIdsMap[activeStateId] ?? [];
@@ -210,10 +315,10 @@ export class InteractionsRegistry implements InteractionsRegistryPort {
210
315
  for (const interaction of this.interactions) {
211
316
  const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
212
317
  const matchingTrigger = interaction.triggers.find((trigger) =>
213
- 'itemId' in trigger
318
+ 'triggerEvent' in trigger
214
319
  && trigger.itemId === itemId
215
320
  && trigger.from === currentStateId
216
- && trigger.type === triggerType
321
+ && trigger.triggerEvent === triggerType
217
322
  );
218
323
  if (!matchingTrigger) continue;
219
324
  const activeStateId = this.getActiveInteractionState(interaction.id);
@@ -366,7 +471,7 @@ type TransitioningStage = {
366
471
  type ActiveStage = { type: 'active'; itemId: string; interactionId: string; stateId?: string; isStartState: boolean; updated: number; };
367
472
  type InteractionStateMap = Record<InteractionId, StateId>;
368
473
  type StateItemsIdsMap = Record<StateId, ItemId[]>;
369
- type TriggerType = InteractionItemTrigger['type'];
474
+ type TriggerType = InteractionItemTrigger['triggerEvent'];
370
475
  type InteractionId = string;
371
476
  type StateId = string;
372
477
  type ItemId = string;
@@ -1,6 +1,6 @@
1
1
  import { InteractionsRegistryPort, ItemInteractionCtrl } from './types';
2
2
  import { getTransition } from './getTransition';
3
- import { getStyleKeysFromCSSProperty } from './CSSPropertyNameMap';
3
+ import { getStyleKeysFromCSSProperty } from '../ItemGeometry/CSSPropertyNameMap';
4
4
  import { FillLayer, InteractionItemTrigger } from '@cntrl-site/sdk';
5
5
 
6
6
  export class ItemInteractionController implements ItemInteractionCtrl {
@@ -30,7 +30,7 @@ export class ItemInteractionController implements ItemInteractionCtrl {
30
30
  };
31
31
  }
32
32
 
33
- getHasTrigger(itemId: string, triggerType: InteractionItemTrigger['type']): boolean {
33
+ getHasTrigger(itemId: string, triggerType: InteractionItemTrigger['triggerEvent']): boolean {
34
34
  const triggers = this.registry.getItemAvailableTriggers(itemId);
35
35
  return triggers.has(triggerType);
36
36
  }
@@ -1,4 +1,4 @@
1
- import { CSSPropertyNameMap } from './CSSPropertyNameMap';
1
+ import { CSSPropertyNameMap } from '../ItemGeometry/CSSPropertyNameMap';
2
2
 
3
3
  export function getTransition(
4
4
  state: Record<string, StateInfo>,
@@ -2,7 +2,7 @@ import { ArticleItemType, FillLayer, InteractionItemTrigger, ItemState } from '@
2
2
 
3
3
  export interface ItemInteractionCtrl {
4
4
  getState<T>(keys: string[]): StateCSSInfo<T>;
5
- getHasTrigger(itemId: string, triggerType: InteractionItemTrigger['type']): boolean;
5
+ getHasTrigger(itemId: string, triggerType: InteractionItemTrigger['triggerEvent']): boolean;
6
6
  sendTrigger(type: 'click' | 'hover-in' | 'hover-out'): void;
7
7
  handleTransitionEnd?: (styleKey: string) => void;
8
8
  handleTransitionStart?: (styleKeys: string[]) => void;
@@ -14,7 +14,7 @@ export interface ItemInteractionCtrl {
14
14
  export interface InteractionsRegistryPort {
15
15
  register(itemId: string, ctrl: ItemInteractionCtrl): void;
16
16
  getStatePropsForItem(itemId: string): StateProps<unknown>;
17
- getItemAvailableTriggers(itemId: string): Set<InteractionItemTrigger['type']>;
17
+ getItemAvailableTriggers(itemId: string): Set<InteractionItemTrigger['triggerEvent']>;
18
18
  notifyItemTrigger(itemId: string, type: TriggerType): void;
19
19
  notifyTransitionEnd(itemId: string): void;
20
20
  }
@@ -3,6 +3,7 @@ import { InteractionsRegistry } from '../interactions/InteractionsRegistry';
3
3
  import { Article } from '@cntrl-site/sdk';
4
4
  import { useCurrentLayout } from '../common/useCurrentLayout';
5
5
  import { ArticleRectContext } from './ArticleRectContext';
6
+ import { ItemGeometryContext } from '../ItemGeometry/ItemGeometryContext';
6
7
 
7
8
  export const InteractionsContext = createContext<InteractionsRegistry | undefined>(undefined);
8
9
 
@@ -13,10 +14,11 @@ interface Props {
13
14
  export const InteractionsProvider: FC<PropsWithChildren<Props>> = ({ article, children }) => {
14
15
  const { layoutId } = useCurrentLayout();
15
16
  const articleRectObserver = useContext(ArticleRectContext);
17
+ const itemGeometryService = useContext(ItemGeometryContext);
16
18
  const registry = useMemo(() => {
17
19
  if (!layoutId) return;
18
- return new InteractionsRegistry(article, layoutId);
19
- }, [layoutId]);
20
+ return new InteractionsRegistry(article, layoutId, itemGeometryService);
21
+ }, [layoutId, itemGeometryService]);
20
22
 
21
23
  useEffect(() => {
22
24
  if (!registry || !articleRectObserver) return;
@@ -0,0 +1,19 @@
1
+ const reCSSNumeric = /([+-]?(?:\d*\.\d|\d)\d*)(\w+)/;
2
+
3
+ interface CSSNumberValue {
4
+ value: number;
5
+ unit?: string;
6
+ }
7
+
8
+ export function parseCSSNumber(input: string): CSSNumberValue {
9
+ const match = reCSSNumeric.exec(input);
10
+ if (!match) return { value: Number.NaN };
11
+ return {
12
+ value: Number.parseFloat(match[1]),
13
+ unit: match[2]
14
+ };
15
+ }
16
+
17
+ export function formatCSSNumber(input: CSSNumberValue): string {
18
+ return `${input.value}${input.unit ?? ''}`;
19
+ }