Files
typst-leaf/DESIGN.md
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

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

  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

<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.