@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.
Files changed (40) hide show
  1. package/dist/components/sandbox/Sandbox.js +96 -29
  2. package/dist/components/sandbox/api.js +73 -1
  3. package/dist/components/sandbox/guardian/code-focus-section.js +4 -2
  4. package/dist/components/sandbox/guardian/context/theme-context.js +47 -0
  5. package/dist/components/sandbox/guardian/demo/guardian-demo.js +4 -4
  6. package/dist/components/sandbox/guardian/demo/left-view.js +13 -7
  7. package/dist/components/sandbox/guardian/guardian-component.js +39 -8
  8. package/dist/components/sandbox/guardian/guardian-playground.js +14 -15
  9. package/dist/components/sandbox/guardian/guardian-style-wrapper.js +8 -6
  10. package/dist/components/sandbox/guardian/header.js +37 -41
  11. package/dist/components/sandbox/guardian/hooks/use-frame-messages.js +24 -8
  12. package/dist/components/sandbox/guardian/hooks/use-frame-params.js +94 -36
  13. package/dist/components/sandbox/guardian/hooks/use-sandbox-url-loader.js +16 -4
  14. package/dist/components/sandbox/guardian/index.js +2 -0
  15. package/dist/components/sandbox/guardian/right-view/pill-file-selector.js +70 -20
  16. package/dist/components/sandbox/guardian/right-view/preview-control-bar.js +5 -2
  17. package/dist/components/sandbox/guardian/right-view/right-top-down-view/network-requests-view.js +117 -0
  18. package/dist/components/sandbox/guardian/right-view/right-top-down-view.js +38 -140
  19. package/dist/components/sandbox/guardian/right-view/right-view.js +3 -3
  20. package/dist/components/sandbox/guardian/right-view/simplified-editor.js +9 -2
  21. package/dist/components/sandbox/guardian/ui/markdown/code-group/code-block.js +7 -33
  22. package/dist/components/sandbox/guardian/ui/markdown.js +23 -14
  23. package/dist/components/sandbox/guardian/utils.js +4 -16
  24. package/dist/components/sandbox/sandbox-home/SandboxCard.js +7 -5
  25. package/dist/components/sandbox/sandbox-home/SandboxHome.js +32 -13
  26. package/dist/components/sandbox/sandbox-home/SearchBar.js +5 -3
  27. package/dist/hooks/use-tree-selector.js +98 -0
  28. package/dist/index.d.ts +631 -21
  29. package/dist/index.es.js +22806 -22028
  30. package/dist/index.js +13 -4
  31. package/dist/index.standalone.umd.js +8 -8
  32. package/dist/lib/api-client.js +68 -0
  33. package/dist/lib/content-matcher.js +99 -0
  34. package/dist/lib/generated-css.js +1 -1
  35. package/dist/lib/shadow-dom-wrapper.js +31 -10
  36. package/dist/lib/tree-utils.js +193 -0
  37. package/dist/lib/types/tree-config.js +36 -0
  38. package/dist/sdk.css +1 -1
  39. package/dist/tailwind.css +2 -2
  40. package/package.json +2 -2
@@ -1,53 +1,49 @@
1
1
  "use client";
2
2
  import React from "react";
3
- export default function Header({ demoOptions, frameworkOptions, firstFrameworkByUseCase, currentFramework, playgroundLogo, currentUseCase, themecolor, }) {
4
- var _a, _b;
5
- // Derive values directly from props (which come from the URL)
6
- const selectedUseCase = currentUseCase || ((_a = demoOptions[0]) === null || _a === void 0 ? void 0 : _a.value);
7
- const selectedFramework = currentFramework || ((_b = frameworkOptions[0]) === null || _b === void 0 ? void 0 : _b.value);
8
- const sections = [
9
- // {
10
- // id: "useCase",
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
+ 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
- const currentValue = section.id === "useCase" ? selectedUseCase : selectedFramework;
28
- return (React.createElement("div", { key: section.id, className: "flex items-center flex-wrap gap-3" },
29
- React.createElement("span", { className: "text-sm font-semibold text-gray-600 dark:text-gray-400 whitespace-nowrap" }, section.label),
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 = currentValue === option.value;
33
- return (React.createElement("button", { key: option.value, onClick: () => {
34
- if (typeof window !== "undefined") {
35
- window.location.href = option.href;
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
- ? "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
- borderColor: themecolor,
41
- color: themecolor,
42
- } : undefined }, option.label));
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: currentValue, onChange: (e) => {
46
- const selectedOption = section.options.find((opt) => opt.value === e.target.value);
47
- if (selectedOption === null || selectedOption === void 0 ? void 0 : selectedOption.href) {
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)))),
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
- * - framework: Switch between frameworks (e.g., "nextjs", "react")
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 isFrame = window.self !== window.top;
22
- if (!isFrame)
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
- // Signal to parent that we're ready to receive messages
25
- if (!hasSignaledReady.current && window.parent) {
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 URL params based on message
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 () => window.removeEventListener("message", handleMessage);
76
+ return () => {
77
+ window.removeEventListener("message", handleMessage);
78
+ };
63
79
  }, []);
64
80
  return {};
65
81
  }
@@ -1,44 +1,102 @@
1
- import { useMemo } from "react";
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
- * Note: This is a simplified version that reads from window.location.search.
7
- * For Next.js apps, you can use the Next.js version that uses useSearchParams.
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
- return useMemo(() => {
11
- if (typeof window === "undefined") {
12
- return {
13
- isFrame: false,
14
- framework: undefined,
15
- theme: undefined,
16
- activeFilePath: undefined,
17
- linesStart: undefined,
18
- linesEnd: undefined,
19
- iframeUrl: undefined,
20
- };
21
- }
22
- const searchParams = new URLSearchParams(window.location.search);
23
- const isFrame = searchParams.get("isFrame") === "true";
24
- const framework = searchParams.get("framework") || undefined;
25
- const theme = searchParams.get("theme") || undefined;
26
- const activeFilePath = searchParams.get("activeFilePath") || undefined;
27
- const linesStart = searchParams.get("linesStart")
28
- ? parseInt(searchParams.get("linesStart"), 10)
29
- : undefined;
30
- const linesEnd = searchParams.get("linesEnd")
31
- ? parseInt(searchParams.get("linesEnd"), 10)
32
- : undefined;
33
- const iframeUrl = searchParams.get("iframeUrl") || undefined;
34
- return {
35
- isFrame,
36
- framework,
37
- theme,
38
- activeFilePath,
39
- linesStart,
40
- linesEnd,
41
- iframeUrl,
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
- }, [typeof window !== "undefined" ? window.location.search : ""]);
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, } from "lucide-react";
7
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "../ui/tooltip";
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, } = useGuardianContext();
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 from frame params when they change
159
+ // Load first file on mount when NOT in frame mode
160
160
  React.useEffect(() => {
161
- // Skip if not in frame mode or no file path specified
162
- if (!frameParams.isFrame || !frameParams.activeFilePath) {
161
+ // Skip if in frame mode (handled by separate effect)
162
+ if (frameParams.isFrame) {
163
163
  return;
164
164
  }
165
- // Skip if we've already initialized this exact file path
166
- if (lastInitializedFilePath.current === frameParams.activeFilePath) {
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 === frameParams.activeFilePath);
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[frameParams.activeFilePath]) {
185
- fileData = generatedCode[frameParams.activeFilePath];
186
- resolvedFilePath = fileData.file_path || frameParams.activeFilePath;
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 = frameParams.activeFilePath; // Track initialized path
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
- : undefined },
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", { className: "truncate max-w-[180px]" }, fileName),
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
  }