Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 25 additions & 20 deletions apps/web/src/components/chat-layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ interface ChatLayoutProps extends ChatLayoutState, ChatLayoutActions {
contentKey: string;
contentNavigation?: ReactNode;
contentActions?: ReactNode;
/** No chat behind this layout (the account-less `/site-editor`): the thread
* stays closed and nothing offers to open it. */
threadless?: boolean;
}

interface ChatLayoutContextValue extends Omit<ChatLayoutProps, "children"> {
Expand All @@ -52,7 +55,7 @@ export function useChatLayout(): ChatLayoutState & ChatLayoutActions {
/** Null outside a ChatLayout (e.g. the settings route), rather than throwing —
* for callers that render in both. */
export function useOptionalChatLayout():
| (ChatLayoutState & ChatLayoutActions)
| (ChatLayoutState & ChatLayoutActions & { threadless?: boolean })
| null {
return use(ChatLayoutContext);
}
Expand Down Expand Up @@ -107,25 +110,27 @@ function ChatLayoutRoot({ children, ...layout }: ChatLayoutProps) {

return (
<ChatLayoutContext value={value}>
<SidebarThreadButtonPortal
open={isMobile ? value.mobileSurface === "chat" : threadOpen}
onToggle={() => {
if (isMobile) {
void navigate({
to: ".",
search: (prev) => ({
...prev,
...mobileSurfaceSearch(
value.mobileSurface === "chat" ? "main" : "chat",
),
}),
replace: true,
});
} else {
layout.toggleThread();
}
}}
/>
{!layout.threadless && (
<SidebarThreadButtonPortal
open={isMobile ? value.mobileSurface === "chat" : threadOpen}
onToggle={() => {
if (isMobile) {
void navigate({
to: ".",
search: (prev) => ({
...prev,
...mobileSurfaceSearch(
value.mobileSurface === "chat" ? "main" : "chat",
),
}),
replace: true,
});
} else {
layout.toggleThread();
}
}}
/>
)}
{isMobile ? (
<div
data-slot="chat-layout"
Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/components/chat/chat-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,18 @@ export function ChatStreamValueProvider({
);
}

/**
* Installs a task value without a thread store. The account-less
* `/site-editor` uses it: the editor surfaces read their project and branch
* from here, and there is no chat to open threads in.
*/
export function ChatTaskValueProvider({
value,
children,
}: PropsWithChildren<{ value: ChatTaskContextValue }>) {
return <ChatTaskCtx.Provider value={value}>{children}</ChatTaskCtx.Provider>;
}

/**
* Blanks the stream and task contexts for a subtree.
*
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/chat/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export {
ChatContextProvider as ChatProvider,
ChatPrefsProvider,
ChatStreamValueProvider,
ChatTaskValueProvider,
DetachedChatContext,
ActiveTaskProvider,
ThreadlessChatProvider,
Expand Down
10 changes: 8 additions & 2 deletions apps/web/src/components/layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,13 @@ function useLayout() {
function LayoutRoot({
children,
notice,
outsideOrg,
}: {
children: ReactNode;
notice?: ReactNode;
/** Outside any org (`/site-editor`): no org apps to record, and the org
* rail only when `rail` (a signed-in person, whose orgs it lists). */
outsideOrg?: { rail: boolean };
}) {
const isMobile = useIsMobile();

Expand All @@ -56,8 +60,10 @@ function LayoutRoot({
<div className="app-shell-root flex flex-col h-dvh overflow-hidden">
{notice}
<div className="flex flex-1 flex-row min-h-0">
{projectFirstNav && <OpenAppRecorder />}
{!isMobile && projectFirstNav && <OrgRail />}
{projectFirstNav && !outsideOrg && <OpenAppRecorder />}
{!isMobile && projectFirstNav && (outsideOrg?.rail ?? true) && (
<OrgRail />
)}
<SidebarLayout
ref={resize.wrapperRef}
className="flex-1 bg-sidebar relative min-h-0"
Expand Down
8 changes: 7 additions & 1 deletion apps/web/src/hooks/use-preferences.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createContext, use } from "react";
import type { Locale } from "@/i18n/locale.ts";
import type { TranslationKey } from "@/i18n/en/index.ts";
import { useLocalStorage } from "./use-local-storage.ts";
Expand Down Expand Up @@ -113,7 +114,12 @@ export function usePreferences() {
);
}

/** Set by `/site-editor`, which always renders in the New Layout, whatever
* this person's preference says. Scoped to that route's tree. */
export const ForceProjectFirstNav = createContext(false);

export function useProjectFirstNav(): boolean {
const forced = use(ForceProjectFirstNav);
const [preferences] = usePreferences();
return preferences.projectFirstNav;
return forced || preferences.projectFirstNav;
}
3 changes: 3 additions & 0 deletions apps/web/src/hooks/use-route-default-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ declare module "@tanstack/react-router" {
mainView?: string;
/** The nested Site Editor surface owned by the matched leaf route. */
siteEditorView?: "preview" | "content" | "code";
/** Served by a tool on this machine, outside any org and project
* (`/site-editor`): the app is open, but there is no project to own it. */
local?: boolean;
}
}

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/layouts/route-page-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export function RoutePageHeader({
/** An app takes the sidebar's place, so its own thread toggle — normally in
* the sidebar header — goes with it. Without this, an app with the chat
* closed has no way back into it. */
const chatToggle = takeover && chatLayout && (
const chatToggle = takeover && chatLayout && !chatLayout.threadless && (
<Tooltip>
<TooltipTrigger asChild>
<ToolbarIconButton
Expand Down
40 changes: 40 additions & 0 deletions apps/web/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,45 @@ const chooseEditorRoute = createRoute({
),
});

// The site editor over `deco serve`, which prints
// `/site-editor#endpoint=…&token=…`. No org, no project, no sign-in needed.
// Its tabs carry the project Site Editor's `staticData` (plus `local`): the
// same app. Always in the New Layout, whatever the preference says.
// It wins over `/$org`, so an organization whose slug is `site-editor` would
// lose its URL. PRE-DEPLOY CHECK (every environment, before this route first
// ships): `SELECT id FROM organization WHERE slug = 'site-editor'` must return
// no rows; rename that organization first if it does.
const siteEditorRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/site-editor",
staticData: { pageTitle: "sidebar.projectNav.siteEditor" },
component: lazyRouteComponent(() => import("./routes/site-editor.tsx")),
});

const siteEditorPreviewRoute = createRoute({
getParentRoute: () => siteEditorRoute,
path: "/",
staticData: {
mainView: "site-editor",
siteEditorView: "preview",
local: true,
},
component: lazyRouteComponent(
() => import("./routes/site-editor.tsx"),
"SiteEditorPreview",
),
});

const siteEditorContentRoute = createRoute({
getParentRoute: () => siteEditorRoute,
path: "/content",
staticData: { mainView: "content", siteEditorView: "content", local: true },
component: lazyRouteComponent(
() => import("./routes/site-editor.tsx"),
"SiteEditorContent",
),
});

// Public report, readable without a session — hence outside the org shell.
const reportRoute = createRoute({
getParentRoute: () => rootRoute,
Expand Down Expand Up @@ -1612,6 +1651,7 @@ const routeTree = rootRoute.addChildren([
reportsOnboardingRoute,
legacyCommerceOnboardingRoute,
chooseEditorRoute,
siteEditorRoute.addChildren([siteEditorPreviewRoute, siteEditorContentRoute]),
reportRoute,
loginRoute,
cliAuthSuccessRoute,
Expand Down
Loading
Loading