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 { formatBytes } from '../utils/formatBytes.js';
import { fileTypeMeta } from '../utils/fileFamily.js';
import { extensionOf } from '../utils/archiveExtensions.js';
const QUALITY_FORMATS = ['jpg', 'jpeg', 'webp', 'avif', 'tiff'];
const ICON_SIZES = [16, 32, 48, 256, 512];
const DEFAULT_ICON_SIZE = 256;
function extensionOf(fileName) {
return fileName.split('.').pop();
}
const ARCHIVE_COMPRESSION_FORMATS = ['zip', 'tar.gz', 'tar.bz2', '7z', 'tar.7z'];
function rangeProgress(value, min, max) {
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' && (
<div className="chip-field">
<span>{t('quality.iconSize')}</span>
+2 -1
View File
@@ -1,5 +1,5 @@
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';
const FAMILY_ICONS = {
@@ -7,6 +7,7 @@ const FAMILY_ICONS = {
documents: FileText,
fonts: TextAa,
ebooks: BookOpen,
archives: Archive,
};
export function FormatsGrid() {
+4
View File
@@ -15,4 +15,8 @@ export const FORMAT_FAMILIES = [
key: 'ebooks',
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-fonts: #f5a524;
--color-family-ebooks: #10b981;
--color-family-archives: #64748b;
--font-sans: 'Plus Jakarta Sans', system-ui, 'Segoe UI', Roboto, sans-serif;
--font-display: 'Bricolage Grotesque', var(--font-sans);
@@ -51,6 +52,7 @@
--color-family-documents: var(--color-aurora-cyan);
--color-family-fonts: #ffb84d;
--color-family-ebooks: #34d399;
--color-family-archives: #94a3b8;
}
*, *::before, *::after {
+2 -1
View File
@@ -40,7 +40,8 @@
"images": "Images",
"documents": "Documents",
"fonts": "Fonts",
"ebooks": "Ebooks"
"ebooks": "Ebooks",
"archives": "Archives"
},
"footer": {
"rights": "Ombrora Convert — Online file conversion"
+2 -1
View File
@@ -40,7 +40,8 @@
"images": "Images",
"documents": "Documents",
"fonts": "Polices",
"ebooks": "Ebooks"
"ebooks": "Ebooks",
"archives": "Archives"
},
"footer": {
"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 { FormatMarquee } from '../components/FormatMarquee.jsx';
import { SeoHead } from '../components/SeoHead.jsx';
import { extensionOf } from '../utils/archiveExtensions.js';
import '../styles/home.css';
import '../styles/sections.css';
@@ -19,14 +20,15 @@ const DEFAULT_QUALITY = {
avif: 50,
tiff: 80,
png: 6,
zip: 6,
'tar.gz': 6,
'tar.bz2': 9,
'7z': 5,
'tar.7z': 5,
};
const DEFAULT_ICON_SIZE = 256;
function extensionOf(fileName) {
return fileName.split('.').pop().toLowerCase();
}
function defaultQualityFor(targetFormat) {
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';
const FAMILY_ICONS = {
@@ -6,6 +6,7 @@ const FAMILY_ICONS = {
documents: FileText,
fonts: TextAa,
ebooks: BookOpen,
archives: Archive,
};
export function familyForExtension(extension) {