fix(lint): resolve all biome warnings across API, web, and image-engine

API:
- Replace string concatenation with template literals (batch, pipeline,
  tool-factory, content-aware-resize, passport-photo, remove-background)
- Remove unused imports (teams, color-adjustments, image-enhancement)
- Replace non-null assertions with guard clauses in bg-effects and stitch
- Use optional chaining in docs route

Image-engine:
- Remove unused OutputFormat imports (compress, convert)
- Use local variables instead of reassigning parameters (optimize-for-web, sharpen)

Web:
- Fix useExhaustiveDependencies: remove genuinely redundant deps, add
  biome-ignore comments for intentional patterns (src prop, cleanup fns)
- Replace non-null assertions with null-safe alternatives
- Add accessible titles to inline SVGs (color-settings, image-enhancement-settings)
- Fix noLabelWithoutControl: associate labels via htmlFor/id or use <span>
  (image-to-base64-settings, edit-metadata-settings, qr-generate-settings)
- Replace div[role="button"] with <button> (pdf-to-image-settings)
- Use stable keys instead of array indices (collage-preview, collage-settings)
- Fix noStaticElementInteractions in collage-preview (role="none")
- Remove unused function parameters and imports
This commit is contained in:
Siddharth Kumar Sah
2026-04-14 22:15:37 +08:00
parent 3982ff4136
commit 93c588b239
39 changed files with 212 additions and 117 deletions
+9 -8
View File
@@ -44,8 +44,8 @@ export async function blurBackground(
*/
export async function addDropShadow(subjectBuffer: Buffer, opacity: number): Promise<Buffer> {
const meta = await sharp(subjectBuffer).metadata();
const width = meta.width!;
const height = meta.height!;
if (!meta.width || !meta.height) throw new Error("Cannot read image dimensions");
const { width, height } = meta;
const normalizedOpacity = Math.max(0, Math.min(100, opacity)) / 100;
// Shadow parameters
@@ -121,6 +121,7 @@ export async function createGradientBackground(
*/
export async function compositeOnColor(subjectBuffer: Buffer, hexColor: string): Promise<Buffer> {
const meta = await sharp(subjectBuffer).metadata();
if (!meta.width || !meta.height) throw new Error("Cannot read image dimensions");
const hex = hexColor.replace("#", "");
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
@@ -128,8 +129,8 @@ export async function compositeOnColor(subjectBuffer: Buffer, hexColor: string):
return sharp({
create: {
width: meta.width!,
height: meta.height!,
width: meta.width,
height: meta.height,
channels: 4,
background: { r, g, b, alpha: 255 },
},
@@ -148,8 +149,8 @@ export async function compositeOnImage(
backgroundBuffer: Buffer,
): Promise<Buffer> {
const meta = await sharp(subjectBuffer).metadata();
const width = meta.width!;
const height = meta.height!;
if (!meta.width || !meta.height) throw new Error("Cannot read image dimensions");
const { width, height } = meta;
const resizedBg = await sharp(backgroundBuffer)
.resize(width, height, { fit: "cover" })
@@ -184,8 +185,8 @@ export async function applyEffects(
},
): Promise<Buffer> {
const meta = await sharp(subjectBuffer).metadata();
const width = meta.width!;
const height = meta.height!;
if (!meta.width || !meta.height) throw new Error("Cannot read image dimensions");
const { width, height } = meta;
const bgType = settings.backgroundType || "transparent";
// Step 1: Add shadow to the subject (before background compositing)
+1 -1
View File
@@ -153,7 +153,7 @@ export async function registerBatchRoutes(app: FastifyInstance): Promise<void> {
processBuffer = await decodeHeic(processBuffer);
// Update extension to match decoded format (HEIC/HEIF → PNG)
const ext = processFilename.match(/\.[^.]+$/)?.[0];
if (ext) processFilename = processFilename.slice(0, -ext.length) + ".png";
if (ext) processFilename = `${processFilename.slice(0, -ext.length)}.png`;
}
if (!skipPreprocess) {
processBuffer = await autoOrient(processBuffer);
+1 -1
View File
@@ -81,7 +81,7 @@ function generateLlmsFullTxt(spec: OpenAPISpec): string {
for (const [method, op] of Object.entries(methods)) {
const tag = op.tags?.[0] || "Other";
if (!tagGroups.has(tag)) tagGroups.set(tag, []);
tagGroups.get(tag)!.push({ method: method.toUpperCase(), path, op });
tagGroups.get(tag)?.push({ method: method.toUpperCase(), path, op });
}
}
+2 -2
View File
@@ -106,7 +106,7 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
fileBuffer = await decodeHeic(fileBuffer);
// Update filename extension to match the decoded format
const ext = filename.match(/\.[^.]+$/)?.[0];
if (ext) filename = filename.slice(0, -ext.length) + ".png";
if (ext) filename = `${filename.slice(0, -ext.length)}.png`;
} catch (err) {
return reply.status(422).send({
error: "Failed to decode HEIC file. Ensure libheif-examples is installed.",
@@ -513,7 +513,7 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
if (validation.format === "heif") {
currentBuffer = await decodeHeic(currentBuffer);
const ext = currentFilename.match(/\.[^.]+$/)?.[0];
if (ext) currentFilename = currentFilename.slice(0, -ext.length) + ".png";
if (ext) currentFilename = `${currentFilename.slice(0, -ext.length)}.png`;
}
// Normalize EXIF orientation
+1 -1
View File
@@ -11,7 +11,7 @@ import { randomUUID } from "node:crypto";
import { eq, sql } from "drizzle-orm";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { db, schema } from "../db/index.js";
import { requireAdmin, requireAuth } from "../plugins/auth.js";
import { requireAdmin } from "../plugins/auth.js";
function validateTeamName(name: unknown): string | null {
if (typeof name !== "string") return "Team name is required";
+1 -1
View File
@@ -156,7 +156,7 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
try {
fileBuffer = await decodeHeic(fileBuffer);
const ext = filename.match(/\.[^.]+$/)?.[0];
if (ext) filename = filename.slice(0, -ext.length) + ".png";
if (ext) filename = `${filename.slice(0, -ext.length)}.png`;
} catch (err) {
return reply.status(422).send({
error: "Failed to decode HEIC file. Ensure libheif-examples is installed.",
@@ -1,7 +1,6 @@
import {
brightness as adjustBrightness,
contrast as adjustContrast,
saturation as adjustSaturation,
sharpen as adjustSharpen,
colorChannels,
grayscale,
@@ -65,7 +65,7 @@ export function registerContentAwareResize(app: FastifyInstance) {
try {
fileBuffer = await decodeHeic(fileBuffer);
const ext = filename.match(/\.[^.]+$/)?.[0];
if (ext) filename = filename.slice(0, -ext.length) + ".png";
if (ext) filename = `${filename.slice(0, -ext.length)}.png`;
} catch (err) {
return reply.status(422).send({
error: "Failed to decode HEIC/HEIF file",
@@ -4,7 +4,6 @@ import sharp from "sharp";
import { z } from "zod";
import { autoOrient } from "../../lib/auto-orient.js";
import { validateImageBuffer } from "../../lib/file-validation.js";
import { sanitizeFilename } from "../../lib/filename.js";
import { decodeHeic } from "../../lib/heic-converter.js";
import { resolveOutputFormat } from "../../lib/output-format.js";
import { createToolRoute } from "../tool-factory.js";
+1 -1
View File
@@ -160,7 +160,7 @@ export function registerPassportPhoto(app: FastifyInstance) {
if (validation.format === "heif") {
fileBuffer = await decodeHeic(fileBuffer);
const ext = filename.match(/\.[^.]+$/)?.[0];
if (ext) filename = filename.slice(0, -ext.length) + ".png";
if (ext) filename = `${filename.slice(0, -ext.length)}.png`;
}
// Auto-orient to fix EXIF rotation
@@ -83,7 +83,7 @@ export function registerRemoveBackground(app: FastifyInstance) {
if (validation.format === "heif") {
fileBuffer = await decodeHeic(fileBuffer);
const ext = filename.match(/\.[^.]+$/)?.[0];
if (ext) filename = filename.slice(0, -ext.length) + ".png";
if (ext) filename = `${filename.slice(0, -ext.length)}.png`;
}
// Auto-orient to fix EXIF rotation
+3 -2
View File
@@ -205,8 +205,9 @@ export function registerStitch(app: FastifyInstance) {
if (settings.cornerRadius > 0) {
const meta = await sharp(result).metadata();
const w = meta.width!;
const h = meta.height!;
if (!meta.width || !meta.height) throw new Error("Cannot read image dimensions");
const w = meta.width;
const h = meta.height;
const r = Math.min(settings.cornerRadius, Math.floor(Math.min(w, h) / 2));
const mask = Buffer.from(
@@ -90,6 +90,7 @@ export function ImageViewer({
}, []);
// Reset state on src change
// biome-ignore lint/correctness/useExhaustiveDependencies: src is a prop that triggers state reset
useEffect(() => {
setZoom(DEFAULT_ZOOM);
setFitMode("fit");
@@ -46,7 +46,10 @@ export function MultiImageViewer() {
const hasNext = selectedIndex < entries.length - 1;
const hasProcessed = !!currentEntry.processedUrl;
const isPreviewable = hasProcessed && canBrowserPreview(currentEntry.processedUrl!);
const isPreviewable =
hasProcessed && currentEntry.processedUrl
? canBrowserPreview(currentEntry.processedUrl)
: false;
const displayUrl = currentEntry.processedPreviewUrl ?? currentEntry.processedUrl;
const processedFilename = currentEntry.processedUrl
@@ -149,7 +149,7 @@ function CollageCanvas({ template }: { template: CollageTemplate }) {
return (
<div
className="flex-1 flex items-center justify-center p-4 min-h-0 overflow-auto"
role="presentation"
role="none"
onClick={() => store.setSelectedCell(null)}
onKeyDown={(e) => e.key === "Escape" && store.setSelectedCell(null)}
>
@@ -178,7 +178,7 @@ function CollageCanvas({ template }: { template: CollageTemplate }) {
return (
<CollageCell
key={`${template.id}-${i}`}
key={`${template.id}-${cell.gridColumn}-${cell.gridRow}`}
cellIndex={i}
image={img}
transform={transform}
@@ -290,6 +290,7 @@ function CollageCell({
);
return (
// biome-ignore lint/a11y/useSemanticElements: cell requires drag/zoom interactions incompatible with button element
<div
ref={cellRef}
role="button"
@@ -406,9 +406,9 @@ function TemplateDiagram({ template, size }: { template: CollageTemplate; size:
role="img"
aria-label={template.label}
>
{rects.map((r, i) => (
{rects.map((r) => (
<rect
key={`${template.id}-${i}`}
key={`${template.id}-${r.x}-${r.y}-${r.w}-${r.h}`}
x={padding + r.x}
y={padding + r.y}
width={r.w}
@@ -14,7 +14,7 @@ interface ColorControlsProps {
}
export function ColorControls({
toolId,
toolId: _toolId,
settings: initialSettings,
onChange,
onPreviewFilter,
@@ -117,8 +117,6 @@ export function ColorControls({
contrast,
exposure,
saturation,
temperature,
tint,
hue,
sharpness,
hasChannelChanges,
@@ -150,6 +148,7 @@ export function ColorControls({
{/* Hidden SVG filters for live preview */}
{hasTempTint && (
<svg width="0" height="0" style={{ position: "absolute" }}>
<title>Temperature and tint color filter</title>
<filter id="ashim-temp-tint-filter" colorInterpolationFilters="sRGB">
<feColorMatrix
type="matrix"
@@ -160,6 +159,7 @@ export function ColorControls({
)}
{hasChannelChanges && (
<svg width="0" height="0" style={{ position: "absolute" }}>
<title>Color channel filter</title>
<filter id="ashim-channel-filter" colorInterpolationFilters="sRGB">
<feColorMatrix
type="matrix"
@@ -170,6 +170,7 @@ export function ColorControls({
)}
{sharpness > 0 && (
<svg width="0" height="0" style={{ position: "absolute" }}>
<title>Sharpen filter</title>
<filter id="ashim-sharpen-filter" colorInterpolationFilters="sRGB">
<feConvolveMatrix
order="3"
@@ -536,8 +536,14 @@ export function EditMetadataSettings() {
</p>
<div className="flex gap-2 items-end">
<div className="space-y-1">
<label className="text-xs font-medium text-foreground">Direction</label>
<label
htmlFor="em-date-shift-direction"
className="text-xs font-medium text-foreground"
>
Direction
</label>
<select
id="em-date-shift-direction"
value={form.dateShiftDirection}
onChange={(e) => setField("dateShiftDirection", e.target.value as "+" | "-")}
className="px-2.5 py-1.5 rounded-md border border-input bg-background text-sm"
@@ -45,6 +45,7 @@ export function FaviconSettings() {
setProcessing(false);
};
// biome-ignore lint/correctness/useExhaustiveDependencies: cleanup uses only stable refs and state setters
const handleProcess = useCallback(() => {
if (files.length === 0) return;
@@ -30,6 +30,7 @@ export function FindDuplicatesSettings() {
const [error, setError] = useState<string | null>(null);
// Reset scan results when files change
// biome-ignore lint/correctness/useExhaustiveDependencies: files is a store value that triggers reset when changed
useEffect(() => {
resetDuplicates();
setError(null);
@@ -147,7 +147,6 @@ export function GifToolsControls({ settings: initialSettings, onChange }: GifToo
width,
height,
percentage,
lockAspect,
colors,
dither,
effort,
@@ -127,7 +127,7 @@ interface ImageEnhancementControlsProps {
}
export function ImageEnhancementControls({
settings: initialSettings,
settings: _initialSettings,
onChange,
onPreviewFilter,
}: ImageEnhancementControlsProps) {
@@ -245,6 +245,7 @@ export function ImageEnhancementControls({
{/* Hidden SVG filters for preview */}
{toggles.whiteBalance && Math.abs(tempAdj) > 0.02 && (
<svg width="0" height="0" style={{ position: "absolute" }}>
<title>White balance temperature filter</title>
<filter id="ashim-enhance-temp-filter" colorInterpolationFilters="sRGB">
<feColorMatrix
type="matrix"
@@ -255,6 +256,7 @@ export function ImageEnhancementControls({
)}
{toggles.sharpness && sharpAdj > 0.02 && (
<svg width="0" height="0" style={{ position: "absolute" }}>
<title>Sharpen filter</title>
<filter id="ashim-enhance-sharpen-filter" colorInterpolationFilters="sRGB">
<feConvolveMatrix
order="3"
@@ -90,7 +90,7 @@ function CopyButton({ text, label }: { text: string; label?: string }) {
function FileResult({ result }: { result: Base64Result }) {
const [activeTab, setActiveTab] = useState<TabId>("datauri");
const tab = TABS.find((t) => t.id === activeTab)!;
const tab = TABS.find((t) => t.id === activeTab) ?? TABS[0];
const output = tab.generate(result);
const handleDownload = useCallback(() => {
@@ -74,7 +74,7 @@ export function ImageToBase64Settings() {
<div className="space-y-4">
{/* Output Format */}
<div>
<label className="text-xs font-medium text-muted-foreground">Output Image Format</label>
<span className="text-xs font-medium text-muted-foreground">Output Image Format</span>
<p className="text-[10px] text-muted-foreground/70 mb-1.5">
Convert before encoding to control MIME type and size
</p>
@@ -100,10 +100,13 @@ export function ImageToBase64Settings() {
{showQuality && (
<div>
<div className="flex items-center justify-between">
<label className="text-xs font-medium text-muted-foreground">Quality</label>
<label htmlFor="b64-quality" className="text-xs font-medium text-muted-foreground">
Quality
</label>
<span className="text-xs font-mono text-foreground">{quality}%</span>
</div>
<input
id="b64-quality"
type="range"
min={1}
max={100}
@@ -119,8 +122,11 @@ export function ImageToBase64Settings() {
{/* Max Width */}
<div>
<label className="text-xs font-medium text-muted-foreground">Max Width (px)</label>
<label htmlFor="b64-max-width" className="text-xs font-medium text-muted-foreground">
Max Width (px)
</label>
<input
id="b64-max-width"
type="number"
min={0}
value={maxWidth}
@@ -132,8 +138,11 @@ export function ImageToBase64Settings() {
{/* Max Height */}
<div>
<label className="text-xs font-medium text-muted-foreground">Max Height (px)</label>
<label htmlFor="b64-max-height" className="text-xs font-medium text-muted-foreground">
Max Height (px)
</label>
<input
id="b64-max-height"
type="number"
min={0}
value={maxHeight}
@@ -140,6 +140,7 @@ export function ImageToPdfSettings() {
setProcessing(false);
};
// biome-ignore lint/correctness/useExhaustiveDependencies: cleanup uses only stable refs and state setters
const handleProcess = useCallback(() => {
if (files.length === 0) return;
@@ -522,6 +522,11 @@ export function PassportPhotoSettings() {
documentType,
bgColor,
maxFileSizeKb,
dpi,
zoom,
isCustom,
customWidthMm,
customHeightMm,
adjustX,
adjustY,
setGenerating,
@@ -70,19 +70,12 @@ export function PdfToImageSettings() {
<div className="space-y-4">
{/* PDF upload area */}
{!store.file ? (
<div
role="button"
tabIndex={0}
<button
type="button"
onDragOver={(e) => e.preventDefault()}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
fileInputRef.current?.click();
}
}}
className="border-2 border-dashed border-border rounded-lg p-6 text-center cursor-pointer hover:border-primary/50 transition-colors"
className="border-2 border-dashed border-border rounded-lg p-6 text-center cursor-pointer hover:border-primary/50 transition-colors w-full"
>
<FileUp className="h-8 w-8 mx-auto mb-2 text-muted-foreground" />
<p className="text-sm text-muted-foreground">Drop a PDF here or click to select</p>
@@ -93,7 +86,7 @@ export function PdfToImageSettings() {
className="hidden"
onChange={(e) => handleFileChange(e.target.files)}
/>
</div>
</button>
) : (
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted">
<div className="flex-1 min-w-0">
@@ -95,15 +95,18 @@ function SortableStep({
className="flex items-center gap-2 p-3 w-full text-left"
>
{/* Drag handle */}
<span
{...attributes}
{...listeners}
className="cursor-grab active:cursor-grabbing p-0.5 rounded hover:bg-muted text-muted-foreground"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<GripVertical className="h-4 w-4" />
</span>
{
// biome-ignore lint/a11y/noStaticElementInteractions: dnd-kit drag handle spreads its own event handlers
<span
{...attributes}
{...listeners}
className="cursor-grab active:cursor-grabbing p-0.5 rounded hover:bg-muted text-muted-foreground"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<GripVertical className="h-4 w-4" />
</span>
}
{/* Step number badge */}
<span className="w-6 h-6 rounded-full bg-primary/10 text-primary text-xs font-semibold flex items-center justify-center shrink-0">
@@ -122,27 +125,18 @@ function SortableStep({
<span className="flex-1" />
{/* Remove button */}
<span
role="button"
tabIndex={0}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onRemove();
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.stopPropagation();
onRemove();
}
}}
title="Remove"
className="p-1 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
>
<X className="h-4 w-4" />
</span>
</button>
</button>
{/* Inline settings panel */}
<div className={isExpanded ? "border-t border-border p-3 bg-muted/10 space-y-3" : "hidden"}>
<PipelineStepSettings
toolId={step.toolId}
@@ -150,6 +144,7 @@ function SortableStep({
onChange={onUpdateSettings}
/>
</div>
;
</div>
);
}
@@ -92,6 +92,7 @@ export function QrGeneratePreview() {
]);
// Create QR instance on mount
// biome-ignore lint/correctness/useExhaustiveDependencies: options is intentionally excluded to only create QR instance once on mount
useEffect(() => {
const qr = new QRCodeStyling(options as never);
qrRef.current = qr;
@@ -99,7 +100,7 @@ export function QrGeneratePreview() {
clearChildren(containerRef.current);
qr.append(containerRef.current);
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
}, []);
// Update QR on state changes (debounced)
useEffect(() => {
@@ -80,8 +80,11 @@ function UrlForm() {
const { textData, setTextData } = useQrStore();
return (
<div>
<label className="text-xs text-muted-foreground">URL</label>
<label htmlFor="qr-url" className="text-xs text-muted-foreground">
URL
</label>
<input
id="qr-url"
type="url"
value={textData}
onChange={(e) => setTextData(e.target.value)}
@@ -97,8 +100,11 @@ function TextForm() {
const { textData, setTextData } = useQrStore();
return (
<div>
<label className="text-xs text-muted-foreground">Text</label>
<label htmlFor="qr-text" className="text-xs text-muted-foreground">
Text
</label>
<textarea
id="qr-text"
value={textData}
onChange={(e) => setTextData(e.target.value)}
placeholder="Enter any text..."
@@ -115,8 +121,11 @@ function WifiForm() {
return (
<div className="space-y-2">
<div>
<label className="text-xs text-muted-foreground">Network Name (SSID)</label>
<label htmlFor="qr-wifi-ssid" className="text-xs text-muted-foreground">
Network Name (SSID)
</label>
<input
id="qr-wifi-ssid"
type="text"
value={wifiData.ssid}
onChange={(e) => setWifiData({ ssid: e.target.value })}
@@ -125,8 +134,11 @@ function WifiForm() {
/>
</div>
<div>
<label className="text-xs text-muted-foreground">Password</label>
<label htmlFor="qr-wifi-password" className="text-xs text-muted-foreground">
Password
</label>
<input
id="qr-wifi-password"
type="text"
value={wifiData.password}
onChange={(e) => setWifiData({ password: e.target.value })}
@@ -135,8 +147,11 @@ function WifiForm() {
/>
</div>
<div>
<label className="text-xs text-muted-foreground">Encryption</label>
<label htmlFor="qr-wifi-encryption" className="text-xs text-muted-foreground">
Encryption
</label>
<select
id="qr-wifi-encryption"
value={wifiData.encryption}
onChange={(e) => setWifiData({ encryption: e.target.value as "WPA" | "WEP" | "nopass" })}
className={INPUT_CLASS}
@@ -163,8 +178,11 @@ function VCardForm() {
const { vcardData, setVcardData } = useQrStore();
const field = (label: string, key: keyof typeof vcardData, placeholder: string) => (
<div key={key}>
<label className="text-xs text-muted-foreground">{label}</label>
<label htmlFor={`qr-vcard-${key}`} className="text-xs text-muted-foreground">
{label}
</label>
<input
id={`qr-vcard-${key}`}
type="text"
value={vcardData[key]}
onChange={(e) => setVcardData({ [key]: e.target.value })}
@@ -193,8 +211,11 @@ function EmailForm() {
return (
<div className="space-y-2">
<div>
<label className="text-xs text-muted-foreground">To</label>
<label htmlFor="qr-email-to" className="text-xs text-muted-foreground">
To
</label>
<input
id="qr-email-to"
type="email"
value={emailData.to}
onChange={(e) => setEmailData({ to: e.target.value })}
@@ -203,8 +224,11 @@ function EmailForm() {
/>
</div>
<div>
<label className="text-xs text-muted-foreground">Subject</label>
<label htmlFor="qr-email-subject" className="text-xs text-muted-foreground">
Subject
</label>
<input
id="qr-email-subject"
type="text"
value={emailData.subject}
onChange={(e) => setEmailData({ subject: e.target.value })}
@@ -213,8 +237,11 @@ function EmailForm() {
/>
</div>
<div>
<label className="text-xs text-muted-foreground">Body</label>
<label htmlFor="qr-email-body" className="text-xs text-muted-foreground">
Body
</label>
<textarea
id="qr-email-body"
value={emailData.body}
onChange={(e) => setEmailData({ body: e.target.value })}
placeholder="Email body..."
@@ -230,8 +257,11 @@ function PhoneForm() {
const { phoneData, setPhoneData } = useQrStore();
return (
<div>
<label className="text-xs text-muted-foreground">Phone Number</label>
<label htmlFor="qr-phone" className="text-xs text-muted-foreground">
Phone Number
</label>
<input
id="qr-phone"
type="tel"
value={phoneData}
onChange={(e) => setPhoneData(e.target.value)}
@@ -247,8 +277,11 @@ function SmsForm() {
return (
<div className="space-y-2">
<div>
<label className="text-xs text-muted-foreground">Phone Number</label>
<label htmlFor="qr-sms-phone" className="text-xs text-muted-foreground">
Phone Number
</label>
<input
id="qr-sms-phone"
type="tel"
value={smsData.phone}
onChange={(e) => setSmsData({ phone: e.target.value })}
@@ -257,8 +290,11 @@ function SmsForm() {
/>
</div>
<div>
<label className="text-xs text-muted-foreground">Message</label>
<label htmlFor="qr-sms-message" className="text-xs text-muted-foreground">
Message
</label>
<textarea
id="qr-sms-message"
value={smsData.message}
onChange={(e) => setSmsData({ message: e.target.value })}
placeholder="Your message..."
@@ -405,7 +441,7 @@ export function QrGenerateSettings() {
<CollapsibleSection title="Style" defaultOpen>
<div className="space-y-3">
<div>
<label className="text-xs text-muted-foreground mb-1.5 block">Dot Pattern</label>
<span className="text-xs text-muted-foreground mb-1.5 block">Dot Pattern</span>
<div className="grid grid-cols-3 gap-1.5">
{DOT_TYPES.map(({ value, label }) => (
<PillButton
@@ -419,7 +455,7 @@ export function QrGenerateSettings() {
</div>
</div>
<div>
<label className="text-xs text-muted-foreground mb-1.5 block">Corner Square</label>
<span className="text-xs text-muted-foreground mb-1.5 block">Corner Square</span>
<div className="grid grid-cols-2 gap-1.5">
{CORNER_SQUARE_TYPES.map(({ value, label }) => (
<PillButton
@@ -433,7 +469,7 @@ export function QrGenerateSettings() {
</div>
</div>
<div>
<label className="text-xs text-muted-foreground mb-1.5 block">Corner Dot</label>
<span className="text-xs text-muted-foreground mb-1.5 block">Corner Dot</span>
<div className="grid grid-cols-3 gap-1.5">
{CORNER_DOT_TYPES.map(({ value, label }) => (
<PillButton
@@ -453,7 +489,7 @@ export function QrGenerateSettings() {
<CollapsibleSection title="Colors">
<div className="space-y-3">
<div>
<label className="text-xs text-muted-foreground">Dot Color</label>
<span className="text-xs text-muted-foreground">Dot Color</span>
<div className="flex items-center gap-2 mt-0.5">
<input
type="color"
@@ -487,8 +523,11 @@ export function QrGenerateSettings() {
<div className="space-y-2 pl-2 border-l-2 border-primary/20 ml-1">
<div className="flex gap-2">
<div className="flex-1">
<label className="text-[10px] text-muted-foreground">From</label>
<label htmlFor="qr-gradient-from" className="text-[10px] text-muted-foreground">
From
</label>
<input
id="qr-gradient-from"
type="color"
value={store.dotGradientColor1}
onChange={(e) => store.setDotGradientColor1(e.target.value)}
@@ -496,8 +535,11 @@ export function QrGenerateSettings() {
/>
</div>
<div className="flex-1">
<label className="text-[10px] text-muted-foreground">To</label>
<label htmlFor="qr-gradient-to" className="text-[10px] text-muted-foreground">
To
</label>
<input
id="qr-gradient-to"
type="color"
value={store.dotGradientColor2}
onChange={(e) => store.setDotGradientColor2(e.target.value)}
@@ -522,12 +564,18 @@ export function QrGenerateSettings() {
{store.dotGradientType === "linear" && (
<div>
<div className="flex justify-between items-center">
<label className="text-[10px] text-muted-foreground">Rotation</label>
<label
htmlFor="qr-gradient-rotation"
className="text-[10px] text-muted-foreground"
>
Rotation
</label>
<span className="text-[10px] font-mono text-foreground">
{store.dotGradientRotation}&deg;
</span>
</div>
<input
id="qr-gradient-rotation"
type="range"
min={0}
max={360}
@@ -543,7 +591,7 @@ export function QrGenerateSettings() {
<div>
<div className="flex items-center justify-between">
<label className="text-xs text-muted-foreground">Background</label>
<span className="text-xs text-muted-foreground">Background</span>
<label className="flex items-center gap-1 text-[10px] text-muted-foreground cursor-pointer">
<input
type="checkbox"
@@ -586,8 +634,14 @@ export function QrGenerateSettings() {
{store.useCustomCornerColors && (
<div className="flex gap-2">
<div className="flex-1">
<label className="text-[10px] text-muted-foreground">Corner Square</label>
<label
htmlFor="qr-corner-square-color"
className="text-[10px] text-muted-foreground"
>
Corner Square
</label>
<input
id="qr-corner-square-color"
type="color"
value={store.cornerSquareColor}
onChange={(e) => store.setCornerSquareColor(e.target.value)}
@@ -595,8 +649,11 @@ export function QrGenerateSettings() {
/>
</div>
<div className="flex-1">
<label className="text-[10px] text-muted-foreground">Corner Dot</label>
<label htmlFor="qr-corner-dot-color" className="text-[10px] text-muted-foreground">
Corner Dot
</label>
<input
id="qr-corner-dot-color"
type="color"
value={store.cornerDotColor}
onChange={(e) => store.setCornerDotColor(e.target.value)}
@@ -651,12 +708,15 @@ export function QrGenerateSettings() {
<>
<div>
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Logo Size</label>
<label htmlFor="qr-logo-size" className="text-xs text-muted-foreground">
Logo Size
</label>
<span className="text-xs font-mono text-foreground">
{Math.round(store.logoSize * 100)}%
</span>
</div>
<input
id="qr-logo-size"
type="range"
min={0.1}
max={0.5}
@@ -668,10 +728,13 @@ export function QrGenerateSettings() {
</div>
<div>
<div className="flex justify-between items-center">
<label className="text-xs text-muted-foreground">Logo Margin</label>
<label htmlFor="qr-logo-margin" className="text-xs text-muted-foreground">
Logo Margin
</label>
<span className="text-xs font-mono text-foreground">{store.logoMargin}px</span>
</div>
<input
id="qr-logo-margin"
type="range"
min={0}
max={20}
@@ -699,7 +762,7 @@ export function QrGenerateSettings() {
<CollapsibleSection title="Download" defaultOpen>
<div className="space-y-3">
<div>
<label className="text-xs text-muted-foreground mb-1.5 block">Format</label>
<span className="text-xs text-muted-foreground mb-1.5 block">Format</span>
<div className="grid grid-cols-2 gap-1.5">
{DOWNLOAD_FORMATS.map(({ value, label, desc }) => (
<button
@@ -729,10 +792,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">{store.size}px</span>
</div>
<input
id="qr-size"
type="range"
min={200}
max={2000}
@@ -744,8 +810,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={store.errorCorrection}
onChange={(e) => store.setErrorCorrection(e.target.value as "L" | "M" | "Q" | "H")}
className={INPUT_CLASS}
@@ -82,7 +82,7 @@ export interface RemoveBgControlsProps {
onChange: (settings: Record<string, unknown>) => void;
}
export function RemoveBgControls({ settings, onChange }: RemoveBgControlsProps) {
export function RemoveBgControls({ settings: _settings, onChange }: RemoveBgControlsProps) {
const [subject, setSubject] = useState<SubjectType>("people");
const [quality, setQuality] = useState<Quality>("balanced");
const [isPassport, setIsPassport] = useState(true);
@@ -543,7 +543,7 @@ export function RemoveBgSettings({ onBgPreview }: RemoveBgSettingsProps = {}) {
const [bgJobId, setBgJobId] = useState<string | null>(null);
const [bgFilename, setBgFilename] = useState<string | null>(null);
const [bgOriginalUrl, setBgOriginalUrl] = useState<string | null>(null);
const [effectsDownloadUrl, setEffectsDownloadUrl] = useState<string | null>(null);
const [_effectsDownloadUrl, setEffectsDownloadUrl] = useState<string | null>(null);
const [applyingEffects, setApplyingEffects] = useState(false);
const [effectsError, setEffectsError] = useState<string | null>(null);
@@ -54,6 +54,7 @@ export function SplitCanvas() {
}, [setImageDimensions, updateDisplaySize]);
// Reset error when blob URL changes (e.g., HEIC decoded preview ready)
// biome-ignore lint/correctness/useExhaustiveDependencies: src is a prop that triggers state reset
useEffect(() => {
setLoadError(false);
setDisplaySize(null);
@@ -67,6 +68,7 @@ export function SplitCanvas() {
}, [updateDisplaySize]);
// Re-measure when grid changes (in case image dimensions affect tile-size calculation)
// biome-ignore lint/correctness/useExhaustiveDependencies: grid.columns and grid.rows trigger re-measurement of display
useEffect(() => {
updateDisplaySize();
}, [grid.columns, grid.rows, updateDisplaySize]);
@@ -188,10 +190,11 @@ export function SplitCanvas() {
viewBox="0 0 100 100"
preserveAspectRatio="none"
>
<title>Split grid overlay</title>
{/* Vertical lines */}
{colPositions.map((x, i) => (
{colPositions.map((x) => (
<line
key={`v-${i}`}
key={`v-${x}`}
x1={x}
y1={0}
x2={x}
@@ -202,9 +205,9 @@ export function SplitCanvas() {
/>
))}
{/* Horizontal lines */}
{rowPositions.map((y, i) => (
{rowPositions.map((y) => (
<line
key={`h-${i}`}
key={`h-${y}`}
x1={0}
y1={y}
x2={100}
@@ -77,6 +77,7 @@ export function SplitSettings() {
? `Tiles will be very small (${tileDims.width}x${tileDims.height}px)`
: null;
// biome-ignore lint/correctness/useExhaustiveDependencies: files is a store value that triggers reset when changed
useEffect(() => {
setTiles([]);
setZipBlobUrl(null);
@@ -188,7 +188,7 @@ export function UpscaleControls({ settings: initialSettings, onChange }: Upscale
}
export function UpscaleSettings() {
const { files, entries } = useFileStore();
const { files } = useFileStore();
const {
processFiles,
processAllFiles,
+1 -1
View File
@@ -1,4 +1,4 @@
import { type FormEvent, useRef, useState } from "react";
import { type FormEvent, useState } from "react";
import { formatHeaders } from "@/lib/api";
/**