commit c1cf06ed67c73d22ba6431866f66045a9502cb1f Author: dev Date: Tue Apr 28 19:47:57 2026 +0000 feat: initial commit — ASCIInator v0.1 Vue 3 + Vite + Tailwind frontend, Fastify API server, execa shell bridge. All components built, server validated, 41 unit tests passing. Co-Authored-By: Claude Sonnet 4.6 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3b510aa --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}] +charset = utf-8 +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +end_of_line = lf +max_line_length = 100 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f81ee87 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo + +.eslintcache + +# Cypress +/cypress/videos/ +/cypress/screenshots/ + +# Vitest +__screenshots__/ + +# Vite +*.timestamp-*-*.mjs + +test-results/ +playwright-report/ diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..aca8928 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,10 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["eslint", "unicorn", "oxc", "vue", "vitest"], + "env": { + "browser": true + }, + "categories": { + "correctness": "error" + } +} diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..63773f1 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "printWidth": 100, + "vueIndentScriptAndStyle": false +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..0c4e4ea --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + "recommendations": [ + "Vue.volar", + "vitest.explorer", + "ms-playwright.playwright", + "dbaeumer.vscode-eslint", + "EditorConfig.EditorConfig", + "oxc.oxc-vscode", + "esbenp.prettier-vscode" + ] +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6af6b06 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,342 @@ +# ASCIInator — CLAUDE.md + +> Project spec for Claude Code. Read this fully before touching any file. +> Stack: Vue 3 + Vite + Tailwind (no component lib) + Fastify + execa +> Purpose: Headless GUI front-end for system ASCII conversion binaries. + +--- + +## 1. Project Overview + +ASCIInator converts images to ASCII art by orchestrating system binaries +(chafa, jp2a, ascii-image-converter, img2txt) via ImageMagick preprocessing. +It is a **pure front-end UI** that talks to a **local Fastify API server** +which shells out to the binaries via `execa`. + +Output format: **plain text** +Input methods: file upload, clipboard paste, drag-and-drop +Theme: dark default, respects `prefers-color-scheme` +CSS: Tailwind utility classes only — no component libraries, no Vuetify, no PrimeVue + +--- + +## 2. Repository Structure + +``` +asciinator/ +├── CLAUDE.md ← you are here +├── package.json ← root (workspaces) +├── vite.config.js +├── tailwind.config.js +├── index.html +├── src/ +│ ├── main.js +│ ├── App.vue +│ ├── assets/ +│ │ └── main.css ← Tailwind base + custom properties +│ └── components/ +│ ├── ImageInput.vue ← Component 1: input (upload / paste / drag-drop) +│ ├── ToolSelector.vue ← Component 2: ASCII tool picker + per-tool options +│ ├── ShellBridge.vue ← Component 3: translates GUI state → API call → shell +│ ├── OutputDisplay.vue ← Component 4: renders plain-text ASCII result +│ ├── ErrorLog.vue ← Component 5: watches and surfaces stderr / API errors +│ └── options/ ← Per-tool flag panels (extracted from ToolSelector) +│ ├── ChafaOptions.vue +│ ├── Jp2aOptions.vue +│ ├── AsciiOptions.vue +│ └── ImgTxtOptions.vue +└── server/ + ├── index.js ← Fastify server entry + ├── routes/ + │ └── convert.js ← POST /convert route + └── lib/ + ├── imagemagick.js ← ImageMagick pre/post processing via execa + └── converters.js ← per-tool execa invocations +``` + +--- + +## 3. Component Architecture + +### 3.1 `ImageInput.vue` +- Accepts: file picker, drag-and-drop onto drop zone, clipboard paste (`Ctrl+V`) +- Emits: `image-ready` with a `{ dataUrl, mimeType, filename }` payload +- Sends raw file to server as `multipart/form-data` +- Shows image preview thumbnail before conversion +- Validates: image/* MIME types only; max 20 MB + +### 3.2 `ToolSelector.vue` +- Provides a segmented control to choose between: + - `chafa` (default) + - `jp2a` + - `ascii-image-converter` + - `img2txt` +- Each tool reveals its own option panel when selected +- **chafa options** (full flag exposure): + - `--width` / `--height` (integer inputs — combined server-side into `--size=WxH`) + - `--colors` (none | 2 | 8 | 16/8 | 16 | 240 | 256 | full) + - `--symbols` (text input, e.g. `block+border+extra`) + - `--dither` (none | ordered | diffusion | noise) + - `--threshold` (0.0–1.0 slider) + - `--font-ratio` (float input) +- **jp2a options**: `--width`, `--height`, `--chars`, `--background` +- **ascii-image-converter options**: `--width`, `--height`, `--color`, `--braille`, `--threshold` +- **img2txt options**: `--width`, `--height`, `--format` (ansi/utf8/html), `--dither`, `--gamma` +- Emits: `tool-config` with `{ tool: string, flags: Record }` + +### 3.3 `ShellBridge.vue` +- Renderless component (``) +- Props: `image` (`{ dataUrl, mimeType, filename }`), `toolConfig` (`{ tool, flags }`) +- Exposes: `convert()` — called via template ref from App.vue on button click +- Converts `dataUrl` back to Blob via `fetch()`, sends as `multipart/form-data` +- POSTs to `/api/convert` (proxied by Vite dev server to `http://localhost:3001/convert`) +- Emits: `conversion-result` (stdout string) | `conversion-error` (stderr string) +- Handles: 30s timeout via `AbortController`, network errors, non-2xx responses + +### 3.4 `OutputDisplay.vue` +- Receives plain-text ASCII output string as prop +- Renders in a `
` block with monospace font (JetBrains Mono or Fira Code via CDN)
+- Controls: copy-to-clipboard button, download as `.txt`, font-size slider
+- Preserves whitespace exactly — never reformat or trim output
+
+### 3.5 `ErrorLog.vue`
+- Receives array of `{ timestamp, source, message }` error objects as prop
+- Collapsible panel, pinned to bottom of viewport
+- Color-coded: yellow = warning, red = error
+- Shows last 50 entries, auto-scrolls to latest
+- Clear button
+
+---
+
+## 4. Fastify Server (`server/`)
+
+### Entry: `server/index.js`
+```js
+// Fastify + @fastify/multipart
+// Listens on localhost:3001
+// Single route: POST /convert
+// CORS: origin: true (reflects requester — permissive for local dev)
+// Logger: warn level only (startup logged via console.log)
+```
+
+### Route: `POST /convert`
+1. Parse multipart body — extract image file + JSON fields (`tool`, `flags`)
+2. Write image to temp file (`/tmp/asciinator-.`)
+3. Pass through ImageMagick preprocessing (see §4.1)
+4. Invoke selected tool via `converters.js`
+5. Stream stdout back as `text/plain`
+6. Clean up temp files in `finally`
+7. On non-zero exit: return 422 with stderr body
+
+### 4.1 ImageMagick Pipeline (`lib/imagemagick.js`)
+**Preprocessing** (always runs before ASCII tool):
+- Convert to PNG by default, JPEG for jp2a (`magick input.x output.{png,jpg}`)
+- Strip metadata (`-strip`)
+- Auto-orient (`-auto-orient`)
+- Resize to max 2000px on longest side if larger (`-resize 2000x2000>`)
+- `jp2a` receives JPEG — it links against libjpeg and rejects PNG input
+
+**Post-processing** (optional, future hook — stub it now):
+- Reserved for potential brightness/contrast adjustment on output
+
+### 4.2 Converters (`lib/converters.js`)
+Each converter is a function:
+```js
+async function runChafa(imagePath, flags) → { stdout, stderr }
+async function runJp2a(imagePath, flags) → { stdout, stderr }
+async function runAsciiImageConverter(imagePath, flags) → { stdout, stderr }
+async function runImgToTxt(imagePath, flags) → { stdout, stderr }
+```
+- Use `execa` for all invocations
+- Map the `flags` object to CLI args (e.g. `{ width: 80 }` → `['--width', '80']`)
+- Validate flag values before constructing args — reject unknown flags
+
+---
+
+## 5. State Management
+
+Use Vue 3 `reactive()` in `App.vue` — no Pinia for this scale.
+
+```js
+const state = reactive({
+  image: null,          // { dataUrl, mimeType, filename }
+  toolConfig: { tool: 'chafa', flags: {} },
+  result: '',           // plain text ASCII output
+  errors: [],           // [{ timestamp, source, message }]
+  isConverting: false,
+})
+```
+
+Pass state slices as props, bubble events up. No global store.
+
+---
+
+## 6. Styling Conventions
+
+- **Tailwind only** — no inline styles, no external CSS except `src/assets/main.css`
+- Always dark — `class="dark"` hardcoded on ``. Light mode support planned (needs CSS variable overrides)
+- CSS custom properties in `main.css` for brand tokens:
+  ```css
+  :root {
+    --ascii-green: #39ff14;   /* neon accent */
+    --ascii-bg: #0d0d0d;
+    --ascii-surface: #1a1a1a;
+    --ascii-border: #2a2a2a;
+    --ascii-text: #e0e0e0;
+    --ascii-error: #ff4444;
+    --ascii-warn: #ffaa00;
+  }
+  ```
+- Monospace font for all output: `font-family: 'JetBrains Mono', 'Fira Code', monospace`
+- UI font: `font-family: 'IBM Plex Sans', sans-serif`
+- Load both via Google Fonts / Bunny Fonts in `index.html`
+
+**`@layer components` utility classes** (defined in `main.css` — use these in templates):
+
+| Class | Purpose |
+|-------|---------|
+| `.ascii-input` | Dark-themed text input or select — full width, bordered, focus ring |
+| `.ascii-label` | Column-flex label wrapper with muted text — wraps a `.ascii-input` |
+| `.ascii-btn` | Ghost button — transparent bg, border, hover turns neon green |
+| `.ascii-btn-primary` | Solid neon-green button — used for the main Convert action |
+
+---
+
+## 7. Neovim IDE v2 Setup
+
+### 7.1 LSP & Tooling (expected config)
+| Tool | Role |
+|------|------|
+| `volar` | Vue 3 LSP (use Takeover mode — disable `tsserver` for `.vue` files) |
+| `eslint-lsp` | Linting via `eslint_d` or native LSP client |
+| `prettier` | Formatting — `.prettierrc` at root |
+| `emmet-ls` | HTML/template expansion in `.vue` files |
+| `cssls` | Tailwind class intellisense (pair with `tailwindcss-language-server`) |
+
+### 7.2 `.prettierrc`
+```json
+{
+  "semi": false,
+  "singleQuote": true,
+  "tabWidth": 2,
+  "printWidth": 100,
+  "vueIndentScriptAndStyle": false
+}
+```
+
+### 7.3 `.eslintrc.cjs`
+```js
+module.exports = {
+  extends: ['plugin:vue/vue3-recommended', 'prettier'],
+  rules: {
+    'vue/multi-word-component-names': 'off'
+  }
+}
+```
+
+### 7.4 Neovim v2 Test Notes
+This project is a test bed for **Neovim IDE v2.1.x**. Log observations here:
+
+- [ ] Volar Takeover mode stability with Vite HMR running
+- [ ] Tailwind intellisense triggering inside `class=""` in `.vue` templates
+- [ ] `emmet-ls` expansion in `