feat(frontend): add design tokens, dark mode hook, and i18n scaffold

Part of the Ombrora Convert redesign (Task 1/5).
This commit is contained in:
2026-07-31 14:49:19 +02:00
parent f499196dae
commit ee18cc2d90
8 changed files with 359 additions and 90 deletions
+157 -1
View File
@@ -8,8 +8,12 @@
"name": "frontend", "name": "frontend",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@phosphor-icons/react": "^2.1.10",
"i18next": "^26.3.6",
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^19.2.7" "react-dom": "^19.2.7",
"react-i18next": "^17.0.11",
"react-router-dom": "^7.18.2"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.2.17", "@types/react": "^19.2.17",
@@ -19,6 +23,15 @@
"vite": "^8.1.1" "vite": "^8.1.1"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/core": { "node_modules/@emnapi/core": {
"version": "1.11.1", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
@@ -432,6 +445,19 @@
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
} }
}, },
"node_modules/@phosphor-icons/react": {
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz",
"integrity": "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">= 16.8",
"react-dom": ">= 16.8"
}
},
"node_modules/@rolldown/binding-android-arm64": { "node_modules/@rolldown/binding-android-arm64": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
@@ -771,6 +797,19 @@
} }
} }
}, },
"node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -821,6 +860,43 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/html-parse-stringify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-4.0.1.tgz",
"integrity": "sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw==",
"license": "MIT",
"funding": {
"url": "https://locize.com"
}
},
"node_modules/i18next": {
"version": "26.3.6",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.6.tgz",
"integrity": "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==",
"funding": [
{
"type": "individual",
"url": "https://www.locize.com/i18next"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
},
{
"type": "individual",
"url": "https://www.locize.com"
}
],
"license": "MIT",
"peerDependencies": {
"typescript": "^5 || ^6 || ^7"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/lightningcss": { "node_modules/lightningcss": {
"version": "1.33.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
@@ -1232,6 +1308,71 @@
"react": "^19.2.8" "react": "^19.2.8"
} }
}, },
"node_modules/react-i18next": {
"version": "17.0.11",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.11.tgz",
"integrity": "sha512-cDtkXgxjuFTWUH6V+aQn1Ve5vDiUztCNPWW5GtSHDccsgRXO1nE6QFWCEmc1KAutrb3OUv87wFShJL5RhUwPXg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.29.2",
"html-parse-stringify": "^4.0.1",
"use-sync-external-store": "^1.6.0"
},
"peerDependencies": {
"i18next": ">= 26.2.0",
"react": ">= 16.8.0",
"typescript": "^5 || ^6 || ^7"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/react-router": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz",
"integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz",
"integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==",
"license": "MIT",
"dependencies": {
"react-router": "7.18.2"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/rolldown": { "node_modules/rolldown": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
@@ -1272,6 +1413,12 @@
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -1307,6 +1454,15 @@
"license": "0BSD", "license": "0BSD",
"optional": true "optional": true
}, },
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "8.1.5", "version": "8.1.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
+5 -1
View File
@@ -10,8 +10,12 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@phosphor-icons/react": "^2.1.10",
"i18next": "^26.3.6",
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^19.2.7" "react-dom": "^19.2.7",
"react-i18next": "^17.0.11",
"react-router-dom": "^7.18.2"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.2.17", "@types/react": "^19.2.17",
+10
View File
@@ -0,0 +1,10 @@
import { Moon, Sun } from '@phosphor-icons/react';
export function ThemeToggle({ theme, onToggle, label }) {
const isDark = theme === 'dark';
return (
<button type="button" className="theme-toggle" onClick={onToggle} aria-label={label} aria-pressed={isDark}>
{isDark ? <Sun size={20} weight="regular" /> : <Moon size={20} weight="regular" />}
</button>
);
}
+27
View File
@@ -0,0 +1,27 @@
import { useEffect, useState } from 'react';
const STORAGE_KEY = 'theme';
function getInitialTheme() {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored === 'light' || stored === 'dark') return stored;
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
export function useTheme() {
const [theme, setTheme] = useState(getInitialTheme);
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
function toggleTheme() {
setTheme((current) => {
const next = current === 'dark' ? 'light' : 'dark';
localStorage.setItem(STORAGE_KEY, next);
return next;
});
}
return { theme, toggleTheme };
}
+16
View File
@@ -0,0 +1,16 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import fr from './locales/fr.json';
import en from './locales/en.json';
i18n.use(initReactI18next).init({
resources: {
fr: { translation: fr },
en: { translation: en },
},
lng: 'fr',
fallbackLng: 'fr',
interpolation: { escapeValue: false },
});
export default i18n;
+50 -88
View File
@@ -1,111 +1,73 @@
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap');
:root { :root {
--text: #6b6375; --color-primary: #2563eb;
--text-h: #08060d; --color-on-primary: #ffffff;
--bg: #fff; --color-secondary: #3b82f6;
--border: #e5e4e7; --color-accent: #d97706;
--code-bg: #f4f3ec; --color-background: #f8fafc;
--accent: #aa3bff; --color-foreground: #0f172a;
--accent-bg: rgba(170, 59, 255, 0.1); --color-muted: #f1f5fd;
--accent-border: rgba(170, 59, 255, 0.5); --color-border: #e4ecfc;
--social-bg: rgba(244, 243, 236, 0.5); --color-destructive: #dc2626;
--shadow: --color-ring: #2563eb;
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--sans: system-ui, 'Segoe UI', Roboto, sans-serif; --font-sans: 'Plus Jakarta Sans', system-ui, 'Segoe UI', Roboto, sans-serif;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
font: 18px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark; color-scheme: light dark;
color: var(--text); font: 16px/1.5 var(--font-sans);
background: var(--bg);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: 1024px) {
font-size: 16px;
}
} }
@media (prefers-color-scheme: dark) { :root[data-theme='dark'] {
:root { --color-primary: #3b82f6;
--text: #9ca3af; --color-on-primary: #0f172a;
--text-h: #f3f4f6; --color-secondary: #60a5fa;
--bg: #16171d; --color-accent: #f59e0b;
--border: #2e303a; --color-background: #0f172a;
--code-bg: #1f2028; --color-foreground: #f1f5f9;
--accent: #c084fc; --color-muted: #1e293b;
--accent-bg: rgba(192, 132, 252, 0.15); --color-border: #334155;
--accent-border: rgba(192, 132, 252, 0.5); --color-destructive: #f87171;
--social-bg: rgba(47, 48, 58, 0.5); --color-ring: #3b82f6;
--shadow: }
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
}
#social .button-icon { *, *::before, *::after {
filter: invert(1) brightness(2); box-sizing: border-box;
}
} }
body { body {
margin: 0; margin: 0;
background: var(--color-background);
color: var(--color-foreground);
transition: background-color 200ms ease, color 200ms ease;
} }
#root { #root {
width: 1126px;
max-width: 100%;
margin: 0 auto;
text-align: center;
border-inline: 1px solid var(--border);
min-height: 100svh; min-height: 100svh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
box-sizing: border-box;
} }
h1, a {
h2 { color: var(--color-primary);
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
} }
h1 { button, input, select {
font-size: 56px; font-family: inherit;
letter-spacing: -1.68px; }
margin: 32px 0;
@media (max-width: 1024px) { button {
font-size: 36px; cursor: pointer;
margin: 20px 0; }
:focus-visible {
outline: 2px solid var(--color-ring);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
transition: none !important;
animation: none !important;
} }
} }
h2 {
font-size: 24px;
line-height: 118%;
letter-spacing: -0.24px;
margin: 0 0 8px;
@media (max-width: 1024px) {
font-size: 20px;
}
}
p {
margin: 0;
}
code,
.counter {
font-family: var(--mono);
display: inline-flex;
border-radius: 4px;
color: var(--text-h);
}
code {
font-size: 15px;
line-height: 135%;
padding: 4px 8px;
background: var(--code-bg);
}
+47
View File
@@ -0,0 +1,47 @@
{
"brand": "Ombrora Convert",
"nav": {
"switchToEnglish": "English",
"switchToFrench": "Français",
"themeToLight": "Switch to light mode",
"themeToDark": "Switch to dark mode"
},
"hero": {
"title": "Convert your files online, for free",
"subtitle": "Drag and drop your files, pick the output format, download the result. No install, no account.",
"dropzoneLabel": "Drag your files here or click to browse",
"convert": "Convert",
"unsupportedFormat": "Unsupported format"
},
"quality": {
"label": "Quality ({{value}})",
"compression": "Compression ({{value}})",
"iconSize": "Icon size",
"compressPdf": "Compress as JPEG"
},
"job": {
"converting": "Converting...",
"download": "Download"
},
"reassurance": {
"title": "Why you can trust us",
"autoDelete": "Files deleted automatically after 1h",
"noAccount": "No account required",
"online": "100% online — nothing to install",
"maxSize": "Up to 100MB per file"
},
"formats": {
"title": "Supported formats",
"images": "Images",
"documents": "Documents",
"fonts": "Fonts",
"ebooks": "Ebooks"
},
"footer": {
"rights": "Ombrora Convert — Online file conversion"
},
"notFound": {
"title": "Page not found",
"backHome": "Back to home"
}
}
+47
View File
@@ -0,0 +1,47 @@
{
"brand": "Ombrora Convert",
"nav": {
"switchToEnglish": "English",
"switchToFrench": "Français",
"themeToLight": "Activer le mode clair",
"themeToDark": "Activer le mode sombre"
},
"hero": {
"title": "Convertissez vos fichiers en ligne, gratuitement",
"subtitle": "Glissez-déposez vos fichiers, choisissez le format de sortie, téléchargez le résultat. Aucune installation, aucun compte.",
"dropzoneLabel": "Glissez vos fichiers ici ou cliquez pour parcourir",
"convert": "Convertir",
"unsupportedFormat": "Format non supporté"
},
"quality": {
"label": "Qualité ({{value}})",
"compression": "Compression ({{value}})",
"iconSize": "Taille de l'icône",
"compressPdf": "Compresser en JPEG"
},
"job": {
"converting": "Conversion en cours...",
"download": "Télécharger"
},
"reassurance": {
"title": "Pourquoi nous faire confiance",
"autoDelete": "Fichiers supprimés automatiquement après 1h",
"noAccount": "Aucun compte requis",
"online": "100% en ligne — rien à installer",
"maxSize": "Jusqu'à 100 Mo par fichier"
},
"formats": {
"title": "Formats pris en charge",
"images": "Images",
"documents": "Documents",
"fonts": "Polices",
"ebooks": "Ebooks"
},
"footer": {
"rights": "Ombrora Convert — Conversion de fichiers en ligne"
},
"notFound": {
"title": "Page introuvable",
"backHome": "Retour à l'accueil"
}
}