init
This commit is contained in:
@@ -0,0 +1,147 @@
|
|||||||
|
import { createCanvas, loadImage, GlobalFonts } from "@napi-rs/canvas";
|
||||||
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { PassThrough, Readable } from "node:stream";
|
||||||
|
import { createZipArchive } from "@/lib/create-zip";
|
||||||
|
import { drawIdText } from "@/lib/draw-id";
|
||||||
|
import { isPresetFont } from "@/lib/fonts";
|
||||||
|
import {
|
||||||
|
CUSTOM_FONT_FAMILY,
|
||||||
|
formatUserId,
|
||||||
|
MAX_FONT_BYTES,
|
||||||
|
MAX_IMAGE_BYTES,
|
||||||
|
parseGenerateConfig,
|
||||||
|
sanitizeFilename,
|
||||||
|
toIdDrawStyle,
|
||||||
|
} from "@/lib/id-config";
|
||||||
|
import { registerPresetFonts } from "@/lib/register-fonts";
|
||||||
|
|
||||||
|
export const runtime = "nodejs";
|
||||||
|
export const maxDuration = 300;
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
function jsonError(message: string, status = 400): Response {
|
||||||
|
return Response.json({ error: message }, { status });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(req: Request): Promise<Response> {
|
||||||
|
let tmpDir: string | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const form = await req.formData();
|
||||||
|
const imageFile = form.get("image");
|
||||||
|
const fontFile = form.get("font");
|
||||||
|
const configRaw = form.get("config");
|
||||||
|
|
||||||
|
if (!(imageFile instanceof File)) {
|
||||||
|
return jsonError("A base image is required");
|
||||||
|
}
|
||||||
|
if (imageFile.size === 0) {
|
||||||
|
return jsonError("The base image is empty");
|
||||||
|
}
|
||||||
|
if (imageFile.size > MAX_IMAGE_BYTES) {
|
||||||
|
return jsonError("Base image must be 10MB or smaller");
|
||||||
|
}
|
||||||
|
if (typeof configRaw !== "string") {
|
||||||
|
return jsonError("Missing generate config");
|
||||||
|
}
|
||||||
|
|
||||||
|
let config;
|
||||||
|
try {
|
||||||
|
config = parseGenerateConfig(JSON.parse(configRaw));
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : "Invalid generate config";
|
||||||
|
return jsonError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.useCustomFont) {
|
||||||
|
if (!(fontFile instanceof File) || fontFile.size === 0) {
|
||||||
|
return jsonError("Custom font file is required when using a custom font");
|
||||||
|
}
|
||||||
|
if (fontFile.size > MAX_FONT_BYTES) {
|
||||||
|
return jsonError("Custom font must be 5MB or smaller");
|
||||||
|
}
|
||||||
|
} else if (!isPresetFont(config.fontFamily)) {
|
||||||
|
return jsonError("Unknown font family");
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpDir = await mkdtemp(path.join(tmpdir(), "kk-card-"));
|
||||||
|
registerPresetFonts();
|
||||||
|
|
||||||
|
const style = toIdDrawStyle(config);
|
||||||
|
|
||||||
|
if (config.useCustomFont && fontFile instanceof File) {
|
||||||
|
const ext = fontFile.name.toLowerCase().endsWith(".otf") ? ".otf" : ".ttf";
|
||||||
|
const fontPath = path.join(tmpDir, `custom${ext}`);
|
||||||
|
await writeFile(fontPath, Buffer.from(await fontFile.arrayBuffer()));
|
||||||
|
GlobalFonts.registerFromPath(fontPath, CUSTOM_FONT_FAMILY);
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = await loadImage(
|
||||||
|
Buffer.from(await imageFile.arrayBuffer()),
|
||||||
|
);
|
||||||
|
const canvas = createCanvas(image.width, image.height);
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
const passThrough = new PassThrough();
|
||||||
|
const archive = createZipArchive();
|
||||||
|
archive.on("error", (error: Error) => {
|
||||||
|
passThrough.destroy(error);
|
||||||
|
});
|
||||||
|
archive.pipe(passThrough);
|
||||||
|
|
||||||
|
const usedNames = new Set<string>();
|
||||||
|
|
||||||
|
const generate = async () => {
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < config.count; i += 1) {
|
||||||
|
const id = formatUserId(
|
||||||
|
config.prefix,
|
||||||
|
config.start + i,
|
||||||
|
config.pad,
|
||||||
|
config.suffix,
|
||||||
|
);
|
||||||
|
let name = `${sanitizeFilename(id)}.png`;
|
||||||
|
if (usedNames.has(name)) {
|
||||||
|
name = `${sanitizeFilename(id)}-${i}.png`;
|
||||||
|
}
|
||||||
|
usedNames.add(name);
|
||||||
|
|
||||||
|
ctx.drawImage(image, 0, 0);
|
||||||
|
drawIdText(ctx, id, image.width, image.height, style);
|
||||||
|
const png = await canvas.encode("png");
|
||||||
|
archive.append(Buffer.from(png), { name });
|
||||||
|
}
|
||||||
|
await archive.finalize();
|
||||||
|
} catch (error) {
|
||||||
|
archive.abort();
|
||||||
|
passThrough.destroy(
|
||||||
|
error instanceof Error ? error : new Error("Failed to generate zip"),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (tmpDir) {
|
||||||
|
await rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void generate();
|
||||||
|
|
||||||
|
return new Response(Readable.toWeb(passThrough) as ReadableStream, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/zip",
|
||||||
|
"Content-Disposition": 'attachment; filename="ids.zip"',
|
||||||
|
"Cache-Control": "no-store",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (tmpDir) {
|
||||||
|
await rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
||||||
|
}
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : "Failed to generate images";
|
||||||
|
return jsonError(message, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
+860
@@ -0,0 +1,860 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
type ChangeEvent,
|
||||||
|
type DragEvent,
|
||||||
|
type PointerEvent as ReactPointerEvent,
|
||||||
|
type ReactNode,
|
||||||
|
} from "react";
|
||||||
|
import {
|
||||||
|
drawIdSelection,
|
||||||
|
drawIdText,
|
||||||
|
hitTestTextBox,
|
||||||
|
measureIdTextBox,
|
||||||
|
} from "@/lib/draw-id";
|
||||||
|
import { PRESET_FONTS } from "@/lib/fonts";
|
||||||
|
import {
|
||||||
|
CUSTOM_FONT_FAMILY,
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
formatUserId,
|
||||||
|
MAX_COUNT,
|
||||||
|
MAX_FONT_BYTES,
|
||||||
|
MAX_IMAGE_BYTES,
|
||||||
|
toIdDrawStyle,
|
||||||
|
type GenerateConfig,
|
||||||
|
type TextAlign,
|
||||||
|
} from "@/lib/id-config";
|
||||||
|
|
||||||
|
const IMAGE_ACCEPT = "image/png,image/jpeg,image/webp";
|
||||||
|
const FONT_ACCEPT = ".ttf,.otf,font/ttf,font/otf";
|
||||||
|
|
||||||
|
function canvasPoint(
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
clientX: number,
|
||||||
|
clientY: number,
|
||||||
|
): { x: number; y: number } {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: ((clientX - rect.left) / rect.width) * canvas.width,
|
||||||
|
y: ((clientY - rect.top) / rect.height) * canvas.height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Editor() {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const imageRef = useRef<HTMLImageElement | null>(null);
|
||||||
|
const imageInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const fontInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const [imageFile, setImageFile] = useState<File | null>(null);
|
||||||
|
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
||||||
|
const [imageReady, setImageReady] = useState(0);
|
||||||
|
const [customFontFile, setCustomFontFile] = useState<File | null>(null);
|
||||||
|
const [customFontName, setCustomFontName] = useState<string | null>(null);
|
||||||
|
const [config, setConfig] = useState<GenerateConfig>(DEFAULT_CONFIG);
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [status, setStatus] = useState<string | null>(null);
|
||||||
|
const [cursor, setCursor] = useState<"grab" | "grabbing" | "default">(
|
||||||
|
"default",
|
||||||
|
);
|
||||||
|
|
||||||
|
const dragRef = useRef<{
|
||||||
|
active: boolean;
|
||||||
|
offsetX: number;
|
||||||
|
offsetY: number;
|
||||||
|
}>({ active: false, offsetX: 0, offsetY: 0 });
|
||||||
|
|
||||||
|
const sampleId = useMemo(
|
||||||
|
() =>
|
||||||
|
formatUserId(config.prefix, config.start, config.pad, config.suffix),
|
||||||
|
[config.prefix, config.start, config.pad, config.suffix],
|
||||||
|
);
|
||||||
|
|
||||||
|
const lastId = useMemo(
|
||||||
|
() =>
|
||||||
|
formatUserId(
|
||||||
|
config.prefix,
|
||||||
|
config.start + config.count - 1,
|
||||||
|
config.pad,
|
||||||
|
config.suffix,
|
||||||
|
),
|
||||||
|
[config.prefix, config.start, config.pad, config.suffix, config.count],
|
||||||
|
);
|
||||||
|
|
||||||
|
const drawStyle = useMemo(() => toIdDrawStyle(config), [config]);
|
||||||
|
|
||||||
|
const patch = useCallback((partial: Partial<GenerateConfig>) => {
|
||||||
|
setConfig((current) => ({ ...current, ...partial }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const revokeImageUrl = useCallback((url: string | null) => {
|
||||||
|
if (url) URL.revokeObjectURL(url);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadImageFile = useCallback(
|
||||||
|
(file: File) => {
|
||||||
|
if (!file.type.startsWith("image/")) {
|
||||||
|
setError("Please choose a PNG, JPG, or WebP image");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (file.size > MAX_IMAGE_BYTES) {
|
||||||
|
setError("Base image must be 10MB or smaller");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
setImageFile(file);
|
||||||
|
setImageUrl((previous) => {
|
||||||
|
revokeImageUrl(previous);
|
||||||
|
return URL.createObjectURL(file);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[revokeImageUrl],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => revokeImageUrl(imageUrl);
|
||||||
|
}, [imageUrl, revokeImageUrl]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!imageUrl) {
|
||||||
|
imageRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = new Image();
|
||||||
|
image.onload = () => {
|
||||||
|
imageRef.current = image;
|
||||||
|
setImageReady((value) => value + 1);
|
||||||
|
};
|
||||||
|
image.onerror = () => {
|
||||||
|
setError("Could not read that image");
|
||||||
|
imageRef.current = null;
|
||||||
|
};
|
||||||
|
image.src = imageUrl;
|
||||||
|
}, [imageUrl]);
|
||||||
|
|
||||||
|
const redraw = useCallback(async () => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
const image = imageRef.current;
|
||||||
|
if (!canvas || !image) return;
|
||||||
|
|
||||||
|
await document.fonts.ready;
|
||||||
|
if (config.useCustomFont) {
|
||||||
|
await document.fonts.load(drawStyle.fontSize + "px " + CUSTOM_FONT_FAMILY);
|
||||||
|
} else {
|
||||||
|
await document.fonts.load(
|
||||||
|
`${config.bold ? "700" : "400"} ${config.fontSize}px "${config.fontFamily}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.width = image.naturalWidth;
|
||||||
|
canvas.height = image.naturalHeight;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
ctx.drawImage(image, 0, 0);
|
||||||
|
drawIdText(ctx, sampleId, image.naturalWidth, image.naturalHeight, drawStyle);
|
||||||
|
const box = measureIdTextBox(
|
||||||
|
ctx,
|
||||||
|
sampleId,
|
||||||
|
image.naturalWidth,
|
||||||
|
image.naturalHeight,
|
||||||
|
drawStyle,
|
||||||
|
);
|
||||||
|
drawIdSelection(ctx, box, image.naturalWidth);
|
||||||
|
}, [config.bold, config.fontFamily, config.fontSize, config.useCustomFont, drawStyle, sampleId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void redraw();
|
||||||
|
}, [redraw, imageReady]);
|
||||||
|
|
||||||
|
const loadCustomFont = useCallback(
|
||||||
|
async (file: File) => {
|
||||||
|
const name = file.name.toLowerCase();
|
||||||
|
if (!name.endsWith(".ttf") && !name.endsWith(".otf")) {
|
||||||
|
setError("Please choose a TTF or OTF font");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (file.size > MAX_FONT_BYTES) {
|
||||||
|
setError("Custom font must be 5MB or smaller");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const buffer = await file.arrayBuffer();
|
||||||
|
const face = new FontFace(CUSTOM_FONT_FAMILY, buffer);
|
||||||
|
await face.load();
|
||||||
|
document.fonts.add(face);
|
||||||
|
setCustomFontFile(file);
|
||||||
|
setCustomFontName(file.name);
|
||||||
|
patch({ useCustomFont: true, fontFamily: CUSTOM_FONT_FAMILY });
|
||||||
|
setError(null);
|
||||||
|
} catch {
|
||||||
|
setError("Could not load that font file");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[patch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onPointerDown = (event: ReactPointerEvent<HTMLCanvasElement>) => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
const image = imageRef.current;
|
||||||
|
if (!canvas || !image) return;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
const point = canvasPoint(canvas, event.clientX, event.clientY);
|
||||||
|
const box = measureIdTextBox(
|
||||||
|
ctx,
|
||||||
|
sampleId,
|
||||||
|
image.naturalWidth,
|
||||||
|
image.naturalHeight,
|
||||||
|
drawStyle,
|
||||||
|
);
|
||||||
|
|
||||||
|
let anchorX = (config.xPercent / 100) * image.naturalWidth;
|
||||||
|
let anchorY = (config.yPercent / 100) * image.naturalHeight;
|
||||||
|
|
||||||
|
if (!hitTestTextBox(box, point.x, point.y, image.naturalWidth)) {
|
||||||
|
anchorX = point.x;
|
||||||
|
anchorY = point.y;
|
||||||
|
patch({
|
||||||
|
xPercent: (point.x / image.naturalWidth) * 100,
|
||||||
|
yPercent: (point.y / image.naturalHeight) * 100,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dragRef.current = {
|
||||||
|
active: true,
|
||||||
|
offsetX: point.x - anchorX,
|
||||||
|
offsetY: point.y - anchorY,
|
||||||
|
};
|
||||||
|
setCursor("grabbing");
|
||||||
|
canvas.setPointerCapture(event.pointerId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerMove = (event: ReactPointerEvent<HTMLCanvasElement>) => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
const image = imageRef.current;
|
||||||
|
if (!canvas || !image) return;
|
||||||
|
|
||||||
|
const point = canvasPoint(canvas, event.clientX, event.clientY);
|
||||||
|
|
||||||
|
if (!dragRef.current.active) {
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
const box = measureIdTextBox(
|
||||||
|
ctx,
|
||||||
|
sampleId,
|
||||||
|
image.naturalWidth,
|
||||||
|
image.naturalHeight,
|
||||||
|
drawStyle,
|
||||||
|
);
|
||||||
|
setCursor(
|
||||||
|
hitTestTextBox(box, point.x, point.y, image.naturalWidth)
|
||||||
|
? "grab"
|
||||||
|
: "default",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = point.x - dragRef.current.offsetX;
|
||||||
|
const y = point.y - dragRef.current.offsetY;
|
||||||
|
patch({
|
||||||
|
xPercent: Math.min(100, Math.max(0, (x / image.naturalWidth) * 100)),
|
||||||
|
yPercent: Math.min(100, Math.max(0, (y / image.naturalHeight) * 100)),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerUp = (event: ReactPointerEvent<HTMLCanvasElement>) => {
|
||||||
|
dragRef.current.active = false;
|
||||||
|
setCursor("grab");
|
||||||
|
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDrop = (event: DragEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const file = event.dataTransfer.files[0];
|
||||||
|
if (file) loadImageFile(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generate = async () => {
|
||||||
|
if (!imageFile) {
|
||||||
|
setError("Choose a base image first");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (config.useCustomFont && !customFontFile) {
|
||||||
|
setError("Upload a custom font or switch back to a preset");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setBusy(true);
|
||||||
|
setError(null);
|
||||||
|
setStatus(`Generating ${config.count} images on the server…`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("image", imageFile);
|
||||||
|
if (config.useCustomFont && customFontFile) {
|
||||||
|
form.append("font", customFontFile);
|
||||||
|
}
|
||||||
|
form.append("config", JSON.stringify(config));
|
||||||
|
|
||||||
|
const response = await fetch("/api/generate", {
|
||||||
|
method: "POST",
|
||||||
|
body: form,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
let message = "Generation failed";
|
||||||
|
try {
|
||||||
|
const payload = (await response.json()) as { error?: string };
|
||||||
|
if (payload.error) message = payload.error;
|
||||||
|
} catch {
|
||||||
|
message = `Generation failed (${response.status})`;
|
||||||
|
}
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.download = "ids.zip";
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
setStatus(`Downloaded ids.zip with ${config.count} PNGs`);
|
||||||
|
} catch (err) {
|
||||||
|
setStatus(null);
|
||||||
|
setError(err instanceof Error ? err.message : "Generation failed");
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen flex-col">
|
||||||
|
<div className="mx-auto grid w-full max-w-7xl flex-1 grid-cols-1 gap-6 p-6 lg:grid-cols-[minmax(0,1fr)_380px]">
|
||||||
|
<section
|
||||||
|
className="flex min-h-[420px] flex-col rounded-2xl border border-zinc-800 bg-zinc-950"
|
||||||
|
onDragOver={(event) => event.preventDefault()}
|
||||||
|
onDrop={onDrop}
|
||||||
|
>
|
||||||
|
{!imageUrl ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => imageInputRef.current?.click()}
|
||||||
|
className="flex flex-1 flex-col items-center justify-center gap-3 px-6 text-center"
|
||||||
|
>
|
||||||
|
<span className="rounded-full border border-zinc-700 bg-zinc-900 px-3 py-1 text-xs text-zinc-300">
|
||||||
|
PNG · JPG · WebP
|
||||||
|
</span>
|
||||||
|
<span className="text-lg font-medium">Select a base photo</span>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-1 items-center justify-center p-4">
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
onPointerDown={onPointerDown}
|
||||||
|
onPointerMove={onPointerMove}
|
||||||
|
onPointerUp={onPointerUp}
|
||||||
|
onPointerCancel={onPointerUp}
|
||||||
|
className="max-h-[70vh] max-w-full rounded-lg shadow-2xl shadow-black/40"
|
||||||
|
style={{ cursor }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside className="flex flex-col gap-5 rounded-2xl border border-zinc-800 bg-zinc-950 p-5 lg:max-h-[calc(100vh-3rem)] lg:overflow-y-auto">
|
||||||
|
<FieldGroup title="Base image">
|
||||||
|
<FileButton
|
||||||
|
onClick={() => imageInputRef.current?.click()}
|
||||||
|
label={imageFile ? imageFile.name : "Choose image"}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
ref={imageInputRef}
|
||||||
|
type="file"
|
||||||
|
accept={IMAGE_ACCEPT}
|
||||||
|
hidden
|
||||||
|
onChange={(event) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file) loadImageFile(file);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FieldGroup>
|
||||||
|
|
||||||
|
<FieldGroup title="User ID">
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<TextField
|
||||||
|
label="Prefix"
|
||||||
|
value={config.prefix}
|
||||||
|
onChange={(value) => patch({ prefix: value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Suffix"
|
||||||
|
value={config.suffix}
|
||||||
|
onChange={(value) => patch({ suffix: value })}
|
||||||
|
/>
|
||||||
|
<NumberField
|
||||||
|
label="Start at"
|
||||||
|
value={config.start}
|
||||||
|
min={0}
|
||||||
|
max={1_000_000}
|
||||||
|
onChange={(value) => patch({ start: value })}
|
||||||
|
/>
|
||||||
|
<NumberField
|
||||||
|
label="Pad digits"
|
||||||
|
value={config.pad}
|
||||||
|
min={0}
|
||||||
|
max={6}
|
||||||
|
onChange={(value) => patch({ pad: value })}
|
||||||
|
/>
|
||||||
|
<NumberField
|
||||||
|
label="Count"
|
||||||
|
value={config.count}
|
||||||
|
min={1}
|
||||||
|
max={MAX_COUNT}
|
||||||
|
onChange={(value) => patch({ count: value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="rounded-lg bg-zinc-900 px-3 py-2 font-mono text-xs text-zinc-300">
|
||||||
|
{sampleId}.png
|
||||||
|
<span className="text-zinc-500"> → </span>
|
||||||
|
{lastId}.png
|
||||||
|
</p>
|
||||||
|
</FieldGroup>
|
||||||
|
|
||||||
|
<FieldGroup title="Text style">
|
||||||
|
<label className="block text-xs text-zinc-400">
|
||||||
|
Font
|
||||||
|
<select
|
||||||
|
className="mt-1 w-full rounded-lg border border-zinc-800 bg-zinc-900 px-3 py-2 text-sm text-zinc-100"
|
||||||
|
value={
|
||||||
|
config.useCustomFont ? CUSTOM_FONT_FAMILY : config.fontFamily
|
||||||
|
}
|
||||||
|
onChange={(event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
if (value === CUSTOM_FONT_FAMILY) {
|
||||||
|
patch({
|
||||||
|
useCustomFont: true,
|
||||||
|
fontFamily: CUSTOM_FONT_FAMILY,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
patch({ useCustomFont: false, fontFamily: value });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{PRESET_FONTS.map((font) => (
|
||||||
|
<option key={font.id} value={font.id}>
|
||||||
|
{font.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
{customFontName ? (
|
||||||
|
<option value={CUSTOM_FONT_FAMILY}>
|
||||||
|
Custom: {customFontName}
|
||||||
|
</option>
|
||||||
|
) : null}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<FileButton
|
||||||
|
onClick={() => fontInputRef.current?.click()}
|
||||||
|
label={
|
||||||
|
customFontName
|
||||||
|
? `Replace font (${customFontName})`
|
||||||
|
: "Upload TTF / OTF"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
ref={fontInputRef}
|
||||||
|
type="file"
|
||||||
|
accept={FONT_ACCEPT}
|
||||||
|
hidden
|
||||||
|
onChange={(event) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file) void loadCustomFont(file);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RangeField
|
||||||
|
label="Size"
|
||||||
|
value={config.fontSize}
|
||||||
|
min={8}
|
||||||
|
max={220}
|
||||||
|
onChange={(value) => patch({ fontSize: value })}
|
||||||
|
suffix="px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label className="block text-xs text-zinc-400">
|
||||||
|
Color
|
||||||
|
<span className="mt-1 flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={config.color.length === 7 ? config.color : "#111111"}
|
||||||
|
onChange={(event) => patch({ color: event.target.value })}
|
||||||
|
className="h-9 w-12 cursor-pointer rounded border border-zinc-700 bg-zinc-900"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
value={config.color}
|
||||||
|
onChange={(event) => patch({ color: event.target.value })}
|
||||||
|
className="w-full rounded-lg border border-zinc-800 bg-zinc-900 px-3 py-2 font-mono text-sm text-zinc-100"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Toggle
|
||||||
|
pressed={config.bold}
|
||||||
|
onClick={() => patch({ bold: !config.bold })}
|
||||||
|
label="Bold"
|
||||||
|
/>
|
||||||
|
<Toggle
|
||||||
|
pressed={config.italic}
|
||||||
|
onClick={() => patch({ italic: !config.italic })}
|
||||||
|
label="Italic"
|
||||||
|
/>
|
||||||
|
{(["left", "center", "right"] as TextAlign[]).map((align) => (
|
||||||
|
<Toggle
|
||||||
|
key={align}
|
||||||
|
pressed={config.align === align}
|
||||||
|
onClick={() => patch({ align })}
|
||||||
|
label={align[0].toUpperCase() + align.slice(1)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</FieldGroup>
|
||||||
|
|
||||||
|
<FieldGroup title="Drop shadow">
|
||||||
|
<Toggle
|
||||||
|
pressed={config.shadowEnabled}
|
||||||
|
onClick={() => patch({ shadowEnabled: !config.shadowEnabled })}
|
||||||
|
label={config.shadowEnabled ? "On" : "Off"}
|
||||||
|
/>
|
||||||
|
{config.shadowEnabled ? (
|
||||||
|
<>
|
||||||
|
<label className="block text-xs text-zinc-400">
|
||||||
|
Color
|
||||||
|
<span className="mt-1 flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={
|
||||||
|
config.shadowColor.length === 7
|
||||||
|
? config.shadowColor
|
||||||
|
: "#000000"
|
||||||
|
}
|
||||||
|
onChange={(event) =>
|
||||||
|
patch({ shadowColor: event.target.value })
|
||||||
|
}
|
||||||
|
className="h-9 w-12 cursor-pointer rounded border border-zinc-700 bg-zinc-900"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
value={config.shadowColor}
|
||||||
|
onChange={(event) =>
|
||||||
|
patch({ shadowColor: event.target.value })
|
||||||
|
}
|
||||||
|
className="w-full rounded-lg border border-zinc-800 bg-zinc-900 px-3 py-2 font-mono text-sm text-zinc-100"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<RangeField
|
||||||
|
label="Opacity"
|
||||||
|
value={Number(config.shadowOpacity.toFixed(2))}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.05}
|
||||||
|
onChange={(value) => patch({ shadowOpacity: value })}
|
||||||
|
/>
|
||||||
|
<RangeField
|
||||||
|
label="Blur"
|
||||||
|
value={config.shadowBlur}
|
||||||
|
min={0}
|
||||||
|
max={40}
|
||||||
|
onChange={(value) => patch({ shadowBlur: value })}
|
||||||
|
suffix="px"
|
||||||
|
/>
|
||||||
|
<RangeField
|
||||||
|
label="Offset X"
|
||||||
|
value={config.shadowOffsetX}
|
||||||
|
min={-30}
|
||||||
|
max={30}
|
||||||
|
onChange={(value) => patch({ shadowOffsetX: value })}
|
||||||
|
suffix="px"
|
||||||
|
/>
|
||||||
|
<RangeField
|
||||||
|
label="Offset Y"
|
||||||
|
value={config.shadowOffsetY}
|
||||||
|
min={-30}
|
||||||
|
max={30}
|
||||||
|
onChange={(value) => patch({ shadowOffsetY: value })}
|
||||||
|
suffix="px"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</FieldGroup>
|
||||||
|
|
||||||
|
<FieldGroup title="Outline">
|
||||||
|
<Toggle
|
||||||
|
pressed={config.outlineEnabled}
|
||||||
|
onClick={() => patch({ outlineEnabled: !config.outlineEnabled })}
|
||||||
|
label={config.outlineEnabled ? "On" : "Off"}
|
||||||
|
/>
|
||||||
|
{config.outlineEnabled ? (
|
||||||
|
<>
|
||||||
|
<label className="block text-xs text-zinc-400">
|
||||||
|
Color
|
||||||
|
<span className="mt-1 flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={
|
||||||
|
config.outlineColor.length === 7
|
||||||
|
? config.outlineColor
|
||||||
|
: "#ffffff"
|
||||||
|
}
|
||||||
|
onChange={(event) =>
|
||||||
|
patch({ outlineColor: event.target.value })
|
||||||
|
}
|
||||||
|
className="h-9 w-12 cursor-pointer rounded border border-zinc-700 bg-zinc-900"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
value={config.outlineColor}
|
||||||
|
onChange={(event) =>
|
||||||
|
patch({ outlineColor: event.target.value })
|
||||||
|
}
|
||||||
|
className="w-full rounded-lg border border-zinc-800 bg-zinc-900 px-3 py-2 font-mono text-sm text-zinc-100"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<RangeField
|
||||||
|
label="Opacity"
|
||||||
|
value={Number(config.outlineOpacity.toFixed(2))}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.05}
|
||||||
|
onChange={(value) => patch({ outlineOpacity: value })}
|
||||||
|
/>
|
||||||
|
<RangeField
|
||||||
|
label="Width"
|
||||||
|
value={config.outlineWidth}
|
||||||
|
min={0.5}
|
||||||
|
max={20}
|
||||||
|
step={0.5}
|
||||||
|
onChange={(value) => patch({ outlineWidth: value })}
|
||||||
|
suffix="px"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</FieldGroup>
|
||||||
|
|
||||||
|
<FieldGroup title="Position">
|
||||||
|
<p className="text-xs text-zinc-500">
|
||||||
|
Drag the ID on the preview, click to place it, or nudge the
|
||||||
|
percentages.
|
||||||
|
</p>
|
||||||
|
<RangeField
|
||||||
|
label="X"
|
||||||
|
value={Number(config.xPercent.toFixed(1))}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={0.1}
|
||||||
|
onChange={(value) => patch({ xPercent: value })}
|
||||||
|
suffix="%"
|
||||||
|
/>
|
||||||
|
<RangeField
|
||||||
|
label="Y"
|
||||||
|
value={Number(config.yPercent.toFixed(1))}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={0.1}
|
||||||
|
onChange={(value) => patch({ yPercent: value })}
|
||||||
|
suffix="%"
|
||||||
|
/>
|
||||||
|
</FieldGroup>
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<p className="rounded-lg border border-red-900 bg-red-950/60 px-3 py-2 text-sm text-red-200">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{status && !error ? (
|
||||||
|
<p className="rounded-lg border border-indigo-900 bg-indigo-950/50 px-3 py-2 text-sm text-indigo-100">
|
||||||
|
{status}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void generate()}
|
||||||
|
disabled={busy || !imageFile}
|
||||||
|
className="rounded-xl bg-indigo-500 px-4 py-3 text-sm font-semibold text-white transition hover:bg-indigo-400 disabled:cursor-not-allowed disabled:bg-zinc-700 disabled:text-zinc-400"
|
||||||
|
>
|
||||||
|
{busy ? "Generating…" : `Generate ${config.count} images`}
|
||||||
|
</button>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldGroup({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section className="flex flex-col gap-3">
|
||||||
|
<h2 className="text-xs font-semibold uppercase tracking-wider text-zinc-500">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FileButton({
|
||||||
|
label,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
className="w-full truncate rounded-lg border border-zinc-800 bg-zinc-900 px-3 py-2 text-left text-sm text-zinc-200 hover:border-zinc-600"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TextField({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<label className="block text-xs text-zinc-400">
|
||||||
|
{label}
|
||||||
|
<input
|
||||||
|
value={value}
|
||||||
|
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange(event.target.value)
|
||||||
|
}
|
||||||
|
className="mt-1 w-full rounded-lg border border-zinc-800 bg-zinc-900 px-3 py-2 text-sm text-zinc-100"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NumberField({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
onChange: (value: number) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<label className="block text-xs text-zinc-400">
|
||||||
|
{label}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={value}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
onChange={(event) => {
|
||||||
|
const next = Number(event.target.value);
|
||||||
|
if (!Number.isFinite(next)) return;
|
||||||
|
onChange(Math.min(max, Math.max(min, Math.trunc(next))));
|
||||||
|
}}
|
||||||
|
className="mt-1 w-full rounded-lg border border-zinc-800 bg-zinc-900 px-3 py-2 text-sm text-zinc-100"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RangeField({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
step = 1,
|
||||||
|
suffix,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step?: number;
|
||||||
|
suffix?: string;
|
||||||
|
onChange: (value: number) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<label className="block text-xs text-zinc-400">
|
||||||
|
<span className="flex justify-between">
|
||||||
|
<span>{label}</span>
|
||||||
|
<span className="font-mono text-zinc-300">
|
||||||
|
{value}
|
||||||
|
{suffix}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
value={value}
|
||||||
|
onChange={(event) => onChange(Number(event.target.value))}
|
||||||
|
className="mt-2 w-full"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Toggle({
|
||||||
|
pressed,
|
||||||
|
onClick,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
pressed: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
label: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
className={`rounded-lg border px-3 py-1.5 text-xs capitalize ${
|
||||||
|
pressed
|
||||||
|
? "border-indigo-400 bg-indigo-500/20 text-indigo-100"
|
||||||
|
: "border-zinc-800 bg-zinc-900 text-zinc-300"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
+93
-8
@@ -1,26 +1,111 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Inter";
|
||||||
|
src: url("/fonts/Inter-Regular.ttf") format("truetype");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Inter";
|
||||||
|
src: url("/fonts/Inter-Bold.ttf") format("truetype");
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Roboto";
|
||||||
|
src: url("/fonts/Roboto-Regular.ttf") format("truetype");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Roboto";
|
||||||
|
src: url("/fonts/Roboto-Bold.ttf") format("truetype");
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Oswald";
|
||||||
|
src: url("/fonts/Oswald-Regular.ttf") format("truetype");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Oswald";
|
||||||
|
src: url("/fonts/Oswald-Bold.ttf") format("truetype");
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Montserrat";
|
||||||
|
src: url("/fonts/Montserrat-Regular.ttf") format("truetype");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Montserrat";
|
||||||
|
src: url("/fonts/Montserrat-Bold.ttf") format("truetype");
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Playfair Display";
|
||||||
|
src: url("/fonts/PlayfairDisplay-Regular.ttf") format("truetype");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Playfair Display";
|
||||||
|
src: url("/fonts/PlayfairDisplay-Bold.ttf") format("truetype");
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #09090b;
|
||||||
--foreground: #171717;
|
--foreground: #f4f4f5;
|
||||||
|
--panel: #18181b;
|
||||||
|
--border: #27272a;
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
|
--color-panel: var(--panel);
|
||||||
|
--color-border: var(--border);
|
||||||
--font-sans: var(--font-geist-sans);
|
--font-sans: var(--font-geist-sans);
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
* {
|
||||||
:root {
|
box-sizing: border-box;
|
||||||
--background: #0a0a0a;
|
|
||||||
--foreground: #ededed;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: var(--font-geist-sans), Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"] {
|
||||||
|
accent-color: #6366f1;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-3
@@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "KK Card Gen",
|
||||||
description: "Generated by create next app",
|
description: "Stamp sequential user IDs onto a base card image and download a zip of PNGs",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({ children }: LayoutProps<"/">) {
|
export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||||
@@ -23,7 +23,9 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
|
|||||||
lang="en"
|
lang="en"
|
||||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||||
>
|
>
|
||||||
<body className="min-h-full flex flex-col">{children}</body>
|
<body className="min-h-full flex flex-col bg-background text-foreground">
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-66
@@ -1,69 +1,5 @@
|
|||||||
import Image from "next/image";
|
import { Editor } from "./editor";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return <Editor />;
|
||||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
|
||||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
|
||||||
<Image
|
|
||||||
className="dark:invert h-5 w-[100px]"
|
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js logo"
|
|
||||||
width={100}
|
|
||||||
height={20}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
|
||||||
To get started, edit the{" "}
|
|
||||||
<code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
|
|
||||||
page.tsx
|
|
||||||
</code>{" "}
|
|
||||||
file.
|
|
||||||
</h1>
|
|
||||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
|
||||||
Looking for a starting point or more instructions? Head over to{" "}
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Templates
|
|
||||||
</a>{" "}
|
|
||||||
or the{" "}
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Learning
|
|
||||||
</a>{" "}
|
|
||||||
center.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
className="dark:invert h-[14px] w-4"
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={16}
|
|
||||||
height={14}
|
|
||||||
/>
|
|
||||||
Deploy Now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { ZipArchive } from "archiver";
|
||||||
|
|
||||||
|
export function createZipArchive(level = 6): ZipArchive {
|
||||||
|
return new ZipArchive({ zlib: { level } });
|
||||||
|
}
|
||||||
+208
@@ -0,0 +1,208 @@
|
|||||||
|
import { buildCanvasFont, hexToRgba, type IdDrawStyle } from "./id-config";
|
||||||
|
|
||||||
|
export type TextMetricsLike = {
|
||||||
|
width: number;
|
||||||
|
actualBoundingBoxAscent?: number;
|
||||||
|
actualBoundingBoxDescent?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Canvas2D = {
|
||||||
|
font: string;
|
||||||
|
fillStyle: string | CanvasGradient | CanvasPattern;
|
||||||
|
strokeStyle: string | CanvasGradient | CanvasPattern;
|
||||||
|
lineWidth: number;
|
||||||
|
textAlign: CanvasTextAlign;
|
||||||
|
textBaseline: CanvasTextBaseline;
|
||||||
|
shadowColor: string;
|
||||||
|
shadowBlur: number;
|
||||||
|
shadowOffsetX: number;
|
||||||
|
shadowOffsetY: number;
|
||||||
|
lineJoin: CanvasLineJoin;
|
||||||
|
miterLimit: number;
|
||||||
|
fillText(text: string, x: number, y: number): void;
|
||||||
|
strokeText(text: string, x: number, y: number): void;
|
||||||
|
measureText(text: string): TextMetricsLike;
|
||||||
|
save(): void;
|
||||||
|
restore(): void;
|
||||||
|
setLineDash(segments: number[]): void;
|
||||||
|
strokeRect(x: number, y: number, w: number, h: number): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TextBox = {
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GlyphMetrics = {
|
||||||
|
width: number;
|
||||||
|
ascent: number;
|
||||||
|
descent: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function positionFromPercent(
|
||||||
|
xPercent: number,
|
||||||
|
yPercent: number,
|
||||||
|
imageWidth: number,
|
||||||
|
imageHeight: number,
|
||||||
|
): { x: number; y: number } {
|
||||||
|
return {
|
||||||
|
x: (xPercent / 100) * imageWidth,
|
||||||
|
y: (yPercent / 100) * imageHeight,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFont(ctx: Canvas2D, style: IdDrawStyle): void {
|
||||||
|
ctx.font = buildCanvasFont(style);
|
||||||
|
ctx.textAlign = style.align;
|
||||||
|
ctx.textBaseline = "alphabetic";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function measureGlyphs(
|
||||||
|
ctx: Canvas2D,
|
||||||
|
text: string,
|
||||||
|
style: IdDrawStyle,
|
||||||
|
): GlyphMetrics {
|
||||||
|
applyFont(ctx, style);
|
||||||
|
const metrics = ctx.measureText(text);
|
||||||
|
const ascent = metrics.actualBoundingBoxAscent ?? style.fontSize * 0.8;
|
||||||
|
const descent = metrics.actualBoundingBoxDescent ?? style.fontSize * 0.2;
|
||||||
|
return {
|
||||||
|
width: metrics.width,
|
||||||
|
ascent,
|
||||||
|
descent,
|
||||||
|
height: ascent + descent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function alphabeticBaselineY(
|
||||||
|
visualCenterY: number,
|
||||||
|
glyphs: GlyphMetrics,
|
||||||
|
): number {
|
||||||
|
return visualCenterY + (glyphs.ascent - glyphs.descent) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyIdStyle(ctx: Canvas2D, style: IdDrawStyle): void {
|
||||||
|
applyFont(ctx, style);
|
||||||
|
ctx.fillStyle = style.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyShadow(ctx: Canvas2D, style: IdDrawStyle): void {
|
||||||
|
if (!style.shadowEnabled) {
|
||||||
|
ctx.shadowColor = "transparent";
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
|
ctx.shadowOffsetX = 0;
|
||||||
|
ctx.shadowOffsetY = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.shadowColor = hexToRgba(style.shadowColor, style.shadowOpacity);
|
||||||
|
ctx.shadowBlur = style.shadowBlur;
|
||||||
|
ctx.shadowOffsetX = style.shadowOffsetX;
|
||||||
|
ctx.shadowOffsetY = style.shadowOffsetY;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drawIdText(
|
||||||
|
ctx: Canvas2D,
|
||||||
|
text: string,
|
||||||
|
imageWidth: number,
|
||||||
|
imageHeight: number,
|
||||||
|
style: IdDrawStyle,
|
||||||
|
): void {
|
||||||
|
const { x, y } = positionFromPercent(
|
||||||
|
style.xPercent,
|
||||||
|
style.yPercent,
|
||||||
|
imageWidth,
|
||||||
|
imageHeight,
|
||||||
|
);
|
||||||
|
const glyphs = measureGlyphs(ctx, text, style);
|
||||||
|
const baselineY = alphabeticBaselineY(y, glyphs);
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
applyIdStyle(ctx, style);
|
||||||
|
applyShadow(ctx, style);
|
||||||
|
|
||||||
|
if (style.outlineEnabled && style.outlineWidth > 0) {
|
||||||
|
ctx.strokeStyle = hexToRgba(style.outlineColor, style.outlineOpacity);
|
||||||
|
ctx.lineWidth = style.outlineWidth;
|
||||||
|
ctx.lineJoin = "round";
|
||||||
|
ctx.miterLimit = 2;
|
||||||
|
ctx.strokeText(text, x, baselineY);
|
||||||
|
ctx.shadowColor = "transparent";
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillText(text, x, baselineY);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function measureIdTextBox(
|
||||||
|
ctx: Canvas2D,
|
||||||
|
text: string,
|
||||||
|
imageWidth: number,
|
||||||
|
imageHeight: number,
|
||||||
|
style: IdDrawStyle,
|
||||||
|
): TextBox {
|
||||||
|
const glyphs = measureGlyphs(ctx, text, style);
|
||||||
|
const { x, y } = positionFromPercent(
|
||||||
|
style.xPercent,
|
||||||
|
style.yPercent,
|
||||||
|
imageWidth,
|
||||||
|
imageHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
let left = x;
|
||||||
|
if (style.align === "center") left = x - glyphs.width / 2;
|
||||||
|
if (style.align === "right") left = x - glyphs.width;
|
||||||
|
|
||||||
|
const outlinePad =
|
||||||
|
style.outlineEnabled && style.outlineWidth > 0
|
||||||
|
? style.outlineWidth / 2
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: left - outlinePad,
|
||||||
|
top: y - glyphs.height / 2 - outlinePad,
|
||||||
|
width: glyphs.width + outlinePad * 2,
|
||||||
|
height: glyphs.height + outlinePad * 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drawIdSelection(
|
||||||
|
ctx: Canvas2D,
|
||||||
|
box: TextBox,
|
||||||
|
imageWidth: number,
|
||||||
|
): void {
|
||||||
|
const pad = Math.max(6, imageWidth / 400);
|
||||||
|
ctx.save();
|
||||||
|
ctx.shadowColor = "transparent";
|
||||||
|
ctx.strokeStyle = "rgba(99, 102, 241, 0.95)";
|
||||||
|
ctx.lineWidth = Math.max(1, imageWidth / 700);
|
||||||
|
ctx.setLineDash([
|
||||||
|
Math.max(4, imageWidth / 180),
|
||||||
|
Math.max(3, imageWidth / 220),
|
||||||
|
]);
|
||||||
|
ctx.strokeRect(
|
||||||
|
box.left - pad,
|
||||||
|
box.top - pad,
|
||||||
|
box.width + pad * 2,
|
||||||
|
box.height + pad * 2,
|
||||||
|
);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hitTestTextBox(
|
||||||
|
box: TextBox,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
imageWidth: number,
|
||||||
|
): boolean {
|
||||||
|
const pad = Math.max(10, imageWidth / 80);
|
||||||
|
return (
|
||||||
|
x >= box.left - pad &&
|
||||||
|
x <= box.left + box.width + pad &&
|
||||||
|
y >= box.top - pad &&
|
||||||
|
y <= box.top + box.height + pad
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
export type PresetFont = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
regular: string;
|
||||||
|
bold: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PRESET_FONTS: PresetFont[] = [
|
||||||
|
{
|
||||||
|
id: "Inter",
|
||||||
|
label: "Inter",
|
||||||
|
regular: "Inter-Regular.ttf",
|
||||||
|
bold: "Inter-Bold.ttf",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Roboto",
|
||||||
|
label: "Roboto",
|
||||||
|
regular: "Roboto-Regular.ttf",
|
||||||
|
bold: "Roboto-Bold.ttf",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Oswald",
|
||||||
|
label: "Oswald",
|
||||||
|
regular: "Oswald-Regular.ttf",
|
||||||
|
bold: "Oswald-Bold.ttf",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Montserrat",
|
||||||
|
label: "Montserrat",
|
||||||
|
regular: "Montserrat-Regular.ttf",
|
||||||
|
bold: "Montserrat-Bold.ttf",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Playfair Display",
|
||||||
|
label: "Playfair Display",
|
||||||
|
regular: "PlayfairDisplay-Regular.ttf",
|
||||||
|
bold: "PlayfairDisplay-Bold.ttf",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function isPresetFont(family: string): boolean {
|
||||||
|
return PRESET_FONTS.some((font) => font.id === family);
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
export type TextAlign = "left" | "center" | "right";
|
||||||
|
|
||||||
|
export type IdDrawStyle = {
|
||||||
|
fontFamily: string;
|
||||||
|
fontSize: number;
|
||||||
|
color: string;
|
||||||
|
bold: boolean;
|
||||||
|
italic: boolean;
|
||||||
|
align: TextAlign;
|
||||||
|
xPercent: number;
|
||||||
|
yPercent: number;
|
||||||
|
shadowEnabled: boolean;
|
||||||
|
shadowColor: string;
|
||||||
|
shadowOpacity: number;
|
||||||
|
shadowBlur: number;
|
||||||
|
shadowOffsetX: number;
|
||||||
|
shadowOffsetY: number;
|
||||||
|
outlineEnabled: boolean;
|
||||||
|
outlineColor: string;
|
||||||
|
outlineOpacity: number;
|
||||||
|
outlineWidth: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GenerateConfig = IdDrawStyle & {
|
||||||
|
prefix: string;
|
||||||
|
suffix: string;
|
||||||
|
start: number;
|
||||||
|
pad: number;
|
||||||
|
count: number;
|
||||||
|
useCustomFont: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MAX_COUNT = 1000;
|
||||||
|
export const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
||||||
|
export const MAX_FONT_BYTES = 5 * 1024 * 1024;
|
||||||
|
export const CUSTOM_FONT_FAMILY = "CustomUpload";
|
||||||
|
|
||||||
|
export const DEFAULT_CONFIG: GenerateConfig = {
|
||||||
|
prefix: "KK",
|
||||||
|
suffix: "",
|
||||||
|
start: 1,
|
||||||
|
pad: 4,
|
||||||
|
count: 1000,
|
||||||
|
fontFamily: "Inter",
|
||||||
|
fontSize: 48,
|
||||||
|
color: "#111111",
|
||||||
|
bold: false,
|
||||||
|
italic: false,
|
||||||
|
align: "center",
|
||||||
|
xPercent: 50,
|
||||||
|
yPercent: 50,
|
||||||
|
shadowEnabled: false,
|
||||||
|
shadowColor: "#000000",
|
||||||
|
shadowOpacity: 0.45,
|
||||||
|
shadowBlur: 8,
|
||||||
|
shadowOffsetX: 2,
|
||||||
|
shadowOffsetY: 3,
|
||||||
|
outlineEnabled: false,
|
||||||
|
outlineColor: "#ffffff",
|
||||||
|
outlineOpacity: 1,
|
||||||
|
outlineWidth: 3,
|
||||||
|
useCustomFont: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const COLOR_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
||||||
|
|
||||||
|
export function formatUserId(
|
||||||
|
prefix: string,
|
||||||
|
n: number,
|
||||||
|
pad: number,
|
||||||
|
suffix: string,
|
||||||
|
): string {
|
||||||
|
return `${prefix}${String(n).padStart(Math.max(0, pad), "0")}${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeFilename(id: string): string {
|
||||||
|
const cleaned = id
|
||||||
|
.replace(/[/\\:*?"<>|]/g, "")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
return cleaned || "id";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCanvasFont(
|
||||||
|
style: Pick<IdDrawStyle, "fontFamily" | "fontSize" | "bold" | "italic">,
|
||||||
|
): string {
|
||||||
|
const italic = style.italic ? "italic " : "";
|
||||||
|
const weight = style.bold ? "700 " : "400 ";
|
||||||
|
return `${italic}${weight}${style.fontSize}px "${style.fontFamily}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clamp(value: number, min: number, max: number): number {
|
||||||
|
return Math.min(max, Math.max(min, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function asNumber(value: unknown, fallback: number): number {
|
||||||
|
const n = typeof value === "number" ? value : Number(value);
|
||||||
|
return Number.isFinite(n) ? n : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function asString(value: unknown, fallback = ""): string {
|
||||||
|
return typeof value === "string" ? value : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function asBool(value: unknown): boolean {
|
||||||
|
return value === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseGenerateConfig(raw: unknown): GenerateConfig {
|
||||||
|
if (!raw || typeof raw !== "object") {
|
||||||
|
throw new Error("Invalid config");
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = raw as Record<string, unknown>;
|
||||||
|
const start = Math.trunc(asNumber(input.start, DEFAULT_CONFIG.start));
|
||||||
|
const pad = Math.trunc(asNumber(input.pad, DEFAULT_CONFIG.pad));
|
||||||
|
const count = Math.trunc(asNumber(input.count, DEFAULT_CONFIG.count));
|
||||||
|
const fontSize = asNumber(input.fontSize, DEFAULT_CONFIG.fontSize);
|
||||||
|
const xPercent = asNumber(input.xPercent, DEFAULT_CONFIG.xPercent);
|
||||||
|
const yPercent = asNumber(input.yPercent, DEFAULT_CONFIG.yPercent);
|
||||||
|
const shadowOpacity = asNumber(
|
||||||
|
input.shadowOpacity,
|
||||||
|
DEFAULT_CONFIG.shadowOpacity,
|
||||||
|
);
|
||||||
|
const shadowBlur = asNumber(input.shadowBlur, DEFAULT_CONFIG.shadowBlur);
|
||||||
|
const shadowOffsetX = asNumber(
|
||||||
|
input.shadowOffsetX,
|
||||||
|
DEFAULT_CONFIG.shadowOffsetX,
|
||||||
|
);
|
||||||
|
const shadowOffsetY = asNumber(
|
||||||
|
input.shadowOffsetY,
|
||||||
|
DEFAULT_CONFIG.shadowOffsetY,
|
||||||
|
);
|
||||||
|
const outlineOpacity = asNumber(
|
||||||
|
input.outlineOpacity,
|
||||||
|
DEFAULT_CONFIG.outlineOpacity,
|
||||||
|
);
|
||||||
|
const outlineWidth = asNumber(
|
||||||
|
input.outlineWidth,
|
||||||
|
DEFAULT_CONFIG.outlineWidth,
|
||||||
|
);
|
||||||
|
const color = asString(input.color, DEFAULT_CONFIG.color);
|
||||||
|
const shadowColor = asString(input.shadowColor, DEFAULT_CONFIG.shadowColor);
|
||||||
|
const outlineColor = asString(
|
||||||
|
input.outlineColor,
|
||||||
|
DEFAULT_CONFIG.outlineColor,
|
||||||
|
);
|
||||||
|
const align = asString(input.align, DEFAULT_CONFIG.align);
|
||||||
|
const prefix = asString(input.prefix).slice(0, 50);
|
||||||
|
const suffix = asString(input.suffix).slice(0, 50);
|
||||||
|
const fontFamily = asString(input.fontFamily, DEFAULT_CONFIG.fontFamily).slice(
|
||||||
|
0,
|
||||||
|
80,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (start < 0 || start > 1_000_000) {
|
||||||
|
throw new Error("Start number must be between 0 and 1000000");
|
||||||
|
}
|
||||||
|
if (pad < 0 || pad > 6) {
|
||||||
|
throw new Error("Pad length must be between 0 and 6");
|
||||||
|
}
|
||||||
|
if (count < 1 || count > MAX_COUNT) {
|
||||||
|
throw new Error(`Count must be between 1 and ${MAX_COUNT}`);
|
||||||
|
}
|
||||||
|
if (fontSize < 8 || fontSize > 400) {
|
||||||
|
throw new Error("Font size must be between 8 and 400");
|
||||||
|
}
|
||||||
|
if (xPercent < 0 || xPercent > 100 || yPercent < 0 || yPercent > 100) {
|
||||||
|
throw new Error("Position must be between 0 and 100 percent");
|
||||||
|
}
|
||||||
|
if (!COLOR_RE.test(color)) {
|
||||||
|
throw new Error("Color must be a hex value like #111111");
|
||||||
|
}
|
||||||
|
if (!COLOR_RE.test(shadowColor)) {
|
||||||
|
throw new Error("Shadow color must be a hex value like #000000");
|
||||||
|
}
|
||||||
|
if (shadowOpacity < 0 || shadowOpacity > 1) {
|
||||||
|
throw new Error("Shadow opacity must be between 0 and 1");
|
||||||
|
}
|
||||||
|
if (shadowBlur < 0 || shadowBlur > 80) {
|
||||||
|
throw new Error("Shadow blur must be between 0 and 80");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
shadowOffsetX < -80 ||
|
||||||
|
shadowOffsetX > 80 ||
|
||||||
|
shadowOffsetY < -80 ||
|
||||||
|
shadowOffsetY > 80
|
||||||
|
) {
|
||||||
|
throw new Error("Shadow offset must be between -80 and 80");
|
||||||
|
}
|
||||||
|
if (!COLOR_RE.test(outlineColor)) {
|
||||||
|
throw new Error("Outline color must be a hex value like #ffffff");
|
||||||
|
}
|
||||||
|
if (outlineOpacity < 0 || outlineOpacity > 1) {
|
||||||
|
throw new Error("Outline opacity must be between 0 and 1");
|
||||||
|
}
|
||||||
|
if (outlineWidth < 0 || outlineWidth > 40) {
|
||||||
|
throw new Error("Outline width must be between 0 and 40");
|
||||||
|
}
|
||||||
|
if (align !== "left" && align !== "center" && align !== "right") {
|
||||||
|
throw new Error("Alignment must be left, center, or right");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
prefix,
|
||||||
|
suffix,
|
||||||
|
start,
|
||||||
|
pad,
|
||||||
|
count,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
color,
|
||||||
|
bold: asBool(input.bold),
|
||||||
|
italic: asBool(input.italic),
|
||||||
|
align,
|
||||||
|
xPercent,
|
||||||
|
yPercent,
|
||||||
|
shadowEnabled: asBool(input.shadowEnabled),
|
||||||
|
shadowColor,
|
||||||
|
shadowOpacity,
|
||||||
|
shadowBlur,
|
||||||
|
shadowOffsetX,
|
||||||
|
shadowOffsetY,
|
||||||
|
outlineEnabled: asBool(input.outlineEnabled),
|
||||||
|
outlineColor,
|
||||||
|
outlineOpacity,
|
||||||
|
outlineWidth,
|
||||||
|
useCustomFont: asBool(input.useCustomFont),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toIdDrawStyle(
|
||||||
|
config: GenerateConfig,
|
||||||
|
fontFamily = config.useCustomFont ? CUSTOM_FONT_FAMILY : config.fontFamily,
|
||||||
|
): IdDrawStyle {
|
||||||
|
return {
|
||||||
|
fontFamily,
|
||||||
|
fontSize: config.fontSize,
|
||||||
|
color: config.color,
|
||||||
|
bold: config.bold,
|
||||||
|
italic: config.italic,
|
||||||
|
align: config.align,
|
||||||
|
xPercent: config.xPercent,
|
||||||
|
yPercent: config.yPercent,
|
||||||
|
shadowEnabled: config.shadowEnabled,
|
||||||
|
shadowColor: config.shadowColor,
|
||||||
|
shadowOpacity: config.shadowOpacity,
|
||||||
|
shadowBlur: config.shadowBlur,
|
||||||
|
shadowOffsetX: config.shadowOffsetX,
|
||||||
|
shadowOffsetY: config.shadowOffsetY,
|
||||||
|
outlineEnabled: config.outlineEnabled,
|
||||||
|
outlineColor: config.outlineColor,
|
||||||
|
outlineOpacity: config.outlineOpacity,
|
||||||
|
outlineWidth: config.outlineWidth,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hexToRgba(hex: string, opacity: number): string {
|
||||||
|
let value = hex.replace("#", "");
|
||||||
|
if (value.length === 3) {
|
||||||
|
value = value
|
||||||
|
.split("")
|
||||||
|
.map((char) => char + char)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
let alpha = clamp(opacity, 0, 1);
|
||||||
|
if (value.length === 8) {
|
||||||
|
alpha *= parseInt(value.slice(6, 8), 16) / 255;
|
||||||
|
value = value.slice(0, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = parseInt(value.slice(0, 2), 16);
|
||||||
|
const g = parseInt(value.slice(2, 4), 16);
|
||||||
|
const b = parseInt(value.slice(4, 6), 16);
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { GlobalFonts } from "@napi-rs/canvas";
|
||||||
|
import path from "node:path";
|
||||||
|
import { PRESET_FONTS } from "./fonts";
|
||||||
|
|
||||||
|
let registered = false;
|
||||||
|
|
||||||
|
export function registerPresetFonts(): void {
|
||||||
|
if (registered) return;
|
||||||
|
|
||||||
|
const dir = path.join(process.cwd(), "public", "fonts");
|
||||||
|
for (const font of PRESET_FONTS) {
|
||||||
|
GlobalFonts.registerFromPath(path.join(dir, font.regular), font.id);
|
||||||
|
GlobalFonts.registerFromPath(path.join(dir, font.bold), font.id);
|
||||||
|
}
|
||||||
|
registered = true;
|
||||||
|
}
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
serverExternalPackages: ["@napi-rs/canvas"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
Generated
+805
@@ -8,12 +8,15 @@
|
|||||||
"name": "kk_card_gen",
|
"name": "kk_card_gen",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@napi-rs/canvas": "^1.0.9",
|
||||||
|
"archiver": "^8.0.0",
|
||||||
"next": "16.3.5",
|
"next": "16.3.5",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8"
|
"react-dom": "19.2.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/archiver": "^8.0.0",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
@@ -1070,6 +1073,255 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@napi-rs/canvas": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-QviPdJImDi/jMAvBfqaw+19BndMd/sizXVW3NnpMd3VJGz++QXkOHcP9kWR/smHG0hNjHeyuHFyrx/5lD0oNcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"e2e/*"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@napi-rs/canvas-android-arm64": "1.0.9",
|
||||||
|
"@napi-rs/canvas-darwin-arm64": "1.0.9",
|
||||||
|
"@napi-rs/canvas-darwin-x64": "1.0.9",
|
||||||
|
"@napi-rs/canvas-linux-arm-gnueabihf": "1.0.9",
|
||||||
|
"@napi-rs/canvas-linux-arm64-gnu": "1.0.9",
|
||||||
|
"@napi-rs/canvas-linux-arm64-musl": "1.0.9",
|
||||||
|
"@napi-rs/canvas-linux-riscv64-gnu": "1.0.9",
|
||||||
|
"@napi-rs/canvas-linux-x64-gnu": "1.0.9",
|
||||||
|
"@napi-rs/canvas-linux-x64-musl": "1.0.9",
|
||||||
|
"@napi-rs/canvas-win32-arm64-msvc": "1.0.9",
|
||||||
|
"@napi-rs/canvas-win32-x64-msvc": "1.0.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-android-arm64": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-4LGXk2/0HVzE29K8SzML5WubgCp++B1FH3qgl35XmSZE+lLdr6P9VRQEnZ0MCLZMTSuJP41yyhtoiVNEuvrTIA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-darwin-arm64": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-YNdfLBzY0W/Pep9fo2L6RmoNlNksnn05LRnX66W63R3ij58S25QOTcjdtEt2v8+PnCESzqZsYzUo+QPeIR44NA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-darwin-x64": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-ceZQSknTEcy3dOXoekv59LTCkXjvnLsq+VW5PeNNDHEPQbRS5Ervkm1EaDa7WLAjiYWMLuSQTRTHao1dEX4prg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-XhfI0Wwv4llhd6nnWDtY3kQKjq0r+y1i91PlJlJI24ag2U9WrnwbG1qS3+fDLEyouwEVFchiKkTEHozK+5iUNA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-012oiYtKaE7i9oxc8q7nraT7kDOpLcaCmFLzVe9Ty34RHDdoDzbWLrVh827CNxYh/EADX1eSikA3ymLjo/nNuw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-Ls5UWYFFn63casTZEczbeyEg3vRDRkv9lscuGwfchtY5yLLQhgOB8SN4YGxmfJ5vTaBwZ2YUxBS3NtmjJmFXdA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-hLKEGxV7ZiRHqndePTokgDMdBlo/rDfzg7P4p4QIv9pUhuYobnu3R2NIFLCRghG0nwfo+s2sw+c1xZFeCmEAsw==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-6kaz3w0QMy77PDWk6rJ1ksIihdad3qzEyX2o2oGT8GwCaypfT5mhjr8buOO5hstyLxcWXDScuz56RsINLtBPIQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-linux-x64-musl": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-xrGvmS3v55hmZ86ls/kBLVNMUTYio3f6Ik0DireemG994VfPAwiA3ZXA0Uf1bByctkB3NQ1Sfb+H5bkdUnnzfQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-win32-arm64-msvc": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-yjmVS3ArZeRVCP7jqbPq4rpZa/BhTeI7ELE2XqJg3snICQBDevLZyArxswHkiTnT34KRic33/4fLirrHI+SY8A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-QlSYQdMQslB81nlABo9wNfQ6npFhE7/O+saCZdqVGueGanyRk4jCogD5EwQenfP3kIq9e+mm6GreQBjX5MrA8g==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.4.tgz",
|
||||||
@@ -1615,6 +1867,17 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/archiver": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-YpXPbEuv9+eUIPPQWUPahj3cvs9isWRuF+J4z+KbdYVDO3rWorWQFxUVHnwPu2AgKwvgpki5F2VMX0Xx+mX45A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*",
|
||||||
|
"@types/readdir-glob": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||||
@@ -1666,6 +1929,16 @@
|
|||||||
"@types/react": "^19.3.0"
|
"@types/react": "^19.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/readdir-glob": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.70.0",
|
"version": "8.70.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.0.tgz",
|
||||||
@@ -2285,6 +2558,18 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/abort-controller": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"event-target-shim": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.18.0",
|
"version": "8.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
|
||||||
@@ -2341,6 +2626,26 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/archiver": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/archiver/-/archiver-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-fV1orZfsnPn9BaSByR/qE67rJCLJEy2Ox5bq7nJh+jquWaNh6Sfec75kJ2T6PtdGUbPQlrVoSVCEOa5SdiTQ1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"async": "^3.2.4",
|
||||||
|
"buffer-crc32": "^1.0.0",
|
||||||
|
"is-stream": "^4.0.0",
|
||||||
|
"lazystream": "^1.0.0",
|
||||||
|
"normalize-path": "^3.0.0",
|
||||||
|
"readable-stream": "^4.0.0",
|
||||||
|
"readdir-glob": "^3.0.0",
|
||||||
|
"tar-stream": "^3.0.0",
|
||||||
|
"zip-stream": "^7.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
@@ -2525,6 +2830,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/async": {
|
||||||
|
"version": "3.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||||
|
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/async-function": {
|
"node_modules/async-function": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
|
||||||
@@ -2571,6 +2882,20 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/b4a": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-native-b4a": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-native-b4a": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -2578,6 +2903,106 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/bare-events": {
|
||||||
|
"version": "2.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz",
|
||||||
|
"integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"bare-abort-controller": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bare-abort-controller": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bare-fs": {
|
||||||
|
"version": "4.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.1.tgz",
|
||||||
|
"integrity": "sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"bare-events": "^2.5.4",
|
||||||
|
"bare-path": "^3.0.0",
|
||||||
|
"bare-stream": "^2.6.4",
|
||||||
|
"bare-url": "^2.2.2",
|
||||||
|
"fast-fifo": "^1.3.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"bare": ">=1.28.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bare-buffer": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bare-buffer": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bare-path": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/bare-stream": {
|
||||||
|
"version": "2.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz",
|
||||||
|
"integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"b4a": "^1.8.1",
|
||||||
|
"streamx": "^2.25.0",
|
||||||
|
"teex": "^1.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bare-abort-controller": "*",
|
||||||
|
"bare-buffer": "*",
|
||||||
|
"bare-events": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bare-abort-controller": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"bare-buffer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"bare-events": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bare-url": {
|
||||||
|
"version": "2.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz",
|
||||||
|
"integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"bare-path": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.11.22",
|
"version": "2.11.22",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.22.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.22.tgz",
|
||||||
@@ -2648,6 +3073,39 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/buffer-crc32": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/call-bind": {
|
"node_modules/call-bind": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
|
||||||
@@ -2771,6 +3229,22 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/compress-commons": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-g0S8KAD8qf4+V//pr3BfB1aBnARLXNz2Gx+jmHU0LEriUuoQUOPOulVquHKTJ8+EAIIO7fhseNDr9wK5Q9FKBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"crc-32": "^1.2.0",
|
||||||
|
"crc32-stream": "^7.0.1",
|
||||||
|
"is-stream": "^4.0.0",
|
||||||
|
"normalize-path": "^3.0.0",
|
||||||
|
"readable-stream": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -2785,6 +3259,37 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/core-util-is": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/crc-32": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"crc32": "bin/crc32.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/crc32-stream": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-IBWsY8xznyQrcHn8h4bC8/4ErNke5elzgG8GcqF4RFPw6aHkWWRc7Tgw6upjaTX/CT/yQgqYENkxYsTYN+hW2g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"crc-32": "^1.2.0",
|
||||||
|
"readable-stream": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -3624,6 +4129,33 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/event-target-shim": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/events": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/events-universal": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"bare-events": "^2.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@@ -3631,6 +4163,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-fifo": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
|
||||||
@@ -4073,6 +4611,26 @@
|
|||||||
"hermes-estree": "0.25.1"
|
"hermes-estree": "0.25.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ieee754": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -4110,6 +4668,12 @@
|
|||||||
"node": ">=0.8.19"
|
"node": ">=0.8.19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/internal-slot": {
|
"node_modules/internal-slot": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
||||||
@@ -4459,6 +5023,18 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-stream": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-string": {
|
"node_modules/is-string": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
|
||||||
@@ -4721,6 +5297,54 @@
|
|||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lazystream": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"readable-stream": "^2.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lazystream/node_modules/isarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lazystream/node_modules/readable-stream": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"core-util-is": "~1.0.0",
|
||||||
|
"inherits": "~2.0.3",
|
||||||
|
"isarray": "~1.0.0",
|
||||||
|
"process-nextick-args": "~2.0.0",
|
||||||
|
"safe-buffer": "~5.1.1",
|
||||||
|
"string_decoder": "~1.1.1",
|
||||||
|
"util-deprecate": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lazystream/node_modules/safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lazystream/node_modules/string_decoder": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/levn": {
|
"node_modules/levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
@@ -5267,6 +5891,15 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/normalize-path": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@@ -5567,6 +6200,21 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/process": {
|
||||||
|
"version": "0.11.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||||
|
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/process-nextick-args": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
@@ -5638,6 +6286,73 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "4.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||||
|
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"string_decoder": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/readdir-glob": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-AhNB2KgKeVJr16nK9LLZbJNWnYoT23ZrumNKFDebHBdkC8KHSqWo871JAUhoWC/RtjEVdqNMFpM6qrwRbaUqpw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"minimatch": "^10.2.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/yqnn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/readdir-glob/node_modules/balanced-match": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/readdir-glob/node_modules/brace-expansion": {
|
||||||
|
"version": "5.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||||
|
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/readdir-glob/node_modules/minimatch": {
|
||||||
|
"version": "10.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
|
||||||
|
"integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^5.0.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reflect.getprototypeof": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||||
@@ -5781,6 +6496,26 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/safe-push-apply": {
|
"node_modules/safe-push-apply": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
|
||||||
@@ -6073,6 +6808,26 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/streamx": {
|
||||||
|
"version": "2.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz",
|
||||||
|
"integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"events-universal": "^1.0.0",
|
||||||
|
"fast-fifo": "^1.3.2",
|
||||||
|
"text-decoder": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string.prototype.includes": {
|
"node_modules/string.prototype.includes": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
|
||||||
@@ -6280,6 +7035,36 @@
|
|||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tar-stream": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"b4a": "^1.6.4",
|
||||||
|
"bare-fs": "^4.5.5",
|
||||||
|
"fast-fifo": "^1.2.0",
|
||||||
|
"streamx": "^2.15.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/teex": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"streamx": "^2.12.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/text-decoder": {
|
||||||
|
"version": "1.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
|
||||||
|
"integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"b4a": "^1.6.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.17",
|
"version": "0.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
@@ -6620,6 +7405,12 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
@@ -6755,6 +7546,20 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/zip-stream": {
|
||||||
|
"version": "7.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-7.0.5.tgz",
|
||||||
|
"integrity": "sha512-dSvYKdvLsAHCDqPOhIwk/q5CvuWtTB3Dgpoe0uVEFjTzIOAmsQpprX25InCvrvJsirEbu1OHyy67n/kAj1Sw/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"compress-commons": "^7.0.0",
|
||||||
|
"normalize-path": "^3.0.0",
|
||||||
|
"readable-stream": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.6.2.tgz",
|
||||||
|
|||||||
+5
-2
@@ -3,18 +3,21 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev --port 47382",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start --port 47382",
|
||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@napi-rs/canvas": "^1.0.9",
|
||||||
|
"archiver": "^8.0.0",
|
||||||
"next": "16.3.5",
|
"next": "16.3.5",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8"
|
"react-dom": "19.2.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/archiver": "^8.0.0",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
Reference in New Issue
Block a user