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 (
+
+ )
+}
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 (
+
+ )
+}
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=="],