@akinon/pz-theme 2.0.141-rc.0 → 2.0.142-beta.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.
package/CHANGELOG.md
CHANGED
|
@@ -1,40 +1,20 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
- Updated dependencies [
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
- Updated dependencies [bfafa3f49]
|
|
19
|
-
- Updated dependencies [57d7eb305]
|
|
20
|
-
- Updated dependencies [d99a6a7d5]
|
|
21
|
-
- Updated dependencies [9db81a714]
|
|
22
|
-
- Updated dependencies [591e345e1]
|
|
23
|
-
- Updated dependencies [4de5303c5]
|
|
24
|
-
- Updated dependencies [95b139dc1]
|
|
25
|
-
- Updated dependencies [1d00f2d06]
|
|
26
|
-
- Updated dependencies [4ac7b2a1e]
|
|
27
|
-
- Updated dependencies [e9598c71d]
|
|
28
|
-
- Updated dependencies [4998a9631]
|
|
29
|
-
- Updated dependencies [804d2bd6c]
|
|
30
|
-
- Updated dependencies [3909d3224]
|
|
31
|
-
- Updated dependencies [6a3d8a631]
|
|
32
|
-
- Updated dependencies [e18836b20]
|
|
33
|
-
- Updated dependencies [b1111d7b1]
|
|
34
|
-
- Updated dependencies [1595d458e]
|
|
35
|
-
- Updated dependencies [dca128112]
|
|
36
|
-
- @akinon/next@2.0.141-rc.0
|
|
37
|
-
- @akinon/theme-contract@2.0.141-rc.0
|
|
3
|
+
## 2.0.142-beta.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [cbbbfd757]
|
|
8
|
+
- @akinon/next@2.0.142-beta.0
|
|
9
|
+
- @akinon/theme-contract@2.0.142-beta.0
|
|
10
|
+
|
|
11
|
+
## 2.0.141
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- cd9f6c4: ZERO-5190: Hide carousel arrows with CSS instead of removing them from the markup
|
|
16
|
+
- @akinon/next@2.0.141
|
|
17
|
+
- @akinon/theme-contract@2.0.141
|
|
38
18
|
|
|
39
19
|
## 2.0.140
|
|
40
20
|
|
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/pz-theme",
|
|
3
3
|
"description": "Theme package for Project Zero Next — ThemePlaceholder system and theme editor infrastructure",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.142-beta.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "jest"
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"@akinon/next": "2.0.
|
|
11
|
+
"@akinon/next": "2.0.142-beta.0",
|
|
12
12
|
"react": "^18.0.0 || ^19.0.0",
|
|
13
13
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@akinon/theme-contract": "2.0.
|
|
16
|
+
"@akinon/theme-contract": "2.0.142-beta.0",
|
|
17
17
|
"clsx": "2.1.1",
|
|
18
18
|
"lottie-web": "^5.13.0",
|
|
19
19
|
"react-multi-carousel": "2.8.4",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"zod": "4.4.1"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@akinon/next": "2.0.
|
|
24
|
+
"@akinon/next": "2.0.142-beta.0",
|
|
25
25
|
"@testing-library/jest-dom": "5.16.4",
|
|
26
26
|
"@testing-library/react": "16.3.2",
|
|
27
27
|
"@testing-library/user-event": "14.1.1",
|
|
@@ -18,7 +18,11 @@ import {
|
|
|
18
18
|
isNarrowBreakpoint,
|
|
19
19
|
setAtAnyBreakpoint
|
|
20
20
|
} from '../utils/breakpoints';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
buildCarouselArrowCss,
|
|
23
|
+
carouselSsrScopeId,
|
|
24
|
+
useCarouselSsr
|
|
25
|
+
} from '../utils/carousel-ssr';
|
|
22
26
|
import {
|
|
23
27
|
authoredSlideIndex,
|
|
24
28
|
TIMER_SLIDE_CHANGE_CEILING
|
|
@@ -306,6 +310,29 @@ const SliderBlock: React.FC<BlockRendererProps> = ({
|
|
|
306
310
|
currentBreakpoint,
|
|
307
311
|
true
|
|
308
312
|
);
|
|
313
|
+
/**
|
|
314
|
+
* Whether the arrow EXISTS is settled without the breakpoint; which widths
|
|
315
|
+
* it is visible at is settled in CSS below. Unauthored still means on, the
|
|
316
|
+
* same default `showArrows` carries.
|
|
317
|
+
*/
|
|
318
|
+
const arrowsConfigured = getConfigEntry('show-arrows');
|
|
319
|
+
const arrowsAnywhere =
|
|
320
|
+
arrowsConfigured === undefined || arrowsConfigured === null
|
|
321
|
+
? true
|
|
322
|
+
: setAtAnyBreakpoint(arrowsConfigured);
|
|
323
|
+
// Not `itemsShown`: that is breakpoint-derived, so it would put the flip
|
|
324
|
+
// back. One slide can never overflow at any width.
|
|
325
|
+
const canOverflow = visibleCount > 1;
|
|
326
|
+
const renderArrows = arrowsAnywhere && canOverflow;
|
|
327
|
+
|
|
328
|
+
const arrowCss = buildCarouselArrowCss(
|
|
329
|
+
carouselSsrScopeId(block.id) || '',
|
|
330
|
+
carouselResponsive,
|
|
331
|
+
(band) =>
|
|
332
|
+
getResponsiveValue(arrowsConfigured, band, true) === true &&
|
|
333
|
+
visibleCount > Number(carouselResponsive?.[band]?.items || 1)
|
|
334
|
+
);
|
|
335
|
+
|
|
309
336
|
const showDots = getResponsiveValue(
|
|
310
337
|
getConfigEntry('show-dots'),
|
|
311
338
|
currentBreakpoint,
|
|
@@ -1257,13 +1284,7 @@ const SliderBlock: React.FC<BlockRendererProps> = ({
|
|
|
1257
1284
|
* `posts-slider-section` folds the same test into its own
|
|
1258
1285
|
* `showArrows`.
|
|
1259
1286
|
*/
|
|
1260
|
-
arrows={
|
|
1261
|
-
isDesigner
|
|
1262
|
-
? false
|
|
1263
|
-
: Boolean(showArrows) &&
|
|
1264
|
-
!arrowsOutside &&
|
|
1265
|
-
visibleCount > itemsShown
|
|
1266
|
-
}
|
|
1287
|
+
arrows={isDesigner ? false : renderArrows && !arrowsOutside}
|
|
1267
1288
|
showDots={Boolean(showDots)}
|
|
1268
1289
|
customDot={<CustomDot onSelect={handleDotSelect} />}
|
|
1269
1290
|
renderDotsOutside={Boolean(showDots)}
|
|
@@ -1299,18 +1320,12 @@ const SliderBlock: React.FC<BlockRendererProps> = ({
|
|
|
1299
1320
|
*/
|
|
1300
1321
|
{...(useLeadIn ? { additionalTransfrom: edgeOffset } : {})}
|
|
1301
1322
|
customLeftArrow={
|
|
1302
|
-
!isDesigner &&
|
|
1303
|
-
showArrows &&
|
|
1304
|
-
!arrowsOutside &&
|
|
1305
|
-
visibleCount > itemsShown ? (
|
|
1323
|
+
!isDesigner && renderArrows && !arrowsOutside ? (
|
|
1306
1324
|
<CustomLeftArrow />
|
|
1307
1325
|
) : undefined
|
|
1308
1326
|
}
|
|
1309
1327
|
customRightArrow={
|
|
1310
|
-
!isDesigner &&
|
|
1311
|
-
showArrows &&
|
|
1312
|
-
!arrowsOutside &&
|
|
1313
|
-
visibleCount > itemsShown ? (
|
|
1328
|
+
!isDesigner && renderArrows && !arrowsOutside ? (
|
|
1314
1329
|
<CustomRightArrow />
|
|
1315
1330
|
) : undefined
|
|
1316
1331
|
}
|
|
@@ -1405,6 +1420,9 @@ const SliderBlock: React.FC<BlockRendererProps> = ({
|
|
|
1405
1420
|
nothing.
|
|
1406
1421
|
*/}
|
|
1407
1422
|
{carouselSsr.gateStyle}
|
|
1423
|
+
{arrowCss ? (
|
|
1424
|
+
<style dangerouslySetInnerHTML={{ __html: arrowCss }} />
|
|
1425
|
+
) : null}
|
|
1408
1426
|
</div>
|
|
1409
1427
|
);
|
|
1410
1428
|
};
|
|
@@ -25,7 +25,11 @@ import {
|
|
|
25
25
|
TABLET_MIN_WIDTH,
|
|
26
26
|
setAtAnyBreakpoint
|
|
27
27
|
} from '../utils/breakpoints';
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
buildCarouselArrowCss,
|
|
30
|
+
carouselSsrScopeId,
|
|
31
|
+
useCarouselSsr
|
|
32
|
+
} from '../utils/carousel-ssr';
|
|
29
33
|
import {
|
|
30
34
|
authoredSlideIndex,
|
|
31
35
|
TIMER_SLIDE_CHANGE_CEILING
|
|
@@ -492,12 +496,21 @@ const PostsSliderSection: React.FC<PostsSliderSectionProps> = ({
|
|
|
492
496
|
const gap = parseNumber(gapValue, 16);
|
|
493
497
|
const slidePadding = gap > 0 ? gap / 2 : 0;
|
|
494
498
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
499
|
+
/**
|
|
500
|
+
* Whether the arrow EXISTS is settled without the breakpoint; which widths
|
|
501
|
+
* it is visible at is settled in CSS. Reading it through
|
|
502
|
+
* `currentBreakpoint` meant three nodes arrived or vanished one commit
|
|
503
|
+
* after hydration — arrows a phone is not meant to have, flashed and then
|
|
504
|
+
* removed, and a tablet getting them late with a layout shift.
|
|
505
|
+
*/
|
|
506
|
+
const arrowsConfigured = getConfigEntry('show-arrows');
|
|
507
|
+
const arrowsAnywhere =
|
|
508
|
+
arrowsConfigured === undefined || arrowsConfigured === null
|
|
509
|
+
? true
|
|
510
|
+
: setAtAnyBreakpoint(arrowsConfigured);
|
|
511
|
+
// Not `itemsPerView`: that is breakpoint-derived and would put the flip
|
|
512
|
+
// back. One slide can never overflow at any width.
|
|
513
|
+
const showArrows = arrowsAnywhere && visibleCount > 1;
|
|
501
514
|
const showDots =
|
|
502
515
|
getResponsiveValue(
|
|
503
516
|
getConfigEntry('show-dots'),
|
|
@@ -548,6 +561,16 @@ const PostsSliderSection: React.FC<PostsSliderSectionProps> = ({
|
|
|
548
561
|
|
|
549
562
|
// Server-render the slides. Without this the strip ships an empty track and
|
|
550
563
|
// occupies no height until hydration — see `utils/carousel-ssr`.
|
|
564
|
+
const arrowScopeId = carouselSsrScopeId(section.id) || '';
|
|
565
|
+
const arrowCss = buildCarouselArrowCss(
|
|
566
|
+
arrowScopeId,
|
|
567
|
+
carouselResponsive,
|
|
568
|
+
(band) =>
|
|
569
|
+
getResponsiveValue(arrowsConfigured, band, true) === true &&
|
|
570
|
+
visibleCount > Number(carouselResponsive?.[band]?.items || 1),
|
|
571
|
+
`[data-section-id="${arrowScopeId}"]`
|
|
572
|
+
);
|
|
573
|
+
|
|
551
574
|
const carouselSsr = useCarouselSsr({
|
|
552
575
|
id: section.id,
|
|
553
576
|
remountKey: carouselKey,
|
|
@@ -1128,6 +1151,9 @@ const PostsSliderSection: React.FC<PostsSliderSectionProps> = ({
|
|
|
1128
1151
|
from being early.
|
|
1129
1152
|
*/}
|
|
1130
1153
|
{carouselSsr.gateStyle}
|
|
1154
|
+
{arrowCss ? (
|
|
1155
|
+
<style dangerouslySetInnerHTML={{ __html: arrowCss }} />
|
|
1156
|
+
) : null}
|
|
1131
1157
|
</div>
|
|
1132
1158
|
|
|
1133
1159
|
{/*
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { buildCarouselArrowCss } from '../carousel-ssr';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Arrow visibility moved out of the markup and into CSS.
|
|
5
|
+
*
|
|
6
|
+
* Deciding whether the arrow EXISTS from `currentBreakpoint` meant three
|
|
7
|
+
* nodes appeared or disappeared one commit after hydration, because that
|
|
8
|
+
* value is seeded 'desktop' and corrected only once the viewport is measured.
|
|
9
|
+
* Rendered at every width and hidden per band, the markup is settled before
|
|
10
|
+
* the first paint.
|
|
11
|
+
*/
|
|
12
|
+
const RESPONSIVE = {
|
|
13
|
+
desktop: { items: 4, breakpoint: { min: 1024, max: 99999 } },
|
|
14
|
+
tablet: { items: 3, breakpoint: { min: 768, max: 1023 } },
|
|
15
|
+
mobile: { items: 1, breakpoint: { min: 0, max: 767 } }
|
|
16
|
+
} as any;
|
|
17
|
+
|
|
18
|
+
describe('buildCarouselArrowCss', () => {
|
|
19
|
+
it('emits nothing when the arrow is wanted at every band', () => {
|
|
20
|
+
expect(buildCarouselArrowCss('abc', RESPONSIVE, () => true)).toBe('');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('hides only the bands that said no', () => {
|
|
24
|
+
const css = buildCarouselArrowCss(
|
|
25
|
+
'abc',
|
|
26
|
+
RESPONSIVE,
|
|
27
|
+
(band) => band !== 'mobile'
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(css).toContain('(max-width:767px)');
|
|
31
|
+
expect(css).not.toContain('(min-width:1024px)');
|
|
32
|
+
expect(css).toContain('display:none !important');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('scopes the library arrow to this carousel', () => {
|
|
36
|
+
const css = buildCarouselArrowCss('abc', RESPONSIVE, () => false);
|
|
37
|
+
|
|
38
|
+
// A slider nested inside another slider's slide must not be restyled by
|
|
39
|
+
// its parent's answer.
|
|
40
|
+
expect(css).toContain('.pz-carousel-ssr-abc .react-multi-carousel-arrow');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('covers a section-rendered arrow only when given its scope', () => {
|
|
44
|
+
const without = buildCarouselArrowCss('abc', RESPONSIVE, () => false);
|
|
45
|
+
expect(without).not.toContain('data-slider-arrow');
|
|
46
|
+
|
|
47
|
+
const withOwner = buildCarouselArrowCss(
|
|
48
|
+
'abc',
|
|
49
|
+
RESPONSIVE,
|
|
50
|
+
() => false,
|
|
51
|
+
'[data-section-id="abc"]'
|
|
52
|
+
);
|
|
53
|
+
expect(withOwner).toContain('[data-section-id="abc"] [data-slider-arrow]');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('skips a band with no width range rather than hiding everywhere', () => {
|
|
57
|
+
// Hiding the arrow at every width is the one outcome worse than showing
|
|
58
|
+
// it late, so a band that cannot be targeted is left alone.
|
|
59
|
+
const css = buildCarouselArrowCss(
|
|
60
|
+
'abc',
|
|
61
|
+
{ odd: { items: 2, breakpoint: {} } } as any,
|
|
62
|
+
() => false
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(css).toBe('');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('gives the same answer however many times it is asked', () => {
|
|
69
|
+
const once = buildCarouselArrowCss('abc', RESPONSIVE, (b) => b === 'desktop');
|
|
70
|
+
const twice = buildCarouselArrowCss('abc', RESPONSIVE, (b) => b === 'desktop');
|
|
71
|
+
|
|
72
|
+
// Server and first client render build this string independently.
|
|
73
|
+
expect(once).toBe(twice);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -186,6 +186,54 @@ export const buildCarouselSsrCss = (
|
|
|
186
186
|
return css;
|
|
187
187
|
};
|
|
188
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Arrow visibility for one carousel, as CSS instead of as markup.
|
|
191
|
+
*
|
|
192
|
+
* `show-arrows` is authored per breakpoint, but `currentBreakpoint` is
|
|
193
|
+
* seeded desktop and corrected only after the viewport is measured — so
|
|
194
|
+
* deciding whether the arrow EXISTS from it meant three nodes appeared or
|
|
195
|
+
* disappeared one commit after the page became interactive. A phone briefly
|
|
196
|
+
* showed arrows it is not meant to have, a tablet got them late with a layout
|
|
197
|
+
* shift, and anything holding a reference to one lost it.
|
|
198
|
+
*
|
|
199
|
+
* Rendered at every width and hidden per band, the answer is settled before
|
|
200
|
+
* the first paint and never changes. Anchored on the scope class rather than
|
|
201
|
+
* the gate attribute, because unlike the SSR stylesheet this one has to
|
|
202
|
+
* outlive hydration.
|
|
203
|
+
*/
|
|
204
|
+
export const buildCarouselArrowCss = (
|
|
205
|
+
scopeId: string,
|
|
206
|
+
responsive: CarouselResponsiveMap,
|
|
207
|
+
isOnAtBand: (bandName: string) => boolean,
|
|
208
|
+
/** Scope for arrows a section renders OUTSIDE the carousel box. */
|
|
209
|
+
ownerSelector?: string
|
|
210
|
+
): string => {
|
|
211
|
+
const scope = `.${CAROUSEL_SSR_SCOPE_PREFIX}${scopeId}`;
|
|
212
|
+
// The library writes its own arrows; `data-slider-arrow` is the hook for
|
|
213
|
+
// the ones a section renders itself, outside the carousel box.
|
|
214
|
+
const targets = [
|
|
215
|
+
`${scope} .react-multi-carousel-arrow`,
|
|
216
|
+
ownerSelector ? `${ownerSelector} [data-slider-arrow]` : null
|
|
217
|
+
]
|
|
218
|
+
.filter(Boolean)
|
|
219
|
+
.join(',');
|
|
220
|
+
|
|
221
|
+
let css = "";
|
|
222
|
+
|
|
223
|
+
Object.keys(responsive || {}).forEach((name) => {
|
|
224
|
+
if (isOnAtBand(name)) return;
|
|
225
|
+
|
|
226
|
+
const query = bandMediaQuery(responsive[name]);
|
|
227
|
+
// A band with no width range cannot be targeted without hiding the arrow
|
|
228
|
+
// everywhere, which is the one outcome worse than showing it late.
|
|
229
|
+
if (!query) return;
|
|
230
|
+
|
|
231
|
+
css += `${query}{${targets}{display:none !important;}}`;
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return css;
|
|
235
|
+
};
|
|
236
|
+
|
|
189
237
|
export interface CarouselSsr {
|
|
190
238
|
/**
|
|
191
239
|
* Spread onto `<Carousel>`. Together these are what make the library emit
|
|
@@ -212,6 +212,10 @@ export const SliderArrow: React.FC<SliderArrowProps> = ({
|
|
|
212
212
|
// The generated stylesheet finds the arrow through this, which is what
|
|
213
213
|
// carries states, part keys and the custom-CSS hatch onto it.
|
|
214
214
|
data-block-id={block?.id}
|
|
215
|
+
// Visibility is CSS, not markup: the arrow ships at every width and the
|
|
216
|
+
// carousel stylesheet hides it where it is not wanted, so nothing
|
|
217
|
+
// appears or disappears a commit after hydration.
|
|
218
|
+
data-slider-arrow=""
|
|
215
219
|
onClick={(event) => {
|
|
216
220
|
event.preventDefault();
|
|
217
221
|
event.stopPropagation();
|