@akinon/pz-theme 2.0.82-beta.0 → 2.0.82
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.
- package/CHANGELOG.md +8 -4
- package/package.json +4 -4
- package/src/blocks/__tests__/link-block.test.tsx +87 -0
- package/src/blocks/__tests__/text-block.test.tsx +88 -0
- package/src/blocks/__tests__/video-block.test.tsx +207 -2
- package/src/blocks/item-template-utils.ts +51 -5
- package/src/blocks/link-block.tsx +43 -0
- package/src/blocks/slider-block.tsx +92 -4
- package/src/blocks/text-block.tsx +42 -4
- package/src/blocks/video-block.tsx +208 -80
- package/src/sections/__tests__/posts-slider-arrows.test.tsx +470 -0
- package/src/sections/__tests__/posts-slider-edges.test.tsx +394 -0
- package/src/sections/__tests__/posts-slider-quick-view-no-fetch.test.tsx +205 -0
- package/src/sections/__tests__/posts-slider-quick-view.test.tsx +660 -0
- package/src/sections/posts-slider-section.tsx +482 -115
- package/src/sections/slider-quick-view.tsx +375 -0
- package/src/utils/__tests__/line-breaks.test.ts +215 -0
- package/src/utils/index.ts +12 -1
- package/src/utils/iterator-utils.ts +46 -0
- package/src/utils/line-breaks.ts +142 -0
- package/src/utils/slider-arrows.tsx +252 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DESKTOP_MIN_WIDTH,
|
|
3
|
+
TABLET_MIN_WIDTH,
|
|
4
|
+
type BreakpointName
|
|
5
|
+
} from './breakpoints';
|
|
6
|
+
import type { Block } from '../theme-block';
|
|
7
|
+
import type { Section } from '../theme-section';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Line breaks that exist at one width and not another.
|
|
11
|
+
*
|
|
12
|
+
* A designed heading is two lines wide and one line narrow — "Şehrin ritmiyle
|
|
13
|
+
* buluşan / güneşli yaşam stili" on the phone, one run of text on the desktop —
|
|
14
|
+
* and until now a theme could express neither half of that. The text of a block
|
|
15
|
+
* is ONE string: `block.value` is keyed by LOCALE, not by breakpoint, and
|
|
16
|
+
* `richtext` is explicitly excluded from `getResponsiveValue` in the editor's
|
|
17
|
+
* panel. So there is no per-breakpoint content to author, and there never will
|
|
18
|
+
* be without a second copy of every sentence to keep translated.
|
|
19
|
+
*
|
|
20
|
+
* What is per-breakpoint is CSS. A `<br>` is an element, `display: none` on it
|
|
21
|
+
* removes the break it produces, and a media query decides where. So the break
|
|
22
|
+
* point is authored once, in the text, where the designer wants it, and its
|
|
23
|
+
* SCOPE rides along in an attribute:
|
|
24
|
+
*
|
|
25
|
+
* Şehrin ritmiyle buluşan<br data-pz-break="mobile tablet"> güneşli yaşam stili
|
|
26
|
+
*
|
|
27
|
+
* — two lines at 1023px and under, one line on the desktop. A plain `<br>` with
|
|
28
|
+
* no attribute is untouched by any of this and still breaks everywhere, which is
|
|
29
|
+
* what every already-published composition contains.
|
|
30
|
+
*
|
|
31
|
+
* ## Why the bands are disjoint here
|
|
32
|
+
*
|
|
33
|
+
* Everywhere else in the theme, `mobile` means "narrower than desktop" and
|
|
34
|
+
* `tablet` is a narrower override layered on top of it — the fall-through chain
|
|
35
|
+
* documented in breakpoints.ts. A break's scope is not a value on that chain: it
|
|
36
|
+
* is a LIST of the widths where the break exists, written out by the author, so
|
|
37
|
+
* "mobile" here means the phone and nothing else. `mobile tablet` is how the
|
|
38
|
+
* author says both, and the toolbar offers exactly that as one choice, so the
|
|
39
|
+
* common intent costs no more clicks than a hidden fallback rule would have.
|
|
40
|
+
*
|
|
41
|
+
* The alternative — inheriting the fall-through — cannot be undone: a break
|
|
42
|
+
* scoped to the phone would appear on tablets with no way to say otherwise,
|
|
43
|
+
* because there is no such thing as an authored-empty entry in an attribute
|
|
44
|
+
* list. Explicit is the only direction that stays reachable.
|
|
45
|
+
*/
|
|
46
|
+
export const LINE_BREAK_ATTRIBUTE = 'data-pz-break';
|
|
47
|
+
|
|
48
|
+
/** The scopes an author can name, and the width band each one means. */
|
|
49
|
+
const BAND_QUERIES: Record<BreakpointName, string> = {
|
|
50
|
+
desktop: `@media (min-width: ${DESKTOP_MIN_WIDTH}px)`,
|
|
51
|
+
tablet: `@media (min-width: ${TABLET_MIN_WIDTH}px) and (max-width: ${
|
|
52
|
+
DESKTOP_MIN_WIDTH - 1
|
|
53
|
+
}px)`,
|
|
54
|
+
mobile: `@media (max-width: ${TABLET_MIN_WIDTH - 1}px)`
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Widest first, so the emitted sheet reads the way the editor authors. */
|
|
58
|
+
const BANDS: BreakpointName[] = ['desktop', 'tablet', 'mobile'];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* `display: none` on a `<br>` is what actually removes the break, and it has to
|
|
62
|
+
* beat whatever the brand's own stylesheet says about `br` — a global
|
|
63
|
+
* `.prose br { display: block }` outranks a bare `br[data-pz-break]` on
|
|
64
|
+
* specificity, and a break that refuses to disappear is the bug this feature
|
|
65
|
+
* exists to fix. Same reason the part rules carry it.
|
|
66
|
+
*/
|
|
67
|
+
const HIDDEN = `display: none !important;`;
|
|
68
|
+
const SHOWN = `display: inline !important;`;
|
|
69
|
+
|
|
70
|
+
const scopeSelector = (band: BreakpointName): string =>
|
|
71
|
+
`br[${LINE_BREAK_ATTRIBUTE}~="${band}"]`;
|
|
72
|
+
|
|
73
|
+
const ANY_SCOPED = `br[${LINE_BREAK_ATTRIBUTE}]`;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The rules that make a scoped break appear only where it was scoped to.
|
|
77
|
+
*
|
|
78
|
+
* `targetBreakpoint` is the editor canvas: it renders a phone layout inside a
|
|
79
|
+
* desktop-width frame, so real media queries would answer for the frame and the
|
|
80
|
+
* designer would see the desktop's line count while authoring the phone's. The
|
|
81
|
+
* canvas gets the one band it is drawing, resolved, exactly as
|
|
82
|
+
* `generateElementCSS` does for every other property.
|
|
83
|
+
*/
|
|
84
|
+
export const buildLineBreakCss = (targetBreakpoint?: string): string => {
|
|
85
|
+
if (targetBreakpoint) {
|
|
86
|
+
const band = BANDS.find((entry) => entry === targetBreakpoint);
|
|
87
|
+
// An unknown breakpoint name must not silently hide every scoped break:
|
|
88
|
+
// falling through to the real queries is the honest answer, and it is what
|
|
89
|
+
// a non-designer render does anyway.
|
|
90
|
+
if (band) {
|
|
91
|
+
return `${ANY_SCOPED}{${HIDDEN}}\n${scopeSelector(band)}{${SHOWN}}\n`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
`${ANY_SCOPED}{${HIDDEN}}\n` +
|
|
97
|
+
BANDS.map(
|
|
98
|
+
(band) =>
|
|
99
|
+
`${BAND_QUERIES[band]}{${scopeSelector(band)}{${SHOWN}}}\n`
|
|
100
|
+
).join('')
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const stringsIn = (value: unknown): string[] => {
|
|
105
|
+
if (typeof value === 'string') return [value];
|
|
106
|
+
if (Array.isArray(value)) return value.flatMap(stringsIn);
|
|
107
|
+
if (value && typeof value === 'object') {
|
|
108
|
+
return Object.values(value as Record<string, unknown>).flatMap(stringsIn);
|
|
109
|
+
}
|
|
110
|
+
return [];
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const blockUsesScopedLineBreaks = (block: Block): boolean => {
|
|
114
|
+
// `value` holds the rich text (a locale map, or a bare string on a
|
|
115
|
+
// single-locale composition); `properties` holds the authored HTML of the
|
|
116
|
+
// blocks that keep their copy there instead.
|
|
117
|
+
const carriers = [block.value, block.properties];
|
|
118
|
+
if (
|
|
119
|
+
carriers.some((carrier) =>
|
|
120
|
+
stringsIn(carrier).some((text) => text.includes(LINE_BREAK_ATTRIBUTE))
|
|
121
|
+
)
|
|
122
|
+
) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return (block.blocks ?? []).some(blockUsesScopedLineBreaks);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Whether anything in the composition actually carries a scoped break.
|
|
131
|
+
*
|
|
132
|
+
* The sheet is four short rules, but "byte for byte the stylesheet it produced
|
|
133
|
+
* before" is a property this generator is expected to keep for compositions that
|
|
134
|
+
* do not use a feature — see the tablet breakpoint's note in breakpoints.ts. A
|
|
135
|
+
* composition with no scoped break emits nothing new.
|
|
136
|
+
*/
|
|
137
|
+
export const compositionUsesScopedLineBreaks = (
|
|
138
|
+
sections: Section[]
|
|
139
|
+
): boolean =>
|
|
140
|
+
sections.some((section) =>
|
|
141
|
+
(section.blocks ?? []).some(blockUsesScopedLineBreaks)
|
|
142
|
+
);
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The arrows a slider SECTION draws from its own blocks.
|
|
3
|
+
*
|
|
4
|
+
* The `slider` BLOCK has drawn custom arrows from two child icon blocks since
|
|
5
|
+
* it was written — that is why a Featured Brand Collection lists "Left Arrow
|
|
6
|
+
* Icon" and "Right Arrow Icon" as layers a designer can select. The slider
|
|
7
|
+
* SECTIONS had nothing of the kind: `arrows` was handed straight to
|
|
8
|
+
* react-multi-carousel and the library drew its own buttons, so the only thing
|
|
9
|
+
* a brand could do with them was switch them off.
|
|
10
|
+
*
|
|
11
|
+
* What this adds over the block's version is reach. The block reads a
|
|
12
|
+
* hand-picked list off its arrow icons — size, colour, background, radius, the
|
|
13
|
+
* four offsets — and an authored `box-shadow` or `border` reached the theme
|
|
14
|
+
* JSON and stopped there. Here the whole style map goes through
|
|
15
|
+
* `getCSSStyles`, the same call every other block renderer makes, and the
|
|
16
|
+
* element carries `data-block-id` so the generated stylesheet reaches it too:
|
|
17
|
+
* states, part keys and the custom-CSS hatch included.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import clsx from 'clsx';
|
|
21
|
+
import React from 'react';
|
|
22
|
+
|
|
23
|
+
import { getCSSStyles, getResponsiveValue, resolveThemeCssVariables } from '.';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The labels that make an icon block an arrow.
|
|
27
|
+
*
|
|
28
|
+
* A contract with the editor, which writes them into the section template and
|
|
29
|
+
* reads them back to keep the pair alive across a datasource attach — see
|
|
30
|
+
* `config/blocks/slider-arrow-blocks.ts` there. The label rather than a flag,
|
|
31
|
+
* because the editor's template cloner drops keys it does not know and the
|
|
32
|
+
* `isArrowIcon: true` a template carries never survives into a real block.
|
|
33
|
+
*/
|
|
34
|
+
export const LEFT_ARROW_ICON_LABEL = 'Left Arrow Icon';
|
|
35
|
+
export const RIGHT_ARROW_ICON_LABEL = 'Right Arrow Icon';
|
|
36
|
+
|
|
37
|
+
/** Whether a block is one of a slider's arrow icons. */
|
|
38
|
+
export const isSliderArrowIconBlock = (block: {
|
|
39
|
+
type?: string;
|
|
40
|
+
label?: string;
|
|
41
|
+
}): boolean =>
|
|
42
|
+
block?.type === 'icon' &&
|
|
43
|
+
(block?.label === LEFT_ARROW_ICON_LABEL ||
|
|
44
|
+
block?.label === RIGHT_ARROW_ICON_LABEL);
|
|
45
|
+
|
|
46
|
+
/** Finds the pair on a section, in either order and however many there are. */
|
|
47
|
+
export const findSliderArrowBlocks = (blocks: any[] | undefined) => ({
|
|
48
|
+
left: (blocks || []).find(
|
|
49
|
+
(block) => block?.type === 'icon' && block?.label === LEFT_ARROW_ICON_LABEL
|
|
50
|
+
),
|
|
51
|
+
right: (blocks || []).find(
|
|
52
|
+
(block) => block?.type === 'icon' && block?.label === RIGHT_ARROW_ICON_LABEL
|
|
53
|
+
)
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const DEFAULT_LEFT_ARROW_SVG =
|
|
57
|
+
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE1IDZMOSAxMkwxNSAxOCIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPg==';
|
|
58
|
+
|
|
59
|
+
const DEFAULT_RIGHT_ARROW_SVG =
|
|
60
|
+
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTkgNkwxNSAxMkw5IDE4IiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Style keys that are not CSS and must not be handed to the button.
|
|
64
|
+
*
|
|
65
|
+
* `size` is the icon block's own control for how big the glyph is, so it
|
|
66
|
+
* belongs on the `<img>`; left in the button's style map it would render as an
|
|
67
|
+
* unknown declaration. `opacity` is dropped by `getCSSStyles` for every block
|
|
68
|
+
* (it feeds the alpha maths instead), and it is applied to the glyph here for
|
|
69
|
+
* the same reason the block's version does: an arrow faded to 40% should not
|
|
70
|
+
* take its plate with it.
|
|
71
|
+
*/
|
|
72
|
+
const GLYPH_ONLY_STYLE_KEYS = ['size'];
|
|
73
|
+
|
|
74
|
+
const readGlyphSize = (
|
|
75
|
+
styles: Record<string, unknown> | undefined,
|
|
76
|
+
currentBreakpoint: string
|
|
77
|
+
): string => {
|
|
78
|
+
const value = getResponsiveValue(styles?.size, currentBreakpoint, '24');
|
|
79
|
+
const numeric = Number(value);
|
|
80
|
+
return Number.isFinite(numeric) ? `${numeric}px` : String(value);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const readGlyphOpacity = (
|
|
84
|
+
styles: Record<string, unknown> | undefined,
|
|
85
|
+
currentBreakpoint: string
|
|
86
|
+
): number | undefined => {
|
|
87
|
+
const raw = styles?.opacity ?? styles?.['icon-opacity'];
|
|
88
|
+
if (raw === undefined || raw === null || raw === '') return undefined;
|
|
89
|
+
|
|
90
|
+
const value = Number(getResponsiveValue(raw, currentBreakpoint, ''));
|
|
91
|
+
if (!Number.isFinite(value)) return undefined;
|
|
92
|
+
|
|
93
|
+
// Authored as a percentage in the panel, like every other opacity control.
|
|
94
|
+
return value > 1 ? Math.min(value / 100, 1) : value;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const isInlineSvg = (value: string): boolean =>
|
|
98
|
+
typeof value === 'string' && value.trim().startsWith('<svg');
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Recolours an SVG to the arrow's own colour and hands it back as a data URI.
|
|
102
|
+
*
|
|
103
|
+
* Both `currentColor` and a literal `stroke`/`fill` are replaced: the shipped
|
|
104
|
+
* default uses the former, an uploaded icon usually the latter, and a designer
|
|
105
|
+
* setting the colour field expects either to follow.
|
|
106
|
+
*/
|
|
107
|
+
const colorizeSvg = (svg: string, color: string): string => {
|
|
108
|
+
const recoloured = svg
|
|
109
|
+
.replace(/currentColor/g, color)
|
|
110
|
+
.replace(/stroke="(?!none)[^"]*"/g, `stroke="${color}"`)
|
|
111
|
+
.replace(/fill="(?!none)[^"]*"/g, `fill="${color}"`);
|
|
112
|
+
|
|
113
|
+
return `data:image/svg+xml;utf8,${encodeURIComponent(recoloured)}`;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const readIconUrl = (
|
|
117
|
+
block: any,
|
|
118
|
+
currentBreakpoint: string,
|
|
119
|
+
fallback: string
|
|
120
|
+
): string => {
|
|
121
|
+
const value = block?.value;
|
|
122
|
+
|
|
123
|
+
if (typeof value === 'string' && value) return value;
|
|
124
|
+
|
|
125
|
+
if (value && typeof value === 'object') {
|
|
126
|
+
const responsive = getResponsiveValue(value, currentBreakpoint, '');
|
|
127
|
+
if (typeof responsive === 'string' && responsive) return responsive;
|
|
128
|
+
if (typeof value.url === 'string' && value.url) return value.url;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return fallback;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export interface SliderArrowProps {
|
|
135
|
+
/** The icon block the arrow is, or undefined when the section has none. */
|
|
136
|
+
block: any;
|
|
137
|
+
direction: 'left' | 'right';
|
|
138
|
+
currentBreakpoint: string;
|
|
139
|
+
themeSettings?: any;
|
|
140
|
+
/** True while the arrow's own layer is selected in the editor. */
|
|
141
|
+
isSelected?: boolean;
|
|
142
|
+
/**
|
|
143
|
+
* Called on click in the designer, to hand selection to the arrow's layer.
|
|
144
|
+
*
|
|
145
|
+
* It runs ALONGSIDE the carousel's own handler rather than instead of it: the
|
|
146
|
+
* slide moving is what the designer clicked for, and the arrow is absolutely
|
|
147
|
+
* positioned chrome that does not move with it, so there is nothing to trade
|
|
148
|
+
* off between paging and selecting.
|
|
149
|
+
*/
|
|
150
|
+
onSelect?: () => void;
|
|
151
|
+
/** react-multi-carousel's own handler. */
|
|
152
|
+
onClick?: () => void;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* One arrow.
|
|
157
|
+
*
|
|
158
|
+
* The order is the contract. The plate's defaults are written first and the
|
|
159
|
+
* authored map second, so every one of them is a default a designer can simply
|
|
160
|
+
* replace — including the circle, which is what the old library button never
|
|
161
|
+
* let go of. What is NOT overridable inline sits in the class list instead
|
|
162
|
+
* (`absolute`, the flex centring), because those are what make the arrow an
|
|
163
|
+
* arrow rather than a styling choice; an authored `display` still beats the
|
|
164
|
+
* class, since an inline declaration outranks it.
|
|
165
|
+
*/
|
|
166
|
+
export const SliderArrow: React.FC<SliderArrowProps> = ({
|
|
167
|
+
block,
|
|
168
|
+
direction,
|
|
169
|
+
currentBreakpoint,
|
|
170
|
+
themeSettings,
|
|
171
|
+
isSelected = false,
|
|
172
|
+
onSelect,
|
|
173
|
+
onClick
|
|
174
|
+
}) => {
|
|
175
|
+
const styles = (block?.styles || {}) as Record<string, unknown>;
|
|
176
|
+
|
|
177
|
+
const plateStyles = React.useMemo(() => {
|
|
178
|
+
const forPlate = { ...styles };
|
|
179
|
+
GLYPH_ONLY_STYLE_KEYS.forEach((key) => {
|
|
180
|
+
delete forPlate[key];
|
|
181
|
+
});
|
|
182
|
+
return getCSSStyles(forPlate, themeSettings, currentBreakpoint);
|
|
183
|
+
}, [styles, themeSettings, currentBreakpoint]);
|
|
184
|
+
|
|
185
|
+
const glyphSize = readGlyphSize(styles, currentBreakpoint);
|
|
186
|
+
const glyphOpacity = readGlyphOpacity(styles, currentBreakpoint);
|
|
187
|
+
|
|
188
|
+
const glyphColor = resolveThemeCssVariables(
|
|
189
|
+
String(getResponsiveValue(styles.color, currentBreakpoint, '#ffffff')),
|
|
190
|
+
themeSettings
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const fallbackSvg =
|
|
194
|
+
direction === 'left' ? DEFAULT_LEFT_ARROW_SVG : DEFAULT_RIGHT_ARROW_SVG;
|
|
195
|
+
const iconUrl = readIconUrl(block, currentBreakpoint, fallbackSvg);
|
|
196
|
+
|
|
197
|
+
const glyphUrl = isInlineSvg(iconUrl)
|
|
198
|
+
? colorizeSvg(iconUrl, glyphColor)
|
|
199
|
+
: iconUrl;
|
|
200
|
+
|
|
201
|
+
// An authored offset takes the axis over from the default. Keeping both would
|
|
202
|
+
// over-constrain the box on the horizontal axis, and on the vertical one the
|
|
203
|
+
// centring translate would shift an authored `top` by half the plate.
|
|
204
|
+
const hasHorizontalOffset =
|
|
205
|
+
plateStyles.left !== undefined || plateStyles.right !== undefined;
|
|
206
|
+
const hasVerticalOffset =
|
|
207
|
+
plateStyles.top !== undefined || plateStyles.bottom !== undefined;
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<button
|
|
211
|
+
type="button"
|
|
212
|
+
// The generated stylesheet finds the arrow through this, which is what
|
|
213
|
+
// carries states, part keys and the custom-CSS hatch onto it.
|
|
214
|
+
data-block-id={block?.id}
|
|
215
|
+
onClick={(event) => {
|
|
216
|
+
event.preventDefault();
|
|
217
|
+
event.stopPropagation();
|
|
218
|
+
onClick?.();
|
|
219
|
+
onSelect?.();
|
|
220
|
+
}}
|
|
221
|
+
className={clsx(
|
|
222
|
+
'absolute z-10 flex items-center justify-center',
|
|
223
|
+
!hasHorizontalOffset && (direction === 'left' ? 'left-4' : 'right-4'),
|
|
224
|
+
!hasVerticalOffset && 'top-1/2 -translate-y-1/2',
|
|
225
|
+
isSelected && 'ring-2 ring-blue-500 ring-offset-2'
|
|
226
|
+
)}
|
|
227
|
+
style={{
|
|
228
|
+
width: '40px',
|
|
229
|
+
height: '40px',
|
|
230
|
+
borderRadius: '50%',
|
|
231
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
232
|
+
border: 'none',
|
|
233
|
+
cursor: 'pointer',
|
|
234
|
+
padding: 0,
|
|
235
|
+
...plateStyles
|
|
236
|
+
}}
|
|
237
|
+
aria-label={direction === 'left' ? 'Previous' : 'Next'}
|
|
238
|
+
>
|
|
239
|
+
<img
|
|
240
|
+
src={glyphUrl}
|
|
241
|
+
alt=""
|
|
242
|
+
aria-hidden="true"
|
|
243
|
+
style={{
|
|
244
|
+
width: glyphSize,
|
|
245
|
+
height: glyphSize,
|
|
246
|
+
opacity: glyphOpacity,
|
|
247
|
+
pointerEvents: 'none'
|
|
248
|
+
}}
|
|
249
|
+
/>
|
|
250
|
+
</button>
|
|
251
|
+
);
|
|
252
|
+
};
|