@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.
@@ -28,17 +28,21 @@ export default function codesurfaceMountLoader(this: LoaderContext, source: stri
28
28
  return;
29
29
  }
30
30
 
31
- // Add import at the top (after "use client" or first import)
32
- // CodeSurface is a "use client" component importing it from an RSC is fine in Next.js
33
- const importStatement = `import { CodeSurface } from "@designtools/next-plugin/codesurface";\n`;
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 import
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) + importStatement + source.slice(firstImportIndex);
43
+ modified = source.slice(0, firstImportIndex) + importStatements + source.slice(firstImportIndex);
40
44
  } else {
41
- modified = importStatement + source;
45
+ modified = importStatements + source;
42
46
  }
43
47
 
44
48
  // Add <CodeSurface /> just before {children}
@@ -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 && el.getAttribute("data-slot")) {
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
- const slot = el.getAttribute("data-slot") || "";
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
- s.selectionMode = true;
776
- document.body.style.cursor = "crosshair";
875
+ enterSelectionMode();
777
876
  break;
778
877
  case "tool:exitSelectionMode":
779
- s.selectionMode = false;
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); // "--shadow-sm" → "shadow-sm"
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
- // This component renders nothing overlays are created imperatively
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 { generatePreviewRoute } from "./preview-route.js";
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 preview route
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
- generatePreviewRoute(appDir);
49
+ generateComponentRegistry(appDir);
52
50
  } catch {
53
51
  // Non-fatal — isolation feature just won't work
54
52
  }
@@ -1,51 +1,52 @@
1
1
  /**
2
- * Preview route generator for component isolation.
2
+ * Component registry generator for component isolation.
3
3
  *
4
- * Writes a catch-all preview page into the target app's app directory
5
- * that can render any component with arbitrary props via postMessage.
6
- * Uses Next.js file-system routing — no custom server needed.
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 PREVIEW_DIR = "designtools-preview";
12
+ const REGISTRY_FILE = "designtools-registry.ts";
13
13
 
14
- /** Generate the preview route files in the target app's app directory. */
15
- export function generatePreviewRoute(appDir: string): void {
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
- // Create directory
23
- fs.mkdirSync(previewDir, { recursive: true });
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
- // Write layout minimal shell, no app chrome
26
- fs.writeFileSync(
27
- path.join(previewDir, "layout.tsx"),
28
- getLayoutTemplate(),
29
- "utf-8"
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
- // Write page — client component that renders previews via postMessage
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 designtools-preview is gitignored
41
+ // Ensure registry file is gitignored
40
42
  ensureGitignore(projectRoot);
41
43
  }
42
44
 
43
- /** Clean up generated preview files. */
44
- export function cleanupPreviewRoute(appDir: string): void {
45
- const previewDir = path.join(appDir, PREVIEW_DIR);
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.rmSync(previewDir, { recursive: true, force: true });
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-preview";
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
- }