mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
@@ -14,7 +14,7 @@ export function DemoBanner() {
|
|||||||
href="https://docs.snapotter.com/guide/getting-started"
|
href="https://docs.snapotter.com/guide/getting-started"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="font-semibold underline underline-offset-2 hover:text-primary-subtle"
|
className="font-semibold underline underline-offset-2 hover:no-underline"
|
||||||
>
|
>
|
||||||
Self-host SnapOtter
|
Self-host SnapOtter
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
@@ -23,7 +23,7 @@ export function DemoBanner() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setDismissed(true)}
|
onClick={() => setDismissed(true)}
|
||||||
className="ms-2 shrink-0 rounded px-1.5 py-0.5 text-xs font-medium hover:bg-primary-dark"
|
className="ms-2 shrink-0 rounded px-1.5 py-0.5 text-xs font-medium hover:bg-primary-light"
|
||||||
aria-label="Dismiss banner"
|
aria-label="Dismiss banner"
|
||||||
>
|
>
|
||||||
Dismiss
|
Dismiss
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ function patchDemoErrors(root: Element) {
|
|||||||
if (!el.textContent?.includes(DEMO_MARKER)) return;
|
if (!el.textContent?.includes(DEMO_MARKER)) return;
|
||||||
if (el.dataset.demoPatch) return;
|
if (el.dataset.demoPatch) return;
|
||||||
el.dataset.demoPatch = "1";
|
el.dataset.demoPatch = "1";
|
||||||
el.classList.remove("text-red-500");
|
el.classList.remove("text-red-500", "text-destructive-ink");
|
||||||
el.style.color = "var(--color-primary)";
|
el.style.color = "var(--color-primary-ink)";
|
||||||
el.style.fontSize = "13px";
|
el.style.fontSize = "13px";
|
||||||
el.style.lineHeight = "1.5";
|
el.style.lineHeight = "1.5";
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
color: #db2777;
|
color: #cf236f;
|
||||||
background: rgba(219, 39, 119, 0.08);
|
background: rgba(219, 39, 119, 0.08);
|
||||||
border: 1px solid rgba(219, 39, 119, 0.3);
|
border: 1px solid rgba(219, 39, 119, 0.3);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const MODALITIES = [
|
|||||||
label: t(locale, "home.categoryCards.file.label"),
|
label: t(locale, "home.categoryCards.file.label"),
|
||||||
blurb: t(locale, "home.categoryCards.file.blurb"),
|
blurb: t(locale, "home.categoryCards.file.blurb"),
|
||||||
icon: "Database",
|
icon: "Database",
|
||||||
color: "#5C8642",
|
color: "#51763A",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -77,21 +77,21 @@ const cards = MODALITIES.map((m) => {
|
|||||||
{cards.map((c) => (
|
{cards.map((c) => (
|
||||||
<a
|
<a
|
||||||
href={localizeHref(locale, `/tools/${c.section}/`)}
|
href={localizeHref(locale, `/tools/${c.section}/`)}
|
||||||
class="group flex flex-col rounded-2xl p-4 text-start text-white no-underline shadow-sm transition-all hover:-translate-y-1 hover:shadow-lg"
|
class="group flex flex-col rounded-2xl p-4 text-start no-underline shadow-sm transition-all hover:-translate-y-1 hover:shadow-lg"
|
||||||
style={`background-color: ${c.color}`}
|
style={`background-color: ${c.color}; color: ${c.id === "image" ? "#1A1814" : "#FFFFFF"}`}
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="flex h-9 w-9 items-center justify-center rounded-lg bg-white/20">
|
<span class={`flex h-9 w-9 items-center justify-center rounded-lg ${c.id === "image" ? "bg-white/20" : "bg-black/20"}`}>
|
||||||
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" set:html={renderIcon(c.icon)} />
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" set:html={renderIcon(c.icon)} />
|
||||||
</span>
|
</span>
|
||||||
<span class="rounded-full bg-white/20 px-2.5 py-0.5 text-xs font-semibold">{t(locale, "home.categoryCards.toolCount", { count: c.count })}</span>
|
<span class={`rounded-full px-2.5 py-0.5 text-xs font-semibold ${c.id === "image" ? "bg-white/20" : "bg-black/20"}`}>{t(locale, "home.categoryCards.toolCount", { count: c.count })}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center gap-1.5">
|
<div class="mt-4 flex items-center gap-1.5">
|
||||||
<p class="font-display text-base font-bold leading-tight">{c.label}</p>
|
<p class="font-display text-base font-bold leading-tight">{c.label}</p>
|
||||||
<svg class="h-4 w-4 -translate-x-1 opacity-0 transition-all group-hover:translate-x-0 group-hover:opacity-100" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
|
<svg class="h-4 w-4 -translate-x-1 opacity-0 transition-all group-hover:translate-x-0 group-hover:opacity-100" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-white/80">{c.blurb}</p>
|
<p class="mt-1 text-xs">{c.blurb}</p>
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ const frameworks = ["SOC 2", "ISO 27001", "HIPAA", "GDPR"];
|
|||||||
set:html={feature.icon}
|
set:html={feature.icon}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="font-mono text-[10px] uppercase tracking-[0.15em] text-primary/60">{feature.tag}</span>
|
<span class="font-mono text-[10px] uppercase tracking-[0.15em] text-primary-light">{feature.tag}</span>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="mt-4 font-display text-base font-bold text-dark-fg">{feature.title}</h3>
|
<h3 class="mt-4 font-display text-base font-bold text-dark-fg">{feature.title}</h3>
|
||||||
<p class="mt-2 text-sm leading-relaxed text-dark-muted">{feature.description}</p>
|
<p class="mt-2 text-sm leading-relaxed text-dark-muted">{feature.description}</p>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const aiPillColors = [
|
|||||||
|
|
||||||
<section class="px-6 py-20 md:py-28">
|
<section class="px-6 py-20 md:py-28">
|
||||||
<div class="reveal mx-auto mb-16 max-w-3xl text-center md:mb-20">
|
<div class="reveal mx-auto mb-16 max-w-3xl text-center md:mb-20">
|
||||||
<span class="inline-flex items-center gap-2 rounded-full border border-primary/20 bg-primary/5 px-4 py-1.5 text-xs font-semibold uppercase tracking-[0.2em] text-primary-dark">
|
<span class="inline-flex items-center gap-2 rounded-full border border-primary/20 bg-primary/5 px-4 py-1.5 text-xs font-semibold uppercase tracking-[0.2em] text-primary-ink">
|
||||||
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||||
{t(locale, "home.features.badge")}
|
{t(locale, "home.features.badge")}
|
||||||
</span>
|
</span>
|
||||||
@@ -58,7 +58,7 @@ const aiPillColors = [
|
|||||||
<!-- Feature 1: Data Sovereignty — text left, visual right -->
|
<!-- Feature 1: Data Sovereignty — text left, visual right -->
|
||||||
<div class="reveal grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-16 items-center">
|
<div class="reveal grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-16 items-center">
|
||||||
<div class="md:order-1">
|
<div class="md:order-1">
|
||||||
<span class="font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary">{t(locale, "home.features.f1.kicker")}</span>
|
<span class="font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary-ink">{t(locale, "home.features.f1.kicker")}</span>
|
||||||
<h3 class="mt-3 font-display text-2xl font-bold tracking-tight sm:text-3xl">
|
<h3 class="mt-3 font-display text-2xl font-bold tracking-tight sm:text-3xl">
|
||||||
{t(locale, "home.features.f1.title")}
|
{t(locale, "home.features.f1.title")}
|
||||||
</h3>
|
</h3>
|
||||||
@@ -70,7 +70,7 @@ const aiPillColors = [
|
|||||||
<div class="rounded-xl border border-border bg-surface p-6 md:p-8">
|
<div class="rounded-xl border border-border bg-surface p-6 md:p-8">
|
||||||
<!-- Outer infrastructure box -->
|
<!-- Outer infrastructure box -->
|
||||||
<div class="rounded-lg border-2 border-dashed border-primary/30 p-5">
|
<div class="rounded-lg border-2 border-dashed border-primary/30 p-5">
|
||||||
<span class="mb-4 block text-xs font-semibold uppercase tracking-wider text-primary">{t(locale, "home.features.f1.yourInfra")}</span>
|
<span class="mb-4 block text-xs font-semibold uppercase tracking-wider text-primary-ink">{t(locale, "home.features.f1.yourInfra")}</span>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div class="flex flex-col items-center gap-2 rounded-lg bg-primary-subtle p-4">
|
<div class="flex flex-col items-center gap-2 rounded-lg bg-primary-subtle p-4">
|
||||||
<svg class="h-8 w-8 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
<svg class="h-8 w-8 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
@@ -78,14 +78,14 @@ const aiPillColors = [
|
|||||||
<path d="M8 12h8M8 16h5" />
|
<path d="M8 12h8M8 16h5" />
|
||||||
<circle cx="18" cy="7" r="1" fill="currentColor" />
|
<circle cx="18" cy="7" r="1" fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
<span class="text-xs font-semibold text-primary">{t(locale, "home.features.f1.snapotter")}</span>
|
<span class="text-xs font-semibold text-primary-ink">{t(locale, "home.features.f1.snapotter")}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-center gap-2 rounded-lg bg-primary-subtle p-4">
|
<div class="flex flex-col items-center gap-2 rounded-lg bg-primary-subtle p-4">
|
||||||
<svg class="h-8 w-8 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
<svg class="h-8 w-8 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z" />
|
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z" />
|
||||||
<polyline points="13 2 13 9 20 9" />
|
<polyline points="13 2 13 9 20 9" />
|
||||||
</svg>
|
</svg>
|
||||||
<span class="text-xs font-semibold text-primary">{t(locale, "home.features.f1.yourFiles")}</span>
|
<span class="text-xs font-semibold text-primary-ink">{t(locale, "home.features.f1.yourFiles")}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Internal arrows -->
|
<!-- Internal arrows -->
|
||||||
@@ -113,7 +113,7 @@ const aiPillColors = [
|
|||||||
<!-- Feature 2: REST API — visual left, text right -->
|
<!-- Feature 2: REST API — visual left, text right -->
|
||||||
<div class="reveal grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-16 items-center">
|
<div class="reveal grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-16 items-center">
|
||||||
<div class="md:order-2">
|
<div class="md:order-2">
|
||||||
<span class="font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary">{t(locale, "home.features.f2.kicker")}</span>
|
<span class="font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary-ink">{t(locale, "home.features.f2.kicker")}</span>
|
||||||
<h3 class="mt-3 font-display text-2xl font-bold tracking-tight sm:text-3xl">
|
<h3 class="mt-3 font-display text-2xl font-bold tracking-tight sm:text-3xl">
|
||||||
{t(locale, "home.features.f2.title")}
|
{t(locale, "home.features.f2.title")}
|
||||||
</h3>
|
</h3>
|
||||||
@@ -138,7 +138,7 @@ const aiPillColors = [
|
|||||||
<!-- Feature 3: AI Models — text left, visual right -->
|
<!-- Feature 3: AI Models — text left, visual right -->
|
||||||
<div class="reveal grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-16 items-center">
|
<div class="reveal grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-16 items-center">
|
||||||
<div class="md:order-1">
|
<div class="md:order-1">
|
||||||
<span class="font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary">{t(locale, "home.features.f3.kicker")}</span>
|
<span class="font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary-ink">{t(locale, "home.features.f3.kicker")}</span>
|
||||||
<h3 class="mt-3 font-display text-2xl font-bold tracking-tight sm:text-3xl">
|
<h3 class="mt-3 font-display text-2xl font-bold tracking-tight sm:text-3xl">
|
||||||
{t(locale, "home.features.f3.title")}
|
{t(locale, "home.features.f3.title")}
|
||||||
</h3>
|
</h3>
|
||||||
@@ -160,7 +160,7 @@ const aiPillColors = [
|
|||||||
<!-- Feature 4: Deployment — visual left, text right -->
|
<!-- Feature 4: Deployment — visual left, text right -->
|
||||||
<div class="reveal grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-16 items-center">
|
<div class="reveal grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-16 items-center">
|
||||||
<div class="md:order-2">
|
<div class="md:order-2">
|
||||||
<span class="font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary">{t(locale, "home.features.f4.kicker")}</span>
|
<span class="font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary-ink">{t(locale, "home.features.f4.kicker")}</span>
|
||||||
<h3 class="mt-3 font-display text-2xl font-bold tracking-tight sm:text-3xl">
|
<h3 class="mt-3 font-display text-2xl font-bold tracking-tight sm:text-3xl">
|
||||||
{t(locale, "home.features.f4.title")}
|
{t(locale, "home.features.f4.title")}
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const trustBadges = [
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- Subtitle: capability breadth plus the infrastructure surfaces -->
|
<!-- Subtitle: capability breadth plus the infrastructure surfaces -->
|
||||||
<p class="animate-fade-up mx-auto mt-6 max-w-2xl text-lg text-white/90 md:text-xl" style="animation-delay: 0.15s; text-shadow: 0 1px 16px rgba(90, 20, 0, 0.22);">
|
<p class="animate-fade-up mx-auto mt-6 max-w-2xl text-lg text-white md:text-xl" style="animation-delay: 0.15s; text-shadow: 0 1px 16px rgba(90, 20, 0, 0.22);">
|
||||||
{t(locale, "home.hero.subtitle")}
|
{t(locale, "home.hero.subtitle")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const DOCKER_CMD =
|
|||||||
---
|
---
|
||||||
|
|
||||||
<div class="animate-fade-up mt-6 text-center" style="animation-delay: 0.24s;">
|
<div class="animate-fade-up mt-6 text-center" style="animation-delay: 0.24s;">
|
||||||
<p class="mb-3 font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary">
|
<p class="mb-3 font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary-ink">
|
||||||
{t(locale, "home.quickstart.label")}
|
{t(locale, "home.quickstart.label")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ const { locale = "en" } = Astro.props;
|
|||||||
// Tool name/description come from shared i18n (translated in all 21 langs).
|
// Tool name/description come from shared i18n (translated in all 21 langs).
|
||||||
const toolStrings = await loadToolStrings(locale);
|
const toolStrings = await loadToolStrings(locale);
|
||||||
|
|
||||||
const MODALITY_META: Record<string, { label: string; color: string }> = {
|
const MODALITY_META: Record<string, { label: string; color: string; ink: string }> = {
|
||||||
image: { label: "Image", color: "#E07832" },
|
image: { label: "Image", color: "#E07832", ink: "#A85518" },
|
||||||
video: { label: "Video", color: "#BE4A3C" },
|
video: { label: "Video", color: "#BE4A3C", ink: "#B54639" },
|
||||||
audio: { label: "Audio", color: "#2C7A75" },
|
audio: { label: "Audio", color: "#2C7A75", ink: "#2B7672" },
|
||||||
document: { label: "PDF", color: "#44568C" },
|
document: { label: "PDF", color: "#44568C", ink: "#44568C" },
|
||||||
file: { label: "Files", color: "#5C8642" },
|
file: { label: "Files", color: "#5C8642", ink: "#51763A" },
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderIcon(iconName: string): string {
|
function renderIcon(iconName: string): string {
|
||||||
@@ -89,7 +89,7 @@ function renderIcon(iconName: string): string {
|
|||||||
<span class="flex-1 text-sm font-medium text-foreground">{toolName}</span>
|
<span class="flex-1 text-sm font-medium text-foreground">{toolName}</span>
|
||||||
<span
|
<span
|
||||||
class="shrink-0 rounded-full px-2 py-0.5 text-[11px] font-semibold"
|
class="shrink-0 rounded-full px-2 py-0.5 text-[11px] font-semibold"
|
||||||
style={`background-color: ${mod?.color}1a; color: ${mod?.color}`}
|
style={`background-color: ${mod?.color}1a; color: ${mod?.ink}`}
|
||||||
>
|
>
|
||||||
{mod?.label}
|
{mod?.label}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const CONTRIBUTE_URL = "https://docs.snapotter.com/guide/translations";
|
|||||||
hidden
|
hidden
|
||||||
>
|
>
|
||||||
<span>{t(locale, "banner.machineTranslated")} </span>
|
<span>{t(locale, "banner.machineTranslated")} </span>
|
||||||
<a href={CONTRIBUTE_URL} target="_blank" rel="noopener noreferrer" class="font-semibold text-primary-dark underline">
|
<a href={CONTRIBUTE_URL} target="_blank" rel="noopener noreferrer" class="font-semibold text-primary-ink underline">
|
||||||
{t(locale, "banner.machineTranslated")}
|
{t(locale, "banner.machineTranslated")}
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const checkIcon =
|
|||||||
<div class="pointer-events-none absolute inset-x-0 top-0 h-32 bg-gradient-to-b from-primary/[0.04] to-transparent"></div>
|
<div class="pointer-events-none absolute inset-x-0 top-0 h-32 bg-gradient-to-b from-primary/[0.04] to-transparent"></div>
|
||||||
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<span class="mb-4 inline-block rounded-full bg-primary/10 px-3 py-1 text-xs font-semibold text-primary">
|
<span class="mb-4 inline-block rounded-full bg-primary/10 px-3 py-1 text-xs font-semibold text-primary-ink">
|
||||||
{t(locale, "home.pricing.oss.badge")}
|
{t(locale, "home.pricing.oss.badge")}
|
||||||
</span>
|
</span>
|
||||||
<h3 class="font-display text-2xl font-bold">{t(locale, "home.pricing.oss.name")}</h3>
|
<h3 class="font-display text-2xl font-bold">{t(locale, "home.pricing.oss.name")}</h3>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const blocks = [
|
|||||||
<div class="not-prose space-y-5">
|
<div class="not-prose space-y-5">
|
||||||
{blocks.map((b) => (
|
{blocks.map((b) => (
|
||||||
<div>
|
<div>
|
||||||
<p class="mb-2 font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary">
|
<p class="mb-2 font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary-ink">
|
||||||
{b.label}
|
{b.label}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -254,12 +254,12 @@ const safeStringsJson = JSON.stringify(toolGridStrings).replace(/</g, "\\u003c")
|
|||||||
class:list={[
|
class:list={[
|
||||||
"tool-command-modality rounded-md px-3 py-2 text-sm font-semibold transition-colors",
|
"tool-command-modality rounded-md px-3 py-2 text-sm font-semibold transition-colors",
|
||||||
modality.id === "all"
|
modality.id === "all"
|
||||||
? "bg-primary text-white shadow-sm"
|
? "bg-primary text-foreground shadow-sm"
|
||||||
: "text-muted hover:bg-background-alt hover:text-foreground",
|
: "text-muted hover:bg-background-alt hover:text-foreground",
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<span>{modality.label}</span>
|
<span>{modality.label}</span>
|
||||||
<span class="ms-1 text-xs opacity-75">{modality.count}</span>
|
<span class="ms-1 text-xs">{modality.count}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -273,7 +273,7 @@ const safeStringsJson = JSON.stringify(toolGridStrings).replace(/</g, "\\u003c")
|
|||||||
<button
|
<button
|
||||||
id="tool-command-reset-workflow"
|
id="tool-command-reset-workflow"
|
||||||
type="button"
|
type="button"
|
||||||
class="hidden rounded-md px-2 py-1 text-xs font-semibold text-primary transition-colors hover:bg-primary-subtle"
|
class="hidden rounded-md px-2 py-1 text-xs font-semibold text-primary-ink transition-colors hover:bg-primary-subtle"
|
||||||
>
|
>
|
||||||
{t(locale, "home.toolGrid.reset")}
|
{t(locale, "home.toolGrid.reset")}
|
||||||
</button>
|
</button>
|
||||||
@@ -304,7 +304,7 @@ const safeStringsJson = JSON.stringify(toolGridStrings).replace(/</g, "\\u003c")
|
|||||||
</span>
|
</span>
|
||||||
<span class="min-w-0">
|
<span class="min-w-0">
|
||||||
<span class="block text-sm font-semibold text-foreground">{workflow.label}</span>
|
<span class="block text-sm font-semibold text-foreground">{workflow.label}</span>
|
||||||
<span class="mt-0.5 block text-xs font-medium text-primary-dark">{workflow.metric}</span>
|
<span class="mt-0.5 block text-xs font-medium text-primary-ink">{workflow.metric}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="mt-2 block text-xs leading-relaxed text-muted">{workflow.description}</span>
|
<span class="mt-2 block text-xs leading-relaxed text-muted">{workflow.description}</span>
|
||||||
@@ -411,13 +411,13 @@ const safeStringsJson = JSON.stringify(toolGridStrings).replace(/</g, "\\u003c")
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
id="tool-command-request"
|
id="tool-command-request"
|
||||||
class="font-semibold text-primary hover:underline"
|
class="font-semibold text-primary-ink hover:underline"
|
||||||
>
|
>
|
||||||
{t(locale, "home.toolGrid.requestTool")}
|
{t(locale, "home.toolGrid.requestTool")}
|
||||||
</a>
|
</a>
|
||||||
{t(locale, "home.toolGrid.requestSuffix")}
|
{t(locale, "home.toolGrid.requestSuffix")}
|
||||||
</div>
|
</div>
|
||||||
<a href={localizeHref(locale, "/tools")} class="inline-flex items-center gap-2 text-sm font-semibold text-primary hover:underline">
|
<a href={localizeHref(locale, "/tools")} class="inline-flex items-center gap-2 text-sm font-semibold text-primary-ink hover:underline">
|
||||||
{t(locale, "home.toolGrid.browseCatalog")}
|
{t(locale, "home.toolGrid.browseCatalog")}
|
||||||
<svg
|
<svg
|
||||||
class="h-4 w-4"
|
class="h-4 w-4"
|
||||||
@@ -545,7 +545,7 @@ const safeStringsJson = JSON.stringify(toolGridStrings).replace(/</g, "\\u003c")
|
|||||||
const reason = formatReason(result.reason);
|
const reason = formatReason(result.reason);
|
||||||
const reasonHtml =
|
const reasonHtml =
|
||||||
showReason && reason
|
showReason && reason
|
||||||
? `<span class="mt-3 inline-flex w-fit rounded-md bg-primary-subtle px-2 py-1 text-[11px] font-semibold text-primary-dark">${escapeHtml(reason)}</span>`
|
? `<span class="mt-3 inline-flex w-fit rounded-md bg-primary-subtle px-2 py-1 text-[11px] font-semibold text-primary-ink">${escapeHtml(reason)}</span>`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@@ -580,7 +580,7 @@ const safeStringsJson = JSON.stringify(toolGridStrings).replace(/</g, "\\u003c")
|
|||||||
const isActive = button.dataset.modality === activeModality;
|
const isActive = button.dataset.modality === activeModality;
|
||||||
button.setAttribute("aria-pressed", String(isActive));
|
button.setAttribute("aria-pressed", String(isActive));
|
||||||
button.classList.toggle("bg-primary", isActive);
|
button.classList.toggle("bg-primary", isActive);
|
||||||
button.classList.toggle("text-white", isActive);
|
button.classList.toggle("text-foreground", isActive);
|
||||||
button.classList.toggle("shadow-sm", isActive);
|
button.classList.toggle("shadow-sm", isActive);
|
||||||
button.classList.toggle("text-muted", !isActive);
|
button.classList.toggle("text-muted", !isActive);
|
||||||
button.classList.toggle("hover:bg-background-alt", !isActive);
|
button.classList.toggle("hover:bg-background-alt", !isActive);
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const stats = [
|
|||||||
{stats.map((stat) => {
|
{stats.map((stat) => {
|
||||||
const inner = (
|
const inner = (
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<p class="font-display text-5xl font-extrabold tracking-tight text-primary md:text-6xl">
|
<p class="font-display text-5xl font-extrabold tracking-tight text-primary-ink md:text-6xl">
|
||||||
{stat.value}
|
{stat.value}
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2 text-sm font-semibold text-foreground">{stat.label}</p>
|
<p class="mt-2 text-sm font-semibold text-foreground">{stat.label}</p>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const locale = "en";
|
|||||||
|
|
||||||
<main id="main" class="flex min-h-[60vh] flex-col items-center justify-center px-6 pt-32 pb-20 text-center">
|
<main id="main" class="flex min-h-[60vh] flex-col items-center justify-center px-6 pt-32 pb-20 text-center">
|
||||||
<div class="reveal">
|
<div class="reveal">
|
||||||
<p class="text-6xl font-bold text-primary">{t(locale, "notFound.code")}</p>
|
<p class="text-6xl font-bold text-primary-ink">{t(locale, "notFound.code")}</p>
|
||||||
<h1 class="mt-4 font-display text-3xl font-bold tracking-tight md:text-4xl">
|
<h1 class="mt-4 font-display text-3xl font-bold tracking-tight md:text-4xl">
|
||||||
{t(locale, "notFound.title")}
|
{t(locale, "notFound.title")}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ const allJsonLd = [breadcrumbJsonLd, ...(faqJsonLd ? [faqJsonLd] : [])];
|
|||||||
<!-- Hero -->
|
<!-- Hero -->
|
||||||
<section class="mx-auto max-w-4xl px-6 pt-8 pb-12">
|
<section class="mx-auto max-w-4xl px-6 pt-8 pb-12">
|
||||||
<div class="reveal">
|
<div class="reveal">
|
||||||
<span class="rounded-full border border-primary/20 bg-primary/5 px-3 py-1 text-xs font-semibold text-primary-dark">
|
<span class="rounded-full border border-primary/20 bg-primary/5 px-3 py-1 text-xs font-semibold text-primary-ink">
|
||||||
{alt.category}
|
{alt.category}
|
||||||
</span>
|
</span>
|
||||||
<h1 class="mt-5 font-display text-4xl font-bold tracking-tight md:text-5xl">
|
<h1 class="mt-5 font-display text-4xl font-bold tracking-tight md:text-5xl">
|
||||||
@@ -160,7 +160,7 @@ const allJsonLd = [breadcrumbJsonLd, ...(faqJsonLd ? [faqJsonLd] : [])];
|
|||||||
<thead class="border-b border-border bg-background-alt">
|
<thead class="border-b border-border bg-background-alt">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-5 py-4 text-start font-semibold"> </th>
|
<th class="px-5 py-4 text-start font-semibold"> </th>
|
||||||
<th class="px-5 py-4 text-start font-semibold text-primary-dark">SnapOtter</th>
|
<th class="px-5 py-4 text-start font-semibold text-primary-ink">SnapOtter</th>
|
||||||
<th class="px-5 py-4 text-start font-semibold">{alt.competitor}</th>
|
<th class="px-5 py-4 text-start font-semibold">{alt.competitor}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -196,7 +196,7 @@ const allJsonLd = [breadcrumbJsonLd, ...(faqJsonLd ? [faqJsonLd] : [])];
|
|||||||
href={source.url}
|
href={source.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="text-primary transition-colors hover:text-primary-dark"
|
class="text-primary-ink transition-colors hover:text-primary-ink-strong"
|
||||||
>
|
>
|
||||||
{source.label}
|
{source.label}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ const formatReviewDate = (date: string) =>
|
|||||||
href={localizeHref(locale, `/alternatives/${alt.slug}`)}
|
href={localizeHref(locale, `/alternatives/${alt.slug}`)}
|
||||||
class="group flex flex-col rounded-xl border border-border bg-surface p-6 transition-all hover:border-primary hover:shadow-md"
|
class="group flex flex-col rounded-xl border border-border bg-surface p-6 transition-all hover:border-primary hover:shadow-md"
|
||||||
>
|
>
|
||||||
<span class="text-xs font-semibold text-primary-dark">{alt.category}</span>
|
<span class="text-xs font-semibold text-primary-ink">{alt.category}</span>
|
||||||
<span class="mt-2 font-display text-lg font-bold tracking-tight">
|
<span class="mt-2 font-display text-lg font-bold tracking-tight">
|
||||||
{alt.competitorOpenSource
|
{alt.competitorOpenSource
|
||||||
? t(locale, "altIndex.card.versus", { competitor: alt.competitor })
|
? t(locale, "altIndex.card.versus", { competitor: alt.competitor })
|
||||||
@@ -103,7 +103,7 @@ const formatReviewDate = (date: string) =>
|
|||||||
<span class="mt-4 text-xs text-muted">
|
<span class="mt-4 text-xs text-muted">
|
||||||
{t(locale, "altIndex.card.reviewed", { date: formatReviewDate(alt.lastReviewed) })}
|
{t(locale, "altIndex.card.reviewed", { date: formatReviewDate(alt.lastReviewed) })}
|
||||||
</span>
|
</span>
|
||||||
<span class="mt-auto inline-flex items-center gap-1 pt-5 text-sm font-medium text-primary transition-all group-hover:gap-2">
|
<span class="mt-auto inline-flex items-center gap-1 pt-5 text-sm font-medium text-primary-ink transition-all group-hover:gap-2">
|
||||||
{t(locale, "altIndex.card.compare")}
|
{t(locale, "altIndex.card.compare")}
|
||||||
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M5 12h14" /><path d="m12 5 7 7-7 7" />
|
<path d="M5 12h14" /><path d="m12 5 7 7-7 7" />
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ const subjects = [
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="mt-10 text-sm text-muted">
|
<p class="mt-10 text-sm text-muted">
|
||||||
{t(locale, "contact.emailPrefix")} <a href="mailto:contact@snapotter.com" class="font-medium text-primary hover:underline">contact@snapotter.com</a>
|
{t(locale, "contact.emailPrefix")} <a href="mailto:contact@snapotter.com" class="font-medium text-primary-ink hover:underline">contact@snapotter.com</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -96,10 +96,10 @@ const permissionMatrix = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const storageProviders = [
|
const storageProviders = [
|
||||||
{ name: t(locale, "enterprise.storage.provider.awsS3"), color: "text-[#FF9900]" },
|
{ name: t(locale, "enterprise.storage.provider.awsS3"), color: "text-[#996306]" },
|
||||||
{ name: t(locale, "enterprise.storage.provider.minio"), color: "text-[#C72C48]" },
|
{ name: t(locale, "enterprise.storage.provider.minio"), color: "text-[#C72C48]" },
|
||||||
{ name: t(locale, "enterprise.storage.provider.r2"), color: "text-[#F6821F]" },
|
{ name: t(locale, "enterprise.storage.provider.r2"), color: "text-[#B45309]" },
|
||||||
{ name: t(locale, "enterprise.storage.provider.local"), color: "text-primary" },
|
{ name: t(locale, "enterprise.storage.provider.local"), color: "text-primary-ink" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const deploymentTargets = [
|
const deploymentTargets = [
|
||||||
@@ -273,10 +273,10 @@ const xSvg =
|
|||||||
</div>
|
</div>
|
||||||
<div class="md:order-2">
|
<div class="md:order-2">
|
||||||
<div class="rounded-xl border border-border bg-surface p-6">
|
<div class="rounded-xl border border-border bg-surface p-6">
|
||||||
<span class="mb-4 block text-xs font-semibold uppercase tracking-wider text-primary">{t(locale, "enterprise.identity.supportedProviders")}</span>
|
<span class="mb-4 block text-xs font-semibold uppercase tracking-wider text-primary-ink">{t(locale, "enterprise.identity.supportedProviders")}</span>
|
||||||
<div class="flex flex-wrap gap-2.5">
|
<div class="flex flex-wrap gap-2.5">
|
||||||
{identityProviders.map((provider) => (
|
{identityProviders.map((provider) => (
|
||||||
<span class="inline-flex items-center gap-1.5 rounded-full border border-primary/20 bg-primary-subtle px-3.5 py-1.5 text-sm font-medium text-primary-dark">
|
<span class="inline-flex items-center gap-1.5 rounded-full border border-primary/20 bg-primary-subtle px-3.5 py-1.5 text-sm font-medium text-primary-ink">
|
||||||
<svg class="h-3.5 w-3.5 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
<svg class="h-3.5 w-3.5 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||||||
{provider}
|
{provider}
|
||||||
</span>
|
</span>
|
||||||
@@ -286,7 +286,7 @@ const xSvg =
|
|||||||
<svg class="h-4 w-4 shrink-0 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg class="h-4 w-4 shrink-0 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="text-xs text-primary-dark">{t(locale, "enterprise.identity.mfaEnforced")}</span>
|
<span class="text-xs text-primary-ink">{t(locale, "enterprise.identity.mfaEnforced")}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -304,7 +304,7 @@ const xSvg =
|
|||||||
</div>
|
</div>
|
||||||
<div class="md:order-1">
|
<div class="md:order-1">
|
||||||
<div class="rounded-xl border border-border bg-surface p-6 overflow-x-auto">
|
<div class="rounded-xl border border-border bg-surface p-6 overflow-x-auto">
|
||||||
<span class="mb-4 block text-xs font-semibold uppercase tracking-wider text-primary">{t(locale, "enterprise.permissions.matrix")}</span>
|
<span class="mb-4 block text-xs font-semibold uppercase tracking-wider text-primary-ink">{t(locale, "enterprise.permissions.matrix")}</span>
|
||||||
<table class="w-full text-xs">
|
<table class="w-full text-xs">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -407,7 +407,7 @@ const xSvg =
|
|||||||
<th class="pb-4 text-start text-sm font-semibold text-muted"></th>
|
<th class="pb-4 text-start text-sm font-semibold text-muted"></th>
|
||||||
<th class="pb-4 text-start text-sm font-semibold text-muted">{t(locale, "enterprise.comparison.cloudHeader")}</th>
|
<th class="pb-4 text-start text-sm font-semibold text-muted">{t(locale, "enterprise.comparison.cloudHeader")}</th>
|
||||||
<th class="pb-4 text-start text-sm font-semibold">
|
<th class="pb-4 text-start text-sm font-semibold">
|
||||||
<span class="text-primary">SnapOtter</span> <span class="text-muted font-normal">{t(locale, "enterprise.comparison.selfHostedHeader")}</span>
|
<span class="text-primary-ink">SnapOtter</span> <span class="text-muted font-normal">{t(locale, "enterprise.comparison.selfHostedHeader")}</span>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ const breadcrumbJsonLd = {
|
|||||||
{t(locale, "privacy.contact.pre")}
|
{t(locale, "privacy.contact.pre")}
|
||||||
<a
|
<a
|
||||||
href="mailto:contact@snapotter.com"
|
href="mailto:contact@snapotter.com"
|
||||||
class="text-primary font-medium hover:underline underline-offset-4"
|
class="text-primary-ink font-medium hover:underline underline-offset-4"
|
||||||
>
|
>
|
||||||
contact@snapotter.com
|
contact@snapotter.com
|
||||||
</a>.
|
</a>.
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ const breadcrumbJsonLd = {
|
|||||||
{t(locale, "terms.license.item4.pre")}
|
{t(locale, "terms.license.item4.pre")}
|
||||||
<a
|
<a
|
||||||
href="mailto:contact@snapotter.com"
|
href="mailto:contact@snapotter.com"
|
||||||
class="text-primary font-medium hover:underline underline-offset-4"
|
class="text-primary-ink font-medium hover:underline underline-offset-4"
|
||||||
>
|
>
|
||||||
contact@snapotter.com
|
contact@snapotter.com
|
||||||
</a>
|
</a>
|
||||||
@@ -134,7 +134,7 @@ const breadcrumbJsonLd = {
|
|||||||
{t(locale, "terms.contact.pre")}
|
{t(locale, "terms.contact.pre")}
|
||||||
<a
|
<a
|
||||||
href="mailto:contact@snapotter.com"
|
href="mailto:contact@snapotter.com"
|
||||||
class="text-primary font-medium hover:underline underline-offset-4"
|
class="text-primary-ink font-medium hover:underline underline-offset-4"
|
||||||
>
|
>
|
||||||
contact@snapotter.com
|
contact@snapotter.com
|
||||||
</a>.
|
</a>.
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ const toolDesc = (id: string, fallback: string) => toolStrings[id]?.description
|
|||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<span class="text-sm font-bold">{toolName(tool.id, tool.name)}</span>
|
<span class="text-sm font-bold">{toolName(tool.id, tool.name)}</span>
|
||||||
<p class="mt-1 line-clamp-2 text-xs leading-relaxed text-muted">{toolDesc(tool.id, tool.description)}</p>
|
<p class="mt-1 line-clamp-2 text-xs leading-relaxed text-muted">{toolDesc(tool.id, tool.description)}</p>
|
||||||
<span class="mt-2 inline-flex items-center gap-1 text-xs font-medium text-primary transition-all group-hover:gap-2">
|
<span class="mt-2 inline-flex items-center gap-1 text-xs font-medium text-primary-ink transition-all group-hover:gap-2">
|
||||||
{t(locale, "common.learnMore")}
|
{t(locale, "common.learnMore")}
|
||||||
<svg class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M5 12h14" /><path d="m12 5 7 7-7 7" />
|
<path d="M5 12h14" /><path d="m12 5 7 7-7 7" />
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ const collectionJsonLd = {
|
|||||||
data-modality={m.id}
|
data-modality={m.id}
|
||||||
class:list={[
|
class:list={[
|
||||||
"tools-mod-btn shrink-0 rounded-full px-4 py-1.5 text-sm font-medium transition-colors",
|
"tools-mod-btn shrink-0 rounded-full px-4 py-1.5 text-sm font-medium transition-colors",
|
||||||
m.id === "all" ? "bg-primary text-white" : "border border-border bg-surface hover:bg-background-alt",
|
m.id === "all" ? "bg-primary text-foreground" : "border border-border bg-surface hover:bg-background-alt",
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{m.label} ({m.count})
|
{m.label} ({m.count})
|
||||||
@@ -263,7 +263,7 @@ const collectionJsonLd = {
|
|||||||
<p class="mt-1 line-clamp-2 text-xs leading-relaxed text-muted">
|
<p class="mt-1 line-clamp-2 text-xs leading-relaxed text-muted">
|
||||||
{toolDesc(tool.id, tool.description)}
|
{toolDesc(tool.id, tool.description)}
|
||||||
</p>
|
</p>
|
||||||
<span class="mt-2 inline-flex items-center gap-1 text-xs font-medium text-primary transition-all group-hover:gap-2">
|
<span class="mt-2 inline-flex items-center gap-1 text-xs font-medium text-primary-ink transition-all group-hover:gap-2">
|
||||||
{t(locale, "common.learnMore")}
|
{t(locale, "common.learnMore")}
|
||||||
<svg class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M5 12h14" /><path d="m12 5 7 7-7 7" />
|
<path d="M5 12h14" /><path d="m12 5 7 7-7 7" />
|
||||||
@@ -341,7 +341,7 @@ const collectionJsonLd = {
|
|||||||
const on = b.getAttribute("data-modality") === activeMod;
|
const on = b.getAttribute("data-modality") === activeMod;
|
||||||
b.className =
|
b.className =
|
||||||
"tools-mod-btn shrink-0 rounded-full px-4 py-1.5 text-sm font-medium transition-colors " +
|
"tools-mod-btn shrink-0 rounded-full px-4 py-1.5 text-sm font-medium transition-colors " +
|
||||||
(on ? "bg-primary text-white" : "border border-border bg-surface hover:bg-background-alt");
|
(on ? "bg-primary text-foreground" : "border border-border bg-surface hover:bg-background-alt");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ const allJsonLd = [breadcrumbJsonLd, ...(faqJsonLd ? [faqJsonLd] : [])];
|
|||||||
<h1 class="font-display text-4xl font-bold tracking-tight md:text-5xl">{uc.h1}</h1>
|
<h1 class="font-display text-4xl font-bold tracking-tight md:text-5xl">{uc.h1}</h1>
|
||||||
<p class="mt-4 text-lg leading-relaxed text-muted md:text-xl">{uc.intro}</p>
|
<p class="mt-4 text-lg leading-relaxed text-muted md:text-xl">{uc.intro}</p>
|
||||||
{uc.isAiBundle && (
|
{uc.isAiBundle && (
|
||||||
<p class="mt-4 inline-flex rounded-full border border-primary/20 bg-primary/5 px-3 py-1 text-xs font-semibold text-primary-dark">
|
<p class="mt-4 inline-flex rounded-full border border-primary/20 bg-primary/5 px-3 py-1 text-xs font-semibold text-primary-ink">
|
||||||
Local AI, one-time bundle install
|
Local AI, one-time bundle install
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -119,12 +119,12 @@ const allJsonLd = [breadcrumbJsonLd, ...(faqJsonLd ? [faqJsonLd] : [])];
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-6 flex flex-wrap gap-x-6 gap-y-2 text-sm">
|
<div class="mt-6 flex flex-wrap gap-x-6 gap-y-2 text-sm">
|
||||||
{uc.toolRoutes.map((route, i) => (
|
{uc.toolRoutes.map((route, i) => (
|
||||||
<a href={route} class="text-primary transition-colors hover:text-primary-dark">
|
<a href={route} class="text-primary-ink transition-colors hover:text-primary-ink-strong">
|
||||||
Open the {uc.toolIds[i]} tool
|
Open the {uc.toolIds[i]} tool
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
{alt && (
|
{alt && (
|
||||||
<a href={`/alternatives/${alt.slug}`} class="text-primary transition-colors hover:text-primary-dark">
|
<a href={`/alternatives/${alt.slug}`} class="text-primary-ink transition-colors hover:text-primary-ink-strong">
|
||||||
Compare with {alt.competitor}
|
Compare with {alt.competitor}
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
@@ -135,7 +135,7 @@ const allJsonLd = [breadcrumbJsonLd, ...(faqJsonLd ? [faqJsonLd] : [])];
|
|||||||
{uc.sources.map((s, i) => (
|
{uc.sources.map((s, i) => (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{i > 0 && ", "}
|
{i > 0 && ", "}
|
||||||
<a href={s.url} target="_blank" rel="noopener noreferrer" class="text-primary hover:text-primary-dark">{s.label}</a>
|
<a href={s.url} target="_blank" rel="noopener noreferrer" class="text-primary-ink hover:text-primary-ink-strong">{s.label}</a>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const description =
|
|||||||
<a href={`/self-hosted/${uc.slug}`} class="group flex flex-col rounded-xl border border-border bg-surface p-6 transition-all hover:border-primary hover:shadow-md">
|
<a href={`/self-hosted/${uc.slug}`} class="group flex flex-col rounded-xl border border-border bg-surface p-6 transition-all hover:border-primary hover:shadow-md">
|
||||||
<span class="font-display text-lg font-bold">{uc.h1}</span>
|
<span class="font-display text-lg font-bold">{uc.h1}</span>
|
||||||
<span class="mt-2 text-sm text-muted">{uc.intro}</span>
|
<span class="mt-2 text-sm text-muted">{uc.intro}</span>
|
||||||
<span class="mt-4 text-xs font-semibold text-primary">Read more →</span>
|
<span class="mt-4 text-xs font-semibold text-primary-ink">Read more →</span>
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ const allJsonLd = [breadcrumbJsonLd, softwareJsonLd];
|
|||||||
/>
|
/>
|
||||||
<span class="mt-3 text-sm font-bold">{t.name}</span>
|
<span class="mt-3 text-sm font-bold">{t.name}</span>
|
||||||
<p class="mt-1.5 text-xs leading-relaxed text-muted">{t.description}</p>
|
<p class="mt-1.5 text-xs leading-relaxed text-muted">{t.description}</p>
|
||||||
<span class="mt-auto inline-flex items-center gap-1 pt-4 text-xs font-medium text-primary transition-all group-hover:gap-2">
|
<span class="mt-auto inline-flex items-center gap-1 pt-4 text-xs font-medium text-primary-ink transition-all group-hover:gap-2">
|
||||||
Learn more
|
Learn more
|
||||||
<svg class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M5 12h14" /><path d="m12 5 7 7-7 7" />
|
<path d="M5 12h14" /><path d="m12 5 7 7-7 7" />
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ html {
|
|||||||
--color-primary-light: #F09550;
|
--color-primary-light: #F09550;
|
||||||
--color-primary-dark: #C06520;
|
--color-primary-dark: #C06520;
|
||||||
--color-primary-subtle: #FFF5ED;
|
--color-primary-subtle: #FFF5ED;
|
||||||
|
--color-primary-ink: #A85518;
|
||||||
|
--color-primary-ink-strong: #8F4712;
|
||||||
|
|
||||||
/* Neutrals -- Warm Cream & Stone (sandy riverbank) */
|
/* Neutrals -- Warm Cream & Stone (sandy riverbank) */
|
||||||
--color-background: #FAFAF7;
|
--color-background: #FAFAF7;
|
||||||
@@ -59,8 +61,8 @@ html {
|
|||||||
--color-terminal-green: #4ADE80;
|
--color-terminal-green: #4ADE80;
|
||||||
|
|
||||||
/* Semantic */
|
/* Semantic */
|
||||||
--color-success: #2D9D78;
|
--color-success: #1E7B5C;
|
||||||
--color-danger: #D44C47;
|
--color-danger: #BE3A35;
|
||||||
|
|
||||||
/* Fonts */
|
/* Fonts */
|
||||||
--font-display: "Bricolage Grotesque", system-ui, sans-serif;
|
--font-display: "Bricolage Grotesque", system-ui, sans-serif;
|
||||||
@@ -281,7 +283,7 @@ code, pre, kbd {
|
|||||||
/* ─── BUTTONS ─── */
|
/* ─── BUTTONS ─── */
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-light) 100%);
|
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-light) 100%);
|
||||||
color: white;
|
color: var(--color-foreground);
|
||||||
transition: box-shadow 0.2s, transform 0.1s;
|
transition: box-shadow 0.2s, transform 0.1s;
|
||||||
}
|
}
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
@@ -318,7 +320,7 @@ code, pre, kbd {
|
|||||||
.code-block .code-string { color: #C3E88D; }
|
.code-block .code-string { color: #C3E88D; }
|
||||||
.code-block .code-flag { color: #82AAFF; }
|
.code-block .code-flag { color: #82AAFF; }
|
||||||
.code-block .code-url { color: #89DDFF; }
|
.code-block .code-url { color: #89DDFF; }
|
||||||
.code-block .code-comment { color: #546E7A; }
|
.code-block .code-comment { color: #7E99A8; }
|
||||||
|
|
||||||
/* ─── SECTION DIVIDER ─── */
|
/* ─── SECTION DIVIDER ─── */
|
||||||
.section-divider {
|
.section-divider {
|
||||||
@@ -343,7 +345,7 @@ code, pre, kbd {
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
background: var(--color-primary);
|
background: var(--color-primary);
|
||||||
color: white;
|
color: var(--color-foreground);
|
||||||
border-radius: 0 0 0.5rem 0.5rem;
|
border-radius: 0 0 0.5rem 0.5rem;
|
||||||
transition: top 0.2s;
|
transition: top 0.2s;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function TemplateCard({ template, onUse }: TemplateCardProps) {
|
|||||||
className="text-start"
|
className="text-start"
|
||||||
data-testid={`template-use-${template.id}`}
|
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 ? (
|
{description ? (
|
||||||
<span className="block text-[11px] text-muted-foreground mt-0.5">{description}</span>
|
<span className="block text-[11px] text-muted-foreground mt-0.5">{description}</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ export function BeforeAfterSlider({
|
|||||||
<span className="px-2 py-1 rounded bg-muted text-muted-foreground">
|
<span className="px-2 py-1 rounded bg-muted text-muted-foreground">
|
||||||
Original: {formatSize(beforeSize)}
|
Original: {formatSize(beforeSize)}
|
||||||
</span>
|
</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)}
|
Processed: {formatSize(afterSize)}
|
||||||
{savingsPercent !== null && Number(savingsPercent) > 0 && (
|
{savingsPercent !== null && Number(savingsPercent) > 0 && (
|
||||||
<span className="ms-1">({savingsPercent}% smaller)</span>
|
<span className="ms-1">({savingsPercent}% smaller)</span>
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ export function CollapsibleSection({
|
|||||||
<ChevronRight className="h-3 w-3 shrink-0" />
|
<ChevronRight className="h-3 w-3 shrink-0" />
|
||||||
)}
|
)}
|
||||||
<span className="flex-1 text-start">{title}</span>
|
<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 && (
|
{badge && (
|
||||||
<span className="px-1.5 py-0.5 rounded bg-muted text-muted-foreground text-[10px]">
|
<span className="px-1.5 py-0.5 rounded bg-muted text-muted-foreground text-[10px]">
|
||||||
{badge}
|
{badge}
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ export function Dropzone({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setShowBulkModal(true);
|
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}
|
{t.dropzone.importMultipleUrls}
|
||||||
</button>
|
</button>
|
||||||
@@ -343,7 +343,7 @@ export function Dropzone({
|
|||||||
|
|
||||||
{hasMultipleFiles && (
|
{hasMultipleFiles && (
|
||||||
<div className="flex flex-col items-center gap-2 mt-1">
|
<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" />
|
<FileImage className="h-3.5 w-3.5" />
|
||||||
{currentFiles.length} files selected
|
{currentFiles.length} files selected
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ export function ImageViewer({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={fitToContainer}
|
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"
|
title="Fit to view"
|
||||||
aria-label={t.a11y.fitToView}
|
aria-label={t.a11y.fitToView}
|
||||||
>
|
>
|
||||||
@@ -233,7 +233,7 @@ export function ImageViewer({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={actualSize}
|
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%)"
|
title="Actual size (100%)"
|
||||||
aria-label={t.a11y.actualSize}
|
aria-label={t.a11y.actualSize}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -33,13 +33,13 @@ export function MetadataGrid({
|
|||||||
return (
|
return (
|
||||||
<div key={k} className="contents">
|
<div key={k} className="contents">
|
||||||
<div
|
<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}
|
title={k}
|
||||||
>
|
>
|
||||||
{labelMap?.[k] ?? k}
|
{labelMap?.[k] ?? k}
|
||||||
</div>
|
</div>
|
||||||
<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)}
|
title={formatExifValue(k, v)}
|
||||||
>
|
>
|
||||||
{formatExifValue(k, v)}
|
{formatExifValue(k, v)}
|
||||||
@@ -50,7 +50,7 @@ export function MetadataGrid({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onRemove(k)}
|
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={
|
title={
|
||||||
isRemoved ? `Restore ${labelMap?.[k] ?? k}` : `Remove ${labelMap?.[k] ?? k}`
|
isRemoved ? `Restore ${labelMap?.[k] ?? k}` : `Remove ${labelMap?.[k] ?? k}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export function MultiImageViewer() {
|
|||||||
!currentEntry.blobUrl ? (
|
!currentEntry.blobUrl ? (
|
||||||
<div className="flex flex-col items-center justify-center h-full gap-3 text-center p-8">
|
<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">
|
<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>
|
</div>
|
||||||
<p className="text-sm font-medium">{processedFilename}</p>
|
<p className="text-sm font-medium">{processedFilename}</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<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-sm font-medium text-foreground truncate">{label}</div>
|
||||||
<div className="text-[11px] text-muted-foreground truncate">{sublabel}</div>
|
<div className="text-[11px] text-muted-foreground truncate">{sublabel}</div>
|
||||||
</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)}%
|
{Math.round(percent)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -123,14 +123,14 @@ export function ReviewPanel({
|
|||||||
|
|
||||||
{/* Success indicator */}
|
{/* Success indicator */}
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
<span className="text-sm font-medium text-foreground">{t.toolPage.conversionComplete}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Batch partial failure summary */}
|
{/* Batch partial failure summary */}
|
||||||
{hasBatchStats && failedCount > 0 && (
|
{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">
|
<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">
|
<span className="text-amber-800 dark:text-amber-300">
|
||||||
{format(t.toolPage.batchPartialSuccess, {
|
{format(t.toolPage.batchPartialSuccess, {
|
||||||
success: successCount,
|
success: successCount,
|
||||||
@@ -157,7 +157,7 @@ export function ReviewPanel({
|
|||||||
{sizeDelta > 0 && (
|
{sizeDelta > 0 && (
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-muted-foreground">{t.toolPage.saved}</span>
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -173,7 +173,7 @@ export function ReviewPanel({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleDownload}
|
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}
|
{t.toolPage.downloadAsFile}
|
||||||
</button>
|
</button>
|
||||||
@@ -200,7 +200,7 @@ export function ReviewPanel({
|
|||||||
{/* Result already auto-saved to the library: show where it went
|
{/* Result already auto-saved to the library: show where it went
|
||||||
instead of the manual save link (avoids duplicate saves). */}
|
instead of the manual save link (avoids duplicate saves). */}
|
||||||
{!isDataOutput && savedLibraryFileId && (
|
{!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" />
|
<CheckCircle2 className="h-3 w-3" />
|
||||||
{t.toolPage.savedToFiles}
|
{t.toolPage.savedToFiles}
|
||||||
<Link to="/files" className="underline underline-offset-2 hover:text-foreground">
|
<Link to="/files" className="underline underline-offset-2 hover:text-foreground">
|
||||||
@@ -219,7 +219,7 @@ export function ReviewPanel({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"text-xs flex items-center gap-1.5 transition-colors",
|
"text-xs flex items-center gap-1.5 transition-colors",
|
||||||
saveStatus === "saved"
|
saveStatus === "saved"
|
||||||
? "text-emerald-600 dark:text-emerald-400"
|
? "text-success-ink"
|
||||||
: "text-muted-foreground hover:text-foreground disabled:opacity-50",
|
: "text-muted-foreground hover:text-foreground disabled:opacity-50",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -271,7 +271,7 @@ export function ReviewPanel({
|
|||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
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" />
|
<ArrowLeft className="h-3 w-3" />
|
||||||
{t.toolPage.backToTools}
|
{t.toolPage.backToTools}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export function SideBySideComparison({
|
|||||||
{/* Size savings */}
|
{/* Size savings */}
|
||||||
{savingsPercent !== null && (
|
{savingsPercent !== null && (
|
||||||
<p
|
<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
|
{Number(savingsPercent) > 0
|
||||||
? format(t.toolSettings["optimize-for-web"].smaller, { percent: savingsPercent })
|
? format(t.toolSettings["optimize-for-web"].smaller, { percent: savingsPercent })
|
||||||
|
|||||||
@@ -59,6 +59,16 @@ const SECTION_COLOR_MAP: Record<string, string> = Object.fromEntries(
|
|||||||
SECTIONS.map((s) => [s.id, s.color]),
|
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({
|
export function ToolCard({
|
||||||
tool,
|
tool,
|
||||||
variant = "compact",
|
variant = "compact",
|
||||||
@@ -95,7 +105,7 @@ export function ToolCard({
|
|||||||
className="text-[10px] px-1.5 py-0.5 rounded-full font-medium shrink-0"
|
className="text-[10px] px-1.5 py-0.5 rounded-full font-medium shrink-0"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: `${sectionColor}20`,
|
backgroundColor: `${sectionColor}20`,
|
||||||
color: sectionColor,
|
color: SECTION_INK_MAP[section] ?? "#3D3832",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{SECTIONS.find((s) => s.id === section)?.name ?? section}
|
{SECTIONS.find((s) => s.id === section)?.name ?? section}
|
||||||
@@ -139,7 +149,7 @@ export function ToolCard({
|
|||||||
</span>
|
</span>
|
||||||
{sectionBadge}
|
{sectionBadge}
|
||||||
{tool.experimental && (
|
{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}
|
{t.common.experimental}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -178,7 +188,7 @@ export function ToolCard({
|
|||||||
</span>
|
</span>
|
||||||
{sectionBadge}
|
{sectionBadge}
|
||||||
{tool.experimental && (
|
{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}
|
{t.common.experimental}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function StatusIcon({ status }: { status: UrlImportEntry["status"] }) {
|
|||||||
case "fetching":
|
case "fetching":
|
||||||
return <Loader2 className="h-4 w-4 text-primary animate-spin" />;
|
return <Loader2 className="h-4 w-4 text-primary animate-spin" />;
|
||||||
case "ready":
|
case "ready":
|
||||||
return <Check className="h-4 w-4 text-emerald-500" />;
|
return <Check className="h-4 w-4 text-success-ink" />;
|
||||||
case "failed":
|
case "failed":
|
||||||
return <AlertCircle className="h-4 w-4 text-destructive" />;
|
return <AlertCircle className="h-4 w-4 text-destructive" />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export function ZoomToolbar({
|
|||||||
title={t.a11y.pan}
|
title={t.a11y.pan}
|
||||||
aria-label={t.a11y.pan}
|
aria-label={t.a11y.pan}
|
||||||
aria-pressed={handToolActive}
|
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" />
|
<Hand className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -568,7 +568,7 @@ export function ExportDialog({ onClose }: { onClose: () => void }) {
|
|||||||
height: canvasSize.height,
|
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})
|
Reset to original size ({canvasSize.width} x {canvasSize.height})
|
||||||
</button>
|
</button>
|
||||||
@@ -826,7 +826,7 @@ export function AutosaveRecoveryBanner({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3 px-4 py-2 bg-yellow-500/10 border-b border-yellow-500/30 text-xs">
|
<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>
|
<span className="text-foreground">Recovered unsaved work from {timeStr}.</span>
|
||||||
<div className="flex items-center gap-2 ms-auto">
|
<div className="flex items-center gap-2 ms-auto">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export function WelcomeScreen() {
|
|||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className="text-xl font-semibold text-foreground mb-1 flex items-center justify-center gap-2">
|
<h2 className="text-xl font-semibold text-foreground mb-1 flex items-center justify-center gap-2">
|
||||||
{t.editor.welcome.heading}
|
{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
|
Beta
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -437,9 +437,9 @@ function LevelsSection() {
|
|||||||
|
|
||||||
const channelColors: Record<LevelsChannel, string> = {
|
const channelColors: Record<LevelsChannel, string> = {
|
||||||
rgb: "text-foreground",
|
rgb: "text-foreground",
|
||||||
red: "text-red-400",
|
red: "text-red-700 dark:text-red-400",
|
||||||
green: "text-green-400",
|
green: "text-green-700 dark:text-green-400",
|
||||||
blue: "text-blue-400",
|
blue: "text-blue-700 dark:text-blue-400",
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -187,23 +187,23 @@ export function HistogramPanel({ imageData }: HistogramPanelProps) {
|
|||||||
<div>
|
<div>
|
||||||
<span className="font-medium">Mean</span>
|
<span className="font-medium">Mean</span>
|
||||||
<br />
|
<br />
|
||||||
<span className="text-red-400">{stats.mean[0]}</span>{" "}
|
<span className="text-red-700 dark:text-red-400">{stats.mean[0]}</span>{" "}
|
||||||
<span className="text-green-400">{stats.mean[1]}</span>{" "}
|
<span className="text-green-700 dark:text-green-400">{stats.mean[1]}</span>{" "}
|
||||||
<span className="text-blue-400">{stats.mean[2]}</span>
|
<span className="text-blue-700 dark:text-blue-400">{stats.mean[2]}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">StdDev</span>
|
<span className="font-medium">StdDev</span>
|
||||||
<br />
|
<br />
|
||||||
<span className="text-red-400">{stats.stdDev[0]}</span>{" "}
|
<span className="text-red-700 dark:text-red-400">{stats.stdDev[0]}</span>{" "}
|
||||||
<span className="text-green-400">{stats.stdDev[1]}</span>{" "}
|
<span className="text-green-700 dark:text-green-400">{stats.stdDev[1]}</span>{" "}
|
||||||
<span className="text-blue-400">{stats.stdDev[2]}</span>
|
<span className="text-blue-700 dark:text-blue-400">{stats.stdDev[2]}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium">Median</span>
|
<span className="font-medium">Median</span>
|
||||||
<br />
|
<br />
|
||||||
<span className="text-red-400">{stats.median[0]}</span>{" "}
|
<span className="text-red-700 dark:text-red-400">{stats.median[0]}</span>{" "}
|
||||||
<span className="text-green-400">{stats.median[1]}</span>{" "}
|
<span className="text-green-700 dark:text-green-400">{stats.median[1]}</span>{" "}
|
||||||
<span className="text-blue-400">{stats.median[2]}</span>
|
<span className="text-blue-700 dark:text-blue-400">{stats.median[2]}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export function HistoryPanel() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 w-full px-2 py-1.5 text-start text-xs transition-colors",
|
"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",
|
isCurrent && "bg-primary/10 text-foreground font-medium",
|
||||||
isFuture && "text-muted-foreground/40",
|
isFuture && "text-muted-foreground",
|
||||||
!isCurrent &&
|
!isCurrent &&
|
||||||
!isFuture &&
|
!isFuture &&
|
||||||
"text-muted-foreground hover:bg-muted hover:text-foreground",
|
"text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||||
|
|||||||
@@ -653,7 +653,7 @@ function LayerContextMenu({
|
|||||||
item.disabled
|
item.disabled
|
||||||
? "text-muted-foreground/40 cursor-not-allowed"
|
? "text-muted-foreground/40 cursor-not-allowed"
|
||||||
: "text-foreground hover:bg-muted",
|
: "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} />
|
<item.icon size={14} />
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ export function FeatureInstallPrompt({
|
|||||||
const statusClass = isCandidateInstalled
|
const statusClass = isCandidateInstalled
|
||||||
? "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"
|
? "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"
|
||||||
: isCandidateInstalling || isCandidateQueued
|
: isCandidateInstalling || isCandidateQueued
|
||||||
? "bg-primary/10 text-primary"
|
? "bg-primary/10 text-primary-ink"
|
||||||
: candidate.status === "error"
|
: candidate.status === "error"
|
||||||
? "bg-destructive/10 text-destructive"
|
? "bg-destructive/10 text-destructive"
|
||||||
: "bg-muted text-muted-foreground";
|
: "bg-muted text-muted-foreground";
|
||||||
|
|||||||
@@ -241,14 +241,14 @@ export function FeedbackDialog({
|
|||||||
href={buildToolRequestDiscussionUrl(searchQuery ?? "")}
|
href={buildToolRequestDiscussionUrl(searchQuery ?? "")}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-primary hover:underline"
|
className="text-primary-ink hover:underline"
|
||||||
>
|
>
|
||||||
{t.feedback.searchMissDiscussionsFallback}
|
{t.feedback.searchMissDiscussionsFallback}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-start gap-3">
|
<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">
|
<div className="space-y-1">
|
||||||
<p className="text-sm font-medium text-foreground">{t.feedback.thanksTitle}</p>
|
<p className="text-sm font-medium text-foreground">{t.feedback.thanksTitle}</p>
|
||||||
<p className="text-sm text-muted-foreground">{t.feedback.thanksDescription}</p>
|
<p className="text-sm text-muted-foreground">{t.feedback.thanksDescription}</p>
|
||||||
@@ -313,7 +313,7 @@ export function FeedbackDialog({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"py-2 rounded-lg border text-sm font-medium transition-colors",
|
"py-2 rounded-lg border text-sm font-medium transition-colors",
|
||||||
sentiment === value
|
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",
|
: "border-border text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -194,9 +194,7 @@ export function ToolFeedbackPrompt({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{thanks && (
|
{thanks && <p className="text-xs text-success-ink">{t.feedback.quickThanks}</p>}
|
||||||
<p className="text-xs text-emerald-600 dark:text-emerald-400">{t.feedback.quickThanks}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FeedbackDialog
|
<FeedbackDialog
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
|
|||||||
@@ -496,9 +496,7 @@ export function FileDetails({ mobile = false }: FileDetailsProps) {
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="rounded-lg border border-border overflow-hidden">
|
<div className="rounded-lg border border-border overflow-hidden">
|
||||||
<div className="bg-primary/10 border-b border-border px-3 py-2">
|
<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">
|
<h4 className="text-sm font-semibold text-primary-ink">{t.files.fileDetailsHeading}</h4>
|
||||||
{t.files.fileDetailsHeading}
|
|
||||||
</h4>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="divide-y divide-border">
|
<div className="divide-y divide-border">
|
||||||
<DetailRow label={t.files.name} value={details.originalName} />
|
<DetailRow label={t.files.name} value={details.originalName} />
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export function FileListItem({ file, disabled }: FileListItemProps) {
|
|||||||
|
|
||||||
{/* Tool chain */}
|
{/* Tool chain */}
|
||||||
{file.toolChain.length > 0 && (
|
{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(" → ")}
|
{file.toolChain.map(toolName).join(" → ")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function FileUploadArea() {
|
|||||||
<div className="flex flex-col items-center gap-2">
|
<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" />
|
<div className="h-10 w-10 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||||
{uploadProgress !== null && uploadProgress > 0 && (
|
{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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export function HelpDialog({ open, onClose }: HelpDialogProps) {
|
|||||||
href="https://github.com/snapotter-hq/snapotter"
|
href="https://github.com/snapotter-hq/snapotter"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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}
|
{t.help.resources.githubLink}
|
||||||
<ExternalLink className="h-3 w-3" />
|
<ExternalLink className="h-3 w-3" />
|
||||||
@@ -127,7 +127,7 @@ export function HelpDialog({ open, onClose }: HelpDialogProps) {
|
|||||||
href="https://github.com/snapotter-hq/snapotter/issues"
|
href="https://github.com/snapotter-hq/snapotter/issues"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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}
|
{t.help.resources.reportIssueLink}
|
||||||
<ExternalLink className="h-3 w-3" />
|
<ExternalLink className="h-3 w-3" />
|
||||||
@@ -136,7 +136,7 @@ export function HelpDialog({ open, onClose }: HelpDialogProps) {
|
|||||||
href="https://docs.snapotter.com/"
|
href="https://docs.snapotter.com/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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}
|
{t.help.resources.docsLink}
|
||||||
<ExternalLink className="h-3 w-3" />
|
<ExternalLink className="h-3 w-3" />
|
||||||
@@ -145,7 +145,7 @@ export function HelpDialog({ open, onClose }: HelpDialogProps) {
|
|||||||
href="/api/docs"
|
href="/api/docs"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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}
|
{t.help.resources.apiRefLink}
|
||||||
<ExternalLink className="h-3 w-3" />
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export function TopNav({
|
|||||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
|
aria-label={t.common.goHome}
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0",
|
"shrink-0",
|
||||||
isDark
|
isDark
|
||||||
@@ -179,7 +180,7 @@ export function TopNav({
|
|||||||
<ChevronRight
|
<ChevronRight
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-3.5 w-3.5 shrink-0",
|
"h-3.5 w-3.5 shrink-0",
|
||||||
isDark ? "text-[#666]" : "text-muted-foreground/50",
|
isDark ? "text-[#8a8a8a]" : "text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{breadcrumb.modalityTab ? (
|
{breadcrumb.modalityTab ? (
|
||||||
@@ -204,7 +205,7 @@ export function TopNav({
|
|||||||
<ChevronRight
|
<ChevronRight
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-3.5 w-3.5 shrink-0",
|
"h-3.5 w-3.5 shrink-0",
|
||||||
isDark ? "text-[#666]" : "text-muted-foreground/50",
|
isDark ? "text-[#8a8a8a]" : "text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -234,7 +235,7 @@ export function TopNav({
|
|||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
{link.badge && (
|
{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}
|
{link.badge}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ export function UsageSurveyOverlay() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 rounded-lg border px-3 py-2.5 text-sm font-medium text-start transition-colors",
|
"flex items-center gap-2 rounded-lg border px-3 py-2.5 text-sm font-medium text-start transition-colors",
|
||||||
usageType === value
|
usageType === value
|
||||||
? "border-primary bg-primary/10 text-primary"
|
? "border-primary bg-primary/10 text-primary-ink"
|
||||||
: "border-border text-foreground hover:bg-muted",
|
: "border-border text-foreground hover:bg-muted",
|
||||||
wide && "col-span-2 justify-center",
|
wide && "col-span-2 justify-center",
|
||||||
)}
|
)}
|
||||||
@@ -238,7 +238,7 @@ export function UsageSurveyOverlay() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 rounded-lg border px-3 py-2.5 text-sm font-medium text-start transition-colors",
|
"flex items-center gap-2 rounded-lg border px-3 py-2.5 text-sm font-medium text-start transition-colors",
|
||||||
importantAreas.includes(value)
|
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",
|
: "border-border text-foreground hover:bg-muted",
|
||||||
wide && "col-span-2 justify-center",
|
wide && "col-span-2 justify-center",
|
||||||
)}
|
)}
|
||||||
@@ -270,7 +270,7 @@ export function UsageSurveyOverlay() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"rounded-lg border px-3 py-2.5 text-sm font-medium text-start transition-colors",
|
"rounded-lg border px-3 py-2.5 text-sm font-medium text-start transition-colors",
|
||||||
installMethod === value
|
installMethod === value
|
||||||
? "border-primary bg-primary/10 text-primary"
|
? "border-primary bg-primary/10 text-primary-ink"
|
||||||
: "border-border text-foreground hover:bg-muted",
|
: "border-border text-foreground hover:bg-muted",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -405,7 +405,7 @@ function ImportBundleSection({ onImported }: { onImported: () => void }) {
|
|||||||
<p
|
<p
|
||||||
role={feedback.type === "error" ? "alert" : "status"}
|
role={feedback.type === "error" ? "alert" : "status"}
|
||||||
aria-live="polite"
|
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}
|
{feedback.message}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -437,7 +437,7 @@ function GeneralSection() {
|
|||||||
{/* User info */}
|
{/* User info */}
|
||||||
<div className="flex items-center justify-between p-4 rounded-lg border border-border bg-muted/20">
|
<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="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 ? (
|
{loading ? (
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
@@ -516,9 +516,7 @@ function GeneralSection() {
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm",
|
"text-sm",
|
||||||
saveMsg === t.settings.general.saveFailed
|
saveMsg === t.settings.general.saveFailed ? "text-destructive" : "text-success-ink",
|
||||||
? "text-destructive"
|
|
||||||
: "text-green-600 dark:text-green-400",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{saveMsg}
|
{saveMsg}
|
||||||
@@ -837,9 +835,7 @@ function SystemSection() {
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm",
|
"text-sm",
|
||||||
saveMsg === t.settings.system.saveFailed
|
saveMsg === t.settings.system.saveFailed ? "text-destructive" : "text-success-ink",
|
||||||
? "text-destructive"
|
|
||||||
: "text-green-600 dark:text-green-400",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{saveMsg}
|
{saveMsg}
|
||||||
@@ -1045,9 +1041,7 @@ function SecuritySection() {
|
|||||||
role="alert"
|
role="alert"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm",
|
"text-sm",
|
||||||
message.type === "error"
|
message.type === "error" ? "text-destructive" : "text-success-ink",
|
||||||
? "text-destructive"
|
|
||||||
: "text-green-600 dark:text-green-400",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{message.text}
|
{message.text}
|
||||||
@@ -1347,7 +1341,7 @@ export function AdminSecuritySettings() {
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm",
|
"text-sm",
|
||||||
saveMsg.type === "error" ? "text-destructive" : "text-green-600 dark:text-green-400",
|
saveMsg.type === "error" ? "text-destructive" : "text-success-ink",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{saveMsg.text}
|
{saveMsg.text}
|
||||||
@@ -1614,7 +1608,7 @@ function PeopleSection() {
|
|||||||
"text-sm px-3 py-2 rounded-lg",
|
"text-sm px-3 py-2 rounded-lg",
|
||||||
actionMsg.type === "error"
|
actionMsg.type === "error"
|
||||||
? "bg-destructive/10 text-destructive"
|
? "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}
|
{actionMsg.text}
|
||||||
@@ -1715,7 +1709,7 @@ function PeopleSection() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 p-2 rounded-lg border border-border transition-colors",
|
"shrink-0 p-2 rounded-lg border border-border transition-colors",
|
||||||
pwCopied
|
pwCopied
|
||||||
? "text-green-500 bg-green-500/10"
|
? "text-success-ink bg-green-500/10"
|
||||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||||
)}
|
)}
|
||||||
title={
|
title={
|
||||||
@@ -1778,7 +1772,7 @@ function PeopleSection() {
|
|||||||
setShowGeneratedPw(true);
|
setShowGeneratedPw(true);
|
||||||
setPwCopied(false);
|
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" />
|
<Sparkles className="h-3 w-3" />
|
||||||
{t.changePassword.generateButton}
|
{t.changePassword.generateButton}
|
||||||
@@ -1801,7 +1795,7 @@ function PeopleSection() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{showGeneratedPw && !pwCopied && (
|
{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" />
|
<Key className="h-3.5 w-3.5 shrink-0" />
|
||||||
{t.settings.people.copyPasswordWarning}
|
{t.settings.people.copyPasswordWarning}
|
||||||
</p>
|
</p>
|
||||||
@@ -1894,7 +1888,7 @@ function PeopleSection() {
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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}
|
{t.settings.people.resetPasswordButton}
|
||||||
</button>
|
</button>
|
||||||
@@ -1942,7 +1936,7 @@ function PeopleSection() {
|
|||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<>
|
<>
|
||||||
{/* Mobile card layout */}
|
{/* 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()}
|
{u.username.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -1966,7 +1960,7 @@ function PeopleSection() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"inline-block px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide",
|
"inline-block px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide",
|
||||||
u.role === "admin"
|
u.role === "admin"
|
||||||
? "bg-primary/15 text-primary"
|
? "bg-primary/15 text-primary-ink"
|
||||||
: "bg-muted text-muted-foreground",
|
: "bg-muted text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -1980,7 +1974,7 @@ function PeopleSection() {
|
|||||||
<>
|
<>
|
||||||
{/* Desktop row layout */}
|
{/* Desktop row layout */}
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<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()}
|
{u.username.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium text-foreground truncate">
|
<span className="text-sm font-medium text-foreground truncate">
|
||||||
@@ -2004,7 +1998,7 @@ function PeopleSection() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"inline-block px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide",
|
"inline-block px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide",
|
||||||
u.role === "admin"
|
u.role === "admin"
|
||||||
? "bg-primary/15 text-primary"
|
? "bg-primary/15 text-primary-ink"
|
||||||
: "bg-muted text-muted-foreground",
|
: "bg-muted text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -2282,7 +2276,11 @@ function ApiKeysSection() {
|
|||||||
title="Copy"
|
title="Copy"
|
||||||
aria-label={t.common.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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">{t.settings.apiKeys.keyWarning}</p>
|
<p className="text-xs text-muted-foreground">{t.settings.apiKeys.keyWarning}</p>
|
||||||
@@ -2311,7 +2309,7 @@ function ApiKeysSection() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{k.expiresAt && (
|
{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()}
|
Expires {new Date(k.expiresAt).toLocaleDateString()}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -2502,7 +2500,7 @@ function TeamsSection() {
|
|||||||
"text-sm px-3 py-2 rounded-lg",
|
"text-sm px-3 py-2 rounded-lg",
|
||||||
actionMsg.type === "error"
|
actionMsg.type === "error"
|
||||||
? "bg-destructive/10 text-destructive"
|
? "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}
|
{actionMsg.text}
|
||||||
@@ -2603,7 +2601,7 @@ function TeamsSection() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleRename(tm.id)}
|
onClick={() => handleRename(tm.id)}
|
||||||
className="text-xs text-primary hover:underline"
|
className="text-xs text-primary-ink hover:underline"
|
||||||
>
|
>
|
||||||
{t.common.save}
|
{t.common.save}
|
||||||
</button>
|
</button>
|
||||||
@@ -2917,7 +2915,7 @@ function RolesSection() {
|
|||||||
"text-sm px-3 py-2 rounded-lg",
|
"text-sm px-3 py-2 rounded-lg",
|
||||||
actionMsg.type === "error"
|
actionMsg.type === "error"
|
||||||
? "bg-destructive/10 text-destructive"
|
? "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}
|
{actionMsg.text}
|
||||||
@@ -3095,7 +3093,7 @@ function RolesSection() {
|
|||||||
{t.settings.roles.builtInBadge}
|
{t.settings.roles.builtInBadge}
|
||||||
</span>
|
</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" : ""}
|
{role.userCount} user{role.userCount !== 1 ? "s" : ""}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -3595,7 +3593,7 @@ function AboutSection() {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<OtterLogo className="h-8 w-8 text-primary" />
|
<OtterLogo className="h-8 w-8 text-primary" />
|
||||||
<div className="text-2xl font-bold text-foreground">
|
<div className="text-2xl font-bold text-foreground">
|
||||||
<span className="text-primary">SnapOtter</span>
|
<span className="text-primary-ink">SnapOtter</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">{t.settings.about.appDescription}</p>
|
<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"
|
href="https://github.com/snapotter-hq/snapotter"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-sm text-primary hover:underline"
|
className="text-sm text-primary-ink hover:underline"
|
||||||
>
|
>
|
||||||
{t.settings.about.githubLink}
|
{t.settings.about.githubLink}
|
||||||
</a>
|
</a>
|
||||||
@@ -3628,7 +3626,7 @@ function AboutSection() {
|
|||||||
href="https://docs.snapotter.com/"
|
href="https://docs.snapotter.com/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-sm text-primary hover:underline"
|
className="text-sm text-primary-ink hover:underline"
|
||||||
>
|
>
|
||||||
{t.settings.about.docsLink}
|
{t.settings.about.docsLink}
|
||||||
</a>
|
</a>
|
||||||
@@ -3636,7 +3634,7 @@ function AboutSection() {
|
|||||||
href="/api/docs"
|
href="/api/docs"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-sm text-primary hover:underline"
|
className="text-sm text-primary-ink hover:underline"
|
||||||
>
|
>
|
||||||
{t.settings.about.apiRefLink}
|
{t.settings.about.apiRefLink}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ export function TwoFactorSettings() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleCopyCodes}
|
onClick={handleCopyCodes}
|
||||||
className="mt-2 text-xs text-primary hover:underline"
|
className="mt-2 text-xs text-primary-ink hover:underline"
|
||||||
>
|
>
|
||||||
{codesCopied
|
{codesCopied
|
||||||
? t.settings.security.twoFactorCodesCopied
|
? t.settings.security.twoFactorCodesCopied
|
||||||
@@ -229,9 +229,7 @@ export function TwoFactorSettings() {
|
|||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm",
|
"text-sm",
|
||||||
message.type === "error"
|
message.type === "error" ? "text-destructive" : "text-success-ink",
|
||||||
? "text-destructive"
|
|
||||||
: "text-green-600 dark:text-green-400",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{message.text}
|
{message.text}
|
||||||
@@ -277,9 +275,7 @@ export function TwoFactorSettings() {
|
|||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm",
|
"text-sm",
|
||||||
message.type === "error"
|
message.type === "error" ? "text-destructive" : "text-success-ink",
|
||||||
? "text-destructive"
|
|
||||||
: "text-green-600 dark:text-green-400",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{message.text}
|
{message.text}
|
||||||
@@ -311,7 +307,7 @@ export function TwoFactorSettings() {
|
|||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm",
|
"text-sm",
|
||||||
message.type === "error" ? "text-destructive" : "text-green-600 dark:text-green-400",
|
message.type === "error" ? "text-destructive" : "text-success-ink",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{message.text}
|
{message.text}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export function UsageSection() {
|
|||||||
{row.total}
|
{row.total}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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}
|
{row.completed}
|
||||||
</span>
|
</span>
|
||||||
<span className="w-10 shrink-0 text-end text-destructive">
|
<span className="w-10 shrink-0 text-end text-destructive">
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ export function AiCanvasExpandSettings() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
@@ -237,7 +237,7 @@ export function AiCanvasExpandSettings() {
|
|||||||
href={downloadUrl}
|
href={downloadUrl}
|
||||||
download
|
download
|
||||||
data-testid="ai-canvas-expand-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 className="h-4 w-4" />
|
||||||
Download
|
Download
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export function AspectPadSettings() {
|
|||||||
<p className="text-[10px] text-muted-foreground mt-0.5">{s.colorHint}</p>
|
<p className="text-[10px] text-muted-foreground mt-0.5">{s.colorHint}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function AudioChannelsSettings() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export function AudioMetadataSettings() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function AudioSpeedSettings() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export function BarcodeGenerateSettings() {
|
|||||||
{ts.includeText}
|
{ts.includeText}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{/* Result preview */}
|
{/* Result preview */}
|
||||||
{resultUrl && (
|
{resultUrl && (
|
||||||
@@ -147,7 +147,7 @@ export function BarcodeGenerateSettings() {
|
|||||||
href={resultUrl}
|
href={resultUrl}
|
||||||
download="barcode.png"
|
download="barcode.png"
|
||||||
data-testid="barcode-generate-download"
|
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" />
|
<Download className="h-4 w-4" />
|
||||||
{ts.download}
|
{ts.download}
|
||||||
|
|||||||
@@ -45,11 +45,11 @@ const FORMAT_LABELS: Record<string, string> = {
|
|||||||
/** Badge colors by barcode family. */
|
/** Badge colors by barcode family. */
|
||||||
function getBadgeColor(type: string): string {
|
function getBadgeColor(type: string): string {
|
||||||
if (type.includes("QR") || type === "Aztec" || type === "DataMatrix" || type === "MaxiCode")
|
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"))
|
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";
|
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 }) {
|
function SectionLabel({ children }: { children: React.ReactNode }) {
|
||||||
@@ -264,7 +264,7 @@ export function BarcodeReadSettings() {
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
{/* Error */}
|
{/* Error */}
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{/* Process button / progress */}
|
{/* Process button / progress */}
|
||||||
{processing ? (
|
{processing ? (
|
||||||
@@ -360,7 +360,7 @@ export function BarcodeReadSettings() {
|
|||||||
title="Copy value"
|
title="Copy value"
|
||||||
>
|
>
|
||||||
{copiedIndex === idx ? (
|
{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" />
|
<Copy className="h-3.5 w-3.5" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1374,7 +1374,7 @@ export function BeautifySettings({
|
|||||||
onBackgroundImage={handleBgImageChange}
|
onBackgroundImage={handleBgImageChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{displayError && <p className="text-xs text-red-500">{displayError}</p>}
|
{displayError && <p className="text-xs text-destructive-ink">{displayError}</p>}
|
||||||
|
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
@@ -1405,7 +1405,7 @@ export function BeautifySettings({
|
|||||||
href={displayDownloadUrl}
|
href={displayDownloadUrl}
|
||||||
download
|
download
|
||||||
data-testid="beautify-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 className="h-4 w-4" />
|
||||||
Download
|
Download
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export function BlurBackgroundSettings() {
|
|||||||
onClick={() => setOutputFormat(fmt)}
|
onClick={() => setOutputFormat(fmt)}
|
||||||
className={`rounded-lg border py-2 px-2 text-xs font-medium uppercase transition-colors ${
|
className={`rounded-lg border py-2 px-2 text-xs font-medium uppercase transition-colors ${
|
||||||
outputFormat === fmt
|
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"
|
: "border-border text-muted-foreground hover:border-primary/50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -114,9 +114,9 @@ export function BlurFacesSettings() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<BlurFacesControls onChange={setSettings} />
|
<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 */}
|
{/* Size info */}
|
||||||
{originalSize != null && processedSize != null && (
|
{originalSize != null && processedSize != null && (
|
||||||
@@ -153,7 +153,7 @@ export function BlurFacesSettings() {
|
|||||||
href={downloadUrl}
|
href={downloadUrl}
|
||||||
download
|
download
|
||||||
data-testid="blur-faces-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" />
|
<Download className="h-4 w-4" />
|
||||||
{t.common.download}
|
{t.common.download}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export function BlurPadSettings() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function BookletPdfSettings() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
|
|||||||
@@ -613,7 +613,7 @@ export function BorderSettings({
|
|||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<BorderControls onChange={handleSettingsChange} onImageStyle={onImageStyle} />
|
<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 ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
@@ -640,7 +640,7 @@ export function BorderSettings({
|
|||||||
href={downloadUrl}
|
href={downloadUrl}
|
||||||
download
|
download
|
||||||
data-testid="border-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 className="h-4 w-4" />
|
||||||
Download
|
Download
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export function BulkRenameSettings() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -135,7 +135,7 @@ export function BulkRenameSettings() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{downloadReady && (
|
{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}
|
<Download className="h-3 w-3" /> {t.toolSettings["bulk-rename"].zipDownloaded}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function BurnSubtitlesSettings() {
|
|||||||
|
|
||||||
<p className="text-[10px] text-muted-foreground">{s.hint}</p>
|
<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 ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function ChangeFpsSettings() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export function ChartMakerSettings() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
@@ -143,7 +143,7 @@ export function ChartMakerSettings() {
|
|||||||
href={downloadUrl}
|
href={downloadUrl}
|
||||||
download
|
download
|
||||||
data-testid="chart-maker-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" />
|
<Download className="h-4 w-4" />
|
||||||
{t.common.download}
|
{t.common.download}
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ export function CircleCropSettings() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
@@ -268,7 +268,7 @@ export function CircleCropSettings() {
|
|||||||
href={downloadUrl}
|
href={downloadUrl}
|
||||||
download
|
download
|
||||||
data-testid="circle-crop-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" />
|
<Download className="h-4 w-4" />
|
||||||
{t.common.download}
|
{t.common.download}
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ function UploadArea() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClick}
|
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 className="h-4 w-4" />
|
||||||
Upload images for collage
|
Upload images for collage
|
||||||
@@ -520,7 +520,7 @@ function CollageCell({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-1 rounded px-1.5 py-1 text-[11px] font-medium backdrop-blur-sm transition-colors",
|
"flex items-center gap-1 rounded px-1.5 py-1 text-[11px] font-medium backdrop-blur-sm transition-colors",
|
||||||
transform.objectFit === "contain"
|
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",
|
: "bg-black/50 text-white hover:bg-black/70",
|
||||||
)}
|
)}
|
||||||
title={transform.objectFit === "cover" ? "Fit entire image" : "Fill cell"}
|
title={transform.objectFit === "cover" ? "Fit entire image" : "Fill cell"}
|
||||||
|
|||||||
@@ -361,7 +361,7 @@ export function CollageSettings() {
|
|||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
|
|
||||||
{/* Error */}
|
{/* Error */}
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<button
|
<button
|
||||||
@@ -380,7 +380,7 @@ export function CollageSettings() {
|
|||||||
href={resultUrl}
|
href={resultUrl}
|
||||||
download
|
download
|
||||||
data-testid="collage-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 className="h-4 w-4" />
|
||||||
Download Collage
|
Download Collage
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export function ColorBlindnessSettings() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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 && (
|
{originalSize != null && processedSize != null && (
|
||||||
<div className="text-xs text-muted-foreground space-y-0.5">
|
<div className="text-xs text-muted-foreground space-y-0.5">
|
||||||
@@ -158,7 +158,7 @@ export function ColorBlindnessSettings() {
|
|||||||
href={downloadUrl}
|
href={downloadUrl}
|
||||||
download
|
download
|
||||||
data-testid="color-blindness-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" />
|
<Download className="h-4 w-4" />
|
||||||
{t.common.download}
|
{t.common.download}
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export function ColorPaletteSettings() {
|
|||||||
{processing ? ts.extracting : ts.submit}
|
{processing ? ts.extracting : ts.submit}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{colors.length > 0 && (
|
{colors.length > 0 && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -157,7 +157,7 @@ export function ColorPaletteSettings() {
|
|||||||
data-testid="color-palette-copy-css"
|
data-testid="color-palette-copy-css"
|
||||||
>
|
>
|
||||||
{copiedExport === "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" />
|
<ClipboardCopy className="h-3 w-3" />
|
||||||
)}
|
)}
|
||||||
@@ -170,7 +170,7 @@ export function ColorPaletteSettings() {
|
|||||||
data-testid="color-palette-copy-json"
|
data-testid="color-palette-copy-json"
|
||||||
>
|
>
|
||||||
{copiedExport === "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" />
|
<ClipboardCopy className="h-3 w-3" />
|
||||||
)}
|
)}
|
||||||
@@ -196,7 +196,7 @@ export function ColorPaletteSettings() {
|
|||||||
{color}
|
{color}
|
||||||
</span>
|
</span>
|
||||||
{copiedIdx === i ? (
|
{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" />
|
<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" />}
|
{channelsOpen ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
|
||||||
Color Channels
|
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>
|
</button>
|
||||||
{channelsOpen && (
|
{channelsOpen && (
|
||||||
<div className="space-y-2 ps-1">
|
<div className="space-y-2 ps-1">
|
||||||
@@ -285,7 +287,7 @@ export function ColorControls({
|
|||||||
onChange={setRed}
|
onChange={setRed}
|
||||||
min={0}
|
min={0}
|
||||||
max={200}
|
max={200}
|
||||||
color="text-red-500"
|
color="text-destructive-ink"
|
||||||
/>
|
/>
|
||||||
<SliderControl
|
<SliderControl
|
||||||
label="Green"
|
label="Green"
|
||||||
@@ -293,7 +295,7 @@ export function ColorControls({
|
|||||||
onChange={setGreen}
|
onChange={setGreen}
|
||||||
min={0}
|
min={0}
|
||||||
max={200}
|
max={200}
|
||||||
color="text-green-500"
|
color="text-success-ink"
|
||||||
/>
|
/>
|
||||||
<SliderControl
|
<SliderControl
|
||||||
label="Blue"
|
label="Blue"
|
||||||
@@ -301,7 +303,7 @@ export function ColorControls({
|
|||||||
onChange={setBlue}
|
onChange={setBlue}
|
||||||
min={0}
|
min={0}
|
||||||
max={200}
|
max={200}
|
||||||
color="text-blue-500"
|
color="text-blue-700 dark:text-blue-400"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -379,7 +381,7 @@ export function ColorSettings({ toolId, onPreviewFilter }: ColorSettingsProps) {
|
|||||||
<form onSubmit={handleSubmit} className="space-y-3">
|
<form onSubmit={handleSubmit} className="space-y-3">
|
||||||
<ColorControls toolId={toolId} onChange={setSettings} onPreviewFilter={onPreviewFilter} />
|
<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 && (
|
{originalSize != null && processedSize != null && (
|
||||||
<div className="text-xs text-muted-foreground space-y-0.5">
|
<div className="text-xs text-muted-foreground space-y-0.5">
|
||||||
@@ -414,7 +416,7 @@ export function ColorSettings({ toolId, onPreviewFilter }: ColorSettingsProps) {
|
|||||||
href={downloadUrl}
|
href={downloadUrl}
|
||||||
download
|
download
|
||||||
data-testid="adjust-colors-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 className="h-4 w-4" />
|
||||||
Download
|
Download
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export function ColorizeSettings() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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 && (
|
{originalSize != null && processedSize != null && (
|
||||||
<div className="text-xs text-muted-foreground space-y-0.5">
|
<div className="text-xs text-muted-foreground space-y-0.5">
|
||||||
@@ -140,7 +140,7 @@ export function ColorizeSettings() {
|
|||||||
href={downloadUrl}
|
href={downloadUrl}
|
||||||
download
|
download
|
||||||
data-testid="colorize-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 className="h-4 w-4" />
|
||||||
Download
|
Download
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export function CompareSettings() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{similarity !== null && (
|
{similarity !== null && (
|
||||||
<div className="p-3 rounded-lg bg-muted">
|
<div className="p-3 rounded-lg bg-muted">
|
||||||
@@ -101,7 +101,7 @@ export function CompareSettings() {
|
|||||||
href={downloadUrl}
|
href={downloadUrl}
|
||||||
download
|
download
|
||||||
data-testid="compare-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 className="h-4 w-4" />
|
||||||
Download Diff Image
|
Download Diff Image
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ export function ComposeSettings() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</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 && (
|
{originalSize != null && processedSize != null && (
|
||||||
<div className="text-xs text-muted-foreground space-y-0.5">
|
<div className="text-xs text-muted-foreground space-y-0.5">
|
||||||
@@ -176,7 +176,7 @@ export function ComposeSettings() {
|
|||||||
href={downloadUrl}
|
href={downloadUrl}
|
||||||
download
|
download
|
||||||
data-testid="compose-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 className="h-4 w-4" />
|
||||||
Download
|
Download
|
||||||
|
|||||||
@@ -51,15 +51,15 @@ export function CompressPdfSettings() {
|
|||||||
<p className="text-[11px] text-muted-foreground">{s.bestEffortHint}</p>
|
<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 && (
|
{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 })}
|
{format(s.targetMissed, { target: targetLabel, size: achievedLabel })}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{targetMet === true && (
|
{targetMet === true && (
|
||||||
<p className="text-xs text-green-600">
|
<p className="text-xs text-success-ink">
|
||||||
{format(s.targetReached, { target: targetLabel, size: achievedLabel })}
|
{format(s.targetReached, { target: targetLabel, size: achievedLabel })}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ export function CompressSettings() {
|
|||||||
<CompressControls onChange={setSettings} />
|
<CompressControls onChange={setSettings} />
|
||||||
|
|
||||||
{/* Error */}
|
{/* Error */}
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{/* Size info */}
|
{/* Size info */}
|
||||||
{originalSize != null && processedSize != null && (
|
{originalSize != null && processedSize != null && (
|
||||||
@@ -255,7 +255,7 @@ export function CompressSettings() {
|
|||||||
href={downloadUrl}
|
href={downloadUrl}
|
||||||
download
|
download
|
||||||
data-testid="compress-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" />
|
<Download className="h-4 w-4" />
|
||||||
{t.common.download}
|
{t.common.download}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export function CompressVideoSettings() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ export function ContentAwareResizeSettings() {
|
|||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<ContentAwareResizeControls onChange={handleSettingsChange} />
|
<ContentAwareResizeControls onChange={handleSettingsChange} />
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
@@ -226,7 +226,7 @@ export function ContentAwareResizeSettings() {
|
|||||||
href={downloadUrl}
|
href={downloadUrl}
|
||||||
download
|
download
|
||||||
data-testid="content-aware-resize-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 className="h-4 w-4" />
|
||||||
Download
|
Download
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export function ConversionPresetSettings() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
@@ -159,7 +159,7 @@ export function ConversionPresetSettings() {
|
|||||||
href={downloadUrl}
|
href={downloadUrl}
|
||||||
download
|
download
|
||||||
data-testid="preset-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" />
|
<Download className="h-4 w-4" />
|
||||||
{t.common.download}
|
{t.common.download}
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export function ConvertAudioSettings() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function ConvertDocumentSettings() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export function ConvertPresentationSettings() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ export function ConvertSettings() {
|
|||||||
<ConvertControls onChange={setSettings} />
|
<ConvertControls onChange={setSettings} />
|
||||||
|
|
||||||
{/* Error */}
|
{/* Error */}
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{/* Size info */}
|
{/* Size info */}
|
||||||
{originalSize != null && processedSize != null && (
|
{originalSize != null && processedSize != null && (
|
||||||
@@ -213,7 +213,7 @@ export function ConvertSettings() {
|
|||||||
href={downloadUrl}
|
href={downloadUrl}
|
||||||
download
|
download
|
||||||
data-testid="convert-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" />
|
<Download className="h-4 w-4" />
|
||||||
{t.common.download}
|
{t.common.download}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export function ConvertSpreadsheetSettings() {
|
|||||||
|
|
||||||
<p className="text-[10px] text-muted-foreground">{s.hint}</p>
|
<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 ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function ConvertVideoSettings() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function CreateZipSettings() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<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 ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function CropPdfSettings() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
|
|||||||
@@ -365,7 +365,7 @@ export function CropSettings({
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
{/* Error */}
|
{/* Error */}
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{/* Process */}
|
{/* Process */}
|
||||||
{processing ? (
|
{processing ? (
|
||||||
@@ -396,7 +396,7 @@ export function CropSettings({
|
|||||||
href={downloadUrl}
|
href={downloadUrl}
|
||||||
download
|
download
|
||||||
data-testid="crop-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" />
|
<Download className="h-4 w-4" />
|
||||||
{t.common.download}
|
{t.common.download}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export function CropVideoSettings() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500">{error}</p>}
|
{error && <p className="text-xs text-destructive-ink">{error}</p>}
|
||||||
|
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<ProgressCard
|
<ProgressCard
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user