@akinon/pz-theme 2.0.42-beta.0 → 2.0.42
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 +4 -4
- package/package.json +4 -4
- package/src/__tests__/chrome-empty-zone.test.tsx +147 -0
- package/src/__tests__/code-owned-region.test.tsx +90 -0
- package/src/chrome/chrome-fallback.tsx +70 -4
- package/src/chrome/chrome-slot.tsx +13 -3
- package/src/chrome/code-owned-region.tsx +85 -0
- package/src/index.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.42
|
|
3
|
+
## 2.0.42
|
|
4
4
|
|
|
5
5
|
### Patch Changes
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
- @akinon/next@2.0.42
|
|
9
|
-
- @akinon/theme-contract@2.0.42
|
|
7
|
+
- 3521bb92: ZERO-4727: Add an empty-zone designer affordance for fallback-less chrome slots and a code-owned region explainer
|
|
8
|
+
- @akinon/next@2.0.42
|
|
9
|
+
- @akinon/theme-contract@2.0.42
|
|
10
10
|
|
|
11
11
|
## 2.0.41
|
|
12
12
|
|
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.42
|
|
4
|
+
"version": "2.0.42",
|
|
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.42
|
|
11
|
+
"@akinon/next": "2.0.42",
|
|
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.42
|
|
16
|
+
"@akinon/theme-contract": "2.0.42",
|
|
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.42
|
|
23
|
+
"@akinon/next": "2.0.42",
|
|
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,147 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, act } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
import { ChromeFallback } from '../chrome/chrome-fallback';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A slot without brand markup (the checkout zones) has no fallback to hover,
|
|
8
|
+
* so before the empty-zone mode an uncomposed one rendered as literally
|
|
9
|
+
* nothing in the designer — undiscoverable, unhoverable, no way in. These
|
|
10
|
+
* tests pin the band that fixes that, and pin that visitors never see it.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const EDITOR_ORIGIN = 'http://localhost:5174';
|
|
14
|
+
|
|
15
|
+
const pushTheme = (
|
|
16
|
+
sections: unknown[],
|
|
17
|
+
slug = 'checkout-top-body',
|
|
18
|
+
origin = EDITOR_ORIGIN
|
|
19
|
+
) =>
|
|
20
|
+
act(() => {
|
|
21
|
+
fireEvent(
|
|
22
|
+
window,
|
|
23
|
+
new MessageEvent('message', {
|
|
24
|
+
origin,
|
|
25
|
+
data: {
|
|
26
|
+
type: 'UPDATE_THEME',
|
|
27
|
+
data: {
|
|
28
|
+
theme: {
|
|
29
|
+
placeholders: [{ slug, sections }]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('ChromeFallback empty zone in the designer', () => {
|
|
38
|
+
beforeAll(() => {
|
|
39
|
+
Object.defineProperty(window, 'top', {
|
|
40
|
+
configurable: true,
|
|
41
|
+
value: { name: 'designer' }
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('draws a visible band with the affordance for an uncomposed zone', () => {
|
|
46
|
+
render(<ChromeFallback slug="checkout-top-body" emptyZone />);
|
|
47
|
+
|
|
48
|
+
// Visible without hover: a hover-only control on an empty area is the
|
|
49
|
+
// exact invisibility this mode exists to fix.
|
|
50
|
+
expect(
|
|
51
|
+
screen.getByText(/checkout-top-body area has no composition yet/i)
|
|
52
|
+
).toBeInTheDocument();
|
|
53
|
+
expect(
|
|
54
|
+
screen.getByRole('button', { name: /edit in theme editor/i })
|
|
55
|
+
).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('asks the editor to compose the zone when the affordance is used', () => {
|
|
59
|
+
const postMessage = jest.fn();
|
|
60
|
+
Object.defineProperty(window, 'parent', {
|
|
61
|
+
configurable: true,
|
|
62
|
+
value: { postMessage }
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
render(<ChromeFallback slug="checkout-summary-body" emptyZone />);
|
|
66
|
+
|
|
67
|
+
// A frame from the editor is what proves its origin to the bridge;
|
|
68
|
+
// without one there is no trusted target and the message is dropped.
|
|
69
|
+
pushTheme([], 'checkout-summary-body');
|
|
70
|
+
|
|
71
|
+
fireEvent.click(
|
|
72
|
+
screen.getByRole('button', { name: /edit in theme editor/i })
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(postMessage).toHaveBeenCalledTimes(1);
|
|
76
|
+
const [envelope, targetOrigin] = postMessage.mock.calls[0];
|
|
77
|
+
expect(envelope).toMatchObject({
|
|
78
|
+
type: 'REQUEST_CHROME_OVERRIDE',
|
|
79
|
+
data: { slug: 'checkout-summary-body' }
|
|
80
|
+
});
|
|
81
|
+
expect(envelope.protocolVersion).toBeGreaterThan(0);
|
|
82
|
+
expect(targetOrigin).toBe(EDITOR_ORIGIN);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('withdraws once the zone is composed and returns when emptied again', () => {
|
|
86
|
+
render(<ChromeFallback slug="checkout-top-body" emptyZone />);
|
|
87
|
+
|
|
88
|
+
pushTheme([{ id: 'section-abc', type: 'custom' }]);
|
|
89
|
+
expect(
|
|
90
|
+
screen.queryByText(/has no composition yet/i)
|
|
91
|
+
).not.toBeInTheDocument();
|
|
92
|
+
|
|
93
|
+
pushTheme([]);
|
|
94
|
+
expect(screen.getByText(/has no composition yet/i)).toBeInTheDocument();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('starts hidden when the server rendered a composition and returns on removal', () => {
|
|
98
|
+
render(
|
|
99
|
+
<ChromeFallback slug="checkout-bottom-body" emptyZone initiallyOverridden />
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(
|
|
103
|
+
screen.queryByText(/has no composition yet/i)
|
|
104
|
+
).not.toBeInTheDocument();
|
|
105
|
+
|
|
106
|
+
pushTheme([], 'checkout-bottom-body');
|
|
107
|
+
expect(screen.getByText(/has no composition yet/i)).toBeInTheDocument();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('ignores a theme pushed from an untrusted origin', () => {
|
|
111
|
+
render(<ChromeFallback slug="checkout-top-body" emptyZone />);
|
|
112
|
+
|
|
113
|
+
pushTheme(
|
|
114
|
+
[{ id: 'section-abc', type: 'custom' }],
|
|
115
|
+
'checkout-top-body',
|
|
116
|
+
'https://evil.example'
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// An embedding page must not be able to toggle designer chrome.
|
|
120
|
+
expect(screen.getByText(/has no composition yet/i)).toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('ChromeFallback empty zone on the live site', () => {
|
|
125
|
+
beforeAll(() => {
|
|
126
|
+
Object.defineProperty(window, 'top', {
|
|
127
|
+
configurable: true,
|
|
128
|
+
value: window
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('renders nothing for an uncomposed zone', () => {
|
|
133
|
+
const { container } = render(
|
|
134
|
+
<ChromeFallback slug="checkout-top-body" emptyZone />
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(container).toBeEmptyDOMElement();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('renders nothing under a server-rendered composition', () => {
|
|
141
|
+
const { container } = render(
|
|
142
|
+
<ChromeFallback slug="checkout-top-body" emptyZone initiallyOverridden />
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(container).toBeEmptyDOMElement();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
import { CodeOwnedRegion } from '../chrome/code-owned-region';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The checkout flow offers no takeover affordance on purpose, but silence
|
|
8
|
+
* reads as breakage: a designer hovering it sees the same nothing a bug would
|
|
9
|
+
* produce. These tests pin the explainer that fills that gap — and pin that
|
|
10
|
+
* it never leaks to visitors and never blocks the flow it wraps.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
describe('CodeOwnedRegion in the designer', () => {
|
|
14
|
+
beforeAll(() => {
|
|
15
|
+
Object.defineProperty(window, 'top', {
|
|
16
|
+
configurable: true,
|
|
17
|
+
value: { name: 'designer' }
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('explains the region on hover and says where the composable zones are', () => {
|
|
22
|
+
render(
|
|
23
|
+
<CodeOwnedRegion label="checkout flow">
|
|
24
|
+
<div data-testid="flow">payment steps</div>
|
|
25
|
+
</CodeOwnedRegion>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
expect(screen.queryByText(/code-owned/i)).not.toBeInTheDocument();
|
|
29
|
+
|
|
30
|
+
fireEvent.mouseEnter(screen.getByTestId('flow').parentElement!);
|
|
31
|
+
|
|
32
|
+
const note = screen.getByText(/checkout flow is code-owned/i);
|
|
33
|
+
expect(note).toBeInTheDocument();
|
|
34
|
+
expect(note.textContent).toMatch(/zones above and below/i);
|
|
35
|
+
|
|
36
|
+
fireEvent.mouseLeave(screen.getByTestId('flow').parentElement!);
|
|
37
|
+
expect(screen.queryByText(/code-owned/i)).not.toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('never intercepts the pointer, so the flow stays usable in the preview', () => {
|
|
41
|
+
render(
|
|
42
|
+
<CodeOwnedRegion label="checkout flow">
|
|
43
|
+
<div data-testid="flow">payment steps</div>
|
|
44
|
+
</CodeOwnedRegion>
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
fireEvent.mouseEnter(screen.getByTestId('flow').parentElement!);
|
|
48
|
+
|
|
49
|
+
const overlay = screen.getByText(/code-owned/i).parentElement!;
|
|
50
|
+
expect(overlay).toHaveStyle({ pointerEvents: 'none' });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('keeps the children rendered and interactive underneath', () => {
|
|
54
|
+
const onClick = jest.fn();
|
|
55
|
+
render(
|
|
56
|
+
<CodeOwnedRegion label="checkout flow">
|
|
57
|
+
<button type="button" onClick={onClick}>
|
|
58
|
+
continue
|
|
59
|
+
</button>
|
|
60
|
+
</CodeOwnedRegion>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
fireEvent.mouseEnter(screen.getByRole('button').parentElement!);
|
|
64
|
+
fireEvent.click(screen.getByRole('button', { name: /continue/i }));
|
|
65
|
+
|
|
66
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('CodeOwnedRegion on the live site', () => {
|
|
71
|
+
beforeAll(() => {
|
|
72
|
+
Object.defineProperty(window, 'top', {
|
|
73
|
+
configurable: true,
|
|
74
|
+
value: window
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('renders the children and no explainer, hovered or not', () => {
|
|
79
|
+
render(
|
|
80
|
+
<CodeOwnedRegion label="checkout flow">
|
|
81
|
+
<div data-testid="flow">payment steps</div>
|
|
82
|
+
</CodeOwnedRegion>
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
expect(screen.getByTestId('flow')).toBeInTheDocument();
|
|
86
|
+
|
|
87
|
+
fireEvent.mouseEnter(screen.getByTestId('flow').parentElement!);
|
|
88
|
+
expect(screen.queryByText(/code-owned/i)).not.toBeInTheDocument();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -18,10 +18,11 @@ export function ChromeFallback({
|
|
|
18
18
|
slug,
|
|
19
19
|
children,
|
|
20
20
|
wrapperClassName,
|
|
21
|
-
initiallyOverridden = false
|
|
21
|
+
initiallyOverridden = false,
|
|
22
|
+
emptyZone = false
|
|
22
23
|
}: {
|
|
23
24
|
slug: string;
|
|
24
|
-
children
|
|
25
|
+
children?: React.ReactNode;
|
|
25
26
|
/**
|
|
26
27
|
* Applied to the live-site wrapper only. The designer overlay below needs
|
|
27
28
|
* a real positioned box for its scrim, so it deliberately ignores this.
|
|
@@ -34,6 +35,14 @@ export function ChromeFallback({
|
|
|
34
35
|
* project markup has to come back.
|
|
35
36
|
*/
|
|
36
37
|
initiallyOverridden?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* A slot with no brand markup (the checkout zones) has nothing to size the
|
|
40
|
+
* hover overlay, so an uncomposed one is invisible in the designer and the
|
|
41
|
+
* area cannot be discovered at all. This mode draws a slim dashed band with
|
|
42
|
+
* the affordance instead; visitors never see it — the live site renders
|
|
43
|
+
* nothing, exactly as before.
|
|
44
|
+
*/
|
|
45
|
+
emptyZone?: boolean;
|
|
37
46
|
}) {
|
|
38
47
|
const [inDesigner, setInDesigner] = useState(false);
|
|
39
48
|
const [isHovered, setIsHovered] = useState(false);
|
|
@@ -91,8 +100,9 @@ export function ChromeFallback({
|
|
|
91
100
|
|
|
92
101
|
if (!inDesigner) {
|
|
93
102
|
// Composed on the live site: the composition owns the area, the brand
|
|
94
|
-
// markup must never double-render under it.
|
|
95
|
-
|
|
103
|
+
// markup must never double-render under it. An empty zone has nothing to
|
|
104
|
+
// show a visitor either way.
|
|
105
|
+
if (initiallyOverridden || emptyZone) return null;
|
|
96
106
|
|
|
97
107
|
return (
|
|
98
108
|
<div data-chrome-fallback={slug} className={wrapperClassName}>
|
|
@@ -107,6 +117,62 @@ export function ChromeFallback({
|
|
|
107
117
|
postToEditor({ type: 'REQUEST_CHROME_OVERRIDE', data: { slug } });
|
|
108
118
|
};
|
|
109
119
|
|
|
120
|
+
if (emptyZone) {
|
|
121
|
+
// Unlike the brand-markup overlay, the band and its button stay visible
|
|
122
|
+
// without hover: there is no markup to obscure, and a hover-only control
|
|
123
|
+
// on an otherwise empty area is exactly the invisibility this mode fixes.
|
|
124
|
+
return (
|
|
125
|
+
<div
|
|
126
|
+
data-chrome-fallback={slug}
|
|
127
|
+
data-chrome-empty-zone=""
|
|
128
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
129
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
130
|
+
style={{
|
|
131
|
+
display: 'flex',
|
|
132
|
+
alignItems: 'center',
|
|
133
|
+
justifyContent: 'center',
|
|
134
|
+
flexWrap: 'wrap',
|
|
135
|
+
gap: '12px',
|
|
136
|
+
minHeight: '56px',
|
|
137
|
+
padding: '10px 16px',
|
|
138
|
+
margin: '8px 0',
|
|
139
|
+
border: `2px dashed ${isHovered ? '#60a5fa' : '#cbd5e1'}`,
|
|
140
|
+
borderRadius: '6px',
|
|
141
|
+
background: isHovered
|
|
142
|
+
? 'rgba(37, 99, 235, 0.08)'
|
|
143
|
+
: 'rgba(148, 163, 184, 0.08)',
|
|
144
|
+
transition: 'background 120ms ease, border-color 120ms ease'
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
<span
|
|
148
|
+
style={{
|
|
149
|
+
color: '#64748b',
|
|
150
|
+
fontSize: '13px',
|
|
151
|
+
fontWeight: 500
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
The {slug} area has no composition yet
|
|
155
|
+
</span>
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
onClick={requestOverride}
|
|
159
|
+
style={{
|
|
160
|
+
background: '#2563eb',
|
|
161
|
+
color: '#ffffff',
|
|
162
|
+
border: 'none',
|
|
163
|
+
borderRadius: '6px',
|
|
164
|
+
padding: '8px 14px',
|
|
165
|
+
fontSize: '13px',
|
|
166
|
+
fontWeight: 600,
|
|
167
|
+
cursor: 'pointer'
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
Edit in theme editor
|
|
171
|
+
</button>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
110
176
|
return (
|
|
111
177
|
<div
|
|
112
178
|
data-chrome-fallback={slug}
|
|
@@ -68,7 +68,10 @@ export default async function ChromeSlot({
|
|
|
68
68
|
{/* The fallback ships hidden: on the live site it never renders, but
|
|
69
69
|
in the designer it must be able to COME BACK when the last
|
|
70
70
|
section is removed — without it in the tree, deleting a menu or
|
|
71
|
-
header composition would blank the area until a full reload.
|
|
71
|
+
header composition would blank the area until a full reload. The
|
|
72
|
+
same applies to a fallback-less zone: its designer band has to be
|
|
73
|
+
able to return, or removing the last section leaves the area
|
|
74
|
+
undiscoverable again. */}
|
|
72
75
|
{fallback ? (
|
|
73
76
|
<ChromeFallback
|
|
74
77
|
slug={slug}
|
|
@@ -77,7 +80,9 @@ export default async function ChromeSlot({
|
|
|
77
80
|
>
|
|
78
81
|
{fallback}
|
|
79
82
|
</ChromeFallback>
|
|
80
|
-
) :
|
|
83
|
+
) : (
|
|
84
|
+
<ChromeFallback slug={slug} emptyZone initiallyOverridden />
|
|
85
|
+
)}
|
|
81
86
|
<ThemePlaceholder slug={slug} pageContext={pageContext} />
|
|
82
87
|
</div>
|
|
83
88
|
);
|
|
@@ -89,7 +94,12 @@ export default async function ChromeSlot({
|
|
|
89
94
|
<ChromeFallback slug={slug} wrapperClassName={fallbackClassName}>
|
|
90
95
|
{fallback}
|
|
91
96
|
</ChromeFallback>
|
|
92
|
-
) :
|
|
97
|
+
) : (
|
|
98
|
+
// No brand markup to take over: an uncomposed zone would render as
|
|
99
|
+
// nothing at all, so the designer gets a visible band instead. The
|
|
100
|
+
// live site keeps rendering nothing.
|
|
101
|
+
<ChromeFallback slug={slug} emptyZone />
|
|
102
|
+
)}
|
|
93
103
|
{/* Empty until the designer overrides this area from the editor */}
|
|
94
104
|
<ThemePlaceholder slug={slug} pageContext={pageContext} />
|
|
95
105
|
</div>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wraps markup that is deliberately NOT composable — the checkout flow being
|
|
7
|
+
* the canonical case. Without this, hovering the area in the designer gives
|
|
8
|
+
* no feedback at all, which reads as "broken" rather than "code-owned": every
|
|
9
|
+
* other region either offers the takeover affordance or an empty-zone band.
|
|
10
|
+
*
|
|
11
|
+
* On the live site the wrapper is inert (one attribute-carrying div, no
|
|
12
|
+
* styles beyond `position: relative`). In the designer, hover draws a neutral
|
|
13
|
+
* outline and a note explaining why there is no edit affordance here and
|
|
14
|
+
* where the composable zones are. The overlay never intercepts the pointer,
|
|
15
|
+
* so the wrapped flow stays fully usable in the preview.
|
|
16
|
+
*/
|
|
17
|
+
export function CodeOwnedRegion({
|
|
18
|
+
label,
|
|
19
|
+
children
|
|
20
|
+
}: {
|
|
21
|
+
/** Human-readable region name, e.g. "checkout flow". */
|
|
22
|
+
label: string;
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
}) {
|
|
25
|
+
const [inDesigner, setInDesigner] = useState(false);
|
|
26
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (typeof window !== 'undefined' && window.self !== window.top) {
|
|
30
|
+
setInDesigner(true);
|
|
31
|
+
}
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
// The wrapper renders on the live site too: switching between a plain
|
|
36
|
+
// fragment and a wrapped tree at designer-detection time would remount
|
|
37
|
+
// the children — for the checkout flow that means refetching the whole
|
|
38
|
+
// checkout state right after hydration.
|
|
39
|
+
<div
|
|
40
|
+
data-code-owned={label}
|
|
41
|
+
style={{ position: 'relative' }}
|
|
42
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
43
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
44
|
+
>
|
|
45
|
+
{children}
|
|
46
|
+
|
|
47
|
+
{inDesigner && isHovered && (
|
|
48
|
+
<div
|
|
49
|
+
style={{
|
|
50
|
+
position: 'absolute',
|
|
51
|
+
inset: 0,
|
|
52
|
+
outline: '2px dashed #cbd5e1',
|
|
53
|
+
outlineOffset: '-2px',
|
|
54
|
+
// Never intercept the pointer: the region is explained, not locked.
|
|
55
|
+
pointerEvents: 'none',
|
|
56
|
+
display: 'flex',
|
|
57
|
+
alignItems: 'flex-start',
|
|
58
|
+
justifyContent: 'center',
|
|
59
|
+
zIndex: 30
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<span
|
|
63
|
+
style={{
|
|
64
|
+
// Sticky keeps the note readable while a tall region (the whole
|
|
65
|
+
// checkout flow) is scrolled under the pointer.
|
|
66
|
+
position: 'sticky',
|
|
67
|
+
top: '12px',
|
|
68
|
+
marginTop: '12px',
|
|
69
|
+
background: '#0f172a',
|
|
70
|
+
color: '#ffffff',
|
|
71
|
+
borderRadius: '9999px',
|
|
72
|
+
padding: '6px 14px',
|
|
73
|
+
fontSize: '12.5px',
|
|
74
|
+
fontWeight: 500,
|
|
75
|
+
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.25)'
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
The {label} is code-owned and cannot be composed — use the zones
|
|
79
|
+
above and below it
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ export type { Block } from './theme-block';
|
|
|
22
22
|
|
|
23
23
|
export { default as ChromeSlot } from './chrome/chrome-slot';
|
|
24
24
|
export { ChromeFallback } from './chrome/chrome-fallback';
|
|
25
|
+
export { CodeOwnedRegion } from './chrome/code-owned-region';
|
|
25
26
|
export { PreviewAuthStateReporter } from './chrome/preview-auth-state';
|
|
26
27
|
export { LiveBasketPageContext } from './chrome/live-basket-page-context';
|
|
27
28
|
export {
|