@designtools/next-plugin 0.1.2
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/chunk-Y6FXYEAI.mjs +10 -0
- package/dist/codecanvas-mount-loader.d.mts +15 -0
- package/dist/codecanvas-mount-loader.d.ts +15 -0
- package/dist/codecanvas-mount-loader.js +51 -0
- package/dist/codecanvas-mount-loader.mjs +32 -0
- package/dist/codecanvas.d.mts +3 -0
- package/dist/codecanvas.d.ts +3 -0
- package/dist/codecanvas.js +426 -0
- package/dist/codecanvas.mjs +403 -0
- package/dist/codesurface-mount-loader.d.mts +15 -0
- package/dist/codesurface-mount-loader.d.ts +15 -0
- package/dist/codesurface-mount-loader.js +51 -0
- package/dist/codesurface-mount-loader.mjs +32 -0
- package/dist/codesurface.d.mts +3 -0
- package/dist/codesurface.d.ts +3 -0
- package/dist/codesurface.js +460 -0
- package/dist/codesurface.mjs +437 -0
- package/dist/index.d.mts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +77 -0
- package/dist/index.mjs +44 -0
- package/dist/loader.d.mts +17 -0
- package/dist/loader.d.ts +17 -0
- package/dist/loader.js +113 -0
- package/dist/loader.mjs +86 -0
- package/package.json +28 -0
- package/src/codesurface-mount-loader.ts +51 -0
- package/src/codesurface.tsx +452 -0
- package/src/index.ts +54 -0
- package/src/loader.ts +125 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* <CodeSurface /> — Selection overlay component for the target app.
|
|
5
|
+
* Mounted automatically by withDesigntools() in development.
|
|
6
|
+
* Communicates with the editor UI via postMessage.
|
|
7
|
+
*
|
|
8
|
+
* Refactored from core/src/inject/selection.ts into a React component
|
|
9
|
+
* with proper lifecycle management.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useEffect, useRef } from "react";
|
|
13
|
+
|
|
14
|
+
// Overlay elements are created imperatively (not React-rendered)
|
|
15
|
+
// because they need to be fixed-position overlays that don't interfere
|
|
16
|
+
// with the app's React tree.
|
|
17
|
+
|
|
18
|
+
export function CodeSurface() {
|
|
19
|
+
const stateRef = useRef({
|
|
20
|
+
selectionMode: false,
|
|
21
|
+
hoveredElement: null as Element | null,
|
|
22
|
+
selectedElement: null as Element | null,
|
|
23
|
+
selectedDomPath: null as string | null,
|
|
24
|
+
overlayRafId: null as number | null,
|
|
25
|
+
inlineStyleBackups: new Map<string, string>(),
|
|
26
|
+
tokenValueBackups: new Map<string, string>(),
|
|
27
|
+
tokenPreviewValues: new Map<string, string>(),
|
|
28
|
+
tokenPreviewStyle: null as HTMLStyleElement | null,
|
|
29
|
+
highlightOverlay: null as HTMLDivElement | null,
|
|
30
|
+
tooltip: null as HTMLDivElement | null,
|
|
31
|
+
selectedOverlay: null as HTMLDivElement | null,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const s = stateRef.current;
|
|
36
|
+
|
|
37
|
+
// --- Create overlay DOM elements ---
|
|
38
|
+
s.highlightOverlay = document.createElement("div");
|
|
39
|
+
s.highlightOverlay.id = "tool-highlight";
|
|
40
|
+
Object.assign(s.highlightOverlay.style, {
|
|
41
|
+
position: "fixed",
|
|
42
|
+
pointerEvents: "none",
|
|
43
|
+
border: "2px solid #3b82f6",
|
|
44
|
+
backgroundColor: "rgba(59, 130, 246, 0.08)",
|
|
45
|
+
borderRadius: "2px",
|
|
46
|
+
zIndex: "99999",
|
|
47
|
+
display: "none",
|
|
48
|
+
transition: "all 0.1s ease",
|
|
49
|
+
});
|
|
50
|
+
document.body.appendChild(s.highlightOverlay);
|
|
51
|
+
|
|
52
|
+
s.tooltip = document.createElement("div");
|
|
53
|
+
s.tooltip.id = "tool-tooltip";
|
|
54
|
+
Object.assign(s.tooltip.style, {
|
|
55
|
+
position: "fixed",
|
|
56
|
+
pointerEvents: "none",
|
|
57
|
+
backgroundColor: "#1e1e2e",
|
|
58
|
+
color: "#cdd6f4",
|
|
59
|
+
padding: "3px 8px",
|
|
60
|
+
borderRadius: "4px",
|
|
61
|
+
fontSize: "11px",
|
|
62
|
+
fontFamily: "ui-monospace, monospace",
|
|
63
|
+
zIndex: "100000",
|
|
64
|
+
display: "none",
|
|
65
|
+
whiteSpace: "nowrap",
|
|
66
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.3)",
|
|
67
|
+
});
|
|
68
|
+
document.body.appendChild(s.tooltip);
|
|
69
|
+
|
|
70
|
+
s.selectedOverlay = document.createElement("div");
|
|
71
|
+
s.selectedOverlay.id = "tool-selected";
|
|
72
|
+
Object.assign(s.selectedOverlay.style, {
|
|
73
|
+
position: "fixed",
|
|
74
|
+
pointerEvents: "none",
|
|
75
|
+
border: "2px solid #f59e0b",
|
|
76
|
+
backgroundColor: "rgba(245, 158, 11, 0.06)",
|
|
77
|
+
borderRadius: "2px",
|
|
78
|
+
zIndex: "99998",
|
|
79
|
+
display: "none",
|
|
80
|
+
});
|
|
81
|
+
document.body.appendChild(s.selectedOverlay);
|
|
82
|
+
|
|
83
|
+
// --- Helpers ---
|
|
84
|
+
function getElementName(el: Element): string {
|
|
85
|
+
const slot = el.getAttribute("data-slot");
|
|
86
|
+
if (slot) return slot.charAt(0).toUpperCase() + slot.slice(1);
|
|
87
|
+
return `<${el.tagName.toLowerCase()}>`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getDomPath(el: Element): string {
|
|
91
|
+
// Build a pure structural path using nth-child at every level.
|
|
92
|
+
// This is stable across class changes (HMR only replaces attributes,
|
|
93
|
+
// not DOM structure) and guaranteed unique.
|
|
94
|
+
const parts: string[] = [];
|
|
95
|
+
let current: Element | null = el;
|
|
96
|
+
while (current && current !== document.body) {
|
|
97
|
+
const parent = current.parentElement;
|
|
98
|
+
if (parent) {
|
|
99
|
+
const idx = Array.from(parent.children).indexOf(current) + 1;
|
|
100
|
+
parts.unshift(`${current.tagName.toLowerCase()}:nth-child(${idx})`);
|
|
101
|
+
} else {
|
|
102
|
+
parts.unshift(current.tagName.toLowerCase());
|
|
103
|
+
}
|
|
104
|
+
current = current.parentElement;
|
|
105
|
+
}
|
|
106
|
+
return parts.join(" > ");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function positionOverlay(overlay: HTMLDivElement, rect: DOMRect) {
|
|
110
|
+
Object.assign(overlay.style, {
|
|
111
|
+
left: `${rect.left}px`,
|
|
112
|
+
top: `${rect.top}px`,
|
|
113
|
+
width: `${rect.width}px`,
|
|
114
|
+
height: `${rect.height}px`,
|
|
115
|
+
display: "block",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function findSelectableElement(target: Element): Element {
|
|
120
|
+
let el: Element | null = target;
|
|
121
|
+
while (el && el !== document.body) {
|
|
122
|
+
if (el.getAttribute("data-slot")) return el;
|
|
123
|
+
el = el.parentElement;
|
|
124
|
+
}
|
|
125
|
+
return target;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const relevantProps = [
|
|
129
|
+
"display", "position", "top", "right", "bottom", "left",
|
|
130
|
+
"z-index", "overflow", "overflow-x", "overflow-y",
|
|
131
|
+
"flex-direction", "flex-wrap", "justify-content", "align-items",
|
|
132
|
+
"align-self", "flex-grow", "flex-shrink", "flex-basis", "order",
|
|
133
|
+
"grid-template-columns", "grid-template-rows",
|
|
134
|
+
"gap", "row-gap", "column-gap",
|
|
135
|
+
"width", "height", "min-width", "min-height", "max-width", "max-height",
|
|
136
|
+
"margin-top", "margin-right", "margin-bottom", "margin-left",
|
|
137
|
+
"padding-top", "padding-right", "padding-bottom", "padding-left",
|
|
138
|
+
"font-family", "font-size", "font-weight", "line-height",
|
|
139
|
+
"letter-spacing", "text-align", "text-decoration", "text-transform",
|
|
140
|
+
"color", "white-space",
|
|
141
|
+
"background-color", "background-image", "background-size", "background-position",
|
|
142
|
+
"border-top-width", "border-right-width", "border-bottom-width", "border-left-width",
|
|
143
|
+
"border-style", "border-color",
|
|
144
|
+
"border-top-left-radius", "border-top-right-radius",
|
|
145
|
+
"border-bottom-right-radius", "border-bottom-left-radius",
|
|
146
|
+
"opacity", "box-shadow", "transform", "transition",
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const inheritableProps = [
|
|
150
|
+
"color", "font-family", "font-size", "font-weight", "line-height",
|
|
151
|
+
"letter-spacing", "text-align", "text-transform", "white-space",
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
function extractElementData(el: Element) {
|
|
155
|
+
const computed = getComputedStyle(el);
|
|
156
|
+
const rect = el.getBoundingClientRect();
|
|
157
|
+
|
|
158
|
+
const computedStyles: Record<string, string> = {};
|
|
159
|
+
for (const prop of relevantProps) {
|
|
160
|
+
computedStyles[prop] = computed.getPropertyValue(prop);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const parentComputedStyles: Record<string, string> = {};
|
|
164
|
+
const parentEl = el.parentElement;
|
|
165
|
+
if (parentEl) {
|
|
166
|
+
const parentComputed = getComputedStyle(parentEl);
|
|
167
|
+
for (const prop of inheritableProps) {
|
|
168
|
+
parentComputedStyles[prop] = parentComputed.getPropertyValue(prop);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const attributes: Record<string, string> = {};
|
|
173
|
+
for (const attr of Array.from(el.attributes)) {
|
|
174
|
+
if (attr.name.startsWith("data-")) {
|
|
175
|
+
attributes[attr.name] = attr.value;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Parse data-source
|
|
180
|
+
let sourceFile: string | null = null;
|
|
181
|
+
let sourceLine: number | null = null;
|
|
182
|
+
let sourceCol: number | null = null;
|
|
183
|
+
|
|
184
|
+
const dataSource = el.getAttribute("data-source");
|
|
185
|
+
if (dataSource) {
|
|
186
|
+
const lastColon = dataSource.lastIndexOf(":");
|
|
187
|
+
const secondLastColon = dataSource.lastIndexOf(":", lastColon - 1);
|
|
188
|
+
if (secondLastColon > 0) {
|
|
189
|
+
sourceFile = dataSource.slice(0, secondLastColon);
|
|
190
|
+
sourceLine = parseInt(dataSource.slice(secondLastColon + 1, lastColon), 10);
|
|
191
|
+
sourceCol = parseInt(dataSource.slice(lastColon + 1), 10);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// For component instances: read data-instance-source directly from the DOM element.
|
|
196
|
+
// The Babel transform adds this attribute to component JSX (<Button>, <Card>)
|
|
197
|
+
// and it propagates via {...props} to the rendered DOM element, carrying exact
|
|
198
|
+
// page-level coordinates of each component usage site.
|
|
199
|
+
let instanceSourceFile: string | null = null;
|
|
200
|
+
let instanceSourceLine: number | null = null;
|
|
201
|
+
let instanceSourceCol: number | null = null;
|
|
202
|
+
let componentName: string | null = null;
|
|
203
|
+
|
|
204
|
+
const instanceSource = el.getAttribute("data-instance-source");
|
|
205
|
+
if (instanceSource && el.getAttribute("data-slot")) {
|
|
206
|
+
const lc = instanceSource.lastIndexOf(":");
|
|
207
|
+
const slc = instanceSource.lastIndexOf(":", lc - 1);
|
|
208
|
+
if (slc > 0) {
|
|
209
|
+
instanceSourceFile = instanceSource.slice(0, slc);
|
|
210
|
+
instanceSourceLine = parseInt(instanceSource.slice(slc + 1, lc), 10);
|
|
211
|
+
instanceSourceCol = parseInt(instanceSource.slice(lc + 1), 10);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Derive component name from data-slot (e.g. "card-title" -> "CardTitle")
|
|
215
|
+
const slot = el.getAttribute("data-slot") || "";
|
|
216
|
+
componentName = slot
|
|
217
|
+
.split("-")
|
|
218
|
+
.map((s: string) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
219
|
+
.join("");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
tag: el.tagName.toLowerCase(),
|
|
224
|
+
className: (el.getAttribute("class") || "").trim(),
|
|
225
|
+
computedStyles,
|
|
226
|
+
parentComputedStyles,
|
|
227
|
+
boundingRect: rect,
|
|
228
|
+
domPath: getDomPath(el),
|
|
229
|
+
textContent: (el.textContent || "").trim().slice(0, 100),
|
|
230
|
+
attributes,
|
|
231
|
+
sourceFile,
|
|
232
|
+
sourceLine,
|
|
233
|
+
sourceCol,
|
|
234
|
+
instanceSourceFile,
|
|
235
|
+
instanceSourceLine,
|
|
236
|
+
instanceSourceCol,
|
|
237
|
+
componentName,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function selectElement(el: Element) {
|
|
242
|
+
s.selectedElement = el;
|
|
243
|
+
s.selectedDomPath = getDomPath(el);
|
|
244
|
+
const data = extractElementData(el);
|
|
245
|
+
if (s.selectedOverlay) {
|
|
246
|
+
positionOverlay(s.selectedOverlay, data.boundingRect);
|
|
247
|
+
}
|
|
248
|
+
startOverlayTracking();
|
|
249
|
+
window.parent.postMessage({ type: "tool:elementSelected", data }, "*");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function reselectCurrentElement() {
|
|
253
|
+
if (!s.selectedDomPath) return;
|
|
254
|
+
const el = document.querySelector(s.selectedDomPath);
|
|
255
|
+
if (el) {
|
|
256
|
+
s.selectedElement = el;
|
|
257
|
+
const data = extractElementData(el);
|
|
258
|
+
if (s.selectedOverlay) {
|
|
259
|
+
positionOverlay(s.selectedOverlay, data.boundingRect);
|
|
260
|
+
}
|
|
261
|
+
window.parent.postMessage({ type: "tool:elementSelected", data }, "*");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function startOverlayTracking() {
|
|
266
|
+
if (s.overlayRafId) cancelAnimationFrame(s.overlayRafId);
|
|
267
|
+
let lastRect = "";
|
|
268
|
+
function tick() {
|
|
269
|
+
if (s.selectedElement && s.selectedOverlay) {
|
|
270
|
+
if (!document.contains(s.selectedElement)) {
|
|
271
|
+
if (s.selectedDomPath) {
|
|
272
|
+
const newEl = document.querySelector(s.selectedDomPath);
|
|
273
|
+
if (newEl) {
|
|
274
|
+
s.selectedElement = newEl;
|
|
275
|
+
reselectCurrentElement();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (s.selectedElement && document.contains(s.selectedElement)) {
|
|
280
|
+
const rect = s.selectedElement.getBoundingClientRect();
|
|
281
|
+
const key = `${rect.left},${rect.top},${rect.width},${rect.height}`;
|
|
282
|
+
if (key !== lastRect) {
|
|
283
|
+
lastRect = key;
|
|
284
|
+
positionOverlay(s.selectedOverlay!, rect);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
s.overlayRafId = requestAnimationFrame(tick);
|
|
289
|
+
}
|
|
290
|
+
tick();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// --- Event handlers ---
|
|
294
|
+
function onMouseMove(e: MouseEvent) {
|
|
295
|
+
if (!s.selectionMode || !s.highlightOverlay || !s.tooltip) return;
|
|
296
|
+
const el = document.elementFromPoint(e.clientX, e.clientY);
|
|
297
|
+
if (!el || el === s.highlightOverlay || el === s.tooltip || el === s.selectedOverlay) return;
|
|
298
|
+
const selectable = findSelectableElement(el);
|
|
299
|
+
if (selectable === s.hoveredElement) return;
|
|
300
|
+
s.hoveredElement = selectable;
|
|
301
|
+
const rect = selectable.getBoundingClientRect();
|
|
302
|
+
positionOverlay(s.highlightOverlay, rect);
|
|
303
|
+
const name = getElementName(selectable);
|
|
304
|
+
s.tooltip.textContent = name;
|
|
305
|
+
s.tooltip.style.display = "block";
|
|
306
|
+
s.tooltip.style.left = `${rect.left}px`;
|
|
307
|
+
s.tooltip.style.top = `${Math.max(0, rect.top - 24)}px`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function onMouseLeave() {
|
|
311
|
+
if (!s.highlightOverlay || !s.tooltip) return;
|
|
312
|
+
s.highlightOverlay.style.display = "none";
|
|
313
|
+
s.tooltip.style.display = "none";
|
|
314
|
+
s.hoveredElement = null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function onClick(e: MouseEvent) {
|
|
318
|
+
if (!s.selectionMode) return;
|
|
319
|
+
e.preventDefault();
|
|
320
|
+
e.stopPropagation();
|
|
321
|
+
const el = document.elementFromPoint(e.clientX, e.clientY);
|
|
322
|
+
if (!el || el === s.highlightOverlay || el === s.tooltip || el === s.selectedOverlay) return;
|
|
323
|
+
const selectable = findSelectableElement(el);
|
|
324
|
+
selectElement(selectable);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function onMessage(e: MessageEvent) {
|
|
328
|
+
const msg = e.data;
|
|
329
|
+
if (!msg || !msg.type || !msg.type.startsWith("tool:")) return;
|
|
330
|
+
|
|
331
|
+
switch (msg.type) {
|
|
332
|
+
case "tool:enterSelectionMode":
|
|
333
|
+
s.selectionMode = true;
|
|
334
|
+
document.body.style.cursor = "crosshair";
|
|
335
|
+
break;
|
|
336
|
+
case "tool:exitSelectionMode":
|
|
337
|
+
s.selectionMode = false;
|
|
338
|
+
document.body.style.cursor = "";
|
|
339
|
+
if (s.highlightOverlay) s.highlightOverlay.style.display = "none";
|
|
340
|
+
if (s.tooltip) s.tooltip.style.display = "none";
|
|
341
|
+
s.hoveredElement = null;
|
|
342
|
+
break;
|
|
343
|
+
case "tool:previewInlineStyle": {
|
|
344
|
+
if (s.selectedElement && s.selectedElement instanceof HTMLElement) {
|
|
345
|
+
const prop = msg.property as string;
|
|
346
|
+
const value = msg.value as string;
|
|
347
|
+
if (!s.inlineStyleBackups.has(prop)) {
|
|
348
|
+
s.inlineStyleBackups.set(prop, s.selectedElement.style.getPropertyValue(prop));
|
|
349
|
+
}
|
|
350
|
+
s.selectedElement.style.setProperty(prop, value, "important");
|
|
351
|
+
}
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
case "tool:revertInlineStyles": {
|
|
355
|
+
if (s.selectedElement && s.selectedElement instanceof HTMLElement) {
|
|
356
|
+
for (const [prop, original] of s.inlineStyleBackups) {
|
|
357
|
+
if (original) {
|
|
358
|
+
s.selectedElement.style.setProperty(prop, original);
|
|
359
|
+
} else {
|
|
360
|
+
s.selectedElement.style.removeProperty(prop);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
s.inlineStyleBackups.clear();
|
|
364
|
+
}
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
case "tool:previewTokenValue": {
|
|
368
|
+
const prop = msg.property as string;
|
|
369
|
+
const value = msg.value as string;
|
|
370
|
+
// Track current preview value
|
|
371
|
+
s.tokenPreviewValues.set(prop, value);
|
|
372
|
+
// Inject a <style> tag override.
|
|
373
|
+
// Tailwind v4 resolves @theme variables at build time and inlines
|
|
374
|
+
// them into utility classes, so setting CSS custom properties on
|
|
375
|
+
// :root has no effect. Instead we target utility classes directly.
|
|
376
|
+
if (!s.tokenPreviewStyle) {
|
|
377
|
+
s.tokenPreviewStyle = document.createElement("style");
|
|
378
|
+
s.tokenPreviewStyle.id = "codesurface-token-preview";
|
|
379
|
+
document.head.appendChild(s.tokenPreviewStyle);
|
|
380
|
+
}
|
|
381
|
+
const cssRules: string[] = [];
|
|
382
|
+
for (const [k, v] of s.tokenPreviewValues) {
|
|
383
|
+
// Derive Tailwind utility class from CSS variable name:
|
|
384
|
+
// --shadow-sm → .shadow-sm, --shadow → .shadow
|
|
385
|
+
if (k.startsWith("--shadow")) {
|
|
386
|
+
const cls = k.slice(2); // "--shadow-sm" → "shadow-sm"
|
|
387
|
+
cssRules.push(`.${cls}, [class*="${cls}"] { box-shadow: ${v} !important; }`);
|
|
388
|
+
} else {
|
|
389
|
+
// For other tokens (colors, spacing, etc.) override the custom property
|
|
390
|
+
cssRules.push(`*, *::before, *::after { ${k}: ${v} !important; }`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
s.tokenPreviewStyle.textContent = cssRules.join("\n");
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
case "tool:revertTokenValues": {
|
|
397
|
+
if (s.tokenPreviewStyle) {
|
|
398
|
+
s.tokenPreviewStyle.remove();
|
|
399
|
+
s.tokenPreviewStyle = null;
|
|
400
|
+
}
|
|
401
|
+
s.tokenPreviewValues.clear();
|
|
402
|
+
s.tokenValueBackups.clear();
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
case "tool:reselectElement":
|
|
406
|
+
reselectCurrentElement();
|
|
407
|
+
break;
|
|
408
|
+
case "tool:setTheme":
|
|
409
|
+
if (msg.theme === "dark") {
|
|
410
|
+
document.documentElement.classList.add("dark");
|
|
411
|
+
} else {
|
|
412
|
+
document.documentElement.classList.remove("dark");
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function notifyPathChanged() {
|
|
419
|
+
const fullPath = window.location.pathname + window.location.search + window.location.hash;
|
|
420
|
+
window.parent.postMessage({ type: "tool:pathChanged", path: fullPath }, "*");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// --- Init ---
|
|
424
|
+
document.addEventListener("mousemove", onMouseMove, true);
|
|
425
|
+
document.addEventListener("mouseleave", onMouseLeave);
|
|
426
|
+
document.addEventListener("click", onClick, true);
|
|
427
|
+
window.addEventListener("message", onMessage);
|
|
428
|
+
window.addEventListener("popstate", notifyPathChanged);
|
|
429
|
+
|
|
430
|
+
// Notify editor that we're ready
|
|
431
|
+
window.parent.postMessage({ type: "tool:injectedReady" }, "*");
|
|
432
|
+
notifyPathChanged();
|
|
433
|
+
|
|
434
|
+
// --- Cleanup ---
|
|
435
|
+
return () => {
|
|
436
|
+
document.removeEventListener("mousemove", onMouseMove, true);
|
|
437
|
+
document.removeEventListener("mouseleave", onMouseLeave);
|
|
438
|
+
document.removeEventListener("click", onClick, true);
|
|
439
|
+
window.removeEventListener("message", onMessage);
|
|
440
|
+
window.removeEventListener("popstate", notifyPathChanged);
|
|
441
|
+
|
|
442
|
+
if (s.overlayRafId) cancelAnimationFrame(s.overlayRafId);
|
|
443
|
+
s.tokenPreviewStyle?.remove();
|
|
444
|
+
s.highlightOverlay?.remove();
|
|
445
|
+
s.tooltip?.remove();
|
|
446
|
+
s.selectedOverlay?.remove();
|
|
447
|
+
};
|
|
448
|
+
}, []);
|
|
449
|
+
|
|
450
|
+
// This component renders nothing — overlays are created imperatively
|
|
451
|
+
return null;
|
|
452
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js config wrapper that adds the designtools source annotation loader
|
|
3
|
+
* and auto-mounts the <CodeSurface /> selection component in development.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* import { withDesigntools } from "@designtools/next-plugin";
|
|
7
|
+
* export default withDesigntools({ ...yourConfig });
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from "path";
|
|
11
|
+
|
|
12
|
+
export function withDesigntools<T extends Record<string, any>>(nextConfig: T = {} as T): T {
|
|
13
|
+
return {
|
|
14
|
+
...nextConfig,
|
|
15
|
+
webpack(config: any, context: any) {
|
|
16
|
+
// Only add the loader in development
|
|
17
|
+
if (context.dev) {
|
|
18
|
+
config.module.rules.push({
|
|
19
|
+
test: /\.(tsx|jsx)$/,
|
|
20
|
+
exclude: /node_modules/,
|
|
21
|
+
use: [
|
|
22
|
+
{
|
|
23
|
+
loader: path.resolve(__dirname, "loader.js"),
|
|
24
|
+
options: {
|
|
25
|
+
cwd: context.dir,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Add a loader for root layout files that auto-mounts <CodeSurface />
|
|
32
|
+
config.module.rules.push({
|
|
33
|
+
test: /layout\.(tsx|jsx)$/,
|
|
34
|
+
include: [
|
|
35
|
+
path.resolve(context.dir, "app"),
|
|
36
|
+
path.resolve(context.dir, "src/app"),
|
|
37
|
+
],
|
|
38
|
+
use: [
|
|
39
|
+
{
|
|
40
|
+
loader: path.resolve(__dirname, "codesurface-mount-loader.js"),
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Call the user's webpack config if provided
|
|
47
|
+
if (typeof nextConfig.webpack === "function") {
|
|
48
|
+
return nextConfig.webpack(config, context);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return config;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
package/src/loader.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webpack loader that adds data-source="file:line:col" attributes to JSX elements.
|
|
3
|
+
* Uses Babel's parser for correct JSX identification (avoids matching TypeScript generics).
|
|
4
|
+
* SWC stays enabled as the primary compiler — Babel is only used here for the source annotation pass.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from "path";
|
|
8
|
+
|
|
9
|
+
interface LoaderContext {
|
|
10
|
+
resourcePath: string;
|
|
11
|
+
rootContext: string;
|
|
12
|
+
getOptions(): { cwd?: string };
|
|
13
|
+
callback(err: Error | null, content?: string, sourceMap?: any): void;
|
|
14
|
+
async(): (err: Error | null, content?: string, sourceMap?: any) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function designtoolsLoader(this: LoaderContext, source: string): void {
|
|
18
|
+
const callback = this.async();
|
|
19
|
+
const opts = this.getOptions();
|
|
20
|
+
const cwd = opts.cwd || this.rootContext || process.cwd();
|
|
21
|
+
const relativePath = path.relative(cwd, this.resourcePath);
|
|
22
|
+
|
|
23
|
+
// Skip node_modules
|
|
24
|
+
if (relativePath.includes("node_modules")) {
|
|
25
|
+
callback(null, source);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Quick check: skip files with no JSX
|
|
30
|
+
if (!source.includes("<")) {
|
|
31
|
+
callback(null, source);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// @babel/core is available at runtime via Next.js — don't bundle it
|
|
37
|
+
const babel = require("@babel/core");
|
|
38
|
+
|
|
39
|
+
const isTsx = this.resourcePath.endsWith(".tsx");
|
|
40
|
+
|
|
41
|
+
const result = babel.transformSync(source, {
|
|
42
|
+
filename: this.resourcePath,
|
|
43
|
+
// No presets — we only want to parse and run our visitor, not compile
|
|
44
|
+
presets: [],
|
|
45
|
+
plugins: [
|
|
46
|
+
function designtoolsSourcePlugin() {
|
|
47
|
+
return {
|
|
48
|
+
visitor: {
|
|
49
|
+
JSXOpeningElement(nodePath: any) {
|
|
50
|
+
const t = babel.types;
|
|
51
|
+
const attrs = nodePath.node.attributes;
|
|
52
|
+
const name = nodePath.node.name;
|
|
53
|
+
|
|
54
|
+
// Skip fragments
|
|
55
|
+
if (t.isJSXIdentifier(name) && name.name === "Fragment") return;
|
|
56
|
+
if (t.isJSXMemberExpression(name) &&
|
|
57
|
+
t.isJSXIdentifier(name.property) &&
|
|
58
|
+
name.property.name === "Fragment") return;
|
|
59
|
+
|
|
60
|
+
const loc = nodePath.node.loc;
|
|
61
|
+
if (!loc) return;
|
|
62
|
+
|
|
63
|
+
const value = `${relativePath}:${loc.start.line}:${loc.start.column}`;
|
|
64
|
+
|
|
65
|
+
// Detect component elements (uppercase first letter or member expression like Foo.Bar)
|
|
66
|
+
const isComponent =
|
|
67
|
+
(t.isJSXIdentifier(name) && name.name[0] === name.name[0].toUpperCase() && name.name[0] !== name.name[0].toLowerCase()) ||
|
|
68
|
+
t.isJSXMemberExpression(name);
|
|
69
|
+
|
|
70
|
+
if (isComponent) {
|
|
71
|
+
// Component elements get data-instance-source — this attribute
|
|
72
|
+
// propagates through {...props} spread to the rendered DOM element,
|
|
73
|
+
// carrying the exact page-level coordinates of each component usage.
|
|
74
|
+
// The component's own data-source (on its root native element) won't
|
|
75
|
+
// collide because it uses a different attribute name.
|
|
76
|
+
const attrName = "data-instance-source";
|
|
77
|
+
if (attrs.some((a: any) =>
|
|
78
|
+
t.isJSXAttribute(a) &&
|
|
79
|
+
t.isJSXIdentifier(a.name) &&
|
|
80
|
+
a.name.name === attrName
|
|
81
|
+
)) return;
|
|
82
|
+
attrs.push(
|
|
83
|
+
t.jsxAttribute(
|
|
84
|
+
t.jsxIdentifier(attrName),
|
|
85
|
+
t.stringLiteral(value)
|
|
86
|
+
)
|
|
87
|
+
);
|
|
88
|
+
} else {
|
|
89
|
+
// Native elements get data-source as before
|
|
90
|
+
if (attrs.some((a: any) =>
|
|
91
|
+
t.isJSXAttribute(a) &&
|
|
92
|
+
t.isJSXIdentifier(a.name) &&
|
|
93
|
+
a.name.name === "data-source"
|
|
94
|
+
)) return;
|
|
95
|
+
attrs.push(
|
|
96
|
+
t.jsxAttribute(
|
|
97
|
+
t.jsxIdentifier("data-source"),
|
|
98
|
+
t.stringLiteral(value)
|
|
99
|
+
)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
// Tell Babel's parser to handle JSX and TypeScript syntax
|
|
108
|
+
parserOpts: {
|
|
109
|
+
plugins: ["jsx", ...(isTsx ? ["typescript" as const] : [])],
|
|
110
|
+
},
|
|
111
|
+
// Preserve original formatting as much as possible
|
|
112
|
+
retainLines: true,
|
|
113
|
+
// Don't look for user's .babelrc or babel.config — isolation
|
|
114
|
+
configFile: false,
|
|
115
|
+
babelrc: false,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
callback(null, result?.code || source);
|
|
119
|
+
} catch (err: any) {
|
|
120
|
+
// If Babel fails, pass through the original source
|
|
121
|
+
// This ensures the app still works even if our plugin has issues
|
|
122
|
+
console.warn(`[designtools] Source annotation skipped for ${relativePath}: ${err.message}`);
|
|
123
|
+
callback(null, source);
|
|
124
|
+
}
|
|
125
|
+
}
|