Add worktree UI branding

This commit is contained in:
Dotta
2026-03-13 11:12:43 -05:00
parent 3b0d9a93f4
commit cce9941464
10 changed files with 566 additions and 169 deletions

View File

@@ -14,6 +14,7 @@ import { NewGoalDialog } from "./NewGoalDialog";
import { NewAgentDialog } from "./NewAgentDialog";
import { ToastViewport } from "./ToastViewport";
import { MobileBottomNav } from "./MobileBottomNav";
import { WorktreeBanner } from "./WorktreeBanner";
import { useDialog } from "../context/DialogContext";
import { usePanel } from "../context/PanelContext";
import { useCompany } from "../context/CompanyContext";
@@ -223,7 +224,7 @@ export function Layout() {
<div
className={cn(
"bg-background text-foreground pt-[env(safe-area-inset-top)]",
isMobile ? "min-h-dvh" : "flex h-dvh overflow-hidden",
isMobile ? "min-h-dvh" : "flex h-dvh flex-col overflow-hidden",
)}
>
<a
@@ -232,145 +233,148 @@ export function Layout() {
>
Skip to Main Content
</a>
{/* Mobile backdrop */}
{isMobile && sidebarOpen && (
<button
type="button"
className="fixed inset-0 z-40 bg-black/50"
onClick={() => setSidebarOpen(false)}
aria-label="Close sidebar"
/>
)}
<WorktreeBanner />
<div className={cn("min-h-0 flex-1", isMobile ? "w-full" : "flex overflow-hidden")}>
{/* Mobile backdrop */}
{isMobile && sidebarOpen && (
<button
type="button"
className="fixed inset-0 z-40 bg-black/50"
onClick={() => setSidebarOpen(false)}
aria-label="Close sidebar"
/>
)}
{/* Combined sidebar area: company rail + inner sidebar + docs bar */}
{isMobile ? (
<div
className={cn(
"fixed inset-y-0 left-0 z-50 flex flex-col overflow-hidden pt-[env(safe-area-inset-top)] transition-transform duration-100 ease-out",
sidebarOpen ? "translate-x-0" : "-translate-x-full"
)}
>
<div className="flex flex-1 min-h-0 overflow-hidden">
<CompanyRail />
{isInstanceSettingsRoute ? <InstanceSidebar /> : <Sidebar />}
</div>
<div className="border-t border-r border-border px-3 py-2 bg-background">
<div className="flex items-center gap-1">
<a
href="https://docs.paperclip.ing/"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2.5 px-3 py-2 text-[13px] font-medium transition-colors text-foreground/80 hover:bg-accent/50 hover:text-foreground flex-1 min-w-0"
>
<BookOpen className="h-4 w-4 shrink-0" />
<span className="truncate">Documentation</span>
</a>
<Button variant="ghost" size="icon-sm" className="text-muted-foreground shrink-0" asChild>
<Link
to="/instance/settings"
aria-label="Instance settings"
title="Instance settings"
onClick={() => {
if (isMobile) setSidebarOpen(false);
}}
>
<Settings className="h-4 w-4" />
</Link>
</Button>
<Button
type="button"
variant="ghost"
size="icon-sm"
className="text-muted-foreground shrink-0"
onClick={toggleTheme}
aria-label={`Switch to ${nextTheme} mode`}
title={`Switch to ${nextTheme} mode`}
>
{theme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</Button>
</div>
</div>
</div>
) : (
<div className="flex flex-col shrink-0 h-full">
<div className="flex flex-1 min-h-0">
<CompanyRail />
<div
className={cn(
"overflow-hidden transition-[width] duration-100 ease-out",
sidebarOpen ? "w-60" : "w-0"
)}
>
{isInstanceSettingsRoute ? <InstanceSidebar /> : <Sidebar />}
</div>
</div>
<div className="border-t border-r border-border px-3 py-2">
<div className="flex items-center gap-1">
<a
href="https://docs.paperclip.ing/"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2.5 px-3 py-2 text-[13px] font-medium transition-colors text-foreground/80 hover:bg-accent/50 hover:text-foreground flex-1 min-w-0"
>
<BookOpen className="h-4 w-4 shrink-0" />
<span className="truncate">Documentation</span>
</a>
<Button variant="ghost" size="icon-sm" className="text-muted-foreground shrink-0" asChild>
<Link
to="/instance/settings"
aria-label="Instance settings"
title="Instance settings"
onClick={() => {
if (isMobile) setSidebarOpen(false);
}}
>
<Settings className="h-4 w-4" />
</Link>
</Button>
<Button
type="button"
variant="ghost"
size="icon-sm"
className="text-muted-foreground shrink-0"
onClick={toggleTheme}
aria-label={`Switch to ${nextTheme} mode`}
title={`Switch to ${nextTheme} mode`}
>
{theme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</Button>
</div>
</div>
</div>
)}
{/* Main content */}
<div className={cn("flex min-w-0 flex-col", isMobile ? "w-full" : "h-full flex-1")}>
<div
className={cn(
isMobile && "sticky top-0 z-20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/85",
)}
>
<BreadcrumbBar />
</div>
<div className={cn(isMobile ? "block" : "flex flex-1 min-h-0")}>
<main
id="main-content"
tabIndex={-1}
{/* Combined sidebar area: company rail + inner sidebar + docs bar */}
{isMobile ? (
<div
className={cn(
"flex-1 p-4 md:p-6",
isMobile ? "overflow-visible pb-[calc(5rem+env(safe-area-inset-bottom))]" : "overflow-auto",
"fixed inset-y-0 left-0 z-50 flex flex-col overflow-hidden pt-[env(safe-area-inset-top)] transition-transform duration-100 ease-out",
sidebarOpen ? "translate-x-0" : "-translate-x-full"
)}
>
{hasUnknownCompanyPrefix ? (
<NotFoundPage
scope="invalid_company_prefix"
requestedPrefix={companyPrefix ?? selectedCompany?.issuePrefix}
/>
) : (
<Outlet />
<div className="flex flex-1 min-h-0 overflow-hidden">
<CompanyRail />
{isInstanceSettingsRoute ? <InstanceSidebar /> : <Sidebar />}
</div>
<div className="border-t border-r border-border px-3 py-2 bg-background">
<div className="flex items-center gap-1">
<a
href="https://docs.paperclip.ing/"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2.5 px-3 py-2 text-[13px] font-medium transition-colors text-foreground/80 hover:bg-accent/50 hover:text-foreground flex-1 min-w-0"
>
<BookOpen className="h-4 w-4 shrink-0" />
<span className="truncate">Documentation</span>
</a>
<Button variant="ghost" size="icon-sm" className="text-muted-foreground shrink-0" asChild>
<Link
to="/instance/settings"
aria-label="Instance settings"
title="Instance settings"
onClick={() => {
if (isMobile) setSidebarOpen(false);
}}
>
<Settings className="h-4 w-4" />
</Link>
</Button>
<Button
type="button"
variant="ghost"
size="icon-sm"
className="text-muted-foreground shrink-0"
onClick={toggleTheme}
aria-label={`Switch to ${nextTheme} mode`}
title={`Switch to ${nextTheme} mode`}
>
{theme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</Button>
</div>
</div>
</div>
) : (
<div className="flex h-full flex-col shrink-0">
<div className="flex flex-1 min-h-0">
<CompanyRail />
<div
className={cn(
"overflow-hidden transition-[width] duration-100 ease-out",
sidebarOpen ? "w-60" : "w-0"
)}
>
{isInstanceSettingsRoute ? <InstanceSidebar /> : <Sidebar />}
</div>
</div>
<div className="border-t border-r border-border px-3 py-2">
<div className="flex items-center gap-1">
<a
href="https://docs.paperclip.ing/"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2.5 px-3 py-2 text-[13px] font-medium transition-colors text-foreground/80 hover:bg-accent/50 hover:text-foreground flex-1 min-w-0"
>
<BookOpen className="h-4 w-4 shrink-0" />
<span className="truncate">Documentation</span>
</a>
<Button variant="ghost" size="icon-sm" className="text-muted-foreground shrink-0" asChild>
<Link
to="/instance/settings"
aria-label="Instance settings"
title="Instance settings"
onClick={() => {
if (isMobile) setSidebarOpen(false);
}}
>
<Settings className="h-4 w-4" />
</Link>
</Button>
<Button
type="button"
variant="ghost"
size="icon-sm"
className="text-muted-foreground shrink-0"
onClick={toggleTheme}
aria-label={`Switch to ${nextTheme} mode`}
title={`Switch to ${nextTheme} mode`}
>
{theme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</Button>
</div>
</div>
</div>
)}
{/* Main content */}
<div className={cn("flex min-w-0 flex-col", isMobile ? "w-full" : "h-full flex-1")}>
<div
className={cn(
isMobile && "sticky top-0 z-20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/85",
)}
</main>
<PropertiesPanel />
>
<BreadcrumbBar />
</div>
<div className={cn(isMobile ? "block" : "flex flex-1 min-h-0")}>
<main
id="main-content"
tabIndex={-1}
className={cn(
"flex-1 p-4 md:p-6",
isMobile ? "overflow-visible pb-[calc(5rem+env(safe-area-inset-bottom))]" : "overflow-auto",
)}
>
{hasUnknownCompanyPrefix ? (
<NotFoundPage
scope="invalid_company_prefix"
requestedPrefix={companyPrefix ?? selectedCompany?.issuePrefix}
/>
) : (
<Outlet />
)}
</main>
<PropertiesPanel />
</div>
</div>
</div>
{isMobile && <MobileBottomNav visible={mobileNavVisible} />}

View File

@@ -0,0 +1,25 @@
import { getWorktreeUiBranding } from "../lib/worktree-branding";
export function WorktreeBanner() {
const branding = getWorktreeUiBranding();
if (!branding) return null;
return (
<div
className="relative overflow-hidden border-b px-3 py-1.5 text-[11px] font-medium tracking-[0.2em] uppercase"
style={{
backgroundColor: branding.color,
color: branding.textColor,
borderColor: `${branding.textColor}22`,
boxShadow: `inset 0 -1px 0 ${branding.textColor}18`,
backgroundImage: `linear-gradient(90deg, ${branding.textColor}14, transparent 28%, transparent 72%, ${branding.textColor}12), repeating-linear-gradient(135deg, transparent 0 10px, ${branding.textColor}08 10px 20px)`,
}}
>
<div className="flex items-center gap-2 overflow-hidden whitespace-nowrap">
<span className="shrink-0 opacity-70">Worktree</span>
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-current opacity-70" aria-hidden="true" />
<span className="truncate font-semibold tracking-[0.12em]">{branding.name}</span>
</div>
</div>
);
}