@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 +15 -31
- package/package.json +4 -4
- package/src/blocks/__tests__/account-button-block.test.tsx +115 -0
- package/src/blocks/account-button-block.tsx +39 -3
- package/src/blocks/order-detail-block.tsx +600 -124
- package/src/blocks/order-list-block.tsx +477 -101
- package/src/chrome/__tests__/theme-config-source.test.ts +3 -0
- package/src/overlays/__tests__/modal-preview-host.test.tsx +36 -0
- package/src/overlays/__tests__/modal-registry.test.ts +37 -0
- package/src/overlays/modal-preview-host.tsx +6 -0
- package/src/overlays/modal-registry.ts +47 -2
- package/src/overlays/overlay-surface.tsx +76 -6
- package/src/overlays/theme-modal-mount.tsx +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,36 +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 [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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
26
|
-
*
|
|
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)
|
|
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',
|