feat(donate): add donate UI

- rework form core and migrate all old usages
This commit is contained in:
2026-06-05 15:24:41 +07:00 Unverified
parent 5efc86b6c8
commit 75e711144f
32 changed files with 1710 additions and 244 deletions
+3
View File
@@ -27,5 +27,8 @@ WEBHOOK_RUBGRAM_SUBMIT=YOUR_WEBHOOK_RUBGRAM_SUBMIT # For Rubgram submit
SLIPOK_API_URL=YOUR_SLIPOK_API_URL SLIPOK_API_URL=YOUR_SLIPOK_API_URL
SLIPOK_API_KEY=YOUR_SLIPOK_API_KEY SLIPOK_API_KEY=YOUR_SLIPOK_API_KEY
# Gemini TTS API (Donate)
GEMINI_TTS_API_KEY=YOUR_GEMINI_TTS_API_KEY
# Environment # Environment
ENVIRONMENT=production ENVIRONMENT=production
+9 -20
View File
@@ -2,7 +2,6 @@ import { desc, eq } from "drizzle-orm";
import { ArrowRight, Save, Trash2 } from "lucide-react"; import { ArrowRight, Save, Trash2 } from "lucide-react";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import type { TypedFormData } from "@/app/(ui)/rubgram/type";
import { CdnChooser } from "@/components/chooser"; import { CdnChooser } from "@/components/chooser";
import { FormAction, FormInput, FormProvider } from "@/components/form"; import { FormAction, FormInput, FormProvider } from "@/components/form";
import { ModalBase } from "@/components/modal"; import { ModalBase } from "@/components/modal";
@@ -37,17 +36,7 @@ export default async function CharacterEditPage({
if (!char) redirect("/admin/char"); if (!char) redirect("/admin/char");
async function submit( async function submit(
form: TypedFormData<{ form: FormData,
name: string;
id: string;
element: (typeof elementEnum.enumValues)[number];
stars: "4" | "5";
image: string;
order: string;
version: string;
weapon: string;
amber: string;
}>,
) { ) {
"use server"; "use server";
if (!(await adminCheck())) redirect("/login"); if (!(await adminCheck())) redirect("/login");
@@ -72,14 +61,14 @@ export default async function CharacterEditPage({
try { try {
data = { data = {
name: form.get("name")!, name: form.get("name") as string,
vision: form.get("element")!, vision: form.get("element") as string,
stars: parseInt(form.get("stars") || "0", 10) as 4 | 5, stars: parseInt((form.get("stars") as string) || "0", 10) as 4 | 5,
image: form.get("image")!, image: form.get("image") as string,
order: parseInt(form.get("order")!, 10), order: parseInt(form.get("order") as string, 10),
version: form.get("version")!, version: form.get("version") as string,
weapon: form.get("weapon")!, weapon: form.get("weapon") as string,
amber: form.get("amber")!, amber: form.get("amber") as string,
}; };
await db.update(characters).set(data).where(eq(characters.id, charId)); await db.update(characters).set(data).where(eq(characters.id, charId));
} catch (e) { } catch (e) {
+10 -21
View File
@@ -2,7 +2,6 @@ import { desc, sql } from "drizzle-orm";
import { ArrowRight, PlusIcon } from "lucide-react"; import { ArrowRight, PlusIcon } from "lucide-react";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import type { TypedFormData } from "@/app/(ui)/rubgram/type";
import { CdnChooser } from "@/components/chooser"; import { CdnChooser } from "@/components/chooser";
import { FormAction, FormInput, FormProvider } from "@/components/form"; import { FormAction, FormInput, FormProvider } from "@/components/form";
import { ModalBase } from "@/components/modal"; import { ModalBase } from "@/components/modal";
@@ -34,17 +33,7 @@ export default async function CharacterCreatePage() {
]); ]);
async function submit( async function submit(
form: TypedFormData<{ form: FormData,
name: string;
id: string;
element: (typeof elementEnum.enumValues)[number];
stars: "4" | "5";
image: string;
order: string;
version: string;
weapon: string;
amber: string;
}>,
) { ) {
"use server"; "use server";
if (!(await adminCheck())) redirect("/login"); if (!(await adminCheck())) redirect("/login");
@@ -69,15 +58,15 @@ export default async function CharacterCreatePage() {
try { try {
data = { data = {
id: form.get("id")!, id: form.get("id") as string,
name: form.get("name")!, name: form.get("name") as string,
vision: form.get("element")!, vision: form.get("element") as string,
stars: parseInt(form.get("stars") || "0", 10) as 4 | 5, stars: parseInt((form.get("stars") as string) || "0", 10) as 4 | 5,
image: form.get("image")!, image: form.get("image") as string,
order: parseInt(form.get("order")!, 10), order: parseInt(form.get("order") as string, 10),
version: form.get("version")!, version: form.get("version") as string,
weapon: form.get("weapon")!, weapon: form.get("weapon") as string,
amber: form.get("amber")!, amber: form.get("amber") as string,
}; };
await db.insert(characters).values(data); await db.insert(characters).values(data);
} catch (e) { } catch (e) {
+5 -11
View File
@@ -2,7 +2,6 @@ import { eq } from "drizzle-orm";
import { SaveIcon, Trash2 } from "lucide-react"; import { SaveIcon, Trash2 } from "lucide-react";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import type { TypedFormData } from "@/app/(ui)/rubgram/type";
import { CdnChooser } from "@/components/chooser"; import { CdnChooser } from "@/components/chooser";
import { import {
FormAction, FormAction,
@@ -29,12 +28,7 @@ export default async function GuideEditPage({
if (!guide) notFound(); if (!guide) notFound();
async function submit( async function submit(
form: TypedFormData<{ form: FormData,
name: string;
link: string;
image: string;
order: string;
}>,
) { ) {
"use server"; "use server";
if (!(await adminCheck())) redirect("/login"); if (!(await adminCheck())) redirect("/login");
@@ -49,10 +43,10 @@ export default async function GuideEditPage({
try { try {
data = { data = {
name: form.get("name")!, name: form.get("name") as string,
image: form.get("image")!, image: form.get("image") as string,
link: form.get("link")!, link: form.get("link") as string,
order: parseInt(form.get("order")!, 10), order: parseInt(form.get("order") as string, 10),
}; };
try { try {
new URL(data.link!); new URL(data.link!);
+5 -11
View File
@@ -2,7 +2,6 @@ import { sql } from "drizzle-orm";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import type { TypedFormData } from "@/app/(ui)/rubgram/type";
import { CdnChooser } from "@/components/chooser"; import { CdnChooser } from "@/components/chooser";
import { import {
FormAction, FormAction,
@@ -29,12 +28,7 @@ export default async function GuideCreatePage() {
}) })
.from(guides); .from(guides);
async function submit( async function submit(
form: TypedFormData<{ form: FormData,
name: string;
link: string;
image: string;
order: string;
}>,
) { ) {
"use server"; "use server";
if (!(await adminCheck())) redirect("/login"); if (!(await adminCheck())) redirect("/login");
@@ -49,10 +43,10 @@ export default async function GuideCreatePage() {
try { try {
data = { data = {
name: form.get("name")!, name: form.get("name") as string,
image: form.get("image")!, image: form.get("image") as string,
link: form.get("link")!, link: form.get("link") as string,
order: parseInt(form.get("order")!, 10), order: parseInt(form.get("order") as string, 10),
}; };
try { try {
new URL(data.link); new URL(data.link);
@@ -3,7 +3,6 @@ import { ArrowRight, PlusIcon, SaveIcon, Trash2 } from "lucide-react";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import Link from "next/link"; import Link from "next/link";
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import type { TypedFormData } from "@/app/(ui)/rubgram/type";
import { CdnChooser } from "@/components/chooser"; import { CdnChooser } from "@/components/chooser";
import { DatePicker } from "@/components/date"; import { DatePicker } from "@/components/date";
import { import {
@@ -44,15 +43,7 @@ export default async function TlVersionEditPage({
if (!ver) notFound(); if (!ver) notFound();
async function submit( async function submit(
form: TypedFormData<{ form: FormData,
name: string;
id: string;
order: string;
from: string;
image: string;
disclaimer: string;
deprecates: string;
}>,
) { ) {
"use server"; "use server";
if (!(await adminCheck())) redirect("/login"); if (!(await adminCheck())) redirect("/login");
@@ -73,13 +64,13 @@ export default async function TlVersionEditPage({
try { try {
data = { data = {
id: form.get("id")!, id: form.get("id") as string,
name: form.get("name")!, name: form.get("name") as string,
image: form.get("image")!, image: form.get("image") as string,
disclaimer: form.get("disclaimer")!, disclaimer: form.get("disclaimer") as string,
from: form.get("from")!, from: form.get("from") as string,
deprecates: form.get("deprecates")!, deprecates: form.get("deprecates") as string,
order: parseInt(form.get("order")!, 10), order: parseInt(form.get("order") as string, 10),
type: typeId, type: typeId,
}; };
await db await db
@@ -3,7 +3,6 @@ import { ArrowRight, PlusIcon } from "lucide-react";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import Link from "next/link"; import Link from "next/link";
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import type { TypedFormData } from "@/app/(ui)/rubgram/type";
import { CdnChooser } from "@/components/chooser"; import { CdnChooser } from "@/components/chooser";
import { DatePicker } from "@/components/date"; import { DatePicker } from "@/components/date";
import { import {
@@ -59,15 +58,7 @@ export default async function TlVersionCreatePage({
if (!type) notFound(); if (!type) notFound();
async function submit( async function submit(
form: TypedFormData<{ form: FormData,
name: string;
id: string;
order: string;
from: string;
image: string;
disclaimer: string;
deprecates: string;
}>,
) { ) {
"use server"; "use server";
if (!(await adminCheck())) redirect("/login"); if (!(await adminCheck())) redirect("/login");
@@ -88,13 +79,13 @@ export default async function TlVersionCreatePage({
try { try {
data = { data = {
id: form.get("id")!, id: form.get("id") as string,
name: form.get("name")!, name: form.get("name") as string,
image: form.get("image")!, image: form.get("image") as string,
disclaimer: form.get("disclaimer")!, disclaimer: form.get("disclaimer") as string,
from: form.get("from")!, from: form.get("from") as string,
deprecates: form.get("deprecates")!, deprecates: form.get("deprecates") as string,
order: parseInt(form.get("order")!, 10), order: parseInt(form.get("order") as string, 10),
type: typeId, type: typeId,
}; };
await db.insert(tierlistVersions).values(data); await db.insert(tierlistVersions).values(data);
@@ -3,7 +3,6 @@ import { eq } from "drizzle-orm";
import { SaveIcon, Trash2 } from "lucide-react"; import { SaveIcon, Trash2 } from "lucide-react";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import type { TypedFormData } from "@/app/(ui)/rubgram/type";
import { CdnChooser } from "@/components/chooser"; import { CdnChooser } from "@/components/chooser";
import { import {
FormAction, FormAction,
@@ -34,13 +33,7 @@ export default async function TlTypeEditPage({
if (!type) notFound(); if (!type) notFound();
async function submit( async function submit(
form: TypedFormData<{ form: FormData,
name: string;
id: string;
order: string;
image: string;
mode: string;
}>,
) { ) {
"use server"; "use server";
if (!(await adminCheck())) redirect("/login"); if (!(await adminCheck())) redirect("/login");
@@ -55,11 +48,11 @@ export default async function TlTypeEditPage({
try { try {
data = { data = {
id: form.get("id")!, id: form.get("id") as string,
name: form.get("name")!, name: form.get("name") as string,
image: form.get("image")!, image: form.get("image") as string,
order: parseInt(form.get("order")!, 10), order: parseInt(form.get("order") as string, 10),
mode: form.get("mode")!, mode: form.get("mode") as string,
}; };
await db await db
.update(tierlistTypes) .update(tierlistTypes)
+6 -13
View File
@@ -2,7 +2,6 @@ import { sql } from "drizzle-orm";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import type { TypedFormData } from "@/app/(ui)/rubgram/type";
import { CdnChooser } from "@/components/chooser"; import { CdnChooser } from "@/components/chooser";
import { FormAction, FormInput, FormProvider } from "@/components/form"; import { FormAction, FormInput, FormProvider } from "@/components/form";
import { ModalBase } from "@/components/modal"; import { ModalBase } from "@/components/modal";
@@ -22,13 +21,7 @@ export default async function TlTypeCreatePage() {
.from(tierlistTypes); .from(tierlistTypes);
async function submit( async function submit(
form: TypedFormData<{ form: FormData,
name: string;
id: string;
order: string;
image: string;
mode: string;
}>,
) { ) {
"use server"; "use server";
if (!(await adminCheck())) redirect("/login"); if (!(await adminCheck())) redirect("/login");
@@ -43,11 +36,11 @@ export default async function TlTypeCreatePage() {
try { try {
data = { data = {
id: form.get("id")!, id: form.get("id") as string,
name: form.get("name")!, name: form.get("name") as string,
image: form.get("image")!, image: form.get("image") as string,
mode: form.get("mode")!, mode: form.get("mode") as string,
order: parseInt(form.get("order")!, 10), order: parseInt(form.get("order") as string, 10),
}; };
await db.insert(tierlistTypes).values(data); await db.insert(tierlistTypes).values(data);
} catch (e) { } catch (e) {
+81
View File
@@ -0,0 +1,81 @@
"use client";
import { SimpleTooltip } from "@/components/tooltip";
import { Kbd } from "@/components/ui/kbd";
import { cn } from "@/lib/utils";
import { X, FileSearch2 } from "lucide-react";
import { useRef, useState, useEffect } from "react";
import { b2sClient } from "../admin/cdn/table";
import { Button } from "@/components/ui/button";
export function DonateImageUpload({
value: externalValue,
onValueChange,
}: {
value?: File | null | undefined;
onValueChange?: (value: File | null) => void;
}) {
const ref = useRef<HTMLInputElement>(null);
const [value, setValue] = useState<File | undefined | null>(externalValue);
const isSelected = !!value?.name;
useEffect(() => {
if (
typeof externalValue === "object" &&
externalValue &&
Object.keys(externalValue).length === 0
)
// ignore {}
return;
setValue(externalValue);
}, [externalValue]);
function choose() {
ref.current?.click();
}
return (
<>
<Button
onClick={isSelected ? undefined : () => ref.current?.click()}
className={cn(
"h-full w-full aspect-square justify-between",
isSelected && "text-emerald-300!",
)}
variant="outline"
type="button"
>
{isSelected ? (
<span onClick={isSelected ? choose : undefined} className="truncate">
{value.name} <Kbd>{b2sClient(Number(value.size))}</Kbd>
</span>
) : (
"a"
)}
{isSelected ? (
<SimpleTooltip text="Unselect">
<X
className="hover:text-red-500 pointer-events-auto!"
onClick={() => {
setValue(null);
onValueChange?.(null);
}}
/>
</SimpleTooltip>
) : (
<FileSearch2 />
)}
</Button>
<input
ref={ref}
type="file"
accept="image/*"
hidden
onChange={() => {
const file = ref.current?.files?.[0] || null;
setValue(file);
onValueChange?.(file);
}}
/>
</>
);
}
+162
View File
@@ -0,0 +1,162 @@
import { formParse } from "@/components/form-submit";
import {
FormAction,
FormChoice,
FormInput,
FormProvider,
FormRow,
FormTab,
} from "@/components/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
CurrencyInput,
SlipUpload,
} from "../rubgram/admin/@modal/manual/client";
import Image from "@/components/image";
import PromptpayImage from "#/assets/promptpay.jpg";
import { DownloadButton } from "../rubgram/client";
import { QrCodeIcon, SendIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import TruemoneyIcon from "#/assets/tmn.webp";
import z from "zod";
import { th } from "zod/v4/locales";
import Cropper from "@/components/cropper";
z.config(th());
const Schema = z
.object({
name: z.string().max(50, { error: "ชื่อยาวสุด 50 ตัวอักษร" }).optional(),
message: z
.string()
.max(500, { error: "ข้อความยาวสุด 200 ตัวอักษร" })
.optional(),
amount: z.coerce
.number({ error: "จำนวนต้องเป็นตัวเลข" })
.min(10, { error: "โดเนทขั้นต่ำ 10 บาท" }),
})
.and(
z.discriminatedUnion("type", [
z.object({
type: z.literal("tmn"),
link: z.httpUrl(),
}),
z.object({
type: z.literal("pp"),
slip: z.file(),
}),
]),
);
export default async function () {
async function submit(data: FormData) {
"use server";
const { $, error } = formParse(Schema, data);
if (error) return { error };
return { toast: "ระบบยังไม่เสร็จ" };
}
return (
<div className="flex items-center justify-center min-h-svh">
<div className="p-5 bg-card border rounded-md w-full max-w-md">
<FormProvider id="tip" inDialog={false} onSubmit={submit}>
<FormRow>
<FormInput name="image" className="h-full">
<Cropper />
</FormInput>
<div className="grid gap-4 [&>label]:-mb-2 grow">
<FormInput name="name" label="ชื่อ" subLabel="ไม่จำเป็น">
<Input placeholder="Anonymous" />
</FormInput>
<FormInput name="amount" label="จำนวนโดเนท" subLabel="ขึ้นจอขั้นต่ำ 10 บาท">
<CurrencyInput placeholder="ขั้นต่ำ 1 บาท" />
</FormInput>
</div>
</FormRow>
<FormInput name="message" label="ข้อความ" subLabel="max. 500 ตัวอักษร">
<Textarea placeholder="ข้อความ" />
</FormInput>
<FormTab
label="วิธีการโอนเงิน"
name="type"
tabs={[
{
label: (
<span className="flex gap-1 items-center">
<Image
src={TruemoneyIcon}
alt="Truemoney"
className="w-12 h-6 object-cover"
/>
TrueMoney
</span>
),
value: "tmn",
},
{
label: (
<span className="flex gap-1 items-center">
<QrCodeIcon className="size-6" />
PromptPay
</span>
),
value: "pp",
},
]}
>
<FormChoice value="tmn">
<FormInput name="link" label="ลิ้งค์อั่งเปา">
<Input placeholder="https://gift.truemoney.com/campaign/?v=..." />
</FormInput>
</FormChoice>
<FormChoice value="pp">
<div className="flex gap-2 w-full pb-2">
<Image
src={PromptpayImage}
alt="Promptpay QR Code"
className="rounded max-w-32 shrink-0"
/>
<div className="flex flex-col shrink-0 relative">
<span className="font-bold text-sm">บัญชีรับโดเนท</span>
<span className="text-sm text-muted-foreground">
ผู้รับ: นาย พัชรพล พลพันธุ์
</span>
<span className="text-sm text-muted-foreground">
บัญชี: xxx-x-x8666-x
</span>
<span className="text-sm text-muted-foreground">
เลขที่อ้างอิง: 004999056945438
</span>
<DownloadButton />
</div>
</div>
<FormInput name="slip">
<SlipUpload />
</FormInput>
</FormChoice>
</FormTab>
<div className="flex justify-end">
<Button asChild>
<FormAction
disabled
type="submit"
loading={
<>
<Spinner />
Creating...
</>
}
>
<SendIcon />
ส่งเลยจัฟลูกพี่
</FormAction>
</Button>
</div>
</FormProvider>
</div>
</div>
);
}
+13 -8
View File
@@ -1,6 +1,13 @@
"use client"; "use client";
import { Feather, Home, LoaderPinwheel, StickyNote, Table } from "lucide-react"; import {
Bitcoin,
Feather,
Home,
LoaderPinwheel,
StickyNote,
Table,
} from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
@@ -11,7 +18,9 @@ export function Navbar() {
const pathname = usePathname(); const pathname = usePathname();
const mobile = useIsMobile(); const mobile = useIsMobile();
if (!/^\/(?:tl(?:\/[a-zA-Z0-9]+)?|rubgram|artifact|guide)$/gm.test(pathname)) if (
!/^\/(?:tl(?:\/[a-zA-Z0-9]+)?|rubgram|artifact|guide|tip)$/gm.test(pathname)
)
return ""; return "";
return ( return (
<div <div
@@ -32,12 +41,8 @@ export function Navbar() {
href="/rubgram" href="/rubgram"
/> />
<Item icon={<Table size={16} />} name="จัดเทียร์ลิสต์" href="/tl" /> <Item icon={<Table size={16} />} name="จัดเทียร์ลิสต์" href="/tl" />
<Item <Item icon={<StickyNote size={16} />} name="ไกด์ตัวละคร" href="/guide" />
icon={<StickyNote size={16} />} <Item icon={<Bitcoin size={16} />} name="โดเนทขึ้นจอ" href="/tip" last />
name="ไกด์ตัวละคร"
href="/guide"
last
/>
</div> </div>
); );
} }
+8 -2
View File
@@ -29,7 +29,7 @@ export default function Home() {
</center> </center>
</section> </section>
<div className="flex flex-wrap my-10"> <div className="flex flex-wrap justify-evenly my-10 gap-5">
<HomeLink <HomeLink
name="เสือกไอดีชาวบ้าน" name="เสือกไอดีชาวบ้าน"
desc="ระบบลงคิวดูอาร์ติแฟกต์เกนชินในไลฟ์สตรีม" desc="ระบบลงคิวดูอาร์ติแฟกต์เกนชินในไลฟ์สตรีม"
@@ -59,6 +59,12 @@ export default function Home() {
href="/guide" href="/guide"
logo={GuideLogo} logo={GuideLogo}
/> />
{/* <HomeLink
name="Donate"
desc="โดเนทขึ้นไลฟ์สตรีม"
href="/tip"
logo={GuideLogo}
/> */}
</div> </div>
<footer className="text-muted-foreground h-full flex flex-col justify-end pb-3 md:justify-center"> <footer className="text-muted-foreground h-full flex flex-col justify-end pb-3 md:justify-center">
<div className="flex justify-between flex-col text-center gap-2 sm:flex-row sm:text-left"> <div className="flex justify-between flex-col text-center gap-2 sm:flex-row sm:text-left">
@@ -144,7 +150,7 @@ function HomeLink({
<SimpleTooltip text={desc}> <SimpleTooltip text={desc}>
<Link <Link
href={href} href={href}
className={cn("flex flex-col p-10 mx-auto text-center", className)} className={cn("flex flex-col p-10 text-center", className)}
> >
<Image <Image
src={logo} src={logo}
@@ -24,14 +24,13 @@ import {
MultiSelectValue, MultiSelectValue,
} from "@/components/ui/multi-select"; } from "@/components/ui/multi-select";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Select, SelectTrigger } from "@/components/ui/select";
import { VirtualizedComboBox } from "@/components/combobox"; import { VirtualizedComboBox } from "@/components/combobox";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
export function CurrencyInput(props: ComponentProps<typeof Input>) { export function CurrencyInput(props: ComponentProps<typeof Input>) {
return ( return (
<InputGroup className="max-w-xs"> <InputGroup>
<InputGroupInput placeholder="ฟรี" {...props} /> <InputGroupInput {...props} />
<InputGroupAddon align="inline-end">฿</InputGroupAddon> <InputGroupAddon align="inline-end">฿</InputGroupAddon>
</InputGroup> </InputGroup>
); );
@@ -131,7 +130,7 @@ export function SlipUpload({
{value.name} <Kbd>{b2sClient(Number(value.size))}</Kbd> {value.name} <Kbd>{b2sClient(Number(value.size))}</Kbd>
</span> </span>
) : ( ) : (
"Upload Slip" "อัพโหลดสลิปโอนเงิน"
)} )}
{isSelected ? ( {isSelected ? (
<SimpleTooltip text="Unselect"> <SimpleTooltip text="Unselect">
@@ -151,7 +150,6 @@ export function SlipUpload({
ref={ref} ref={ref}
type="file" type="file"
accept="image/*" accept="image/*"
required
hidden hidden
onChange={() => { onChange={() => {
const file = ref.current?.files?.[0] || null; const file = ref.current?.files?.[0] || null;
+12 -4
View File
@@ -22,7 +22,6 @@ import {
endgameSubmissions, endgameSubmissions,
endgameTypes, endgameTypes,
} from "@/lib/db/schema"; } from "@/lib/db/schema";
import type { TypedFormData, TypedFormDataShape } from "../../../type";
import { CurrencyInput, ServiceSelect, SlipUpload, UserSelect } from "./client"; import { CurrencyInput, ServiceSelect, SlipUpload, UserSelect } from "./client";
import { import {
Select, Select,
@@ -35,6 +34,7 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { SlipokResponse } from "../../../api"; import { SlipokResponse } from "../../../api";
import { getDiscordUsers } from "@/app/api/discord/users/api"; import { getDiscordUsers } from "@/app/api/discord/users/api";
import { th } from "zod/v4/locales";
const Schema = z.object({ const Schema = z.object({
name: z.string(), name: z.string(),
@@ -48,14 +48,22 @@ const Schema = z.object({
export default async function RgManualCreateModal() { export default async function RgManualCreateModal() {
if (!(await adminCheck())) redirect("/login"); if (!(await adminCheck())) redirect("/login");
const types = await db.select().from(endgameTypes); const types = await db.select().from(endgameTypes);
async function submit(form: TypedFormData<TypedFormDataShape>) { async function submit(form: FormData) {
"use server"; "use server";
if (!(await adminCheck())) redirect("/login"); if (!(await adminCheck())) redirect("/login");
const raw = Object.fromEntries([...form.entries()]); const raw = Object.fromEntries([...form.entries()]);
z.config(th());
const parsed = Schema.safeParse(raw); const parsed = Schema.safeParse(raw);
console.log(raw, parsed); console.log(raw, parsed);
if (!parsed.success) return { error: z.prettifyError(parsed.error) }; if (!parsed.success) {
return {
error: parsed.error.issues.map((issue) => ({
what: issue.message,
where: issue.path.join("."),
})),
};
}
const { data } = parsed; const { data } = parsed;
const [{ id: slipId }] = await db const [{ id: slipId }] = await db
@@ -101,7 +109,7 @@ export default async function RgManualCreateModal() {
<Input placeholder="Dreamgineer" autoFocus /> <Input placeholder="Dreamgineer" autoFocus />
</FormInput> </FormInput>
<FormInput name="price" label="Price" className="flex-1"> <FormInput name="price" label="Price" className="flex-1">
<CurrencyInput /> <CurrencyInput placeholder="ฟรี" />
</FormInput> </FormInput>
</FormRow> </FormRow>
<FormInput name="services" label="Services"> <FormInput name="services" label="Services">
+8 -22
View File
@@ -20,8 +20,6 @@ import {
} from "@/lib/db/schema"; } from "@/lib/db/schema";
import { sse } from "@/lib/db/sse-endpoints"; import { sse } from "@/lib/db/sse-endpoints";
import { getPostHogClient } from "@/lib/posthog-server"; import { getPostHogClient } from "@/lib/posthog-server";
import type { TypedFormData } from "./type";
const { const {
SLIPOK_API_URL, SLIPOK_API_URL,
SLIPOK_API_KEY, SLIPOK_API_KEY,
@@ -205,19 +203,12 @@ export async function getEndgameConfig() {
}; };
} }
export type EndgameFormData = TypedFormData<{ export async function submitEndgame(formData: FormData) {
name: string;
server: "as" | "eu" | "us" | "tw";
service: string;
user: string; // user snowflake
}>;
export async function submitEndgame(formData: EndgameFormData) {
const { full, locked, limit, types, free } = await getEndgameConfig(); const { full, locked, limit, types, free } = await getEndgameConfig();
const name = formData.get("name"); const name = formData.get("name") as string;
const server = formData.get("server"); const server = formData.get("server") as "as" | "eu" | "us" | "tw";
const service = formData.getAll("service"); const service = formData.getAll("service") as string[];
const user = formData.get("user"); const user = formData.get("user") as string;
if (!name || !server || !service.length || !user) if (!name || !server || !service.length || !user)
return "กรุณากรอกข้อมูลให้ครบถ้วน"; return "กรุณากรอกข้อมูลให้ครบถ้วน";
@@ -267,14 +258,9 @@ export async function submitEndgame(formData: EndgameFormData) {
return queue; return queue;
} }
export type EndgamePaymentFormData = TypedFormData<{ export async function submitEndgamePayment(formData: FormData) {
sid: string; const sid = formData.get("sid") as string;
slip: File; const slip = formData.get("slip") as File;
}>;
export async function submitEndgamePayment(formData: EndgamePaymentFormData) {
const sid = formData.get("sid");
const slip = formData.get("slip");
if (!sid || !slip) return "กรุณาอัพโหลดสลิปให้ครบถ้วน"; if (!sid || !slip) return "กรุณาอัพโหลดสลิปให้ครบถ้วน";
const arrayBuffer = await slip.arrayBuffer(); const arrayBuffer = await slip.arrayBuffer();
+1 -3
View File
@@ -5,8 +5,6 @@ import { useRouter } from "next/navigation";
import posthog from "posthog-js"; import posthog from "posthog-js";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
type EndgameFormData,
type EndgamePaymentFormData,
submitEndgame, submitEndgame,
submitEndgamePayment, submitEndgamePayment,
} from "./api"; } from "./api";
@@ -26,7 +24,7 @@ export function EndgameFormWrapper({
); );
({ registration: submitEndgame, payment: submitEndgamePayment }) ({ registration: submitEndgame, payment: submitEndgamePayment })
[type](data as unknown as EndgameFormData & EndgamePaymentFormData) [type](data)
.then((q) => { .then((q) => {
if (typeof q === "string") return toast.error(q); if (typeof q === "string") return toast.error(q);
try { try {
-31
View File
@@ -1,31 +0,0 @@
export type TypedFormDataShape = Record<
string,
string | Blob | File | (string | Blob | File)[]
>;
export interface TypedFormData<T extends TypedFormDataShape> {
append<K extends keyof T>(
key: K,
value: T[K] extends unknown[] ? T[K][number] : T[K],
): void;
set<K extends keyof T>(
key: K,
value: T[K] extends unknown[] ? T[K][number] : T[K],
): void;
get<K extends keyof T>(
key: K,
): T[K] extends unknown[] ? T[K][number] | null : T[K] | null;
getAll<K extends keyof T>(key: K): T[K] extends unknown[] ? T[K] : T[K][];
delete<K extends keyof T>(key: K): void;
has<K extends keyof T>(key: K): boolean;
entries(): ReturnType<FormData["entries"]>;
/** underlying raw FormData */
readonly raw: FormData;
}
+10
View File
@@ -0,0 +1,10 @@
import { sse } from "@/lib/db/sse-endpoints";
import { NextRequest } from "next/server";
export async function PATCH(req: NextRequest) {
const tag = Number(req.nextUrl.searchParams.get("tag") ?? "abc");
if (Number.isNaN(tag))
return Response.json({ error: "Invalid Tag" }, { status: 400 });
sse.donate.pub("heartbeat", tag);
return Response.json({ success: true });
}
+77
View File
@@ -0,0 +1,77 @@
import { GoogleGenAI } from "@google/genai";
import wav from "wav";
import { Readable, PassThrough } from "stream";
import z from "zod";
import { env } from "process";
import { redis } from "@/lib/db/redis";
import { hash } from "bun";
const Schema = z.object({
message: z.string().max(1000),
});
const voices = ["Zephyr", "Charon", "Fenrir", "Kore", "Sulafat"];
export async function GET(request: Request) {
const data = Schema.safeParse(
Object.fromEntries(new URL(request.url).searchParams.entries()),
);
if (data.error)
return new Response(z.prettifyError(data.error), { status: 400 });
const {
data: { message },
} = data;
const textHash = hash(message).toString(36);
const cached = await redis!.get(`tts:${textHash}`);
if (cached)
return new Response(pcmToWav(cached), {
headers: { "Content-Type": "audio/wav" },
});
const client = new GoogleGenAI({ apiKey: env.GEMINI_TTS_API_KEY });
const response = await client.models.generateContent({
model: "gemini-2.5-flash-preview-tts",
contents: [{ parts: [{ text: message }] }],
config: {
responseModalities: ["AUDIO"],
speechConfig: {
voiceConfig: {
prebuiltVoiceConfig: {
voiceName: voices[Math.floor(Math.random() * voices.length)],
},
},
},
},
});
const audioData =
response.candidates?.[0].content?.parts?.[0].inlineData?.data;
if (!audioData)
return Response.json({ error: "Response is empty" }, { status: 500 });
queueMicrotask(() => {
redis!.setex(`tts:${textHash}`, 604800, audioData);
});
return new Response(pcmToWav(audioData), {
headers: { "Content-Type": "audio/wav" },
});
}
function pcmToWav(audioData: string) {
const audioBuffer = Buffer.from(audioData, "base64");
const pcmStream = Readable.from([audioBuffer]);
const writer = new wav.Writer({
channels: 1,
sampleRate: 24000,
bitDepth: 16,
});
const out = new PassThrough();
pcmStream.pipe(writer);
writer.on("data", (chunk) => out.write(chunk));
writer.on("end", () => out.end());
writer.on("error", () => out.end());
return Readable.toWeb(out) as unknown as globalThis.ReadableStream;
}
+154
View File
@@ -0,0 +1,154 @@
"use client";
import Image from "next/image";
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useRef, useState } from "react";
import { VersionCheck } from "@/app/client";
import { cn } from "@/lib/utils";
import { sse } from "@/lib/db/sse-endpoints";
type DonateData = {
name: string;
image: string;
amount: number;
message: string;
};
export default function () {
const [current, setCurrent] = useState<DonateData>();
const [mounted, setMounted] = useState(false);
const sfx = useRef<HTMLAudioElement | null>(null);
async function ping(data: DonateData) {
setCurrent(data);
const { name, amount, message } = data;
console.log("Requesting TTS");
const tts = new Audio(
`/api/tts?message=${encodeURIComponent(`${name} โดเนทมา ${amount} บาท\n${message}`)}`,
);
tts.load();
console.log("Waiting for response");
const ttsAvailable = await new Promise(
(r) => (
(tts.onloadeddata = () => r(true)),
(tts.onerror = () => r(false))
),
);
console.log("Transitioning in");
setMounted(true);
console.log("Running SFX");
if (sfx.current) {
const player = sfx.current;
player.play();
console.log("Waiting for SFX to end");
await new Promise((r) => (player.onended = r));
}
console.log("Playing TTS");
tts.play();
if (ttsAvailable) await new Promise((r) => (tts.onended = r));
console.log("Done");
setTimeout(() => setMounted(false), 3000);
}
const { name, amount, message, image } = current ?? {};
useEffect(() => {
const pendingHeartbeat: Record<number, (v?: unknown) => void> = {};
const { clean } = sse.donate.subMany({
heartbeat: (tag) => pendingHeartbeat[tag]?.(),
ping,
});
setInterval(async () => {
const tag = Math.floor(Math.random() * 1000);
const promise = new Promise(
(r, j) => ((pendingHeartbeat[tag] = r), setTimeout(j, 30000)),
);
await fetch(`/api/donate/hb?tag=${tag}`, {
method: "PATCH",
}).catch(() => {});
try {
await promise;
console.log("Heartbeat OK");
} catch {
console.error("Heartbeat timed out");
}
}, 10000);
return clean;
// ping({
// image: TestImage as unknown as string,
// name: "Gayshin",
// amount: 1,
// message: `สวัสดีครับ วันนี้เรากำลังทดสอบระบบ Text to Speech แบบ mixed language โดยประโยคนี้จะสลับระหว่างภาษาไทยและ English หลายครั้ง เพื่อดูว่า model สามารถ handle pronunciation, pacing, และ sentence transition ได้ดีแค่ไหน ตัวอย่างเช่น "The quick brown fox jumps over the lazy dog" แล้วกลับมาเป็นภาษาไทยอีกครั้ง จากนั้นปิดท้ายด้วยคำว่า thank you for listening และขอให้มีวันที่ดีครับ`,
// // message: "ฮีตดฮีตดฮีตดฮีตดฮีตดฮีตด",
// });
}, []);
return (
<>
<AnimatePresence>
{mounted && (
<div className="flex items-center justify-center h-full">
<div className="relative w-162.5 overflow-hidden">
<motion.div
className="absolute left-1/2 top-1/2 z-10 -translate-1/2"
initial={{ top: "200%", rotateZ: "600deg" }}
animate={{ top: "50%", rotateZ: "0deg" }}
exit={{ opacity: 0, transition: { delay: 0, duration: 2 } }}
transition={{ duration: 1, ease: "circOut" }}
>
<motion.div
className="min-w-32"
initial={{ width: "1px", padding: 0 }}
animate={{ width: "650px", padding: "20px" }}
transition={{ delay: 1, duration: 1, ease: "circOut" }}
>
<Image
src={image ?? ""}
alt="User submitted image"
className="aspect-square size-32 shrink-0 rounded-2xl bg-black/50"
/>
</motion.div>
</motion.div>
<motion.div
className="rounded-4xl bg-black/50 p-5 flex gap-3 max-w-162.5 h-39 overflow-hidden mx-auto"
initial={{ width: 0, padding: 0 }}
animate={{ width: "100%", padding: "20px" }}
exit={{ opacity: 0, transition: { delay: 0, duration: 2 } }}
transition={{ delay: 1, duration: 1, ease: "circOut" }}
>
<div className="aspect-square size-32 shrink-0 rounded-2xl" />
<div className="flex flex-col font-semibold text-3xl min-w-0">
<motion.span
className="text-[#CB5959] whitespace-nowrap"
initial={{ paddingTop: "20px" }}
animate={{ paddingTop: 0 }}
transition={{ delay: 1, duration: 1, ease: "circOut" }}
>
{name} :{" "}
<span className="text-[#FFCC00]">โดเนทมา {amount}฿</span>
</motion.span>
<motion.span
className={cn(
"whitespace-break-spaces wrap-break-word h-20 w-full",
(message ?? "").length > 80
? "text-xl line-clamp-3"
: "text-3xl line-clamp-2",
)}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.5, duration: 1 }}
>
{message}
</motion.span>
</div>
</motion.div>
</div>
</div>
)}
</AnimatePresence>
<audio src="/assets/donate-sfx.wav" ref={sfx} preload="auto" />
<VersionCheck headless />
</>
);
}
+108 -3
View File
@@ -8,10 +8,12 @@
"@bprogress/next": "^3.2.12", "@bprogress/next": "^3.2.12",
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@google/genai": "^2.8.0",
"@headless-tree/core": "^1.6.0", "@headless-tree/core": "^1.6.0",
"@headless-tree/react": "^1.6.0", "@headless-tree/react": "^1.6.0",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@next/bundle-analyzer": "16.2.6", "@next/bundle-analyzer": "16.2.6",
"@origin-space/image-cropper": "^0.1.9",
"@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.8", "@radix-ui/react-aspect-ratio": "^1.1.8",
@@ -53,6 +55,7 @@
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"motion": "^12.40.0",
"next": "16.2.6", "next": "16.2.6",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"pg": "^8.16.3", "pg": "^8.16.3",
@@ -72,6 +75,7 @@
"usehooks-ts": "^3.1.1", "usehooks-ts": "^3.1.1",
"uuidv7": "^1.1.0", "uuidv7": "^1.1.0",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"wav": "^1.0.2",
"yaml": "^2.8.2", "yaml": "^2.8.2",
"zod": "^4.1.13", "zod": "^4.1.13",
}, },
@@ -83,6 +87,7 @@
"@types/node": "^20.19.26", "@types/node": "^20.19.26",
"@types/react": "19.2.14", "@types/react": "19.2.14",
"@types/react-dom": "19.2.3", "@types/react-dom": "19.2.3",
"@types/wav": "^1.0.4",
"drizzle-kit": "^0.31.8", "drizzle-kit": "^0.31.8",
"oxfmt": "^0.52.0", "oxfmt": "^0.52.0",
"oxlint": "^1.67.0", "oxlint": "^1.67.0",
@@ -302,6 +307,8 @@
"@floating-ui/utils": ["@floating-ui/[email protected]", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], "@floating-ui/utils": ["@floating-ui/[email protected]", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
"@google/genai": ["@google/[email protected]", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-pc2ayxqO5+O7AvnHBqpNHIk7PAZkHZgL31tbyx0gJZBSS9qPYiQoqwK7oYOw/ePmG6QY4EMSu+304vD5QlhXAw=="],
"@headless-tree/core": ["@headless-tree/[email protected]", "", {}, "sha512-en0EOaZfiCRF2B8DEnhGhSaUf3hVr9Bauye8G8aswPbHOKSyhJiN4bsczz1GvqF4Xb7Ga3LP0vLA6Zih7YLoyw=="], "@headless-tree/core": ["@headless-tree/[email protected]", "", {}, "sha512-en0EOaZfiCRF2B8DEnhGhSaUf3hVr9Bauye8G8aswPbHOKSyhJiN4bsczz1GvqF4Xb7Ga3LP0vLA6Zih7YLoyw=="],
"@headless-tree/react": ["@headless-tree/[email protected]", "", { "peerDependencies": { "@headless-tree/core": "*", "react": "*", "react-dom": "*" } }, "sha512-aiRwG6e2EPBSec9uLLy9GlTvAuCtSTouU30Nwcr5ZTsYjG/i7B/ouC8f8Zu4unzo/v1h5ztbemp+EH2TPTKh+g=="], "@headless-tree/react": ["@headless-tree/[email protected]", "", { "peerDependencies": { "@headless-tree/core": "*", "react": "*", "react-dom": "*" } }, "sha512-aiRwG6e2EPBSec9uLLy9GlTvAuCtSTouU30Nwcr5ZTsYjG/i7B/ouC8f8Zu4unzo/v1h5ztbemp+EH2TPTKh+g=="],
@@ -416,6 +423,8 @@
"@opentelemetry/semantic-conventions": ["@opentelemetry/[email protected]", "", {}, "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg=="], "@opentelemetry/semantic-conventions": ["@opentelemetry/[email protected]", "", {}, "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg=="],
"@origin-space/image-cropper": ["@origin-space/[email protected]", "", { "peerDependencies": { "react": "^19.1.0", "react-dom": "^19.1.0" } }, "sha512-Ebl57I6beh7uKjSv8+r3VObEoOXIXhx4SicDUdxa9U7NVY18SzEGfqPDuhNyXuFIB5G/S8nYUToNlIRp9wwB9Q=="],
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-17EMSJnQ9g+upVHrAUYDMfH5lvRKQ9Nvg8WtEoH72oDr1VpWz+7/o3tD97U1EToen2YAQ/68JmtDYkQUi20dfQ=="], "@oxfmt/binding-android-arm-eabi": ["@oxfmt/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-17EMSJnQ9g+upVHrAUYDMfH5lvRKQ9Nvg8WtEoH72oDr1VpWz+7/o3tD97U1EToen2YAQ/68JmtDYkQUi20dfQ=="],
"@oxfmt/binding-android-arm64": ["@oxfmt/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-A2G1IdwGEW2lLJkIxcvuirRH1CzSl/e0NX11zTlW1gvxJThfwbI/BEoaKrTNpm7M2FchvIf6guvIQU7d5iz+OQ=="], "@oxfmt/binding-android-arm64": ["@oxfmt/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-A2G1IdwGEW2lLJkIxcvuirRH1CzSl/e0NX11zTlW1gvxJThfwbI/BEoaKrTNpm7M2FchvIf6guvIQU7d5iz+OQ=="],
@@ -728,20 +737,28 @@
"@types/react-dom": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], "@types/react-dom": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/retry": ["@types/[email protected]", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
"@types/trusted-types": ["@types/[email protected]", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@types/trusted-types": ["@types/[email protected]", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"@types/wav": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-s8C6tD2lPnkNkX2rWl0FMSpOXrKb3zB9eRO36p2bViSVw9lVJLSJRK7sOe7lOQ13YpM22/K91o/stMempoWPFw=="],
"@xobotyi/scrollbar-width": ["@xobotyi/[email protected]", "", {}, "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ=="], "@xobotyi/scrollbar-width": ["@xobotyi/[email protected]", "", {}, "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ=="],
"acorn": ["[email protected]", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "acorn": ["[email protected]", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn-walk": ["[email protected]", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], "acorn-walk": ["[email protected]", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="],
"agent-base": ["[email protected]", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"aria-hidden": ["[email protected]", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], "aria-hidden": ["[email protected]", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
"auto-text-size": ["[email protected]", "", { "peerDependencies": { "react": "*" } }, "sha512-OycpKD8n5jxnNaOiAJoNr3BkBsxRzHHhFQk15rgsOYq+O9lsMSPLbVN7ZibxjTwT7bl6Y5jawOtZJxRWaoxdcQ=="], "auto-text-size": ["[email protected]", "", { "peerDependencies": { "react": "*" } }, "sha512-OycpKD8n5jxnNaOiAJoNr3BkBsxRzHHhFQk15rgsOYq+O9lsMSPLbVN7ZibxjTwT7bl6Y5jawOtZJxRWaoxdcQ=="],
"babel-plugin-react-compiler": ["[email protected]", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], "babel-plugin-react-compiler": ["[email protected]", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
"base64-js": ["[email protected]", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"baseline-browser-mapping": ["[email protected]", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA=="], "baseline-browser-mapping": ["[email protected]", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA=="],
"better-auth": ["[email protected]", "", { "dependencies": { "@better-auth/core": "1.4.17", "@better-auth/telemetry": "1.4.17", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-VmHGQyKsEahkEs37qguROKg/6ypYpNF13D7v/lkbO7w7Aivz0Bv2h+VyUkH4NzrGY0QBKXi1577mGhDCVwp0ew=="], "better-auth": ["[email protected]", "", { "dependencies": { "@better-auth/core": "1.4.17", "@better-auth/telemetry": "1.4.17", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-VmHGQyKsEahkEs37qguROKg/6ypYpNF13D7v/lkbO7w7Aivz0Bv2h+VyUkH4NzrGY0QBKXi1577mGhDCVwp0ew=="],
@@ -750,8 +767,18 @@
"better-sqlite3": ["[email protected]", "", {}, "sha512-b8h94RVpETWkVV59x62NsY++79bM7Si6Dxq7a4iVxRcJU3ZJJ4vaiC7wUZwM8WDK0ySRL+i+T/1SMAzbJLejYA=="], "better-sqlite3": ["[email protected]", "", {}, "sha512-b8h94RVpETWkVV59x62NsY++79bM7Si6Dxq7a4iVxRcJU3ZJJ4vaiC7wUZwM8WDK0ySRL+i+T/1SMAzbJLejYA=="],
"bignumber.js": ["[email protected]", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
"browserslist": ["[email protected]", "", { "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", "electron-to-chromium": "^1.5.218", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A=="], "browserslist": ["[email protected]", "", { "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", "electron-to-chromium": "^1.5.218", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A=="],
"buffer-alloc": ["[email protected]", "", { "dependencies": { "buffer-alloc-unsafe": "^1.1.0", "buffer-fill": "^1.0.0" } }, "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow=="],
"buffer-alloc-unsafe": ["[email protected]", "", {}, "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="],
"buffer-equal-constant-time": ["[email protected]", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
"buffer-fill": ["[email protected]", "", {}, "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ=="],
"buffer-from": ["[email protected]", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "buffer-from": ["[email protected]", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], "bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
@@ -790,6 +817,8 @@
"core-js": ["[email protected]", "", {}, "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg=="], "core-js": ["[email protected]", "", {}, "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg=="],
"core-util-is": ["[email protected]", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"css-in-js-utils": ["[email protected]", "", { "dependencies": { "hyphenate-style-name": "^1.0.3" } }, "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A=="], "css-in-js-utils": ["[email protected]", "", { "dependencies": { "hyphenate-style-name": "^1.0.3" } }, "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A=="],
"css-tree": ["[email protected]", "", { "dependencies": { "mdn-data": "2.0.14", "source-map": "^0.6.1" } }, "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q=="], "css-tree": ["[email protected]", "", { "dependencies": { "mdn-data": "2.0.14", "source-map": "^0.6.1" } }, "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q=="],
@@ -818,13 +847,15 @@
"d3-timer": ["[email protected]", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], "d3-timer": ["[email protected]", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
"data-uri-to-buffer": ["[email protected]", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
"date-fns": ["[email protected]", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], "date-fns": ["[email protected]", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
"date-fns-jalali": ["[email protected]", "", {}, "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="], "date-fns-jalali": ["[email protected]", "", {}, "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="],
"debounce": ["[email protected]", "", {}, "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="], "debounce": ["[email protected]", "", {}, "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"decimal.js-light": ["[email protected]", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], "decimal.js-light": ["[email protected]", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
@@ -854,6 +885,8 @@
"duplexer": ["[email protected]", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="], "duplexer": ["[email protected]", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="],
"ecdsa-sig-formatter": ["[email protected]", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
"electron-to-chromium": ["[email protected]", "", {}, "sha512-kWAoUu/bwzvnhpdZSIc6KUyvkI1rbRXMT0Eq8pKReyOyaPZcctMli+EgvcN1PAvwVc7Tdo4Fxi2PsLNDU05mdg=="], "electron-to-chromium": ["[email protected]", "", {}, "sha512-kWAoUu/bwzvnhpdZSIc6KUyvkI1rbRXMT0Eq8pKReyOyaPZcctMli+EgvcN1PAvwVc7Tdo4Fxi2PsLNDU05mdg=="],
"embla-carousel": ["[email protected]", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="], "embla-carousel": ["[email protected]", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="],
@@ -878,6 +911,8 @@
"exsolve": ["[email protected]", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="], "exsolve": ["[email protected]", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="],
"extend": ["[email protected]", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"fast-deep-equal": ["[email protected]", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-deep-equal": ["[email protected]", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-equals": ["[email protected]", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="], "fast-equals": ["[email protected]", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="],
@@ -886,10 +921,20 @@
"fastest-stable-stringify": ["[email protected]", "", {}, "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q=="], "fastest-stable-stringify": ["[email protected]", "", {}, "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q=="],
"fetch-blob": ["[email protected]", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
"fflate": ["[email protected]", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="], "fflate": ["[email protected]", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="],
"formdata-polyfill": ["[email protected]", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
"framer-motion": ["[email protected]", "", { "dependencies": { "motion-dom": "^12.40.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg=="],
"fsevents": ["[email protected]", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], "fsevents": ["[email protected]", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
"gaxios": ["[email protected]", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="],
"gcp-metadata": ["[email protected]", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
"gensync": ["[email protected]", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], "gensync": ["[email protected]", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-nonce": ["[email protected]", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], "get-nonce": ["[email protected]", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
@@ -898,14 +943,22 @@
"giget": ["[email protected]", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], "giget": ["[email protected]", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
"google-auth-library": ["[email protected]", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw=="],
"google-logging-utils": ["[email protected]", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
"graceful-fs": ["[email protected]", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "graceful-fs": ["[email protected]", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"gzip-size": ["[email protected]", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q=="], "gzip-size": ["[email protected]", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q=="],
"html-escaper": ["[email protected]", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], "html-escaper": ["[email protected]", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="],
"https-proxy-agent": ["[email protected]", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"hyphenate-style-name": ["[email protected]", "", {}, "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw=="], "hyphenate-style-name": ["[email protected]", "", {}, "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw=="],
"inherits": ["[email protected]", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"inline-style-prefixer": ["[email protected]", "", { "dependencies": { "css-in-js-utils": "^3.1.0" } }, "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw=="], "inline-style-prefixer": ["[email protected]", "", { "dependencies": { "css-in-js-utils": "^3.1.0" } }, "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw=="],
"input-otp": ["[email protected]", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="], "input-otp": ["[email protected]", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="],
@@ -920,6 +973,8 @@
"is-wsl": ["[email protected]", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], "is-wsl": ["[email protected]", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
"isarray": ["[email protected]", "", {}, "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="],
"jiti": ["[email protected]", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ=="], "jiti": ["[email protected]", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ=="],
"jose": ["[email protected]", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="], "jose": ["[email protected]", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="],
@@ -930,8 +985,14 @@
"jsesc": ["[email protected]", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "jsesc": ["[email protected]", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-bigint": ["[email protected]", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
"json5": ["[email protected]", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "json5": ["[email protected]", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"jwa": ["[email protected]", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
"jws": ["[email protected]", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
"kleur": ["[email protected]", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], "kleur": ["[email protected]", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
"kysely": ["[email protected]", "", {}, "sha512-u/cAuTL4DRIiO2/g4vNGRgklEKNIj5Q3CG7RoUB5DV5SfEC2hMvPxKi0GWPmnzwL2ryIeud2VTcEEmqzTzEPNw=="], "kysely": ["[email protected]", "", {}, "sha512-u/cAuTL4DRIiO2/g4vNGRgklEKNIj5Q3CG7RoUB5DV5SfEC2hMvPxKi0GWPmnzwL2ryIeud2VTcEEmqzTzEPNw=="],
@@ -978,9 +1039,15 @@
"mdn-data": ["[email protected]", "", {}, "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="], "mdn-data": ["[email protected]", "", {}, "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="],
"motion": ["[email protected]", "", { "dependencies": { "framer-motion": "^12.40.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-yjrHUrBFW6kQvjJwRsoiPSAhC5tRwRqNGJWmiJ4CrGnbKp0V88AdzkhBmDoqIsIPfarOe0Uddd37Xq43/gIocA=="],
"motion-dom": ["[email protected]", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg=="],
"motion-utils": ["[email protected]", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
"mrmime": ["[email protected]", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], "mrmime": ["[email protected]", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"nano-css": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "css-tree": "^1.1.2", "csstype": "^3.1.2", "fastest-stable-stringify": "^2.0.2", "inline-style-prefixer": "^7.0.1", "rtl-css-js": "^1.16.1", "stacktrace-js": "^2.0.2", "stylis": "^4.3.0" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw=="], "nano-css": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "css-tree": "^1.1.2", "csstype": "^3.1.2", "fastest-stable-stringify": "^2.0.2", "inline-style-prefixer": "^7.0.1", "rtl-css-js": "^1.16.1", "stacktrace-js": "^2.0.2", "stylis": "^4.3.0" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw=="],
@@ -992,6 +1059,10 @@
"next-themes": ["[email protected]", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], "next-themes": ["[email protected]", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
"node-domexception": ["[email protected]", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
"node-fetch": ["[email protected]", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"node-fetch-native": ["[email protected]", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], "node-fetch-native": ["[email protected]", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
"node-releases": ["[email protected]", "", {}, "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw=="], "node-releases": ["[email protected]", "", {}, "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw=="],
@@ -1010,6 +1081,8 @@
"oxlint": ["[email protected]", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.67.0", "@oxlint/binding-android-arm64": "1.67.0", "@oxlint/binding-darwin-arm64": "1.67.0", "@oxlint/binding-darwin-x64": "1.67.0", "@oxlint/binding-freebsd-x64": "1.67.0", "@oxlint/binding-linux-arm-gnueabihf": "1.67.0", "@oxlint/binding-linux-arm-musleabihf": "1.67.0", "@oxlint/binding-linux-arm64-gnu": "1.67.0", "@oxlint/binding-linux-arm64-musl": "1.67.0", "@oxlint/binding-linux-ppc64-gnu": "1.67.0", "@oxlint/binding-linux-riscv64-gnu": "1.67.0", "@oxlint/binding-linux-riscv64-musl": "1.67.0", "@oxlint/binding-linux-s390x-gnu": "1.67.0", "@oxlint/binding-linux-x64-gnu": "1.67.0", "@oxlint/binding-linux-x64-musl": "1.67.0", "@oxlint/binding-openharmony-arm64": "1.67.0", "@oxlint/binding-win32-arm64-msvc": "1.67.0", "@oxlint/binding-win32-ia32-msvc": "1.67.0", "@oxlint/binding-win32-x64-msvc": "1.67.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.22.1", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-blwwaHPdoH8piQ5/z0KHeoHFR7FZgl12WluKJfu4qFLPkZl6mK04PkLE45Fw1NxfBRSlh40Gu7MkxHUw++ociQ=="], "oxlint": ["[email protected]", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.67.0", "@oxlint/binding-android-arm64": "1.67.0", "@oxlint/binding-darwin-arm64": "1.67.0", "@oxlint/binding-darwin-x64": "1.67.0", "@oxlint/binding-freebsd-x64": "1.67.0", "@oxlint/binding-linux-arm-gnueabihf": "1.67.0", "@oxlint/binding-linux-arm-musleabihf": "1.67.0", "@oxlint/binding-linux-arm64-gnu": "1.67.0", "@oxlint/binding-linux-arm64-musl": "1.67.0", "@oxlint/binding-linux-ppc64-gnu": "1.67.0", "@oxlint/binding-linux-riscv64-gnu": "1.67.0", "@oxlint/binding-linux-riscv64-musl": "1.67.0", "@oxlint/binding-linux-s390x-gnu": "1.67.0", "@oxlint/binding-linux-x64-gnu": "1.67.0", "@oxlint/binding-linux-x64-musl": "1.67.0", "@oxlint/binding-openharmony-arm64": "1.67.0", "@oxlint/binding-win32-arm64-msvc": "1.67.0", "@oxlint/binding-win32-ia32-msvc": "1.67.0", "@oxlint/binding-win32-x64-msvc": "1.67.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.22.1", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-blwwaHPdoH8piQ5/z0KHeoHFR7FZgl12WluKJfu4qFLPkZl6mK04PkLE45Fw1NxfBRSlh40Gu7MkxHUw++ociQ=="],
"p-retry": ["[email protected]", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
"pathe": ["[email protected]", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "pathe": ["[email protected]", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"perfect-debounce": ["[email protected]", "", {}, "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow=="], "perfect-debounce": ["[email protected]", "", {}, "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow=="],
@@ -1096,6 +1169,8 @@
"react-use": ["[email protected]", "", { "dependencies": { "@types/js-cookie": "^2.2.6", "@xobotyi/scrollbar-width": "^1.9.5", "copy-to-clipboard": "^3.3.1", "fast-deep-equal": "^3.1.3", "fast-shallow-equal": "^1.0.0", "js-cookie": "^2.2.1", "nano-css": "^5.6.2", "react-universal-interface": "^0.6.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.1.0", "set-harmonic-interval": "^1.0.1", "throttle-debounce": "^3.0.1", "ts-easing": "^0.2.0", "tslib": "^2.1.0" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g=="], "react-use": ["[email protected]", "", { "dependencies": { "@types/js-cookie": "^2.2.6", "@xobotyi/scrollbar-width": "^1.9.5", "copy-to-clipboard": "^3.3.1", "fast-deep-equal": "^3.1.3", "fast-shallow-equal": "^1.0.0", "js-cookie": "^2.2.1", "nano-css": "^5.6.2", "react-universal-interface": "^0.6.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.1.0", "set-harmonic-interval": "^1.0.1", "throttle-debounce": "^3.0.1", "ts-easing": "^0.2.0", "tslib": "^2.1.0" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g=="],
"readable-stream": ["[email protected]", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ=="],
"readdirp": ["[email protected]", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "readdirp": ["[email protected]", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"recharts": ["[email protected]", "", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw=="], "recharts": ["[email protected]", "", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw=="],
@@ -1110,12 +1185,16 @@
"resolve-pkg-maps": ["[email protected]", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], "resolve-pkg-maps": ["[email protected]", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"retry": ["[email protected]", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
"rou3": ["[email protected]", "", {}, "sha512-ELguG3ENDw5NKNmWHO3OGEjcgdxkCNvnMR22gKHEgRXuwiriap5RIYdummOaOiqUNcC5yU5txGCHWNm7KlHuAA=="], "rou3": ["[email protected]", "", {}, "sha512-ELguG3ENDw5NKNmWHO3OGEjcgdxkCNvnMR22gKHEgRXuwiriap5RIYdummOaOiqUNcC5yU5txGCHWNm7KlHuAA=="],
"rtl-css-js": ["[email protected]", "", { "dependencies": { "@babel/runtime": "^7.1.2" } }, "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg=="], "rtl-css-js": ["[email protected]", "", { "dependencies": { "@babel/runtime": "^7.1.2" } }, "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg=="],
"run-applescript": ["[email protected]", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], "run-applescript": ["[email protected]", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
"safe-buffer": ["[email protected]", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"scheduler": ["[email protected]", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "scheduler": ["[email protected]", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"screenfull": ["[email protected]", "", {}, "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA=="], "screenfull": ["[email protected]", "", {}, "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA=="],
@@ -1150,6 +1229,10 @@
"stacktrace-js": ["[email protected]", "", { "dependencies": { "error-stack-parser": "^2.0.6", "stack-generator": "^2.0.5", "stacktrace-gps": "^3.0.4" } }, "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg=="], "stacktrace-js": ["[email protected]", "", { "dependencies": { "error-stack-parser": "^2.0.6", "stack-generator": "^2.0.5", "stacktrace-gps": "^3.0.4" } }, "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg=="],
"stream-parser": ["[email protected]", "", { "dependencies": { "debug": "2" } }, "sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ=="],
"string_decoder": ["[email protected]", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="],
"styled-jsx": ["[email protected]", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], "styled-jsx": ["[email protected]", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
"stylis": ["[email protected]", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="], "stylis": ["[email protected]", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="],
@@ -1198,11 +1281,15 @@
"victory-vendor": ["[email protected]", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="], "victory-vendor": ["[email protected]", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="],
"wav": ["[email protected]", "", { "dependencies": { "buffer-alloc": "^1.1.0", "buffer-from": "^1.0.0", "debug": "^2.2.0", "readable-stream": "^1.1.14", "stream-parser": "^0.3.1" } }, "sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg=="],
"web-streams-polyfill": ["[email protected]", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"web-vitals": ["[email protected]", "", {}, "sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA=="], "web-vitals": ["[email protected]", "", {}, "sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA=="],
"webpack-bundle-analyzer": ["[email protected]", "", { "dependencies": { "@discoveryjs/json-ext": "0.5.7", "acorn": "^8.0.4", "acorn-walk": "^8.0.0", "commander": "^7.2.0", "debounce": "^1.2.1", "escape-string-regexp": "^4.0.0", "gzip-size": "^6.0.0", "html-escaper": "^2.0.2", "is-plain-object": "^5.0.0", "opener": "^1.5.2", "picocolors": "^1.0.0", "sirv": "^2.0.3", "ws": "^7.3.1" }, "bin": { "webpack-bundle-analyzer": "lib/bin/analyzer.js" } }, "sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ=="], "webpack-bundle-analyzer": ["[email protected]", "", { "dependencies": { "@discoveryjs/json-ext": "0.5.7", "acorn": "^8.0.4", "acorn-walk": "^8.0.0", "commander": "^7.2.0", "debounce": "^1.2.1", "escape-string-regexp": "^4.0.0", "gzip-size": "^6.0.0", "html-escaper": "^2.0.2", "is-plain-object": "^5.0.0", "opener": "^1.5.2", "picocolors": "^1.0.0", "sirv": "^2.0.3", "ws": "^7.3.1" }, "bin": { "webpack-bundle-analyzer": "lib/bin/analyzer.js" } }, "sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ=="],
"ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
"wsl-utils": ["[email protected]", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], "wsl-utils": ["[email protected]", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
@@ -1218,12 +1305,16 @@
"zod": ["[email protected]", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "zod": ["[email protected]", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"@babel/core/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"@babel/core/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/core/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/helper-compilation-targets/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-compilation-targets/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/helper-create-class-features-plugin/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-create-class-features-plugin/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/traverse/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"@better-auth/cli/drizzle-orm": ["[email protected]", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q=="], "@better-auth/cli/drizzle-orm": ["[email protected]", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q=="],
"@esbuild-kit/core-utils/esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@esbuild-kit/core-utils/esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
@@ -1294,6 +1385,10 @@
"browserslist/caniuse-lite": ["[email protected]", "", {}, "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ=="], "browserslist/caniuse-lite": ["[email protected]", "", {}, "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ=="],
"esbuild-register/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"https-proxy-agent/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"lightningcss/detect-libc": ["[email protected]", "", {}, "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw=="], "lightningcss/detect-libc": ["[email protected]", "", {}, "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw=="],
"nano-css/csstype": ["[email protected]", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "nano-css/csstype": ["[email protected]", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
@@ -1322,6 +1417,12 @@
"webpack-bundle-analyzer/commander": ["[email protected]", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], "webpack-bundle-analyzer/commander": ["[email protected]", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
"webpack-bundle-analyzer/ws": ["[email protected]", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
"@babel/core/debug/ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"@babel/traverse/debug/ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
@@ -1365,5 +1466,9 @@
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
"esbuild-register/debug/ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"https-proxy-agent/debug/ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
} }
} }
+317
View File
@@ -0,0 +1,317 @@
// oxlint-disable next/no-img-element
"use client";
import {
ArrowLeftIcon,
PlusIcon,
XIcon,
ZoomInIcon,
ZoomOutIcon,
} from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useFileUpload } from "@/hooks/use-file-upload";
import { Button } from "@/components/ui/button";
import {
Cropper,
CropperCropArea,
CropperDescription,
CropperImage,
} from "@/components/ui/cropper";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Slider } from "@/components/ui/slider";
// Define type for pixel crop area
type Area = { x: number; y: number; width: number; height: number };
// Helper function to create a cropped image blob
const createImage = (url: string): Promise<HTMLImageElement> =>
new Promise((resolve, reject) => {
const image = new Image();
image.addEventListener("load", () => resolve(image));
image.addEventListener("error", (error) => reject(error));
image.setAttribute("crossOrigin", "anonymous"); // Needed for canvas Tainted check
image.src = url;
});
async function getCroppedImg(
imageSrc: string,
pixelCrop: Area,
outputWidth: number = pixelCrop.width, // Optional: specify output size
outputHeight: number = pixelCrop.height,
): Promise<Blob | null> {
try {
const image = await createImage(imageSrc);
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) {
return null;
}
// Set canvas size to desired output size
canvas.width = outputWidth;
canvas.height = outputHeight;
// Draw the cropped image onto the canvas
ctx.drawImage(
image,
pixelCrop.x,
pixelCrop.y,
pixelCrop.width,
pixelCrop.height,
0,
0,
outputWidth, // Draw onto the output size
outputHeight,
);
// Convert canvas to blob
return new Promise((resolve) => {
canvas.toBlob((blob) => {
resolve(blob);
}, "image/jpeg"); // Specify format and quality if needed
});
} catch (error) {
console.error("Error in getCroppedImg:", error);
return null;
}
}
export default function Component() {
const [
{ files, isDragging },
{
handleDragEnter,
handleDragLeave,
handleDragOver,
handleDrop,
openFileDialog,
removeFile,
getInputProps,
},
] = useFileUpload({
accept: "image/*",
});
const previewUrl = files[0]?.preview || null;
const fileId = files[0]?.id;
const [finalImageUrl, setFinalImageUrl] = useState<string | null>(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);
// Ref to track the previous file ID to detect new uploads
const previousFileIdRef = useRef<string | undefined | null>(null);
// State to store the desired crop area in pixels
const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null);
// State for zoom level
const [zoom, setZoom] = useState(1);
// Callback for Cropper to provide crop data - Wrap with useCallback
const handleCropChange = useCallback((pixels: Area | null) => {
setCroppedAreaPixels(pixels);
}, []);
const handleApply = async () => {
// Check if we have the necessary data
if (!previewUrl || !fileId || !croppedAreaPixels) {
console.error("Missing data for apply:", {
croppedAreaPixels,
fileId,
previewUrl,
});
// Remove file if apply is clicked without crop data?
if (fileId) {
removeFile(fileId);
setCroppedAreaPixels(null);
}
return;
}
try {
// 1. Get the cropped image blob using the helper
const croppedBlob = await getCroppedImg(previewUrl, croppedAreaPixels);
if (!croppedBlob) {
throw new Error("Failed to generate cropped image blob.");
}
// 2. Create a NEW object URL from the cropped blob
const newFinalUrl = URL.createObjectURL(croppedBlob);
// 3. Revoke the OLD finalImageUrl if it exists
if (finalImageUrl) {
URL.revokeObjectURL(finalImageUrl);
}
// 4. Set the final avatar state to the NEW URL
setFinalImageUrl(newFinalUrl);
// 5. Close the dialog (don't remove the file yet)
setIsDialogOpen(false);
} catch (error) {
console.error("Error during apply:", error);
// Close the dialog even if cropping fails
setIsDialogOpen(false);
}
};
const handleRemoveFinalImage = () => {
if (finalImageUrl) {
URL.revokeObjectURL(finalImageUrl);
}
setFinalImageUrl(null);
};
useEffect(() => {
const currentFinalUrl = finalImageUrl;
// Cleanup function
return () => {
if (currentFinalUrl?.startsWith("blob:")) {
URL.revokeObjectURL(currentFinalUrl);
}
};
}, [finalImageUrl]);
// Effect to open dialog when a *new* file is ready
useEffect(() => {
// Check if fileId exists and is different from the previous one
if (fileId && fileId !== previousFileIdRef.current) {
setIsDialogOpen(true); // Open dialog for the new file
setCroppedAreaPixels(null); // Reset crop area for the new file
setZoom(1); // Reset zoom for the new file
}
// Update the ref to the current fileId for the next render
previousFileIdRef.current = fileId;
}, [fileId]); // Depend only on fileId
return (
<>
<div className="relative inline-flex">
{/* Drop area - uses finalImageUrl */}
<button
aria-label={finalImageUrl ? "Change image" : "Upload image"}
className="relative flex size-32 items-center justify-center overflow-hidden rounded-lg border border-input border-dashed outline-none transition-colors hover:bg-accent/50 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-disabled:pointer-events-none has-[img]:border-none has-disabled:opacity-50 data-[dragging=true]:bg-accent/50"
data-dragging={isDragging || undefined}
onClick={openFileDialog}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
type="button"
>
{finalImageUrl ? (
<img
alt="User avatar"
className="size-full object-cover"
height={128}
src={finalImageUrl}
style={{ objectFit: "cover" }}
width={128}
/>
) : (
<div aria-hidden="true">
<PlusIcon className="size-8 opacity-60" />
</div>
)}
</button>
{/* Remove button - depends on finalImageUrl */}
{finalImageUrl && (
<Button
aria-label="Remove image"
className="-top-1 -right-1 absolute size-6 rounded-full border-2 border-background shadow-none focus-visible:border-background"
onClick={handleRemoveFinalImage}
size="icon"
>
<XIcon className="size-3.5" />
</Button>
)}
<input
{...getInputProps()}
aria-label="Upload image file"
className="sr-only"
tabIndex={-1}
/>
</div>
{/* Cropper Dialog - Use isDialogOpen for open prop */}
<Dialog onOpenChange={setIsDialogOpen} open={isDialogOpen}>
<DialogContent className="gap-0 p-0 sm:max-w-140 *:[button]:hidden">
<DialogDescription className="sr-only">
Crop image dialog
</DialogDescription>
<DialogHeader className="contents space-y-0 text-left">
<DialogTitle className="flex items-center justify-between border-b p-4 text-base">
<div className="flex items-center gap-2">
<Button
aria-label="Cancel"
className="-my-1 opacity-60"
onClick={() => setIsDialogOpen(false)}
size="icon"
type="button"
variant="ghost"
>
<ArrowLeftIcon aria-hidden="true" />
</Button>
<span>Crop image</span>
</div>
<Button
autoFocus
className="-my-1"
disabled={!previewUrl}
onClick={handleApply}
>
Apply
</Button>
</DialogTitle>
</DialogHeader>
{previewUrl && (
<Cropper
className="h-96 sm:h-120"
image={previewUrl}
onCropChange={handleCropChange}
onZoomChange={setZoom}
zoom={zoom}
>
<CropperDescription />
<CropperImage />
<CropperCropArea />
</Cropper>
)}
<DialogFooter className="border-t px-4 py-6">
<div className="mx-auto flex w-full max-w-80 items-center gap-4">
<ZoomOutIcon
aria-hidden="true"
className="shrink-0 opacity-60"
size={16}
/>
<Slider
aria-label="Zoom slider"
defaultValue={[1]}
max={3}
min={1}
onValueChange={(value) => setZoom(value[0])}
step={0.1}
value={[zoom]}
/>
<ZoomInIcon
aria-hidden="true"
className="shrink-0 opacity-60"
size={16}
/>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
+65
View File
@@ -0,0 +1,65 @@
import type { z } from "zod";
export type FormSubmitResult =
| {
toast?: string;
close?: boolean;
error?:
| string
| { what: string; where: string }
| { what: string; where: string }[];
}
| undefined;
export function defineFormHandler<TSchema extends z.ZodType>(
schema: TSchema,
action: (input: z.output<TSchema>) => Promise<FormSubmitResult>,
): (form: FormData) => Promise<FormSubmitResult>;
export function defineFormHandler<
TAction extends (form: FormData) => Promise<FormSubmitResult>,
>(action: TAction): TAction;
export function defineFormHandler(
schemaOrAction: z.ZodType | ((form: FormData) => Promise<FormSubmitResult>),
action?: (input: unknown) => Promise<FormSubmitResult>,
): (form: FormData) => Promise<FormSubmitResult> {
if (!action)
return schemaOrAction as (form: FormData) => Promise<FormSubmitResult>;
const schema = schemaOrAction as z.ZodType;
return async (form: FormData) => {
// "use server";
const raw = Object.fromEntries([...form.entries()]);
const parsed = schema.safeParse(raw);
if (!parsed.success) {
return {
error: parsed.error.issues.map((issue) => ({
what: issue.message,
where: issue.path.join("."),
})),
};
}
return action(parsed.data);
};
}
export function formParse<TSchema extends z.ZodType>(
schema: TSchema,
data: FormData,
): { $: z.output<TSchema>; error: { what: string; where: string }[] | string } {
const raw = Object.fromEntries([...data.entries()]);
const parsed = schema.safeParse(raw);
if (!parsed.success) {
if (
parsed.error.issues.length === 1 &&
parsed.error.issues[0].path.length === 0
)
return { error: parsed.error.issues[0].message, $: undefined as any };
return {
error: parsed.error.issues.map((issue) => ({
what: issue.message,
where: issue.path.join("."),
})),
$: undefined as any,
};
}
return { $: parsed.data, error: undefined as any };
}
+120 -43
View File
@@ -2,14 +2,12 @@
import * as React from "react"; import * as React from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import type {
TypedFormData,
TypedFormDataShape,
} from "@/app/(ui)/rubgram/type";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { DialogClose } from "./ui/dialog"; import { DialogClose } from "./ui/dialog";
import { Label } from "./ui/label"; import { Label } from "./ui/label";
import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
import { Divide } from "lucide-react"; import { Divide } from "lucide-react";
import type { FormSubmitResult } from "./form-submit";
const AUTOSAVE_TTL_MS = 10 * 60 * 1000; const AUTOSAVE_TTL_MS = 10 * 60 * 1000;
const STORAGE_PREFIX = "form:autosave:"; const STORAGE_PREFIX = "form:autosave:";
@@ -19,12 +17,14 @@ type FormValues = Record<string, unknown>;
type FormContextValue = { type FormContextValue = {
id: string; id: string;
values: FormValues; values: FormValues;
errors: Record<string, string>;
loading: boolean; loading: boolean;
setValue: (name: string, value: unknown) => void; setValue: (name: string, value: unknown) => void;
updateValues: (updater: (prev: FormValues) => FormValues) => void; updateValues: (updater: (prev: FormValues) => FormValues) => void;
clear: () => void; clear: () => void;
submit: () => Promise<void>; submit: () => Promise<void>;
closeDialog: () => void; closeDialog: () => void;
setErrors: React.Dispatch<React.SetStateAction<Record<string, string>>>;
}; };
const FormContext = React.createContext<FormContextValue | null>(null); const FormContext = React.createContext<FormContextValue | null>(null);
@@ -87,19 +87,6 @@ function appendFormValue(form: FormData, key: string, value: unknown) {
form.append(key, typeof value === "string" ? value : String(value)); form.append(key, typeof value === "string" ? value : String(value));
} }
function toTypedFormData<T extends TypedFormDataShape>(
formData: FormData,
): TypedFormData<T> {
const base = formData as unknown as TypedFormData<T>;
if (!("raw" in base)) {
Object.defineProperty(base, "raw", {
value: formData,
enumerable: false,
});
}
return base;
}
function getInputValue(eventOrValue: unknown) { function getInputValue(eventOrValue: unknown) {
const event = eventOrValue as React.ChangeEvent<HTMLInputElement>; const event = eventOrValue as React.ChangeEvent<HTMLInputElement>;
const target = event?.target as const target = event?.target as
@@ -118,35 +105,25 @@ function getInputValue(eventOrValue: unknown) {
return eventOrValue; return eventOrValue;
} }
type SubmitFnResult = type FormProviderProps = Omit<React.ComponentProps<"form">, "id" | "onSubmit"> & {
| {
toast?: string;
close?: boolean;
error?: string;
}
| undefined;
type FormProviderProps<T extends TypedFormDataShape> = Omit<
React.ComponentProps<"form">,
"id" | "onSubmit"
> & {
// Unique Autosave Key // Unique Autosave Key
id: string; id: string;
onSubmit?: ( onSubmit?: (form: FormData) => FormSubmitResult | Promise<FormSubmitResult>;
form: TypedFormData<T>,
) => SubmitFnResult | Promise<SubmitFnResult>;
values?: FormValues; values?: FormValues;
inDialog?: boolean;
}; };
export function FormProvider<T extends TypedFormDataShape>({ export function FormProvider({
id, id,
onSubmit, onSubmit,
className, className,
children, children,
inDialog = true,
values: defaultValues = {}, values: defaultValues = {},
...props ...props
}: FormProviderProps<T>) { }: FormProviderProps) {
const [values, setValues] = React.useState<FormValues>(defaultValues); const [values, setValues] = React.useState<FormValues>(defaultValues);
const [errors, setErrors] = React.useState<Record<string, string>>({});
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const formRef = React.useRef<HTMLFormElement | null>(null); const formRef = React.useRef<HTMLFormElement | null>(null);
const [storageLock, setStorageLock] = React.useState<"r" | "w" | null>("r"); const [storageLock, setStorageLock] = React.useState<"r" | "w" | null>("r");
@@ -180,6 +157,12 @@ export function FormProvider<T extends TypedFormDataShape>({
const setValue = React.useCallback((name: string, value: unknown) => { const setValue = React.useCallback((name: string, value: unknown) => {
setValues((prev) => ({ ...prev, [name]: value })); setValues((prev) => ({ ...prev, [name]: value }));
setErrors((prev) => {
if (!prev[name]) return prev;
const next = { ...prev };
delete next[name];
return next;
});
}, []); }, []);
const updateValues = React.useCallback( const updateValues = React.useCallback(
(updater: (prev: FormValues) => FormValues) => { (updater: (prev: FormValues) => FormValues) => {
@@ -190,6 +173,7 @@ export function FormProvider<T extends TypedFormDataShape>({
const clear = React.useCallback(() => { const clear = React.useCallback(() => {
setValues({}); setValues({});
setErrors({});
localStorage.removeItem(storageKey(id)); localStorage.removeItem(storageKey(id));
formRef.current?.reset(); formRef.current?.reset();
}, [id]); }, [id]);
@@ -204,12 +188,25 @@ export function FormProvider<T extends TypedFormDataShape>({
appendFormValue(formData, key, value); appendFormValue(formData, key, value);
} }
setLoading(true); setLoading(true);
setErrors({});
try { try {
const res = await onSubmit?.(toTypedFormData(formData)); const res = await onSubmit?.(formData);
if (res && typeof res === "object") { if (res && typeof res === "object") {
if ("error" in res) { if ("error" in res) {
const { error } = res; const { error } = res;
if (typeof error === "string") throw new Error(error); if (typeof error === "string") {
toast.error(error);
} else if (Array.isArray(error)) {
const fieldErrors: Record<string, string> = {};
for (const e of error) {
fieldErrors[e.where] = e.what;
}
setErrors(fieldErrors);
} else if (error) {
setErrors({ [error.where]: error.what });
}
} else {
setErrors({});
} }
if ("toast" in res) { if ("toast" in res) {
const { toast: toastMsg } = res; const { toast: toastMsg } = res;
@@ -239,14 +236,16 @@ export function FormProvider<T extends TypedFormDataShape>({
() => ({ () => ({
id, id,
values, values,
errors,
loading, loading,
setValue, setValue,
updateValues, updateValues,
clear, clear,
submit, submit,
closeDialog, closeDialog,
setErrors,
}), }),
[id, values, loading, setValue, updateValues, clear, submit, closeDialog], [id, values, errors, loading, setValue, updateValues, clear, submit, closeDialog, setErrors],
); );
return ( return (
@@ -263,7 +262,7 @@ export function FormProvider<T extends TypedFormDataShape>({
> >
{children} {children}
</form> </form>
<DialogClose ref={closerRef} /> {inDialog && <DialogClose ref={closerRef} />}
</FormContext.Provider> </FormContext.Provider>
); );
} }
@@ -286,7 +285,7 @@ export function FormInput({
subLabel, subLabel,
className, className,
}: FormInputProps) { }: FormInputProps) {
const { values, setValue, updateValues } = useFormContext(); const { values, setValue, updateValues, errors } = useFormContext();
const childProps = (children.props ?? {}) as Record<string, unknown>; const childProps = (children.props ?? {}) as Record<string, unknown>;
const value = values[name] ?? childProps?.defaultValue ?? ""; const value = values[name] ?? childProps?.defaultValue ?? "";
const nextProps: Record<string, unknown> = { name, defaultValue: undefined }; const nextProps: Record<string, unknown> = { name, defaultValue: undefined };
@@ -356,9 +355,17 @@ export function FormInput({
)} )}
</Label> </Label>
{React.cloneElement(children || Divide, nextProps)} {React.cloneElement(children || Divide, nextProps)}
{errors[name] && (
<span className="text-destructive text-sm">{errors[name]}</span>
)}
</div> </div>
) : ( ) : (
React.cloneElement(children || Divide, nextProps) <div className={cn(className)}>
{React.cloneElement(children || Divide, nextProps)}
{errors[name] && (
<span className="text-destructive text-sm">{errors[name]}</span>
)}
</div>
)} )}
</> </>
); );
@@ -370,7 +377,7 @@ type FormActionProps = Omit<
> & { > & {
type: "clear" | "submit" | "action"; type: "clear" | "submit" | "action";
action?: () => SubmitFnResult | Promise<SubmitFnResult>; action?: () => FormSubmitResult | Promise<FormSubmitResult>;
children?: React.ReactNode; children?: React.ReactNode;
loading?: React.ReactNode; loading?: React.ReactNode;
asChild?: boolean; asChild?: boolean;
@@ -384,7 +391,7 @@ export function FormAction({
loading: loadingComponent, loading: loadingComponent,
...props ...props
}: FormActionProps) { }: FormActionProps) {
const { loading: formLoading, clear, closeDialog } = useFormContext(); const { loading: formLoading, clear, closeDialog, setErrors } = useFormContext();
const [actionLoading, setActionLoading] = React.useState(false); const [actionLoading, setActionLoading] = React.useState(false);
const loading = formLoading || actionLoading; const loading = formLoading || actionLoading;
@@ -401,7 +408,19 @@ export function FormAction({
if (res && typeof res === "object") { if (res && typeof res === "object") {
if ("error" in res) { if ("error" in res) {
const { error } = res; const { error } = res;
if (typeof error === "string") throw new Error(error); if (typeof error === "string") {
toast.error(error);
} else if (Array.isArray(error)) {
const fieldErrors: Record<string, string> = {};
for (const e of error) {
fieldErrors[e.where] = e.what;
}
setErrors(fieldErrors);
} else if (error) {
setErrors({ [error.where]: error.what });
}
} else {
setErrors({});
} }
if ("toast" in res) { if ("toast" in res) {
const { toast: toastMsg } = res; const { toast: toastMsg } = res;
@@ -452,3 +471,61 @@ export function FormRow({ className, ...props }: React.ComponentProps<"div">) {
/> />
); );
} }
export function FormTab({
label,
name,
tabs,
children,
}: {
label?: React.ReactNode;
name: string;
tabs: { label: React.ReactNode; value: string }[];
children: React.ReactNode;
}) {
const { values, setValue } = useFormContext();
const active = (values[name] as string | undefined) ?? tabs[0]?.value;
React.useEffect(() => {
if (values[name] === undefined && tabs[0]) {
setValue(name, tabs[0].value);
}
// biome-ignore lint/correctness/useExhaustiveDependencies: only run on mount to seed default
}, []);
return (
<div className="flex flex-col gap-2">
<div className="flex w-full justify-between items-end">
{label}
<Tabs value={active} onValueChange={(v) => setValue(name, v)}>
<TabsList>
{tabs.map((tab) => (
<TabsTrigger key={tab.value} value={tab.value}>
{tab.label}
</TabsTrigger>
))}
</TabsList>
</Tabs>
</div>
<div className="bg-muted-foreground/10 p-3 rounded-md">
{React.Children.map(children, (child) => {
if (!React.isValidElement(child)) return null;
const childValue = (child.props as Record<string, unknown>).value;
return childValue === active ? child : null;
})}
</div>
</div>
);
}
export function FormChoice({
children,
}: {
// biome-ignore lint/style/useExportType: not a type
value: string;
children: React.ReactNode;
}) {
return <>{children}</>;
}
+68
View File
@@ -0,0 +1,68 @@
"use client";
import { Cropper as CropperPrimitive } from "@origin-space/image-cropper";
import { cn } from "@/lib/utils";
function Cropper({
className,
...props
}: React.ComponentProps<typeof CropperPrimitive.Root>) {
return (
<CropperPrimitive.Root
className={cn(
"relative flex w-full cursor-move touch-none items-center justify-center overflow-hidden focus:outline-none",
className,
)}
data-slot="cropper"
{...props}
/>
);
}
function CropperDescription({
className,
...props
}: React.ComponentProps<typeof CropperPrimitive.Description>) {
return (
<CropperPrimitive.Description
className={cn("sr-only", className)}
data-slot="cropper-description"
{...props}
/>
);
}
function CropperImage({
className,
...props
}: React.ComponentProps<typeof CropperPrimitive.Image>) {
return (
<CropperPrimitive.Image
className={cn(
"pointer-events-none h-full w-full object-cover",
className,
)}
data-slot="cropper-image"
{...props}
/>
);
}
function CropperCropArea({
className,
...props
}: React.ComponentProps<typeof CropperPrimitive.CropArea>) {
return (
<CropperPrimitive.CropArea
className={cn(
"pointer-events-none absolute border-3 border-white shadow-[0_0_0_9999px_rgba(0,0,0,0.3)] in-[[data-slot=cropper]:focus-visible]:ring-[3px] in-[[data-slot=cropper]:focus-visible]:ring-white/50",
className,
)}
data-slot="cropper-crop-area"
{...props}
/>
);
}
export { Cropper, CropperDescription, CropperImage, CropperCropArea };
+414
View File
@@ -0,0 +1,414 @@
"use client";
import {
type ChangeEvent,
type DragEvent,
type InputHTMLAttributes,
useCallback,
useRef,
useState,
} from "react";
export type FileMetadata = {
name: string;
size: number;
type: string;
url: string;
id: string;
};
export type FileWithPreview = {
file: File | FileMetadata;
id: string;
preview?: string;
};
export type FileUploadOptions = {
maxFiles?: number; // Only used when multiple is true, defaults to Infinity
maxSize?: number; // in bytes
accept?: string;
multiple?: boolean; // Defaults to false
initialFiles?: FileMetadata[];
onFilesChange?: (files: FileWithPreview[]) => void; // Callback when files change
onFilesAdded?: (addedFiles: FileWithPreview[]) => void; // Callback when new files are added
};
export type FileUploadState = {
files: FileWithPreview[];
isDragging: boolean;
errors: string[];
};
export type FileUploadActions = {
addFiles: (files: FileList | File[]) => void;
removeFile: (id: string) => void;
clearFiles: () => void;
clearErrors: () => void;
handleDragEnter: (e: DragEvent<HTMLElement>) => void;
handleDragLeave: (e: DragEvent<HTMLElement>) => void;
handleDragOver: (e: DragEvent<HTMLElement>) => void;
handleDrop: (e: DragEvent<HTMLElement>) => void;
handleFileChange: (e: ChangeEvent<HTMLInputElement>) => void;
openFileDialog: () => void;
getInputProps: (
props?: InputHTMLAttributes<HTMLInputElement>,
) => InputHTMLAttributes<HTMLInputElement> & {
// Use `any` here to avoid cross-React ref type conflicts across packages
// biome-ignore lint/suspicious/noExplicitAny: intentional
ref: any;
};
};
export const useFileUpload = (
options: FileUploadOptions = {},
): [FileUploadState, FileUploadActions] => {
const {
maxFiles = Number.POSITIVE_INFINITY,
maxSize = Number.POSITIVE_INFINITY,
accept = "*",
multiple = false,
initialFiles = [],
onFilesChange,
onFilesAdded,
} = options;
const [state, setState] = useState<FileUploadState>({
errors: [],
files: initialFiles.map((file) => ({
file,
id: file.id,
preview: file.url,
})),
isDragging: false,
});
const inputRef = useRef<HTMLInputElement>(null);
const validateFile = useCallback(
(file: File | FileMetadata): string | null => {
if (file instanceof File) {
if (file.size > maxSize) {
return `File "${file.name}" exceeds the maximum size of ${formatBytes(maxSize)}.`;
}
} else {
if (file.size > maxSize) {
return `File "${file.name}" exceeds the maximum size of ${formatBytes(maxSize)}.`;
}
}
if (accept !== "*") {
const acceptedTypes = accept.split(",").map((type) => type.trim());
const fileType = file instanceof File ? file.type || "" : file.type;
const fileExtension = `.${file instanceof File ? file.name.split(".").pop() : file.name.split(".").pop()}`;
const isAccepted = acceptedTypes.some((type) => {
if (type.startsWith(".")) {
return fileExtension.toLowerCase() === type.toLowerCase();
}
if (type.endsWith("/*")) {
const baseType = type.split("/")[0];
return fileType.startsWith(`${baseType}/`);
}
return fileType === type;
});
if (!isAccepted) {
return `File "${file instanceof File ? file.name : file.name}" is not an accepted file type.`;
}
}
return null;
},
[accept, maxSize],
);
const createPreview = useCallback(
(file: File | FileMetadata): string | undefined => {
if (file instanceof File) {
return URL.createObjectURL(file);
}
return file.url;
},
[],
);
const generateUniqueId = useCallback((file: File | FileMetadata): string => {
if (file instanceof File) {
return `${file.name}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
}
return file.id;
}, []);
const clearFiles = useCallback(() => {
setState((prev) => {
// Clean up object URLs
for (const file of prev.files ?? []) {
if (
file.preview &&
file.file instanceof File &&
file.file.type.startsWith("image/")
) {
URL.revokeObjectURL(file.preview);
}
}
if (inputRef.current) {
inputRef.current.value = "";
}
const newState = {
...prev,
errors: [],
files: [],
};
onFilesChange?.(newState.files);
return newState;
});
}, [onFilesChange]);
const addFiles = useCallback(
(newFiles: FileList | File[]) => {
if (!newFiles || newFiles.length === 0) return;
const newFilesArray = Array.from(newFiles);
const errors: string[] = [];
// Clear existing errors when new files are uploaded
setState((prev) => ({ ...prev, errors: [] }));
// In single file mode, clear existing files first
if (!multiple) {
clearFiles();
}
// Check if adding these files would exceed maxFiles (only in multiple mode)
if (
multiple &&
maxFiles !== Number.POSITIVE_INFINITY &&
state.files.length + newFilesArray.length > maxFiles
) {
errors.push(`You can only upload a maximum of ${maxFiles} files.`);
setState((prev) => ({ ...prev, errors }));
return;
}
const validFiles: FileWithPreview[] = [];
for (const file of newFilesArray) {
if (multiple) {
const isDuplicate = state.files.some(
(existingFile) =>
existingFile.file.name === file.name &&
existingFile.file.size === file.size,
);
if (isDuplicate) {
continue;
}
}
if (file.size > maxSize) {
errors.push(
multiple
? `Some files exceed the maximum size of ${formatBytes(maxSize)}.`
: `File exceeds the maximum size of ${formatBytes(maxSize)}.`,
);
continue;
}
const error = validateFile(file);
if (error) {
errors.push(error);
continue;
}
validFiles.push({
file,
id: generateUniqueId(file),
preview: createPreview(file),
});
}
// Only update state if we have valid files to add
if (validFiles.length > 0) {
// Call the onFilesAdded callback with the newly added valid files
onFilesAdded?.(validFiles);
setState((prev) => {
const newFiles = !multiple
? validFiles
: [...prev.files, ...validFiles];
onFilesChange?.(newFiles);
return {
...prev,
errors,
files: newFiles,
};
});
} else if (errors.length > 0) {
setState((prev) => ({
...prev,
errors,
}));
}
// Reset input value after handling files
if (inputRef.current) {
inputRef.current.value = "";
}
},
[
state.files,
maxFiles,
multiple,
maxSize,
validateFile,
createPreview,
generateUniqueId,
clearFiles,
onFilesChange,
onFilesAdded,
],
);
const removeFile = useCallback(
(id: string) => {
setState((prev) => {
const fileToRemove = prev.files.find((file) => file.id === id);
if (
fileToRemove?.preview &&
fileToRemove.file instanceof File &&
fileToRemove.file.type.startsWith("image/")
) {
URL.revokeObjectURL(fileToRemove.preview);
}
const newFiles = prev.files.filter((file) => file.id !== id);
onFilesChange?.(newFiles);
return {
...prev,
errors: [],
files: newFiles,
};
});
},
[onFilesChange],
);
const clearErrors = useCallback(() => {
setState((prev) => ({
...prev,
errors: [],
}));
}, []);
const handleDragEnter = useCallback((e: DragEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
setState((prev) => ({ ...prev, isDragging: true }));
}, []);
const handleDragLeave = useCallback((e: DragEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
if (e.currentTarget.contains(e.relatedTarget as Node)) {
return;
}
setState((prev) => ({ ...prev, isDragging: false }));
}, []);
const handleDragOver = useCallback((e: DragEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
}, []);
const handleDrop = useCallback(
(e: DragEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
setState((prev) => ({ ...prev, isDragging: false }));
// Don't process files if the input is disabled
if (inputRef.current?.disabled) {
return;
}
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
// In single file mode, only use the first file
if (!multiple) {
const file = e.dataTransfer.files[0];
addFiles([file]);
} else {
addFiles(e.dataTransfer.files);
}
}
},
[addFiles, multiple],
);
const handleFileChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
addFiles(e.target.files);
}
},
[addFiles],
);
const openFileDialog = useCallback(() => {
if (inputRef.current) {
inputRef.current.click();
}
}, []);
const getInputProps = useCallback(
(props: InputHTMLAttributes<HTMLInputElement> = {}) => {
return {
...props,
accept: props.accept || accept,
multiple: props.multiple !== undefined ? props.multiple : multiple,
onChange: handleFileChange,
// Cast to `any` to prevent mismatched React ref type errors across workspaces
// biome-ignore lint/suspicious/noExplicitAny: Intentional
ref: inputRef as any,
type: "file" as const,
};
},
[accept, multiple, handleFileChange],
);
return [
state,
{
addFiles,
clearErrors,
clearFiles,
getInputProps,
handleDragEnter,
handleDragLeave,
handleDragOver,
handleDrop,
handleFileChange,
openFileDialog,
removeFile,
},
];
};
// Helper function to format bytes to human-readable format
export const formatBytes = (bytes: number, decimals = 2): string => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Number.parseFloat((bytes / k ** i).toFixed(dm)) + sizes[i];
};
+13
View File
@@ -342,6 +342,19 @@ export const guides = pgTable("guides", {
//#endregion //#endregion
//#region Donate
export const schDonate = pgSchema("donate");
export const donations = schDonate.table("donations", {
id: text().primaryKey().$defaultFn(uuidv7),
name: text().notNull(),
amount: numeric().notNull(),
image: bytea(),
message: text(),
});
//#endregion
//#region Better Auth //#region Better Auth
export const user = pgTable("user", { export const user = pgTable("user", {
id: text("id").primaryKey(), id: text("id").primaryKey(),
+11
View File
@@ -31,6 +31,17 @@ export const sse = sseEndpointMap({
}), }),
), ),
}, },
donate: {
ping: z.object({
name: z.string(),
amount: z.number(),
image: z.string(),
message: z.string(),
}),
// roundtrip SSE system check
heartbeat: z.number(),
update: z.null(),
},
// Passive Update Checker // Passive Update Checker
active: { active: {
version: z.string(), version: z.string(),
+5
View File
@@ -22,10 +22,12 @@
"@bprogress/next": "^3.2.12", "@bprogress/next": "^3.2.12",
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@google/genai": "^2.8.0",
"@headless-tree/core": "^1.6.0", "@headless-tree/core": "^1.6.0",
"@headless-tree/react": "^1.6.0", "@headless-tree/react": "^1.6.0",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@next/bundle-analyzer": "16.2.6", "@next/bundle-analyzer": "16.2.6",
"@origin-space/image-cropper": "^0.1.9",
"@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.8", "@radix-ui/react-aspect-ratio": "^1.1.8",
@@ -67,6 +69,7 @@
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"motion": "^12.40.0",
"next": "16.2.6", "next": "16.2.6",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"pg": "^8.16.3", "pg": "^8.16.3",
@@ -86,6 +89,7 @@
"usehooks-ts": "^3.1.1", "usehooks-ts": "^3.1.1",
"uuidv7": "^1.1.0", "uuidv7": "^1.1.0",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"wav": "^1.0.2",
"yaml": "^2.8.2", "yaml": "^2.8.2",
"zod": "^4.1.13" "zod": "^4.1.13"
}, },
@@ -97,6 +101,7 @@
"@types/node": "^20.19.26", "@types/node": "^20.19.26",
"@types/react": "19.2.14", "@types/react": "19.2.14",
"@types/react-dom": "19.2.3", "@types/react-dom": "19.2.3",
"@types/wav": "^1.0.4",
"drizzle-kit": "^0.31.8", "drizzle-kit": "^0.31.8",
"oxfmt": "^0.52.0", "oxfmt": "^0.52.0",
"oxlint": "^1.67.0", "oxlint": "^1.67.0",
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB