@doyourjob/gravity-ui-page-constructor 5.31.254 → 5.31.256

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.
@@ -30,19 +30,44 @@ unpredictable css rules order in build */
30
30
  gap: 16px;
31
31
  }
32
32
  .pc-logo-rotator-block__item {
33
+ position: relative;
33
34
  display: block;
34
35
  height: 100px;
35
- transition: opacity 0.5s ease 0.1s;
36
- opacity: 1;
37
- }
38
- .pc-logo-rotator-block__item_hidden {
39
- opacity: 0;
36
+ overflow: hidden;
40
37
  }
41
38
  @media (max-width: 769px) {
42
39
  .pc-logo-rotator-block__item {
43
40
  height: 80px;
44
41
  }
45
42
  }
43
+ .pc-logo-rotator-block__item-link, .pc-logo-rotator-block__row-item-link {
44
+ display: block;
45
+ height: 100%;
46
+ }
47
+ .pc-logo-rotator-block__logo-layer {
48
+ display: block;
49
+ width: 100%;
50
+ height: 100%;
51
+ }
52
+ .pc-logo-rotator-block__item_swapping .pc-logo-rotator-block__logo-layer, .pc-logo-rotator-block__row-item_swapping .pc-logo-rotator-block__logo-layer {
53
+ position: absolute;
54
+ inset: 0;
55
+ }
56
+ .pc-logo-rotator-block__logo-layer_from {
57
+ pointer-events: none;
58
+ }
59
+ .pc-logo-rotator-block__logo-layer_fade-from {
60
+ animation: logo-rotator-fade-out 0.5s ease 0.1s both;
61
+ }
62
+ .pc-logo-rotator-block__logo-layer_fade-to {
63
+ animation: logo-rotator-fade-in 0.5s ease 0.6s both;
64
+ }
65
+ .pc-logo-rotator-block__logo-layer_morph-from {
66
+ animation: logo-rotator-morph-out 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
67
+ }
68
+ .pc-logo-rotator-block__logo-layer_morph-to {
69
+ animation: logo-rotator-morph-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
70
+ }
46
71
  .pc-logo-rotator-block__image {
47
72
  display: block;
48
73
  width: 100%;
@@ -57,13 +82,57 @@ unpredictable css rules order in build */
57
82
  gap: 46px 16px;
58
83
  }
59
84
  .pc-logo-rotator-block__row-item {
85
+ position: relative;
60
86
  display: block;
61
87
  height: 36px;
62
88
  min-width: 160px;
63
89
  flex: 1;
64
- transition: opacity 0.5s ease 0.1s;
65
- opacity: 1;
90
+ overflow: hidden;
66
91
  }
67
- .pc-logo-rotator-block__row-item_hidden {
68
- opacity: 0;
92
+
93
+ @keyframes logo-rotator-fade-out {
94
+ from {
95
+ opacity: 1;
96
+ }
97
+ to {
98
+ opacity: 0;
99
+ }
100
+ }
101
+ @keyframes logo-rotator-fade-in {
102
+ from {
103
+ opacity: 0;
104
+ }
105
+ to {
106
+ opacity: 1;
107
+ }
108
+ }
109
+ @keyframes logo-rotator-morph-out {
110
+ from {
111
+ clip-path: inset(0 0 0 0);
112
+ transform: translateX(0) scaleX(1);
113
+ opacity: 1;
114
+ }
115
+ to {
116
+ clip-path: inset(0 100% 0 0);
117
+ transform: translateX(-8%) scaleX(0.9);
118
+ opacity: 0;
119
+ }
120
+ }
121
+ @keyframes logo-rotator-morph-in {
122
+ from {
123
+ clip-path: inset(0 0 0 100%);
124
+ transform: translateX(8%) scaleX(0.9);
125
+ opacity: 0;
126
+ }
127
+ to {
128
+ clip-path: inset(0 0 0 0);
129
+ transform: translateX(0) scaleX(1);
130
+ opacity: 1;
131
+ }
132
+ }
133
+ @media (prefers-reduced-motion: reduce) {
134
+ .pc-logo-rotator-block__logo-layer_fade-from, .pc-logo-rotator-block__logo-layer_fade-to, .pc-logo-rotator-block__logo-layer_morph-from, .pc-logo-rotator-block__logo-layer_morph-to {
135
+ animation-duration: 1ms;
136
+ animation-delay: 0ms;
137
+ }
69
138
  }
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useMemo, useRef, useState } from 'react';
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { Link } from '@gravity-ui/uikit';
3
3
  import { ImageBase, Title } from '../../components';
4
4
  import AnimateBlock from '../../components/AnimateBlock/AnimateBlock';
@@ -7,28 +7,12 @@ import { Grid, Row } from '../../grid';
7
7
  import useWindowBreakpoint from '../../hooks/useWindowBreakpoint';
8
8
  import { block } from '../../utils';
9
9
  import Item from './Item';
10
+ import { DEFAULT_SWAP_ANIMATION, SWAP_ANIMATION_DURATIONS, getInitialSlots, getLayerModifiers, } from './utils';
10
11
  import './LogoRotator.css';
11
12
  const b = block('logo-rotator-block');
12
13
  const DEFAULT_MIN_ROTATE_COUNT = 2;
13
14
  const DEFAULT_MAX_ROTATE_COUNT = 4;
14
- const getInitialSlots = (items, count) => {
15
- var _a;
16
- const staticIdxList = items.map((item, i) => (item.isStatic ? i : -1)).filter((i) => i !== -1);
17
- const rotatableIdxList = items
18
- .map((item, i) => (item.isStatic ? -1 : i))
19
- .filter((i) => i !== -1);
20
- const initial = [];
21
- let rotatablePointer = 0;
22
- for (let slot = 0; slot < count; slot++) {
23
- if (slot < staticIdxList.length) {
24
- initial.push(staticIdxList[slot]);
25
- }
26
- else {
27
- initial.push((_a = rotatableIdxList[rotatablePointer++]) !== null && _a !== void 0 ? _a : 0);
28
- }
29
- }
30
- return initial;
31
- };
15
+ const emptyTransitions = (count) => Array(count).fill(undefined);
32
16
  const pickRandomSlots = (slotIndices, count) => {
33
17
  const shuffled = [...slotIndices];
34
18
  for (let i = shuffled.length - 1; i > 0; i--) {
@@ -38,34 +22,47 @@ const pickRandomSlots = (slotIndices, count) => {
38
22
  return shuffled.slice(0, count);
39
23
  };
40
24
  export const LogoRotatorBlock = (props) => {
41
- const { animated, title, theme, items, count, desktopCount, minRotateCount = DEFAULT_MIN_ROTATE_COUNT, maxRotateCount = DEFAULT_MAX_ROTATE_COUNT, colSizes, rowMode, } = props;
25
+ const { animated, title, theme, items, countMobile, countDesktop, minRotateCount = DEFAULT_MIN_ROTATE_COUNT, maxRotateCount = DEFAULT_MAX_ROTATE_COUNT, swapAnimation = DEFAULT_SWAP_ANIMATION, colSizes, rowMode, } = props;
42
26
  const breakpoint = useWindowBreakpoint();
43
- const activeCount = desktopCount !== undefined && breakpoint >= BREAKPOINTS.md ? desktopCount : count;
27
+ const activeCount = countDesktop !== undefined && breakpoint >= BREAKPOINTS.md ? countDesktop : countMobile;
44
28
  // Индексы логотипов, которые участвуют в ротации (не статичные)
45
29
  const rotatableIndices = useMemo(() => items.map((item, i) => (item.isStatic ? -1 : i)).filter((i) => i !== -1), [items]);
46
30
  // Инициализация слотов: статичные вставляются в начало, остальные по порядку
47
31
  const [slots, setSlots] = useState(() => getInitialSlots(items, activeCount));
48
- const [hidden, setHidden] = useState(() => Array(activeCount).fill(false));
32
+ const [transitions, setTransitions] = useState(() => emptyTransitions(activeCount));
49
33
  const nextIndexRef = useRef(activeCount - 1);
50
- const isHoveredRef = useRef(false);
34
+ const hoveredSlotIndicesRef = useRef(new Set());
51
35
  // Держим актуальные slots в ref, чтобы не пересоздавать интервал при каждом изменении
52
36
  const slotsRef = useRef(slots);
53
37
  useEffect(() => {
54
38
  slotsRef.current = slots;
55
39
  }, [slots]);
56
40
  useEffect(() => {
57
- setSlots(getInitialSlots(items, activeCount));
58
- setHidden(Array(activeCount).fill(false));
41
+ const initialSlots = getInitialSlots(items, activeCount);
42
+ slotsRef.current = initialSlots;
43
+ setSlots(initialSlots);
44
+ setTransitions(emptyTransitions(activeCount));
59
45
  nextIndexRef.current = activeCount - 1;
46
+ hoveredSlotIndicesRef.current.clear();
60
47
  }, [activeCount, items]);
48
+ const handleSlotMouseEnter = useCallback((slotIndex) => {
49
+ hoveredSlotIndicesRef.current.add(slotIndex);
50
+ }, []);
51
+ const handleSlotMouseLeave = useCallback((slotIndex) => {
52
+ hoveredSlotIndicesRef.current.delete(slotIndex);
53
+ }, []);
61
54
  useEffect(() => {
62
55
  let timeout = null;
63
56
  const interval = setInterval(() => {
64
- if (isHoveredRef.current)
65
- return;
66
- // Выбираем только не-статичные слоты для замены
67
- const rotatableSlotIndices = slotsRef.current
68
- .map((itemIdx, slotIdx) => { var _a; return (((_a = items[itemIdx]) === null || _a === void 0 ? void 0 : _a.isStatic) ? -1 : slotIdx); })
57
+ const currentSlots = slotsRef.current;
58
+ // Выбираем только не-статичные слоты, на которые не наведен указатель
59
+ const rotatableSlotIndices = currentSlots
60
+ .map((itemIdx, slotIdx) => {
61
+ var _a;
62
+ return ((_a = items[itemIdx]) === null || _a === void 0 ? void 0 : _a.isStatic) || hoveredSlotIndicesRef.current.has(slotIdx)
63
+ ? -1
64
+ : slotIdx;
65
+ })
69
66
  .filter((i) => i !== -1);
70
67
  if (rotatableSlotIndices.length === 0)
71
68
  return;
@@ -73,35 +70,56 @@ export const LogoRotatorBlock = (props) => {
73
70
  const rotateMax = Math.max(minRotateCount, maxRotateCount);
74
71
  const rotateCount = rotateMin + Math.floor(Math.random() * (rotateMax - rotateMin + 1));
75
72
  const slotIndices = pickRandomSlots(rotatableSlotIndices, Math.min(rotateCount, rotatableSlotIndices.length));
76
- setHidden((prev) => {
73
+ let available = rotatableIndices.filter((i) => !currentSlots.includes(i));
74
+ const blockedTargetSrcs = new Set();
75
+ const nextTransitions = [];
76
+ slotIndices.forEach((slotIndex) => {
77
+ var _a, _b;
78
+ if (available.length === 0)
79
+ return;
80
+ const currentItemIndex = currentSlots[slotIndex];
81
+ const currentSrc = (_a = items[currentItemIndex]) === null || _a === void 0 ? void 0 : _a.src;
82
+ const slotAvailable = available.filter((i) => {
83
+ var _a;
84
+ const src = (_a = items[i]) === null || _a === void 0 ? void 0 : _a.src;
85
+ return src !== currentSrc && !blockedTargetSrcs.has(src);
86
+ });
87
+ if (slotAvailable.length === 0)
88
+ return;
89
+ const newValue = slotAvailable[nextIndexRef.current % slotAvailable.length];
90
+ nextIndexRef.current++;
91
+ available = available.filter((i) => i !== newValue);
92
+ const newSrc = (_b = items[newValue]) === null || _b === void 0 ? void 0 : _b.src;
93
+ if (newSrc) {
94
+ blockedTargetSrcs.add(newSrc);
95
+ }
96
+ nextTransitions.push({ slotIndex, from: currentItemIndex, to: newValue });
97
+ });
98
+ if (nextTransitions.length === 0)
99
+ return;
100
+ setTransitions((prev) => {
77
101
  const next = [...prev];
78
- slotIndices.forEach((slotIndex) => {
79
- next[slotIndex] = true;
102
+ nextTransitions.forEach(({ slotIndex, from, to }) => {
103
+ next[slotIndex] = { from, to };
80
104
  });
81
105
  return next;
82
106
  });
83
107
  timeout = setTimeout(() => {
84
108
  setSlots((prevSlots) => {
85
109
  const newSlots = [...prevSlots];
86
- let available = rotatableIndices.filter((i) => !newSlots.includes(i));
87
- slotIndices.forEach((slotIndex) => {
88
- if (available.length === 0)
89
- return;
90
- const newValue = available[nextIndexRef.current % available.length];
91
- nextIndexRef.current++;
92
- newSlots[slotIndex] = newValue;
93
- available = available.filter((i) => i !== newValue);
110
+ nextTransitions.forEach(({ slotIndex, to }) => {
111
+ newSlots[slotIndex] = to;
94
112
  });
95
113
  return newSlots;
96
114
  });
97
- setHidden((prev) => {
115
+ setTransitions((prev) => {
98
116
  const next = [...prev];
99
- slotIndices.forEach((slotIndex) => {
100
- next[slotIndex] = false;
117
+ nextTransitions.forEach(({ slotIndex }) => {
118
+ next[slotIndex] = undefined;
101
119
  });
102
120
  return next;
103
121
  });
104
- }, 500);
122
+ }, SWAP_ANIMATION_DURATIONS[swapAnimation]);
105
123
  }, 2000);
106
124
  return () => {
107
125
  clearInterval(interval);
@@ -109,29 +127,49 @@ export const LogoRotatorBlock = (props) => {
109
127
  clearTimeout(timeout);
110
128
  }
111
129
  };
112
- }, [activeCount, items, maxRotateCount, minRotateCount, rotatableIndices]);
113
- const renderItems = useMemo(() => slots.map((slot, index) => (React.createElement(Item, { key: index, colSizes: colSizes, url: items[slot].url, src: items[slot].src, hidden: hidden[index] }))), [colSizes, hidden, items, slots]);
130
+ }, [activeCount, items, maxRotateCount, minRotateCount, rotatableIndices, swapAnimation]);
131
+ const renderItems = useMemo(() => slots.map((slot, index) => {
132
+ var _a;
133
+ const transition = transitions[index];
134
+ const item = items[(_a = transition === null || transition === void 0 ? void 0 : transition.to) !== null && _a !== void 0 ? _a : slot];
135
+ const previousItem = transition ? items[transition.from] : undefined;
136
+ return (React.createElement(Item, { key: index, colSizes: colSizes, url: item.url, src: item.src, previousUrl: previousItem === null || previousItem === void 0 ? void 0 : previousItem.url, previousSrc: previousItem === null || previousItem === void 0 ? void 0 : previousItem.src, swapAnimation: swapAnimation, onMouseEnter: () => handleSlotMouseEnter(index), onMouseLeave: () => handleSlotMouseLeave(index) }));
137
+ }), [
138
+ colSizes,
139
+ handleSlotMouseEnter,
140
+ handleSlotMouseLeave,
141
+ items,
142
+ slots,
143
+ swapAnimation,
144
+ transitions,
145
+ ]);
114
146
  const titleProps = !title || typeof title === 'string'
115
147
  ? { text: title, textSize: 'l' }
116
148
  : title;
117
149
  const hasTitle = Boolean(title);
118
150
  return (React.createElement(AnimateBlock, { className: b({ theme }), animate: animated },
119
- React.createElement("div", { className: b('root'), onMouseEnter: () => {
120
- isHoveredRef.current = true;
121
- }, onMouseLeave: () => {
122
- isHoveredRef.current = false;
123
- } },
151
+ React.createElement("div", { className: b('root') },
124
152
  hasTitle && (React.createElement(Title, { title: titleProps, className: b('title'), colSizes: { all: 12 } })),
125
153
  rowMode ? (React.createElement("div", { className: b('row-items') }, slots.map((slot, index) => {
126
- const item = items[slot];
127
- if (!item)
154
+ var _a;
155
+ const transition = transitions[index];
156
+ const item = items[(_a = transition === null || transition === void 0 ? void 0 : transition.to) !== null && _a !== void 0 ? _a : slot];
157
+ const previousItem = transition ? items[transition.from] : undefined;
158
+ if (item === undefined)
128
159
  return null;
129
- if (item.url) {
130
- return (React.createElement(Link, { key: index, href: item.url, className: b('row-item', { hidden: hidden[index] }) },
131
- React.createElement(ImageBase, { src: item.src, className: b('image'), alt: "", "aria-hidden": "true" })));
132
- }
133
- return (React.createElement("div", { key: index, className: b('row-item', { hidden: hidden[index] }) },
134
- React.createElement(ImageBase, { src: item.src, className: b('image'), alt: "", "aria-hidden": "true" })));
160
+ const renderRowLayer = (layerItem, layer) => {
161
+ const layerClassName = b('logo-layer', getLayerModifiers(layer, swapAnimation));
162
+ if (layerItem.url) {
163
+ return (React.createElement(Link, { href: layerItem.url, className: `${b('row-item-link')} ${layerClassName}` },
164
+ React.createElement(ImageBase, { src: layerItem.src, className: b('image'), alt: "", "aria-hidden": "true" })));
165
+ }
166
+ return (React.createElement("div", { className: layerClassName },
167
+ React.createElement(ImageBase, { src: layerItem.src, className: b('image'), alt: "", "aria-hidden": "true" })));
168
+ };
169
+ const content = (React.createElement(React.Fragment, null,
170
+ previousItem && renderRowLayer(previousItem, 'from'),
171
+ renderRowLayer(item, previousItem ? 'to' : 'current')));
172
+ return (React.createElement("div", { key: index, className: b('row-item', { swapping: Boolean(previousItem) }), onMouseEnter: () => handleSlotMouseEnter(index), onMouseLeave: () => handleSlotMouseLeave(index) }, content));
135
173
  }))) : (React.createElement(Grid, { className: b('items') },
136
174
  React.createElement(Row, { className: b('row') }, renderItems))))));
137
175
  };
@@ -182,10 +182,10 @@ export declare const LogoRotatorBlock: {
182
182
  };
183
183
  };
184
184
  };
185
- count: {
185
+ countMobile: {
186
186
  type: string;
187
187
  };
188
- desktopCount: {
188
+ countDesktop: {
189
189
  type: string;
190
190
  };
191
191
  minRotateCount: {
@@ -194,6 +194,10 @@ export declare const LogoRotatorBlock: {
194
194
  maxRotateCount: {
195
195
  type: string;
196
196
  };
197
+ swapAnimation: {
198
+ type: string;
199
+ enum: string[];
200
+ };
197
201
  colSizes: {
198
202
  type: string;
199
203
  additionalProperties: {
@@ -2,7 +2,7 @@ import { AnimatableProps, BaseProps, ThemeProps, TitleProps, containerSizesObjec
2
2
  export const LogoRotatorBlock = {
3
3
  'logo-rotator-block': {
4
4
  additionalProperties: false,
5
- required: ['items', 'count'],
5
+ required: ['items', 'countMobile'],
6
6
  properties: Object.assign(Object.assign(Object.assign({}, BaseProps), AnimatableProps), { title: {
7
7
  oneOf: [
8
8
  {
@@ -24,14 +24,17 @@ export const LogoRotatorBlock = {
24
24
  isStatic: { type: 'boolean' },
25
25
  },
26
26
  },
27
- }, count: {
27
+ }, countMobile: {
28
28
  type: 'number',
29
- }, desktopCount: {
29
+ }, countDesktop: {
30
30
  type: 'number',
31
31
  }, minRotateCount: {
32
32
  type: 'number',
33
33
  }, maxRotateCount: {
34
34
  type: 'number',
35
+ }, swapAnimation: {
36
+ type: 'string',
37
+ enum: ['fade', 'morph'],
35
38
  }, colSizes: {
36
39
  type: 'object',
37
40
  additionalProperties: containerSizesObject,
@@ -0,0 +1,7 @@
1
+ import { LogoRotatorBlockProps } from '../../models';
2
+ export type LogoRotatorLayer = 'current' | 'from' | 'to';
3
+ export type LogoRotatorSwapAnimation = NonNullable<LogoRotatorBlockProps['swapAnimation']>;
4
+ export declare const DEFAULT_SWAP_ANIMATION: LogoRotatorSwapAnimation;
5
+ export declare const SWAP_ANIMATION_DURATIONS: Record<LogoRotatorSwapAnimation, number>;
6
+ export declare const getLayerModifiers: (layer: LogoRotatorLayer, swapAnimation: LogoRotatorSwapAnimation) => Record<string, boolean>;
7
+ export declare const getInitialSlots: (items: LogoRotatorBlockProps['items'], count: number) => number[];
@@ -0,0 +1,31 @@
1
+ export const DEFAULT_SWAP_ANIMATION = 'fade';
2
+ export const SWAP_ANIMATION_DURATIONS = {
3
+ fade: 1100,
4
+ morph: 500,
5
+ };
6
+ export const getLayerModifiers = (layer, swapAnimation) => {
7
+ const animationModifier = layer === 'current' ? undefined : `${swapAnimation}-${layer}`;
8
+ const modifiers = { [layer]: true };
9
+ if (animationModifier) {
10
+ modifiers[animationModifier] = true;
11
+ }
12
+ return modifiers;
13
+ };
14
+ export const getInitialSlots = (items, count) => {
15
+ var _a;
16
+ const staticIdxList = items.map((item, i) => (item.isStatic ? i : -1)).filter((i) => i !== -1);
17
+ const rotatableIdxList = items
18
+ .map((item, i) => (item.isStatic ? -1 : i))
19
+ .filter((i) => i !== -1);
20
+ const initial = [];
21
+ let rotatablePointer = 0;
22
+ for (let slot = 0; slot < count; slot++) {
23
+ if (slot < staticIdxList.length) {
24
+ initial.push(staticIdxList[slot]);
25
+ }
26
+ else {
27
+ initial.push((_a = rotatableIdxList[rotatablePointer++]) !== null && _a !== void 0 ? _a : 0);
28
+ }
29
+ }
30
+ return initial;
31
+ };
@@ -302,10 +302,11 @@ export interface LogoRotatorBlockProps extends Animatable {
302
302
  src: string;
303
303
  isStatic?: boolean;
304
304
  }[];
305
- count: number;
306
- desktopCount?: number;
305
+ countMobile: number;
306
+ countDesktop?: number;
307
307
  minRotateCount?: number;
308
308
  maxRotateCount?: number;
309
+ swapAnimation?: 'fade' | 'morph';
309
310
  colSizes?: Partial<Record<GridColumnSize, number>>;
310
311
  theme?: TextTheme;
311
312
  rowMode?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doyourjob/gravity-ui-page-constructor",
3
- "version": "5.31.254",
3
+ "version": "5.31.256",
4
4
  "description": "Gravity UI Page Constructor",
5
5
  "license": "MIT",
6
6
  "repository": {