@akinon/pz-theme 2.0.140-rc.0 → 2.0.141-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.140-rc.0
4
-
5
- ### Patch Changes
6
-
7
- - Updated dependencies [86187b1c2]
8
- - Updated dependencies [0cf9ea239]
9
- - Updated dependencies [324f97d55]
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.141-beta.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [cbbbfd757]
8
+ - @akinon/next@2.0.141-beta.0
9
+ - @akinon/theme-contract@2.0.141-beta.0
10
+
11
+ ## 2.0.140
12
+
13
+ ### Patch Changes
14
+
15
+ - 80aed36: ZERO-5189: Keep a carousel mounted when the breakpoint resolves
16
+ - @akinon/next@2.0.140
17
+ - @akinon/theme-contract@2.0.140
38
18
 
39
19
  ## 2.0.139
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.140-rc.0",
4
+ "version": "2.0.141-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.140-rc.0",
11
+ "@akinon/next": "2.0.141-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.140-rc.0",
16
+ "@akinon/theme-contract": "2.0.141-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.140-rc.0",
24
+ "@akinon/next": "2.0.141-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",
@@ -13,7 +13,11 @@ import {
13
13
  partAttrs,
14
14
  resolveThemeCssVariables
15
15
  } from '../utils';
16
- import { DESKTOP_MIN_WIDTH, isNarrowBreakpoint } from '../utils/breakpoints';
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
- const useInfinite = useAutoPlay;
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}-${useAutoPlay}-${autoPlaySpeed}`;
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 { DESKTOP_MIN_WIDTH, TABLET_MIN_WIDTH } from '../utils/breakpoints';
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
- const useInfinite = infinite && hasMultipleSlides;
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}-${currentBreakpoint}-${useInfinite}-${useAutoPlay}-${autoPlaySpeed}`;
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
+ });
@@ -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
+ };