@cntrl-site/sdk-nextjs 0.3.0 → 0.4.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 (53) hide show
  1. package/cntrl-site-sdk-nextjs-0.4.0.tgz +0 -0
  2. package/lib/common/useCurrentLayout.js +42 -0
  3. package/lib/common/useKeyframeValue.js +66 -0
  4. package/lib/components/Article.js +6 -1
  5. package/lib/components/Item.js +21 -15
  6. package/lib/components/Page.js +6 -2
  7. package/lib/components/items/ImageItem.js +7 -4
  8. package/lib/components/items/RectangleItem.js +8 -5
  9. package/lib/components/items/VideoItem.js +7 -4
  10. package/lib/components/items/VimeoEmbed.js +14 -15
  11. package/lib/components/items/YoutubeEmbed.js +14 -15
  12. package/lib/components/items/useEmbedVideoItem.js +16 -0
  13. package/lib/components/items/useFileItem.js +22 -0
  14. package/lib/components/items/useRectangleItem.js +29 -0
  15. package/lib/components/useItemAngle.js +9 -0
  16. package/lib/components/useItemDimensions.js +19 -0
  17. package/lib/components/useItemPosition.js +34 -0
  18. package/lib/provider/ArticleRectContext.js +5 -0
  19. package/lib/provider/CntrlSdkContext.js +9 -3
  20. package/lib/provider/Keyframes.js +12 -0
  21. package/lib/provider/KeyframesContext.js +6 -0
  22. package/lib/utils/Animator/Animator.js +159 -0
  23. package/lib/utils/ArticleRectManager/ArticleRectObserver.js +61 -0
  24. package/lib/utils/ArticleRectManager/useArticleRectObserver.js +18 -0
  25. package/lib/utils/EventEmitter.js +35 -0
  26. package/lib/utils/binSearchInsertAt.js +36 -0
  27. package/package.json +5 -3
  28. package/src/common/useCurrentLayout.ts +47 -0
  29. package/src/common/useKeyframeValue.ts +74 -0
  30. package/src/components/Article.tsx +9 -4
  31. package/src/components/Item.tsx +27 -17
  32. package/src/components/Page.tsx +10 -4
  33. package/src/components/items/ImageItem.tsx +9 -4
  34. package/src/components/items/RectangleItem.tsx +10 -5
  35. package/src/components/items/VideoItem.tsx +9 -4
  36. package/src/components/items/VimeoEmbed.tsx +17 -17
  37. package/src/components/items/YoutubeEmbed.tsx +16 -16
  38. package/src/components/items/useEmbedVideoItem.ts +19 -0
  39. package/src/components/items/useFileItem.ts +29 -0
  40. package/src/components/items/useRectangleItem.ts +40 -0
  41. package/src/components/useItemAngle.ts +12 -0
  42. package/src/components/useItemDimensions.ts +23 -0
  43. package/src/components/useItemPosition.ts +36 -0
  44. package/src/provider/ArticleRectContext.ts +4 -0
  45. package/src/provider/CntrlSdkContext.ts +14 -5
  46. package/src/provider/Keyframes.ts +11 -0
  47. package/src/provider/KeyframesContext.ts +5 -0
  48. package/src/utils/Animator/Animator.ts +206 -0
  49. package/src/utils/ArticleRectManager/ArticleRectObserver.ts +69 -0
  50. package/src/utils/ArticleRectManager/useArticleRectObserver.ts +17 -0
  51. package/src/utils/EventEmitter.ts +41 -0
  52. package/src/utils/binSearchInsertAt.ts +33 -0
  53. package/.idea/inspectionProfiles/Project_Default.xml +0 -15
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Animator = void 0;
4
+ const sdk_1 = require("@cntrl-site/sdk");
5
+ const binSearchInsertAt_1 = require("../binSearchInsertAt");
6
+ const compare = (lhs, rhs) => lhs.position - rhs.position;
7
+ const insertBin = (0, binSearchInsertAt_1.createInsert)(binSearchInsertAt_1.binSearchInsertAt, compare);
8
+ class Animator {
9
+ constructor(keyframes) {
10
+ this.keyframes = keyframes;
11
+ this.keyframesMap = createKeyframesMap();
12
+ this.fillKeyframesMap();
13
+ }
14
+ static pushKeyframeToMap(keyframe, map) {
15
+ insertBin(map[keyframe.type], keyframe);
16
+ }
17
+ getDimensions(values, pos) {
18
+ const keyframes = this.keyframesMap[sdk_1.KeyframeType.Dimensions];
19
+ if (!keyframes || !keyframes.length)
20
+ return values;
21
+ if (keyframes.length === 1) {
22
+ const [keyframe] = keyframes;
23
+ return {
24
+ width: keyframe.value.width,
25
+ height: keyframe.value.height
26
+ };
27
+ }
28
+ const { start, end } = this.getStartEnd(pos, keyframes);
29
+ return {
30
+ width: rangeMap(pos, start.position, end.position, start.value.width, end.value.width, true),
31
+ height: rangeMap(pos, start.position, end.position, start.value.height, end.value.height, true)
32
+ };
33
+ }
34
+ getPositions(values, pos) {
35
+ const keyframes = this.keyframesMap[sdk_1.KeyframeType.Position];
36
+ if (!keyframes || !keyframes.length)
37
+ return values;
38
+ if (keyframes.length === 1) {
39
+ const [keyframe] = keyframes;
40
+ return {
41
+ left: keyframe.value.left,
42
+ top: keyframe.value.top
43
+ };
44
+ }
45
+ const { start, end } = this.getStartEnd(pos, keyframes);
46
+ return {
47
+ left: rangeMap(pos, start.position, end.position, start.value.left, end.value.left, true),
48
+ top: rangeMap(pos, start.position, end.position, start.value.top, end.value.top, true)
49
+ };
50
+ }
51
+ getColor(values, pos) {
52
+ const keyframes = this.keyframesMap[sdk_1.KeyframeType.Color];
53
+ if (!keyframes || !keyframes.length)
54
+ return values;
55
+ if (keyframes.length === 1) {
56
+ const [keyframe] = keyframes;
57
+ return {
58
+ color: keyframe.value.color
59
+ };
60
+ }
61
+ const { start, end } = this.getStartEnd(pos, keyframes);
62
+ return {
63
+ color: this.getRgba(start, end, pos)
64
+ };
65
+ }
66
+ getRadius(values, pos) {
67
+ const keyframes = this.keyframesMap[sdk_1.KeyframeType.BorderRadius];
68
+ if (!keyframes || !keyframes.length)
69
+ return values;
70
+ if (keyframes.length === 1) {
71
+ const [keyframe] = keyframes;
72
+ return {
73
+ radius: keyframe.value.radius
74
+ };
75
+ }
76
+ const { start, end } = this.getStartEnd(pos, keyframes);
77
+ return {
78
+ radius: rangeMap(pos, start.position, end.position, start.value.radius, end.value.radius, true)
79
+ };
80
+ }
81
+ getBorderWidth(values, pos) {
82
+ const keyframes = this.keyframesMap[sdk_1.KeyframeType.BorderWidth];
83
+ if (!keyframes || !keyframes.length)
84
+ return values;
85
+ if (keyframes.length === 1) {
86
+ const [keyframe] = keyframes;
87
+ return {
88
+ borderWidth: keyframe.value.borderWidth
89
+ };
90
+ }
91
+ const { start, end } = this.getStartEnd(pos, keyframes);
92
+ return {
93
+ borderWidth: rangeMap(pos, start.position, end.position, start.value.borderWidth, end.value.borderWidth, true)
94
+ };
95
+ }
96
+ getRotation(values, pos) {
97
+ const keyframes = this.keyframesMap[sdk_1.KeyframeType.Rotation];
98
+ if (!keyframes || !keyframes.length)
99
+ return values;
100
+ if (keyframes.length === 1) {
101
+ const [keyframe] = keyframes;
102
+ return {
103
+ angle: keyframe.value.angle
104
+ };
105
+ }
106
+ const { start, end } = this.getStartEnd(pos, keyframes);
107
+ return {
108
+ angle: rangeMap(pos, start.position, end.position, start.value.angle, end.value.angle, true)
109
+ };
110
+ }
111
+ getStartEnd(position, keyframes) {
112
+ const index = (0, binSearchInsertAt_1.binSearchInsertAt)(keyframes, { position }, compare);
113
+ const end = index === keyframes.length ? index - 1 : index;
114
+ const start = end === 0 ? end : end - 1;
115
+ return { start: keyframes[start], end: keyframes[end] };
116
+ }
117
+ fillKeyframesMap() {
118
+ this.keyframesMap = createKeyframesMap();
119
+ for (const keyframe of this.keyframes) {
120
+ Animator.pushKeyframeToMap(keyframe, this.keyframesMap);
121
+ }
122
+ }
123
+ getRgba(start, end, position) {
124
+ const [startR, startG, startB, startA] = parseRgba(start.value.color);
125
+ const [endR, endG, endB, endA] = parseRgba(end.value.color);
126
+ const r = rangeMap(position, start.position, end.position, startR, endR, true);
127
+ const g = rangeMap(position, start.position, end.position, startG, endG, true);
128
+ const b = rangeMap(position, start.position, end.position, startB, endB, true);
129
+ const a = rangeMap(position, start.position, end.position, startA, endA, true);
130
+ return `rgba(${r},${g},${b},${a})`;
131
+ }
132
+ }
133
+ exports.Animator = Animator;
134
+ const reRgba = /^rgba?\( *(\d+) *, *(\d+) *, *(\d+)(?: *, *(\d+(?:\.\d+)?))? *\)$/i;
135
+ function parseRgba(color) {
136
+ const match = reRgba.exec(color);
137
+ if (!match)
138
+ return [0, 0, 0, 1];
139
+ const [, r, g, b, a] = match;
140
+ return [parseInt(r, 10), parseInt(g, 10), parseInt(b, 10), a ? parseFloat(a) : NaN];
141
+ }
142
+ function createKeyframesMap() {
143
+ return {
144
+ [sdk_1.KeyframeType.Dimensions]: [],
145
+ [sdk_1.KeyframeType.Position]: [],
146
+ [sdk_1.KeyframeType.BorderWidth]: [],
147
+ [sdk_1.KeyframeType.BorderRadius]: [],
148
+ [sdk_1.KeyframeType.Color]: [],
149
+ [sdk_1.KeyframeType.Rotation]: []
150
+ };
151
+ }
152
+ function rangeMap(n, start1, stop1, start2, stop2, withinBounds = false) {
153
+ const mapped = (n - start1) / (stop1 - start1) * (stop2 - start2) + start2;
154
+ if (withinBounds && n < start1)
155
+ return start2;
156
+ if (withinBounds && n > stop1)
157
+ return stop2;
158
+ return mapped;
159
+ }
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ArticleRectObserver = void 0;
4
+ const EventEmitter_1 = require("../EventEmitter");
5
+ class ArticleRectObserver extends EventEmitter_1.EventEmitter {
6
+ constructor() {
7
+ super();
8
+ this.articleWidth = 0;
9
+ this.scrollPos = window.scrollY;
10
+ this.animationFrame = NaN;
11
+ this.handleScroll = (scroll) => {
12
+ this.setScroll(scroll / this.articleWidth);
13
+ };
14
+ this.handleResize = (entries) => {
15
+ const [entry] = entries;
16
+ if (!entry)
17
+ return;
18
+ const element = entry.target;
19
+ if (!(element instanceof HTMLElement))
20
+ return;
21
+ this.notify(element);
22
+ this.emit('resize', undefined);
23
+ };
24
+ this.resizeObserver = new ResizeObserver(this.handleResize);
25
+ }
26
+ get scroll() {
27
+ return this.scrollPos;
28
+ }
29
+ get width() {
30
+ return this.articleWidth;
31
+ }
32
+ setScroll(scroll) {
33
+ this.scrollPos = scroll;
34
+ }
35
+ start(el) {
36
+ this.resizeObserver.observe(el);
37
+ const onScroll = () => {
38
+ this.handleScroll(window.scrollY);
39
+ if (!isNaN(this.animationFrame))
40
+ return;
41
+ this.animationFrame = window.requestAnimationFrame(() => {
42
+ this.animationFrame = NaN;
43
+ this.emit('scroll', undefined);
44
+ });
45
+ };
46
+ window.addEventListener('scroll', onScroll);
47
+ return () => {
48
+ this.resizeObserver.unobserve(el);
49
+ window.removeEventListener('scroll', onScroll);
50
+ if (!isNaN(this.animationFrame)) {
51
+ window.cancelAnimationFrame(this.animationFrame);
52
+ this.animationFrame = NaN;
53
+ }
54
+ };
55
+ }
56
+ notify(el) {
57
+ const elBoundary = el.getBoundingClientRect();
58
+ this.articleWidth = elBoundary.width;
59
+ }
60
+ }
61
+ exports.ArticleRectObserver = ArticleRectObserver;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useArticleRectObserver = void 0;
4
+ const react_1 = require("react");
5
+ const ArticleRectObserver_1 = require("./ArticleRectObserver");
6
+ const useArticleRectObserver = (el) => {
7
+ const [articleRectObserver, setArticleRectObserver] = (0, react_1.useState)(null);
8
+ (0, react_1.useEffect)(() => {
9
+ setArticleRectObserver(new ArticleRectObserver_1.ArticleRectObserver());
10
+ }, []);
11
+ (0, react_1.useEffect)(() => {
12
+ if (!el || !articleRectObserver)
13
+ return;
14
+ return articleRectObserver.start(el);
15
+ }, [el, articleRectObserver]);
16
+ return articleRectObserver;
17
+ };
18
+ exports.useArticleRectObserver = useArticleRectObserver;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EventEmitter = void 0;
4
+ class EventEmitter {
5
+ constructor() {
6
+ this.listeners = {};
7
+ }
8
+ on(event, listener) {
9
+ if (!Array.isArray(this.listeners[event])) {
10
+ this.listeners[event] = [];
11
+ }
12
+ this.listeners[event]?.push(listener);
13
+ return () => {
14
+ this.off(event, listener);
15
+ };
16
+ }
17
+ off(event, listener) {
18
+ const filtered = this.listeners[event]?.filter(l => l !== listener);
19
+ this.listeners[event] = filtered && filtered.length > 0 ? filtered : undefined;
20
+ }
21
+ emit(event, payload) {
22
+ const listeners = this.listeners[event];
23
+ if (!Array.isArray(listeners))
24
+ return;
25
+ for (const listener of listeners) {
26
+ try {
27
+ listener(payload);
28
+ }
29
+ catch (error) {
30
+ console.error(error instanceof Error ? error.stack : error);
31
+ }
32
+ }
33
+ }
34
+ }
35
+ exports.EventEmitter = EventEmitter;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createInsert = exports.binSearchInsertAt = void 0;
4
+ function binSearchInsertAt(list, item, compare) {
5
+ switch (list.length) {
6
+ case 0: return 0;
7
+ case 1: return compare(item, list[0]) < 0 ? 0 : 1;
8
+ }
9
+ let start = 0;
10
+ let end = list.length;
11
+ let pivot;
12
+ let cmpResult;
13
+ while (start < end) {
14
+ pivot = start + Math.floor((end - start) / 2);
15
+ cmpResult = compare(item, list[pivot]);
16
+ if (cmpResult === 0)
17
+ return pivot + 1;
18
+ if (cmpResult < 0) {
19
+ end = pivot;
20
+ }
21
+ if (cmpResult > 0) {
22
+ start = pivot + 1;
23
+ }
24
+ }
25
+ if (start >= list.length)
26
+ return list.length;
27
+ return compare(item, list[start]) < 0 ? start : start + 1;
28
+ }
29
+ exports.binSearchInsertAt = binSearchInsertAt;
30
+ function createInsert(start, compare) {
31
+ return (list, item) => {
32
+ const pos = start(list, item, compare);
33
+ list.splice(pos, 0, item);
34
+ };
35
+ }
36
+ exports.createInsert = createInsert;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cntrl-site/sdk-nextjs",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "SDK for Next.js",
5
5
  "main": "lib/index.js",
6
6
  "types": "src/index.js",
@@ -21,18 +21,20 @@
21
21
  },
22
22
  "homepage": "https://github.com/cntrl-site/sdk-nextjs#readme",
23
23
  "dependencies": {
24
- "@cntrl-site/sdk": "^0.3.0",
24
+ "@cntrl-site/sdk": "^0.4.1",
25
25
  "html-react-parser": "^3.0.1",
26
26
  "styled-jsx": "^5.0.2"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "next": "^12.2.3",
30
30
  "react": "^18.2.0",
31
- "react-dom": "^18.2.0"
31
+ "react-dom": "^18.2.0",
32
+ "lodash.isequal": "^4.5.0"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@tsconfig/node16": "^1.0.3",
35
36
  "@tsconfig/recommended": "^1.0.1",
37
+ "@types/lodash.isequal": "^4.5.6",
36
38
  "@types/jest": "^29.2.5",
37
39
  "@types/node": "^18.11.7",
38
40
  "@types/react": "^18.0.15",
@@ -0,0 +1,47 @@
1
+ import { useCntrlContext } from '../provider/useCntrlContext';
2
+ import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
3
+ import { ArticleRectContext } from '../provider/ArticleRectContext';
4
+
5
+ interface LayoutData {
6
+ layoutId: string;
7
+ start: number;
8
+ end: number;
9
+ }
10
+
11
+ export const useCurrentLayout = () => {
12
+ const { layouts } = useCntrlContext();
13
+ const [initialLayout] = layouts;
14
+ const articleRectObserver = useContext(ArticleRectContext);
15
+ const layoutRanges = useMemo(() => {
16
+ const sorted = layouts.slice().sort((la, lb) => la.startsWith - lb.startsWith);
17
+ return sorted.reduce<LayoutData[]>((acc, layout, i, layouts) => {
18
+ const next = layouts[i + 1];
19
+ return [
20
+ ...acc,
21
+ {
22
+ layoutId: layout.id,
23
+ start: layout.startsWith,
24
+ end: next ? next.startsWith : Number.MAX_SAFE_INTEGER
25
+ }
26
+ ];
27
+ }, []);
28
+ }, [layouts]);
29
+ const getCurrentLayout = useCallback((articleWidth: number) => {
30
+ return layoutRanges.find(l => articleWidth >= l.start && articleWidth < l.end)!.layoutId;
31
+ }, [layoutRanges]);
32
+ const [layoutId, setLayoutId] = useState<string>(getCurrentLayout(isServer() ? 0 : window.innerWidth));
33
+
34
+ useEffect(() => {
35
+ if (!articleRectObserver) return;
36
+ return articleRectObserver.on('resize', () => {
37
+ const articleWidth = articleRectObserver.width;
38
+ setLayoutId(getCurrentLayout(articleWidth));
39
+ });
40
+ }, [articleRectObserver, getCurrentLayout]);
41
+
42
+ return layoutId;
43
+ };
44
+
45
+ function isServer(): boolean {
46
+ return typeof window === 'undefined';
47
+ }
@@ -0,0 +1,74 @@
1
+ import { KeyframeType, TArticleItemAny } from '@cntrl-site/sdk';
2
+ import isEqual from 'lodash.isequal';
3
+ import { DependencyList, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
4
+ import { ArticleRectContext } from '../provider/ArticleRectContext';
5
+ import { useCurrentLayout } from './useCurrentLayout';
6
+ import { KeyframesContext } from '../provider/KeyframesContext';
7
+ import { AnimationData, Animator } from '../utils/Animator/Animator';
8
+
9
+ export type AnimatorGetter<T> = (animator: Animator, scroll: number, value: T) => T;
10
+ type ItemParamGetter<T> = (item: TArticleItemAny, layoutId: string) => T;
11
+ const emptyDeps: DependencyList = [];
12
+
13
+ export const useKeyframeValue = <T>(
14
+ item: TArticleItemAny,
15
+ itemParamsGetter: ItemParamGetter<T>,
16
+ animatorGetter: AnimatorGetter<T>,
17
+ deps: DependencyList = emptyDeps
18
+ ) => {
19
+ const animatorGetterRef = useRef(animatorGetter);
20
+ const itemParamsGetterRef = useRef(itemParamsGetter);
21
+
22
+ animatorGetterRef.current = animatorGetter;
23
+ itemParamsGetterRef.current = itemParamsGetter;
24
+
25
+ const articleRectObserver = useContext(ArticleRectContext);
26
+ const layoutId = useCurrentLayout();
27
+ const keyframesRepo = useContext(KeyframesContext);
28
+ const keyframes = keyframesRepo.getItemKeyframes(item.id);
29
+ const paramValue = useMemo<T>(() => {
30
+ return itemParamsGetterRef.current(item, layoutId);
31
+ }, [item, layoutId, ...deps]);
32
+
33
+ const [adjustedValue, setAdjustedValue] = useState<T>(paramValue);
34
+ const adjustedValueRef = useRef<T>(adjustedValue);
35
+ adjustedValueRef.current = adjustedValue;
36
+
37
+ const animator = useMemo(() => {
38
+ if (!layoutId || !keyframes.length) return;
39
+ const animationData = keyframes
40
+ .filter(k => k.layoutId === layoutId)
41
+ .map<AnimationData<KeyframeType>>(({ position, type, value }) => ({
42
+ position,
43
+ type,
44
+ value
45
+ }));
46
+ return new Animator(animationData);
47
+ }, [keyframes, layoutId]);
48
+
49
+ useEffect(() => {
50
+ setAdjustedValue(paramValue);
51
+ }, [paramValue]);
52
+
53
+ useEffect(() => {
54
+ if (!animator || !articleRectObserver) return;
55
+ return articleRectObserver.on('resize', () => {
56
+ const scroll = articleRectObserver.scroll;
57
+ const newValue = animatorGetterRef.current(animator, scroll, paramValue);
58
+ setAdjustedValue(newValue);
59
+ });
60
+ }, [animator, articleRectObserver]);
61
+
62
+ useEffect(() => {
63
+ if (!animator || !articleRectObserver) return;
64
+ return articleRectObserver.on('scroll', () => {
65
+ const scroll = articleRectObserver.scroll;
66
+ const newValue = animatorGetterRef.current(animator, scroll, paramValue);
67
+ if (!isEqual(newValue, adjustedValueRef.current)) {
68
+ setAdjustedValue(newValue);
69
+ }
70
+ });
71
+ }, [animator, articleRectObserver]);
72
+ return adjustedValue;
73
+ };
74
+
@@ -1,16 +1,21 @@
1
- import { FC } from 'react';
1
+ import { FC, useRef } from 'react';
2
2
  import { TArticle } from '@cntrl-site/sdk';
3
3
  import { Section } from './Section';
4
4
  import { Item } from './Item';
5
+ import { useArticleRectObserver } from '../utils/ArticleRectManager/useArticleRectObserver';
6
+ import { ArticleRectContext } from '../provider/ArticleRectContext';
5
7
 
6
8
  interface Props {
7
9
  article: TArticle;
8
10
  }
9
11
 
10
12
  export const Article: FC<Props> = ({ article }) => {
13
+ const articleRef = useRef<HTMLDivElement | null>(null);
14
+ const articleRectObserver = useArticleRectObserver(articleRef.current);
15
+
11
16
  return (
12
- <>
13
- <div className="article">
17
+ <ArticleRectContext.Provider value={articleRectObserver}>
18
+ <div className="article" ref={articleRef}>
14
19
  {article.sections.map((section, i) => (
15
20
  <Section section={section} key={section.id}>
16
21
  {article.sections[i].items.map(item => (
@@ -25,6 +30,6 @@ export const Article: FC<Props> = ({ article }) => {
25
30
  overflow: hidden;
26
31
  }
27
32
  `}</style>
28
- </>
33
+ </ArticleRectContext.Provider>
29
34
  );
30
35
  };
@@ -1,10 +1,9 @@
1
- import { ComponentType, FC } from 'react';
1
+ import { ComponentType, FC, useEffect, useRef } from 'react';
2
2
  import {
3
3
  getLayoutStyles,
4
4
  ArticleItemType,
5
5
  ArticleItemSizingType as SizingType,
6
- TArticleItemAny,
7
- AnchorSide
6
+ TArticleItemAny
8
7
  } from '@cntrl-site/sdk';
9
8
  import { RectangleItem } from './items/RectangleItem';
10
9
  import { ImageItem } from './items/ImageItem';
@@ -14,6 +13,9 @@ import { VimeoEmbedItem } from './items/VimeoEmbed';
14
13
  import { YoutubeEmbedItem } from './items/YoutubeEmbed';
15
14
  import { CustomItem } from './items/CustomItem';
16
15
  import { useCntrlContext } from '../provider/useCntrlContext';
16
+ import { useItemAngle } from './useItemAngle';
17
+ import { getItemTopStyle, useItemPosition } from './useItemPosition';
18
+ import { useItemDimensions } from './useItemDimensions';
17
19
 
18
20
  export interface ItemProps<I extends TArticleItemAny> {
19
21
  item: I;
@@ -33,7 +35,11 @@ const noop = () => null;
33
35
 
34
36
  export const Item: FC<ItemProps<TArticleItemAny>> = ({ item }) => {
35
37
  const { layouts } = useCntrlContext();
38
+ const angle = useItemAngle(item);
39
+ const { top, left } = useItemPosition(item);
40
+ const { width, height } = useItemDimensions(item);
36
41
  const layoutValues: Record<string, any>[] = [item.area];
42
+ const isInitialRef = useRef(true);
37
43
  if (item.layoutParams) {
38
44
  layoutValues.push(item.layoutParams);
39
45
  }
@@ -41,13 +47,30 @@ export const Item: FC<ItemProps<TArticleItemAny>> = ({ item }) => {
41
47
  const sizingAxis = parseSizing(item.commonParams.sizing);
42
48
  const ItemComponent = itemsMap[item.type] || noop;
43
49
 
50
+ useEffect(() => {
51
+ isInitialRef.current = false;
52
+ }, []);
53
+
54
+ const styles = {
55
+ transform: `rotate(${angle}deg)`,
56
+ left: `${left * 100}vw`,
57
+ width: `${sizingAxis.x === SizingType.Manual ? `${width * 100}vw` : 'max-content'}`,
58
+ height: `${sizingAxis.y === SizingType.Manual ? `${height * 100}vw` : 'unset'}`,
59
+ top
60
+ };
61
+
44
62
  return (
45
- <div className={`item-${item.id}`}>
63
+ <div
64
+ suppressHydrationWarning={true}
65
+ className={`item-${item.id}`}
66
+ style={isInitialRef.current ? {} : styles }
67
+ >
46
68
  <ItemComponent item={item} />
47
69
  <style jsx>{`
48
70
  ${getLayoutStyles(layouts, layoutValues, ([area]) => (`
49
71
  .item-${item.id} {
50
72
  position: absolute;
73
+ z-index: ${area.zIndex};
51
74
  top: ${getItemTopStyle(area.top, area.anchorSide)};
52
75
  left: ${area.left * 100}vw;
53
76
  width: ${sizingAxis.x === SizingType.Manual ? `${area.width * 100}vw` : 'max-content'};
@@ -61,19 +84,6 @@ export const Item: FC<ItemProps<TArticleItemAny>> = ({ item }) => {
61
84
  );
62
85
  };
63
86
 
64
- function getItemTopStyle(top: number, anchorSide: AnchorSide) {
65
- const defaultValue = `${top * 100}vw`;
66
- if (!anchorSide) return defaultValue;
67
- switch (anchorSide) {
68
- case AnchorSide.Top:
69
- return defaultValue;
70
- case AnchorSide.Center:
71
- return `calc(50% + ${top * 100}vw)`;
72
- case AnchorSide.Bottom:
73
- return `calc(100% + ${top * 100}vw)`;
74
- }
75
- }
76
-
77
87
  function parseSizing(sizing: string): Axis {
78
88
  const axisSizing = sizing.split(' ');
79
89
  return {
@@ -1,26 +1,32 @@
1
- import React, { FC } from 'react';
1
+ import React, { FC, useMemo } from 'react';
2
2
  import HTMLReactParser from 'html-react-parser';
3
- import { TArticle, TProject, TMeta } from '@cntrl-site/sdk';
3
+ import { TArticle, TProject, TMeta, TKeyframeAny } from '@cntrl-site/sdk';
4
4
  import { Article } from './Article';
5
+ import { KeyframesContext } from '../provider/KeyframesContext';
5
6
  import { CNTRLHead } from './Head';
6
7
  import { useCntrlContext } from '../provider/useCntrlContext';
7
8
  import { generateTypePresetStyles } from '../utils/generateTypePresetStyles/generateTypePresetStyles';
9
+ import { Keyframes } from '../provider/Keyframes';
8
10
 
9
11
  interface Props {
10
12
  article: TArticle;
11
13
  project: TProject;
12
14
  meta: TMeta;
15
+ keyframes: TKeyframeAny[];
13
16
  }
14
17
 
15
- export const Page: FC<Props> = ({ article, project, meta }) => {
18
+ export const Page: FC<Props> = ({ article, project, meta, keyframes }) => {
16
19
  const afterBodyOpen = HTMLReactParser(project.html.afterBodyOpen);
17
20
  const beforeBodyClose = HTMLReactParser(project.html.beforeBodyClose);
21
+ const keyframesRepo = useMemo(() => new Keyframes(keyframes), [keyframes]);
18
22
  const { typePresets, layouts } = useCntrlContext();
19
23
  return (
20
24
  <>
21
25
  <CNTRLHead project={project} meta={meta} />
22
26
  {afterBodyOpen}
23
- <Article article={article} />
27
+ <KeyframesContext.Provider value={keyframesRepo}>
28
+ <Article article={article} />
29
+ </KeyframesContext.Provider>
24
30
  {beforeBodyClose}
25
31
  {typePresets && typePresets.presets.length > 0 && (
26
32
  <style>{generateTypePresetStyles(typePresets, layouts)}</style>
@@ -3,17 +3,24 @@ import { getLayoutStyles, TImageItem } from '@cntrl-site/sdk';
3
3
  import { ItemProps } from '../Item';
4
4
  import { LinkWrapper } from '../LinkWrapper';
5
5
  import { useCntrlContext } from '../../provider/useCntrlContext';
6
+ import { useFileItem } from './useFileItem';
6
7
 
7
8
  export const ImageItem: FC<ItemProps<TImageItem>> = ({ item }) => {
8
9
  const { layouts } = useCntrlContext();
10
+ const { radius, strokeWidth } = useFileItem(item);
9
11
  return (
10
12
  <LinkWrapper url={item.link?.url}>
11
13
  <>
12
- <div className={`image-wrapper-${item.id}`}>
14
+ <div className={`image-wrapper-${item.id}`}
15
+ style={{
16
+ borderRadius: `${radius * 100}vw`,
17
+ borderWidth: `${strokeWidth * 100}vw`
18
+ }}
19
+ >
13
20
  <img className="image" src={item.commonParams.url} />
14
21
  </div>
15
22
  <style jsx>{`
16
- ${getLayoutStyles(layouts, [item.layoutParams], ([{ strokeColor, radius, strokeWidth, opacity }]) => (`
23
+ ${getLayoutStyles(layouts, [item.layoutParams], ([{ strokeColor, opacity }]) => (`
17
24
  .image-wrapper-${item.id} {
18
25
  position: absolute;
19
26
  overflow: hidden;
@@ -22,9 +29,7 @@ export const ImageItem: FC<ItemProps<TImageItem>> = ({ item }) => {
22
29
  border-style: solid;
23
30
  box-sizing: border-box;
24
31
  border-color: ${strokeColor};
25
- border-radius: ${radius * 100}vw;
26
32
  opacity: ${opacity};
27
- border-width: ${strokeWidth * 100}vw;
28
33
  }`
29
34
  ))
30
35
  }