@cntrl-site/sdk-nextjs 1.8.36-alpha.1 → 1.8.37

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 (29) hide show
  1. package/lib/components/items/CodeEmbedItem/CodeEmbedItem.js +1 -0
  2. package/lib/components/items/ComponentItem/ComponentItem.js +1 -0
  3. package/lib/components/items/EmbedVideoItem/VimeoEmbed.js +1 -0
  4. package/lib/components/items/EmbedVideoItem/YoutubeEmbed.js +1 -0
  5. package/lib/components/items/FileItem/ImageItem.js +1 -0
  6. package/lib/components/items/FileItem/VideoItem.js +1 -0
  7. package/lib/components/items/GroupItem/GroupItem.js +1 -0
  8. package/lib/components/items/Item.js +2 -1
  9. package/lib/components/items/RectangleItem/RectangleItem.js +1 -0
  10. package/lib/components/items/RichTextItem/RichTextItem.js +1 -0
  11. package/lib/interactions/InteractionsRegistry.js +111 -5
  12. package/lib/interactions/ItemInteractionCtrl.js +1 -1
  13. package/lib/provider/InteractionsContext.js +29 -0
  14. package/package.json +2 -2
  15. package/src/components/ScrollPlaybackVideo.tsx +1 -1
  16. package/src/components/items/CodeEmbedItem/CodeEmbedItem.tsx +1 -0
  17. package/src/components/items/ComponentItem/ComponentItem.tsx +1 -0
  18. package/src/components/items/EmbedVideoItem/VimeoEmbed.tsx +1 -0
  19. package/src/components/items/EmbedVideoItem/YoutubeEmbed.tsx +1 -0
  20. package/src/components/items/FileItem/ImageItem.tsx +1 -0
  21. package/src/components/items/FileItem/VideoItem.tsx +1 -0
  22. package/src/components/items/GroupItem/GroupItem.tsx +1 -0
  23. package/src/components/items/Item.tsx +2 -1
  24. package/src/components/items/RectangleItem/RectangleItem.tsx +1 -0
  25. package/src/components/items/RichTextItem/RichTextItem.tsx +1 -0
  26. package/src/interactions/InteractionsRegistry.ts +106 -9
  27. package/src/interactions/ItemInteractionCtrl.ts +4 -3
  28. package/src/interactions/types.ts +4 -4
  29. package/src/provider/InteractionsContext.tsx +32 -1
@@ -85,6 +85,7 @@ const CodeEmbedItem = ({ item, sectionId, onResize, interactionCtrl, onVisibilit
85
85
  .embed-wrapper-${item.id} {
86
86
  position: absolute;
87
87
  width: 100%;
88
+ will-change: transform;
88
89
  height: 100%;
89
90
  }
90
91
  .embed-${item.id} {
@@ -36,6 +36,7 @@ const ComponentItem = ({ item, sectionId, onResize, interactionCtrl }) => {
36
36
  width: 100%;
37
37
  height: 100%;
38
38
  pointer-events: auto;
39
+ will-change: transform;
39
40
  }
40
41
  ${(0, sdk_1.getLayoutStyles)(layouts, layoutValues, ([area, layoutParams]) => {
41
42
  return (`
@@ -144,6 +144,7 @@ const VimeoEmbedItem = ({ item, sectionId, onResize, interactionCtrl, onVisibili
144
144
  position: absolute;
145
145
  width: 100%;
146
146
  height: 100%;
147
+ will-change: transform;
147
148
  }
148
149
  .embed-video {
149
150
  width: 100%;
@@ -136,6 +136,7 @@ const YoutubeEmbedItem = ({ item, sectionId, onResize, interactionCtrl, onVisibi
136
136
  .embed-youtube-video-wrapper-${item.id},
137
137
  .embed-${item.id} {
138
138
  position: absolute;
139
+ will-change: transform;
139
140
  width: 100%;
140
141
  height: 100%;
141
142
  }
@@ -72,6 +72,7 @@ const ImageItem = ({ item, sectionId, onResize, interactionCtrl, onVisibilityCha
72
72
  position: absolute;
73
73
  width: 100%;
74
74
  height: 100%;
75
+ will-change: transform;
75
76
  box-sizing: border-box;
76
77
  display: flex;
77
78
  }
@@ -138,6 +138,7 @@ const VideoItem = ({ item, sectionId, onResize, interactionCtrl, onVisibilityCha
138
138
  height: 100%;
139
139
  box-sizing: border-box;
140
140
  opacity: ${opacity};
141
+ will-change: transform;
141
142
  }
142
143
  .video-overlay-${item.id} {
143
144
  position: absolute;
@@ -40,6 +40,7 @@ const GroupItem = ({ item, sectionId, onResize, interactionCtrl, onVisibilityCha
40
40
  width: 100%;
41
41
  height: 100%;
42
42
  box-sizing: border-box;
43
+ will-change: transform;
43
44
  }
44
45
  ${(0, sdk_1.getLayoutStyles)(layouts, layoutValues, ([area, layoutParams]) => {
45
46
  return (`
@@ -53,7 +53,7 @@ const Item = ({ item, sectionId, articleHeight, isParentVisible = true, isInGrou
53
53
  const [position, setPosition] = (0, react_1.useState)({ x: 0, y: 0 });
54
54
  const [isDraggingActive, setIsDraggingActive] = (0, react_1.useState)(false);
55
55
  const wrapperStateProps = interactionCtrl === null || interactionCtrl === void 0 ? void 0 : interactionCtrl.getState(['top', 'left']);
56
- const innerStateProps = interactionCtrl === null || interactionCtrl === void 0 ? void 0 : interactionCtrl.getState(['width', 'height', 'scale', 'top', 'left']);
56
+ const innerStateProps = interactionCtrl === null || interactionCtrl === void 0 ? void 0 : interactionCtrl.getState(['width', 'height', 'scale']);
57
57
  const { width, height, top, left } = (0, useItemArea_1.useItemArea)(item, sectionId, {
58
58
  top: (_b = wrapperStateProps === null || wrapperStateProps === void 0 ? void 0 : wrapperStateProps.styles) === null || _b === void 0 ? void 0 : _b.top,
59
59
  left: (_c = wrapperStateProps === null || wrapperStateProps === void 0 ? void 0 : wrapperStateProps.styles) === null || _c === void 0 ? void 0 : _c.left,
@@ -160,6 +160,7 @@ const Item = ({ item, sectionId, articleHeight, isParentVisible = true, isInGrou
160
160
  .item-wrapper-${item.id} {
161
161
  position: ${area.positionType === sdk_1.PositionType.ScreenBased ? 'fixed' : 'absolute'};
162
162
  z-index: ${area.zIndex};
163
+ ${!isInGroup && stickyFix}
163
164
  pointer-events: none;
164
165
  bottom: ${isScreenBasedBottom ? `${-area.top * 100}vw` : 'unset'};
165
166
  top: ${isScreenBasedBottom ? 'unset' : (0, getItemTopStyle_1.getItemTopStyle)(area.top, area.anchorSide)};
@@ -62,6 +62,7 @@ const RectangleItem = ({ item, sectionId, onResize, interactionCtrl, onVisibilit
62
62
  border-radius: ${layoutParams.radius * 100}vw;
63
63
  border-width: ${layoutParams.strokeWidth * 100}vw;
64
64
  transform: rotate(${area.angle}deg);
65
+ will-change: transform;
65
66
  filter: ${layoutParams.blur !== 0 ? `blur(${layoutParams.blur * 100}vw)` : 'unset'};
66
67
  backdrop-filter: ${layoutParams.backdropBlur !== 0 ? `blur(${layoutParams.backdropBlur * 100}vw)` : 'unset'};
67
68
  -webkit-backdrop-filter: ${layoutParams.backdropBlur !== 0 ? `blur(${layoutParams.backdropBlur * 100}vw)` : 'unset'};
@@ -68,6 +68,7 @@ const RichTextItem = ({ item, sectionId, onResize, interactionCtrl, onVisibility
68
68
  font-variant: ${layoutParams.fontVariant};
69
69
  color: ${color.fmt('rgba')};
70
70
  transform: rotate(${area.angle}deg);
71
+ will-change: transform;
71
72
  filter: ${layoutParams.blur !== 0 ? `blur(${layoutParams.blur * 100}vw)` : 'unset'};
72
73
  text-transform: ${layoutParams.textTransform};
73
74
  }
@@ -29,7 +29,6 @@ class InteractionsRegistry {
29
29
  }
30
30
  return map;
31
31
  }, {});
32
- const itemStages = this.getDefaultItemStages();
33
32
  const stateItemsIdsMap = activeStatesIds.reduce((map, stateId) => {
34
33
  map[stateId] = this.items
35
34
  .filter((item) => {
@@ -42,9 +41,10 @@ class InteractionsRegistry {
42
41
  return map;
43
42
  }, {});
44
43
  this.interactions = interactions;
45
- this.itemsStages = itemStages;
46
44
  this.stateItemsIdsMap = stateItemsIdsMap;
47
45
  this.interactionStateMap = interactionStateMap;
46
+ const itemStages = this.getDefaultItemStages();
47
+ this.itemsStages = itemStages;
48
48
  }
49
49
  register(itemId, ctrl) {
50
50
  this.ctrls.set(itemId, ctrl);
@@ -93,6 +93,8 @@ class InteractionsRegistry {
93
93
  for (const interaction of this.interactions) {
94
94
  const { triggers } = interaction;
95
95
  for (const trigger of triggers) {
96
+ if (!('itemId' in trigger))
97
+ continue;
96
98
  if (trigger.itemId !== itemId)
97
99
  continue;
98
100
  if (activeStates.includes(trigger.from)) {
@@ -102,12 +104,112 @@ class InteractionsRegistry {
102
104
  }
103
105
  return available;
104
106
  }
105
- notifyTrigger(itemId, triggerType) {
107
+ notifyLoad() {
108
+ var _a, _b;
109
+ const timestamp = Date.now();
110
+ for (const interaction of this.interactions) {
111
+ const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
112
+ const matchingTrigger = interaction.triggers.find(trigger => 'position' in trigger && trigger.position === 0 && trigger.from === currentStateId);
113
+ if (!matchingTrigger)
114
+ continue;
115
+ const activeStateId = this.getActiveInteractionState(interaction.id);
116
+ const isNewStateActive = matchingTrigger.to === activeStateId;
117
+ this.setCurrentStateForInteraction(interaction.id, matchingTrigger.to);
118
+ const transitioningItems = (_a = this.stateItemsIdsMap[activeStateId]) !== null && _a !== void 0 ? _a : [];
119
+ const state = interaction.states.find((state) => state.id === matchingTrigger.to);
120
+ const actions = (_b = state === null || state === void 0 ? void 0 : state.actions) !== null && _b !== void 0 ? _b : [];
121
+ for (const action of actions) {
122
+ const ctrl = this.ctrls.get(action.itemId);
123
+ if (!ctrl)
124
+ continue;
125
+ ctrl.receiveAction(action.type);
126
+ }
127
+ this.itemsStages = this.itemsStages.map((stage) => {
128
+ if (stage.interactionId !== interaction.id)
129
+ return stage;
130
+ return {
131
+ itemId: stage.itemId,
132
+ interactionId: stage.interactionId,
133
+ type: 'transitioning',
134
+ from: stage.type === 'transitioning' ? stage.to : stage.stateId,
135
+ to: matchingTrigger.to,
136
+ direction: isNewStateActive ? 'in' : 'out',
137
+ updated: timestamp
138
+ };
139
+ });
140
+ const itemsToNotify = new Set(transitioningItems);
141
+ for (const trigger of interaction.triggers) {
142
+ if (!('itemId' in trigger))
143
+ continue;
144
+ itemsToNotify.add(trigger.itemId);
145
+ }
146
+ this.notifyItemCtrlsChange(Array.from(itemsToNotify));
147
+ this.notifyTransitionStartForItems(transitioningItems, activeStateId);
148
+ }
149
+ }
150
+ notifyScroll(position) {
151
+ var _a, _b, _c;
152
+ const timestamp = Date.now();
153
+ for (const interaction of this.interactions) {
154
+ const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
155
+ const activeStateId = (_a = interaction.states.find((state) => state.id !== interaction.startStateId)) === null || _a === void 0 ? void 0 : _a.id;
156
+ const matchingTrigger = interaction.triggers.find((trigger) => {
157
+ if (!('position' in trigger) || trigger.position === 0)
158
+ return false;
159
+ const triggerPosition = trigger.position * window.innerWidth;
160
+ const isScrolledPastTrigger = triggerPosition < position;
161
+ if (!isScrolledPastTrigger && !trigger.isReverse)
162
+ return false;
163
+ const stateId = isScrolledPastTrigger ? trigger.from : trigger.to;
164
+ return stateId === currentStateId;
165
+ });
166
+ if (!matchingTrigger || !('position' in matchingTrigger) || !activeStateId)
167
+ continue;
168
+ const triggerPosition = matchingTrigger.position * window.innerWidth;
169
+ const isScrolledPastTrigger = triggerPosition < position;
170
+ const targetStateId = isScrolledPastTrigger ? matchingTrigger.to : matchingTrigger.from;
171
+ this.setCurrentStateForInteraction(interaction.id, targetStateId);
172
+ const transitioningItems = (_b = this.stateItemsIdsMap[activeStateId]) !== null && _b !== void 0 ? _b : [];
173
+ const state = interaction.states.find((state) => state.id === targetStateId);
174
+ const actions = (_c = state === null || state === void 0 ? void 0 : state.actions) !== null && _c !== void 0 ? _c : [];
175
+ for (const action of actions) {
176
+ const ctrl = this.ctrls.get(action.itemId);
177
+ if (!ctrl)
178
+ continue;
179
+ ctrl.receiveAction(action.type);
180
+ }
181
+ const itemsStages = this.itemsStages.map((stage) => {
182
+ if (stage.interactionId !== interaction.id)
183
+ return stage;
184
+ const newStage = {
185
+ itemId: stage.itemId,
186
+ interactionId: stage.interactionId,
187
+ type: 'transitioning',
188
+ from: stage.type === 'transitioning' ? stage.to : stage.stateId,
189
+ to: targetStateId,
190
+ direction: targetStateId === activeStateId ? 'in' : 'out',
191
+ updated: timestamp
192
+ };
193
+ return newStage;
194
+ });
195
+ this.itemsStages = itemsStages;
196
+ const itemsToNotify = new Set(transitioningItems);
197
+ for (const trigger of interaction.triggers) {
198
+ if (!('itemId' in trigger))
199
+ continue;
200
+ itemsToNotify.add(trigger.itemId);
201
+ }
202
+ this.notifyItemCtrlsChange(Array.from(itemsToNotify));
203
+ this.notifyTransitionStartForItems(transitioningItems, activeStateId);
204
+ }
205
+ }
206
+ notifyItemTrigger(itemId, triggerType) {
106
207
  var _a, _b;
107
208
  const timestamp = Date.now();
108
209
  for (const interaction of this.interactions) {
109
210
  const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
110
- const matchingTrigger = interaction.triggers.find((trigger) => trigger.itemId === itemId
211
+ const matchingTrigger = interaction.triggers.find((trigger) => 'itemId' in trigger
212
+ && trigger.itemId === itemId
111
213
  && trigger.from === currentStateId
112
214
  && trigger.type === triggerType);
113
215
  if (!matchingTrigger)
@@ -139,6 +241,8 @@ class InteractionsRegistry {
139
241
  });
140
242
  const itemsToNotify = new Set(transitioningItems);
141
243
  for (const trigger of interaction.triggers) {
244
+ if (!('itemId' in trigger))
245
+ continue;
142
246
  itemsToNotify.add(trigger.itemId);
143
247
  }
144
248
  this.notifyItemCtrlsChange(Array.from(itemsToNotify));
@@ -223,6 +327,7 @@ class InteractionsRegistry {
223
327
  return allItems;
224
328
  }
225
329
  getDefaultItemStages() {
330
+ var _a;
226
331
  const timestamp = Date.now();
227
332
  const { items } = this;
228
333
  const stages = [];
@@ -239,7 +344,8 @@ class InteractionsRegistry {
239
344
  interactionId,
240
345
  type: 'active',
241
346
  isStartState: true,
242
- updated: timestamp
347
+ updated: timestamp,
348
+ stateId: (_a = this.interactions.find((interaction) => interaction.id === interactionId)) === null || _a === void 0 ? void 0 : _a.startStateId
243
349
  });
244
350
  }
245
351
  }
@@ -56,7 +56,7 @@ class ItemInteractionController {
56
56
  return triggers.has(triggerType);
57
57
  }
58
58
  sendTrigger(type) {
59
- this.registry.notifyTrigger(this.itemId, type);
59
+ this.registry.notifyItemTrigger(this.itemId, type);
60
60
  }
61
61
  receiveAction(type) {
62
62
  var _a;
@@ -5,14 +5,43 @@ const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_1 = require("react");
6
6
  const InteractionsRegistry_1 = require("../interactions/InteractionsRegistry");
7
7
  const useCurrentLayout_1 = require("../common/useCurrentLayout");
8
+ const ArticleRectContext_1 = require("./ArticleRectContext");
8
9
  exports.InteractionsContext = (0, react_1.createContext)(undefined);
9
10
  const InteractionsProvider = ({ article, children }) => {
10
11
  const { layoutId } = (0, useCurrentLayout_1.useCurrentLayout)();
12
+ const articleRectObserver = (0, react_1.useContext)(ArticleRectContext_1.ArticleRectContext);
11
13
  const registry = (0, react_1.useMemo)(() => {
12
14
  if (!layoutId)
13
15
  return;
14
16
  return new InteractionsRegistry_1.InteractionsRegistry(article, layoutId);
15
17
  }, [layoutId]);
18
+ (0, react_1.useEffect)(() => {
19
+ if (!registry || !articleRectObserver)
20
+ return;
21
+ const handleScroll = () => {
22
+ const scrollY = window.scrollY;
23
+ registry.notifyScroll(scrollY);
24
+ };
25
+ return articleRectObserver.on('scroll', handleScroll);
26
+ }, [registry, articleRectObserver]);
27
+ const notifyLoad = (0, react_1.useCallback)(() => {
28
+ if (!registry)
29
+ return;
30
+ requestAnimationFrame(() => {
31
+ setTimeout(() => {
32
+ registry.notifyLoad();
33
+ }, 0);
34
+ });
35
+ }, [registry]);
36
+ (0, react_1.useEffect)(() => {
37
+ if (document.readyState === 'complete') {
38
+ notifyLoad();
39
+ }
40
+ else {
41
+ window.addEventListener('load', notifyLoad);
42
+ }
43
+ return () => window.removeEventListener('load', notifyLoad);
44
+ }, [notifyLoad]);
16
45
  return ((0, jsx_runtime_1.jsx)(exports.InteractionsContext.Provider, { value: registry, children: children }));
17
46
  };
18
47
  exports.InteractionsProvider = InteractionsProvider;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cntrl-site/sdk-nextjs",
3
- "version": "1.8.36-alpha.1",
3
+ "version": "1.8.37",
4
4
  "description": "SDK for Next.js",
5
5
  "author": "arsen@momdesign.nyc",
6
6
  "license": "MIT",
@@ -31,7 +31,7 @@
31
31
  "@antfu/eslint-config": "^3.8.0",
32
32
  "@cntrl-site/color": "^1.0.0",
33
33
  "@cntrl-site/effects": "^1.4.0",
34
- "@cntrl-site/sdk": "^1.22.21",
34
+ "@cntrl-site/sdk": "^1.23.0",
35
35
  "@types/vimeo__player": "^2.18.0",
36
36
  "@vimeo/player": "^2.25.0",
37
37
  "html-react-parser": "^3.0.1",
@@ -24,7 +24,7 @@ export const ScrollPlaybackVideo: FC<Props> = ({ sectionId, src, playbackParams,
24
24
  const scrollPos = articleRectObserver.getSectionScroll(sectionId);
25
25
  const time = rangeMap(scrollPos, playbackParams.from, playbackParams.to, 0, 1, true);
26
26
  setTime(toFixed(time));
27
- })
27
+ });
28
28
  }, [playbackParams?.from, playbackParams?.to, time]);
29
29
 
30
30
  const scrollVideoManager = useMemo<ScrollPlaybackVideoManager | null>(() => {
@@ -110,6 +110,7 @@ export const CodeEmbedItem: FC<ItemProps<TCodeEmbedItem>> = ({ item, sectionId,
110
110
  .embed-wrapper-${item.id} {
111
111
  position: absolute;
112
112
  width: 100%;
113
+ will-change: transform;
113
114
  height: 100%;
114
115
  }
115
116
  .embed-${item.id} {
@@ -49,6 +49,7 @@ export const ComponentItem: FC<ItemProps<TComponentItem>> = ({ item, sectionId,
49
49
  width: 100%;
50
50
  height: 100%;
51
51
  pointer-events: auto;
52
+ will-change: transform;
52
53
  }
53
54
  ${getLayoutStyles(layouts, layoutValues, ([area, layoutParams]) => {
54
55
  return (`
@@ -177,6 +177,7 @@ export const VimeoEmbedItem: FC<ItemProps<TVimeoEmbedItem>> = ({ item, sectionId
177
177
  position: absolute;
178
178
  width: 100%;
179
179
  height: 100%;
180
+ will-change: transform;
180
181
  }
181
182
  .embed-video {
182
183
  width: 100%;
@@ -160,6 +160,7 @@ export const YoutubeEmbedItem: FC<ItemProps<TYoutubeEmbedItem>> = ({ item, secti
160
160
  .embed-youtube-video-wrapper-${item.id},
161
161
  .embed-${item.id} {
162
162
  position: absolute;
163
+ will-change: transform;
163
164
  width: 100%;
164
165
  height: 100%;
165
166
  }
@@ -114,6 +114,7 @@ export const ImageItem: FC<ItemProps<TImageItem>> = ({ item, sectionId, onResize
114
114
  position: absolute;
115
115
  width: 100%;
116
116
  height: 100%;
117
+ will-change: transform;
117
118
  box-sizing: border-box;
118
119
  display: flex;
119
120
  }
@@ -211,6 +211,7 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
211
211
  height: 100%;
212
212
  box-sizing: border-box;
213
213
  opacity: ${opacity};
214
+ will-change: transform;
214
215
  }
215
216
  .video-overlay-${item.id} {
216
217
  position: absolute;
@@ -63,6 +63,7 @@ export const GroupItem: FC<ItemProps<TGroupItem>> = ({ item, sectionId, onResize
63
63
  width: 100%;
64
64
  height: 100%;
65
65
  box-sizing: border-box;
66
+ will-change: transform;
66
67
  }
67
68
  ${getLayoutStyles(layouts, layoutValues, ([area, layoutParams]) => {
68
69
  return (`
@@ -83,7 +83,7 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight, isP
83
83
  const [position, setPosition] = useState({ x: 0, y: 0 });
84
84
  const [isDraggingActive, setIsDraggingActive] = useState(false);
85
85
  const wrapperStateProps = interactionCtrl?.getState(['top', 'left']);
86
- const innerStateProps = interactionCtrl?.getState(['width', 'height', 'scale', 'top', 'left']);
86
+ const innerStateProps = interactionCtrl?.getState(['width', 'height', 'scale']);
87
87
  const { width, height, top, left } = useItemArea(item, sectionId, {
88
88
  top: wrapperStateProps?.styles?.top as number,
89
89
  left: wrapperStateProps?.styles?.left as number,
@@ -236,6 +236,7 @@ export const Item: FC<ItemWrapperProps> = ({ item, sectionId, articleHeight, isP
236
236
  .item-wrapper-${item.id} {
237
237
  position: ${area.positionType === PositionType.ScreenBased ? 'fixed' : 'absolute'};
238
238
  z-index: ${area.zIndex};
239
+ ${!isInGroup && stickyFix}
239
240
  pointer-events: none;
240
241
  bottom: ${isScreenBasedBottom ? `${-area.top * 100}vw` : 'unset'};
241
242
  top: ${isScreenBasedBottom ? 'unset' : getItemTopStyle(area.top, area.anchorSide)};
@@ -82,6 +82,7 @@ export const RectangleItem: FC<ItemProps<TRectangleItem>> = ({ item, sectionId,
82
82
  border-radius: ${layoutParams.radius * 100}vw;
83
83
  border-width: ${layoutParams.strokeWidth * 100}vw;
84
84
  transform: rotate(${area.angle}deg);
85
+ will-change: transform;
85
86
  filter: ${layoutParams.blur !== 0 ? `blur(${layoutParams.blur * 100}vw)` : 'unset'};
86
87
  backdrop-filter: ${layoutParams.backdropBlur !== 0 ? `blur(${layoutParams.backdropBlur * 100}vw)` : 'unset'};
87
88
  -webkit-backdrop-filter: ${layoutParams.backdropBlur !== 0 ? `blur(${layoutParams.backdropBlur * 100}vw)` : 'unset'};
@@ -89,6 +89,7 @@ export const RichTextItem: FC<ItemProps<TRichTextItem>> = ({ item, sectionId, on
89
89
  font-variant: ${layoutParams.fontVariant};
90
90
  color: ${color.fmt('rgba')};
91
91
  transform: rotate(${area.angle}deg);
92
+ will-change: transform;
92
93
  filter: ${layoutParams.blur !== 0 ? `blur(${layoutParams.blur * 100}vw)` : 'unset'};
93
94
  text-transform: ${layoutParams.textTransform};
94
95
  }
@@ -3,7 +3,7 @@ import {
3
3
  Article,
4
4
  ArticleItemType,
5
5
  Interaction,
6
- InteractionTrigger,
6
+ InteractionItemTrigger,
7
7
  ItemAny,
8
8
  } from '@cntrl-site/sdk';
9
9
  import { isItemType } from '../utils/isItemType';
@@ -39,7 +39,6 @@ export class InteractionsRegistry implements InteractionsRegistryPort {
39
39
  }
40
40
  return map;
41
41
  }, {});
42
- const itemStages = this.getDefaultItemStages();
43
42
  const stateItemsIdsMap = activeStatesIds.reduce<StateItemsIdsMap>((map, stateId) => {
44
43
  map[stateId] = this.items
45
44
  .filter((item) => {
@@ -51,9 +50,10 @@ export class InteractionsRegistry implements InteractionsRegistryPort {
51
50
  return map;
52
51
  }, {});
53
52
  this.interactions = interactions;
54
- this.itemsStages = itemStages;
55
53
  this.stateItemsIdsMap = stateItemsIdsMap;
56
54
  this.interactionStateMap = interactionStateMap;
55
+ const itemStages = this.getDefaultItemStages();
56
+ this.itemsStages = itemStages;
57
57
  }
58
58
 
59
59
  register(itemId: ItemId, ctrl: ItemInteractionCtrl) {
@@ -96,12 +96,13 @@ export class InteractionsRegistry implements InteractionsRegistryPort {
96
96
  return itemStyles;
97
97
  }
98
98
 
99
- getItemAvailableTriggers(itemId: string): Set<InteractionTrigger['type']> {
100
- const available = new Set<InteractionTrigger['type']>();
99
+ getItemAvailableTriggers(itemId: string): Set<InteractionItemTrigger['type']> {
100
+ const available = new Set<InteractionItemTrigger['type']>();
101
101
  const activeStates = Object.values(this.interactionStateMap);
102
102
  for (const interaction of this.interactions) {
103
103
  const { triggers } = interaction;
104
104
  for (const trigger of triggers) {
105
+ if (!('itemId' in trigger)) continue;
105
106
  if (trigger.itemId !== itemId) continue;
106
107
  if (activeStates.includes(trigger.from)) {
107
108
  available.add(trigger.type);
@@ -111,12 +112,106 @@ export class InteractionsRegistry implements InteractionsRegistryPort {
111
112
  return available;
112
113
  }
113
114
 
114
- notifyTrigger(itemId: string, triggerType: TriggerType): void {
115
+ notifyLoad() {
116
+ const timestamp = Date.now();
117
+ for (const interaction of this.interactions) {
118
+ const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
119
+ const matchingTrigger = interaction.triggers.find(trigger =>
120
+ 'position' in trigger && trigger.position === 0 && trigger.from === currentStateId
121
+ );
122
+ if (!matchingTrigger) continue;
123
+ const activeStateId = this.getActiveInteractionState(interaction.id);
124
+ const isNewStateActive = matchingTrigger.to === activeStateId;
125
+ this.setCurrentStateForInteraction(interaction.id, matchingTrigger.to);
126
+ const transitioningItems = this.stateItemsIdsMap[activeStateId] ?? [];
127
+ const state = interaction.states.find((state) => state.id === matchingTrigger.to);
128
+ const actions = state?.actions ?? [];
129
+ for (const action of actions) {
130
+ const ctrl = this.ctrls.get(action.itemId);
131
+ if (!ctrl) continue;
132
+ ctrl.receiveAction(action.type);
133
+ }
134
+
135
+ this.itemsStages = this.itemsStages.map((stage) => {
136
+ if (stage.interactionId !== interaction.id) return stage;
137
+ return {
138
+ itemId: stage.itemId,
139
+ interactionId: stage.interactionId,
140
+ type: 'transitioning',
141
+ from: stage.type === 'transitioning' ? stage.to : stage.stateId!,
142
+ to: matchingTrigger.to,
143
+ direction: isNewStateActive ? 'in' : 'out',
144
+ updated: timestamp
145
+ };
146
+ });
147
+
148
+ const itemsToNotify = new Set<ItemId>(transitioningItems);
149
+ for (const trigger of interaction.triggers) {
150
+ if (!('itemId' in trigger)) continue;
151
+ itemsToNotify.add(trigger.itemId);
152
+ }
153
+ this.notifyItemCtrlsChange(Array.from(itemsToNotify));
154
+ this.notifyTransitionStartForItems(transitioningItems, activeStateId);
155
+ }
156
+ }
157
+
158
+ notifyScroll(position: number) {
159
+ const timestamp = Date.now();
160
+ for (const interaction of this.interactions) {
161
+ const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
162
+ const activeStateId = interaction.states.find((state) => state.id !== interaction.startStateId)?.id;
163
+ 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;
167
+ if (!isScrolledPastTrigger && !trigger.isReverse) return false;
168
+ const stateId = isScrolledPastTrigger ? trigger.from : trigger.to;
169
+ return stateId === currentStateId;
170
+ });
171
+ if (!matchingTrigger || !('position' in matchingTrigger) || !activeStateId) continue;
172
+ const triggerPosition = matchingTrigger.position * window.innerWidth;
173
+ const isScrolledPastTrigger = triggerPosition < position;
174
+ const targetStateId = isScrolledPastTrigger ? matchingTrigger.to : matchingTrigger.from;
175
+ this.setCurrentStateForInteraction(interaction.id, targetStateId);
176
+ const transitioningItems = this.stateItemsIdsMap[activeStateId] ?? [];
177
+ const state = interaction.states.find((state) => state.id === targetStateId);
178
+ const actions = state?.actions ?? [];
179
+ for (const action of actions) {
180
+ const ctrl = this.ctrls.get(action.itemId);
181
+ if (!ctrl) continue;
182
+ ctrl.receiveAction(action.type);
183
+ }
184
+ const itemsStages = this.itemsStages.map((stage) => {
185
+ if (stage.interactionId !== interaction.id) return stage;
186
+ const newStage = {
187
+ itemId: stage.itemId,
188
+ interactionId: stage.interactionId,
189
+ type: 'transitioning' as const,
190
+ from: stage.type === 'transitioning' ? stage.to : stage.stateId!,
191
+ to: targetStateId,
192
+ direction: targetStateId === activeStateId ? 'in' as const : 'out' as const,
193
+ updated: timestamp
194
+ };
195
+ return newStage;
196
+ });
197
+ this.itemsStages = itemsStages;
198
+ const itemsToNotify = new Set<ItemId>(transitioningItems);
199
+ for (const trigger of interaction.triggers) {
200
+ if (!('itemId' in trigger)) continue;
201
+ itemsToNotify.add(trigger.itemId);
202
+ }
203
+ this.notifyItemCtrlsChange(Array.from(itemsToNotify));
204
+ this.notifyTransitionStartForItems(transitioningItems, activeStateId);
205
+ }
206
+ }
207
+
208
+ notifyItemTrigger(itemId: string, triggerType: TriggerType): void {
115
209
  const timestamp = Date.now();
116
210
  for (const interaction of this.interactions) {
117
211
  const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
118
212
  const matchingTrigger = interaction.triggers.find((trigger) =>
119
- trigger.itemId === itemId
213
+ 'itemId' in trigger
214
+ && trigger.itemId === itemId
120
215
  && trigger.from === currentStateId
121
216
  && trigger.type === triggerType
122
217
  );
@@ -146,6 +241,7 @@ export class InteractionsRegistry implements InteractionsRegistryPort {
146
241
  });
147
242
  const itemsToNotify = new Set<ItemId>(transitioningItems);
148
243
  for (const trigger of interaction.triggers) {
244
+ if (!('itemId' in trigger)) continue;
149
245
  itemsToNotify.add(trigger.itemId);
150
246
  }
151
247
  this.notifyItemCtrlsChange(Array.from(itemsToNotify));
@@ -248,7 +344,8 @@ export class InteractionsRegistry implements InteractionsRegistryPort {
248
344
  interactionId,
249
345
  type: 'active',
250
346
  isStartState: true,
251
- updated: timestamp
347
+ updated: timestamp,
348
+ stateId: this.interactions.find((interaction) => interaction.id === interactionId)?.startStateId
252
349
  });
253
350
  }
254
351
  }
@@ -269,7 +366,7 @@ type TransitioningStage = {
269
366
  type ActiveStage = { type: 'active'; itemId: string; interactionId: string; stateId?: string; isStartState: boolean; updated: number; };
270
367
  type InteractionStateMap = Record<InteractionId, StateId>;
271
368
  type StateItemsIdsMap = Record<StateId, ItemId[]>;
272
- type TriggerType = InteractionTrigger['type'];
369
+ type TriggerType = InteractionItemTrigger['type'];
273
370
  type InteractionId = string;
274
371
  type StateId = string;
275
372
  type ItemId = string;
@@ -1,11 +1,12 @@
1
1
  import { InteractionsRegistryPort, ItemInteractionCtrl } from './types';
2
2
  import { getTransition } from './getTransition';
3
3
  import { getStyleKeysFromCSSProperty } from './CSSPropertyNameMap';
4
- import { InteractionTrigger } from '@cntrl-site/sdk';
4
+ import { InteractionItemTrigger } from '@cntrl-site/sdk';
5
5
 
6
6
  export class ItemInteractionController implements ItemInteractionCtrl {
7
7
  private transitionsInProgress: Set<string> = new Set();
8
8
  private actionReceiver: ((type: 'play' | 'pause') => void) | undefined;
9
+
9
10
  constructor(
10
11
  private itemId: string,
11
12
  private registry: InteractionsRegistryPort,
@@ -29,13 +30,13 @@ export class ItemInteractionController implements ItemInteractionCtrl {
29
30
  };
30
31
  }
31
32
 
32
- getHasTrigger(itemId: string, triggerType: InteractionTrigger['type']): boolean {
33
+ getHasTrigger(itemId: string, triggerType: InteractionItemTrigger['type']): boolean {
33
34
  const triggers = this.registry.getItemAvailableTriggers(itemId);
34
35
  return triggers.has(triggerType);
35
36
  }
36
37
 
37
38
  sendTrigger(type: 'click' | 'hover-in' | 'hover-out') {
38
- this.registry.notifyTrigger(this.itemId, type);
39
+ this.registry.notifyItemTrigger(this.itemId, type);
39
40
  }
40
41
 
41
42
  receiveAction(type: 'play' | 'pause') {
@@ -1,8 +1,8 @@
1
- import { ArticleItemType, InteractionTrigger, ItemState } from '@cntrl-site/sdk';
1
+ import { ArticleItemType, InteractionItemTrigger, ItemState } from '@cntrl-site/sdk';
2
2
 
3
3
  export interface ItemInteractionCtrl {
4
4
  getState(keys: string[]): StateCSSInfo;
5
- getHasTrigger(itemId: string, triggerType: InteractionTrigger['type']): boolean;
5
+ getHasTrigger(itemId: string, triggerType: InteractionItemTrigger['type']): boolean;
6
6
  sendTrigger(type: 'click' | 'hover-in' | 'hover-out'): void;
7
7
  handleTransitionEnd?: (styleKey: string) => void;
8
8
  handleTransitionStart?: (styleKeys: string[]) => void;
@@ -14,8 +14,8 @@ export interface ItemInteractionCtrl {
14
14
  export interface InteractionsRegistryPort {
15
15
  register(itemId: string, ctrl: ItemInteractionCtrl): void;
16
16
  getStatePropsForItem(itemId: string): StateProps;
17
- getItemAvailableTriggers(itemId: string): Set<InteractionTrigger['type']>;
18
- notifyTrigger(itemId: string, type: TriggerType): void;
17
+ getItemAvailableTriggers(itemId: string): Set<InteractionItemTrigger['type']>;
18
+ notifyItemTrigger(itemId: string, type: TriggerType): void;
19
19
  notifyTransitionEnd(itemId: string): void;
20
20
  }
21
21
 
@@ -1,7 +1,8 @@
1
- import { createContext, FC, PropsWithChildren, useContext, useMemo } from 'react';
1
+ import { createContext, FC, PropsWithChildren, useCallback, useContext, useEffect, useMemo } from 'react';
2
2
  import { InteractionsRegistry } from '../interactions/InteractionsRegistry';
3
3
  import { Article } from '@cntrl-site/sdk';
4
4
  import { useCurrentLayout } from '../common/useCurrentLayout';
5
+ import { ArticleRectContext } from './ArticleRectContext';
5
6
 
6
7
  export const InteractionsContext = createContext<InteractionsRegistry | undefined>(undefined);
7
8
 
@@ -11,10 +12,40 @@ interface Props {
11
12
 
12
13
  export const InteractionsProvider: FC<PropsWithChildren<Props>> = ({ article, children }) => {
13
14
  const { layoutId } = useCurrentLayout();
15
+ const articleRectObserver = useContext(ArticleRectContext);
14
16
  const registry = useMemo(() => {
15
17
  if (!layoutId) return;
16
18
  return new InteractionsRegistry(article, layoutId);
17
19
  }, [layoutId]);
20
+
21
+ useEffect(() => {
22
+ if (!registry || !articleRectObserver) return;
23
+ const handleScroll = () => {
24
+ const scrollY = window.scrollY;
25
+ registry.notifyScroll(scrollY);
26
+ };
27
+ return articleRectObserver.on('scroll', handleScroll);
28
+ }, [registry, articleRectObserver]);
29
+
30
+ const notifyLoad = useCallback(() => {
31
+ if (!registry) return;
32
+ requestAnimationFrame(() => {
33
+ setTimeout(() => {
34
+ registry.notifyLoad();
35
+ }, 0);
36
+ });
37
+ }, [registry]);
38
+
39
+ useEffect(() => {
40
+ if (document.readyState === 'complete') {
41
+ notifyLoad();
42
+ } else {
43
+ window.addEventListener('load', notifyLoad);
44
+ }
45
+
46
+ return () => window.removeEventListener('load', notifyLoad);
47
+ }, [notifyLoad]);
48
+
18
49
  return (
19
50
  <InteractionsContext.Provider value={registry}>
20
51
  {children}