From ab95e3e699dc871fbc9f68d31737ded30b70c600 Mon Sep 17 00:00:00 2001 From: Anthony GAEREMYNCK <1@anthony.sh> Date: Fri, 31 Jul 2026 14:27:15 +0200 Subject: [PATCH] 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). --- ...-07-31-ombrora-frontend-redesign-design.md | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-31-ombrora-frontend-redesign-design.md diff --git a/docs/superpowers/specs/2026-07-31-ombrora-frontend-redesign-design.md b/docs/superpowers/specs/2026-07-31-ombrora-frontend-redesign-design.md new file mode 100644 index 0000000..d517427 --- /dev/null +++ b/docs/superpowers/specs/2026-07-31-ombrora-frontend-redesign-design.md @@ -0,0 +1,116 @@ +# 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 ``, 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, 150–300ms 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 ``, and PDF-compression checkbox+slider — same logic as current `App.jsx`, restyled into a card layout instead of an inline `
  • ` 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 `` ("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.