@sampleapp.ai/sdk 1.0.42 → 1.0.44
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/components/sandbox/Sandbox.js +96 -29
- package/dist/components/sandbox/api.js +73 -1
- package/dist/components/sandbox/guardian/code-focus-section.js +4 -2
- package/dist/components/sandbox/guardian/context/theme-context.js +47 -0
- package/dist/components/sandbox/guardian/demo/guardian-demo.js +4 -4
- package/dist/components/sandbox/guardian/demo/left-view.js +13 -7
- package/dist/components/sandbox/guardian/guardian-component.js +39 -8
- package/dist/components/sandbox/guardian/guardian-playground.js +14 -15
- package/dist/components/sandbox/guardian/guardian-style-wrapper.js +8 -6
- package/dist/components/sandbox/guardian/header.js +37 -41
- package/dist/components/sandbox/guardian/hooks/use-frame-messages.js +24 -8
- package/dist/components/sandbox/guardian/hooks/use-frame-params.js +94 -36
- package/dist/components/sandbox/guardian/hooks/use-sandbox-url-loader.js +16 -4
- package/dist/components/sandbox/guardian/index.js +2 -0
- package/dist/components/sandbox/guardian/right-view/pill-file-selector.js +70 -20
- package/dist/components/sandbox/guardian/right-view/preview-control-bar.js +5 -2
- package/dist/components/sandbox/guardian/right-view/right-top-down-view/network-requests-view.js +117 -0
- package/dist/components/sandbox/guardian/right-view/right-top-down-view.js +38 -140
- package/dist/components/sandbox/guardian/right-view/right-view.js +3 -3
- package/dist/components/sandbox/guardian/right-view/simplified-editor.js +9 -2
- package/dist/components/sandbox/guardian/ui/markdown/code-group/code-block.js +7 -33
- package/dist/components/sandbox/guardian/ui/markdown.js +23 -14
- package/dist/components/sandbox/guardian/utils.js +4 -16
- package/dist/components/sandbox/sandbox-home/SandboxCard.js +7 -5
- package/dist/components/sandbox/sandbox-home/SandboxHome.js +32 -13
- package/dist/components/sandbox/sandbox-home/SearchBar.js +5 -3
- package/dist/hooks/use-tree-selector.js +98 -0
- package/dist/index.d.ts +631 -21
- package/dist/index.es.js +22806 -22028
- package/dist/index.js +13 -4
- package/dist/index.standalone.umd.js +8 -8
- package/dist/lib/api-client.js +68 -0
- package/dist/lib/content-matcher.js +99 -0
- package/dist/lib/generated-css.js +1 -1
- package/dist/lib/shadow-dom-wrapper.js +31 -10
- package/dist/lib/tree-utils.js +193 -0
- package/dist/lib/types/tree-config.js +36 -0
- package/dist/sdk.css +1 -1
- package/dist/tailwind.css +2 -2
- package/package.json +2 -2
|
@@ -1,53 +1,49 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import React from "react";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// options: demoOptions.map((opt) => ({
|
|
13
|
-
// ...opt,
|
|
14
|
-
// href: `/${opt.value}?framework=${firstFrameworkByUseCase?.[opt.value] || frameworkOptions[0]?.value
|
|
15
|
-
// }`,
|
|
16
|
-
// })),
|
|
17
|
-
// },
|
|
18
|
-
{
|
|
19
|
-
id: "framework",
|
|
20
|
-
label: "Framework:",
|
|
21
|
-
options: frameworkOptions.map((opt) => (Object.assign(Object.assign({}, opt), { href: `/${selectedUseCase}?framework=${opt.value}` }))),
|
|
22
|
-
},
|
|
23
|
-
];
|
|
3
|
+
import { useSdkTheme } from "./context/theme-context";
|
|
4
|
+
/**
|
|
5
|
+
* Header component for dynamic tech stack selection
|
|
6
|
+
*
|
|
7
|
+
* Uses sections from useTreeSelector to render selector buttons
|
|
8
|
+
* based on the tech_stack_config tree structure.
|
|
9
|
+
*/
|
|
10
|
+
export default function Header({ sections, onSelect, playgroundLogo, themecolor }) {
|
|
11
|
+
const { isDark } = useSdkTheme();
|
|
24
12
|
return (React.createElement("div", { className: "w-full sticky top-0 z-50 px-4 lg:px-8 backdrop-blur-md" },
|
|
25
|
-
React.createElement("div", { className: "flex flex-wrap items-center justify-between" },
|
|
13
|
+
React.createElement("div", { className: "flex flex-wrap items-center justify-between py-2" },
|
|
26
14
|
React.createElement("div", { className: "flex flex-wrap items-center gap-6" }, sections.map((section) => {
|
|
27
|
-
|
|
28
|
-
return (React.createElement("div", { key: section.
|
|
29
|
-
React.createElement("span", { className:
|
|
15
|
+
var _a;
|
|
16
|
+
return (React.createElement("div", { key: section.nodeType, className: "flex items-center flex-wrap gap-3" },
|
|
17
|
+
React.createElement("span", { className: `text-sm font-semibold whitespace-nowrap ${isDark ? "text-gray-400" : "text-gray-600"}` }, section.label),
|
|
30
18
|
React.createElement("div", { className: "hidden md:block" },
|
|
31
19
|
React.createElement("div", { className: "flex items-center gap-2" }, section.options.map((option) => {
|
|
32
|
-
const isSelected =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
20
|
+
const isSelected = option.isSelected;
|
|
21
|
+
// Only disable non-leaf nodes that have no content
|
|
22
|
+
// Leaf nodes should always be clickable (they represent final selections)
|
|
23
|
+
const isDisabled = option.hasContent === false && !option.isLeaf;
|
|
24
|
+
return (React.createElement("button", { key: option.key, onClick: () => {
|
|
25
|
+
if (!isDisabled) {
|
|
26
|
+
onSelect(section.nodeType, option.key);
|
|
36
27
|
}
|
|
37
|
-
}, className: `inline-flex items-center px-3 py-1.5 text-xs font-medium rounded-md border transition-all cursor-pointer active:scale-[0.98] ${isSelected
|
|
38
|
-
?
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
28
|
+
}, disabled: isDisabled, className: `inline-flex items-center px-3 py-1.5 text-xs font-medium rounded-md border transition-all cursor-pointer active:scale-[0.98] ${isSelected
|
|
29
|
+
? isDark
|
|
30
|
+
? "bg-zinc-900 hover:bg-zinc-800"
|
|
31
|
+
: "bg-white hover:bg-zinc-100"
|
|
32
|
+
: isDark
|
|
33
|
+
? "bg-zinc-900 border-zinc-700 text-zinc-100 hover:bg-zinc-800"
|
|
34
|
+
: "bg-white border-zinc-300 text-zinc-900 hover:bg-zinc-100"} ${isDisabled ? "opacity-50 cursor-not-allowed" : ""}`, style: isSelected && themecolor
|
|
35
|
+
? {
|
|
36
|
+
borderColor: themecolor,
|
|
37
|
+
color: themecolor
|
|
38
|
+
}
|
|
39
|
+
: undefined }, option.label));
|
|
43
40
|
}))),
|
|
44
41
|
React.createElement("div", { className: "block md:hidden relative" },
|
|
45
|
-
React.createElement("select", { value:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
}, className: "appearance-none bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md px-3 py-1.5 pr-8 text-xs font-medium text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-primary dark:focus:ring-primary-light focus:border-transparent transition-all hover:bg-zinc-100 dark:hover:bg-zinc-800" }, section.options.map((option) => (React.createElement("option", { key: option.value, value: option.value }, option.label)))),
|
|
42
|
+
React.createElement("select", { value: ((_a = section.options.find((o) => o.isSelected)) === null || _a === void 0 ? void 0 : _a.key) || "", onChange: (e) => {
|
|
43
|
+
onSelect(section.nodeType, e.target.value);
|
|
44
|
+
}, className: `appearance-none border rounded-md px-3 py-1.5 pr-8 text-xs font-medium focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all ${isDark
|
|
45
|
+
? "bg-zinc-900 border-zinc-700 text-zinc-100 hover:bg-zinc-800"
|
|
46
|
+
: "bg-white border-zinc-300 text-zinc-900 hover:bg-zinc-100"}` }, section.options.map((option) => (React.createElement("option", { key: option.key, value: option.key, disabled: option.hasContent === false && !option.isLeaf }, option.label)))),
|
|
51
47
|
React.createElement("svg", { "aria-hidden": "true", width: "16", height: "16", viewBox: "0 0 16 16", xmlns: "http://www.w3.org/2000/svg", className: "absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none text-zinc-400" },
|
|
52
48
|
React.createElement("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M2.35 9.915a.875.875 0 0 1 1.235-.065L8 13.823l4.415-3.973a.875.875 0 0 1 1.17 1.3l-5 4.5a.873.873 0 0 1-1.17 0l-5-4.5a.875.875 0 0 1-.065-1.235ZM7.415.35a.873.873 0 0 1 1.17 0l5 4.5a.875.875 0 1 1-1.17 1.3L8 2.177 3.585 6.15a.875.875 0 0 1-1.17-1.3l5-4.5Z", fill: "currentColor" })))));
|
|
53
49
|
})))));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
|
+
import { SELECTABLE_NODE_TYPES } from "../../../../lib/types/tree-config";
|
|
3
4
|
/**
|
|
4
5
|
* Hook to handle postMessage communication with parent window when in iframe mode.
|
|
5
6
|
* - Sends IFRAME_READY when mounted
|
|
@@ -7,10 +8,11 @@ import { useEffect, useRef } from "react";
|
|
|
7
8
|
* - Works with useFrameParams() which reads the updated URL params
|
|
8
9
|
*
|
|
9
10
|
* Supported UPDATE_VIEW message fields:
|
|
10
|
-
* -
|
|
11
|
+
* - Node type selections: product, version, architecture, frontend, backend, framework, integration, platform
|
|
11
12
|
* - activeFilePath: Change the active file in the editor
|
|
12
13
|
* - linesStart/linesEnd: Highlight specific line range in the editor
|
|
13
14
|
* - theme: Override the theme color (hex color)
|
|
15
|
+
* - iframeUrl: Override the preview iframe URL
|
|
14
16
|
*/
|
|
15
17
|
export function useFrameMessages() {
|
|
16
18
|
const hasSignaledReady = useRef(false);
|
|
@@ -18,11 +20,16 @@ export function useFrameMessages() {
|
|
|
18
20
|
if (typeof window === "undefined")
|
|
19
21
|
return;
|
|
20
22
|
// Check if we're in an iframe
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
+
const isInIframe = window.self !== window.top;
|
|
24
|
+
// Also check URL param for isFrame (for testing without actual iframe)
|
|
25
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
26
|
+
const isFrameParam = searchParams.get("isFrame") === "true";
|
|
27
|
+
// Listen for messages if we're in an iframe OR if isFrame=true in URL
|
|
28
|
+
if (!isInIframe && !isFrameParam) {
|
|
23
29
|
return;
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
}
|
|
31
|
+
// Signal to parent that we're ready to receive messages (only if actually in iframe)
|
|
32
|
+
if (isInIframe && !hasSignaledReady.current && window.parent) {
|
|
26
33
|
window.parent.postMessage({ type: "IFRAME_READY" }, "*");
|
|
27
34
|
hasSignaledReady.current = true;
|
|
28
35
|
}
|
|
@@ -32,10 +39,18 @@ export function useFrameMessages() {
|
|
|
32
39
|
const data = event.data;
|
|
33
40
|
if (data.type === "UPDATE_VIEW") {
|
|
34
41
|
const url = new URL(window.location.href);
|
|
35
|
-
// Update
|
|
42
|
+
// Update all node type params
|
|
43
|
+
for (const nodeType of SELECTABLE_NODE_TYPES) {
|
|
44
|
+
const value = data[nodeType];
|
|
45
|
+
if (value !== undefined && typeof value === "string") {
|
|
46
|
+
url.searchParams.set(nodeType, value);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Legacy framework param (also handled above via SELECTABLE_NODE_TYPES)
|
|
36
50
|
if (data.framework !== undefined) {
|
|
37
51
|
url.searchParams.set("framework", data.framework);
|
|
38
52
|
}
|
|
53
|
+
// Editor-related params
|
|
39
54
|
if (data.activeFilePath !== undefined) {
|
|
40
55
|
url.searchParams.set("activeFilePath", data.activeFilePath);
|
|
41
56
|
}
|
|
@@ -54,12 +69,13 @@ export function useFrameMessages() {
|
|
|
54
69
|
// Update URL without page reload
|
|
55
70
|
window.history.replaceState({}, "", url.toString());
|
|
56
71
|
// Dispatch a custom event so React components can react immediately
|
|
57
|
-
// (useFrameParams may have a slight delay)
|
|
58
72
|
window.dispatchEvent(new CustomEvent("frameParamsUpdated", { detail: data }));
|
|
59
73
|
}
|
|
60
74
|
};
|
|
61
75
|
window.addEventListener("message", handleMessage);
|
|
62
|
-
return () =>
|
|
76
|
+
return () => {
|
|
77
|
+
window.removeEventListener("message", handleMessage);
|
|
78
|
+
};
|
|
63
79
|
}, []);
|
|
64
80
|
return {};
|
|
65
81
|
}
|
|
@@ -1,44 +1,102 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { SELECTABLE_NODE_TYPES } from "../../../../lib/types/tree-config";
|
|
3
|
+
/**
|
|
4
|
+
* Helper to read all frame params from URL
|
|
5
|
+
*/
|
|
6
|
+
function readFrameParamsFromUrl() {
|
|
7
|
+
if (typeof window === "undefined") {
|
|
8
|
+
return {
|
|
9
|
+
isFrame: false,
|
|
10
|
+
framework: undefined,
|
|
11
|
+
theme: undefined,
|
|
12
|
+
activeFilePath: undefined,
|
|
13
|
+
linesStart: undefined,
|
|
14
|
+
linesEnd: undefined,
|
|
15
|
+
iframeUrl: undefined,
|
|
16
|
+
nodeTypes: {}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
20
|
+
const isFrame = searchParams.get("isFrame") === "true";
|
|
21
|
+
const theme = searchParams.get("theme") || undefined;
|
|
22
|
+
const activeFilePath = searchParams.get("activeFilePath") || undefined;
|
|
23
|
+
const linesStart = searchParams.get("linesStart")
|
|
24
|
+
? parseInt(searchParams.get("linesStart"), 10)
|
|
25
|
+
: undefined;
|
|
26
|
+
const linesEnd = searchParams.get("linesEnd")
|
|
27
|
+
? parseInt(searchParams.get("linesEnd"), 10)
|
|
28
|
+
: undefined;
|
|
29
|
+
const iframeUrl = searchParams.get("iframeUrl") || undefined;
|
|
30
|
+
// Read all node type params
|
|
31
|
+
const nodeTypes = {};
|
|
32
|
+
for (const nodeType of SELECTABLE_NODE_TYPES) {
|
|
33
|
+
const value = searchParams.get(nodeType);
|
|
34
|
+
if (value) {
|
|
35
|
+
nodeTypes[nodeType] = value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Legacy framework param - also populate nodeTypes.framework if present
|
|
39
|
+
const framework = searchParams.get("framework") || undefined;
|
|
40
|
+
if (framework && !nodeTypes.framework) {
|
|
41
|
+
nodeTypes.framework = framework;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
isFrame,
|
|
45
|
+
framework,
|
|
46
|
+
theme,
|
|
47
|
+
activeFilePath,
|
|
48
|
+
linesStart,
|
|
49
|
+
linesEnd,
|
|
50
|
+
iframeUrl,
|
|
51
|
+
nodeTypes
|
|
52
|
+
};
|
|
53
|
+
}
|
|
2
54
|
/**
|
|
3
55
|
* Hook to read frame-mode search parameters from URL.
|
|
4
56
|
* These params override config/context when isFrame=true.
|
|
5
57
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
58
|
+
* Supports all node type parameters:
|
|
59
|
+
* - product, version, architecture, frontend, backend, framework, integration, platform
|
|
60
|
+
*
|
|
61
|
+
* Note: This hook now uses state and listens for the "frameParamsUpdated" custom event
|
|
62
|
+
* to react immediately when URL params are updated via postMessage.
|
|
8
63
|
*/
|
|
9
64
|
export function useFrameParams() {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
65
|
+
// Initialize with empty values - will be populated on mount
|
|
66
|
+
const [params, setParams] = useState(() => ({
|
|
67
|
+
isFrame: false,
|
|
68
|
+
framework: undefined,
|
|
69
|
+
theme: undefined,
|
|
70
|
+
activeFilePath: undefined,
|
|
71
|
+
linesStart: undefined,
|
|
72
|
+
linesEnd: undefined,
|
|
73
|
+
iframeUrl: undefined,
|
|
74
|
+
nodeTypes: {}
|
|
75
|
+
}));
|
|
76
|
+
// Read initial values on mount (client-side only)
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (typeof window === "undefined")
|
|
79
|
+
return;
|
|
80
|
+
// Read initial params from URL
|
|
81
|
+
const initialParams = readFrameParamsFromUrl();
|
|
82
|
+
setParams(initialParams);
|
|
83
|
+
}, []);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (typeof window === "undefined")
|
|
86
|
+
return;
|
|
87
|
+
// Handler for custom event dispatched by useFrameMessages
|
|
88
|
+
const handleFrameParamsUpdated = (event) => {
|
|
89
|
+
const newParams = readFrameParamsFromUrl();
|
|
90
|
+
setParams(newParams);
|
|
91
|
+
};
|
|
92
|
+
// Listen for the custom event
|
|
93
|
+
window.addEventListener("frameParamsUpdated", handleFrameParamsUpdated);
|
|
94
|
+
// Also listen for popstate (browser back/forward)
|
|
95
|
+
window.addEventListener("popstate", handleFrameParamsUpdated);
|
|
96
|
+
return () => {
|
|
97
|
+
window.removeEventListener("frameParamsUpdated", handleFrameParamsUpdated);
|
|
98
|
+
window.removeEventListener("popstate", handleFrameParamsUpdated);
|
|
42
99
|
};
|
|
43
|
-
}, [
|
|
100
|
+
}, []);
|
|
101
|
+
return params;
|
|
44
102
|
}
|
|
@@ -50,6 +50,12 @@ export function useSandboxUrlLoader(startSandboxConfig) {
|
|
|
50
50
|
// Edge case: If browserUrl is empty but we have chatUid, call startSandbox first
|
|
51
51
|
let targetUrl = browserUrl;
|
|
52
52
|
if (!targetUrl || targetUrl.trim() === "") {
|
|
53
|
+
// If hasPreview is false, we don't need to start a container
|
|
54
|
+
// Just return empty string - no preview will be shown
|
|
55
|
+
if ((startSandboxConfig === null || startSandboxConfig === void 0 ? void 0 : startSandboxConfig.hasPreview) === false) {
|
|
56
|
+
setVmUrl(cacheKey, "");
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
53
59
|
if (startSandboxConfig === null || startSandboxConfig === void 0 ? void 0 : startSandboxConfig.chatUid) {
|
|
54
60
|
// When browserUrl (published_url) is empty, env is required to start sandbox
|
|
55
61
|
if (!startSandboxConfig.env) {
|
|
@@ -58,7 +64,7 @@ export function useSandboxUrlLoader(startSandboxConfig) {
|
|
|
58
64
|
// First, start the sandbox to get the container_url
|
|
59
65
|
const sandboxResponse = await client.sdk.startSandbox({
|
|
60
66
|
env: startSandboxConfig.env,
|
|
61
|
-
chatUid: startSandboxConfig.chatUid
|
|
67
|
+
chatUid: startSandboxConfig.chatUid
|
|
62
68
|
});
|
|
63
69
|
targetUrl = sandboxResponse.container_url;
|
|
64
70
|
}
|
|
@@ -69,7 +75,7 @@ export function useSandboxUrlLoader(startSandboxConfig) {
|
|
|
69
75
|
const data = await client.sdk.startVm({
|
|
70
76
|
url: targetUrl,
|
|
71
77
|
mode: "json",
|
|
72
|
-
resolution: config.resolution
|
|
78
|
+
resolution: config.resolution
|
|
73
79
|
});
|
|
74
80
|
if (!data.vncUrl) {
|
|
75
81
|
console.error(`No vncUrl in response for ${sandboxUid}:`, data);
|
|
@@ -98,6 +104,12 @@ export function useSandboxUrlLoader(startSandboxConfig) {
|
|
|
98
104
|
return browserUrl;
|
|
99
105
|
}
|
|
100
106
|
else if (startSandboxConfig) {
|
|
107
|
+
// If hasPreview is false, we don't need to start a container
|
|
108
|
+
// Just return empty string - no preview will be shown
|
|
109
|
+
if (startSandboxConfig.hasPreview === false) {
|
|
110
|
+
setVmUrl(cacheKey, "");
|
|
111
|
+
return "";
|
|
112
|
+
}
|
|
101
113
|
// No published_url available - fall back to calling startSandbox API with chat_uid
|
|
102
114
|
try {
|
|
103
115
|
if (!startSandboxConfig.chatUid) {
|
|
@@ -116,7 +128,7 @@ export function useSandboxUrlLoader(startSandboxConfig) {
|
|
|
116
128
|
const client = createApiClient({ apiKey });
|
|
117
129
|
const response = await client.sdk.startSandbox({
|
|
118
130
|
env: startSandboxConfig.env,
|
|
119
|
-
chatUid: startSandboxConfig.chatUid
|
|
131
|
+
chatUid: startSandboxConfig.chatUid
|
|
120
132
|
});
|
|
121
133
|
// Use the container_url from the response as the browserUrl
|
|
122
134
|
const containerUrl = response.container_url;
|
|
@@ -161,6 +173,6 @@ export function useSandboxUrlLoader(startSandboxConfig) {
|
|
|
161
173
|
return {
|
|
162
174
|
loadSandboxUrl,
|
|
163
175
|
getSandboxUrl,
|
|
164
|
-
preloadSandboxUrls
|
|
176
|
+
preloadSandboxUrls
|
|
165
177
|
};
|
|
166
178
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { default as GuardianPlayground } from "./guardian-playground";
|
|
2
2
|
export { default as GuardianComponent } from "./guardian-component";
|
|
3
|
+
export { default as Header } from "./header";
|
|
3
4
|
export { VmProvider, useVmContext } from "./context/vm-context";
|
|
4
5
|
export { GuardianProvider, useGuardianContext } from "./context/guardian-context";
|
|
6
|
+
export { SdkThemeProvider, useSdkTheme, useThemeClass } from "./context/theme-context";
|
|
5
7
|
export * from "./types";
|
|
6
8
|
export { default as CodeFocusSection } from "./code-focus-section";
|
|
7
9
|
export { buildGuardianConfig } from "./utils";
|
|
@@ -3,8 +3,8 @@ import React from "react";
|
|
|
3
3
|
import { useGuardianContext } from "../context/guardian-context";
|
|
4
4
|
import { cn } from "../../../../lib/utils";
|
|
5
5
|
import { useFrameParams } from "../hooks/use-frame-params";
|
|
6
|
-
import { FileCode, FileJson, FileText, Settings, Layout, Database, Image, Terminal
|
|
7
|
-
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger
|
|
6
|
+
import { FileCode, FileJson, FileText, Settings, Layout, Database, Image, Terminal } from "lucide-react";
|
|
7
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
|
|
8
8
|
const FILE_ICONS = {
|
|
9
9
|
// JavaScript/React
|
|
10
10
|
js: { icon: FileCode, color: "text-yellow-600" },
|
|
@@ -42,13 +42,13 @@ const FILE_ICONS = {
|
|
|
42
42
|
svg: { icon: Image, color: "text-green-500" },
|
|
43
43
|
ico: { icon: Image, color: "text-green-500" },
|
|
44
44
|
// Default
|
|
45
|
-
default: { icon: FileText, color: "text-zinc-500" }
|
|
45
|
+
default: { icon: FileText, color: "text-zinc-500" }
|
|
46
46
|
};
|
|
47
47
|
export default function PillFileSelector({ themeColor }) {
|
|
48
48
|
const containerRef = React.useRef(null);
|
|
49
49
|
const fileRefs = React.useRef({});
|
|
50
50
|
const lastInitializedFilePath = React.useRef(null);
|
|
51
|
-
const { generatedCode, updateCode, setLanguage, activeFilePath: contextActiveFilePath, setActiveFileName, setActiveFilePath, setActiveDependenciesToInstall, setActiveLineRange, filesEdited
|
|
51
|
+
const { generatedCode, updateCode, setLanguage, activeFilePath: contextActiveFilePath, setActiveFileName, setActiveFilePath, setActiveDependenciesToInstall, setActiveLineRange, filesEdited } = useGuardianContext();
|
|
52
52
|
// Read frame params - these override context when isFrame=true
|
|
53
53
|
const frameParams = useFrameParams();
|
|
54
54
|
// Use frame param overrides when in frame mode
|
|
@@ -94,7 +94,7 @@ export default function PillFileSelector({ themeColor }) {
|
|
|
94
94
|
"vite-plugin-react-instrumentation.ts",
|
|
95
95
|
"instrumentation-client.ts",
|
|
96
96
|
// Global styles (often not needed in code editor)
|
|
97
|
-
"globals.css"
|
|
97
|
+
"globals.css"
|
|
98
98
|
];
|
|
99
99
|
const filteredFiles = fileEntries.filter(([filePath]) => {
|
|
100
100
|
const fileName = filePath.split("/").pop() || filePath;
|
|
@@ -128,7 +128,7 @@ export default function PillFileSelector({ themeColor }) {
|
|
|
128
128
|
activeButton.scrollIntoView({
|
|
129
129
|
behavior: "instant",
|
|
130
130
|
block: "nearest",
|
|
131
|
-
inline: "center"
|
|
131
|
+
inline: "center"
|
|
132
132
|
});
|
|
133
133
|
}
|
|
134
134
|
}, 50);
|
|
@@ -156,38 +156,88 @@ export default function PillFileSelector({ themeColor }) {
|
|
|
156
156
|
window.addEventListener("frameParamsUpdated", handleFrameParamsUpdate);
|
|
157
157
|
return () => window.removeEventListener("frameParamsUpdated", handleFrameParamsUpdate);
|
|
158
158
|
}, [frameParams.isFrame, scrollActivePillIntoView]);
|
|
159
|
-
// Load file
|
|
159
|
+
// Load first file on mount when NOT in frame mode
|
|
160
160
|
React.useEffect(() => {
|
|
161
|
-
// Skip if
|
|
162
|
-
if (
|
|
161
|
+
// Skip if in frame mode (handled by separate effect)
|
|
162
|
+
if (frameParams.isFrame) {
|
|
163
163
|
return;
|
|
164
164
|
}
|
|
165
|
-
// Skip if
|
|
166
|
-
if (
|
|
165
|
+
// Skip if generatedCode isn't loaded yet
|
|
166
|
+
if (!generatedCode || Object.keys(generatedCode).length === 0) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// Skip if already initialized
|
|
170
|
+
if (lastInitializedFilePath.current) {
|
|
167
171
|
return;
|
|
168
172
|
}
|
|
173
|
+
// Get filtered files (excluding certain files)
|
|
174
|
+
const fileEntries = Object.entries(generatedCode)
|
|
175
|
+
.filter(([filePath]) => {
|
|
176
|
+
const fileName = filePath.split("/").pop() || filePath;
|
|
177
|
+
return !excludedFiles.includes(fileName);
|
|
178
|
+
})
|
|
179
|
+
.sort(([a], [b]) => {
|
|
180
|
+
const aName = a.split("/").pop() || a;
|
|
181
|
+
const bName = b.split("/").pop() || b;
|
|
182
|
+
return aName.localeCompare(bName);
|
|
183
|
+
});
|
|
184
|
+
if (fileEntries.length === 0) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
// Load the first file
|
|
188
|
+
const [firstFilePath, firstFileData] = fileEntries[0];
|
|
189
|
+
handleFileClick(firstFilePath, firstFileData);
|
|
190
|
+
lastInitializedFilePath.current = firstFilePath;
|
|
191
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
192
|
+
}, [frameParams.isFrame, generatedCode]);
|
|
193
|
+
// Load file from frame params when they change, or load first file if in frame mode
|
|
194
|
+
React.useEffect(() => {
|
|
169
195
|
// Skip if generatedCode isn't loaded yet
|
|
170
196
|
if (!generatedCode || Object.keys(generatedCode).length === 0) {
|
|
171
197
|
return;
|
|
172
198
|
}
|
|
199
|
+
// Get filtered files (excluding certain files)
|
|
200
|
+
const fileEntries = Object.entries(generatedCode).filter(([filePath]) => {
|
|
201
|
+
const fileName = filePath.split("/").pop() || filePath;
|
|
202
|
+
return !excludedFiles.includes(fileName);
|
|
203
|
+
});
|
|
204
|
+
// If not in frame mode, skip this effect
|
|
205
|
+
if (!frameParams.isFrame) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
// Determine which file to load
|
|
209
|
+
let targetFilePath;
|
|
210
|
+
if (frameParams.activeFilePath) {
|
|
211
|
+
// Frame params specify a file - use it
|
|
212
|
+
targetFilePath = frameParams.activeFilePath;
|
|
213
|
+
}
|
|
214
|
+
else if (fileEntries.length > 0) {
|
|
215
|
+
// No file specified - load the first available file
|
|
216
|
+
const [firstFilePath] = fileEntries[0];
|
|
217
|
+
targetFilePath = firstFilePath;
|
|
218
|
+
}
|
|
219
|
+
// Skip if we've already initialized this exact file path
|
|
220
|
+
if (!targetFilePath || lastInitializedFilePath.current === targetFilePath) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
173
223
|
// Search by file_path property first (source of truth), then fallback to key lookup
|
|
174
224
|
let fileData;
|
|
175
225
|
let resolvedFilePath;
|
|
176
226
|
// First, search through all code outputs to find one matching file_path
|
|
177
|
-
const matchingCodeOutput = Object.values(generatedCode).find((output) => output.file_path ===
|
|
227
|
+
const matchingCodeOutput = Object.values(generatedCode).find((output) => output.file_path === targetFilePath);
|
|
178
228
|
if (matchingCodeOutput) {
|
|
179
229
|
fileData = matchingCodeOutput;
|
|
180
230
|
resolvedFilePath = matchingCodeOutput.file_path;
|
|
181
231
|
}
|
|
182
232
|
else {
|
|
183
233
|
// Fallback: try to get from generatedCode by key
|
|
184
|
-
if (generatedCode[
|
|
185
|
-
fileData = generatedCode[
|
|
186
|
-
resolvedFilePath = fileData.file_path ||
|
|
234
|
+
if (generatedCode[targetFilePath]) {
|
|
235
|
+
fileData = generatedCode[targetFilePath];
|
|
236
|
+
resolvedFilePath = fileData.file_path || targetFilePath;
|
|
187
237
|
}
|
|
188
238
|
}
|
|
189
239
|
if (fileData && resolvedFilePath) {
|
|
190
|
-
lastInitializedFilePath.current =
|
|
240
|
+
lastInitializedFilePath.current = targetFilePath; // Track initialized path
|
|
191
241
|
const fileName = fileData.code_file_name ||
|
|
192
242
|
resolvedFilePath.split("/").pop() ||
|
|
193
243
|
resolvedFilePath;
|
|
@@ -202,7 +252,7 @@ export default function PillFileSelector({ themeColor }) {
|
|
|
202
252
|
if (frameParams.linesStart !== undefined) {
|
|
203
253
|
setActiveLineRange({
|
|
204
254
|
start: frameParams.linesStart,
|
|
205
|
-
end: frameParams.linesEnd
|
|
255
|
+
end: frameParams.linesEnd
|
|
206
256
|
});
|
|
207
257
|
}
|
|
208
258
|
else {
|
|
@@ -232,14 +282,14 @@ export default function PillFileSelector({ themeColor }) {
|
|
|
232
282
|
}
|
|
233
283
|
}, onClick: () => handleFileClick(filePath, fileData), className: cn("flex items-center gap-1.5 px-2.5 py-1 rounded text-xs font-normal transition-colors whitespace-nowrap", isActive
|
|
234
284
|
? "rounded-2xl text-zinc-100"
|
|
235
|
-
: "text-zinc-400 hover:text-zinc-200", isEdited && "ring-1 ring-yellow-500/50"), style: isActive
|
|
285
|
+
: " rounded-2xl text-zinc-400 hover:text-zinc-200", isEdited && "ring-1 ring-yellow-500/50"), style: Object.assign({ maxWidth: "none", flexShrink: 0 }, (isActive
|
|
236
286
|
? {
|
|
237
287
|
backgroundColor: `${effectiveThemeColor}33`, // ~20% opacity
|
|
238
288
|
color: effectiveThemeColor,
|
|
239
289
|
}
|
|
240
|
-
:
|
|
290
|
+
: {})) },
|
|
241
291
|
React.createElement(IconComponent, { size: 12, className: cn("flex-shrink-0", !isActive && "text-zinc-500"), style: isActive ? { color: effectiveThemeColor } : undefined }),
|
|
242
|
-
React.createElement("span", {
|
|
292
|
+
React.createElement("span", { style: { maxWidth: "none" } }, fileName),
|
|
243
293
|
isEdited && (React.createElement("span", { className: "h-1 w-1 rounded-full bg-yellow-400 flex-shrink-0" })))),
|
|
244
294
|
React.createElement(TooltipContent, { side: "top", className: "bg-zinc-900 text-zinc-200 text-[11px] px-2 py-1 rounded shadow border border-zinc-700 whitespace-pre font-mono" }, filePath)));
|
|
245
295
|
}))));
|
|
@@ -4,10 +4,13 @@ import { useGuardianContext } from "../context/guardian-context";
|
|
|
4
4
|
import { ChevronDown, RefreshCw, Maximize2, Minimize2
|
|
5
5
|
// ExternalLink,
|
|
6
6
|
} from "lucide-react";
|
|
7
|
-
export default function PreviewControlBar({ isMinimized, onToggle, onRefresh, themeColor, externalUrl // <-- NEW PROP
|
|
7
|
+
export default function PreviewControlBar({ isMinimized, onToggle, onRefresh, themeColor, externalUrl, // <-- NEW PROP
|
|
8
|
+
isFrame = false, // <-- NEW PROP for frame mode awareness
|
|
8
9
|
}) {
|
|
9
10
|
const { isBrowserMaximized, setIsBrowserMaximized } = useGuardianContext();
|
|
10
11
|
const frameParams = useFrameParams();
|
|
12
|
+
// Use prop isFrame if provided, otherwise fall back to frameParams.isFrame
|
|
13
|
+
const effectiveIsFrame = isFrame !== null && isFrame !== void 0 ? isFrame : frameParams.isFrame;
|
|
11
14
|
const effectiveThemeColor = frameParams.isFrame && frameParams.theme ? frameParams.theme : themeColor;
|
|
12
15
|
return (React.createElement("div", { className: "w-full flex items-center justify-between px-4 py-[0.32rem] border-b bg-black rounded-t-lg" },
|
|
13
16
|
React.createElement("button", { onClick: onToggle, className: "flex items-center gap-2 transition-colors", title: isMinimized ? "Show preview" : "Hide preview" },
|
|
@@ -20,5 +23,5 @@ export default function PreviewControlBar({ isMinimized, onToggle, onRefresh, th
|
|
|
20
23
|
React.createElement("div", { className: "flex items-center gap-2" },
|
|
21
24
|
React.createElement("button", { onClick: onRefresh, className: "p-1.5 rounded transition-colors hover:bg-zinc-800", title: "Refresh preview", style: { color: effectiveThemeColor } },
|
|
22
25
|
React.createElement(RefreshCw, { className: "w-4 h-4" })),
|
|
23
|
-
React.createElement("button", { onClick: () => setIsBrowserMaximized(!isBrowserMaximized), className: "p-1.5 rounded transition-colors hover:bg-zinc-800", title: isBrowserMaximized ? "Exit fullscreen" : "Fullscreen preview", style: { color: effectiveThemeColor } }, isBrowserMaximized ? (React.createElement(Minimize2, { className: "w-4 h-4" })) : (React.createElement(Maximize2, { className: "w-4 h-4" }))))));
|
|
26
|
+
!effectiveIsFrame && (React.createElement("button", { onClick: () => setIsBrowserMaximized(!isBrowserMaximized), className: "p-1.5 rounded transition-colors hover:bg-zinc-800", title: isBrowserMaximized ? "Exit fullscreen" : "Fullscreen preview", style: { color: effectiveThemeColor } }, isBrowserMaximized ? (React.createElement(Minimize2, { className: "w-4 h-4" })) : (React.createElement(Maximize2, { className: "w-4 h-4" })))))));
|
|
24
27
|
}
|