fix(a11y): WCAG AA contrast retune for the Otter Orange palette (#567)

Fixes #557. Vivid fill, ink label: brand #E07832 stays on fills while primary-foreground flips to #1A1814 (5.83:1); new theme-aware ink tokens carry orange, destructive, and success text roles; opacity-modified text purged; landing, demo, and the docs fund button retuned. Guarded by a CSS-parsing unit contrast test, rebuilt axe baselines with zero contrast entries, a new landing axe smoke, and fully regenerated darwin visual baselines.
This commit is contained in:
SnapOtter
2026-07-18 12:56:48 +08:00
committed by GitHub
parent 6241f8719f
commit 51022628dc
566 changed files with 856 additions and 707 deletions
@@ -32,7 +32,7 @@ export function TemplateCard({ template, onUse }: TemplateCardProps) {
className="text-start"
data-testid={`template-use-${template.id}`}
>
<span className="text-xs font-medium text-foreground hover:text-primary">{name}</span>
<span className="text-xs font-medium text-foreground hover:text-primary-ink">{name}</span>
{description ? (
<span className="block text-[11px] text-muted-foreground mt-0.5">{description}</span>
) : null}
@@ -302,7 +302,7 @@ export function BeforeAfterSlider({
<span className="px-2 py-1 rounded bg-muted text-muted-foreground">
Original: {formatSize(beforeSize)}
</span>
<span className="px-2 py-1 rounded bg-primary/10 text-primary font-medium">
<span className="px-2 py-1 rounded bg-primary/10 text-primary-ink font-medium">
Processed: {formatSize(afterSize)}
{savingsPercent !== null && Number(savingsPercent) > 0 && (
<span className="ms-1">({savingsPercent}% smaller)</span>
@@ -29,7 +29,9 @@ export function CollapsibleSection({
<ChevronRight className="h-3 w-3 shrink-0" />
)}
<span className="flex-1 text-start">{title}</span>
{warning && <AlertTriangle className="h-3 w-3 text-amber-500 shrink-0" />}
{warning && (
<AlertTriangle className="h-3 w-3 text-amber-700 dark:text-amber-400 shrink-0" />
)}
{badge && (
<span className="px-1.5 py-0.5 rounded bg-muted text-muted-foreground text-[10px]">
{badge}
+2 -2
View File
@@ -334,7 +334,7 @@ export function Dropzone({
e.stopPropagation();
setShowBulkModal(true);
}}
className="text-xs text-primary hover:text-primary/80"
className="text-xs text-primary-ink hover:text-primary-ink-strong"
>
{t.dropzone.importMultipleUrls}
</button>
@@ -343,7 +343,7 @@ export function Dropzone({
{hasMultipleFiles && (
<div className="flex flex-col items-center gap-2 mt-1">
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-primary/10 text-primary text-xs font-medium">
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-primary/10 text-primary-ink text-xs font-medium">
<FileImage className="h-3.5 w-3.5" />
{currentFiles.length} files selected
</span>
@@ -224,7 +224,7 @@ export function ImageViewer({
<button
type="button"
onClick={fitToContainer}
className={`px-2 py-1 rounded text-xs ${fitMode === "fit" ? "bg-primary/10 text-primary" : "text-muted-foreground hover:text-foreground hover:bg-muted"}`}
className={`px-2 py-1 rounded text-xs ${fitMode === "fit" ? "bg-primary/10 text-primary-ink" : "text-muted-foreground hover:text-foreground hover:bg-muted"}`}
title="Fit to view"
aria-label={t.a11y.fitToView}
>
@@ -233,7 +233,7 @@ export function ImageViewer({
<button
type="button"
onClick={actualSize}
className={`px-2 py-1 rounded text-xs ${fitMode === "actual" && zoom === 100 ? "bg-primary/10 text-primary" : "text-muted-foreground hover:text-foreground hover:bg-muted"}`}
className={`px-2 py-1 rounded text-xs ${fitMode === "actual" && zoom === 100 ? "bg-primary/10 text-primary-ink" : "text-muted-foreground hover:text-foreground hover:bg-muted"}`}
title="Actual size (100%)"
aria-label={t.a11y.actualSize}
>
@@ -33,13 +33,13 @@ export function MetadataGrid({
return (
<div key={k} className="contents">
<div
className={`text-[10px] text-muted-foreground truncate ${isRemoved ? "line-through opacity-50" : ""}`}
className={`text-[10px] text-muted-foreground truncate ${isRemoved ? "line-through" : ""}`}
title={k}
>
{labelMap?.[k] ?? k}
</div>
<div
className={`text-[10px] text-foreground font-mono truncate ${isRemoved ? "line-through opacity-50" : ""}`}
className={`text-[10px] text-foreground font-mono truncate ${isRemoved ? "line-through" : ""}`}
title={formatExifValue(k, v)}
>
{formatExifValue(k, v)}
@@ -50,7 +50,7 @@ export function MetadataGrid({
<button
type="button"
onClick={() => onRemove(k)}
className={`p-0.5 rounded hover:bg-muted/50 transition-colors ${isRemoved ? "text-red-500" : "text-muted-foreground hover:text-red-500"}`}
className={`p-0.5 rounded hover:bg-muted/50 transition-colors ${isRemoved ? "text-destructive-ink" : "text-muted-foreground hover:text-destructive-ink"}`}
title={
isRemoved ? `Restore ${labelMap?.[k] ?? k}` : `Remove ${labelMap?.[k] ?? k}`
}
@@ -95,7 +95,7 @@ export function MultiImageViewer() {
!currentEntry.blobUrl ? (
<div className="flex flex-col items-center justify-center h-full gap-3 text-center p-8">
<div className="w-12 h-12 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
<CheckCircle2 className="h-6 w-6 text-green-600 dark:text-green-400" />
<CheckCircle2 className="h-6 w-6 text-success-ink" />
</div>
<p className="text-sm font-medium">{processedFilename}</p>
<p className="text-xs text-muted-foreground">
@@ -43,7 +43,7 @@ export function ProgressCard({ active, phase, label, stage, percent, elapsed }:
<div className="text-sm font-medium text-foreground truncate">{label}</div>
<div className="text-[11px] text-muted-foreground truncate">{sublabel}</div>
</div>
<span className="text-sm font-semibold text-primary font-mono tabular-nums">
<span className="text-sm font-semibold text-primary-ink font-mono tabular-nums">
{Math.round(percent)}%
</span>
</div>
@@ -123,14 +123,14 @@ export function ReviewPanel({
{/* Success indicator */}
<div className="flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 text-emerald-600 shrink-0" />
<CheckCircle2 className="h-4 w-4 text-success-ink shrink-0" />
<span className="text-sm font-medium text-foreground">{t.toolPage.conversionComplete}</span>
</div>
{/* Batch partial failure summary */}
{hasBatchStats && failedCount > 0 && (
<div className="flex items-start gap-2 rounded-lg bg-amber-50 dark:bg-amber-950/30 p-2.5 text-xs">
<AlertCircle className="h-3.5 w-3.5 text-amber-600 dark:text-amber-400 shrink-0 mt-0.5" />
<AlertCircle className="h-3.5 w-3.5 text-amber-700 dark:text-amber-400 shrink-0 mt-0.5" />
<span className="text-amber-800 dark:text-amber-300">
{format(t.toolPage.batchPartialSuccess, {
success: successCount,
@@ -157,7 +157,7 @@ export function ReviewPanel({
{sizeDelta > 0 && (
<div className="flex justify-between">
<span className="text-muted-foreground">{t.toolPage.saved}</span>
<span className="tabular-nums font-medium text-emerald-600">{sizeDelta}%</span>
<span className="tabular-nums font-medium text-success-ink">{sizeDelta}%</span>
</div>
)}
</div>
@@ -173,7 +173,7 @@ export function ReviewPanel({
<button
type="button"
onClick={handleDownload}
className="w-full text-center text-xs text-primary hover:text-primary/80 underline underline-offset-2"
className="w-full text-center text-xs text-primary-ink hover:text-primary-ink-strong underline underline-offset-2"
>
{t.toolPage.downloadAsFile}
</button>
@@ -200,7 +200,7 @@ export function ReviewPanel({
{/* Result already auto-saved to the library: show where it went
instead of the manual save link (avoids duplicate saves). */}
{!isDataOutput && savedLibraryFileId && (
<div className="flex items-center justify-center gap-1.5 text-xs text-emerald-600 dark:text-emerald-400">
<div className="flex items-center justify-center gap-1.5 text-xs text-success-ink">
<CheckCircle2 className="h-3 w-3" />
{t.toolPage.savedToFiles}
<Link to="/files" className="underline underline-offset-2 hover:text-foreground">
@@ -219,7 +219,7 @@ export function ReviewPanel({
className={cn(
"text-xs flex items-center gap-1.5 transition-colors",
saveStatus === "saved"
? "text-emerald-600 dark:text-emerald-400"
? "text-success-ink"
: "text-muted-foreground hover:text-foreground disabled:opacity-50",
)}
>
@@ -271,7 +271,7 @@ export function ReviewPanel({
<div className="flex justify-center">
<Link
to="/"
className="text-xs text-muted-foreground/60 hover:text-muted-foreground flex items-center gap-1"
className="text-xs text-muted-foreground hover:text-foreground flex items-center gap-1"
>
<ArrowLeft className="h-3 w-3" />
{t.toolPage.backToTools}
@@ -92,7 +92,7 @@ export function SideBySideComparison({
{/* Size savings */}
{savingsPercent !== null && (
<p
className={`text-sm font-medium ${Number(savingsPercent) > 0 ? "text-green-600 dark:text-green-400" : "text-red-500"}`}
className={`text-sm font-medium ${Number(savingsPercent) > 0 ? "text-success-ink" : "text-destructive-ink"}`}
>
{Number(savingsPercent) > 0
? format(t.toolSettings["optimize-for-web"].smaller, { percent: savingsPercent })
+13 -3
View File
@@ -59,6 +59,16 @@ const SECTION_COLOR_MAP: Record<string, string> = Object.fromEntries(
SECTIONS.map((s) => [s.id, s.color]),
);
// Darkened per-section text colors: the vivid section colors sit below the
// WCAG AA 4.5:1 floor as small text on the 12.5% tint chips (issue #557).
const SECTION_INK_MAP: Record<string, string> = {
image: "#0B61ED",
video: "#D31212",
audio: "#0B7C57",
pdf: "#7942F5",
files: "#996306",
};
export function ToolCard({
tool,
variant = "compact",
@@ -95,7 +105,7 @@ export function ToolCard({
className="text-[10px] px-1.5 py-0.5 rounded-full font-medium shrink-0"
style={{
backgroundColor: `${sectionColor}20`,
color: sectionColor,
color: SECTION_INK_MAP[section] ?? "#3D3832",
}}
>
{SECTIONS.find((s) => s.id === section)?.name ?? section}
@@ -139,7 +149,7 @@ export function ToolCard({
</span>
{sectionBadge}
{tool.experimental && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-orange-100 text-orange-600 font-medium">
<span className="text-[10px] px-1.5 py-0.5 rounded bg-primary-subtle text-primary-ink font-medium">
{t.common.experimental}
</span>
)}
@@ -178,7 +188,7 @@ export function ToolCard({
</span>
{sectionBadge}
{tool.experimental && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-orange-100 text-orange-600 font-medium">
<span className="text-[10px] px-1.5 py-0.5 rounded bg-primary-subtle text-primary-ink font-medium">
{t.common.experimental}
</span>
)}
@@ -22,7 +22,7 @@ function StatusIcon({ status }: { status: UrlImportEntry["status"] }) {
case "fetching":
return <Loader2 className="h-4 w-4 text-primary animate-spin" />;
case "ready":
return <Check className="h-4 w-4 text-emerald-500" />;
return <Check className="h-4 w-4 text-success-ink" />;
case "failed":
return <AlertCircle className="h-4 w-4 text-destructive" />;
}
@@ -94,7 +94,7 @@ export function ZoomToolbar({
title={t.a11y.pan}
aria-label={t.a11y.pan}
aria-pressed={handToolActive}
className={`${btn} ${handToolActive ? "bg-primary/10 text-primary hover:text-primary" : ""}`}
className={`${btn} ${handToolActive ? "bg-primary/10 text-primary-ink hover:text-primary-ink" : ""}`}
>
<Hand className="h-4 w-4" />
</button>
@@ -568,7 +568,7 @@ export function ExportDialog({ onClose }: { onClose: () => void }) {
height: canvasSize.height,
}))
}
className="mt-1 text-[10px] text-primary hover:underline"
className="mt-1 text-[10px] text-primary-ink hover:underline"
>
Reset to original size ({canvasSize.width} x {canvasSize.height})
</button>
@@ -826,7 +826,7 @@ export function AutosaveRecoveryBanner({
return (
<div className="flex items-center gap-3 px-4 py-2 bg-yellow-500/10 border-b border-yellow-500/30 text-xs">
<Save size={14} className="text-yellow-600 shrink-0" />
<Save size={14} className="text-amber-700 dark:text-amber-400 shrink-0" />
<span className="text-foreground">Recovered unsaved work from {timeStr}.</span>
<div className="flex items-center gap-2 ms-auto">
<button
@@ -74,7 +74,7 @@ export function WelcomeScreen() {
<div className="text-center">
<h2 className="text-xl font-semibold text-foreground mb-1 flex items-center justify-center gap-2">
{t.editor.welcome.heading}
<span className="rounded px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide bg-primary/15 text-primary">
<span className="rounded px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide bg-primary/15 text-primary-ink">
Beta
</span>
</h2>
@@ -437,9 +437,9 @@ function LevelsSection() {
const channelColors: Record<LevelsChannel, string> = {
rgb: "text-foreground",
red: "text-red-400",
green: "text-green-400",
blue: "text-blue-400",
red: "text-red-700 dark:text-red-400",
green: "text-green-700 dark:text-green-400",
blue: "text-blue-700 dark:text-blue-400",
};
return (
@@ -187,23 +187,23 @@ export function HistogramPanel({ imageData }: HistogramPanelProps) {
<div>
<span className="font-medium">Mean</span>
<br />
<span className="text-red-400">{stats.mean[0]}</span>{" "}
<span className="text-green-400">{stats.mean[1]}</span>{" "}
<span className="text-blue-400">{stats.mean[2]}</span>
<span className="text-red-700 dark:text-red-400">{stats.mean[0]}</span>{" "}
<span className="text-green-700 dark:text-green-400">{stats.mean[1]}</span>{" "}
<span className="text-blue-700 dark:text-blue-400">{stats.mean[2]}</span>
</div>
<div>
<span className="font-medium">StdDev</span>
<br />
<span className="text-red-400">{stats.stdDev[0]}</span>{" "}
<span className="text-green-400">{stats.stdDev[1]}</span>{" "}
<span className="text-blue-400">{stats.stdDev[2]}</span>
<span className="text-red-700 dark:text-red-400">{stats.stdDev[0]}</span>{" "}
<span className="text-green-700 dark:text-green-400">{stats.stdDev[1]}</span>{" "}
<span className="text-blue-700 dark:text-blue-400">{stats.stdDev[2]}</span>
</div>
<div>
<span className="font-medium">Median</span>
<br />
<span className="text-red-400">{stats.median[0]}</span>{" "}
<span className="text-green-400">{stats.median[1]}</span>{" "}
<span className="text-blue-400">{stats.median[2]}</span>
<span className="text-red-700 dark:text-red-400">{stats.median[0]}</span>{" "}
<span className="text-green-700 dark:text-green-400">{stats.median[1]}</span>{" "}
<span className="text-blue-700 dark:text-blue-400">{stats.median[2]}</span>
</div>
</div>
)}
@@ -210,7 +210,7 @@ export function HistoryPanel() {
className={cn(
"flex items-center gap-2 w-full px-2 py-1.5 text-start text-xs transition-colors",
isCurrent && "bg-primary/10 text-foreground font-medium",
isFuture && "text-muted-foreground/40",
isFuture && "text-muted-foreground",
!isCurrent &&
!isFuture &&
"text-muted-foreground hover:bg-muted hover:text-foreground",
@@ -653,7 +653,7 @@ function LayerContextMenu({
item.disabled
? "text-muted-foreground/40 cursor-not-allowed"
: "text-foreground hover:bg-muted",
item.label === "Delete" && !item.disabled && "text-red-500",
item.label === "Delete" && !item.disabled && "text-destructive-ink",
)}
>
<item.icon size={14} />
@@ -218,7 +218,7 @@ export function FeatureInstallPrompt({
const statusClass = isCandidateInstalled
? "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"
: isCandidateInstalling || isCandidateQueued
? "bg-primary/10 text-primary"
? "bg-primary/10 text-primary-ink"
: candidate.status === "error"
? "bg-destructive/10 text-destructive"
: "bg-muted text-muted-foreground";
@@ -241,14 +241,14 @@ export function FeedbackDialog({
href={buildToolRequestDiscussionUrl(searchQuery ?? "")}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
className="text-primary-ink hover:underline"
>
{t.feedback.searchMissDiscussionsFallback}
</a>
</p>
) : (
<div className="flex items-start gap-3">
<CheckCircle2 className="h-5 w-5 text-emerald-600 shrink-0 mt-0.5" />
<CheckCircle2 className="h-5 w-5 text-success-ink shrink-0 mt-0.5" />
<div className="space-y-1">
<p className="text-sm font-medium text-foreground">{t.feedback.thanksTitle}</p>
<p className="text-sm text-muted-foreground">{t.feedback.thanksDescription}</p>
@@ -313,7 +313,7 @@ export function FeedbackDialog({
className={cn(
"py-2 rounded-lg border text-sm font-medium transition-colors",
sentiment === value
? "border-primary bg-primary/10 text-primary"
? "border-primary bg-primary/10 text-primary-ink"
: "border-border text-muted-foreground hover:bg-muted hover:text-foreground",
)}
>
@@ -194,9 +194,7 @@ export function ToolFeedbackPrompt({
</div>
)}
{thanks && (
<p className="text-xs text-emerald-600 dark:text-emerald-400">{t.feedback.quickThanks}</p>
)}
{thanks && <p className="text-xs text-success-ink">{t.feedback.quickThanks}</p>}
<FeedbackDialog
open={dialogOpen}
@@ -496,9 +496,7 @@ export function FileDetails({ mobile = false }: FileDetailsProps) {
<div className="flex-1">
<div className="rounded-lg border border-border overflow-hidden">
<div className="bg-primary/10 border-b border-border px-3 py-2">
<h4 className="text-sm font-semibold text-primary-dark dark:text-primary-light">
{t.files.fileDetailsHeading}
</h4>
<h4 className="text-sm font-semibold text-primary-ink">{t.files.fileDetailsHeading}</h4>
</div>
<div className="divide-y divide-border">
<DetailRow label={t.files.name} value={details.originalName} />
@@ -69,7 +69,7 @@ export function FileListItem({ file, disabled }: FileListItemProps) {
{/* Tool chain */}
{file.toolChain.length > 0 && (
<span className="hidden md:block text-xs text-primary shrink-0">
<span className="hidden md:block text-xs text-primary-ink shrink-0">
{file.toolChain.map(toolName).join(" → ")}
</span>
)}
@@ -49,7 +49,7 @@ export function FileUploadArea() {
<div className="flex flex-col items-center gap-2">
<div className="h-10 w-10 border-2 border-primary border-t-transparent rounded-full animate-spin" />
{uploadProgress !== null && uploadProgress > 0 && (
<span className="text-xs font-medium text-primary">{uploadProgress}%</span>
<span className="text-xs font-medium text-primary-ink">{uploadProgress}%</span>
)}
</div>
) : (
+4 -4
View File
@@ -118,7 +118,7 @@ export function HelpDialog({ open, onClose }: HelpDialogProps) {
href="https://github.com/snapotter-hq/snapotter"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-sm text-primary hover:underline"
className="flex items-center gap-1.5 text-sm text-primary-ink hover:underline"
>
{t.help.resources.githubLink}
<ExternalLink className="h-3 w-3" />
@@ -127,7 +127,7 @@ export function HelpDialog({ open, onClose }: HelpDialogProps) {
href="https://github.com/snapotter-hq/snapotter/issues"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-sm text-primary hover:underline"
className="flex items-center gap-1.5 text-sm text-primary-ink hover:underline"
>
{t.help.resources.reportIssueLink}
<ExternalLink className="h-3 w-3" />
@@ -136,7 +136,7 @@ export function HelpDialog({ open, onClose }: HelpDialogProps) {
href="https://docs.snapotter.com/"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-sm text-primary hover:underline"
className="flex items-center gap-1.5 text-sm text-primary-ink hover:underline"
>
{t.help.resources.docsLink}
<ExternalLink className="h-3 w-3" />
@@ -145,7 +145,7 @@ export function HelpDialog({ open, onClose }: HelpDialogProps) {
href="/api/docs"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-sm text-primary hover:underline"
className="flex items-center gap-1.5 text-sm text-primary-ink hover:underline"
>
{t.help.resources.apiRefLink}
<ExternalLink className="h-3 w-3" />
+4 -3
View File
@@ -79,6 +79,7 @@ export function TopNav({
<div className="flex items-center gap-2 flex-1 min-w-0">
<Link
to="/"
aria-label={t.common.goHome}
className={cn(
"shrink-0",
isDark
@@ -179,7 +180,7 @@ export function TopNav({
<ChevronRight
className={cn(
"h-3.5 w-3.5 shrink-0",
isDark ? "text-[#666]" : "text-muted-foreground/50",
isDark ? "text-[#8a8a8a]" : "text-muted-foreground",
)}
/>
{breadcrumb.modalityTab ? (
@@ -204,7 +205,7 @@ export function TopNav({
<ChevronRight
className={cn(
"h-3.5 w-3.5 shrink-0",
isDark ? "text-[#666]" : "text-muted-foreground/50",
isDark ? "text-[#8a8a8a]" : "text-muted-foreground",
)}
/>
<span
@@ -234,7 +235,7 @@ export function TopNav({
>
{link.label}
{link.badge && (
<span className="ml-1.5 rounded px-1 py-0.5 text-[9px] font-bold uppercase tracking-wide bg-primary/15 text-primary align-middle">
<span className="ml-1.5 rounded px-1 py-0.5 text-[9px] font-bold uppercase tracking-wide bg-primary/15 text-primary-ink-strong align-middle">
{link.badge}
</span>
)}
@@ -205,7 +205,7 @@ export function UsageSurveyOverlay() {
className={cn(
"flex items-center gap-2 rounded-lg border px-3 py-2.5 text-sm font-medium text-start transition-colors",
usageType === value
? "border-primary bg-primary/10 text-primary"
? "border-primary bg-primary/10 text-primary-ink"
: "border-border text-foreground hover:bg-muted",
wide && "col-span-2 justify-center",
)}
@@ -238,7 +238,7 @@ export function UsageSurveyOverlay() {
className={cn(
"flex items-center gap-2 rounded-lg border px-3 py-2.5 text-sm font-medium text-start transition-colors",
importantAreas.includes(value)
? "border-primary bg-primary/10 text-primary"
? "border-primary bg-primary/10 text-primary-ink"
: "border-border text-foreground hover:bg-muted",
wide && "col-span-2 justify-center",
)}
@@ -270,7 +270,7 @@ export function UsageSurveyOverlay() {
className={cn(
"rounded-lg border px-3 py-2.5 text-sm font-medium text-start transition-colors",
installMethod === value
? "border-primary bg-primary/10 text-primary"
? "border-primary bg-primary/10 text-primary-ink"
: "border-border text-foreground hover:bg-muted",
)}
>
@@ -405,7 +405,7 @@ function ImportBundleSection({ onImported }: { onImported: () => void }) {
<p
role={feedback.type === "error" ? "alert" : "status"}
aria-live="polite"
className={`text-xs ${feedback.type === "success" ? "text-green-600 dark:text-green-400" : "text-destructive"}`}
className={`text-xs ${feedback.type === "success" ? "text-success-ink" : "text-destructive"}`}
>
{feedback.message}
</p>
@@ -437,7 +437,7 @@ function GeneralSection() {
{/* User info */}
<div className="flex items-center justify-between p-4 rounded-lg border border-border bg-muted/20">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary font-semibold">
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary-ink font-semibold">
{loading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
@@ -516,9 +516,7 @@ function GeneralSection() {
<span
className={cn(
"text-sm",
saveMsg === t.settings.general.saveFailed
? "text-destructive"
: "text-green-600 dark:text-green-400",
saveMsg === t.settings.general.saveFailed ? "text-destructive" : "text-success-ink",
)}
>
{saveMsg}
@@ -837,9 +835,7 @@ function SystemSection() {
<span
className={cn(
"text-sm",
saveMsg === t.settings.system.saveFailed
? "text-destructive"
: "text-green-600 dark:text-green-400",
saveMsg === t.settings.system.saveFailed ? "text-destructive" : "text-success-ink",
)}
>
{saveMsg}
@@ -1045,9 +1041,7 @@ function SecuritySection() {
role="alert"
className={cn(
"text-sm",
message.type === "error"
? "text-destructive"
: "text-green-600 dark:text-green-400",
message.type === "error" ? "text-destructive" : "text-success-ink",
)}
>
{message.text}
@@ -1347,7 +1341,7 @@ export function AdminSecuritySettings() {
<span
className={cn(
"text-sm",
saveMsg.type === "error" ? "text-destructive" : "text-green-600 dark:text-green-400",
saveMsg.type === "error" ? "text-destructive" : "text-success-ink",
)}
>
{saveMsg.text}
@@ -1614,7 +1608,7 @@ function PeopleSection() {
"text-sm px-3 py-2 rounded-lg",
actionMsg.type === "error"
? "bg-destructive/10 text-destructive"
: "bg-green-500/10 text-green-600 dark:text-green-400",
: "bg-green-500/10 text-success-ink",
)}
>
{actionMsg.text}
@@ -1715,7 +1709,7 @@ function PeopleSection() {
className={cn(
"shrink-0 p-2 rounded-lg border border-border transition-colors",
pwCopied
? "text-green-500 bg-green-500/10"
? "text-success-ink bg-green-500/10"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={
@@ -1778,7 +1772,7 @@ function PeopleSection() {
setShowGeneratedPw(true);
setPwCopied(false);
}}
className="flex items-center gap-1.5 px-3 py-2 rounded-lg border border-primary/30 bg-primary/10 text-xs text-primary hover:bg-primary/20 font-medium transition-colors"
className="flex items-center gap-1.5 px-3 py-2 rounded-lg border border-primary/30 bg-primary/10 text-xs text-primary-ink hover:bg-primary/20 font-medium transition-colors"
>
<Sparkles className="h-3 w-3" />
{t.changePassword.generateButton}
@@ -1801,7 +1795,7 @@ function PeopleSection() {
</button>
</div>
{showGeneratedPw && !pwCopied && (
<p className="text-xs text-amber-500 flex items-center gap-1.5">
<p className="text-xs text-amber-700 dark:text-amber-400 flex items-center gap-1.5">
<Key className="h-3.5 w-3.5 shrink-0" />
{t.settings.people.copyPasswordWarning}
</p>
@@ -1894,7 +1888,7 @@ function PeopleSection() {
/>
<button
type="submit"
className="px-4 py-2 rounded-lg bg-orange-500 text-white text-sm font-medium hover:bg-orange-600 transition-colors"
className="px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium hover:bg-primary-light transition-colors"
>
{t.settings.people.resetPasswordButton}
</button>
@@ -1942,7 +1936,7 @@ function PeopleSection() {
{isMobile ? (
<>
{/* Mobile card layout */}
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-semibold text-sm shrink-0">
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary-ink font-semibold text-sm shrink-0">
{u.username.charAt(0).toUpperCase()}
</div>
<div className="flex-1 min-w-0">
@@ -1966,7 +1960,7 @@ function PeopleSection() {
className={cn(
"inline-block px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide",
u.role === "admin"
? "bg-primary/15 text-primary"
? "bg-primary/15 text-primary-ink"
: "bg-muted text-muted-foreground",
)}
>
@@ -1980,7 +1974,7 @@ function PeopleSection() {
<>
{/* Desktop row layout */}
<div className="flex items-center gap-3 min-w-0">
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-semibold text-sm shrink-0">
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary-ink font-semibold text-sm shrink-0">
{u.username.charAt(0).toUpperCase()}
</div>
<span className="text-sm font-medium text-foreground truncate">
@@ -2004,7 +1998,7 @@ function PeopleSection() {
className={cn(
"inline-block px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide",
u.role === "admin"
? "bg-primary/15 text-primary"
? "bg-primary/15 text-primary-ink"
: "bg-muted text-muted-foreground",
)}
>
@@ -2282,7 +2276,11 @@ function ApiKeysSection() {
title="Copy"
aria-label={t.common.copy}
>
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
{copied ? (
<Check className="h-4 w-4 text-success-ink" />
) : (
<Copy className="h-4 w-4" />
)}
</button>
</div>
<p className="text-xs text-muted-foreground">{t.settings.apiKeys.keyWarning}</p>
@@ -2311,7 +2309,7 @@ function ApiKeysSection() {
</p>
)}
{k.expiresAt && (
<span className="text-xs text-amber-500">
<span className="text-xs text-amber-700 dark:text-amber-400">
Expires {new Date(k.expiresAt).toLocaleDateString()}
</span>
)}
@@ -2502,7 +2500,7 @@ function TeamsSection() {
"text-sm px-3 py-2 rounded-lg",
actionMsg.type === "error"
? "bg-destructive/10 text-destructive"
: "bg-green-500/10 text-green-600 dark:text-green-400",
: "bg-green-500/10 text-success-ink",
)}
>
{actionMsg.text}
@@ -2603,7 +2601,7 @@ function TeamsSection() {
<button
type="button"
onClick={() => handleRename(tm.id)}
className="text-xs text-primary hover:underline"
className="text-xs text-primary-ink hover:underline"
>
{t.common.save}
</button>
@@ -2917,7 +2915,7 @@ function RolesSection() {
"text-sm px-3 py-2 rounded-lg",
actionMsg.type === "error"
? "bg-destructive/10 text-destructive"
: "bg-green-500/10 text-green-600 dark:text-green-400",
: "bg-green-500/10 text-success-ink",
)}
>
{actionMsg.text}
@@ -3095,7 +3093,7 @@ function RolesSection() {
{t.settings.roles.builtInBadge}
</span>
)}
<span className="inline-block px-2 py-0.5 rounded-full bg-primary/10 text-xs font-medium text-primary">
<span className="inline-block px-2 py-0.5 rounded-full bg-primary/10 text-xs font-medium text-primary-ink">
{role.userCount} user{role.userCount !== 1 ? "s" : ""}
</span>
</div>
@@ -3595,7 +3593,7 @@ function AboutSection() {
<div className="flex items-center gap-3">
<OtterLogo className="h-8 w-8 text-primary" />
<div className="text-2xl font-bold text-foreground">
<span className="text-primary">SnapOtter</span>
<span className="text-primary-ink">SnapOtter</span>
</div>
</div>
<p className="text-sm text-muted-foreground">{t.settings.about.appDescription}</p>
@@ -3620,7 +3618,7 @@ function AboutSection() {
href="https://github.com/snapotter-hq/snapotter"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-primary hover:underline"
className="text-sm text-primary-ink hover:underline"
>
{t.settings.about.githubLink}
</a>
@@ -3628,7 +3626,7 @@ function AboutSection() {
href="https://docs.snapotter.com/"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-primary hover:underline"
className="text-sm text-primary-ink hover:underline"
>
{t.settings.about.docsLink}
</a>
@@ -3636,7 +3634,7 @@ function AboutSection() {
href="/api/docs"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-primary hover:underline"
className="text-sm text-primary-ink hover:underline"
>
{t.settings.about.apiRefLink}
</a>
@@ -198,7 +198,7 @@ export function TwoFactorSettings() {
<button
type="button"
onClick={handleCopyCodes}
className="mt-2 text-xs text-primary hover:underline"
className="mt-2 text-xs text-primary-ink hover:underline"
>
{codesCopied
? t.settings.security.twoFactorCodesCopied
@@ -229,9 +229,7 @@ export function TwoFactorSettings() {
<p
className={cn(
"text-sm",
message.type === "error"
? "text-destructive"
: "text-green-600 dark:text-green-400",
message.type === "error" ? "text-destructive" : "text-success-ink",
)}
>
{message.text}
@@ -277,9 +275,7 @@ export function TwoFactorSettings() {
<p
className={cn(
"text-sm",
message.type === "error"
? "text-destructive"
: "text-green-600 dark:text-green-400",
message.type === "error" ? "text-destructive" : "text-success-ink",
)}
>
{message.text}
@@ -311,7 +307,7 @@ export function TwoFactorSettings() {
<p
className={cn(
"text-sm",
message.type === "error" ? "text-destructive" : "text-green-600 dark:text-green-400",
message.type === "error" ? "text-destructive" : "text-success-ink",
)}
>
{message.text}
@@ -106,7 +106,7 @@ export function UsageSection() {
{row.total}
</span>
</div>
<span className="w-14 shrink-0 text-end text-green-600 dark:text-green-400">
<span className="w-14 shrink-0 text-end text-success-ink">
{row.completed}
</span>
<span className="w-10 shrink-0 text-end text-destructive">
@@ -208,7 +208,7 @@ export function AiCanvasExpandSettings() {
</p>
)}
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -237,7 +237,7 @@ export function AiCanvasExpandSettings() {
href={downloadUrl}
download
data-testid="ai-canvas-expand-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
Download
@@ -65,7 +65,7 @@ export function AspectPadSettings() {
<p className="text-[10px] text-muted-foreground mt-0.5">{s.colorHint}</p>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -46,7 +46,7 @@ export function AudioChannelsSettings() {
</select>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -86,7 +86,7 @@ export function AudioMetadataSettings() {
/>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -44,7 +44,7 @@ export function AudioSpeedSettings() {
/>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -133,7 +133,7 @@ export function BarcodeGenerateSettings() {
{ts.includeText}
</label>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{/* Result preview */}
{resultUrl && (
@@ -147,7 +147,7 @@ export function BarcodeGenerateSettings() {
href={resultUrl}
download="barcode.png"
data-testid="barcode-generate-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
{ts.download}
@@ -45,11 +45,11 @@ const FORMAT_LABELS: Record<string, string> = {
/** Badge colors by barcode family. */
function getBadgeColor(type: string): string {
if (type.includes("QR") || type === "Aztec" || type === "DataMatrix" || type === "MaxiCode")
return "bg-primary/15 text-primary-dark dark:text-primary-light";
return "bg-primary/15 text-primary-ink";
if (type.includes("EAN") || type.includes("UPC") || type.includes("DataBar"))
return "bg-green-500/15 text-green-600 dark:text-green-400";
return "bg-green-500/15 text-success-ink";
if (type === "PDF417") return "bg-purple-500/15 text-purple-600 dark:text-purple-400";
return "bg-amber-500/15 text-amber-600 dark:text-amber-400";
return "bg-amber-500/15 text-amber-700 dark:text-amber-400";
}
function SectionLabel({ children }: { children: React.ReactNode }) {
@@ -264,7 +264,7 @@ export function BarcodeReadSettings() {
</label>
{/* Error */}
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{/* Process button / progress */}
{processing ? (
@@ -360,7 +360,7 @@ export function BarcodeReadSettings() {
title="Copy value"
>
{copiedIndex === idx ? (
<Check className="h-3.5 w-3.5 text-green-500" />
<Check className="h-3.5 w-3.5 text-success-ink" />
) : (
<Copy className="h-3.5 w-3.5" />
)}
@@ -1374,7 +1374,7 @@ export function BeautifySettings({
onBackgroundImage={handleBgImageChange}
/>
{displayError && <p className="text-xs text-red-500">{displayError}</p>}
{displayError && <p className="text-xs text-destructive-ink">{displayError}</p>}
{processing ? (
<ProgressCard
@@ -1405,7 +1405,7 @@ export function BeautifySettings({
href={displayDownloadUrl}
download
data-testid="beautify-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
Download
@@ -92,7 +92,7 @@ export function BlurBackgroundSettings() {
onClick={() => setOutputFormat(fmt)}
className={`rounded-lg border py-2 px-2 text-xs font-medium uppercase transition-colors ${
outputFormat === fmt
? "border-primary bg-primary/10 text-primary"
? "border-primary bg-primary/10 text-primary-ink"
: "border-border text-muted-foreground hover:border-primary/50"
}`}
>
@@ -114,9 +114,9 @@ export function BlurFacesSettings() {
<div className="space-y-4">
<BlurFacesControls onChange={setSettings} />
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{warning && <p className="text-xs text-amber-600 dark:text-amber-400">{warning}</p>}
{warning && <p className="text-xs text-amber-700 dark:text-amber-400">{warning}</p>}
{/* Size info */}
{originalSize != null && processedSize != null && (
@@ -153,7 +153,7 @@ export function BlurFacesSettings() {
href={downloadUrl}
download
data-testid="blur-faces-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
{t.common.download}
@@ -65,7 +65,7 @@ export function BlurPadSettings() {
/>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -47,7 +47,7 @@ export function BookletPdfSettings() {
</select>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -613,7 +613,7 @@ export function BorderSettings({
<form onSubmit={handleSubmit} className="space-y-4">
<BorderControls onChange={handleSettingsChange} onImageStyle={onImageStyle} />
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -640,7 +640,7 @@ export function BorderSettings({
href={downloadUrl}
download
data-testid="border-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
Download
@@ -119,7 +119,7 @@ export function BulkRenameSettings() {
</div>
)}
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
<button
type="button"
@@ -135,7 +135,7 @@ export function BulkRenameSettings() {
</button>
{downloadReady && (
<p className="text-xs text-green-600 flex items-center gap-1">
<p className="text-xs text-success-ink flex items-center gap-1">
<Download className="h-3 w-3" /> {t.toolSettings["bulk-rename"].zipDownloaded}
</p>
)}
@@ -38,7 +38,7 @@ export function BurnSubtitlesSettings() {
<p className="text-[10px] text-muted-foreground">{s.hint}</p>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -44,7 +44,7 @@ export function ChangeFpsSettings() {
/>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -116,7 +116,7 @@ export function ChartMakerSettings() {
/>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -143,7 +143,7 @@ export function ChartMakerSettings() {
href={downloadUrl}
download
data-testid="chart-maker-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
{t.common.download}
@@ -238,7 +238,7 @@ export function CircleCropSettings() {
/>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -268,7 +268,7 @@ export function CircleCropSettings() {
href={downloadUrl}
download
data-testid="circle-crop-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
{t.common.download}
@@ -137,7 +137,7 @@ function UploadArea() {
<button
type="button"
onClick={handleClick}
className="flex items-center gap-2 px-6 py-2.5 rounded-lg border border-primary text-primary hover:bg-primary/5 transition-colors text-sm font-medium"
className="flex items-center gap-2 px-6 py-2.5 rounded-lg border border-primary text-primary-ink hover:bg-primary/5 transition-colors text-sm font-medium"
>
<Upload className="h-4 w-4" />
Upload images for collage
@@ -520,7 +520,7 @@ function CollageCell({
className={cn(
"flex items-center gap-1 rounded px-1.5 py-1 text-[11px] font-medium backdrop-blur-sm transition-colors",
transform.objectFit === "contain"
? "bg-primary/80 text-white hover:bg-primary/90"
? "bg-primary/80 text-primary-foreground hover:bg-primary/90"
: "bg-black/50 text-white hover:bg-black/70",
)}
title={transform.objectFit === "cover" ? "Fit entire image" : "Fill cell"}
@@ -361,7 +361,7 @@ export function CollageSettings() {
</CollapsibleSection>
{/* Error */}
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{/* Actions */}
<button
@@ -380,7 +380,7 @@ export function CollageSettings() {
href={resultUrl}
download
data-testid="collage-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
Download Collage
@@ -121,7 +121,7 @@ export function ColorBlindnessSettings() {
)}
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{originalSize != null && processedSize != null && (
<div className="text-xs text-muted-foreground space-y-0.5">
@@ -158,7 +158,7 @@ export function ColorBlindnessSettings() {
href={downloadUrl}
download
data-testid="color-blindness-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
{t.common.download}
@@ -130,7 +130,7 @@ export function ColorPaletteSettings() {
{processing ? ts.extracting : ts.submit}
</button>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{colors.length > 0 && (
<div className="space-y-3">
@@ -157,7 +157,7 @@ export function ColorPaletteSettings() {
data-testid="color-palette-copy-css"
>
{copiedExport === "css" ? (
<Check className="h-3 w-3 text-green-500" />
<Check className="h-3 w-3 text-success-ink" />
) : (
<ClipboardCopy className="h-3 w-3" />
)}
@@ -170,7 +170,7 @@ export function ColorPaletteSettings() {
data-testid="color-palette-copy-json"
>
{copiedExport === "json" ? (
<Check className="h-3 w-3 text-green-500" />
<Check className="h-3 w-3 text-success-ink" />
) : (
<ClipboardCopy className="h-3 w-3" />
)}
@@ -196,7 +196,7 @@ export function ColorPaletteSettings() {
{color}
</span>
{copiedIdx === i ? (
<Check className="h-3 w-3 text-green-500 shrink-0" />
<Check className="h-3 w-3 text-success-ink shrink-0" />
) : (
<Copy className="h-3 w-3 text-muted-foreground shrink-0" />
)}
@@ -275,7 +275,9 @@ export function ColorControls({
>
{channelsOpen ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
Color Channels
{hasChannelChanges && <span className="ms-auto text-primary text-[10px]">modified</span>}
{hasChannelChanges && (
<span className="ms-auto text-primary-ink text-[10px]">modified</span>
)}
</button>
{channelsOpen && (
<div className="space-y-2 ps-1">
@@ -285,7 +287,7 @@ export function ColorControls({
onChange={setRed}
min={0}
max={200}
color="text-red-500"
color="text-destructive-ink"
/>
<SliderControl
label="Green"
@@ -293,7 +295,7 @@ export function ColorControls({
onChange={setGreen}
min={0}
max={200}
color="text-green-500"
color="text-success-ink"
/>
<SliderControl
label="Blue"
@@ -301,7 +303,7 @@ export function ColorControls({
onChange={setBlue}
min={0}
max={200}
color="text-blue-500"
color="text-blue-700 dark:text-blue-400"
/>
</div>
)}
@@ -379,7 +381,7 @@ export function ColorSettings({ toolId, onPreviewFilter }: ColorSettingsProps) {
<form onSubmit={handleSubmit} className="space-y-3">
<ColorControls toolId={toolId} onChange={setSettings} onPreviewFilter={onPreviewFilter} />
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{originalSize != null && processedSize != null && (
<div className="text-xs text-muted-foreground space-y-0.5">
@@ -414,7 +416,7 @@ export function ColorSettings({ toolId, onPreviewFilter }: ColorSettingsProps) {
href={downloadUrl}
download
data-testid="adjust-colors-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
Download
@@ -104,7 +104,7 @@ export function ColorizeSettings() {
</p>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{originalSize != null && processedSize != null && (
<div className="text-xs text-muted-foreground space-y-0.5">
@@ -140,7 +140,7 @@ export function ColorizeSettings() {
href={downloadUrl}
download
data-testid="colorize-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
Download
@@ -72,7 +72,7 @@ export function CompareSettings() {
</button>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{similarity !== null && (
<div className="p-3 rounded-lg bg-muted">
@@ -101,7 +101,7 @@ export function CompareSettings() {
href={downloadUrl}
download
data-testid="compare-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
Download Diff Image
@@ -151,7 +151,7 @@ export function ComposeSettings() {
</select>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{originalSize != null && processedSize != null && (
<div className="text-xs text-muted-foreground space-y-0.5">
@@ -176,7 +176,7 @@ export function ComposeSettings() {
href={downloadUrl}
download
data-testid="compose-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
Download
@@ -51,15 +51,15 @@ export function CompressPdfSettings() {
<p className="text-[11px] text-muted-foreground">{s.bestEffortHint}</p>
)}
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{targetMet === false && (
<p className="text-xs text-amber-600">
<p className="text-xs text-amber-700 dark:text-amber-400">
{format(s.targetMissed, { target: targetLabel, size: achievedLabel })}
</p>
)}
{targetMet === true && (
<p className="text-xs text-green-600">
<p className="text-xs text-success-ink">
{format(s.targetReached, { target: targetLabel, size: achievedLabel })}
</p>
)}
@@ -206,7 +206,7 @@ export function CompressSettings() {
<CompressControls onChange={setSettings} />
{/* Error */}
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{/* Size info */}
{originalSize != null && processedSize != null && (
@@ -255,7 +255,7 @@ export function CompressSettings() {
href={downloadUrl}
download
data-testid="compress-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
{t.common.download}
@@ -65,7 +65,7 @@ export function CompressVideoSettings() {
</select>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -197,7 +197,7 @@ export function ContentAwareResizeSettings() {
<form onSubmit={handleSubmit} className="space-y-4">
<ContentAwareResizeControls onChange={handleSettingsChange} />
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -226,7 +226,7 @@ export function ContentAwareResizeSettings() {
href={downloadUrl}
download
data-testid="content-aware-resize-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
Download
@@ -130,7 +130,7 @@ export function ConversionPresetSettings() {
</div>
)}
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -159,7 +159,7 @@ export function ConversionPresetSettings() {
href={downloadUrl}
download
data-testid="preset-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
{t.common.download}
@@ -128,7 +128,7 @@ export function ConvertAudioSettings() {
</select>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -64,7 +64,7 @@ export function ConvertDocumentSettings() {
</select>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -62,7 +62,7 @@ export function ConvertPresentationSettings() {
</select>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -174,7 +174,7 @@ export function ConvertSettings() {
<ConvertControls onChange={setSettings} />
{/* Error */}
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{/* Size info */}
{originalSize != null && processedSize != null && (
@@ -213,7 +213,7 @@ export function ConvertSettings() {
href={downloadUrl}
download
data-testid="convert-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
{t.common.download}
@@ -65,7 +65,7 @@ export function ConvertSpreadsheetSettings() {
<p className="text-[10px] text-muted-foreground">{s.hint}</p>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -64,7 +64,7 @@ export function ConvertVideoSettings() {
</select>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -17,7 +17,7 @@ export function CreateZipSettings() {
return (
<div className="space-y-4">
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -44,7 +44,7 @@ export function CropPdfSettings() {
/>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -365,7 +365,7 @@ export function CropSettings({
</label>
{/* Error */}
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{/* Process */}
{processing ? (
@@ -396,7 +396,7 @@ export function CropSettings({
href={downloadUrl}
download
data-testid="crop-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
{t.common.download}
@@ -88,7 +88,7 @@ export function CropVideoSettings() {
/>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -48,7 +48,7 @@ export function CsvExcelSettings() {
</div>
)}
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -38,7 +38,7 @@ export function CsvJsonSettings() {
{s.pretty}
</label>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -185,7 +185,7 @@ export function DuotoneSettings({ onImageStyle, onImageOverlay }: DuotoneSetting
/>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -214,7 +214,7 @@ export function DuotoneSettings({ onImageStyle, onImageOverlay }: DuotoneSetting
href={downloadUrl}
download
data-testid="duotone-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
{t.common.download}
@@ -587,9 +587,9 @@ export function EditMetadataSettings() {
<div className="space-y-2.5">
{gpsCoords && (
<div className="flex items-start gap-2 px-2.5 py-2 rounded-md bg-amber-500/10 border border-amber-500/20">
<MapPin className="h-3.5 w-3.5 text-amber-500 shrink-0 mt-0.5" />
<MapPin className="h-3.5 w-3.5 text-amber-700 dark:text-amber-400 shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
<p className="text-[11px] text-amber-600 dark:text-amber-400 font-medium">
<p className="text-[11px] text-amber-700 dark:text-amber-400 font-medium">
Location data found
</p>
<p className="text-[10px] text-muted-foreground font-mono">
@@ -659,13 +659,13 @@ export function EditMetadataSettings() {
{form.keywords.map((kw) => (
<span
key={kw}
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-primary/10 text-primary text-[11px]"
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-primary/10 text-primary-ink text-[11px]"
>
{kw}
<button
type="button"
onClick={() => removeKeyword(kw)}
className="hover:text-red-500"
className="hover:text-destructive-ink"
>
<X className="h-2.5 w-2.5" />
</button>
@@ -783,7 +783,7 @@ export function EditMetadataSettings() {
<button
type="button"
onClick={() => deleteTemplate(t.name)}
className="p-1 text-muted-foreground hover:text-red-500"
className="p-1 text-muted-foreground hover:text-destructive-ink"
>
<X className="h-3 w-3" />
</button>
@@ -820,7 +820,7 @@ export function EditMetadataSettings() {
</div>
)}
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{/* Changes summary */}
{hasFile && changes.total > 0 && !processing && (
@@ -866,7 +866,7 @@ export function EditMetadataSettings() {
href={downloadUrl}
download
data-testid="edit-metadata-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
Download
@@ -35,7 +35,7 @@ export function EmbedSubtitlesSettings() {
<p className="text-[10px] text-muted-foreground mt-0.5">{s.languageHint}</p>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -172,7 +172,7 @@ export function EnhanceFacesSettings() {
<EnhanceFacesControls onChange={setSettings} />
{/* Error */}
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{/* Size info */}
{originalSize != null && processedSize != null && (
@@ -209,7 +209,7 @@ export function EnhanceFacesSettings() {
href={downloadUrl}
download
data-testid="enhance-faces-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
Download
@@ -49,7 +49,7 @@ export function EpubConvertSettings() {
<p className="text-[10px] text-muted-foreground">{s.hint}</p>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -580,7 +580,7 @@ export function EraseObjectSettings({
)}
{/* Error */}
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{/* Size info */}
{currentEntry?.originalSize != null &&
@@ -621,7 +621,7 @@ export function EraseObjectSettings({
href={currentEntry.processedUrl}
download
data-testid="erase-object-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
Download
@@ -25,7 +25,7 @@ export function ExcelToPdfSettings() {
return (
<div className="space-y-4">
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -46,7 +46,7 @@ export function ExtractAudioSettings() {
</select>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -42,7 +42,7 @@ export function ExtractPagesSettings() {
<p className="text-[10px] text-muted-foreground mt-0.5">{s.rangeHint}</p>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -25,7 +25,7 @@ export function ExtractSubtitlesSettings() {
return (
<div className="space-y-4">
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -25,7 +25,7 @@ export function ExtractZipSettings() {
return (
<div className="space-y-4">
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -61,7 +61,7 @@ export function FadeAudioSettings() {
/>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -351,7 +351,7 @@ export function FaviconSettings() {
</p>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{busy ? (
<ProgressCard
@@ -385,7 +385,7 @@ export function FaviconSettings() {
href={downloadUrl}
download="favicons.zip"
data-testid="favicon-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
Download Favicons ZIP
@@ -27,19 +27,21 @@ function OverviewGrid() {
<div className="w-px h-4 bg-border" />
<div className="flex items-center gap-1.5">
<span className="text-muted-foreground">Duplicate groups:</span>
<span className="text-yellow-500 font-semibold">{results.duplicateGroups.length}</span>
<span className="text-amber-700 dark:text-amber-400 font-semibold">
{results.duplicateGroups.length}
</span>
</div>
<div className="w-px h-4 bg-border" />
<div className="flex items-center gap-1.5">
<span className="text-muted-foreground">Unique:</span>
<span className="text-green-500 font-semibold">{results.uniqueImages}</span>
<span className="text-success-ink font-semibold">{results.uniqueImages}</span>
</div>
{results.spaceSaveable > 0 && (
<>
<div className="w-px h-4 bg-border" />
<div className="flex items-center gap-1.5">
<span className="text-muted-foreground">Space saveable:</span>
<span className="text-primary font-semibold">
<span className="text-primary-ink font-semibold">
{formatFileSize(results.spaceSaveable)}
</span>
</div>
@@ -50,7 +52,7 @@ function OverviewGrid() {
{results.duplicateGroups.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 gap-3">
<div className="w-12 h-12 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
<Search className="h-6 w-6 text-green-600 dark:text-green-400" />
<Search className="h-6 w-6 text-success-ink" />
</div>
<p className="text-sm font-medium text-foreground">No duplicates found</p>
<p className="text-xs text-muted-foreground">
@@ -147,7 +149,7 @@ function DetailComparison() {
<button
type="button"
onClick={() => setViewMode("overview")}
className="flex items-center gap-1 text-xs text-primary hover:text-primary/80"
className="flex items-center gap-1 text-xs text-primary-ink hover:text-primary-ink-strong"
>
<ArrowLeft className="h-3.5 w-3.5" />
Back to Overview
@@ -173,7 +175,7 @@ function DetailComparison() {
<ChevronRight className="h-3.5 w-3.5" />
</button>
</div>
<span className="text-xs font-medium text-yellow-500">
<span className="text-xs font-medium text-amber-700 dark:text-amber-400">
{Math.max(...group.files.map((f) => f.similarity))}% Similar
</span>
</div>
@@ -228,7 +230,7 @@ function DetailComparison() {
<span className="text-foreground text-end">{file.format.toUpperCase()}</span>
<span className="text-muted-foreground">Similarity</span>
<span
className={`text-end font-medium ${file.similarity === 100 ? "text-green-500" : "text-yellow-500"}`}
className={`text-end font-medium ${file.similarity === 100 ? "text-success-ink" : "text-amber-700 dark:text-amber-400"}`}
>
{file.similarity}%
</span>
@@ -276,7 +276,7 @@ export function FindDuplicatesSettings() {
{activeDesc && <p className="text-[10px] text-muted-foreground">{activeDesc}</p>}
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{/* Scan button + progress */}
{!results && (
@@ -317,16 +317,18 @@ export function FindDuplicatesSettings() {
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Duplicate groups</span>
<span className="text-yellow-500 font-medium">{results.duplicateGroups.length}</span>
<span className="text-amber-700 dark:text-amber-400 font-medium">
{results.duplicateGroups.length}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Unique images</span>
<span className="text-green-500 font-medium">{results.uniqueImages}</span>
<span className="text-success-ink font-medium">{results.uniqueImages}</span>
</div>
{results.spaceSaveable > 0 && (
<div className="flex justify-between">
<span className="text-muted-foreground">Space saveable</span>
<span className="text-primary font-medium">
<span className="text-primary-ink font-medium">
{formatFileSize(results.spaceSaveable)}
</span>
</div>
@@ -334,14 +336,16 @@ export function FindDuplicatesSettings() {
{results.skippedFiles && results.skippedFiles.length > 0 && (
<div className="flex justify-between">
<span className="text-muted-foreground">Skipped</span>
<span className="text-orange-500 font-medium">{results.skippedFiles.length}</span>
<span className="text-amber-700 dark:text-amber-400 font-medium">
{results.skippedFiles.length}
</span>
</div>
)}
</div>
{results.skippedFiles && results.skippedFiles.length > 0 && (
<details className="text-xs">
<summary className="text-orange-500 cursor-pointer">
<summary className="text-amber-700 dark:text-amber-400 cursor-pointer">
{results.skippedFiles.length} file{results.skippedFiles.length > 1 ? "s" : ""} could
not be analyzed
</summary>
@@ -369,7 +373,7 @@ export function FindDuplicatesSettings() {
<button
type="button"
onClick={handleDownloadUnique}
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
Download Unique Only
@@ -25,7 +25,7 @@ export function FlattenPdfSettings() {
return (
<div className="space-y-4">
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -45,7 +45,7 @@ export function GifToVideoSettings() {
</select>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -194,7 +194,7 @@ export function GifToolsControls({ settings: initialSettings, onChange }: GifToo
</>
) : (
<>
<span className="text-amber-500 font-medium">Static image</span>
<span className="text-amber-700 dark:text-amber-400 font-medium">Static image</span>
<span className="text-border">|</span>
<span>
{info.width}x{info.height}
@@ -673,7 +673,7 @@ export function GifToolsSettings() {
<div className="space-y-4">
<GifToolsControls onChange={setSettings} />
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{originalSize != null && processedSize != null && (
<div className="text-xs text-muted-foreground space-y-0.5">
@@ -717,7 +717,7 @@ export function GifToolsSettings() {
href={downloadUrl}
download
data-testid="gif-tools-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
Download
@@ -106,7 +106,7 @@ export function GifWebpSettings() {
/>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -136,7 +136,7 @@ export function GifWebpSettings() {
href={downloadUrl}
download
data-testid="gif-webp-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
{t.common.download}
@@ -25,7 +25,7 @@ export function GrayscalePdfSettings() {
return (
<div className="space-y-4">
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -91,7 +91,7 @@ export function HistogramSettings() {
Generates an RGB histogram chart showing the color distribution of the image.
</p>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -199,7 +199,7 @@ export function HistogramSettings() {
</span>
</div>
))}
<p className="col-span-2 text-[9px] text-muted-foreground/60 mt-0.5">
<p className="col-span-2 text-[9px] text-muted-foreground mt-0.5">
mean / median / stdev
</p>
</div>
@@ -212,7 +212,7 @@ export function HistogramSettings() {
href={downloadUrl}
download
data-testid="histogram-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
{t.common.download}
@@ -27,7 +27,7 @@ export function HtmlToPdfSettings() {
<div className="space-y-4">
<p className="text-[10px] text-muted-foreground">{s.hint}</p>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -380,7 +380,7 @@ export function ImageEnhancementControls({
onClick={() => toggleKey && toggleCorrection(toggleKey)}
className={`inline-flex items-center gap-1 text-[11px] px-2 py-1 rounded-full transition-colors ${
isEnabled
? "bg-amber-500/15 text-amber-600 dark:text-amber-400"
? "bg-amber-500/15 text-amber-700 dark:text-amber-400"
: "bg-muted text-muted-foreground line-through"
}`}
>
@@ -462,7 +462,7 @@ export function ImageEnhancementSettings({
<form onSubmit={handleSubmit} className="space-y-3">
<ImageEnhancementControls onChange={setSettings} onPreviewFilter={onPreviewFilter} />
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{originalSize != null && processedSize != null && (
<div className="text-xs text-muted-foreground space-y-0.5">
@@ -499,7 +499,7 @@ export function ImageEnhancementSettings({
href={downloadUrl}
download
data-testid="image-enhancement-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
Download
@@ -270,7 +270,7 @@ export function ImagePadSettings({ onImageStyle, onImageOverlay }: ImagePadSetti
/>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
{error && <p className="text-xs text-destructive-ink">{error}</p>}
{processing ? (
<ProgressCard
@@ -299,7 +299,7 @@ export function ImagePadSettings({ onImageStyle, onImageOverlay }: ImagePadSetti
href={downloadUrl}
download
data-testid="image-pad-download"
className="w-full py-2.5 rounded-lg border border-primary text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
className="w-full py-2.5 rounded-lg border border-primary text-primary-ink font-medium flex items-center justify-center gap-2 hover:bg-primary/5"
>
<Download className="h-4 w-4" />
{t.common.download}
@@ -117,7 +117,9 @@ function FileResult({ result }: { result: Base64Result }) {
<p className="text-[11px] text-muted-foreground">
{result.width}x{result.height} &middot; {formatBytes(result.originalSize)} &rarr;{" "}
{formatBytes(result.encodedSize)}{" "}
<span className={result.overheadPercent > 50 ? "text-amber-500" : ""}>
<span
className={result.overheadPercent > 50 ? "text-amber-700 dark:text-amber-400" : ""}
>
(+{result.overheadPercent}%)
</span>
</p>
@@ -133,7 +135,7 @@ function FileResult({ result }: { result: Base64Result }) {
onClick={() => setActiveTab(t.id)}
className={`px-3 py-1.5 text-xs whitespace-nowrap transition-colors ${
activeTab === t.id
? "text-primary border-b-2 border-primary font-medium"
? "text-primary-ink border-b-2 border-primary font-medium"
: "text-muted-foreground hover:text-foreground"
}`}
>
@@ -308,8 +310,10 @@ export function ImageToBase64Results() {
) : currentError ? (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<p className="text-sm text-red-500 font-medium mb-1">{currentError.filename}</p>
<p className="text-xs text-red-500/80">{currentError.error}</p>
<p className="text-sm text-destructive-ink font-medium mb-1">
{currentError.filename}
</p>
<p className="text-xs text-destructive-ink">{currentError.error}</p>
</div>
</div>
) : (

Some files were not shown because too many files have changed in this diff Show More