@designtools/next-plugin 0.1.2 → 0.1.3
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/dist/codesurface.js +357 -0
- package/dist/codesurface.mjs +357 -0
- package/dist/index.js +197 -4
- package/dist/index.mjs +197 -4
- package/package.json +1 -1
- package/src/codesurface.tsx +476 -0
- package/src/index.ts +11 -0
- package/src/preview-route.ts +231 -0
package/dist/codesurface.mjs
CHANGED
|
@@ -99,6 +99,329 @@ function CodeSurface() {
|
|
|
99
99
|
}
|
|
100
100
|
return target;
|
|
101
101
|
}
|
|
102
|
+
const overlayIds = /* @__PURE__ */ new Set(["tool-highlight", "tool-tooltip", "tool-selected", "codesurface-token-preview"]);
|
|
103
|
+
const semanticTags = /* @__PURE__ */ new Set(["header", "main", "nav", "section", "article", "footer", "aside"]);
|
|
104
|
+
const skipTags = /* @__PURE__ */ new Set([
|
|
105
|
+
"html",
|
|
106
|
+
"body",
|
|
107
|
+
"head",
|
|
108
|
+
// document structure
|
|
109
|
+
"br",
|
|
110
|
+
"hr",
|
|
111
|
+
"wbr",
|
|
112
|
+
// void/formatting
|
|
113
|
+
"template",
|
|
114
|
+
"slot"
|
|
115
|
+
// shadow DOM
|
|
116
|
+
]);
|
|
117
|
+
const frameworkPatterns = [
|
|
118
|
+
/^Fragment$/,
|
|
119
|
+
/^Suspense$/,
|
|
120
|
+
/^ErrorBoundary$/,
|
|
121
|
+
/^Provider$/,
|
|
122
|
+
/^Consumer$/,
|
|
123
|
+
/Context$/,
|
|
124
|
+
/^ForwardRef$/,
|
|
125
|
+
/^Memo$/,
|
|
126
|
+
/^Lazy$/,
|
|
127
|
+
// Next.js routing internals
|
|
128
|
+
/^InnerLayoutRouter$/,
|
|
129
|
+
/^OuterLayoutRouter$/,
|
|
130
|
+
/^LayoutRouter$/,
|
|
131
|
+
/^RenderFromTemplateContext$/,
|
|
132
|
+
/^TemplateContext$/,
|
|
133
|
+
/^RedirectBoundary$/,
|
|
134
|
+
/^RedirectErrorBoundary$/,
|
|
135
|
+
/^NotFoundBoundary$/,
|
|
136
|
+
/^LoadingBoundary$/,
|
|
137
|
+
/^HTTPAccessFallbackBoundary$/,
|
|
138
|
+
/^HTTPAccessFallbackErrorBoundary$/,
|
|
139
|
+
/^ClientPageRoot$/,
|
|
140
|
+
/^HotReload$/,
|
|
141
|
+
/^ReactDevOverlay$/,
|
|
142
|
+
/^PathnameContextProviderAdapter$/,
|
|
143
|
+
// Next.js App Router internals (segment tree)
|
|
144
|
+
/^SegmentViewNode$/,
|
|
145
|
+
/^SegmentTrieNode$/,
|
|
146
|
+
/^SegmentViewStateNode$/,
|
|
147
|
+
/^SegmentBoundaryTriggerNode$/,
|
|
148
|
+
/^SegmentStateProvider$/,
|
|
149
|
+
/^ScrollAndFocusHandler$/,
|
|
150
|
+
/^InnerScrollAndFocusHandler$/,
|
|
151
|
+
/^AppRouter$/,
|
|
152
|
+
/^Router$/,
|
|
153
|
+
/^Root$/,
|
|
154
|
+
/^ServerRoot$/,
|
|
155
|
+
/^RootErrorBoundary$/,
|
|
156
|
+
/^ErrorBoundaryHandler$/,
|
|
157
|
+
/^AppRouterAnnouncer$/,
|
|
158
|
+
/^HistoryUpdater$/,
|
|
159
|
+
/^RuntimeStyles$/,
|
|
160
|
+
/^DevRootHTTPAccessFallbackBoundary$/,
|
|
161
|
+
/^AppDevOverlayErrorBoundary$/,
|
|
162
|
+
/^ReplaySsrOnlyErrors$/,
|
|
163
|
+
/^HeadManagerContext$/,
|
|
164
|
+
/^Head$/,
|
|
165
|
+
/^MetadataOutlet$/,
|
|
166
|
+
/^AsyncMetadataOutlet$/,
|
|
167
|
+
/^__next_/
|
|
168
|
+
// All __next_ prefixed components
|
|
169
|
+
];
|
|
170
|
+
function isFrameworkComponent(name) {
|
|
171
|
+
return frameworkPatterns.some((p) => p.test(name));
|
|
172
|
+
}
|
|
173
|
+
function getFiber(el) {
|
|
174
|
+
const key = Object.keys(el).find((k) => k.startsWith("__reactFiber$"));
|
|
175
|
+
return key ? el[key] : null;
|
|
176
|
+
}
|
|
177
|
+
function getDirectText(el) {
|
|
178
|
+
let text = "";
|
|
179
|
+
for (const node of Array.from(el.childNodes)) {
|
|
180
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
181
|
+
text += (node.textContent || "").trim();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return text.slice(0, 40);
|
|
185
|
+
}
|
|
186
|
+
function inferScope(sourcePath) {
|
|
187
|
+
if (!sourcePath) return null;
|
|
188
|
+
const colonIdx = sourcePath.indexOf(":");
|
|
189
|
+
const file = colonIdx > 0 ? sourcePath.slice(0, colonIdx) : sourcePath;
|
|
190
|
+
if (/\/layout\.[tjsx]+$/i.test(file) || /^layout\.[tjsx]+$/i.test(file)) return "layout";
|
|
191
|
+
if (/\/page\.[tjsx]+$/i.test(file) || /^page\.[tjsx]+$/i.test(file)) return "page";
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
function getScopeForElement(el, parentScope) {
|
|
195
|
+
if (!el) return parentScope;
|
|
196
|
+
const instanceSource = el.getAttribute("data-instance-source");
|
|
197
|
+
const fromInstance = inferScope(instanceSource);
|
|
198
|
+
if (fromInstance) return fromInstance;
|
|
199
|
+
const source = el.getAttribute("data-source");
|
|
200
|
+
const fromSource = inferScope(source);
|
|
201
|
+
if (fromSource) return fromSource;
|
|
202
|
+
return parentScope;
|
|
203
|
+
}
|
|
204
|
+
function buildComponentTree(rootEl) {
|
|
205
|
+
const fiber = getFiber(rootEl);
|
|
206
|
+
if (!fiber) {
|
|
207
|
+
return buildDataSlotTree(rootEl);
|
|
208
|
+
}
|
|
209
|
+
let fiberRoot = fiber;
|
|
210
|
+
while (fiberRoot.return) fiberRoot = fiberRoot.return;
|
|
211
|
+
const results = [];
|
|
212
|
+
walkFiber(fiberRoot.child, results, null);
|
|
213
|
+
return results;
|
|
214
|
+
}
|
|
215
|
+
function walkFiber(fiber, siblings, parentScope) {
|
|
216
|
+
while (fiber) {
|
|
217
|
+
const node = processFiber(fiber, parentScope);
|
|
218
|
+
if (node) {
|
|
219
|
+
siblings.push(node);
|
|
220
|
+
} else {
|
|
221
|
+
if (fiber.child) {
|
|
222
|
+
let childScope = parentScope;
|
|
223
|
+
if (typeof fiber.type === "string" && fiber.stateNode instanceof Element) {
|
|
224
|
+
childScope = getScopeForElement(fiber.stateNode, parentScope);
|
|
225
|
+
} else if (typeof fiber.type === "function" || typeof fiber.type === "object") {
|
|
226
|
+
const hostEl = findOwnHostElement(fiber);
|
|
227
|
+
if (hostEl) {
|
|
228
|
+
childScope = getScopeForElement(hostEl, parentScope);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
walkFiber(fiber.child, siblings, childScope);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
fiber = fiber.sibling;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function processFiber(fiber, parentScope) {
|
|
238
|
+
if (typeof fiber.type === "string") {
|
|
239
|
+
return processHostFiber(fiber, parentScope);
|
|
240
|
+
}
|
|
241
|
+
if (typeof fiber.type === "function" || typeof fiber.type === "object") {
|
|
242
|
+
return processComponentFiber(fiber, parentScope);
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
function processHostFiber(fiber, parentScope) {
|
|
247
|
+
const tag = fiber.type;
|
|
248
|
+
const el = fiber.stateNode;
|
|
249
|
+
if (el && el.id && overlayIds.has(el.id)) return null;
|
|
250
|
+
if (["script", "style", "link", "noscript"].includes(tag)) return null;
|
|
251
|
+
const scope = getScopeForElement(el, parentScope);
|
|
252
|
+
const dataSlot = el?.getAttribute("data-slot") || null;
|
|
253
|
+
if (dataSlot) {
|
|
254
|
+
const name = dataSlot.split("-").map((s2) => s2.charAt(0).toUpperCase() + s2.slice(1)).join("");
|
|
255
|
+
const children = [];
|
|
256
|
+
if (fiber.child) walkFiber(fiber.child, children, scope);
|
|
257
|
+
return {
|
|
258
|
+
id: el ? getDomPath(el) : "",
|
|
259
|
+
name,
|
|
260
|
+
type: "component",
|
|
261
|
+
dataSlot,
|
|
262
|
+
source: el?.getAttribute("data-source") || null,
|
|
263
|
+
scope,
|
|
264
|
+
textContent: el ? getDirectText(el) : "",
|
|
265
|
+
children
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (semanticTags.has(tag)) {
|
|
269
|
+
const children = [];
|
|
270
|
+
if (fiber.child) walkFiber(fiber.child, children, scope);
|
|
271
|
+
const text = el ? getDirectText(el) : "";
|
|
272
|
+
if (children.length > 0 || text) {
|
|
273
|
+
return {
|
|
274
|
+
id: el ? getDomPath(el) : "",
|
|
275
|
+
name: `<${tag}>`,
|
|
276
|
+
type: "element",
|
|
277
|
+
dataSlot: null,
|
|
278
|
+
source: el?.getAttribute("data-source") || null,
|
|
279
|
+
scope,
|
|
280
|
+
textContent: text,
|
|
281
|
+
children
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (el?.hasAttribute("data-source") && !skipTags.has(tag)) {
|
|
286
|
+
const children = [];
|
|
287
|
+
if (fiber.child) walkFiber(fiber.child, children, scope);
|
|
288
|
+
const text = el ? getDirectText(el) : "";
|
|
289
|
+
return {
|
|
290
|
+
id: getDomPath(el),
|
|
291
|
+
name: `<${tag}>`,
|
|
292
|
+
type: "element",
|
|
293
|
+
dataSlot: null,
|
|
294
|
+
source: el.getAttribute("data-source"),
|
|
295
|
+
scope,
|
|
296
|
+
textContent: text,
|
|
297
|
+
children
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
function processComponentFiber(fiber, parentScope) {
|
|
303
|
+
const type = fiber.type;
|
|
304
|
+
const name = type?.displayName || type?.name || null;
|
|
305
|
+
if (!name) return null;
|
|
306
|
+
if (isFrameworkComponent(name)) return null;
|
|
307
|
+
if (name === "CodeSurface") return null;
|
|
308
|
+
const hostEl = findOwnHostElement(fiber);
|
|
309
|
+
const hasInstanceSource = hostEl?.getAttribute("data-instance-source");
|
|
310
|
+
const hasDataSlot = hostEl?.getAttribute("data-slot");
|
|
311
|
+
if (!hasInstanceSource && !hasDataSlot) return null;
|
|
312
|
+
const scope = getScopeForElement(hostEl, parentScope);
|
|
313
|
+
const dataSlot = hasDataSlot || null;
|
|
314
|
+
const children = [];
|
|
315
|
+
const hostFiber = dataSlot ? findHostFiber(fiber) : null;
|
|
316
|
+
const childFiber = hostFiber ? hostFiber.child : fiber.child;
|
|
317
|
+
if (childFiber) walkFiber(childFiber, children, scope);
|
|
318
|
+
if (children.length === 1 && !dataSlot && !(hostEl && getDirectText(hostEl))) {
|
|
319
|
+
const child = children[0];
|
|
320
|
+
if (child.type === "component") {
|
|
321
|
+
return child;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
id: hostEl ? getDomPath(hostEl) : "",
|
|
326
|
+
name,
|
|
327
|
+
type: "component",
|
|
328
|
+
dataSlot,
|
|
329
|
+
source: hostEl?.getAttribute("data-source") || null,
|
|
330
|
+
scope,
|
|
331
|
+
textContent: hostEl ? getDirectText(hostEl) : "",
|
|
332
|
+
children
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function findHostFiber(fiber) {
|
|
336
|
+
let child = fiber.child;
|
|
337
|
+
while (child) {
|
|
338
|
+
if (child.stateNode instanceof Element) return child;
|
|
339
|
+
const tag = child.tag;
|
|
340
|
+
const isComponentBoundary = tag === 0 || tag === 1 || tag === 11 || tag === 14 || tag === 15;
|
|
341
|
+
if (!isComponentBoundary && child.child) {
|
|
342
|
+
const found = findHostFiber(child);
|
|
343
|
+
if (found) return found;
|
|
344
|
+
}
|
|
345
|
+
child = child.sibling;
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
function findOwnHostElement(fiber) {
|
|
350
|
+
let child = fiber.child;
|
|
351
|
+
while (child) {
|
|
352
|
+
if (child.stateNode instanceof Element) return child.stateNode;
|
|
353
|
+
const tag = child.tag;
|
|
354
|
+
const isComponentBoundary = tag === 0 || tag === 1 || tag === 11 || tag === 14 || tag === 15;
|
|
355
|
+
if (!isComponentBoundary && child.child) {
|
|
356
|
+
const found = findOwnHostElement(child);
|
|
357
|
+
if (found) return found;
|
|
358
|
+
}
|
|
359
|
+
child = child.sibling;
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
function buildDataSlotTree(root) {
|
|
364
|
+
const results = [];
|
|
365
|
+
for (const child of Array.from(root.children)) {
|
|
366
|
+
walkDomForSlots(child, results, null);
|
|
367
|
+
}
|
|
368
|
+
return results;
|
|
369
|
+
}
|
|
370
|
+
function walkDomForSlots(el, siblings, parentScope) {
|
|
371
|
+
if (el.id && overlayIds.has(el.id)) return;
|
|
372
|
+
const dataSlot = el.getAttribute("data-slot");
|
|
373
|
+
const tag = el.tagName.toLowerCase();
|
|
374
|
+
const scope = getScopeForElement(el, parentScope);
|
|
375
|
+
if (dataSlot) {
|
|
376
|
+
const name = dataSlot.split("-").map((s2) => s2.charAt(0).toUpperCase() + s2.slice(1)).join("");
|
|
377
|
+
const children = [];
|
|
378
|
+
for (const child of Array.from(el.children)) {
|
|
379
|
+
walkDomForSlots(child, children, scope);
|
|
380
|
+
}
|
|
381
|
+
siblings.push({
|
|
382
|
+
id: getDomPath(el),
|
|
383
|
+
name,
|
|
384
|
+
type: "component",
|
|
385
|
+
dataSlot,
|
|
386
|
+
source: el.getAttribute("data-source") || null,
|
|
387
|
+
scope,
|
|
388
|
+
textContent: getDirectText(el),
|
|
389
|
+
children
|
|
390
|
+
});
|
|
391
|
+
} else if (semanticTags.has(tag)) {
|
|
392
|
+
const children = [];
|
|
393
|
+
for (const child of Array.from(el.children)) {
|
|
394
|
+
walkDomForSlots(child, children, scope);
|
|
395
|
+
}
|
|
396
|
+
if (children.length > 0 || getDirectText(el)) {
|
|
397
|
+
siblings.push({
|
|
398
|
+
id: getDomPath(el),
|
|
399
|
+
name: `<${tag}>`,
|
|
400
|
+
type: "element",
|
|
401
|
+
dataSlot: null,
|
|
402
|
+
source: el.getAttribute("data-source") || null,
|
|
403
|
+
scope,
|
|
404
|
+
textContent: getDirectText(el),
|
|
405
|
+
children
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
} else {
|
|
409
|
+
for (const child of Array.from(el.children)) {
|
|
410
|
+
walkDomForSlots(child, siblings, scope);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function sendComponentTree() {
|
|
415
|
+
const tree = buildComponentTree(document.body);
|
|
416
|
+
window.parent.postMessage({ type: "tool:componentTree", tree }, "*");
|
|
417
|
+
}
|
|
418
|
+
let debounceTimer = null;
|
|
419
|
+
function debouncedSendTree() {
|
|
420
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
421
|
+
debounceTimer = setTimeout(sendComponentTree, 300);
|
|
422
|
+
}
|
|
423
|
+
const treeObserver = new MutationObserver(debouncedSendTree);
|
|
424
|
+
treeObserver.observe(document.body, { childList: true, subtree: true });
|
|
102
425
|
const relevantProps = [
|
|
103
426
|
"display",
|
|
104
427
|
"position",
|
|
@@ -404,6 +727,38 @@ function CodeSurface() {
|
|
|
404
727
|
document.documentElement.classList.remove("dark");
|
|
405
728
|
}
|
|
406
729
|
break;
|
|
730
|
+
case "tool:requestComponentTree":
|
|
731
|
+
sendComponentTree();
|
|
732
|
+
break;
|
|
733
|
+
case "tool:highlightByTreeId": {
|
|
734
|
+
const id = msg.id;
|
|
735
|
+
if (!id || !s.highlightOverlay || !s.tooltip) break;
|
|
736
|
+
const target = document.querySelector(id);
|
|
737
|
+
if (target) {
|
|
738
|
+
const rect = target.getBoundingClientRect();
|
|
739
|
+
positionOverlay(s.highlightOverlay, rect);
|
|
740
|
+
const name = getElementName(target);
|
|
741
|
+
s.tooltip.textContent = name;
|
|
742
|
+
s.tooltip.style.display = "block";
|
|
743
|
+
s.tooltip.style.left = `${rect.left}px`;
|
|
744
|
+
s.tooltip.style.top = `${Math.max(0, rect.top - 24)}px`;
|
|
745
|
+
}
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
case "tool:clearHighlight":
|
|
749
|
+
if (s.highlightOverlay) s.highlightOverlay.style.display = "none";
|
|
750
|
+
if (s.tooltip) s.tooltip.style.display = "none";
|
|
751
|
+
break;
|
|
752
|
+
case "tool:selectByTreeId": {
|
|
753
|
+
const id = msg.id;
|
|
754
|
+
if (!id) break;
|
|
755
|
+
const target = document.querySelector(id);
|
|
756
|
+
if (target) {
|
|
757
|
+
const selectable = findSelectableElement(target);
|
|
758
|
+
selectElement(selectable);
|
|
759
|
+
}
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
407
762
|
}
|
|
408
763
|
}
|
|
409
764
|
function notifyPathChanged() {
|
|
@@ -423,6 +778,8 @@ function CodeSurface() {
|
|
|
423
778
|
document.removeEventListener("click", onClick, true);
|
|
424
779
|
window.removeEventListener("message", onMessage);
|
|
425
780
|
window.removeEventListener("popstate", notifyPathChanged);
|
|
781
|
+
treeObserver.disconnect();
|
|
782
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
426
783
|
if (s.overlayRafId) cancelAnimationFrame(s.overlayRafId);
|
|
427
784
|
s.tokenPreviewStyle?.remove();
|
|
428
785
|
s.highlightOverlay?.remove();
|
package/dist/index.js
CHANGED
|
@@ -33,7 +33,195 @@ __export(index_exports, {
|
|
|
33
33
|
withDesigntools: () => withDesigntools
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
var import_path2 = __toESM(require("path"));
|
|
37
|
+
|
|
38
|
+
// src/preview-route.ts
|
|
39
|
+
var import_fs = __toESM(require("fs"));
|
|
36
40
|
var import_path = __toESM(require("path"));
|
|
41
|
+
var PREVIEW_DIR = "designtools-preview";
|
|
42
|
+
function generatePreviewRoute(appDir) {
|
|
43
|
+
const projectRoot = import_path.default.dirname(appDir);
|
|
44
|
+
const previewDir = import_path.default.join(appDir, PREVIEW_DIR);
|
|
45
|
+
const componentPaths = discoverComponentFiles(projectRoot);
|
|
46
|
+
import_fs.default.mkdirSync(previewDir, { recursive: true });
|
|
47
|
+
import_fs.default.writeFileSync(
|
|
48
|
+
import_path.default.join(previewDir, "layout.tsx"),
|
|
49
|
+
getLayoutTemplate(),
|
|
50
|
+
"utf-8"
|
|
51
|
+
);
|
|
52
|
+
import_fs.default.writeFileSync(
|
|
53
|
+
import_path.default.join(previewDir, "page.tsx"),
|
|
54
|
+
getPageTemplate(componentPaths),
|
|
55
|
+
"utf-8"
|
|
56
|
+
);
|
|
57
|
+
ensureGitignore(projectRoot);
|
|
58
|
+
}
|
|
59
|
+
function ensureGitignore(projectRoot) {
|
|
60
|
+
const gitignorePath = import_path.default.join(projectRoot, ".gitignore");
|
|
61
|
+
const entry = "app/designtools-preview";
|
|
62
|
+
try {
|
|
63
|
+
const existing = import_fs.default.existsSync(gitignorePath) ? import_fs.default.readFileSync(gitignorePath, "utf-8") : "";
|
|
64
|
+
if (!existing.includes(entry)) {
|
|
65
|
+
import_fs.default.appendFileSync(gitignorePath, `
|
|
66
|
+
# Generated by @designtools/next-plugin
|
|
67
|
+
${entry}
|
|
68
|
+
`);
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function discoverComponentFiles(projectRoot) {
|
|
74
|
+
const dirs = ["components/ui", "src/components/ui"];
|
|
75
|
+
for (const dir of dirs) {
|
|
76
|
+
const fullDir = import_path.default.join(projectRoot, dir);
|
|
77
|
+
if (import_fs.default.existsSync(fullDir)) {
|
|
78
|
+
const files = import_fs.default.readdirSync(fullDir);
|
|
79
|
+
return files.filter((f) => f.endsWith(".tsx") || f.endsWith(".ts") || f.endsWith(".jsx")).map((f) => `${dir}/${f.replace(/\.(tsx|ts|jsx|js)$/, "")}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
function getLayoutTemplate() {
|
|
85
|
+
return `// Auto-generated by @designtools/next-plugin \u2014 do not edit
|
|
86
|
+
export default function PreviewLayout({ children }: { children: React.ReactNode }) {
|
|
87
|
+
return (
|
|
88
|
+
<div style={{ padding: 32, background: "var(--background, #fff)", minHeight: "100vh" }}>
|
|
89
|
+
{children}
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
function getPageTemplate(componentPaths) {
|
|
96
|
+
const registryEntries = componentPaths.map((p) => ` "${p}": () => import("@/${p}"),`).join("\n");
|
|
97
|
+
return `// Auto-generated by @designtools/next-plugin \u2014 do not edit
|
|
98
|
+
"use client";
|
|
99
|
+
|
|
100
|
+
import { useState, useEffect, useCallback, createElement } from "react";
|
|
101
|
+
|
|
102
|
+
/* Static import registry \u2014 webpack can analyze these imports */
|
|
103
|
+
const COMPONENT_REGISTRY: Record<string, () => Promise<any>> = {
|
|
104
|
+
${registryEntries}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
interface Combination {
|
|
108
|
+
label: string;
|
|
109
|
+
props: Record<string, string>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface RenderMsg {
|
|
113
|
+
type: "tool:renderPreview";
|
|
114
|
+
componentPath: string;
|
|
115
|
+
exportName: string;
|
|
116
|
+
combinations: Combination[];
|
|
117
|
+
defaultChildren: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default function PreviewPage() {
|
|
121
|
+
const [Component, setComponent] = useState<React.ComponentType<any> | null>(null);
|
|
122
|
+
const [combinations, setCombinations] = useState<Combination[]>([]);
|
|
123
|
+
const [defaultChildren, setDefaultChildren] = useState("");
|
|
124
|
+
const [error, setError] = useState<string | null>(null);
|
|
125
|
+
|
|
126
|
+
const handleMessage = useCallback(async (e: MessageEvent) => {
|
|
127
|
+
const msg = e.data;
|
|
128
|
+
if (msg?.type !== "tool:renderPreview") return;
|
|
129
|
+
|
|
130
|
+
const { componentPath, exportName, combinations: combos, defaultChildren: children } = msg as RenderMsg;
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
setError(null);
|
|
134
|
+
setCombinations(combos);
|
|
135
|
+
setDefaultChildren(children || exportName);
|
|
136
|
+
|
|
137
|
+
const loader = COMPONENT_REGISTRY[componentPath];
|
|
138
|
+
if (!loader) {
|
|
139
|
+
setError(\`Component "\${componentPath}" not found in registry. Available: \${Object.keys(COMPONENT_REGISTRY).join(", ")}\`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const mod = await loader();
|
|
144
|
+
const Comp = mod[exportName] || mod.default;
|
|
145
|
+
if (!Comp) {
|
|
146
|
+
setError(\`Export "\${exportName}" not found in \${componentPath}\`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
setComponent(() => Comp);
|
|
151
|
+
|
|
152
|
+
// Notify editor that preview is ready
|
|
153
|
+
window.parent.postMessage(
|
|
154
|
+
{ type: "tool:previewReady", cellCount: combos.length },
|
|
155
|
+
"*"
|
|
156
|
+
);
|
|
157
|
+
} catch (err: any) {
|
|
158
|
+
setError(\`Failed to load component: \${err.message}\`);
|
|
159
|
+
}
|
|
160
|
+
}, []);
|
|
161
|
+
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
window.addEventListener("message", handleMessage);
|
|
164
|
+
// Signal readiness to the editor
|
|
165
|
+
window.parent.postMessage({ type: "tool:injectedReady" }, "*");
|
|
166
|
+
return () => window.removeEventListener("message", handleMessage);
|
|
167
|
+
}, [handleMessage]);
|
|
168
|
+
|
|
169
|
+
if (error) {
|
|
170
|
+
return (
|
|
171
|
+
<div style={{ padding: 32, color: "#ef4444", fontFamily: "monospace", fontSize: 14 }}>
|
|
172
|
+
{error}
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!Component) {
|
|
178
|
+
return (
|
|
179
|
+
<div style={{ padding: 32, color: "#888", fontFamily: "system-ui", fontSize: 14 }}>
|
|
180
|
+
Waiting for component\u2026
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div style={{ fontFamily: "system-ui" }}>
|
|
187
|
+
<div style={{
|
|
188
|
+
display: "grid",
|
|
189
|
+
gridTemplateColumns: "repeat(auto-fill, minmax(240px, 1fr))",
|
|
190
|
+
gap: 24,
|
|
191
|
+
}}>
|
|
192
|
+
{combinations.map((combo, i) => (
|
|
193
|
+
<div key={i} style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
194
|
+
<div style={{
|
|
195
|
+
fontSize: 11,
|
|
196
|
+
fontWeight: 600,
|
|
197
|
+
color: "#888",
|
|
198
|
+
textTransform: "uppercase",
|
|
199
|
+
letterSpacing: "0.05em",
|
|
200
|
+
}}>
|
|
201
|
+
{combo.label}
|
|
202
|
+
</div>
|
|
203
|
+
<div style={{
|
|
204
|
+
padding: 16,
|
|
205
|
+
border: "1px solid #e5e7eb",
|
|
206
|
+
borderRadius: 8,
|
|
207
|
+
display: "flex",
|
|
208
|
+
alignItems: "center",
|
|
209
|
+
justifyContent: "center",
|
|
210
|
+
minHeight: 64,
|
|
211
|
+
background: "#fff",
|
|
212
|
+
}}>
|
|
213
|
+
{createElement(Component, combo.props, defaultChildren)}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
))}
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/index.ts
|
|
37
225
|
function withDesigntools(nextConfig = {}) {
|
|
38
226
|
return {
|
|
39
227
|
...nextConfig,
|
|
@@ -44,7 +232,7 @@ function withDesigntools(nextConfig = {}) {
|
|
|
44
232
|
exclude: /node_modules/,
|
|
45
233
|
use: [
|
|
46
234
|
{
|
|
47
|
-
loader:
|
|
235
|
+
loader: import_path2.default.resolve(__dirname, "loader.js"),
|
|
48
236
|
options: {
|
|
49
237
|
cwd: context.dir
|
|
50
238
|
}
|
|
@@ -54,15 +242,20 @@ function withDesigntools(nextConfig = {}) {
|
|
|
54
242
|
config.module.rules.push({
|
|
55
243
|
test: /layout\.(tsx|jsx)$/,
|
|
56
244
|
include: [
|
|
57
|
-
|
|
58
|
-
|
|
245
|
+
import_path2.default.resolve(context.dir, "app"),
|
|
246
|
+
import_path2.default.resolve(context.dir, "src/app")
|
|
59
247
|
],
|
|
60
248
|
use: [
|
|
61
249
|
{
|
|
62
|
-
loader:
|
|
250
|
+
loader: import_path2.default.resolve(__dirname, "codesurface-mount-loader.js")
|
|
63
251
|
}
|
|
64
252
|
]
|
|
65
253
|
});
|
|
254
|
+
const appDir = import_path2.default.resolve(context.dir, "app");
|
|
255
|
+
try {
|
|
256
|
+
generatePreviewRoute(appDir);
|
|
257
|
+
} catch {
|
|
258
|
+
}
|
|
66
259
|
}
|
|
67
260
|
if (typeof nextConfig.webpack === "function") {
|
|
68
261
|
return nextConfig.webpack(config, context);
|