Files
convert/docs/superpowers/specs/2026-07-31-ombrora-frontend-redesign-design.md
T
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

12 KiB
Raw Blame History

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.jsretentionHours (RETENTION_HOURS env, default 1), enforced by src/cleanup.js (cron-driven in production per README.md).
    • Max file size: src/config.jsmaxFileSizeMb (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 stateFileCard 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.