diff --git a/apps/web/src/actions/procedures/index.ts b/apps/web/src/actions/procedures/index.ts index c999072e3..70c25bc8e 100644 --- a/apps/web/src/actions/procedures/index.ts +++ b/apps/web/src/actions/procedures/index.ts @@ -9,7 +9,7 @@ import { createServerActionProcedure } from 'zsa' * Docs: https://zsa.vercel.app/docs/procedures */ export const authProcedure = createServerActionProcedure().handler(async () => { - const data = (await getCurrentUser()).unwrap() + const data = await getCurrentUser() return { session: data.session!, workspace: data.workspace, user: data.user } }) diff --git a/apps/web/src/app/(private)/_components/CommitDetailPage/BreadcrumpBadge/index.tsx b/apps/web/src/app/(private)/_components/CommitDetailPage/BreadcrumpBadge/index.tsx new file mode 100644 index 000000000..a92e97daf --- /dev/null +++ b/apps/web/src/app/(private)/_components/CommitDetailPage/BreadcrumpBadge/index.tsx @@ -0,0 +1,41 @@ +'use client' + +import { useCallback } from 'react' + +import { HEAD_COMMIT } from '@latitude-data/core/browser' +import { Badge, Icons, Text, Tooltip } from '@latitude-data/web-ui' + +export default function BreadcrumpCommit({ + uuid: fullUuid, + title, + isLatest, +}: { + uuid: string + title?: string | null + isLatest: boolean +}) { + const [uuid] = isLatest ? [HEAD_COMMIT] : fullUuid.split('-') + const onCopy = useCallback(() => { + navigator.clipboard.writeText(fullUuid) + }, [fullUuid]) + return ( +
+ {title ?? 'Untitled'} + + {uuid} + +
+ } + > + Click to copy: {fullUuid} + + + ) +} diff --git a/apps/web/src/app/(private)/_components/CommitDetailPage/ClientPushNavigation/index.ts b/apps/web/src/app/(private)/_components/CommitDetailPage/ClientPushNavigation/index.ts new file mode 100644 index 000000000..9da7cdcbf --- /dev/null +++ b/apps/web/src/app/(private)/_components/CommitDetailPage/ClientPushNavigation/index.ts @@ -0,0 +1,21 @@ +'use client' + +import { useEffect } from 'react' + +export default function ClientPushNavigation({ + url, +}: { + url: string | undefined +}) { + useEffect(() => { + if (!url) return + + window.history.replaceState( + { ...window.history.state, as: url, url }, + '', + url, + ) + }, [url]) + + return null +} diff --git a/apps/web/src/app/(private)/_components/CommitDetailPage/index.tsx b/apps/web/src/app/(private)/_components/CommitDetailPage/index.tsx new file mode 100644 index 000000000..13568e9fa --- /dev/null +++ b/apps/web/src/app/(private)/_components/CommitDetailPage/index.tsx @@ -0,0 +1,46 @@ +import { type Commit, type Project } from '@latitude-data/core' +import { CommitDetail } from '@latitude-data/web-ui' +import BreadcrumpCommit from '$/app/(private)/_components/CommitDetailPage/BreadcrumpBadge' +import ClientPushNavigation from '$/app/(private)/_components/CommitDetailPage/ClientPushNavigation' +import { NAV_LINKS } from '$/app/(private)/_lib/constants' +import { SessionData } from '$/services/auth/getCurrentUser' + +type Props = { + session: SessionData + project: Project + commit: Commit + isLatest?: boolean + url?: string +} +export default function CommitDetailPage({ + session, + project, + commit, + url, + isLatest = true, +}: Props) { + return ( + <> + + + ), + }, + ], + }} + /> + + ) +} diff --git a/apps/web/src/app/(private)/_data-access/index.ts b/apps/web/src/app/(private)/_data-access/index.ts new file mode 100644 index 000000000..4db2d4ff6 --- /dev/null +++ b/apps/web/src/app/(private)/_data-access/index.ts @@ -0,0 +1,36 @@ +import { cache } from 'react' + +import { + findCommit as originalfindCommit, + findProject as originalFindProject, + getFirstProject as originalGetFirstProject, + type FindCommitProps, + type FindProjectProps, +} from '@latitude-data/core' + +export const getFirstProject = cache( + async ({ workspaceId }: { workspaceId: number }) => { + const result = await originalGetFirstProject({ workspaceId }) + const project = result.unwrap() + + return project + }, +) + +export const findProject = cache( + async ({ projectId, workspaceId }: FindProjectProps) => { + const result = await originalFindProject({ projectId, workspaceId }) + const project = result.unwrap() + + return project + }, +) + +export const findCommit = cache( + async ({ isLatest, uuid, projectId }: FindCommitProps) => { + const result = await originalfindCommit({ isLatest, uuid, projectId }) + const commit = result.unwrap() + + return commit + }, +) diff --git a/apps/web/src/app/(private)/_lib/constants.ts b/apps/web/src/app/(private)/_lib/constants.ts new file mode 100644 index 000000000..d971b4f2f --- /dev/null +++ b/apps/web/src/app/(private)/_lib/constants.ts @@ -0,0 +1,12 @@ +import { + LATITUDE_CHANGELOG_URL, + LATITUDE_DOCS_URL, + LATITUDE_HELP_URL, +} from '@latitude-data/core/browser' + +export const NAV_LINKS = [ + { label: 'Feedback', href: '' }, + { label: 'Docs', href: LATITUDE_DOCS_URL }, + { label: 'Help', href: LATITUDE_HELP_URL }, + { label: 'Changelog', href: LATITUDE_CHANGELOG_URL }, +] diff --git a/apps/web/src/app/(private)/error.tsx b/apps/web/src/app/(private)/error.tsx new file mode 100644 index 000000000..a58d89aaf --- /dev/null +++ b/apps/web/src/app/(private)/error.tsx @@ -0,0 +1,34 @@ +'use client' + +import { useEffect } from 'react' + +import { + AppLayout, + ErrorComponent, + useSession, +} from '@latitude-data/web-ui/browser' +import { NAV_LINKS } from '$/app/(private)/_lib/constants' + +export default function Error({ + error, +}: { + error: Error & { digest?: string } + reset: () => void // Re-render of page +}) { + const session = useSession() + useEffect(() => { + console.error(error) + }, [error]) + return ( + + + + ) +} diff --git a/apps/web/src/app/(private)/layout.tsx b/apps/web/src/app/(private)/layout.tsx index 709e988ba..1887322ed 100644 --- a/apps/web/src/app/(private)/layout.tsx +++ b/apps/web/src/app/(private)/layout.tsx @@ -1,25 +1,15 @@ import { ReactNode } from 'react' -import Sidebar from '$/components/Sidebar' -import { getSession } from '$/services/auth/getSession' -import { ROUTES } from '$/services/routes' -import { redirect } from 'next/navigation' +import { SessionProvider } from '@latitude-data/web-ui/browser' +import { getCurrentUser } from '$/services/auth/getCurrentUser' export default async function PrivateLayout({ children, -}: { - children: ReactNode -}) { - const data = await getSession() - if (!data.session) { - return redirect(ROUTES.auth.login) - } +}: Readonly<{ children: ReactNode }>) { + const session = await getCurrentUser() return ( -
-
- -
-
{children}
-
+ + {children} + ) } diff --git a/apps/web/src/app/(private)/page.tsx b/apps/web/src/app/(private)/page.tsx index 8fc6009c5..128c0eeb0 100644 --- a/apps/web/src/app/(private)/page.tsx +++ b/apps/web/src/app/(private)/page.tsx @@ -1,21 +1,38 @@ -import { FocusHeader, FocusLayout } from '@latitude-data/web-ui' -import DummyLogoutButton from '$/app/(private)/_components/DummyLogoutButton' +import { Commit, NotFoundError, Project } from '@latitude-data/core' +import CommitDetailPage from '$/app/(private)/_components/CommitDetailPage' +import { findCommit, getFirstProject } from '$/app/(private)/_data-access' +import { getCurrentUser, SessionData } from '$/services/auth/getCurrentUser' +import { ROUTES } from '$/services/routes' +import { notFound } from 'next/navigation' export const dynamic = 'force-dynamic' +const PROJECT_ROUTE = ROUTES.projects.detail + +export default async function AppRoot() { + let session: SessionData + let project: Project + let commit: Commit + let url + + try { + session = await getCurrentUser() + project = await getFirstProject({ workspaceId: session.workspace.id }) + commit = await findCommit({ isLatest: true, projectId: project.id }) + url = PROJECT_ROUTE({ id: project.id }).commits.latest + } catch (error) { + if (error instanceof NotFoundError) { + return notFound() + } + + throw error + } -export default async function Home() { return ( - - } - > -
- -
-
+ ) } diff --git a/apps/web/src/app/(private)/projects/[id]/page.tsx b/apps/web/src/app/(private)/projects/[id]/page.tsx new file mode 100644 index 000000000..81f0b52d9 --- /dev/null +++ b/apps/web/src/app/(private)/projects/[id]/page.tsx @@ -0,0 +1,43 @@ +import { Commit, NotFoundError, Project } from '@latitude-data/core' +import CommitDetailPage from '$/app/(private)/_components/CommitDetailPage' +import { findCommit, findProject } from '$/app/(private)/_data-access' +import { getCurrentUser, SessionData } from '$/services/auth/getCurrentUser' +import { ROUTES } from '$/services/routes' +import { notFound } from 'next/navigation' + +export const dynamic = 'force-dynamic' +const PROJECT_ROUTE = ROUTES.projects.detail + +export type ProjectPageParams = { + params: { id: string } +} +export default async function ProjectPage({ params }: ProjectPageParams) { + let session: SessionData + let project: Project + let commit: Commit + let url + + try { + session = await getCurrentUser() + project = await findProject({ + projectId: params.id, + workspaceId: session.workspace.id, + }) + commit = await findCommit({ isLatest: true, projectId: project.id }) + url = PROJECT_ROUTE({ id: +project.id }).commits.latest + } catch (error) { + if (error instanceof NotFoundError) { + return notFound() + } + throw error + } + + return ( + + ) +} diff --git a/apps/web/src/app/(private)/projects/[id]/versions/[uuid]/page.tsx b/apps/web/src/app/(private)/projects/[id]/versions/[uuid]/page.tsx new file mode 100644 index 000000000..c351e5ea4 --- /dev/null +++ b/apps/web/src/app/(private)/projects/[id]/versions/[uuid]/page.tsx @@ -0,0 +1,49 @@ +import { + Commit, + HEAD_COMMIT, + NotFoundError, + Project, +} from '@latitude-data/core' +import CommitDetailPage from '$/app/(private)/_components/CommitDetailPage' +import { findCommit, findProject } from '$/app/(private)/_data-access' +import { ProjectPageParams } from '$/app/(private)/projects/[id]/page' +import { getCurrentUser, SessionData } from '$/services/auth/getCurrentUser' +import { notFound } from 'next/navigation' + +export const dynamic = 'force-dynamic' + +export type CommitPageParams = { + params: ProjectPageParams['params'] & { uuid: string } +} +export default async function CommitRoot({ params }: CommitPageParams) { + const isLatest = params.uuid === HEAD_COMMIT + let session: SessionData + let project: Project + let commit: Commit + try { + session = await getCurrentUser() + project = await findProject({ + projectId: params.id, + workspaceId: session.workspace.id, + }) + commit = await findCommit({ + isLatest, + uuid: params.uuid, + projectId: project.id, + }) + } catch (error) { + if (error instanceof NotFoundError) { + return notFound() + } + throw error + } + + return ( + + ) +} diff --git a/apps/web/src/app/(private)/projects/[id]/versions/page.tsx b/apps/web/src/app/(private)/projects/[id]/versions/page.tsx new file mode 100644 index 000000000..f8858e89f --- /dev/null +++ b/apps/web/src/app/(private)/projects/[id]/versions/page.tsx @@ -0,0 +1,5 @@ +import ProjectPage from '$/app/(private)/projects/[id]/page' + +export const dynamic = 'force-dynamic' + +export default ProjectPage diff --git a/apps/web/src/app/(private)/projects/page.tsx b/apps/web/src/app/(private)/projects/page.tsx new file mode 100644 index 000000000..7fe055c5f --- /dev/null +++ b/apps/web/src/app/(private)/projects/page.tsx @@ -0,0 +1,5 @@ +import AppRoot from '$/app/(private)/page' + +export const dynamic = 'force-dynamic' + +export default AppRoot diff --git a/apps/web/src/app/(public)/login/page.tsx b/apps/web/src/app/(public)/login/page.tsx index d2eb8db18..6bc9f334f 100644 --- a/apps/web/src/app/(public)/login/page.tsx +++ b/apps/web/src/app/(public)/login/page.tsx @@ -1,9 +1,5 @@ -import { - Card, - CardContent, - FocusHeader, - FocusLayout, -} from '@latitude-data/web-ui' +import { Card, CardContent, FocusHeader } from '@latitude-data/web-ui' +import { FocusLayout } from '@latitude-data/web-ui/browser' import AuthFooter from '$/app/(public)/_components/Footer' import { isWorkspaceCreated } from '$/data-access' import { ROUTES } from '$/services/routes' diff --git a/apps/web/src/app/(public)/setup/page.tsx b/apps/web/src/app/(public)/setup/page.tsx index 5f42f6aeb..7004ae9c7 100644 --- a/apps/web/src/app/(public)/setup/page.tsx +++ b/apps/web/src/app/(public)/setup/page.tsx @@ -1,9 +1,5 @@ -import { - Card, - CardContent, - FocusHeader, - FocusLayout, -} from '@latitude-data/web-ui' +import { Card, CardContent, FocusHeader } from '@latitude-data/web-ui' +import { FocusLayout } from '@latitude-data/web-ui/browser' import AuthFooter from '$/app/(public)/_components/Footer' import SetupForm from './SetupForm' diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 8ff171dcb..bf7bc25c8 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -4,7 +4,7 @@ import type { Metadata } from 'next' import '@latitude-data/web-ui/styles.css' -import { ToastProvider } from '@latitude-data/web-ui' +import { ToastProvider, TooltipProvider } from '@latitude-data/web-ui' import localFont from 'next/font/local' const fontSans = localFont({ @@ -157,7 +157,7 @@ export default function RootLayout({ - {children} + {children} diff --git a/apps/web/src/app/not-found.tsx b/apps/web/src/app/not-found.tsx new file mode 100644 index 000000000..183d5d224 --- /dev/null +++ b/apps/web/src/app/not-found.tsx @@ -0,0 +1,19 @@ +import { AppLayout, ErrorComponent } from '@latitude-data/web-ui/browser' +import { NAV_LINKS } from '$/app/(private)/_lib/constants' +import { getSafeCurrentUser } from '$/services/auth/getCurrentUser' + +export default async function GlobalNoFound() { + const session = await getSafeCurrentUser() + return ( + + + + ) +} diff --git a/apps/web/src/data-access/users.ts b/apps/web/src/data-access/users.ts index 70b05bbee..a529b2756 100644 --- a/apps/web/src/data-access/users.ts +++ b/apps/web/src/data-access/users.ts @@ -1,8 +1,8 @@ import { database, getUser, + NotFoundError, Result, - UserNotFoundError, users, verifyPassword, type PromisedResult, @@ -11,13 +11,15 @@ import { import { getWorkspace } from '$/data-access/workspaces' import { eq } from 'drizzle-orm' +const NOT_FOUND_MSG = 'Not found user' + export async function getUserFromCredentials({ email, password, }: { email: string password: string -}): PromisedResult { +}): PromisedResult { const user = await database.query.users.findFirst({ columns: { id: true, @@ -29,13 +31,13 @@ export async function getUserFromCredentials({ }) if (!user) { - return Result.error(new UserNotFoundError()) + return Result.error(new NotFoundError(NOT_FOUND_MSG)) } const validPassword = await verifyPassword(password, user.encryptedPassword) if (!validPassword) { - Result.error(new UserNotFoundError()) + Result.error(new NotFoundError(NOT_FOUND_MSG)) } const wpResult = await getWorkspace({ userId: user.id }) @@ -43,7 +45,7 @@ export async function getUserFromCredentials({ return Result.error(wpResult.error) } - const workspace = wpResult.value + const workspace = wpResult.value! return Result.ok({ user: { id: user.id, @@ -58,18 +60,19 @@ export async function getCurrentUserFromDB({ userId, }: { userId: string | undefined -}): PromisedResult { +}): PromisedResult { const user = await getUser(userId) if (!user) { - return Result.error(new UserNotFoundError()) + return Result.error(new NotFoundError(NOT_FOUND_MSG)) } const wpResult = await getWorkspace({ userId: user.id }) + if (wpResult.error) { - return Result.error(wpResult.error) + Result.error(new NotFoundError(NOT_FOUND_MSG)) } - const workspace = wpResult.value + const workspace = wpResult.value! return Result.ok({ user: { id: user.id, diff --git a/apps/web/src/data-access/workspaces.ts b/apps/web/src/data-access/workspaces.ts index bf9660df9..70c71f63d 100644 --- a/apps/web/src/data-access/workspaces.ts +++ b/apps/web/src/data-access/workspaces.ts @@ -1,4 +1,9 @@ -import { database, Result, workspaces } from '@latitude-data/core' +import { + database, + NotFoundError, + Result, + workspaces, +} from '@latitude-data/core' import { eq } from 'drizzle-orm' export async function getWorkspace({ userId }: { userId: string }) { @@ -7,7 +12,7 @@ export async function getWorkspace({ userId }: { userId: string }) { }) if (!workspace) { - return Result.error(new Error('Workspace not found')) + return Result.error(new NotFoundError('Workspace not found')) } return Result.ok(workspace) diff --git a/apps/web/src/helpers/apiHandler.ts b/apps/web/src/helpers/apiHandler.ts deleted file mode 100644 index 59433aedb..000000000 --- a/apps/web/src/helpers/apiHandler.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -export default function apiHandler( - handler: (req: NextRequest, params?: any) => Promise>, -) { - return async (req: NextRequest) => { - try { - handler(req) - } catch (error: any) { - console.error(error) - - return NextResponse.json( - { error: error.message || 'Internal Server Error' }, - { status: error.statusCode || 500 }, - ) - } - } -} diff --git a/apps/web/src/services/auth/getCurrentUser.ts b/apps/web/src/services/auth/getCurrentUser.ts index 76b0d8a97..0aca18847 100644 --- a/apps/web/src/services/auth/getCurrentUser.ts +++ b/apps/web/src/services/auth/getCurrentUser.ts @@ -1,18 +1,42 @@ import { cache } from 'react' -import { Result } from '@latitude-data/core' +import { SafeUser } from '@latitude-data/core' import { getCurrentUserFromDB } from '$/data-access' +import { Session } from 'lucia' import { getSession } from './getSession' +export type SessionData = { + session: Session + user: SafeUser + workspace: { + id: number + name: string + } +} export const getCurrentUser = cache(async () => { const sessionData = await getSession() const result = await getCurrentUserFromDB({ userId: sessionData?.user?.id }) - if (result.error) return result + const { user, workspace } = result.unwrap() - return Result.ok({ + return { session: sessionData.session!, - user: result.value.user, - workspace: result.value.workspace, - }) + user, + workspace, + } +}) + +export const getSafeCurrentUser = cache(async () => { + const sessionData = await getSession() + const result = await getCurrentUserFromDB({ userId: sessionData?.user?.id }) + + if (result.error) return null + + const { user, workspace } = result.value + + return { + session: sessionData.session!, + user, + workspace, + } }) diff --git a/apps/web/src/services/auth/index.ts b/apps/web/src/services/auth/index.ts index 41e8cecf1..5db1fb04b 100644 --- a/apps/web/src/services/auth/index.ts +++ b/apps/web/src/services/auth/index.ts @@ -12,6 +12,14 @@ interface DatabaseSessionAttributes { currentWorkspaceId: number } +declare module 'lucia' { + interface Register { + Lucia: typeof lucia + DatabaseUserAttributes: DatabaseUserAttributes + DatabaseSessionAttributes: DatabaseSessionAttributes + } +} + export const lucia = new Lucia(adapter, { sessionCookie: { expires: false, @@ -24,12 +32,9 @@ export const lucia = new Lucia(adapter, { email: attributes.email, } }, + getSessionAttributes: (attributes) => { + return { + currentWorkspaceId: attributes.currentWorkspaceId, + } + }, }) - -declare module 'lucia' { - interface Register { - Lucia: typeof lucia - DatabaseUserAttributes: DatabaseUserAttributes - DatabaseSessionAttributes: DatabaseSessionAttributes - } -} diff --git a/apps/web/src/services/routes.ts b/apps/web/src/services/routes.ts index dcf1df143..35af9e12a 100644 --- a/apps/web/src/services/routes.ts +++ b/apps/web/src/services/routes.ts @@ -1,5 +1,25 @@ +import { HEAD_COMMIT } from '@latitude-data/core' + +const ROOT_PATH = '/' +const PROJECTS_PATH = `${ROOT_PATH}projects` + export const ROUTES = { - root: '/', + root: ROOT_PATH, + projects: { + root: PROJECTS_PATH, + detail: ({ id }: { id: number }) => { + const root = `${PROJECTS_PATH}/${id}` + const rootCommits = `${root}/versions` + return { + root, + commits: { + root: rootCommits, + latest: `${rootCommits}/${HEAD_COMMIT}`, + detail: ({ id }: { id: number }) => `${rootCommits}/${id}`, + }, + } + }, + }, auth: { setup: '/setup', login: '/login', diff --git a/package.json b/package.json index 725c0b02a..397a652b9 100644 --- a/package.json +++ b/package.json @@ -30,5 +30,17 @@ "dependencies": { "drizzle-orm": "0.31.4", "pg": "8.12.0" + }, + "pnpm": { + "peerDependencyRules": { + "allowedVersions": { + "react": "18.x", + "react-dom": "18.x", + "react": "19.x", + "react-dom": "19.x", + "eslint": "8.x", + "eslint": "9.x" + } + } } } diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index 67402396c..1803e1b24 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -1,8 +1,10 @@ export const LATITUDE_DOCS_URL = '' export const LATITUDE_EMAIL = '' +export const LATITUDE_HELP_URL = '' export const LATITUDE_SLACK_URL = 'https://join.slack.com/t/trylatitude/shared_invite/zt-17dyj4elt-rwM~h2OorAA3NtgmibhnLA' -export const HEAD_COMMIT = 'HEAD' +export const HEAD_COMMIT = 'latest' +export const LATITUDE_CHANGELOG_URL = '' export enum DocumentType { Document = 'document', Folder = 'folder', diff --git a/packages/core/src/data-access/commits.ts b/packages/core/src/data-access/commits.ts index e605596ad..46842f86b 100644 --- a/packages/core/src/data-access/commits.ts +++ b/packages/core/src/data-access/commits.ts @@ -1,7 +1,14 @@ import { database } from '$core/client' -import { HEAD_COMMIT } from '$core/constants' +import { Result, Transaction } from '$core/lib' +import { NotFoundError, UnprocessableEntityError } from '$core/lib/errors' import { commits } from '$core/schema' -import { desc, eq, isNull } from 'drizzle-orm' +import { and, desc, eq, isNull } from 'drizzle-orm' + +class CommitNotFoundError extends Error { + constructor() { + super('Commit not found') + } +} const selectCondition = (uuid?: Exclude) => { if (!uuid) return isNull(commits.nextCommitId) @@ -9,12 +16,40 @@ const selectCondition = (uuid?: Exclude) => { return eq(commits.uuid, uuid) } -export async function findCommit({ uuid }: { uuid?: string }, tx = database) { - if (uuid === HEAD_COMMIT) { - return tx.query.commits.findFirst({ orderBy: desc(commits.id) }) - } +export type FindCommitProps = { + isLatest?: boolean + uuid?: string + projectId: number +} +export async function findCommit( + { uuid, isLatest, projectId }: FindCommitProps, + tx = database, +) { + try { + const belongsToProject = eq(commits.projectId, projectId) + if (isLatest) { + const commit = await tx.query.commits.findFirst({ + orderBy: desc(commits.id), + where: belongsToProject, + }) + if (!commit) return Result.error(new CommitNotFoundError()) + + return Result.ok(commit) + } - return tx.query.commits.findFirst({ where: selectCondition(uuid) }) + const commit = await tx.query.commits.findFirst({ + where: and(selectCondition(uuid), belongsToProject), + }) + if (!commit) return Result.error(new CommitNotFoundError()) + + return Result.ok(commit) + } catch (err) { + const result = Transaction.toResultError(err) + if (result.error instanceof UnprocessableEntityError) { + return Result.error(new NotFoundError('commit not found')) + } + return result + } } export async function listCommits() { diff --git a/packages/core/src/data-access/projects.ts b/packages/core/src/data-access/projects.ts index b9f92c341..8dc9bd692 100644 --- a/packages/core/src/data-access/projects.ts +++ b/packages/core/src/data-access/projects.ts @@ -1,30 +1,46 @@ import { database } from '$core/client' import { Result } from '$core/lib' +import { NotFoundError } from '$core/lib/errors' import { projects } from '$core/schema' import { and, eq } from 'drizzle-orm' -class ProjectNotFoundError extends Error { - constructor() { - super('Project not found') - } -} +const NOT_FOUND_MSG = 'Project not found' +export type FindProjectProps = { + projectId: number | string + workspaceId: number +} export async function findProject({ projectId, workspaceId, +}: FindProjectProps) { + const id = Number(projectId) + if (isNaN(id)) { + return Result.error(new NotFoundError(NOT_FOUND_MSG)) + } + + const project = await database.query.projects.findFirst({ + where: and(eq(projects.workspaceId, workspaceId), eq(projects.id, id)), + }) + + if (!project) { + return Result.error(new NotFoundError(NOT_FOUND_MSG)) + } + + return Result.ok(project!) +} + +export async function getFirstProject({ + workspaceId, }: { - projectId: number workspaceId: number }) { const project = await database.query.projects.findFirst({ - where: and( - eq(projects.workspaceId, workspaceId), - eq(projects.id, projectId), - ), + where: eq(projects.workspaceId, workspaceId), }) if (!project) { - return Result.error(new ProjectNotFoundError()) + return Result.error(new NotFoundError(NOT_FOUND_MSG)) } return Result.ok(project!) diff --git a/packages/core/src/data-access/users.ts b/packages/core/src/data-access/users.ts index 399c88121..1b97787a9 100644 --- a/packages/core/src/data-access/users.ts +++ b/packages/core/src/data-access/users.ts @@ -1,12 +1,6 @@ import { database, SafeUser, users } from '@latitude-data/core' import { eq } from 'drizzle-orm' -export class UserNotFoundError extends Error { - constructor() { - super('User not found') - } -} - export type SessionData = { user: SafeUser workspace: { id: number; name: string } diff --git a/packages/core/src/lib/Transaction.ts b/packages/core/src/lib/Transaction.ts index 3c8e2f2d3..b51a1eefc 100644 --- a/packages/core/src/lib/Transaction.ts +++ b/packages/core/src/lib/Transaction.ts @@ -4,7 +4,7 @@ import type { ExtractTablesWithRelations } from 'drizzle-orm' import { PgQueryResultHKT, PgTransaction } from 'drizzle-orm/pg-core' import { DatabaseError } from 'pg' -import { ConflictError } from './errors' +import { ConflictError, UnprocessableEntityError } from './errors' import { ErrorResult, Result, TypedResult } from './Result' export type DBSchema = typeof schema @@ -13,10 +13,13 @@ export type ITransaction = PgTransaction< T, ExtractTablesWithRelations > -export type PromisedResult = Promise> +export type PromisedResult = Promise< + TypedResult +> const DB_ERROR_CODES = { UNIQUE_VIOLATION: '23505', + INPUT_SYTAXT_ERROR: '22P02', TRANSACTION_ABORTED: '25P02', } @@ -38,7 +41,7 @@ export default class Transaction { return result! } catch (error) { - return this.toResultError(error) + return Transaction.toResultError(error) } } @@ -46,11 +49,13 @@ export default class Transaction { * Refer to the errors list at * https://github.com/rails/rails/blob/main/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L769. */ - private toResultError(error: unknown): ErrorResult { + static toResultError(error: unknown): ErrorResult { const code = (error as DatabaseError)?.code switch (code) { case DB_ERROR_CODES.UNIQUE_VIOLATION: return Result.error(new ConflictError('Database conflict')) + case DB_ERROR_CODES.INPUT_SYTAXT_ERROR: + return Result.error(new UnprocessableEntityError('Invalid input', {})) default: return Result.error(error as Error) } diff --git a/packages/core/src/lib/errors.ts b/packages/core/src/lib/errors.ts index 26b6839ae..0225936b4 100644 --- a/packages/core/src/lib/errors.ts +++ b/packages/core/src/lib/errors.ts @@ -13,6 +13,7 @@ export class LatitudeError extends Error { constructor(message: string, details: ErrorType = {}) { super(message) this.details = details + this.name = this.constructor.name } } @@ -24,6 +25,7 @@ export class ConflictError extends LatitudeError { export class UnprocessableEntityError extends LatitudeError { public statusCode = 422 public name = 'UnprocessableEntityError' + constructor(message: string, details: ErrorType) { super(message, details) } diff --git a/packages/core/src/lib/index.ts b/packages/core/src/lib/index.ts index a7d9f0c60..36c1b2d13 100644 --- a/packages/core/src/lib/index.ts +++ b/packages/core/src/lib/index.ts @@ -1,3 +1,4 @@ export * from './password' export { default as Transaction, type PromisedResult } from './Transaction' export * from './Result' +export * from './errors' diff --git a/packages/core/src/schema/errorCodes.ts b/packages/core/src/schema/errorCodes.ts new file mode 100644 index 000000000..acae4a2bf --- /dev/null +++ b/packages/core/src/schema/errorCodes.ts @@ -0,0 +1,3 @@ +export enum DBErrorCode { + inputSyntax = '22P02', +} diff --git a/packages/core/src/services/documentVersions/create.ts b/packages/core/src/services/documentVersions/create.ts index 0635c86d0..9f17a026a 100644 --- a/packages/core/src/services/documentVersions/create.ts +++ b/packages/core/src/services/documentVersions/create.ts @@ -48,7 +48,11 @@ export async function createDocumentVersion({ documentType?: DocumentType parentId?: number }) { - let commit = await findCommit({ uuid: commitUuid }) + const resultFindCommit = await findCommit({ uuid: commitUuid, projectId }) + + if (resultFindCommit.error) return resultFindCommit + let commit = resultFindCommit.value + return Transaction.call(async (tx) => { if (!commit) { const resultCommit = await createCommit({ projectId, db: tx }) diff --git a/packages/core/src/services/projects/create.ts b/packages/core/src/services/projects/create.ts new file mode 100644 index 000000000..2adf27ad4 --- /dev/null +++ b/packages/core/src/services/projects/create.ts @@ -0,0 +1,28 @@ +import { + database, + projects, + Result, + Transaction, + type Project, +} from '@latitude-data/core' +import createCommit from '$core/services/commits/create' + +export async function createProject( + { + workspaceId, + name = 'First Project', + }: { + workspaceId: number + name?: string + }, + db = database, +) { + return Transaction.call(async (tx) => { + const project = ( + await tx.insert(projects).values({ workspaceId, name }).returning() + )[0] + await createCommit({ projectId: project!.id, db: tx }) + + return Result.ok(project!) + }, db) +} diff --git a/packages/core/src/services/projects/index.ts b/packages/core/src/services/projects/index.ts new file mode 100644 index 000000000..f756a8bb3 --- /dev/null +++ b/packages/core/src/services/projects/index.ts @@ -0,0 +1 @@ +export * from './create' diff --git a/packages/core/src/services/workspaces/create.ts b/packages/core/src/services/workspaces/create.ts index 789cb508e..c198f625d 100644 --- a/packages/core/src/services/workspaces/create.ts +++ b/packages/core/src/services/workspaces/create.ts @@ -6,6 +6,7 @@ import { workspaces, type Workspace, } from '@latitude-data/core' +import { createProject } from '$core/services/projects' export async function createWorkspace( { @@ -27,6 +28,7 @@ export async function createWorkspace( await tx .insert(memberships) .values({ workspaceId: newWorkspace.id, userId: creatorId }) + await createProject({ workspaceId: newWorkspace.id }, tx) return Result.ok(newWorkspace) }, db) diff --git a/packages/web-ui/package.json b/packages/web-ui/package.json index a246b41da..9a36ad25d 100644 --- a/packages/web-ui/package.json +++ b/packages/web-ui/package.json @@ -12,6 +12,7 @@ "main": "src/index.ts", "exports": { ".": "./src/index.ts", + "./browser": "./src/browser.ts", "./styles.css": "./styles.css", "./tailwind.config.js": "./tailwind.config.js" }, @@ -22,10 +23,13 @@ ], "dependencies": { "@latitude-data/compiler": "workspace:^", + "@latitude-data/core": "workspace:^", "@monaco-editor/react": "^4.6.0", - "monaco-editor": "^0.50.0", + "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", @@ -33,6 +37,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "lucide-react": "^0.403.0", + "monaco-editor": "^0.50.0", "react": "18.3.0", "react-dom": "18.3.0", "tailwind-merge": "^2.4.0", diff --git a/packages/web-ui/src/browser.ts b/packages/web-ui/src/browser.ts new file mode 100644 index 000000000..005e04ebd --- /dev/null +++ b/packages/web-ui/src/browser.ts @@ -0,0 +1,3 @@ +export * from './layouts' +export * from './providers' +export * from './ds/molecules/browser' diff --git a/packages/web-ui/src/ds/atoms/Avatar/index.tsx b/packages/web-ui/src/ds/atoms/Avatar/index.tsx new file mode 100644 index 000000000..0c6f5bbef --- /dev/null +++ b/packages/web-ui/src/ds/atoms/Avatar/index.tsx @@ -0,0 +1,75 @@ +'use client' + +import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react' +import * as AvatarPrimitive from '@radix-ui/react-avatar' + +import { cn } from '$ui/lib/utils' + +const AvatarRoot = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(function Avatar({ className, ...props }, ref) { + return ( + + ) +}) + +const AvatarImage = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(function AvatarImage({ className, ...props }, ref) { + return ( + + ) +}) + +const AvatarFallback = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(function AvatartFallback({ className, ...props }, ref) { + return ( + + ) +}) + +export type Props = { + alt: string + fallback: { + initials: string + bgColorClass?: string + } + url?: string | undefined | null + className?: string +} + +function Avatar({ url, alt, fallback, className }: Props) { + return ( + + {url ? : null} + {fallback && ( + + {fallback.initials} + + )} + + ) +} +export { Avatar, AvatarRoot, AvatarImage, AvatarFallback } diff --git a/packages/web-ui/src/ds/atoms/Badge/index.tsx b/packages/web-ui/src/ds/atoms/Badge/index.tsx new file mode 100644 index 000000000..684fb1857 --- /dev/null +++ b/packages/web-ui/src/ds/atoms/Badge/index.tsx @@ -0,0 +1,38 @@ +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '$ui/lib/utils' + +const badgeVariants = cva( + 'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', + { + variants: { + variant: { + default: + 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', + secondary: + 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', + accent: + 'border-transparent bg-accent text-accent-foreground hover:bg-accent/80', + destructive: + 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', + outline: 'text-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/packages/web-ui/src/ds/atoms/Icons/custom-icons/LogoMonochrome.tsx b/packages/web-ui/src/ds/atoms/Icons/custom-icons/LogoMonochrome.tsx new file mode 100644 index 000000000..934cec99b --- /dev/null +++ b/packages/web-ui/src/ds/atoms/Icons/custom-icons/LogoMonochrome.tsx @@ -0,0 +1,18 @@ +import { type LucideProps } from 'lucide-react' + +export default function LogoMonochrome(props: LucideProps) { + return ( + + + + + + + + ) +} diff --git a/packages/web-ui/src/ds/atoms/Icons/custom-icons/index.ts b/packages/web-ui/src/ds/atoms/Icons/custom-icons/index.ts index 00b10bfa3..a28ccc1cf 100644 --- a/packages/web-ui/src/ds/atoms/Icons/custom-icons/index.ts +++ b/packages/web-ui/src/ds/atoms/Icons/custom-icons/index.ts @@ -1 +1,2 @@ export { default as LatitudeLogo } from './Logo' +export { default as LatitudeLogoMonochrome } from './LogoMonochrome' diff --git a/packages/web-ui/src/ds/atoms/Icons/index.tsx b/packages/web-ui/src/ds/atoms/Icons/index.tsx index edf5dadf1..a49f14f55 100644 --- a/packages/web-ui/src/ds/atoms/Icons/index.tsx +++ b/packages/web-ui/src/ds/atoms/Icons/index.tsx @@ -1,9 +1,11 @@ -import { type LucideIcon } from 'lucide-react' +import { Copy, type LucideIcon } from 'lucide-react' -import { LatitudeLogo } from './custom-icons' +import { LatitudeLogo, LatitudeLogoMonochrome } from './custom-icons' export type Icon = LucideIcon export const Icons = { logo: LatitudeLogo, + logoMonochrome: LatitudeLogoMonochrome, + clipboard: Copy, } diff --git a/packages/web-ui/src/ds/atoms/Modal/Primitives/index.tsx b/packages/web-ui/src/ds/atoms/Modal/Primitives/index.tsx new file mode 100644 index 000000000..01667516f --- /dev/null +++ b/packages/web-ui/src/ds/atoms/Modal/Primitives/index.tsx @@ -0,0 +1,131 @@ +'use client' + +import { + ComponentPropsWithoutRef, + ElementRef, + forwardRef, + HTMLAttributes, +} from 'react' +import { X } from 'lucide-react' +import * as DialogPrimitive from '@radix-ui/react-dialog' + +import Text from '$ui/ds/atoms/Text' +import { cn } from '$ui/lib/utils' + +const Dialog = DialogPrimitive.Root +const DialogTrigger = DialogPrimitive.Trigger +const DialogPortal = DialogPrimitive.Portal +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className: _cn, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className: _cn, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: HTMLAttributes) => ( +
+) +DialogHeader.displayName = 'DialogHeader' + +const DialogFooter = ({ + className, + ...props +}: HTMLAttributes) => ( +
+) +DialogFooter.displayName = 'DialogFooter' + +type DialogTitleProps = Omit< + ComponentPropsWithoutRef, + 'className' +> +const DialogTitle = forwardRef< + ElementRef, + DialogTitleProps +>(({ children, ...props }, ref) => ( + + {children} + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +type DialogDescriptionProps = Omit< + ComponentPropsWithoutRef, + 'className' +> +const DialogDescription = forwardRef< + ElementRef, + DialogDescriptionProps +>(({ children, ...props }, ref) => ( + + {children} + +)) + +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/packages/web-ui/src/ds/atoms/Modal/index.tsx b/packages/web-ui/src/ds/atoms/Modal/index.tsx new file mode 100644 index 000000000..eb2d16586 --- /dev/null +++ b/packages/web-ui/src/ds/atoms/Modal/index.tsx @@ -0,0 +1,46 @@ +'use client' + +import { ReactNode } from 'react' + +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from './Primitives' + +export const ModalTrigger = DialogTrigger + +export function Modal({ + children, + footer, + title, + description, +}: { + title?: string + description?: string + children: ReactNode + footer?: ReactNode +}) { + return ( + + + {title || description ? ( + + {title ? {title} : null} + {description ? ( + {description} + ) : null} + + ) : null} + +
{children}
+ + {footer ? {footer} : null} +
+
+ ) +} diff --git a/packages/web-ui/src/ds/atoms/Popover/index.tsx b/packages/web-ui/src/ds/atoms/Popover/index.tsx new file mode 100644 index 000000000..b73982e7c --- /dev/null +++ b/packages/web-ui/src/ds/atoms/Popover/index.tsx @@ -0,0 +1,44 @@ +'use client' + +import { forwardRef } from 'react' +import * as RadixPopover from '@radix-ui/react-popover' + +import { cn } from '$ui/lib/utils' + +type Props = RadixPopover.PopoverContentProps & { + inPortal?: boolean + scrollable?: boolean +} +const PopoverContent = forwardRef(function Content( + { inPortal = true, scrollable = true, className = '', ...rest }, + ref, +) { + const props = { + ...rest, + className: cn( + className, + 'will-change-[transform,opacity] data-[state=open]:data-[side=top]:animate-slideDownAndFade data-[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-slideRightAndFade', + { + 'custom-scrollbar': scrollable, + }, + ), + } + if (!inPortal) return + + return ( + + + + ) +}) + +namespace Popover { + export const Root = RadixPopover.Root + export const Anchor = RadixPopover.Anchor + export const Trigger = RadixPopover.Trigger + export const Portal = RadixPopover.Portal + export const Close = RadixPopover.Close + export const Content = PopoverContent +} + +export default Popover diff --git a/packages/web-ui/src/ds/atoms/Tooltip/index.tsx b/packages/web-ui/src/ds/atoms/Tooltip/index.tsx index d1aa0900b..a27582049 100644 --- a/packages/web-ui/src/ds/atoms/Tooltip/index.tsx +++ b/packages/web-ui/src/ds/atoms/Tooltip/index.tsx @@ -27,7 +27,7 @@ const TooltipContent = forwardRef< ref={ref} sideOffset={sideOffset} className={cn( - 'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', + 'z-50 overflow-hidden rounded-md bg-foreground text-white px-3 py-1.5 text-xs animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', className, )} {...props} diff --git a/packages/web-ui/src/ds/atoms/index.ts b/packages/web-ui/src/ds/atoms/index.ts index 62e893c4b..5fba88427 100644 --- a/packages/web-ui/src/ds/atoms/index.ts +++ b/packages/web-ui/src/ds/atoms/index.ts @@ -12,3 +12,6 @@ export * from './Toast' export * from './Toast/ToastProvider' export * from './Toast/useToast' export * from './Icons' +export * from './Modal' +export * from './Popover' +export * from './Badge' diff --git a/packages/web-ui/src/ds/molecules/ErrorComponent/index.tsx b/packages/web-ui/src/ds/molecules/ErrorComponent/index.tsx new file mode 100644 index 000000000..34de3a2fb --- /dev/null +++ b/packages/web-ui/src/ds/molecules/ErrorComponent/index.tsx @@ -0,0 +1,26 @@ +import { Icons, Text } from '$ui/ds/atoms' +import { cn } from '$ui/lib/utils' + +export function ErrorComponent({ + message, + type, +}: { + message: string + type: 'gray' | 'red' +}) { + return ( +
+
+ + + {message} + +
+
+ ) +} diff --git a/packages/web-ui/src/ds/molecules/FocusHeader/index.tsx b/packages/web-ui/src/ds/molecules/FocusHeader/index.tsx index 1252b8eef..954356485 100644 --- a/packages/web-ui/src/ds/molecules/FocusHeader/index.tsx +++ b/packages/web-ui/src/ds/molecules/FocusHeader/index.tsx @@ -11,7 +11,9 @@ export default function FocusHeader({
- {title} + + {title} + {description && ( {description} diff --git a/packages/web-ui/src/ds/molecules/browser.ts b/packages/web-ui/src/ds/molecules/browser.ts new file mode 100644 index 000000000..65cde2ce5 --- /dev/null +++ b/packages/web-ui/src/ds/molecules/browser.ts @@ -0,0 +1 @@ +export * from './ErrorComponent' diff --git a/packages/web-ui/src/index.ts b/packages/web-ui/src/index.ts index 3b623a7cc..7c4fd32b0 100644 --- a/packages/web-ui/src/index.ts +++ b/packages/web-ui/src/index.ts @@ -1,5 +1,4 @@ export * from './ds/tokens' export * from './ds/atoms' export * from './ds/molecules' -export * from './layouts' export * from './sections' diff --git a/packages/web-ui/src/layouts/AppLayout/Header/index.tsx b/packages/web-ui/src/layouts/AppLayout/Header/index.tsx new file mode 100644 index 000000000..a23891c07 --- /dev/null +++ b/packages/web-ui/src/layouts/AppLayout/Header/index.tsx @@ -0,0 +1,109 @@ +import { ReactNode } from 'react' + +import { Avatar } from '$ui/ds/atoms/Avatar' +import { Icons } from '$ui/ds/atoms/Icons' +import Text from '$ui/ds/atoms/Text' +import getUserInfoFromSession from '$ui/lib/getUserInfo' +import { SessionUser } from '$ui/providers' +import { Fragment } from 'react/jsx-runtime' + +function BreadcrumpSeparator() { + return ( + + + + ) +} + +type IBreadCrumb = { name: string | ReactNode } +function Breadcrump({ breadcrumbs }: { breadcrumbs: IBreadCrumb[] }) { + return ( +
    +
  • + +
  • + {breadcrumbs.length === 0 ? null : ( +
  • + +
  • + )} + {breadcrumbs.map((breadcrumb, idx) => { + const isLast = idx === breadcrumbs.length - 1 + return ( + +
  • + {typeof breadcrumb.name === 'string' ? ( + {breadcrumb.name as string} + ) : ( + breadcrumb.name + )} +
  • + {!isLast ? ( +
  • + +
  • + ) : null} +
    + ) + })} +
+ ) +} +type INavigationLink = { + label: string + href?: string + onClick?: () => void + _target?: '_blank' | '_self' +} + +function NavLink({ label, href, onClick, _target }: INavigationLink) { + return ( + + + {label} + + + ) +} + +export type AppHeaderProps = { + navigationLinks: INavigationLink[] + currentUser: SessionUser | undefined + breadcrumbs?: IBreadCrumb[] +} +export default function AppHeader({ + breadcrumbs = [], + navigationLinks, + currentUser, +}: AppHeaderProps) { + const info = currentUser ? getUserInfoFromSession(currentUser) : null + return ( +
+ +
+ + {info ? ( + + ) : null} +
+
+ ) +} diff --git a/packages/web-ui/src/layouts/AppLayout/index.tsx b/packages/web-ui/src/layouts/AppLayout/index.tsx new file mode 100644 index 000000000..456ae9712 --- /dev/null +++ b/packages/web-ui/src/layouts/AppLayout/index.tsx @@ -0,0 +1,26 @@ +import { ReactNode } from 'react' + +import AppHeader, { AppHeaderProps } from '$ui/layouts/AppLayout/Header' +import { SessionUser } from '$ui/providers' + +export type AppLayoutProps = AppHeaderProps & { + children: ReactNode + currentUser: SessionUser | undefined +} +export default function AppLayout({ + children, + currentUser, + breadcrumbs, + navigationLinks, +}: AppLayoutProps) { + return ( +
+ + {children} +
+ ) +} diff --git a/packages/web-ui/src/layouts/index.ts b/packages/web-ui/src/layouts/index.ts index 3501148e9..3c4ce74d4 100644 --- a/packages/web-ui/src/layouts/index.ts +++ b/packages/web-ui/src/layouts/index.ts @@ -1 +1,3 @@ export { default as FocusLayout } from './FocusLayout' +export { default as AppLayout } from './AppLayout' +export { default as AppHeader } from './AppLayout/Header' diff --git a/packages/web-ui/src/lib/getUserInfo.ts b/packages/web-ui/src/lib/getUserInfo.ts new file mode 100644 index 000000000..53a14e258 --- /dev/null +++ b/packages/web-ui/src/lib/getUserInfo.ts @@ -0,0 +1,44 @@ +import { SessionUser } from '$ui/providers' + +const BG_COLORS = { + yellow: 'bg-yellow-500 text-yellow-100', + orange: 'bg-orange-500 text-orange-100', + red: 'bg-red-500 text-red-100', + pink: 'bg-pink-500 text-pink-100', + purple: 'bg-purple-500 text-purple-100', + indigo: 'bg-indigo-500 text-indigo-100', + blue: 'bg-blue-500 text-blue-100', + cyan: 'bg-cyan-500 text-cyan-100', +} + +function getFallback(name: string | null | undefined) { + if (!name || name === '') return { initials: 'X' } + + const [first, last] = name.split(' ') + if (!first) return { initials: 'X' } + + const initials = first.charAt(0) + (last ? last.charAt(0) : '') + + const charCode = initials + .split('') + .map((char) => char.charCodeAt(0)) + .join('') + const colorIndex = parseInt(charCode, 10) % Object.keys(BG_COLORS).length + const bgColorClass = Object.values(BG_COLORS)[colorIndex]! + + return { initials, bgColorClass } +} + +export default function getUserInfoFromSession({ name }: SessionUser) { + if (!name) { + return { + name: 'Unknown', + fallback: { initials: 'X' }, + } + } + + return { + name: name ?? 'Unknown', + fallback: getFallback(name), + } +} diff --git a/packages/web-ui/src/providers/SessionProvider/index.tsx b/packages/web-ui/src/providers/SessionProvider/index.tsx new file mode 100644 index 000000000..eb56d7497 --- /dev/null +++ b/packages/web-ui/src/providers/SessionProvider/index.tsx @@ -0,0 +1,40 @@ +'use client' + +import { createContext, ReactNode, useContext } from 'react' + +export type SessionUser = { + id: string + name: string | null | undefined + email: string +} +export type SessionWorkspace = { id: number; name: string } +interface ISessionContext { + currentUser: SessionUser + workspace: SessionWorkspace +} + +const SessionContext = createContext({} as ISessionContext) + +const SessionProvider = ({ + children, + ...context +}: { + children: ReactNode +} & ISessionContext) => { + return ( + + {children} + + ) +} + +const useSession = () => { + const context = useContext(SessionContext) + + if (!context) { + throw new Error('useSession must be used within a SessionProvider') + } + return context +} + +export { SessionProvider, useSession } diff --git a/packages/web-ui/src/providers/index.ts b/packages/web-ui/src/providers/index.ts new file mode 100644 index 000000000..d7dfd2a0f --- /dev/null +++ b/packages/web-ui/src/providers/index.ts @@ -0,0 +1 @@ +export * from './SessionProvider' diff --git a/packages/web-ui/src/sections/CommitDetail/index.tsx b/packages/web-ui/src/sections/CommitDetail/index.tsx new file mode 100644 index 000000000..561d48f27 --- /dev/null +++ b/packages/web-ui/src/sections/CommitDetail/index.tsx @@ -0,0 +1,10 @@ +import AppLayout, { AppLayoutProps } from '$ui/layouts/AppLayout' + +type Props = { appLayout: Omit } +export default async function CommitDetail({ appLayout }: Props) { + return ( + +
Sidebar + Content
+
+ ) +} diff --git a/packages/web-ui/src/sections/DocumentEditor/index.tsx b/packages/web-ui/src/sections/DocumentEditor/index.tsx index 87d8a8642..0ba88a97a 100644 --- a/packages/web-ui/src/sections/DocumentEditor/index.tsx +++ b/packages/web-ui/src/sections/DocumentEditor/index.tsx @@ -3,7 +3,7 @@ import { ReactNode, Suspense, useEffect, useMemo, useState } from 'react' import { ConversationMetadata, readMetadata } from '@latitude-data/compiler' -import { Input, Text } from '$ui/ds/atoms' +import { Badge, Input, Text } from '$ui/ds/atoms' import { DocumentTextEditor, DocumentTextEditorFallback, @@ -49,11 +49,9 @@ export function DocumentEditor({ document }: { document: string }) { {inputs.length > 0 ? ( inputs.map((param) => (
-
- - {{{param}}} - -
+ + {{{param}}} +
)) diff --git a/packages/web-ui/src/sections/index.ts b/packages/web-ui/src/sections/index.ts index 603ac68b1..ca903374b 100644 --- a/packages/web-ui/src/sections/index.ts +++ b/packages/web-ui/src/sections/index.ts @@ -1 +1,2 @@ export * from './DocumentEditor' +export { default as CommitDetail } from './CommitDetail' diff --git a/packages/web-ui/tailwind.config.js b/packages/web-ui/tailwind.config.js index bbec3a0fe..7addf21af 100644 --- a/packages/web-ui/tailwind.config.js +++ b/packages/web-ui/tailwind.config.js @@ -1,15 +1,13 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - darkMode: ["class"], - content: [ - './src/**/*.{ts,tsx}', - ], + darkMode: ['class'], + content: ['./src/**/*.{ts,tsx}'], theme: { container: { center: true, - padding: "2rem", + padding: '2rem', screens: { - "2xl": "1400px", + '2xl': '1400px', }, }, fontFamily: { @@ -56,25 +54,28 @@ module.exports = { }, }, borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', }, keyframes: { - "accordion-down": { - from: { height: "0" }, - to: { height: "var(--radix-accordion-content-height)" }, + 'accordion-down': { + from: { height: '0' }, + to: { height: 'var(--radix-accordion-content-height)' }, }, - "accordion-up": { - from: { height: "var(--radix-accordion-content-height)" }, - to: { height: "0" }, + 'accordion-up': { + from: { height: 'var(--radix-accordion-content-height)' }, + to: { height: '0' }, }, }, animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + }, + maxWidth: { + modal: '580px', }, }, }, - plugins: [require("tailwindcss-animate")], + plugins: [require('tailwindcss-animate')], } diff --git a/packages/web-ui/tsconfig.json b/packages/web-ui/tsconfig.json index d1adba744..81a71b5d0 100644 --- a/packages/web-ui/tsconfig.json +++ b/packages/web-ui/tsconfig.json @@ -6,8 +6,11 @@ "module": "ESNext", "baseUrl": ".", "paths": { - "$ui/*": ["src/*"], + "@latitude-data/core": ["../core/src/*"], + "@latitude-data/compiler": ["../compiler/src/*"], "$compiler/*": ["../compiler/src/*"], + "$core/*": ["../core/src/*"], + "$ui/*": ["src/*"], "acorn": ["node_modules/@latitude-data/typescript-config/types/acorn"] } }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f7a2d573..a4cee2a00 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -438,15 +438,27 @@ importers: '@latitude-data/compiler': specifier: workspace:^ version: link:../compiler + '@latitude-data/core': + specifier: workspace:^ + version: link:../core '@monaco-editor/react': specifier: ^4.6.0 version: 4.6.0(monaco-editor@0.50.0)(react-dom@18.3.0)(react@18.3.0) + '@radix-ui/react-avatar': + specifier: ^1.1.0 + version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0) + '@radix-ui/react-dialog': + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0) '@radix-ui/react-icons': specifier: ^1.3.0 version: 1.3.0(react@18.3.0) '@radix-ui/react-label': specifier: ^2.1.0 version: 2.1.0(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0) + '@radix-ui/react-popover': + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0) '@radix-ui/react-select': specifier: ^2.1.1 version: 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0) @@ -594,7 +606,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} peerDependencies: '@babel/core': ^7.11.0 - eslint: ^7.5.0 || ^8.0.0 || ^9.0.0 + eslint: ^7.5.0 || ^8.0.0 || ^9.0.0 || 9.x dependencies: '@babel/core': 7.24.7 '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 @@ -1447,7 +1459,7 @@ packages: resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 || 9.x dependencies: eslint: 8.57.0 eslint-visitor-keys: 3.4.3 @@ -1457,7 +1469,7 @@ packages: resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 || 9.x dependencies: eslint: 9.6.0 eslint-visitor-keys: 3.4.3 @@ -1549,8 +1561,8 @@ packages: /@floating-ui/react-dom@2.1.1(react-dom@18.3.0)(react@18.3.0): resolution: {integrity: sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==} peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' + react: '>=16.8.0 || 19.x' + react-dom: '>=16.8.0 || 19.x' dependencies: '@floating-ui/dom': 1.6.7 react: 18.3.0 @@ -1895,8 +1907,8 @@ packages: resolution: {integrity: sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==} peerDependencies: monaco-editor: '>= 0.25.0 < 1' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || 19.x + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || 19.x dependencies: '@monaco-editor/loader': 1.4.0(monaco-editor@0.50.0) monaco-editor: 0.50.0 @@ -1908,8 +1920,8 @@ packages: resolution: {integrity: sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==} peerDependencies: monaco-editor: '>= 0.25.0 < 1' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || 19.x + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || 19.x dependencies: '@monaco-editor/loader': 1.4.0(monaco-editor@0.50.0) monaco-editor: 0.50.0 @@ -2409,8 +2421,8 @@ packages: 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 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2424,13 +2436,36 @@ packages: react-dom: 18.3.0(react@18.3.0) dev: false + /@radix-ui/react-avatar@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0): + resolution: {integrity: sha512-Q/PbuSMk/vyAd/UoIShVGZ7StHHeRFYU7wXmi5GV+8cLXflZAEpHL/F697H1klrzxKXNtZ97vWiC0q3RKUH8UA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-context': 1.1.0(@types/react@18.3.0)(react@18.3.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.0)(react@18.3.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.0)(react@18.3.0) + '@types/react': 18.3.0 + '@types/react-dom': 18.3.0 + react: 18.3.0 + react-dom: 18.3.0(react@18.3.0) + dev: false + /@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0): resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} 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 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2451,7 +2486,7 @@ packages: resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2464,20 +2499,53 @@ packages: resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.3.0 + react: 18.3.0 + dev: false + + /@radix-ui/react-dialog@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0): + resolution: {integrity: sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true + '@types/react-dom': + optional: true dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.0)(react@18.3.0) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.0)(react@18.3.0) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0) + '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.0)(react@18.3.0) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.0)(react@18.3.0) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.0)(react@18.3.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.0)(react@18.3.0) '@types/react': 18.3.0 + '@types/react-dom': 18.3.0 + aria-hidden: 1.2.4 react: 18.3.0 + react-dom: 18.3.0(react@18.3.0) + react-remove-scroll: 2.5.7(@types/react@18.3.0)(react@18.3.0) dev: false /@radix-ui/react-direction@1.1.0(@types/react@18.3.0)(react@18.3.0): resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2491,8 +2559,8 @@ packages: 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 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2514,7 +2582,7 @@ packages: resolution: {integrity: sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2528,8 +2596,8 @@ packages: 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 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2548,7 +2616,7 @@ packages: /@radix-ui/react-icons@1.3.0(react@18.3.0): resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==} peerDependencies: - react: ^16.x || ^17.x || ^18.x + react: ^16.x || ^17.x || ^18.x || 19.x dependencies: react: 18.3.0 dev: false @@ -2557,7 +2625,7 @@ packages: resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2572,8 +2640,8 @@ packages: 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 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2587,13 +2655,47 @@ packages: react-dom: 18.3.0(react@18.3.0) dev: false + /@radix-ui/react-popover@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0): + resolution: {integrity: sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.0)(react@18.3.0) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.0)(react@18.3.0) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0) + '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.0)(react@18.3.0) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.0)(react@18.3.0) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.0)(react@18.3.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.0)(react@18.3.0) + '@types/react': 18.3.0 + '@types/react-dom': 18.3.0 + aria-hidden: 1.2.4 + react: 18.3.0 + react-dom: 18.3.0(react@18.3.0) + react-remove-scroll: 2.5.7(@types/react@18.3.0)(react@18.3.0) + dev: false + /@radix-ui/react-popper@1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0): resolution: {integrity: sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==} 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 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2621,8 +2723,8 @@ packages: 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 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2642,8 +2744,8 @@ packages: 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 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2663,8 +2765,8 @@ packages: 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 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2683,8 +2785,8 @@ packages: 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 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2722,7 +2824,7 @@ packages: resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2737,8 +2839,8 @@ packages: 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 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2768,8 +2870,8 @@ packages: 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 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2798,7 +2900,7 @@ packages: resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2811,7 +2913,7 @@ packages: resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2825,7 +2927,7 @@ packages: resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2839,7 +2941,7 @@ packages: resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2852,7 +2954,7 @@ packages: resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2865,7 +2967,7 @@ packages: resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2879,7 +2981,7 @@ packages: resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -2894,8 +2996,8 @@ packages: 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 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -3348,7 +3450,7 @@ packages: engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha - eslint: ^7.0.0 || ^8.0.0 + eslint: ^7.0.0 || ^8.0.0 || 9.x typescript: '*' peerDependenciesMeta: typescript: @@ -3377,7 +3479,7 @@ packages: engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: '@typescript-eslint/parser': ^7.0.0 - eslint: ^8.56.0 + eslint: ^8.56.0 || 9.x typescript: '*' peerDependenciesMeta: typescript: @@ -3403,7 +3505,7 @@ packages: resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 + eslint: ^7.0.0 || ^8.0.0 || 9.x typescript: '*' peerDependenciesMeta: typescript: @@ -3424,7 +3526,7 @@ packages: resolution: {integrity: sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: - eslint: ^8.56.0 + eslint: ^8.56.0 || 9.x typescript: '*' peerDependenciesMeta: typescript: @@ -3469,7 +3571,7 @@ packages: resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 + eslint: ^7.0.0 || ^8.0.0 || 9.x typescript: '*' peerDependenciesMeta: typescript: @@ -3489,7 +3591,7 @@ packages: resolution: {integrity: sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: - eslint: ^8.56.0 + eslint: ^8.56.0 || 9.x typescript: '*' peerDependenciesMeta: typescript: @@ -3589,7 +3691,7 @@ packages: resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 || 9.x dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.6.0) '@types/json-schema': 7.0.15 @@ -3609,7 +3711,7 @@ packages: resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 + eslint: ^7.0.0 || ^8.0.0 || 9.x dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.6.0) '@types/json-schema': 7.0.15 @@ -3628,7 +3730,7 @@ packages: resolution: {integrity: sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: - eslint: ^8.56.0 + eslint: ^8.56.0 || 9.x dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.6.0) '@typescript-eslint/scope-manager': 7.16.0 @@ -3673,7 +3775,7 @@ packages: engines: {node: '>=16'} peerDependencies: '@next/eslint-plugin-next': '>=12.3.0 <15' - eslint: '>=8.48.0 <9' + eslint: '>=8.48.0 <9 || 9.x' prettier: '>=3.0.0 <4' typescript: '>=4.8.0 <6' peerDependenciesMeta: @@ -4704,7 +4806,7 @@ packages: pg: '>=8' postgres: '>=3' prisma: '*' - react: '>=18' + react: '>=18 || 19.x' sql.js: '>=1' sqlite3: '>=5' peerDependenciesMeta: @@ -5058,7 +5160,7 @@ packages: resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} hasBin: true peerDependencies: - eslint: '>=7.0.0' + eslint: '>=7.0.0 || 9.x' dependencies: eslint: 9.6.0 dev: true @@ -5066,7 +5168,7 @@ packages: /eslint-config-turbo@2.0.6(eslint@9.6.0): resolution: {integrity: sha512-PkRjFnZUZWPcrYT4Xoi5OWOUtnn6xVGh88I6TsayiH4AQZuLs/MDmzfJRK+PiWIrI7Q7sbsVEQP+nUyyRE3uAw==} peerDependencies: - eslint: '>6.6.0' + eslint: '>6.6.0 || 9.x' dependencies: eslint: 9.6.0 eslint-plugin-turbo: 2.0.6(eslint@9.6.0) @@ -5176,7 +5278,7 @@ packages: /eslint-plugin-drizzle@0.2.3(eslint@9.6.0): resolution: {integrity: sha512-BO+ymHo33IUNoJlC0rbd7HP9EwwpW4VIp49R/tWQF/d2E1K2kgTf0tCXT0v9MSiBr6gGR1LtPwMLapTKEWSg9A==} peerDependencies: - eslint: '>=8.0.0' + eslint: '>=8.0.0 || 9.x' dependencies: eslint: 9.6.0 dev: true @@ -5185,7 +5287,7 @@ packages: resolution: {integrity: sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==} engines: {node: '>=8.10.0'} peerDependencies: - eslint: '>=4.19.1' + eslint: '>=4.19.1 || 9.x' dependencies: eslint: 9.6.0 eslint-utils: 2.1.0 @@ -5196,7 +5298,7 @@ packages: resolution: {integrity: sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==} engines: {node: '>=6.5.0'} peerDependencies: - eslint: '>=4.19.1' + eslint: '>=4.19.1 || 9.x' dependencies: escape-string-regexp: 1.0.5 eslint: 9.6.0 @@ -5208,7 +5310,7 @@ packages: engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || 9.x peerDependenciesMeta: '@typescript-eslint/parser': optional: true @@ -5243,7 +5345,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@typescript-eslint/eslint-plugin': ^5.0.0 || ^6.0.0 || ^7.0.0 - eslint: ^7.0.0 || ^8.0.0 + eslint: ^7.0.0 || ^8.0.0 || 9.x jest: '*' peerDependenciesMeta: '@typescript-eslint/eslint-plugin': @@ -5263,7 +5365,7 @@ packages: resolution: {integrity: sha512-nOFOCaJG2pYqORjK19lqPqxMO/JpvdCZdPtNdxY3kvom3jTvkAbOvQvD8wuD0G8BYR0IGAGYDlzqWJOh/ybn2g==} engines: {node: '>=4.0'} peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || 9.x dependencies: aria-query: 5.1.3 array-includes: 3.1.8 @@ -5288,7 +5390,7 @@ packages: resolution: {integrity: sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==} engines: {node: '>=8.10.0'} peerDependencies: - eslint: '>=5.16.0' + eslint: '>=5.16.0 || 9.x' dependencies: eslint: 9.6.0 eslint-plugin-es: 3.0.1(eslint@9.6.0) @@ -5307,7 +5409,7 @@ packages: /eslint-plugin-playwright@0.16.0(eslint-plugin-jest@27.9.0)(eslint@9.6.0): resolution: {integrity: sha512-DcHpF0SLbNeh9MT4pMzUGuUSnJ7q5MWbP8sSEFIMS6j7Ggnduq8ghNlfhURgty4c1YFny7Ge9xYTO1FSAoV2Vw==} peerDependencies: - eslint: '>=7' + eslint: '>=7 || 9.x' eslint-plugin-jest: '>=25' peerDependenciesMeta: eslint-plugin-jest: @@ -5321,7 +5423,7 @@ packages: resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} engines: {node: '>=10'} peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || 9.x dependencies: eslint: 9.6.0 dev: true @@ -5330,7 +5432,7 @@ packages: resolution: {integrity: sha512-aoW4MV891jkUulwDApQbPYTVZmeuSyFrudpbTAQuj5Fv8VL+o6df2xIGpw8B0hPjAaih1/Fb0om9grCdyFYemA==} engines: {node: '>=4'} peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || 9.x dependencies: array-includes: 3.1.8 array.prototype.findlast: 1.2.5 @@ -5357,7 +5459,7 @@ packages: resolution: {integrity: sha512-1E94YOTUDnOjSLyvOwmbVDzQi/WkKm3WVrMXu6SmBr6DN95xTGZmI6HJ/eOkSXh/DlheRsxaPsJvZByDBhWLVQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} peerDependencies: - eslint: ^7.5.0 || ^8.0.0 + eslint: ^7.5.0 || ^8.0.0 || 9.x dependencies: '@typescript-eslint/utils': 5.62.0(eslint@9.6.0)(typescript@5.5.3) eslint: 9.6.0 @@ -5376,7 +5478,7 @@ packages: /eslint-plugin-turbo@2.0.6(eslint@9.6.0): resolution: {integrity: sha512-yGnpMvyBxI09ZrF5bGpaniBz57MiExTCsRnNxP+JnbMFD+xU3jG3ukRzehVol8LYNdC/G7E4HoH+x7OEpoSGAQ==} peerDependencies: - eslint: '>6.6.0' + eslint: '>6.6.0 || 9.x' dependencies: dotenv: 16.0.3 eslint: 9.6.0 @@ -5386,7 +5488,7 @@ packages: resolution: {integrity: sha512-FW+4r20myG/DqFcCSzoumaddKBicIPeFnTrifon2mWIzlfyvzwyqZjqVP7m4Cqr/ZYisS2aiLghkUWaPg6vtCw==} engines: {node: '>=16'} peerDependencies: - eslint: '>=8.44.0' + eslint: '>=8.44.0 || 9.x' dependencies: '@babel/helper-validator-identifier': 7.24.7 '@eslint-community/eslint-utils': 4.4.0(eslint@9.6.0) @@ -6714,7 +6816,7 @@ packages: /lucide-react@0.403.0(react@18.3.0): resolution: {integrity: sha512-ldD3NT444k4Mi+n+43xC8XaHr88MNdhOIApIE15r7O+PcVYEBXNOOFo904cBC6Hsja+zHc1lqw0mqUgBECeisw==} peerDependencies: - react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 || 19.x dependencies: react: 18.3.0 dev: false @@ -6964,8 +7066,8 @@ packages: '@opentelemetry/api': ^1.1.0 '@playwright/test': ^1.41.2 babel-plugin-react-compiler: '*' - react: 19.0.0-rc-f994737d14-20240522 - react-dom: 19.0.0-rc-f994737d14-20240522 + react: 19.0.0-rc-f994737d14-20240522 || 19.x + react-dom: 19.0.0-rc-f994737d14-20240522 || 19.x sass: ^1.3.0 peerDependenciesMeta: '@opentelemetry/api': @@ -7660,7 +7762,7 @@ packages: /react-dom@18.3.0(react@18.3.0): resolution: {integrity: sha512-zaKdLBftQJnvb7FtDIpZtsAIb2MZU087RM8bRDZU8LVCCFYjPTsDZJNFUWPcVz3HFSN1n/caxi0ca4B/aaVQGQ==} peerDependencies: - react: ^18.3.0 + react: ^18.3.0 || 19.x dependencies: loose-envify: 1.4.0 react: 18.3.0 @@ -7670,7 +7772,7 @@ packages: /react-dom@19.0.0-rc-378b305958-20240710(react@19.0.0-rc-378b305958-20240710): resolution: {integrity: sha512-WpE0+4pnFMyuC9WxJGj7+XysxNChbwnCcFS7INR428/uUoECSTRmlQt05hTGFIyfpYBO1zGlXfMBAkrFmqh3wg==} peerDependencies: - react: 19.0.0-rc-378b305958-20240710 + react: 19.0.0-rc-378b305958-20240710 || 19.x dependencies: react: 19.0.0-rc-378b305958-20240710 scheduler: 0.25.0-rc-378b305958-20240710 @@ -7689,7 +7791,7 @@ packages: engines: {node: '>=10'} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -7705,7 +7807,7 @@ packages: engines: {node: '>=10'} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -7724,7 +7826,7 @@ packages: engines: {node: '>=10'} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -8352,7 +8454,7 @@ packages: peerDependencies: '@babel/core': '*' babel-plugin-macros: '*' - react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0 || 19.x' peerDependenciesMeta: '@babel/core': optional: true @@ -8422,7 +8524,7 @@ packages: /swr@2.2.5(react@19.0.0-rc-378b305958-20240710): resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==} peerDependencies: - react: ^16.11.0 || ^17.0.0 || ^18.0.0 + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || 19.x dependencies: client-only: 0.0.1 react: 19.0.0-rc-378b305958-20240710 @@ -8846,7 +8948,7 @@ packages: engines: {node: '>=10'} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -8861,7 +8963,7 @@ packages: engines: {node: '>=10'} peerDependencies: '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || 19.x peerDependenciesMeta: '@types/react': optional: true @@ -8875,7 +8977,7 @@ packages: /use-sync-external-store@1.2.2(react@19.0.0-rc-378b305958-20240710): resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || 19.x dependencies: react: 19.0.0-rc-378b305958-20240710 dev: false @@ -9191,7 +9293,7 @@ packages: /zsa-react@0.2.2(react@19.0.0-rc-378b305958-20240710)(zod@3.23.8): resolution: {integrity: sha512-0jUGzIy4PRc8Ur3JS+YBdRyD3rnsErCzNeJOVLhTxPqaKDDQ+JEmIHGYAK354RNpchIlvGVDcuedZXEGL6Nl6Q==} peerDependencies: - react: ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 || 19.x zod: ^3.23.5 dependencies: react: 19.0.0-rc-378b305958-20240710