From f1740c6bb6bcca2ff99a8ec9c123af22a79321c5 Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Sun, 15 Mar 2026 09:57:51 +0530 Subject: [PATCH] docs: add detailed implementation plan for v5.0 migration Phase 1 (Foundation) tasks with step-by-step instructions: - Task 1-2: Project initialization (Vite, React, TypeScript) - Task 3-4: Tailwind CSS + Shadcn/ui configuration - Task 5: Zustand stores (editor, settings, theme, sidebar) - Task 6-8: Platform adapter pattern (types, web, tauri stubs) - Task 9: Tauri project initialization - Task 10: Basic layout components Each task includes: - Exact file paths - Complete code snippets - Build verification steps - Commit messages Amit Haridas --- ...26-03-15-react-tauri-pwa-implementation.md | 1728 +++++++++++++++++ 1 file changed, 1728 insertions(+) create mode 100644 docs/plans/2026-03-15-react-tauri-pwa-implementation.md 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 */} +
+