# 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 */}