Skip to content

Commit b1cd5ad

Browse files
Add requirements and rebuild specification for DiffTool
Document the requirements and specifications for the DiffTool web app.
1 parent 01f6db8 commit b1cd5ad

1 file changed

Lines changed: 301 additions & 0 deletions

File tree

requirement_app.md

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
# DiffTool — Requirements & Rebuild Specification
2+
3+
A fully client-side React web app for comparing two code files or text snippets.
4+
No backend. Deployable as a static site on GitHub Pages.
5+
6+
---
7+
8+
## 1. Goals
9+
10+
- User can paste two blocks of text/code and compare them.
11+
- User can upload two files and compare them.
12+
- Both sides can mix paste and upload independently.
13+
- Supports any plain-text format including `.ipynb` Jupyter notebooks.
14+
- Diff renders with line numbers, added/removed highlighting, and a change summary.
15+
- Zero server dependency — everything runs in the browser.
16+
17+
---
18+
19+
## 2. Tech Stack
20+
21+
| Tool | Version | Purpose |
22+
|---|---|---|
23+
| React | ^18.3.1 | UI framework |
24+
| Vite | ^5.4.10 | Build tool + dev server |
25+
| @vitejs/plugin-react | ^4.3.1 | JSX support in Vite |
26+
| diff (jsdiff) | ^5.2.0 | Compute unified diff patches and line-level stats |
27+
| diff2html | ^3.4.48 | Render unified diff as HTML (side-by-side or unified) |
28+
29+
No CSS framework, no router, no state management library.
30+
31+
---
32+
33+
## 3. Directory Structure
34+
35+
```
36+
project-root/
37+
├── .github/
38+
│ └── workflows/
39+
│ └── deploy.yml # GitHub Actions: build + deploy to Pages
40+
├── src/
41+
│ ├── components/
42+
│ │ ├── InputPane.jsx # One input panel (paste tab + upload tab)
43+
│ │ └── DiffViewer.jsx # Renders the diff output
44+
│ ├── utils/
45+
│ │ └── parseFile.js # Reads File objects → plain text string
46+
│ ├── App.jsx # Root component, all state lives here
47+
│ ├── App.css # All styles (single CSS file, no modules)
48+
│ └── main.jsx # React entry point
49+
├── index.html
50+
├── package.json
51+
└── vite.config.js
52+
```
53+
54+
---
55+
56+
## 4. Feature Requirements
57+
58+
### 4.1 Input Panes
59+
60+
There are two input panes side by side: **Original** (left) and **Modified** (right).
61+
62+
Each pane has two tabs:
63+
64+
**Paste tab**
65+
- Monospace `<textarea>` with `spellCheck={false}`.
66+
- Placeholder: `"Paste original code here..."` / `"Paste modified code here..."`.
67+
- User can type or paste any text.
68+
69+
**Upload tab**
70+
- A dashed-border drop zone that accepts drag-and-drop or click-to-browse.
71+
- Clicking the zone opens a hidden `<input type="file">`.
72+
- Dragging a file over the zone changes its border and background color (visual feedback).
73+
- After a file is loaded:
74+
- Show the file name and a file icon.
75+
- Show two buttons: **Change** (re-opens file picker) and **Remove** (clears the pane).
76+
- Do NOT switch back to the Paste tab — stay on Upload tab showing the loaded file.
77+
- The parsed file text is fed into the same state as the paste textarea (shared `value`).
78+
79+
### 4.2 Swap Button
80+
81+
A narrow vertical button between the two panes with a swap/arrows icon.
82+
- Clicking it swaps left content ↔ right content AND left filename ↔ right filename.
83+
- Resets `compared` state to `false` (hides the old diff output).
84+
85+
### 4.3 Controls Bar
86+
87+
A horizontal bar below the input section containing:
88+
89+
**Left side:**
90+
- **Compare** button — primary action, indigo/purple, disabled when both sides are empty.
91+
- **Clear** button — ghost style, disabled when both sides are empty. Resets all state.
92+
93+
**Right side:**
94+
- **Diff stats badge** — shown only after Compare is clicked. Displays `+N` (green) and `-N` (red) line counts. Label: "lines changed".
95+
- **View toggle** — two buttons: "Side by Side" and "Unified". Switching is instant; does NOT require re-clicking Compare.
96+
97+
### 4.4 Diff Output
98+
99+
Shown only after Compare is clicked. Hidden (replaced by empty state) before first Compare.
100+
101+
- Uses `diff2html` to render the diff.
102+
- `drawFileList: false` — do not show the file list header.
103+
- `matching: 'lines'` — match lines for better diff quality.
104+
- `outputFormat`: `'side-by-side'` or `'line-by-line'` based on the view toggle.
105+
- The diff is computed from a **snapshot** taken at the moment Compare is clicked, not live. Editing the textarea after Compare does not update the diff until Compare is clicked again.
106+
- The `fileName` passed to `createPatch` is the uploaded file name if one exists, otherwise `'file'`.
107+
108+
### 4.5 Empty State
109+
110+
When no diff has been run yet, show a centered placeholder with:
111+
- A faint SVG icon (diff-like visual).
112+
- Text: `Paste code or upload files above, then click Compare`.
113+
114+
### 4.6 Footer
115+
116+
Full-width footer at the bottom:
117+
> "Runs entirely in your browser — no data is sent to any server"
118+
119+
---
120+
121+
## 5. File Parsing Logic (`src/utils/parseFile.js`)
122+
123+
```
124+
parseFile(file: File) → Promise<string>
125+
```
126+
127+
- Read the file as text using `file.text()`.
128+
- If `file.name` ends with `.ipynb`: run `parseNotebook(text)`.
129+
- Otherwise: return the raw text string.
130+
131+
**`parseNotebook(text: string) → string`**
132+
133+
- Parse JSON. If parse fails, return raw text as fallback.
134+
- Access `nb.cells` array (default to `[]`).
135+
- For each cell:
136+
- Extract `cell.source`: if it's an array, join with `''`; if string, use as-is; fallback to `''`.
137+
- Build a header: `# ── Cell N [cell_type] ──` where N is 1-indexed.
138+
- Combine: `header + '\n' + source`.
139+
- Join all cells with `'\n\n'` between them.
140+
141+
---
142+
143+
## 6. State (App.jsx)
144+
145+
| State variable | Type | Purpose |
146+
|---|---|---|
147+
| `left` | string | Current text in the left pane |
148+
| `right` | string | Current text in the right pane |
149+
| `leftFile` | string \| null | Filename of the uploaded left file |
150+
| `rightFile` | string \| null | Filename of the uploaded right file |
151+
| `viewType` | `'side-by-side'` \| `'unified'` | Which diff view to render |
152+
| `compared` | boolean | Whether a diff has been computed and shown |
153+
| `diffSnapshot` | `{ left, right, fileName }` | Frozen copy of content at Compare time |
154+
155+
**Key behavior:**
156+
- `compared` resets to `false` whenever either `left` or `right` content changes (typing or new file).
157+
- `compared` resets to `false` on Swap and Clear.
158+
- `stats` (added/removed line counts) is derived via `useMemo` from `diffSnapshot` when `compared` is `true`.
159+
- `isEmpty` is `true` when both `left.trim()` and `right.trim()` are empty strings — used to disable Compare and Clear.
160+
161+
---
162+
163+
## 7. Component Props
164+
165+
### `InputPane`
166+
167+
| Prop | Type | Description |
168+
|---|---|---|
169+
| `label` | string | `"Original"` or `"Modified"` — shown in pane header, used in textarea placeholder |
170+
| `value` | string | Controlled text value |
171+
| `onChange` | `(text: string) => void` | Called when text changes (paste or file upload) |
172+
| `onFile` | `(name: string \| null) => void` | Called with filename when file is loaded, or `null` when removed |
173+
174+
Internal state: `tab` (`'paste'` \| `'upload'`), `fileName` (string \| null), `dragging` (boolean).
175+
176+
### `DiffViewer`
177+
178+
| Prop | Type | Description |
179+
|---|---|---|
180+
| `left` | string | Original text (from snapshot) |
181+
| `right` | string | Modified text (from snapshot) |
182+
| `viewType` | string | `'side-by-side'` or `'unified'` |
183+
| `fileName` | string | Passed to `createPatch` as the file name |
184+
185+
Implementation: uses `useEffect` + `useRef`. On every prop change, calls `createPatch` from the `diff` library, then `Diff2Html.html()`, and sets `containerRef.current.innerHTML` to the result.
186+
187+
Import the diff2html CSS bundle: `import 'diff2html/bundles/css/diff2html.min.css'`
188+
189+
---
190+
191+
## 8. Design System
192+
193+
### Color Palette
194+
195+
| Role | Value |
196+
|---|---|
197+
| Page background | `#f0f2f5` |
198+
| Primary text | `#1a1a2e` |
199+
| Header background | `#1e1b4b` (dark indigo) |
200+
| Header accent border | `#4f46e5` (indigo) |
201+
| Header icon color | `#818cf8` |
202+
| Header subtitle | `#a5b4fc` |
203+
| Primary button / active states | `#4f46e5` |
204+
| Primary button hover | `#4338ca` |
205+
| Added lines stat | `#16a34a` (green) |
206+
| Removed lines stat | `#dc2626` (red) |
207+
| Panel background | `#ffffff` |
208+
| Panel border | `#e2e8f0` |
209+
| Muted text / labels | `#64748b` |
210+
| Disabled / placeholder | `#94a3b8` |
211+
| Pane header background | `#f8fafc` |
212+
| Drop zone background | `#fafbfc` |
213+
| Drop zone hover background | `#f5f3ff` |
214+
| Drop zone dragging background | `#ede9fe` |
215+
216+
### Typography
217+
218+
- Body / UI: `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif`
219+
- Code textarea and diff table: `'JetBrains Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace`
220+
- Code textarea font size: `0.82rem`, line-height `1.6`
221+
222+
### Layout
223+
224+
- Max content width: `1600px`, centered with `margin: 0 auto`.
225+
- Input section: two panes (`flex: 1`) separated by a 40px wide swap button column. No gap between panes — borders handle separation.
226+
- Textarea minimum height: `280px`, `resize: vertical`.
227+
- Drop zone minimum height: `280px`.
228+
- Controls bar: `justify-content: space-between` with left group (Compare, Clear) and right group (stats badge + view toggle).
229+
230+
### diff2html CSS Overrides
231+
232+
Apply these overrides on `.diff-output` to integrate with the app style:
233+
- Remove wrapper margins: `.d2h-wrapper { margin: 0 }`
234+
- Remove file wrapper border/radius/margin
235+
- Style `.d2h-file-header` with `#f8fafc` background, `0.8rem` font size
236+
- Use monospace font on `.d2h-diff-table`
237+
- Line number cells: `color: #94a3b8`, `background: #f8fafc` with `!important`
238+
239+
---
240+
241+
## 9. Build Configuration
242+
243+
### `vite.config.js`
244+
```js
245+
import { defineConfig } from 'vite'
246+
import react from '@vitejs/plugin-react'
247+
248+
export default defineConfig({
249+
plugins: [react()],
250+
base: './', // relative base — required for GitHub Pages subdirectory hosting
251+
})
252+
```
253+
254+
### `index.html`
255+
Standard Vite HTML entry. Script tag: `<script type="module" src="/src/main.jsx"></script>`.
256+
257+
### `package.json` scripts
258+
```json
259+
"dev": "vite",
260+
"build": "vite build",
261+
"preview": "vite preview"
262+
```
263+
264+
---
265+
266+
## 10. GitHub Pages Deployment
267+
268+
### GitHub Actions workflow (`.github/workflows/deploy.yml`)
269+
270+
Trigger: push to `main` branch.
271+
272+
Required permissions: `contents: read`, `pages: write`, `id-token: write`.
273+
274+
Steps:
275+
1. `actions/checkout@v4`
276+
2. `actions/setup-node@v4` — Node 20, cache `npm`
277+
3. `npm ci`
278+
4. `npm run build` — outputs to `dist/`
279+
5. `actions/upload-pages-artifact@v3` — path: `dist`
280+
6. `actions/deploy-pages@v4` — deploys artifact
281+
282+
Environment name: `github-pages`. Concurrency group: `pages`, cancel-in-progress: `false`.
283+
284+
### One-time GitHub repo setup
285+
286+
After pushing, go to:
287+
**Repo → Settings → Pages → Build and deployment → Source → GitHub Actions**
288+
289+
The app will then auto-deploy on every push to `main`.
290+
291+
---
292+
293+
## 11. Known Behaviors & Edge Cases
294+
295+
- **Both-empty guard**: Compare and Clear buttons are disabled when both sides are empty (checked via `.trim()`).
296+
- **Identical content**: diff2html renders a "no changes" message — this is acceptable behavior.
297+
- **Binary files**: The browser's `file.text()` will attempt to decode them as UTF-8. No explicit binary detection is implemented; garbled output is acceptable.
298+
- **Large files**: No size limit is enforced. Performance degrades gracefully in the browser.
299+
- **`.ipynb` parse failure**: If JSON parsing fails, `parseNotebook` returns the raw text string as a fallback.
300+
- **Changing view type** (side-by-side ↔ unified) does NOT re-snapshot; it re-renders from the existing `diffSnapshot` — this is intentional for instant toggling without re-clicking Compare.
301+
- **Changing content after Compare**: `compared` is set to `false`, hiding the old diff. The user must click Compare again to see the new diff.

0 commit comments

Comments
 (0)