diff --git a/docs/plans/2026-03-15-react-tauri-pwa-implementation.md b/docs/plans/2026-03-15-react-tauri-pwa-implementation.md
new file mode 100644
index 0000000..f8047dc
--- /dev/null
+++ b/docs/plans/2026-03-15-react-tauri-pwa-implementation.md
@@ -0,0 +1,1728 @@
+# React + Tauri + PWA Migration Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Migrate MarkdownConverter from Electron to a React + Tauri + PWA multi-platform architecture.
+
+**Architecture:** Single codebase with platform adapters pattern. React components use Zustand for state, Shadcn/ui for UI, CodeMirror 6 for editing. Platform-specific code isolated in adapter files that auto-detect Tauri vs Web environment.
+
+**Tech Stack:** React 18, TypeScript (strict), Vite, Tailwind CSS, Shadcn/ui, Zustand, CodeMirror 6, Tauri 1.5, Rust
+
+---
+
+## Phase 1: Foundation (Tasks 1-15)
+
+### Task 1: Create New Project Directory
+
+**Files:**
+- Create: `markdown-converter-v5/` (new directory alongside current project)
+
+**Step 1: Create the project directory**
+
+```bash
+mkdir -p C:/coding/markdown-converter-v5
+cd C:/coding/markdown-converter-v5
+```
+
+**Step 2: Initialize git repository**
+
+```bash
+git init
+echo "node_modules/" > .gitignore
+echo "dist/" >> .gitignore
+echo "src-tauri/target/" >> .gitignore
+echo ".tauri/" >> .gitignore
+git add .gitignore
+git commit -m "chore: initialize project"
+```
+
+---
+
+### Task 2: Initialize Vite + React + TypeScript Project
+
+**Files:**
+- Create: `package.json`
+- Create: `tsconfig.json`
+- Create: `tsconfig.node.json`
+- Create: `vite.config.ts`
+- Create: `index.html`
+- Create: `src/main.tsx`
+- Create: `src/App.tsx`
+- Create: `src/vite-env.d.ts`
+
+**Step 1: Create package.json**
+
+```bash
+cd C:/coding/markdown-converter-v5
+```
+
+Create `package.json`:
+```json
+{
+ "name": "markdown-converter-v5",
+ "version": "5.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "tauri": "tauri",
+ "tauri:dev": "tauri dev",
+ "tauri:build": "tauri build"
+ },
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "zustand": "^4.5.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.0",
+ "@types/react-dom": "^18.2.0",
+ "@vitejs/plugin-react": "^4.2.0",
+ "typescript": "^5.3.0",
+ "vite": "^5.0.0"
+ }
+}
+```
+
+**Step 2: Install dependencies**
+
+```bash
+npm install
+```
+
+Expected output:
+```
+added X packages in Xs
+```
+
+**Step 3: Create tsconfig.json**
+
+```json
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "strictNullChecks": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
+```
+
+**Step 4: Create tsconfig.node.json**
+
+```json
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "strict": true
+ },
+ "include": ["vite.config.ts"]
+}
+```
+
+**Step 5: Create vite.config.ts**
+
+```typescript
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ },
+ },
+ build: {
+ target: 'esnext',
+ minify: 'esbuild',
+ sourcemap: true,
+ rollupOptions: {
+ output: {
+ manualChunks: {
+ 'vendor-react': ['react', 'react-dom'],
+ },
+ },
+ },
+ },
+ server: {
+ port: 5173,
+ strictPort: true,
+ },
+});
+```
+
+**Step 6: Create index.html**
+
+```html
+
+
+
+
+
+
+ Markdown Converter
+
+
+
+
+
+
+```
+
+**Step 7: Create src/vite-env.d.ts**
+
+```typescript
+///
+```
+
+**Step 8: Create src/main.tsx**
+
+```typescript
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+import './styles/globals.css';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
+```
+
+**Step 9: Create src/App.tsx**
+
+```typescript
+function App() {
+ return (
+
+
Markdown Converter v5
+
React + Tauri + PWA
+
+ );
+}
+
+export default App;
+```
+
+**Step 10: Create placeholder CSS**
+
+```bash
+mkdir -p C:/coding/markdown-converter-v5/src/styles
+```
+
+Create `src/styles/globals.css`:
+```css
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: system-ui, -apple-system, sans-serif;
+}
+
+.app {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100vh;
+}
+
+h1 {
+ font-size: 2rem;
+ margin-bottom: 1rem;
+}
+```
+
+**Step 11: Verify build works**
+
+```bash
+npm run build
+```
+
+Expected output:
+```
+vite v5.x.x building for production...
+✓ X modules transformed.
+dist/index.html X.xx kB │ gzip: X.xx kB
+dist/assets/index-XXXXXXXX.js X.xx kB │ gzip: X.xx kB
+✓ built in X.XXs
+```
+
+**Step 12: Commit**
+
+```bash
+git add .
+git commit -m "chore: initialize Vite + React + TypeScript project"
+```
+
+---
+
+### Task 3: Configure Tailwind CSS
+
+**Files:**
+- Modify: `package.json`
+- Create: `tailwind.config.ts`
+- Create: `postcss.config.js`
+- Modify: `src/styles/globals.css`
+
+**Step 1: Install Tailwind dependencies**
+
+```bash
+npm install -D tailwindcss postcss autoprefixer
+npx tailwindcss init -p
+```
+
+Expected output:
+```
+Created tailwind.config.js
+Created postcss.config.js
+```
+
+**Step 2: Update tailwind.config.ts**
+
+Rename `tailwind.config.js` to `tailwind.config.ts` and replace content:
+```typescript
+import type { Config } from 'tailwindcss';
+
+export default {
+ content: [
+ "./index.html",
+ "./src/**/*.{js,ts,jsx,tsx}",
+ ],
+ darkMode: 'class',
+ theme: {
+ extend: {
+ fontFamily: {
+ sans: ['Inter', 'system-ui', 'sans-serif'],
+ mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
+ },
+ },
+ },
+ plugins: [],
+} satisfies Config;
+```
+
+**Step 3: Update globals.css**
+
+```css
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* Custom base styles */
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 222.2 84% 4.9%;
+ --primary: 227 44% 52%;
+ --primary-foreground: 0 0% 100%;
+ }
+
+ .dark {
+ --background: 222.2 84% 4.9%;
+ --foreground: 210 40% 98%;
+ --primary: 227 44% 52%;
+ --primary-foreground: 0 0% 100%;
+ }
+}
+
+body {
+ @apply bg-background text-foreground;
+ font-feature-settings: "rlig" 1, "calt" 1;
+}
+```
+
+**Step 4: Update App.tsx to use Tailwind**
+
+```typescript
+function App() {
+ return (
+
+
+ Markdown Converter v5
+
+
React + Tauri + PWA
+
+ );
+}
+
+export default App;
+```
+
+**Step 5: Verify build still works**
+
+```bash
+npm run build
+```
+
+Expected: Build succeeds without errors
+
+**Step 6: Commit**
+
+```bash
+git add .
+git commit -m "chore: configure Tailwind CSS"
+```
+
+---
+
+### Task 4: Setup Shadcn/ui
+
+**Files:**
+- Modify: `package.json`
+- Create: `components.json`
+- Create: `src/lib/utils.ts`
+- Create: `src/components/ui/button.tsx`
+
+**Step 1: Install Shadcn/ui dependencies**
+
+```bash
+npm install class-variance-authority clsx tailwind-merge
+npm install -D @types/node
+```
+
+**Step 2: Create components.json**
+
+```json
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "src/styles/globals.css",
+ "baseColor": "slate",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils"
+ }
+}
+```
+
+**Step 3: Create src/lib/utils.ts**
+
+```typescript
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
+```
+
+**Step 4: Update tailwind.config.ts with Shadcn colors**
+
+```typescript
+import type { Config } from 'tailwindcss';
+
+export default {
+ darkMode: ["class"],
+ content: [
+ "./index.html",
+ "./src/**/*.{js,ts,jsx,tsx}",
+ ],
+ theme: {
+ container: {
+ center: true,
+ padding: "2rem",
+ screens: {
+ "2xl": "1400px",
+ },
+ },
+ extend: {
+ fontFamily: {
+ sans: ['Inter', 'system-ui', 'sans-serif'],
+ mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
+ },
+ colors: {
+ border: "hsl(var(--border))",
+ input: "hsl(var(--input))",
+ ring: "hsl(var(--ring))",
+ background: "hsl(var(--background))",
+ foreground: "hsl(var(--foreground))",
+ primary: {
+ DEFAULT: "hsl(var(--primary))",
+ foreground: "hsl(var(--primary-foreground))",
+ },
+ secondary: {
+ DEFAULT: "hsl(var(--secondary))",
+ foreground: "hsl(var(--secondary-foreground))",
+ },
+ destructive: {
+ DEFAULT: "hsl(var(--destructive))",
+ foreground: "hsl(var(--destructive-foreground))",
+ },
+ muted: {
+ DEFAULT: "hsl(var(--muted))",
+ foreground: "hsl(var(--muted-foreground))",
+ },
+ accent: {
+ DEFAULT: "hsl(var(--accent))",
+ foreground: "hsl(var(--accent-foreground))",
+ },
+ },
+ borderRadius: {
+ lg: "var(--radius)",
+ md: "calc(var(--radius) - 2px)",
+ sm: "calc(var(--radius) - 4px)",
+ },
+ keyframes: {
+ "accordion-down": {
+ from: { height: "0" },
+ to: { height: "var(--radix-accordion-content-height)" },
+ },
+ "accordion-up": {
+ from: { height: "var(--radix-accordion-content-height)" },
+ to: { height: "0" },
+ },
+ },
+ animation: {
+ "accordion-down": "accordion-down 0.2s ease-out",
+ "accordion-up": "accordion-up 0.2s ease-out",
+ },
+ },
+ },
+ plugins: [require("tailwindcss-animate")],
+} satisfies Config;
+```
+
+**Step 5: Install tailwindcss-animate**
+
+```bash
+npm install -D tailwindcss-animate
+```
+
+**Step 6: Update globals.css with full Shadcn variables**
+
+```css
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 222.2 84% 4.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+ --primary: 227 44% 52%;
+ --primary-foreground: 210 40% 98%;
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 227 44% 52%;
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 222.2 84% 4.9%;
+ --foreground: 210 40% 98%;
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+ --primary: 227 44% 52%;
+ --primary-foreground: 222.2 47.4% 11.2%;
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 227 44% 52%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ font-feature-settings: "rlig" 1, "calt" 1;
+ }
+}
+```
+
+**Step 7: Verify build**
+
+```bash
+npm run build
+```
+
+**Step 8: Commit**
+
+```bash
+git add .
+git commit -m "chore: setup Shadcn/ui configuration"
+```
+
+---
+
+### Task 5: Create Zustand Stores
+
+**Files:**
+- Create: `src/stores/editorStore.ts`
+- Create: `src/stores/settingsStore.ts`
+- Create: `src/stores/themeStore.ts`
+- Create: `src/stores/sidebarStore.ts`
+- Create: `src/types/editor.ts`
+
+**Step 1: Create type definitions**
+
+Create `src/types/editor.ts`:
+```typescript
+export interface CursorPosition {
+ line: number;
+ column: number;
+}
+
+export interface Tab {
+ id: string;
+ title: string;
+ content: string;
+ filePath?: string;
+ isDirty: boolean;
+ cursorPosition: CursorPosition;
+}
+
+export interface FileInfo {
+ name: string;
+ isDir: boolean;
+ size: number;
+ modified: number;
+}
+```
+
+**Step 2: Create editorStore.ts**
+
+Create `src/stores/editorStore.ts`:
+```typescript
+import { create } from 'zustand';
+import { persist, createJSONStorage } from 'zustand/middleware';
+import type { Tab, CursorPosition } from '@/types/editor';
+
+interface EditorState {
+ tabs: Tab[];
+ activeTabId: string | null;
+
+ createTab: (title?: string, content?: string) => string;
+ closeTab: (id: string) => void;
+ setActiveTab: (id: string) => void;
+ updateContent: (id: string, content: string) => void;
+ updateCursorPosition: (id: string, pos: CursorPosition) => void;
+ markSaved: (id: string, filePath?: string) => void;
+ getActiveTab: () => Tab | null;
+}
+
+export const useEditorStore = create()(
+ persist(
+ (set, get) => ({
+ tabs: [],
+ activeTabId: null,
+
+ createTab: (title = 'Untitled', content = '') => {
+ const id = crypto.randomUUID();
+ const tab: Tab = {
+ id,
+ title,
+ content,
+ isDirty: false,
+ cursorPosition: { line: 1, column: 1 },
+ };
+ set((state) => ({
+ tabs: [...state.tabs, tab],
+ activeTabId: id,
+ }));
+ return id;
+ },
+
+ closeTab: (id) => {
+ set((state) => {
+ const newTabs = state.tabs.filter((t) => t.id !== id);
+ const newActiveId =
+ state.activeTabId === id
+ ? newTabs[newTabs.length - 1]?.id ?? null
+ : state.activeTabId;
+ return { tabs: newTabs, activeTabId: newActiveId };
+ });
+ },
+
+ setActiveTab: (id) => set({ activeTabId: id }),
+
+ updateContent: (id, content) => {
+ set((state) => ({
+ tabs: state.tabs.map((tab) =>
+ tab.id === id ? { ...tab, content, isDirty: true } : tab
+ ),
+ }));
+ },
+
+ updateCursorPosition: (id, pos) => {
+ set((state) => ({
+ tabs: state.tabs.map((tab) =>
+ tab.id === id ? { ...tab, cursorPosition: pos } : tab
+ ),
+ }));
+ },
+
+ markSaved: (id, filePath) => {
+ set((state) => ({
+ tabs: state.tabs.map((tab) =>
+ tab.id === id
+ ? { ...tab, isDirty: false, filePath, title: filePath?.split('/').pop() ?? tab.title }
+ : tab
+ ),
+ }));
+ },
+
+ getActiveTab: () => {
+ const state = get();
+ return state.tabs.find((t) => t.id === state.activeTabId) ?? null;
+ },
+ }),
+ {
+ name: 'editor-storage',
+ storage: createJSONStorage(() => localStorage),
+ partialize: (state) => ({
+ tabs: state.tabs.map((t) => ({ ...t, content: '' })), // Don't persist content
+ }),
+ }
+ )
+);
+```
+
+**Step 3: Create settingsStore.ts**
+
+Create `src/stores/settingsStore.ts`:
+```typescript
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+
+type PreviewMode = 'split' | 'editor' | 'preview';
+
+interface SettingsState {
+ fontSize: number;
+ fontFamily: string;
+ previewMode: PreviewMode;
+ showLineNumbers: boolean;
+ wordWrap: boolean;
+ autoSave: boolean;
+ autoSaveInterval: number;
+
+ setFontSize: (size: number) => void;
+ setFontFamily: (family: string) => void;
+ setPreviewMode: (mode: PreviewMode) => void;
+ toggleLineNumbers: () => void;
+ toggleWordWrap: () => void;
+ toggleAutoSave: () => void;
+ setAutoSaveInterval: (interval: number) => void;
+}
+
+export const useSettingsStore = create()(
+ persist(
+ (set) => ({
+ fontSize: 14,
+ fontFamily: 'JetBrains Mono',
+ previewMode: 'split',
+ showLineNumbers: true,
+ wordWrap: true,
+ autoSave: true,
+ autoSaveInterval: 30000,
+
+ setFontSize: (fontSize) => set({ fontSize }),
+ setFontFamily: (fontFamily) => set({ fontFamily }),
+ setPreviewMode: (previewMode) => set({ previewMode }),
+ toggleLineNumbers: () => set((state) => ({ showLineNumbers: !state.showLineNumbers })),
+ toggleWordWrap: () => set((state) => ({ wordWrap: !state.wordWrap })),
+ toggleAutoSave: () => set((state) => ({ autoSave: !state.autoSave })),
+ setAutoSaveInterval: (autoSaveInterval) => set({ autoSaveInterval }),
+ }),
+ { name: 'settings-storage' }
+ )
+);
+```
+
+**Step 4: Create themeStore.ts**
+
+Create `src/stores/themeStore.ts`:
+```typescript
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+
+type ThemeName =
+ | 'github-light'
+ | 'github-dark'
+ | 'solarized'
+ | 'monokai'
+ | 'dracula'
+ | 'nord'
+ | 'onedark'
+ | 'atomonelight'
+ | 'material'
+ | 'gruvbox-dark'
+ | 'gruvbox-light'
+ | 'tokyonight'
+ | 'palenight';
+
+interface ThemeState {
+ theme: ThemeName;
+ isDark: boolean;
+
+ setTheme: (theme: ThemeName) => void;
+ toggleDarkMode: () => void;
+}
+
+const darkThemes: ThemeName[] = [
+ 'github-dark',
+ 'monokai',
+ 'dracula',
+ 'nord',
+ 'onedark',
+ 'material',
+ 'gruvbox-dark',
+ 'tokyonight',
+ 'palenight',
+];
+
+export const useThemeStore = create()(
+ persist(
+ (set) => ({
+ theme: 'github-light',
+ isDark: false,
+
+ setTheme: (theme) => {
+ const isDark = darkThemes.includes(theme);
+ set({ theme, isDark });
+
+ // Update document class for Tailwind dark mode
+ if (isDark) {
+ document.documentElement.classList.add('dark');
+ } else {
+ document.documentElement.classList.remove('dark');
+ }
+ },
+
+ toggleDarkMode: () => {
+ set((state) => {
+ const newIsDark = !state.isDark;
+ if (newIsDark) {
+ document.documentElement.classList.add('dark');
+ } else {
+ document.documentElement.classList.remove('dark');
+ }
+ return { isDark: newIsDark };
+ });
+ },
+ }),
+ { name: 'theme-storage' }
+ )
+);
+```
+
+**Step 5: Create sidebarStore.ts**
+
+Create `src/stores/sidebarStore.ts`:
+```typescript
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+
+type SidebarPanel = 'explorer' | 'git' | 'snippets' | 'templates';
+
+interface SidebarState {
+ activePanel: SidebarPanel;
+ collapsed: boolean;
+ width: number;
+
+ setActivePanel: (panel: SidebarPanel) => void;
+ toggleCollapse: () => void;
+ setWidth: (width: number) => void;
+}
+
+export const useSidebarStore = create()(
+ persist(
+ (set) => ({
+ activePanel: 'explorer',
+ collapsed: false,
+ width: 280,
+
+ setActivePanel: (activePanel) => set({ activePanel, collapsed: false }),
+ toggleCollapse: () => set((state) => ({ collapsed: !state.collapsed })),
+ setWidth: (width) => set({ width }),
+ }),
+ { name: 'sidebar-storage' }
+ )
+);
+```
+
+**Step 6: Create stores index**
+
+Create `src/stores/index.ts`:
+```typescript
+export { useEditorStore } from './editorStore';
+export { useSettingsStore } from './settingsStore';
+export { useThemeStore } from './themeStore';
+export { useSidebarStore } from './sidebarStore';
+```
+
+**Step 7: Verify build**
+
+```bash
+npm run build
+```
+
+**Step 8: Commit**
+
+```bash
+git add .
+git commit -m "feat: add Zustand stores for editor, settings, theme, and sidebar"
+```
+
+---
+
+### Task 6: Create Platform Adapter Types
+
+**Files:**
+- Create: `src/adapters/types.ts`
+- Create: `src/types/platform.ts`
+
+**Step 1: Create platform types**
+
+Create `src/types/platform.ts`:
+```typescript
+import type { FileInfo } from './editor';
+
+export interface PdfOptions {
+ paperSize: string;
+ margins: string;
+ header?: string;
+ footer?: string;
+ toc: boolean;
+}
+
+export interface DocxOptions {
+ template?: string;
+ margins?: string;
+}
+
+export interface HtmlOptions {
+ standalone: boolean;
+ includeCss: boolean;
+}
+
+export interface ConversionResult {
+ input: string;
+ output: string;
+ success: boolean;
+ error?: string;
+}
+
+export interface WatchCallback {
+ (event: { type: string; path: string }): void;
+}
+
+export interface PlatformCapabilities {
+ hasPandoc: boolean;
+ hasFfmpeg: boolean;
+ hasLibreOffice: boolean;
+ hasDirectFs: boolean;
+ hasSystemNotifications: boolean;
+}
+
+export interface FileSystemAdapter {
+ readFile: (path: string) => Promise;
+ writeFile: (path: string, content: string) => Promise;
+ deleteFile: (path: string) => Promise;
+ listDirectory: (path: string) => Promise;
+ exists: (path: string) => Promise;
+ watchDirectory?: (path: string, callback: WatchCallback) => () => void;
+}
+
+export interface ConversionAdapter {
+ toPdf: (content: string, options: PdfOptions, outputPath: string) => Promise;
+ toDocx: (content: string, options: DocxOptions, outputPath: string) => Promise;
+ toHtml: (content: string, options: HtmlOptions) => Promise;
+ batchConvert: (files: string[], outputDir: string, format: string) => Promise;
+}
+```
+
+**Step 2: Create adapter types**
+
+Create `src/adapters/types.ts`:
+```typescript
+import type {
+ FileSystemAdapter,
+ ConversionAdapter,
+ PlatformCapabilities,
+} from '@/types/platform';
+
+export interface PlatformAdapter {
+ readonly name: 'tauri' | 'web';
+ readonly fs: FileSystemAdapter;
+ readonly convert: ConversionAdapter;
+ readonly capabilities: PlatformCapabilities;
+}
+```
+
+**Step 3: Verify build**
+
+```bash
+npm run build
+```
+
+**Step 4: Commit**
+
+```bash
+git add .
+git commit -m "feat: add platform adapter type definitions"
+```
+
+---
+
+### Task 7: Create Web Platform Adapter
+
+**Files:**
+- Create: `src/adapters/web/index.ts`
+- Create: `src/adapters/web/fs.ts`
+- Create: `src/adapters/web/convert.ts`
+- Create: `src/adapters/web/system.ts`
+- Create: `src/lib/db.ts`
+
+**Step 1: Create IndexedDB helper**
+
+Create `src/lib/db.ts`:
+```typescript
+import { openDB, type DBSchema, type IDBPDatabase } from 'idb';
+
+interface MarkdownConverterDB extends DBSchema {
+ files: {
+ key: string;
+ value: {
+ path: string;
+ content: string;
+ modified: number;
+ size: number;
+ };
+ };
+ settings: {
+ key: string;
+ value: unknown;
+ };
+}
+
+let db: IDBPDatabase | null = null;
+
+export async function getDB(): Promise> {
+ if (db) return db;
+
+ db = await openDB('markdown-converter', 1, {
+ upgrade(database) {
+ database.createObjectStore('files', { keyPath: 'path' });
+ database.createObjectStore('settings');
+ },
+ });
+
+ return db;
+}
+
+export const dbStorage = {
+ async saveFile(path: string, content: string): Promise {
+ const database = await getDB();
+ await database.put('files', {
+ path,
+ content,
+ modified: Date.now(),
+ size: content.length,
+ });
+ },
+
+ async getFile(path: string): Promise {
+ const database = await getDB();
+ const file = await database.get('files', path);
+ return file?.content;
+ },
+
+ async deleteFile(path: string): Promise {
+ const database = await getDB();
+ await database.delete('files', path);
+ },
+
+ async listFiles(): Promise {
+ const database = await getDB();
+ return (await database.getAllKeys('files')) as string[];
+ },
+};
+```
+
+**Step 2: Install idb**
+
+```bash
+npm install idb
+```
+
+**Step 3: Create web fs adapter**
+
+Create `src/adapters/web/fs.ts`:
+```typescript
+import type { FileSystemAdapter } from '@/types/platform';
+import type { FileInfo } from '@/types/editor';
+import { dbStorage } from '@/lib/db';
+
+export const webFsAdapter: FileSystemAdapter = {
+ async readFile(path: string): Promise {
+ const content = await dbStorage.getFile(path);
+ if (content === undefined) {
+ throw new Error(`File not found: ${path}`);
+ }
+ return content;
+ },
+
+ async writeFile(path: string, content: string): Promise {
+ await dbStorage.saveFile(path, content);
+ },
+
+ async deleteFile(path: string): Promise {
+ await dbStorage.deleteFile(path);
+ },
+
+ async listDirectory(_path: string): Promise {
+ // In web mode, we use a flat file list from IndexedDB
+ const paths = await dbStorage.listFiles();
+ return paths.map((path) => ({
+ name: path.split('/').pop() ?? path,
+ isDir: false,
+ size: 0,
+ modified: Date.now(),
+ }));
+ },
+
+ async exists(path: string): Promise {
+ const content = await dbStorage.getFile(path);
+ return content !== undefined;
+ },
+};
+```
+
+**Step 4: Create web convert adapter**
+
+Create `src/adapters/web/convert.ts`:
+```typescript
+import type { ConversionAdapter } from '@/types/platform';
+
+export const webConvertAdapter: ConversionAdapter = {
+ async toPdf(_content: string, _options, _outputPath): Promise {
+ // Web uses WASM or cloud service - placeholder for now
+ throw new Error('PDF conversion requires desktop app or cloud service');
+ },
+
+ async toDocx(_content: string, _options, _outputPath): Promise {
+ throw new Error('DOCX conversion requires desktop app');
+ },
+
+ async toHtml(content: string, options): Promise {
+ // Basic HTML conversion using marked (will be implemented later)
+ const { marked } = await import('marked');
+ const html = await marked(content);
+
+ if (options.standalone) {
+ return `
+
+
+
+ Exported Markdown
+ ${options.includeCss ? '' : ''}
+
+
+${html}
+
+`;
+ }
+
+ return html;
+ },
+
+ async batchConvert(_files: string[], _outputDir: string, _format: string) {
+ throw new Error('Batch conversion requires desktop app');
+ },
+};
+```
+
+**Step 5: Create web system adapter**
+
+Create `src/adapters/web/system.ts`:
+```typescript
+import type { PlatformCapabilities } from '@/types/platform';
+
+export const webCapabilities: PlatformCapabilities = {
+ hasPandoc: false,
+ hasFfmpeg: false,
+ hasLibreOffice: false,
+ hasDirectFs: false,
+ hasSystemNotifications: 'Notification' in window,
+};
+```
+
+**Step 6: Create web adapter index**
+
+Create `src/adapters/web/index.ts`:
+```typescript
+import type { PlatformAdapter } from '../types';
+import { webFsAdapter } from './fs';
+import { webConvertAdapter } from './convert';
+import { webCapabilities } from './system';
+
+export const webAdapter: PlatformAdapter = {
+ name: 'web',
+ fs: webFsAdapter,
+ convert: webConvertAdapter,
+ capabilities: webCapabilities,
+};
+```
+
+**Step 7: Install marked**
+
+```bash
+npm install marked
+```
+
+**Step 8: Verify build**
+
+```bash
+npm run build
+```
+
+**Step 9: Commit**
+
+```bash
+git add .
+git commit -m "feat: add web platform adapter with IndexedDB storage"
+```
+
+---
+
+### Task 8: Create Tauri Platform Adapter (Stub)
+
+**Files:**
+- Create: `src/adapters/tauri/index.ts`
+- Create: `src/adapters/tauri/fs.ts`
+- Create: `src/adapters/tauri/convert.ts`
+- Create: `src/adapters/tauri/system.ts`
+
+**Step 1: Create tauri fs adapter stub**
+
+Create `src/adapters/tauri/fs.ts`:
+```typescript
+import type { FileSystemAdapter } from '@/types/platform';
+import type { FileInfo } from '@/types/editor';
+
+// These will be implemented when Tauri is set up
+export const tauriFsAdapter: FileSystemAdapter = {
+ async readFile(path: string): Promise {
+ const { invoke } = await import('@tauri-apps/api/tauri');
+ return invoke('read_file', { path });
+ },
+
+ async writeFile(path: string, content: string): Promise {
+ const { invoke } = await import('@tauri-apps/api/tauri');
+ await invoke('write_file', { path, content });
+ },
+
+ async deleteFile(path: string): Promise {
+ const { invoke } = await import('@tauri-apps/api/tauri');
+ await invoke('delete_file', { path });
+ },
+
+ async listDirectory(path: string): Promise {
+ const { invoke } = await import('@tauri-apps/api/tauri');
+ return invoke('list_directory', { path });
+ },
+
+ async exists(path: string): Promise {
+ const { invoke } = await import('@tauri-apps/api/tauri');
+ return invoke('path_exists', { path });
+ },
+
+ watchDirectory(path: string, callback) {
+ // Will use Tauri events for file watching
+ console.log('File watching not yet implemented for:', path);
+ return () => {};
+ },
+};
+```
+
+**Step 2: Create tauri convert adapter stub**
+
+Create `src/adapters/tauri/convert.ts`:
+```typescript
+import type { ConversionAdapter } from '@/types/platform';
+
+export const tauriConvertAdapter: ConversionAdapter = {
+ async toPdf(content: string, options, outputPath): Promise {
+ const { invoke } = await import('@tauri-apps/api/tauri');
+ return invoke('convert_to_pdf', { content, options, outputPath });
+ },
+
+ async toDocx(content: string, options, outputPath): Promise {
+ const { invoke } = await import('@tauri-apps/api/tauri');
+ return invoke('convert_to_docx', { content, options, outputPath });
+ },
+
+ async toHtml(content: string, options): Promise {
+ const { invoke } = await import('@tauri-apps/api/tauri');
+ return invoke('convert_to_html', { content, options });
+ },
+
+ async batchConvert(files: string[], outputDir: string, format: string) {
+ const { invoke } = await import('@tauri-apps/api/tauri');
+ return invoke('batch_convert', { files, outputDir, format });
+ },
+};
+```
+
+**Step 3: Create tauri system adapter stub**
+
+Create `src/adapters/tauri/system.ts`:
+```typescript
+import type { PlatformCapabilities } from '@/types/platform';
+
+export const tauriCapabilities: PlatformCapabilities = {
+ hasPandoc: true, // Will be detected at runtime
+ hasFfmpeg: true,
+ hasLibreOffice: true,
+ hasDirectFs: true,
+ hasSystemNotifications: true,
+};
+```
+
+**Step 4: Create tauri adapter index**
+
+Create `src/adapters/tauri/index.ts`:
+```typescript
+import type { PlatformAdapter } from '../types';
+import { tauriFsAdapter } from './fs';
+import { tauriConvertAdapter } from './convert';
+import { tauriCapabilities } from './system';
+
+export const tauriAdapter: PlatformAdapter = {
+ name: 'tauri',
+ fs: tauriFsAdapter,
+ convert: tauriConvertAdapter,
+ capabilities: tauriCapabilities,
+};
+```
+
+**Step 5: Create main adapter export with platform detection**
+
+Create `src/adapters/index.ts`:
+```typescript
+import type { PlatformAdapter } from './types';
+import { webAdapter } from './web';
+import { tauriAdapter } from './tauri';
+
+// Detect platform at runtime
+const isTauri = typeof window !== 'undefined' && '__TAURI__' in window;
+
+export const adapter: PlatformAdapter = isTauri ? tauriAdapter : webAdapter;
+
+export type { PlatformAdapter } from './types';
+```
+
+**Step 6: Verify build**
+
+```bash
+npm run build
+```
+
+**Step 7: Commit**
+
+```bash
+git add .
+git commit -m "feat: add Tauri platform adapter stubs"
+```
+
+---
+
+### Task 9: Initialize Tauri Project
+
+**Files:**
+- Create: `src-tauri/` directory with Tauri configuration
+
+**Step 1: Install Tauri CLI**
+
+```bash
+npm install -D @tauri-apps/cli
+```
+
+**Step 2: Initialize Tauri**
+
+```bash
+npm run tauri init -- --app-name "Markdown Converter" --window-title "Markdown Converter" --dev-url http://localhost:5173 --before-dev-command "npm run dev" --before-build-command "npm run build"
+```
+
+Expected output:
+```
+[1/3] Doing some setup...
+[2/3] Building initial Tauri app...
+[3/3] Setup complete!
+```
+
+**Step 3: Update tauri.conf.json**
+
+After initialization, update `src-tauri/tauri.conf.json`:
+```json
+{
+ "build": {
+ "beforeDevCommand": "npm run dev",
+ "beforeBuildCommand": "npm run build",
+ "devPath": "http://localhost:5173",
+ "distDir": "../dist"
+ },
+ "package": {
+ "productName": "Markdown Converter",
+ "version": "5.0.0"
+ },
+ "tauri": {
+ "allowlist": {
+ "all": false,
+ "fs": {
+ "all": true,
+ "scope": ["$HOME/**", "$DOCUMENT/**", "$DOWNLOAD/**"]
+ },
+ "path": {
+ "all": true
+ },
+ "shell": {
+ "all": true,
+ "execute": true,
+ "scope": [
+ {
+ "name": "pandoc",
+ "cmd": "pandoc",
+ "args": true
+ },
+ {
+ "name": "ffmpeg",
+ "cmd": "ffmpeg",
+ "args": true
+ }
+ ]
+ },
+ "dialog": {
+ "all": true
+ },
+ "notification": {
+ "all": true
+ }
+ },
+ "bundle": {
+ "active": true,
+ "icon": [
+ "icons/32x32.png",
+ "icons/128x128.png",
+ "icons/128x128@2x.png",
+ "icons/icon.icns",
+ "icons/icon.ico"
+ ],
+ "identifier": "com.concreteinfo.markdownconverter",
+ "targets": "all"
+ },
+ "security": {
+ "csp": null
+ },
+ "windows": [
+ {
+ "title": "Markdown Converter",
+ "width": 1200,
+ "height": 800,
+ "minWidth": 800,
+ "minHeight": 600,
+ "resizable": true,
+ "fullscreen": false
+ }
+ ]
+ }
+}
+```
+
+**Step 4: Verify Tauri builds**
+
+```bash
+npm run tauri build -- --debug
+```
+
+Note: This may fail if Rust toolchain is not installed. Install from https://rustup.rs
+
+**Step 5: Commit**
+
+```bash
+git add .
+git commit -m "chore: initialize Tauri project structure"
+```
+
+---
+
+### Task 10: Create Basic Layout Components
+
+**Files:**
+- Create: `src/components/layout/MainLayout.tsx`
+- Create: `src/components/layout/TitleBar.tsx`
+- Create: `src/components/layout/StatusBar.tsx`
+- Create: `src/components/layout/index.ts`
+
+**Step 1: Create TitleBar component**
+
+Create `src/components/layout/TitleBar.tsx`:
+```typescript
+import { useThemeStore } from '@/stores';
+
+export function TitleBar() {
+ const { isDark, toggleDarkMode } = useThemeStore();
+
+ // Only show custom title bar in Tauri
+ const isTauri = typeof window !== 'undefined' && '__TAURI__' in window;
+
+ if (!isTauri) return null;
+
+ return (
+
+
+ Markdown Converter
+
+
+
+ );
+}
+```
+
+**Step 2: Create StatusBar component**
+
+Create `src/components/layout/StatusBar.tsx`:
+```typescript
+import { useEditorStore, useSettingsStore } from '@/stores';
+
+export function StatusBar() {
+ const { tabs, activeTabId } = useEditorStore();
+ const { fontSize, previewMode } = useSettingsStore();
+
+ const activeTab = tabs.find((t) => t.id === activeTabId);
+
+ const lineCount = activeTab?.content.split('\n').length ?? 0;
+ const cursorPos = activeTab?.cursorPosition ?? { line: 1, column: 1 };
+
+ return (
+
+
+ Ln {cursorPos.line}, Col {cursorPos.column}
+ {lineCount} lines
+
+
+ {fontSize}px
+ {previewMode}
+ UTF-8
+
+
+ );
+}
+```
+
+**Step 3: Create MainLayout component**
+
+Create `src/components/layout/MainLayout.tsx`:
+```typescript
+import { ReactNode } from 'react';
+import { TitleBar } from './TitleBar';
+import { StatusBar } from './StatusBar';
+
+interface MainLayoutProps {
+ children: ReactNode;
+}
+
+export function MainLayout({ children }: MainLayoutProps) {
+ return (
+
+
+ {children}
+
+
+ );
+}
+```
+
+**Step 4: Create index export**
+
+Create `src/components/layout/index.ts`:
+```typescript
+export { MainLayout } from './MainLayout';
+export { TitleBar } from './TitleBar';
+export { StatusBar } from './StatusBar';
+```
+
+**Step 5: Update App.tsx to use layout**
+
+Update `src/App.tsx`:
+```typescript
+import { MainLayout } from '@/components/layout';
+import { useEditorStore } from '@/stores';
+
+function App() {
+ const { tabs, activeTabId, createTab, updateContent } = useEditorStore();
+
+ // Create initial tab if none exists
+ if (tabs.length === 0) {
+ createTab('Welcome');
+ }
+
+ const activeTab = tabs.find((t) => t.id === activeTabId);
+
+ return (
+
+
+ {/* Editor Pane */}
+
+
+
+ {/* Preview Pane */}
+
+
+
+ Preview will be implemented with marked + highlight.js
+
+
+
+
+
+ );
+}
+
+export default App;
+```
+
+**Step 6: Verify build**
+
+```bash
+npm run build
+```
+
+**Step 7: Commit**
+
+```bash
+git add .
+git commit -m "feat: add basic layout components (TitleBar, StatusBar, MainLayout)"
+```
+
+---
+
+## Phase 1 Summary
+
+After completing Tasks 1-10, you will have:
+
+1. ✅ New project initialized with Vite + React + TypeScript
+2. ✅ Tailwind CSS configured with Shadcn/ui design tokens
+3. ✅ Zustand stores for state management
+4. ✅ Platform adapter pattern with web implementation
+5. ✅ Tauri project structure initialized
+6. ✅ Basic layout components
+
+**Next Phase:** Core Editor (CodeMirror integration, Markdown preview, themes)
+
+---
+
+## Remaining Phases (High-Level)
+
+### Phase 2: Core Editor (Week 3-4)
+- Task 11: Integrate CodeMirror 6 editor
+- Task 12: Create MarkdownPreview component
+- Task 13: Implement theme system
+- Task 14: Add tab management UI
+- Task 15: Implement keyboard shortcuts
+
+### Phase 3: Sidebar & Panels (Week 5)
+- Task 16: Create collapsible sidebar
+- Task 17: Implement ExplorerPanel
+- Task 18: Implement GitPanel
+- Task 19: Implement SnippetsPanel
+- Task 20: Implement TemplatesPanel
+
+### Phase 4: Platform Adapters (Week 6)
+- Task 21: Complete Tauri Rust commands
+- Task 22: Add capability detection
+- Task 23: Implement file watching
+
+### Phase 5: Export & Conversion (Week 7)
+- Task 24-30: Export dialogs
+
+### Phase 6: PWA & Polish (Week 8)
+- Task 31-35: PWA configuration and optimization
+
+---
+
+*Plan generated: 2026-03-15*
+*Based on design: docs/plans/2026-03-15-react-tauri-pwa-design.md*