@akinon/pz-theme 2.0.140-rc.0 → 2.0.140
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,12 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.140
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
- Updated dependencies [51ea06888]
|
|
11
|
-
- Updated dependencies
|
|
12
|
-
- Updated dependencies [2fea41430]
|
|
13
|
-
- Updated dependencies [b55acb768]
|
|
14
|
-
- Updated dependencies [760258c1c]
|
|
15
|
-
- Updated dependencies [143be2b9d]
|
|
16
|
-
- Updated dependencies [7889b08fe]
|
|
17
|
-
- Updated dependencies [9f8cd3bc5]
|
|
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.140-rc.0
|
|
37
|
-
- @akinon/theme-contract@2.0.140-rc.0
|
|
3
|
+
## 2.0.140
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 80aed36: ZERO-5189: Keep a carousel mounted when the breakpoint resolves
|
|
8
|
+
- @akinon/next@2.0.140
|
|
9
|
+
- @akinon/theme-contract@2.0.140
|
|
38
10
|
|
|
39
11
|
## 2.0.139
|
|
40
12
|
|
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.140
|
|
4
|
+
"version": "2.0.140",
|
|
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.140
|
|
11
|
+
"@akinon/next": "2.0.140",
|
|
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.140
|
|
16
|
+
"@akinon/theme-contract": "2.0.140",
|
|
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.140
|
|
24
|
+
"@akinon/next": "2.0.140",
|
|
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",
|
|
@@ -13,7 +13,11 @@ import {
|
|
|
13
13
|
partAttrs,
|
|
14
14
|
resolveThemeCssVariables
|
|
15
15
|
} from '../utils';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
DESKTOP_MIN_WIDTH,
|
|
18
|
+
isNarrowBreakpoint,
|
|
19
|
+
setAtAnyBreakpoint
|
|
20
|
+
} from '../utils/breakpoints';
|
|
17
21
|
import { useCarouselSsr } from '../utils/carousel-ssr';
|
|
18
22
|
import {
|
|
19
23
|
authoredSlideIndex,
|
|
@@ -322,7 +326,18 @@ const SliderBlock: React.FC<BlockRendererProps> = ({
|
|
|
322
326
|
false
|
|
323
327
|
);
|
|
324
328
|
|
|
325
|
-
|
|
329
|
+
/**
|
|
330
|
+
* Asked WITHOUT the breakpoint, unlike everything around it.
|
|
331
|
+
*
|
|
332
|
+
* react-multi-carousel reads `infinite` when it mounts and builds its
|
|
333
|
+
* clones from it; nothing re-reads it on a prop change, so the only way to
|
|
334
|
+
* honour a later answer is to remount — and a remount destroys and rebuilds
|
|
335
|
+
* every slide one commit after the page became interactive. Since
|
|
336
|
+
* `currentBreakpoint` is seeded desktop and corrected after mount, a
|
|
337
|
+
* per-breakpoint autoplay made that happen on every phone and tablet load.
|
|
338
|
+
* One answer at every width, one mount.
|
|
339
|
+
*/
|
|
340
|
+
const useInfinite = setAtAnyBreakpoint(getConfigEntry('autoplay'));
|
|
326
341
|
|
|
327
342
|
/*
|
|
328
343
|
* Read through the breakpoint like every other entry above it.
|
|
@@ -359,7 +374,7 @@ const SliderBlock: React.FC<BlockRendererProps> = ({
|
|
|
359
374
|
* to come back with it. Two copies of this template literal would eventually
|
|
360
375
|
* disagree.
|
|
361
376
|
*/
|
|
362
|
-
const carouselKey = `${desktopItems}-${mobileItems}-${desktopSlides}-${mobileSlides}-${visibleCount}-${useInfinite}
|
|
377
|
+
const carouselKey = `${desktopItems}-${mobileItems}-${desktopSlides}-${mobileSlides}-${visibleCount}-${useInfinite}`;
|
|
363
378
|
|
|
364
379
|
// Server-render the slides. Without this the track ships empty and the whole
|
|
365
380
|
// band is 0px tall until hydration — see `utils/carousel-ssr`.
|
|
@@ -20,7 +20,11 @@ import {
|
|
|
20
20
|
getResponsiveValue,
|
|
21
21
|
SECTION_CONTENT_ATTRIBUTE
|
|
22
22
|
} from '../utils';
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
DESKTOP_MIN_WIDTH,
|
|
25
|
+
TABLET_MIN_WIDTH,
|
|
26
|
+
setAtAnyBreakpoint
|
|
27
|
+
} from '../utils/breakpoints';
|
|
24
28
|
import { useCarouselSsr } from '../utils/carousel-ssr';
|
|
25
29
|
import {
|
|
26
30
|
authoredSlideIndex,
|
|
@@ -512,7 +516,15 @@ const PostsSliderSection: React.FC<PostsSliderSectionProps> = ({
|
|
|
512
516
|
getResponsiveValue(getConfigEntry('infinite'), currentBreakpoint, false) ===
|
|
513
517
|
true;
|
|
514
518
|
const hasMultipleSlides = visibleCount > itemsPerView;
|
|
515
|
-
|
|
519
|
+
/**
|
|
520
|
+
* Asked WITHOUT the breakpoint — see `setAtAnyBreakpoint`. The library
|
|
521
|
+
* builds its clones from `infinite` at mount and never re-reads it, and
|
|
522
|
+
* `itemsPerView` is breakpoint-derived, so the old expression changed its
|
|
523
|
+
* answer the moment the viewport was measured and took the whole strip
|
|
524
|
+
* down with it on every phone and tablet load.
|
|
525
|
+
*/
|
|
526
|
+
const useInfinite =
|
|
527
|
+
setAtAnyBreakpoint(getConfigEntry('infinite')) && visibleCount > 1;
|
|
516
528
|
const useAutoPlay = autoPlay && hasMultipleSlides;
|
|
517
529
|
|
|
518
530
|
/*
|
|
@@ -530,7 +542,9 @@ const PostsSliderSection: React.FC<PostsSliderSectionProps> = ({
|
|
|
530
542
|
* change. Without the remount, switching device in the editor would leave the
|
|
531
543
|
* canvas showing the previous breakpoint's item count.
|
|
532
544
|
*/
|
|
533
|
-
const carouselKey = `${desktopItems}-${tabletItems}-${mobileItems}-${desktopSlides}-${tabletSlides}-${mobileSlides}-${visibleCount}-${
|
|
545
|
+
const carouselKey = `${desktopItems}-${tabletItems}-${mobileItems}-${desktopSlides}-${tabletSlides}-${mobileSlides}-${visibleCount}-${
|
|
546
|
+
isDesigner ? currentBreakpoint : ''
|
|
547
|
+
}-${useInfinite}`;
|
|
534
548
|
|
|
535
549
|
// Server-render the slides. Without this the strip ships an empty track and
|
|
536
550
|
// occupies no height until hydration — see `utils/carousel-ssr`.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { setAtAnyBreakpoint } from '../breakpoints';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The primitive that keeps a carousel mounted once.
|
|
5
|
+
*
|
|
6
|
+
* `currentBreakpoint` is seeded 'desktop' and corrected only after the
|
|
7
|
+
* viewport is measured, so a structural setting read through it answers one
|
|
8
|
+
* thing in the server HTML and another a paint later. react-multi-carousel
|
|
9
|
+
* builds its clones from `infinite` at mount and never re-reads it, so the
|
|
10
|
+
* only way to honour the later answer is a remount — which destroys and
|
|
11
|
+
* rebuilds every slide on every phone load. Asked this way the question has
|
|
12
|
+
* one answer at every width.
|
|
13
|
+
*/
|
|
14
|
+
describe('setAtAnyBreakpoint', () => {
|
|
15
|
+
it('is true when the switch is on at any single breakpoint', () => {
|
|
16
|
+
expect(setAtAnyBreakpoint({ desktop: true, mobile: false })).toBe(true);
|
|
17
|
+
expect(setAtAnyBreakpoint({ mobile: true })).toBe(true);
|
|
18
|
+
expect(setAtAnyBreakpoint({ tablet: true })).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('is false when it is off everywhere', () => {
|
|
22
|
+
expect(setAtAnyBreakpoint({ desktop: false, mobile: false })).toBe(false);
|
|
23
|
+
expect(setAtAnyBreakpoint({})).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('treats an absent value as off', () => {
|
|
27
|
+
expect(setAtAnyBreakpoint(undefined)).toBe(false);
|
|
28
|
+
expect(setAtAnyBreakpoint(null)).toBe(false);
|
|
29
|
+
expect(setAtAnyBreakpoint('')).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('accepts a flat value, which is how an older composition stores it', () => {
|
|
33
|
+
expect(setAtAnyBreakpoint(true)).toBe(true);
|
|
34
|
+
expect(setAtAnyBreakpoint('true')).toBe(true);
|
|
35
|
+
expect(setAtAnyBreakpoint(false)).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('ignores a key that is not a breakpoint', () => {
|
|
39
|
+
expect(setAtAnyBreakpoint({ phablet: true })).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('gives the same answer whatever breakpoint the caller is on', () => {
|
|
43
|
+
// The whole point: no argument can change it, so the server and the
|
|
44
|
+
// first client render cannot disagree.
|
|
45
|
+
const authored = { desktop: true, tablet: false, mobile: false };
|
|
46
|
+
const answers = ['desktop', 'tablet', 'mobile'].map(() =>
|
|
47
|
+
setAtAnyBreakpoint(authored)
|
|
48
|
+
);
|
|
49
|
+
expect(new Set(answers).size).toBe(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('rejects an array rather than reading index 0 as a breakpoint', () => {
|
|
53
|
+
expect(setAtAnyBreakpoint([true])).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
});
|
package/src/utils/breakpoints.ts
CHANGED
|
@@ -336,3 +336,27 @@ export const PHONE_AND_TABLET_MEDIA_CONDITION = `(max-width: ${
|
|
|
336
336
|
export const TABLET_ONLY_MEDIA_CONDITION = `(min-width: ${TABLET_MIN_WIDTH}px) and (max-width: ${
|
|
337
337
|
DESKTOP_MIN_WIDTH - 1
|
|
338
338
|
}px)`;
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* True when a per-breakpoint switch is on at ANY width.
|
|
342
|
+
*
|
|
343
|
+
* `currentBreakpoint` is seeded `'desktop'` and corrected only after mount, so
|
|
344
|
+
* a value read through it answers one thing in the server HTML and another a
|
|
345
|
+
* paint later. For a setting that is merely styling that is harmless. For one
|
|
346
|
+
* that decides a carousel's STRUCTURE — whether the library builds clones —
|
|
347
|
+
* it is not: react-multi-carousel reads `infinite` when it mounts and never
|
|
348
|
+
* again, so the only way to honour a change is to remount, which destroys and
|
|
349
|
+
* rebuilds every slide a beat after the page became interactive.
|
|
350
|
+
*
|
|
351
|
+
* Asking the question without the breakpoint gives one answer at every width,
|
|
352
|
+
* so the carousel mounts once. A slider set to loop on desktop also loops on a
|
|
353
|
+
* phone; that is the price, and it is far cheaper than throwing the band away
|
|
354
|
+
* and redrawing it on every mobile load.
|
|
355
|
+
*/
|
|
356
|
+
export const setAtAnyBreakpoint = (value: unknown): boolean => {
|
|
357
|
+
if (value === null || value === undefined || value === '') return false;
|
|
358
|
+
if (typeof value !== 'object') return Boolean(value);
|
|
359
|
+
if (Array.isArray(value)) return false;
|
|
360
|
+
const perBreakpoint = value as Record<string, unknown>;
|
|
361
|
+
return BREAKPOINTS.some((breakpoint) => Boolean(perBreakpoint[breakpoint]));
|
|
362
|
+
};
|