chore: remove internal docs from repo, update public documentation

Remove docs/superpowers/, .claude/ config, and PRD.md from version
control (kept locally via .gitignore). Update README, CHANGELOG,
VitePress docs, and .env.example to reflect recent features: Files
page, teams, admin settings, persistent storage, and various API
improvements.
This commit is contained in:
Siddharth Kumar Sah
2026-03-26 01:11:40 +08:00
parent 3b4f522bf4
commit 627ff8a82c
99 changed files with 853 additions and 15277 deletions
@@ -62,6 +62,7 @@ export function BarcodeReadSettings() {
</p>
<button
type="button"
onClick={handleProcess}
disabled={!hasFile || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -79,6 +80,7 @@ export function BarcodeReadSettings() {
<p className="text-xs text-muted-foreground">Decoded Text:</p>
<p className="text-sm text-foreground font-mono break-all">{result.text}</p>
<button
type="button"
onClick={copyText}
className="flex items-center gap-1.5 text-xs text-primary hover:text-primary/80"
>
@@ -26,10 +26,13 @@ export function BlurFacesSettings() {
{/* Blur radius */}
<div>
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Blur Radius</label>
<label htmlFor="blur-faces-blur-radius" className="text-xs text-muted-foreground">
Blur Radius
</label>
<span className="text-xs font-mono text-foreground">{blurRadius}</span>
</div>
<input
id="blur-faces-blur-radius"
type="range"
min={5}
max={80}
@@ -46,10 +49,13 @@ export function BlurFacesSettings() {
{/* Sensitivity */}
<div>
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Detection Sensitivity</label>
<label htmlFor="blur-faces-sensitivity" className="text-xs text-muted-foreground">
Detection Sensitivity
</label>
<span className="text-xs font-mono text-foreground">{sensitivity}%</span>
</div>
<input
id="blur-faces-sensitivity"
type="range"
min={10}
max={90}
@@ -91,6 +97,7 @@ export function BlurFacesSettings() {
/>
) : (
<button
type="button"
onClick={handleProcess}
disabled={!hasFile || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -38,10 +38,13 @@ export function BorderSettings() {
<div className="space-y-4">
<div>
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Border Width</label>
<label htmlFor="border-border-width" className="text-xs text-muted-foreground">
Border Width
</label>
<span className="text-xs font-mono text-foreground">{borderWidth}px</span>
</div>
<input
id="border-border-width"
type="range"
min={0}
max={100}
@@ -52,8 +55,11 @@ export function BorderSettings() {
</div>
<div>
<label className="text-xs text-muted-foreground">Border Color</label>
<label htmlFor="border-border-color" className="text-xs text-muted-foreground">
Border Color
</label>
<input
id="border-border-color"
type="color"
value={borderColor}
onChange={(e) => setBorderColor(e.target.value)}
@@ -63,10 +69,13 @@ export function BorderSettings() {
<div>
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Corner Radius</label>
<label htmlFor="border-corner-radius" className="text-xs text-muted-foreground">
Corner Radius
</label>
<span className="text-xs font-mono text-foreground">{cornerRadius}px</span>
</div>
<input
id="border-corner-radius"
type="range"
min={0}
max={200}
@@ -78,10 +87,13 @@ export function BorderSettings() {
<div>
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Padding</label>
<label htmlFor="border-padding" className="text-xs text-muted-foreground">
Padding
</label>
<span className="text-xs font-mono text-foreground">{padding}px</span>
</div>
<input
id="border-padding"
type="range"
min={0}
max={100}
@@ -93,10 +105,13 @@ export function BorderSettings() {
<div>
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Shadow</label>
<label htmlFor="border-shadow" className="text-xs text-muted-foreground">
Shadow
</label>
<span className="text-xs font-mono text-foreground">{shadowBlur}px</span>
</div>
<input
id="border-shadow"
type="range"
min={0}
max={50}
@@ -126,6 +141,7 @@ export function BorderSettings() {
/>
) : (
<button
type="button"
onClick={handleProcess}
disabled={!hasFile || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -72,8 +72,11 @@ export function BulkRenameSettings() {
return (
<div className="space-y-4">
<div>
<label className="text-xs text-muted-foreground">Pattern</label>
<label htmlFor="bulk-rename-pattern" className="text-xs text-muted-foreground">
Pattern
</label>
<input
id="bulk-rename-pattern"
type="text"
value={pattern}
onChange={(e) => setPattern(e.target.value)}
@@ -85,8 +88,11 @@ export function BulkRenameSettings() {
</div>
<div>
<label className="text-xs text-muted-foreground">Start Index</label>
<label htmlFor="bulk-rename-start-index" className="text-xs text-muted-foreground">
Start Index
</label>
<input
id="bulk-rename-start-index"
type="number"
value={startIndex}
onChange={(e) => setStartIndex(Number(e.target.value))}
@@ -97,11 +103,11 @@ export function BulkRenameSettings() {
{previewNames.length > 0 && (
<div>
<label className="text-xs text-muted-foreground">Preview</label>
<p className="text-xs text-muted-foreground">Preview</p>
<div className="mt-1 space-y-0.5">
{previewNames.map((name, i) => (
{previewNames.map((name) => (
<div
key={i}
key={name}
className="text-xs font-mono text-foreground bg-muted px-2 py-0.5 rounded truncate"
>
{name}
@@ -117,6 +123,7 @@ export function BulkRenameSettings() {
{error && <p className="text-xs text-red-500">{error}</p>}
<button
type="button"
onClick={handleProcess}
disabled={!hasFiles || processing || !pattern}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -71,10 +71,11 @@ export function CollageSettings() {
return (
<div className="space-y-4">
<div>
<label className="text-xs text-muted-foreground">Layout</label>
<p className="text-xs text-muted-foreground">Layout</p>
<div className="grid grid-cols-3 gap-1 mt-1">
{LAYOUTS.map((l) => (
<button
type="button"
key={l.value}
onClick={() => setLayout(l.value)}
className={`text-xs py-1.5 rounded ${layout === l.value ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
@@ -87,10 +88,13 @@ export function CollageSettings() {
<div>
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Gap</label>
<label htmlFor="collage-gap" className="text-xs text-muted-foreground">
Gap
</label>
<span className="text-xs font-mono text-foreground">{gap}px</span>
</div>
<input
id="collage-gap"
type="range"
min={0}
max={50}
@@ -101,8 +105,11 @@ export function CollageSettings() {
</div>
<div>
<label className="text-xs text-muted-foreground">Background Color</label>
<label htmlFor="collage-background-color" className="text-xs text-muted-foreground">
Background Color
</label>
<input
id="collage-background-color"
type="color"
value={backgroundColor}
onChange={(e) => setBackgroundColor(e.target.value)}
@@ -120,6 +127,7 @@ export function CollageSettings() {
)}
<button
type="button"
onClick={handleProcess}
disabled={!hasFiles || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -57,6 +57,7 @@ export function ColorPaletteSettings() {
return (
<div className="space-y-4">
<button
type="button"
onClick={handleProcess}
disabled={!hasFile || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -69,13 +70,14 @@ export function ColorPaletteSettings() {
{colors.length > 0 && (
<div className="space-y-2">
<label className="text-xs font-medium text-muted-foreground">
<p className="text-xs font-medium text-muted-foreground">
Dominant Colors ({colors.length})
</label>
</p>
<div className="grid grid-cols-2 gap-1.5">
{colors.map((color, i) => (
<button
key={i}
type="button"
key={color}
onClick={() => copyColor(color, i)}
className="flex items-center gap-2 p-1.5 rounded border border-border hover:bg-muted transition-colors"
>
@@ -160,7 +160,7 @@ export function ColorSettings({ toolId }: ColorSettingsProps) {
{/* Effects */}
{tab === "effects" && (
<div className="space-y-2">
<label className="text-xs text-muted-foreground">Color Effect</label>
<p className="text-xs text-muted-foreground">Color Effect</p>
<div className="grid grid-cols-2 gap-1">
{(["none", "grayscale", "sepia", "invert"] as const).map((e) => (
<button
@@ -261,13 +261,17 @@ function SliderControl({
max: number;
color?: string;
}) {
const id = `color-slider-${label.toLowerCase()}`;
return (
<div>
<div className="flex justify-between items-center">
<label className={`text-xs ${color || "text-muted-foreground"}`}>{label}</label>
<label htmlFor={id} className={`text-xs ${color || "text-muted-foreground"}`}>
{label}
</label>
<span className="text-xs font-mono text-foreground">{value}</span>
</div>
<input
id={id}
type="range"
min={min}
max={max}
@@ -53,8 +53,11 @@ export function CompareSettings() {
return (
<div className="space-y-4">
<div>
<label className="text-xs text-muted-foreground">Second Image</label>
<label htmlFor="compare-second-image" className="text-xs text-muted-foreground">
Second Image
</label>
<input
id="compare-second-image"
ref={secondInputRef}
type="file"
accept="image/*"
@@ -62,6 +65,7 @@ export function CompareSettings() {
className="hidden"
/>
<button
type="button"
onClick={() => secondInputRef.current?.click()}
className="w-full mt-0.5 px-2 py-2 rounded border border-dashed border-border bg-background text-sm text-muted-foreground hover:text-foreground flex items-center justify-center gap-2"
>
@@ -84,6 +88,7 @@ export function CompareSettings() {
)}
<button
type="button"
onClick={handleProcess}
disabled={!hasFile || !secondFile || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -62,8 +62,11 @@ export function ComposeSettings() {
return (
<div className="space-y-4">
<div>
<label className="text-xs text-muted-foreground">Overlay Image</label>
<label htmlFor="compose-overlay-image" className="text-xs text-muted-foreground">
Overlay Image
</label>
<input
id="compose-overlay-image"
ref={overlayInputRef}
type="file"
accept="image/*"
@@ -71,6 +74,7 @@ export function ComposeSettings() {
className="hidden"
/>
<button
type="button"
onClick={() => overlayInputRef.current?.click()}
className="w-full mt-0.5 px-2 py-2 rounded border border-dashed border-border bg-background text-sm text-muted-foreground hover:text-foreground flex items-center justify-center gap-2"
>
@@ -81,8 +85,11 @@ export function ComposeSettings() {
<div className="flex gap-2">
<div className="flex-1">
<label className="text-xs text-muted-foreground">X Position</label>
<label htmlFor="compose-x-position" className="text-xs text-muted-foreground">
X Position
</label>
<input
id="compose-x-position"
type="number"
value={x}
onChange={(e) => setX(Number(e.target.value))}
@@ -91,8 +98,11 @@ export function ComposeSettings() {
/>
</div>
<div className="flex-1">
<label className="text-xs text-muted-foreground">Y Position</label>
<label htmlFor="compose-y-position" className="text-xs text-muted-foreground">
Y Position
</label>
<input
id="compose-y-position"
type="number"
value={y}
onChange={(e) => setY(Number(e.target.value))}
@@ -104,10 +114,13 @@ export function ComposeSettings() {
<div>
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Opacity</label>
<label htmlFor="compose-opacity" className="text-xs text-muted-foreground">
Opacity
</label>
<span className="text-xs font-mono text-foreground">{opacity}%</span>
</div>
<input
id="compose-opacity"
type="range"
min={0}
max={100}
@@ -118,8 +131,11 @@ export function ComposeSettings() {
</div>
<div>
<label className="text-xs text-muted-foreground">Blend Mode</label>
<label htmlFor="compose-blend-mode" className="text-xs text-muted-foreground">
Blend Mode
</label>
<select
id="compose-blend-mode"
value={blendMode}
onChange={(e) => setBlendMode(e.target.value)}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
@@ -147,6 +163,7 @@ export function ComposeSettings() {
)}
<button
type="button"
onClick={handleProcess}
disabled={!hasFile || !overlayFile || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -49,7 +49,7 @@ export function CompressSettings() {
<form onSubmit={handleSubmit} className="space-y-4">
{/* Mode toggle */}
<div>
<label className="text-sm font-medium text-muted-foreground">Compression Mode</label>
<p className="text-sm font-medium text-muted-foreground">Compression Mode</p>
<div className="flex gap-1 mt-1">
<button
type="button"
@@ -71,10 +71,13 @@ export function CompressSettings() {
{mode === "quality" ? (
<div>
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Quality</label>
<label htmlFor="compress-quality" className="text-xs text-muted-foreground">
Quality
</label>
<span className="text-xs font-mono text-foreground">{quality}</span>
</div>
<input
id="compress-quality"
type="range"
min={1}
max={100}
@@ -89,8 +92,11 @@ export function CompressSettings() {
</div>
) : (
<div>
<label className="text-xs text-muted-foreground">Target Size (KB)</label>
<label htmlFor="compress-target-size" className="text-xs text-muted-foreground">
Target Size (KB)
</label>
<input
id="compress-target-size"
type="number"
value={targetSizeKb}
onChange={(e) => setTargetSizeKb(e.target.value)}
@@ -55,7 +55,7 @@ export function ConvertSettings() {
{/* Source format */}
{hasFile && (
<div>
<label className="text-xs text-muted-foreground">Source Format</label>
<p className="text-xs text-muted-foreground">Source Format</p>
<div className="mt-0.5 px-2 py-1.5 rounded bg-muted text-sm text-foreground uppercase font-mono">
{sourceExt}
</div>
@@ -64,8 +64,11 @@ export function ConvertSettings() {
{/* Target format */}
<div>
<label className="text-xs text-muted-foreground">Target Format</label>
<label htmlFor="convert-target-format" className="text-xs text-muted-foreground">
Target Format
</label>
<select
id="convert-target-format"
value={format}
onChange={(e) => setFormat(e.target.value)}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
@@ -82,10 +85,13 @@ export function ConvertSettings() {
{isLossy && (
<div>
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Quality</label>
<label htmlFor="convert-quality" className="text-xs text-muted-foreground">
Quality
</label>
<span className="text-xs font-mono text-foreground">{quality}</span>
</div>
<input
id="convert-quality"
type="range"
min={1}
max={100}
@@ -165,7 +165,7 @@ export function CropSettings({
{/* Aspect Ratio */}
<div>
<div className="flex items-center justify-between mb-1">
<label className="text-xs text-muted-foreground">Aspect Ratio</label>
<p className="text-xs text-muted-foreground">Aspect Ratio</p>
{aspect !== undefined && (
<button
type="button"
@@ -197,13 +197,14 @@ export function CropSettings({
{/* Position & Size */}
<div>
<label className="text-xs text-muted-foreground">Position & Size</label>
<p className="text-xs text-muted-foreground">Position & Size</p>
<div className="grid grid-cols-2 gap-2 mt-1">
<div>
<label className="text-[10px] text-muted-foreground">
<label htmlFor="crop-x" className="text-[10px] text-muted-foreground">
X{imgDimensions ? ` (of ${imgDimensions.width})` : ""}
</label>
<input
id="crop-x"
type="number"
value={pixels.left}
onChange={(e) => handlePixelChange("left", Number(e.target.value))}
@@ -213,10 +214,11 @@ export function CropSettings({
/>
</div>
<div>
<label className="text-[10px] text-muted-foreground">
<label htmlFor="crop-y" className="text-[10px] text-muted-foreground">
Y{imgDimensions ? ` (of ${imgDimensions.height})` : ""}
</label>
<input
id="crop-y"
type="number"
value={pixels.top}
onChange={(e) => handlePixelChange("top", Number(e.target.value))}
@@ -226,10 +228,11 @@ export function CropSettings({
/>
</div>
<div>
<label className="text-[10px] text-muted-foreground">
<label htmlFor="crop-width" className="text-[10px] text-muted-foreground">
Width{imgDimensions ? ` (of ${imgDimensions.width})` : ""}
</label>
<input
id="crop-width"
type="number"
value={pixels.width}
onChange={(e) => handlePixelChange("width", Number(e.target.value))}
@@ -239,10 +242,11 @@ export function CropSettings({
/>
</div>
<div>
<label className="text-[10px] text-muted-foreground">
<label htmlFor="crop-height" className="text-[10px] text-muted-foreground">
Height{imgDimensions ? ` (of ${imgDimensions.height})` : ""}
</label>
<input
id="crop-height"
type="number"
value={pixels.height}
onChange={(e) => handlePixelChange("height", Number(e.target.value))}
@@ -114,17 +114,28 @@ export function EraseObjectSettings() {
<div className="space-y-4">
{/* Mask upload */}
<div>
<label className="text-sm font-medium text-muted-foreground">Mask Image</label>
<label htmlFor="erase-object-mask" className="text-sm font-medium text-muted-foreground">
Mask Image
</label>
<p className="text-[10px] text-muted-foreground mt-0.5 mb-1.5">
Upload a black &amp; white mask where white areas will be erased. Create the mask in any
image editor.
</p>
<label className="flex items-center gap-2 px-3 py-2 rounded border border-dashed border-border cursor-pointer hover:border-primary">
<label
htmlFor="erase-object-mask"
className="flex items-center gap-2 px-3 py-2 rounded border border-dashed border-border cursor-pointer hover:border-primary"
>
<Upload className="h-4 w-4 text-muted-foreground" />
<span className="text-xs text-muted-foreground">
{maskFile ? maskFile.name : "Select mask image..."}
</span>
<input type="file" accept="image/*" onChange={handleMaskSelect} className="hidden" />
<input
id="erase-object-mask"
type="file"
accept="image/*"
onChange={handleMaskSelect}
className="hidden"
/>
</label>
</div>
@@ -162,6 +173,7 @@ export function EraseObjectSettings() {
/>
) : (
<button
type="button"
onClick={handleProcess}
disabled={!hasFile || !maskFile || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -67,7 +67,7 @@ export function FaviconSettings() {
</p>
<div>
<label className="text-xs font-medium text-muted-foreground">Generated Sizes</label>
<p className="text-xs font-medium text-muted-foreground">Generated Sizes</p>
<div className="mt-1 space-y-0.5">
{SIZES.map((s) => (
<div key={s.name} className="flex justify-between text-xs text-foreground">
@@ -82,6 +82,7 @@ export function FaviconSettings() {
{error && <p className="text-xs text-red-500">{error}</p>}
<button
type="button"
onClick={handleProcess}
disabled={!hasFile || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -62,6 +62,7 @@ export function FindDuplicatesSettings() {
</p>
<button
type="button"
onClick={handleProcess}
disabled={!hasFiles || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -84,10 +85,13 @@ export function FindDuplicatesSettings() {
<p className="text-xs text-muted-foreground">No duplicates found.</p>
) : (
result.duplicateGroups.map((group, gi) => (
<div key={gi} className="p-2 rounded border border-border space-y-1">
<div
key={group.files.map((f) => f.filename).join(",")}
className="p-2 rounded border border-border space-y-1"
>
<p className="text-xs font-medium text-foreground">Group {gi + 1}</p>
{group.files.map((f, fi) => (
<div key={fi} className="flex justify-between text-xs">
{group.files.map((f) => (
<div key={f.filename} className="flex justify-between text-xs">
<span className="text-foreground truncate">{f.filename}</span>
<span className="text-muted-foreground shrink-0 ml-2">{f.similarity}%</span>
</div>
@@ -32,15 +32,17 @@ export function GifToolsSettings() {
return (
<div className="space-y-4">
<div>
<label className="text-xs text-muted-foreground">Mode</label>
<p className="text-xs text-muted-foreground">Mode</p>
<div className="flex gap-1 mt-1">
<button
type="button"
onClick={() => setMode("resize")}
className={`flex-1 text-xs py-1.5 rounded ${mode === "resize" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
>
Resize
</button>
<button
type="button"
onClick={() => setMode("extract")}
className={`flex-1 text-xs py-1.5 rounded ${mode === "extract" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
>
@@ -53,8 +55,11 @@ export function GifToolsSettings() {
<>
<div className="flex gap-2">
<div className="flex-1">
<label className="text-xs text-muted-foreground">Width (px)</label>
<label htmlFor="gif-tools-width" className="text-xs text-muted-foreground">
Width (px)
</label>
<input
id="gif-tools-width"
type="number"
value={width}
onChange={(e) => setWidth(e.target.value)}
@@ -63,8 +68,11 @@ export function GifToolsSettings() {
/>
</div>
<div className="flex-1">
<label className="text-xs text-muted-foreground">Height (px)</label>
<label htmlFor="gif-tools-height" className="text-xs text-muted-foreground">
Height (px)
</label>
<input
id="gif-tools-height"
type="number"
value={height}
onChange={(e) => setHeight(e.target.value)}
@@ -74,8 +82,12 @@ export function GifToolsSettings() {
</div>
</div>
<label className="flex items-center gap-2 text-sm text-foreground">
<label
htmlFor="gif-tools-optimize"
className="flex items-center gap-2 text-sm text-foreground"
>
<input
id="gif-tools-optimize"
type="checkbox"
checked={optimize}
onChange={(e) => setOptimize(e.target.checked)}
@@ -86,8 +98,11 @@ export function GifToolsSettings() {
</>
) : (
<div>
<label className="text-xs text-muted-foreground">Frame Number</label>
<label htmlFor="gif-tools-frame" className="text-xs text-muted-foreground">
Frame Number
</label>
<input
id="gif-tools-frame"
type="number"
value={extractFrame}
onChange={(e) => setExtractFrame(e.target.value)}
@@ -118,6 +133,7 @@ export function GifToolsSettings() {
/>
) : (
<button
type="button"
onClick={handleProcess}
disabled={!hasFile || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -57,8 +57,11 @@ export function ImageToPdfSettings() {
</p>
<div>
<label className="text-xs text-muted-foreground">Page Size</label>
<label htmlFor="image-to-pdf-page-size" className="text-xs text-muted-foreground">
Page Size
</label>
<select
id="image-to-pdf-page-size"
value={pageSize}
onChange={(e) => setPageSize(e.target.value as typeof pageSize)}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
@@ -71,15 +74,17 @@ export function ImageToPdfSettings() {
</div>
<div>
<label className="text-xs text-muted-foreground">Orientation</label>
<p className="text-xs text-muted-foreground">Orientation</p>
<div className="flex gap-1 mt-1">
<button
type="button"
onClick={() => setOrientation("portrait")}
className={`flex-1 text-xs py-1.5 rounded ${orientation === "portrait" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
>
Portrait
</button>
<button
type="button"
onClick={() => setOrientation("landscape")}
className={`flex-1 text-xs py-1.5 rounded ${orientation === "landscape" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
>
@@ -90,10 +95,13 @@ export function ImageToPdfSettings() {
<div>
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Margin</label>
<label htmlFor="image-to-pdf-margin" className="text-xs text-muted-foreground">
Margin
</label>
<span className="text-xs font-mono text-foreground">{margin}pt</span>
</div>
<input
id="image-to-pdf-margin"
type="range"
min={0}
max={100}
@@ -106,6 +114,7 @@ export function ImageToPdfSettings() {
{error && <p className="text-xs text-red-500">{error}</p>}
<button
type="button"
onClick={handleProcess}
disabled={!hasFiles || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -79,6 +79,7 @@ export function InfoSettings() {
return (
<div className="space-y-4">
<button
type="button"
onClick={handleProcess}
disabled={!hasFile || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -122,7 +123,7 @@ export function InfoSettings() {
{/* Histogram */}
<div>
<label className="text-xs font-medium text-muted-foreground">Channel Stats</label>
<p className="text-xs font-medium text-muted-foreground">Channel Stats</p>
<div className="mt-1 space-y-1.5">
{info.histogram.map((ch) => (
<div key={ch.channel} className="space-y-0.5">
+11 -3
View File
@@ -129,9 +129,10 @@ export function OcrSettings() {
<div className="space-y-4">
{/* Engine selector */}
<div>
<label className="text-sm font-medium text-muted-foreground">OCR Engine</label>
<p className="text-sm font-medium text-muted-foreground">OCR Engine</p>
<div className="flex gap-1 mt-1">
<button
type="button"
onClick={() => setEngine("tesseract")}
className={`flex-1 text-xs py-1.5 rounded ${
engine === "tesseract"
@@ -142,6 +143,7 @@ export function OcrSettings() {
Tesseract
</button>
<button
type="button"
onClick={() => setEngine("paddleocr")}
className={`flex-1 text-xs py-1.5 rounded ${
engine === "paddleocr"
@@ -156,8 +158,11 @@ export function OcrSettings() {
{/* Language selector */}
<div>
<label className="text-xs text-muted-foreground">Language</label>
<label htmlFor="ocr-language" className="text-xs text-muted-foreground">
Language
</label>
<select
id="ocr-language"
value={language}
onChange={(e) => setLanguage(e.target.value)}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
@@ -185,6 +190,7 @@ export function OcrSettings() {
/>
) : (
<button
type="button"
onClick={handleProcess}
disabled={!hasFile || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -197,10 +203,11 @@ export function OcrSettings() {
{text !== null && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-xs font-medium text-muted-foreground">
<label htmlFor="ocr-result-text" className="text-xs font-medium text-muted-foreground">
Extracted Text ({detectedEngine})
</label>
<button
type="button"
onClick={handleCopy}
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
>
@@ -209,6 +216,7 @@ export function OcrSettings() {
</button>
</div>
<textarea
id="ocr-result-text"
readOnly
value={text}
rows={8}
@@ -158,7 +158,8 @@ export function PipelineBuilder({
return (
<div className="space-y-6">
{/* File Upload Area */}
<div
<section
aria-label="File upload area"
onDragOver={(e) => e.preventDefault()}
onDrop={handleFileDrop}
className={cn(
@@ -178,6 +179,7 @@ export function PipelineBuilder({
</span>
</div>
<button
type="button"
onClick={() => setFile(null)}
className="p-1 rounded hover:bg-muted text-muted-foreground"
>
@@ -186,6 +188,7 @@ export function PipelineBuilder({
</div>
) : (
<button
type="button"
onClick={handleFileSelect}
className="flex items-center gap-2 mx-auto px-4 py-2 rounded-lg border border-primary text-primary hover:bg-primary/5 transition-colors text-sm"
>
@@ -193,7 +196,7 @@ export function PipelineBuilder({
Upload image to process
</button>
)}
</div>
</section>
{/* Pipeline Steps */}
<div className="space-y-2">
@@ -226,6 +229,7 @@ export function PipelineBuilder({
{/* Controls */}
<div className="flex items-center gap-0.5 shrink-0">
<button
type="button"
onClick={() => setExpandedStep(isExpanded ? null : step.id)}
className="p-1 rounded hover:bg-muted text-muted-foreground"
title="Settings"
@@ -235,6 +239,7 @@ export function PipelineBuilder({
/>
</button>
<button
type="button"
onClick={() => moveStep(step.id, "up")}
disabled={idx === 0}
className="p-1 rounded hover:bg-muted text-muted-foreground disabled:opacity-30"
@@ -243,6 +248,7 @@ export function PipelineBuilder({
<ChevronUp className="h-4 w-4" />
</button>
<button
type="button"
onClick={() => moveStep(step.id, "down")}
disabled={idx === steps.length - 1}
className="p-1 rounded hover:bg-muted text-muted-foreground disabled:opacity-30"
@@ -251,6 +257,7 @@ export function PipelineBuilder({
<ChevronDown className="h-4 w-4" />
</button>
<button
type="button"
onClick={() => removeStep(step.id)}
className="p-1 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
title="Remove"
@@ -283,6 +290,7 @@ export function PipelineBuilder({
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-foreground">Add a step</span>
<button
type="button"
onClick={() => setShowToolPicker(false)}
className="p-1 rounded hover:bg-muted text-muted-foreground"
>
@@ -294,6 +302,7 @@ export function PipelineBuilder({
return (
<button
key={tool.id}
type="button"
onClick={() => addStep(tool.id)}
className="flex items-center gap-2 w-full px-3 py-2 rounded-lg hover:bg-muted text-sm text-left transition-colors"
>
@@ -308,6 +317,7 @@ export function PipelineBuilder({
</div>
) : (
<button
type="button"
onClick={() => setShowToolPicker(true)}
className="flex items-center gap-2 w-full justify-center px-4 py-2.5 rounded-lg border border-dashed border-border text-sm text-muted-foreground hover:border-primary hover:text-primary transition-colors"
>
@@ -343,6 +353,7 @@ export function PipelineBuilder({
{/* Action Buttons */}
<div className="flex items-center gap-3">
<button
type="button"
onClick={handleExecute}
disabled={steps.length === 0 || !file || executing}
className="flex items-center gap-2 px-5 py-2.5 rounded-lg bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
@@ -362,6 +373,7 @@ export function PipelineBuilder({
{!showSaveForm ? (
<button
type="button"
onClick={() => setShowSaveForm(true)}
disabled={steps.length === 0}
className="flex items-center gap-2 px-4 py-2.5 rounded-lg border border-border text-sm text-foreground hover:bg-muted transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
@@ -386,6 +398,7 @@ export function PipelineBuilder({
className="px-3 py-2 rounded-lg border border-border bg-background text-sm text-foreground flex-1 hidden sm:block"
/>
<button
type="button"
onClick={handleSave}
disabled={!saveName.trim() || saving}
className="px-3 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 disabled:opacity-50"
@@ -393,6 +406,7 @@ export function PipelineBuilder({
{saving ? "Saving..." : "Save"}
</button>
<button
type="button"
onClick={() => setShowSaveForm(false)}
className="p-2 rounded-lg hover:bg-muted text-muted-foreground"
>
@@ -526,8 +526,11 @@ export function PipelineStepSettings({ toolId, settings, onChange }: PipelineSte
case "number":
return (
<div key={field.key}>
<label className="text-xs text-muted-foreground">{field.label}</label>
<label htmlFor={`pipeline-${field.key}`} className="text-xs text-muted-foreground">
{field.label}
</label>
<input
id={`pipeline-${field.key}`}
type="number"
value={value != null && value !== "" ? Number(value) : ""}
onChange={(e) =>
@@ -548,8 +551,11 @@ export function PipelineStepSettings({ toolId, settings, onChange }: PipelineSte
case "text":
return (
<div key={field.key}>
<label className="text-xs text-muted-foreground">{field.label}</label>
<label htmlFor={`pipeline-${field.key}`} className="text-xs text-muted-foreground">
{field.label}
</label>
<input
id={`pipeline-${field.key}`}
type="text"
value={String(value ?? "")}
onChange={(e) => updateField(field.key, e.target.value || undefined)}
@@ -560,12 +566,12 @@ export function PipelineStepSettings({ toolId, settings, onChange }: PipelineSte
);
case "select": {
const opts = field.options!;
const opts = field.options ?? [];
// Use button group for <= 4 options, dropdown for more
if (opts.length <= 4) {
return (
<div key={field.key}>
<label className="text-xs text-muted-foreground">{field.label}</label>
<p className="text-xs text-muted-foreground">{field.label}</p>
<div className="flex gap-1 mt-0.5">
{opts.map((opt) => (
<button
@@ -587,8 +593,11 @@ export function PipelineStepSettings({ toolId, settings, onChange }: PipelineSte
}
return (
<div key={field.key}>
<label className="text-xs text-muted-foreground">{field.label}</label>
<label htmlFor={`pipeline-${field.key}`} className="text-xs text-muted-foreground">
{field.label}
</label>
<select
id={`pipeline-${field.key}`}
value={String(value ?? "")}
onChange={(e) => updateField(field.key, e.target.value)}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
@@ -623,15 +632,22 @@ export function PipelineStepSettings({ toolId, settings, onChange }: PipelineSte
case "color":
return (
<div key={field.key}>
<label className="text-xs text-muted-foreground">{field.label}</label>
<label
htmlFor={`pipeline-${field.key}-text`}
className="text-xs text-muted-foreground"
>
{field.label}
</label>
<div className="flex gap-2 mt-0.5">
<input
id={`pipeline-${field.key}-picker`}
type="color"
value={String(value || "#000000").slice(0, 7)}
onChange={(e) => updateField(field.key, e.target.value)}
className="h-8 w-8 rounded border border-border cursor-pointer bg-background"
/>
<input
id={`pipeline-${field.key}-text`}
type="text"
value={String(value ?? "")}
onChange={(e) => updateField(field.key, e.target.value || undefined)}
@@ -52,8 +52,11 @@ export function QrGenerateSettings() {
return (
<div className="space-y-4">
<div>
<label className="text-xs text-muted-foreground">Text / URL</label>
<label htmlFor="qr-text" className="text-xs text-muted-foreground">
Text / URL
</label>
<textarea
id="qr-text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Enter text or URL..."
@@ -64,10 +67,13 @@ export function QrGenerateSettings() {
<div>
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Size</label>
<label htmlFor="qr-size" className="text-xs text-muted-foreground">
Size
</label>
<span className="text-xs font-mono text-foreground">{size}px</span>
</div>
<input
id="qr-size"
type="range"
min={100}
max={2000}
@@ -79,8 +85,11 @@ export function QrGenerateSettings() {
</div>
<div>
<label className="text-xs text-muted-foreground">Error Correction</label>
<label htmlFor="qr-error-correction" className="text-xs text-muted-foreground">
Error Correction
</label>
<select
id="qr-error-correction"
value={errorCorrection}
onChange={(e) => setErrorCorrection(e.target.value as "L" | "M" | "Q" | "H")}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
@@ -94,8 +103,11 @@ export function QrGenerateSettings() {
<div className="flex gap-2">
<div className="flex-1">
<label className="text-xs text-muted-foreground">Foreground</label>
<label htmlFor="qr-foreground" className="text-xs text-muted-foreground">
Foreground
</label>
<input
id="qr-foreground"
type="color"
value={foreground}
onChange={(e) => setForeground(e.target.value)}
@@ -103,8 +115,11 @@ export function QrGenerateSettings() {
/>
</div>
<div className="flex-1">
<label className="text-xs text-muted-foreground">Background</label>
<label htmlFor="qr-background" className="text-xs text-muted-foreground">
Background
</label>
<input
id="qr-background"
type="color"
value={background}
onChange={(e) => setBackground(e.target.value)}
@@ -116,6 +131,7 @@ export function QrGenerateSettings() {
{error && <p className="text-xs text-red-500">{error}</p>}
<button
type="button"
onClick={handleGenerate}
disabled={!text || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -133,7 +149,7 @@ export function QrGenerateSettings() {
style={{ maxHeight: 200 }}
/>
<a
href={downloadUrl!}
href={downloadUrl ?? undefined}
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"
>
@@ -65,13 +65,14 @@ export function RemoveBgSettings() {
<div className="space-y-4">
{/* Subject type */}
<div>
<label className="text-sm font-medium text-muted-foreground">What's in the photo?</label>
<p className="text-sm font-medium text-muted-foreground">What's in the photo?</p>
<div className="grid grid-cols-3 gap-1.5 mt-1.5">
{SUBJECT_OPTIONS.map((opt) => {
const Icon = opt.icon;
return (
<button
key={opt.value}
type="button"
onClick={() => {
setSubject(opt.value);
if (opt.value !== "people") setIsPassport(false);
@@ -105,11 +106,12 @@ export function RemoveBgSettings() {
{/* Quality */}
<div>
<label className="text-sm font-medium text-muted-foreground">Quality</label>
<p className="text-sm font-medium text-muted-foreground">Quality</p>
<div className="grid grid-cols-3 gap-1.5 mt-1.5">
{QUALITY_OPTIONS.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => setQuality(opt.value)}
className={`py-2 px-2 rounded-lg border text-xs font-medium transition-colors ${
quality === opt.value
@@ -126,11 +128,12 @@ export function RemoveBgSettings() {
{/* Background color - intuitive preset buttons */}
<div>
<label className="text-sm font-medium text-muted-foreground">Output Background</label>
<p className="text-sm font-medium text-muted-foreground">Output Background</p>
<div className="flex gap-1.5 mt-1.5 flex-wrap">
{BG_PRESETS.map((preset) => (
<button
key={preset.label}
type="button"
onClick={() => setBgColor(preset.color)}
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border text-xs font-medium transition-colors ${
bgColor === preset.color
@@ -197,6 +200,7 @@ export function RemoveBgSettings() {
/>
) : (
<button
type="button"
onClick={handleProcess}
disabled={!hasFile || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -36,9 +36,12 @@ export function ReplaceColorSettings() {
return (
<div className="space-y-4">
<div>
<label className="text-xs text-muted-foreground">Source Color (to replace)</label>
<label htmlFor="replace-source-color" className="text-xs text-muted-foreground">
Source Color (to replace)
</label>
<div className="flex items-center gap-2 mt-0.5">
<input
id="replace-source-color"
type="color"
value={sourceColor}
onChange={(e) => setSourceColor(e.target.value)}
@@ -60,9 +63,12 @@ export function ReplaceColorSettings() {
{!makeTransparent && (
<div>
<label className="text-xs text-muted-foreground">Target Color (replacement)</label>
<label htmlFor="replace-target-color" className="text-xs text-muted-foreground">
Target Color (replacement)
</label>
<div className="flex items-center gap-2 mt-0.5">
<input
id="replace-target-color"
type="color"
value={targetColor}
onChange={(e) => setTargetColor(e.target.value)}
@@ -75,10 +81,13 @@ export function ReplaceColorSettings() {
<div>
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Tolerance</label>
<label htmlFor="replace-tolerance" className="text-xs text-muted-foreground">
Tolerance
</label>
<span className="text-xs font-mono text-foreground">{tolerance}</span>
</div>
<input
id="replace-tolerance"
type="range"
min={0}
max={255}
@@ -112,6 +121,7 @@ export function ReplaceColorSettings() {
/>
) : (
<button
type="button"
onClick={handleProcess}
disabled={!hasFile || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -144,8 +144,11 @@ export function ResizeSettings() {
<div className="space-y-3">
<div className="flex items-end gap-2">
<div className="flex-1">
<label className="text-xs text-muted-foreground">Width (px)</label>
<label htmlFor="resize-width" className="text-xs text-muted-foreground">
Width (px)
</label>
<input
id="resize-width"
type="number"
value={width}
onChange={(e) => setWidth(e.target.value)}
@@ -162,8 +165,11 @@ export function ResizeSettings() {
{lockAspect ? <Link className="h-4 w-4" /> : <Unlink className="h-4 w-4" />}
</button>
<div className="flex-1">
<label className="text-xs text-muted-foreground">Height (px)</label>
<label htmlFor="resize-height" className="text-xs text-muted-foreground">
Height (px)
</label>
<input
id="resize-height"
type="number"
value={height}
onChange={(e) => setHeight(e.target.value)}
@@ -175,7 +181,7 @@ export function ResizeSettings() {
{/* Fit mode */}
<div>
<label className="text-xs text-muted-foreground">Fit Mode</label>
<p className="text-xs text-muted-foreground">Fit Mode</p>
<div className="flex gap-1 mt-1">
{(Object.keys(FIT_LABELS) as FitMode[]).map((f) => (
<button
@@ -207,8 +213,11 @@ export function ResizeSettings() {
{tab === "scale" && (
<div className="space-y-3">
<div>
<label className="text-xs text-muted-foreground">Scale (%)</label>
<label htmlFor="resize-scale" className="text-xs text-muted-foreground">
Scale (%)
</label>
<input
id="resize-scale"
type="number"
value={percentage}
onChange={(e) => setPercentage(e.target.value)}
@@ -83,7 +83,7 @@ export function RotateSettings({ onPreviewTransform }: RotateSettingsProps) {
<form onSubmit={handleSubmit} className="space-y-4">
{/* Quick rotate */}
<div>
<label className="text-xs text-muted-foreground">Rotate</label>
<p className="text-xs text-muted-foreground">Rotate</p>
<div className="flex items-center gap-2 mt-1">
<button
type="button"
@@ -112,13 +112,16 @@ export function RotateSettings({ onPreviewTransform }: RotateSettingsProps) {
{/* Straighten */}
<div>
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Straighten</label>
<label htmlFor="rotate-straighten" className="text-xs text-muted-foreground">
Straighten
</label>
<span className="text-xs font-mono tabular-nums text-muted-foreground">
{straighten > 0 ? "+" : ""}
{straighten}°
</span>
</div>
<input
id="rotate-straighten"
type="range"
min={-45}
max={45}
@@ -136,7 +139,7 @@ export function RotateSettings({ onPreviewTransform }: RotateSettingsProps) {
{/* Flip buttons */}
<div>
<label className="text-xs text-muted-foreground">Flip</label>
<p className="text-xs text-muted-foreground">Flip</p>
<div className="flex gap-2 mt-1">
<button
type="button"
@@ -46,8 +46,11 @@ export function SmartCropSettings() {
<div className="space-y-4">
{/* Aspect ratio preset */}
<div>
<label className="text-sm font-medium text-muted-foreground">Target Aspect Ratio</label>
<label htmlFor="smart-crop-preset" className="text-sm font-medium text-muted-foreground">
Target Aspect Ratio
</label>
<select
id="smart-crop-preset"
value={preset}
onChange={(e) => handlePreset(e.target.value)}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
@@ -63,8 +66,11 @@ export function SmartCropSettings() {
{/* Width / Height */}
<div className="flex gap-2">
<div className="flex-1">
<label className="text-xs text-muted-foreground">Width (px)</label>
<label htmlFor="smart-crop-width" className="text-xs text-muted-foreground">
Width (px)
</label>
<input
id="smart-crop-width"
type="number"
value={width}
onChange={(e) => {
@@ -76,8 +82,11 @@ export function SmartCropSettings() {
/>
</div>
<div className="flex-1">
<label className="text-xs text-muted-foreground">Height (px)</label>
<label htmlFor="smart-crop-height" className="text-xs text-muted-foreground">
Height (px)
</label>
<input
id="smart-crop-height"
type="number"
value={height}
onChange={(e) => {
@@ -119,6 +128,7 @@ export function SmartCropSettings() {
/>
) : (
<button
type="button"
onClick={handleProcess}
disabled={!hasFile || !canProcess || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -63,11 +63,12 @@ export function SplitSettings() {
return (
<div className="space-y-4">
<div>
<label className="text-xs text-muted-foreground">Grid Presets</label>
<p className="text-xs text-muted-foreground">Grid Presets</p>
<div className="flex gap-1 mt-1 flex-wrap">
{presets.map((p) => (
<button
key={p.label}
type="button"
onClick={() => {
setColumns(p.c);
setRows(p.r);
@@ -82,8 +83,11 @@ export function SplitSettings() {
<div className="flex gap-2">
<div className="flex-1">
<label className="text-xs text-muted-foreground">Columns</label>
<label htmlFor="split-columns" className="text-xs text-muted-foreground">
Columns
</label>
<input
id="split-columns"
type="number"
value={columns}
onChange={(e) => setColumns(Math.max(1, Number(e.target.value)))}
@@ -93,8 +97,11 @@ export function SplitSettings() {
/>
</div>
<div className="flex-1">
<label className="text-xs text-muted-foreground">Rows</label>
<label htmlFor="split-rows" className="text-xs text-muted-foreground">
Rows
</label>
<input
id="split-rows"
type="number"
value={rows}
onChange={(e) => setRows(Math.max(1, Number(e.target.value)))}
@@ -110,6 +117,7 @@ export function SplitSettings() {
{error && <p className="text-xs text-red-500">{error}</p>}
<button
type="button"
onClick={handleProcess}
disabled={!hasFile || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -229,7 +229,7 @@ export function StripMetadataSettings() {
}
const data: MetadataResult = await res.json();
setMetadata(data);
setMetadataCache((prev) => new Map(prev).set(fileKey!, data));
if (fileKey) setMetadataCache((prev) => new Map(prev).set(fileKey, data));
} catch (err) {
if ((err as Error).name === "AbortError") return;
setInspectError(err instanceof Error ? err.message : "Failed to inspect metadata");
@@ -239,7 +239,7 @@ export function StripMetadataSettings() {
})();
return () => controller.abort();
}, [currentFile, fileKey, metadataCache.get]);
}, [currentFile, fileKey, metadataCache]);
const handleStripAllChange = (checked: boolean) => {
setStripAll(checked);
@@ -278,7 +278,7 @@ export function StripMetadataSettings() {
{/* Metadata Display */}
{hasFile && (
<div className="space-y-2">
<label className="text-xs font-medium text-muted-foreground">Current Metadata</label>
<p className="text-xs font-medium text-muted-foreground">Current Metadata</p>
{inspecting && (
<div className="flex items-center gap-2 text-xs text-muted-foreground py-2">
@@ -307,13 +307,13 @@ export function StripMetadataSettings() {
</div>
)}
{hasExif && (
{hasExif && metadata.exif && (
<CollapsibleSection
title="EXIF"
badge={`${Object.keys(metadata.exif!).filter((k) => !SKIP_KEYS.has(k) && !k.startsWith("_")).length} fields`}
badge={`${Object.keys(metadata.exif).filter((k) => !SKIP_KEYS.has(k) && !k.startsWith("_")).length} fields`}
defaultOpen
>
<MetadataGrid data={metadata.exif!} labelMap={EXIF_LABELS} />
<MetadataGrid data={metadata.exif} labelMap={EXIF_LABELS} />
</CollapsibleSection>
)}
@@ -321,31 +321,31 @@ export function StripMetadataSettings() {
<p className="text-[11px] text-muted-foreground">EXIF: {metadata.exifError}</p>
)}
{hasGps && (
{hasGps && metadata.gps && (
<CollapsibleSection
title="GPS"
warning
badge={`${Object.keys(metadata.gps!).filter((k) => !k.startsWith("_")).length} fields`}
badge={`${Object.keys(metadata.gps).filter((k) => !k.startsWith("_")).length} fields`}
>
<MetadataGrid data={metadata.gps!} />
<MetadataGrid data={metadata.gps} />
</CollapsibleSection>
)}
{hasIcc && (
{hasIcc && metadata.icc && (
<CollapsibleSection
title="ICC Profile"
badge={`${Object.keys(metadata.icc!).length} fields`}
badge={`${Object.keys(metadata.icc).length} fields`}
>
<MetadataGrid data={metadata.icc!} />
<MetadataGrid data={metadata.icc} />
</CollapsibleSection>
)}
{hasXmp && (
{hasXmp && metadata.xmp && (
<CollapsibleSection
title="XMP"
badge={`${Object.keys(metadata.xmp!).length} fields`}
badge={`${Object.keys(metadata.xmp).length} fields`}
>
<MetadataGrid data={metadata.xmp!} />
<MetadataGrid data={metadata.xmp} />
</CollapsibleSection>
)}
@@ -374,7 +374,7 @@ export function StripMetadataSettings() {
{/* Individual options */}
<div className="space-y-2">
<label className="text-xs text-muted-foreground">Or select specific metadata:</label>
<p className="text-xs text-muted-foreground">Or select specific metadata:</p>
<label
className={`flex items-center gap-2 text-sm ${stripAll ? "text-muted-foreground" : "text-foreground"}`}
@@ -389,7 +389,7 @@ export function StripMetadataSettings() {
Strip EXIF (camera info, date, exposure)
{hasExif && !stripAll && (
<span className="ml-auto text-[10px] text-muted-foreground">
{Object.keys(metadata?.exif!).filter((k) => !SKIP_KEYS.has(k)).length} fields
{Object.keys(metadata?.exif ?? {}).filter((k) => !SKIP_KEYS.has(k)).length} fields
</span>
)}
</label>
@@ -67,8 +67,11 @@ export function SvgToRasterSettings() {
<div className="space-y-4">
<div className="flex gap-2">
<div className="flex-1">
<label className="text-xs text-muted-foreground">Width (px)</label>
<label htmlFor="svg-raster-width" className="text-xs text-muted-foreground">
Width (px)
</label>
<input
id="svg-raster-width"
type="number"
value={width}
onChange={(e) => setWidth(Number(e.target.value))}
@@ -78,8 +81,11 @@ export function SvgToRasterSettings() {
/>
</div>
<div className="flex-1">
<label className="text-xs text-muted-foreground">Height (px)</label>
<label htmlFor="svg-raster-height" className="text-xs text-muted-foreground">
Height (px)
</label>
<input
id="svg-raster-height"
type="number"
value={height}
onChange={(e) => setHeight(e.target.value)}
@@ -90,8 +96,11 @@ export function SvgToRasterSettings() {
</div>
<div>
<label className="text-xs text-muted-foreground">Output Format</label>
<label htmlFor="svg-raster-format" className="text-xs text-muted-foreground">
Output Format
</label>
<select
id="svg-raster-format"
value={outputFormat}
onChange={(e) => setOutputFormat(e.target.value as "png" | "jpg" | "webp")}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
@@ -115,8 +124,11 @@ export function SvgToRasterSettings() {
{!transparent && (
<div>
<label className="text-xs text-muted-foreground">Background Color</label>
<label htmlFor="svg-raster-bg-color" className="text-xs text-muted-foreground">
Background Color
</label>
<input
id="svg-raster-bg-color"
type="color"
value={backgroundColor.slice(0, 7)}
onChange={(e) => setBackgroundColor(e.target.value)}
@@ -135,6 +147,7 @@ export function SvgToRasterSettings() {
)}
<button
type="button"
onClick={handleProcess}
disabled={!hasFile || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -39,8 +39,11 @@ export function TextOverlaySettings() {
return (
<div className="space-y-4">
<div>
<label className="text-xs text-muted-foreground">Text</label>
<label htmlFor="text-overlay-text" className="text-xs text-muted-foreground">
Text
</label>
<input
id="text-overlay-text"
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
@@ -50,10 +53,13 @@ export function TextOverlaySettings() {
<div>
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Font Size</label>
<label htmlFor="text-overlay-font-size" className="text-xs text-muted-foreground">
Font Size
</label>
<span className="text-xs font-mono text-foreground">{fontSize}px</span>
</div>
<input
id="text-overlay-font-size"
type="range"
min={8}
max={200}
@@ -64,8 +70,11 @@ export function TextOverlaySettings() {
</div>
<div>
<label className="text-xs text-muted-foreground">Text Color</label>
<label htmlFor="text-overlay-color" className="text-xs text-muted-foreground">
Text Color
</label>
<input
id="text-overlay-color"
type="color"
value={color}
onChange={(e) => setColor(e.target.value)}
@@ -74,8 +83,11 @@ export function TextOverlaySettings() {
</div>
<div>
<label className="text-xs text-muted-foreground">Position</label>
<label htmlFor="text-overlay-position" className="text-xs text-muted-foreground">
Position
</label>
<select
id="text-overlay-position"
value={position}
onChange={(e) => setPosition(e.target.value as "top" | "center" | "bottom")}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
@@ -108,8 +120,11 @@ export function TextOverlaySettings() {
{backgroundBox && (
<div>
<label className="text-xs text-muted-foreground">Box Color</label>
<label htmlFor="text-overlay-box-color" className="text-xs text-muted-foreground">
Box Color
</label>
<input
id="text-overlay-box-color"
type="color"
value={backgroundColor}
onChange={(e) => setBackgroundColor(e.target.value)}
@@ -138,6 +153,7 @@ export function TextOverlaySettings() {
/>
) : (
<button
type="button"
onClick={handleProcess}
disabled={!hasFile || processing || !text}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -21,11 +21,12 @@ export function UpscaleSettings() {
<div className="space-y-4">
{/* Scale factor */}
<div>
<label className="text-sm font-medium text-muted-foreground">Scale Factor</label>
<p className="text-sm font-medium text-muted-foreground">Scale Factor</p>
<div className="flex gap-1 mt-1">
{[2, 4].map((s) => (
<button
key={s}
type="button"
onClick={() => setScale(s)}
className={`flex-1 text-xs py-1.5 rounded ${
scale === s
@@ -68,6 +69,7 @@ export function UpscaleSettings() {
/>
) : (
<button
type="button"
onClick={handleProcess}
disabled={!hasFile || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -58,15 +58,17 @@ export function VectorizeSettings() {
return (
<div className="space-y-4">
<div>
<label className="text-xs text-muted-foreground">Color Mode</label>
<p className="text-xs text-muted-foreground">Color Mode</p>
<div className="flex gap-1 mt-1">
<button
type="button"
onClick={() => setColorMode("bw")}
className={`flex-1 text-xs py-1.5 rounded ${colorMode === "bw" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
>
Black & White
</button>
<button
type="button"
onClick={() => setColorMode("color")}
className={`flex-1 text-xs py-1.5 rounded ${colorMode === "color" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
>
@@ -77,10 +79,13 @@ export function VectorizeSettings() {
<div>
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Threshold</label>
<label htmlFor="vectorize-threshold" className="text-xs text-muted-foreground">
Threshold
</label>
<span className="text-xs font-mono text-foreground">{threshold}</span>
</div>
<input
id="vectorize-threshold"
type="range"
min={0}
max={255}
@@ -91,8 +96,11 @@ export function VectorizeSettings() {
</div>
<div>
<label className="text-xs text-muted-foreground">Detail Level</label>
<label htmlFor="vectorize-detail" className="text-xs text-muted-foreground">
Detail Level
</label>
<select
id="vectorize-detail"
value={detail}
onChange={(e) => setDetail(e.target.value as "low" | "medium" | "high")}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
@@ -113,6 +121,7 @@ export function VectorizeSettings() {
)}
<button
type="button"
onClick={handleProcess}
disabled={!hasFile || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -63,7 +63,7 @@ export function WatermarkImageSettings() {
return (
<div className="space-y-4">
<div>
<label className="text-xs text-muted-foreground">Watermark Image</label>
<p className="text-xs text-muted-foreground">Watermark Image</p>
<input
ref={watermarkInputRef}
type="file"
@@ -72,6 +72,7 @@ export function WatermarkImageSettings() {
className="hidden"
/>
<button
type="button"
onClick={() => watermarkInputRef.current?.click()}
className="w-full mt-0.5 px-2 py-2 rounded border border-dashed border-border bg-background text-sm text-muted-foreground hover:text-foreground flex items-center justify-center gap-2"
>
@@ -81,8 +82,11 @@ export function WatermarkImageSettings() {
</div>
<div>
<label className="text-xs text-muted-foreground">Position</label>
<label htmlFor="watermark-image-position" className="text-xs text-muted-foreground">
Position
</label>
<select
id="watermark-image-position"
value={position}
onChange={(e) => setPosition(e.target.value as Position)}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
@@ -97,10 +101,13 @@ export function WatermarkImageSettings() {
<div>
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Opacity</label>
<label htmlFor="watermark-image-opacity" className="text-xs text-muted-foreground">
Opacity
</label>
<span className="text-xs font-mono text-foreground">{opacity}%</span>
</div>
<input
id="watermark-image-opacity"
type="range"
min={0}
max={100}
@@ -112,10 +119,13 @@ export function WatermarkImageSettings() {
<div>
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Scale</label>
<label htmlFor="watermark-image-scale" className="text-xs text-muted-foreground">
Scale
</label>
<span className="text-xs font-mono text-foreground">{scale}%</span>
</div>
<input
id="watermark-image-scale"
type="range"
min={5}
max={100}
@@ -135,6 +145,7 @@ export function WatermarkImageSettings() {
)}
<button
type="button"
onClick={handleProcess}
disabled={!hasFile || !watermarkFile || processing}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@@ -40,8 +40,11 @@ export function WatermarkTextSettings() {
return (
<div className="space-y-4">
<div>
<label className="text-xs text-muted-foreground">Watermark Text</label>
<label htmlFor="watermark-text-text" className="text-xs text-muted-foreground">
Watermark Text
</label>
<input
id="watermark-text-text"
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
@@ -51,10 +54,13 @@ export function WatermarkTextSettings() {
<div>
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Font Size</label>
<label htmlFor="watermark-text-font-size" className="text-xs text-muted-foreground">
Font Size
</label>
<span className="text-xs font-mono text-foreground">{fontSize}px</span>
</div>
<input
id="watermark-text-font-size"
type="range"
min={8}
max={200}
@@ -66,8 +72,11 @@ export function WatermarkTextSettings() {
<div className="flex gap-2">
<div className="flex-1">
<label className="text-xs text-muted-foreground">Color</label>
<label htmlFor="watermark-text-color" className="text-xs text-muted-foreground">
Color
</label>
<input
id="watermark-text-color"
type="color"
value={color}
onChange={(e) => setColor(e.target.value)}
@@ -76,10 +85,13 @@ export function WatermarkTextSettings() {
</div>
<div className="flex-1">
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Opacity</label>
<label htmlFor="watermark-text-opacity" className="text-xs text-muted-foreground">
Opacity
</label>
<span className="text-xs font-mono text-foreground">{opacity}%</span>
</div>
<input
id="watermark-text-opacity"
type="range"
min={0}
max={100}
@@ -91,8 +103,11 @@ export function WatermarkTextSettings() {
</div>
<div>
<label className="text-xs text-muted-foreground">Position</label>
<label htmlFor="watermark-text-position" className="text-xs text-muted-foreground">
Position
</label>
<select
id="watermark-text-position"
value={position}
onChange={(e) => setPosition(e.target.value as Position)}
className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"
@@ -108,10 +123,13 @@ export function WatermarkTextSettings() {
<div>
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Rotation</label>
<label htmlFor="watermark-text-rotation" className="text-xs text-muted-foreground">
Rotation
</label>
<span className="text-xs font-mono text-foreground">{rotation}&deg;</span>
</div>
<input
id="watermark-text-rotation"
type="range"
min={-180}
max={180}
@@ -141,6 +159,7 @@ export function WatermarkTextSettings() {
/>
) : (
<button
type="button"
onClick={handleProcess}
disabled={!hasFile || processing || !text}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"