257 lines
13 KiB
TypeScript
257 lines
13 KiB
TypeScript
import { type Component, type JSX, createSignal, createEffect, Show, lazy, Suspense, onMount, onCleanup } from "solid-js";
|
|
import { Sidebar, BottomNav, ContextSwitcher } from "./Navigation";
|
|
import { store, setStore, activeTaskId, setActiveTaskId, requestImmediateNotesMetadataLoad } from "@/store";
|
|
import { PanelLeftOpen, PanelLeftClose } from "lucide-solid";
|
|
|
|
const TaskDetail = lazy(() => import("./TaskDetail").then(m => ({ default: m.TaskDetail })));
|
|
const QuickEntry = lazy(() => import("./QuickEntry").then(m => ({ default: m.QuickEntry })));
|
|
const NotepadView = lazy(() => import("../views/NotepadView").then(m => ({ default: m.NotepadView })));
|
|
import { NotesSidebar } from "./NotesSidebar";
|
|
import { Button } from "./ui/button";
|
|
import { cn } from "@/lib/utils";
|
|
import { TaskSearchPopover } from "./TaskSearchPopover";
|
|
|
|
interface LayoutProps {
|
|
children: JSX.Element;
|
|
currentView: string;
|
|
setView: (v: string) => void;
|
|
}
|
|
|
|
export const Layout: Component<LayoutProps> = (props) => {
|
|
const [isSidebarLocked, setIsSidebarLocked] = createSignal(true);
|
|
const [isSidebarPeeking, setIsSidebarPeeking] = createSignal(false);
|
|
const [isDropdownOpen, setIsDropdownOpen] = createSignal(false);
|
|
|
|
// Lift selectedNoteId up to Layout so it can be passed to NotesSidebar and NotepadView
|
|
const [selectedNoteId, setSelectedNoteId] = createSignal<string | null>(null);
|
|
|
|
// Make it available globally for QuickEntry auto-tagging without circular deps
|
|
createEffect(() => {
|
|
(window as any)._activeNoteId = selectedNoteId();
|
|
});
|
|
|
|
onMount(() => {
|
|
const handleOpenNote = (e: Event) => {
|
|
const detail = (e as CustomEvent).detail;
|
|
if (detail && detail.noteId) {
|
|
setStore('isNotepadMode', true);
|
|
setSelectedNoteId(detail.noteId);
|
|
}
|
|
};
|
|
window.addEventListener('open-note', handleOpenNote);
|
|
onCleanup(() => window.removeEventListener('open-note', handleOpenNote));
|
|
|
|
// Preload heavy components in background when idle
|
|
const idleCallback = (window as any).requestIdleCallback || ((cb: any) => setTimeout(cb, 2000));
|
|
idleCallback(() => {
|
|
import("./TaskDetail");
|
|
import("./QuickEntry");
|
|
import("./TaskEditor");
|
|
import("../views/NotepadView");
|
|
});
|
|
});
|
|
|
|
// Derived active task for the detail view
|
|
const activeTask = () => store.tasks.find(t => t.id === activeTaskId());
|
|
|
|
createEffect(() => {
|
|
if (store.isNotepadMode) {
|
|
void requestImmediateNotesMetadataLoad();
|
|
}
|
|
});
|
|
|
|
createEffect(() => {
|
|
const noteId = selectedNoteId();
|
|
if (!noteId) return;
|
|
const noteExists = store.notes.some(note => note.id === noteId);
|
|
if (!noteExists) {
|
|
setSelectedNoteId(null);
|
|
}
|
|
});
|
|
|
|
return (
|
|
<div class="flex h-screen w-full min-w-0 bg-background overflow-hidden relative font-sans antialiased text-foreground">
|
|
{/* Desktop UI Container */}
|
|
<div
|
|
style={{ "will-change": "width, transform" }}
|
|
class={cn(
|
|
"hidden md:flex flex-col border-r border-border bg-card h-screen fixed left-0 top-0 transition-all duration-200 ease-in-out z-[50] overflow-hidden",
|
|
isSidebarLocked() ? (store.isNotepadMode ? "w-80 translate-x-0" : "w-48 translate-x-0") : (store.isNotepadMode ? "w-80 -translate-x-full" : "w-48 -translate-x-full"),
|
|
!isSidebarLocked() && (isSidebarPeeking() || (store.isNotepadMode && !selectedNoteId())) && "translate-x-0 shadow-2xl ring-1 ring-border"
|
|
)}
|
|
onMouseEnter={() => !isSidebarLocked() && setIsSidebarPeeking(true)}
|
|
onMouseLeave={() => {
|
|
if (!isSidebarLocked() && !isDropdownOpen()) {
|
|
setIsSidebarPeeking(false);
|
|
}
|
|
}}>
|
|
|
|
<div class="relative flex-1 flex flex-col min-h-0">
|
|
{/* Task Sidebar Wrapper */}
|
|
<div
|
|
style={{ "will-change": "opacity, transform" }}
|
|
class={cn(
|
|
"absolute inset-0 flex flex-col transition-all duration-200 ease-in-out",
|
|
store.isNotepadMode ? "opacity-0 pointer-events-none -translate-x-8" : "opacity-100 translate-x-0"
|
|
)}>
|
|
<Sidebar
|
|
currentView={props.currentView}
|
|
setView={props.setView}
|
|
isLocked={isSidebarLocked()}
|
|
setIsLocked={setIsSidebarLocked}
|
|
isPeeking={isSidebarPeeking()}
|
|
setIsPeeking={setIsSidebarPeeking}
|
|
isEmbed={true}
|
|
onDropdownOpenChange={setIsDropdownOpen}
|
|
/>
|
|
</div>
|
|
|
|
{/* Notes Sidebar Wrapper */}
|
|
<div
|
|
style={{ "will-change": "opacity, transform" }}
|
|
class={cn(
|
|
"absolute inset-0 flex flex-col transition-all duration-200 ease-in-out bg-card",
|
|
!store.isNotepadMode ? "opacity-0 pointer-events-none translate-x-8" : "opacity-100 translate-x-0"
|
|
)}>
|
|
<div class={cn("p-4 flex items-center justify-between transition-all duration-200", !isSidebarLocked() && "pl-14")}>
|
|
<h1 class={cn(
|
|
"text-xl font-bold tracking-tighter text-foreground transition-all duration-200",
|
|
(isSidebarLocked() || isSidebarPeeking())
|
|
? "opacity-100 translate-x-0"
|
|
: "opacity-0 -translate-x-4"
|
|
)}>
|
|
Tasgrid
|
|
</h1>
|
|
<Show when={isSidebarLocked()}>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
class="h-8 w-8 text-muted-foreground hover:text-foreground"
|
|
onClick={() => {
|
|
setIsSidebarLocked(false);
|
|
setIsSidebarPeeking(false);
|
|
}}
|
|
>
|
|
<PanelLeftClose size={16} />
|
|
</Button>
|
|
</Show>
|
|
</div>
|
|
|
|
<div class={cn("flex-1 overflow-hidden transition-all duration-200", (!isSidebarLocked() && !isSidebarPeeking() && !(store.isNotepadMode && !selectedNoteId())) ? "opacity-0 pointer-events-none scale-95" : "opacity-100 scale-100")}>
|
|
<NotesSidebar selectedNoteId={selectedNoteId()} setSelectedNoteId={setSelectedNoteId} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
style={{ "will-change": "padding-left" }}
|
|
class={cn(
|
|
"flex-1 flex flex-col min-w-0 transition-all duration-200 ease-in-out h-screen",
|
|
// Only apply md padding if locked
|
|
isSidebarLocked() ? (store.isNotepadMode ? "md:pl-80" : "md:pl-48") : "md:pl-0"
|
|
)}>
|
|
{/* Expand Trigger Button (visible when not locked) */}
|
|
{!isSidebarLocked() && (
|
|
<div class="fixed top-4 left-4 z-[60] md:flex hidden">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onMouseEnter={() => setIsSidebarPeeking(true)}
|
|
onClick={() => setIsSidebarLocked(true)}
|
|
class="bg-background/80 backdrop-blur-sm border border-border hover:bg-muted shadow-sm"
|
|
>
|
|
<PanelLeftOpen size={18} />
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Mobile Context Switcher */}
|
|
<div class="fixed top-4 left-4 z-[60] md:hidden">
|
|
<ContextSwitcher isMobile={true} />
|
|
</div>
|
|
|
|
{/* Mode Switcher */}
|
|
<div class="fixed top-4 left-1/2 -translate-x-1/2 z-[60]">
|
|
<div class="bg-background/80 backdrop-blur-sm border border-border p-1 rounded-xl flex items-center shadow-lg relative min-w-[160px] h-10">
|
|
{/* Sliding Indicator */}
|
|
<div
|
|
style={{ "will-change": "transform" }}
|
|
class={cn(
|
|
"absolute top-1 bottom-1 w-[calc(50%-4px)] bg-primary rounded-lg shadow-sm transition-transform duration-200 ease-out z-0",
|
|
store.isNotepadMode ? "translate-x-[calc(100%+0px)]" : "translate-x-0"
|
|
)} />
|
|
|
|
<button
|
|
class={cn(
|
|
"flex-1 h-8 text-[0.7rem] font-bold uppercase tracking-widest z-10 transition-colors duration-200 flex items-center justify-center rounded-lg",
|
|
!store.isNotepadMode ? "text-primary-foreground" : "text-muted-foreground hover:text-foreground"
|
|
)}
|
|
onClick={() => setStore('isNotepadMode', false)}
|
|
>
|
|
Tasks
|
|
</button>
|
|
<button
|
|
class={cn(
|
|
"flex-1 h-8 text-[0.7rem] font-bold uppercase tracking-widest z-10 transition-colors duration-200 flex items-center justify-center rounded-lg",
|
|
store.isNotepadMode ? "text-primary-foreground" : "text-muted-foreground hover:text-foreground"
|
|
)}
|
|
onClick={() => setStore('isNotepadMode', true)}
|
|
>
|
|
Notes
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sync Progress Bar Overlay */}
|
|
<Show when={store.isInitializing || (store.isNotepadMode && store.isNotesLoading)}>
|
|
<div class="fixed top-0 left-0 w-full h-0.5 z-[100] overflow-hidden bg-primary/10">
|
|
<div class="h-full bg-primary animate-progress-fast w-1/3" />
|
|
</div>
|
|
</Show>
|
|
|
|
{/* Main Content Area */}
|
|
<main class={cn(
|
|
"flex-1 min-w-0 overflow-y-auto overflow-x-hidden relative scroll-smooth",
|
|
!store.isNotepadMode ? "sm:pb-6" : ""
|
|
)}>
|
|
<div class={cn(
|
|
"w-full min-h-full mx-auto",
|
|
!store.isNotepadMode ? "max-w-screen-2xl px-4 sm:px-8 lg:px-12 pt-16 sm:pt-20 mobile-footer-clearance space-y-6 sm:space-y-8" : "max-w-screen-2xl md:px-8 lg:px-12 md:pt-20 md:pb-8 pt-14"
|
|
)}>
|
|
<div class={cn(store.isNotepadMode ? "hidden" : "block")}>
|
|
<Show when={props.currentView.toLowerCase() !== "settings"}>
|
|
<TaskSearchPopover />
|
|
</Show>
|
|
{props.children}
|
|
</div>
|
|
<div class={cn(!store.isNotepadMode ? "hidden" : "block", "h-[calc(100vh-3.5rem)] md:h-[calc(100vh-8rem)] w-full")}>
|
|
<Suspense>
|
|
<NotepadView selectedNoteId={selectedNoteId()} setSelectedNoteId={setSelectedNoteId} />
|
|
</Suspense>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
{/* Mobile Bottom Nav */}
|
|
<div class={cn("md:hidden", store.isNotepadMode ? "hidden" : "block")}>
|
|
<BottomNav currentView={props.currentView} setView={props.setView} />
|
|
</div>
|
|
</div>
|
|
|
|
<Suspense>
|
|
<QuickEntry />
|
|
</Suspense>
|
|
|
|
<Show when={activeTask()}>
|
|
<Suspense>
|
|
<TaskDetail
|
|
task={activeTask()!}
|
|
isOpen={!!activeTaskId()}
|
|
onClose={() => setActiveTaskId(null)}
|
|
/>
|
|
</Suspense>
|
|
</Show>
|
|
</div >
|
|
);
|
|
};
|