@akinon/pz-theme 2.0.64-rc.0 → 2.0.65-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,36 +1,20 @@
1
1
  # @akinon/pz-theme
2
2
 
3
- ## 2.0.64-rc.0
4
-
5
- ### Patch Changes
6
-
7
- - Updated dependencies [0cf9ea239]
8
- - Updated dependencies [324f97d55]
9
- - Updated dependencies [51ea06888]
10
- - Updated dependencies
11
- - Updated dependencies [2fea4143]
12
- - Updated dependencies [b55acb768]
13
- - Updated dependencies [760258c1c]
14
- - Updated dependencies [143be2b9d]
15
- - Updated dependencies [7889b08fe]
16
- - Updated dependencies [9f8cd3bc5]
17
- - Updated dependencies [bfafa3f49]
18
- - Updated dependencies [57d7eb305]
19
- - Updated dependencies [d99a6a7d5]
20
- - Updated dependencies [9db81a714]
21
- - Updated dependencies [591e345e1]
22
- - Updated dependencies [4de5303c5]
23
- - Updated dependencies [95b139dc1]
24
- - Updated dependencies [1d00f2d06]
25
- - Updated dependencies [4ac7b2a1e]
26
- - Updated dependencies [e9598c71]
27
- - Updated dependencies [4998a9631]
28
- - Updated dependencies [804d2bd6]
29
- - Updated dependencies [3909d3224]
30
- - Updated dependencies [6a3d8a63]
31
- - Updated dependencies [e18836b20]
32
- - @akinon/next@2.0.64-rc.0
33
- - @akinon/theme-contract@2.0.64-rc.0
3
+ ## 2.0.65-beta.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [cbbbfd75]
8
+ - @akinon/next@2.0.65-beta.0
9
+ - @akinon/theme-contract@2.0.65-beta.0
10
+
11
+ ## 2.0.64
12
+
13
+ ### Patch Changes
14
+
15
+ - d0687da: ZERO-4814: Add modal width units, backdrop paint and account-button guest modal trigger
16
+ - @akinon/next@2.0.64
17
+ - @akinon/theme-contract@2.0.64
34
18
 
35
19
  ## 2.0.63
36
20
 
package/package.json CHANGED
@@ -1,26 +1,26 @@
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.64-rc.0",
4
+ "version": "2.0.65-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.64-rc.0",
11
+ "@akinon/next": "2.0.65-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.64-rc.0",
16
+ "@akinon/theme-contract": "2.0.65-beta.0",
17
17
  "clsx": "2.1.1",
18
18
  "lottie-web": "^5.13.0",
19
19
  "tailwind-merge": "^2.5.4",
20
20
  "zod": "4.4.1"
21
21
  },
22
22
  "devDependencies": {
23
- "@akinon/next": "2.0.64-rc.0",
23
+ "@akinon/next": "2.0.65-beta.0",
24
24
  "@testing-library/jest-dom": "5.16.4",
25
25
  "@testing-library/react": "16.3.2",
26
26
  "@testing-library/user-event": "14.1.1",
@@ -0,0 +1,115 @@
1
+ import React from 'react';
2
+ import { fireEvent, render } from '@testing-library/react';
3
+ import AccountButtonBlock from '../account-button-block';
4
+ import { __resetOverlayBusForTests } from '../../overlays/overlay-bus';
5
+
6
+ /**
7
+ * The account button is the session fork of the header: a guest gets the way
8
+ * IN (login page, or a composed login modal opened in place), a signed-in
9
+ * shopper gets their account. The modal branch must never leak to the
10
+ * signed-in side — that is the whole reason it is this block's own property
11
+ * rather than the shared, unconditional On Click action.
12
+ */
13
+
14
+ let mockSessionStatus = 'unauthenticated';
15
+
16
+ jest.mock('next-auth/react', () => ({
17
+ useSession: () => ({ data: null, status: mockSessionStatus }),
18
+ signIn: () => {},
19
+ signOut: () => {}
20
+ }));
21
+
22
+ const renderBlock = (
23
+ properties: Record<string, unknown> = {},
24
+ extra: Record<string, unknown> = {}
25
+ ) =>
26
+ render(
27
+ <AccountButtonBlock
28
+ {...({
29
+ block: {
30
+ id: 'account',
31
+ type: 'account-button',
32
+ label: 'Account',
33
+ properties,
34
+ styles: {},
35
+ order: 0,
36
+ hidden: false
37
+ },
38
+ placeholderId: 'header',
39
+ sectionId: 's1',
40
+ isDesigner: false,
41
+ ...extra
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
+ } as any)}
44
+ />
45
+ );
46
+
47
+ const link = (container: HTMLElement) =>
48
+ container.querySelector('[data-account-button]') as HTMLAnchorElement;
49
+
50
+ beforeEach(() => {
51
+ mockSessionStatus = 'unauthenticated';
52
+ __resetOverlayBusForTests();
53
+ });
54
+
55
+ describe('AccountButtonBlock', () => {
56
+ it('sends a guest to the login page by default', () => {
57
+ const { container } = renderBlock();
58
+
59
+ expect(link(container).getAttribute('href')).toBe('/auth');
60
+ expect(link(container).getAttribute('data-auth-state')).toBe('guest');
61
+ expect(link(container).getAttribute('aria-haspopup')).toBeNull();
62
+ });
63
+
64
+ it('sends a signed-in shopper to their account', () => {
65
+ mockSessionStatus = 'authenticated';
66
+ const { container } = renderBlock();
67
+
68
+ expect(link(container).getAttribute('href')).toBe('/account');
69
+ expect(link(container).getAttribute('data-auth-state')).toBe(
70
+ 'authenticated'
71
+ );
72
+ });
73
+
74
+ it('opens the configured modal for a guest instead of navigating', () => {
75
+ const { container } = renderBlock({
76
+ guestClick: 'open-modal',
77
+ guestModalId: 'm-login'
78
+ });
79
+
80
+ // Announced as a dialog trigger, with the login page kept as the
81
+ // no-JavaScript fallback on the anchor itself.
82
+ expect(link(container).getAttribute('aria-haspopup')).toBe('dialog');
83
+ expect(link(container).getAttribute('aria-expanded')).toBe('false');
84
+ expect(link(container).getAttribute('href')).toBe('/auth');
85
+
86
+ const click = fireEvent.click(link(container));
87
+
88
+ // The navigation is cancelled and the overlay bus now holds the modal
89
+ // open — aria-expanded tracks the bus, so it is the end-to-end proof.
90
+ expect(click).toBe(false);
91
+ expect(link(container).getAttribute('aria-expanded')).toBe('true');
92
+ });
93
+
94
+ it('never offers the modal to a signed-in shopper', () => {
95
+ mockSessionStatus = 'authenticated';
96
+ const { container } = renderBlock({
97
+ guestClick: 'open-modal',
98
+ guestModalId: 'm-login'
99
+ });
100
+
101
+ expect(link(container).getAttribute('href')).toBe('/account');
102
+ expect(link(container).getAttribute('aria-haspopup')).toBeNull();
103
+ });
104
+
105
+ it('stays inert in the designer, where a click means selection', () => {
106
+ const { container } = renderBlock(
107
+ { guestClick: 'open-modal', guestModalId: 'm-login' },
108
+ { isDesigner: true }
109
+ );
110
+
111
+ fireEvent.click(link(container));
112
+
113
+ expect(link(container).getAttribute('aria-expanded')).toBe('false');
114
+ });
115
+ });
@@ -4,6 +4,7 @@ import React from 'react';
4
4
  import NextLink from 'next/link';
5
5
  import { useSession } from 'next-auth/react';
6
6
  import { getResponsiveValue, partAttrs } from '../utils';
7
+ import useBlockAction from '../hooks/use-block-action';
7
8
  import { BlockRendererProps } from './block-renderer-registry';
8
9
  import { readToggle } from './product-block-utils';
9
10
 
@@ -22,8 +23,17 @@ const clampNumber = (
22
23
  * Functional chrome block: session-aware account entry point. Renders a
23
24
  * login link for guests and an account link for authenticated users; the
24
25
  * editor owns placement, labels, icon and styling.
25
- * Properties: accountHref, loginHref, accountLabel, loginLabel, showLabel,
26
- * showIcon, iconSize, gap, labelPosition, ariaLabel.
26
+ *
27
+ * A guest click can also open a composed modal instead of navigating
28
+ * (`guestClick: 'open-modal'` + `guestModalId`) — the login form arrives in
29
+ * place, while a signed-in shopper keeps going straight to their account.
30
+ * That branch is THIS block's, not the shared On Click action, because the
31
+ * shared action is unconditional: pointed at a login modal it would open it
32
+ * for the signed-in shopper too, who has nothing to log in to. The Login URL
33
+ * stays on the anchor as the no-JavaScript fallback.
34
+ *
35
+ * Properties: accountHref, loginHref, guestClick, guestModalId, accountLabel,
36
+ * loginLabel, showLabel, showIcon, iconSize, gap, labelPosition, ariaLabel.
27
37
  * Parts: link, icon, label. The signed-in state stays readable from custom
28
38
  * CSS through the block's own `data-auth-state` attribute.
29
39
  */
@@ -46,6 +56,25 @@ const AccountButtonBlock = ({
46
56
  const label = isAuthenticated
47
57
  ? prop('accountLabel', 'Account')
48
58
  : prop('loginLabel', 'Login');
59
+
60
+ const guestModalId = prop('guestModalId', '');
61
+ const guestOpensModal =
62
+ !isAuthenticated &&
63
+ prop('guestClick', 'navigate') === 'open-modal' &&
64
+ Boolean(guestModalId);
65
+ // Routed through the shared action hook so a guest trigger behaves exactly
66
+ // like every other modal trigger: inert in the designer, announced as a
67
+ // dialog trigger, aria-expanded tracking the bus. The synthetic properties
68
+ // make the hook act only on the guest branch.
69
+ const guestModalAction = useBlockAction(
70
+ {
71
+ properties: {
72
+ actionType: guestOpensModal ? 'open-modal' : 'none',
73
+ actionTarget: guestModalId
74
+ }
75
+ },
76
+ { currentBreakpoint, isDesigner }
77
+ );
49
78
  const showLabel =
50
79
  getResponsiveValue(
51
80
  block.properties?.showLabel,
@@ -94,10 +123,17 @@ const AccountButtonBlock = ({
94
123
  data-auth-state={isAuthenticated ? 'authenticated' : 'guest'}
95
124
  {...partAttrs('link')}
96
125
  aria-label={ariaLabel || label}
126
+ {...guestModalAction.ariaProps}
97
127
  // Navigating inside the designer iframe would drop the author out of the
98
128
  // page they are editing.
99
129
  onClick={(event) => {
100
- if (isDesigner) event.preventDefault();
130
+ if (isDesigner) {
131
+ event.preventDefault();
132
+ return;
133
+ }
134
+ // Prevents the navigation itself when it fires — a trigger that also
135
+ // navigated would race its own modal.
136
+ guestModalAction.onClick?.(event);
101
137
  }}
102
138
  style={{
103
139
  display: 'inline-flex',