mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 07:04:48 +00:00
Polish Foundry desktop UI: billing redesign, sidebar hover menu, org switching fix
- Redesign billing page with task-hours pricing model (Free: 8h, Pro: 200h/seat) - Add bulk hour purchase packages and Stripe payment management - Remove Usage nav section, add upgrade CTA in Members for free plan - Fix gear icon to open menu on hover with debounced timers - Fix org switching in workspace flyout (portal outside-click detection) - Fix tab strip padding when sidebar is collapsed - Update website components and Tauri config Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f6656a90af
commit
ed6e6f6fa5
24 changed files with 1746 additions and 1028 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "foundry-desktop"
|
name = "foundry"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, LogicalPosition, Manager, WebviewUrl, WebviewWindowBuilder};
|
||||||
use tauri_plugin_shell::process::CommandChild;
|
use tauri_plugin_shell::process::CommandChild;
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
|
||||||
|
|
@ -95,6 +95,29 @@ pub fn run() {
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![get_backend_url, backend_health])
|
.invoke_handler(tauri::generate_handler![get_backend_url, backend_health])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
// Create main window programmatically so we can set traffic light position
|
||||||
|
let url = if cfg!(debug_assertions) {
|
||||||
|
WebviewUrl::External("http://localhost:4173".parse().unwrap())
|
||||||
|
} else {
|
||||||
|
WebviewUrl::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut builder = WebviewWindowBuilder::new(app, "main", url)
|
||||||
|
.title("Foundry")
|
||||||
|
.inner_size(1280.0, 800.0)
|
||||||
|
.min_inner_size(900.0, 600.0)
|
||||||
|
.resizable(true)
|
||||||
|
.theme(Some(tauri::Theme::Dark))
|
||||||
|
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
||||||
|
.hidden_title(true);
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
builder = builder.traffic_light_position(LogicalPosition::new(14.0, 14.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.build()?;
|
||||||
|
|
||||||
// In debug mode, assume the developer is running the backend externally
|
// In debug mode, assume the developer is running the backend externally
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
eprintln!("[foundry-desktop] Dev mode: skipping sidecar spawn. Run the backend separately.");
|
eprintln!("[foundry-desktop] Dev mode: skipping sidecar spawn. Run the backend separately.");
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
foundry_desktop::run();
|
foundry::run();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,24 +4,12 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"identifier": "dev.sandboxagent.foundry",
|
"identifier": "dev.sandboxagent.foundry",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "VITE_DESKTOP=1 pnpm --filter @sandbox-agent/foundry-frontend dev",
|
"beforeDevCommand": "FOUNDRY_FRONTEND_CLIENT_MODE=mock VITE_DESKTOP=1 pnpm --filter @sandbox-agent/foundry-frontend dev",
|
||||||
"devUrl": "http://localhost:4173",
|
"devUrl": "http://localhost:4173",
|
||||||
"frontendDist": "../frontend-dist"
|
"frontendDist": "../frontend-dist"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [],
|
||||||
{
|
|
||||||
"title": "Foundry",
|
|
||||||
"width": 1280,
|
|
||||||
"height": 800,
|
|
||||||
"minWidth": 900,
|
|
||||||
"minHeight": 600,
|
|
||||||
"resizable": true,
|
|
||||||
"theme": "Dark",
|
|
||||||
"titleBarStyle": "Overlay",
|
|
||||||
"hiddenTitle": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"security": {
|
"security": {
|
||||||
"csp": null
|
"csp": null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import type { FoundryBillingPlanId } from "@sandbox-agent/foundry-shared";
|
||||||
import { Navigate, Outlet, createRootRoute, createRoute, createRouter, useRouterState } from "@tanstack/react-router";
|
import { Navigate, Outlet, createRootRoute, createRoute, createRouter, useRouterState } from "@tanstack/react-router";
|
||||||
import { MockLayout } from "../components/mock-layout";
|
import { MockLayout } from "../components/mock-layout";
|
||||||
import {
|
import {
|
||||||
|
MockAccountSettingsPage,
|
||||||
MockHostedCheckoutPage,
|
MockHostedCheckoutPage,
|
||||||
MockOrganizationBillingPage,
|
MockOrganizationBillingPage,
|
||||||
MockOrganizationSelectorPage,
|
MockOrganizationSelectorPage,
|
||||||
|
|
@ -30,6 +31,12 @@ const signInRoute = createRoute({
|
||||||
component: SignInRoute,
|
component: SignInRoute,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const accountRoute = createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: "/account",
|
||||||
|
component: AccountRoute,
|
||||||
|
});
|
||||||
|
|
||||||
const organizationsRoute = createRoute({
|
const organizationsRoute = createRoute({
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
path: "/organizations",
|
path: "/organizations",
|
||||||
|
|
@ -84,6 +91,7 @@ const repoRoute = createRoute({
|
||||||
const routeTree = rootRoute.addChildren([
|
const routeTree = rootRoute.addChildren([
|
||||||
indexRoute,
|
indexRoute,
|
||||||
signInRoute,
|
signInRoute,
|
||||||
|
accountRoute,
|
||||||
organizationsRoute,
|
organizationsRoute,
|
||||||
organizationSettingsRoute,
|
organizationSettingsRoute,
|
||||||
organizationBillingRoute,
|
organizationBillingRoute,
|
||||||
|
|
@ -152,6 +160,18 @@ function SignInRoute() {
|
||||||
return <MockSignInPage />;
|
return <MockSignInPage />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AccountRoute() {
|
||||||
|
const snapshot = useMockAppSnapshot();
|
||||||
|
if (!isMockFrontendClient && isAppSnapshotBootstrapping(snapshot)) {
|
||||||
|
return <AppLoadingScreen label="Loading account..." />;
|
||||||
|
}
|
||||||
|
if (snapshot.auth.status === "signed_out") {
|
||||||
|
return <Navigate to="/signin" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <MockAccountSettingsPage />;
|
||||||
|
}
|
||||||
|
|
||||||
function OrganizationsRoute() {
|
function OrganizationsRoute() {
|
||||||
const snapshot = useMockAppSnapshot();
|
const snapshot = useMockAppSnapshot();
|
||||||
if (!isMockFrontendClient && isAppSnapshotBootstrapping(snapshot)) {
|
if (!isMockFrontendClient && isAppSnapshotBootstrapping(snapshot)) {
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,12 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
onSetActiveTabId,
|
onSetActiveTabId,
|
||||||
onSetLastAgentTabId,
|
onSetLastAgentTabId,
|
||||||
onSetOpenDiffs,
|
onSetOpenDiffs,
|
||||||
|
sidebarCollapsed,
|
||||||
|
onToggleSidebar,
|
||||||
|
onSidebarPeekStart,
|
||||||
|
onSidebarPeekEnd,
|
||||||
|
rightSidebarCollapsed,
|
||||||
|
onToggleRightSidebar,
|
||||||
}: {
|
}: {
|
||||||
taskWorkbenchClient: ReturnType<typeof getTaskWorkbenchClient>;
|
taskWorkbenchClient: ReturnType<typeof getTaskWorkbenchClient>;
|
||||||
task: Task;
|
task: Task;
|
||||||
|
|
@ -82,6 +88,12 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
onSetActiveTabId: (tabId: string | null) => void;
|
onSetActiveTabId: (tabId: string | null) => void;
|
||||||
onSetLastAgentTabId: (tabId: string | null) => void;
|
onSetLastAgentTabId: (tabId: string | null) => void;
|
||||||
onSetOpenDiffs: (paths: string[]) => void;
|
onSetOpenDiffs: (paths: string[]) => void;
|
||||||
|
sidebarCollapsed?: boolean;
|
||||||
|
onToggleSidebar?: () => void;
|
||||||
|
onSidebarPeekStart?: () => void;
|
||||||
|
onSidebarPeekEnd?: () => void;
|
||||||
|
rightSidebarCollapsed?: boolean;
|
||||||
|
onToggleRightSidebar?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [defaultModel, setDefaultModel] = useState<ModelId>("claude-sonnet-4");
|
const [defaultModel, setDefaultModel] = useState<ModelId>("claude-sonnet-4");
|
||||||
const [editingField, setEditingField] = useState<"title" | "branch" | null>(null);
|
const [editingField, setEditingField] = useState<"title" | "branch" | null>(null);
|
||||||
|
|
@ -446,6 +458,12 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
setTabUnread(activeAgentTab.id, unread);
|
setTabUnread(activeAgentTab.id, unread);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
sidebarCollapsed={sidebarCollapsed}
|
||||||
|
onToggleSidebar={onToggleSidebar}
|
||||||
|
onSidebarPeekStart={onSidebarPeekStart}
|
||||||
|
onSidebarPeekEnd={onSidebarPeekEnd}
|
||||||
|
rightSidebarCollapsed={rightSidebarCollapsed}
|
||||||
|
onToggleRightSidebar={onToggleRightSidebar}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -456,11 +474,10 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
backgroundColor: "#09090b",
|
backgroundColor: "#09090b",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
borderTopLeftRadius: "12px",
|
borderTopLeftRadius: "12px",
|
||||||
|
borderTopRightRadius: rightSidebarCollapsed ? "12px" : 0,
|
||||||
borderBottomLeftRadius: "24px",
|
borderBottomLeftRadius: "24px",
|
||||||
borderLeft: "1px solid rgba(255, 255, 255, 0.10)",
|
borderBottomRightRadius: rightSidebarCollapsed ? "24px" : 0,
|
||||||
borderRight: "1px solid rgba(255, 255, 255, 0.10)",
|
border: "1px solid rgba(255, 255, 255, 0.10)",
|
||||||
borderTop: "1px solid rgba(255, 255, 255, 0.10)",
|
|
||||||
borderBottom: "1px solid rgba(255, 255, 255, 0.10)",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TabStrip
|
<TabStrip
|
||||||
|
|
@ -478,6 +495,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
||||||
onCloseTab={closeTab}
|
onCloseTab={closeTab}
|
||||||
onCloseDiffTab={closeDiffTab}
|
onCloseDiffTab={closeDiffTab}
|
||||||
onAddTab={addTab}
|
onAddTab={addTab}
|
||||||
|
sidebarCollapsed={sidebarCollapsed}
|
||||||
/>
|
/>
|
||||||
{activeDiff ? (
|
{activeDiff ? (
|
||||||
<DiffContent
|
<DiffContent
|
||||||
|
|
@ -873,6 +891,7 @@ function MockWorkspaceOrgBar() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: MockLayoutProps) {
|
export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: MockLayoutProps) {
|
||||||
|
const [css] = useStyletron();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const taskWorkbenchClient = useMemo(() => getTaskWorkbenchClient(workspaceId), [workspaceId]);
|
const taskWorkbenchClient = useMemo(() => getTaskWorkbenchClient(workspaceId), [workspaceId]);
|
||||||
const viewModel = useSyncExternalStore(
|
const viewModel = useSyncExternalStore(
|
||||||
|
|
@ -912,6 +931,17 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
const autoCreatingSessionForTaskRef = useRef<Set<string>>(new Set());
|
const autoCreatingSessionForTaskRef = useRef<Set<string>>(new Set());
|
||||||
const [leftSidebarOpen, setLeftSidebarOpen] = useState(true);
|
const [leftSidebarOpen, setLeftSidebarOpen] = useState(true);
|
||||||
const [rightSidebarOpen, setRightSidebarOpen] = useState(true);
|
const [rightSidebarOpen, setRightSidebarOpen] = useState(true);
|
||||||
|
const [leftSidebarPeeking, setLeftSidebarPeeking] = useState(false);
|
||||||
|
const peekTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
const startPeek = useCallback(() => {
|
||||||
|
if (peekTimeoutRef.current) clearTimeout(peekTimeoutRef.current);
|
||||||
|
setLeftSidebarPeeking(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const endPeek = useCallback(() => {
|
||||||
|
peekTimeoutRef.current = setTimeout(() => setLeftSidebarPeeking(false), 200);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
leftWidthRef.current = leftWidth;
|
leftWidthRef.current = leftWidth;
|
||||||
|
|
@ -1212,154 +1242,6 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
[activeTask, lastAgentTabIdByTask],
|
[activeTask, lastAgentTabIdByTask],
|
||||||
);
|
);
|
||||||
|
|
||||||
const dismissStarRepoPrompt = useCallback(() => {
|
|
||||||
setStarRepoError(null);
|
|
||||||
try {
|
|
||||||
globalThis.localStorage?.setItem(STAR_SANDBOX_AGENT_REPO_STORAGE_KEY, "dismissed");
|
|
||||||
} catch {
|
|
||||||
// ignore storage failures
|
|
||||||
}
|
|
||||||
setStarRepoPromptOpen(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const starSandboxAgentRepo = useCallback(() => {
|
|
||||||
setStarRepoPending(true);
|
|
||||||
setStarRepoError(null);
|
|
||||||
void backendClient
|
|
||||||
.starSandboxAgentRepo(workspaceId)
|
|
||||||
.then(() => {
|
|
||||||
try {
|
|
||||||
globalThis.localStorage?.setItem(STAR_SANDBOX_AGENT_REPO_STORAGE_KEY, "completed");
|
|
||||||
} catch {
|
|
||||||
// ignore storage failures
|
|
||||||
}
|
|
||||||
setStarRepoPromptOpen(false);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setStarRepoError(error instanceof Error ? error.message : String(error));
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setStarRepoPending(false);
|
|
||||||
});
|
|
||||||
}, [workspaceId]);
|
|
||||||
|
|
||||||
const starRepoPrompt = starRepoPromptOpen ? (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
inset: 0,
|
|
||||||
zIndex: 10000,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
padding: "24px",
|
|
||||||
background: "rgba(0, 0, 0, 0.68)",
|
|
||||||
}}
|
|
||||||
data-testid="onboarding-star-repo-modal"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: "min(440px, 100%)",
|
|
||||||
border: "1px solid rgba(255, 255, 255, 0.10)",
|
|
||||||
borderRadius: "12px",
|
|
||||||
background: "rgba(24, 24, 27, 0.98)",
|
|
||||||
backdropFilter: "blur(16px)",
|
|
||||||
boxShadow: "0 24px 64px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.04)",
|
|
||||||
padding: "28px",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "20px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "6px",
|
|
||||||
fontSize: "11px",
|
|
||||||
letterSpacing: "0.06em",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "rgba(255, 255, 255, 0.4)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 130 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect x="2" y="1" width="126" height="126" rx="44" fill="#0F0F0F" />
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M88.0429 44.2658C89.3803 43.625 90.8907 44.1955 91.5731 45.3776C92.2556 46.5596 91.9945 48.1529 90.7709 48.9907L72.3923 62.885C71.8013 63.2262 71.4248 63.7062 71.1029 64.2861C70.781 64.8659 70.5554 65.3922 70.5443 66.0553L67.7403 88.9495C67.521 90.3894 66.4114 91.423 64.9867 91.4576C63.5619 91.4922 62.3731 90.3429 62.24 88.9751L59.3859 66.0642C59.3971 65.4011 59.2126 64.8489 58.8714 64.2579C58.5302 63.6669 58.1442 63.231 57.5643 62.9091L39.15 48.9819C38.032 48.1828 37.6311 46.5786 38.3734 45.362C39.1157 44.1454 40.5656 43.7013 41.9223 44.2314L63.1512 53.2502C63.731 53.5721 64.2996 53.6398 64.9627 53.651C65.6259 53.6622 66.2298 53.5761 66.8208 53.2349L88.0429 44.2658Z"
|
|
||||||
fill="white"
|
|
||||||
/>
|
|
||||||
<rect x="19.25" y="18.25" width="91.5" height="91.5" rx="25.75" stroke="#F0F0F0" strokeWidth="8.5" />
|
|
||||||
</svg>
|
|
||||||
Welcome to Foundry
|
|
||||||
</div>
|
|
||||||
<h2 style={{ margin: 0, fontSize: "18px", fontWeight: 500, lineHeight: 1.3 }}>Support Sandbox Agent</h2>
|
|
||||||
<p style={{ margin: 0, color: "rgba(255, 255, 255, 0.55)", fontSize: "13px", lineHeight: 1.6 }}>
|
|
||||||
Star the repo to help us grow and stay up to date with new releases.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{starRepoError ? (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
borderRadius: "8px",
|
|
||||||
border: "1px solid rgba(255, 110, 110, 0.24)",
|
|
||||||
background: "rgba(255, 110, 110, 0.06)",
|
|
||||||
padding: "10px 12px",
|
|
||||||
color: "#ff9b9b",
|
|
||||||
fontSize: "12px",
|
|
||||||
}}
|
|
||||||
data-testid="onboarding-star-repo-error"
|
|
||||||
>
|
|
||||||
{starRepoError}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div style={{ display: "flex", justifyContent: "flex-end", gap: "8px" }}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={dismissStarRepoPrompt}
|
|
||||||
style={{
|
|
||||||
border: "1px solid rgba(255, 255, 255, 0.10)",
|
|
||||||
borderRadius: "6px",
|
|
||||||
padding: "8px 14px",
|
|
||||||
background: "rgba(255, 255, 255, 0.05)",
|
|
||||||
color: "rgba(255, 255, 255, 0.7)",
|
|
||||||
cursor: "pointer",
|
|
||||||
fontSize: "12px",
|
|
||||||
fontWeight: 500,
|
|
||||||
transition: "all 160ms ease",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Maybe later
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={starSandboxAgentRepo}
|
|
||||||
disabled={starRepoPending}
|
|
||||||
style={{
|
|
||||||
border: 0,
|
|
||||||
borderRadius: "6px",
|
|
||||||
padding: "8px 14px",
|
|
||||||
background: starRepoPending ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.12)",
|
|
||||||
color: "#ffffff",
|
|
||||||
cursor: starRepoPending ? "progress" : "pointer",
|
|
||||||
fontSize: "12px",
|
|
||||||
fontWeight: 600,
|
|
||||||
transition: "all 160ms ease",
|
|
||||||
}}
|
|
||||||
data-testid="onboarding-star-repo-submit"
|
|
||||||
>
|
|
||||||
{starRepoPending ? "Starring..." : "Star the repo"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
const isDesktop = !!import.meta.env.VITE_DESKTOP;
|
const isDesktop = !!import.meta.env.VITE_DESKTOP;
|
||||||
const onDragMouseDown = useCallback((event: ReactPointerEvent) => {
|
const onDragMouseDown = useCallback((event: ReactPointerEvent) => {
|
||||||
if (event.button !== 0) return;
|
if (event.button !== 0) return;
|
||||||
|
|
@ -1397,7 +1279,7 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
const collapsedToggleStyle: React.CSSProperties = {
|
const collapsedToggleClass = css({
|
||||||
width: "26px",
|
width: "26px",
|
||||||
height: "26px",
|
height: "26px",
|
||||||
borderRadius: "6px",
|
borderRadius: "6px",
|
||||||
|
|
@ -1409,7 +1291,8 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
position: "relative",
|
position: "relative",
|
||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
};
|
":hover": { color: "#a1a1aa", backgroundColor: "rgba(255, 255, 255, 0.06)" },
|
||||||
|
});
|
||||||
|
|
||||||
const sidebarTransition = "width 200ms ease";
|
const sidebarTransition = "width 200ms ease";
|
||||||
const contentFrameStyle: React.CSSProperties = {
|
const contentFrameStyle: React.CSSProperties = {
|
||||||
|
|
@ -1420,6 +1303,7 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
marginBottom: "8px",
|
marginBottom: "8px",
|
||||||
marginRight: "8px",
|
marginRight: "8px",
|
||||||
|
marginLeft: leftSidebarOpen ? 0 : "8px",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!activeTask) {
|
if (!activeTask) {
|
||||||
|
|
@ -1455,16 +1339,24 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{leftSidebarOpen ? null : (
|
|
||||||
<div style={{ flexShrink: 0, padding: "6px 4px 0 6px", paddingTop: isDesktop ? "38px" : "6px" }}>
|
|
||||||
<div style={collapsedToggleStyle} onClick={() => setLeftSidebarOpen(true)}>
|
|
||||||
<PanelLeft size={16} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div style={contentFrameStyle}>
|
<div style={contentFrameStyle}>
|
||||||
{leftSidebarOpen ? <PanelResizeHandle onResizeStart={onLeftResizeStart} onResize={onLeftResize} /> : null}
|
{leftSidebarOpen ? <PanelResizeHandle onResizeStart={onLeftResizeStart} onResize={onLeftResize} /> : null}
|
||||||
<SPanel $style={{ backgroundColor: "#09090b", flex: 1, minWidth: 0 }}>
|
<SPanel $style={{ backgroundColor: "#09090b", flex: 1, minWidth: 0 }}>
|
||||||
|
{!leftSidebarOpen || !rightSidebarOpen ? (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", padding: "8px 8px 0 8px" }}>
|
||||||
|
{leftSidebarOpen ? null : (
|
||||||
|
<div className={collapsedToggleClass} onClick={() => setLeftSidebarOpen(true)}>
|
||||||
|
<PanelLeft size={14} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ flex: 1 }} />
|
||||||
|
{rightSidebarOpen ? null : (
|
||||||
|
<div className={collapsedToggleClass} onClick={() => setRightSidebarOpen(true)}>
|
||||||
|
<PanelRight size={14} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<ScrollBody>
|
<ScrollBody>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -1528,13 +1420,6 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{rightSidebarOpen ? null : (
|
|
||||||
<div style={{ flexShrink: 0, padding: "6px 6px 0 4px" }}>
|
|
||||||
<div style={collapsedToggleStyle} onClick={() => setRightSidebarOpen(true)}>
|
|
||||||
<PanelRight size={16} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Shell>
|
</Shell>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
@ -1543,7 +1428,7 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{dragRegion}
|
{dragRegion}
|
||||||
<Shell>
|
<Shell $style={{ position: "relative" }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: leftSidebarOpen ? `${leftWidth}px` : 0,
|
width: leftSidebarOpen ? `${leftWidth}px` : 0,
|
||||||
|
|
@ -1572,13 +1457,59 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{leftSidebarOpen ? null : (
|
{!leftSidebarOpen && leftSidebarPeeking ? (
|
||||||
<div style={{ flexShrink: 0, padding: "6px 4px 0 6px", paddingTop: isDesktop ? "38px" : "6px" }}>
|
<>
|
||||||
<div style={collapsedToggleStyle} onClick={() => setLeftSidebarOpen(true)}>
|
<div
|
||||||
<PanelLeft size={16} />
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.4)",
|
||||||
|
zIndex: 99,
|
||||||
|
}}
|
||||||
|
onClick={() => setLeftSidebarPeeking(false)}
|
||||||
|
onMouseEnter={endPeek}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: `${leftWidth}px`,
|
||||||
|
zIndex: 100,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
boxShadow: "4px 0 24px rgba(0, 0, 0, 0.5)",
|
||||||
|
}}
|
||||||
|
onMouseEnter={startPeek}
|
||||||
|
onMouseLeave={endPeek}
|
||||||
|
>
|
||||||
|
<Sidebar
|
||||||
|
projects={projects}
|
||||||
|
newTaskRepos={viewModel.repos}
|
||||||
|
selectedNewTaskRepoId={selectedNewTaskRepoId}
|
||||||
|
activeId={activeTask.id}
|
||||||
|
onSelect={(id) => {
|
||||||
|
selectTask(id);
|
||||||
|
setLeftSidebarPeeking(false);
|
||||||
|
}}
|
||||||
|
onCreate={createTask}
|
||||||
|
onSelectNewTaskRepo={setSelectedNewTaskRepoId}
|
||||||
|
onMarkUnread={markTaskUnread}
|
||||||
|
onRenameTask={renameTask}
|
||||||
|
onRenameBranch={renameBranch}
|
||||||
|
onReorderProjects={reorderProjects}
|
||||||
|
onToggleSidebar={() => {
|
||||||
|
setLeftSidebarPeeking(false);
|
||||||
|
setLeftSidebarOpen(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)}
|
) : null}
|
||||||
<div style={contentFrameStyle}>
|
<div style={contentFrameStyle}>
|
||||||
{leftSidebarOpen ? <PanelResizeHandle onResizeStart={onLeftResizeStart} onResize={onLeftResize} /> : null}
|
{leftSidebarOpen ? <PanelResizeHandle onResizeStart={onLeftResizeStart} onResize={onLeftResize} /> : null}
|
||||||
<div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column" }}>
|
<div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column" }}>
|
||||||
|
|
@ -1598,6 +1529,15 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
onSetOpenDiffs={(paths) => {
|
onSetOpenDiffs={(paths) => {
|
||||||
setOpenDiffsByTask((current) => ({ ...current, [activeTask.id]: paths }));
|
setOpenDiffsByTask((current) => ({ ...current, [activeTask.id]: paths }));
|
||||||
}}
|
}}
|
||||||
|
sidebarCollapsed={!leftSidebarOpen}
|
||||||
|
onToggleSidebar={() => {
|
||||||
|
setLeftSidebarPeeking(false);
|
||||||
|
setLeftSidebarOpen(true);
|
||||||
|
}}
|
||||||
|
onSidebarPeekStart={startPeek}
|
||||||
|
onSidebarPeekEnd={endPeek}
|
||||||
|
rightSidebarCollapsed={!rightSidebarOpen}
|
||||||
|
onToggleRightSidebar={() => setRightSidebarOpen(true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{rightSidebarOpen ? <PanelResizeHandle onResizeStart={onRightResizeStart} onResize={onRightResize} /> : null}
|
{rightSidebarOpen ? <PanelResizeHandle onResizeStart={onRightResizeStart} onResize={onRightResize} /> : null}
|
||||||
|
|
@ -1626,13 +1566,6 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{rightSidebarOpen ? null : (
|
|
||||||
<div style={{ flexShrink: 0, padding: "6px 6px 0 4px" }}>
|
|
||||||
<div style={collapsedToggleStyle} onClick={() => setRightSidebarOpen(true)}>
|
|
||||||
<PanelRight size={16} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Shell>
|
</Shell>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,16 @@ export const RightSidebar = memo(function RightSidebar({
|
||||||
const contextMenu = useContextMenu();
|
const contextMenu = useContextMenu();
|
||||||
const changedPaths = useMemo(() => new Set(task.fileChanges.map((file) => file.path)), [task.fileChanges]);
|
const changedPaths = useMemo(() => new Set(task.fileChanges.map((file) => file.path)), [task.fileChanges]);
|
||||||
const isTerminal = task.status === "archived";
|
const isTerminal = task.status === "archived";
|
||||||
|
const [compact, setCompact] = useState(false);
|
||||||
|
const headerRef = useCallback((node: HTMLDivElement | null) => {
|
||||||
|
if (!node) return;
|
||||||
|
const observer = new ResizeObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
setCompact(entry.contentRect.width < 400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(node);
|
||||||
|
}, []);
|
||||||
const pullRequestUrl = task.pullRequest != null ? `https://github.com/${task.repoName}/pull/${task.pullRequest.number}` : null;
|
const pullRequestUrl = task.pullRequest != null ? `https://github.com/${task.repoName}/pull/${task.pullRequest.number}` : null;
|
||||||
|
|
||||||
const copyFilePath = useCallback(async (path: string) => {
|
const copyFilePath = useCallback(async (path: string) => {
|
||||||
|
|
@ -137,121 +147,128 @@ export const RightSidebar = memo(function RightSidebar({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SPanel $style={{ backgroundColor: "#09090b" }}>
|
<SPanel $style={{ backgroundColor: "#09090b", minWidth: 0 }}>
|
||||||
<PanelHeaderBar $style={{ backgroundColor: "#0f0f11", borderBottom: "none" }}>
|
<PanelHeaderBar $style={{ backgroundColor: "#0f0f11", borderBottom: "none", overflow: "hidden" }}>
|
||||||
<div className={css({ flex: 1 })} />
|
<div ref={headerRef} className={css({ display: "flex", alignItems: "center", flex: 1, minWidth: 0, justifyContent: "flex-end", gap: "2px" })}>
|
||||||
{!isTerminal ? (
|
{!isTerminal ? (
|
||||||
<div className={css({ display: "flex", alignItems: "center", gap: "4px" })}>
|
<div className={css({ display: "flex", alignItems: "center", gap: "2px", flexShrink: 1, minWidth: 0 })}>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (pullRequestUrl) {
|
if (pullRequestUrl) {
|
||||||
window.open(pullRequestUrl, "_blank", "noopener,noreferrer");
|
window.open(pullRequestUrl, "_blank", "noopener,noreferrer");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onPublishPr();
|
onPublishPr();
|
||||||
|
}}
|
||||||
|
className={css({
|
||||||
|
appearance: "none",
|
||||||
|
WebkitAppearance: "none",
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
margin: "0",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "5px",
|
||||||
|
padding: compact ? "4px 6px" : "4px 10px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
fontSize: "11px",
|
||||||
|
fontWeight: 500,
|
||||||
|
lineHeight: 1,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
flexShrink: 0,
|
||||||
|
color: theme.colors.contentSecondary,
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: "all 200ms ease",
|
||||||
|
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: theme.colors.contentPrimary },
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<GitPullRequest size={12} style={{ flexShrink: 0 }} />
|
||||||
|
{!compact && <span>{pullRequestUrl ? "Open PR" : "Publish PR"}</span>}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={css({
|
||||||
|
appearance: "none",
|
||||||
|
WebkitAppearance: "none",
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
margin: "0",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "5px",
|
||||||
|
padding: compact ? "4px 6px" : "4px 10px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
fontSize: "11px",
|
||||||
|
fontWeight: 500,
|
||||||
|
lineHeight: 1,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
flexShrink: 0,
|
||||||
|
color: theme.colors.contentSecondary,
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: "all 200ms ease",
|
||||||
|
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: theme.colors.contentPrimary },
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<ArrowUpFromLine size={12} style={{ flexShrink: 0 }} />
|
||||||
|
{!compact && <span>Push</span>}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onArchive}
|
||||||
|
className={css({
|
||||||
|
appearance: "none",
|
||||||
|
WebkitAppearance: "none",
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
margin: "0",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "5px",
|
||||||
|
padding: compact ? "4px 6px" : "4px 10px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
fontSize: "11px",
|
||||||
|
fontWeight: 500,
|
||||||
|
lineHeight: 1,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
flexShrink: 0,
|
||||||
|
color: theme.colors.contentSecondary,
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: "all 200ms ease",
|
||||||
|
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: theme.colors.contentPrimary },
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Archive size={12} style={{ flexShrink: 0 }} />
|
||||||
|
{!compact && <span>Archive</span>}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{onToggleSidebar ? (
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={onToggleSidebar}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") onToggleSidebar();
|
||||||
}}
|
}}
|
||||||
className={css({
|
className={css({
|
||||||
appearance: "none",
|
width: "26px",
|
||||||
WebkitAppearance: "none",
|
height: "26px",
|
||||||
background: "none",
|
borderRadius: "6px",
|
||||||
border: "none",
|
color: "#71717a",
|
||||||
margin: "0",
|
|
||||||
boxSizing: "border-box",
|
|
||||||
display: "inline-flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "6px",
|
|
||||||
padding: "6px 12px",
|
|
||||||
borderRadius: "8px",
|
|
||||||
fontSize: "12px",
|
|
||||||
fontWeight: 500,
|
|
||||||
lineHeight: 1,
|
|
||||||
color: "#e4e4e7",
|
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
transition: "all 200ms ease",
|
display: "flex",
|
||||||
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: "#ffffff" },
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
flexShrink: 0,
|
||||||
|
":hover": { color: "#a1a1aa", backgroundColor: "rgba(255, 255, 255, 0.06)" },
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<GitPullRequest size={12} style={{ flexShrink: 0 }} />
|
<PanelRight size={14} />
|
||||||
<span className={css({ "@media screen and (max-width: 768px)": { display: "none" } })}>{pullRequestUrl ? "Open PR" : "Publish PR"}</span>
|
</div>
|
||||||
</button>
|
) : null}
|
||||||
<button
|
</div>
|
||||||
className={css({
|
|
||||||
appearance: "none",
|
|
||||||
WebkitAppearance: "none",
|
|
||||||
background: "none",
|
|
||||||
border: "none",
|
|
||||||
margin: "0",
|
|
||||||
boxSizing: "border-box",
|
|
||||||
display: "inline-flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "6px",
|
|
||||||
padding: "6px 12px",
|
|
||||||
borderRadius: "8px",
|
|
||||||
fontSize: "12px",
|
|
||||||
fontWeight: 500,
|
|
||||||
lineHeight: 1,
|
|
||||||
color: "#e4e4e7",
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "all 200ms ease",
|
|
||||||
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: "#ffffff" },
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<ArrowUpFromLine size={12} style={{ flexShrink: 0 }} />{" "}
|
|
||||||
<span className={css({ "@media screen and (max-width: 768px)": { display: "none" } })}>Push</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={onArchive}
|
|
||||||
className={css({
|
|
||||||
appearance: "none",
|
|
||||||
WebkitAppearance: "none",
|
|
||||||
background: "none",
|
|
||||||
border: "none",
|
|
||||||
margin: "0",
|
|
||||||
boxSizing: "border-box",
|
|
||||||
display: "inline-flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "6px",
|
|
||||||
padding: "6px 12px",
|
|
||||||
borderRadius: "8px",
|
|
||||||
fontSize: "12px",
|
|
||||||
fontWeight: 500,
|
|
||||||
lineHeight: 1,
|
|
||||||
color: "#e4e4e7",
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "all 200ms ease",
|
|
||||||
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: "#ffffff" },
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Archive size={12} style={{ flexShrink: 0 }} />{" "}
|
|
||||||
<span className={css({ "@media screen and (max-width: 768px)": { display: "none" } })}>Archive</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{onToggleSidebar ? (
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={onToggleSidebar}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === "Enter" || event.key === " ") onToggleSidebar();
|
|
||||||
}}
|
|
||||||
className={css({
|
|
||||||
width: "26px",
|
|
||||||
height: "26px",
|
|
||||||
borderRadius: "6px",
|
|
||||||
color: "#71717a",
|
|
||||||
cursor: "pointer",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
flexShrink: 0,
|
|
||||||
":hover": { color: "#a1a1aa", backgroundColor: "rgba(255, 255, 255, 0.06)" },
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<PanelRight size={14} />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</PanelHeaderBar>
|
</PanelHeaderBar>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,26 @@
|
||||||
import { memo, useRef, useState } from "react";
|
import { memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { useStyletron } from "baseui";
|
import { useStyletron } from "baseui";
|
||||||
import { LabelSmall, LabelXSmall } from "baseui/typography";
|
import { LabelSmall, LabelXSmall } from "baseui/typography";
|
||||||
import { ChevronDown, ChevronUp, CloudUpload, GitPullRequestDraft, ListChecks, PanelLeft, Plus } from "lucide-react";
|
import {
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
ChevronUp,
|
||||||
|
CloudUpload,
|
||||||
|
CreditCard,
|
||||||
|
GitPullRequestDraft,
|
||||||
|
ListChecks,
|
||||||
|
LogOut,
|
||||||
|
PanelLeft,
|
||||||
|
Plus,
|
||||||
|
Settings,
|
||||||
|
User,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
import { formatRelativeAge, type Task, type ProjectSection } from "./view-model";
|
import { formatRelativeAge, type Task, type ProjectSection } from "./view-model";
|
||||||
import { ContextMenuOverlay, TaskIndicator, PanelHeaderBar, SPanel, ScrollBody, useContextMenu } from "./ui";
|
import { ContextMenuOverlay, TaskIndicator, PanelHeaderBar, SPanel, ScrollBody, useContextMenu } from "./ui";
|
||||||
|
import { activeMockOrganization, eligibleOrganizations, useMockAppClient, useMockAppSnapshot } from "../../lib/mock-app";
|
||||||
|
|
||||||
const PROJECT_COLORS = ["#6366f1", "#f59e0b", "#10b981", "#ef4444", "#8b5cf6", "#ec4899", "#06b6d4", "#f97316"];
|
const PROJECT_COLORS = ["#6366f1", "#f59e0b", "#10b981", "#ef4444", "#8b5cf6", "#ec4899", "#06b6d4", "#f97316"];
|
||||||
|
|
||||||
|
|
@ -400,7 +416,343 @@ export const Sidebar = memo(function Sidebar({
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</ScrollBody>
|
</ScrollBody>
|
||||||
|
<SidebarFooter />
|
||||||
{contextMenu.menu ? <ContextMenuOverlay menu={contextMenu.menu} onClose={contextMenu.close} /> : null}
|
{contextMenu.menu ? <ContextMenuOverlay menu={contextMenu.menu} onClose={contextMenu.close} /> : null}
|
||||||
</SPanel>
|
</SPanel>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const menuButtonStyle = (highlight: boolean) =>
|
||||||
|
({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
width: "100%",
|
||||||
|
padding: "8px 12px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
border: "none",
|
||||||
|
background: highlight ? "rgba(255, 255, 255, 0.06)" : "transparent",
|
||||||
|
color: "rgba(255, 255, 255, 0.75)",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "13px",
|
||||||
|
fontWeight: 400 as const,
|
||||||
|
textAlign: "left" as const,
|
||||||
|
transition: "background 120ms ease, color 120ms ease",
|
||||||
|
}) satisfies React.CSSProperties;
|
||||||
|
|
||||||
|
function SidebarFooter() {
|
||||||
|
const [css] = useStyletron();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const client = useMockAppClient();
|
||||||
|
const snapshot = useMockAppSnapshot();
|
||||||
|
const organization = activeMockOrganization(snapshot);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [workspaceFlyoutOpen, setWorkspaceFlyoutOpen] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const flyoutTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const hoverTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const workspaceTriggerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const flyoutRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [flyoutPos, setFlyoutPos] = useState<{ top: number; left: number } | null>(null);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (workspaceFlyoutOpen && workspaceTriggerRef.current) {
|
||||||
|
const rect = workspaceTriggerRef.current.getBoundingClientRect();
|
||||||
|
setFlyoutPos({ top: rect.top, left: rect.right + 4 });
|
||||||
|
}
|
||||||
|
}, [workspaceFlyoutOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
function handleClick(event: MouseEvent) {
|
||||||
|
const target = event.target as Node;
|
||||||
|
const inContainer = containerRef.current?.contains(target);
|
||||||
|
const inFlyout = flyoutRef.current?.contains(target);
|
||||||
|
if (!inContainer && !inFlyout) {
|
||||||
|
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
||||||
|
setOpen(false);
|
||||||
|
setWorkspaceFlyoutOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener("mousedown", handleClick);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClick);
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const switchToOrg = useCallback(
|
||||||
|
(org: (typeof snapshot.organizations)[number]) => {
|
||||||
|
setOpen(false);
|
||||||
|
setWorkspaceFlyoutOpen(false);
|
||||||
|
void (async () => {
|
||||||
|
await client.selectOrganization(org.id);
|
||||||
|
await navigate({ to: `/workspaces/${org.workspaceId}` as never });
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
[client, navigate],
|
||||||
|
);
|
||||||
|
|
||||||
|
const openFlyout = useCallback(() => {
|
||||||
|
if (flyoutTimerRef.current) clearTimeout(flyoutTimerRef.current);
|
||||||
|
setWorkspaceFlyoutOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeFlyout = useCallback(() => {
|
||||||
|
flyoutTimerRef.current = setTimeout(() => setWorkspaceFlyoutOpen(false), 150);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const menuItems: Array<{ icon: React.ReactNode; label: string; danger?: boolean; onClick: () => void }> = [];
|
||||||
|
|
||||||
|
if (organization) {
|
||||||
|
menuItems.push(
|
||||||
|
{
|
||||||
|
icon: <Settings size={14} />,
|
||||||
|
label: "Settings",
|
||||||
|
onClick: () => {
|
||||||
|
setOpen(false);
|
||||||
|
void navigate({ to: "/organizations/$organizationId/settings" as never, params: { organizationId: organization.id } as never });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <CreditCard size={14} />,
|
||||||
|
label: "Billing",
|
||||||
|
onClick: () => {
|
||||||
|
setOpen(false);
|
||||||
|
void navigate({ to: "/organizations/$organizationId/billing" as never, params: { organizationId: organization.id } as never });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
menuItems.push(
|
||||||
|
{
|
||||||
|
icon: <User size={14} />,
|
||||||
|
label: "Account",
|
||||||
|
onClick: () => {
|
||||||
|
setOpen(false);
|
||||||
|
void navigate({ to: "/account" as never });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <LogOut size={14} />,
|
||||||
|
label: "Sign Out",
|
||||||
|
danger: true,
|
||||||
|
onClick: () => {
|
||||||
|
setOpen(false);
|
||||||
|
void (async () => {
|
||||||
|
await client.signOut();
|
||||||
|
await navigate({ to: "/signin" });
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const popoverStyle = css({
|
||||||
|
borderRadius: "10px",
|
||||||
|
border: "1px solid rgba(255, 255, 255, 0.10)",
|
||||||
|
backgroundColor: "#18181b",
|
||||||
|
boxShadow: "0 12px 40px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.04)",
|
||||||
|
padding: "4px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "2px",
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
||||||
|
hoverTimerRef.current = setTimeout(() => setOpen(true), 300);
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
||||||
|
hoverTimerRef.current = setTimeout(() => {
|
||||||
|
setOpen(false);
|
||||||
|
setWorkspaceFlyoutOpen(false);
|
||||||
|
}, 200);
|
||||||
|
}}
|
||||||
|
className={css({ position: "relative", flexShrink: 0 })}
|
||||||
|
>
|
||||||
|
{open ? (
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
position: "absolute",
|
||||||
|
bottom: "100%",
|
||||||
|
left: "8px",
|
||||||
|
right: "8px",
|
||||||
|
marginBottom: "4px",
|
||||||
|
zIndex: 9999,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className={popoverStyle}>
|
||||||
|
{/* Workspace flyout trigger */}
|
||||||
|
{organization ? (
|
||||||
|
<div ref={workspaceTriggerRef} onMouseEnter={openFlyout} onMouseLeave={closeFlyout}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setWorkspaceFlyoutOpen((prev) => !prev)}
|
||||||
|
className={css({
|
||||||
|
...menuButtonStyle(workspaceFlyoutOpen),
|
||||||
|
fontWeight: 500,
|
||||||
|
":hover": {
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
||||||
|
color: "#ffffff",
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={css({
|
||||||
|
width: "18px",
|
||||||
|
height: "18px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
background: `linear-gradient(135deg, ${projectIconColor(organization.settings.displayName)}, ${projectIconColor(organization.settings.displayName + "x")})`,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: "9px",
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "#ffffff",
|
||||||
|
flexShrink: 0,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{organization.settings.displayName.charAt(0).toUpperCase()}
|
||||||
|
</span>
|
||||||
|
<span className={css({ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" })}>
|
||||||
|
{organization.settings.displayName}
|
||||||
|
</span>
|
||||||
|
<ChevronRight size={12} className={css({ flexShrink: 0, color: "rgba(255, 255, 255, 0.35)" })} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* Workspace flyout portal */}
|
||||||
|
{workspaceFlyoutOpen && organization && flyoutPos
|
||||||
|
? createPortal(
|
||||||
|
<div
|
||||||
|
ref={flyoutRef}
|
||||||
|
className={css({
|
||||||
|
position: "fixed",
|
||||||
|
top: `${flyoutPos.top}px`,
|
||||||
|
left: `${flyoutPos.left}px`,
|
||||||
|
minWidth: "200px",
|
||||||
|
zIndex: 10000,
|
||||||
|
})}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
openFlyout();
|
||||||
|
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
closeFlyout();
|
||||||
|
hoverTimerRef.current = setTimeout(() => {
|
||||||
|
setOpen(false);
|
||||||
|
setWorkspaceFlyoutOpen(false);
|
||||||
|
}, 200);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={popoverStyle}>
|
||||||
|
{eligibleOrganizations(snapshot).map((org) => {
|
||||||
|
const isActive = organization.id === org.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={org.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (!isActive) switchToOrg(org);
|
||||||
|
else {
|
||||||
|
setOpen(false);
|
||||||
|
setWorkspaceFlyoutOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={css({
|
||||||
|
...menuButtonStyle(isActive),
|
||||||
|
fontWeight: isActive ? 600 : 400,
|
||||||
|
color: isActive ? "#ffffff" : "rgba(255, 255, 255, 0.65)",
|
||||||
|
":hover": {
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
||||||
|
color: "#ffffff",
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={css({
|
||||||
|
width: "18px",
|
||||||
|
height: "18px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
background: `linear-gradient(135deg, ${projectIconColor(org.settings.displayName)}, ${projectIconColor(org.settings.displayName + "x")})`,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: "9px",
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "#ffffff",
|
||||||
|
flexShrink: 0,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{org.settings.displayName.charAt(0).toUpperCase()}
|
||||||
|
</span>
|
||||||
|
<span className={css({ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" })}>
|
||||||
|
{org.settings.displayName}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
|
||||||
|
{menuItems.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item.label}
|
||||||
|
type="button"
|
||||||
|
onClick={item.onClick}
|
||||||
|
className={css({
|
||||||
|
...menuButtonStyle(false),
|
||||||
|
color: item.danger ? "#ffa198" : "rgba(255, 255, 255, 0.75)",
|
||||||
|
":hover": {
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
||||||
|
color: item.danger ? "#ff6b6b" : "#ffffff",
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className={css({ padding: "8px" })}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
||||||
|
setOpen((prev) => {
|
||||||
|
if (prev) setWorkspaceFlyoutOpen(false);
|
||||||
|
return !prev;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className={css({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "28px",
|
||||||
|
height: "28px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
border: "none",
|
||||||
|
background: open ? "rgba(255, 255, 255, 0.06)" : "transparent",
|
||||||
|
color: open ? "#ffffff" : "#71717a",
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: "all 160ms ease",
|
||||||
|
":hover": {
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
||||||
|
color: "#a1a1aa",
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Settings size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ export const TabStrip = memo(function TabStrip({
|
||||||
onCloseTab,
|
onCloseTab,
|
||||||
onCloseDiffTab,
|
onCloseDiffTab,
|
||||||
onAddTab,
|
onAddTab,
|
||||||
|
sidebarCollapsed,
|
||||||
}: {
|
}: {
|
||||||
task: Task;
|
task: Task;
|
||||||
activeTabId: string | null;
|
activeTabId: string | null;
|
||||||
|
|
@ -36,8 +37,10 @@ export const TabStrip = memo(function TabStrip({
|
||||||
onCloseTab: (tabId: string) => void;
|
onCloseTab: (tabId: string) => void;
|
||||||
onCloseDiffTab: (path: string) => void;
|
onCloseDiffTab: (path: string) => void;
|
||||||
onAddTab: () => void;
|
onAddTab: () => void;
|
||||||
|
sidebarCollapsed?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [css, theme] = useStyletron();
|
const [css, theme] = useStyletron();
|
||||||
|
const isDesktop = !!import.meta.env.VITE_DESKTOP;
|
||||||
const contextMenu = useContextMenu();
|
const contextMenu = useContextMenu();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -53,7 +56,7 @@ export const TabStrip = memo(function TabStrip({
|
||||||
borderBottom: `1px solid ${theme.colors.borderOpaque}`,
|
borderBottom: `1px solid ${theme.colors.borderOpaque}`,
|
||||||
gap: "4px",
|
gap: "4px",
|
||||||
backgroundColor: "#09090b",
|
backgroundColor: "#09090b",
|
||||||
paddingLeft: "6px",
|
paddingLeft: sidebarCollapsed ? "14px" : "6px",
|
||||||
height: "41px",
|
height: "41px",
|
||||||
minHeight: "41px",
|
minHeight: "41px",
|
||||||
overflowX: "auto",
|
overflowX: "auto",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { useStyletron } from "baseui";
|
import { useStyletron } from "baseui";
|
||||||
import { LabelSmall } from "baseui/typography";
|
import { LabelSmall } from "baseui/typography";
|
||||||
import { Clock, MailOpen } from "lucide-react";
|
import { Clock, MailOpen, PanelLeft, PanelRight } from "lucide-react";
|
||||||
|
|
||||||
import { PanelHeaderBar } from "./ui";
|
import { PanelHeaderBar } from "./ui";
|
||||||
import { type AgentTab, type Task } from "./view-model";
|
import { type AgentTab, type Task } from "./view-model";
|
||||||
|
|
@ -16,6 +16,12 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
||||||
onCommitEditingField,
|
onCommitEditingField,
|
||||||
onCancelEditingField,
|
onCancelEditingField,
|
||||||
onSetActiveTabUnread,
|
onSetActiveTabUnread,
|
||||||
|
sidebarCollapsed,
|
||||||
|
onToggleSidebar,
|
||||||
|
onSidebarPeekStart,
|
||||||
|
onSidebarPeekEnd,
|
||||||
|
rightSidebarCollapsed,
|
||||||
|
onToggleRightSidebar,
|
||||||
}: {
|
}: {
|
||||||
task: Task;
|
task: Task;
|
||||||
activeTab: AgentTab | null | undefined;
|
activeTab: AgentTab | null | undefined;
|
||||||
|
|
@ -26,11 +32,40 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
||||||
onCommitEditingField: (field: "title" | "branch") => void;
|
onCommitEditingField: (field: "title" | "branch") => void;
|
||||||
onCancelEditingField: () => void;
|
onCancelEditingField: () => void;
|
||||||
onSetActiveTabUnread: (unread: boolean) => void;
|
onSetActiveTabUnread: (unread: boolean) => void;
|
||||||
|
sidebarCollapsed?: boolean;
|
||||||
|
onToggleSidebar?: () => void;
|
||||||
|
onSidebarPeekStart?: () => void;
|
||||||
|
onSidebarPeekEnd?: () => void;
|
||||||
|
rightSidebarCollapsed?: boolean;
|
||||||
|
onToggleRightSidebar?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [css, theme] = useStyletron();
|
const [css, theme] = useStyletron();
|
||||||
|
const isDesktop = !!import.meta.env.VITE_DESKTOP;
|
||||||
|
const needsTrafficLightInset = isDesktop && sidebarCollapsed;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelHeaderBar $style={{ backgroundColor: "#0f0f11", borderBottom: "none" }}>
|
<PanelHeaderBar $style={{ backgroundColor: "#0f0f11", borderBottom: "none", paddingLeft: needsTrafficLightInset ? "74px" : "14px" }}>
|
||||||
|
{sidebarCollapsed && onToggleSidebar ? (
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
width: "26px",
|
||||||
|
height: "26px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
cursor: "pointer",
|
||||||
|
color: "#71717a",
|
||||||
|
flexShrink: 0,
|
||||||
|
":hover": { color: "#a1a1aa", backgroundColor: "rgba(255, 255, 255, 0.06)" },
|
||||||
|
})}
|
||||||
|
onClick={onToggleSidebar}
|
||||||
|
onMouseEnter={onSidebarPeekStart}
|
||||||
|
onMouseLeave={onSidebarPeekEnd}
|
||||||
|
>
|
||||||
|
<PanelLeft size={14} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{editingField === "title" ? (
|
{editingField === "title" ? (
|
||||||
<input
|
<input
|
||||||
autoFocus
|
autoFocus
|
||||||
|
|
@ -170,6 +205,25 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
||||||
<span className={css({ "@media screen and (max-width: 768px)": { display: "none" } })}>{activeTab.unread ? "Mark read" : "Mark unread"}</span>
|
<span className={css({ "@media screen and (max-width: 768px)": { display: "none" } })}>{activeTab.unread ? "Mark read" : "Mark unread"}</span>
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
|
{rightSidebarCollapsed && onToggleRightSidebar ? (
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
width: "26px",
|
||||||
|
height: "26px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
cursor: "pointer",
|
||||||
|
color: "#71717a",
|
||||||
|
flexShrink: 0,
|
||||||
|
":hover": { color: "#a1a1aa", backgroundColor: "rgba(255, 255, 255, 0.06)" },
|
||||||
|
})}
|
||||||
|
onClick={onToggleRightSidebar}
|
||||||
|
>
|
||||||
|
<PanelRight size={14} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</PanelHeaderBar>
|
</PanelHeaderBar>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -105,7 +105,7 @@ export function DownloadFoundry() {
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{showDropdown && (
|
{showDropdown && (
|
||||||
<div className="absolute left-1/2 top-full mt-2 -translate-x-1/2 rounded-lg border border-white/10 bg-zinc-900 p-2 shadow-xl">
|
<div className="absolute left-1/2 top-full mt-2 -translate-x-1/2 rounded-lg border border-white/10 bg-[#0f0f11] p-2 shadow-xl">
|
||||||
{secondary.map((p) => (
|
{secondary.map((p) => (
|
||||||
<a
|
<a
|
||||||
key={p.arch}
|
key={p.arch}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export function FeatureGrid() {
|
||||||
className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"
|
className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"
|
||||||
>
|
>
|
||||||
{/* Universal Agent API - Span full width */}
|
{/* Universal Agent API - Span full width */}
|
||||||
<div className="group col-span-full flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
<div className="group col-span-full flex flex-col gap-4 rounded-xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="text-zinc-500 transition-colors group-hover:text-orange-400">
|
<div className="text-zinc-500 transition-colors group-hover:text-orange-400">
|
||||||
<Workflow className="h-4 w-4" />
|
<Workflow className="h-4 w-4" />
|
||||||
|
|
@ -49,7 +49,7 @@ export function FeatureGrid() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Streaming Events */}
|
{/* Streaming Events */}
|
||||||
<div className="group flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
<div className="group flex flex-col gap-4 rounded-xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="text-zinc-500 transition-colors group-hover:text-green-400">
|
<div className="text-zinc-500 transition-colors group-hover:text-green-400">
|
||||||
<Server className="h-4 w-4" />
|
<Server className="h-4 w-4" />
|
||||||
|
|
@ -62,7 +62,7 @@ export function FeatureGrid() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Universal Schema */}
|
{/* Universal Schema */}
|
||||||
<div className="group flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
<div className="group flex flex-col gap-4 rounded-xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="text-zinc-500 transition-colors group-hover:text-purple-400">
|
<div className="text-zinc-500 transition-colors group-hover:text-purple-400">
|
||||||
<Database className="h-4 w-4" />
|
<Database className="h-4 w-4" />
|
||||||
|
|
@ -75,7 +75,7 @@ export function FeatureGrid() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Runs Inside Any Sandbox */}
|
{/* Runs Inside Any Sandbox */}
|
||||||
<div className="group lg:col-span-2 flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
<div className="group lg:col-span-2 flex flex-col gap-4 rounded-xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="text-zinc-500 transition-colors group-hover:text-blue-400">
|
<div className="text-zinc-500 transition-colors group-hover:text-blue-400">
|
||||||
<Globe className="h-4 w-4" />
|
<Globe className="h-4 w-4" />
|
||||||
|
|
@ -88,7 +88,7 @@ export function FeatureGrid() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Session Management */}
|
{/* Session Management */}
|
||||||
<div className="group lg:col-span-2 flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
<div className="group lg:col-span-2 flex flex-col gap-4 rounded-xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="text-zinc-500 transition-colors group-hover:text-amber-400">
|
<div className="text-zinc-500 transition-colors group-hover:text-amber-400">
|
||||||
<Download className="h-4 w-4" />
|
<Download className="h-4 w-4" />
|
||||||
|
|
@ -101,7 +101,7 @@ export function FeatureGrid() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* OpenCode SDK & UI Support */}
|
{/* OpenCode SDK & UI Support */}
|
||||||
<div className="group lg:col-span-2 flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
<div className="group lg:col-span-2 flex flex-col gap-4 rounded-xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="text-zinc-500 transition-colors group-hover:text-pink-400">
|
<div className="text-zinc-500 transition-colors group-hover:text-pink-400">
|
||||||
<Plug className="h-4 w-4" />
|
<Plug className="h-4 w-4" />
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ const footer = {
|
||||||
|
|
||||||
export function Footer() {
|
export function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer className="border-t border-white/10 bg-black">
|
<footer className="border-t border-white/10 bg-[#09090b]">
|
||||||
<div className="mx-auto max-w-6xl px-6 py-16 lg:py-20">
|
<div className="mx-auto max-w-6xl px-6 py-16 lg:py-20">
|
||||||
<div className="xl:grid xl:grid-cols-12 xl:gap-16">
|
<div className="xl:grid xl:grid-cols-12 xl:gap-16">
|
||||||
{/* Logo & Social */}
|
{/* Logo & Social */}
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ export function GetStarted() {
|
||||||
className="grid grid-cols-1 gap-4 md:grid-cols-3"
|
className="grid grid-cols-1 gap-4 md:grid-cols-3"
|
||||||
>
|
>
|
||||||
{/* Option 1: SDK */}
|
{/* Option 1: SDK */}
|
||||||
<div className="group flex flex-col rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
<div className="group flex flex-col rounded-xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
||||||
<div className="mb-4 flex items-center gap-3">
|
<div className="mb-4 flex items-center gap-3">
|
||||||
<div className="text-zinc-500">
|
<div className="text-zinc-500">
|
||||||
<Code className="h-4 w-4" />
|
<Code className="h-4 w-4" />
|
||||||
|
|
@ -142,7 +142,7 @@ export function GetStarted() {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col">
|
<div className="flex-1 flex flex-col">
|
||||||
<div className="overflow-hidden rounded-lg border border-white/10 bg-black/50 flex-1 flex flex-col">
|
<div className="overflow-hidden rounded-lg border border-white/10 bg-[#0c0c0e] flex-1 flex flex-col">
|
||||||
<div className="flex items-center justify-between border-b border-white/10 bg-white/5 px-3 py-2">
|
<div className="flex items-center justify-between border-b border-white/10 bg-white/5 px-3 py-2">
|
||||||
<span className="text-[10px] font-medium text-zinc-500">example.ts</span>
|
<span className="text-[10px] font-medium text-zinc-500">example.ts</span>
|
||||||
<CopyButton text={sdkCodeRaw} />
|
<CopyButton text={sdkCodeRaw} />
|
||||||
|
|
@ -153,7 +153,7 @@ export function GetStarted() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Option 2: HTTP API */}
|
{/* Option 2: HTTP API */}
|
||||||
<div className="group flex flex-col rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
<div className="group flex flex-col rounded-xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
||||||
<div className="mb-4 flex items-center gap-3">
|
<div className="mb-4 flex items-center gap-3">
|
||||||
<div className="text-zinc-500">
|
<div className="text-zinc-500">
|
||||||
<Server className="h-4 w-4" />
|
<Server className="h-4 w-4" />
|
||||||
|
|
@ -169,7 +169,7 @@ export function GetStarted() {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col">
|
<div className="flex-1 flex flex-col">
|
||||||
<div className="overflow-hidden rounded-lg border border-white/10 bg-black/50 flex-1 flex flex-col">
|
<div className="overflow-hidden rounded-lg border border-white/10 bg-[#0c0c0e] flex-1 flex flex-col">
|
||||||
<div className="flex items-center justify-between border-b border-white/10 bg-white/5 px-3 py-2">
|
<div className="flex items-center justify-between border-b border-white/10 bg-white/5 px-3 py-2">
|
||||||
<span className="text-[10px] font-medium text-zinc-500">terminal</span>
|
<span className="text-[10px] font-medium text-zinc-500">terminal</span>
|
||||||
<CopyButton text={sandboxCommand} />
|
<CopyButton text={sandboxCommand} />
|
||||||
|
|
@ -190,7 +190,7 @@ export function GetStarted() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Option 3: Open Source */}
|
{/* Option 3: Open Source */}
|
||||||
<div className="group flex flex-col rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
<div className="group flex flex-col rounded-xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
|
||||||
<div className="mb-4 flex items-center gap-3">
|
<div className="mb-4 flex items-center gap-3">
|
||||||
<div className="text-zinc-500">
|
<div className="text-zinc-500">
|
||||||
<GitBranch className="h-4 w-4" />
|
<GitBranch className="h-4 w-4" />
|
||||||
|
|
@ -206,7 +206,7 @@ export function GetStarted() {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col">
|
<div className="flex-1 flex flex-col">
|
||||||
<div className="overflow-hidden rounded-lg border border-white/10 bg-black/50 flex-1 flex flex-col">
|
<div className="overflow-hidden rounded-lg border border-white/10 bg-[#0c0c0e] flex-1 flex flex-col">
|
||||||
<div className="flex items-center justify-between border-b border-white/10 bg-white/5 px-3 py-2">
|
<div className="flex items-center justify-between border-b border-white/10 bg-white/5 px-3 py-2">
|
||||||
<span className="text-[10px] font-medium text-zinc-500">terminal</span>
|
<span className="text-[10px] font-medium text-zinc-500">terminal</span>
|
||||||
<CopyButton text={sourceCommands} />
|
<CopyButton text={sourceCommands} />
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ function UniversalAPIDiagram() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full aspect-[4/3] sm:aspect-[16/9] bg-[#050505] rounded-2xl border border-white/10 overflow-hidden flex items-center justify-center shadow-2xl">
|
<div className="relative w-full aspect-[4/3] sm:aspect-[16/9] bg-[#0c0c0e] rounded-xl border border-white/10 overflow-hidden flex items-center justify-center shadow-2xl">
|
||||||
{/* Background Dots - color changes with active adapter */}
|
{/* Background Dots - color changes with active adapter */}
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 opacity-[0.15] pointer-events-none transition-all duration-1000"
|
className="absolute inset-0 opacity-[0.15] pointer-events-none transition-all duration-1000"
|
||||||
|
|
@ -46,7 +46,7 @@ function UniversalAPIDiagram() {
|
||||||
|
|
||||||
{/* YOUR APP NODE - Glass dark effect with backdrop blur */}
|
{/* YOUR APP NODE - Glass dark effect with backdrop blur */}
|
||||||
<foreignObject x="60" y="175" width="180" height="100">
|
<foreignObject x="60" y="175" width="180" height="100">
|
||||||
<div className="w-full h-full rounded-2xl border border-white/10 bg-black/40 backdrop-blur-md flex items-center justify-center">
|
<div className="w-full h-full rounded-xl border border-white/10 bg-[#09090b]/60 backdrop-blur-md flex items-center justify-center">
|
||||||
<span className="text-white text-xl font-bold">Your App</span>
|
<span className="text-white text-xl font-bold">Your App</span>
|
||||||
</div>
|
</div>
|
||||||
</foreignObject>
|
</foreignObject>
|
||||||
|
|
@ -69,7 +69,7 @@ function UniversalAPIDiagram() {
|
||||||
|
|
||||||
{/* SANDBOX BOUNDARY - Glass dark effect with backdrop blur */}
|
{/* SANDBOX BOUNDARY - Glass dark effect with backdrop blur */}
|
||||||
<foreignObject x="360" y="45" width="410" height="360">
|
<foreignObject x="360" y="45" width="410" height="360">
|
||||||
<div className="w-full h-full rounded-3xl border border-white/10 bg-black/40 backdrop-blur-md">
|
<div className="w-full h-full rounded-xl border border-white/10 bg-[#09090b]/60 backdrop-blur-md">
|
||||||
<div className="text-white text-sm font-extrabold tracking-[0.2em] text-center pt-4">SANDBOX</div>
|
<div className="text-white text-sm font-extrabold tracking-[0.2em] text-center pt-4">SANDBOX</div>
|
||||||
</div>
|
</div>
|
||||||
</foreignObject>
|
</foreignObject>
|
||||||
|
|
@ -228,7 +228,7 @@ export function Hero() {
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
className="mb-6 text-3xl font-medium leading-[1.1] tracking-tight text-white md:text-5xl"
|
className="mb-6 text-3xl font-semibold leading-[1.1] tracking-tight text-white md:text-5xl"
|
||||||
>
|
>
|
||||||
Run Coding Agents in Sandboxes.
|
Run Coding Agents in Sandboxes.
|
||||||
<br />
|
<br />
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export function Inspector() {
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.5, delay: 0.2 }}
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||||||
className="overflow-hidden rounded-2xl border border-white/10"
|
className="overflow-hidden rounded-xl border border-white/10"
|
||||||
>
|
>
|
||||||
<img src="/images/inspector.png" alt="Sandbox Agent Inspector" className="w-full" />
|
<img src="/images/inspector.png" alt="Sandbox Agent Inspector" className="w-full" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,14 @@ const integrations = ["Daytona", "E2B", "AI SDK", "Anthropic", "OpenAI", "Docker
|
||||||
|
|
||||||
export function Integrations() {
|
export function Integrations() {
|
||||||
return (
|
return (
|
||||||
<section id="integrations" className="py-24 bg-zinc-900/20 border-t border-white/5 relative overflow-hidden">
|
<section id="integrations" className="py-24 bg-[#0f0f11]/50 border-t border-white/5 relative overflow-hidden">
|
||||||
<div className="max-w-4xl mx-auto px-6 text-center">
|
<div className="max-w-4xl mx-auto px-6 text-center">
|
||||||
<h2 className="text-3xl font-bold text-white mb-6">Works with your stack</h2>
|
<h2 className="text-3xl font-semibold text-white mb-6">Works with your stack</h2>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
{integrations.map((item) => (
|
{integrations.map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item}
|
key={item}
|
||||||
className="h-16 flex items-center justify-center rounded-xl border border-white/5 bg-zinc-900/50 text-zinc-300 font-mono text-sm hover:border-accent/40 hover:text-accent transition-all cursor-default"
|
className="h-16 flex items-center justify-center rounded-xl border border-white/5 bg-white/[0.02] text-zinc-300 font-mono text-sm hover:border-accent/40 hover:text-accent transition-all cursor-default"
|
||||||
>
|
>
|
||||||
{item}
|
{item}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { GitHubStars } from "./GitHubStars";
|
||||||
|
|
||||||
function NavItem({ href, children }: { href: string; children: React.ReactNode }) {
|
function NavItem({ href, children }: { href: string; children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<a href={href} className="px-3 py-2 text-sm font-medium text-zinc-400 transition-colors duration-200 hover:text-white">
|
<a href={href} className="px-3 py-2 text-sm font-normal text-zinc-400 transition-colors duration-200 hover:text-white">
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
@ -28,19 +28,19 @@ export function Navigation() {
|
||||||
return (
|
return (
|
||||||
<div className="fixed top-0 z-50 w-full max-w-[1200px] md:left-1/2 md:top-4 md:-translate-x-1/2 md:px-8">
|
<div className="fixed top-0 z-50 w-full max-w-[1200px] md:left-1/2 md:top-4 md:-translate-x-1/2 md:px-8">
|
||||||
<div
|
<div
|
||||||
className={`relative before:pointer-events-none before:absolute before:inset-[-1px] before:z-20 before:hidden before:rounded-2xl before:border before:content-[''] before:transition-colors before:duration-300 before:ease-in-out md:before:block ${
|
className={`relative before:pointer-events-none before:absolute before:inset-[-1px] before:z-20 before:hidden before:rounded-xl before:border before:content-[''] before:transition-colors before:duration-300 before:ease-in-out md:before:block ${
|
||||||
isScrolled ? "before:border-white/10" : "before:border-transparent"
|
isScrolled ? "before:border-white/10" : "before:border-transparent"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Background with blur */}
|
{/* Background with blur */}
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-0 -z-[1] hidden overflow-hidden rounded-2xl transition-all duration-300 ease-in-out md:block ${
|
className={`absolute inset-0 -z-[1] hidden overflow-hidden rounded-xl transition-all duration-300 ease-in-out md:block ${
|
||||||
isScrolled ? "bg-black/80 backdrop-blur-lg" : "bg-transparent backdrop-blur-none"
|
isScrolled ? "bg-[#09090b]/80 backdrop-blur-lg" : "bg-transparent backdrop-blur-none"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<header
|
<header
|
||||||
className={`bg-black/60 border-b-transparent sticky top-0 z-10 flex flex-col items-center border-b backdrop-blur-md pt-2 pb-2 md:static md:bg-transparent md:rounded-2xl md:max-w-[1200px] md:border-transparent md:backdrop-blur-none transition-all hover:opacity-100 ${
|
className={`bg-[#09090b]/60 border-b-transparent sticky top-0 z-10 flex flex-col items-center border-b backdrop-blur-md pt-2 pb-2 md:static md:bg-transparent md:rounded-xl md:max-w-[1200px] md:border-transparent md:backdrop-blur-none transition-all hover:opacity-100 ${
|
||||||
isScrolled ? "opacity-100" : "opacity-80"
|
isScrolled ? "opacity-100" : "opacity-80"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
@ -91,7 +91,7 @@ export function Navigation() {
|
||||||
|
|
||||||
{/* Mobile menu */}
|
{/* Mobile menu */}
|
||||||
{mobileMenuOpen && (
|
{mobileMenuOpen && (
|
||||||
<div className="md:hidden border border-white/10 bg-black/95 backdrop-blur-lg rounded-2xl mt-2 mx-2 shadow-xl">
|
<div className="md:hidden border border-white/10 bg-[#09090b]/95 backdrop-blur-lg rounded-xl mt-2 mx-2 shadow-xl">
|
||||||
<div className="px-4 py-4 space-y-1">
|
<div className="px-4 py-4 space-y-1">
|
||||||
<a
|
<a
|
||||||
href="/docs"
|
href="/docs"
|
||||||
|
|
|
||||||
|
|
@ -26,18 +26,18 @@ const problems = [
|
||||||
|
|
||||||
export function ProblemsSolved() {
|
export function ProblemsSolved() {
|
||||||
return (
|
return (
|
||||||
<section id="features" className="py-24 bg-zinc-950 border-y border-white/5">
|
<section id="features" className="py-24 bg-[#0f0f11] border-y border-white/5">
|
||||||
<div className="max-w-7xl mx-auto px-6">
|
<div className="max-w-7xl mx-auto px-6">
|
||||||
<div className="text-center mb-16">
|
<div className="text-center mb-16">
|
||||||
<h2 className="text-3xl font-bold text-white mb-4">Why Coding Agent SDK?</h2>
|
<h2 className="text-3xl font-semibold text-white mb-4">Why Coding Agent SDK?</h2>
|
||||||
<p className="text-zinc-400 max-w-xl mx-auto">Solving the three fundamental friction points of agentic software development.</p>
|
<p className="text-zinc-400 max-w-xl mx-auto">Solving the three fundamental friction points of agentic software development.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-3 gap-8">
|
<div className="grid md:grid-cols-3 gap-8">
|
||||||
{problems.map((item, idx) => (
|
{problems.map((item, idx) => (
|
||||||
<div key={idx} className="group p-8 rounded-2xl bg-zinc-900/40 border border-white/5 hover:border-accent/30 transition-all duration-300">
|
<div key={idx} className="group p-8 rounded-xl bg-white/[0.02] border border-white/5 hover:border-accent/30 transition-all duration-300">
|
||||||
<FeatureIcon icon={item.icon} color={item.color} />
|
<FeatureIcon icon={item.icon} color={item.color} />
|
||||||
<h3 className="text-xl font-bold text-white mb-3">{item.title}</h3>
|
<h3 className="text-xl font-semibold text-white mb-3">{item.title}</h3>
|
||||||
<p className="text-zinc-400 text-sm leading-relaxed">{item.desc}</p>
|
<p className="text-zinc-400 text-sm leading-relaxed">{item.desc}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,11 @@ interface ButtonProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Button({ children, variant = "primary", size = "md", href, onClick, className = "" }: ButtonProps) {
|
export function Button({ children, variant = "primary", size = "md", href, onClick, className = "" }: ButtonProps) {
|
||||||
const baseStyles = "inline-flex items-center justify-center font-bold rounded-lg transition-all";
|
const baseStyles = "inline-flex items-center justify-center font-medium rounded-lg transition-all";
|
||||||
|
|
||||||
const variants = {
|
const variants = {
|
||||||
primary: "bg-white text-black hover:bg-zinc-200",
|
primary: "bg-white text-black hover:bg-zinc-200",
|
||||||
secondary: "bg-zinc-900 border border-white/10 text-white hover:bg-zinc-800",
|
secondary: "bg-[#0f0f11] border border-white/10 text-white hover:bg-white/[0.06]",
|
||||||
ghost: "text-zinc-400 hover:text-white",
|
ghost: "text-zinc-400 hover:text-white",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ const structuredData = {
|
||||||
<meta name="keywords" content="coding agents, AI SDK, Claude Code, Codex, OpenCode, Amp, Pi, sandbox, remote code execution, developer tools, AI coding assistant, code automation" />
|
<meta name="keywords" content="coding agents, AI SDK, Claude Code, Codex, OpenCode, Amp, Pi, sandbox, remote code execution, developer tools, AI coding assistant, code automation" />
|
||||||
<meta name="author" content="Rivet" />
|
<meta name="author" content="Rivet" />
|
||||||
<meta name="robots" content="index, follow" />
|
<meta name="robots" content="index, follow" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#09090b" />
|
||||||
<link rel="canonical" href={canonicalURL} />
|
<link rel="canonical" href={canonicalURL} />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
|
||||||
|
|
@ -48,8 +48,8 @@ const structuredData = {
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
|
||||||
<!-- Manrope + JetBrains Mono (from Google Fonts) -->
|
<!-- IBM Plex Sans + IBM Plex Mono (from Google Fonts) -->
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||||
|
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,14 @@
|
||||||
--header-height: 3.5rem;
|
--header-height: 3.5rem;
|
||||||
|
|
||||||
/* Theme colors (HSL for flexibility) */
|
/* Theme colors (HSL for flexibility) */
|
||||||
--background: 20 14.3% 4.1%;
|
--background: 240 10% 3.9%;
|
||||||
--foreground: 60 9.1% 97.8%;
|
--foreground: 0 0% 100%;
|
||||||
--primary: 18.5 100% 50%;
|
--primary: 18 100% 50%;
|
||||||
--primary-foreground: 60 9.1% 97.8%;
|
--primary-foreground: 0 0% 100%;
|
||||||
--muted: 34 10% 10%;
|
--muted: 240 5% 7%;
|
||||||
--muted-foreground: 24 5.4% 63.9%;
|
--muted-foreground: 240 4% 66%;
|
||||||
--border: 12 6.5% 15.1%;
|
--border: 0 0% 100% / 0.10;
|
||||||
--card: 0 9.09% 6.47%;
|
--card: 240 6% 4%;
|
||||||
|
|
||||||
/* Shiki syntax highlighting */
|
/* Shiki syntax highlighting */
|
||||||
--shiki-color-text: theme('colors.white');
|
--shiki-color-text: theme('colors.white');
|
||||||
|
|
@ -34,9 +34,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-black text-white antialiased;
|
@apply bg-[#09090b] text-white antialiased;
|
||||||
font-family: 'Manrope', system-ui, sans-serif;
|
font-family: 'IBM Plex Sans', 'Segoe UI', system-ui, sans-serif;
|
||||||
font-weight: 500;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Text selection - matches rivet.dev */
|
/* Text selection - matches rivet.dev */
|
||||||
|
|
@ -124,7 +124,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-strong {
|
.glass-strong {
|
||||||
@apply bg-black/95 backdrop-blur-lg border border-white/10;
|
@apply bg-[#09090b]/95 backdrop-blur-lg border border-white/10;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bento box card effects */
|
/* Bento box card effects */
|
||||||
|
|
@ -179,11 +179,11 @@
|
||||||
|
|
||||||
/* Glow effect for buttons and interactive elements */
|
/* Glow effect for buttons and interactive elements */
|
||||||
.glow-accent {
|
.glow-accent {
|
||||||
box-shadow: 0 0 20px rgba(255, 69, 0, 0.3);
|
box-shadow: 0 0 20px rgba(255, 79, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.glow-accent-hover:hover {
|
.glow-accent-hover:hover {
|
||||||
box-shadow: 0 0 30px rgba(255, 69, 0, 0.5);
|
box-shadow: 0 0 30px rgba(255, 79, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Code highlight styling */
|
/* Code highlight styling */
|
||||||
|
|
@ -196,7 +196,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-highlight-ref.is-active {
|
.code-highlight-ref.is-active {
|
||||||
background-color: rgba(255, 69, 0, 0.1);
|
background-color: rgba(255, 79, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-highlight-ref.is-active::before {
|
.code-highlight-ref.is-active::before {
|
||||||
|
|
@ -206,7 +206,7 @@
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 2px;
|
width: 2px;
|
||||||
background-color: #ff4500;
|
background-color: #ff4f00;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide scrollbar */
|
/* Hide scrollbar */
|
||||||
|
|
@ -228,12 +228,12 @@
|
||||||
|
|
||||||
/* Backdrop with blur */
|
/* Backdrop with blur */
|
||||||
.backdrop-glow {
|
.backdrop-glow {
|
||||||
@apply backdrop-blur-lg bg-black/80;
|
@apply backdrop-blur-lg bg-[#09090b]/80;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Better focus ring */
|
/* Better focus ring */
|
||||||
.focus-ring {
|
.focus-ring {
|
||||||
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-black;
|
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-[#09090b];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,26 @@ export default {
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
// Primary accent (OrangeRed)
|
// Primary accent (Foundry orange)
|
||||||
accent: "#FF4500",
|
accent: "#ff4f00",
|
||||||
// Extended color palette
|
// Extended color palette
|
||||||
background: "#000000",
|
background: "#09090b",
|
||||||
"text-primary": "#FAFAFA",
|
"bg-secondary": "#0f0f11",
|
||||||
"text-secondary": "#A0A0A0",
|
"bg-tertiary": "#0c0c0e",
|
||||||
border: "#252525",
|
"text-primary": "#ffffff",
|
||||||
|
"text-secondary": "#a1a1aa",
|
||||||
|
"text-tertiary": "#71717a",
|
||||||
|
border: "rgba(255, 255, 255, 0.10)",
|
||||||
// Code syntax highlighting
|
// Code syntax highlighting
|
||||||
"code-keyword": "#c084fc",
|
"code-keyword": "#c084fc",
|
||||||
"code-function": "#60a5fa",
|
"code-function": "#60a5fa",
|
||||||
"code-string": "#4ade80",
|
"code-string": "#4ade80",
|
||||||
"code-comment": "#737373",
|
"code-comment": "#71717a",
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ["Manrope", "system-ui", "sans-serif"],
|
sans: ["IBM Plex Sans", "Segoe UI", "system-ui", "sans-serif"],
|
||||||
heading: ["Manrope", "system-ui", "sans-serif"],
|
heading: ["IBM Plex Sans", "Segoe UI", "system-ui", "sans-serif"],
|
||||||
mono: ["JetBrains Mono", "monospace"],
|
mono: ["IBM Plex Mono", "SFMono-Regular", "monospace"],
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
"fade-in-up": "fade-in-up 0.8s ease-out forwards",
|
"fade-in-up": "fade-in-up 0.8s ease-out forwards",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue