feat(frontend): add archive format family, compression control, and fixed double-extension parsing

Also adds a --color-family-archives CSS variable (light + dark) that
the plan didn't call out explicitly but is needed for the new family
tile/icon to render with a color instead of falling back silently.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-01 13:03:30 +02:00
co-authored by Claude Sonnet 5
parent ef6de6b583
commit 48c70254c1
9 changed files with 39 additions and 12 deletions
+12 -4
View File
@@ -1,14 +1,12 @@
import { X } from '@phosphor-icons/react'; import { X } from '@phosphor-icons/react';
import { formatBytes } from '../utils/formatBytes.js'; import { formatBytes } from '../utils/formatBytes.js';
import { fileTypeMeta } from '../utils/fileFamily.js'; import { fileTypeMeta } from '../utils/fileFamily.js';
import { extensionOf } from '../utils/archiveExtensions.js';
const QUALITY_FORMATS = ['jpg', 'jpeg', 'webp', 'avif', 'tiff']; const QUALITY_FORMATS = ['jpg', 'jpeg', 'webp', 'avif', 'tiff'];
const ICON_SIZES = [16, 32, 48, 256, 512]; const ICON_SIZES = [16, 32, 48, 256, 512];
const DEFAULT_ICON_SIZE = 256; const DEFAULT_ICON_SIZE = 256;
const ARCHIVE_COMPRESSION_FORMATS = ['zip', 'tar.gz', 'tar.bz2', '7z', 'tar.7z'];
function extensionOf(fileName) {
return fileName.split('.').pop();
}
function rangeProgress(value, min, max) { function rangeProgress(value, min, max) {
return `${((value - min) / (max - min)) * 100}%`; return `${((value - min) / (max - min)) * 100}%`;
@@ -93,6 +91,16 @@ export function FileConfigCard({ item, index, t, onTargetFormatChange, onQuality
/> />
)} )}
{ARCHIVE_COMPRESSION_FORMATS.includes(item.targetFormat) && (
<RangeField
label={t('quality.compression')}
value={item.quality}
min={0}
max={9}
onChange={(value) => onQualityChange(index, value)}
/>
)}
{item.targetFormat === 'ico' && ( {item.targetFormat === 'ico' && (
<div className="chip-field"> <div className="chip-field">
<span>{t('quality.iconSize')}</span> <span>{t('quality.iconSize')}</span>
+2 -1
View File
@@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Image, FileText, TextAa, BookOpen } from '@phosphor-icons/react'; import { Image, FileText, TextAa, BookOpen, Archive } from '@phosphor-icons/react';
import { FORMAT_FAMILIES } from '../data/formats.js'; import { FORMAT_FAMILIES } from '../data/formats.js';
const FAMILY_ICONS = { const FAMILY_ICONS = {
@@ -7,6 +7,7 @@ const FAMILY_ICONS = {
documents: FileText, documents: FileText,
fonts: TextAa, fonts: TextAa,
ebooks: BookOpen, ebooks: BookOpen,
archives: Archive,
}; };
export function FormatsGrid() { export function FormatsGrid() {
+4
View File
@@ -15,4 +15,8 @@ export const FORMAT_FAMILIES = [
key: 'ebooks', key: 'ebooks',
formats: ['epub', 'fb2', 'lrf', 'mobi', 'pdb', 'rb', 'snb', 'tcr', 'azw3', 'pdf'], formats: ['epub', 'fb2', 'lrf', 'mobi', 'pdb', 'rb', 'snb', 'tcr', 'azw3', 'pdf'],
}, },
{
key: 'archives',
formats: ['zip', 'tar', 'tar.gz', 'tar.bz2', '7z', 'tar.7z', 'rar'],
},
]; ];
+2
View File
@@ -23,6 +23,7 @@
--color-family-documents: var(--color-aurora-cyan); --color-family-documents: var(--color-aurora-cyan);
--color-family-fonts: #f5a524; --color-family-fonts: #f5a524;
--color-family-ebooks: #10b981; --color-family-ebooks: #10b981;
--color-family-archives: #64748b;
--font-sans: 'Plus Jakarta Sans', system-ui, 'Segoe UI', Roboto, sans-serif; --font-sans: 'Plus Jakarta Sans', system-ui, 'Segoe UI', Roboto, sans-serif;
--font-display: 'Bricolage Grotesque', var(--font-sans); --font-display: 'Bricolage Grotesque', var(--font-sans);
@@ -51,6 +52,7 @@
--color-family-documents: var(--color-aurora-cyan); --color-family-documents: var(--color-aurora-cyan);
--color-family-fonts: #ffb84d; --color-family-fonts: #ffb84d;
--color-family-ebooks: #34d399; --color-family-ebooks: #34d399;
--color-family-archives: #94a3b8;
} }
*, *::before, *::after { *, *::before, *::after {
+2 -1
View File
@@ -40,7 +40,8 @@
"images": "Images", "images": "Images",
"documents": "Documents", "documents": "Documents",
"fonts": "Fonts", "fonts": "Fonts",
"ebooks": "Ebooks" "ebooks": "Ebooks",
"archives": "Archives"
}, },
"footer": { "footer": {
"rights": "Ombrora Convert — Online file conversion" "rights": "Ombrora Convert — Online file conversion"
+2 -1
View File
@@ -40,7 +40,8 @@
"images": "Images", "images": "Images",
"documents": "Documents", "documents": "Documents",
"fonts": "Polices", "fonts": "Polices",
"ebooks": "Ebooks" "ebooks": "Ebooks",
"archives": "Archives"
}, },
"footer": { "footer": {
"rights": "Ombrora Convert — Conversion de fichiers en ligne" "rights": "Ombrora Convert — Conversion de fichiers en ligne"
+6 -4
View File
@@ -9,6 +9,7 @@ import { ReassuranceStrip } from '../components/ReassuranceStrip.jsx';
import { FormatsGrid } from '../components/FormatsGrid.jsx'; import { FormatsGrid } from '../components/FormatsGrid.jsx';
import { FormatMarquee } from '../components/FormatMarquee.jsx'; import { FormatMarquee } from '../components/FormatMarquee.jsx';
import { SeoHead } from '../components/SeoHead.jsx'; import { SeoHead } from '../components/SeoHead.jsx';
import { extensionOf } from '../utils/archiveExtensions.js';
import '../styles/home.css'; import '../styles/home.css';
import '../styles/sections.css'; import '../styles/sections.css';
@@ -19,14 +20,15 @@ const DEFAULT_QUALITY = {
avif: 50, avif: 50,
tiff: 80, tiff: 80,
png: 6, png: 6,
zip: 6,
'tar.gz': 6,
'tar.bz2': 9,
'7z': 5,
'tar.7z': 5,
}; };
const DEFAULT_ICON_SIZE = 256; const DEFAULT_ICON_SIZE = 256;
function extensionOf(fileName) {
return fileName.split('.').pop().toLowerCase();
}
function defaultQualityFor(targetFormat) { function defaultQualityFor(targetFormat) {
return DEFAULT_QUALITY[targetFormat] ?? null; return DEFAULT_QUALITY[targetFormat] ?? null;
} }
+7
View File
@@ -0,0 +1,7 @@
const DOUBLE_EXTENSIONS = ['tar.gz', 'tar.bz2', 'tar.7z'];
export function extensionOf(filename) {
const lower = filename.toLowerCase();
const match = DOUBLE_EXTENSIONS.find((ext) => lower.endsWith(`.${ext}`));
return match ?? filename.split('.').pop().toLowerCase();
}
+2 -1
View File
@@ -1,4 +1,4 @@
import { Image, FileText, TextAa, BookOpen, File as FileIcon } from '@phosphor-icons/react'; import { Image, FileText, TextAa, BookOpen, Archive, File as FileIcon } from '@phosphor-icons/react';
import { FORMAT_FAMILIES } from '../data/formats.js'; import { FORMAT_FAMILIES } from '../data/formats.js';
const FAMILY_ICONS = { const FAMILY_ICONS = {
@@ -6,6 +6,7 @@ const FAMILY_ICONS = {
documents: FileText, documents: FileText,
fonts: TextAa, fonts: TextAa,
ebooks: BookOpen, ebooks: BookOpen,
archives: Archive,
}; };
export function familyForExtension(extension) { export function familyForExtension(extension) {