Files
typst-leaf/typst-leaf-frontend/src/components/TopBar.tsx
T
nav.sikand 008dab1cf9 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.
2026-07-02 02:05:04 +05:30

106 lines
4.2 KiB
TypeScript

import { useApp } from "../app-state";
export function TopBar() {
const { state, dispatch, lsp } = useApp();
const lspChip = () => {
switch (lsp.status) {
case "ready":
return (
<span className="text-xs px-2 py-0.5 rounded-full bg-emerald-50 text-emerald-700 border border-emerald-200 flex items-center gap-1">
<span className="inline-block size-1.5 rounded-full bg-emerald-500" />
LSP
</span>
);
case "connecting":
return (
<span className="text-xs px-2 py-0.5 rounded-full bg-amber-50 text-amber-700 border border-amber-200 flex items-center gap-1">
<span className="inline-block size-1.5 rounded-full bg-amber-400 animate-pulse" />
LSP
</span>
);
case "error":
return (
<span className="text-xs px-2 py-0.5 rounded-full bg-rose-50 text-rose-700 border border-rose-200 flex items-center gap-1">
<span className="inline-block size-1.5 rounded-full bg-rose-500" />
LSP
</span>
);
default:
return null;
}
};
const previewChip = () => {
// No preview without an open file (tinymist previews a document).
if (!state.openFile) return null;
if (lsp.previewReady) {
return (
<span className="text-xs px-2 py-0.5 rounded-full bg-stone-100 text-zinc-600 border border-zinc-200 flex items-center gap-1">
<span className="inline-block size-1.5 rounded-full bg-emerald-500" />
Preview
</span>
);
}
if (lsp.status === "ready") {
return (
<span className="text-xs px-2 py-0.5 rounded-full bg-stone-100 text-zinc-400 border border-zinc-200 flex items-center gap-1">
<span className="inline-block size-1.5 rounded-full bg-zinc-300" />
Preview · starting
</span>
);
}
return null;
};
return (
<header className="flex items-center gap-3 border-b border-zinc-200 bg-white px-4 h-11 shrink-0">
<span className="font-semibold tracking-tight text-zinc-800">typst-leaf</span>
<span className="text-zinc-300 select-none">·</span>
{state.currentProject ? (
<>
<span className="text-sm text-zinc-800 font-medium truncate max-w-36">
{state.currentProject}
</span>
{state.openFile && (
<>
<span className="text-zinc-300 select-none">/</span>
<span className="text-sm font-mono text-zinc-500 truncate max-w-48">
{state.openFile}
{state.fileDirty && <span className="text-amber-500 ml-0.5">*</span>}
</span>
</>
)}
<div className="ml-auto flex items-center gap-2">
{lspChip()}
{previewChip()}
<button
type="button"
onClick={() => {
document.dispatchEvent(new CustomEvent("typst-leaf:save"));
}}
disabled={!state.openFile}
className="px-3 py-1 text-sm rounded bg-emerald-600 hover:bg-emerald-700 text-white disabled:opacity-40 disabled:cursor-not-allowed"
>
Save
</button>
<button
type="button"
onClick={() => dispatch({ type: "setActivePanel", panel: state.activePanel === "settings" ? "files" : "settings" })}
className={`p-1.5 rounded-md ${state.activePanel === "settings" ? "text-emerald-700 bg-emerald-50" : "text-zinc-500 hover:bg-stone-100"}`}
title="Settings"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="size-4">
<path d="M10 3.75a2.25 2.25 0 0 0-4.312.75H2.5a.75.75 0 0 0 0 1.5h3.188A2.25 2.25 0 0 0 10 6.25a2.25 2.25 0 0 0 4.312-.75h3.188a.75.75 0 0 0 0-1.5h-3.188A2.25 2.25 0 0 0 10 3.75zM10 13.75a2.25 2.25 0 0 0-4.312.75H2.5a.75.75 0 0 0 0 1.5h3.188A2.25 2.25 0 0 0 10 16.25a2.25 2.25 0 0 0 4.312.75h3.188a.75.75 0 0 0 0-1.5h-3.188A2.25 2.25 0 0 0 10 13.75z" />
</svg>
</button>
</div>
</>
) : (
<span className="text-sm text-zinc-400">Select a project to begin</span>
)}
</header>
);
}