@designtools/next-plugin 0.1.2 → 0.1.6
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/codecanvas-mount-loader.d.mts +0 -15
- package/dist/codecanvas-mount-loader.d.ts +0 -15
- package/dist/codecanvas-mount-loader.js +0 -51
- package/dist/codecanvas-mount-loader.mjs +0 -32
- package/dist/codecanvas.d.mts +0 -3
- package/dist/codecanvas.d.ts +0 -3
- package/dist/codecanvas.js +0 -426
- package/dist/codecanvas.mjs +0 -403
package/src/codesurface.tsx
CHANGED
|
@@ -125,6 +125,448 @@ export function CodeSurface() {
|
|
|
125
125
|
return target;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// --- Component tree extraction (React Fiber) ---
|
|
129
|
+
|
|
130
|
+
// IDs of overlay elements to skip during tree building
|
|
131
|
+
const overlayIds = new Set(["tool-highlight", "tool-tooltip", "tool-selected", "codesurface-token-preview"]);
|
|
132
|
+
|
|
133
|
+
// Semantic HTML elements shown as structural landmarks
|
|
134
|
+
const semanticTags = new Set(["header", "main", "nav", "section", "article", "footer", "aside"]);
|
|
135
|
+
|
|
136
|
+
// Tags to skip even if authored — document-level elements and void
|
|
137
|
+
// elements that aren't meaningful to designers
|
|
138
|
+
const skipTags = new Set([
|
|
139
|
+
"html", "body", "head", // document structure
|
|
140
|
+
"br", "hr", "wbr", // void/formatting
|
|
141
|
+
"template", "slot", // shadow DOM
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
// React and Next.js framework components to hide from the tree
|
|
145
|
+
const frameworkPatterns = [
|
|
146
|
+
/^Fragment$/, /^Suspense$/, /^ErrorBoundary$/,
|
|
147
|
+
/^Provider$/, /^Consumer$/, /Context$/,
|
|
148
|
+
/^ForwardRef$/, /^Memo$/, /^Lazy$/,
|
|
149
|
+
// Next.js routing internals
|
|
150
|
+
/^InnerLayoutRouter$/, /^OuterLayoutRouter$/, /^LayoutRouter$/,
|
|
151
|
+
/^RenderFromTemplateContext$/, /^TemplateContext$/,
|
|
152
|
+
/^RedirectBoundary$/, /^RedirectErrorBoundary$/,
|
|
153
|
+
/^NotFoundBoundary$/, /^LoadingBoundary$/,
|
|
154
|
+
/^HTTPAccessFallbackBoundary$/, /^HTTPAccessFallbackErrorBoundary$/,
|
|
155
|
+
/^ClientPageRoot$/, /^HotReload$/, /^ReactDevOverlay$/,
|
|
156
|
+
/^PathnameContextProviderAdapter$/,
|
|
157
|
+
// Next.js App Router internals (segment tree)
|
|
158
|
+
/^SegmentViewNode$/, /^SegmentTrieNode$/,
|
|
159
|
+
/^SegmentViewStateNode$/, /^SegmentBoundaryTriggerNode$/,
|
|
160
|
+
/^SegmentStateProvider$/,
|
|
161
|
+
/^ScrollAndFocusHandler$/, /^InnerScrollAndFocusHandler$/,
|
|
162
|
+
/^AppRouter$/, /^Router$/, /^Root$/, /^ServerRoot$/,
|
|
163
|
+
/^RootErrorBoundary$/, /^ErrorBoundaryHandler$/,
|
|
164
|
+
/^AppRouterAnnouncer$/, /^HistoryUpdater$/, /^RuntimeStyles$/,
|
|
165
|
+
/^DevRootHTTPAccessFallbackBoundary$/,
|
|
166
|
+
/^AppDevOverlayErrorBoundary$/, /^ReplaySsrOnlyErrors$/,
|
|
167
|
+
/^HeadManagerContext$/, /^Head$/,
|
|
168
|
+
/^MetadataOutlet$/, /^AsyncMetadataOutlet$/,
|
|
169
|
+
/^__next_/, // All __next_ prefixed components
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
function isFrameworkComponent(name: string): boolean {
|
|
173
|
+
return frameworkPatterns.some(p => p.test(name));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get the React fiber for a DOM element.
|
|
178
|
+
* React attaches fibers via __reactFiber$<randomKey> in dev mode.
|
|
179
|
+
* Stable since React 17 through React 19.
|
|
180
|
+
*/
|
|
181
|
+
function getFiber(el: Element): any | null {
|
|
182
|
+
const key = Object.keys(el).find(k => k.startsWith("__reactFiber$"));
|
|
183
|
+
return key ? (el as any)[key] : null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get direct text content of an element (not descendant text).
|
|
188
|
+
*/
|
|
189
|
+
function getDirectText(el: Element): string {
|
|
190
|
+
let text = "";
|
|
191
|
+
for (const node of Array.from(el.childNodes)) {
|
|
192
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
193
|
+
text += (node.textContent || "").trim();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return text.slice(0, 40);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
interface TreeNode {
|
|
200
|
+
id: string;
|
|
201
|
+
name: string;
|
|
202
|
+
type: "component" | "element";
|
|
203
|
+
dataSlot: string | null;
|
|
204
|
+
source: string | null;
|
|
205
|
+
scope: "layout" | "page" | null;
|
|
206
|
+
textContent: string;
|
|
207
|
+
children: TreeNode[];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Infer routing scope from a data-source or data-instance-source path.
|
|
212
|
+
* Framework-specific: Next.js uses layout.tsx / page.tsx file naming.
|
|
213
|
+
*/
|
|
214
|
+
function inferScope(sourcePath: string | null): "layout" | "page" | null {
|
|
215
|
+
if (!sourcePath) return null;
|
|
216
|
+
// data-source format is "file:line:col" — extract the file part
|
|
217
|
+
const colonIdx = sourcePath.indexOf(":");
|
|
218
|
+
const file = colonIdx > 0 ? sourcePath.slice(0, colonIdx) : sourcePath;
|
|
219
|
+
if (/\/layout\.[tjsx]+$/i.test(file) || /^layout\.[tjsx]+$/i.test(file)) return "layout";
|
|
220
|
+
if (/\/page\.[tjsx]+$/i.test(file) || /^page\.[tjsx]+$/i.test(file)) return "page";
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Determine scope for a component by checking its instance-source
|
|
226
|
+
* (where it's used) or its own source (where it's defined).
|
|
227
|
+
* Instance source takes priority — a Button in layout.tsx has layout scope.
|
|
228
|
+
*/
|
|
229
|
+
function getScopeForElement(el: Element | null, parentScope: "layout" | "page" | null): "layout" | "page" | null {
|
|
230
|
+
if (!el) return parentScope;
|
|
231
|
+
// Check instance-source first (where this component is used)
|
|
232
|
+
const instanceSource = el.getAttribute("data-instance-source");
|
|
233
|
+
const fromInstance = inferScope(instanceSource);
|
|
234
|
+
if (fromInstance) return fromInstance;
|
|
235
|
+
// Check own source (where this element is defined)
|
|
236
|
+
const source = el.getAttribute("data-source");
|
|
237
|
+
const fromSource = inferScope(source);
|
|
238
|
+
if (fromSource) return fromSource;
|
|
239
|
+
// Inherit from parent context
|
|
240
|
+
return parentScope;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Build a component tree by walking the React fiber tree.
|
|
245
|
+
* Filters to: user-defined components, data-slot components, semantic HTML.
|
|
246
|
+
*/
|
|
247
|
+
function buildComponentTree(rootEl: Element): TreeNode[] {
|
|
248
|
+
const fiber = getFiber(rootEl);
|
|
249
|
+
if (!fiber) {
|
|
250
|
+
// Fallback: data-slot-only tree via DOM walk
|
|
251
|
+
return buildDataSlotTree(rootEl);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Walk up to the fiber root
|
|
255
|
+
let fiberRoot = fiber;
|
|
256
|
+
while (fiberRoot.return) fiberRoot = fiberRoot.return;
|
|
257
|
+
|
|
258
|
+
const results: TreeNode[] = [];
|
|
259
|
+
walkFiber(fiberRoot.child, results, null);
|
|
260
|
+
return results;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function walkFiber(fiber: any | null, siblings: TreeNode[], parentScope: "layout" | "page" | null): void {
|
|
264
|
+
while (fiber) {
|
|
265
|
+
const node = processFiber(fiber, parentScope);
|
|
266
|
+
if (node) {
|
|
267
|
+
siblings.push(node);
|
|
268
|
+
} else {
|
|
269
|
+
// This fiber was filtered out — but still walk its children
|
|
270
|
+
// so nested visible components bubble up.
|
|
271
|
+
// Infer scope from this invisible fiber for its children.
|
|
272
|
+
if (fiber.child) {
|
|
273
|
+
let childScope = parentScope;
|
|
274
|
+
if (typeof fiber.type === "string" && fiber.stateNode instanceof Element) {
|
|
275
|
+
// Host element (div, html, body, etc.) — check its data-source
|
|
276
|
+
childScope = getScopeForElement(fiber.stateNode, parentScope);
|
|
277
|
+
} else if (typeof fiber.type === "function" || typeof fiber.type === "object") {
|
|
278
|
+
// Filtered-out component — check its root host element for scope.
|
|
279
|
+
// This catches cases like RootLayout -> <html data-source="app/layout.tsx:...">
|
|
280
|
+
// where the component itself is filtered but its root element carries scope.
|
|
281
|
+
const hostEl = findOwnHostElement(fiber);
|
|
282
|
+
if (hostEl) {
|
|
283
|
+
childScope = getScopeForElement(hostEl, parentScope);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
walkFiber(fiber.child, siblings, childScope);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
fiber = fiber.sibling;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function processFiber(fiber: any, parentScope: "layout" | "page" | null): TreeNode | null {
|
|
294
|
+
// Skip text nodes and fragments
|
|
295
|
+
if (typeof fiber.type === "string") {
|
|
296
|
+
// This is a host element (div, span, etc.)
|
|
297
|
+
return processHostFiber(fiber, parentScope);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (typeof fiber.type === "function" || typeof fiber.type === "object") {
|
|
301
|
+
return processComponentFiber(fiber, parentScope);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Other fiber types (portals, etc.) — walk children transparently
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function processHostFiber(fiber: any, parentScope: "layout" | "page" | null): TreeNode | null {
|
|
309
|
+
const tag = fiber.type as string;
|
|
310
|
+
const el = fiber.stateNode as Element | null;
|
|
311
|
+
|
|
312
|
+
// Skip our overlay elements
|
|
313
|
+
if (el && el.id && overlayIds.has(el.id)) return null;
|
|
314
|
+
|
|
315
|
+
// Skip script, style, link, noscript
|
|
316
|
+
if (["script", "style", "link", "noscript"].includes(tag)) return null;
|
|
317
|
+
|
|
318
|
+
const scope = getScopeForElement(el, parentScope);
|
|
319
|
+
|
|
320
|
+
// Check for data-slot — this is a design system component root element
|
|
321
|
+
const dataSlot = el?.getAttribute("data-slot") || null;
|
|
322
|
+
if (dataSlot) {
|
|
323
|
+
const name = dataSlot.split("-").map((s: string) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
324
|
+
const children: TreeNode[] = [];
|
|
325
|
+
if (fiber.child) walkFiber(fiber.child, children, scope);
|
|
326
|
+
return {
|
|
327
|
+
id: el ? getDomPath(el) : "",
|
|
328
|
+
name,
|
|
329
|
+
type: "component",
|
|
330
|
+
dataSlot,
|
|
331
|
+
source: el?.getAttribute("data-source") || null,
|
|
332
|
+
scope,
|
|
333
|
+
textContent: el ? getDirectText(el) : "",
|
|
334
|
+
children,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Show semantic HTML landmarks
|
|
339
|
+
if (semanticTags.has(tag)) {
|
|
340
|
+
const children: TreeNode[] = [];
|
|
341
|
+
if (fiber.child) walkFiber(fiber.child, children, scope);
|
|
342
|
+
const text = el ? getDirectText(el) : "";
|
|
343
|
+
if (children.length > 0 || text) {
|
|
344
|
+
return {
|
|
345
|
+
id: el ? getDomPath(el) : "",
|
|
346
|
+
name: `<${tag}>`,
|
|
347
|
+
type: "element",
|
|
348
|
+
dataSlot: null,
|
|
349
|
+
source: el?.getAttribute("data-source") || null,
|
|
350
|
+
scope,
|
|
351
|
+
textContent: text,
|
|
352
|
+
children,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Show authored elements — data-source is added by our Babel transform
|
|
358
|
+
// to every JSX element, so its presence proves this was deliberately
|
|
359
|
+
// written in user code. Skip document/void tags that aren't meaningful.
|
|
360
|
+
if (el?.hasAttribute("data-source") && !skipTags.has(tag)) {
|
|
361
|
+
const children: TreeNode[] = [];
|
|
362
|
+
if (fiber.child) walkFiber(fiber.child, children, scope);
|
|
363
|
+
const text = el ? getDirectText(el) : "";
|
|
364
|
+
return {
|
|
365
|
+
id: getDomPath(el),
|
|
366
|
+
name: `<${tag}>`,
|
|
367
|
+
type: "element",
|
|
368
|
+
dataSlot: null,
|
|
369
|
+
source: el.getAttribute("data-source"),
|
|
370
|
+
scope,
|
|
371
|
+
textContent: text,
|
|
372
|
+
children,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Generic containers and elements without data-source: skip this node,
|
|
377
|
+
// but walk children (children bubble up to parent's list via walkFiber)
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function processComponentFiber(fiber: any, parentScope: "layout" | "page" | null): TreeNode | null {
|
|
382
|
+
// Get component name
|
|
383
|
+
const type = fiber.type;
|
|
384
|
+
const name = type?.displayName || type?.name || null;
|
|
385
|
+
|
|
386
|
+
// No name = anonymous component, skip
|
|
387
|
+
if (!name) return null;
|
|
388
|
+
|
|
389
|
+
// Skip framework internals
|
|
390
|
+
if (isFrameworkComponent(name)) return null;
|
|
391
|
+
|
|
392
|
+
// Skip the CodeSurface component itself
|
|
393
|
+
if (name === "CodeSurface") return null;
|
|
394
|
+
|
|
395
|
+
// Find this component's own root host element — only walk down through
|
|
396
|
+
// non-host fibers (other components, fragments, etc.) to find the first
|
|
397
|
+
// DOM element this component directly renders. Don't descend into child
|
|
398
|
+
// components, which would give us a different component's element.
|
|
399
|
+
const hostEl = findOwnHostElement(fiber);
|
|
400
|
+
|
|
401
|
+
// Check if this component comes from user code by looking for
|
|
402
|
+
// data-instance-source (set on component JSX by our Babel transform)
|
|
403
|
+
// on the host element. data-instance-source proves the component
|
|
404
|
+
// usage was in a user file processed by our loader.
|
|
405
|
+
// Also accept data-slot as proof of being a known component.
|
|
406
|
+
const hasInstanceSource = hostEl?.getAttribute("data-instance-source");
|
|
407
|
+
const hasDataSlot = hostEl?.getAttribute("data-slot");
|
|
408
|
+
if (!hasInstanceSource && !hasDataSlot) return null;
|
|
409
|
+
|
|
410
|
+
const scope = getScopeForElement(hostEl, parentScope);
|
|
411
|
+
const dataSlot = hasDataSlot || null;
|
|
412
|
+
const children: TreeNode[] = [];
|
|
413
|
+
|
|
414
|
+
// When this component's root host element has data-slot, the child
|
|
415
|
+
// walker would also pick it up via processHostFiber and create a
|
|
416
|
+
// duplicate node. To avoid that, find the host fiber and walk its
|
|
417
|
+
// children directly (skipping the host element itself).
|
|
418
|
+
const hostFiber = dataSlot ? findHostFiber(fiber) : null;
|
|
419
|
+
const childFiber = hostFiber ? hostFiber.child : fiber.child;
|
|
420
|
+
if (childFiber) walkFiber(childFiber, children, scope);
|
|
421
|
+
|
|
422
|
+
// Collapse: if this component has exactly one child component and no
|
|
423
|
+
// direct text, skip this wrapper and promote the child
|
|
424
|
+
if (children.length === 1 && !dataSlot && !(hostEl && getDirectText(hostEl))) {
|
|
425
|
+
const child = children[0];
|
|
426
|
+
if (child.type === "component") {
|
|
427
|
+
return child;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
id: hostEl ? getDomPath(hostEl) : "",
|
|
433
|
+
name,
|
|
434
|
+
type: "component",
|
|
435
|
+
dataSlot,
|
|
436
|
+
source: hostEl?.getAttribute("data-source") || null,
|
|
437
|
+
scope,
|
|
438
|
+
textContent: hostEl ? getDirectText(hostEl) : "",
|
|
439
|
+
children,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Find a component fiber's own root host fiber (not the element).
|
|
445
|
+
* Same walk as findOwnHostElement but returns the fiber itself,
|
|
446
|
+
* so we can skip it in the tree walk and avoid data-slot duplication.
|
|
447
|
+
*/
|
|
448
|
+
function findHostFiber(fiber: any): any | null {
|
|
449
|
+
let child = fiber.child;
|
|
450
|
+
while (child) {
|
|
451
|
+
if (child.stateNode instanceof Element) return child;
|
|
452
|
+
const tag = child.tag;
|
|
453
|
+
const isComponentBoundary = tag === 0 || tag === 1 || tag === 11 || tag === 14 || tag === 15;
|
|
454
|
+
if (!isComponentBoundary && child.child) {
|
|
455
|
+
const found = findHostFiber(child);
|
|
456
|
+
if (found) return found;
|
|
457
|
+
}
|
|
458
|
+
child = child.sibling;
|
|
459
|
+
}
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Find a component fiber's own root host DOM element.
|
|
465
|
+
* Walks through transparent fibers (fragments, mode, profiler) but
|
|
466
|
+
* stops at component boundaries (function/class/forwardRef/memo) to
|
|
467
|
+
* avoid descending into child components.
|
|
468
|
+
*/
|
|
469
|
+
function findOwnHostElement(fiber: any): Element | null {
|
|
470
|
+
let child = fiber.child;
|
|
471
|
+
while (child) {
|
|
472
|
+
// Found a DOM element — this is our root host element
|
|
473
|
+
if (child.stateNode instanceof Element) return child.stateNode;
|
|
474
|
+
|
|
475
|
+
// Check fiber tag to determine if this is a component boundary.
|
|
476
|
+
// React fiber tags: 0=FunctionComponent, 1=ClassComponent,
|
|
477
|
+
// 11=ForwardRef, 14=MemoComponent, 15=SimpleMemoComponent.
|
|
478
|
+
// These are component boundaries — don't descend.
|
|
479
|
+
const tag = child.tag;
|
|
480
|
+
const isComponentBoundary = tag === 0 || tag === 1 || tag === 11 || tag === 14 || tag === 15;
|
|
481
|
+
|
|
482
|
+
if (!isComponentBoundary && child.child) {
|
|
483
|
+
// Transparent fiber (fragment, mode, context, etc.) — walk through
|
|
484
|
+
const found = findOwnHostElement(child);
|
|
485
|
+
if (found) return found;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
child = child.sibling;
|
|
489
|
+
}
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Fallback: build tree from DOM using only data-slot elements.
|
|
495
|
+
* Used when React fiber access is unavailable.
|
|
496
|
+
*/
|
|
497
|
+
function buildDataSlotTree(root: Element): TreeNode[] {
|
|
498
|
+
const results: TreeNode[] = [];
|
|
499
|
+
for (const child of Array.from(root.children)) {
|
|
500
|
+
walkDomForSlots(child, results, null);
|
|
501
|
+
}
|
|
502
|
+
return results;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function walkDomForSlots(el: Element, siblings: TreeNode[], parentScope: "layout" | "page" | null): void {
|
|
506
|
+
// Skip overlay elements
|
|
507
|
+
if (el.id && overlayIds.has(el.id)) return;
|
|
508
|
+
|
|
509
|
+
const dataSlot = el.getAttribute("data-slot");
|
|
510
|
+
const tag = el.tagName.toLowerCase();
|
|
511
|
+
const scope = getScopeForElement(el, parentScope);
|
|
512
|
+
|
|
513
|
+
if (dataSlot) {
|
|
514
|
+
const name = dataSlot.split("-").map((s: string) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
515
|
+
const children: TreeNode[] = [];
|
|
516
|
+
for (const child of Array.from(el.children)) {
|
|
517
|
+
walkDomForSlots(child, children, scope);
|
|
518
|
+
}
|
|
519
|
+
siblings.push({
|
|
520
|
+
id: getDomPath(el),
|
|
521
|
+
name,
|
|
522
|
+
type: "component",
|
|
523
|
+
dataSlot,
|
|
524
|
+
source: el.getAttribute("data-source") || null,
|
|
525
|
+
scope,
|
|
526
|
+
textContent: getDirectText(el),
|
|
527
|
+
children,
|
|
528
|
+
});
|
|
529
|
+
} else if (semanticTags.has(tag)) {
|
|
530
|
+
const children: TreeNode[] = [];
|
|
531
|
+
for (const child of Array.from(el.children)) {
|
|
532
|
+
walkDomForSlots(child, children, scope);
|
|
533
|
+
}
|
|
534
|
+
if (children.length > 0 || getDirectText(el)) {
|
|
535
|
+
siblings.push({
|
|
536
|
+
id: getDomPath(el),
|
|
537
|
+
name: `<${tag}>`,
|
|
538
|
+
type: "element",
|
|
539
|
+
dataSlot: null,
|
|
540
|
+
source: el.getAttribute("data-source") || null,
|
|
541
|
+
scope,
|
|
542
|
+
textContent: getDirectText(el),
|
|
543
|
+
children,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
} else {
|
|
547
|
+
// Skip this element, but walk its children
|
|
548
|
+
for (const child of Array.from(el.children)) {
|
|
549
|
+
walkDomForSlots(child, siblings, scope);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function sendComponentTree() {
|
|
555
|
+
const tree = buildComponentTree(document.body);
|
|
556
|
+
window.parent.postMessage({ type: "tool:componentTree", tree }, "*");
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Debounce helper for MutationObserver
|
|
560
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
561
|
+
function debouncedSendTree() {
|
|
562
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
563
|
+
debounceTimer = setTimeout(sendComponentTree, 300);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// MutationObserver to send updated tree on DOM changes (HMR, dynamic content)
|
|
567
|
+
const treeObserver = new MutationObserver(debouncedSendTree);
|
|
568
|
+
treeObserver.observe(document.body, { childList: true, subtree: true });
|
|
569
|
+
|
|
128
570
|
const relevantProps = [
|
|
129
571
|
"display", "position", "top", "right", "bottom", "left",
|
|
130
572
|
"z-index", "overflow", "overflow-x", "overflow-y",
|
|
@@ -412,6 +854,38 @@ export function CodeSurface() {
|
|
|
412
854
|
document.documentElement.classList.remove("dark");
|
|
413
855
|
}
|
|
414
856
|
break;
|
|
857
|
+
case "tool:requestComponentTree":
|
|
858
|
+
sendComponentTree();
|
|
859
|
+
break;
|
|
860
|
+
case "tool:highlightByTreeId": {
|
|
861
|
+
const id = msg.id as string;
|
|
862
|
+
if (!id || !s.highlightOverlay || !s.tooltip) break;
|
|
863
|
+
const target = document.querySelector(id);
|
|
864
|
+
if (target) {
|
|
865
|
+
const rect = target.getBoundingClientRect();
|
|
866
|
+
positionOverlay(s.highlightOverlay, rect);
|
|
867
|
+
const name = getElementName(target);
|
|
868
|
+
s.tooltip.textContent = name;
|
|
869
|
+
s.tooltip.style.display = "block";
|
|
870
|
+
s.tooltip.style.left = `${rect.left}px`;
|
|
871
|
+
s.tooltip.style.top = `${Math.max(0, rect.top - 24)}px`;
|
|
872
|
+
}
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
case "tool:clearHighlight":
|
|
876
|
+
if (s.highlightOverlay) s.highlightOverlay.style.display = "none";
|
|
877
|
+
if (s.tooltip) s.tooltip.style.display = "none";
|
|
878
|
+
break;
|
|
879
|
+
case "tool:selectByTreeId": {
|
|
880
|
+
const id = msg.id as string;
|
|
881
|
+
if (!id) break;
|
|
882
|
+
const target = document.querySelector(id);
|
|
883
|
+
if (target) {
|
|
884
|
+
const selectable = findSelectableElement(target);
|
|
885
|
+
selectElement(selectable);
|
|
886
|
+
}
|
|
887
|
+
break;
|
|
888
|
+
}
|
|
415
889
|
}
|
|
416
890
|
}
|
|
417
891
|
|
|
@@ -439,6 +913,8 @@ export function CodeSurface() {
|
|
|
439
913
|
window.removeEventListener("message", onMessage);
|
|
440
914
|
window.removeEventListener("popstate", notifyPathChanged);
|
|
441
915
|
|
|
916
|
+
treeObserver.disconnect();
|
|
917
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
442
918
|
if (s.overlayRafId) cancelAnimationFrame(s.overlayRafId);
|
|
443
919
|
s.tokenPreviewStyle?.remove();
|
|
444
920
|
s.highlightOverlay?.remove();
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import path from "path";
|
|
11
|
+
import { generatePreviewRoute } from "./preview-route.js";
|
|
11
12
|
|
|
12
13
|
export function withDesigntools<T extends Record<string, any>>(nextConfig: T = {} as T): T {
|
|
13
14
|
return {
|
|
@@ -41,6 +42,16 @@ export function withDesigntools<T extends Record<string, any>>(nextConfig: T = {
|
|
|
41
42
|
},
|
|
42
43
|
],
|
|
43
44
|
});
|
|
45
|
+
|
|
46
|
+
// Generate the component isolation preview route
|
|
47
|
+
// This creates app/__designtools/preview/page.tsx which Next.js
|
|
48
|
+
// picks up as a route automatically via file-system routing.
|
|
49
|
+
const appDir = path.resolve(context.dir, "app");
|
|
50
|
+
try {
|
|
51
|
+
generatePreviewRoute(appDir);
|
|
52
|
+
} catch {
|
|
53
|
+
// Non-fatal — isolation feature just won't work
|
|
54
|
+
}
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
// Call the user's webpack config if provided
|