@sampleapp.ai/sdk 1.0.42 → 1.0.43
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 +50 -29
- package/dist/components/sandbox/api.js +73 -1
- package/dist/components/sandbox/guardian/demo/guardian-demo.js +3 -3
- package/dist/components/sandbox/guardian/guardian-component.js +5 -3
- package/dist/components/sandbox/guardian/guardian-playground.js +10 -14
- package/dist/components/sandbox/guardian/header.js +26 -39
- package/dist/components/sandbox/guardian/hooks/use-sandbox-url-loader.js +16 -4
- package/dist/components/sandbox/guardian/index.js +1 -0
- package/dist/components/sandbox/guardian/right-view/pill-file-selector.js +68 -18
- package/dist/components/sandbox/guardian/right-view/preview-control-bar.js +5 -2
- package/dist/components/sandbox/guardian/right-view/right-top-down-view.js +3 -4
- package/dist/components/sandbox/guardian/right-view/right-view.js +2 -2
- package/dist/components/sandbox/guardian/right-view/simplified-editor.js +9 -2
- package/dist/components/sandbox/guardian/utils.js +4 -16
- package/dist/hooks/use-tree-selector.js +98 -0
- package/dist/index.d.ts +579 -20
- package/dist/index.es.js +17937 -17550
- package/dist/index.js +12 -4
- package/dist/lib/api-client.js +68 -0
- package/dist/lib/content-matcher.js +99 -0
- package/dist/lib/shadow-dom-wrapper.js +31 -10
- package/dist/lib/tree-utils.js +191 -0
- package/dist/lib/types/tree-config.js +18 -0
- package/package.json +2 -2
|
@@ -1,11 +1,49 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import React, { useEffect, useState } from "react";
|
|
3
|
-
import {
|
|
2
|
+
import React, { useEffect, useState, useCallback } from "react";
|
|
3
|
+
import { fetchSandboxConfigWithContent, buildConfigFromContent, } from "./api";
|
|
4
4
|
import GuardianPlayground from "./guardian/guardian-playground";
|
|
5
5
|
import { GuardianProvider } from "./guardian/context/guardian-context";
|
|
6
6
|
import { VmProvider } from "./guardian/context/vm-context";
|
|
7
|
-
import { buildGuardianConfig } from "./guardian/utils";
|
|
8
7
|
import { Skeleton } from "../ui/skeleton";
|
|
8
|
+
import { useTreeSelector } from "../../hooks/use-tree-selector";
|
|
9
|
+
/**
|
|
10
|
+
* Inner component that uses the tree selector hook
|
|
11
|
+
* Separated to ensure hooks are called after data is loaded
|
|
12
|
+
*/
|
|
13
|
+
function SandboxInner({ configWithContent, apiKey, env, themeColor, theme, config, }) {
|
|
14
|
+
var _a;
|
|
15
|
+
const [currentConfig, setCurrentConfig] = useState(configWithContent.legacyConfig);
|
|
16
|
+
// Handle selection changes - update the config when user selects different options
|
|
17
|
+
const handleSelectionChange = useCallback((selection, content) => {
|
|
18
|
+
if (content) {
|
|
19
|
+
const newConfig = buildConfigFromContent(content);
|
|
20
|
+
// Apply theme color override if provided
|
|
21
|
+
if (themeColor) {
|
|
22
|
+
newConfig.themeColor = themeColor;
|
|
23
|
+
newConfig.useCases = newConfig.useCases.map((uc) => (Object.assign(Object.assign({}, uc), { themeColor: themeColor, frameworks: uc.frameworks.map((fw) => (Object.assign(Object.assign({}, fw), { themeColor: themeColor }))) })));
|
|
24
|
+
}
|
|
25
|
+
setCurrentConfig(newConfig);
|
|
26
|
+
}
|
|
27
|
+
}, [themeColor]);
|
|
28
|
+
// Use the tree selector hook for managing selection state
|
|
29
|
+
const { sections, selectOption, currentContent } = useTreeSelector({
|
|
30
|
+
config: configWithContent.techStackConfig,
|
|
31
|
+
allContent: configWithContent.allContent,
|
|
32
|
+
initialPath: configWithContent.initialSelectionPath,
|
|
33
|
+
onSelectionChange: handleSelectionChange,
|
|
34
|
+
});
|
|
35
|
+
// Get the first use case and framework from current config
|
|
36
|
+
const firstUseCase = currentConfig.useCases[0];
|
|
37
|
+
const firstFramework = (_a = firstUseCase === null || firstUseCase === void 0 ? void 0 : firstUseCase.frameworks[0]) === null || _a === void 0 ? void 0 : _a.key;
|
|
38
|
+
if (!firstUseCase || !firstFramework) {
|
|
39
|
+
return (React.createElement("div", { className: "flex items-center justify-center h-screen bg-white dark:bg-black" },
|
|
40
|
+
React.createElement("div", { className: "text-red-500" }, "No use cases or frameworks found")));
|
|
41
|
+
}
|
|
42
|
+
return (React.createElement(GuardianProvider, null,
|
|
43
|
+
React.createElement(VmProvider, null,
|
|
44
|
+
React.createElement("div", { className: "h-screen w-screen bg-white dark:bg-black" },
|
|
45
|
+
React.createElement(GuardianPlayground, { sandboxConfig: currentConfig, sections: sections, onSelect: selectOption, useCase: firstUseCase.id, framework: firstFramework, isFrame: config === null || config === void 0 ? void 0 : config.isFrame, apiKey: apiKey, env: env, chatUid: currentConfig.chatUid, theme: theme })))));
|
|
46
|
+
}
|
|
9
47
|
/**
|
|
10
48
|
* Sandbox component - simplified API for embedding sandboxes
|
|
11
49
|
*
|
|
@@ -33,14 +71,13 @@ import { Skeleton } from "../ui/skeleton";
|
|
|
33
71
|
* />
|
|
34
72
|
* ```
|
|
35
73
|
*/
|
|
36
|
-
export default function Sandbox({ apiKey, sandboxId, env, themeColor, theme = "dark", }) {
|
|
37
|
-
var _a;
|
|
74
|
+
export default function Sandbox({ apiKey, sandboxId, env, themeColor, theme = "dark", config, }) {
|
|
38
75
|
// Validate apiKey immediately
|
|
39
76
|
if (!apiKey || apiKey.trim() === "") {
|
|
40
77
|
return (React.createElement("div", { className: "flex items-center justify-center h-screen bg-white dark:bg-black" },
|
|
41
78
|
React.createElement("div", { className: "text-red-500" }, "Error: apiKey is required. Please provide a valid API key.")));
|
|
42
79
|
}
|
|
43
|
-
const [
|
|
80
|
+
const [configWithContent, setConfigWithContent] = useState(null);
|
|
44
81
|
const [loading, setLoading] = useState(true);
|
|
45
82
|
const [error, setError] = useState(null);
|
|
46
83
|
useEffect(() => {
|
|
@@ -49,14 +86,15 @@ export default function Sandbox({ apiKey, sandboxId, env, themeColor, theme = "d
|
|
|
49
86
|
try {
|
|
50
87
|
setLoading(true);
|
|
51
88
|
setError(null);
|
|
52
|
-
const fetchedConfig = await
|
|
89
|
+
const fetchedConfig = await fetchSandboxConfigWithContent(apiKey, sandboxId);
|
|
53
90
|
if (!cancelled) {
|
|
54
91
|
// Override theme color if provided
|
|
55
92
|
if (themeColor) {
|
|
56
|
-
fetchedConfig.themeColor = themeColor;
|
|
57
|
-
fetchedConfig.useCases =
|
|
93
|
+
fetchedConfig.legacyConfig.themeColor = themeColor;
|
|
94
|
+
fetchedConfig.legacyConfig.useCases =
|
|
95
|
+
fetchedConfig.legacyConfig.useCases.map((uc) => (Object.assign(Object.assign({}, uc), { themeColor: themeColor, frameworks: uc.frameworks.map((fw) => (Object.assign(Object.assign({}, fw), { themeColor: themeColor }))) })));
|
|
58
96
|
}
|
|
59
|
-
|
|
97
|
+
setConfigWithContent(fetchedConfig);
|
|
60
98
|
setLoading(false);
|
|
61
99
|
}
|
|
62
100
|
}
|
|
@@ -82,25 +120,8 @@ export default function Sandbox({ apiKey, sandboxId, env, themeColor, theme = "d
|
|
|
82
120
|
"Error: ",
|
|
83
121
|
error)));
|
|
84
122
|
}
|
|
85
|
-
if (!
|
|
123
|
+
if (!configWithContent) {
|
|
86
124
|
return null;
|
|
87
125
|
}
|
|
88
|
-
|
|
89
|
-
const nestedConfig = buildGuardianConfig(config.useCases, {
|
|
90
|
-
playgroundUid: sandboxId,
|
|
91
|
-
playgroundLogo: config.playgroundLogo || (React.createElement("div", { className: "text-zinc-900 dark:text-white font-bold text-xl" }, config.name)),
|
|
92
|
-
});
|
|
93
|
-
// Get the first use case and framework as defaults
|
|
94
|
-
const firstUseCase = config.useCases[0];
|
|
95
|
-
const firstFramework = (_a = firstUseCase === null || firstUseCase === void 0 ? void 0 : firstUseCase.frameworks[0]) === null || _a === void 0 ? void 0 : _a.key;
|
|
96
|
-
if (!firstUseCase || !firstFramework) {
|
|
97
|
-
return (React.createElement("div", { className: "flex items-center justify-center h-screen bg-white dark:bg-black" },
|
|
98
|
-
React.createElement("div", { className: "text-red-500" }, "No use cases or frameworks found")));
|
|
99
|
-
}
|
|
100
|
-
// TODO: Pass env variables to the container runtime
|
|
101
|
-
// This will be implemented when integrating with the container technology
|
|
102
|
-
return (React.createElement(GuardianProvider, null,
|
|
103
|
-
React.createElement(VmProvider, null,
|
|
104
|
-
React.createElement("div", { className: "h-screen w-screen bg-white dark:bg-black" },
|
|
105
|
-
React.createElement(GuardianPlayground, { nestedConfig: nestedConfig, useCase: firstUseCase.id, framework: firstFramework, isFrame: false, apiKey: apiKey, env: env, chatUid: config.chatUid, theme: theme })))));
|
|
126
|
+
return (React.createElement(SandboxInner, { configWithContent: configWithContent, apiKey: apiKey, env: env, themeColor: themeColor, theme: theme, config: config }));
|
|
106
127
|
}
|
|
@@ -15,10 +15,82 @@ function createDownloadLink(downloadEndpoint) {
|
|
|
15
15
|
document.body.removeChild(link);
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
* Fetches sandbox configuration
|
|
18
|
+
* Fetches sandbox configuration with all content for upfront loading
|
|
19
|
+
* This enables instant switching between tech stack options without API calls
|
|
20
|
+
*
|
|
21
|
+
* @param apiKey - API key from SandboxProps
|
|
22
|
+
* @param sandboxId - Sandbox content UID (maps to sandbox_content_uid)
|
|
23
|
+
* @returns Extended sandbox configuration with all content
|
|
24
|
+
*/
|
|
25
|
+
export async function fetchSandboxConfigWithContent(apiKey, sandboxId) {
|
|
26
|
+
const client = createApiClient({ apiKey });
|
|
27
|
+
// 1. Fetch the initially requested sandbox content
|
|
28
|
+
const initialContent = await client.sandboxContent.getPublic(sandboxId);
|
|
29
|
+
// 2. Fetch the use case to get tech_stack_config
|
|
30
|
+
const useCase = await client.sandboxUseCase.getById(initialContent.use_case_id);
|
|
31
|
+
// 3. Fetch ALL sandbox content for this use case (for local filtering)
|
|
32
|
+
const allContent = await client.sandboxContent.getByUseCaseId(initialContent.use_case_id);
|
|
33
|
+
// 4. Build legacy config for backward compatibility
|
|
34
|
+
const legacyConfig = buildLegacyConfig(initialContent);
|
|
35
|
+
return {
|
|
36
|
+
useCase,
|
|
37
|
+
techStackConfig: useCase.tech_stack_config,
|
|
38
|
+
allContent,
|
|
39
|
+
initialContent,
|
|
40
|
+
initialSelectionPath: initialContent.selection_path || [],
|
|
41
|
+
legacyConfig,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Build legacy SandboxConfig from SandboxContent for backward compatibility
|
|
46
|
+
*/
|
|
47
|
+
function buildLegacyConfig(sandboxContent) {
|
|
48
|
+
const frameworkKey = sandboxContent.framework || Framework.NEXTJS;
|
|
49
|
+
const generatedCode = sandboxContent.generated_code || {};
|
|
50
|
+
const completeCodeZipFile = "";
|
|
51
|
+
return {
|
|
52
|
+
id: sandboxContent.uid,
|
|
53
|
+
name: sandboxContent.uid,
|
|
54
|
+
description: sandboxContent.markdown || "",
|
|
55
|
+
themeColor: "#3b82f6",
|
|
56
|
+
hasPreview: sandboxContent.has_preview,
|
|
57
|
+
chatUid: sandboxContent.chat_uid || "",
|
|
58
|
+
useCases: [
|
|
59
|
+
{
|
|
60
|
+
id: sandboxContent.uid,
|
|
61
|
+
name: sandboxContent.uid,
|
|
62
|
+
description: sandboxContent.markdown || "",
|
|
63
|
+
themeColor: "#3b82f6",
|
|
64
|
+
hasPreview: sandboxContent.has_preview,
|
|
65
|
+
frameworks: [
|
|
66
|
+
{
|
|
67
|
+
key: frameworkKey,
|
|
68
|
+
browserUrl: sandboxContent.published_url || "",
|
|
69
|
+
sandboxUid: sandboxContent.uid,
|
|
70
|
+
codeZipFile: Object.keys(generatedCode).length > 0 ? generatedCode : "",
|
|
71
|
+
completeCodeZipFile,
|
|
72
|
+
useVm: sandboxContent.is_vm_enabled || false,
|
|
73
|
+
CustomConsole: sandboxContent.markdown || "",
|
|
74
|
+
GuideView: "Default Guide",
|
|
75
|
+
gitUrl: sandboxContent.github_url || undefined,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Build SandboxConfig from any SandboxContent (for switching between selections)
|
|
84
|
+
*/
|
|
85
|
+
export function buildConfigFromContent(content) {
|
|
86
|
+
return buildLegacyConfig(content);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Fetches sandbox configuration from the API (legacy method)
|
|
19
90
|
* @param apiKey - API key from SandboxProps
|
|
20
91
|
* @param sandboxId - Sandbox content UID (maps to sandbox_content_uid)
|
|
21
92
|
* @returns Sandbox configuration
|
|
93
|
+
* @deprecated Use fetchSandboxConfigWithContent for tree-based selection
|
|
22
94
|
*/
|
|
23
95
|
export async function fetchSandboxConfig(apiKey, sandboxId) {
|
|
24
96
|
const client = createApiClient({
|
|
@@ -24,12 +24,12 @@ export default function GuardianDemo({ CustomConsole, GuideView, browserUrl, pla
|
|
|
24
24
|
}, [setCurrentView]);
|
|
25
25
|
// If isFrame, just return RightView directly
|
|
26
26
|
if (isFrame) {
|
|
27
|
-
return (React.createElement(RightView, { reloadCounter: reloadCounter, overlayStage: overlayStage, browserUrl: browserUrl, useVm: useVm, codeZipFile: codeZipFile, hasPreview: hasPreview, variant: variant, themeColor: themeColor, completeCodeZipFile: completeCodeZipFile, isGuardian: isGuardian, isBrowserMaximized: isBrowserMaximized, isBrowserUrlReady: isBrowserUrlReady, sandboxId: sandboxId, apiKey: apiKey, sandboxError: sandboxError }));
|
|
27
|
+
return (React.createElement(RightView, { reloadCounter: reloadCounter, overlayStage: overlayStage, browserUrl: browserUrl, useVm: useVm, codeZipFile: codeZipFile, hasPreview: hasPreview, variant: variant, themeColor: themeColor, completeCodeZipFile: completeCodeZipFile, isGuardian: isGuardian, isBrowserMaximized: isBrowserMaximized, isBrowserUrlReady: isBrowserUrlReady, isFrame: isFrame, sandboxId: sandboxId, apiKey: apiKey, sandboxError: sandboxError }));
|
|
28
28
|
}
|
|
29
29
|
// If browser is maximized, render RightView at full size without the console
|
|
30
30
|
if (isBrowserMaximized && isGuardian) {
|
|
31
31
|
return (React.createElement("div", { className: "w-full h-full rounded-2xl overflow-hidden" },
|
|
32
|
-
React.createElement(RightView, { reloadCounter: reloadCounter, overlayStage: overlayStage, browserUrl: browserUrl, useVm: useVm, codeZipFile: codeZipFile, hasPreview: hasPreview, variant: variant, themeColor: themeColor, completeCodeZipFile: completeCodeZipFile, isGuardian: isGuardian, isBrowserMaximized: true, isBrowserUrlReady: isBrowserUrlReady, sandboxId: sandboxId, apiKey: apiKey, sandboxError: sandboxError })));
|
|
32
|
+
React.createElement(RightView, { reloadCounter: reloadCounter, overlayStage: overlayStage, browserUrl: browserUrl, useVm: useVm, codeZipFile: codeZipFile, hasPreview: hasPreview, variant: variant, themeColor: themeColor, completeCodeZipFile: completeCodeZipFile, isGuardian: isGuardian, isBrowserMaximized: true, isBrowserUrlReady: isBrowserUrlReady, isFrame: isFrame, sandboxId: sandboxId, apiKey: apiKey, sandboxError: sandboxError })));
|
|
33
33
|
}
|
|
34
|
-
return (React.createElement(ConsoleWithApp, { containerClassName: "", console: React.createElement(ConsoleWithGuide, { CustomConsole: CustomConsole, GuideView: GuideView, playgroundUid: playgroundUid, onReloadPreview: handleReloadPreview, onStageChange: handleStageChange, codeZipFile: codeZipFile, themeColor: themeColor, browserUrl: browserUrl, gitUrl: gitUrl, sandboxId: sandboxId, apiKey: apiKey }), app: React.createElement(RightView, { reloadCounter: reloadCounter, overlayStage: overlayStage, browserUrl: browserUrl, useVm: useVm, codeZipFile: codeZipFile, hasPreview: hasPreview, variant: variant, themeColor: themeColor, completeCodeZipFile: completeCodeZipFile, isGuardian: isGuardian, isBrowserMaximized: isBrowserMaximized, isBrowserUrlReady: isBrowserUrlReady, sandboxId: sandboxId, apiKey: apiKey, sandboxError: sandboxError }) }));
|
|
34
|
+
return (React.createElement(ConsoleWithApp, { containerClassName: "", console: React.createElement(ConsoleWithGuide, { CustomConsole: CustomConsole, GuideView: GuideView, playgroundUid: playgroundUid, onReloadPreview: handleReloadPreview, onStageChange: handleStageChange, codeZipFile: codeZipFile, themeColor: themeColor, browserUrl: browserUrl, gitUrl: gitUrl, sandboxId: sandboxId, apiKey: apiKey }), app: React.createElement(RightView, { reloadCounter: reloadCounter, overlayStage: overlayStage, browserUrl: browserUrl, useVm: useVm, codeZipFile: codeZipFile, hasPreview: hasPreview, variant: variant, themeColor: themeColor, completeCodeZipFile: completeCodeZipFile, isGuardian: isGuardian, isBrowserMaximized: isBrowserMaximized, isBrowserUrlReady: isBrowserUrlReady, isFrame: isFrame, sandboxId: sandboxId, apiKey: apiKey, sandboxError: sandboxError }) }));
|
|
35
35
|
}
|
|
@@ -8,7 +8,7 @@ import { useSandboxUrlLoader } from "./hooks/use-sandbox-url-loader";
|
|
|
8
8
|
import { useFrameMessages } from "./hooks/use-frame-messages";
|
|
9
9
|
import { useVmContext } from "./context/vm-context";
|
|
10
10
|
import { cn } from "../../../lib/utils";
|
|
11
|
-
export default function GuardianComponent({
|
|
11
|
+
export default function GuardianComponent({ sections, onSelect, currentFramework, currentUseCase, CustomConsole, GuideView, playgroundLogo, playgroundUid, browserUrl, useVm, sandboxUid, codeZipFile, completeCodeZipFile, variant, themeColor, hasPreview = true, isFrame = false, apiKey, env, chatUid, hideHeader = false, // Hardcoded to true by default, not exposed in Sandbox.tsx
|
|
12
12
|
gitUrl, }) {
|
|
13
13
|
const { previewUrl, setPreviewUrl } = useGuardianContext();
|
|
14
14
|
const { vmResolution } = useVmContext();
|
|
@@ -16,7 +16,9 @@ gitUrl, }) {
|
|
|
16
16
|
// Build startSandboxConfig for use-sandbox-url-loader
|
|
17
17
|
// apiKey is always included (guaranteed by parent validation)
|
|
18
18
|
// Include chatUid and env when browserUrl is missing (needed for startSandbox API call)
|
|
19
|
-
|
|
19
|
+
// Include hasPreview to skip env/chatUid validation when no preview is needed
|
|
20
|
+
const startSandboxConfig = Object.assign({ apiKey,
|
|
21
|
+
hasPreview }, (!browserUrl && chatUid ? { env, chatUid } : {}));
|
|
20
22
|
// Determine if browserUrl is ready to use immediately (from published_url)
|
|
21
23
|
// If browserUrl exists, it's ready (startSandboxConfig may still exist with just apiKey for VM calls)
|
|
22
24
|
const isBrowserUrlReady = !!browserUrl;
|
|
@@ -80,7 +82,7 @@ gitUrl, }) {
|
|
|
80
82
|
return (React.createElement("div", { className: "h-[100vh]" },
|
|
81
83
|
React.createElement(GuardianDemo, { CustomConsole: CustomConsole, GuideView: GuideView, browserUrl: previewUrl, playgroundUid: playgroundUid, useVm: useVm, codeZipFile: codeZipFile, completeCodeZipFile: completeCodeZipFile, variant: variant, themeColor: themeColor, hasPreview: hasPreview, isFrame: isFrame, isGuardian: true, gitUrl: gitUrl, isBrowserUrlReady: isBrowserUrlReady, sandboxId: sandboxUid, apiKey: apiKey, sandboxError: sandboxError })));
|
|
82
84
|
}
|
|
83
|
-
return (React.createElement(AppLayoutNoSidebar, { header: !hideHeader ? (React.createElement(Header, {
|
|
85
|
+
return (React.createElement(AppLayoutNoSidebar, { header: !hideHeader ? (React.createElement(Header, { sections: sections, onSelect: onSelect, playgroundLogo: playgroundLogo, themecolor: themeColor })) : undefined, hasBodyPadding: false },
|
|
84
86
|
React.createElement("div", { className: cn("flex-1 min-h-0 flex flex-col", hideHeader && "pt-4") },
|
|
85
87
|
React.createElement(GuardianDemo, { CustomConsole: CustomConsole, GuideView: GuideView, browserUrl: previewUrl, playgroundUid: playgroundUid, useVm: useVm, codeZipFile: codeZipFile, completeCodeZipFile: completeCodeZipFile, variant: variant, themeColor: themeColor, hasPreview: hasPreview, isFrame: isFrame, isGuardian: true, gitUrl: gitUrl, isBrowserUrlReady: isBrowserUrlReady, sandboxId: sandboxUid, apiKey: apiKey, sandboxError: sandboxError }))));
|
|
86
88
|
}
|
|
@@ -3,17 +3,21 @@ import React from "react";
|
|
|
3
3
|
import GuardianComponent from "./guardian-component";
|
|
4
4
|
import { useFrameParams } from "./hooks/use-frame-params";
|
|
5
5
|
import { GuardianStyleWrapper } from "./guardian-style-wrapper";
|
|
6
|
+
import { buildGuardianConfig } from "./utils";
|
|
6
7
|
/**
|
|
7
|
-
* Guardian Playground component with
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* When isFrame=true, reads framework from URL params client-side.
|
|
8
|
+
* Guardian Playground component with tree-based tech stack selection.
|
|
9
|
+
* Uses sections from useTreeSelector for dynamic header rendering.
|
|
11
10
|
*/
|
|
12
|
-
export default function GuardianPlayground({
|
|
11
|
+
export default function GuardianPlayground({ sandboxConfig, sections, onSelect, useCase, framework: serverFramework, isFrame = false, apiKey, env, chatUid, theme = "dark", }) {
|
|
13
12
|
var _a;
|
|
14
13
|
// When in frame mode, allow URL params to override framework
|
|
15
14
|
const frameParams = useFrameParams();
|
|
16
15
|
const framework = isFrame && frameParams.framework ? frameParams.framework : serverFramework;
|
|
16
|
+
// Build the nested config from use cases
|
|
17
|
+
const nestedConfig = buildGuardianConfig(sandboxConfig.useCases, {
|
|
18
|
+
playgroundUid: sandboxConfig.id,
|
|
19
|
+
playgroundLogo: sandboxConfig.playgroundLogo || (React.createElement("div", { className: "text-zinc-900 dark:text-white font-bold text-xl" }, sandboxConfig.name)),
|
|
20
|
+
});
|
|
17
21
|
const useCaseConfig = nestedConfig[useCase];
|
|
18
22
|
if (!useCaseConfig) {
|
|
19
23
|
return (React.createElement("div", { className: "flex items-center justify-center h-screen" },
|
|
@@ -32,14 +36,6 @@ export default function GuardianPlayground({ nestedConfig, useCase, framework: s
|
|
|
32
36
|
useCase,
|
|
33
37
|
"\"")));
|
|
34
38
|
}
|
|
35
|
-
// Build a map of use case ID to first available framework key
|
|
36
|
-
const firstFrameworkByUseCase = {};
|
|
37
|
-
Object.entries(nestedConfig).forEach(([useCaseId, uc]) => {
|
|
38
|
-
const firstFrameworkKey = Object.keys(uc.frameworks)[0];
|
|
39
|
-
if (firstFrameworkKey) {
|
|
40
|
-
firstFrameworkByUseCase[useCaseId] = firstFrameworkKey;
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
39
|
return (React.createElement(GuardianStyleWrapper, { themeColor: frameworkConfig.themeColor, theme: theme },
|
|
44
|
-
React.createElement(GuardianComponent, {
|
|
40
|
+
React.createElement(GuardianComponent, { sections: sections, onSelect: onSelect, currentFramework: frameworkConfig.currentFramework, CustomConsole: frameworkConfig.CustomConsole, GuideView: frameworkConfig.GuideView, playgroundUid: frameworkConfig.playgroundUid, browserUrl: (_a = frameParams.iframeUrl) !== null && _a !== void 0 ? _a : frameworkConfig.browserUrl, useVm: frameworkConfig.useVm, playgroundLogo: frameworkConfig.playgroundLogo, sandboxUid: frameworkConfig.sandboxUid, name: frameworkConfig.name, description: frameworkConfig.description, codeZipFile: frameworkConfig.codeZipFile, completeCodeZipFile: frameworkConfig.completeCodeZipFile, variant: frameworkConfig.variant, themeColor: frameworkConfig.themeColor, hasPreview: frameworkConfig.hasPreview, currentUseCase: useCase, isFrame: isFrame, apiKey: apiKey, env: env, chatUid: chatUid, gitUrl: frameworkConfig.gitUrl, hideHeader: false })));
|
|
45
41
|
}
|
|
@@ -1,53 +1,40 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import React from "react";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// label: "Use Case:",
|
|
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
|
+
/**
|
|
4
|
+
* Header component for dynamic tech stack selection
|
|
5
|
+
*
|
|
6
|
+
* Uses sections from useTreeSelector to render selector buttons
|
|
7
|
+
* based on the tech_stack_config tree structure.
|
|
8
|
+
*/
|
|
9
|
+
export default function Header({ sections, onSelect, playgroundLogo, themecolor }) {
|
|
10
|
+
console.log("playgroundLogo", playgroundLogo);
|
|
24
11
|
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" },
|
|
12
|
+
React.createElement("div", { className: "flex flex-wrap items-center justify-between py-2" },
|
|
26
13
|
React.createElement("div", { className: "flex flex-wrap items-center gap-6" }, sections.map((section) => {
|
|
27
|
-
|
|
28
|
-
return (React.createElement("div", { key: section.
|
|
14
|
+
var _a;
|
|
15
|
+
return (React.createElement("div", { key: section.nodeType, className: "flex items-center flex-wrap gap-3" },
|
|
29
16
|
React.createElement("span", { className: "text-sm font-semibold text-gray-600 dark:text-gray-400 whitespace-nowrap" }, section.label),
|
|
30
17
|
React.createElement("div", { className: "hidden md:block" },
|
|
31
18
|
React.createElement("div", { className: "flex items-center gap-2" }, section.options.map((option) => {
|
|
32
|
-
const isSelected =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
19
|
+
const isSelected = option.isSelected;
|
|
20
|
+
const isDisabled = option.hasContent === false;
|
|
21
|
+
return (React.createElement("button", { key: option.key, onClick: () => {
|
|
22
|
+
if (!isDisabled) {
|
|
23
|
+
onSelect(section.nodeType, option.key);
|
|
36
24
|
}
|
|
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
|
|
25
|
+
}, 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
|
|
38
26
|
? "bg-white dark:bg-zinc-900 hover:bg-zinc-100 dark:hover:bg-zinc-800"
|
|
39
|
-
: "bg-white dark:bg-zinc-900 border-zinc-300 dark:border-zinc-700 text-zinc-900 dark:text-zinc-100 hover:bg-zinc-100 dark:hover:bg-zinc-800"}`, style: isSelected && themecolor
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
27
|
+
: "bg-white dark:bg-zinc-900 border-zinc-300 dark:border-zinc-700 text-zinc-900 dark:text-zinc-100 hover:bg-zinc-100 dark:hover:bg-zinc-800"} ${isDisabled ? "opacity-50 cursor-not-allowed" : ""}`, style: isSelected && themecolor
|
|
28
|
+
? {
|
|
29
|
+
borderColor: themecolor,
|
|
30
|
+
color: themecolor
|
|
31
|
+
}
|
|
32
|
+
: undefined }, option.label));
|
|
43
33
|
}))),
|
|
44
34
|
React.createElement("div", { className: "block md:hidden relative" },
|
|
45
|
-
React.createElement("select", { value:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
window.location.href = selectedOption.href;
|
|
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)))),
|
|
35
|
+
React.createElement("select", { value: ((_a = section.options.find((o) => o.isSelected)) === null || _a === void 0 ? void 0 : _a.key) || "", onChange: (e) => {
|
|
36
|
+
onSelect(section.nodeType, e.target.value);
|
|
37
|
+
}, 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.key, value: option.key, disabled: option.hasContent === false }, option.label)))),
|
|
51
38
|
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
39
|
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
40
|
})))));
|
|
@@ -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,5 +1,6 @@
|
|
|
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";
|
|
5
6
|
export * from "./types";
|
|
@@ -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 {
|
|
@@ -235,7 +285,7 @@ export default function PillFileSelector({ themeColor }) {
|
|
|
235
285
|
: "text-zinc-400 hover:text-zinc-200", isEdited && "ring-1 ring-yellow-500/50"), style: isActive
|
|
236
286
|
? {
|
|
237
287
|
backgroundColor: `${effectiveThemeColor}33`, // ~20% opacity
|
|
238
|
-
color: effectiveThemeColor
|
|
288
|
+
color: effectiveThemeColor
|
|
239
289
|
}
|
|
240
290
|
: undefined },
|
|
241
291
|
React.createElement(IconComponent, { size: 12, className: cn("flex-shrink-0", !isActive && "text-zinc-500"), style: isActive ? { color: effectiveThemeColor } : undefined }),
|