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:
2026-07-02 02:05:04 +05:30
parent 7618476b60
commit 008dab1cf9
24 changed files with 1727 additions and 331 deletions
+179 -59
View File
@@ -1,80 +1,200 @@
# Next Chunk: Git + Vim + SyncTeX (Core VISION)
# Next Chunk: IDE Shell, File Management, UI Rehaul, Reliability, Git Workflow
## Part A — Vim Mode (frontend-only)
This chunk is the next deliverable on top of the `bfb4d8b` commit. It picks **options 1, 2, and 3**: IDE shell + file management + UI rehaul, reliability and LSP/Preview hardening, and Git workflow upgrade. Direction and aesthetic decisions live in `DESIGN.md` — every UI decision below should read against that doc.
**Goal:** Off-by-default vim toggle, persisted to localStorage.
## Guiding Constraints
- `Editor.tsx`:
- Always render `<div ref={vimStatusRef} />` (empty when vim is off, monaco-vim populates it when on).
- On `handleMount`: if `localStorage.getItem("typst-leaf.vim") === "true"`, `initVimMode(editor, vimStatusRef.current)`.
- Add "Vim" toggle button in the editor toolbar (emerald when active).
- On toggle: persist to localStorage; init or dispose accordingly.
- Store `initVimMode`'s returned controller in a ref for disposal on unmount/toggle-off.
- Tailwind only, light theme only, no dark variant.
- Browser-first; Tauri APIs only behind `window.__TAURI__` guards.
- No databases; filesystem + `.git` remain the source of truth.
- No polling for file or preview changes.
- Backend stays a thin proxy. No Typst or LSP reimplementation in TypeScript.
- `tinymist` remains the LSP/preview source of truth.
- Keep the raw JSON-RPC LSP client. Do not reintroduce `monaco-languageclient`.
- Strict TypeScript with zod at API boundaries.
## Part B — Git
## Aesthetic Direction (see DESIGN.md)
- Document-editor tone: warm paper background, subtle emerald accents, no marketing gradients.
- Dense-but-readable developer layout. System sans for chrome, monospace for paths/git.
- Rounded corners and borders restrained to "panel" and "card"; default to `rounded-md`.
- Status indicators must read clearly (label + color, never color alone).
- Splitter, focus rings, etc. all designed in DESIGN.md; the implementation must match.
## Phase 1 — Path Utilities and Jump Reliability
Move path/jump helpers into a single module so all callers use the same normalization rules.
**New file**
- `typst-leaf-frontend/src/path-utils.ts`:
- `normalizeFsPath(input: string): string`
- `encodeVirtualPathForFileUri(path: string): string`
- `relativePathFromProject(projectUri: string, filepath: string): string | null`
**State changes** (`app-state.tsx`)
- `jumpTarget: { id: number; file: string; start?: [number,number]; end?: [number,number] } | null`
- `setJumpTarget` action carries an `id`; reducer increments counter and stamps.
- New `pushNotice` action for transient messages ("Preview source outside project").
**LSP hardening** (`lsp.ts`)
- Validate `tinymist/preview/scrollSource` payload strictly: `filepath` string, optional numeric `start/end`.
- Forward only well-formed paths to `onJumpToSource`.
**Editor** (`Editor.tsx`)
- Use shared path utility.
- Compare URIs via `monaco.Uri.equals` after parsing both sides.
- Dedupe by `jump.id`, not object identity.
- Keep the existing `ResizeObserver` and `editor.layout()` call before reveal.
**Verification**
- External preview path ignored with non-blocking message.
- Numeric start/end coercion.
- Rapid clicks keep latest, no swallowed targets.
## Phase 2 — UI Foundation (Rehaul)
Replace the current sparse shell with the layered "IDE" layout in DESIGN.md.
**Files**
- `typst-leaf-frontend/src/App.tsx` (restructure)
- `typst-leaf-frontend/src/index.css` (extend `tailwindcss` import; nothing else without need)
- New components:
- `TopBar.tsx`
- `StatusBar.tsx`
- `Sidebar.tsx`
- `PanelSection.tsx`
- `ActivityBar.tsx`
- `ResizableSplit.tsx`
- `ToastHost.tsx`
- `EmptyState.tsx`
**Layout**
- Top bar: project switcher, current file/dirty indicator, save, LSP/preview/git chips, settings.
- Sidebar tabs: Files / Git / Settings.
- Main: resizable editor / preview split.
- Status bar: branch, clean/dirty, LSP, preview, vim state, cursor (optional).
**Aesthetic tokens** (mirror DESIGN.md)
- `bg-stone-50` for chrome, `bg-white` for editor surface, `bg-emerald-600` for primary actions, `border-zinc-200` for panel dividers, `text-zinc-500` for muted metadata.
- Tailwind reference snippets are kept in DESIGN.md.
**QOL**
- Toasts for save/commit/push/pull, configured via `pushNotice`.
- Clear empty states ("Select a project", "Open a file", "No changes to commit").
- Keyboard shortcuts surfaced in `StatusBar` hints later if time.
## Phase 3 — Resizable Workspace and Preview UX
- Build `ResizableSplit.tsx` (pointer-driven, double-click reset, persisted ratio).
- Persist `typst-leaf.split` in `localStorage`.
- Add `PreviewToolbar` on top of iframe: status chip, reload button, "Click to jump" hint.
- During drag and on drag-end, call `editor.layout()` so Monaco keeps accurate viewports.
## Phase 4 — File Management
Make projects usable from the UI.
### Backend
`typst-leaf-backend/src/routes/files.ts`:
- `POST /api/projects/:project/file`
- `DELETE /api/projects/:project/file?path=…`
- `PATCH /api/projects/:project/file`
- `POST /api/projects/:project/folder`
- `git.ts`: factory `gitFor(project: string)``simpleGit(projectDir(project))`.
- `routes/git.ts` (mounted at `/api/projects/:project/git`):
- `POST /init``git init`, set default identity from env, initial commit, `.gitignore`.
- `GET /status` — branch, ahead/behind, staged/unstaged, clean.
- `GET /log?limit=20` — commit history.
- `POST /commit` — body `{ message, all?, identity? }`. Uses `-c user.name=... -c user.email=...` per-commit flags (not repo config).
- `PUT /remote` — body `{ url }``git remote add` or `set-url`.
- `POST /push` / `POST /pull` — to origin, return `{ ok, error? }`.
- `routes/projects.ts`: `POST /:project` calls `git.init` after seeding `main.typ`.
- `index.ts`: mount gitRouter.
All paths through `resolveFile`/`relPathSchema`. zod validation stays.
### Frontend
`api.ts`:
- `createFile`, `deleteFile`, `renameFile`, `createFolder`.
- `api.ts`: gitInit, gitStatus, gitLog, gitCommit, gitPush, gitPull, setRemote.
- `types.ts`: FileStatus, CommitInfo, GitStatus.
- `GitPanel.tsx` (sidebar, below FileTree):
- Branch name, ahead/behind badges, unstaged file count.
- Inline commit: message input + Commit button (Enter to submit).
- Push/Pull buttons; prompts for remote URL if none configured.
- Refreshes on project open + after saves + after commits.
- `SettingsPanel.tsx` (gear icon in sidebar header toggles panel):
- Git Author Name, Git Author Email inputs.
- Persisted to `localStorage["typst-leaf.git-identity"]`.
- Sent with every commit as `identity`.
- `Editor.tsx`: `CtrlCmd+Shift+KeyS` → dispatch `commitFocus` via app state.
- `app-state.tsx`: `gitStatus`, `gitVersion` (incremented on save to trigger refresh), `commitFocus` (incremented on shortcut).
`types.ts`:
- Add request types.
## Part C — SyncTeX (Preview → Editor)
`FileTree.tsx`:
- Group tree view; folders from existing file paths.
- Inline create/rename/delete actions; confirm destructive deletes.
- After mutation: refresh tree, dispatch `bumpGitVersion`, open new file or close deleted open file (with model disposal).
### LSP Client (`lsp.ts`)
`app-state.tsx`:
- Action `setTree` already exists. Add `bumpFileTree` counter or reuse `setProjects` invalidation by re-fetching tree on a `treeVersion`.
Two LSP message paths from tinymist, handle both:
## Phase 5 — SyncTeX/LSP Reliability Hardening
1. **`tinymist/preview/scrollSource` notification**:
- Params: `{ filepath: string, start?: [number, number], end?: [number, number] }`
- Convert: `filepath` → relPath, `start/end` are 0-indexed [row, col]
- Call `onJumpToSource({ filepath, start, end })`
(See Phase 1.) This phase covers the user-visible behavior of the jump path:
2. **`window/showDocument` request**:
- Params: `{ uri: string, selection?: { start: {line, character}, end: {line, character} } }`
- Convert: `uri` (file://) → relPath, selection is 0-indexed LspRange
- Call `onJumpToSource(...)`, respond `{ success: true }`
- Stable IDs prevent dropped or duplicated jumps.
- External path is announced via toast instead of silently dropping.
- Vue of preview-after-resize: drag resize triggers `editor.layout()`. Preview iframe reload button re-syncs.
- A small dev-only "Diagnostics" panel hidden behind `?debug=1` later is optional.
### App State (`app-state.tsx`)
## Phase 6 — Git Workflow Upgrade
- Add `jumpTarget: { file: string; start?: [number, number]; end?: [number, number] } | null` to state.
- In `startLsp`, pass `onJumpToSource` callback:
1. Convert absolute `filepath` → relPath (strip `new URL(projectUri).pathname` prefix)
2. **Ignore if filepath is not under the project** (e.g. imported package file)
3. If `relPath !== state.openFile`, dispatch `openFile`
4. Dispatch `setJumpTarget({ file: relPath, start, end })`
Backend
- `GET /api/projects/:project/git/diff` returning unified diffs (later; not blocking if skipped).
- Optional `POST /api/projects/:project/git/stage` for partial staging (later).
### Editor (`Editor.tsx`)
Frontend
- `GitPanel.tsx` becomes a workflow panel:
- Branch + ahead/behind + clean state
- Changed files list grouped by staged/unstaged/untracked (icons per status)
- Commit composer
- Push/Pull controls
- Remote inline input
- Recent commits list (keep existing)
- `FileTree.tsx` shows dirty file badges.
- Toasts on success/failure: "Committed abc1234", "Pushed to origin/main", etc.
- Disable commit when clean; message should explain why.
- Effect deps: `[jumpTarget, model]`
- When `jumpTarget` is truthy AND `model` is the correct file:
- Convert 0-indexed [row, col] → Monaco 1-indexed (line = row + 1, column = col + 1)
- `editor.setSelection(range)`, `editor.revealRangeInCenter(range)`, focus editor
- Dispatch `clearJumpTarget`
`types.ts`:
- Confirm `FileStatusInfo` carries `A|M|D|?` (already does).
## Part D — Doc Cleanup
## Phase 7 — State and UX Infrastructure
- `AGENTS.md`: Remove stale `execa` (backend has simple-git, not execa). Update Architecture & Boundaries to reflect that frontend uses raw LSP, not `monaco-languageclient`. Fix stale `vscode-ws-jsonrpc` references (frontend removed it; backend still uses it).
Keep state in `app-state.tsx` (no Zustand yet). Add UI actions:
- `setActivePanel` (files | git | settings)
- `setFileDirty`
- `setSaving`
- `pushNotice` / `dismissNotice`
- `setSplitRatio` (effect persists via localStorage directly; not strict state)
Toast component lives once at `App.tsx`, reads notices via context.
## Phase 8 — Doc and Metadata Cleanup
- `README.md`: port 1420, raw LSP client, preview-via-LSP-and-proxy model.
- `VISION.md`: note raw LSP client; soften `monaco-languageclient` claim.
- `typst-leaf-frontend/README.md`: replace Tauri-template text.
- `PLAN.md`: replace with this plan at commit time. Keep `DESIGN.md` separate and referenced.
## Implementation Order
1. Shared path utilities + jump IDs.
2. SyncTeX hardening (rely on shared utils, not yet aesthetic).
3. UI shell rehaul (TopBar, Sidebar, ResizableSplit, StatusBar, ToastHost).
4. File CRUD backend + frontend integration, FileTree rebuild.
5. Git workflow UI rebuild + badges in tree.
6. Preview toolbar and reload.
7. Doc cleanup.
8. Verification:
- `pnpm -r typecheck`
- `pnpm --filter typst-leaf-frontend build`
- Manual checks: file CRUD, resize-during-jump, Git flow, external preview path ignored.
## Risks
- File URI/path encoding is still the highest-risk area; reuse existing helpers in `Editor.tsx`.
- Rename/delete of currently open file must dispose Monaco model.
- Git status can stale if a file mutation does not bump `gitVersion`.
- Resizable panes must call `layout()` during and after drag.
- Preview iframe behavior is owned by typst-preview; do not over-customize it.
## Scope Boundary
In:
- IDE shell rehaul, resizable split, file CRUD, Git workflow UI upgrade, SyncTeX reliability, toasts, status bar.
Out:
- Per-file staging workflow.
- Rich diff viewer.
- Drag/drop reordering, multi-tab editor, collaborative cursors.
- Tauri native file picker.