@cntrl-site/sdk-nextjs 1.0.17 → 1.0.19-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.idea/codeStyles/codeStyleConfig.xml +5 -0
  2. package/.idea/inspectionProfiles/Project_Default.xml +15 -0
  3. package/README.md +4 -1
  4. package/jest.config.js +2 -2
  5. package/lib/components/Article.js +9 -8
  6. package/lib/components/Item.js +32 -14
  7. package/lib/components/items/CodeEmbedItem.js +9 -8
  8. package/lib/components/items/CustomItem.js +8 -7
  9. package/lib/components/items/GroupItem.js +9 -8
  10. package/lib/components/items/ImageItem.js +13 -12
  11. package/lib/components/items/RectangleItem.js +9 -8
  12. package/lib/components/items/RichTextItem.js +8 -7
  13. package/lib/components/items/VideoItem.js +13 -12
  14. package/lib/components/items/VimeoEmbed.js +16 -15
  15. package/lib/components/items/YoutubeEmbed.js +14 -13
  16. package/lib/components/useStatesClassNames.js +18 -0
  17. package/lib/provider/InteractionsContext.js +45 -0
  18. package/lib/utils/{HoverStyles/HoverStyles.js → StateStyles/StateStyles.js} +18 -16
  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 +36 -19
  24. package/src/components/items/CodeEmbedItem.tsx +10 -9
  25. package/src/components/items/CustomItem.tsx +10 -9
  26. package/src/components/items/GroupItem.tsx +10 -9
  27. package/src/components/items/ImageItem.tsx +16 -15
  28. package/src/components/items/RectangleItem.tsx +11 -10
  29. package/src/components/items/RichTextItem.tsx +15 -9
  30. package/src/components/items/VideoItem.tsx +16 -15
  31. package/src/components/items/VimeoEmbed.tsx +17 -16
  32. package/src/components/items/YoutubeEmbed.tsx +15 -15
  33. package/src/components/useStatesClassNames.ts +23 -0
  34. package/src/provider/InteractionsContext.test.tsx +97 -0
  35. package/src/provider/InteractionsContext.tsx +65 -0
  36. package/src/utils/{HoverStyles/HoverStyles.ts → StateStyles/StateStyles.ts} +22 -21
  37. package/src/utils/getStatesCSS.ts +21 -0
  38. package/cntrl-site-sdk-nextjs-1.0.17.tgz +0 -0
@@ -13,8 +13,9 @@ const useEmbedVideoItem_1 = require("./useEmbedVideoItem");
13
13
  const useItemAngle_1 = require("../useItemAngle");
14
14
  const sdk_1 = require("@cntrl-site/sdk");
15
15
  const useCntrlContext_1 = require("../../provider/useCntrlContext");
16
- const HoverStyles_1 = require("../../utils/HoverStyles/HoverStyles");
17
16
  const useRegisterResize_1 = require("../../common/useRegisterResize");
17
+ const useStatesClassNames_1 = require("../useStatesClassNames");
18
+ const getStatesCSS_1 = require("../../utils/getStatesCSS");
18
19
  const VimeoEmbedItem = ({ item, sectionId, onResize }) => {
19
20
  var _a, _b, _c;
20
21
  const id = (0, react_1.useId)();
@@ -27,7 +28,9 @@ const VimeoEmbedItem = ({ item, sectionId, onResize }) => {
27
28
  const [ref, setRef] = (0, react_1.useState)(null);
28
29
  const [imgRef, setImgRef] = (0, react_1.useState)(null);
29
30
  const [isCoverVisible, setIsCoverVisible] = (0, react_1.useState)(false);
30
- const layoutValues = [item.area, item.layoutParams, item.state.hover];
31
+ const layoutValues = [item.area, item.layoutParams, item.state];
32
+ const wrapperClassNames = (0, useStatesClassNames_1.useStatesClassNames)(item.id, item.state, 'embed-video-wrapper');
33
+ const videoClassNames = (0, useStatesClassNames_1.useStatesClassNames)(item.id, item.state, 'embed-video');
31
34
  (0, useRegisterResize_1.useRegisterResize)(ref, onResize);
32
35
  const getValidVimeoUrl = (url) => {
33
36
  const validURL = new URL(url);
@@ -61,7 +64,7 @@ const VimeoEmbedItem = ({ item, sectionId, onResize }) => {
61
64
  vimeoPlayer.play();
62
65
  setIsCoverVisible(false);
63
66
  };
64
- return ((0, jsx_runtime_1.jsxs)(LinkWrapper_1.LinkWrapper, { url: (_a = item.link) === null || _a === void 0 ? void 0 : _a.url, target: (_b = item.link) === null || _b === void 0 ? void 0 : _b.target, children: [(0, jsx_runtime_1.jsxs)("div", { className: `embed-video-wrapper-${item.id}`, ref: setRef, style: Object.assign(Object.assign(Object.assign({}, (opacity !== undefined ? { opacity } : {})), (angle !== undefined ? { transform: `rotate(${angle}deg)` } : {})), (blur !== undefined ? { filter: `blur(${blur * 100}vw)` } : {})), onMouseEnter: () => {
67
+ return ((0, jsx_runtime_1.jsxs)(LinkWrapper_1.LinkWrapper, { url: (_a = item.link) === null || _a === void 0 ? void 0 : _a.url, target: (_b = item.link) === null || _b === void 0 ? void 0 : _b.target, children: [(0, jsx_runtime_1.jsxs)("div", { className: `embed-video-wrapper-${item.id} ${wrapperClassNames}`, ref: setRef, style: Object.assign(Object.assign(Object.assign({}, (opacity !== undefined ? { opacity } : {})), (angle !== undefined ? { transform: `rotate(${angle}deg)` } : {})), (blur !== undefined ? { filter: `blur(${blur * 100}vw)` } : {})), onMouseEnter: () => {
65
68
  if (!vimeoPlayer || play !== 'on-hover')
66
69
  return;
67
70
  vimeoPlayer.play();
@@ -78,37 +81,35 @@ const VimeoEmbedItem = ({ item, sectionId, onResize }) => {
78
81
  width: '100%',
79
82
  top: '0',
80
83
  left: '0'
81
- }, alt: "Cover img" })), (0, jsx_runtime_1.jsx)("iframe", { ref: setIframeRef, className: "embedVideo", src: validUrl || '', allow: "autoplay; fullscreen; picture-in-picture;", allowFullScreen: true, style: Object.assign({}, (radius !== undefined ? { borderRadius: `${radius * 100}vw` } : {})) })] }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
84
+ }, alt: "Cover img" })), (0, jsx_runtime_1.jsx)("iframe", { ref: setIframeRef, className: `embed-video ${videoClassNames}`, src: validUrl || '', allow: "autoplay; fullscreen; picture-in-picture;", allowFullScreen: true, style: Object.assign({}, (radius !== undefined ? { borderRadius: `${radius * 100}vw` } : {})) })] }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
82
85
  .embed-video-wrapper-${item.id} {
83
86
  position: absolute;
84
87
  width: 100%;
85
88
  height: 100%;
86
89
  }
87
- .embedVideo {
90
+ .embed-video {
88
91
  width: 100%;
89
92
  height: 100%;
90
93
  z-index: 1;
91
94
  border: none;
92
95
  overflow: hidden;
93
96
  }
94
- ${(0, sdk_1.getLayoutStyles)(layouts, layoutValues, ([area, layoutParams, hoverParams]) => {
97
+ ${(0, sdk_1.getLayoutStyles)(layouts, layoutValues, ([area, layoutParams, stateParams]) => {
98
+ const wrapperStatesCSS = (0, getStatesCSS_1.getStatesCSS)(item.id, 'embed-video-wrapper', ['angle', 'blur', 'opacity'], stateParams);
99
+ const videoStatesCSS = (0, getStatesCSS_1.getStatesCSS)(item.id, 'embed-video', ['radius'], stateParams);
95
100
  return (`
96
101
  .embed-video-wrapper-${item.id} {
97
102
  opacity: ${layoutParams.opacity};
98
103
  transform: rotate(${area.angle}deg);
99
104
  filter: ${layoutParams.blur !== 0 ? `blur(${layoutParams.blur * 100}vw)` : 'unset'};
100
- transition: ${(0, HoverStyles_1.getTransitions)(['angle', 'blur', 'opacity'], hoverParams)};
105
+ transition: all 0.2s ease;
101
106
  }
102
- .embed-video-wrapper-${item.id}:hover {
103
- ${(0, HoverStyles_1.getHoverStyles)(['angle', 'blur', 'opacity'], hoverParams)}
104
- }
105
- .embed-video-wrapper-${item.id} .embedVideo {
107
+ .embed-video-wrapper-${item.id} .embed-video {
106
108
  border-radius: ${layoutParams.radius * 100}vw;
107
- transition: ${(0, HoverStyles_1.getTransitions)(['radius'], hoverParams)};
108
- }
109
- .embed-video-wrapper-${item.id}:hover .embedVideo {
110
- ${(0, HoverStyles_1.getHoverStyles)(['radius'], hoverParams)};
109
+ transition: all 0.2s ease;
111
110
  }
111
+ ${wrapperStatesCSS}
112
+ ${videoStatesCSS}
112
113
  `);
113
114
  })}
114
115
  ` })] }));
@@ -12,10 +12,11 @@ const getValidYoutubeUrl_1 = require("../../utils/getValidYoutubeUrl");
12
12
  const useEmbedVideoItem_1 = require("./useEmbedVideoItem");
13
13
  const useItemAngle_1 = require("../useItemAngle");
14
14
  const sdk_1 = require("@cntrl-site/sdk");
15
- const HoverStyles_1 = require("../../utils/HoverStyles/HoverStyles");
16
15
  const useCntrlContext_1 = require("../../provider/useCntrlContext");
17
16
  const useYouTubeIframeApi_1 = require("../../utils/Youtube/useYouTubeIframeApi");
18
17
  const useRegisterResize_1 = require("../../common/useRegisterResize");
18
+ const useStatesClassNames_1 = require("../useStatesClassNames");
19
+ const getStatesCSS_1 = require("../../utils/getStatesCSS");
19
20
  const YoutubeEmbedItem = ({ item, sectionId, onResize }) => {
20
21
  var _a, _b, _c;
21
22
  const id = (0, react_1.useId)();
@@ -28,7 +29,9 @@ const YoutubeEmbedItem = ({ item, sectionId, onResize }) => {
28
29
  const [player, setPlayer] = (0, react_1.useState)(undefined);
29
30
  const [isCoverVisible, setIsCoverVisible] = (0, react_1.useState)(false);
30
31
  const [imgRef, setImgRef] = (0, react_1.useState)(null);
31
- const layoutValues = [item.area, item.layoutParams, item.state.hover];
32
+ const layoutValues = [item.area, item.layoutParams, item.state];
33
+ const wrapperClassNames = (0, useStatesClassNames_1.useStatesClassNames)(item.id, item.state, 'embed-youtube-video-wrapper');
34
+ const embedClassNames = (0, useStatesClassNames_1.useStatesClassNames)(item.id, item.state, 'embed');
32
35
  (0, useRegisterResize_1.useRegisterResize)(div, onResize);
33
36
  (0, react_1.useEffect)(() => {
34
37
  const newUrl = new URL(url);
@@ -80,7 +83,7 @@ const YoutubeEmbedItem = ({ item, sectionId, onResize }) => {
80
83
  setIsCoverVisible(true);
81
84
  }
82
85
  }, []);
83
- return ((0, jsx_runtime_1.jsxs)(LinkWrapper_1.LinkWrapper, { url: (_a = item.link) === null || _a === void 0 ? void 0 : _a.url, target: (_b = item.link) === null || _b === void 0 ? void 0 : _b.target, children: [(0, jsx_runtime_1.jsxs)("div", { className: `embed-youtube-video-wrapper-${item.id}`, onMouseEnter: () => {
86
+ return ((0, jsx_runtime_1.jsxs)(LinkWrapper_1.LinkWrapper, { url: (_a = item.link) === null || _a === void 0 ? void 0 : _a.url, target: (_b = item.link) === null || _b === void 0 ? void 0 : _b.target, children: [(0, jsx_runtime_1.jsxs)("div", { className: `embed-youtube-video-wrapper-${item.id} ${wrapperClassNames}`, onMouseEnter: () => {
84
87
  if (!player || play !== 'on-hover')
85
88
  return;
86
89
  player.playVideo();
@@ -98,7 +101,7 @@ const YoutubeEmbedItem = ({ item, sectionId, onResize }) => {
98
101
  top: '0',
99
102
  left: '0',
100
103
  zIndex: 1
101
- }, alt: "Cover img" })), (0, jsx_runtime_1.jsx)("div", { className: `embed-${item.id}`, ref: setDiv, style: Object.assign({}, (radius !== undefined ? { borderRadius: `${radius * 100}vw` } : {})) })] }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
104
+ }, alt: "Cover img" })), (0, jsx_runtime_1.jsx)("div", { className: `embed-${item.id} ${embedClassNames}`, ref: setDiv, style: Object.assign({}, (radius !== undefined ? { borderRadius: `${radius * 100}vw` } : {})) })] }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
102
105
  .embed-youtube-video-wrapper-${item.id},
103
106
  .embed-${item.id} {
104
107
  position: absolute;
@@ -114,24 +117,22 @@ const YoutubeEmbedItem = ({ item, sectionId, onResize }) => {
114
117
  z-index: 1;
115
118
  border: none;
116
119
  }
117
- ${(0, sdk_1.getLayoutStyles)(layouts, layoutValues, ([area, layoutParams, hoverParams]) => {
120
+ ${(0, sdk_1.getLayoutStyles)(layouts, layoutValues, ([area, layoutParams, stateParams]) => {
121
+ const wrapperStatesCSS = (0, getStatesCSS_1.getStatesCSS)(item.id, 'embed-youtube-video-wrapper', ['angle', 'blur', 'opacity'], stateParams);
122
+ const embedStatesCSS = (0, getStatesCSS_1.getStatesCSS)(item.id, 'embed', ['radius'], stateParams);
118
123
  return (`
119
124
  .embed-youtube-video-wrapper-${item.id} {
120
125
  opacity: ${layoutParams.opacity};
121
126
  transform: rotate(${area.angle}deg);
122
127
  filter: ${layoutParams.blur !== 0 ? `blur(${layoutParams.blur * 100}vw)` : 'unset'};
123
- transition: ${(0, HoverStyles_1.getTransitions)(['angle', 'blur', 'opacity'], hoverParams)};
128
+ transition: all 0.2s ease;
124
129
  }
125
130
  .embed-youtube-video-wrapper-${item.id} .embed-${item.id} {
126
131
  border-radius: ${layoutParams.radius * 100}vw;
127
- transition: ${(0, HoverStyles_1.getTransitions)(['radius'], hoverParams)};
128
- }
129
- .embed-youtube-video-wrapper-${item.id}:hover {
130
- ${(0, HoverStyles_1.getHoverStyles)(['angle', 'blur', 'opacity'], hoverParams)};
131
- }
132
- .embed-youtube-video-wrapper-${item.id}:hover .embed-${item.id} {
133
- ${(0, HoverStyles_1.getHoverStyles)(['radius'], hoverParams)};
132
+ transition: all 0.2s ease;
134
133
  }
134
+ ${wrapperStatesCSS}
135
+ ${embedStatesCSS}
135
136
  `);
136
137
  })}
137
138
  ` })] }));
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useStatesClassNames = void 0;
4
+ const react_1 = require("react");
5
+ const InteractionsContext_1 = require("../provider/InteractionsContext");
6
+ const useCurrentLayout_1 = require("../common/useCurrentLayout");
7
+ function useStatesClassNames(itemId, itemStates, uniquePrefix) {
8
+ const { interactionsStatesMap } = (0, react_1.useContext)(InteractionsContext_1.InteractionsContext);
9
+ const { layoutId } = (0, useCurrentLayout_1.useCurrentLayout)();
10
+ const activeStates = Object.values(interactionsStatesMap);
11
+ const statesForLayout = layoutId ? itemStates[layoutId] : {};
12
+ const stateClassNames = Object.keys(statesForLayout !== null && statesForLayout !== void 0 ? statesForLayout : {})
13
+ .filter((stateId) => activeStates.includes(stateId))
14
+ .map((stateId) => `${uniquePrefix}-${itemId}-state-${stateId}`)
15
+ .join(' ');
16
+ return stateClassNames;
17
+ }
18
+ exports.useStatesClassNames = useStatesClassNames;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InteractionsProvider = exports.InteractionsContext = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const defaultState = {
7
+ interactionsStatesMap: {},
8
+ interactions: [],
9
+ transitionTo: () => { },
10
+ getItemTrigger: () => null
11
+ };
12
+ exports.InteractionsContext = (0, react_1.createContext)(defaultState);
13
+ const InteractionsProvider = ({ interactions, children }) => {
14
+ const defaultStatesMap = interactions.reduce((map, { id, startStateId }) => {
15
+ map[id] = startStateId;
16
+ return map;
17
+ }, {});
18
+ const [interactionsStatesMap, setInteractionsStatesMap] = (0, react_1.useState)(defaultStatesMap);
19
+ const transitionTo = (interactionId, stateId) => {
20
+ setInteractionsStatesMap((map) => (Object.assign(Object.assign({}, map), { [interactionId]: stateId })));
21
+ };
22
+ const getItemTrigger = (itemId, triggerType) => {
23
+ for (const interaction of interactions) {
24
+ const activeStateId = interactionsStatesMap[interaction.id];
25
+ const matchingTrigger = interaction.triggers.find((trigger) => trigger.itemId === itemId &&
26
+ trigger.from === activeStateId &&
27
+ trigger.type === triggerType);
28
+ if (matchingTrigger) {
29
+ return {
30
+ id: interaction.id,
31
+ from: matchingTrigger.from,
32
+ to: matchingTrigger.to,
33
+ };
34
+ }
35
+ }
36
+ return null;
37
+ };
38
+ return ((0, jsx_runtime_1.jsx)(exports.InteractionsContext.Provider, { value: {
39
+ transitionTo,
40
+ interactionsStatesMap,
41
+ interactions,
42
+ getItemTrigger
43
+ }, children: children }));
44
+ };
45
+ exports.InteractionsProvider = InteractionsProvider;
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getHoverStyles = exports.getTransitions = void 0;
3
+ exports.getStateStyles = exports.getTransitions = void 0;
4
4
  const color_1 = require("@cntrl-site/color");
5
5
  const getItemTopStyle_1 = require("../getItemTopStyle");
6
- const hoverTransformationMap = {
6
+ const stateTransformationMap = {
7
7
  'width': (width) => `width: ${width * 100}vw !important;`,
8
8
  'height': (height) => `height: ${height * 100}vw !important;`,
9
9
  'top': (top, anchorSide) => `top: ${(0, getItemTopStyle_1.getItemTopStyle)(top, anchorSide)} !important;`,
@@ -39,15 +39,16 @@ const CSSPropertyNameMap = {
39
39
  'wordSpacing': 'word-spacing',
40
40
  'color': 'color'
41
41
  };
42
- function getTransitions(values, hover) {
43
- if (!hover)
42
+ function getTransitions(values, state) {
43
+ if (!state)
44
44
  return 'unset';
45
45
  const transitionValues = values.reduce((acc, valueName) => {
46
- if (valueName in hover && hover[valueName] !== undefined) {
47
- const hoverProperties = hover[valueName];
46
+ if (valueName in state && state[valueName] !== undefined) {
47
+ // @ts-ignore TODO
48
+ const stateProperties = state[valueName];
48
49
  return [
49
50
  ...acc,
50
- `${CSSPropertyNameMap[valueName]} ${hoverProperties.duration}ms ${hoverProperties.timing} ${hoverProperties.delay}ms`
51
+ `${CSSPropertyNameMap[valueName]} ${stateProperties.duration}ms ${stateProperties.timing} ${stateProperties.delay}ms`
51
52
  ];
52
53
  }
53
54
  return acc;
@@ -57,21 +58,22 @@ function getTransitions(values, hover) {
57
58
  return transitionValues.join(', ');
58
59
  }
59
60
  exports.getTransitions = getTransitions;
60
- function getHoverStyles(values, hover, anchorSide) {
61
- if (!hover)
61
+ function getStateStyles(values, state, anchorSide) {
62
+ if (!state)
62
63
  return '';
63
- const hoverValues = values.reduce((acc, valueName) => {
64
- if (valueName in hover && hover[valueName] !== undefined) {
65
- const hoverProperties = hover[valueName];
64
+ const stateValues = values.reduce((acc, valueName) => {
65
+ if (valueName in state && state[valueName] !== undefined) {
66
+ // @ts-ignore TODO
67
+ const stateProperties = state[valueName];
66
68
  return [
67
69
  ...acc,
68
- hoverTransformationMap[valueName](hoverProperties.value, anchorSide)
70
+ stateTransformationMap[valueName](stateProperties.value, anchorSide)
69
71
  ];
70
72
  }
71
73
  return acc;
72
74
  }, []);
73
- if (!hoverValues.length)
75
+ if (!stateValues.length)
74
76
  return '';
75
- return hoverValues.join('\n');
77
+ return stateValues.join('\n');
76
78
  }
77
- exports.getHoverStyles = getHoverStyles;
79
+ exports.getStateStyles = getStateStyles;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getStatesCSS = void 0;
4
+ const StateStyles_1 = require("./StateStyles/StateStyles");
5
+ function getStatesCSS(itemId, classNamePrefix, keys, states, anchorSide) {
6
+ return states
7
+ ? Object.entries(states).map(([stateId, params]) => {
8
+ return `
9
+ .${classNamePrefix}-${itemId}-state-${stateId} {
10
+ ${(0, StateStyles_1.getStateStyles)(keys, params, anchorSide)};
11
+ }
12
+ `;
13
+ }).join('\n')
14
+ : '';
15
+ }
16
+ exports.getStatesCSS = getStatesCSS;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cntrl-site/sdk-nextjs",
3
- "version": "1.0.17",
3
+ "version": "1.0.19-alpha.0",
4
4
  "description": "SDK for Next.js",
5
5
  "main": "lib/index.js",
6
6
  "types": "src/index.ts",
@@ -23,7 +23,7 @@
23
23
  "dependencies": {
24
24
  "@cntrl-site/color": "^1.0.0",
25
25
  "@cntrl-site/effects": "^1.2.0",
26
- "@cntrl-site/sdk": "^1.12.3",
26
+ "@cntrl-site/sdk": "^1.12.4-alpha.7",
27
27
  "@types/vimeo__player": "^2.18.0",
28
28
  "@vimeo/player": "^2.20.1",
29
29
  "html-react-parser": "^3.0.1",
@@ -38,12 +38,15 @@
38
38
  "react-dom": "^18.3.1"
39
39
  },
40
40
  "devDependencies": {
41
+ "@testing-library/jest-dom": "^6.4.8",
42
+ "@testing-library/react": "^16.0.0",
41
43
  "@tsconfig/node16": "^1.0.3",
42
44
  "@tsconfig/recommended": "^1.0.1",
43
- "@types/jest": "^29.2.5",
45
+ "@types/jest": "^29.5.12",
44
46
  "@types/node": "^18.11.7",
45
47
  "@types/react": "^18.0.15",
46
- "jest": "^29.3.1",
48
+ "jest": "^29.7.0",
49
+ "jest-environment-jsdom": "^29.7.0",
47
50
  "ts-jest": "^29.0.3",
48
51
  "typescript": "^5.2.2"
49
52
  },
@@ -1,11 +1,12 @@
1
1
  import { FC, useEffect, useId, useRef, useState } from 'react';
2
2
  import JSXStyle from 'styled-jsx/style';
3
- import { Article as TArticle } from '@cntrl-site/sdk';
3
+ import { Article as TArticle, ArticleItemType, ItemState, ItemStateParams } from '@cntrl-site/sdk';
4
4
  import { Section } from './Section';
5
5
  import { Item } from './Item';
6
6
  import { useArticleRectObserver } from '../utils/ArticleRectManager/useArticleRectObserver';
7
7
  import { ArticleRectContext } from '../provider/ArticleRectContext';
8
8
  import { ArticleWrapper } from './ArticleWrapper';
9
+ import { InteractionsProvider } from '../provider/InteractionsContext';
9
10
 
10
11
  interface Props {
11
12
  article: TArticle;
@@ -27,35 +28,37 @@ export const Article: FC<Props> = ({ article, sectionData }) => {
27
28
 
28
29
  return (
29
30
  <ArticleRectContext.Provider value={articleRectObserver}>
30
- <ArticleWrapper>
31
- <div className="article" ref={articleRef}>
32
- {article.sections.map((section, i) => {
33
- const data = section.name ? sectionData[section.name] : {};
34
- return (
35
- <Section
36
- section={section}
37
- key={section.id}
38
- data={data}
39
- >
40
- {article.sections[i].items.map(item => (
41
- <Item
42
- item={item}
43
- key={item.id}
44
- sectionId={section.id}
45
- articleHeight={articleHeight}
46
- />
47
- ))}
48
- </Section>
49
- );
50
- })}
51
- </div>
52
- </ArticleWrapper>
53
- <JSXStyle id={id}>{`
31
+ <InteractionsProvider interactions={article.interactions}>
32
+ <ArticleWrapper>
33
+ <div className="article" ref={articleRef}>
34
+ {article.sections.map((section, i) => {
35
+ const data = section.name ? sectionData[section.name] : {};
36
+ return (
37
+ <Section
38
+ section={section}
39
+ key={section.id}
40
+ data={data}
41
+ >
42
+ {article.sections[i].items.map(item => (
43
+ <Item
44
+ item={item}
45
+ key={item.id}
46
+ sectionId={section.id}
47
+ articleHeight={articleHeight}
48
+ />
49
+ ))}
50
+ </Section>
51
+ );
52
+ })}
53
+ </div>
54
+ </ArticleWrapper>
55
+ <JSXStyle id={id}>{`
54
56
  .article {
55
- position: relative;
56
- overflow: clip;
57
- }
57
+ position: relative;
58
+ overflow: clip;
59
+ }
58
60
  `}</JSXStyle>
61
+ </InteractionsProvider>
59
62
  </ArticleRectContext.Provider>
60
63
  );
61
64
  };
@@ -1,5 +1,4 @@
1
- import React, { CSSProperties, FC, PropsWithChildren, useEffect, useId, useState } from 'react';
2
- import JSXStyle from 'styled-jsx/style';
1
+ import React, { CSSProperties, FC, PropsWithChildren } from 'react';
3
2
  import { useCurrentLayout } from '../common/useCurrentLayout';
4
3
  import { LayoutContext } from '../provider/LayoutContext';
5
4
 
@@ -21,7 +21,6 @@ import { useItemDimensions } from './useItemDimensions';
21
21
  import { useItemScale } from './useItemScale';
22
22
  import { ScaleAnchorMap } from '../utils/ScaleAnchorMap';
23
23
  import { useSectionHeightData } from './useSectionHeightMap';
24
- import { getHoverStyles, getTransitions } from '../utils/HoverStyles/HoverStyles';
25
24
  import { getItemTopStyle } from '../utils/getItemTopStyle';
26
25
  import { useStickyItemTop } from './items/useStickyItemTop';
27
26
  import { getAnchoredItemTop } from '../utils/getAnchoredItemTop';
@@ -32,6 +31,9 @@ import { GroupItem } from './items/GroupItem';
32
31
  import { CodeEmbedItem } from './items/CodeEmbedItem';
33
32
  import { AreaAnchor } from '@cntrl-site/sdk/src/types/article/ItemArea';
34
33
  import { KeyframesContext } from '../provider/KeyframesContext';
34
+ import { InteractionsContext } from '../provider/InteractionsContext';
35
+ import { getStatesCSS } from '../utils/getStatesCSS';
36
+ import { useStatesClassNames } from './useStatesClassNames';
35
37
 
36
38
  export interface ItemProps<I extends ItemAny> {
37
39
  item: I;
@@ -93,14 +95,14 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight, isI
93
95
  const sectionHeight = useSectionHeightData(sectionId);
94
96
  const stickyTop = useStickyItemTop(item, sectionHeight, sectionId);
95
97
  const dimensions = useItemDimensions(item, sectionId);
96
- const layoutValues: Record<string, any>[] = [item.area, item.hidden, item.state.hover];
98
+ const layoutValues: Record<string, any>[] = [item.area, item.hidden, item.state];
97
99
  const isInitialRef = useRef(true);
100
+ const { transitionTo, getItemTrigger } = useContext(InteractionsContext);
98
101
  layoutValues.push(item.sticky);
99
102
  layoutValues.push(sectionHeight);
100
103
  if (item.layoutParams) {
101
104
  layoutValues.push(item.layoutParams);
102
105
  }
103
-
104
106
  const sizing = layout && isItemType(item, ArticleItemType.RichText)
105
107
  ? item.layoutParams[layout].sizing
106
108
  : undefined;
@@ -130,9 +132,11 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight, isI
130
132
  }, []);
131
133
 
132
134
  const isRichText = isItemType(item, ArticleItemType.RichText);
135
+ const innerStatesClassNames = useStatesClassNames(item.id, item.state, 'item-inner');
136
+ const wrapperStatesClassNames = useStatesClassNames(item.id, item.state, 'item-wrapper');
133
137
  return (
134
138
  <div
135
- className={`item-wrapper-${item.id}`}
139
+ className={`item-wrapper-${item.id} ${wrapperStatesClassNames}`}
136
140
  ref={itemWrapperRef}
137
141
  style={{
138
142
  ...(position ? { top: position.top } : {}),
@@ -152,7 +156,22 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight, isI
152
156
  >
153
157
  <RichTextWrapper isRichText={isRichText}>
154
158
  <div
155
- className={`item-${item.id}-inner`}
159
+ className={`item-${item.id}-inner ${innerStatesClassNames}`}
160
+ onClick={() => {
161
+ const trigger = getItemTrigger(item.id, 'click');
162
+ if (!trigger) return;
163
+ transitionTo(trigger.id, trigger.to);
164
+ }}
165
+ onMouseEnter={() => {
166
+ const trigger = getItemTrigger(item.id, 'hover-in');
167
+ if (!trigger) return;
168
+ transitionTo(trigger.id, trigger.to);
169
+ }}
170
+ onMouseLeave={() => {
171
+ const trigger = getItemTrigger(item.id, 'hover-out');
172
+ if (!trigger) return;
173
+ transitionTo(trigger.id, trigger.to);
174
+ }}
156
175
  style={{
157
176
  ...(dimensions ? {
158
177
  width: `${sizingAxis.x === 'manual'
@@ -161,7 +180,7 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight, isI
161
180
  : `${dimensions.width * 100}vw`
162
181
  : 'max-content'}`,
163
182
  height: `${sizingAxis.y === 'manual' ? `${dimensions.height * 100}vw` : 'unset'}` } : {}),
164
- ...(scale !== undefined ? { transform: `scale(${scale}) translateZ(0)`, 'WebkitTransform': `scale(${scale}) translateZ(0)` } : {}),
183
+ ...(scale !== undefined ? { transform: `scale(${scale})`, 'WebkitTransform': `scale(${scale})` } : {}),
165
184
  }}
166
185
  >
167
186
  <ItemComponent item={item} sectionId={sectionId} onResize={handleItemResize} articleHeight={articleHeight} />
@@ -169,10 +188,12 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight, isI
169
188
  </RichTextWrapper>
170
189
  </div>
171
190
  <JSXStyle id={id}>{`
172
- ${getLayoutStyles(layouts, layoutValues, ([area, hidden, hoverParams, sticky, sectionHeight, layoutParams], exemplary) => {
191
+ ${getLayoutStyles(layouts, layoutValues, ([area, hidden, states, sticky, sectionHeight, layoutParams], exemplary) => {
173
192
  const sizingAxis = parseSizing(layoutParams.sizing);
174
193
  const isScreenBasedBottom = area.positionType === PositionType.ScreenBased && area.anchorSide === AnchorSide.Bottom;
175
194
  const scaleAnchor = area.scaleAnchor as AreaAnchor;
195
+ const statesInnerCSS = getStatesCSS(item.id, 'item-inner', ['width', 'height', 'scale'], states);
196
+ const statesWrapperCSS = getStatesCSS(item.id, 'item-wrapper', ['top', 'left'], states, area.anchorSide);
176
197
  return (`
177
198
  .item-${item.id} {
178
199
  position: ${sticky ? 'sticky' : 'absolute'};
@@ -183,7 +204,6 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight, isI
183
204
  height: fit-content;
184
205
  }
185
206
  .item-${item.id}-inner {
186
- transition: ${getTransitions(['width', 'height', 'scale'], hoverParams)};
187
207
  pointer-events: auto;
188
208
  width: ${sizingAxis.x === 'manual'
189
209
  ? `${area.width * 100}vw`
@@ -191,6 +211,7 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight, isI
191
211
  height: ${sizingAxis.y === 'manual' ? `${area.height * 100}vw` : 'unset'};
192
212
  transform-origin: ${ScaleAnchorMap[scaleAnchor]};
193
213
  transform: scale(${area.scale});
214
+ transition: all 0.2s ease;
194
215
  }
195
216
  .item-wrapper-${item.id} {
196
217
  position: ${area.positionType === PositionType.ScreenBased ? 'fixed': 'absolute'};
@@ -200,14 +221,10 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight, isI
200
221
  bottom: ${isScreenBasedBottom ? `${-area.top * 100}vw` : 'unset'};
201
222
  top: ${isScreenBasedBottom ? 'unset' : getItemTopStyle(area.top, area.anchorSide)};
202
223
  left: ${area.left * 100}vw;
203
- transition: ${getTransitions(['left', 'top'], hoverParams)};
204
- }
205
- .item-${item.id}-inner:hover {
206
- ${getHoverStyles(['width', 'height', 'scale'], hoverParams)};
207
- }
208
- .item-wrapper-${item.id}:hover {
209
- ${getHoverStyles(['left', 'top'], hoverParams, area.anchorSide)};
224
+ transition: all 0.2s ease;
210
225
  }
226
+ ${statesWrapperCSS}
227
+ ${statesInnerCSS}
211
228
  `);
212
229
  })}
213
230
  `}</JSXStyle>
@@ -223,11 +240,11 @@ function parseSizing(sizing: string = 'manual'): Axis {
223
240
  } as Axis;
224
241
  }
225
242
 
243
+ export function isItemType<T extends ArticleItemType>(item: ItemAny, itemType: T): item is TItem<T> {
244
+ return item.type === itemType;
245
+ }
246
+
226
247
  interface Axis {
227
248
  x: 'manual' | 'auto';
228
249
  y: 'manual' | 'auto';
229
250
  }
230
-
231
- export function isItemType<T extends ArticleItemType>(item: ItemAny, itemType: T): item is TItem<T> {
232
- return item.type === itemType;
233
- }
@@ -1,13 +1,14 @@
1
- import { ArticleItemType, getLayoutStyles, CodeEmbedItem as TCodeEmbedItem, AreaAnchor } from '@cntrl-site/sdk';
1
+ import { getLayoutStyles, CodeEmbedItem as TCodeEmbedItem, AreaAnchor } from '@cntrl-site/sdk';
2
2
  import { FC, useEffect, useId, useState } from 'react';
3
3
  import { useCntrlContext } from '../../provider/useCntrlContext';
4
4
  import { ItemProps } from '../Item';
5
- import { getHoverStyles, getTransitions } from '../../utils/HoverStyles/HoverStyles';
6
5
  import JSXStyle from 'styled-jsx/style';
7
6
  import { useRegisterResize } from "../../common/useRegisterResize";
8
7
  import { useItemAngle } from '../useItemAngle';
9
8
  import { LinkWrapper } from '../LinkWrapper';
10
9
  import { useCodeEmbedItem } from './useCodeEmbedItem';
10
+ import { getStatesCSS } from '../../utils/getStatesCSS';
11
+ import { useStatesClassNames } from '../useStatesClassNames';
11
12
 
12
13
  const stylesMap = {
13
14
  [AreaAnchor.TopLeft]: {},
@@ -30,7 +31,8 @@ export const CodeEmbedItem: FC<ItemProps<TCodeEmbedItem>> = ({ item, sectionId,
30
31
  const [ref, setRef] = useState<HTMLDivElement | null>(null);
31
32
  useRegisterResize(ref, onResize);
32
33
  const pos = stylesMap[anchor];
33
- const layoutValues: Record<string, any>[] = [item.area, item.layoutParams, item.state.hover];
34
+ const layoutValues: Record<string, any>[] = [item.area, item.layoutParams, item.state];
35
+ const statesClassNames = useStatesClassNames(item.id, item.state, 'embed-wrapper');
34
36
 
35
37
  useEffect(() => {
36
38
  if (!ref) return;
@@ -56,7 +58,7 @@ export const CodeEmbedItem: FC<ItemProps<TCodeEmbedItem>> = ({ item, sectionId,
56
58
  return (
57
59
  <LinkWrapper url={item.link?.url} target={item.link?.target}>
58
60
  <div
59
- className={`embed-wrapper-${item.id}`}
61
+ className={`embed-wrapper-${item.id} ${statesClassNames}`}
60
62
  style={{
61
63
  ...(angle !== undefined ? { transform: `rotate(${angle}deg)` } : {}),
62
64
  ...(blur !== undefined ? { filter: `blur(${blur * 100}vw)` } : {}),
@@ -93,21 +95,20 @@ export const CodeEmbedItem: FC<ItemProps<TCodeEmbedItem>> = ({ item, sectionId,
93
95
  z-index: 1;
94
96
  border: none;
95
97
  }
96
- ${getLayoutStyles(layouts, layoutValues, ([area, layoutParams, hoverParams], exemplary) => {
98
+ ${getLayoutStyles(layouts, layoutValues, ([area, layoutParams, stateParams], exemplary) => {
99
+ const statesCSS = getStatesCSS(item.id, 'embed-wrapper', ['angle', 'blur', 'opacity'], stateParams);
97
100
  return (`
98
101
  .embed-wrapper-${item.id} {
99
102
  opacity: ${layoutParams.opacity};
100
103
  transform: rotate(${area.angle}deg);
101
104
  filter: ${layoutParams.blur !== 0 ? `blur(${layoutParams.blur * 100}vw)` : 'unset'};
102
- transition: ${getTransitions<ArticleItemType.CodeEmbed>(['angle', 'blur', 'opacity'], hoverParams)};
105
+ transition: all 0.2s ease;
103
106
  }
104
107
  .embed-${item.id} {
105
108
  width: ${item.commonParams.scale ? `${area.width * exemplary}px` : '100%'};
106
109
  height: ${item.commonParams.scale ? `${area.height * exemplary}px` : '100%'};
107
110
  }
108
- .embed-wrapper-${item.id}:hover {
109
- ${getHoverStyles<ArticleItemType.CodeEmbed>(['angle', 'blur', 'opacity'], hoverParams)}
110
- }
111
+ ${statesCSS}
111
112
  `);
112
113
  })}
113
114
  `}</JSXStyle>