refactor(nexus): mount new frame in Layout.tsx; kill old chrome (phase 8)
Rewrites Layout.tsx to compose the new Phase 8 frame (IconRail +
TopStrip) and remove the old chrome elements specified as killed in
docs/specs/2026-04-11-nexus-layout-overhaul.md §2:
Removed from chrome:
- 280px collapsible Sidebar / InstanceSidebar
- ChatPanel global slide-in right rail
- PropertiesPanel global slide-in right rail
- BreadcrumbBar (replaced by ModeBreadcrumb inside TopStrip)
- Footer row with Docs link, version tooltip, instance settings button,
chat toggle button, theme toggle button
- Effect that closed PropertiesPanel when chat opened
- Mobile sidebar drawer block
- Mobile sidebar swipe gesture listener
Preserved:
- Company-prefix URL sync and fallback redirect machinery
- First-run onboarding trigger
- WorktreeBanner, DevRestartBanner
- Scroll-based mobile nav visibility tracking
- Body overflow management
- Instance settings path memory
- Dialog overlays (NewIssue, NewProject, NewGoal, NewAgent)
- ToastViewport, CommandPalette
- MobileBottomNav (mobile only; Phase 15 replaces)
Added:
- IconRail mount with derived companyPrefix from matchedCompany or
selectedCompany
- TopStrip mount above the main content area
- hasUnknownCompanyPrefix fallback defaults to /assistant instead of
/dashboard (Dashboard is killed in the new IA)
- useKeyboardShortcuts.onSearch dispatches the same synthetic Meta+K
keydown as the CmdKButton shim
The Sidebar, InstanceSidebar, BreadcrumbBar, ChatPanel, PropertiesPanel,
ThemeContext, and useChatPanel files remain in the repo; Phase 16
deletes dead files after the other Phase 8 tasks are proven stable.
Pages render unchanged in the new frame and will look visually wrong
until Phases 9-13 rebuild their internals. That is the expected
intermediate state per the spec.
Part of Phase 8 of the Nexus layout overhaul (task 6 of 7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c521ae4403
commit
d87f644cde
1 changed files with 57 additions and 285 deletions
|
|
@ -1,14 +1,8 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { BookOpen, MessageSquare, Moon, Settings, Sun } from "lucide-react";
|
||||
import { Link, Navigate, Outlet, useLocation, useNavigate, useParams } from "@/lib/router";
|
||||
import { Navigate, Outlet, useLocation, useNavigate, useParams } from "@/lib/router";
|
||||
// [nexus] CompanyRail intentionally not rendered — single-workspace mode.
|
||||
// The file is preserved for upstream rebase compatibility.
|
||||
import { Sidebar } from "./Sidebar";
|
||||
import { InstanceSidebar } from "./InstanceSidebar";
|
||||
import { BreadcrumbBar } from "./BreadcrumbBar";
|
||||
import { ChatPanel } from "./ChatPanel";
|
||||
import { PropertiesPanel } from "./PropertiesPanel";
|
||||
import { CommandPalette } from "./CommandPalette";
|
||||
import { NewIssueDialog } from "./NewIssueDialog";
|
||||
import { NewProjectDialog } from "./NewProjectDialog";
|
||||
|
|
@ -18,13 +12,12 @@ import { ToastViewport } from "./ToastViewport";
|
|||
import { MobileBottomNav } from "./MobileBottomNav";
|
||||
import { WorktreeBanner } from "./WorktreeBanner";
|
||||
import { DevRestartBanner } from "./DevRestartBanner";
|
||||
import { IconRail } from "./frame/IconRail";
|
||||
import { TopStrip } from "./frame/TopStrip";
|
||||
import { useDialog } from "../context/DialogContext";
|
||||
import { GeneralSettingsProvider } from "../context/GeneralSettingsContext";
|
||||
import { usePanel } from "../context/PanelContext";
|
||||
import { useChatPanel } from "../context/ChatPanelContext";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useSidebar } from "../context/SidebarContext";
|
||||
import { useTheme } from "../context/ThemeContext";
|
||||
import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts";
|
||||
import { useCompanyPageMemory } from "../hooks/useCompanyPageMemory";
|
||||
import { healthApi } from "../api/health";
|
||||
|
|
@ -37,8 +30,6 @@ import {
|
|||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { cn } from "../lib/utils";
|
||||
import { NotFoundPage } from "../pages/NotFound";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
|
||||
|
||||
const INSTANCE_SETTINGS_MEMORY_KEY = "paperclip.lastInstanceSettingsPath";
|
||||
|
||||
|
|
@ -52,10 +43,8 @@ function readRememberedInstanceSettingsPath(): string {
|
|||
}
|
||||
|
||||
export function Layout() {
|
||||
const { sidebarOpen, setSidebarOpen, toggleSidebar, isMobile } = useSidebar();
|
||||
const { isMobile } = useSidebar();
|
||||
const { openNewIssue, openOnboarding } = useDialog();
|
||||
const { togglePanelVisible, setPanelVisible } = usePanel();
|
||||
const { chatOpen, setChatOpen, toggleChat } = useChatPanel();
|
||||
const {
|
||||
companies,
|
||||
loading: companiesLoading,
|
||||
|
|
@ -64,17 +53,14 @@ export function Layout() {
|
|||
selectionSource,
|
||||
setSelectedCompanyId,
|
||||
} = useCompany();
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const isDarkTheme = theme === "dark";
|
||||
const nextThemeLabel = isDarkTheme ? "Light" : "Dark";
|
||||
const { companyPrefix } = useParams<{ companyPrefix: string }>();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const isInstanceSettingsRoute = location.pathname.startsWith("/instance/");
|
||||
const onboardingTriggered = useRef(false);
|
||||
const lastMainScrollTop = useRef(0);
|
||||
const [mobileNavVisible, setMobileNavVisible] = useState(true);
|
||||
const [instanceSettingsTarget, setInstanceSettingsTarget] = useState<string>(() => readRememberedInstanceSettingsPath());
|
||||
const [, setInstanceSettingsTarget] = useState<string>(() => readRememberedInstanceSettingsPath());
|
||||
|
||||
const matchedCompany = useMemo(() => {
|
||||
if (!companyPrefix) return null;
|
||||
const requestedPrefix = companyPrefix.toUpperCase();
|
||||
|
|
@ -82,6 +68,9 @@ export function Layout() {
|
|||
}, [companies, companyPrefix]);
|
||||
const hasUnknownCompanyPrefix =
|
||||
Boolean(companyPrefix) && !companiesLoading && companies.length > 0 && !matchedCompany;
|
||||
|
||||
const railCompanyPrefix = matchedCompany?.issuePrefix ?? selectedCompany?.issuePrefix ?? null;
|
||||
|
||||
const { data: health } = useQuery({
|
||||
queryKey: queryKeys.health,
|
||||
queryFn: () => healthApi.get(),
|
||||
|
|
@ -97,16 +86,9 @@ export function Layout() {
|
|||
queryFn: () => instanceSettingsApi.getGeneral(),
|
||||
}).data?.keyboardShortcuts === true;
|
||||
|
||||
// [nexus] Removed the `health?.deploymentMode === "authenticated"` gate.
|
||||
// Paperclip's upstream assumed authenticated mode = hosted multi-tenant where
|
||||
// users self-onboard via invites, so the first-run wizard was suppressed.
|
||||
// Nexus's authenticated mode is single-user LAN (BETTER_AUTH), which still
|
||||
// needs the first-run wizard. Note: this effect is mostly dead code for the
|
||||
// zero-company case because CompanyRootRedirect navigates to /onboarding
|
||||
// before Layout ever mounts — the real auto-open trigger lives in
|
||||
// OnboardingRoutePage (App.tsx). This effect is retained as a belt-and-
|
||||
// suspenders fallback for edge cases where a user reaches a Layout-mounted
|
||||
// route with zero companies (e.g. /instance/settings/*).
|
||||
// [nexus] First-run onboarding trigger. Retained as a belt-and-suspenders
|
||||
// fallback for edge cases where a user reaches a Layout-mounted route with
|
||||
// zero companies (e.g. /instance/settings/*).
|
||||
useEffect(() => {
|
||||
if (companiesLoading || onboardingTriggered.current) return;
|
||||
if (companies.length === 0) {
|
||||
|
|
@ -115,6 +97,7 @@ export function Layout() {
|
|||
}
|
||||
}, [companies, companiesLoading, openOnboarding]);
|
||||
|
||||
// Company-prefix URL sync.
|
||||
useEffect(() => {
|
||||
if (!companyPrefix || companiesLoading || companies.length === 0) return;
|
||||
|
||||
|
|
@ -156,25 +139,22 @@ export function Layout() {
|
|||
setSelectedCompanyId,
|
||||
]);
|
||||
|
||||
const togglePanel = togglePanelVisible;
|
||||
|
||||
// Close PropertiesPanel when chat panel opens
|
||||
useEffect(() => {
|
||||
if (chatOpen) {
|
||||
setPanelVisible(false);
|
||||
}
|
||||
}, [chatOpen, setPanelVisible]);
|
||||
|
||||
useCompanyPageMemory();
|
||||
|
||||
useKeyboardShortcuts({
|
||||
enabled: keyboardShortcutsEnabled,
|
||||
onNewIssue: () => openNewIssue(),
|
||||
onToggleSidebar: toggleSidebar,
|
||||
onTogglePanel: togglePanel,
|
||||
onToggleSidebar: () => {
|
||||
// Phase 8: sidebar toggle is a no-op from keyboard — the rail is fixed.
|
||||
// Kept as a stub so useKeyboardShortcuts' type contract is satisfied.
|
||||
},
|
||||
onTogglePanel: () => {
|
||||
// Phase 8: PropertiesPanel is no longer mounted globally.
|
||||
},
|
||||
onSearch: () => {
|
||||
if (!chatOpen) setChatOpen(true);
|
||||
requestAnimationFrame(() => window.dispatchEvent(new Event("nexus:focus-chat-search")));
|
||||
// Phase 8: open the command palette via synthetic keydown, mirroring
|
||||
// the CmdKButton shim. Phase 14 replaces with a real palette context.
|
||||
document.dispatchEvent(new KeyboardEvent("keydown", { key: "k", metaKey: true, bubbles: true }));
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -187,62 +167,11 @@ export function Layout() {
|
|||
setMobileNavVisible(true);
|
||||
}, [isMobile]);
|
||||
|
||||
// Swipe gesture to open/close sidebar on mobile
|
||||
useEffect(() => {
|
||||
if (!isMobile) return;
|
||||
|
||||
const EDGE_ZONE = 30; // px from left edge to start open-swipe
|
||||
const MIN_DISTANCE = 50; // minimum horizontal swipe distance
|
||||
const MAX_VERTICAL = 75; // max vertical drift before we ignore
|
||||
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
|
||||
const onTouchStart = (e: TouchEvent) => {
|
||||
const t = e.touches[0]!;
|
||||
startX = t.clientX;
|
||||
startY = t.clientY;
|
||||
};
|
||||
|
||||
const onTouchEnd = (e: TouchEvent) => {
|
||||
const t = e.changedTouches[0]!;
|
||||
const dx = t.clientX - startX;
|
||||
const dy = Math.abs(t.clientY - startY);
|
||||
|
||||
if (dy > MAX_VERTICAL) return; // vertical scroll, ignore
|
||||
|
||||
// Swipe right from left edge → open
|
||||
if (!sidebarOpen && startX < EDGE_ZONE && dx > MIN_DISTANCE) {
|
||||
setSidebarOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Swipe left when open → close
|
||||
if (sidebarOpen && dx < -MIN_DISTANCE) {
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("touchstart", onTouchStart, { passive: true });
|
||||
document.addEventListener("touchend", onTouchEnd, { passive: true });
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("touchstart", onTouchStart);
|
||||
document.removeEventListener("touchend", onTouchEnd);
|
||||
};
|
||||
}, [isMobile, sidebarOpen, setSidebarOpen]);
|
||||
|
||||
const updateMobileNavVisibility = useCallback((currentTop: number) => {
|
||||
const delta = currentTop - lastMainScrollTop.current;
|
||||
|
||||
if (currentTop <= 24) {
|
||||
setMobileNavVisible(true);
|
||||
} else if (delta > 8) {
|
||||
setMobileNavVisible(false);
|
||||
} else if (delta < -8) {
|
||||
setMobileNavVisible(true);
|
||||
}
|
||||
|
||||
if (currentTop <= 24) setMobileNavVisible(true);
|
||||
else if (delta > 8) setMobileNavVisible(false);
|
||||
else if (delta < -8) setMobileNavVisible(true);
|
||||
lastMainScrollTop.current = currentTop;
|
||||
}, []);
|
||||
|
||||
|
|
@ -252,24 +181,17 @@ export function Layout() {
|
|||
lastMainScrollTop.current = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const onScroll = () => {
|
||||
updateMobileNavVisibility(window.scrollY || document.documentElement.scrollTop || 0);
|
||||
};
|
||||
|
||||
onScroll();
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", onScroll);
|
||||
};
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
}, [isMobile, updateMobileNavVisibility]);
|
||||
|
||||
useEffect(() => {
|
||||
const previousOverflow = document.body.style.overflow;
|
||||
|
||||
document.body.style.overflow = isMobile ? "visible" : "hidden";
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = previousOverflow;
|
||||
};
|
||||
|
|
@ -277,12 +199,10 @@ export function Layout() {
|
|||
|
||||
useEffect(() => {
|
||||
if (!location.pathname.startsWith("/instance/settings/")) return;
|
||||
|
||||
const nextPath = normalizeRememberedInstanceSettingsPath(
|
||||
`${location.pathname}${location.search}${location.hash}`,
|
||||
);
|
||||
setInstanceSettingsTarget(nextPath);
|
||||
|
||||
try {
|
||||
window.localStorage.setItem(INSTANCE_SETTINGS_MEMORY_KEY, nextPath);
|
||||
} catch {
|
||||
|
|
@ -293,183 +213,36 @@ export function Layout() {
|
|||
return (
|
||||
<GeneralSettingsProvider value={{ keyboardShortcutsEnabled }}>
|
||||
<div
|
||||
className={cn(
|
||||
"bg-background text-foreground pt-[env(safe-area-inset-top)]",
|
||||
isMobile ? "min-h-dvh" : "flex h-dvh flex-col overflow-hidden",
|
||||
)}
|
||||
>
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:fixed focus:left-3 focus:top-3 focus:z-[200] focus:rounded-md focus:bg-background focus:px-3 focus:py-2 focus:text-sm focus:font-medium focus:shadow-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
Skip to Main Content
|
||||
</a>
|
||||
<WorktreeBanner />
|
||||
<DevRestartBanner devServer={health?.devServer} />
|
||||
<div className={cn("min-h-0 flex-1", isMobile ? "w-full" : "flex overflow-hidden")}>
|
||||
{isMobile && sidebarOpen && (
|
||||
<button
|
||||
type="button"
|
||||
className="fixed inset-0 z-40 bg-black/50"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
aria-label="Close sidebar"
|
||||
/>
|
||||
className={cn(
|
||||
"bg-background text-foreground pt-[env(safe-area-inset-top)]",
|
||||
isMobile ? "min-h-dvh" : "flex h-dvh flex-col overflow-hidden",
|
||||
)}
|
||||
>
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:fixed focus:left-3 focus:top-3 focus:z-[200] focus:rounded-md focus:bg-background focus:px-3 focus:py-2 focus:text-sm focus:font-medium focus:shadow-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
Skip to Main Content
|
||||
</a>
|
||||
|
||||
{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">
|
||||
{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>
|
||||
{health?.version && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="px-2 text-xs text-muted-foreground shrink-0 cursor-default">v</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>v{health.version}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Button variant="ghost" size="icon-sm" className="text-muted-foreground shrink-0" asChild>
|
||||
<Link
|
||||
to={instanceSettingsTarget}
|
||||
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 ${nextThemeLabel}`}
|
||||
title={`Switch to ${nextThemeLabel}`}
|
||||
>
|
||||
{isDarkTheme ? <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">
|
||||
<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>
|
||||
{health?.version && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="px-2 text-xs text-muted-foreground shrink-0 cursor-default">v</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>v{health.version}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Button variant="ghost" size="icon-sm" className="text-muted-foreground shrink-0" asChild>
|
||||
<Link
|
||||
to={instanceSettingsTarget}
|
||||
aria-label="Instance settings"
|
||||
title="Instance settings"
|
||||
onClick={() => {
|
||||
if (isMobile) setSidebarOpen(false);
|
||||
}}
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="hidden md:inline-flex text-muted-foreground shrink-0"
|
||||
onClick={toggleChat}
|
||||
aria-label={chatOpen ? "Close chat" : "Open chat"}
|
||||
>
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{chatOpen ? "Close chat" : "Open chat"}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="text-muted-foreground shrink-0"
|
||||
onClick={toggleTheme}
|
||||
aria-label={`Switch to ${nextThemeLabel}`}
|
||||
title={`Switch to ${nextThemeLabel}`}
|
||||
>
|
||||
{isDarkTheme ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<WorktreeBanner />
|
||||
<DevRestartBanner devServer={health?.devServer} />
|
||||
|
||||
<div className={cn("min-h-0 flex-1", isMobile ? "w-full" : "flex overflow-hidden")}>
|
||||
<IconRail companyPrefix={railCompanyPrefix} />
|
||||
|
||||
<div className={cn("flex min-w-0 flex-col", isMobile ? "w-full" : "h-full flex-1")}>
|
||||
<TopStrip />
|
||||
|
||||
<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}
|
||||
className={cn(
|
||||
"flex-1 p-4 md:p-6",
|
||||
isMobile ? "overflow-visible pb-[calc(5rem+env(safe-area-inset-bottom))]" : "overflow-auto",
|
||||
"flex-1",
|
||||
isMobile ? "overflow-visible pb-[calc(5rem+env(safe-area-inset-bottom))]" : "overflow-auto p-4 md:p-6",
|
||||
)}
|
||||
>
|
||||
{hasUnknownCompanyPrefix ? (
|
||||
// [nexus] Auto-recover from bogus URL prefixes by redirecting
|
||||
// to the same path under the first available company, instead
|
||||
// of leaving the user stranded on an "Invite not available"
|
||||
// style dead end with no way back. hasUnknownCompanyPrefix is
|
||||
// already gated on companies.length > 0, so the fallback is
|
||||
// guaranteed to resolve. If no fallback exists for some reason,
|
||||
// fall through to the old NotFoundPage.
|
||||
(() => {
|
||||
const fallbackCompany = selectedCompany ?? companies[0] ?? null;
|
||||
if (!fallbackCompany) {
|
||||
|
|
@ -480,7 +253,7 @@ export function Layout() {
|
|||
/>
|
||||
);
|
||||
}
|
||||
const restOfPath = location.pathname.replace(/^\/[^/]+/, "") || "/dashboard";
|
||||
const restOfPath = location.pathname.replace(/^\/[^/]+/, "") || "/assistant";
|
||||
return (
|
||||
<Navigate
|
||||
to={`/${fallbackCompany.issuePrefix}${restOfPath}${location.search}${location.hash}`}
|
||||
|
|
@ -492,18 +265,17 @@ export function Layout() {
|
|||
<Outlet />
|
||||
)}
|
||||
</main>
|
||||
<ChatPanel />
|
||||
<PropertiesPanel />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isMobile && <MobileBottomNav visible={mobileNavVisible} />}
|
||||
<CommandPalette />
|
||||
<NewIssueDialog />
|
||||
<NewProjectDialog />
|
||||
<NewGoalDialog />
|
||||
<NewAgentDialog />
|
||||
<ToastViewport />
|
||||
|
||||
{isMobile && <MobileBottomNav visible={mobileNavVisible} />}
|
||||
|
||||
<CommandPalette />
|
||||
<NewIssueDialog />
|
||||
<NewProjectDialog />
|
||||
<NewGoalDialog />
|
||||
<NewAgentDialog />
|
||||
<ToastViewport />
|
||||
</div>
|
||||
</GeneralSettingsProvider>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue