008dab1cf9
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.
8.0 KiB
8.0 KiB
DESIGN.md — typst-leaf UI Rehaul
This document is the aesthetic reference for the next chunk in PLAN.md. Every UI decision in PLAN.md should match this guide. Direction is "document editor" — warmth, restraint, a thin layer of leaf.
Brand
- Name: typst-leaf — warm, organic, paper-and-ink.
- Personality: quiet, considered, a serious editor for prose, papers, and beautiful documents.
- Not "developer marketing." Not "dashboard SaaS." Closer to a focused notebook.
Aesthetic Pillars
- Paper, not glass. Surfaces are warm off-white, not slate. Borders separate panels; we don't blur or float.
- Functional color, decorative restraint. Greens only when communicating success/active/leaf. Use amber for warnings, slate for muted text, rose for errors. No gradients on chrome.
- Typography is the interface. System sans for chrome, monospace for paths/git/lsp status. We say things in words, not icons.
- A whisper, not a shout.
Rounded corners are subtle (
rounded-md). Focus rings are visible but quiet.
Palette (Tailwind v4 via CSS-first)
| Token | Use | Tailwind class |
|---|---|---|
| Page background | Shell chrome | bg-stone-50 |
| Panel surface | Cards, panels | bg-white |
| Sidebar | Activity + sections | bg-stone-50 with inner panels bg-white |
| Text primary | Body | text-zinc-800 |
| Text muted | Meta | text-zinc-500 |
| Text subtle | File paths | text-zinc-400 |
| Divider | 1px borders | border-zinc-200 |
| Hover surface | Buttons/list rows | hover:bg-stone-100 |
| Accent (primary) | Save, commit | bg-emerald-600 hover:bg-emerald-700 text-white |
| Accent (soft) | Active file, focus | bg-emerald-50 text-emerald-800 ring-emerald-200 |
| Warning | Behind | text-amber-600 |
| Error | Dirty error | text-rose-600 |
| Pill bg (status) | Status chips | bg-white border-zinc-200 |
We do not introduce dark classes. Tailwind v4 is in CSS-first mode (@import "tailwindcss"); tokens live in plain CSS, not tailwind.config.js.
Typography
- UI:
font-sans(system stack already from Tailwind). - Monospace:
font-monofor paths, git refs, LSP logs, status messages. - Toolbar/header:
text-sm font-medium tracking-tight. - Meta and toasts:
text-xs text-zinc-500. - File names:
text-sm font-mono.
Layout Skeleton
┌──────────────────────────────────────────────────────────────────────┐
│ TopBar: leaf · [project ▾] · main.typ · Save · ●LSP ◌PRV │
├──────────┬───────────────────────────────────────────────────────────┤
│ Sidebar │ │
│ Files │ │
│ ▣ main │ Monaco Editor │
│ ▣ lib/ │ │
│ Git │ │
│ ⚙ set │ │
│ ├─────────────────────────────────────┬─────────────────────┤
│ │ Editor (resizable) │ Preview │
│ │ │ (iframe) │
│ │ │ │
└──────────┴─────────────────────────────────────┴─────────────────────┘
│ StatusBar: main · clean · ready · vim:n · 1:23 │
└──────────────────────────────────────────────────────────────────────┘
Component Specs (Tailwind)
TopBar
<header class="flex items-center gap-3 border-b border-zinc-200 bg-white px-4 h-11">
<span class="font-semibold tracking-tight">typst-leaf</span>
<span class="text-zinc-300">·</span>
<button class="text-sm hover:bg-stone-100 px-2 py-1 rounded-md">project ▾</button>
<span class="ml-2 text-sm font-mono text-zinc-500 truncate">main.typ</span>
<span class="ml-auto flex items-center gap-2">
<span class="text-xs px-2 py-0.5 rounded-full bg-emerald-50 text-emerald-700 border border-emerald-200">LSP · ready</span>
<span class="text-xs px-2 py-0.5 rounded-full bg-stone-100 text-zinc-600 border border-zinc-200">PRV · starting</span>
<button class="text-sm bg-emerald-600 hover:bg-emerald-700 text-white rounded-md px-3 py-1">Save</button>
<button class="text-sm text-zinc-500 hover:bg-stone-100 px-2 py-1 rounded-md">⚙</button>
</span>
</header>
Sidebar (PanelSection + tabs)
<aside class="w-64 border-r border-zinc-200 bg-stone-50 flex flex-col min-h-0">
<div class="px-3 py-2 text-[11px] uppercase tracking-wider text-zinc-400">Files</div>
<nav class="flex-1 overflow-auto p-1">
<button class="w-full text-left text-sm px-2 py-1 rounded-md bg-emerald-50 text-emerald-800">main.typ</button>
<button class="w-full text-left text-sm px-2 py-1 rounded-md hover:bg-stone-100">lib/util.typ</button>
</nav>
<div class="border-t border-zinc-200 p-2 text-xs text-zinc-500">12 uncommitted</div>
</aside>
ResizableSplit
<div class="flex h-full min-h-0">
<div class="min-w-[280px]" style={{ width: `${left}%` }}>/* editor */</div>
<div
role="separator"
aria-orientation="vertical"
class="w-1 bg-zinc-200 hover:bg-emerald-300 cursor-col-resize"
onPointerDown={startDrag}
onDoubleClick={reset}
/>
<div class="flex-1 min-w-[280px]">/* preview */</div>
</div>
EmptyState
<div class="flex-1 flex flex-col items-center justify-center text-zinc-400 gap-2">
<div class="text-sm">Open a file from the sidebar.</div>
<div class="text-xs text-zinc-500">Tip: ⌘P to jump, ⌘S to save.</div>
</div>
Toast
<div class="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
<div class="bg-white border border-zinc-200 rounded-md shadow-sm px-3 py-2 text-sm">
<span class="text-emerald-700 mr-2">●</span>
Committed <span class="font-mono">a1b2c3d</span>
</div>
</div>
Status chips
<span class="text-[11px] px-2 py-0.5 rounded-full border">
<span class="inline-block size-1.5 rounded-full bg-emerald-500 mr-1" />
LSP · ready
</span>
States
- No project: large empty state in main area; sidebar shows "No project yet."
- Connecting: preview shows "starting…" with a quiet spinner (CSS, not library).
- External preview source: toast "Source outside project — ignored."
- Save error: rose-600 message in toast + small dot in TopBar file path.
- Git dirty: emerald-50 row tint + small dot next to filename in tree.
Motion
- Hover:
transition-colors duration-100. - Splitter drag: no transition.
- Toast in/out: 150ms opacity, no spring.
Accessibility
- Visible focus rings:
focus-visible:ring-2 focus-visible:ring-emerald-300 focus-visible:ring-offset-1. - All interactive controls reachable by keyboard.
- Status communicated by text + color, never color alone.
Don'ts
- No gradients on chrome.
- No shadows on whitespace panels.
- No animated chrome unless it's a real state change.
- No emoji decorations in product UI (only as icons in narrow toolbars).
Thumbnail: vibe check
Imagine a warm, slightly off-white IDE with a single green accent for "commit ready" and a quiet three-dot status footer. It's the IDE equivalent of a blank page of cream paper, not a glass dashboard.