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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user