feat: IDE shell, file management, UI rehaul, reliability, and git workflow
This is the next major chunk on top of the barebones MVP. It implements
the IDE shell, resizable editor/preview workspace, file CRUD, Git workflow
UI upgrade, SyncTeX/LSP reliability hardening, and documentation updates.
Highlights:
* New shared path utilities (typst-leaf-frontend/src/path-utils.ts):
normalizeFsPath, encodeVirtualPathForFileUri, relativePathFromProject.
* SyncTeX reliability:
* Jump targets now carry stable numeric IDs for deduplication.
* tinymist/preview/scrollSource and window/showDocument payloads are
validated and normalized through shared path utilities.
* External preview paths are reported via a toast instead of silent drop.
* Editor uses monaco.Uri.equals for URI comparison.
* IDE shell rehaul:
* New components: TopBar, StatusBar, PanelSection, ResizableSplit,
ToastHost, EmptyState.
* App.tsx now uses a layered layout: top bar, sidebar tabs
(Files/Git/Settings), resizable main workspace, status bar, toasts.
* Sidebar tabs replace the old inline Git/settings panels.
* Resizable workspace:
* Pointer-driven resizable split between editor and preview.
* Split ratio persisted to localStorage (typst-leaf.split).
* Double-click splitter resets to 55%.
* onResize prop dispatches typst-leaf:layout so Monaco re-layouts.
* File management:
* Backend CRUD: POST /file, DELETE /file, PATCH /file, POST /folder.
* Frontend API wrappers and inline FileTree create/rename/delete.
* Backend tree endpoint returns empty directories in a folders array.
* Rename protects the .typ extension and rejects overwriting existing
files atomically on POSIX.
* Newly created files are opened automatically.
* Dirty file badges in the tree.
* Git workflow upgrade:
* GitPanel now shows changed files grouped by staged/unstaged, deduped
by path, with per-file status icons.
* Click a changed file to open it.
* Commit disabled when clean or no message.
* Push/pull with success/error toasts.
* FileTree shows dirty badges from git status.
* Preview UX:
* Preview toolbar with status chip, reload button, and SyncTeX hint.
* Preview hidden/marked as "open a file" when no file is open.
* State/UX infrastructure (app-state.tsx):
* activePanel, fileDirty, saving, notice (toast), vimOn, treeVersion,
folders, and stable jumpTarget.id.
* focusCommit switches to the Git panel.
* selectProject resets more state to avoid stale data.
* Editor improvements:
* Monaco model disposed on unmount so LSP sees didClose.
* mountedRef survives React StrictMode remounts.
* Global save/layout events bridge TopBar and ResizableSplit to the
editor instance.
* Vim mode state moved to global state/localStorage.
* Removed redundant automaticLayout and inline Save button.
* Documentation:
* Added DESIGN.md with the full UI aesthetic guide.
* Updated PLAN.md, README.md, VISION.md, and the frontend README.
Verification:
* pnpm -r typecheck passes.
* pnpm --filter typst-leaf-frontend build passes.
This commit is contained in:
+105
-29
@@ -4,41 +4,117 @@ import { FileTree } from "./components/FileTree";
|
||||
import { GitPanel } from "./components/GitPanel";
|
||||
import { Preview } from "./components/Preview";
|
||||
import { ProjectPicker } from "./components/ProjectPicker";
|
||||
import { TopBar } from "./components/TopBar";
|
||||
import { StatusBar } from "./components/StatusBar";
|
||||
import { ToastHost } from "./components/ToastHost";
|
||||
import { EmptyState } from "./components/EmptyState";
|
||||
import { PanelSection } from "./components/PanelSection";
|
||||
import { SettingsPanel } from "./components/SettingsPanel";
|
||||
import { ResizableSplit } from "./components/ResizableSplit";
|
||||
|
||||
function Shell() {
|
||||
const { state, lsp } = useApp();
|
||||
const { state, dispatch } = useApp();
|
||||
|
||||
return (
|
||||
<div className="h-screen w-screen bg-white text-zinc-800 flex">
|
||||
<aside className="w-60 border-r border-zinc-200 flex flex-col bg-zinc-50">
|
||||
<div className="px-3 py-2 border-b border-zinc-200 text-sm font-semibold flex items-center gap-2">
|
||||
<span>typst-leaf</span>
|
||||
<span className="ml-auto flex items-center gap-2 text-xs font-normal text-zinc-400">
|
||||
<SettingsPanel />
|
||||
{lsp.status === "ready"
|
||||
? "●"
|
||||
: lsp.status === "connecting"
|
||||
? "◌"
|
||||
: lsp.status === "error"
|
||||
? "✕"
|
||||
: "○"}
|
||||
</span>
|
||||
<div className="h-screen w-screen bg-stone-50 text-zinc-800 flex flex-col overflow-hidden">
|
||||
<TopBar />
|
||||
|
||||
{state.error && (
|
||||
<div className="bg-rose-50 text-rose-700 text-xs px-4 py-1.5 border-b border-rose-200 shrink-0">
|
||||
{state.error}
|
||||
</div>
|
||||
<ProjectPicker />
|
||||
<div className="flex-1 overflow-auto">
|
||||
<FileTree />
|
||||
</div>
|
||||
<GitPanel />
|
||||
{state.error && (
|
||||
<div className="p-2 text-xs text-red-500 border-t border-zinc-200">
|
||||
{state.error}
|
||||
)}
|
||||
|
||||
<div className="flex flex-1 min-h-0">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-64 border-r border-zinc-200 bg-stone-50 flex flex-col min-h-0 shrink-0">
|
||||
<ProjectPicker />
|
||||
|
||||
{/* Sidebar tabs */}
|
||||
<div className="flex border-b border-zinc-200 text-xs">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => dispatch({ type: "setActivePanel", panel: "files" })}
|
||||
className={`flex-1 py-1.5 text-center uppercase tracking-wider ${
|
||||
state.activePanel === "files"
|
||||
? "text-emerald-700 border-b-2 border-emerald-500 bg-white"
|
||||
: "text-zinc-400 hover:text-zinc-600 hover:bg-stone-100"
|
||||
}`}
|
||||
>
|
||||
Files
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => dispatch({ type: "setActivePanel", panel: "git" })}
|
||||
className={`flex-1 py-1.5 text-center uppercase tracking-wider ${
|
||||
state.activePanel === "git"
|
||||
? "text-emerald-700 border-b-2 border-emerald-500 bg-white"
|
||||
: "text-zinc-400 hover:text-zinc-600 hover:bg-stone-100"
|
||||
}`}
|
||||
>
|
||||
Git
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => dispatch({ type: "setActivePanel", panel: "settings" })}
|
||||
className={`flex-1 py-1.5 text-center uppercase tracking-wider ${
|
||||
state.activePanel === "settings"
|
||||
? "text-emerald-700 border-b-2 border-emerald-500 bg-white"
|
||||
: "text-zinc-400 hover:text-zinc-600 hover:bg-stone-100"
|
||||
}`}
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
<main className="flex-1 flex min-w-0">
|
||||
<EditorView />
|
||||
<Preview />
|
||||
</main>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
{state.activePanel === "files" && (
|
||||
<PanelSection title="Files">
|
||||
{state.currentProject ? <FileTree /> : (
|
||||
<div className="px-3 py-2 text-xs text-zinc-400">select a project</div>
|
||||
)}
|
||||
</PanelSection>
|
||||
)}
|
||||
{state.activePanel === "git" && <GitPanel />}
|
||||
{state.activePanel === "settings" && <SettingsPanel />}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main workspace */}
|
||||
<main className="flex-1 flex min-w-0 bg-white">
|
||||
{state.currentProject && state.openFile ? (
|
||||
<ResizableSplit
|
||||
left={<EditorView />}
|
||||
right={<Preview />}
|
||||
onResize={() => {
|
||||
document.dispatchEvent(new CustomEvent("typst-leaf:layout"));
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex-1 flex min-h-0">
|
||||
{!state.currentProject ? (
|
||||
<div className="flex-1 flex">
|
||||
<div className="flex-1">
|
||||
<EmptyState message="Select a project to begin." hint="Use the sidebar to pick or create a project." />
|
||||
</div>
|
||||
</div>
|
||||
) : !state.openFile ? (
|
||||
<div className="flex-1 flex">
|
||||
<div className="flex-1">
|
||||
<EmptyState message="Open a file from the sidebar." hint=".typ files are listed under the Files section." />
|
||||
</div>
|
||||
<div className="w-96 border-l border-zinc-200">
|
||||
<Preview />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<StatusBar />
|
||||
<ToastHost />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user