Files
convert/docs/superpowers/specs/2026-07-31-ombrora-frontend-redesign-design.md
anthony ab95e3e699 docs: add Ombrora Convert frontend redesign design spec
Foundation sub-project: merged homepage+tool page, FR/EN i18n via URL
prefix, dark mode, SEO basics, and reassurance elements backed by
actual backend guarantees (1h auto-delete, 100MB max, no account).
2026-07-31 14:27:15 +02:00

117 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ombrora Convert — frontend redesign (foundation)
Date: 2026-07-31
## Goal
Redesign `frontend/` into a modern, minimalist, dark-mode-capable, bilingual (FR/EN), SEO-friendly single page that embeds the existing file-conversion tool directly in the homepage, with reassurance elements built from real backend facts. This is Sub-project 1 of two: it delivers the shared shell (design system, i18n, dark mode, SEO fundamentals). A follow-up Sub-project 2 (programmatic per-format-pair landing pages, e.g. `/fr/jpg-vers-png`) is deliberately out of scope here — it needs this foundation to exist first, and needs its own content/routing spec.
## Scope decisions (from brainstorming)
- **One merged page, not two.** Originally considered a marketing Home + separate `/convertir` tool page; changed after user feedback — the upload/convert flow is embedded directly in the homepage hero. There is nothing left for a separate tool route to do, so it's dropped.
- **i18n via URL prefix**, not a client-side toggle: `/fr/` and `/en/` are distinct, indexable, bilingual URLs connected by `hreflang`. Chosen over a single-URL toggle specifically because Google can only index one language per URL.
- **Root `/` redirects** to `/fr/` or `/en/` based on `Accept-Language`, defaulting to `/fr/` if undetected — avoids duplicate content on `/` vs `/fr/`.
- **Legal pages (mentions légales, confidentialité, CGU) are deferred.** No real company/hosting/legal data exists yet to put in them; publishing placeholder legal text would be worse than not having the pages. Revisit once that content is ready.
- **Reassurance claims are limited to what the backend actually guarantees today**, verified in code, not invented:
- Auto-deletion: `src/config.js``retentionHours` (`RETENTION_HOURS` env, default `1`), enforced by `src/cleanup.js` (cron-driven in production per `README.md`).
- Max file size: `src/config.js``maxFileSizeMb` (`MAX_FILE_SIZE_MB` env, default `100`).
- No account required: true today — `POST /api/jobs` requires no auth.
- 100% online / nothing to install: true — browser upload, server-side conversion.
- RGPD/hosting-location messaging was explicitly **not** requested by the user and is not included.
- **Brand name: "Ombrora Convert"** (per user), replacing the generic "file-converter"/"Convertisseur de fichiers" placeholder title.
- **No SSR/prerendering in this sprint.** The site remains a client-rendered Vite SPA; SEO relies on on-page tags, structured data, and Googlebot's JS rendering. Flagged as a known limitation, not a gap to silently fix — a candidate for Sub-project 2 if organic performance requires it.
- **No new frontend test harness.** `frontend/` has no test setup today; adding one is a separate decision, not bundled into a redesign. Verification for this sprint is manual (`npm run dev`, both languages, both themes, multiple viewport widths).
## Architecture
### Dependencies (new, all pure JS — no native bindings, safe for o2switch)
Added to `frontend/package.json`:
- `react-router-dom` — routing.
- `react-i18next`, `i18next`, `i18next-browser-languagedetector` — i18n.
- `@phosphor-icons/react` — icon set (outline style, per design system below).
### Routing
- `frontend/src/main.jsx` wraps `App` in a `BrowserRouter`.
- `App.jsx` becomes a route table:
- `/` → redirect component: reads `navigator.language`, maps anything starting `en` to `/en/`, everything else (including undetected) to `/fr/`.
- `/:lang/``HomePage` (only `fr`/`en` accepted; anything else renders a `NotFound`, since there's no other route to fall back to).
- `HomePage` renders inside a shared `Layout` (header + footer), and is code-split via `React.lazy` (per stack guidance: lazy-load route-level components rather than importing everything upfront) — a single-page site doesn't gain much from this today, but it establishes the pattern Sub-project 2 needs when more routes exist.
### i18n
- `frontend/src/locales/fr.json`, `frontend/src/locales/en.json` — flat key/value dictionaries for all UI strings (headline, reassurance bullets, format-family labels, button/label text, tool status messages).
- `react-i18next` initialized with the `:lang` route param as the single source of truth for the active language (via `i18n.changeLanguage(lang)` in a `useEffect` keyed on the param) — no separate persisted-locale state, so the URL and displayed language can never disagree.
- Language switcher in the header: a two-item control (FR/EN) that navigates to the sibling `/:otherLang/` path, preserving no query/hash state (there is none to preserve).
### Dark mode
- Toggle in the header, plus first-load detection of `prefers-color-scheme`. Per the design system's own guidance ("avoid dark mode by default"), the *default* is light — `prefers-color-scheme: dark` is honored as an initial value, not forced.
- Implementation: a `data-theme="light"|"dark"` attribute on `<html>`, set on mount from (in priority order) `localStorage.getItem('theme')``matchMedia('(prefers-color-scheme: dark)')``"light"`. Toggling writes the explicit choice back to `localStorage`, which then wins on every subsequent load regardless of system preference.
- `frontend/src/index.css` currently already defines a CSS-variable token set with a `prefers-color-scheme: dark` media query (leftover from the Vite template scaffold, purple accent). This is replaced outright by the new token set (below), driven by `[data-theme="dark"]` attribute selectors instead of the media query alone, so the manual toggle can override system preference.
## Design system
From `ui-ux-pro-max --design-system "file conversion SaaS tool modern minimal trustworthy"` (Flat Design × Minimal Single Column pattern):
- **Typography:** Plus Jakarta Sans (Google Fonts `@import`), replacing the current `system-ui` stack.
- **Color tokens** (light, with dark override for each):
- `--color-primary: #2563EB` / dark `#3B82F6`
- `--color-accent: #D97706` (CTA — the upload/convert action)
- `--color-background: #F8FAFC` / dark `#16171d`-class dark surface
- `--color-foreground: #0F172A` / dark light-gray text
- `--color-muted`, `--color-border`, `--color-destructive: #DC2626` (error states — reuses the existing `.error` semantic role in `FileCard.jsx`)
- **Icons:** Phosphor, outline weight, never emoji — `UploadSimple` (dropzone), `File`/`FileText` (per-file cards), `DownloadSimple` (download link), plus one icon per reassurance bullet (e.g. a clock for auto-delete, a shield or lock for no-account).
- **Effects:** flat surfaces, no shadows/gradients, 150300ms transitions, `prefers-reduced-motion` respected (transitions become instant when set).
- **Accessibility:** 4.5:1 minimum contrast in both themes, visible focus rings (not removed for aesthetics), all interactive controls ≥44×44px touch target.
## Page structure (single page, FR content shown — EN mirrors via i18n keys)
1. **Header** — logo/wordmark "Ombrora Convert", language switcher (FR/EN), dark-mode toggle. Single row at every breakpoint (no nav items to overflow, so no hamburger menu needed).
2. **Hero** — headline + one-line description + the **upload dropzone embedded directly in the hero** (drag-and-drop via native HTML5 DnD events on a styled dropzone, or click to open the file picker — no drag-drop library needed).
3. **Selected-files state** (conditionally rendered, same trigger as today's `pendingFiles.length > 0`): per-file cards directly below the dropzone, each with the existing target-format `<select>`, quality slider (image formats), PNG compression slider, ICO size `<select>`, and PDF-compression checkbox+slider — same logic as current `App.jsx`, restyled into a card layout instead of an inline `<li>` row (today's single-row-per-file layout is what breaks on narrow viewports; this is a real layout fix, not cosmetic). "Convertir" button triggers the same `handleConvert`.
4. **Submitted-jobs state**`FileCard` components (existing polling/download logic unchanged), restyled to match the new card visual language.
5. **Reassurance strip** — 4 items, icon + short label, sourced only from the verified facts above: auto-delete after 1h, no account required, 100% online/nothing to install, up to 100MB per file.
6. **Supported-formats section** — grouped by family, grid layout (1 col mobile → 2 tablet → 4 desktop):
- **Images:** jpg, jpeg, png, webp, gif, tiff, avif, heic/heif, ico (all-pairs within the image set, plus HEIC/ICO as documented in `src/converters/heic.js`/`ico.js`, plus image→PDF from `imageToPdf.js`)
- **Documents:** docx, txt, html, md, pdf, csv, xlsx (per `src/converters/document.js`'s registered pairs)
- **Polices:** ttf, otf, woff, dfont (per `src/converters/font.js`/`dfont.js`)
- **Ebooks:** epub, fb2, lrf, mobi, pdb, rb, snb, tcr, azw3, pdf (per `src/converters/ebook.js`'s `EBOOK_FORMATS`)
- This list is **hand-maintained in the frontend**, not fetched live from `GET /api/formats` — that endpoint answers "given this one source format, what targets exist," which isn't the right shape for a "here's everything we support" marketing grid. Risk: it can drift from the registry if a converter is added/removed later without updating this section — accepted for this sprint (YAGNI a generated-list build step until the format catalog changes often enough to justify one).
7. **Footer** — reassurance recap (short form) + format-family recap + language switcher. No legal links (deferred, see above).
## Responsive behavior
- Mobile-first, breakpoints 375 / 768 / 1024 / 1440px.
- Dropzone and per-file cards: full-width, stacked vertically on mobile; format/quality controls stack under the filename instead of the current single inline row.
- Formats grid: 1 → 2 → 4 columns across the breakpoints above.
- Header: fixed single-row layout at all sizes.
- All interactive elements (dropzone, toggle, language switcher, sliders, selects) sized for ≥44×44px touch targets.
## SEO
- `frontend/index.html`: real `<title>` ("Ombrora Convert — Conversion de fichiers en ligne" / EN equivalent set dynamically per route — see note below), `<meta name="description">`, OpenGraph (`og:title`, `og:description`, `og:type=website`, `og:locale`), Twitter card tags.
- Per-language `<title>`/meta description and `<link rel="alternate" hreflang="fr">` / `hreflang="en"` / `hreflang="x-default"` are set client-side per route via a small `useEffect`-based head manager (no new dependency — direct `document.title`/meta-tag DOM updates keyed on the `:lang` param), since this is a single static `index.html` shell shared by both language routes.
- JSON-LD `SoftwareApplication` schema injected on `HomePage` (name "Ombrora Convert", `applicationCategory: "UtilitiesApplication"`, `offers` with price `0`).
- `frontend/public/robots.txt` (allow all) and `frontend/public/sitemap.xml` (2 URLs: `/fr/`, `/en/`) added as static files.
- Existing PostHog snippet in `index.html` is untouched.
## Out of scope (explicitly deferred)
- Sub-project 2: programmatic per-format-pair SEO landing pages, sitemap generation at scale, internal linking between them.
- Legal pages (mentions légales, confidentialité, CGU) — pending real company/hosting data.
- SSR/prerendering.
- A frontend automated test harness.
- RGPD/hosting-location trust messaging (not requested).
## Testing
Manual only, per the "no new test harness" decision above:
- `npm run dev` in `frontend/` (proxying `/api` to a locally running `src/server.js`/`src/worker.js` per existing `vite.config.js` proxy).
- Verify both `/fr/` and `/en/` render correctly, `/` redirects appropriately, language switcher round-trips.
- Verify dark-mode toggle, and first-load respecting `prefers-color-scheme` before any manual toggle has been made (test via OS-level dark mode with `localStorage` cleared).
- Verify upload → format/quality selection → convert → poll → download flow still works end-to-end (functional logic is carried over from `App.jsx`/`FileCard.jsx`/`api.js`, not rewritten).
- Verify responsive layout at 375/768/1024/1440px widths, particularly the per-file card controls and formats grid.
- Verify reassurance figures shown in the UI (1h, 100MB) match whatever `RETENTION_HOURS`/`MAX_FILE_SIZE_MB` actually resolve to in the running backend, rather than being hardcoded strings that could drift from `.env` — confirm during implementation whether these should be hardcoded copy (matching today's defaults) or fetched from a small backend config-exposure endpoint; hardcoding is the default assumption for this spec since no such endpoint exists today and adding one is a backend change beyond "frontend redesign," but flag this as a decision point for the implementation plan.