diff --git a/apps/web/src/components/auth/AuthDivider.test.tsx b/apps/web/src/components/auth/AuthDivider.test.tsx
new file mode 100644
index 0000000..3b1ae83
--- /dev/null
+++ b/apps/web/src/components/auth/AuthDivider.test.tsx
@@ -0,0 +1,27 @@
+import { describe, it, expect } from "vitest";
+import { render, screen } from "@testing-library/react";
+import { AuthDivider } from "./AuthDivider";
+
+describe("AuthDivider", (): void => {
+ it("should render with default text", (): void => {
+ render();
+ expect(screen.getByText("or continue with email")).toBeInTheDocument();
+ });
+
+ it("should render with custom text", (): void => {
+ render();
+ expect(screen.getByText("or sign up")).toBeInTheDocument();
+ });
+
+ it("should render a horizontal divider line", (): void => {
+ const { container } = render();
+ const line = container.querySelector("span.border-t");
+ expect(line).toBeInTheDocument();
+ });
+
+ it("should apply uppercase styling to text", (): void => {
+ const { container } = render();
+ const textWrapper = container.querySelector(".uppercase");
+ expect(textWrapper).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/src/components/auth/AuthDivider.tsx b/apps/web/src/components/auth/AuthDivider.tsx
new file mode 100644
index 0000000..4d2f499
--- /dev/null
+++ b/apps/web/src/components/auth/AuthDivider.tsx
@@ -0,0 +1,18 @@
+interface AuthDividerProps {
+ text?: string;
+}
+
+export function AuthDivider({
+ text = "or continue with email",
+}: AuthDividerProps): React.ReactElement {
+ return (
+
+ );
+}
diff --git a/apps/web/src/components/auth/AuthErrorBanner.test.tsx b/apps/web/src/components/auth/AuthErrorBanner.test.tsx
new file mode 100644
index 0000000..22576c6
--- /dev/null
+++ b/apps/web/src/components/auth/AuthErrorBanner.test.tsx
@@ -0,0 +1,79 @@
+import { describe, it, expect, vi } from "vitest";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { AuthErrorBanner } from "./AuthErrorBanner";
+
+describe("AuthErrorBanner", (): void => {
+ it("should render the message text", (): void => {
+ render();
+ expect(
+ screen.getByText("Authentication paused. Please try again when ready.")
+ ).toBeInTheDocument();
+ });
+
+ it("should have role alert and aria-live polite for accessibility", (): void => {
+ render();
+ const alert = screen.getByRole("alert");
+ expect(alert).toBeInTheDocument();
+ expect(alert).toHaveAttribute("aria-live", "polite");
+ });
+
+ it("should render the info icon, not a warning icon", (): void => {
+ const { container } = render();
+ // Info icon from lucide-react renders as an SVG
+ const svgs = container.querySelectorAll("svg");
+ expect(svgs.length).toBeGreaterThanOrEqual(1);
+ // The container should use blue styling, not red/yellow
+ const alert = screen.getByRole("alert");
+ expect(alert.className).toContain("bg-blue-50");
+ expect(alert.className).toContain("text-blue-700");
+ expect(alert.className).not.toContain("red");
+ expect(alert.className).not.toContain("yellow");
+ });
+
+ it("should render dismiss button when onDismiss is provided", (): void => {
+ const onDismiss = vi.fn();
+ render();
+ const dismissButton = screen.getByLabelText("Dismiss");
+ expect(dismissButton).toBeInTheDocument();
+ });
+
+ it("should not render dismiss button when onDismiss is not provided", (): void => {
+ render();
+ expect(screen.queryByLabelText("Dismiss")).not.toBeInTheDocument();
+ });
+
+ it("should call onDismiss when dismiss button is clicked", async (): Promise => {
+ const user = userEvent.setup();
+ const onDismiss = vi.fn();
+ render();
+
+ const dismissButton = screen.getByLabelText("Dismiss");
+ await user.click(dismissButton);
+
+ expect(onDismiss).toHaveBeenCalledTimes(1);
+ });
+
+ it("should use blue info styling, not red or alarming colors", (): void => {
+ render();
+ const alert = screen.getByRole("alert");
+ expect(alert.className).toContain("bg-blue-50");
+ expect(alert.className).toContain("border-blue-200");
+ expect(alert.className).toContain("text-blue-700");
+ });
+
+ it("should render all PDA-friendly error messages", (): void => {
+ const messages = [
+ "Authentication paused. Please try again when ready.",
+ "The email and password combination wasn't recognized.",
+ "Unable to connect. Check your network and try again.",
+ "The service is taking a break. Please try again in a moment.",
+ ];
+
+ for (const message of messages) {
+ const { unmount } = render();
+ expect(screen.getByText(message)).toBeInTheDocument();
+ unmount();
+ }
+ });
+});
diff --git a/apps/web/src/components/auth/AuthErrorBanner.tsx b/apps/web/src/components/auth/AuthErrorBanner.tsx
new file mode 100644
index 0000000..cb6eba0
--- /dev/null
+++ b/apps/web/src/components/auth/AuthErrorBanner.tsx
@@ -0,0 +1,32 @@
+"use client";
+
+import type { ReactElement } from "react";
+import { Info, X } from "lucide-react";
+
+export interface AuthErrorBannerProps {
+ message: string;
+ onDismiss?: () => void;
+}
+
+export function AuthErrorBanner({ message, onDismiss }: AuthErrorBannerProps): ReactElement {
+ return (
+
+
+ {message}
+ {onDismiss && (
+
+ )}
+
+ );
+}
diff --git a/apps/web/src/components/auth/LoginForm.test.tsx b/apps/web/src/components/auth/LoginForm.test.tsx
new file mode 100644
index 0000000..52a0ed7
--- /dev/null
+++ b/apps/web/src/components/auth/LoginForm.test.tsx
@@ -0,0 +1,168 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { LoginForm } from "./LoginForm";
+
+describe("LoginForm", (): void => {
+ const mockOnSubmit = vi.fn();
+
+ beforeEach((): void => {
+ mockOnSubmit.mockClear();
+ });
+
+ it("should render email and password fields with labels", (): void => {
+ render();
+ expect(screen.getByLabelText("Email")).toBeInTheDocument();
+ expect(screen.getByLabelText("Password")).toBeInTheDocument();
+ });
+
+ it("should render a Continue submit button", (): void => {
+ render();
+ expect(screen.getByRole("button", { name: "Continue" })).toBeInTheDocument();
+ });
+
+ it("should auto-focus the email input on mount", (): void => {
+ render();
+ const emailInput = screen.getByLabelText("Email");
+ expect(document.activeElement).toBe(emailInput);
+ });
+
+ it("should validate email format on submit", async (): Promise => {
+ const user = userEvent.setup();
+ render();
+
+ const emailInput = screen.getByLabelText("Email");
+ const passwordInput = screen.getByLabelText("Password");
+ const submitButton = screen.getByRole("button", { name: "Continue" });
+
+ await user.type(emailInput, "invalid-email");
+ await user.type(passwordInput, "password123");
+ await user.click(submitButton);
+
+ expect(screen.getByText("Please enter a valid email address.")).toBeInTheDocument();
+ expect(mockOnSubmit).not.toHaveBeenCalled();
+ });
+
+ it("should validate non-empty password on submit", async (): Promise => {
+ const user = userEvent.setup();
+ render();
+
+ const emailInput = screen.getByLabelText("Email");
+ const submitButton = screen.getByRole("button", { name: "Continue" });
+
+ await user.type(emailInput, "user@example.com");
+ await user.click(submitButton);
+
+ expect(screen.getByText("Password is recommended.")).toBeInTheDocument();
+ expect(mockOnSubmit).not.toHaveBeenCalled();
+ });
+
+ it("should call onSubmit with email and password when valid", async (): Promise => {
+ const user = userEvent.setup();
+ render();
+
+ const emailInput = screen.getByLabelText("Email");
+ const passwordInput = screen.getByLabelText("Password");
+ const submitButton = screen.getByRole("button", { name: "Continue" });
+
+ await user.type(emailInput, "user@example.com");
+ await user.type(passwordInput, "password123");
+ await user.click(submitButton);
+
+ expect(mockOnSubmit).toHaveBeenCalledWith("user@example.com", "password123");
+ });
+
+ it("should show loading state with spinner and Signing in text", (): void => {
+ render();
+ expect(screen.getByText("Signing in...")).toBeInTheDocument();
+ expect(screen.queryByText("Continue")).not.toBeInTheDocument();
+ });
+
+ it("should disable inputs when loading", (): void => {
+ render();
+ expect(screen.getByLabelText("Email")).toBeDisabled();
+ expect(screen.getByLabelText("Password")).toBeDisabled();
+ expect(screen.getByRole("button")).toBeDisabled();
+ });
+
+ it("should display error message when error prop is provided", (): void => {
+ render(
+
+ );
+ expect(
+ screen.getByText("The email and password combination wasn't recognized.")
+ ).toBeInTheDocument();
+ });
+
+ it("should dismiss error when dismiss button is clicked", async (): Promise => {
+ const user = userEvent.setup();
+ render(
+
+ );
+
+ expect(
+ screen.getByText("Authentication paused. Please try again when ready.")
+ ).toBeInTheDocument();
+
+ const dismissButton = screen.getByLabelText("Dismiss");
+ await user.click(dismissButton);
+
+ expect(
+ screen.queryByText("Authentication paused. Please try again when ready.")
+ ).not.toBeInTheDocument();
+ });
+
+ it("should have htmlFor on email label pointing to email input", (): void => {
+ render();
+ const emailLabel = screen.getByText("Email");
+ const emailInput = screen.getByLabelText("Email");
+ expect(emailLabel).toHaveAttribute("for", emailInput.id);
+ });
+
+ it("should have htmlFor on password label pointing to password input", (): void => {
+ render();
+ const passwordLabel = screen.getByText("Password");
+ const passwordInput = screen.getByLabelText("Password");
+ expect(passwordLabel).toHaveAttribute("for", passwordInput.id);
+ });
+
+ it("should clear email validation error when user types a valid email", async (): Promise => {
+ const user = userEvent.setup();
+ render();
+
+ const emailInput = screen.getByLabelText("Email");
+ const submitButton = screen.getByRole("button", { name: "Continue" });
+
+ // Trigger validation error
+ await user.type(emailInput, "invalid");
+ await user.click(submitButton);
+ expect(screen.getByText("Please enter a valid email address.")).toBeInTheDocument();
+
+ // Fix the email
+ await user.clear(emailInput);
+ await user.type(emailInput, "user@example.com");
+
+ await waitFor((): void => {
+ expect(screen.queryByText("Please enter a valid email address.")).not.toBeInTheDocument();
+ });
+ });
+
+ it("should set aria-invalid on email input when validation fails", async (): Promise => {
+ const user = userEvent.setup();
+ render();
+
+ const emailInput = screen.getByLabelText("Email");
+ const submitButton = screen.getByRole("button", { name: "Continue" });
+
+ await user.type(emailInput, "invalid");
+ await user.click(submitButton);
+
+ expect(emailInput).toHaveAttribute("aria-invalid", "true");
+ });
+});
diff --git a/apps/web/src/components/auth/LoginForm.tsx b/apps/web/src/components/auth/LoginForm.tsx
new file mode 100644
index 0000000..d20c496
--- /dev/null
+++ b/apps/web/src/components/auth/LoginForm.tsx
@@ -0,0 +1,172 @@
+"use client";
+
+import { useRef, useEffect, useState, useCallback } from "react";
+import type { ReactElement } from "react";
+import { Loader2 } from "lucide-react";
+import { AuthErrorBanner } from "./AuthErrorBanner";
+
+export interface LoginFormProps {
+ onSubmit: (email: string, password: string) => void | Promise;
+ isLoading?: boolean;
+ error?: string | null;
+}
+
+export function LoginForm({
+ onSubmit,
+ isLoading = false,
+ error = null,
+}: LoginFormProps): ReactElement {
+ const emailRef = useRef(null);
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [emailError, setEmailError] = useState(null);
+ const [passwordError, setPasswordError] = useState(null);
+ const [dismissedError, setDismissedError] = useState(false);
+
+ useEffect((): void => {
+ emailRef.current?.focus();
+ }, []);
+
+ // Reset dismissed state when a new error comes in
+ useEffect((): void => {
+ if (error) {
+ setDismissedError(false);
+ }
+ }, [error]);
+
+ const validateEmail = useCallback((value: string): boolean => {
+ if (!value.includes("@")) {
+ setEmailError("Please enter a valid email address.");
+ return false;
+ }
+ setEmailError(null);
+ return true;
+ }, []);
+
+ const validatePassword = useCallback((value: string): boolean => {
+ if (value.length === 0) {
+ setPasswordError("Password is recommended.");
+ return false;
+ }
+ setPasswordError(null);
+ return true;
+ }, []);
+
+ const handleSubmit = (e: React.SyntheticEvent): void => {
+ e.preventDefault();
+
+ const isEmailValid = validateEmail(email);
+ const isPasswordValid = validatePassword(password);
+
+ if (!isEmailValid || !isPasswordValid) {
+ return;
+ }
+
+ void onSubmit(email, password);
+ };
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/components/auth/OAuthButton.test.tsx b/apps/web/src/components/auth/OAuthButton.test.tsx
new file mode 100644
index 0000000..f6fb417
--- /dev/null
+++ b/apps/web/src/components/auth/OAuthButton.test.tsx
@@ -0,0 +1,89 @@
+import { describe, it, expect, vi } from "vitest";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { OAuthButton } from "./OAuthButton";
+
+describe("OAuthButton", (): void => {
+ const defaultProps = {
+ providerName: "Authentik",
+ providerId: "authentik",
+ onClick: vi.fn(),
+ };
+
+ it("should render with provider name", (): void => {
+ render();
+ expect(screen.getByText("Continue with Authentik")).toBeInTheDocument();
+ });
+
+ it("should have full width styling", (): void => {
+ render();
+ const button = screen.getByRole("button");
+ expect(button.className).toContain("w-full");
+ });
+
+ it("should call onClick when clicked", async (): Promise => {
+ const user = userEvent.setup();
+ const onClick = vi.fn();
+ render();
+
+ const button = screen.getByRole("button");
+ await user.click(button);
+
+ expect(onClick).toHaveBeenCalledTimes(1);
+ });
+
+ it("should show loading state with spinner and Connecting text", (): void => {
+ render();
+ expect(screen.getByText("Connecting...")).toBeInTheDocument();
+ expect(screen.queryByText("Continue with Authentik")).not.toBeInTheDocument();
+ });
+
+ it("should be disabled when isLoading is true", (): void => {
+ render();
+ const button = screen.getByRole("button");
+ expect(button).toBeDisabled();
+ });
+
+ it("should be disabled when disabled prop is true", (): void => {
+ render();
+ const button = screen.getByRole("button");
+ expect(button).toBeDisabled();
+ });
+
+ it("should have reduced opacity when disabled", (): void => {
+ render();
+ const button = screen.getByRole("button");
+ expect(button.className).toContain("opacity-50");
+ expect(button.className).toContain("pointer-events-none");
+ });
+
+ it("should have aria-label with provider name", (): void => {
+ render();
+ const button = screen.getByRole("button");
+ expect(button).toHaveAttribute("aria-label", "Continue with Authentik");
+ });
+
+ it("should have aria-label Connecting when loading", (): void => {
+ render();
+ const button = screen.getByRole("button");
+ expect(button).toHaveAttribute("aria-label", "Connecting");
+ });
+
+ it("should render a spinner SVG when loading", (): void => {
+ const { container } = render();
+ const spinner = container.querySelector("svg");
+ expect(spinner).toBeInTheDocument();
+ expect(spinner?.getAttribute("class")).toContain("animate-spin");
+ });
+
+ it("should not call onClick when disabled", async (): Promise => {
+ const user = userEvent.setup();
+ const onClick = vi.fn();
+ render();
+
+ const button = screen.getByRole("button");
+ await user.click(button);
+
+ expect(onClick).not.toHaveBeenCalled();
+ });
+});
diff --git a/apps/web/src/components/auth/OAuthButton.tsx b/apps/web/src/components/auth/OAuthButton.tsx
new file mode 100644
index 0000000..afd1023
--- /dev/null
+++ b/apps/web/src/components/auth/OAuthButton.tsx
@@ -0,0 +1,49 @@
+"use client";
+
+import type { ReactElement } from "react";
+import { Loader2 } from "lucide-react";
+
+export interface OAuthButtonProps {
+ providerName: string;
+ providerId: string;
+ onClick: () => void;
+ isLoading?: boolean;
+ disabled?: boolean;
+}
+
+export function OAuthButton({
+ providerName,
+ onClick,
+ isLoading = false,
+ disabled = false,
+}: OAuthButtonProps): ReactElement {
+ const isDisabled = disabled || isLoading;
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/components/auth/SessionExpiryWarning.test.tsx b/apps/web/src/components/auth/SessionExpiryWarning.test.tsx
new file mode 100644
index 0000000..811621d
--- /dev/null
+++ b/apps/web/src/components/auth/SessionExpiryWarning.test.tsx
@@ -0,0 +1,79 @@
+import { describe, it, expect, vi } from "vitest";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { SessionExpiryWarning } from "./SessionExpiryWarning";
+
+describe("SessionExpiryWarning", (): void => {
+ it("should render with minutes remaining", (): void => {
+ render();
+ expect(screen.getByText(/your session will end in 5 minutes/i)).toBeInTheDocument();
+ });
+
+ it("should use singular 'minute' when 1 minute remaining", (): void => {
+ render();
+ expect(screen.getByText(/your session will end in 1 minute\./i)).toBeInTheDocument();
+ });
+
+ it("should show extend button when onExtend is provided", (): void => {
+ const onExtend = vi.fn();
+ render();
+ expect(screen.getByText("Extend Session")).toBeInTheDocument();
+ });
+
+ it("should not show extend button when onExtend is not provided", (): void => {
+ render();
+ expect(screen.queryByText("Extend Session")).not.toBeInTheDocument();
+ });
+
+ it("should call onExtend when extend button is clicked", async (): Promise => {
+ const user = userEvent.setup();
+ const onExtend = vi.fn();
+ render();
+
+ await user.click(screen.getByText("Extend Session"));
+ expect(onExtend).toHaveBeenCalledOnce();
+ });
+
+ it("should show dismiss button when onDismiss is provided", (): void => {
+ const onDismiss = vi.fn();
+ render();
+ expect(screen.getByLabelText("Dismiss")).toBeInTheDocument();
+ });
+
+ it("should not show dismiss button when onDismiss is not provided", (): void => {
+ render();
+ expect(screen.queryByLabelText("Dismiss")).not.toBeInTheDocument();
+ });
+
+ it("should call onDismiss when dismiss button is clicked", async (): Promise => {
+ const user = userEvent.setup();
+ const onDismiss = vi.fn();
+ render();
+
+ await user.click(screen.getByLabelText("Dismiss"));
+ expect(onDismiss).toHaveBeenCalledOnce();
+ });
+
+ it("should have role='status' for accessibility", (): void => {
+ render();
+ expect(screen.getByRole("status")).toBeInTheDocument();
+ });
+
+ it("should have aria-live='polite' for screen readers", (): void => {
+ render();
+ const statusElement = screen.getByRole("status");
+ expect(statusElement).toHaveAttribute("aria-live", "polite");
+ });
+
+ it("should use blue theme (not red) for PDA-friendly design", (): void => {
+ render();
+ const statusElement = screen.getByRole("status");
+ expect(statusElement.className).toContain("bg-blue-50");
+ expect(statusElement.className).toContain("border-blue-200");
+ });
+
+ it("should include saving work reminder in message", (): void => {
+ render();
+ expect(screen.getByText(/consider saving your work/i)).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/src/components/auth/SessionExpiryWarning.tsx b/apps/web/src/components/auth/SessionExpiryWarning.tsx
new file mode 100644
index 0000000..ac205c9
--- /dev/null
+++ b/apps/web/src/components/auth/SessionExpiryWarning.tsx
@@ -0,0 +1,50 @@
+import { Info, X } from "lucide-react";
+
+interface SessionExpiryWarningProps {
+ minutesRemaining: number;
+ onExtend?: () => void;
+ onDismiss?: () => void;
+}
+
+export function SessionExpiryWarning({
+ minutesRemaining,
+ onExtend,
+ onDismiss,
+}: SessionExpiryWarningProps): React.ReactElement {
+ return (
+
+
+
+
+ Your session will end in {minutesRemaining}{" "}
+ {minutesRemaining === 1 ? "minute" : "minutes"}. Consider saving your work.
+
+
+ {onExtend ? (
+
+ ) : null}
+
+
+ {onDismiss ? (
+
+ ) : null}
+
+ );
+}
diff --git a/apps/web/src/providers/ThemeProvider.test.tsx b/apps/web/src/providers/ThemeProvider.test.tsx
new file mode 100644
index 0000000..5d592b5
--- /dev/null
+++ b/apps/web/src/providers/ThemeProvider.test.tsx
@@ -0,0 +1,120 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { render, screen, act } from "@testing-library/react";
+import { ThemeProvider, useTheme } from "./ThemeProvider";
+
+function ThemeConsumer(): React.JSX.Element {
+ const { theme, resolvedTheme, setTheme, toggleTheme } = useTheme();
+ return (
+
+ {theme}
+ {resolvedTheme}
+
+
+
+
+ );
+}
+
+describe("ThemeProvider", (): void => {
+ let mockMatchMedia: ReturnType;
+
+ beforeEach((): void => {
+ localStorage.clear();
+ document.documentElement.classList.remove("light", "dark");
+
+ mockMatchMedia = vi.fn().mockReturnValue({
+ matches: false,
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ });
+ Object.defineProperty(window, "matchMedia", {
+ writable: true,
+ value: mockMatchMedia,
+ });
+ });
+
+ afterEach((): void => {
+ vi.restoreAllMocks();
+ });
+
+ it("should use 'mosaic-theme' as storage key", (): void => {
+ localStorage.setItem("mosaic-theme", "light");
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByTestId("theme")).toHaveTextContent("light");
+ });
+
+ it("should NOT read from old 'jarvis-theme' storage key", (): void => {
+ localStorage.setItem("jarvis-theme", "light");
+
+ render(
+
+
+
+ );
+
+ // Should default to system, not read from jarvis-theme
+ expect(screen.getByTestId("theme")).toHaveTextContent("system");
+ });
+
+ it("should store theme under 'mosaic-theme' key", (): void => {
+ render(
+
+
+
+ );
+
+ act(() => {
+ screen.getByText("Set Light").click();
+ });
+
+ expect(localStorage.getItem("mosaic-theme")).toBe("light");
+ expect(localStorage.getItem("jarvis-theme")).toBeNull();
+ });
+
+ it("should render children", (): void => {
+ render(
+
+ Hello
+
+ );
+
+ expect(screen.getByTestId("child")).toBeInTheDocument();
+ });
+
+ it("should throw when useTheme is used outside provider", (): void => {
+ // Suppress console.error for expected error
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {
+ // Intentionally empty
+ });
+
+ expect(() => {
+ render();
+ }).toThrow("useTheme must be used within a ThemeProvider");
+
+ consoleSpy.mockRestore();
+ });
+});
diff --git a/apps/web/src/providers/ThemeProvider.tsx b/apps/web/src/providers/ThemeProvider.tsx
index d199ece..623e7fb 100644
--- a/apps/web/src/providers/ThemeProvider.tsx
+++ b/apps/web/src/providers/ThemeProvider.tsx
@@ -13,7 +13,7 @@ interface ThemeContextValue {
const ThemeContext = createContext(null);
-const STORAGE_KEY = "jarvis-theme";
+const STORAGE_KEY = "mosaic-theme";
function getSystemTheme(): "light" | "dark" {
if (typeof window === "undefined") return "dark";
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1ae2c3e..a21fc33 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -7408,7 +7408,7 @@ snapshots:
chalk: 5.6.2
commander: 12.1.0
dotenv: 17.2.4
- drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
+ drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
open: 10.2.0
pg: 8.17.2
prettier: 3.8.1
@@ -10410,7 +10410,7 @@ snapshots:
optionalDependencies:
'@prisma/client': 5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
better-sqlite3: 12.6.2
- drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
+ drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
next: 16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
pg: 8.17.2
prisma: 6.19.2(magicast@0.3.5)(typescript@5.9.3)
@@ -10435,7 +10435,7 @@ snapshots:
optionalDependencies:
'@prisma/client': 6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3)
better-sqlite3: 12.6.2
- drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
+ drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
next: 16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
pg: 8.17.2
prisma: 6.19.2(magicast@0.3.5)(typescript@5.9.3)
@@ -11229,6 +11229,17 @@ snapshots:
dotenv@17.2.4: {}
+ drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)):
+ optionalDependencies:
+ '@opentelemetry/api': 1.9.0
+ '@prisma/client': 5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
+ '@types/pg': 8.16.0
+ better-sqlite3: 12.6.2
+ kysely: 0.28.10
+ pg: 8.17.2
+ postgres: 3.4.8
+ prisma: 6.19.2(magicast@0.3.5)(typescript@5.9.3)
+
drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)):
optionalDependencies:
'@opentelemetry/api': 1.9.0
@@ -11239,6 +11250,7 @@ snapshots:
pg: 8.17.2
postgres: 3.4.8
prisma: 6.19.2(magicast@0.3.5)(typescript@5.9.3)
+ optional: true
dunder-proto@1.0.1:
dependencies: