diff --git a/apps/editor/components/editor/index.tsx b/apps/editor/components/editor/index.tsx index ce675101..523f8a5e 100644 --- a/apps/editor/components/editor/index.tsx +++ b/apps/editor/components/editor/index.tsx @@ -7,6 +7,7 @@ import useEditor from '@/store/use-editor' import { ZoneSystem } from '../systems/zone/zone-system' import { ToolManager } from '../tools/tool-manager' import { ActionMenu } from '../ui/action-menu' +import { CloudSaveButton } from '../ui/cloud-save-button' import { PanelManager } from '../ui/panels/panel-manager' import { SidebarProvider } from '../ui/primitives/sidebar' import { AppSidebar } from '../ui/sidebar/app-sidebar' @@ -27,6 +28,7 @@ export default function Editor() {
+ diff --git a/apps/editor/components/ui/cloud-save-button.tsx b/apps/editor/components/ui/cloud-save-button.tsx new file mode 100644 index 00000000..5e7243be --- /dev/null +++ b/apps/editor/components/ui/cloud-save-button.tsx @@ -0,0 +1,55 @@ +'use client' + +import { Cloud } from 'lucide-react' +import { useState } from 'react' +import { useAuth } from '@/lib/auth/use-auth' +import { ProfileDropdown } from './profile-dropdown' +import { PropertyDropdown } from './property-dropdown' +import { SignInDialog } from './sign-in-dialog' + +/** + * CloudSaveButton - Shows authentication state and property management + * + * Not authenticated: Shows "Save to cloud" button + * Authenticated: Shows PropertyDropdown and ProfileDropdown + */ +export function CloudSaveButton() { + const { isAuthenticated, isLoading } = useAuth() + const [isSignInDialogOpen, setIsSignInDialogOpen] = useState(false) + + if (isLoading) { + return ( +
+
+
+
+
+ ) + } + + if (!isAuthenticated) { + return ( + <> +
+ +
+ + + ) + } + + return ( +
+
+ + +
+
+ ) +} diff --git a/apps/editor/components/ui/new-property-dialog.tsx b/apps/editor/components/ui/new-property-dialog.tsx new file mode 100644 index 00000000..999b8559 --- /dev/null +++ b/apps/editor/components/ui/new-property-dialog.tsx @@ -0,0 +1,132 @@ +'use client' + +import { X } from 'lucide-react' +import { useState } from 'react' +import { createProperty } from '@/lib/properties/actions' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from './primitives/dialog' + +interface NewPropertyDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + onSuccess?: () => void +} + +/** + * NewPropertyDialog - Dialog for creating a new property + * + * TODO: Add Google Maps address search integration + * TODO: Add address parsing and validation + * TODO: Add duplicate checking before creation + */ +export function NewPropertyDialog({ open, onOpenChange, onSuccess }: NewPropertyDialogProps) { + const [name, setName] = useState('') + const [isCreating, setIsCreating] = useState(false) + const [error, setError] = useState(null) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError(null) + setIsCreating(true) + + try { + // TODO: Replace with actual address data from Google Maps + const result = await createProperty({ + name, + center: [0, 0], // TODO: Get from Google Maps + city: '', + state: '', + postalCode: '', + country: 'US', + }) + + if (result.success) { + onOpenChange(false) + setName('') + onSuccess?.() + } else { + setError(result.error || 'Failed to create property') + } + } catch (err) { + setError(err instanceof Error ? err.message : 'An unexpected error occurred') + } finally { + setIsCreating(false) + } + } + + const handleClose = () => { + if (!isCreating) { + onOpenChange(false) + setName('') + setError(null) + } + } + + return ( + + + + Add New Property + + + +
+ {/* TODO: Add Google Maps address search component */} +
+ + setName(e.target.value)} + /> +
+ + {/* TODO: Add address fields with Google Maps autocomplete */} +
+

TODO: Google Maps address search will be integrated here

+

+ For now, property creation is not fully functional. This is a placeholder for the UI. +

+
+ + {error && ( +
+ {error} +
+ )} + +
+ + +
+
+
+
+ ) +} diff --git a/apps/editor/components/ui/profile-dropdown.tsx b/apps/editor/components/ui/profile-dropdown.tsx new file mode 100644 index 00000000..fffc5ed3 --- /dev/null +++ b/apps/editor/components/ui/profile-dropdown.tsx @@ -0,0 +1,58 @@ +'use client' + +import { useAuth } from '@/lib/auth/use-auth' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from './primitives/dropdown-menu' + +function getInitials(name: string): string { + return name + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase() + .slice(0, 2) +} + +/** + * ProfileDropdown - User profile menu with sign out + */ +export function ProfileDropdown() { + const { user, signOut } = useAuth() + + const handleSignOut = async () => { + await signOut() + // TODO: Show sign-in dialog or redirect + console.log('Signed out') + } + + const initials = user?.name ? getInitials(user.name) : user?.email?.[0]?.toUpperCase() || 'U' + + return ( + + + + + + {user?.name && ( +
+
{user.name}
+ {user.email &&
{user.email}
} +
+ )} + {user?.name && } + + Sign out + +
+
+ ) +} diff --git a/apps/editor/components/ui/property-dropdown.tsx b/apps/editor/components/ui/property-dropdown.tsx new file mode 100644 index 00000000..7287ec4b --- /dev/null +++ b/apps/editor/components/ui/property-dropdown.tsx @@ -0,0 +1,100 @@ +'use client' + +import { Check, ChevronDown, Home, Plus } from 'lucide-react' +import { useState } from 'react' +import { useActiveProperty, useProperties } from '@/lib/properties/use-properties' +import { cn } from '@/lib/utils' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from './primitives/dropdown-menu' +import { NewPropertyDialog } from './new-property-dialog' + +/** + * PropertyDropdown - Shows active property and allows switching between properties + */ +export function PropertyDropdown() { + const { properties, isLoading: propertiesLoading, refetch } = useProperties() + const { activeProperty, setActiveProperty, isPending } = useActiveProperty() + const [isNewPropertyDialogOpen, setIsNewPropertyDialogOpen] = useState(false) + + const handlePropertySelect = async (propertyId: string) => { + await setActiveProperty(propertyId) + } + + const handleAddNew = () => { + setIsNewPropertyDialogOpen(true) + } + + const handlePropertyCreated = () => { + refetch() + } + + return ( + <> + + + + + + {/* Property list */} + {properties.length > 0 ? ( +
+ {properties.map((property) => ( + + activeProperty?.id === property.id ? null : handlePropertySelect(property.id) + } + > +
+
{property.name}
+ {activeProperty?.id === property.id && ( + + )} +
+
+ ))} +
+ ) : ( +
+ No properties yet +
+ )} + + {/* Add new property option */} + + + Add new property + +
+
+ + + + ) +} diff --git a/apps/editor/components/ui/sign-in-dialog.tsx b/apps/editor/components/ui/sign-in-dialog.tsx new file mode 100644 index 00000000..e1c313e1 --- /dev/null +++ b/apps/editor/components/ui/sign-in-dialog.tsx @@ -0,0 +1,148 @@ +'use client' + +import { Mail, X } from 'lucide-react' +import { useState } from 'react' +import { authClient } from '@/lib/auth/auth-client' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from './primitives/dialog' + +interface SignInDialogProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +/** + * SignInDialog - Magic link authentication dialog + */ +export function SignInDialog({ open, onOpenChange }: SignInDialogProps) { + const [email, setEmail] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(false) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError(null) + setIsLoading(true) + + try { + // Use better-auth's magic link sign in + const result = await authClient.signIn.magicLink({ + email, + callbackURL: window.location.origin, + }) + + if (result.error) { + setError(result.error.message || 'Failed to send magic link') + } else { + setSuccess(true) + setEmail('') + } + } catch (err) { + setError(err instanceof Error ? err.message : 'An unexpected error occurred') + } finally { + setIsLoading(false) + } + } + + const handleClose = () => { + if (!isLoading) { + onOpenChange(false) + // Reset state after a short delay to avoid flash + setTimeout(() => { + setEmail('') + setError(null) + setSuccess(false) + }, 200) + } + } + + return ( + + + + Sign in to Pascal + + + + {success ? ( +
+
+
+ +
+
+

Check your email

+

+ We've sent a magic link to {email} +

+

+ Click the link in the email to sign in to your account. +

+
+
+ +
+ ) : ( +
+
+ + setEmail(e.target.value)} + /> +
+ + {error && ( +
+ {error} +
+ )} + + + +

+ We'll send you a magic link to sign in without a password. +

+
+ )} +
+
+ ) +} diff --git a/apps/editor/lib/auth/auth-client.ts b/apps/editor/lib/auth/auth-client.ts new file mode 100644 index 00000000..515dac5b --- /dev/null +++ b/apps/editor/lib/auth/auth-client.ts @@ -0,0 +1,65 @@ +/** + * Auth client for the editor using better-auth + * Connects to the Pascal monorepo backend + */ + +import { createAuthClient } from 'better-auth/react' +import { + customSessionClient, + magicLinkClient, + organizationClient, +} from 'better-auth/client/plugins' + +/** + * Get the backend API URL + * Default: http://localhost:3000 (monorepo backend) + */ +function getBackendURL(): string { + // Check if we have an env variable for the backend URL + if (typeof process !== 'undefined' && process.env.NEXT_PUBLIC_API_URL) { + return process.env.NEXT_PUBLIC_API_URL + } + + // In browser, try to use the current origin if it's the same host + if (typeof window !== 'undefined') { + // For local development, always use localhost:3000 (monorepo backend) + if (window.location.hostname === 'localhost') { + return 'http://localhost:3000' + } + // For production, use the same origin + return window.location.origin + } + + // SSR fallback + return 'http://localhost:3000' +} + +/** + * Auth client instance with better-auth + * Configured to work with the Pascal monorepo backend + */ +export const authClient = createAuthClient({ + baseURL: getBackendURL(), + plugins: [ + magicLinkClient(), + organizationClient(), + customSessionClient<{ + session: { + activePropertyId: string | null + activeOrganizationId: string | null + } + }>(), + ], +}) + +/** + * Export types for use in components + */ +export type AuthState = { + user: (typeof authClient)['$Infer']['Session']['user'] | null + session: (typeof authClient)['$Infer']['Session']['session'] | null + isLoading: boolean +} + +export type User = NonNullable +export type Session = NonNullable diff --git a/apps/editor/lib/auth/use-auth.ts b/apps/editor/lib/auth/use-auth.ts new file mode 100644 index 00000000..6c8fb110 --- /dev/null +++ b/apps/editor/lib/auth/use-auth.ts @@ -0,0 +1,20 @@ +'use client' + +import { authClient } from './auth-client' + +/** + * Hook to access authentication state using better-auth + * @returns Current auth state including user, session, and loading status + */ +export function useAuth() { + const session = authClient.useSession() + + return { + user: session.data?.user ?? null, + session: session.data?.session ?? null, + isAuthenticated: !!session.data?.user && !!session.data?.session, + isLoading: session.isPending, + signOut: () => authClient.signOut(), + signIn: authClient.signIn, + } +} diff --git a/apps/editor/lib/properties/actions.ts b/apps/editor/lib/properties/actions.ts new file mode 100644 index 00000000..ceba7d38 --- /dev/null +++ b/apps/editor/lib/properties/actions.ts @@ -0,0 +1,216 @@ +/** + * Property actions - API client for property management + * Makes HTTP requests to the Pascal monorepo backend + */ + +import type { CreatePropertyParams, Property } from './types' + +export type ActionResult = { + success: boolean + data?: T + error?: string + message?: string +} + +/** + * Get the backend API URL + */ +function getBackendURL(): string { + if (typeof process !== 'undefined' && process.env.NEXT_PUBLIC_API_URL) { + return process.env.NEXT_PUBLIC_API_URL + } + if (typeof window !== 'undefined' && window.location.hostname === 'localhost') { + return 'http://localhost:3000' + } + return typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000' +} + +/** + * Fetch all properties for the current user + */ +export async function getUserProperties(): Promise> { + try { + const response = await fetch(`${getBackendURL()}/api/properties`, { + method: 'GET', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }) + + if (!response.ok) { + const error = await response.text() + return { + success: false, + error: error || 'Failed to fetch properties', + data: [], + } + } + + const result = await response.json() + return { + success: true, + data: result.data || [], + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to fetch properties', + data: [], + } + } +} + +/** + * Get the active property for the current session + */ +export async function getActiveProperty(): Promise> { + try { + const response = await fetch(`${getBackendURL()}/api/properties/active`, { + method: 'GET', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }) + + if (!response.ok) { + const error = await response.text() + return { + success: false, + error: error || 'Failed to fetch active property', + data: null, + } + } + + const result = await response.json() + return { + success: true, + data: result.data || null, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to fetch active property', + data: null, + } + } +} + +/** + * Set the active property for the current session + */ +export async function setActiveProperty(propertyId: string | null): Promise { + try { + const response = await fetch(`${getBackendURL()}/api/properties/active`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ propertyId }), + }) + + if (!response.ok) { + const error = await response.text() + return { + success: false, + error: error || 'Failed to set active property', + } + } + + const result = await response.json() + return { + success: true, + message: result.message || (propertyId ? 'Active property updated' : 'Active property cleared'), + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to set active property', + } + } +} + +/** + * Check if a property with the given address already exists + */ +export async function checkPropertyDuplicate(params: { + streetNumber?: string + route?: string + city?: string + state?: string + postalCode?: string +}): Promise< + ActionResult<{ + isDuplicate: boolean + isUserProperty?: boolean + existingProperty?: Property + }> +> { + try { + const response = await fetch(`${getBackendURL()}/api/properties/check-duplicate`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }) + + if (!response.ok) { + const error = await response.text() + return { + success: false, + error: error || 'Failed to check for duplicates', + } + } + + const result = await response.json() + return { + success: true, + data: result.data || { isDuplicate: false }, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to check for duplicates', + } + } +} + +/** + * Create a new property + */ +export async function createProperty(params: CreatePropertyParams): Promise> { + try { + const response = await fetch(`${getBackendURL()}/api/properties`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }) + + if (!response.ok) { + const error = await response.text() + return { + success: false, + error: error || 'Failed to create property', + } + } + + const result = await response.json() + return { + success: true, + data: result.data, + message: result.message || 'Property created successfully', + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to create property', + } + } +} diff --git a/apps/editor/lib/properties/types.ts b/apps/editor/lib/properties/types.ts new file mode 100644 index 00000000..4d6721b8 --- /dev/null +++ b/apps/editor/lib/properties/types.ts @@ -0,0 +1,43 @@ +/** + * Property-related type definitions + * Isolated from monorepo database schema + */ + +export type Property = { + id: string + name: string + ownerId: string + organizationId: string | null + addressId: string + createdAt: Date + updatedAt: Date + address: { + id: string + streetNumber?: string + route?: string + city?: string + state?: string + postalCode?: string + country?: string + latitude?: string + longitude?: string + } +} + +export type CreatePropertyParams = { + name: string + center: [number, number] + streetNumber?: string + route?: string + routeShort?: string + neighborhood?: string + city?: string + county?: string + state?: string + stateLong?: string + postalCode?: string + postalCodeSuffix?: string + country?: string + countryLong?: string + rawJson?: Record +} diff --git a/apps/editor/lib/properties/use-properties.ts b/apps/editor/lib/properties/use-properties.ts new file mode 100644 index 00000000..49c8d77f --- /dev/null +++ b/apps/editor/lib/properties/use-properties.ts @@ -0,0 +1,198 @@ +'use client' + +import { useCallback, useEffect, useState } from 'react' +import { + getActiveProperty, + getUserProperties, + setActiveProperty as setActivePropertyAction, +} from './actions' +import type { Property } from './types' + +interface UsePropertiesReturn { + properties: Property[] + isLoading: boolean + error: string | null + refetch: () => Promise +} + +/** + * Hook to fetch and manage user properties + */ +export function useProperties(): UsePropertiesReturn { + const [properties, setProperties] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + const fetchProperties = useCallback(async () => { + try { + setIsLoading(true) + setError(null) + + const result = await getUserProperties() + + if (result.success) { + setProperties(result.data || []) + } else { + setError(result.error || 'Failed to fetch properties') + setProperties([]) + } + } catch (err) { + setError(err instanceof Error ? err.message : 'An unexpected error occurred') + setProperties([]) + } finally { + setIsLoading(false) + } + }, []) + + useEffect(() => { + fetchProperties() + }, [fetchProperties]) + + return { + properties, + isLoading, + error, + refetch: fetchProperties, + } +} + +/** + * Hook to fetch a single property by ID + */ +export function useProperty(propertyId: string | undefined) { + const [property, setProperty] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + if (!propertyId) { + setProperty(null) + setIsLoading(false) + return + } + + const fetchProperty = async () => { + try { + setIsLoading(true) + setError(null) + + const result = await getUserProperties() + + if (result.success) { + const found = result.data?.find((p) => p.id === propertyId) + if (found) { + setProperty(found) + } else { + setError('Property not found') + setProperty(null) + } + } else { + setError(result.error || 'Failed to fetch property') + setProperty(null) + } + } catch (err) { + setError(err instanceof Error ? err.message : 'An unexpected error occurred') + setProperty(null) + } finally { + setIsLoading(false) + } + } + + fetchProperty() + }, [propertyId]) + + return { + property, + isLoading, + error, + } +} + +/** + * Hook to manage the active property for the current session + */ +export function useActiveProperty() { + const [activeProperty, setActivePropertyState] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + const [isPending, setIsPending] = useState(false) + + const fetchActiveProperty = useCallback(async () => { + try { + setIsLoading(true) + setError(null) + + const result = await getActiveProperty() + + if (result.success) { + setActivePropertyState(result.data || null) + + // If no active property is set, automatically set the first property as active + if (!result.data) { + const propertiesResult = await getUserProperties() + + if ( + propertiesResult.success && + propertiesResult.data && + propertiesResult.data.length > 0 + ) { + const firstProperty = propertiesResult.data[0] + if (firstProperty) { + const setActiveResult = await setActivePropertyAction(firstProperty.id) + + if (setActiveResult.success) { + setActivePropertyState(firstProperty) + } + } + } + } + } else { + setError(result.error || 'Failed to fetch active property') + setActivePropertyState(null) + } + } catch (err) { + setError(err instanceof Error ? err.message : 'An unexpected error occurred') + setActivePropertyState(null) + } finally { + setIsLoading(false) + } + }, []) + + const changeActiveProperty = useCallback( + async (propertyId: string | null) => { + try { + setIsPending(true) + const result = await setActivePropertyAction(propertyId) + + if (result.success) { + // Fetch the updated active property + if (propertyId) { + await fetchActiveProperty() + } else { + setActivePropertyState(null) + } + } else { + console.error(result.error || 'Failed to update active property') + } + } catch (err) { + console.error(err instanceof Error ? err.message : 'An unexpected error occurred') + } finally { + setIsPending(false) + } + }, + [fetchActiveProperty], + ) + + useEffect(() => { + fetchActiveProperty() + }, [fetchActiveProperty]) + + return { + activeProperty, + isLoading, + error, + setActiveProperty: changeActiveProperty, + isPending, + refetch: fetchActiveProperty, + } +} diff --git a/apps/editor/package.json b/apps/editor/package.json index 922ff855..63f00f2b 100644 --- a/apps/editor/package.json +++ b/apps/editor/package.json @@ -4,7 +4,7 @@ "type": "module", "private": true, "scripts": { - "dev": "next dev --port 3000", + "dev": "next dev --port 3002", "build": "next build", "start": "next start", "lint": "biome lint", @@ -29,6 +29,7 @@ "@repo/ui": "*", "@tailwindcss/postcss": "^4.1.18", "@types/three": "^0.182.0", + "better-auth": "^1.4.18", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.562.0", diff --git a/bun.lock b/bun.lock index 5ed59f8c..67c49783 100644 --- a/bun.lock +++ b/bun.lock @@ -40,6 +40,7 @@ "@repo/ui": "*", "@tailwindcss/postcss": "^4.1.18", "@types/three": "^0.182.0", + "better-auth": "^1.4.18", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.562.0", @@ -151,6 +152,14 @@ "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + "@better-auth/core": ["@better-auth/core@1.4.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.8", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-q+awYgC7nkLEBdx2sW0iJjkzgSHlIxGnOpsN1r/O1+a4m7osJNHtfK2mKJSL1I+GfNyIlxJF8WvD/NLuYMpmcg=="], + + "@better-auth/telemetry": ["@better-auth/telemetry@1.4.18", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.18" } }, "sha512-e5rDF8S4j3Um/0LIVATL2in9dL4lfO2fr2v1Wio4qTMRbfxqnUDTa+6SZtwdeJrbc4O+a3c+IyIpjG9Q/6GpfQ=="], + + "@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], + + "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], + "@biomejs/biome": ["@biomejs/biome@2.3.13", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.13", "@biomejs/cli-darwin-x64": "2.3.13", "@biomejs/cli-linux-arm64": "2.3.13", "@biomejs/cli-linux-arm64-musl": "2.3.13", "@biomejs/cli-linux-x64": "2.3.13", "@biomejs/cli-linux-x64-musl": "2.3.13", "@biomejs/cli-win32-arm64": "2.3.13", "@biomejs/cli-win32-x64": "2.3.13" }, "bin": { "biome": "bin/biome" } }, "sha512-Fw7UsV0UAtWIBIm0M7g5CRerpu1eKyKAXIazzxhbXYUyMkwNrkX/KLkGI7b+uVDQ5cLUMfOC9vR60q9IDYDstA=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0OCwP0/BoKzyJHnFdaTk/i7hIP9JHH9oJJq6hrSCPmJPo8JWcJhprK4gQlhFzrwdTBAW4Bjt/RmCf3ZZe59gwQ=="], @@ -299,6 +308,10 @@ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Tr0j94MphimCCks+1rtYPzQFK+faJuhHWCegU9S9gDlgyOk8Y3kPmO64UcjyzZAlligeBtYZ/2bEyrKq0d2wqQ=="], + "@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], + + "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], @@ -411,6 +424,8 @@ "@repo/ui": ["@repo/ui@workspace:packages/ui"], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], @@ -531,6 +546,10 @@ "baseline-browser-mapping": ["baseline-browser-mapping@2.9.14", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg=="], + "better-auth": ["better-auth@1.4.18", "", { "dependencies": { "@better-auth/core": "1.4.18", "@better-auth/telemetry": "1.4.18", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-bnyifLWBPcYVltH3RhS7CM62MoelEqC6Q+GnZwfiDWNfepXoQZBjEvn4urcERC7NTKgKq5zNBM8rvPvRBa6xcg=="], + + "better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="], + "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], @@ -595,6 +614,8 @@ "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + "detect-gpu": ["detect-gpu@5.0.70", "", { "dependencies": { "webgl-constants": "^1.1.1" } }, "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -805,6 +826,8 @@ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], @@ -821,6 +844,8 @@ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "kysely": ["kysely@0.28.11", "", {}, "sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg=="], + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], @@ -889,6 +914,8 @@ "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], + "nanostores": ["nanostores@1.1.0", "", {}, "sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA=="], + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], "next": ["next@16.1.0", "", { "dependencies": { "@next/env": "16.1.0", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.0", "@next/swc-darwin-x64": "16.1.0", "@next/swc-linux-arm64-gnu": "16.1.0", "@next/swc-linux-arm64-musl": "16.1.0", "@next/swc-linux-x64-gnu": "16.1.0", "@next/swc-linux-x64-musl": "16.1.0", "@next/swc-win32-arm64-msvc": "16.1.0", "@next/swc-win32-x64-msvc": "16.1.0", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-Y+KbmDbefYtHDDQKLNrmzE/YYzG2msqo2VXhzh5yrJ54tx/6TmGdkR5+kP9ma7i7LwZpZMfoY3m/AoPPPKxtVw=="], @@ -975,6 +1002,8 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], @@ -987,6 +1016,8 @@ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],