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.
172 lines
8.0 KiB
Markdown
172 lines
8.0 KiB
Markdown
# 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
|
|
|
|
1. **Paper, not glass.**
|
|
Surfaces are warm off-white, not slate. Borders separate panels; we don't blur or float.
|
|
2. **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.
|
|
3. **Typography is the interface.**
|
|
System sans for chrome, monospace for paths/git/lsp status. We say things in words, not icons.
|
|
4. **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-mono` for 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
|
|
```tsx
|
|
<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)
|
|
```tsx
|
|
<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
|
|
```tsx
|
|
<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
|
|
```tsx
|
|
<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
|
|
```tsx
|
|
<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
|
|
```tsx
|
|
<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.
|