@alpaca-headless/alpaca-headless-nextjs 1.0.4615 → 1.0.4617

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.
@@ -1,18 +1,35 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import React, { Fragment, useCallback, useEffect, useState, } from "react";
3
+ import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState, } from "react";
4
4
  export function MultiComponentEditor({ children, component, placeholderName, thumbZoom, thumbWidth, thumbHeight, onSelectedComponentChange, }) {
5
- var _a, _b;
5
+ var _a, _b, _c;
6
6
  const [selectedId, setSelectedId] = useState(null);
7
+ const thumbnailContainerRef = useRef(null);
7
8
  const placeholder = component.placeholders.find((x) => x.name === placeholderName);
8
- if (!placeholder)
9
- return;
10
- const components = placeholder.components;
11
- if (!components)
12
- return;
9
+ const components = useMemo(() => { var _a; return (_a = placeholder === null || placeholder === void 0 ? void 0 : placeholder.components) !== null && _a !== void 0 ? _a : null; }, [placeholder]);
13
10
  const handleMessage = useCallback((event) => {
14
- if (event.data.type === "componentsSelected") {
15
- setSelectedId(event.data.componentIds[0]);
11
+ var _a, _b, _c, _d;
12
+ const data = event.data;
13
+ if (!data || !components)
14
+ return;
15
+ if (data.type === "componentsSelected") {
16
+ setSelectedId((_b = (_a = data.componentIds) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : null);
17
+ return;
18
+ }
19
+ // Parhelia (editing-bridge) selects the surrounding component as a
20
+ // whole, so only ids that resolve to one of our items may change the
21
+ // selection — everything else would snap back to the first item.
22
+ if (data.family === "parhelia/editing-bridge" &&
23
+ data.name === "setSelection") {
24
+ const ids = (_d = (_c = data.payload) === null || _c === void 0 ? void 0 : _c.componentIds) !== null && _d !== void 0 ? _d : [];
25
+ const matches = components.filter((c) => ids.some((id) => matchesComponent(id, c)));
26
+ if (matches.length === 1) {
27
+ setSelectedId(matches[0].id);
28
+ }
29
+ else if (matches.length > 1) {
30
+ console.warn("[MultiComponentEditor] ambiguous setSelection: %d components matched ids %o, selecting first", matches.length, ids);
31
+ setSelectedId(matches[0].id);
32
+ }
16
33
  }
17
34
  }, [components]);
18
35
  useEffect(() => {
@@ -20,11 +37,35 @@ export function MultiComponentEditor({ children, component, placeholderName, thu
20
37
  return () => {
21
38
  window.removeEventListener("message", handleMessage);
22
39
  };
40
+ }, [handleMessage]);
41
+ // Parhelia's interaction overlay swallows click and pointerup before they
42
+ // reach the thumbnails (its pointer capture retargets the release event),
43
+ // but pointerdown still hits the DOM — select thumbnails with a native
44
+ // capture listener so switching works in both editors.
45
+ useEffect(() => {
46
+ const container = thumbnailContainerRef.current;
47
+ if (!container)
48
+ return;
49
+ const handlePointerDown = (event) => {
50
+ var _a;
51
+ if (event.button !== 0)
52
+ return;
53
+ const thumb = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest("[data-mce-component-id]");
54
+ const id = thumb === null || thumb === void 0 ? void 0 : thumb.getAttribute("data-mce-component-id");
55
+ if (id) {
56
+ setSelectedId(id);
57
+ selectComponents([id]);
58
+ }
59
+ };
60
+ container.addEventListener("pointerdown", handlePointerDown, true);
61
+ return () => {
62
+ container.removeEventListener("pointerdown", handlePointerDown, true);
63
+ };
23
64
  }, []);
24
- const selectedIndex = components.findIndex((x) => x.id === selectedId);
65
+ const selectedIndex = (_a = components === null || components === void 0 ? void 0 : components.findIndex((x) => x.id === selectedId)) !== null && _a !== void 0 ? _a : -1;
25
66
  const resolvedIndex = selectedIndex < 0 ? 0 : selectedIndex;
26
- const resolvedComponent = (_a = components[resolvedIndex]) !== null && _a !== void 0 ? _a : null;
27
- const resolvedId = (_b = resolvedComponent === null || resolvedComponent === void 0 ? void 0 : resolvedComponent.id) !== null && _b !== void 0 ? _b : null;
67
+ const resolvedComponent = (_b = components === null || components === void 0 ? void 0 : components[resolvedIndex]) !== null && _b !== void 0 ? _b : null;
68
+ const resolvedId = (_c = resolvedComponent === null || resolvedComponent === void 0 ? void 0 : resolvedComponent.id) !== null && _c !== void 0 ? _c : null;
28
69
  useEffect(() => {
29
70
  if (resolvedId !== null && selectedId !== resolvedId) {
30
71
  setSelectedId(resolvedId);
@@ -36,9 +77,11 @@ export function MultiComponentEditor({ children, component, placeholderName, thu
36
77
  selectedIndex: resolvedIndex,
37
78
  });
38
79
  }, [resolvedId, resolvedIndex, onSelectedComponentChange]);
80
+ if (!placeholder || !components)
81
+ return;
39
82
  const childrenArray = React.Children.toArray(children);
40
83
  const filteredChildren = childrenArray.filter((child) => child.type !== "script");
41
- return (_jsxs(_Fragment, { children: [filteredChildren[resolvedIndex], _jsxs("div", { className: "a-editor-mce", children: [_jsx("script", { "data-placeholder-start": placeholder.key, "data-orientation": "horizontal" }), filteredChildren.map((x, index) => thumbnail(x, index)), _jsx("script", { "data-placeholder-end": placeholder.key })] })] }));
84
+ return (_jsxs(_Fragment, { children: [filteredChildren[resolvedIndex], _jsxs("div", { className: "a-editor-mce", ref: thumbnailContainerRef, children: [_jsx("script", { "data-placeholder-start": placeholder.key, "data-orientation": "horizontal" }), filteredChildren.map((x, index) => thumbnail(x, index)), _jsx("script", { "data-placeholder-end": placeholder.key })] })] }));
42
85
  function thumbnail(x, index) {
43
86
  var _a, _b, _c, _d, _e, _f, _g;
44
87
  const style = {};
@@ -48,14 +91,31 @@ export function MultiComponentEditor({ children, component, placeholderName, thu
48
91
  if (thumbWidth) {
49
92
  style.width = thumbWidth;
50
93
  }
94
+ // Mid-transition renders (RSC refetch, fresh inserts) can deliver child
95
+ // elements whose nesting doesn't match yet — render an empty tile for
96
+ // that frame instead of crashing into the placeholder error boundary.
51
97
  const componentArray = (_c = (_b = (_a = x.props) === null || _a === void 0 ? void 0 : _a.children) === null || _b === void 0 ? void 0 : _b.props) === null || _c === void 0 ? void 0 : _c.children;
98
+ const thumbnailContent = Array.isArray(componentArray)
99
+ ? componentArray.slice(1, -1)
100
+ : null;
52
101
  const component = components[index];
53
102
  return (_jsxs(Fragment, { children: [_jsxs("div", { className: "a-editor-mce-thumbnail" +
54
- (index === resolvedIndex ? " a-editor-mce-thumbnail--selected" : ""), style: style, onClick: () => {
55
- selectComponents([components[index].id]);
56
- }, children: [_jsx("script", { "data-component-start": component.id, "data-itemid": ((_e = (_d = component._editor) === null || _d === void 0 ? void 0 : _d.linkedComponentItem) === null || _e === void 0 ? void 0 : _e.id) || component.id, "data-layoutid": (_g = (_f = component._editor) === null || _f === void 0 ? void 0 : _f.hostingPageItem) === null || _g === void 0 ? void 0 : _g.id }), _jsx("div", { style: { pointerEvents: "none", zoom: thumbZoom || 0.25 }, children: componentArray.slice(1, -1) }), _jsx("script", { "data-component-end": component.id })] }), _jsx("div", { "data-dropzone": components[index].id })] }, index));
103
+ (index === resolvedIndex ? " a-editor-mce-thumbnail--selected" : ""), style: style, "data-mce-component-id": component.id, children: [_jsx("script", { "data-component-start": component.id, "data-itemid": ((_e = (_d = component._editor) === null || _d === void 0 ? void 0 : _d.linkedComponentItem) === null || _e === void 0 ? void 0 : _e.id) || component.id, "data-layoutid": (_g = (_f = component._editor) === null || _f === void 0 ? void 0 : _f.hostingPageItem) === null || _g === void 0 ? void 0 : _g.id }), _jsx("div", { style: { pointerEvents: "none", zoom: thumbZoom || 0.25 }, children: thumbnailContent }), _jsx("script", { "data-component-end": component.id })] }), _jsx("div", { "data-dropzone": component.id })] }, index));
57
104
  }
58
105
  }
106
+ function matchesComponent(id, component) {
107
+ var _a, _b;
108
+ const normalized = normalizeId(id);
109
+ if (!normalized)
110
+ return false;
111
+ return (normalized === normalizeId(component.id) ||
112
+ normalized === normalizeId((_b = (_a = component._editor) === null || _a === void 0 ? void 0 : _a.linkedComponentItem) === null || _b === void 0 ? void 0 : _b.id));
113
+ }
114
+ function normalizeId(id) {
115
+ // Like Editor/packages/core/src/editor/utils.ts#stripGuid, but also removes
116
+ // dashes to handle Parhelia's editing-bridge id format variations.
117
+ return id ? id.replace(/[{}-]/g, "").toLowerCase() : null;
118
+ }
59
119
  function selectComponents(componentIds) {
60
120
  var _a;
61
121
  (_a = window.top) === null || _a === void 0 ? void 0 : _a.window.postMessage({ componentIds, type: "componentsSelected" }, "*");
@@ -1,29 +1,21 @@
1
1
  import axios from "axios";
2
- // import { HttpsProxyAgent } from "https-proxy-agent";
3
2
  import { NextResponse } from "next/server";
4
- async function GET(req, target
5
- //{ proxy }: { proxy?: string }
6
- ) {
3
+ async function GET(req, target) {
7
4
  const reqUrl = new URL(req.url);
8
5
  const url = target + reqUrl.pathname + "?" + reqUrl.searchParams.toString();
9
6
  const headers = {
10
7
  Authorization: req.headers.get("Authorization") || "",
11
8
  Cookie: req.headers.get("Cookie") || "",
12
9
  };
13
- // const agent = proxy ? new HttpsProxyAgent(proxy) : undefined;
14
10
  const externalResponse = await axios.get(url, {
15
11
  headers: headers,
16
- // httpAgent: agent,
17
- // httpsAgent: agent,
18
12
  responseType: "stream",
19
13
  });
20
14
  return new NextResponse(nodeReadableToReadableStream(externalResponse.data), {
21
15
  status: externalResponse.status,
22
16
  });
23
17
  }
24
- async function POST(req
25
- //{ proxy }: { proxy?: string }
26
- ) {
18
+ async function POST(req) {
27
19
  const reqUrl = new URL(req.url);
28
20
  const body = await req.text();
29
21
  const url = process.env.EDITOR_SERVICE_URL +
@@ -35,11 +27,8 @@ async function POST(req
35
27
  Authorization: req.headers.get("Authorization") || "",
36
28
  Cookie: req.headers.get("Cookie") || "",
37
29
  };
38
- // const agent = proxy ? new HttpsProxyAgent(proxy) : undefined;
39
30
  const externalResponse = await axios.post(url, body, {
40
31
  headers: headers,
41
- // httpAgent: agent,
42
- // httpsAgent: agent,
43
32
  responseType: "stream",
44
33
  });
45
34
  const responseHeaders = getResponseHeaders(externalResponse);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alpaca-headless/alpaca-headless-nextjs",
3
- "version": "1.0.4615",
3
+ "version": "1.0.4617",
4
4
  "type": "module",
5
5
  "description": "Alpaca Headless",
6
6
  "main": "dist/index.js",
@@ -44,17 +44,15 @@
44
44
  },
45
45
  "homepage": "https://github.com/yourusername/your-module-name#readme",
46
46
  "devDependencies": {
47
- "@types/node": "^20.14.2",
48
- "@types/react": "19.2.2",
49
- "@types/react-dom": "19.2.2",
50
- "eslint": "^9.22.0",
47
+ "@types/node": "^25.5.0",
48
+ "@types/react": "19.2.14",
49
+ "eslint": "^9.39.4",
51
50
  "eslint-config-next": "16.2.6",
52
- "typescript": "^5"
51
+ "typescript": "5.9.3"
53
52
  },
54
53
  "dependencies": {
55
- "@alpaca-headless/alpaca-headless": "^1.0.4615",
54
+ "@alpaca-headless/alpaca-headless": "^1.0.4617",
56
55
  "axios": "^1.16.1",
57
- "https-proxy-agent": "^7.0.4",
58
56
  "next": "16.2.6",
59
57
  "react": "19.2.4",
60
58
  "react-dom": "19.2.4",
@@ -7,6 +7,8 @@ import React, {
7
7
  ReactNode,
8
8
  useCallback,
9
9
  useEffect,
10
+ useMemo,
11
+ useRef,
10
12
  useState,
11
13
  } from "react";
12
14
 
@@ -33,17 +35,46 @@ export function MultiComponentEditor({
33
35
  onSelectedComponentChange,
34
36
  }: MultiComponentEditorProps) {
35
37
  const [selectedId, setSelectedId] = useState<string | null>(null);
38
+ const thumbnailContainerRef = useRef<HTMLDivElement | null>(null);
36
39
  const placeholder = component.placeholders.find(
37
40
  (x) => x.name === placeholderName
38
41
  );
39
- if (!placeholder) return;
40
- const components = placeholder.components;
41
- if (!components) return;
42
+ const components = useMemo(
43
+ () => placeholder?.components ?? null,
44
+ [placeholder]
45
+ );
42
46
 
43
47
  const handleMessage = useCallback(
44
48
  (event: MessageEvent) => {
45
- if (event.data.type === "componentsSelected") {
46
- setSelectedId(event.data.componentIds[0]);
49
+ const data = event.data;
50
+ if (!data || !components) return;
51
+
52
+ if (data.type === "componentsSelected") {
53
+ setSelectedId(data.componentIds?.[0] ?? null);
54
+ return;
55
+ }
56
+
57
+ // Parhelia (editing-bridge) selects the surrounding component as a
58
+ // whole, so only ids that resolve to one of our items may change the
59
+ // selection — everything else would snap back to the first item.
60
+ if (
61
+ data.family === "parhelia/editing-bridge" &&
62
+ data.name === "setSelection"
63
+ ) {
64
+ const ids: string[] = data.payload?.componentIds ?? [];
65
+ const matches = components.filter((c: ComponentData) =>
66
+ ids.some((id) => matchesComponent(id, c))
67
+ );
68
+ if (matches.length === 1) {
69
+ setSelectedId(matches[0].id);
70
+ } else if (matches.length > 1) {
71
+ console.warn(
72
+ "[MultiComponentEditor] ambiguous setSelection: %d components matched ids %o, selecting first",
73
+ matches.length,
74
+ ids
75
+ );
76
+ setSelectedId(matches[0].id);
77
+ }
47
78
  }
48
79
  },
49
80
  [components]
@@ -55,13 +86,38 @@ export function MultiComponentEditor({
55
86
  return () => {
56
87
  window.removeEventListener("message", handleMessage);
57
88
  };
89
+ }, [handleMessage]);
90
+
91
+ // Parhelia's interaction overlay swallows click and pointerup before they
92
+ // reach the thumbnails (its pointer capture retargets the release event),
93
+ // but pointerdown still hits the DOM — select thumbnails with a native
94
+ // capture listener so switching works in both editors.
95
+ useEffect(() => {
96
+ const container = thumbnailContainerRef.current;
97
+ if (!container) return;
98
+
99
+ const handlePointerDown = (event: PointerEvent) => {
100
+ if (event.button !== 0) return;
101
+ const thumb = (event.target as Element | null)?.closest(
102
+ "[data-mce-component-id]"
103
+ );
104
+ const id = thumb?.getAttribute("data-mce-component-id");
105
+ if (id) {
106
+ setSelectedId(id);
107
+ selectComponents([id]);
108
+ }
109
+ };
110
+
111
+ container.addEventListener("pointerdown", handlePointerDown, true);
112
+ return () => {
113
+ container.removeEventListener("pointerdown", handlePointerDown, true);
114
+ };
58
115
  }, []);
59
116
 
60
- const selectedIndex = components.findIndex(
61
- (x: ComponentData) => x.id === selectedId
62
- );
117
+ const selectedIndex =
118
+ components?.findIndex((x: ComponentData) => x.id === selectedId) ?? -1;
63
119
  const resolvedIndex = selectedIndex < 0 ? 0 : selectedIndex;
64
- const resolvedComponent = components[resolvedIndex] ?? null;
120
+ const resolvedComponent = components?.[resolvedIndex] ?? null;
65
121
  const resolvedId = resolvedComponent?.id ?? null;
66
122
 
67
123
  useEffect(() => {
@@ -77,6 +133,8 @@ export function MultiComponentEditor({
77
133
  });
78
134
  }, [resolvedId, resolvedIndex, onSelectedComponentChange]);
79
135
 
136
+ if (!placeholder || !components) return;
137
+
80
138
  const childrenArray = React.Children.toArray(children) as ReactElement[];
81
139
 
82
140
  const filteredChildren = childrenArray.filter(
@@ -86,7 +144,7 @@ export function MultiComponentEditor({
86
144
  return (
87
145
  <>
88
146
  {filteredChildren[resolvedIndex]}
89
- <div className="a-editor-mce">
147
+ <div className="a-editor-mce" ref={thumbnailContainerRef}>
90
148
  <script
91
149
  data-placeholder-start={placeholder.key}
92
150
  data-orientation="horizontal"
@@ -112,7 +170,13 @@ export function MultiComponentEditor({
112
170
  style.width = thumbWidth;
113
171
  }
114
172
 
173
+ // Mid-transition renders (RSC refetch, fresh inserts) can deliver child
174
+ // elements whose nesting doesn't match yet — render an empty tile for
175
+ // that frame instead of crashing into the placeholder error boundary.
115
176
  const componentArray = (x as any).props?.children?.props?.children;
177
+ const thumbnailContent = Array.isArray(componentArray)
178
+ ? componentArray.slice(1, -1)
179
+ : null;
116
180
 
117
181
  const component = components![index];
118
182
 
@@ -124,9 +188,7 @@ export function MultiComponentEditor({
124
188
  (index === resolvedIndex ? " a-editor-mce-thumbnail--selected" : "")
125
189
  }
126
190
  style={style}
127
- onClick={() => {
128
- selectComponents([components![index].id]);
129
- }}
191
+ data-mce-component-id={component.id}
130
192
  >
131
193
  <script
132
194
  data-component-start={component.id}
@@ -136,16 +198,31 @@ export function MultiComponentEditor({
136
198
  data-layoutid={component._editor?.hostingPageItem?.id}
137
199
  />
138
200
  <div style={{ pointerEvents: "none", zoom: thumbZoom || 0.25 }}>
139
- {componentArray.slice(1, -1)}
201
+ {thumbnailContent}
140
202
  </div>
141
203
  <script data-component-end={component.id} />
142
204
  </div>
143
- <div data-dropzone={components[index].id}></div>
205
+ <div data-dropzone={component.id}></div>
144
206
  </Fragment>
145
207
  );
146
208
  }
147
209
  }
148
210
 
211
+ function matchesComponent(id: string, component: ComponentData) {
212
+ const normalized = normalizeId(id);
213
+ if (!normalized) return false;
214
+ return (
215
+ normalized === normalizeId(component.id) ||
216
+ normalized === normalizeId(component._editor?.linkedComponentItem?.id)
217
+ );
218
+ }
219
+
220
+ function normalizeId(id: string | undefined | null) {
221
+ // Like Editor/packages/core/src/editor/utils.ts#stripGuid, but also removes
222
+ // dashes to handle Parhelia's editing-bridge id format variations.
223
+ return id ? id.replace(/[{}-]/g, "").toLowerCase() : null;
224
+ }
225
+
149
226
  function selectComponents(componentIds: string[]) {
150
227
  window.top?.window.postMessage(
151
228
  { componentIds, type: "componentsSelected" },
@@ -1,11 +1,9 @@
1
1
  import axios from "axios";
2
- // import { HttpsProxyAgent } from "https-proxy-agent";
3
2
  import { NextResponse } from "next/server";
4
3
 
5
4
  async function GET(
6
5
  req: Request,
7
6
  target: string
8
- //{ proxy }: { proxy?: string }
9
7
  ) {
10
8
  const reqUrl = new URL(req.url);
11
9
 
@@ -17,12 +15,8 @@ async function GET(
17
15
  Cookie: req.headers.get("Cookie") || "",
18
16
  };
19
17
 
20
- // const agent = proxy ? new HttpsProxyAgent(proxy) : undefined;
21
-
22
18
  const externalResponse = await axios.get(url, {
23
19
  headers: headers,
24
- // httpAgent: agent,
25
- // httpsAgent: agent,
26
20
  responseType: "stream",
27
21
  });
28
22
 
@@ -31,10 +25,7 @@ async function GET(
31
25
  });
32
26
  }
33
27
 
34
- async function POST(
35
- req: Request
36
- //{ proxy }: { proxy?: string }
37
- ) {
28
+ async function POST(req: Request) {
38
29
  const reqUrl = new URL(req.url);
39
30
  const body = await req.text();
40
31
 
@@ -50,12 +41,8 @@ async function POST(
50
41
  Cookie: req.headers.get("Cookie") || "",
51
42
  };
52
43
 
53
- // const agent = proxy ? new HttpsProxyAgent(proxy) : undefined;
54
-
55
44
  const externalResponse = await axios.post(url, body, {
56
45
  headers: headers,
57
- // httpAgent: agent,
58
- // httpsAgent: agent,
59
46
  responseType: "stream",
60
47
  });
61
48
 
package/tsconfig.json CHANGED
@@ -5,6 +5,7 @@
5
5
  "moduleResolution": "bundler",
6
6
  "jsx": "react-jsx",
7
7
  "declaration": true,
8
+ "rootDir": "./src",
8
9
  "outDir": "./dist",
9
10
  "strict": true,
10
11
  "lib": ["dom", "dom.iterable", "es2021"],
@@ -0,0 +1 @@
1
+ {"root":["./src/editorintegration.tsx","./src/editorintegrationclient.tsx","./src/handlebasicauthroute.ts","./src/index.ts","./src/useexposerefreshfunction.ts","./src/client-components/multicomponenteditor.tsx","./src/client-components/index.ts","./src/middleware/handlerequest.ts","./src/middleware/index.ts","./src/proxy/index.ts"],"version":"5.9.3"}