Files
convert/docs/superpowers/specs/2026-07-31-file-card-polish-design.md
T
anthonyandClaude Sonnet 5 d0ff81344f docs: add design spec for file card / language switcher polish
Covers flag-icons for the language switcher, a shared header layout
across the choose/convert/download states, human-readable file sizes,
and restyled format/quality controls and download button.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 15:43:46 +02:00

7.7 KiB

File card / language switcher polish

Date: 2026-07-31

Goal

Polish pass on top of the existing frontend redesign (2026-07-31-ombrora-frontend-redesign-design): flag icons for the language switcher, a friendlier destination-format/quality picker, a consistent visual skeleton across the three file-lifecycle states (choosing format, converting, downloading), and human-readable file size shown alongside the filename in both the choosing and downloading states.

Problem

  • LanguageSwitcher.jsx renders a plain text link ("English"/"Français") instead of a flag.
  • FileConfigCard (choosing step) shows a generic FileIcon + filename; FileCard (download step) shows only the filename with no icon — different markup between the two, which reads as the icon "disappearing" when a file moves from one list to the other.
  • Neither step shows file size.
  • The target-format <select>, quality slider, and download link use only default browser/plain styling.

Scope decisions (from brainstorming)

  • Flags via the flag-icons npm package, not emoji or hand-drawn SVG. Chosen over emoji (unreliable flag emoji rendering on some Windows/Linux font configs) and over hand-rolled SVG (user preferred pulling in the library). flag-icons ships CSS + font/SVG assets only, no native binary — safe for o2switch. Per the o2switch single-package.json constraint documented in CLAUDE.md, it must be added to both frontend/package.json and root package.json dependencies, with npm install run in both places.
  • FileConfigCard and FileCard remain separate components (different responsibilities: one edits pending config, one polls job status) rather than merging into one mega-component. Instead, both adopt the same header markup shape and shared CSS classes so they render identically at the "icon + name + size" level, with only the right-hand, state-specific content differing (controls / spinner / download button / error).
  • Target-format picker stays a native <select>, not a custom chip grid. Chosen for lower implementation cost and to keep native accessibility/keyboard behavior, restyled with border/focus/theme-token styling plus a small badge next to it showing the currently selected extension.
  • File size is computed and carried client-side, no backend change. File.size is already available at selection time in HomePage.handleFilesSelected. For the download step, uploadFiles(items) returns jobs in the same order as the request array (confirmed in src/app.js's POST /api/jobs handler, which builds jobs by iterating req.files index-for-index) — so handleConvert can zip the returned jobs with the original validItems by index to attach size onto each job object before it's stored in submittedJobs, with no API contract change.
  • "Converting" state gets a spinning icon, not a progress bar (no real progress percentage exists to drive one) — CircleNotch from the already-installed @phosphor-icons/react, animated via a CSS @keyframes spin, respecting prefers-reduced-motion (existing design-system rule from the foundation spec).
  • Download link becomes a filled button matching .convert-button's visual language (same accent color, radius, min-height), not just a restyled inline link.

Architecture

New dependency

Add flag-icons to dependencies in both package.json (root) and frontend/package.json, then npm install in both locations to keep both lockfiles in sync (per existing o2switch mirroring rule).

New util

frontend/src/utils/formatBytes.js — pure function formatBytes(bytes) returning a human-readable string ("2.4 MB", "512 KB", "0 B" for falsy/zero/undefined input). Used by both FileConfigCard and FileCard.

Shared header markup/CSS

Both components render the same header shape:

<div class="file-tile-header">
  <FileIcon />
  <span class="file-tile-name">{name}</span>
  <span class="file-tile-size">{formatBytes(size)}</span>
</div>

CSS classes .file-tile-header, .file-tile-name, .file-tile-size added to frontend/src/styles/home.css (renaming/superseding today's .file-config-name, which only exists in that file today and is used by both components already — checked: FileCard.jsx currently reuses .file-config-name for its filename span, so this is a rename/extension of an existing shared class, not a net-new pattern). FileCard gains the icon it's currently missing.

FileConfigCard.jsx changes

  • Add size display next to filename (via item.file.size).
  • Restyle the target-format <select> (new .format-select class: themed border, focus ring, padding) and add a small .format-badge span next to it showing item.targetFormat.toUpperCase().
  • Quality slider, PNG compression slider, ICO size select, PDF checkbox: consistent spacing/label styling under a shared .file-config-controls refinement — no logic changes, styling only.

FileCard.jsx changes

  • Add FileIcon to the header (parity with FileConfigCard).
  • Add size display — needs size passed as a new prop from HomePage.
  • "Converting" state: CircleNotch icon (spin animation) next to the existing translated text.
  • "Done" state: download link restyled as a filled button (new .download-button class reusing .convert-button's visual tokens), keeping the existing <a href={downloadUrl(jobId)}> semantics.

HomePage.jsx changes

  • handleConvert: after uploadFiles(validItems) resolves, zip the returned jobs array with validItems by index to attach size: validItems[i].file.size onto each job object before appending to submittedJobs.
  • Pass size={job.size} down to FileCard.

LanguageSwitcher.jsx changes

  • Import flag-icons/css/flag-icons.min.css once (in main.jsx, alongside other global CSS imports — checked: main.jsx currently imports ./index.css, matching the existing pattern for global stylesheet imports).
  • Replace the text label with <span class={fi fi-${otherLang === 'en' ? 'gb' : 'fr'}} /> (flag-icons uses gb for the English/UK flag, not en — there's no ISO country code "en"), keeping an aria-label/title set to the existing translated label (t('nav.switchToEnglish')/t('nav.switchToFrench')) for accessibility, since a flag alone doesn't convey the target language to screen readers.

Out of scope

  • Replacing the native <select> with a custom chip/grid picker (considered, declined — see scope decisions).
  • Any backend/API changes — file size is entirely a client-side, already-available value.
  • A progress-percentage-driven progress bar for the "converting" state (no real progress data exists).
  • Automated frontend tests (no test harness exists yet, per the foundation spec's own out-of-scope note).

Testing

Manual only, consistent with the foundation spec's approach (no frontend test harness exists):

  • npm run dev in frontend/, both /fr/ and /en/.
  • Upload one or more files, verify: file icon + name + size shown in the choosing step; format select is restyled with the extension badge visible; quality/PNG/ICO/PDF controls still function exactly as before.
  • Convert, verify: converting state shows the spinning icon; on completion the download step shows the same icon + name + size layout as the choosing step, plus a filled download button; clicking it downloads the converted file.
  • Verify the language switcher shows the correct flag for the other language (FR page shows GB flag and vice versa) and that keyboard/screen-reader users still get a meaningful accessible name.
  • Verify at 375/768/1024/1440px widths that the new header layout doesn't break wrapping (existing flex-wrap behavior in .file-config-card/.file-card should still apply).
  • Verify dark mode: flag icons, badges, and the new button render with sufficient contrast in both themes.