@designtools/next-plugin 0.1.6 → 0.1.7
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/README.md +58 -0
- package/dist/chunk-6DZX6EAA.mjs +37 -0
- package/dist/codesurface-mount-loader.js +6 -4
- package/dist/codesurface-mount-loader.mjs +6 -4
- package/dist/codesurface.d.mts +3 -1
- package/dist/codesurface.d.ts +3 -1
- package/dist/codesurface.js +192 -12
- package/dist/codesurface.mjs +193 -13
- package/dist/index.js +19 -155
- package/dist/index.mjs +19 -155
- package/package.json +2 -2
- package/src/codesurface-mount-loader.ts +10 -6
- package/src/codesurface.tsx +254 -25
- package/src/index.ts +3 -5
- package/src/preview-route.ts +30 -174
|
@@ -28,17 +28,21 @@ export default function codesurfaceMountLoader(this: LoaderContext, source: stri
|
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
// Add
|
|
32
|
-
//
|
|
33
|
-
const
|
|
31
|
+
// Add imports at the top (after "use client" or first import)
|
|
32
|
+
// The registry import is a side-effect — it self-registers on window.
|
|
33
|
+
const importStatements = [
|
|
34
|
+
`import { CodeSurface } from "@designtools/next-plugin/codesurface";`,
|
|
35
|
+
`import "./designtools-registry";`,
|
|
36
|
+
].join("\n") + "\n";
|
|
37
|
+
|
|
34
38
|
let modified = source;
|
|
35
39
|
|
|
36
|
-
// Find a good insertion point for the
|
|
40
|
+
// Find a good insertion point for the imports
|
|
37
41
|
const firstImportIndex = source.indexOf("import ");
|
|
38
42
|
if (firstImportIndex !== -1) {
|
|
39
|
-
modified = source.slice(0, firstImportIndex) +
|
|
43
|
+
modified = source.slice(0, firstImportIndex) + importStatements + source.slice(firstImportIndex);
|
|
40
44
|
} else {
|
|
41
|
-
modified =
|
|
45
|
+
modified = importStatements + source;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
// Add <CodeSurface /> just before {children}
|
package/src/codesurface.tsx
CHANGED
|
@@ -9,12 +9,18 @@
|
|
|
9
9
|
* with proper lifecycle management.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { useEffect, useRef } from "react";
|
|
12
|
+
import { useEffect, useRef, useState, createElement } from "react";
|
|
13
|
+
import { createPortal } from "react-dom";
|
|
13
14
|
|
|
14
15
|
// Overlay elements are created imperatively (not React-rendered)
|
|
15
16
|
// because they need to be fixed-position overlays that don't interfere
|
|
16
17
|
// with the app's React tree.
|
|
17
18
|
|
|
19
|
+
interface PreviewCombination {
|
|
20
|
+
label: string;
|
|
21
|
+
props: Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
export function CodeSurface() {
|
|
19
25
|
const stateRef = useRef({
|
|
20
26
|
selectionMode: false,
|
|
@@ -31,6 +37,29 @@ export function CodeSurface() {
|
|
|
31
37
|
selectedOverlay: null as HTMLDivElement | null,
|
|
32
38
|
});
|
|
33
39
|
|
|
40
|
+
// Preview overlay state
|
|
41
|
+
const [previewComponent, setPreviewComponent] = useState<React.ComponentType<any> | null>(null);
|
|
42
|
+
const [previewCombinations, setPreviewCombinations] = useState<PreviewCombination[]>([]);
|
|
43
|
+
const [previewDefaultChildren, setPreviewDefaultChildren] = useState("");
|
|
44
|
+
const [previewError, setPreviewError] = useState<string | null>(null);
|
|
45
|
+
const [showPreview, setShowPreview] = useState(false);
|
|
46
|
+
|
|
47
|
+
// Ref so the imperative useEffect can trigger state updates
|
|
48
|
+
const setPreviewRef = useRef({
|
|
49
|
+
setPreviewComponent,
|
|
50
|
+
setPreviewCombinations,
|
|
51
|
+
setPreviewDefaultChildren,
|
|
52
|
+
setPreviewError,
|
|
53
|
+
setShowPreview,
|
|
54
|
+
});
|
|
55
|
+
setPreviewRef.current = {
|
|
56
|
+
setPreviewComponent,
|
|
57
|
+
setPreviewCombinations,
|
|
58
|
+
setPreviewDefaultChildren,
|
|
59
|
+
setPreviewError,
|
|
60
|
+
setShowPreview,
|
|
61
|
+
};
|
|
62
|
+
|
|
34
63
|
useEffect(() => {
|
|
35
64
|
const s = stateRef.current;
|
|
36
65
|
|
|
@@ -593,6 +622,49 @@ export function CodeSurface() {
|
|
|
593
622
|
"letter-spacing", "text-align", "text-transform", "white-space",
|
|
594
623
|
];
|
|
595
624
|
|
|
625
|
+
/**
|
|
626
|
+
* Walk UP the fiber tree from a DOM element's fiber to find the
|
|
627
|
+
* component that renders this element as its root host element.
|
|
628
|
+
* Checks tags 0 (Function), 1 (Class), 11 (ForwardRef),
|
|
629
|
+
* 14 (Memo), 15 (SimpleMemo) as component boundaries.
|
|
630
|
+
*/
|
|
631
|
+
function findComponentFiberAbove(el: Element): any | null {
|
|
632
|
+
const fiber = getFiber(el);
|
|
633
|
+
if (!fiber) return null;
|
|
634
|
+
let candidate = fiber.return;
|
|
635
|
+
while (candidate) {
|
|
636
|
+
const tag = candidate.tag;
|
|
637
|
+
if (tag === 0 || tag === 1 || tag === 11 || tag === 14 || tag === 15) {
|
|
638
|
+
// Check if this component's root host element is our element
|
|
639
|
+
if (findOwnHostElement(candidate) === el) return candidate;
|
|
640
|
+
}
|
|
641
|
+
candidate = candidate.return;
|
|
642
|
+
}
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Read memoizedProps from a fiber, filtering to simple values only.
|
|
648
|
+
* Skips children, ref, key, className, style, and data-* props.
|
|
649
|
+
*/
|
|
650
|
+
function extractFiberProps(fiber: any): Record<string, string | number | boolean> | null {
|
|
651
|
+
const props = fiber?.memoizedProps;
|
|
652
|
+
if (!props || typeof props !== "object") return null;
|
|
653
|
+
const skipKeys = new Set(["children", "ref", "key", "className", "style"]);
|
|
654
|
+
const result: Record<string, string | number | boolean> = {};
|
|
655
|
+
let count = 0;
|
|
656
|
+
for (const k of Object.keys(props)) {
|
|
657
|
+
if (skipKeys.has(k)) continue;
|
|
658
|
+
if (k.startsWith("data-")) continue;
|
|
659
|
+
const v = props[k];
|
|
660
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
661
|
+
result[k] = v;
|
|
662
|
+
count++;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return count > 0 ? result : null;
|
|
666
|
+
}
|
|
667
|
+
|
|
596
668
|
function extractElementData(el: Element) {
|
|
597
669
|
const computed = getComputedStyle(el);
|
|
598
670
|
const rect = el.getBoundingClientRect();
|
|
@@ -635,16 +707,14 @@ export function CodeSurface() {
|
|
|
635
707
|
}
|
|
636
708
|
|
|
637
709
|
// For component instances: read data-instance-source directly from the DOM element.
|
|
638
|
-
// The Babel transform adds this attribute to component JSX (<Button>, <Card>)
|
|
639
|
-
// and it propagates via {...props} to the rendered DOM element, carrying exact
|
|
640
|
-
// page-level coordinates of each component usage site.
|
|
641
710
|
let instanceSourceFile: string | null = null;
|
|
642
711
|
let instanceSourceLine: number | null = null;
|
|
643
712
|
let instanceSourceCol: number | null = null;
|
|
644
713
|
let componentName: string | null = null;
|
|
645
714
|
|
|
715
|
+
const dataSlot = el.getAttribute("data-slot");
|
|
646
716
|
const instanceSource = el.getAttribute("data-instance-source");
|
|
647
|
-
if (instanceSource &&
|
|
717
|
+
if (instanceSource && dataSlot) {
|
|
648
718
|
const lc = instanceSource.lastIndexOf(":");
|
|
649
719
|
const slc = instanceSource.lastIndexOf(":", lc - 1);
|
|
650
720
|
if (slc > 0) {
|
|
@@ -654,13 +724,21 @@ export function CodeSurface() {
|
|
|
654
724
|
}
|
|
655
725
|
|
|
656
726
|
// Derive component name from data-slot (e.g. "card-title" -> "CardTitle")
|
|
657
|
-
|
|
658
|
-
componentName = slot
|
|
727
|
+
componentName = dataSlot
|
|
659
728
|
.split("-")
|
|
660
729
|
.map((s: string) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
661
730
|
.join("");
|
|
662
731
|
}
|
|
663
732
|
|
|
733
|
+
// Extract runtime props from React fiber for component instances
|
|
734
|
+
let fiberProps: Record<string, string | number | boolean> | null = null;
|
|
735
|
+
if (dataSlot) {
|
|
736
|
+
const compFiber = findComponentFiberAbove(el);
|
|
737
|
+
if (compFiber) {
|
|
738
|
+
fiberProps = extractFiberProps(compFiber);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
664
742
|
return {
|
|
665
743
|
tag: el.tagName.toLowerCase(),
|
|
666
744
|
className: (el.getAttribute("class") || "").trim(),
|
|
@@ -677,6 +755,7 @@ export function CodeSurface() {
|
|
|
677
755
|
instanceSourceLine,
|
|
678
756
|
instanceSourceCol,
|
|
679
757
|
componentName,
|
|
758
|
+
fiberProps,
|
|
680
759
|
};
|
|
681
760
|
}
|
|
682
761
|
|
|
@@ -732,6 +811,27 @@ export function CodeSurface() {
|
|
|
732
811
|
tick();
|
|
733
812
|
}
|
|
734
813
|
|
|
814
|
+
// --- Helper to hide/show selection overlays ---
|
|
815
|
+
function hideSelectionOverlays() {
|
|
816
|
+
if (s.highlightOverlay) s.highlightOverlay.style.display = "none";
|
|
817
|
+
if (s.tooltip) s.tooltip.style.display = "none";
|
|
818
|
+
if (s.selectedOverlay) s.selectedOverlay.style.display = "none";
|
|
819
|
+
s.hoveredElement = null;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function enterSelectionMode() {
|
|
823
|
+
s.selectionMode = true;
|
|
824
|
+
document.body.style.cursor = "crosshair";
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function exitSelectionMode() {
|
|
828
|
+
s.selectionMode = false;
|
|
829
|
+
document.body.style.cursor = "";
|
|
830
|
+
if (s.highlightOverlay) s.highlightOverlay.style.display = "none";
|
|
831
|
+
if (s.tooltip) s.tooltip.style.display = "none";
|
|
832
|
+
s.hoveredElement = null;
|
|
833
|
+
}
|
|
834
|
+
|
|
735
835
|
// --- Event handlers ---
|
|
736
836
|
function onMouseMove(e: MouseEvent) {
|
|
737
837
|
if (!s.selectionMode || !s.highlightOverlay || !s.tooltip) return;
|
|
@@ -772,15 +872,10 @@ export function CodeSurface() {
|
|
|
772
872
|
|
|
773
873
|
switch (msg.type) {
|
|
774
874
|
case "tool:enterSelectionMode":
|
|
775
|
-
|
|
776
|
-
document.body.style.cursor = "crosshair";
|
|
875
|
+
enterSelectionMode();
|
|
777
876
|
break;
|
|
778
877
|
case "tool:exitSelectionMode":
|
|
779
|
-
|
|
780
|
-
document.body.style.cursor = "";
|
|
781
|
-
if (s.highlightOverlay) s.highlightOverlay.style.display = "none";
|
|
782
|
-
if (s.tooltip) s.tooltip.style.display = "none";
|
|
783
|
-
s.hoveredElement = null;
|
|
878
|
+
exitSelectionMode();
|
|
784
879
|
break;
|
|
785
880
|
case "tool:previewInlineStyle": {
|
|
786
881
|
if (s.selectedElement && s.selectedElement instanceof HTMLElement) {
|
|
@@ -809,12 +904,7 @@ export function CodeSurface() {
|
|
|
809
904
|
case "tool:previewTokenValue": {
|
|
810
905
|
const prop = msg.property as string;
|
|
811
906
|
const value = msg.value as string;
|
|
812
|
-
// Track current preview value
|
|
813
907
|
s.tokenPreviewValues.set(prop, value);
|
|
814
|
-
// Inject a <style> tag override.
|
|
815
|
-
// Tailwind v4 resolves @theme variables at build time and inlines
|
|
816
|
-
// them into utility classes, so setting CSS custom properties on
|
|
817
|
-
// :root has no effect. Instead we target utility classes directly.
|
|
818
908
|
if (!s.tokenPreviewStyle) {
|
|
819
909
|
s.tokenPreviewStyle = document.createElement("style");
|
|
820
910
|
s.tokenPreviewStyle.id = "codesurface-token-preview";
|
|
@@ -822,13 +912,10 @@ export function CodeSurface() {
|
|
|
822
912
|
}
|
|
823
913
|
const cssRules: string[] = [];
|
|
824
914
|
for (const [k, v] of s.tokenPreviewValues) {
|
|
825
|
-
// Derive Tailwind utility class from CSS variable name:
|
|
826
|
-
// --shadow-sm → .shadow-sm, --shadow → .shadow
|
|
827
915
|
if (k.startsWith("--shadow")) {
|
|
828
|
-
const cls = k.slice(2);
|
|
916
|
+
const cls = k.slice(2);
|
|
829
917
|
cssRules.push(`.${cls}, [class*="${cls}"] { box-shadow: ${v} !important; }`);
|
|
830
918
|
} else {
|
|
831
|
-
// For other tokens (colors, spacing, etc.) override the custom property
|
|
832
919
|
cssRules.push(`*, *::before, *::after { ${k}: ${v} !important; }`);
|
|
833
920
|
}
|
|
834
921
|
}
|
|
@@ -886,6 +973,66 @@ export function CodeSurface() {
|
|
|
886
973
|
}
|
|
887
974
|
break;
|
|
888
975
|
}
|
|
976
|
+
case "tool:renderPreview": {
|
|
977
|
+
const { componentPath, exportName, combinations: combos, defaultChildren: children } = msg;
|
|
978
|
+
const currentRegistry = (window as any).__DESIGNTOOLS_REGISTRY__ as Record<string, () => Promise<any>> | undefined;
|
|
979
|
+
|
|
980
|
+
if (!currentRegistry) {
|
|
981
|
+
setPreviewRef.current.setPreviewError("No component registry available. Ensure designtools-registry.ts is imported.");
|
|
982
|
+
setPreviewRef.current.setShowPreview(true);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const loader = currentRegistry[componentPath];
|
|
987
|
+
if (!loader) {
|
|
988
|
+
setPreviewRef.current.setPreviewError(
|
|
989
|
+
`Component "${componentPath}" not found in registry. Available: ${Object.keys(currentRegistry).join(", ")}`
|
|
990
|
+
);
|
|
991
|
+
setPreviewRef.current.setShowPreview(true);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Disable selection mode and hide overlays
|
|
996
|
+
exitSelectionMode();
|
|
997
|
+
hideSelectionOverlays();
|
|
998
|
+
|
|
999
|
+
// Load the component
|
|
1000
|
+
loader().then((mod: any) => {
|
|
1001
|
+
const Comp = mod[exportName] || mod.default;
|
|
1002
|
+
if (!Comp) {
|
|
1003
|
+
setPreviewRef.current.setPreviewError(`Export "${exportName}" not found in ${componentPath}`);
|
|
1004
|
+
setPreviewRef.current.setShowPreview(true);
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
setPreviewRef.current.setPreviewError(null);
|
|
1009
|
+
setPreviewRef.current.setPreviewComponent(() => Comp);
|
|
1010
|
+
setPreviewRef.current.setPreviewCombinations(combos || []);
|
|
1011
|
+
setPreviewRef.current.setPreviewDefaultChildren(children || exportName);
|
|
1012
|
+
setPreviewRef.current.setShowPreview(true);
|
|
1013
|
+
|
|
1014
|
+
// Notify editor that preview is ready
|
|
1015
|
+
window.parent.postMessage(
|
|
1016
|
+
{ type: "tool:previewReady", cellCount: (combos || []).length },
|
|
1017
|
+
"*"
|
|
1018
|
+
);
|
|
1019
|
+
}).catch((err: any) => {
|
|
1020
|
+
setPreviewRef.current.setPreviewError(`Failed to load component: ${err.message}`);
|
|
1021
|
+
setPreviewRef.current.setShowPreview(true);
|
|
1022
|
+
});
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
1025
|
+
case "tool:exitPreview": {
|
|
1026
|
+
setPreviewRef.current.setShowPreview(false);
|
|
1027
|
+
setPreviewRef.current.setPreviewComponent(null);
|
|
1028
|
+
setPreviewRef.current.setPreviewCombinations([]);
|
|
1029
|
+
setPreviewRef.current.setPreviewDefaultChildren("");
|
|
1030
|
+
setPreviewRef.current.setPreviewError(null);
|
|
1031
|
+
|
|
1032
|
+
// Restore selection mode
|
|
1033
|
+
enterSelectionMode();
|
|
1034
|
+
break;
|
|
1035
|
+
}
|
|
889
1036
|
}
|
|
890
1037
|
}
|
|
891
1038
|
|
|
@@ -923,6 +1070,88 @@ export function CodeSurface() {
|
|
|
923
1070
|
};
|
|
924
1071
|
}, []);
|
|
925
1072
|
|
|
926
|
-
//
|
|
927
|
-
return null;
|
|
1073
|
+
// --- Preview overlay rendered via portal ---
|
|
1074
|
+
if (!showPreview) return null;
|
|
1075
|
+
|
|
1076
|
+
if (previewError) {
|
|
1077
|
+
return createPortal(
|
|
1078
|
+
<div style={{
|
|
1079
|
+
position: "fixed",
|
|
1080
|
+
inset: 0,
|
|
1081
|
+
zIndex: 999999,
|
|
1082
|
+
background: "var(--background, white)",
|
|
1083
|
+
overflow: "auto",
|
|
1084
|
+
padding: 32,
|
|
1085
|
+
}}>
|
|
1086
|
+
<div style={{ color: "var(--destructive, #ef4444)", fontFamily: "monospace", fontSize: 14 }}>
|
|
1087
|
+
{previewError}
|
|
1088
|
+
</div>
|
|
1089
|
+
</div>,
|
|
1090
|
+
document.body
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
if (!previewComponent) {
|
|
1095
|
+
return createPortal(
|
|
1096
|
+
<div style={{
|
|
1097
|
+
position: "fixed",
|
|
1098
|
+
inset: 0,
|
|
1099
|
+
zIndex: 999999,
|
|
1100
|
+
background: "var(--background, white)",
|
|
1101
|
+
overflow: "auto",
|
|
1102
|
+
padding: 32,
|
|
1103
|
+
}}>
|
|
1104
|
+
<div style={{ color: "var(--muted-foreground, #888)", fontFamily: "inherit", fontSize: 14 }}>
|
|
1105
|
+
Loading component...
|
|
1106
|
+
</div>
|
|
1107
|
+
</div>,
|
|
1108
|
+
document.body
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const Component = previewComponent;
|
|
1113
|
+
|
|
1114
|
+
return createPortal(
|
|
1115
|
+
<div style={{
|
|
1116
|
+
position: "fixed",
|
|
1117
|
+
inset: 0,
|
|
1118
|
+
zIndex: 999999,
|
|
1119
|
+
background: "var(--background, white)",
|
|
1120
|
+
overflow: "auto",
|
|
1121
|
+
padding: 32,
|
|
1122
|
+
}}>
|
|
1123
|
+
<div style={{
|
|
1124
|
+
display: "grid",
|
|
1125
|
+
gridTemplateColumns: "repeat(auto-fill, minmax(240px, 1fr))",
|
|
1126
|
+
gap: 24,
|
|
1127
|
+
}}>
|
|
1128
|
+
{previewCombinations.map((combo, i) => (
|
|
1129
|
+
<div key={i} style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
1130
|
+
<div style={{
|
|
1131
|
+
fontSize: 11,
|
|
1132
|
+
fontWeight: 600,
|
|
1133
|
+
color: "var(--muted-foreground, #888)",
|
|
1134
|
+
textTransform: "uppercase" as const,
|
|
1135
|
+
letterSpacing: "0.05em",
|
|
1136
|
+
}}>
|
|
1137
|
+
{combo.label}
|
|
1138
|
+
</div>
|
|
1139
|
+
<div style={{
|
|
1140
|
+
padding: 16,
|
|
1141
|
+
border: "1px solid var(--border, #e5e7eb)",
|
|
1142
|
+
borderRadius: 8,
|
|
1143
|
+
display: "flex",
|
|
1144
|
+
alignItems: "center",
|
|
1145
|
+
justifyContent: "center",
|
|
1146
|
+
minHeight: 64,
|
|
1147
|
+
background: "var(--card, var(--background, #fff))",
|
|
1148
|
+
}}>
|
|
1149
|
+
{createElement(Component, combo.props, previewDefaultChildren)}
|
|
1150
|
+
</div>
|
|
1151
|
+
</div>
|
|
1152
|
+
))}
|
|
1153
|
+
</div>
|
|
1154
|
+
</div>,
|
|
1155
|
+
document.body
|
|
1156
|
+
);
|
|
928
1157
|
}
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import path from "path";
|
|
11
|
-
import {
|
|
11
|
+
import { generateComponentRegistry } from "./preview-route.js";
|
|
12
12
|
|
|
13
13
|
export function withDesigntools<T extends Record<string, any>>(nextConfig: T = {} as T): T {
|
|
14
14
|
return {
|
|
@@ -43,12 +43,10 @@ export function withDesigntools<T extends Record<string, any>>(nextConfig: T = {
|
|
|
43
43
|
],
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
// Generate the component isolation
|
|
47
|
-
// This creates app/__designtools/preview/page.tsx which Next.js
|
|
48
|
-
// picks up as a route automatically via file-system routing.
|
|
46
|
+
// Generate the component registry for isolation overlay
|
|
49
47
|
const appDir = path.resolve(context.dir, "app");
|
|
50
48
|
try {
|
|
51
|
-
|
|
49
|
+
generateComponentRegistry(appDir);
|
|
52
50
|
} catch {
|
|
53
51
|
// Non-fatal — isolation feature just won't work
|
|
54
52
|
}
|
package/src/preview-route.ts
CHANGED
|
@@ -1,51 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Component registry generator for component isolation.
|
|
3
3
|
*
|
|
4
|
-
* Writes a
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Writes a static import map file into the target app's app directory.
|
|
5
|
+
* The <CodeSurface /> component uses this registry to dynamically load
|
|
6
|
+
* components for the isolation overlay — no route change needed.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import fs from "fs";
|
|
10
10
|
import path from "path";
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const REGISTRY_FILE = "designtools-registry.ts";
|
|
13
13
|
|
|
14
|
-
/** Generate the
|
|
15
|
-
export function
|
|
14
|
+
/** Generate the component registry file in the target app's app directory. */
|
|
15
|
+
export function generateComponentRegistry(appDir: string): void {
|
|
16
16
|
const projectRoot = path.dirname(appDir);
|
|
17
|
-
const previewDir = path.join(appDir, PREVIEW_DIR);
|
|
18
17
|
|
|
19
18
|
// Discover component files so we can generate static imports
|
|
20
19
|
const componentPaths = discoverComponentFiles(projectRoot);
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const registryEntries = componentPaths
|
|
22
|
+
.map((p) => ` "${p}": () => import("@/${p}"),`)
|
|
23
|
+
.join("\n");
|
|
24
|
+
|
|
25
|
+
const content = `"use client";
|
|
26
|
+
// Auto-generated by @designtools/next-plugin — do not edit
|
|
27
|
+
|
|
28
|
+
const COMPONENT_REGISTRY: Record<string, () => Promise<any>> = {
|
|
29
|
+
${registryEntries}
|
|
30
|
+
};
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
// Self-register on window so CodeSurface can access it without
|
|
33
|
+
// passing functions through the RSC serialization boundary.
|
|
34
|
+
if (typeof window !== "undefined") {
|
|
35
|
+
(window as any).__DESIGNTOOLS_REGISTRY__ = COMPONENT_REGISTRY;
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
fs.writeFileSync(
|
|
34
|
-
path.join(previewDir, "page.tsx"),
|
|
35
|
-
getPageTemplate(componentPaths),
|
|
36
|
-
"utf-8"
|
|
37
|
-
);
|
|
39
|
+
fs.writeFileSync(path.join(appDir, REGISTRY_FILE), content, "utf-8");
|
|
38
40
|
|
|
39
|
-
// Ensure
|
|
41
|
+
// Ensure registry file is gitignored
|
|
40
42
|
ensureGitignore(projectRoot);
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
/** Clean up generated
|
|
44
|
-
export function
|
|
45
|
-
const
|
|
45
|
+
/** Clean up generated registry file. */
|
|
46
|
+
export function cleanupComponentRegistry(appDir: string): void {
|
|
47
|
+
const registryPath = path.join(appDir, REGISTRY_FILE);
|
|
46
48
|
try {
|
|
47
|
-
fs.
|
|
48
|
-
// Directory itself is removed by rmSync above
|
|
49
|
+
fs.unlinkSync(registryPath);
|
|
49
50
|
} catch {
|
|
50
51
|
// ignore
|
|
51
52
|
}
|
|
@@ -53,7 +54,7 @@ export function cleanupPreviewRoute(appDir: string): void {
|
|
|
53
54
|
|
|
54
55
|
function ensureGitignore(projectRoot: string): void {
|
|
55
56
|
const gitignorePath = path.join(projectRoot, ".gitignore");
|
|
56
|
-
const entry = "app/designtools-
|
|
57
|
+
const entry = "app/designtools-registry.ts";
|
|
57
58
|
|
|
58
59
|
try {
|
|
59
60
|
const existing = fs.existsSync(gitignorePath)
|
|
@@ -84,148 +85,3 @@ function discoverComponentFiles(projectRoot: string): string[] {
|
|
|
84
85
|
}
|
|
85
86
|
return [];
|
|
86
87
|
}
|
|
87
|
-
|
|
88
|
-
function getLayoutTemplate(): string {
|
|
89
|
-
return `// Auto-generated by @designtools/next-plugin — do not edit
|
|
90
|
-
export default function PreviewLayout({ children }: { children: React.ReactNode }) {
|
|
91
|
-
return (
|
|
92
|
-
<div style={{ padding: 32, background: "var(--background, #fff)", minHeight: "100vh" }}>
|
|
93
|
-
{children}
|
|
94
|
-
</div>
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
`;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function getPageTemplate(componentPaths: string[]): string {
|
|
101
|
-
// Generate static import map entries — webpack can analyze these
|
|
102
|
-
const registryEntries = componentPaths
|
|
103
|
-
.map((p) => ` "${p}": () => import("@/${p}"),`)
|
|
104
|
-
.join("\n");
|
|
105
|
-
|
|
106
|
-
return `// Auto-generated by @designtools/next-plugin — do not edit
|
|
107
|
-
"use client";
|
|
108
|
-
|
|
109
|
-
import { useState, useEffect, useCallback, createElement } from "react";
|
|
110
|
-
|
|
111
|
-
/* Static import registry — webpack can analyze these imports */
|
|
112
|
-
const COMPONENT_REGISTRY: Record<string, () => Promise<any>> = {
|
|
113
|
-
${registryEntries}
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
interface Combination {
|
|
117
|
-
label: string;
|
|
118
|
-
props: Record<string, string>;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
interface RenderMsg {
|
|
122
|
-
type: "tool:renderPreview";
|
|
123
|
-
componentPath: string;
|
|
124
|
-
exportName: string;
|
|
125
|
-
combinations: Combination[];
|
|
126
|
-
defaultChildren: string;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export default function PreviewPage() {
|
|
130
|
-
const [Component, setComponent] = useState<React.ComponentType<any> | null>(null);
|
|
131
|
-
const [combinations, setCombinations] = useState<Combination[]>([]);
|
|
132
|
-
const [defaultChildren, setDefaultChildren] = useState("");
|
|
133
|
-
const [error, setError] = useState<string | null>(null);
|
|
134
|
-
|
|
135
|
-
const handleMessage = useCallback(async (e: MessageEvent) => {
|
|
136
|
-
const msg = e.data;
|
|
137
|
-
if (msg?.type !== "tool:renderPreview") return;
|
|
138
|
-
|
|
139
|
-
const { componentPath, exportName, combinations: combos, defaultChildren: children } = msg as RenderMsg;
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
setError(null);
|
|
143
|
-
setCombinations(combos);
|
|
144
|
-
setDefaultChildren(children || exportName);
|
|
145
|
-
|
|
146
|
-
const loader = COMPONENT_REGISTRY[componentPath];
|
|
147
|
-
if (!loader) {
|
|
148
|
-
setError(\`Component "\${componentPath}" not found in registry. Available: \${Object.keys(COMPONENT_REGISTRY).join(", ")}\`);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const mod = await loader();
|
|
153
|
-
const Comp = mod[exportName] || mod.default;
|
|
154
|
-
if (!Comp) {
|
|
155
|
-
setError(\`Export "\${exportName}" not found in \${componentPath}\`);
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
setComponent(() => Comp);
|
|
160
|
-
|
|
161
|
-
// Notify editor that preview is ready
|
|
162
|
-
window.parent.postMessage(
|
|
163
|
-
{ type: "tool:previewReady", cellCount: combos.length },
|
|
164
|
-
"*"
|
|
165
|
-
);
|
|
166
|
-
} catch (err: any) {
|
|
167
|
-
setError(\`Failed to load component: \${err.message}\`);
|
|
168
|
-
}
|
|
169
|
-
}, []);
|
|
170
|
-
|
|
171
|
-
useEffect(() => {
|
|
172
|
-
window.addEventListener("message", handleMessage);
|
|
173
|
-
// Signal readiness to the editor
|
|
174
|
-
window.parent.postMessage({ type: "tool:injectedReady" }, "*");
|
|
175
|
-
return () => window.removeEventListener("message", handleMessage);
|
|
176
|
-
}, [handleMessage]);
|
|
177
|
-
|
|
178
|
-
if (error) {
|
|
179
|
-
return (
|
|
180
|
-
<div style={{ padding: 32, color: "#ef4444", fontFamily: "monospace", fontSize: 14 }}>
|
|
181
|
-
{error}
|
|
182
|
-
</div>
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (!Component) {
|
|
187
|
-
return (
|
|
188
|
-
<div style={{ padding: 32, color: "#888", fontFamily: "system-ui", fontSize: 14 }}>
|
|
189
|
-
Waiting for component…
|
|
190
|
-
</div>
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return (
|
|
195
|
-
<div style={{ fontFamily: "system-ui" }}>
|
|
196
|
-
<div style={{
|
|
197
|
-
display: "grid",
|
|
198
|
-
gridTemplateColumns: "repeat(auto-fill, minmax(240px, 1fr))",
|
|
199
|
-
gap: 24,
|
|
200
|
-
}}>
|
|
201
|
-
{combinations.map((combo, i) => (
|
|
202
|
-
<div key={i} style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
203
|
-
<div style={{
|
|
204
|
-
fontSize: 11,
|
|
205
|
-
fontWeight: 600,
|
|
206
|
-
color: "#888",
|
|
207
|
-
textTransform: "uppercase",
|
|
208
|
-
letterSpacing: "0.05em",
|
|
209
|
-
}}>
|
|
210
|
-
{combo.label}
|
|
211
|
-
</div>
|
|
212
|
-
<div style={{
|
|
213
|
-
padding: 16,
|
|
214
|
-
border: "1px solid #e5e7eb",
|
|
215
|
-
borderRadius: 8,
|
|
216
|
-
display: "flex",
|
|
217
|
-
alignItems: "center",
|
|
218
|
-
justifyContent: "center",
|
|
219
|
-
minHeight: 64,
|
|
220
|
-
background: "#fff",
|
|
221
|
-
}}>
|
|
222
|
-
{createElement(Component, combo.props, defaultChildren)}
|
|
223
|
-
</div>
|
|
224
|
-
</div>
|
|
225
|
-
))}
|
|
226
|
-
</div>
|
|
227
|
-
</div>
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
`;
|
|
231
|
-
}
|