Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 61 additions & 25 deletions DEVELOPMENT_PLAN.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# DiffBro – Development Plan

Goal: cross-platform (Windows + macOS) desktop diff viewer with GitHub-style
rendering. Text files first; Word, PDF, and image comparison later via the
rendering. Text files first; Word and Excel comparison later via the
adapter pattern.

Stack: Electron + electron-vite + Vue 3 + Pinia + Monaco diff editor +
Expand Down Expand Up @@ -154,9 +154,12 @@ First run: `npm install && npm run dev`
every diagram type in real Chromium before adopting the dep). SVG is
inserted via `DOMParser` + `replaceChildren`, never `innerHTML`/`v-html`;
`securityLevel: 'strict'` (DOMPurify) is never lowered.
- [x] Live preview in the snippet editor + a resizable, zoom/pan diagram viewer;
diagram theme paired to the app theme (dark → `dark`, light → `default`),
re-rendered on theme switch so text never blends into the canvas
- [x] Live preview in the snippet editor + a zoom/pan diagram viewer, drag-resizable
from any of its four corners (`useResizable` + pure `utils/resizeRect.js`) and
auto-maximised when the app window enters fullscreen (main pushes
`window:fullscreen`, `useFullScreen` relays it); diagram theme paired to the
app theme (dark → `dark`, light → `default`), re-rendered on theme switch so
text never blends into the canvas
- [x] Auto-detect for the snippet editor's syntax picker
(`utils/detectLanguage.js`): distinctive, low-ambiguity signals for every
offered language (JSON, Mermaid, SQL, Markdown, YAML/K8s, Python, shell,
Expand All @@ -174,7 +177,11 @@ First run: `npm install && npm run dev`
visibility, and user-raisable comparison-file / snippet size limits with
safe defaults and hard ceilings (main enforces the file limit from it)
- [x] Reorderable sidebar sections behind a shared `SectionHeader`; Saved /
External / Snippets each extracted into a self-contained component
External / Snippets each extracted into a self-contained component. Reorder
by dragging a whole header (`useSectionReorder`) or via its up/down
steppers; a single toolbar padlock freezes the arrangement
(`settings.sectionsLocked`, persisted — locked headers drop the drag handle
and steppers)
- [x] Diff search gains match-case, whole-word, and safety-limited regex
(`utils/searchRegex.js` refuses over-long / catastrophic patterns)
- [x] Partial paste mode: diff pasted text against a dropped/chosen file
Expand Down Expand Up @@ -226,21 +233,51 @@ First run: `npm install && npm run dev`
- [ ] Extend `file:read` IPC to return a Buffer for binary formats
- [ ] Note limitation in UI: content diff, not formatting diff

## Phase 5 – PDF (~3–4 days)

- [ ] `pdfAdapter`: extract text with `pdfjs-dist` (renderer-side is fine)
- [ ] Normalize extraction artifacts: hyphenation, line-order in multi-column
layouts, page markers
- [ ] Out of scope initially: scanned PDFs (would need tesseract.js OCR) and
visual/pixel diff of rendered pages — decide later if needed

## Phase 6 – Images (~2 days)

- [ ] `imageAdapter` returning `{ kind: 'image', dataUrl }` — first non-text
comparable kind
- [ ] New `ImageDiffViewer.vue`: side-by-side, overlay slider, and pixel-diff
mode via `pixelmatch` (render highlighted-difference canvas)
- [ ] Router in the content area: pick viewer component by comparable `kind`
## Phase 5 – Excel spreadsheets (~5–7 days)

A **structured grid diff** (not text extraction): sheet tabs, aligned grids,
cell/row/column-level highlighting. `.xlsx` only (zip-of-XML); legacy `.xls`
(BIFF) is out of scope.

- [x] **Parser spike (Phase 0):** custom, minimal, read-only OOXML reader in the
**main process** on `fflate` (zip) + `saxen` (streaming SAX) — chosen over
SheetJS (npm frozen at 0.18.5 with unpatched CVE-2023-30533 +
CVE-2024-22363; fixes CDN-only) and exceljs (21 MB, write surface). Lives
in `src/main/xlsx/`, fully unit-tested (`tests/main/xlsx/`, 16 cases incl.
bomb / DOCTYPE / cell-budget / proto-pollution). Security by *not parsing*:
only `workbook.xml`, its rels, `sharedStrings.xml`, and `worksheets/sheetN.xml`
are inflated; formulas (`<f>`) are never read or evaluated; external links,
VBA, drawings, media, styles are never touched. Caps: decompression-bomb
(input/entry/total/ratio), per-sheet cell budget; `DOCTYPE` rejected (XXE).
- [x] `file:read` detects `.xlsx` (extension + `PK` zip magic) before the binary
sniff and parses it in main, returning `{ kind:'spreadsheet', sheets }` or a
polite `{ error:'xlsx' }` (`src/main/files.js`). Shares the binary read path
with the Word/`docx` phase.
- [x] `xlsxAdapter` returning `{ kind: 'spreadsheet', sheets }`; registered ahead
of textAdapter (`adapters/xlsxAdapter.js`).
- [x] **Router in the content area:** `App.vue` picks the viewer by
`store.comparableKind` (`DiffViewer` for text, `SpreadsheetDiffViewer` for
spreadsheets).
- [x] `SpreadsheetDiffViewer.vue` (+ `SheetTabBar`, `SpreadsheetGrid`): sheet tabs
with per-sheet change counts, two aligned grids sharing one scroll region,
changed-cell / added-row / removed-row highlighting, and a status strip.
- [x] Row-alignment algorithm (`utils/alignRows.js`, unit-tested): LCS over row
signatures with key-column pairing so an inserted/deleted row doesn't
cascade; O(n·m) LCS under a 4M-product budget, else O(n) positional.
- [x] Hang protection: the grid caps rendered rows at `RENDER_ROW_CAP` (3000) with
a "first N rows shown" note — no virtualization yet. Text already has its
guards (10 MB file-size prompt on load + `MAX_DIFF_LINES` on the patch).
Stress benchmark: `tests/stress/diff-stress.test.js` (opt-in, `STRESS=1`).
Measured on this machine: parse+align stays smooth to 10k rows (~99 ms),
"ok" to 50k (~0.5 s), sluggish at 100k / 5.5 MB (~1 s); the DOM render is the
real viewing ceiling (Docker-measured).
- [ ] Note limitation in UI: value diff, not formatting; dates read as serials
(styles deliberately not parsed).
- [ ] Follow-up: true row virtualization for the grid (replace `RENDER_ROW_CAP`);
swap the synchronous unzip for `fflate` streaming `Unzip` with a hard
byte-abort so the bomb bound is enforced *during* inflation.
- [ ] E2E (`e2e/spreadsheet.spec.mjs`) written — verify under `make e2e` (Docker),
it can't launch Electron on the host.

---

Expand Down Expand Up @@ -270,9 +307,9 @@ the machine where the app runs. Measures already implemented in the scaffold:
Google).
- **External navigation blocked**: `setWindowOpenHandler` denies all popups;
`will-navigate` is restricted to the app itself.
- **No CDN assets**: Monaco (and later mammoth/pdfjs/pixelmatch) are bundled
from npm at build time. `npm install` needs network on the *dev* machine
only; the packaged app does not.
- **No CDN assets**: Monaco (and mammoth for docx, fflate + saxen for xlsx) are
bundled from npm at build time. `npm install` needs network on the *dev*
machine only; the packaged app does not.
- **No telemetry, no crash reporting, no auto-update** — none included, keep
it that way. When adding any dependency, check it makes no network calls.

Expand All @@ -290,5 +327,4 @@ Verification checklist before each release:
the editor chunk.
- **Very large files**: Monaco handles a few MB well; beyond that, consider
a jsdiff + virtual-scroll fallback view.
- **macOS builds require macOS**: plan on GitHub Actions early if you don't
have a Mac available.

31 changes: 27 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,28 @@ a hard promise: it never touches the network.
- **Diff** two files or pasted text — split or inline, word-level highlights,
syntax highlighting, in-view search, and a live re-diff when a file changes on
disk. Copy the result as a git-style unified patch.
- **Excel (.xlsx) comparison** — a structured **grid** diff with sheet tabs and
cell / row-level highlighting, aligned so an inserted row doesn't cascade.
Parsed entirely offline by a small custom reader (no heavyweight dependency).
- **Paste mode** for quick throwaway comparisons, including pasted text against a
real file.
real file — or just hit **Ctrl/Cmd+V** to paste straight into a comparison.
- **Drag & drop** files onto the window; it warns before discarding unsaved work.
- **Saved diffs** — encrypted, auto-expiring, organized into categories.
- **Share** a diff as a sealed, signed file only its intended recipient can open.
- **Snippets** — an encrypted, tagged text library with per-language
highlighting and live **Mermaid** diagram rendering.
highlighting and live **Mermaid** diagram rendering, in a viewer you can drag
bigger from any corner (and that fills the window when the app goes fullscreen).
- **Tools** — Base64, JSON / XML / SQL format + validate, and passphrase text
encryption.
- **Yours to arrange** — five themes, a rearrangeable sidebar, and adjustable
limits, all remembered between sessions.
- **Yours to arrange** — eight themes (incl. Nord, Sepia, and a playful Nyan with
a reward cat), a sidebar whose sections you drag to
reorder (and can lock in place), and adjustable limits, all remembered between
sessions.

<p align="center">
<img src="docs/screenshots/spreadsheet-diff.png" width="820" alt="Two Excel files compared as aligned grids: changed cells boxed, a removed row and an added row shown as striped gaps, with per-sheet change counts">
<br><em>Excel (.xlsx) files compared as aligned grids — changed cells boxed, added/removed rows aligned.</em>
</p>

<table>
<tr>
Expand All @@ -52,6 +63,13 @@ a hard promise: it never touches the network.
<p align="center"><em>Saved diffs are encrypted on-device and auto-expire.</em></p>
</td>
</tr>
<tr>
<td width="50%" valign="top">
<img src="docs/screenshots/empty-state.png" alt="The start screen listing supported file types: Excel, JSON, XML, YAML, CSV, Markdown, and any text or code file">
<p align="center"><em>Drop or choose two files — Excel, JSON/XML, or any text.</em></p>
</td>
<td width="50%" valign="top"></td>
</tr>
</table>

## Download
Expand Down Expand Up @@ -96,7 +114,12 @@ No local Node? The same flow runs in Docker: `make dev`, `make check`,
## Docs

- [Architecture](docs/architecture.md) — processes, trust boundary, directory map.
- [IPC & security](docs/ipc-security.md) — how renderer↔main talk, and what the
sandbox blocks (with diagrams).
- [Security model](docs/security.md) — offline guarantee, sharing, keys, backup.
- [Packaging & releasing](docs/packaging.md) — installers, signing notes, CI.
- [Chocolatey release](docs/chocolatey.md) — plan + package skeleton for
`choco install diffbro`.
- [Glossary](docs/glossary.md) — every term and abbreviation (IPC, CSP, GCM, …).
- Coding standards live in [CLAUDE.md](CLAUDE.md); roadmap in
[DEVELOPMENT_PLAN.md](DEVELOPMENT_PLAN.md).
96 changes: 96 additions & 0 deletions THEMES_TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Themes — backlog

Theme concepts to implement later. Design pitch (with live mockups):
https://claude.ai/code/artifact/7228542b-2337-4506-b3b4-aeeb957b3db1

## How to add a theme (for each below)

1. Add a `:root[data-theme='<id>']` block to `src/renderer/src/styles/themes.css`
redefining **every** palette token — nothing structural, so alignment/sizing
stay identical across themes.
2. Register it in `src/renderer/src/utils/themes.js` (`THEMES` array) with a
`swatch: { bg, accent, add, del }` for the Settings → Appearance preview.
3. Verify in **both** the diff view and the empty state, in the Docker env.
4. Monaco/Mermaid ground keys off `isDarkTheme(id)` — add the id there if it's a
dark theme.

Palette token names: `--bg --bg-panel --bg-hover --border --text --text-dim
--text-hint --accent --warning-bg/border/text --danger-bg/border/text
--success-text --favorite --text-on-accent`.

---

## Useful

- ~~**Nord** `id: nord`~~ — ✅ **SHIPPED** (themes.css + themes.js).
- ~~**Sepia** `id: sepia`~~ — ✅ **SHIPPED** (deepened to a saturated parchment so
it stands apart from Solar's pale cream).

---

## Retro (need a small per-skin extension beyond tokens)

### Windows 98 `id: win98` (light)
Teal desktop, silver bevelled panels, navy title-bar gradient.
- bg `#008080` · panel `#c0c0c0` · hover `#d4d0c8` · border `#808080`
- text `#000000` · dim `#404040` · accent `#000080`
- success/add `#008000` · danger/del `#800000` · text-on-accent `#ffffff`
- **Extra:** bevel treatment (raised `#dfdfdf` top-left / `#808080` bottom-right)
on panels + buttons; square corners (radius 0); Tahoma/MS-Sans font stack.
Gate the bevels behind a `[data-theme='win98']` skin block so base components
are untouched.

### Mac Platinum `id: platinum` (light)
System 7/8 grayscale, pinstripe chrome, near-monochrome.
- bg `#b8b8b8` · panel `#dcdcdc` · hover `#cfcfcf` · border `#808080`
- text `#101010` · dim `#555555` · accent `#303030`
- success/add `#2f7d4f` · danger/del `#b23a3a`
- **Extra:** pinstripe background (`repeating-linear-gradient`) on title bands;
Charcoal/Geneva font stack.

---

## Novelty (opt-in, more work)

### Phosphor CRT `id: crt` (dark)
Black ground, green phosphor text, faint scanlines + glow.
- bg `#050805` · panel `#0b120b` · hover `#10200f` · border `#154a15`
- text `#33ff66` · dim `#1f9a3f` · accent `#7dffa0`
- success/add `#b6ff00` · danger/del `#ff5f5f`
- **Extra:** a scanline overlay (`::after` repeating-linear-gradient, low
opacity) + `text-shadow` glow. Keep the overlay subtle (readability). Respect
`prefers-reduced-motion` if any flicker is added. `isDarkTheme` → true.

### Nyan `id: nyan` (dark) — ✅ **SHIPPED** (Trail × Achievement combo)

Built as the "Trail × Achievement" combo from the variants pitch
(https://claude.ai/code/artifact/0b961526-e8e7-4a9d-b05d-4337f30d66de):
a readable deep-violet base (chaos kept out of content), plus `NyanLane.vue` — a
slim rainbow lane under the toolbar (`App.vue`, nyan theme only) where the cat
streaks across trailing a smoke-dissipating rainbow on a "No differences!" match
or a saved diff (`useNyanReward` rising-edge trigger). Honors
`prefers-reduced-motion`.

Per feedback: NOT the unusable full-screen scrolling-rainbow-puke from the pitch.
Keep a **readable base** so diffing actually works, and confine the chaos to
non-content chrome:
- **Readable dark base:** bg `#160a20` · panel `#231033` · hover `#2e1642`
· border `#5a2b7a` · text `#f4e9ff` · dim `#b79fcf`.
- **Playful accents:** accent `#ff2ecb` (hot pink) · success/add `#63ff4d`
(lime) · danger/del `#ff5470`.
- **Where the fun lives (distracting-but-usable):** an animated rainbow accent
stripe on the toolbar band, and the Nyan cat + rainbow **only in the empty
state** (the `SupportedFormats` / "drop two files" area) — never over the diff
panes, so content stays legible.
- **Extra:** the Nyan cat (inline SVG) + rainbow trail keyframes from the pitch,
scoped to `[data-theme='nyan'] .empty`; MUST honor `prefers-reduced-motion`
(park the cat, freeze the rainbow). `isDarkTheme` → true.

---

## Suggested order

1. ~~**Nord**, **Sepia**~~ — ✅ shipped.
2. **Win98**, **Platinum** — add the shared per-skin bevel/pinstripe mechanism.
3. **CRT**, **Nyan** — the overlay + animation layer (and a stern review for Nyan).
See the expanded Nyan variant ideas artifact linked at the top.
Loading
Loading