@cntrl-site/sdk-nextjs 1.0.18 → 1.0.19-alpha.1

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 (40) hide show
  1. package/.idea/codeStyles/codeStyleConfig.xml +5 -0
  2. package/.idea/inspectionProfiles/Project_Default.xml +15 -0
  3. package/jest.config.js +2 -2
  4. package/lib/components/Article.js +9 -8
  5. package/lib/components/Item.js +33 -13
  6. package/lib/components/items/CodeEmbedItem.js +11 -8
  7. package/lib/components/items/CustomItem.js +10 -7
  8. package/lib/components/items/GroupItem.js +11 -8
  9. package/lib/components/items/ImageItem.js +21 -15
  10. package/lib/components/items/RectangleItem.js +11 -8
  11. package/lib/components/items/RichTextItem.js +10 -7
  12. package/lib/components/items/VideoItem.js +16 -12
  13. package/lib/components/items/VimeoEmbed.js +19 -15
  14. package/lib/components/items/YoutubeEmbed.js +18 -13
  15. package/lib/components/useStatesClassNames.js +18 -0
  16. package/lib/components/useStatesTransitions.js +89 -0
  17. package/lib/provider/InteractionsContext.js +45 -0
  18. package/lib/utils/{HoverStyles/HoverStyles.js → StateStyles/StateStyles.js} +36 -24
  19. package/lib/utils/getStatesCSS.js +16 -0
  20. package/package.json +7 -4
  21. package/src/components/Article.tsx +31 -28
  22. package/src/components/ArticleWrapper.tsx +1 -2
  23. package/src/components/Item.tsx +38 -18
  24. package/src/components/items/CodeEmbedItem.tsx +12 -9
  25. package/src/components/items/CustomItem.tsx +12 -9
  26. package/src/components/items/GroupItem.tsx +12 -9
  27. package/src/components/items/ImageItem.tsx +26 -19
  28. package/src/components/items/RectangleItem.tsx +13 -10
  29. package/src/components/items/RichTextItem.tsx +17 -9
  30. package/src/components/items/VideoItem.tsx +19 -15
  31. package/src/components/items/VimeoEmbed.tsx +20 -16
  32. package/src/components/items/YoutubeEmbed.tsx +20 -16
  33. package/src/components/useStatesClassNames.ts +23 -0
  34. package/src/components/useStatesTransitions.ts +95 -0
  35. package/src/provider/InteractionsContext.test.tsx +97 -0
  36. package/src/provider/InteractionsContext.tsx +65 -0
  37. package/src/utils/{HoverStyles/HoverStyles.ts → StateStyles/StateStyles.ts} +41 -27
  38. package/src/utils/getStatesCSS.ts +24 -0
  39. package/cntrl-site-sdk-nextjs-1.0.17.tgz +0 -0
  40. package/cntrl-site-sdk-nextjs-1.0.18.tgz +0 -0
@@ -1,16 +1,18 @@
1
1
  import { FC, useId, useMemo, useRef, useState } from 'react';
2
2
  import JSXStyle from 'styled-jsx/style';
3
3
  import { CntrlColor } from '@cntrl-site/color';
4
- import { ArticleItemType, getLayoutStyles, VideoItem as TVideoItem } from '@cntrl-site/sdk';
4
+ import { getLayoutStyles, VideoItem as TVideoItem } from '@cntrl-site/sdk';
5
5
  import { ItemProps } from '../Item';
6
6
  import { LinkWrapper } from '../LinkWrapper';
7
7
  import { useFileItem } from './useFileItem';
8
8
  import { useItemAngle } from '../useItemAngle';
9
9
  import { useCntrlContext } from '../../provider/useCntrlContext';
10
- import { getHoverStyles, getTransitions } from '../../utils/HoverStyles/HoverStyles';
11
10
  import { useRegisterResize } from "../../common/useRegisterResize";
12
11
  import { useLayoutContext } from '../useLayoutContext';
13
12
  import { ScrollPlaybackVideo } from '../ScrollPlaybackVideo';
13
+ import { useStatesClassNames } from '../useStatesClassNames';
14
+ import { getStatesCSS } from '../../utils/getStatesCSS';
15
+ import { useStatesTransitions } from '../useStatesTransitions';
14
16
 
15
17
  export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize }) => {
16
18
  const id = useId();
@@ -22,8 +24,12 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
22
24
  const videoRef = useRef<HTMLVideoElement | null>(null);
23
25
  const layoutId = useLayoutContext();
24
26
  const scrollPlayback = layoutId ? item.layoutParams[layoutId].scrollPlayback : null;
25
- const layoutValues: Record<string, any>[] = [item.area, item.layoutParams, item.state.hover];
27
+ const layoutValues: Record<string, any>[] = [item.area, item.layoutParams, item.state];
28
+ const statesWrapperClassNames = useStatesClassNames(item.id, item.state, 'video-wrapper');
29
+ const statesVideoClassNames = useStatesClassNames(item.id, item.state, 'video');
26
30
  const hasScrollPlayback = scrollPlayback !== null;
31
+ useStatesTransitions(ref, item.state, ['angle', 'opacity', 'blur']);
32
+ useStatesTransitions(videoRef.current, item.state, ['strokeWidth', 'radius', 'strokeColor']);
27
33
  useRegisterResize(ref, onResize);
28
34
  const inlineStyles = {
29
35
  ...(radius !== undefined ? { borderRadius: `${radius * 100}vw` } : {}),
@@ -34,7 +40,7 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
34
40
  return (
35
41
  <LinkWrapper url={item.link?.url} target={item.link?.target}>
36
42
  <div
37
- className={`video-wrapper-${item.id}`}
43
+ className={`video-wrapper-${item.id} ${statesWrapperClassNames}`}
38
44
  ref={setRef}
39
45
  style={{
40
46
  ...(opacity !== undefined ? { opacity } : {}),
@@ -48,7 +54,7 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
48
54
  src={item.commonParams.url}
49
55
  playbackParams={scrollPlayback}
50
56
  style={inlineStyles}
51
- className={`video video-playback-wrapper video-${item.id}`}
57
+ className={`video video-playback-wrapper video-${item.id} ${statesWrapperClassNames}`}
52
58
  />
53
59
  ) : (
54
60
  <video
@@ -58,7 +64,7 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
58
64
  muted
59
65
  loop
60
66
  playsInline
61
- className={`video video-${item.id}`}
67
+ className={`video video-${item.id} ${statesVideoClassNames}`}
62
68
  style={inlineStyles}
63
69
  >
64
70
  <source src={item.commonParams.url} />
@@ -92,26 +98,24 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
92
98
  display: flex;
93
99
  justify-content: center;
94
100
  }
95
- ${getLayoutStyles(layouts, layoutValues, ([area, layoutParams, hoverParams]) => {
101
+ ${getLayoutStyles(layouts, layoutValues, ([area, layoutParams, stateParams]) => {
102
+ const wrapperStatesCSS = getStatesCSS(item.id, 'video-wrapper', ['angle', 'opacity', 'blur'], stateParams);
103
+ const videoStatesCSS = getStatesCSS(item.id, 'video', ['strokeWidth', 'radius', 'strokeColor'], stateParams);
96
104
  return (`
97
105
  .video-wrapper-${item.id} {
98
106
  opacity: ${layoutParams.opacity};
99
107
  transform: rotate(${area.angle}deg);
100
108
  filter: ${layoutParams.blur !== 0 ? `blur(${layoutParams.blur * 100}vw)` : 'unset'};
101
- transition: ${getTransitions<ArticleItemType.Video>(['angle', 'opacity', 'blur'], hoverParams)};
102
- }
103
- .video-wrapper-${item.id}:hover {
104
- ${getHoverStyles<ArticleItemType.Video>(['angle', 'opacity', 'blur'], hoverParams)};
109
+ transition: all 0.2s ease;
105
110
  }
106
111
  .video-${item.id} {
107
112
  border-color: ${CntrlColor.parse(layoutParams.strokeColor).fmt('rgba')};
108
113
  border-radius: ${layoutParams.radius * 100}vw;
109
114
  border-width: ${layoutParams.strokeWidth * 100}vw;
110
- transition: ${getTransitions<ArticleItemType.Video>(['strokeWidth', 'radius', 'strokeColor'], hoverParams)};
111
- }
112
- .video-wrapper-${item.id}:hover .video {
113
- ${getHoverStyles<ArticleItemType.Video>(['strokeWidth', 'radius', 'strokeColor'], hoverParams)};
115
+ transition: all 0.2s ease;
114
116
  }
117
+ ${wrapperStatesCSS}
118
+ ${videoStatesCSS}
115
119
  `);
116
120
  })}
117
121
  `}</JSXStyle>
@@ -5,10 +5,12 @@ import { ItemProps } from '../Item';
5
5
  import { LinkWrapper } from '../LinkWrapper';
6
6
  import { useEmbedVideoItem } from './useEmbedVideoItem';
7
7
  import { useItemAngle } from '../useItemAngle';
8
- import { ArticleItemType, getLayoutStyles, VimeoEmbedItem as TVimeoEmbedItem } from '@cntrl-site/sdk';
8
+ import { getLayoutStyles, VimeoEmbedItem as TVimeoEmbedItem } from '@cntrl-site/sdk';
9
9
  import { useCntrlContext } from '../../provider/useCntrlContext';
10
- import { getHoverStyles, getTransitions } from '../../utils/HoverStyles/HoverStyles';
11
10
  import { useRegisterResize } from "../../common/useRegisterResize";
11
+ import { useStatesClassNames } from '../useStatesClassNames';
12
+ import { getStatesCSS } from '../../utils/getStatesCSS';
13
+ import { useStatesTransitions } from '../useStatesTransitions';
12
14
 
13
15
  export const VimeoEmbedItem: FC<ItemProps<TVimeoEmbedItem>> = ({ item, sectionId, onResize }) => {
14
16
  const id = useId();
@@ -21,8 +23,12 @@ export const VimeoEmbedItem: FC<ItemProps<TVimeoEmbedItem>> = ({ item, sectionId
21
23
  const [ref, setRef] = useState<HTMLDivElement | null>(null);
22
24
  const [imgRef, setImgRef] = useState<HTMLImageElement | null>(null);
23
25
  const [isCoverVisible, setIsCoverVisible] = useState(false);
24
- const layoutValues: Record<string, any>[] = [item.area, item.layoutParams, item.state.hover];
26
+ const layoutValues: Record<string, any>[] = [item.area, item.layoutParams, item.state];
27
+ const wrapperClassNames = useStatesClassNames(item.id, item.state, 'embed-video-wrapper');
28
+ const videoClassNames = useStatesClassNames(item.id, item.state, 'embed-video');
25
29
  useRegisterResize(ref, onResize);
30
+ useStatesTransitions(ref, item.state, ['angle', 'blur', 'opacity']);
31
+ useStatesTransitions(iframeRef, item.state, ['radius']);
26
32
  const getValidVimeoUrl = (url: string): string => {
27
33
  const validURL = new URL(url);
28
34
  validURL.searchParams.append('controls', String(controls));
@@ -60,7 +66,7 @@ export const VimeoEmbedItem: FC<ItemProps<TVimeoEmbedItem>> = ({ item, sectionId
60
66
  return (
61
67
  <LinkWrapper url={item.link?.url} target={item.link?.target}>
62
68
  <div
63
- className={`embed-video-wrapper-${item.id}`}
69
+ className={`embed-video-wrapper-${item.id} ${wrapperClassNames}`}
64
70
  ref={setRef}
65
71
  style={{
66
72
  ...(opacity !== undefined ? { opacity } : {}),
@@ -96,7 +102,7 @@ export const VimeoEmbedItem: FC<ItemProps<TVimeoEmbedItem>> = ({ item, sectionId
96
102
  )}
97
103
  <iframe
98
104
  ref={setIframeRef}
99
- className="embedVideo"
105
+ className={`embed-video ${videoClassNames}`}
100
106
  src={validUrl || ''}
101
107
  allow="autoplay; fullscreen; picture-in-picture;"
102
108
  allowFullScreen
@@ -111,31 +117,29 @@ export const VimeoEmbedItem: FC<ItemProps<TVimeoEmbedItem>> = ({ item, sectionId
111
117
  width: 100%;
112
118
  height: 100%;
113
119
  }
114
- .embedVideo {
120
+ .embed-video {
115
121
  width: 100%;
116
122
  height: 100%;
117
123
  z-index: 1;
118
124
  border: none;
119
125
  overflow: hidden;
120
126
  }
121
- ${getLayoutStyles(layouts, layoutValues, ([area, layoutParams, hoverParams]) => {
127
+ ${getLayoutStyles(layouts, layoutValues, ([area, layoutParams, stateParams]) => {
128
+ const wrapperStatesCSS = getStatesCSS(item.id, 'embed-video-wrapper', ['angle', 'blur', 'opacity'], stateParams);
129
+ const videoStatesCSS = getStatesCSS(item.id, 'embed-video', ['radius'], stateParams);
122
130
  return (`
123
131
  .embed-video-wrapper-${item.id} {
124
132
  opacity: ${layoutParams.opacity};
125
133
  transform: rotate(${area.angle}deg);
126
134
  filter: ${layoutParams.blur !== 0 ? `blur(${layoutParams.blur * 100}vw)` : 'unset'};
127
- transition: ${getTransitions<ArticleItemType.VimeoEmbed>(['angle', 'blur', 'opacity'], hoverParams)};
135
+ transition: all 0.2s ease;
128
136
  }
129
- .embed-video-wrapper-${item.id}:hover {
130
- ${getHoverStyles<ArticleItemType.VimeoEmbed>(['angle', 'blur', 'opacity'], hoverParams)}
131
- }
132
- .embed-video-wrapper-${item.id} .embedVideo {
137
+ .embed-video-wrapper-${item.id} .embed-video {
133
138
  border-radius: ${layoutParams.radius * 100}vw;
134
- transition: ${getTransitions<ArticleItemType.VimeoEmbed>(['radius'], hoverParams)};
135
- }
136
- .embed-video-wrapper-${item.id}:hover .embedVideo {
137
- ${getHoverStyles<ArticleItemType.VimeoEmbed>(['radius'], hoverParams)};
139
+ transition: all 0.2s ease;
138
140
  }
141
+ ${wrapperStatesCSS}
142
+ ${videoStatesCSS}
139
143
  `);
140
144
  })}
141
145
  `}</JSXStyle>
@@ -5,12 +5,14 @@ import { LinkWrapper } from '../LinkWrapper';
5
5
  import { getYoutubeId } from '../../utils/getValidYoutubeUrl';
6
6
  import { useEmbedVideoItem } from './useEmbedVideoItem';
7
7
  import { useItemAngle } from '../useItemAngle';
8
- import { ArticleItemType, getLayoutStyles, YoutubeEmbedItem as TYoutubeEmbedItem } from '@cntrl-site/sdk';
9
- import { getHoverStyles, getTransitions } from '../../utils/HoverStyles/HoverStyles';
8
+ import { getLayoutStyles, YoutubeEmbedItem as TYoutubeEmbedItem } from '@cntrl-site/sdk';
10
9
  import { useCntrlContext } from '../../provider/useCntrlContext';
11
10
  import { useYouTubeIframeApi } from '../../utils/Youtube/useYouTubeIframeApi';
12
11
  import { YTPlayer } from '../../utils/Youtube/YoutubeIframeApi';
13
12
  import { useRegisterResize } from "../../common/useRegisterResize";
13
+ import { useStatesClassNames } from '../useStatesClassNames';
14
+ import { getStatesCSS } from '../../utils/getStatesCSS';
15
+ import { useStatesTransitions } from '../useStatesTransitions';
14
16
 
15
17
  export const YoutubeEmbedItem: FC<ItemProps<TYoutubeEmbedItem>> = ({ item, sectionId, onResize }) => {
16
18
  const id = useId();
@@ -20,12 +22,16 @@ export const YoutubeEmbedItem: FC<ItemProps<TYoutubeEmbedItem>> = ({ item, secti
20
22
  const angle = useItemAngle(item, sectionId);
21
23
  const YT = useYouTubeIframeApi();
22
24
  const [div, setDiv] = useState<HTMLDivElement | null>(null);
25
+ const [wrapperRef, setWrapperRef] = useState<HTMLDivElement | null>(null);
23
26
  const [player, setPlayer] = useState<YTPlayer | undefined>(undefined);
24
27
  const [isCoverVisible, setIsCoverVisible] = useState(false);
25
28
  const [imgRef, setImgRef] = useState<HTMLImageElement | null>(null);
26
- const layoutValues: Record<string, any>[] = [item.area, item.layoutParams, item.state.hover];
29
+ const layoutValues: Record<string, any>[] = [item.area, item.layoutParams, item.state];
30
+ const wrapperClassNames = useStatesClassNames(item.id, item.state, 'embed-youtube-video-wrapper');
31
+ const embedClassNames = useStatesClassNames(item.id, item.state, 'embed');
27
32
  useRegisterResize(div, onResize);
28
-
33
+ useStatesTransitions(wrapperRef, item.state, ['angle', 'blur', 'opacity']);
34
+ useStatesTransitions(div, item.state, ['radius']);
29
35
  useEffect(() => {
30
36
  const newUrl = new URL(url);
31
37
  const videoId = getYoutubeId(newUrl);
@@ -78,7 +84,8 @@ export const YoutubeEmbedItem: FC<ItemProps<TYoutubeEmbedItem>> = ({ item, secti
78
84
  return (
79
85
  <LinkWrapper url={item.link?.url} target={item.link?.target}>
80
86
  <div
81
- className={`embed-youtube-video-wrapper-${item.id}`}
87
+ ref={setWrapperRef}
88
+ className={`embed-youtube-video-wrapper-${item.id} ${wrapperClassNames}`}
82
89
  onMouseEnter={() => {
83
90
  if (!player || play !== 'on-hover') return;
84
91
  player.playVideo();
@@ -113,11 +120,10 @@ export const YoutubeEmbedItem: FC<ItemProps<TYoutubeEmbedItem>> = ({ item, secti
113
120
  />
114
121
  )}
115
122
  <div
116
- className={`embed-${item.id}`}
123
+ className={`embed-${item.id} ${embedClassNames}`}
117
124
  ref={setDiv}
118
125
  style={{
119
126
  ...(radius !== undefined ? { borderRadius: `${radius * 100}vw` } : {}),
120
-
121
127
  }}
122
128
  />
123
129
  </div>
@@ -137,24 +143,22 @@ export const YoutubeEmbedItem: FC<ItemProps<TYoutubeEmbedItem>> = ({ item, secti
137
143
  z-index: 1;
138
144
  border: none;
139
145
  }
140
- ${getLayoutStyles(layouts, layoutValues, ([area, layoutParams, hoverParams]) => {
146
+ ${getLayoutStyles(layouts, layoutValues, ([area, layoutParams, stateParams]) => {
147
+ const wrapperStatesCSS = getStatesCSS(item.id, 'embed-youtube-video-wrapper', ['angle', 'blur', 'opacity'], stateParams);
148
+ const embedStatesCSS = getStatesCSS(item.id, 'embed', ['radius'], stateParams);
141
149
  return (`
142
150
  .embed-youtube-video-wrapper-${item.id} {
143
151
  opacity: ${layoutParams.opacity};
144
152
  transform: rotate(${area.angle}deg);
145
153
  filter: ${layoutParams.blur !== 0 ? `blur(${layoutParams.blur * 100}vw)` : 'unset'};
146
- transition: ${getTransitions<ArticleItemType.YoutubeEmbed>(['angle', 'blur', 'opacity'], hoverParams)};
154
+ transition: all 0.2s ease;
147
155
  }
148
156
  .embed-youtube-video-wrapper-${item.id} .embed-${item.id} {
149
157
  border-radius: ${layoutParams.radius * 100}vw;
150
- transition: ${getTransitions<ArticleItemType.YoutubeEmbed>(['radius'], hoverParams)};
151
- }
152
- .embed-youtube-video-wrapper-${item.id}:hover {
153
- ${getHoverStyles<ArticleItemType.YoutubeEmbed>(['angle', 'blur', 'opacity'], hoverParams)};
154
- }
155
- .embed-youtube-video-wrapper-${item.id}:hover .embed-${item.id} {
156
- ${getHoverStyles<ArticleItemType.YoutubeEmbed>(['radius'], hoverParams)};
158
+ transition: all 0.2s ease;
157
159
  }
160
+ ${wrapperStatesCSS}
161
+ ${embedStatesCSS}
158
162
  `);
159
163
  })}
160
164
  `}</JSXStyle>
@@ -0,0 +1,23 @@
1
+ import { ItemStateParams } from '@cntrl-site/sdk';
2
+ import { useContext } from 'react';
3
+ import { InteractionsContext } from '../provider/InteractionsContext';
4
+ import { useCurrentLayout } from '../common/useCurrentLayout';
5
+
6
+ export function useStatesClassNames(
7
+ itemId: string,
8
+ itemStates: Record<LayoutId, Record<StateId, ItemStateParams>>,
9
+ uniquePrefix: string, // unique in terms of item type, ie. rectangle, code-embed, group etc
10
+ ): string {
11
+ const { interactionsStatesMap } = useContext(InteractionsContext);
12
+ const { layoutId } = useCurrentLayout();
13
+ const activeStates = Object.values(interactionsStatesMap);
14
+ const statesForLayout = layoutId ? itemStates[layoutId] : {};
15
+ const stateClassNames = Object.keys(statesForLayout ?? {})
16
+ .filter((stateId) => activeStates.includes(stateId))
17
+ .map((stateId) => `${uniquePrefix}-${itemId}-state-${stateId}`)
18
+ .join(' ');
19
+ return stateClassNames;
20
+ }
21
+
22
+ type StateId = string;
23
+ type LayoutId = string;
@@ -0,0 +1,95 @@
1
+ import { ArticleItemType, ItemState, ItemStateParams, ItemStatesMap } from '@cntrl-site/sdk';
2
+ import { useContext, useEffect, useMemo } from 'react';
3
+ import { InteractionsContext } from '../provider/InteractionsContext';
4
+ import { useCurrentLayout } from '../common/useCurrentLayout';
5
+
6
+ type AllKeys<T> = T extends any ? keyof T : never;
7
+ type StatePropertyKey = AllKeys<ItemStatesMap[keyof ItemStatesMap]>;
8
+
9
+ export function useStatesTransitions(
10
+ el: HTMLElement | null,
11
+ state: ItemState<ArticleItemType>,
12
+ values: StatePropertyKey[]
13
+ ) {
14
+ const { interactionsStatesMap } = useContext(InteractionsContext);
15
+ const { layoutId } = useCurrentLayout();
16
+ const activeStates = Object.values(interactionsStatesMap);
17
+ const statesForLayout = useMemo(() => layoutId ? state[layoutId] : {}, [layoutId]);
18
+ const itemStatesIds = Object.keys(statesForLayout);
19
+ const itemActiveStateId = activeStates.find((stateId) => itemStatesIds.includes(stateId));
20
+ useEffect(() => {
21
+ if (!itemActiveStateId || !el) return;
22
+ const state = statesForLayout[itemActiveStateId];
23
+ const transitionStr = getTransition(state, 'in', values);
24
+ const slowestProp = getSlowestProperty(state, 'in', values);
25
+ if (!transitionStr) return;
26
+ el.style.transition = transitionStr;
27
+ el.ontransitionend = (e) => {
28
+ e.stopPropagation();
29
+ if (e.target !== el || e.propertyName !== slowestProp) return;
30
+ el.style.transition = 'none';
31
+ };
32
+ return () => {
33
+ const transitionStr = getTransition(state, 'out', values);
34
+ const slowestProp = getSlowestProperty(state, 'out', values);
35
+ if (!transitionStr) return;
36
+ el.style.transition = transitionStr;
37
+ el.ontransitionend = (e) => {
38
+ e.stopPropagation();
39
+ if (e.target !== el || e.propertyName !== slowestProp) return;
40
+ el.style.transition = 'none';
41
+ };
42
+ };
43
+ }, [itemActiveStateId, statesForLayout, el]);
44
+ }
45
+
46
+ const CSSPropertyNameMap: Record<keyof ItemState<ArticleItemType>, string> = {
47
+ 'width': 'width',
48
+ 'height': 'height',
49
+ 'top': 'top',
50
+ 'left': 'left',
51
+ 'scale': 'transform',
52
+ 'angle': 'transform',
53
+ 'opacity': 'opacity',
54
+ 'radius': 'border-radius',
55
+ 'strokeWidth': 'border-width',
56
+ 'strokeColor': 'border-color',
57
+ 'fillColor': 'background-color',
58
+ 'blur': 'filter',
59
+ 'backdropBlur': 'backdrop-filter',
60
+ 'letterSpacing': 'letter-spacing',
61
+ 'wordSpacing': 'word-spacing',
62
+ 'color': 'color'
63
+ };
64
+
65
+ function getTransition(
66
+ state: ItemStateParams,
67
+ direction: 'in' | 'out',
68
+ values: string[]
69
+ ) {
70
+ return Object.entries(state)
71
+ .filter(([key]) => values.includes(key))
72
+ .map(([key, params]) => {
73
+ const cssKey = CSSPropertyNameMap[key];
74
+ if (!cssKey) {
75
+ throw new Error(`Cannot translate "${key}" to a CSS property.`);
76
+ }
77
+ return `${cssKey} ${params[direction].duration}ms ${params[direction].timing} ${params[direction].delay}ms`;
78
+ }, [])
79
+ .join(', ');
80
+ }
81
+
82
+ function getSlowestProperty(state: ItemStateParams, direction: 'in' | 'out', values: string[]): string {
83
+ const mappedEntries = Object.entries(state)
84
+ .filter(([key]) => values.includes(key))
85
+ .map(([key, params]) => {
86
+ const transitionParams = params[direction];
87
+ return {
88
+ key,
89
+ time: transitionParams.duration + transitionParams.delay
90
+ }
91
+ });
92
+ if (mappedEntries.length === 0) return '';
93
+ const { key } = mappedEntries.reduce((slowest, current) => current.time > slowest.time ? current : slowest);
94
+ return CSSPropertyNameMap[key];
95
+ }
@@ -0,0 +1,97 @@
1
+ import React from 'react';
2
+ import { render, waitFor } from '@testing-library/react';
3
+ import { InteractionsProvider, InteractionsContext } from './InteractionsContext';
4
+ import { Interaction } from '@cntrl-site/sdk';
5
+
6
+ describe('InteractionsProvider', () => {
7
+ const interactions: Interaction[] = [
8
+ {
9
+ id: 'interaction1',
10
+ startStateId: 'state1',
11
+ triggers: [
12
+ { itemId: 'item1', type: 'click', from: 'state1', to: 'state2' }
13
+ ],
14
+ states: [{ id: 'state1' }, { id: 'state2' }]
15
+ },
16
+ {
17
+ id: 'interaction2',
18
+ startStateId: 'state3',
19
+ triggers: [
20
+ { itemId: 'item2', type: 'hover-in', from: 'state3', to: 'state4' }
21
+ ],
22
+ states: [{ id: 'state3' }, { id: 'state4' }]
23
+ }
24
+ ];
25
+
26
+ it('should generate correct default interactionsStatesMap', () => {
27
+ let contextValue;
28
+
29
+ render(
30
+ <InteractionsProvider interactions={interactions}>
31
+ <InteractionsContext.Consumer>
32
+ {value => {
33
+ contextValue = value;
34
+ return null;
35
+ }}
36
+ </InteractionsContext.Consumer>
37
+ </InteractionsProvider>
38
+ );
39
+
40
+ expect(contextValue!).toBeDefined();
41
+ expect(contextValue!.interactionsStatesMap).toEqual({
42
+ interaction1: 'state1',
43
+ interaction2: 'state3'
44
+ });
45
+ });
46
+
47
+ it('should correctly update interactionsStatesMap when transitionTo is called', async () => {
48
+ let contextValue;
49
+
50
+ render(
51
+ <InteractionsProvider interactions={interactions}>
52
+ <InteractionsContext.Consumer>
53
+ {value => {
54
+ contextValue = value;
55
+ return null;
56
+ }}
57
+ </InteractionsContext.Consumer>
58
+ </InteractionsProvider>
59
+ );
60
+
61
+ expect(contextValue!).toBeDefined();
62
+ contextValue!.transitionTo('interaction1', 'state2');
63
+
64
+ await waitFor(() => {
65
+ expect(contextValue!.interactionsStatesMap['interaction1']).toBe('state2');
66
+ });
67
+ });
68
+
69
+ it('should return the correct trigger using getItemTrigger', () => {
70
+ let contextValue;
71
+
72
+ render(
73
+ <InteractionsProvider interactions={interactions}>
74
+ <InteractionsContext.Consumer>
75
+ {value => {
76
+ contextValue = value;
77
+ return null;
78
+ }}
79
+ </InteractionsContext.Consumer>
80
+ </InteractionsProvider>
81
+ );
82
+
83
+ expect(contextValue!).toBeDefined();
84
+
85
+ // Check the correct trigger is returned
86
+ const trigger = contextValue!.getItemTrigger('item1', 'click');
87
+ expect(trigger).toEqual({
88
+ id: 'interaction1',
89
+ from: 'state1',
90
+ to: 'state2',
91
+ });
92
+
93
+ // Check that no trigger is returned when conditions don't match
94
+ const noTrigger = contextValue!.getItemTrigger('item1', 'hover-on');
95
+ expect(noTrigger).toBeNull();
96
+ });
97
+ });
@@ -0,0 +1,65 @@
1
+ import { createContext, FC, PropsWithChildren, useState } from 'react';
2
+ import { Interaction, InteractionTrigger } from '@cntrl-site/sdk';
3
+
4
+ const defaultState = {
5
+ interactionsStatesMap: {},
6
+ interactions: [],
7
+ transitionTo: () => {},
8
+ getItemTrigger: () => null
9
+ };
10
+
11
+ export const InteractionsContext = createContext<{
12
+ interactionsStatesMap: StatesMap,
13
+ interactions: Interaction[],
14
+ transitionTo: (interactionId: string, stateId: string) => void,
15
+ getItemTrigger: (itemId: string, triggerType: TriggerType) => Trigger | null
16
+ }>(defaultState);
17
+
18
+ interface Props {
19
+ interactions: Interaction[];
20
+ }
21
+
22
+ export const InteractionsProvider: FC<PropsWithChildren<Props>> = ({ interactions, children }) => {
23
+ const defaultStatesMap = interactions.reduce<Record<string, string>>((map, { id, startStateId }) => {
24
+ map[id] = startStateId;
25
+ return map;
26
+ }, {});
27
+ const [interactionsStatesMap, setInteractionsStatesMap] = useState(defaultStatesMap);
28
+ const transitionTo = (interactionId: string, stateId: string) => {
29
+ setInteractionsStatesMap((map) => ({ ...map, [interactionId]: stateId }));
30
+ };
31
+ const getItemTrigger = (itemId: string, triggerType: TriggerType): Trigger | null => {
32
+ for (const interaction of interactions) {
33
+ const activeStateId = interactionsStatesMap[interaction.id];
34
+ const matchingTrigger = interaction.triggers.find((trigger) =>
35
+ trigger.itemId === itemId &&
36
+ trigger.from === activeStateId &&
37
+ trigger.type === triggerType
38
+ );
39
+ if (matchingTrigger) {
40
+ return {
41
+ id: interaction.id,
42
+ from: matchingTrigger.from,
43
+ to: matchingTrigger.to,
44
+ };
45
+ }
46
+ }
47
+ return null;
48
+ };
49
+ return (
50
+ <InteractionsContext.Provider value={{
51
+ transitionTo,
52
+ interactionsStatesMap,
53
+ interactions,
54
+ getItemTrigger
55
+ }}>
56
+ {children}
57
+ </InteractionsContext.Provider>
58
+ );
59
+ };
60
+
61
+ type StatesMap = Record<InteractionId, StateId>;
62
+ type Trigger = { id: InteractionId, from: StateId, to: StateId };
63
+ type TriggerType = InteractionTrigger['type'];
64
+ type InteractionId = string;
65
+ type StateId = string;
@@ -1,12 +1,12 @@
1
- import { HoverParams, ArticleItemType, AnchorSide, ItemHoverState } from '@cntrl-site/sdk';
1
+ import { StateParams, ArticleItemType, AnchorSide, ItemState, ItemStatesMap } from '@cntrl-site/sdk';
2
2
  import { CntrlColor } from '@cntrl-site/color';
3
3
  import { getItemTopStyle } from '../getItemTopStyle';
4
4
 
5
- type UnionToIntersection<U> = (U extends any ? (arg: U) => void : never) extends (arg: infer I) => void ? I : never;
6
- type HoverParamsGetter = (value: any, anchorSide?: AnchorSide) => string;
7
- type ItemHoverParams = Omit<UnionToIntersection<ItemHoverState>, 'autoplay'>;
5
+ type StateParamsGetter = (value: any, anchorSide?: AnchorSide) => string;
6
+ type AllKeys<T> = T extends any ? keyof T : never;
7
+ type StatePropertyKey = AllKeys<ItemStatesMap[keyof ItemStatesMap]>;
8
8
 
9
- const hoverTransformationMap: Record<keyof ItemHoverParams, HoverParamsGetter> = {
9
+ const stateTransformationMap: Record<StatePropertyKey, StateParamsGetter> = {
10
10
  'width': (width: number) => `width: ${width * 100}vw !important;`,
11
11
  'height': (height: number) => `height: ${height * 100}vw !important;`,
12
12
  'top': (top: number, anchorSide?: AnchorSide) => `top: ${getItemTopStyle(top, anchorSide)} !important;`,
@@ -25,7 +25,7 @@ const hoverTransformationMap: Record<keyof ItemHoverParams, HoverParamsGetter> =
25
25
  'wordSpacing': (wordSpacing: number) => `word-spacing: ${wordSpacing * 100}vw !important;`
26
26
  };
27
27
 
28
- const CSSPropertyNameMap: Record<keyof ItemHoverParams, string> = {
28
+ const CSSPropertyNameMap: Record<keyof ItemState<ArticleItemType>, string> = {
29
29
  'width': 'width',
30
30
  'height': 'height',
31
31
  'top': 'top',
@@ -44,42 +44,56 @@ const CSSPropertyNameMap: Record<keyof ItemHoverParams, string> = {
44
44
  'color': 'color'
45
45
  };
46
46
 
47
- export function getTransitions<T extends ArticleItemType>(
48
- values: Array<keyof ItemHoverParams>,
49
- hover?: ItemHoverParams
47
+ export function getStateStyles<T extends ArticleItemType>(
48
+ values: Array<StatePropertyKey>,
49
+ state?: ItemStatesMap[T],
50
+ anchorSide?: AnchorSide
50
51
  ): string {
51
- if (!hover) return 'unset';
52
- const transitionValues = values.reduce<string[]>((acc, valueName) => {
53
- if (valueName in hover && hover[valueName] !== undefined) {
54
- const hoverProperties = hover[valueName] as HoverParams<string | number>;
52
+ if (!state) return '';
53
+
54
+ const stateValues = values.reduce<string[]>((acc, valueName) => {
55
+ // @ts-ignore
56
+ if (valueName in state && state[valueName] !== undefined) {
57
+ // @ts-ignore
58
+ const stateProperties = state[valueName] as StateParams<string | number>;
59
+ const getter = stateTransformationMap[valueName] as StateParamsGetter;
55
60
  return [
56
61
  ...acc,
57
- `${CSSPropertyNameMap[valueName]} ${hoverProperties!.duration}ms ${hoverProperties!.timing} ${hoverProperties!.delay}ms`
62
+ getter(stateProperties.value, anchorSide)
58
63
  ];
59
64
  }
60
65
  return acc;
61
66
  }, []);
62
- if (!transitionValues.length) return 'unset';
63
- return transitionValues.join(', ');
67
+
68
+ if (!stateValues.length) return '';
69
+ // @ts-ignore
70
+ const transitionStr = getTransitions(values, state);
71
+ stateValues.push(`transition: ${transitionStr};`);
72
+ return stateValues.join('\n');
64
73
  }
65
74
 
66
- export function getHoverStyles<T extends ArticleItemType>(
67
- values: Array<keyof ItemHoverParams>,
68
- hover?: ItemHoverParams,
69
- anchorSide?: AnchorSide
75
+ export function getTransitions<T extends ArticleItemType>(
76
+ values: Array<StatePropertyKey>,
77
+ state?: ItemStatesMap[T]
70
78
  ): string {
71
- if (!hover) return '';
72
- const hoverValues = values.reduce<string[]>((acc, valueName) => {
73
- if (valueName in hover && hover[valueName] !== undefined) {
74
- const hoverProperties = hover[valueName] as HoverParams<string | number>;
79
+ if (!state) return 'unset';
80
+ const transitionValues = values.reduce<string[]>((acc, valueName) => {
81
+ // @ts-ignore TODO
82
+ if (valueName in state && state[valueName] !== undefined) {
83
+ // @ts-ignore TODO
84
+ const stateProperties = state[valueName] as StateParams<string | number>;
75
85
  return [
76
86
  ...acc,
77
- hoverTransformationMap[valueName](hoverProperties.value, anchorSide)
87
+ // @ts-ignore
88
+ `${CSSPropertyNameMap[valueName]} ${stateProperties!.duration}ms ${stateProperties!.timing} ${stateProperties!.delay}ms`
78
89
  ];
79
90
  }
80
91
  return acc;
81
92
  }, []);
82
- if (!hoverValues.length) return '';
83
- return hoverValues.join('\n');
93
+ if (!transitionValues.length) return 'unset';
94
+ return transitionValues.join(', ');
84
95
  }
85
96
 
97
+ // export function getStateTransitions<T extends ArticleItemType>(
98
+ //
99
+ // )