Files
markdown-converter/docs/superpowers/specs/2026-06-05-react-ui-redesign-design.md
T
amitwh a999f598cc docs(spec): add React + shadcn/ui UI redesign design spec
Brainstormed via superpowers:brainstorming. Locks in:
- Full feature parity with legacy renderer
- Polished + Glassy (Raycast/Arc) visual style
- IDE-style layout with draggable divider
- shadcn/ui + Motion (Framer Motion) + Zustand stack
- A/B/D modal patterns (no command palette)
- 10-phase vertical-slice implementation plan
2026-06-05 08:26:02 +05:30

24 KiB
Raw Blame History

MarkdownConverter — React + shadcn/ui UI Redesign

Date: 2026-06-05 Status: Design (awaiting user approval) Branch: react-electron Author: Brainstormed with user via superpowers:brainstorming

Summary

Replace the legacy vanilla JS renderer (src/renderer.js is 213 KB; src/styles.css is 74 KB) with a modern React 19 + Vite + TypeScript + shadcn/ui renderer that achieves visual feature parity while delivering a Polished + Glassy (Raycast/Arc-style) aesthetic. The Electron main process, preload bridge, and IPC contracts stay unchanged. Work proceeds via vertical slices — each PR ships a demoable feature.

Goals

  1. Replace every render-time feature of the legacy renderer with an idiomatic React + shadcn/ui implementation.
  2. Adopt the "Polished + Glassy" visual language (subtle shadows, 812 px radii, gradient accents, gentle depth) on top of the existing ConcreteInfo brand tokens and design system.
  3. Use a single component foundation (shadcn/ui) and a single motion library (Motion / Framer Motion) to keep the dependency surface narrow and the design coherent.
  4. Keep the main process, preload, and IPC contracts untouched. The renderer is the only surface being rewritten.
  5. Keep the app runnable at every commit. No "big bang" merge.

Non-Goals

  • Migrating the main process to TypeScript (out of scope for this spec).
  • Adding new features the legacy renderer does not have (plugin system, AI features, etc. — those are future specs).
  • Replacing Pandoc/FFmpeg/ImageMagick orchestration.
  • Changing the packaging pipeline (electron-builder config stays).
  • Replacing the CodeMirror 6 editor — it is the right tool and is already wired up.

Decisions Locked During Brainstorming

Decision Choice Why
Scope Full feature parity with legacy renderer User-selected option. Renderer is one cohesive surface; splitting it across specs would force premature contracts.
Visual style B — Polished + Glassy (Raycast/Arc aesthetic) "Fancy" comes from elevation, gradient accents, and material depth — not over-designed visuals.
Layout 2 — IDE-style (equal editor/preview, draggable divider, collapsible sidebar) Power-user markdown apps converge on this. Draggable divider + keyboard reset.
Modal patterns A (Centered Dialog), B (Right Side-Sheet), D (Toasts) No command palette.
Command palette No — full menus, no ⌘K Power users can use the OS-level launcher. App stays focused on writing.
Animation Motion (Framer Motion) Best for layout transitions, modal/drawer enter/exit, drag. Sparingly used.
Component library shadcn/ui Tailwind + Radix primitives, copy-paste ownership, perfect fit for the "glassy" aesthetic and the HSL-CSS-variable foundation that is already in place.
Implementation strategy Vertical slices — one feature end-to-end per PR App stays runnable. Each PR is reviewable.

Defaults (Used Unless Overridden Later)

Item Default Why
State management Zustand (already in deps) + Immer for nested patches Already in package.json; perfect for editor state.
Theming shadcn next-themes, dark default + light, system-aware shadcn canonical pattern. Brand colors already in CSS vars.
Icons lucide-react (already in deps) 1000+ tree-shakable icons.
Forms react-hook-form + zod Standard for shadcn forms.
Drag/drop @dnd-kit/core De facto React drag lib (file tree reordering, divider resize).
Testing Vitest + RTL + Playwright (E2E + visual regression) Standard for React + Electron.
TypeScript Strict mode (already wired) Already in deps and vite.renderer.config.ts.

Architecture

The Electron app keeps its existing process model. Only the renderer is rewritten.

┌──────────────────────────────────────────────────────────┐
│  Electron Main (UNCHANGED)                                │
│  - BrowserWindow, IPC handlers, file/fs ops               │
│  - Pandoc, FFmpeg, ImageMagick orchestration              │
└────────────┬─────────────────────────────────────────────┘
             │ contextBridge (UNCHANGED preload.js)
┌────────────▼─────────────────────────────────────────────┐
│  React Renderer (REWRITTEN)                                │
│  ┌────────────────────────────────────────────────────┐  │
│  │  AppShell (layout)                                  │  │
│  │  ├─ MenuBar (native)                                │  │
│  │  ├─ AppHeader (logo, breadcrumbs, theme toggle)     │  │
│  │  ├─ TabBar (open files)                             │  │
│  │  ├─ Toolbar (formatting)                            │  │
│  │  ├─ ResizablePaneGroup (sidebar | editor | preview) │  │
│  │  │   ├─ Sidebar (file tree, outline)                │  │
│  │  │   ├─ EditorPane (CodeMirror 6)                   │  │
│  │  │   └─ PreviewPane (marked + KaTeX + Mermaid)      │  │
│  │  ├─ StatusBar (word count, encoding, cursor pos)    │  │
│  │  └─ ModalLayer (Dialog, SideSheet, Toaster)         │  │
│  └────────────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────────────┐  │
│  │  Feature Modules (each owns UI + state slice)       │  │
│  │  ├─ editor/    CodeMirror wrapper, syntax, themes   │  │
│  │  ├─ preview/   markdown→html, KaTeX, Mermaid       │  │
│  │  ├─ tabs/      open files, dirty state              │  │
│  │  ├─ sidebar/   file tree, outline, search results   │  │
│  │  ├─ modals/    export, settings, about, etc.        │  │
│  │  ├─ tools/     zen, repl, ascii-gen, table-gen      │  │
│  │  └─ export/    pdf, docx, html, image batch         │  │
│  └────────────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────────────┐  │
│  │  Shared Infrastructure                              │  │
│  │  ├─ stores/  Zustand slices per feature             │  │
│  │  ├─ hooks/   useFile, useEditor, useTheme, etc.     │  │
│  │  ├─ lib/     cn, ipc, formatters, validators        │  │
│  │  ├─ ui/      shadcn primitives: button, dialog…     │  │
│  │  └─ types/   shared TS types                        │  │
│  └────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────┘

Architectural Rules

  • Shell components (AppHeader, TabBar, StatusBar) subscribe only to useAppStore.
  • Feature components subscribe to their own feature's store.
  • Cross-feature access goes through hooks, not direct store imports (e.g., useFileTree() wraps useFileStore).
  • Feature modules are self-contained — each owns its components, its Zustand slice, and its types. Safe to develop in parallel later.
  • No direct window.electronAPI calls from feature code. All IPC goes through lib/ipc.ts for type safety and error normalization.

State Management

stores/
├─ useAppStore        // global UI: theme, sidebar, pane sizes, modals open
├─ useFileStore       // file system: tree, open files, active tab
├─ useEditorStore     // editor: per-file content, cursor, selection, dirty
├─ usePreviewStore    // preview: scroll sync, zoom, theme
├─ useSettingsStore   // user prefs (persisted to electron-store)
└─ useCommandStore    // menu actions registry, recent actions

Why slices instead of one mega-store? Editor content can be megabytes. Keeping it in its own slice lets React avoid re-rendering the preview when only the editor changes — components subscribe with selectors.

Persistent state (auto-saved to electron-store via Zustand persist middleware): theme, sidebar visibility, last open files, pane divider positions, settings.

Ephemeral state (lost on quit): active modal, hover states, search query, current file content (saved to disk on idle).

Visual Design System

Color Tokens

The existing globals.css HSL variables stay. Additions for the "glassy" aesthetic:

--shadow-sm: 0 1px 2px rgba(13, 11, 9, 0.06);
--shadow-md: 0 4px 12px rgba(13, 11, 9, 0.08), 0 0 0 1px rgba(13, 11, 9, 0.04);
--shadow-lg: 0 12px 32px rgba(13, 11, 9, 0.12), 0 0 0 1px rgba(13, 11, 9, 0.06);
--shadow-glow-brand: 0 0 24px rgba(229, 70, 31, 0.25);

--glass-bg-light: rgba(255, 255, 255, 0.72);
--glass-bg-dark:  rgba(13, 11, 9, 0.72);
--glass-border-light: rgba(255, 255, 255, 0.4);
--glass-border-dark:  rgba(255, 255, 255, 0.08);

shadcn Components

Install via npx shadcn@latest add:

Primitives (need all of these): button, dialog, sheet, popover, tooltip, select, dropdown-menu, tabs, separator, scroll-area, toggle, switch, slider, input, textarea, label, form, skeleton, sonner, command.

Composite (custom-built on top of primitives): file-tree, pane-group, divider, status-bar, menu-bar, toast.

Typography

  • Body: Plus Jakarta Sans 15 px / line-height 1.6
  • Code: JetBrains Mono 13.5 px
  • Display: Barlow Condensed 700/800 — used sparingly (page titles, big metrics)
  • Headings: Plus Jakarta Sans 600/700 with tight letter-spacing
  • Labels (.label): Plus Jakarta Sans 500, 12 px, uppercase, 0.05 em letter-spacing

Motion Choreography

Element Motion Duration Easing
Modal (Dialog A) Scale 0.96→1, opacity 0→1 200 ms ease-out
Side sheet (B) TranslateX 100%→0, opacity 0→1 300 ms cubic-bezier(0.16, 1, 0.3, 1)
Toast TranslateY 100%→0, opacity 250 ms spring(stiffness: 300, damping: 30)
Sidebar toggle Width 264 px↔72 px, content fade 250 ms ease-in-out
Divider drag Live width update 0 ms none (instant)
Theme switch CSS variables, opacity overlay 300 ms ease-in-out
Tab switch Underline slide 200 ms ease-out
Hover (buttons) bg + shadow shift 150 ms ease-out
Focus ring Outline grow 100 ms ease-out

Rules: Every motion has a purpose. No gratuitous animation. Respects prefers-reduced-motion (Motion handles this automatically).

Empty / Loading / Error States

Every async surface ships all three. Sonner toasts for ephemeral feedback. Skeleton screens for content areas. Empty-state components with icon + message + primary CTA.

Modal/Overlay Patterns

Use case Pattern Why
Export (PDF/DOCX/HTML) A — Centered Dialog Focused decision, ~35 fields, "do one thing"
Settings B — Right Side-Sheet Long form, tabbed sections, stays open while editing
Find/Replace Inline toolbar (not modal) Always-visible utility
About A — Centered Dialog Tiny info dump
Confirm destructive (delete, close unsaved) A — Centered Dialog Forces attention
File save error D — Toast Non-blocking info
Pandoc/FFmpeg progress D — Toast → sticky on >3 s Background work feedback
Plugin manager B — Right Side-Sheet Lists + per-item actions
ASCII/Table generators A — Centered Dialog Tool-style focused input → output
Print preview A — Centered Dialog Full-screen-ish modal overlay
Welcome (first launch) A — Centered Dialog One-time onboarding
Word export A — Centered Dialog Template selection
Zen mode Full-viewport toggle (no modal) Replaces the editor entirely
REPL Bottom-pinned panel (split-pane) Persistent terminal-like UI
Quick file open B — Right Side-Sheet File tree, search, recent

Dialog A Anatomy (Export as example)

┌─ Backdrop (rgba(13,11,9,0.45) + backdrop-blur 4px) ─────────┐
│                                                            │
│   ┌──── Modal (max-w-md, rounded-2xl, shadow-lg) ─────┐    │
│   │ ┌── Header ────────────────────────────────────┐  │    │
│   │ │ [Icon] Export as PDF                  [×]    │  │    │
│   │ │ Choose format options                       │  │    │
│   │ └─────────────────────────────────────────────┘  │    │
│   │ ┌── Body ──────────────────────────────────────┐  │    │
│   │ │ Format: [Letter] [A4] [Legal]                │  │    │
│   │ │ Margins: ────●────                           │  │    │
│   │ │ ☐ Include table of contents                  │  │    │
│   │ │ ☐ Embed fonts                                │  │    │
│   │ └─────────────────────────────────────────────┘  │    │
│   │ ┌── Footer ────────────────────────────────────┐  │    │
│   │ │                       [Cancel] [Export →]    │  │    │
│   │ └─────────────────────────────────────────────┘  │    │
│   └──────────────────────────────────────────────────┘    │
│                                                            │
└────────────────────────────────────────────────────────────┘

Side-Sheet B Anatomy (Settings as example)

Slides in from right, ~50 % width (max 560 px). Backdrop is rgba(13,11,9,0.2) with backdrop-blur(2 px) (lighter than dialog — the sheet is the focus). Tab navigation in the sheet header (Editor / Theme / Export / Plugins / About).

Toast D Anatomy (Sonner)

Stacked bottom-right, max 3 visible. Glass background. Color-coded 3 px left border — success #1a7a56, error #ef4444, info #0ea5e9, warning #eab308. Icon + title + description. Optional action button. Auto-dismiss 4 s for success, sticky for error.

Data Flow (Editor → Preview Pipeline)

User types
  ↓
CodeMirror onChange
  ↓
useEditorStore.updateContent(tabId, content)  ← debounced 50 ms
  ↓
┌─ Persist to electron-store on idle (1 s) ─┐
│                                            │
└─ Publish to usePreviewStore (subscribed) ─┘
  ↓
marked(content) → sanitized HTML
  ↓
PreviewPane renders HTML
  KaTeX post-processes $...$
  Mermaid post-processes ```mermaid
  Highlight.js post-processes ```lang
  ↓
useEffect: scroll-sync editor cursor → preview position

Bidirectional scroll sync: editor scroll → preview (primary), preview click → editor cursor (secondary). Throttled to 60 fps via requestAnimationFrame.

IPC Contract

The preload bridge stays unchanged. A new src/renderer/lib/ipc.ts wraps every channel with TypeScript types and normalizes errors to a discriminated union:

type IpcResult<T> =
  | { ok: true; data: T }
  | { ok: false; error: { code: string; message: string } };

export const ipc = {
  file: {
    open: (): Promise<IpcResult<FileResult>>,
    read: (path: string): Promise<IpcResult<string>>,
    write: (path: string, content: string): Promise<IpcResult<void>>,
    list: (dir: string): Promise<IpcResult<FileEntry[]>>,
    onChange: (cb: (path: string) => void) => () => void,
  },
  export: {
    pdf: (opts: PdfOptions): Promise<IpcResult<ExportResult>>,
    docx: (opts: DocxOptions): Promise<IpcResult<ExportResult>>,
    html: (opts: HtmlOptions): Promise<IpcResult<ExportResult>>,
    batch: (items: BatchItem[], opts: BatchOptions): Promise<IpcResult<BatchResult>>,
  },
  app: {
    getVersion: (): Promise<IpcResult<string>>,
    openExternal: (url: string): Promise<IpcResult<void>>,
    showItemInFolder: (path: string): Promise<IpcResult<void>>,
  },
};

Error Handling

Layered defense — no single failure can crash the app:

  1. IPC errors → caught in lib/ipc.ts wrapper, returned as IpcResult<T> discriminated union. Components show toast on error.
  2. Component errors → React error boundary per feature area (one for editor, one for preview, one for modals). On error: show inline error UI with "Reload this panel" + "Copy details" actions.
  3. Async operations (export, file ops) → loading state on button + toast on completion. Long ops (>2 s) get a sticky toast with cancel.
  4. Validation errors (settings, export options) → inline form errors via react-hook-form + zod. shadcn Form component for consistent error display.
  5. Pandoc/FFmpeg missing → detected at startup, banner + disable export menu items. Don't surprise-fail at export time.

Testing Strategy

  • Unit (Vitest): stores, hooks, lib utilities, formatters, validators
  • Component (Vitest + RTL): shadcn wrappers, dialog interactions, sidebar toggle, divider drag, theme toggle
  • Integration (Vitest + RTL): editor ↔ preview flow, file open → tab add → content display, settings change persists
  • E2E (Playwright): app launches, file opens, markdown renders, export to PDF works
  • Visual regression (Playwright snapshots): locked-in screenshots for header, sidebar, dialogs, toasts — catch accidental style drift
  • Target coverage: stores/hooks/lib ≥ 90 %, components ≥ 75 %, E2E covers all critical paths

Accessibility (WCAG 2.1 AA)

Baked in via shadcn + Radix:

  • All dialogs focus-trap, Esc to close, return focus to trigger
  • All interactive elements keyboard-reachable
  • Visible focus rings (2 px brand ring)
  • 4.5:1 contrast minimum (already met by brand colors)
  • aria-label on icon buttons, role="alert" on toasts
  • Respects prefers-reduced-motion (Motion handles this automatically)

Implementation Phases (Vertical Slices)

Each phase is a shippable PR that keeps the app runnable. The legacy renderer.js is gradually replaced; until each phase is complete, the old renderer code still runs the missing parts.

# Phase Output Acceptance
1 Foundation shadcn installed; next-themes, motion, react-hook-form, zod, dnd-kit, sonner installed; design tokens finalized; lib/utils.ts (cn), lib/ipc.ts typed wrappers, lib/motion.ts preset transitions, app.tsx shell skeleton renders npm run build succeeds; dev server shows the new shell with theme toggle working
2 App shell + layout AppHeader, TabBar, Toolbar, Breadcrumb, StatusBar, ResizablePaneGroup with sidebar toggle and draggable divider. Empty editor/preview panes. Resize divider with mouse and arrow keys; sidebar collapses; pane sizes persist.
3 Editor pane CodeMirror 6 wrapped, dark/light themes wired to shadcn theme, syntax highlighting, line numbers, search, autocomplete Open .md file → renders in editor; can edit and save.
4 Preview pane marked + DOMPurify + KaTeX + Mermaid + highlight.js. Bidirectional scroll sync with editor. Open file → preview renders, follows cursor.
5 File tree + tabs Sidebar file tree (open from disk, lazy load). Tabs for open files. Dirty state indicator. Open folder → tree populates; click file → opens in tab; close tab reverts dirty.
6 Native menus + toolbar Replace the legacy CommandPalette with full menus (File / Edit / View / Insert / Format / Tools / Help) bound to keyboard shortcuts. Toolbar buttons. All menu items invoke the right action; shortcuts work; toolbar reflects active state.
7 Modals Export dialog (PDF/DOCX/HTML/batch), Settings side-sheet (Editor/Theme/Export/Plugins/About tabs), About dialog, Confirm-destructive dialog All dialogs and sheet render with proper motion; settings persist; export works end-to-end.
8 Toasts Sonner wired into all async operations (save, export, errors, tool missing) All operations give appropriate feedback; no silent failures.
9 Advanced tools Zen mode (full-viewport toggle), REPL (bottom-pinned panel), ASCII generator, Table generator, Word export template picker, Print preview Each tool is feature-complete vs. legacy version.
10 Polish + delete legacy Visual regression snapshots locked; remove renderer.js and old styles*.css references from build; npm run build:linux produces a working installer Final PR ships an installer with no legacy code paths.

Risks and Mitigations

Risk Mitigation
shadcn CLI requires Vite (not vanilla Electron renderer) Vite is already configured for the renderer in vite.renderer.config.ts; the CLI works against the same project.
shadcn CLI assumes Tailwind is at project root; we have it in renderer/ only Point the CLI at src/renderer/components.json and set tailwind.css to the right path.
Marked + KaTeX + Mermaid + highlight.js together is heavy Lazy-load Mermaid (only when mermaid code block encountered). KaTeX is loaded once, cached.
Editor content state can be megabytes → re-render storms Editor content lives in its own slice; preview subscribes to a derived/cached HTML string; components use useShallow / selector subscriptions.
Bidirectional scroll sync loops useEffect debounce + ignore if cursor/selection is the sync source.
CodeMirror 6 doesn't ship a Tailwind theme by default Use @codemirror/theme-one-dark for dark; build a custom theme that pulls from CSS variables for light.
The legacy renderer.js and styles*.css are referenced from index.html (now src/renderer/index.html) Phase 10 deletes the references; until then, both run side-by-side and the new React app sits in a known root div.
Electron nodeIntegration is off; we use contextBridge Already the case. Document the contract clearly in lib/ipc.ts.

Open Questions (to confirm before implementation)

None. All major decisions are locked. Defaults will be used unless the user overrides them when reviewing the implementation plan.

References

  • src/renderer.js (213 KB) — legacy renderer being replaced
  • src/styles.css (74 KB), src/styles-modern.css (71 KB), src/styles-concreteinfo.css (21 KB) — legacy styles being replaced
  • src/renderer/styles/globals.css — already shadcn-compatible (HSL CSS variables)
  • src/renderer/App.tsx — empty skeleton that references components we will build
  • tailwind.config.js — brand colors and fonts already defined
  • vite.renderer.config.ts — Vite + React plugin already configured
  • package.json — CodeMirror 6, TanStack Table, lucide-react, cva, clsx, tailwind-merge, tailwindcss-animate already installed