@akinon/pz-theme 2.0.26-rc.0 → 2.0.26

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,34 +1,11 @@
1
1
  # @akinon/pz-theme
2
2
 
3
- ## 2.0.26-rc.0
4
-
5
- ### Patch Changes
6
-
7
- - Updated dependencies [0cf9ea23]
8
- - Updated dependencies [324f97d5]
9
- - Updated dependencies [51ea0688]
10
- - Updated dependencies
11
- - Updated dependencies [b55acb76]
12
- - Updated dependencies [760258c1]
13
- - Updated dependencies [143be2b9]
14
- - Updated dependencies [7889b08f]
15
- - Updated dependencies [9f8cd3bc]
16
- - Updated dependencies [d51fa68e]
17
- - Updated dependencies [bfafa3f4]
18
- - Updated dependencies [57d7eb30]
19
- - Updated dependencies [d99a6a7d]
20
- - Updated dependencies [9db81a71]
21
- - Updated dependencies [591e345e]
22
- - Updated dependencies [4de5303c]
23
- - Updated dependencies [95b139dc]
24
- - Updated dependencies [1d00f2d0]
25
- - Updated dependencies [4ac7b2a1]
26
- - Updated dependencies [4998a963]
27
- - Updated dependencies [804d2bd]
28
- - Updated dependencies [3909d322]
29
- - Updated dependencies [6a3d8a6]
30
- - Updated dependencies [e18836b2]
31
- - @akinon/next@2.0.26-rc.0
3
+ ## 2.0.26
4
+
5
+ ### Patch Changes
6
+
7
+ - 40bc4a3: ZERO-4545: Refactor product query hooks to use currentData for better state management across args
8
+ - @akinon/next@2.0.26
32
9
 
33
10
  ## 2.0.25
34
11
 
package/package.json CHANGED
@@ -1,11 +1,11 @@
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.26-rc.0",
4
+ "version": "2.0.26",
5
5
  "license": "MIT",
6
6
  "main": "src/index.ts",
7
7
  "peerDependencies": {
8
- "@akinon/next": "2.0.26-rc.0",
8
+ "@akinon/next": "2.0.26",
9
9
  "react": "^18.0.0 || ^19.0.0",
10
10
  "react-dom": "^18.0.0 || ^19.0.0"
11
11
  },
@@ -15,7 +15,7 @@
15
15
  "tailwind-merge": "^2.5.4"
16
16
  },
17
17
  "devDependencies": {
18
- "@akinon/next": "2.0.26-rc.0",
18
+ "@akinon/next": "2.0.26",
19
19
  "@types/node": "^18.7.8",
20
20
  "@types/react": "^18.0.17",
21
21
  "@types/react-dom": "^18.0.6",
@@ -257,7 +257,9 @@ export default function FeaturedProductSpotlightSection({
257
257
  const fallbackProductPk = parsePositiveInt(
258
258
  getResponsiveValue(section.properties?.['product-pk'], currentBreakpoint, '')
259
259
  );
260
- const { data: fallbackProductResponse } = useGetProductByPkQuery(
260
+ // currentData (not data): data is sticky across args, so a cleared
261
+ // product-pk would keep rendering the previously fetched product.
262
+ const { currentData: fallbackProductResponse } = useGetProductByPkQuery(
261
263
  fallbackProductPk as number,
262
264
  {
263
265
  skip: products.length > 0 || !fallbackProductPk
@@ -593,7 +593,9 @@ export default function FindInStoreSection({
593
593
  currentBreakpoint
594
594
  );
595
595
 
596
- const { data: productResponse, isLoading: productLoading } =
596
+ // currentData (not data): data is sticky across args, so a cleared
597
+ // product-pk would keep rendering the previously fetched product.
598
+ const { currentData: productResponse, isLoading: productLoading } =
597
599
  useGetProductByPkQuery(resolvedProductPk as number, {
598
600
  skip: !resolvedProductPk
599
601
  });
@@ -565,13 +565,15 @@ export default function InstallmentOptionsSection({
565
565
  const resolvedProductPk = parseProductPk(collectionProduct?.pk) ?? propertyProductPk;
566
566
 
567
567
  const shouldFetchProduct = !collectionProduct && resolvedProductPk !== null;
568
- const { data: productResponse } = useGetProductByPkQuery(
568
+ // currentData (not data): data is sticky across args, so a cleared
569
+ // product-pk would keep rendering the previous product/installments.
570
+ const { currentData: productResponse } = useGetProductByPkQuery(
569
571
  resolvedProductPk as number,
570
572
  {
571
573
  skip: !shouldFetchProduct
572
574
  }
573
575
  );
574
- const { data: installmentsData, isLoading } = useGetInstallmentsQuery(
576
+ const { currentData: installmentsData, isLoading } = useGetInstallmentsQuery(
575
577
  resolvedProductPk as number,
576
578
  {
577
579
  skip: resolvedProductPk === null
@@ -2,6 +2,7 @@
2
2
 
3
3
  import React, { useEffect, useMemo, useState } from 'react';
4
4
  import { useGetProductByPkQuery } from '@akinon/next/data/client/product';
5
+ import { useLocalization } from '@akinon/next/hooks';
5
6
 
6
7
  import ThemeBlock, { Block } from '../theme-block';
7
8
  import { useThemeSettingsContext } from '../theme-settings-context';
@@ -216,10 +217,16 @@ const getRemainingTime = (
216
217
  };
217
218
  };
218
219
 
219
- const formatLaunchDate = (value: Date | null): string | null => {
220
+ // Locale must be explicit: Intl with `undefined` resolves to the runtime
221
+ // default, which differs between the SSR node process and the browser and
222
+ // causes hydration mismatches on the rendered date text.
223
+ const formatLaunchDate = (
224
+ value: Date | null,
225
+ locale?: string
226
+ ): string | null => {
220
227
  if (!value) return null;
221
228
 
222
- return new Intl.DateTimeFormat(undefined, {
229
+ return new Intl.DateTimeFormat(locale || 'en', {
223
230
  month: 'long',
224
231
  day: 'numeric',
225
232
  year: 'numeric'
@@ -304,6 +311,7 @@ export default function PreOrderLaunchBannerSection({
304
311
  selectedBlockId = null
305
312
  }: PreOrderLaunchBannerSectionProps) {
306
313
  const themeSettings = useThemeSettingsContext();
314
+ const { locale } = useLocalization();
307
315
 
308
316
  const maxWidth = getResponsiveValue(
309
317
  section.styles?.['max-width'],
@@ -362,7 +370,9 @@ export default function PreOrderLaunchBannerSection({
362
370
  ''
363
371
  )
364
372
  );
365
- const { data: fallbackProductResponse } = useGetProductByPkQuery(
373
+ // currentData (not data): data is sticky across args, so a cleared
374
+ // product-pk would keep rendering the previously fetched product.
375
+ const { currentData: fallbackProductResponse } = useGetProductByPkQuery(
366
376
  fallbackProductPk as number,
367
377
  {
368
378
  skip: products.length > 0 || !fallbackProductPk
@@ -385,8 +395,8 @@ export default function PreOrderLaunchBannerSection({
385
395
  [product, propertyLaunchDate]
386
396
  );
387
397
  const launchDateText = useMemo(
388
- () => formatLaunchDate(launchDate),
389
- [launchDate]
398
+ () => formatLaunchDate(launchDate, locale),
399
+ [launchDate, locale]
390
400
  );
391
401
 
392
402
  const showCountdown = parseBoolean(
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import React from 'react';
3
+ import React, { useEffect, useRef } from 'react';
4
4
  import ActionToolbar from '../components/action-toolbar';
5
5
  import { twMerge } from 'tailwind-merge';
6
6
  import clsx from 'clsx';
@@ -58,6 +58,30 @@ export default function SectionWrapper({
58
58
  section.properties?.['error-message']
59
59
  ).trim();
60
60
 
61
+ const sectionRef = useRef<HTMLElement>(null);
62
+
63
+ // Custom-renderer sections render through this wrapper instead of the
64
+ // default <section> in theme-section, so the editor's SCROLL_TO_SECTION
65
+ // must be handled here too.
66
+ useEffect(() => {
67
+ if (!isDesigner) return;
68
+
69
+ const handleMessage = (event: MessageEvent) => {
70
+ if (
71
+ event.data?.type === 'SCROLL_TO_SECTION' &&
72
+ event.data?.data?.sectionId === section.id
73
+ ) {
74
+ sectionRef.current?.scrollIntoView({
75
+ behavior: 'smooth',
76
+ block: 'center'
77
+ });
78
+ }
79
+ };
80
+
81
+ window.addEventListener('message', handleMessage);
82
+ return () => window.removeEventListener('message', handleMessage);
83
+ }, [section.id, isDesigner]);
84
+
61
85
  const handleClick = (e: React.MouseEvent) => {
62
86
  if (isDesigner && onSelect) {
63
87
  e.stopPropagation();
@@ -80,6 +104,7 @@ export default function SectionWrapper({
80
104
 
81
105
  return (
82
106
  <section
107
+ ref={sectionRef}
83
108
  data-section-id={section.id}
84
109
  data-newsletter-signup={isNewsletterSection ? 'true' : undefined}
85
110
  data-newsletter-endpoint={
@@ -10,10 +10,14 @@ import { useVisibilityContext } from './hooks/use-visibility-context';
10
10
  import { applyVisibilityRulesToSections } from './utils/visibility-rules';
11
11
  import { isPublishWindowVisible } from './utils/publish-window';
12
12
 
13
- // Global flag to track if IFRAME_READY has been sent (survives component remount)
13
+ // Global flags that must survive component remounts (e.g. in-iframe route
14
+ // changes): IFRAME_READY is sent once per document, and the designer flag
15
+ // arrives once from the editor in response — remounted components must not
16
+ // reset to non-designer state.
14
17
  declare global {
15
18
  interface Window {
16
19
  __iframeReadySent?: boolean;
20
+ __themeEditorDesigner?: boolean;
17
21
  }
18
22
  }
19
23
 
@@ -36,7 +40,9 @@ export default function ThemePlaceholderClient({
36
40
  );
37
41
  const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
38
42
  const [currentBreakpoint, setCurrentBreakpoint] = useState<string>('desktop');
39
- const [isDesigner, setIsDesigner] = useState(false);
43
+ const [isDesigner, setIsDesigner] = useState(
44
+ () => typeof window !== 'undefined' && window.__themeEditorDesigner === true
45
+ );
40
46
  const visibilityContext = useVisibilityContext(currentBreakpoint);
41
47
  const renderedSections = applyVisibilityRulesToSections(
42
48
  sections,
@@ -73,6 +79,7 @@ export default function ThemePlaceholderClient({
73
79
  switch (event.data?.type) {
74
80
  case 'SET_THEME_EDITOR_COOKIE':
75
81
  if (event.data.data) {
82
+ window.__themeEditorDesigner = true;
76
83
  setIsDesigner(true);
77
84
  }
78
85
  break;
@@ -297,7 +297,13 @@ export default function ThemePlaceholderWrapper({
297
297
  );
298
298
  const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
299
299
  const [currentBreakpoint, setCurrentBreakpoint] = useState<string>('desktop');
300
- const [isDesigner, setIsDesigner] = useState(isDesignMode);
300
+ // Designer flag survives remounts via the window global; the editor sends
301
+ // SET_THEME_EDITOR_COOKIE only once per document (IFRAME_READY guard).
302
+ const [isDesigner, setIsDesigner] = useState(
303
+ () =>
304
+ isDesignMode ||
305
+ (typeof window !== 'undefined' && window.__themeEditorDesigner === true)
306
+ );
301
307
  const [dataSources, setDataSources] = useState<any[]>(initialDataSources);
302
308
  const [themeSettings, setThemeSettings] = useState<Record<
303
309
  string,
@@ -502,6 +508,7 @@ export default function ThemePlaceholderWrapper({
502
508
  switch (event.data?.type) {
503
509
  case 'SET_THEME_EDITOR_COOKIE':
504
510
  if (event.data.data) {
511
+ window.__themeEditorDesigner = true;
505
512
  setIsDesigner(true);
506
513
  }
507
514
  break;