tr]:last:border-b-0",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
+ return (
+
+ )
+}
+
+function TableHead({ className, ...props }: React.ComponentProps<"th">) {
+ return (
+ [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableCell({ className, ...props }: React.ComponentProps<"td">) {
+ return (
+ | [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableCaption({
+ className,
+ ...props
+}: React.ComponentProps<"caption">) {
+ return (
+
+ )
+}
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+}
diff --git a/components/ui/textarea.tsx b/components/ui/textarea.tsx
new file mode 100644
index 0000000..e67d8fe
--- /dev/null
+++ b/components/ui/textarea.tsx
@@ -0,0 +1,18 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
+ return (
+
+ )
+}
+
+export { Textarea }
diff --git a/deployment/minio-cors.json b/deployment/minio-cors.json
new file mode 100644
index 0000000..e2269fe
--- /dev/null
+++ b/deployment/minio-cors.json
@@ -0,0 +1,9 @@
+[
+ {
+ "AllowedOrigins": ["*"],
+ "AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
+ "AllowedHeaders": ["*"],
+ "ExposeHeaders": ["ETag"],
+ "MaxAgeSeconds": 3000
+ }
+]
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..d11b9d4
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,144 @@
+services:
+ wedding-db:
+ image: postgres:17-alpine
+ container_name: wedding-db
+ restart: unless-stopped
+ environment:
+ POSTGRES_DB: ${POSTGRES_DB:-wedding}
+ POSTGRES_USER: ${POSTGRES_USER:-wedding}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-wedding}
+ volumes:
+ - wedding-db-data:/var/lib/postgresql/data
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-wedding} -d ${POSTGRES_DB:-wedding}"]
+ interval: 5s
+ timeout: 5s
+ retries: 10
+ networks:
+ - wedding-private
+
+ wedding-minio:
+ image: minio/minio:latest
+ container_name: wedding-minio
+ restart: unless-stopped
+ command: server /data --console-address ":9001"
+ environment:
+ MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
+ MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin}
+ volumes:
+ - wedding-minio-data:/data
+ labels:
+ - traefik.enable=true
+ - traefik.docker.network=${TRAEFIK_NETWORK:-traefik}
+ - traefik.http.routers.wedding-minio-api.rule=Host(`${WEDDING_MINIO_API_HOST:-wedding-minio.localhost}`)
+ - traefik.http.routers.wedding-minio-api.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}
+ - traefik.http.routers.wedding-minio-api.tls=true
+ - traefik.http.routers.wedding-minio-api.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-letsencrypt}
+ - traefik.http.routers.wedding-minio-api.service=wedding-minio-api
+ - traefik.http.services.wedding-minio-api.loadbalancer.server.port=9000
+ - traefik.http.routers.wedding-minio-console.rule=Host(`${WEDDING_MINIO_CONSOLE_HOST:-wedding-minio-console.localhost}`)
+ - traefik.http.routers.wedding-minio-console.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}
+ - traefik.http.routers.wedding-minio-console.tls=true
+ - traefik.http.routers.wedding-minio-console.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-letsencrypt}
+ - traefik.http.routers.wedding-minio-console.service=wedding-minio-console
+ - traefik.http.services.wedding-minio-console.loadbalancer.server.port=9001
+ networks:
+ - wedding-private
+ - traefik-public
+
+ wedding-minio-init:
+ image: minio/mc:latest
+ container_name: wedding-minio-init
+ depends_on:
+ - wedding-minio
+ environment:
+ S3_BUCKET: ${S3_BUCKET:-wedding-uploads}
+ MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
+ MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin}
+ volumes:
+ - ./deployment/minio-cors.json:/etc/minio/cors.json:ro
+ entrypoint: >
+ /bin/sh -c "
+ until mc alias set local http://wedding-minio:9000 $$MINIO_ROOT_USER $$MINIO_ROOT_PASSWORD; do
+ echo 'Waiting for MinIO...';
+ sleep 2;
+ done;
+ mc mb --ignore-existing local/$$S3_BUCKET &&
+ mc anonymous set none local/$$S3_BUCKET &&
+ mc cors set local/$$S3_BUCKET /etc/minio/cors.json
+ "
+ networks:
+ - wedding-private
+
+ wedding-migrate:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ image: wedding-website:latest
+ container_name: wedding-migrate
+ restart: "no"
+ command: ["node", "scripts/migrate-sql.mjs"]
+ depends_on:
+ wedding-db:
+ condition: service_healthy
+ environment:
+ DATABASE_URL: ${DATABASE_URL:-postgres://wedding:wedding@wedding-db:5432/wedding}
+ networks:
+ - wedding-private
+
+ wedding-app:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ image: wedding-website:latest
+ container_name: wedding-app
+ restart: unless-stopped
+ depends_on:
+ wedding-db:
+ condition: service_healthy
+ wedding-minio-init:
+ condition: service_completed_successfully
+ wedding-migrate:
+ condition: service_completed_successfully
+ environment:
+ NODE_ENV: production
+ PORT: 3000
+ HOSTNAME: 0.0.0.0
+ DATABASE_URL: ${DATABASE_URL:-postgres://wedding:wedding@wedding-db:5432/wedding}
+ AUTH_SECRET: ${AUTH_SECRET}
+ AUTH_GOOGLE_ID: ${AUTH_GOOGLE_ID}
+ AUTH_GOOGLE_SECRET: ${AUTH_GOOGLE_SECRET}
+ ADMIN_EMAILS: ${ADMIN_EMAILS}
+ APP_URL: ${APP_URL:-https://wedding.localhost}
+ AUTH_URL: ${AUTH_URL:-https://wedding.localhost}
+ RSVP_DEADLINE: ${RSVP_DEADLINE}
+ S3_ENDPOINT: ${S3_ENDPOINT:-http://wedding-minio:9000}
+ S3_REGION: ${S3_REGION:-us-east-1}
+ S3_BUCKET: ${S3_BUCKET:-wedding-uploads}
+ S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-minioadmin}
+ S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-minioadmin}
+ S3_FORCE_PATH_STYLE: ${S3_FORCE_PATH_STYLE:-true}
+ labels:
+ - traefik.enable=true
+ - traefik.docker.network=${TRAEFIK_NETWORK:-traefik}
+ - traefik.http.routers.wedding-app.rule=Host(`${WEDDING_APP_HOST:-wedding.localhost}`)
+ - traefik.http.routers.wedding-app.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}
+ - traefik.http.routers.wedding-app.tls=true
+ - traefik.http.routers.wedding-app.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-letsencrypt}
+ - traefik.http.services.wedding-app.loadbalancer.server.port=3000
+ networks:
+ - wedding-private
+ - traefik-public
+
+volumes:
+ wedding-db-data:
+ name: wedding-db-data
+ wedding-minio-data:
+ name: wedding-minio-data
+
+networks:
+ wedding-private:
+ name: wedding-private
+ traefik-public:
+ name: ${TRAEFIK_NETWORK:-traefik}
+ external: true
diff --git a/lib/admin-auth.ts b/lib/admin-auth.ts
new file mode 100644
index 0000000..923f3ab
--- /dev/null
+++ b/lib/admin-auth.ts
@@ -0,0 +1,12 @@
+export function getAdminEmails() {
+ return (process.env.ADMIN_EMAILS ?? "")
+ .split(",")
+ .map((email) => email.trim().toLowerCase())
+ .filter(Boolean)
+}
+
+export function isAdminEmail(email: string | null | undefined) {
+ if (!email) return false
+
+ return getAdminEmails().includes(email.trim().toLowerCase())
+}
diff --git a/lib/config.ts b/lib/config.ts
new file mode 100644
index 0000000..a16764b
--- /dev/null
+++ b/lib/config.ts
@@ -0,0 +1,66 @@
+import { z } from "zod";
+
+const booleanStringSchema = z
+ .string()
+ .trim()
+ .transform((value) => value.toLowerCase())
+ .pipe(z.enum(["true", "false", "1", "0", "yes", "no"]))
+ .transform((value) => ["true", "1", "yes"].includes(value));
+
+const serverConfigSchema = z.object({
+ DATABASE_URL: z.string().url(),
+ AUTH_SECRET: z.string().min(1),
+ AUTH_GOOGLE_ID: z.string().min(1),
+ AUTH_GOOGLE_SECRET: z.string().min(1),
+ ADMIN_EMAILS: z
+ .string()
+ .min(1)
+ .transform((value) =>
+ value
+ .split(",")
+ .map((email) => email.trim().toLowerCase())
+ .filter(Boolean),
+ ),
+ APP_URL: z.string().url(),
+ RSVP_DEADLINE: z.iso.date(),
+ S3_ENDPOINT: z.string().url(),
+ S3_REGION: z.string().min(1),
+ S3_BUCKET: z.string().min(1),
+ S3_ACCESS_KEY_ID: z.string().min(1),
+ S3_SECRET_ACCESS_KEY: z.string().min(1),
+ S3_FORCE_PATH_STYLE: booleanStringSchema.default(true),
+});
+
+export type ServerConfig = z.infer;
+
+const buildTimeConfigDefaults = {
+ DATABASE_URL: "postgres://wedding:wedding@wedding-db:5432/wedding",
+ AUTH_SECRET: "docker-build-placeholder",
+ AUTH_GOOGLE_ID: "docker-build-placeholder",
+ AUTH_GOOGLE_SECRET: "docker-build-placeholder",
+ ADMIN_EMAILS: "admin@example.com",
+ APP_URL: "https://wedding.example.com",
+ RSVP_DEADLINE: "2099-12-31",
+ S3_ENDPOINT: "http://wedding-minio:9000",
+ S3_REGION: "us-east-1",
+ S3_BUCKET: "wedding-uploads",
+ S3_ACCESS_KEY_ID: "minioadmin",
+ S3_SECRET_ACCESS_KEY: "minioadmin",
+ S3_FORCE_PATH_STYLE: "true",
+};
+
+export function getServerConfig(env: NodeJS.ProcessEnv = process.env) {
+ if (env.SKIP_ENV_VALIDATION === "1") {
+ return serverConfigSchema.parse({ ...buildTimeConfigDefaults, ...env });
+ }
+
+ return serverConfigSchema.parse(env);
+}
+
+export function getDatabaseUrl(env: NodeJS.ProcessEnv = process.env) {
+ if (env.SKIP_ENV_VALIDATION === "1") {
+ return buildTimeConfigDefaults.DATABASE_URL;
+ }
+
+ return z.string().url().parse(env.DATABASE_URL);
+}
diff --git a/lib/db/client.ts b/lib/db/client.ts
new file mode 100644
index 0000000..0a7ca8a
--- /dev/null
+++ b/lib/db/client.ts
@@ -0,0 +1,25 @@
+import { Kysely, PostgresDialect } from "kysely";
+import { Pool } from "pg";
+
+import { getDatabaseUrl } from "@/lib/config";
+import type { Database } from "@/lib/db/types";
+
+declare global {
+ var __weddingDb: Kysely | undefined;
+}
+
+function createDatabase() {
+ return new Kysely({
+ dialect: new PostgresDialect({
+ pool: new Pool({
+ connectionString: getDatabaseUrl(),
+ }),
+ }),
+ });
+}
+
+export const db = globalThis.__weddingDb ?? createDatabase();
+
+if (process.env.NODE_ENV !== "production") {
+ globalThis.__weddingDb = db;
+}
diff --git a/lib/db/queries/interactions.ts b/lib/db/queries/interactions.ts
new file mode 100644
index 0000000..45e9525
--- /dev/null
+++ b/lib/db/queries/interactions.ts
@@ -0,0 +1,56 @@
+import { db } from "@/lib/db/client"
+import type { ToastRequest } from "@/lib/db/types"
+import type { InteractionSubmissionInput, ToastStatus } from "@/lib/validation/interactions"
+
+export interface InvitationInteractions {
+ toastRequests: ToastRequest[]
+}
+
+export async function getInvitationInteractions(
+ invitationId: string,
+): Promise {
+ const toastRequests = await db
+ .selectFrom("toast_requests")
+ .selectAll()
+ .where("invitation_id", "=", invitationId)
+ .orderBy("created_at", "asc")
+ .execute()
+
+ return { toastRequests }
+}
+
+export async function replaceInvitationInteractions(input: InteractionSubmissionInput) {
+ return db.transaction().execute(async (trx) => {
+ await trx
+ .deleteFrom("toast_requests")
+ .where("invitation_id", "=", input.invitationId)
+ .execute()
+
+ if (input.toastRequest) {
+ await trx
+ .insertInto("toast_requests")
+ .values({
+ invitation_id: input.invitationId,
+ member_id: input.toastRequest.memberId,
+ speaker_name: input.toastRequest.speakerName,
+ estimated_minutes: input.toastRequest.estimatedMinutes,
+ status: "pending",
+ })
+ .execute()
+ }
+
+ await trx
+ .updateTable("invitations")
+ .set({ updated_at: new Date() })
+ .where("id", "=", input.invitationId)
+ .execute()
+ })
+}
+
+export async function updateToastRequestStatus(id: string, status: ToastStatus) {
+ await db
+ .updateTable("toast_requests")
+ .set({ status, updated_at: new Date() })
+ .where("id", "=", id)
+ .execute()
+}
diff --git a/lib/db/queries/invitations.ts b/lib/db/queries/invitations.ts
new file mode 100644
index 0000000..dfeb519
--- /dev/null
+++ b/lib/db/queries/invitations.ts
@@ -0,0 +1,260 @@
+import { nanoid } from "nanoid"
+
+import { db } from "@/lib/db/client"
+import type { Invitation, InvitationMember, InvitationResponse } from "@/lib/db/types"
+import type { CreateInvitationInput } from "@/lib/validation/invitation"
+import type { SubmitInvitationResponseInput } from "@/lib/validation/response"
+
+export type DerivedInvitationStatus = "Pending" | "Partially answered" | "Attending" | "Declined" | "Partial"
+
+export interface InvitationListItem {
+ id: string
+ token: string
+ displayName: string
+ language: string
+ contactEmail: string | null
+ contactPhone: string | null
+ maxPartySize: number
+ allowMemberEdits: boolean
+ membersCount: number
+ attendingCount: number
+ pendingCount: number
+ derivedStatus: DerivedInvitationStatus
+ updatedAt: Date
+}
+
+export interface InvitationDetail {
+ invitation: Invitation
+ members: InvitationMember[]
+ response: InvitationResponse | null
+ derivedStatus: DerivedInvitationStatus
+}
+
+function deriveStatus(members: InvitationMember[], submittedAt?: Date | null): DerivedInvitationStatus {
+ if (!submittedAt) return "Pending"
+
+ const pendingCount = members.filter((member) => member.attending === null).length
+ const attendingCount = members.filter((member) => member.attending === true).length
+ const declinedCount = members.filter((member) => member.attending === false).length
+
+ if (pendingCount > 0) return "Partially answered"
+ if (attendingCount > 0 && declinedCount > 0) return "Partial"
+ if (attendingCount > 0) return "Attending"
+ return "Declined"
+}
+
+export async function createInvitationWithMembers(input: CreateInvitationInput) {
+ return db.transaction().execute(async (trx) => {
+ const invitation = await trx
+ .insertInto("invitations")
+ .values({
+ token: nanoid(32),
+ display_name: input.displayName,
+ language: input.language,
+ contact_email: input.contactEmail,
+ contact_phone: input.contactPhone,
+ max_party_size: input.maxPartySize,
+ allow_member_edits: input.allowMemberEdits,
+ notes: input.notes,
+ })
+ .returningAll()
+ .executeTakeFirstOrThrow()
+
+ await trx
+ .insertInto("invitation_members")
+ .values(
+ input.members.map((name, index) => ({
+ invitation_id: invitation.id,
+ name,
+ is_primary_contact: index === 0,
+ })),
+ )
+ .execute()
+
+ return invitation
+ })
+}
+
+export async function listInvitations(): Promise {
+ const invitations = await db
+ .selectFrom("invitations")
+ .selectAll()
+ .orderBy("updated_at", "desc")
+ .execute()
+
+ if (invitations.length === 0) return []
+
+ const invitationIds = invitations.map((invitation) => invitation.id)
+
+ const [members, responses] = await Promise.all([
+ db
+ .selectFrom("invitation_members")
+ .selectAll()
+ .where("invitation_id", "in", invitationIds)
+ .execute(),
+ db
+ .selectFrom("invitation_responses")
+ .selectAll()
+ .where("invitation_id", "in", invitationIds)
+ .execute(),
+ ])
+
+ return invitations.map((invitation) => {
+ const invitationMembers = members.filter(
+ (member) => member.invitation_id === invitation.id,
+ )
+ const response = responses.find(
+ (invitationResponse) => invitationResponse.invitation_id === invitation.id,
+ )
+
+ return {
+ id: invitation.id,
+ token: invitation.token,
+ displayName: invitation.display_name,
+ language: invitation.language,
+ contactEmail: invitation.contact_email,
+ contactPhone: invitation.contact_phone,
+ maxPartySize: invitation.max_party_size,
+ allowMemberEdits: invitation.allow_member_edits,
+ membersCount: invitationMembers.length,
+ attendingCount: invitationMembers.filter((member) => member.attending === true).length,
+ pendingCount: invitationMembers.filter((member) => member.attending === null).length,
+ derivedStatus: deriveStatus(invitationMembers, response?.submitted_at ?? null),
+ updatedAt: invitation.updated_at,
+ }
+ })
+}
+
+export async function getInvitationDetail(id: string): Promise {
+ const invitation = await db
+ .selectFrom("invitations")
+ .selectAll()
+ .where("id", "=", id)
+ .executeTakeFirst()
+
+ if (!invitation) return null
+
+ const [members, response] = await Promise.all([
+ db
+ .selectFrom("invitation_members")
+ .selectAll()
+ .where("invitation_id", "=", id)
+ .orderBy("is_primary_contact", "desc")
+ .orderBy("created_at", "asc")
+ .execute(),
+ db
+ .selectFrom("invitation_responses")
+ .selectAll()
+ .where("invitation_id", "=", id)
+ .executeTakeFirst(),
+ ])
+
+ return {
+ invitation,
+ members,
+ response: response ?? null,
+ derivedStatus: deriveStatus(members, response?.submitted_at ?? null),
+ }
+}
+
+export async function getInvitationByToken(token: string): Promise {
+ const invitation = await db
+ .selectFrom("invitations")
+ .selectAll()
+ .where("token", "=", token)
+ .executeTakeFirst()
+
+ if (!invitation) return null
+
+ const [members, response] = await Promise.all([
+ db
+ .selectFrom("invitation_members")
+ .selectAll()
+ .where("invitation_id", "=", invitation.id)
+ .orderBy("is_primary_contact", "desc")
+ .orderBy("created_at", "asc")
+ .execute(),
+ db
+ .selectFrom("invitation_responses")
+ .selectAll()
+ .where("invitation_id", "=", invitation.id)
+ .executeTakeFirst(),
+ ])
+
+ return {
+ invitation,
+ members,
+ response: response ?? null,
+ derivedStatus: deriveStatus(members, response?.submitted_at ?? null),
+ }
+}
+
+export async function submitInvitationResponse(input: SubmitInvitationResponseInput) {
+ return db.transaction().execute(async (trx) => {
+ const invitation = await trx
+ .selectFrom("invitations")
+ .select(["id"])
+ .where("token", "=", input.token)
+ .executeTakeFirst()
+
+ if (!invitation) {
+ throw new Error("Invitation not found")
+ }
+
+ const existingMembers = await trx
+ .selectFrom("invitation_members")
+ .select(["id"])
+ .where("invitation_id", "=", invitation.id)
+ .execute()
+
+ const existingMemberIds = new Set(existingMembers.map((member) => member.id))
+
+ for (const member of input.members) {
+ if (!existingMemberIds.has(member.memberId)) {
+ throw new Error("Invalid invitation member")
+ }
+
+ await trx
+ .updateTable("invitation_members")
+ .set({
+ attending: member.attending,
+ meal_preference: member.attending ? member.mealPreference : null,
+ dietary_restrictions: member.attending ? member.dietaryRestrictions : null,
+ updated_at: new Date(),
+ })
+ .where("id", "=", member.memberId)
+ .where("invitation_id", "=", invitation.id)
+ .execute()
+ }
+
+ await trx
+ .insertInto("invitation_responses")
+ .values({
+ invitation_id: invitation.id,
+ submitted_at: new Date(),
+ arrival_date: input.arrivalDate,
+ departure_date: input.departureDate,
+ stay_location: input.stayLocation,
+ needs_transport_help: input.needsTransportHelp,
+ message_to_couple: input.messageToCouple,
+ })
+ .onConflict((oc) =>
+ oc.column("invitation_id").doUpdateSet({
+ submitted_at: new Date(),
+ arrival_date: input.arrivalDate,
+ departure_date: input.departureDate,
+ stay_location: input.stayLocation,
+ needs_transport_help: input.needsTransportHelp,
+ message_to_couple: input.messageToCouple,
+ updated_at: new Date(),
+ }),
+ )
+ .execute()
+
+ await trx
+ .updateTable("invitations")
+ .set({ updated_at: new Date() })
+ .where("id", "=", invitation.id)
+ .execute()
+ })
+}
diff --git a/lib/db/queries/members.ts b/lib/db/queries/members.ts
new file mode 100644
index 0000000..aadd9b8
--- /dev/null
+++ b/lib/db/queries/members.ts
@@ -0,0 +1,11 @@
+import { db } from "@/lib/db/client"
+
+export async function listInvitationMembers(invitationId: string) {
+ return db
+ .selectFrom("invitation_members")
+ .selectAll()
+ .where("invitation_id", "=", invitationId)
+ .orderBy("is_primary_contact", "desc")
+ .orderBy("created_at", "asc")
+ .execute()
+}
diff --git a/lib/db/queries/uploads.ts b/lib/db/queries/uploads.ts
new file mode 100644
index 0000000..3f6adf2
--- /dev/null
+++ b/lib/db/queries/uploads.ts
@@ -0,0 +1,46 @@
+import { db } from "@/lib/db/client"
+import type { PhotoUpload } from "@/lib/db/types"
+
+export async function getPhotoUploadByInvitationId(
+ invitationId: string,
+): Promise {
+ return (
+ (await db
+ .selectFrom("photo_uploads")
+ .selectAll()
+ .where("invitation_id", "=", invitationId)
+ .executeTakeFirst()) ?? null
+ )
+}
+
+export async function upsertPhotoUpload(input: {
+ invitationId: string
+ bucket: string
+ objectKey: string
+ originalFilename: string
+ contentType: string
+ sizeBytes: number
+}) {
+ return db
+ .insertInto("photo_uploads")
+ .values({
+ invitation_id: input.invitationId,
+ bucket: input.bucket,
+ object_key: input.objectKey,
+ original_filename: input.originalFilename,
+ content_type: input.contentType,
+ size_bytes: input.sizeBytes,
+ })
+ .onConflict((oc) =>
+ oc.column("invitation_id").doUpdateSet({
+ bucket: input.bucket,
+ object_key: input.objectKey,
+ original_filename: input.originalFilename,
+ content_type: input.contentType,
+ size_bytes: input.sizeBytes,
+ updated_at: new Date(),
+ }),
+ )
+ .returningAll()
+ .executeTakeFirstOrThrow()
+}
diff --git a/lib/db/types.ts b/lib/db/types.ts
new file mode 100644
index 0000000..8326414
--- /dev/null
+++ b/lib/db/types.ts
@@ -0,0 +1,120 @@
+import type { ColumnType, Generated, Insertable, Selectable, Updateable } from "kysely";
+
+export type Timestamp = ColumnType;
+export type NullableTimestamp = ColumnType<
+ Date | null,
+ Date | string | null | undefined,
+ Date | string | null
+>;
+export type DateString = ColumnType;
+export type Json = JsonValue;
+
+export type JsonValue =
+ | string
+ | number
+ | boolean
+ | null
+ | JsonValue[]
+ | { [key: string]: JsonValue };
+
+export interface InvitationsTable {
+ id: Generated;
+ token: string;
+ display_name: string;
+ language: string;
+ contact_email: string | null;
+ contact_phone: string | null;
+ max_party_size: number;
+ allow_member_edits: boolean;
+ notes: string | null;
+ created_at: Timestamp;
+ updated_at: Timestamp;
+}
+
+export interface InvitationMembersTable {
+ id: Generated;
+ invitation_id: string;
+ name: string;
+ is_primary_contact: boolean;
+ attending: boolean | null;
+ meal_preference: string | null;
+ dietary_restrictions: string | null;
+ notes: string | null;
+ created_at: Timestamp;
+ updated_at: Timestamp;
+}
+
+export interface InvitationResponsesTable {
+ id: Generated;
+ invitation_id: string;
+ submitted_at: NullableTimestamp;
+ arrival_date: DateString | null;
+ departure_date: DateString | null;
+ stay_location: string | null;
+ needs_transport_help: boolean | null;
+ message_to_couple: string | null;
+ created_at: Timestamp;
+ updated_at: Timestamp;
+}
+
+export interface PhotoUploadsTable {
+ id: Generated;
+ invitation_id: string;
+ bucket: string;
+ object_key: string;
+ original_filename: string | null;
+ content_type: string | null;
+ size_bytes: number | null;
+ created_at: Timestamp;
+ updated_at: Timestamp;
+}
+
+export interface ToastRequestsTable {
+ id: Generated;
+ invitation_id: string;
+ member_id: string | null;
+ speaker_name: string;
+ estimated_minutes: number | null;
+ status: string;
+ created_at: Timestamp;
+ updated_at: Timestamp;
+}
+
+export interface AdminAuditEventsTable {
+ id: Generated;
+ admin_email: string;
+ action: string;
+ target_type: string | null;
+ target_id: string | null;
+ metadata: Json | null;
+ created_at: Timestamp;
+}
+
+export interface Database {
+ invitations: InvitationsTable;
+ invitation_members: InvitationMembersTable;
+ invitation_responses: InvitationResponsesTable;
+ photo_uploads: PhotoUploadsTable;
+ toast_requests: ToastRequestsTable;
+ admin_audit_events: AdminAuditEventsTable;
+}
+
+export type Invitation = Selectable;
+export type NewInvitation = Insertable;
+export type InvitationUpdate = Updateable;
+
+export type InvitationMember = Selectable;
+export type NewInvitationMember = Insertable;
+export type InvitationMemberUpdate = Updateable;
+
+export type InvitationResponse = Selectable;
+export type NewInvitationResponse = Insertable;
+export type InvitationResponseUpdate = Updateable;
+
+export type PhotoUpload = Selectable;
+export type NewPhotoUpload = Insertable;
+export type PhotoUploadUpdate = Updateable;
+
+export type ToastRequest = Selectable;
+export type NewToastRequest = Insertable;
+export type ToastRequestUpdate = Updateable;
diff --git a/lib/i18n/config.ts b/lib/i18n/config.ts
new file mode 100644
index 0000000..ae92bd9
--- /dev/null
+++ b/lib/i18n/config.ts
@@ -0,0 +1,19 @@
+export const supportedLanguages = [
+ { code: "en", label: "English" },
+ { code: "es", label: "Spanish" },
+ { code: "de", label: "German" },
+] as const
+
+export type SupportedLanguage = (typeof supportedLanguages)[number]["code"]
+
+export const supportedLanguageCodes = supportedLanguages.map(
+ (language) => language.code,
+) as [SupportedLanguage, ...SupportedLanguage[]]
+
+export function getLanguageLabel(code: string) {
+ return supportedLanguages.find((language) => language.code === code)?.label ?? code
+}
+
+export function isSupportedLanguage(code: string): code is SupportedLanguage {
+ return supportedLanguageCodes.includes(code as SupportedLanguage)
+}
diff --git a/lib/i18n/dictionaries/de.ts b/lib/i18n/dictionaries/de.ts
new file mode 100644
index 0000000..5caaea4
--- /dev/null
+++ b/lib/i18n/dictionaries/de.ts
@@ -0,0 +1,80 @@
+export const de = {
+ invalid: {
+ title: "Einladung nicht gefunden",
+ description: "Wir konnten diese Einladung nicht finden. Bitte prüfe den Link oder kontaktiere uns.",
+ },
+ hero: {
+ eyebrow: "Hochzeitseinladung",
+ title: "Wir freuen uns sehr, mit dir zu feiern",
+ greeting: "Hallo, {name}!",
+ description:
+ "Bitte bestätige, wer kommen wird, und teile ein paar Reisedetails, damit wir alles gut planen können.",
+ },
+ deadline: {
+ open: "Du kannst diese Antwort bis zum {date} bearbeiten.",
+ closed: "Die RSVP-Frist ist abgelaufen. Deine Antworten sind jetzt schreibgeschützt.",
+ },
+ sections: {
+ wedding: "Hochzeitsdetails",
+ rsvp: "RSVP",
+ travel: "Reisedetails",
+ interactions: "Mach es persönlich",
+ },
+ wedding: {
+ dateLabel: "Datum",
+ dateValue: "Das Hochzeitsdatum wird bald ergänzt",
+ locationLabel: "Ort",
+ locationValue: "Details zum Veranstaltungsort folgen bald",
+ accommodationLabel: "Unterkunft",
+ accommodationValue: "Empfehlungen für Unterkünfte in der Nähe werden später ergänzt.",
+ },
+ form: {
+ attending: "Nimmt teil?",
+ yes: "Ja",
+ no: "Nein",
+ mealPreference: "Essenswunsch",
+ mealPlaceholder: "Essen auswählen",
+ dietaryRestrictions: "Ernährungseinschränkungen",
+ noRestrictions: "Keine Einschränkungen",
+ arrivalDate: "Anreisedatum",
+ departureDate: "Abreisedatum",
+ stayLocation: "Wo übernachtest du?",
+ needsTransportHelp: "Brauchst du Hilfe beim Transport?",
+ messageToCouple: "Nachricht an das Paar",
+ submit: "RSVP speichern",
+ submitted: "Danke — deine RSVP wurde gespeichert.",
+ },
+ meals: {
+ meat: "Fleisch",
+ fish: "Fisch",
+ vegetarian: "Vegetarisch",
+ vegan: "Vegan",
+ kids: "Kindermenü",
+ other: "Andere",
+ },
+ errors: {
+ generic: "Bitte prüfe das Formular und versuche es erneut.",
+ closed: "Die RSVP-Frist ist abgelaufen.",
+ },
+ interactions: {
+ intro: "Diese Frage ist optional und hilft uns, den Ablauf der Feier zu planen.",
+ toastTitle: "Toast oder Rede",
+ toastDescription: "Möchte jemand aus deiner Gruppe ein paar Worte sagen? Das ist optional und wird von uns geplant.",
+ wantsToast: "Jemand möchte sprechen",
+ speakerName: "Name der sprechenden Person",
+ estimatedMinutes: "Ungefähre Minuten",
+ },
+ upload: {
+ title: "Foto für die Hochzeitsüberraschung",
+ description: "Lade ein Foto von dir oder deiner Gruppe für eine Hochzeitsaktivität hoch. Du kannst es bis zur Frist ersetzen.",
+ current: "Aktuell hochgeladenes Foto: {filename}",
+ choose: "Bild auswählen",
+ upload: "Foto hochladen",
+ replace: "Foto ersetzen",
+ uploading: "Wird hochgeladen...",
+ success: "Foto erfolgreich hochgeladen.",
+ error: "Das Foto konnte nicht hochgeladen werden. Bitte versuche es erneut.",
+ disabled: "Foto-Uploads sind geschlossen, da die RSVP-Frist abgelaufen ist.",
+ requirements: "JPG, PNG oder WebP. Maximal 10 MB.",
+ },
+} as const
diff --git a/lib/i18n/dictionaries/en.ts b/lib/i18n/dictionaries/en.ts
new file mode 100644
index 0000000..f643a8b
--- /dev/null
+++ b/lib/i18n/dictionaries/en.ts
@@ -0,0 +1,80 @@
+export const en = {
+ invalid: {
+ title: "Invitation not found",
+ description: "We could not find this invitation. Please check the link or contact us.",
+ },
+ hero: {
+ eyebrow: "Wedding invitation",
+ title: "We cannot wait to celebrate with you",
+ greeting: "Hello, {name}!",
+ description:
+ "Please confirm who will attend and share a few travel details so we can plan everything with care.",
+ },
+ deadline: {
+ open: "You can edit this response until {date}.",
+ closed: "The RSVP deadline has passed. Your answers are now read-only.",
+ },
+ sections: {
+ wedding: "Wedding details",
+ rsvp: "RSVP",
+ travel: "Travel details",
+ interactions: "Make it personal",
+ },
+ wedding: {
+ dateLabel: "Date",
+ dateValue: "Wedding date coming soon",
+ locationLabel: "Location",
+ locationValue: "Venue details coming soon",
+ accommodationLabel: "Accommodation",
+ accommodationValue: "Nearby stay recommendations will be added later.",
+ },
+ form: {
+ attending: "Attending?",
+ yes: "Yes",
+ no: "No",
+ mealPreference: "Meal preference",
+ mealPlaceholder: "Select a meal",
+ dietaryRestrictions: "Dietary restrictions",
+ noRestrictions: "No restrictions",
+ arrivalDate: "Arrival date",
+ departureDate: "Departure date",
+ stayLocation: "Where are you staying?",
+ needsTransportHelp: "Do you need transportation help?",
+ messageToCouple: "Message to the couple",
+ submit: "Save RSVP",
+ submitted: "Thank you — your RSVP was saved.",
+ },
+ meals: {
+ meat: "Meat",
+ fish: "Fish",
+ vegetarian: "Vegetarian",
+ vegan: "Vegan",
+ kids: "Kids menu",
+ other: "Other",
+ },
+ errors: {
+ generic: "Please check the RSVP form and try again.",
+ closed: "The RSVP deadline has passed.",
+ },
+ interactions: {
+ intro: "This question is optional and helps us plan the celebration schedule.",
+ toastTitle: "Toast or speech",
+ toastDescription: "Would someone in your group like to say a few words? This is optional and will be scheduled by us.",
+ wantsToast: "Someone would like to speak",
+ speakerName: "Speaker name",
+ estimatedMinutes: "Estimated minutes",
+ },
+ upload: {
+ title: "Wedding surprise photo",
+ description: "Upload one photo of yourself or your group for a wedding activity. You can replace it until the deadline.",
+ current: "Current photo uploaded: {filename}",
+ choose: "Choose image",
+ upload: "Upload photo",
+ replace: "Replace photo",
+ uploading: "Uploading...",
+ success: "Photo uploaded successfully.",
+ error: "We could not upload the photo. Please try again.",
+ disabled: "Photo uploads are closed because the RSVP deadline has passed.",
+ requirements: "JPG, PNG, or WebP. Maximum 10 MB.",
+ },
+} as const
diff --git a/lib/i18n/dictionaries/es.ts b/lib/i18n/dictionaries/es.ts
new file mode 100644
index 0000000..89f6cb7
--- /dev/null
+++ b/lib/i18n/dictionaries/es.ts
@@ -0,0 +1,80 @@
+export const es = {
+ invalid: {
+ title: "Invitación no encontrada",
+ description: "No pudimos encontrar esta invitación. Revisa el enlace o contáctanos.",
+ },
+ hero: {
+ eyebrow: "Invitación de boda",
+ title: "Nos emociona mucho celebrar contigo",
+ greeting: "¡Hola, {name}!",
+ description:
+ "Confirma quién asistirá y comparte algunos detalles del viaje para que podamos planificar todo con cariño.",
+ },
+ deadline: {
+ open: "Puedes editar esta respuesta hasta el {date}.",
+ closed: "La fecha límite para confirmar asistencia ya pasó. Tus respuestas están en modo lectura.",
+ },
+ sections: {
+ wedding: "Detalles de la boda",
+ rsvp: "Confirmación",
+ travel: "Detalles del viaje",
+ interactions: "Hazlo personal",
+ },
+ wedding: {
+ dateLabel: "Fecha",
+ dateValue: "La fecha de la boda se agregará pronto",
+ locationLabel: "Lugar",
+ locationValue: "Los detalles del lugar se agregarán pronto",
+ accommodationLabel: "Alojamiento",
+ accommodationValue: "Pronto agregaremos recomendaciones para quedarse cerca.",
+ },
+ form: {
+ attending: "¿Asiste?",
+ yes: "Sí",
+ no: "No",
+ mealPreference: "Preferencia de comida",
+ mealPlaceholder: "Selecciona una opción",
+ dietaryRestrictions: "Restricciones alimentarias",
+ noRestrictions: "Sin restricciones",
+ arrivalDate: "Fecha de llegada",
+ departureDate: "Fecha de salida",
+ stayLocation: "¿Dónde te hospedarás?",
+ needsTransportHelp: "¿Necesitas ayuda con transporte?",
+ messageToCouple: "Mensaje para la pareja",
+ submit: "Guardar confirmación",
+ submitted: "Gracias — guardamos tu confirmación.",
+ },
+ meals: {
+ meat: "Carne",
+ fish: "Pescado",
+ vegetarian: "Vegetariano",
+ vegan: "Vegano",
+ kids: "Menú infantil",
+ other: "Otro",
+ },
+ errors: {
+ generic: "Revisa el formulario e inténtalo de nuevo.",
+ closed: "La fecha límite para confirmar asistencia ya pasó.",
+ },
+ interactions: {
+ intro: "Esta pregunta es opcional y nos ayuda a organizar el horario de la celebración.",
+ toastTitle: "Brindis o discurso",
+ toastDescription: "¿Alguien de tu grupo quiere decir unas palabras? Es opcional y nosotros lo organizaremos.",
+ wantsToast: "Alguien quiere hablar",
+ speakerName: "Nombre de la persona",
+ estimatedMinutes: "Minutos aproximados",
+ },
+ upload: {
+ title: "Foto para la sorpresa de la boda",
+ description: "Sube una foto tuya o de tu grupo para una actividad de la boda. Puedes reemplazarla hasta la fecha límite.",
+ current: "Foto actual subida: {filename}",
+ choose: "Elegir imagen",
+ upload: "Subir foto",
+ replace: "Reemplazar foto",
+ uploading: "Subiendo...",
+ success: "Foto subida correctamente.",
+ error: "No pudimos subir la foto. Inténtalo de nuevo.",
+ disabled: "La subida de fotos está cerrada porque ya pasó la fecha límite.",
+ requirements: "JPG, PNG o WebP. Máximo 10 MB.",
+ },
+} as const
diff --git a/lib/i18n/dictionaries/index.ts b/lib/i18n/dictionaries/index.ts
new file mode 100644
index 0000000..2e06c14
--- /dev/null
+++ b/lib/i18n/dictionaries/index.ts
@@ -0,0 +1,11 @@
+import { de } from "./de"
+import { en } from "./en"
+import { es } from "./es"
+
+export const dictionaries = {
+ en,
+ es,
+ de,
+} as const
+
+export type GuestDictionary = typeof en
diff --git a/lib/i18n/get-dictionary.ts b/lib/i18n/get-dictionary.ts
new file mode 100644
index 0000000..3d2810e
--- /dev/null
+++ b/lib/i18n/get-dictionary.ts
@@ -0,0 +1,10 @@
+import { dictionaries } from "@/lib/i18n/dictionaries"
+import { isSupportedLanguage } from "@/lib/i18n/config"
+
+export function getGuestDictionary(language: string) {
+ if (isSupportedLanguage(language)) {
+ return dictionaries[language]
+ }
+
+ return dictionaries.en
+}
diff --git a/lib/rsvp-deadline.ts b/lib/rsvp-deadline.ts
new file mode 100644
index 0000000..8e28094
--- /dev/null
+++ b/lib/rsvp-deadline.ts
@@ -0,0 +1,14 @@
+export function getRsvpDeadline() {
+ const value = process.env.RSVP_DEADLINE
+ if (!value) return null
+
+ return value
+}
+
+export function isRsvpClosed(now = new Date()) {
+ const deadline = getRsvpDeadline()
+ if (!deadline) return false
+
+ const deadlineEnd = new Date(`${deadline}T23:59:59.999Z`)
+ return now > deadlineEnd
+}
diff --git a/lib/storage/s3.ts b/lib/storage/s3.ts
new file mode 100644
index 0000000..8ebe6da
--- /dev/null
+++ b/lib/storage/s3.ts
@@ -0,0 +1,21 @@
+import { S3Client } from "@aws-sdk/client-s3"
+
+import { getServerConfig } from "@/lib/config"
+
+export function getS3Client() {
+ const config = getServerConfig()
+
+ return new S3Client({
+ endpoint: config.S3_ENDPOINT,
+ region: config.S3_REGION,
+ forcePathStyle: config.S3_FORCE_PATH_STYLE,
+ credentials: {
+ accessKeyId: config.S3_ACCESS_KEY_ID,
+ secretAccessKey: config.S3_SECRET_ACCESS_KEY,
+ },
+ })
+}
+
+export function getUploadBucket() {
+ return getServerConfig().S3_BUCKET
+}
diff --git a/lib/storage/uploads.ts b/lib/storage/uploads.ts
new file mode 100644
index 0000000..ff714ea
--- /dev/null
+++ b/lib/storage/uploads.ts
@@ -0,0 +1,74 @@
+import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"
+import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
+import { nanoid } from "nanoid"
+
+import { getS3Client, getUploadBucket } from "@/lib/storage/s3"
+
+function safeFilename(filename: string) {
+ const normalized = filename
+ .normalize("NFKD")
+ .replace(/[\u0300-\u036f]/g, "")
+ .replace(/[^a-zA-Z0-9._-]/g, "-")
+ .replace(/-+/g, "-")
+ .replace(/^-|-$/g, "")
+
+ return normalized || "upload"
+}
+
+export function createInvitationUploadKey(invitationId: string, filename: string) {
+ return `invitations/${invitationId}/${nanoid(12)}-${safeFilename(filename)}`
+}
+
+export function isObjectKeyForInvitation(objectKey: string, invitationId: string) {
+ return objectKey.startsWith(`invitations/${invitationId}/`)
+}
+
+export async function createPresignedUploadUrl({
+ objectKey,
+ contentType,
+ sizeBytes,
+}: {
+ objectKey: string
+ contentType: string
+ sizeBytes: number
+}) {
+ const client = getS3Client()
+ const bucket = getUploadBucket()
+
+ return getSignedUrl(
+ client,
+ new PutObjectCommand({
+ Bucket: bucket,
+ Key: objectKey,
+ ContentType: contentType,
+ ContentLength: sizeBytes,
+ }),
+ { expiresIn: 60 * 5 },
+ )
+}
+
+export async function createPresignedDownloadUrl(objectKey: string) {
+ const client = getS3Client()
+ const bucket = getUploadBucket()
+
+ return getSignedUrl(
+ client,
+ new GetObjectCommand({
+ Bucket: bucket,
+ Key: objectKey,
+ }),
+ { expiresIn: 60 * 10 },
+ )
+}
+
+export async function deleteUploadObject(objectKey: string) {
+ const client = getS3Client()
+ const bucket = getUploadBucket()
+
+ await client.send(
+ new DeleteObjectCommand({
+ Bucket: bucket,
+ Key: objectKey,
+ }),
+ )
+}
diff --git a/lib/utils.ts b/lib/utils.ts
new file mode 100644
index 0000000..bd0c391
--- /dev/null
+++ b/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/lib/validation/interactions.ts b/lib/validation/interactions.ts
new file mode 100644
index 0000000..8e2ceaa
--- /dev/null
+++ b/lib/validation/interactions.ts
@@ -0,0 +1,27 @@
+import { z } from "zod"
+
+const optionalPositiveInteger = z.preprocess(
+ (value) => {
+ if (typeof value !== "string") return null
+
+ const trimmed = value.trim()
+ return trimmed.length > 0 ? trimmed : null
+ },
+ z.coerce.number().int().positive().nullable(),
+)
+
+export const toastRequestSchema = z.object({
+ memberId: z.uuid().nullable(),
+ speakerName: z.string().trim().min(1).max(160),
+ estimatedMinutes: optionalPositiveInteger,
+})
+
+export const interactionSubmissionSchema = z.object({
+ invitationId: z.uuid(),
+ toastRequest: toastRequestSchema.nullable(),
+})
+
+export const toastStatusSchema = z.enum(["pending", "accepted", "declined", "scheduled"])
+
+export type InteractionSubmissionInput = z.infer
+export type ToastStatus = z.infer
diff --git a/lib/validation/invitation.ts b/lib/validation/invitation.ts
new file mode 100644
index 0000000..357f737
--- /dev/null
+++ b/lib/validation/invitation.ts
@@ -0,0 +1,48 @@
+import { z } from "zod"
+
+import { supportedLanguageCodes } from "@/lib/i18n/config"
+
+const optionalText = z.preprocess(
+ (value) => {
+ if (typeof value !== "string") return null
+
+ const trimmed = value.trim()
+ return trimmed.length > 0 ? trimmed : null
+ },
+ z.string().nullable(),
+)
+
+const optionalEmail = z.preprocess(
+ (value) => {
+ if (typeof value !== "string") return null
+
+ const trimmed = value.trim()
+ return trimmed.length > 0 ? trimmed : null
+ },
+ z.email().nullable(),
+)
+
+export const createInvitationSchema = z
+ .object({
+ displayName: z.string().trim().min(1, "Display name is required"),
+ language: z.enum(supportedLanguageCodes, {
+ error: "Select a supported language",
+ }),
+ contactEmail: optionalEmail.optional().default(null),
+ contactPhone: optionalText.optional().default(null),
+ maxPartySize: z.coerce
+ .number()
+ .int("Party size must be a whole number")
+ .min(1, "Party size must be at least 1"),
+ allowMemberEdits: z.coerce.boolean().default(false),
+ notes: optionalText.optional().default(null),
+ members: z
+ .array(z.string().trim().min(1))
+ .min(1, "Add at least one invitation member"),
+ })
+ .refine((value) => value.members.length <= value.maxPartySize, {
+ message: "Members cannot exceed max party size",
+ path: ["members"],
+ })
+
+export type CreateInvitationInput = z.infer
diff --git a/lib/validation/response.ts b/lib/validation/response.ts
new file mode 100644
index 0000000..e8229ce
--- /dev/null
+++ b/lib/validation/response.ts
@@ -0,0 +1,55 @@
+import { z } from "zod"
+
+export const mealPreferenceSchema = z.enum([
+ "meat",
+ "fish",
+ "vegetarian",
+ "vegan",
+ "kids",
+ "other",
+])
+
+const optionalText = z.preprocess(
+ (value) => {
+ if (typeof value !== "string") return null
+
+ const trimmed = value.trim()
+ return trimmed.length > 0 ? trimmed : null
+ },
+ z.string().nullable(),
+)
+
+const optionalDate = z.preprocess(
+ (value) => {
+ if (typeof value !== "string") return null
+
+ const trimmed = value.trim()
+ return trimmed.length > 0 ? trimmed : null
+ },
+ z.iso.date().nullable(),
+)
+
+const memberResponseSchema = z
+ .object({
+ memberId: z.uuid(),
+ attending: z.boolean(),
+ mealPreference: mealPreferenceSchema.nullable(),
+ dietaryRestrictions: optionalText,
+ })
+ .refine((value) => !value.attending || value.mealPreference !== null, {
+ message: "Meal preference is required for attending guests",
+ path: ["mealPreference"],
+ })
+
+export const submitInvitationResponseSchema = z.object({
+ token: z.string().min(1),
+ members: z.array(memberResponseSchema).min(1),
+ arrivalDate: optionalDate,
+ departureDate: optionalDate,
+ stayLocation: optionalText,
+ needsTransportHelp: z.boolean().nullable(),
+ messageToCouple: optionalText,
+})
+
+export type SubmitInvitationResponseInput = z.infer
+export type MealPreference = z.infer
diff --git a/lib/validation/upload.ts b/lib/validation/upload.ts
new file mode 100644
index 0000000..fd2ef8b
--- /dev/null
+++ b/lib/validation/upload.ts
@@ -0,0 +1,18 @@
+import { z } from "zod"
+
+export const allowedImageContentTypes = ["image/jpeg", "image/png", "image/webp"] as const
+export const maxUploadSizeBytes = 10 * 1024 * 1024
+
+export const presignUploadSchema = z.object({
+ token: z.string().min(1),
+ filename: z.string().trim().min(1).max(255),
+ contentType: z.enum(allowedImageContentTypes),
+ sizeBytes: z.number().int().positive().max(maxUploadSizeBytes),
+})
+
+export const completeUploadSchema = presignUploadSchema.extend({
+ objectKey: z.string().min(1),
+})
+
+export type PresignUploadInput = z.infer
+export type CompleteUploadInput = z.infer
diff --git a/migrations/0001_initial_schema.sql b/migrations/0001_initial_schema.sql
new file mode 100644
index 0000000..228e80f
--- /dev/null
+++ b/migrations/0001_initial_schema.sql
@@ -0,0 +1,124 @@
+-- Wedding website bootstrap schema
+-- Requires: PostgreSQL 13+
+
+CREATE EXTENSION IF NOT EXISTS pgcrypto;
+
+CREATE TABLE IF NOT EXISTS invitations (
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
+ token text NOT NULL,
+ display_name text NOT NULL,
+ language text NOT NULL,
+ contact_email text NULL,
+ contact_phone text NULL,
+ max_party_size integer NOT NULL DEFAULT 1,
+ allow_member_edits boolean NOT NULL DEFAULT false,
+ notes text NULL,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz NOT NULL DEFAULT now(),
+ CONSTRAINT invitations_max_party_size_positive CHECK (max_party_size > 0)
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS invitations_token_unique ON invitations (token);
+
+CREATE TABLE IF NOT EXISTS invitation_members (
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
+ invitation_id uuid NOT NULL REFERENCES invitations(id) ON DELETE CASCADE,
+ name text NOT NULL,
+ is_primary_contact boolean NOT NULL DEFAULT false,
+ attending boolean NULL,
+ meal_preference text NULL,
+ dietary_restrictions text NULL,
+ notes text NULL,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz NOT NULL DEFAULT now()
+);
+
+CREATE INDEX IF NOT EXISTS invitation_members_invitation_id_idx
+ ON invitation_members (invitation_id);
+
+CREATE TABLE IF NOT EXISTS invitation_responses (
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
+ invitation_id uuid NOT NULL REFERENCES invitations(id) ON DELETE CASCADE,
+ submitted_at timestamptz NULL,
+ arrival_date date NULL,
+ departure_date date NULL,
+ stay_location text NULL,
+ coming_from text NULL,
+ needs_transport_help boolean NULL,
+ message_to_couple text NULL,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz NOT NULL DEFAULT now()
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS invitation_responses_invitation_id_unique
+ ON invitation_responses (invitation_id);
+
+CREATE TABLE IF NOT EXISTS photo_uploads (
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
+ invitation_id uuid NOT NULL REFERENCES invitations(id) ON DELETE CASCADE,
+ bucket text NOT NULL,
+ object_key text NOT NULL,
+ original_filename text NULL,
+ content_type text NULL,
+ size_bytes integer NULL,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz NOT NULL DEFAULT now(),
+ CONSTRAINT photo_uploads_size_bytes_non_negative
+ CHECK (size_bytes IS NULL OR size_bytes >= 0)
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS photo_uploads_invitation_id_unique
+ ON photo_uploads (invitation_id);
+
+CREATE TABLE IF NOT EXISTS song_requests (
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
+ invitation_id uuid NOT NULL REFERENCES invitations(id) ON DELETE CASCADE,
+ title text NOT NULL,
+ artist text NULL,
+ note text NULL,
+ created_at timestamptz NOT NULL DEFAULT now()
+);
+
+CREATE INDEX IF NOT EXISTS song_requests_invitation_id_idx
+ ON song_requests (invitation_id);
+
+CREATE TABLE IF NOT EXISTS toast_requests (
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
+ invitation_id uuid NOT NULL REFERENCES invitations(id) ON DELETE CASCADE,
+ member_id uuid NULL REFERENCES invitation_members(id) ON DELETE SET NULL,
+ speaker_name text NOT NULL,
+ estimated_minutes integer NULL,
+ note text NULL,
+ status text NOT NULL DEFAULT 'pending',
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz NOT NULL DEFAULT now(),
+ CONSTRAINT toast_requests_status_valid
+ CHECK (status IN ('pending', 'accepted', 'declined', 'scheduled')),
+ CONSTRAINT toast_requests_estimated_minutes_positive
+ CHECK (estimated_minutes IS NULL OR estimated_minutes > 0)
+);
+
+CREATE INDEX IF NOT EXISTS toast_requests_invitation_id_idx
+ ON toast_requests (invitation_id);
+
+CREATE TABLE IF NOT EXISTS guest_messages (
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
+ invitation_id uuid NOT NULL REFERENCES invitations(id) ON DELETE CASCADE,
+ type text NOT NULL,
+ message text NOT NULL,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ CONSTRAINT guest_messages_type_valid CHECK (type IN ('advice', 'memory', 'wish'))
+);
+
+CREATE INDEX IF NOT EXISTS guest_messages_invitation_id_idx
+ ON guest_messages (invitation_id);
+
+CREATE TABLE IF NOT EXISTS admin_audit_events (
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
+ admin_email text NOT NULL,
+ action text NOT NULL,
+ target_type text NULL,
+ target_id uuid NULL,
+ metadata jsonb NULL,
+ created_at timestamptz NOT NULL DEFAULT now()
+);
diff --git a/migrations/0002_remove_discarded_guest_fields.sql b/migrations/0002_remove_discarded_guest_fields.sql
new file mode 100644
index 0000000..281ea37
--- /dev/null
+++ b/migrations/0002_remove_discarded_guest_fields.sql
@@ -0,0 +1,10 @@
+-- Remove guest-facing fields that are no longer collected.
+
+DROP TABLE IF EXISTS song_requests;
+DROP TABLE IF EXISTS guest_messages;
+
+ALTER TABLE invitation_responses
+ DROP COLUMN IF EXISTS coming_from;
+
+ALTER TABLE toast_requests
+ DROP COLUMN IF EXISTS note;
diff --git a/next.config.ts b/next.config.ts
index e9ffa30..cd8f14f 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,7 +1,10 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
- /* config options here */
+ output: "standalone",
+ turbopack: {
+ root: process.cwd(),
+ },
};
export default nextConfig;
diff --git a/package-lock.json b/package-lock.json
index 8890bdc..f29f8f3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,17 +8,31 @@
"name": "wedding",
"version": "0.1.0",
"dependencies": {
+ "@aws-sdk/client-s3": "^3.940.0",
+ "@aws-sdk/s3-request-presigner": "^3.940.0",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "kysely": "^0.28.8",
+ "lucide-react": "^1.17.0",
+ "nanoid": "^5.1.6",
"next": "16.2.9",
+ "next-auth": "^5.0.0-beta.31",
+ "pg": "^8.16.3",
+ "radix-ui": "^1.5.0",
"react": "19.2.4",
- "react-dom": "19.2.4"
+ "react-dom": "19.2.4",
+ "tailwind-merge": "^3.6.0",
+ "zod": "^4.1.13"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
+ "@types/pg": "^8.16.0",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.9",
+ "kysely-codegen": "^0.19.0",
"tailwindcss": "^4",
"typescript": "^5"
}
@@ -36,6 +50,482 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@auth/core": {
+ "version": "0.41.2",
+ "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.2.tgz",
+ "integrity": "sha512-Hx5MNBxN2fJTbJKGUKAA0wca43D0Akl3TvufY54Gn8lop7F+34vU1zA1pn0vQfIoVuLIrpfc2nkyjwIaPJMW7w==",
+ "license": "ISC",
+ "dependencies": {
+ "@panva/hkdf": "^1.2.1",
+ "jose": "^6.0.6",
+ "oauth4webapi": "^3.3.0",
+ "preact": "10.24.3",
+ "preact-render-to-string": "6.5.11"
+ },
+ "peerDependencies": {
+ "@simplewebauthn/browser": "^9.0.1",
+ "@simplewebauthn/server": "^9.0.2",
+ "nodemailer": "^7.0.7"
+ },
+ "peerDependenciesMeta": {
+ "@simplewebauthn/browser": {
+ "optional": true
+ },
+ "@simplewebauthn/server": {
+ "optional": true
+ },
+ "nodemailer": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@aws-crypto/crc32": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz",
+ "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/crc32c": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz",
+ "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-crypto/sha1-browser": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz",
+ "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/supports-web-crypto": "^5.2.0",
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "@aws-sdk/util-locate-window": "^3.0.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-crypto/sha256-browser": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz",
+ "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-js": "^5.2.0",
+ "@aws-crypto/supports-web-crypto": "^5.2.0",
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "@aws-sdk/util-locate-window": "^3.0.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-crypto/sha256-js": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz",
+ "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/supports-web-crypto": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz",
+ "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-crypto/util": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz",
+ "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.222.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-sdk/checksums": {
+ "version": "3.1000.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1000.5.tgz",
+ "integrity": "sha512-zOXUUnilC6lgCsQtp77p/QNPmRlTES9Xi6tlDwbR6kfC/kz5PCzZckgHWm5z+8DskdwuMAbFDq61x3zr10GEEQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/crc32": "5.2.0",
+ "@aws-crypto/crc32c": "5.2.0",
+ "@aws-crypto/util": "5.2.0",
+ "@aws-sdk/core": "^3.974.20",
+ "@aws-sdk/types": "^3.973.12",
+ "@smithy/core": "^3.24.6",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-s3": {
+ "version": "3.1066.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1066.0.tgz",
+ "integrity": "sha512-jfJTg6Xbyws8+YF5sVQXgCA01elkenGEYeX6+HQOovu9m/Td1Ln2xcY3lHKetVNltdArp5rykjNd56z7bnA9dw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha1-browser": "5.2.0",
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "^3.974.20",
+ "@aws-sdk/credential-provider-node": "^3.972.55",
+ "@aws-sdk/middleware-flexible-checksums": "^3.974.30",
+ "@aws-sdk/middleware-sdk-s3": "^3.972.51",
+ "@aws-sdk/signature-v4-multi-region": "^3.996.34",
+ "@aws-sdk/types": "^3.973.12",
+ "@smithy/core": "^3.24.6",
+ "@smithy/fetch-http-handler": "^5.4.6",
+ "@smithy/node-http-handler": "^4.7.6",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/core": {
+ "version": "3.974.20",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.20.tgz",
+ "integrity": "sha512-7sDi2B2N3mc3nf1nz6FyEx/FCrJ1N1QnBmraHHQNabFaeAh2IaOOLml48/rHOD1bICHgTRkbBgNTvUzEr5Z35g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.12",
+ "@aws-sdk/xml-builder": "^3.972.29",
+ "@aws/lambda-invoke-store": "^0.2.2",
+ "@smithy/core": "^3.24.6",
+ "@smithy/signature-v4": "^5.4.6",
+ "@smithy/types": "^4.14.3",
+ "bowser": "^2.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-env": {
+ "version": "3.972.46",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.46.tgz",
+ "integrity": "sha512-+GPXVS2srMOlH74S+SmC1gVuP2TvUZ0siuC0onKO93q+udP+M72dmY8wJfVQ5CX9z/9X5A1HHwz5yRIGBtskvQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.974.20",
+ "@aws-sdk/types": "^3.973.12",
+ "@smithy/core": "^3.24.6",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-http": {
+ "version": "3.972.48",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.48.tgz",
+ "integrity": "sha512-fA5loSdlocacRxyUXtpoHSMuk5rsIKRDzQYVMnMxjcmFeZshaJlJ8lymy/hYKji6sne/UmNGj5pxuEs6kq/Qcg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.974.20",
+ "@aws-sdk/types": "^3.973.12",
+ "@smithy/core": "^3.24.6",
+ "@smithy/fetch-http-handler": "^5.4.6",
+ "@smithy/node-http-handler": "^4.7.6",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-ini": {
+ "version": "3.972.53",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.53.tgz",
+ "integrity": "sha512-ZfdhIOR41q8TcWEnUac+gCOb+O2LBWdHLmjedXpXz4IEFW2ppNuFcm6p0sMTavpM+zD5TYfpH5Gp7guRyqSgsQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.974.20",
+ "@aws-sdk/credential-provider-env": "^3.972.46",
+ "@aws-sdk/credential-provider-http": "^3.972.48",
+ "@aws-sdk/credential-provider-login": "^3.972.52",
+ "@aws-sdk/credential-provider-process": "^3.972.46",
+ "@aws-sdk/credential-provider-sso": "^3.972.52",
+ "@aws-sdk/credential-provider-web-identity": "^3.972.52",
+ "@aws-sdk/nested-clients": "^3.997.20",
+ "@aws-sdk/types": "^3.973.12",
+ "@smithy/core": "^3.24.6",
+ "@smithy/credential-provider-imds": "^4.3.7",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-login": {
+ "version": "3.972.52",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.52.tgz",
+ "integrity": "sha512-9hu2oR0qH7Fst5Tzdx+UWxm+w5zCXtErTLtOOW5hwwQc170CLwOeniRxyFY6s9mHfGEfC5zFukNBdKBwJR8mhQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.974.20",
+ "@aws-sdk/nested-clients": "^3.997.20",
+ "@aws-sdk/types": "^3.973.12",
+ "@smithy/core": "^3.24.6",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-node": {
+ "version": "3.972.55",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.55.tgz",
+ "integrity": "sha512-zMGLa/dhESVqmCD7mmIFFKSwSFrJGScvCXcjvBZEVOOMauFS5JRQvLTMukFpMEFWiV6dTAlsen2ATDBulLPtbg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/credential-provider-env": "^3.972.46",
+ "@aws-sdk/credential-provider-http": "^3.972.48",
+ "@aws-sdk/credential-provider-ini": "^3.972.53",
+ "@aws-sdk/credential-provider-process": "^3.972.46",
+ "@aws-sdk/credential-provider-sso": "^3.972.52",
+ "@aws-sdk/credential-provider-web-identity": "^3.972.52",
+ "@aws-sdk/types": "^3.973.12",
+ "@smithy/core": "^3.24.6",
+ "@smithy/credential-provider-imds": "^4.3.7",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-process": {
+ "version": "3.972.46",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.46.tgz",
+ "integrity": "sha512-VUoNFBIjWrUN8NbFiQiuxQEgFjvziAlBRPK+ddh27aj65gk0BYu6bLZnrdrNZwpW6vAihtSUtEMQ1PUJ32QRPA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.974.20",
+ "@aws-sdk/types": "^3.973.12",
+ "@smithy/core": "^3.24.6",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-sso": {
+ "version": "3.972.52",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.52.tgz",
+ "integrity": "sha512-nb2/n4o/HQf+FVpVbZe9vCTFngmuDoIsltMgLAtjixaKzvzhB4J8WSDFyWgnErgLHk55ctWH+I4PU+LIHhyffg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.974.20",
+ "@aws-sdk/nested-clients": "^3.997.20",
+ "@aws-sdk/token-providers": "3.1066.0",
+ "@aws-sdk/types": "^3.973.12",
+ "@smithy/core": "^3.24.6",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-web-identity": {
+ "version": "3.972.52",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.52.tgz",
+ "integrity": "sha512-lKj6aRSGbqLmpYmM24bY7a1Xmfcq2vkE3hv8CSPYfc1yCu0BPu/XEJ1L4Fm61MsU6ULLNSG8UGsffNoFUBjESA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.974.20",
+ "@aws-sdk/nested-clients": "^3.997.20",
+ "@aws-sdk/types": "^3.973.12",
+ "@smithy/core": "^3.24.6",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-flexible-checksums": {
+ "version": "3.974.30",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.30.tgz",
+ "integrity": "sha512-OaIhub+3yTgfFWPzKO8OzOZFIMUoJaiS5v67y3spQg7SoULGoMx4jKVBbE+uhnzkiZXQ+rEDS0RqrK4/aD1yJw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/checksums": "^3.1000.5",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-sdk-s3": {
+ "version": "3.972.51",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.51.tgz",
+ "integrity": "sha512-keQgcIUTcHL0Qn7guhsuLaxQU36r9norCrxgaPH4DNCwon4TPtXdI/UdYuycl9vj3Dlwc3YR1dfL3U+6iIwJ6w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.974.20",
+ "@aws-sdk/signature-v4-multi-region": "^3.996.34",
+ "@aws-sdk/types": "^3.973.12",
+ "@smithy/core": "^3.24.6",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients": {
+ "version": "3.997.20",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.20.tgz",
+ "integrity": "sha512-IYJuLpXp2DEILVQpQOy0PMpkftv0AHEOCn52o0atyOaumA0CdWQ3klPyXdViGYLbNpESsVFMVybvHUeZAuiGxA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "^3.974.20",
+ "@aws-sdk/signature-v4-multi-region": "^3.996.34",
+ "@aws-sdk/types": "^3.973.12",
+ "@smithy/core": "^3.24.6",
+ "@smithy/fetch-http-handler": "^5.4.6",
+ "@smithy/node-http-handler": "^4.7.6",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/s3-request-presigner": {
+ "version": "3.1066.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1066.0.tgz",
+ "integrity": "sha512-hgjGUVKggCKcwS+e7HOoHlElQrhz4nlZkMWGKez/Ycjv2qZaQeTZ8Ps7HGOrpqR3KBPWt7iSrOBcOCVoUYYRWw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.974.20",
+ "@aws-sdk/signature-v4-multi-region": "^3.996.34",
+ "@aws-sdk/types": "^3.973.12",
+ "@smithy/core": "^3.24.6",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/signature-v4-multi-region": {
+ "version": "3.996.34",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.34.tgz",
+ "integrity": "sha512-mx1L5qlumSOt/nKM3BFaHE2HVkWwz0i4Bw0pyYO42FfX/FeLlo8YI6csC0gSPprEk6fTIqI+CZN9RwUwKd5krQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.12",
+ "@smithy/signature-v4": "^5.4.6",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/token-providers": {
+ "version": "3.1066.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1066.0.tgz",
+ "integrity": "sha512-UqEUJq7dqa44hneLDUcX7UJy95cg8YqEWyakRpvIPnrNS3Mq+UlQHgCDGu5pvwAPtlIW4qcYbvW6reG6++FyvA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.974.20",
+ "@aws-sdk/nested-clients": "^3.997.20",
+ "@aws-sdk/types": "^3.973.12",
+ "@smithy/core": "^3.24.6",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/types": {
+ "version": "3.973.12",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.12.tgz",
+ "integrity": "sha512-43ajd1NF0RMgX5k0hxCNUyEdrtFUsb2aHT2QvpktSC/2Eyb2Jr/JPVqdp0XIoaHWikZJq5tNWSLO6kB5q2eMCA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/util-locate-window": {
+ "version": "3.965.7",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.7.tgz",
+ "integrity": "sha512-M0D6oIpohdNHjc7udzTHEQyot0+0iuA36jc2I9Hps+f/GtKi2HO/pyijQnCnNcwZqLB5+rtn81z3eZK/GyjAmA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/xml-builder": {
+ "version": "3.972.29",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.29.tgz",
+ "integrity": "sha512-fk0niuGFxfi8yIJuMVM4mhwObkiQSuwZFj3tAPrLVx64Pk3BkrEIpqjzHKY4hKoEBUD6Jg/S74Zj9jy+5F3DnQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.14.3",
+ "fast-xml-parser": "5.7.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws/lambda-invoke-store": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz",
+ "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
@@ -453,6 +943,44 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
+ "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
+ "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.5",
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz",
+ "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.6"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
+ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+ "license": "MIT"
+ },
"node_modules/@humanfs/core": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@@ -1198,6 +1726,18 @@
"node": ">= 10"
}
},
+ "node_modules/@nodable/entities": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz",
+ "integrity": "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodable"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1246,6 +1786,1509 @@
"node": ">=12.4.0"
}
},
+ "node_modules/@panva/hkdf": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
+ "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.2.tgz",
+ "integrity": "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.4.tgz",
+ "integrity": "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-accessible-icon": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.9.tgz",
+ "integrity": "sha512-5W9KzJz/3DeYbGJHbZv8Q6AkxMOKUmALfc+PRg9dWwJZMk6zD37Sz8sZrF7UD6CBkiJvn7dNeRzn5G7XiCMyig==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-visually-hidden": "1.2.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-accordion": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.13.tgz",
+ "integrity": "sha512-xITxBB2p5m5tAe7M0F95kb4uAh7jSIKGlExMEm93HlW+XxZHV2eXFbPWLktd4JhRiwcnXNbO7iekcrbZy6ZCvA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collapsible": "1.1.13",
+ "@radix-ui/react-collection": "1.1.9",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-alert-dialog": {
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.16.tgz",
+ "integrity": "sha512-vPaIgo0mxYlvcFaM9jB2Uot9TjGXMuAPEvrc6BOLeV+I5U8s1dkIoouYaa6lmSfc5SPMo5x5djOTOTvaigdGMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-dialog": "1.1.16",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-slot": "1.2.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.9.tgz",
+ "integrity": "sha512-yqHW5WQ/cTpU/un7dqqIKNy2iRU8BC0JB78PEzTfCCYvZu1U6W9KwObAniMk9nhSfyotKPQTYaUD/HB0f5muig==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-aspect-ratio": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.9.tgz",
+ "integrity": "sha512-Xy+Dpxt/5n9rVTdPrNFmf8GwG1NlT1pzCF/z1MgOGZMLZWdWl+km+ZRWGQAPEhbkzSwYEsfYmTca8NhUtVxqnw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-avatar": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.12.tgz",
+ "integrity": "sha512-NQCQyWC7QrDPhjMn8hUqFeU0lUrprIgm1AyMgLbzuQJibNnatdc3SSMo3/UGFu/eUkJUU1cEcKCnyhXTQzq6tA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-is-hydrated": "0.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.4.tgz",
+ "integrity": "sha512-m3JmIOAX5ZzZ6VPjxEU2dbTOhoHi0nT5riwcDwe8idocsWf4a5DXJLDtZ6LfJwMBx7W+A2b7kp2TgPEKtaiF6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-previous": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collapsible": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.13.tgz",
+ "integrity": "sha512-F0s8+p2XNpfc3k02zBfB0jPWbkHVG162+p7BdUMyJ2308QMqZ+oaclX+FAzKFovgL5OqRU+Rvy6f/vbdlJVaqA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.9.tgz",
+ "integrity": "sha512-zuSVi7ziP7uQRqc+yGxsKJfNkdyHv3ZKDaHe0gzg4dRgws96TPKWIiz84tVHP4GEcEl8bC0mdt17NkcxaJHmaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-slot": "1.2.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz",
+ "integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.4.tgz",
+ "integrity": "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context-menu": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.3.0.tgz",
+ "integrity": "sha512-d7CouXhAW+CGmFOqmB+IEvd3E9GcaqfgvfjCc3hfulp2pkaUCEVEGa0SN5nNWYA+IvQ6g1Pt+S5dpNn1AoY9hg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-menu": "2.1.17",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.16.tgz",
+ "integrity": "sha512-l9ok83YBclEZhbjgzt76Hw733e6cvRKPNgO6GJ/IETlufXG9p+fRu2wlvpImQvR6xdJ8h7J8J2DBvsPEiEsKMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-dismissable-layer": "1.1.12",
+ "@radix-ui/react-focus-guards": "1.1.4",
+ "@radix-ui/react-focus-scope": "1.1.9",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-portal": "1.1.11",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-slot": "1.2.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.7.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.2.tgz",
+ "integrity": "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.12.tgz",
+ "integrity": "sha512-MhoruH6xEzsbvOmo4TNgMfmtvRGyDZw4MDSdf4ybMHfezjqwzv6hyd4lsMzBp8K9Sn6sGzCF62x1I7BYUECXOg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-escape-keydown": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.1.17",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.17.tgz",
+ "integrity": "sha512-S6b3Jm57sY5EdDyOMLkacbB0qMnKhy1RCKZCt795ZkmtUOAvojYIZ5p7dXHIh5Cyr3jCLLI5/g64V3FKLudZmw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-menu": "2.1.17",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.4.tgz",
+ "integrity": "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.9.tgz",
+ "integrity": "sha512-9Se8t+Zry+1rEOL7Y6l/4ANYU/TOtAtf8O2fKdwLltcaMcm6kOqYGbzO4tMFQ0bvzO920pRAoHpFZ4W85S3keQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-callback-ref": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-form": {
+ "version": "0.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.9.tgz",
+ "integrity": "sha512-eTPyThIKDacJ3mJDvYwf/PSmsEYlOyA2Qcb+aGyWwYv+P5w57VPUkMVA2XJ9z0Du2KBY1HoHQzhPV9iYL/r4hg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-label": "2.1.9",
+ "@radix-ui/react-primitive": "2.1.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-hover-card": {
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.16.tgz",
+ "integrity": "sha512-hAileDBtd6CX7nlZOarOnISQ6PP4q0e16BX51ulzdZ+7IzjL0sDTVpFdmSYrIjw6zVNsfQBao5gG6AWr3qwfvA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-dismissable-layer": "1.1.12",
+ "@radix-ui/react-popper": "1.3.0",
+ "@radix-ui/react-portal": "1.1.11",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz",
+ "integrity": "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.9.tgz",
+ "integrity": "sha512-rDoTeMbCwRVcnmo7NGT9IlPo1yXmEI+xc1URP3oeewwZEV4mdTp1dYUhYbQdo4D1q2SjKVvv4N1gNY77QAQtjA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.17",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.17.tgz",
+ "integrity": "sha512-fmbNnFyf+JYCN0DhhWnEdUTDnZD1mXaPQWivdsPIb8oOSbARfD3LIQJbLCG8a8QLCwoMxiJ7GVPIFcC8Dw8v2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.9",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.12",
+ "@radix-ui/react-focus-guards": "1.1.4",
+ "@radix-ui/react-focus-scope": "1.1.9",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-popper": "1.3.0",
+ "@radix-ui/react-portal": "1.1.11",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-roving-focus": "1.1.12",
+ "@radix-ui/react-slot": "1.2.5",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.7.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menubar": {
+ "version": "1.1.17",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.17.tgz",
+ "integrity": "sha512-AKtZ4O782yO7qwIyq73WpulYt1IHhQ0htDb6wNcxzxnSDCcSWMVBiU9ycpcA90XzQO4IVIxIErtak6Kg/Vt0rQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.9",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-menu": "2.1.17",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-roving-focus": "1.1.12",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-navigation-menu": {
+ "version": "1.2.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.15.tgz",
+ "integrity": "sha512-/fS8hKCcRt4DwCGa5QIB3juRXmfYSOk4a2AEe/BDIyy7Hm+eje2Y13oUx5zejl+wFt1owrM7E8NWlbaEl5EGpg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.9",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.12",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-layout-effect": "1.1.2",
+ "@radix-ui/react-use-previous": "1.1.2",
+ "@radix-ui/react-visually-hidden": "1.2.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-one-time-password-field": {
+ "version": "0.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.9.tgz",
+ "integrity": "sha512-fvCzA9hm7yN5xxTPJIi4VhSmH5gv+76ILsxguBK3cm3icD5BR4vW7POQmu8Zio0yh91uuouG/Kang40IbMkaSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.2",
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.9",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-roving-focus": "1.1.12",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-effect-event": "0.0.3",
+ "@radix-ui/react-use-is-hydrated": "0.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-password-toggle-field": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.4.tgz",
+ "integrity": "sha512-qoDSkObZ9faJlsjlwyBH6ia7kq9vaJ2QwWTowT3nQpzPvUTAKesmWuGJYpd91HIoJqS+5ZPXy5uFPp+HlwdaAg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-effect-event": "0.0.3",
+ "@radix-ui/react-use-is-hydrated": "0.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover": {
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.16.tgz",
+ "integrity": "sha512-8brVpAU5Uq7Bh0c8EFc4ZTf2JJTYn0o+1L+CUJB3UYIOkTjKGMgoHvduylrahdmNlr3DfH0rFq2DrbNZXgaspw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-dismissable-layer": "1.1.12",
+ "@radix-ui/react-focus-guards": "1.1.4",
+ "@radix-ui/react-focus-scope": "1.1.9",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-popper": "1.3.0",
+ "@radix-ui/react-portal": "1.1.11",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-slot": "1.2.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.7.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.0.tgz",
+ "integrity": "sha512-9PB589e1aWZbrlFUHdz6WiPCL+xLZHQFX7oibqG/6Q0SwOkxDyQX9W/cyPa+sAPPKuC8cpLCpRczE5a/1DiwVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.9",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.2",
+ "@radix-ui/react-use-rect": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2",
+ "@radix-ui/rect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.11.tgz",
+ "integrity": "sha512-UEytdjgEh2tJGgD/gZK4FUx6t1rNIlM3U0DENhSrG7I75FGm1DnaDuVUWF1pWAWUwGmn1sCJ1VGHn8LhN1aTOw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.6.tgz",
+ "integrity": "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.5.tgz",
+ "integrity": "sha512-zifXeB8Y88qCYx8PLZ5oQb32KwZub+s925mMoZsBBq9KUQqWKkREubTfs6ASjRPPBe7Jt9O8OHH89+95VG+grA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-progress": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.9.tgz",
+ "integrity": "sha512-+EOkvg1Zn1vI1+fRDfRSAiJ7BWfcDAo5ASMmbqrcLZ4s4USk2FGkoHgeb2X+CkUgo2zJMiyObwf1k44CrRWsyw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-radio-group": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.4.0.tgz",
+ "integrity": "sha512-eHdV5bLx9sH+tBnbDjkIBdvQEH/c6MEtQYhTbxkaDK9qsIFFLtmJYEQFVdwhnruWotLfQmIuWEL/J+L3utE8rQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-roving-focus": "1.1.12",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-previous": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.12.tgz",
+ "integrity": "sha512-FvgPt1bRmg8Xt2QpF7NUZW3dE0ZQHGm41dAdgT2J2GJPoIXz+9Em3NobAxf4fupcxhgHu03E5CRiU2MWvObXyg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.9",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-scroll-area": {
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.11.tgz",
+ "integrity": "sha512-DS39ziOgea75U/TrXKU2/oKp0be2jrDHnzFLvahg/0iNAT1Zq16e4Uw0WXwyXvsK+mG3BRyMb7A3NRZMDuEXtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.2",
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.0.tgz",
+ "integrity": "sha512-mENc7WpJvJcW8hlMpzfFcHcEhTvYS5JMBmi9HVC1Q00uhBwML086MHYUV8QQdQv6lcu0Wg8dzd1RB8AFADcG/g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.2",
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.9",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.12",
+ "@radix-ui/react-focus-guards": "1.1.4",
+ "@radix-ui/react-focus-scope": "1.1.9",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-popper": "1.3.0",
+ "@radix-ui/react-portal": "1.1.11",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-slot": "1.2.5",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-layout-effect": "1.1.2",
+ "@radix-ui/react-use-previous": "1.1.2",
+ "@radix-ui/react-visually-hidden": "1.2.5",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.7.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-separator": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.9.tgz",
+ "integrity": "sha512-gvgW+JV/Mbjj6darztTetnmElpQEzZrXpJvfj+dOxNAxiyHEAyUvEjjl4zxblvmjmKmi3jfPoy7ZdxzCuUBJSA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slider": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.4.0.tgz",
+ "integrity": "sha512-RHcPlLOThRJM51DSIC33ZnpDEBYhyEFroVWkd2P54PGGjkmAt14RboYUU9E1MFst666zFHM0tGtWvMjSOtU1pw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.2",
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.9",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-layout-effect": "1.1.2",
+ "@radix-ui/react-use-previous": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.5.tgz",
+ "integrity": "sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.3.0.tgz",
+ "integrity": "sha512-GP1EZwhoZO/GGnhM1P5/2Vpm8iN8EnngyU0oezn2l78kN8tj25pyrvjIaT7azBhK615KSt+P2w39y57YV5jVkA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-previous": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.14.tgz",
+ "integrity": "sha512-D5jwp9JNuwDeCw3CYD2Fz+sSHo0droQjC8u75dJHe4aWr5q6yBiXZU+hurXnKudRgEpUkD5TsI6bjHPo5ThUxA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-roving-focus": "1.1.12",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toast": {
+ "version": "1.2.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.16.tgz",
+ "integrity": "sha512-WUymDDiN2DpoGudRN1aW4wF5O3BNQjZZO/5nngPoNiEVqjyOzirvZZNO0R6dC1ifucSINVaSv8JX1aq47VGgiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.9",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-dismissable-layer": "1.1.12",
+ "@radix-ui/react-portal": "1.1.11",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-layout-effect": "1.1.2",
+ "@radix-ui/react-visually-hidden": "1.2.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toggle": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.11.tgz",
+ "integrity": "sha512-FikrKJemoBGZQ6uRID0HJqSPBP6D7OppdD2OhLl0ZYLlAyPXI7MezoYGmumwNkrAoRm35xXkb4C8JPfJZZzcaw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toggle-group": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.12.tgz",
+ "integrity": "sha512-TEgECgJaWGAHJJZGzNNEYTNBdIXqX7LchANycpyP7DkfjmuiSN7ISt1k/ZRGVJgVJonsgP4vwaiKMn5utrcwWQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-roving-focus": "1.1.12",
+ "@radix-ui/react-toggle": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toolbar": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.12.tgz",
+ "integrity": "sha512-4wHtJVdIgqMmEwUvxA0BYg/2JMRbt0L3+8UD8Ml/nhKkfXtiZcM8u/S15gQ5xj9YEd/0qlrm5bE805LsjQ+J8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-roving-focus": "1.1.12",
+ "@radix-ui/react-separator": "1.1.9",
+ "@radix-ui/react-toggle-group": "1.1.12"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip": {
+ "version": "1.2.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.9.tgz",
+ "integrity": "sha512-u6F9MmTtBSLkiXNVDrtB/yPCZarM9smNswC24YYLV/M+bth6J3Gs3vlJezEoFwKZvPvxhCpUYdUnOsNG/0XOlA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-dismissable-layer": "1.1.12",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-popper": "1.3.0",
+ "@radix-ui/react-portal": "1.1.11",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-slot": "1.2.5",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-visually-hidden": "1.2.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz",
+ "integrity": "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz",
+ "integrity": "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.3",
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz",
+ "integrity": "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.2.tgz",
+ "integrity": "sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-is-hydrated": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.1.tgz",
+ "integrity": "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz",
+ "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-previous": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.2.tgz",
+ "integrity": "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.2.tgz",
+ "integrity": "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.2.tgz",
+ "integrity": "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.5.tgz",
+ "integrity": "sha512-tPcHNI3FajdDBFpl/Ez1m2WL0ufJqBKyHxMDBvKitopamK36WwBGOMicuMEZKkM5Wce41QxUyv6BsiqfrWBiGg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.2.tgz",
+ "integrity": "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==",
+ "license": "MIT"
+ },
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -1253,6 +3296,126 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@smithy/core": {
+ "version": "3.24.6",
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.6.tgz",
+ "integrity": "sha512-wBXDRup6UU97VKyaiRo8AssnfStPtG0oAAfpq/bC0a1YYau8pM86YB4kM6ccoVi1mS8l/UHbn9oDM+7uozr/ug==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/crc32": "5.2.0",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/credential-provider-imds": {
+ "version": "4.3.8",
+ "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.8.tgz",
+ "integrity": "sha512-5cAM+KZC02sTqDt6NaLXyu50M/GNMd1eTzDVR8Lb0BBsVtu7RWHo47VPPEEv1vt3Yub6uzr+M5FHC+GtoT0USg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.24.6",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/fetch-http-handler": {
+ "version": "5.4.6",
+ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.6.tgz",
+ "integrity": "sha512-FEwEYJ1jlBKdhe9TPzfghEi1bP55ZeEImlDkEa62bBBYzUcnB6RUCyuiS2mqKt6ZVjUbBgcNhzfIctH+Hevx9g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.24.6",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/is-array-buffer": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
+ "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/node-http-handler": {
+ "version": "4.7.7",
+ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.7.tgz",
+ "integrity": "sha512-ZAFvHXrEk6K180EVhmZVg8GU5pUH5BSFqRs27JW3j1qEFx9YyYwWFx17x/MHcjALYimGAji7qEOlF1++be+G5A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.24.6",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/signature-v4": {
+ "version": "5.4.6",
+ "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.6.tgz",
+ "integrity": "sha512-Ojg4B6oIDlIr1R86xCDJt1zJWnYa0VINmqdjfe9qxWjdRivHalZ3iSlQgVqYbW0MdpFOC5XfHEWsnbmdnpIILQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.24.6",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/types": {
+ "version": "4.14.3",
+ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.3.tgz",
+ "integrity": "sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-buffer-from": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
+ "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^2.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/util-utf8": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
+ "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^2.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -1575,11 +3738,23 @@
"undici-types": "~6.21.0"
}
},
+ "node_modules/@types/pg": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz",
+ "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "pg-protocol": "*",
+ "pg-types": "^2.2.0"
+ }
+ },
"node_modules/@types/react": {
"version": "19.2.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -1589,7 +3764,7 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
@@ -2270,6 +4445,18 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/anynum": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.0.tgz",
+ "integrity": "sha512-xjR9/zBVnUOP6ztMIIgShjsxui80nQUQH+5xJnvrYLs+90bF25/KJqaAi8mk+B4RDtX1Nspi6fmp4YTEts8SfA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -2277,6 +4464,18 @@
"dev": true,
"license": "Python-2.0"
},
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/aria-query": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
@@ -2519,6 +4718,12 @@
"node": ">=6.0.0"
}
},
+ "node_modules/bowser": {
+ "version": "2.14.1",
+ "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
+ "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
+ "license": "MIT"
+ },
"node_modules/brace-expansion": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
@@ -2674,12 +4879,33 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
+ },
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2714,6 +4940,33 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cosmiconfig": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz",
+ "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "env-paths": "^2.2.1",
+ "import-fresh": "^3.3.0",
+ "js-yaml": "^4.1.0",
+ "parse-json": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/d-fischer"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.9.5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2733,7 +4986,7 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/damerau-levenshtein": {
@@ -2868,6 +5121,22 @@
"node": ">=8"
}
},
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
+ "node_modules/diff": {
+ "version": "3.5.1",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.1.tgz",
+ "integrity": "sha512-Z3u54A8qGyqFOSr2pk0ijYs8mOE9Qz8kTvtKeBI+upoG9j04Sq+oI7W8zAJiQybDcESET8/uIdHzs0p3k4fZlw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -2881,6 +5150,48 @@
"node": ">=0.10.0"
}
},
+ "node_modules/dotenv": {
+ "version": "17.4.2",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
+ "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dotenv-expand": {
+ "version": "12.0.3",
+ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz",
+ "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dotenv": "^16.4.5"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dotenv-expand/node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2924,6 +5235,26 @@
"node": ">=10.13.0"
}
},
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
+ "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
"node_modules/es-abstract": {
"version": "1.24.2",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
@@ -3581,6 +5912,43 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-xml-builder": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz",
+ "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "path-expression-matcher": "^1.5.0",
+ "xml-naming": "^0.1.0"
+ }
+ },
+ "node_modules/fast-xml-parser": {
+ "version": "5.7.3",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz",
+ "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@nodable/entities": "^2.1.0",
+ "fast-xml-builder": "^1.1.7",
+ "path-expression-matcher": "^1.5.0",
+ "strnum": "^2.2.3"
+ },
+ "bin": {
+ "fxparser": "src/cli/cli.js"
+ }
+ },
"node_modules/fastq": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
@@ -3671,6 +6039,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -3757,6 +6132,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
@@ -3802,6 +6186,123 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
+ "node_modules/git-diff": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/git-diff/-/git-diff-2.0.6.tgz",
+ "integrity": "sha512-/Iu4prUrydE3Pb3lCBMbcSNIf81tgGt0W1ZwknnyF62t3tHmtiJTRj0f+1ZIhp3+Rh0ktz1pJVoa7ZXUCskivA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "chalk": "^2.3.2",
+ "diff": "^3.5.0",
+ "loglevel": "^1.6.1",
+ "shelljs": "^0.8.1",
+ "shelljs.exec": "^1.1.7"
+ },
+ "engines": {
+ "node": ">= 4.8.0"
+ }
+ },
+ "node_modules/git-diff/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/git-diff/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/git-diff/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/git-diff/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/git-diff/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/git-diff/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/git-diff/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -4013,6 +6514,25 @@
"node": ">=0.8.19"
}
},
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/internal-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -4028,6 +6548,16 @@
"node": ">= 0.4"
}
},
+ "node_modules/interpret": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
+ "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -4046,6 +6576,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/is-async-function": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
@@ -4485,6 +7022,15 @@
"jiti": "lib/jiti-cli.mjs"
}
},
+ "node_modules/jose": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
+ "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -4535,6 +7081,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -4588,6 +7141,83 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/kysely": {
+ "version": "0.28.17",
+ "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.17.tgz",
+ "integrity": "sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/kysely-codegen": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/kysely-codegen/-/kysely-codegen-0.19.0.tgz",
+ "integrity": "sha512-ZpdQQnpfY0kh45CA6yPA9vdFsBE+b06Fx7QVcbL5rX//yjbA0yYGZGhnH7GTd4P4BY/HIv5uAfuOD83JVZf95w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "4.1.2",
+ "cosmiconfig": "^9.0.0",
+ "dotenv": "^17.2.1",
+ "dotenv-expand": "^12.0.2",
+ "git-diff": "^2.0.6",
+ "micromatch": "^4.0.8",
+ "minimist": "^1.2.8",
+ "pluralize": "^8.0.0",
+ "zod": "^4.1.5"
+ },
+ "bin": {
+ "kysely-codegen": "dist/cli/bin.js"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@libsql/kysely-libsql": ">=0.3.0 <0.5.0",
+ "@tediousjs/connection-string": ">=0.5.0 <0.6.0",
+ "better-sqlite3": ">=7.6.2 <13.0.0",
+ "kysely": ">=0.27.0 <1.0.0",
+ "kysely-bun-sqlite": ">=0.3.2 <1.0.0",
+ "kysely-bun-worker": ">=1.2.0 <2.0.0",
+ "mysql2": ">=2.3.3 <4.0.0",
+ "pg": ">=8.8.0 <9.0.0",
+ "tarn": ">=3.0.0 <4.0.0",
+ "tedious": ">=18.0.0 <20.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@libsql/kysely-libsql": {
+ "optional": true
+ },
+ "@tediousjs/connection-string": {
+ "optional": true
+ },
+ "better-sqlite3": {
+ "optional": true
+ },
+ "kysely": {
+ "optional": false
+ },
+ "kysely-bun-sqlite": {
+ "optional": true
+ },
+ "kysely-bun-worker": {
+ "optional": true
+ },
+ "mysql2": {
+ "optional": true
+ },
+ "pg": {
+ "optional": true
+ },
+ "tarn": {
+ "optional": true
+ },
+ "tedious": {
+ "optional": true
+ }
+ }
+ },
"node_modules/language-subtag-registry": {
"version": "0.3.23",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
@@ -4883,6 +7513,13 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -4906,6 +7543,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/loglevel": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
+ "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ },
+ "funding": {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/loglevel"
+ }
+ },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -4929,6 +7580,15 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react": {
+ "version": "1.17.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.17.0.tgz",
+ "integrity": "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -5004,9 +7664,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
- "version": "3.3.12",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
- "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "version": "5.1.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz",
+ "integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==",
"funding": [
{
"type": "github",
@@ -5015,10 +7675,10 @@
],
"license": "MIT",
"bin": {
- "nanoid": "bin/nanoid.cjs"
+ "nanoid": "bin/nanoid.js"
},
"engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ "node": "^18 || >=20"
}
},
"node_modules/napi-postinstall": {
@@ -5097,6 +7757,51 @@
}
}
},
+ "node_modules/next-auth": {
+ "version": "5.0.0-beta.31",
+ "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.31.tgz",
+ "integrity": "sha512-1OBgCKPzo+S7UWWMp3xgvGvIJ0OpV7B3vR4ZDRqD9a4Ch+OT6dakLXG9ivhtmIWVa71nTSXattOHyCg8sNi8/Q==",
+ "license": "ISC",
+ "dependencies": {
+ "@auth/core": "0.41.2"
+ },
+ "peerDependencies": {
+ "@simplewebauthn/browser": "^9.0.1",
+ "@simplewebauthn/server": "^9.0.2",
+ "next": "^14.0.0-0 || ^15.0.0 || ^16.0.0",
+ "nodemailer": "^7.0.7",
+ "react": "^18.2.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@simplewebauthn/browser": {
+ "optional": true
+ },
+ "@simplewebauthn/server": {
+ "optional": true
+ },
+ "nodemailer": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/next/node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -5154,6 +7859,15 @@
"node": ">=18"
}
},
+ "node_modules/oauth4webapi": {
+ "version": "3.8.6",
+ "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.6.tgz",
+ "integrity": "sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -5277,6 +7991,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -5358,6 +8082,25 @@
"node": ">=6"
}
},
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -5368,6 +8111,31 @@
"node": ">=8"
}
},
+ "node_modules/path-expression-matcher": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz",
+ "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -5385,6 +8153,95 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/pg": {
+ "version": "8.21.0",
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz",
+ "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-connection-string": "^2.13.0",
+ "pg-pool": "^3.14.0",
+ "pg-protocol": "^1.14.0",
+ "pg-types": "2.2.0",
+ "pgpass": "1.0.5"
+ },
+ "engines": {
+ "node": ">= 16.0.0"
+ },
+ "optionalDependencies": {
+ "pg-cloudflare": "^1.4.0"
+ },
+ "peerDependencies": {
+ "pg-native": ">=3.0.1"
+ },
+ "peerDependenciesMeta": {
+ "pg-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pg-cloudflare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
+ "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pg-connection-string": {
+ "version": "2.13.0",
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz",
+ "integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==",
+ "license": "MIT"
+ },
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-pool": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
+ "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "pg": ">=8.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.14.0",
+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz",
+ "integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==",
+ "license": "MIT"
+ },
+ "node_modules/pg-types": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pgpass": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.1.0"
+ }
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -5404,6 +8261,16 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pluralize": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
+ "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -5443,6 +8310,83 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/postcss/node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/postgres-array": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/preact": {
+ "version": "10.24.3",
+ "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
+ "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/preact"
+ }
+ },
+ "node_modules/preact-render-to-string": {
+ "version": "6.5.11",
+ "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz",
+ "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "preact": ">=10"
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -5496,6 +8440,83 @@
],
"license": "MIT"
},
+ "node_modules/radix-ui": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.5.0.tgz",
+ "integrity": "sha512-Nzh2HNpClgB31FBHRqt2xG8XNUfVfQRpf34hACC5PNrXTd5JdXdqOXwLs3BL+D8CNYiNQiJiT8QGr5Q4vq+00w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-accessible-icon": "1.1.9",
+ "@radix-ui/react-accordion": "1.2.13",
+ "@radix-ui/react-alert-dialog": "1.1.16",
+ "@radix-ui/react-arrow": "1.1.9",
+ "@radix-ui/react-aspect-ratio": "1.1.9",
+ "@radix-ui/react-avatar": "1.1.12",
+ "@radix-ui/react-checkbox": "1.3.4",
+ "@radix-ui/react-collapsible": "1.1.13",
+ "@radix-ui/react-collection": "1.1.9",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-context-menu": "2.3.0",
+ "@radix-ui/react-dialog": "1.1.16",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.12",
+ "@radix-ui/react-dropdown-menu": "2.1.17",
+ "@radix-ui/react-focus-guards": "1.1.4",
+ "@radix-ui/react-focus-scope": "1.1.9",
+ "@radix-ui/react-form": "0.1.9",
+ "@radix-ui/react-hover-card": "1.1.16",
+ "@radix-ui/react-label": "2.1.9",
+ "@radix-ui/react-menu": "2.1.17",
+ "@radix-ui/react-menubar": "1.1.17",
+ "@radix-ui/react-navigation-menu": "1.2.15",
+ "@radix-ui/react-one-time-password-field": "0.1.9",
+ "@radix-ui/react-password-toggle-field": "0.1.4",
+ "@radix-ui/react-popover": "1.1.16",
+ "@radix-ui/react-popper": "1.3.0",
+ "@radix-ui/react-portal": "1.1.11",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.5",
+ "@radix-ui/react-progress": "1.1.9",
+ "@radix-ui/react-radio-group": "1.4.0",
+ "@radix-ui/react-roving-focus": "1.1.12",
+ "@radix-ui/react-scroll-area": "1.2.11",
+ "@radix-ui/react-select": "2.3.0",
+ "@radix-ui/react-separator": "1.1.9",
+ "@radix-ui/react-slider": "1.4.0",
+ "@radix-ui/react-slot": "1.2.5",
+ "@radix-ui/react-switch": "1.3.0",
+ "@radix-ui/react-tabs": "1.1.14",
+ "@radix-ui/react-toast": "1.2.16",
+ "@radix-ui/react-toggle": "1.1.11",
+ "@radix-ui/react-toggle-group": "1.1.12",
+ "@radix-ui/react-toolbar": "1.1.12",
+ "@radix-ui/react-tooltip": "1.2.9",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-effect-event": "0.0.3",
+ "@radix-ui/react-use-escape-keydown": "1.1.2",
+ "@radix-ui/react-use-is-hydrated": "0.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2",
+ "@radix-ui/react-visually-hidden": "1.2.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
@@ -5524,6 +8545,109 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
+ "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/rechoir": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
+ "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==",
+ "dev": true,
+ "dependencies": {
+ "resolve": "^1.1.6"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/rechoir/node_modules/resolve": {
+ "version": "1.22.12",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -5848,6 +8972,34 @@
"node": ">=8"
}
},
+ "node_modules/shelljs": {
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz",
+ "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "glob": "^7.0.0",
+ "interpret": "^1.0.0",
+ "rechoir": "^0.6.2"
+ },
+ "bin": {
+ "shjs": "bin/shjs"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/shelljs.exec": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/shelljs.exec/-/shelljs.exec-1.1.8.tgz",
+ "integrity": "sha512-vFILCw+lzUtiwBAHV8/Ex8JsFjelFMdhONIsgKNLgTzeRckp2AOYRQtHJE/9LhNvdMmE27AGtzWx0+DHpwIwSw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
"node_modules/side-channel": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
@@ -5933,6 +9085,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
"node_modules/stable-hash": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
@@ -6091,6 +9252,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/strnum": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.0.tgz",
+ "integrity": "sha512-sHrVyWWdq28RbhjuJdZsA1SnGRJV6NiXbk6AXBxDOsgAcA+lmpUZCYjOdLBxkXMwis6RRe7dlZt4VlIWFVzkmg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "anynum": "^1.0.0"
+ }
+ },
"node_modules/styled-jsx": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
@@ -6140,6 +9316,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/tailwind-merge": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz",
+ "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
"node_modules/tailwindcss": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz",
@@ -6501,6 +9687,49 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -6616,6 +9845,37 @@
"node": ">=0.10.0"
}
},
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/xml-naming": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz",
+ "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -6640,7 +9900,6 @@
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
- "dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
diff --git a/package.json b/package.json
index 8a581bb..70981cf 100644
--- a/package.json
+++ b/package.json
@@ -6,20 +6,36 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
- "lint": "eslint"
+ "lint": "eslint",
+ "db:migrate": "node scripts/migrate-sql.mjs",
+ "db:generate": "kysely-codegen --env-file .env --dialect postgres --out-file lib/db/types.ts"
},
"dependencies": {
+ "@aws-sdk/client-s3": "^3.940.0",
+ "@aws-sdk/s3-request-presigner": "^3.940.0",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "kysely": "^0.28.8",
+ "lucide-react": "^1.17.0",
+ "nanoid": "^5.1.6",
"next": "16.2.9",
+ "next-auth": "^5.0.0-beta.31",
+ "pg": "^8.16.3",
+ "radix-ui": "^1.5.0",
"react": "19.2.4",
- "react-dom": "19.2.4"
+ "react-dom": "19.2.4",
+ "tailwind-merge": "^3.6.0",
+ "zod": "^4.1.13"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
+ "@types/pg": "^8.16.0",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.9",
+ "kysely-codegen": "^0.19.0",
"tailwindcss": "^4",
"typescript": "^5"
}
diff --git a/plans/milestone-01-infra-db.md b/plans/milestone-01-infra-db.md
new file mode 100644
index 0000000..e09abc1
--- /dev/null
+++ b/plans/milestone-01-infra-db.md
@@ -0,0 +1,106 @@
+# Milestone 01 — Infrastructure and Database Foundation
+
+## Reference
+
+Read `plans/wedding-bootstrap-plan.md` first. It is the source of truth for architecture, naming, schema intent, and constraints.
+
+## Goal
+
+Create the local/runtime foundation for the app: dependencies, Docker Compose services, environment template, Kysely database client, SQL migration script, and initial schema.
+
+## Scope
+
+Implement only:
+
+- Required dependencies for PostgreSQL, Kysely, migrations, validation, auth/upload placeholders where needed.
+- `docker-compose.yml` with PostgreSQL and MinIO services, plus MinIO bucket initialization.
+- `.env.example` with all required bootstrap variables.
+- Typed config helper.
+- Kysely PostgreSQL client.
+- Node `.mjs` SQL migration runner under `scripts/`.
+- Initial pure SQL migration for the core schema.
+- Initial database typings, either hand-written or generated if practical.
+- Package scripts needed to run migrations.
+
+## Non-goals
+
+Do not implement:
+
+- Google auth.
+- `/admin` pages.
+- shadcn UI setup unless strictly required by project initialization.
+- Guest RSVP pages.
+- MinIO upload endpoints.
+- Presigned upload logic.
+- Song/toast/message forms.
+- Final visual design.
+
+## Required constraints
+
+- Use PostgreSQL.
+- Use Kysely. Do not add Prisma, Drizzle, TypeORM, Sequelize, or another ORM.
+- Keep SQL schema aligned with `wedding-bootstrap-plan.md`.
+- `invitations.language` is required from the first migration.
+- `invitation_members.attending` is the source of truth for attendance.
+- Do not duplicate attendance status on `invitation_responses`.
+- `photo_uploads.invitation_id` must be unique to enforce one photo per invitation.
+
+## Implementation tasks
+
+- Add dependencies:
+ - `kysely`
+ - `pg`
+ - `zod`
+ - `nanoid`
+ - AWS S3 packages if useful to install now
+- Add dev dependencies:
+ - `@types/pg`
+- `kysely-codegen` if used
+- Add `.env.example`.
+- Add `docker-compose.yml` with:
+ - `postgres`
+ - `minio`
+ - `minio-init`
+- Add `lib/config.ts` for server-side env parsing.
+- Add `lib/db/client.ts`.
+- Add `scripts/migrate-sql.mjs`.
+- Add `migrations/0001_initial_schema.sql`.
+- Add `lib/db/types.ts`.
+- Add package scripts:
+ - `db:migrate`
+ - optionally `db:generate` if using `kysely-codegen`
+
+## Initial tables
+
+Create:
+
+- `invitations`
+- `invitation_members`
+- `invitation_responses`
+- `photo_uploads`
+- `song_requests`
+- `toast_requests`
+- `guest_messages`
+- optionally `admin_audit_events`
+
+## Verification
+
+Run:
+
+```bash
+docker compose up -d postgres minio minio-init
+npm run db:migrate
+npm run lint
+npm run build
+```
+
+If Docker is unavailable in the execution environment, still implement the files and clearly report that Docker verification could not be run.
+
+## Acceptance criteria
+
+- Docker Compose defines working PostgreSQL and MinIO services.
+- `.env.example` documents all required variables.
+- Migrations create the planned schema.
+- The Node `.mjs` migration script can run pure SQL files against PostgreSQL.
+- The app still passes lint/build.
+- No future milestone features were implemented.
diff --git a/plans/milestone-02-auth-admin-shell.md b/plans/milestone-02-auth-admin-shell.md
new file mode 100644
index 0000000..c1ecdf5
--- /dev/null
+++ b/plans/milestone-02-auth-admin-shell.md
@@ -0,0 +1,82 @@
+# Milestone 02 — Google Auth and Admin Shell
+
+## Reference
+
+Read `plans/wedding-bootstrap-plan.md` first. Also confirm the current Next.js/Auth.js setup before implementation because this project uses Next.js 16.
+
+## Goal
+
+Add Google login restricted to the couple and create a protected `/admin` shell using shadcn/ui components only.
+
+## Scope
+
+Implement only:
+
+- Google OAuth authentication.
+- Admin email allowlist via `ADMIN_EMAILS`.
+- Route protection for `/admin` and `/admin/*`.
+- Basic `/admin` dashboard shell.
+- Sign-in/sign-out UI.
+- shadcn/ui setup and required admin shell components.
+
+## Non-goals
+
+Do not implement:
+
+- Invitation creation forms.
+- Invitation list/detail functionality.
+- Guest invite pages.
+- RSVP submission.
+- MinIO uploads.
+- Song/toast/message workflows.
+
+## Required constraints
+
+- `/admin` and `/admin/*` require authenticated admin access.
+- Only emails in `ADMIN_EMAILS` can access admin pages.
+- Guest pages must not require login.
+- Admin UI must use shadcn/ui components only.
+- Do not introduce another admin component library.
+- If a component is needed, add it through the shadcn workflow.
+
+## Implementation tasks
+
+- Add/configure Auth.js/NextAuth Google provider.
+- Add `auth.ts` or equivalent auth config.
+- Add `app/api/auth/[...nextauth]/route.ts` or current-version equivalent.
+- Add middleware or route-level protection for `/admin`.
+- Add forbidden/unauthorized handling.
+- Initialize shadcn/ui if not already initialized.
+- Add required shadcn components for the admin shell, likely:
+ - button
+ - card
+ - dropdown-menu
+ - avatar or badge if useful
+ - alert
+- Create `/admin` page with basic dashboard placeholder cards.
+- Add sign-in/sign-out controls.
+
+## Verification
+
+Run:
+
+```bash
+npm run lint
+npm run build
+```
+
+Manual checks:
+
+- Unauthenticated user cannot access `/admin`.
+- Allowed Google email can access `/admin`.
+- Non-allowlisted Google email is denied.
+- Public/guest routes remain accessible without login.
+- Admin page uses only shadcn/ui components.
+
+## Acceptance criteria
+
+- Admin auth works with Google OAuth and email allowlist.
+- `/admin` is protected.
+- Admin shell exists and builds.
+- No non-shadcn admin UI component library was added.
+- No future milestone features were implemented.
diff --git a/plans/milestone-03-admin-invitations.md b/plans/milestone-03-admin-invitations.md
new file mode 100644
index 0000000..5fd0079
--- /dev/null
+++ b/plans/milestone-03-admin-invitations.md
@@ -0,0 +1,106 @@
+# Milestone 03 — Admin Invitation Management
+
+## Reference
+
+Read `plans/wedding-bootstrap-plan.md` first. This milestone depends on Milestone 01 and Milestone 02 being complete.
+
+## Goal
+
+Allow admins to create, list, and view invitations and invitation members from the protected admin portal.
+
+## Scope
+
+Implement only:
+
+- Invitation query modules.
+- Invitation member query modules.
+- Admin invitation list page.
+- Admin create invitation page.
+- Admin invitation detail page.
+- Required shadcn/ui admin components.
+- Copy invite link affordance.
+- Language selection per invitation.
+
+## Non-goals
+
+Do not implement:
+
+- Guest RSVP submission.
+- Guest-facing localized page.
+- MinIO uploads.
+- Song/toast/message guest forms.
+- CSV export unless trivial and explicitly requested later.
+- Final design polish.
+
+## Required constraints
+
+- Admin UI must use shadcn/ui components only.
+- The admin create/edit form must require `language`.
+- Use the invitation/member model from the main plan.
+- Generate unguessable invitation tokens.
+- Use Kysely query modules; do not add an ORM.
+- Keep routes protected by admin auth.
+
+## Implementation tasks
+
+- Add query helpers for:
+ - create invitation with members
+ - list invitations with derived counts/status
+ - get invitation detail by id
+ - update invitation metadata if included
+ - add/update/remove members if included
+- Add validation schema for invitation creation.
+- Add supported language config if not already present.
+- Add `/admin/guests` page.
+- Add `/admin/guests/new` page.
+- Add `/admin/guests/[id]` page.
+- Use shadcn components for:
+ - table
+ - form
+ - input
+ - select
+ - checkbox
+ - button
+ - card
+ - badge
+ - dropdown/action menu if needed
+ - alert/toast if needed
+
+## Admin fields
+
+Create invitation form must include:
+
+- display name
+- language
+- contact email
+- contact phone
+- max party size
+- allow member edits
+- notes
+- initial members
+
+## Verification
+
+Run:
+
+```bash
+npm run lint
+npm run build
+```
+
+Manual checks:
+
+- Admin can create an invitation with multiple members.
+- Language is required and saved.
+- Invitation token is generated.
+- Invite link can be copied or displayed.
+- Invitation appears in list.
+- Invitation detail page displays members and metadata.
+- Non-admin cannot access these pages.
+- Admin pages use shadcn/ui only.
+
+## Acceptance criteria
+
+- Admin can manage the initial invitation/member data needed for guest RSVP.
+- Invitation language exists in the UI and database.
+- The implementation does not include guest RSVP or uploads yet.
diff --git a/plans/milestone-04-guest-rsvp-i18n.md b/plans/milestone-04-guest-rsvp-i18n.md
new file mode 100644
index 0000000..2498559
--- /dev/null
+++ b/plans/milestone-04-guest-rsvp-i18n.md
@@ -0,0 +1,97 @@
+# Milestone 04 — Guest RSVP and Invitation Language
+
+## Reference
+
+Read `plans/wedding-bootstrap-plan.md` first. This milestone depends on Milestones 01 and 03. Admin auth from Milestone 02 should already protect admin pages.
+
+## Goal
+
+Build the guest-facing `/invite/[token]` flow with localized copy based on `invitations.language`, per-member attendance, per-member meal/dietary fields, group logistics, and deadline behavior.
+
+## Scope
+
+Implement only:
+
+- `/invite/[token]` page.
+- Invalid invitation handling.
+- Localization dictionary foundation for guest-facing copy.
+- Per-member RSVP form.
+- Group logistics form.
+- Save/update `invitation_members` attendance/meal/dietary fields.
+- Save/update `invitation_responses` logistics and `submitted_at`.
+- RSVP deadline read-only behavior.
+- Admin visibility of submitted RSVP data if not already present.
+
+## Non-goals
+
+Do not implement:
+
+- MinIO photo upload.
+- Song requests.
+- Toast requests.
+- Guest messages.
+- Advanced final visual design.
+- New admin component libraries.
+
+## Required constraints
+
+- `invitations.language` is the source of truth for guest-facing language.
+- Do not rely only on browser language detection.
+- `invitation_members.attending` is the source of truth for attendance.
+- Do not store duplicate attendance status on `invitation_responses`.
+- Edits are allowed until `RSVP_DEADLINE`.
+- After the deadline, guest RSVP data is read-only.
+- Guest-facing design may be custom; shadcn-only restriction applies to admin pages.
+
+## Implementation tasks
+
+- Add `lib/i18n/config.ts` with supported languages.
+- Add translation dictionaries under `lib/i18n/dictionaries/`.
+- Add helper to resolve dictionary by invitation language.
+- Add response/member validation schemas.
+- Add query helpers for:
+ - find invitation by token with members/response
+ - update member RSVP fields
+ - upsert invitation response
+- Implement `/invite/[token]`.
+- Add form handling using current Next.js best practice.
+- Add deadline helper.
+- Ensure admin detail/list can show derived RSVP status and submitted response data.
+
+## Localized guest copy must include
+
+- Page headings.
+- RSVP labels.
+- Meal/dietary labels.
+- Travel/logistics labels.
+- Validation messages.
+- Submit/success/error messages.
+- Deadline/read-only messages.
+
+## Verification
+
+Run:
+
+```bash
+npm run lint
+npm run build
+```
+
+Manual checks:
+
+- Invalid token shows a friendly invalid invitation page.
+- Valid token shows personalized invitation.
+- Page renders using the invitation's configured language.
+- Different members can have different attendance values.
+- Different members can have different meal preferences/dietary restrictions.
+- Group logistics save correctly.
+- `submitted_at` is set after submission.
+- Admin can see submitted RSVP data.
+- After deadline, guest form is read-only.
+
+## Acceptance criteria
+
+- Guest RSVP MVP works end-to-end.
+- Localization is driven by invitation language.
+- Attendance is stored only per member.
+- No upload or optional interaction features were implemented yet.
diff --git a/plans/milestone-05-guest-interactions.md b/plans/milestone-05-guest-interactions.md
new file mode 100644
index 0000000..6a8c72a
--- /dev/null
+++ b/plans/milestone-05-guest-interactions.md
@@ -0,0 +1,84 @@
+# Milestone 05 — Optional Guest Interactions
+
+## Reference
+
+Read `plans/wedding-bootstrap-plan.md` first. This milestone depends on Milestone 04.
+
+## Goal
+
+Add optional guest interaction sections that make the wedding experience more personal: song requests, toast/speech requests, and advice/wish/memory messages.
+
+## Scope
+
+Implement only:
+
+- Guest song request form section.
+- Guest toast/speech request form section.
+- Guest advice/wish/memory message form section.
+- Query modules for these tables.
+- Validation schemas.
+- Admin display and basic management for these submissions.
+- Localization copy for these sections.
+
+## Non-goals
+
+Do not implement:
+
+- MinIO photo upload.
+- Presigned URLs.
+- CSV export unless explicitly requested later.
+- Complex scheduling UI for toasts.
+- Final design polish.
+
+## Required constraints
+
+- Guest-facing copy must use `invitations.language`.
+- Admin UI must use shadcn/ui components only.
+- Use Kysely query modules; do not add an ORM.
+- Song requests should be limited, recommended max: 3 per invitation.
+- Toast requests are voluntary and admin-approved.
+
+## Implementation tasks
+
+- Add query helpers for:
+ - song requests
+ - toast requests
+ - guest messages
+- Add Zod schemas for interactions.
+- Add localized copy for interaction sections.
+- Add guest-facing form sections to `/invite/[token]`.
+- Persist submissions.
+- Add admin views on invitation detail page for:
+ - song requests
+ - toast requests
+ - guest messages
+- Add toast status controls if feasible:
+ - pending
+ - accepted
+ - declined
+ - scheduled
+
+## Verification
+
+Run:
+
+```bash
+npm run lint
+npm run build
+```
+
+Manual checks:
+
+- Guest can submit up to 3 song requests.
+- Guest can submit optional toast/speech request.
+- Guest can submit advice/wish/memory message.
+- Guest-facing text is localized by invitation language.
+- Admin can view all submitted interaction data.
+- Admin can update toast request status if implemented.
+- Admin pages use shadcn/ui only.
+
+## Acceptance criteria
+
+- Optional guest interactions work end-to-end.
+- Admin can review them.
+- No upload functionality was implemented in this milestone.
diff --git a/plans/milestone-06-minio-uploads.md b/plans/milestone-06-minio-uploads.md
new file mode 100644
index 0000000..4a6ca5d
--- /dev/null
+++ b/plans/milestone-06-minio-uploads.md
@@ -0,0 +1,100 @@
+# Milestone 06 — MinIO Photo Uploads
+
+## Reference
+
+Read `plans/wedding-bootstrap-plan.md` first. This milestone depends on Milestones 01, 03, and 04.
+
+## Goal
+
+Add one-photo-per-invitation upload support using MinIO through S3-compatible presigned URLs.
+
+## Scope
+
+Implement only:
+
+- S3/MinIO client.
+- Presigned upload endpoint.
+- Upload completion endpoint.
+- Guest-facing photo upload UI.
+- One-current-photo-per-invitation behavior.
+- Replacement until deadline.
+- Admin signed preview/download URL.
+- Upload validation.
+
+## Non-goals
+
+Do not implement:
+
+- Multiple photos per invitation.
+- Public unauthenticated photo browsing.
+- Image processing/resizing unless explicitly requested later.
+- Complex gallery UI.
+- Another storage provider.
+
+## Required constraints
+
+- Use MinIO via S3-compatible AWS SDK APIs.
+- One image per invitation/group.
+- Replacing the image is allowed until `RSVP_DEADLINE`.
+- After the deadline, guest upload is read-only/disabled.
+- Validate invitation token before presigning upload.
+- Admin UI must use shadcn/ui components only.
+- Guest-facing copy must use `invitations.language`.
+
+## Implementation tasks
+
+- Add `lib/storage/s3.ts`.
+- Add upload helper functions in `lib/storage/uploads.ts`.
+- Add validation schema for presign/complete requests.
+- Add `app/api/uploads/presign/route.ts`.
+- Add `app/api/uploads/complete/route.ts`.
+- Generate object keys like:
+
+```txt
+invitations/{invitationId}/{uuid}-{safeFilename}
+```
+
+- Upsert `photo_uploads` metadata.
+- Optionally delete old object when replacing.
+- Add guest upload component/section.
+- Add localized upload copy.
+- Add admin preview/download via short-lived signed GET URL.
+
+## Validation rules
+
+- Allowed MIME types:
+ - `image/jpeg`
+ - `image/png`
+ - `image/webp`
+- Enforce a maximum file size.
+- Require valid invitation token.
+- Require deadline not passed for guest upload/replace.
+
+## Verification
+
+Run:
+
+```bash
+docker compose up -d postgres minio minio-init
+npm run lint
+npm run build
+```
+
+Manual checks:
+
+- Guest can upload one valid image.
+- Admin can preview/download the image.
+- Guest can replace image before deadline.
+- Metadata in `photo_uploads` is updated on replace.
+- Invalid MIME type is rejected.
+- Oversized file is rejected.
+- Invalid token cannot presign upload.
+- After deadline, upload/replace is disabled.
+- Admin pages use shadcn/ui only.
+
+## Acceptance criteria
+
+- MinIO upload flow works end-to-end.
+- Exactly one current photo is associated with each invitation.
+- Uploads are secured by invitation token and deadline.
+- Admin can access uploaded photo previews/downloads.
diff --git a/plans/wedding-bootstrap-plan.md b/plans/wedding-bootstrap-plan.md
new file mode 100644
index 0000000..757865f
--- /dev/null
+++ b/plans/wedding-bootstrap-plan.md
@@ -0,0 +1,883 @@
+# Wedding Website Bootstrap Plan
+
+## Goal
+
+Build the dynamic foundation for a wedding website before final visual/content design is complete.
+
+The website should support:
+
+- A public wedding homepage with static sections and personalized guest content.
+- Unique invitation links for each invited group/household.
+- RSVP and per-person attendance/meal collection.
+- Optional guest contributions such as song requests, toast requests, messages, and one photo upload.
+- A private `/admin` portal restricted to the couple through Google login.
+- A Docker Compose runtime with PostgreSQL and MinIO.
+
+## Confirmed decisions
+
+### Application stack
+
+- Framework: Next.js App Router.
+- Database: PostgreSQL.
+- Query layer: Kysely, no ORM.
+- Authentication: Google OAuth.
+- Admin access: restricted by email allowlist from `.env`.
+- Object storage: MinIO using S3-compatible APIs.
+- Deployment/runtime style: Docker Compose.
+- Admin UI component system: shadcn/ui only.
+
+### Admin UI requirement
+
+All UI components used in the admin area must come from shadcn/ui.
+
+This applies to every admin page under:
+
+```txt
+/admin
+/admin/*
+```
+
+The admin area must use shadcn/ui components for:
+
+- layout primitives where applicable
+- cards
+- buttons
+- forms
+- inputs
+- selects
+- checkboxes
+- dialogs
+- dropdown menus
+- tables
+- badges
+- tabs
+- alerts
+- toasts/sonner notifications
+- action menus
+- empty states where applicable
+- loading/skeleton states
+
+Custom admin UI should only be composed from shadcn/ui components and small local wrappers around them. Do not introduce another component library for the admin portal.
+
+If a needed admin component is not yet installed from shadcn/ui, add it through the shadcn workflow and then reuse it consistently.
+
+The guest-facing invitation pages may have custom wedding-themed design, but the admin management UI must remain shadcn-based.
+
+### Invitation model
+
+- The primary guest-facing unit is an `Invitation`, not an individual guest.
+- An invitation represents a household, couple, family, individual guest, or group.
+- Each invitation has one unique private token and one invite URL:
+
+```txt
+/invite/[token]
+```
+
+- Each invitation must have a required language.
+- The invitation language is the source of truth for which language the guest-facing invitation page uses.
+- This is a core requirement because many guests may only speak one of the wedding languages.
+- Admins choose the invitation language when creating or editing an invitation.
+- Admin pages can remain in the couple's preferred management language initially, but guest-facing copy must respect the invitation language.
+
+### Invitation members
+
+- Each invitation contains one or more `InvitationMember` records.
+- Members are modeled from the start because attendance and food choices can differ per person.
+- Per-person attendance lives on `invitation_members.attending`.
+- The response table must not duplicate attendance status.
+
+### RSVP/response model
+
+- `invitation_members` owns per-person answers:
+ - attending
+ - meal preference
+ - dietary restrictions
+- `invitation_responses` owns group-level logistics:
+ - submitted timestamp
+ - travel dates
+ - stay location
+ - transport help
+ - message to the couple
+- RSVP edits are allowed until a configured deadline.
+
+### Photo uploads
+
+- One image per invitation/group.
+- The image may be replaced until the deadline.
+- Store files in MinIO.
+- Store object metadata in PostgreSQL.
+
+### Optional wedding interactions
+
+Include these from the bootstrap if feasible:
+
+- Song requests.
+- Optional toast/speech request.
+- Advice, wish, or favorite memory message.
+
+## Proposed environment variables
+
+```env
+DATABASE_URL=postgres://wedding:wedding@localhost:5432/wedding
+
+AUTH_SECRET=change-me
+AUTH_GOOGLE_ID=change-me
+AUTH_GOOGLE_SECRET=change-me
+ADMIN_EMAILS=person-one@example.com,person-two@example.com
+
+APP_URL=http://localhost:3000
+RSVP_DEADLINE=2026-08-01
+
+S3_ENDPOINT=http://localhost:9000
+S3_REGION=us-east-1
+S3_BUCKET=wedding-uploads
+S3_ACCESS_KEY_ID=minioadmin
+S3_SECRET_ACCESS_KEY=minioadmin
+S3_FORCE_PATH_STYLE=true
+```
+
+## Target folder structure
+
+```txt
+app/
+ page.tsx
+ invite/
+ [token]/
+ page.tsx
+ admin/
+ page.tsx
+ guests/
+ page.tsx
+ new/
+ page.tsx
+ [id]/
+ page.tsx
+ api/
+ auth/
+ [...nextauth]/
+ route.ts
+ uploads/
+ presign/
+ route.ts
+ complete/
+ route.ts
+
+lib/
+ auth.ts
+ config.ts
+ db/
+ client.ts
+ types.ts
+ queries/
+ invitations.ts
+ members.ts
+ responses.ts
+ uploads.ts
+ song-requests.ts
+ toast-requests.ts
+ guest-messages.ts
+ storage/
+ s3.ts
+ uploads.ts
+ i18n/
+ config.ts
+ dictionaries/
+ en.ts
+ es.ts
+ de.ts
+ validation/
+ invitation.ts
+ response.ts
+ upload.ts
+ interactions.ts
+
+scripts/
+ migrate-sql.mjs
+
+migrations/
+ 0001_initial_schema.sql
+
+docker-compose.yml
+plans/
+ wedding-bootstrap-plan.md
+```
+
+## Database schema plan
+
+### `invitations`
+
+Represents one invite/group/household.
+
+```txt
+id uuid primary key
+token text unique not null
+display_name text not null
+language text not null
+contact_email text
+contact_phone text
+max_party_size integer not null default 1
+allow_member_edits boolean not null default false
+notes text
+created_at timestamptz not null
+updated_at timestamptz not null
+```
+
+Notes:
+
+- `token` must be unguessable.
+- `display_name` is what the guest sees, e.g. `Ana & Luis` or `The Garcia Family`.
+- `language` controls guest-facing page copy and form labels for this invitation.
+- Recommended initial language values should be short locale codes such as `en`, `es`, `de`, etc.
+- `allow_member_edits` allows flexible +1 or family edits later.
+
+### `invitation_members`
+
+Represents each individual person under an invitation.
+
+```txt
+id uuid primary key
+invitation_id uuid not null references invitations(id) on delete cascade
+name text not null
+is_primary_contact boolean not null default false
+attending boolean
+meal_preference text
+dietary_restrictions text
+notes text
+created_at timestamptz not null
+updated_at timestamptz not null
+```
+
+Notes:
+
+- `attending = null` means pending/unanswered.
+- Attendance status is derived from this table, not from `invitation_responses`.
+- Meal preference examples: `meat`, `fish`, `vegetarian`, `vegan`, `kids`, `other`.
+
+### `invitation_responses`
+
+Stores group-level RSVP metadata and logistics.
+
+```txt
+id uuid primary key
+invitation_id uuid unique not null references invitations(id) on delete cascade
+submitted_at timestamptz
+arrival_date date
+departure_date date
+stay_location text
+coming_from text
+needs_transport_help boolean
+message_to_couple text
+created_at timestamptz not null
+updated_at timestamptz not null
+```
+
+Notes:
+
+- No attendance status here.
+- No attendee count here.
+- Admin dashboard derives totals from `invitation_members`.
+
+### `photo_uploads`
+
+Stores one uploaded activity/surprise image per invitation.
+
+```txt
+id uuid primary key
+invitation_id uuid unique not null references invitations(id) on delete cascade
+bucket text not null
+object_key text not null
+original_filename text
+content_type text
+size_bytes integer
+created_at timestamptz not null
+updated_at timestamptz not null
+```
+
+Notes:
+
+- Unique `invitation_id` enforces one photo per invitation.
+- Replacing a photo updates this record and may delete/overwrite the old object.
+
+### `song_requests`
+
+Stores optional party song requests.
+
+```txt
+id uuid primary key
+invitation_id uuid not null references invitations(id) on delete cascade
+title text not null
+artist text
+note text
+created_at timestamptz not null
+```
+
+Notes:
+
+- Recommended limit: up to 3 song requests per invitation.
+
+### `toast_requests`
+
+Stores optional requests to say a few words.
+
+```txt
+id uuid primary key
+invitation_id uuid not null references invitations(id) on delete cascade
+member_id uuid references invitation_members(id) on delete set null
+speaker_name text not null
+estimated_minutes integer
+note text
+status text not null default 'pending'
+created_at timestamptz not null
+updated_at timestamptz not null
+```
+
+Recommended statuses:
+
+```txt
+pending
+accepted
+declined
+scheduled
+```
+
+Notes:
+
+- Toast requests are voluntary.
+- Admin approval is required before scheduling.
+
+### `guest_messages`
+
+Stores optional advice, wishes, or memories.
+
+```txt
+id uuid primary key
+invitation_id uuid not null references invitations(id) on delete cascade
+type text not null
+message text not null
+created_at timestamptz not null
+```
+
+Recommended message types:
+
+```txt
+advice
+memory
+wish
+```
+
+### `admin_audit_events` optional
+
+Useful but not required for the first pass.
+
+```txt
+id uuid primary key
+admin_email text not null
+action text not null
+target_type text
+target_id uuid
+metadata jsonb
+created_at timestamptz not null
+```
+
+## Derived RSVP status logic
+
+Admin-facing status should be computed instead of stored redundantly.
+
+Suggested derived statuses:
+
+```txt
+Pending
+ invitation_response.submitted_at is null
+
+Partially answered
+ submitted_at exists and at least one member has attending = null
+
+Attending
+ at least one member has attending = true and no members are pending
+
+Declined
+ all members have attending = false and no members are pending
+
+Partial
+ at least one member attending = true and at least one member attending = false
+```
+
+Dashboard metrics can derive:
+
+- total invitations
+- total invited members
+- attending members
+- declined members
+- pending members
+- invitations with submitted responses
+- invitations with uploaded photos
+- song request count
+- pending toast requests
+
+## Guest-facing invite flow
+
+Route:
+
+```txt
+/invite/[token]
+```
+
+Flow:
+
+1. Resolve invitation by token.
+2. If token is invalid, show a friendly invalid-invitation page.
+3. If valid, render personalized wedding page shell.
+4. Load guest-facing copy based on `invitations.language`.
+5. Show static wedding information:
+ - hero
+ - date
+ - countdown
+ - location
+ - accommodation information
+ - short couple story placeholder
+6. Show RSVP section with all invitation members.
+7. For each member:
+ - attending yes/no
+ - meal preference if attending
+ - dietary restrictions if attending
+8. Show group logistics:
+ - arrival date
+ - departure date
+ - stay location
+ - coming from
+ - transport help
+ - message to the couple
+9. Show optional interaction sections:
+ - song requests
+ - toast/speech request
+ - advice/wish/memory
+ - one photo upload
+10. Before deadline, allow edits.
+11. After deadline, show submitted answers read-only and explain that changes require contacting the couple.
+
+Guest-facing localized content should include at least:
+
+- page headings
+- navigation/section labels
+- RSVP form labels
+- validation messages
+- success/error messages
+- deadline/read-only messages
+- upload instructions
+- optional interaction prompts
+
+Wedding factual content, such as venue names and addresses, can remain shared, but surrounding explanatory copy should be localized.
+
+## Admin portal plan
+
+Admin routes:
+
+```txt
+/admin
+/admin/guests
+/admin/guests/new
+/admin/guests/[id]
+```
+
+Although the domain model is `invitations`, the UI can still use guest-friendly labels like `Guests`.
+
+### `/admin`
+
+Dashboard cards:
+
+- Total invitations.
+- Total invited people.
+- RSVP pending.
+- Attending people.
+- Declined people.
+- Uploaded photos.
+- Song requests.
+- Pending toast requests.
+
+### `/admin/guests`
+
+Invitation list table:
+
+- display name
+- language
+- contact email
+- contact phone
+- members count
+- derived RSVP status
+- attending count
+- invite link
+- last updated
+
+Actions:
+
+- copy invite link
+- view details
+- edit
+
+### `/admin/guests/new`
+
+Create invitation form:
+
+- display name
+- language
+- contact email
+- contact phone
+- max party size
+- allow member edits
+- notes
+- initial members
+
+On submit:
+
+- generate token
+- create invitation
+- create initial members
+- show generated invite URL
+
+The language field is required.
+
+### `/admin/guests/[id]`
+
+Detail page:
+
+- invitation metadata
+- invite token/link
+- member list and per-person RSVP answers
+- group logistics
+- uploaded photo preview/link
+- song requests
+- toast requests with admin status controls
+- advice/wish/memory messages
+- admin notes
+
+Admin actions:
+
+- edit invitation
+- add/edit/remove members if needed
+- regenerate invite token
+- manually adjust RSVP/member data
+- accept/decline/schedule toast request
+
+## Authentication plan
+
+Use Google OAuth with an email allowlist.
+
+Implementation targets:
+
+```txt
+auth.ts
+app/api/auth/[...nextauth]/route.ts
+middleware.ts
+```
+
+Rules:
+
+- Only emails listed in `ADMIN_EMAILS` may access `/admin` and `/admin/*`.
+- Non-admin Google users are rejected or shown a forbidden page.
+- Guest invite pages do not require login.
+- Admin emails remain in `.env` for the bootstrap.
+
+Before implementation, verify the exact current Next.js/Auth.js setup for the installed framework version.
+
+## MinIO upload plan
+
+Use AWS SDK S3-compatible client.
+
+Upload flow:
+
+1. Guest selects one image.
+2. Client asks the app for a presigned upload URL.
+3. App validates invitation token and deadline.
+4. App creates an object key:
+
+```txt
+invitations/{invitationId}/{uuid}-{safeFilename}
+```
+
+5. Browser uploads directly to MinIO using presigned PUT.
+6. Client calls complete endpoint.
+7. App upserts `photo_uploads` metadata for the invitation.
+
+Validation:
+
+- Allow only image MIME types:
+ - `image/jpeg`
+ - `image/png`
+ - `image/webp`
+- Enforce a max size.
+- Enforce one current photo per invitation.
+
+Admin viewing:
+
+- Generate short-lived signed GET URLs for previews/downloads.
+
+## Docker Compose plan
+
+Services:
+
+```txt
+app
+postgres
+minio
+minio-init
+```
+
+Postgres:
+
+- expose `5432`
+- database: `wedding`
+- user: `wedding`
+- password: `wedding`
+
+MinIO:
+
+- API: `9000`
+- console: `9001`
+- bucket: `wedding-uploads`
+
+`minio-init`:
+
+- wait for MinIO
+- create bucket if it does not exist
+
+## Implementation sequence
+
+### Step 1: Review framework-specific docs
+
+Because this project uses Next.js 16, review the locally installed Next documentation before coding framework-specific pieces.
+
+Focus areas:
+
+- App Router route handlers.
+- Server actions/forms.
+- Middleware behavior.
+- Authentication integration compatibility.
+
+### Step 2: Add dependencies
+
+Install likely packages:
+
+```bash
+npm install kysely pg zod next-auth @aws-sdk/client-s3 @aws-sdk/s3-request-presigner nanoid
+npm install -D @types/pg kysely-codegen
+```
+
+Adjust if current package versions require different auth package names or setup.
+
+### Step 3: Add environment handling
+
+- Create `.env.example`.
+- Add typed config helper in `lib/config.ts`.
+- Validate required env vars at startup/server usage.
+
+### Step 4: Add Docker Compose
+
+- Add `docker-compose.yml`.
+- Define Postgres service.
+- Define MinIO service.
+- Define MinIO bucket initialization.
+- Optionally define app service.
+
+### Step 5: Add Kysely database client
+
+- Create `lib/db/client.ts`.
+- Create initial manual DB types in `lib/db/types.ts`.
+- Add SQL migration runner in `scripts/migrate-sql.mjs`.
+- Add package script:
+
+```json
+"db:migrate": "node scripts/migrate-sql.mjs"
+```
+
+### Step 6: Create initial SQL migration
+
+Create `migrations/0001_initial_schema.sql` for:
+
+- `invitations`
+- `invitation_members`
+- `invitation_responses`
+- `photo_uploads`
+- `song_requests`
+- `toast_requests`
+- `guest_messages`
+- optional `admin_audit_events`
+
+### Step 7: Add query modules
+
+Create query helpers for:
+
+- invitations
+- invitation members
+- invitation responses
+- uploads
+- song requests
+- toast requests
+- guest messages
+
+Keep SQL explicit and typed through Kysely.
+
+### Step 8: Add validation schemas
+
+Use Zod for:
+
+- creating/updating invitations
+- creating/updating members
+- submitting RSVP/member attendance
+- submitting logistics
+- song requests
+- toast requests
+- guest messages
+- upload presign/complete requests
+
+Invitation validation must require a supported language code.
+
+### Step 9: Implement Google admin auth
+
+- Configure Google provider.
+- Restrict sign-in to `ADMIN_EMAILS`.
+- Protect `/admin` routes.
+- Add sign-in/sign-out UI as needed.
+
+### Step 10: Build admin MVP
+
+Implement:
+
+- `/admin` dashboard.
+- `/admin/guests` invitation list.
+- `/admin/guests/new` create form.
+- `/admin/guests/[id]` detail page.
+
+Minimum admin operations:
+
+- create invitation
+- add members
+- copy invite link
+- view submitted RSVP data
+- view optional guest interactions
+
+### Step 11: Build guest invite MVP
+
+Implement `/invite/[token]` with:
+
+- invalid token handling
+- personalized greeting
+- language-aware guest-facing copy based on `invitations.language`
+- wedding info shell
+- countdown
+- member attendance form
+- per-member meal/dietary fields
+- group logistics form
+- submission/edit behavior until deadline
+
+### Step 12: Add optional guest interactions
+
+Add form sections for:
+
+- up to 3 song requests
+- optional toast/speech request
+- advice/wish/memory message
+
+Admin should be able to view all of them.
+
+### Step 13: Add MinIO photo upload
+
+Implement:
+
+- S3 client.
+- presigned PUT endpoint.
+- upload complete endpoint.
+- one-photo-per-invitation metadata upsert.
+- admin signed preview/download URL.
+
+### Step 14: Add basic design shell
+
+Replace default starter UI with a simple wedding-appropriate layout:
+
+- elegant typography
+- hero section
+- countdown
+- location/accommodation placeholders
+- story placeholder
+- clear guest forms
+- responsive mobile-first layout
+
+Final visual design can remain future work.
+
+### Step 14.5: Add localization foundation
+
+Add a simple localization layer for guest-facing invitation pages.
+
+Recommended bootstrap approach:
+
+- Keep supported language codes in config.
+- Store the selected language on each invitation.
+- Resolve guest-facing copy from `invitations.language` on `/invite/[token]`.
+- Create translation dictionaries for guest-facing copy.
+- Fallback to a default language only if necessary, but admin should always select a supported language.
+
+Suggested structure:
+
+```txt
+lib/
+ i18n/
+ config.ts
+ dictionaries/
+ en.ts
+ es.ts
+ de.ts
+```
+
+Do not rely only on browser language detection for invited guests. The invitation language is the source of truth.
+
+### Step 15: Verification
+
+Run:
+
+```bash
+npm run lint
+npm run build
+```
+
+Manual happy path:
+
+1. Start Docker Compose.
+2. Run migrations.
+3. Log in as admin with allowed Google account.
+4. Create an invitation with multiple members.
+5. Set the invitation language.
+6. Copy invite link.
+7. Open invite link.
+8. Confirm the page renders in the invitation's configured language.
+9. Submit attendance with different meal preferences per member.
+10. Add travel details.
+11. Add song request, toast request, and message.
+12. Upload one photo.
+13. Return to admin and confirm all data is visible.
+14. Test invalid invite token.
+15. Test non-admin access to `/admin`.
+16. Test deadline read-only behavior.
+
+## Open implementation questions
+
+These can be decided during implementation:
+
+1. Should guests be able to add/edit member names when `allow_member_edits = true`, or only fill unnamed placeholders?
+2. What should the exact RSVP deadline be?
+3. What max upload size should be allowed?
+4. Should admin be able to export guest/RSVP data as CSV in the first version?
+5. Which exact languages should be supported in the first version?
+
+## Recommended first milestone
+
+The first useful milestone should be:
+
+- Docker Compose with Postgres and MinIO.
+- Initial pure SQL migration executed by `scripts/migrate-sql.mjs`.
+- Google admin login restricted by `.env` emails.
+- Admin can create invitation with members.
+- Admin can select a language per invitation.
+- Guest can open `/invite/[token]` and submit per-member RSVP answers.
+- Guest invite page renders in the invitation's configured language.
+- Admin can see submitted answers.
+
+After that, add photo uploads and the fun interaction sections.
diff --git a/proxy.ts b/proxy.ts
new file mode 100644
index 0000000..23941cc
--- /dev/null
+++ b/proxy.ts
@@ -0,0 +1,5 @@
+export { auth as proxy } from "@/auth"
+
+export const config = {
+ matcher: ["/admin/:path*"],
+}
diff --git a/scripts/migrate-sql.mjs b/scripts/migrate-sql.mjs
new file mode 100644
index 0000000..f13e19a
--- /dev/null
+++ b/scripts/migrate-sql.mjs
@@ -0,0 +1,70 @@
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import { Client } from "pg";
+
+function loadEnvFileIfNeeded() {
+ if (process.env.DATABASE_URL) return;
+
+ const envPath = path.join(process.cwd(), ".env");
+ if (!fs.existsSync(envPath)) return;
+
+ const text = fs.readFileSync(envPath, "utf8");
+ for (const rawLine of text.split("\n")) {
+ const line = rawLine.trim();
+ if (!line || line.startsWith("#")) continue;
+ const idx = line.indexOf("=");
+ if (idx === -1) continue;
+
+ const key = line.slice(0, idx).trim();
+ const value = line.slice(idx + 1).trim();
+ if (!process.env[key]) process.env[key] = value;
+ }
+}
+
+const migrationsDir = path.join(
+ path.dirname(fileURLToPath(import.meta.url)),
+ "..",
+ "migrations",
+);
+
+function listSqlFiles(dir) {
+ if (!fs.existsSync(dir)) return [];
+
+ return fs
+ .readdirSync(dir)
+ .filter((file) => file.endsWith(".sql"))
+ .sort((a, b) => a.localeCompare(b));
+}
+
+async function main() {
+ loadEnvFileIfNeeded();
+
+ const url = process.env.DATABASE_URL;
+ if (!url) {
+ console.error("DATABASE_URL is not set. Add it to .env or your environment.");
+ process.exit(1);
+ }
+
+ const client = new Client({ connectionString: url });
+ await client.connect();
+
+ try {
+ const files = listSqlFiles(migrationsDir);
+ for (const file of files) {
+ const fullPath = path.join(migrationsDir, file);
+ const sql = fs.readFileSync(fullPath, "utf8");
+ console.log(`Applying ${fullPath}`);
+ await client.query(sql);
+ }
+ console.log("Migrations applied successfully");
+ } finally {
+ await client.end();
+ }
+}
+
+main().catch((error) => {
+ console.error("Migration failed.");
+ console.error(error);
+ process.exit(1);
+});
|