diff --git a/.gitignore b/.gitignore
index 5ef6a52..e25f3d5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,7 @@
# misc
.DS_Store
*.pem
+.idea
# debug
npm-debug.log*
diff --git a/README.md b/README.md
index 3774656..ec84385 100644
--- a/README.md
+++ b/README.md
@@ -1,91 +1,64 @@
-# React Developer Assignment: Cross-Tab Collaboration Dashboard
+# Cross‑Tab Collaboration Dashboard
-## Time Limit: 3 hours
+Simple Next.js demo that syncs presence, a shared counter, chat (typing + delete‑own + expiration), and theme across browser tabs using `react-broadcast-sync`.
-You are expected to focus on the core requirements. Bonus features are optional and may be partially implemented if time allows.
-
-## Overview
-
-Build a real-time collaboration dashboard that synchronizes user activity across multiple browser tabs using the
-react-broadcast-sync library.
+Open this page in multiple tabs to try it.
---
-## Setup Instructions
+## Quick start
-1. Create a new React project using any setup you prefer (e.g., Vite, CRA, Next.js)
-2. Install the required package:
- ```bash
- npm install react-broadcast-sync
- ```
-3. Use any styling approach (CSS, Tailwind, MUI, etc.)
+1) Install and run
+```bash
+pnpm install
+pnpm dev
+# then open http://localhost:3000
+```
+
+2) Optional: install the library directly (already listed in dependencies)
+```bash
+pnpm add react-broadcast-sync
+```
---
-## Requirements - Mandatory
-
-### 1. Custom Hook
-Create a custom hook called `useCollaborativeSession` that:
-- Sets up the broadcast channel
-- Manages internal state for users, chat, counter
-- Exposes state and actions:
- - `users`, `messages`, `counter`
- - `sendMessage()`, `updateCounter()`, `markTyping()`, etc.
-- Internally uses `react-broadcast-sync` to handle cross-tab communication
-
-### 2. User Presence System
-- Detect and display active users (based on tabs)
-- Show a user list with:
- - Username or ID (you can generate random names if needed)
- - Last activity timestamp
-- Detect and visually indicate when a user joins or leaves
-
-### 3. Shared Counter
-- Counter that stays synchronized across all tabs
-- Any user can increment/decrement the value
-- Show which user performed the last action
-- Display timestamp of last action
-
-### 4. Real-time Chat
-- Text area for message writing
-- Show typing indicators when users are actively typing
-- Synchronize conversation content across all tabs
-- For each message display which user sent it and its timestamp
-- Allow users to delete **their own** messages from the chat with syncing across tabs
-- Allow users to send a messages with expiration
-
-### 5. Technical Standards
-- Use proper error handling and cleanup
-- Use TypeScript or well-typed PropTypes
-- Abstract logic into reusable components/hooks
-- Keep the code modular, readable, and clean
-- Synchronize existing state on page load (rehydrate from current messages/users)
+## What’s inside
----
+- Custom hook `useCollaborativeSession`
+ - Uses `useBroadcastChannel('collab-dashboard')` from `react-broadcast-sync`
+ - Presence: per‑tab join/leave + lastActive
+ - Chat: send, delete own, timestamps, optional expiration, typing indicators (debounced)
+ - Counter: synced value + last actor and time
+ - Rehydration: new tab requests snapshot, existing tabs respond; merge is deterministic
+ - Theme sync (bonus): toggled theme broadcasts to all tabs
-## Bonus Features
-- Theme sync across tabs (light/dark mode)
-- Include debouncing for frequent updates
-- Implement loading states
-- Add responsive layout
-- Activity feed showing recent actions
-- User avatar system
-- Focus/cursor position indicators
+- Components
+ - `PresenceList` – active users with last seen
+ - `SharedCounter` – inc/dec + last action
+ - `ChatPanel` – messages, typing, delete‑own, expiration input
---
-## Deliverables
-1. Complete source code
-2. README with setup instructions and implementation notes
-3. Working demo (open multiple tabs to test)
+## How to test
+
+1. Open two tabs at `http://localhost:3000`.
+2. Type in one tab – the other shows a typing indicator.
+3. Send a message, try deleting your own; add an expiration (ms) and watch it disappear.
+4. Increment/decrement the counter – value and last actor/time stay in sync.
+5. Toggle the theme – other tabs follow instantly.
---
-## Evaluation Criteria
-- Proper use and integration of `react-broadcast-sync`
-- Correct and clean custom hook abstraction
-- Working real-time sync across tabs
-- Well-structured and maintainable code
-- Functional and user-friendly UI
-- Handling of edge cases (e.g., expired messages, tab close)
-- Bonus points for creative features, polish, or great UX
+## Notes
+
+- User identity (`userId`) now persists per browser profile using `localStorage` (`collab_user_id`); opening new tabs shares the same user while each tab still has a unique `tabId` for presence granularity.
+- The hook clears processed messages via `clearReceivedMessages` to avoid reprocessing.
+- Expired messages are pruned every 2s.
+- Presence pings every 5s to refresh `lastActive`.
+- Identity (username) is deterministic from the stable `userId` via a hash (see `randomUsername`).
+- Stale presence cleanup: tabs inactive >30s are pruned (unless it's your own user).
+- Deterministic avatar colors derived from `userId` (see `avatarColorFromUserId`).
+- Focus updates throttled (120ms) to reduce broadcast spam.
+- Feed now includes expiration events (`chat:expire`).
+
+---
diff --git a/app/layout.tsx b/app/layout.tsx
index 42fc323..f177b61 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
-import "./globals.css";
+import { CollaborativeSessionProvider } from "@/lib/context/CollaborativeSessionContext";
+import "../globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -13,8 +14,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "Collaboration Dashboard",
+ description: "Simple cross‑tab presence, chat, and counter demo",
};
export default function RootLayout({
@@ -25,7 +26,9 @@ export default function RootLayout({
return (
- {children}
+
+ {children}
+
);
diff --git a/app/page.tsx b/app/page.tsx
index e974b4c..866c255 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,97 +1,100 @@
-import Image from "next/image";
-import styles from "./page.module.css";
+"use client";
+import React from "react";
+import { useCollaborativeSessionContext } from "@/lib/context/CollaborativeSessionContext";
+import PresenceList from "@/components/PresenceList";
+import SharedCounter from "@/components/SharedCounter";
+import ChatPanel from "@/components/ChatPanel";
+import { Button } from "@/components/ui/button";
+import { Sun, Moon } from "lucide-react";
+import ActivityFeed from "@/components/ActivityFeed";
export default function Home() {
- return (
-
-
Home
-
-
-
-
-
- Get started by editing app/page.tsx.
-
- Save and see your changes instantly.
-
+ const {
+ loading,
+ users,
+ messages,
+ counter,
+ typingUsers,
+ theme,
+ currentUser,
+ sendMessage,
+ deleteMessage,
+ updateCounter,
+ markTyping,
+ toggleTheme,
+ feed,
+ focus,
+ updateFocus,
+ } = useCollaborativeSessionContext();
-
);
}
diff --git a/components/ActivityFeed.tsx b/components/ActivityFeed.tsx
new file mode 100644
index 0000000..2753456
--- /dev/null
+++ b/components/ActivityFeed.tsx
@@ -0,0 +1,58 @@
+"use client";
+import React from "react";
+import type { FeedItem } from "@/lib/types/session";
+import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
+import { History } from "lucide-react";
+
+const KIND_LABEL: Record = {
+ "presence:join": "Joined",
+ "presence:leave": "Left",
+ "chat:send": "Message",
+ "chat:delete": "Delete",
+ "counter:update": "Counter",
+ "theme": "Theme",
+ "rehydrate": "Sync",
+};
+
+const kindColor = (kind: string) => {
+ if (kind.startsWith("presence")) return "bg-sky-500";
+ if (kind.startsWith("chat")) return "bg-purple-500";
+ if (kind.startsWith("counter")) return "bg-amber-500";
+ if (kind === "theme") return "bg-rose-500";
+ if (kind === "rehydrate") return "bg-teal-500";
+ return "bg-muted";
+};
+
+export const ActivityFeed = ({ items }: { items: FeedItem[] }) => {
+ return (
+
+
+
+ Activity
+
+
+
+ {items.length === 0 ? (
+ No activity yet
+ ) : (
+
+ {items.map(i => (
+
+
+
+
+ {KIND_LABEL[i.kind] ?? i.kind}
+ {new Date(i.ts).toLocaleTimeString()}
+
+
{i.text}{i.actorName ? ({i.actorName}) : null}
+
+
+ ))}
+
+ )}
+
+
+ );
+};
+
+export default ActivityFeed;
diff --git a/components/ChatPanel.tsx b/components/ChatPanel.tsx
new file mode 100644
index 0000000..6541809
--- /dev/null
+++ b/components/ChatPanel.tsx
@@ -0,0 +1,159 @@
+"use client";
+import React, { useMemo } from "react";
+import type { ChatMessage } from "@/lib/types/session";
+import { useForm } from "react-hook-form";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { MessageSquare, Trash2, Clock, Send } from "lucide-react";
+
+type Props = {
+ messages: ChatMessage[];
+ currentUserId: string;
+ typingUsers: { userId: string; username: string }[];
+ onSendAction: (text: string, opts?: { expiresInMs?: number }) => void;
+ onDeleteAction: (id: string) => void;
+ onTypingAction: () => void;
+ onFocusUpdateAction?: (element: string, cursorPos?: number) => void;
+};
+
+export function ChatPanel({ messages, currentUserId, typingUsers, onSendAction, onDeleteAction, onTypingAction, onFocusUpdateAction }: Props) {
+ const { register, handleSubmit, reset, watch } = useForm<{ text: string; expires: number | string }>({
+ defaultValues: { text: "", expires: "0" },
+ });
+
+ const submit = handleSubmit(({ text, expires }) => {
+ const content = (text ?? "").trim();
+ if (!content) return;
+ const expiresInMs = parseInt(String(expires || 0), 10) || 0;
+ onSendAction(content, { expiresInMs: expiresInMs > 0 ? expiresInMs : undefined });
+ reset({ text: "", expires });
+ });
+
+ const visibleMessages = useMemo(() => messages.filter((m) => !m.deleted), [messages]);
+ const textValue = watch("text") ?? "";
+
+ return (
+
+
+
+
+ Chat
+ {visibleMessages.length > 0 && (
+
+ {visibleMessages.length} message{visibleMessages.length !== 1 ? "s" : ""}
+
+ )}
+
+
+
+
+ {visibleMessages.length === 0 ? (
+
+
+
+
No messages yet
+
Say hello to get started!
+
+
+ ) : (
+ visibleMessages.map((m) => {
+ const isSelf = m.userId === currentUserId;
+ const time = new Date(m.timestamp).toLocaleTimeString();
+ const expiresIn = m.expiresAt ? m.expiresAt - Date.now() : 0;
+ return (
+
+
+
+
+ {m.username.slice(0, 2).toUpperCase()}
+
+
+
+ {m.username}
+ {isSelf && (
+
+ You
+
+ )}
+
+
{time}
+
+
+ {isSelf && (
+
onDeleteAction(m.id)}
+ className="opacity-0 group-hover:opacity-100 transition-opacity"
+ aria-label="delete message"
+ variant="ghost"
+ size="sm"
+ >
+
+
+ )}
+
+
{m.text}
+ {m.expiresAt && (
+
+
+ Expires {expiresIn > 0 ? `in ${Math.ceil(expiresIn / 1000)}s` : "soon"}
+
+ )}
+
+ );
+ })
+ )}
+
+ {typingUsers.length > 0 && (
+
+
+
+
+
+
+
{typingUsers.map((t) => t.username).join(", ")} typing...
+
+ )}
+
+
+
+ );
+}
+
+export default ChatPanel;
diff --git a/components/PresenceList.tsx b/components/PresenceList.tsx
new file mode 100644
index 0000000..dbcf362
--- /dev/null
+++ b/components/PresenceList.tsx
@@ -0,0 +1,68 @@
+"use client";
+import React from "react";
+import type { User } from "@/lib/types/session";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Users } from "lucide-react";
+
+type Props = {
+ users: User[];
+ currentUserId: string;
+};
+
+export const PresenceList = ({ users, currentUserId }: Props) => {
+ return (
+
+
+
+
+
+ Active Users
+
+
+ {users.length}
+
+
+
+
+
+ {users.map((u) => {
+ const isSelf = u.userId === currentUserId;
+ const lastSeen = new Date(u.lastActive).toLocaleTimeString();
+ return (
+
+
+
+
+ {u.username}
+ {isSelf && (
+
+ You
+
+ )}
+
+
+ {lastSeen}
+
+
+
+ );
+ })}
+
+
+
+ );
+};
+
+export default PresenceList;
diff --git a/components/SharedCounter.tsx b/components/SharedCounter.tsx
new file mode 100644
index 0000000..5944375
--- /dev/null
+++ b/components/SharedCounter.tsx
@@ -0,0 +1,61 @@
+"use client";
+import React from "react";
+import type { CounterState } from "@/lib/types/session";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Hash } from "lucide-react";
+
+type Props = {
+ counter: CounterState;
+ onChangeAction: (delta: number) => void;
+};
+
+export const SharedCounter = ({ counter, onChangeAction }: Props) => {
+ const last = counter.lastUpdatedAt ? new Date(counter.lastUpdatedAt).toLocaleTimeString() : "-";
+ return (
+
+
+
+
+ Shared Counter
+
+
+
+
+
onChangeAction(-1)}
+ aria-label="decrement"
+ variant="outline"
+ size="sm"
+ className="h-12 w-12 rounded-lg text-lg font-bold hover:scale-[1.03] transition-transform"
+ >
+ −
+
+
+ {counter.value}
+
+
onChangeAction(1)}
+ aria-label="increment"
+ variant="outline"
+ size="sm"
+ className="h-12 w-12 rounded-lg text-lg font-bold hover:scale-[1.03] transition-transform"
+ >
+ +
+
+
+
+
+ {counter.lastActorName ? (
+ <>Last updated by {counter.lastActorName} at {last}>
+ ) : (
+ "No updates yet"
+ )}
+
+
+
+
+ );
+};
+
+export default SharedCounter;
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
new file mode 100644
index 0000000..44ccc12
--- /dev/null
+++ b/components/ui/button.tsx
@@ -0,0 +1,46 @@
+"use client";
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+import { cn } from "@/lib/utils";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center whitespace-nowrap rounded-lg text-sm font-semibold transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background active:scale-95",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow-md shadow-primary/25 hover:bg-primary/90 hover:shadow-lg hover:shadow-primary/30",
+ outline:
+ "border-2 border-input bg-transparent hover:bg-accent hover:text-accent-foreground hover:border-primary/50",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-8 rounded-md px-3 text-xs",
+ lg: "h-11 rounded-lg px-8 text-base",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+);
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, ...props }, ref) => {
+ return (
+
+ );
+ }
+);
+Button.displayName = "Button";
+
+export { Button, buttonVariants };
diff --git a/components/ui/card.tsx b/components/ui/card.tsx
new file mode 100644
index 0000000..a43799c
--- /dev/null
+++ b/components/ui/card.tsx
@@ -0,0 +1,33 @@
+"use client";
+import * as React from "react";
+import { cn } from "@/lib/utils";
+
+const Card = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+Card.displayName = "Card";
+
+const CardHeader = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+CardHeader.displayName = "CardHeader";
+
+const CardTitle = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+CardTitle.displayName = "CardTitle";
+
+const CardContent = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+CardContent.displayName = "CardContent";
+
+export { Card, CardHeader, CardTitle, CardContent };
diff --git a/components/ui/input.tsx b/components/ui/input.tsx
new file mode 100644
index 0000000..4ba6c10
--- /dev/null
+++ b/components/ui/input.tsx
@@ -0,0 +1,21 @@
+"use client";
+import * as React from "react";
+import { cn } from "@/lib/utils";
+
+type InputProps = React.InputHTMLAttributes;
+
+const Input = React.forwardRef(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+Input.displayName = "Input";
+
+export { Input };
diff --git a/components/ui/label.tsx b/components/ui/label.tsx
new file mode 100644
index 0000000..4ede3a6
--- /dev/null
+++ b/components/ui/label.tsx
@@ -0,0 +1,16 @@
+"use client";
+import * as React from "react";
+import { cn } from "@/lib/utils";
+
+type LabelProps = React.LabelHTMLAttributes;
+
+const Label = React.forwardRef(({ className, ...props }, ref) => (
+
+));
+Label.displayName = "Label";
+
+export { Label };
diff --git a/components/ui/textarea.tsx b/components/ui/textarea.tsx
new file mode 100644
index 0000000..431568c
--- /dev/null
+++ b/components/ui/textarea.tsx
@@ -0,0 +1,21 @@
+"use client";
+import * as React from "react";
+import { cn } from "@/lib/utils";
+
+type TextareaProps = React.TextareaHTMLAttributes;
+
+const Textarea = React.forwardRef(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+Textarea.displayName = "Textarea";
+
+export { Textarea };
diff --git a/globals.css b/globals.css
new file mode 100644
index 0000000..8b3f63b
--- /dev/null
+++ b/globals.css
@@ -0,0 +1,84 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ /*
+ NOTE: These custom properties are consumed as hsl(var(--token)) in tailwind.config.ts.
+ They must be HSL component triples: .
+ Previous values looked like RGB triples (e.g. 240 244 248) which produced invalid / extreme HSL and broken styles.
+ Adjusted to a balanced light theme palette.
+ */
+ --background: 0 0% 100%; /* white */
+ --foreground: 224 71% 4%;
+ --card: 0 0% 100%;
+ --card-foreground: 224 71% 4%;
+ --primary: 243 75% 59%; /* indigo-500 */
+ --primary-foreground: 0 0% 100%;
+ --secondary: 240 5% 96%;
+ --secondary-foreground: 240 6% 10%;
+ --muted: 240 5% 96%;
+ --muted-foreground: 240 4% 46%;
+ --accent: 270 100% 97%; /* light violet */
+ --accent-foreground: 271 74% 39%;
+ --destructive: 0 84% 60%;
+ --destructive-foreground: 0 0% 100%;
+ --border: 240 6% 90%;
+ --input: 240 6% 90%;
+ --ring: 243 75% 59%;
+ --radius: 0.75rem;
+ --success: 142 72% 29%; /* green-700 */
+ --warning: 31 92% 50%; /* amber-500 */
+ }
+
+ html[data-theme="dark"] {
+ /* Dark theme palette mirroring light tokens */
+ --background: 240 6% 10%;
+ --foreground: 0 0% 98%;
+ --card: 240 5% 15%;
+ --card-foreground: 0 0% 98%;
+ --primary: 243 75% 59%;
+ --primary-foreground: 0 0% 100%;
+ --secondary: 240 4% 18%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 240 4% 18%;
+ --muted-foreground: 240 5% 65%;
+ --accent: 270 60% 30%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 63% 45%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 4% 25%;
+ --input: 240 4% 25%;
+ --ring: 243 75% 59%;
+ --success: 142 72% 40%;
+ --warning: 31 92% 60%;
+ }
+
+ * {
+ @apply border-border;
+ }
+
+ body {
+ @apply bg-background text-foreground antialiased;
+ font-feature-settings: "rlig" 1, "calt" 1;
+ }
+
+ /* Custom scrollbar */
+ ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ }
+
+ ::-webkit-scrollbar-track {
+ @apply bg-transparent;
+ }
+
+ ::-webkit-scrollbar-thumb {
+ @apply bg-muted-foreground/30 rounded-full;
+ }
+
+ ::-webkit-scrollbar-thumb:hover {
+ @apply bg-muted-foreground/50;
+ }
+}
diff --git a/jest.config.ts b/jest.config.ts
index 6536ee7..b352675 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -8,6 +8,14 @@ const createJestConfig = nextJest({
const customJestConfig: Config = {
setupFilesAfterEnv: ["/jest.setup.ts"],
testEnvironment: "jsdom",
+ // Allow transforming specific ESM packages in node_modules (e.g., lodash-es)
+ transformIgnorePatterns: [
+ "/node_modules/(?!lodash-es/|nanoid/)",
+ ],
+ moduleNameMapper: {
+ // Use CJS build in tests to avoid ESM parsing issues
+ "^lodash-es$": "lodash",
+ },
};
export default createJestConfig(customJestConfig);
diff --git a/jest.setup.ts b/jest.setup.ts
index 7b0828b..37dca5d 100644
--- a/jest.setup.ts
+++ b/jest.setup.ts
@@ -1 +1,30 @@
import '@testing-library/jest-dom';
+// Mock react-broadcast-sync to avoid cross-tab side effects in tests
+// Provide a shared channel instance with spies to assert calls
+const channelData: { bus: Array<{id:string; type:string; message:any}>; subs: Set<(msgs:any[])=>void> } = { bus: [], subs: new Set() };
+function useBroadcastChannelMock() {
+ const React = require('react');
+ const [messages, setMessages] = React.useState(channelData.bus);
+ React.useEffect(() => {
+ channelData.subs.add(setMessages);
+ return () => { channelData.subs.delete(setMessages); };
+ }, []);
+ const broadcastUpdate = () => {
+ channelData.bus = [...channelData.bus];
+ channelData.subs.forEach(fn => fn(channelData.bus));
+ };
+ const postMessage = (type: string, message: any) => {
+ channelData.bus.push({ id: Math.random().toString(36), type, message });
+ broadcastUpdate();
+ };
+ const clearReceivedMessages = ({ ids }: { ids: string[] }) => {
+ channelData.bus = channelData.bus.filter(m => !ids.includes(m.id));
+ broadcastUpdate();
+ };
+ return { messages, postMessage, clearReceivedMessages };
+}
+jest.mock('react-broadcast-sync', () => ({ useBroadcastChannel: () => useBroadcastChannelMock() }));
+// Mock nanoid to avoid ESM transform issues and to produce stable IDs in tests
+let __idCounter = 0;
+jest.mock('nanoid', () => ({ nanoid: () => `test-id-${++__idCounter}` }));
+// Note: IDs in app code use nanoid; no crypto.randomUUID polyfill needed
diff --git a/lib/__tests__/hook.behavior.test.ts b/lib/__tests__/hook.behavior.test.ts
new file mode 100644
index 0000000..1499bc9
--- /dev/null
+++ b/lib/__tests__/hook.behavior.test.ts
@@ -0,0 +1,74 @@
+import React from "react";
+import { renderHook, act } from "@testing-library/react";
+import { useCollaborativeSession } from "@/lib/hooks/useCollaborativeSession";
+import { CollabEventType } from "@/lib/types/events";
+
+jest.mock("react-broadcast-sync", () => {
+ const React = require("react");
+ let bus: Array<{ id: string; type: string; message: any }> = [];
+ const subscribers = new Set>>();
+ const notify = () => {
+ const snapshot = [...bus];
+ subscribers.forEach(set => set(snapshot));
+ };
+ const push = (type: string, message: any) => {
+ bus.push({ id: Math.random().toString(36).slice(2), type, message });
+ notify();
+ };
+ return {
+ useBroadcastChannel: () => {
+ const [messages, setMessages] = React.useState(bus);
+ React.useEffect(() => {
+ subscribers.add(setMessages);
+ return () => { subscribers.delete(setMessages); };
+ }, []);
+ const postMessage = (type: string, message: unknown) => push(type, message);
+ const clearReceivedMessages = ({ ids }: { ids: string[] }) => {
+ bus = bus.filter(m => !ids.includes(m.id));
+ notify();
+ };
+ return { messages, postMessage, clearReceivedMessages };
+ },
+ __pushTestMessage: push,
+ };
+});
+
+describe("useCollaborativeSession behavior", () => {
+ beforeEach(() => {
+ localStorage.clear();
+ jest.useFakeTimers();
+ });
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it("counter update sets last actor fields", () => {
+ const { result } = renderHook(() => useCollaborativeSession());
+ act(() => result.current.updateCounter(3));
+ expect(result.current.counter.value).toBe(3);
+ expect(result.current.counter.lastActorId).toBe(result.current.currentUser.userId);
+ expect(result.current.counter.lastActorName).toBe(result.current.currentUser.username);
+ expect(result.current.counter.lastUpdatedAt).toBeGreaterThan(0);
+ });
+
+ it("message expiration removes expired messages after cleanup interval", () => {
+ const { result } = renderHook(() => useCollaborativeSession());
+ act(() => result.current.sendMessage("temp", { expiresInMs: 500 }));
+ expect(result.current.messages).toHaveLength(1);
+ act(() => { jest.advanceTimersByTime(600); });
+ expect(result.current.messages).toHaveLength(1);
+ act(() => { jest.advanceTimersByTime(1500); });
+ expect(result.current.messages).toHaveLength(0);
+ });
+
+ it("cannot delete another user's message", () => {
+ const { result } = renderHook(() => useCollaborativeSession());
+ const remoteMsg = { id: "remote1", userId: "remote-user", username: "remote", text: "hello", timestamp: Date.now() };
+ const { __pushTestMessage } = require("react-broadcast-sync");
+ act(() => { __pushTestMessage(CollabEventType.ChatSend, remoteMsg); });
+ expect(result.current.messages.find(m => m.id === "remote1")).toBeTruthy();
+ act(() => result.current.deleteMessage("remote1"));
+ const after = result.current.messages.find(m => m.id === "remote1");
+ expect(after?.deleted).toBeFalsy();
+ });
+});
diff --git a/lib/__tests__/hook.feed.test.ts b/lib/__tests__/hook.feed.test.ts
new file mode 100644
index 0000000..638aa9e
--- /dev/null
+++ b/lib/__tests__/hook.feed.test.ts
@@ -0,0 +1,32 @@
+import { renderHook, act } from "@testing-library/react";
+import { useCollaborativeSession } from "@/lib/hooks/useCollaborativeSession";
+
+jest.mock("react-broadcast-sync", () => {
+ const React = require("react");
+ let bus: Array<{ id: string; type: string; message: any }> = [];
+ const subscribers = new Set>>();
+ const notify = () => subscribers.forEach(set => set([...bus]));
+ const push = (type: string, message: any) => { bus.push({ id: Math.random().toString(36).slice(2), type, message }); notify(); };
+ return {
+ useBroadcastChannel: () => {
+ const [messages, setMessages] = React.useState(bus);
+ React.useEffect(() => { subscribers.add(setMessages); return () => subscribers.delete(setMessages); }, []);
+ return { messages, postMessage: push, clearReceivedMessages: ({ ids }: { ids: string[] }) => { bus = bus.filter(m => !ids.includes(m.id)); notify(); } };
+ },
+ __pushTestMessage: push,
+ };
+});
+
+describe("activity feed", () => {
+ beforeEach(() => { localStorage.clear(); });
+
+ it("adds feed items for chat send and counter update", () => {
+ const { result } = renderHook(() => useCollaborativeSession());
+ act(() => result.current.sendMessage("hello world"));
+ act(() => result.current.updateCounter(1));
+ expect(result.current.feed.length).toBeGreaterThanOrEqual(2);
+ const kinds = result.current.feed.map(f => f.kind);
+ expect(kinds).toContain("chat:send");
+ expect(kinds).toContain("counter:update");
+ });
+});
diff --git a/lib/__tests__/hook.persistence.test.ts b/lib/__tests__/hook.persistence.test.ts
new file mode 100644
index 0000000..2592407
--- /dev/null
+++ b/lib/__tests__/hook.persistence.test.ts
@@ -0,0 +1,51 @@
+import { renderHook } from "@testing-library/react";
+import { useCollaborativeSession } from "@/lib/hooks/useCollaborativeSession";
+
+jest.mock("react-broadcast-sync", () => {
+ const listeners: Record = {};
+ return {
+ useBroadcastChannel: (name: string) => {
+ return {
+ messages: [],
+ postMessage: (type: string, message: unknown) => {
+ (listeners[name] || []).forEach(fn => fn({ id: Math.random().toString(), type, message }));
+ },
+ clearReceivedMessages: () => void 0,
+ };
+ },
+ };
+});
+
+describe("useCollaborativeSession persistence", () => {
+ beforeEach(() => {
+ localStorage.clear();
+ sessionStorage.clear();
+ jest.useFakeTimers();
+ });
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it("persists userId across remounts", () => {
+ const { result, unmount } = renderHook(() => useCollaborativeSession());
+ const firstId = result.current.currentUser.userId;
+ unmount();
+ const { result: result2 } = renderHook(() => useCollaborativeSession());
+ expect(result2.current.currentUser.userId).toBe(firstId);
+ });
+
+ it("assigns new userId for a simulated new tab (sessionStorage cleared)", () => {
+ const { result, unmount } = renderHook(() => useCollaborativeSession());
+ const firstId = result.current.currentUser.userId;
+ unmount();
+ const { result: result2 } = renderHook(() => useCollaborativeSession());
+ expect(result2.current.currentUser.userId).toBe(firstId);
+ sessionStorage.clear();
+ const originalNanoid = require('nanoid').nanoid;
+ (require('nanoid').nanoid as any) = () => 'test-id-2';
+ const { result: result3 } = renderHook(() => useCollaborativeSession());
+ expect(result3.current.currentUser.userId).toBe('test-id-2');
+ expect(result3.current.currentUser.userId).not.toBe(firstId);
+ (require('nanoid').nanoid as any) = originalNanoid;
+ });
+});
diff --git a/lib/__tests__/utils.identity.test.ts b/lib/__tests__/utils.identity.test.ts
new file mode 100644
index 0000000..5107b81
--- /dev/null
+++ b/lib/__tests__/utils.identity.test.ts
@@ -0,0 +1,11 @@
+import { randomUsername } from "@/lib/utils/identity";
+
+describe("identity utils", () => {
+ it("randomUsername is deterministic per seed", () => {
+ const a = randomUsername("seed-123");
+ const b = randomUsername("seed-123");
+ const c = randomUsername("seed-456");
+ expect(a).toBe(b);
+ expect(a).not.toBe(c);
+ });
+});
diff --git a/lib/__tests__/utils.merge.test.ts b/lib/__tests__/utils.merge.test.ts
new file mode 100644
index 0000000..273af26
--- /dev/null
+++ b/lib/__tests__/utils.merge.test.ts
@@ -0,0 +1,53 @@
+import { mergeUsers, sortUsers, mergeMessages, chooseNewerCounter } from "@/lib/utils/merge";
+import type { ChatMessage, CounterState, User } from "@/lib/types/session";
+
+describe("merge utils", () => {
+ test("mergeUsers prefers newer lastActive per userId:tabId", () => {
+ const a: User[] = [
+ { userId: "u1", username: "a", tabId: "t1", lastActive: 10 },
+ ];
+ const b: User[] = [
+ { userId: "u1", username: "a2", tabId: "t1", lastActive: 20 },
+ { userId: "u2", username: "b", tabId: "t2", lastActive: 15 },
+ ];
+ const merged = mergeUsers(a, b);
+ const u1 = merged.find(u => u.userId === "u1" && u.tabId === "t1")!;
+ expect(u1.lastActive).toBe(20);
+ expect(merged).toHaveLength(2);
+ });
+
+ test("sortUsers orders by lastActive desc", () => {
+ const users: User[] = [
+ { userId: "a", username: "a", tabId: "t1", lastActive: 1 },
+ { userId: "b", username: "b", tabId: "t2", lastActive: 3 },
+ { userId: "c", username: "c", tabId: "t3", lastActive: 2 },
+ ];
+ const sorted = sortUsers(users);
+ expect(sorted.map(u => u.userId)).toEqual(["b", "c", "a"]);
+ });
+
+ test("mergeMessages prefers deleted or newer timestamp", () => {
+ const a: ChatMessage[] = [
+ { id: "1", userId: "u", username: "x", text: "hi", timestamp: 1 },
+ ];
+ const b: ChatMessage[] = [
+ { id: "1", userId: "u", username: "x", text: "hi2", timestamp: 2 },
+ { id: "2", userId: "u", username: "x", text: "other", timestamp: 3, deleted: true },
+ ];
+ const c: ChatMessage[] = [
+ { id: "2", userId: "u", username: "x", text: "should be deleted", timestamp: 4 },
+ ];
+ const merged = mergeMessages(mergeMessages(a, b), c);
+ const m1 = merged.find(m => m.id === "1")!;
+ const m2 = merged.find(m => m.id === "2")!;
+ expect(m1.text).toBe("hi2");
+ expect(m2.deleted).toBe(true);
+ });
+
+ test("chooseNewerCounter picks by lastUpdatedAt", () => {
+ const a: CounterState = { value: 1, lastUpdatedAt: 10 };
+ const b: CounterState = { value: 2, lastUpdatedAt: 20 };
+ expect(chooseNewerCounter(a, b)).toBe(b);
+ expect(chooseNewerCounter(b, a)).toBe(b);
+ });
+});
diff --git a/lib/context/CollaborativeSessionContext.tsx b/lib/context/CollaborativeSessionContext.tsx
new file mode 100644
index 0000000..8693b70
--- /dev/null
+++ b/lib/context/CollaborativeSessionContext.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+import React, { createContext, useContext, ReactNode } from "react";
+import { useCollaborativeSession } from "@/lib/hooks/useCollaborativeSession";
+import type { ChatMessage, CounterState, Theme, User } from "@/lib/types/session";
+
+type CollaborativeSessionContextType = {
+ loading: boolean;
+ users: User[];
+ messages: ChatMessage[];
+ counter: CounterState;
+ typingUsers: { userId: string; username: string }[];
+ theme: Theme;
+ currentUser: User;
+ sendMessage: (text: string, opts?: { expiresInMs?: number }) => void;
+ deleteMessage: (id: string) => void;
+ updateCounter: (delta: number) => void;
+ markTyping: () => void;
+ toggleTheme: () => void;
+ feed: import("@/lib/types/session").FeedItem[];
+ focus: import("@/lib/types/session").FocusState;
+ updateFocus: (element: string, cursorPos?: number) => void;
+};
+
+const CollaborativeSessionContext = createContext(null);
+
+type CollaborativeSessionProviderProps = {
+ children: ReactNode;
+};
+
+export const CollaborativeSessionProvider = ({ children }: CollaborativeSessionProviderProps) => {
+ const session = useCollaborativeSession();
+ return {children} ;
+};
+
+export const useCollaborativeSessionContext = () => {
+ const context = useContext(CollaborativeSessionContext);
+ if (!context) throw new Error("useCollaborativeSessionContext must be used within a CollaborativeSessionProvider");
+ return context;
+};
diff --git a/lib/hooks/useCollaborativeSession.ts b/lib/hooks/useCollaborativeSession.ts
new file mode 100644
index 0000000..c0cc592
--- /dev/null
+++ b/lib/hooks/useCollaborativeSession.ts
@@ -0,0 +1,272 @@
+"use client";
+
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { nanoid } from "nanoid";
+import { useBroadcastChannel } from "react-broadcast-sync";
+import { debounce as lodashDebounce } from "lodash-es";
+import type { ChatMessage, CounterState, Theme, TypingState, User, FeedItem, FocusState } from "@/lib/types/session";
+import { CollabEventType, type CollabEvent, isCollabEventType } from "@/lib/types/events";
+import { mergeMessages, mergeUsers, sortUsers, chooseNewerCounter } from "@/lib/utils/merge";
+import { randomUsername } from "@/lib/utils/identity";
+import { avatarColorFromUserId } from "@/lib/utils/identity";
+
+const CHANNEL_NAME = "collab-dashboard" as const;
+const PRESENCE_PING_INTERVAL_MS = 5000;
+const EXPIRY_CLEANUP_INTERVAL_MS = 2000;
+const TYPING_DEBOUNCE_MS = 250;
+const TYPING_VISIBLE_WINDOW_MS = 1500;
+const THEME_STORAGE_KEY = "collab_theme";
+const USER_ID_STORAGE_KEY = "collab_user_id"; // per-tab identity (sessionStorage)
+const USER_ID_SALT_KEY = "collab_user_salt";
+const STALE_USER_MS = 30000; // remove users inactive >30s
+const FOCUS_THROTTLE_MS = 120; // throttle focus updates
+
+export const useCollaborativeSession = () => {
+ const { messages: busMessages, postMessage, clearReceivedMessages } = useBroadcastChannel(CHANNEL_NAME);
+ const [tabId] = useState(() => nanoid());
+ const [userId] = useState(() => {
+ let existing: string | null = null;
+ try { existing = sessionStorage.getItem(USER_ID_STORAGE_KEY); } catch { /* noop */ }
+ if (!existing) {
+ const salt = Date.now().toString(36) + Math.random().toString(36).slice(2,8);
+ try { sessionStorage.setItem(USER_ID_SALT_KEY, salt); } catch {}
+ existing = nanoid();
+ try { sessionStorage.setItem(USER_ID_STORAGE_KEY, existing); } catch { /* noop */ }
+ }
+ return existing;
+ });
+ const username = useMemo(() => randomUsername(userId), [userId]);
+
+ const selfUserRef = useRef(null);
+ if (!selfUserRef.current) selfUserRef.current = { userId, username, tabId, lastActive: Date.now(), avatarColor: avatarColorFromUserId(userId) };
+ const selfUser = selfUserRef.current;
+
+ const [users, setUsers] = useState([selfUser]);
+ const [messages, setMessages] = useState([]);
+ const [counter, setCounter] = useState({ value: 0 });
+ const [typing, setTyping] = useState({});
+ const [theme, setTheme] = useState("light");
+ const [loading, setLoading] = useState(true);
+ const [focus, setFocus] = useState({});
+ const [feed, setFeed] = useState([]);
+
+ const pushFeed = useCallback((item: Omit) => {
+ setFeed(prev => [{ id: nanoid(), ...item }, ...prev].slice(0, 100));
+ }, []);
+
+ const postMessageRef = useRef(postMessage);
+ useEffect(() => { postMessageRef.current = postMessage; }, [postMessage]);
+
+ const publish = useCallback((ev: CollabEvent) => {
+ try { postMessageRef.current(ev.type, ev.payload as unknown); } catch (e) { console.warn("broadcast failed", e); }
+ }, []);
+
+ const snapshotRef = useRef({ users: [] as User[], messages: [] as ChatMessage[], counter: { value: 0 } as CounterState, theme: "light" as Theme });
+ useEffect(() => { snapshotRef.current = { users, messages, counter, theme }; }, [users, messages, counter, theme]);
+
+ useEffect(() => {
+ if (!busMessages?.length) return;
+ const idsToClear: string[] = [];
+ for (const m of busMessages) {
+ if (!isCollabEventType(m.type)) { idsToClear.push(m.id); continue; }
+ const ev = { type: m.type, payload: m.message } as CollabEvent;
+ switch (ev.type) {
+ case CollabEventType.PresenceJoin:
+ case CollabEventType.PresencePing: {
+ const u = ev.payload as User;
+ if (u.tabId === tabId) break;
+ setUsers(prev => {
+ const others = prev.filter(p => !(p.tabId === u.tabId && p.userId === u.userId));
+ return [...others, { ...u, lastActive: Date.now() }];
+ });
+ if (ev.type === CollabEventType.PresenceJoin) pushFeed({ ts: Date.now(), kind: "presence:join", text: `${u.username} joined`, actorId: u.userId, actorName: u.username });
+ break;
+ }
+ case CollabEventType.PresenceLeave: {
+ const u = ev.payload as User;
+ setUsers(prev => prev.filter(p => !(p.tabId === u.tabId && p.userId === u.userId)));
+ pushFeed({ ts: Date.now(), kind: "presence:leave", text: `${u.username} left`, actorId: u.userId, actorName: u.username });
+ break;
+ }
+ case CollabEventType.ChatSend: {
+ const msg = ev.payload as ChatMessage;
+ setMessages(prev => {
+ const next = [...prev.filter(x => x.id !== msg.id), msg];
+ return next.sort((a, b) => a.timestamp - b.timestamp);
+ });
+ if ((msg.userId !== userId)) pushFeed({ ts: Date.now(), kind: "chat:send", text: `${msg.username}: ${msg.text.slice(0,40)}` , actorId: msg.userId, actorName: msg.username});
+ break;
+ }
+ case CollabEventType.ChatDelete: {
+ const { id } = ev.payload as { id: string; userId: string };
+ setMessages(prev => prev.map(x => x.id === id ? { ...x, deleted: true } : x));
+ pushFeed({ ts: Date.now(), kind: "chat:delete", text: `message deleted`, actorId: (ev.payload as { id: string; userId: string }).userId, actorName: users.find(u=>u.userId===(ev.payload as { id: string; userId: string }).userId)?.username });
+ break;
+ }
+ case CollabEventType.CounterUpdate: setCounter(ev.payload as CounterState); if (((ev.payload as CounterState).lastActorId !== userId)) pushFeed({ ts: Date.now(), kind: "counter:update", text: `counter -> ${(ev.payload as CounterState).value}`, actorId: (ev.payload as CounterState).lastActorId, actorName: (ev.payload as CounterState).lastActorName }); break;
+ case CollabEventType.ThemeUpdate: setTheme((ev.payload as { theme: Theme }).theme); pushFeed({ ts: Date.now(), kind: "theme", text: `theme ${(ev.payload as { theme: Theme }).theme}`, actorId: userId, actorName: username }); break;
+ case CollabEventType.FocusUpdate: {
+ const f = ev.payload as { userId: string; element: string; cursorPos?: number; ts: number };
+ setFocus((prev: FocusState) => ({ ...prev, [f.userId]: f }));
+ break;
+ }
+ case CollabEventType.RehydrateRequest: {
+ const { fromTabId } = ev.payload as { fromTabId: string };
+ if (fromTabId === tabId) break;
+ const snap = snapshotRef.current;
+ publish({ type: CollabEventType.RehydrateResponse, payload: { toTabId: fromTabId, users: snap.users, messages: snap.messages, counter: snap.counter, theme: snap.theme } });
+ break;
+ }
+ case CollabEventType.RehydrateResponse: {
+ const payload = ev.payload as { toTabId: string; users: User[]; messages: ChatMessage[]; counter: CounterState; theme: Theme };
+ if (payload.toTabId !== tabId) break;
+ setUsers(prev => mergeUsers(prev, payload.users));
+ setMessages(prev => mergeMessages(prev, payload.messages));
+ setCounter(prev => chooseNewerCounter(prev, payload.counter));
+ setTheme(payload.theme);
+ setLoading(false);
+ pushFeed({ ts: Date.now(), kind: "rehydrate", text: `state synced`, actorId: userId, actorName: username });
+ break;
+ }
+ case CollabEventType.Typing: {
+ const { userId: uid, ts } = ev.payload as { userId: string; ts: number };
+ if (uid !== userId) {
+ setTyping(prev => ({ ...prev, [uid]: ts }));
+ }
+ break;
+ }
+ }
+ idsToClear.push(m.id);
+ }
+ if (idsToClear.length) clearReceivedMessages({ ids: idsToClear });
+ }, [busMessages, clearReceivedMessages, publish, tabId]);
+
+ useEffect(() => {
+ const selfUserSnapshot = selfUserRef.current!;
+ publish({ type: CollabEventType.PresenceJoin, payload: selfUserSnapshot });
+ publish({ type: CollabEventType.RehydrateRequest, payload: { fromTabId: tabId } });
+ const onUnload = () => publish({ type: CollabEventType.PresenceLeave, payload: selfUserSnapshot });
+ window.addEventListener("beforeunload", onUnload);
+ setUsers(prev => mergeUsers(prev, [selfUserSnapshot]));
+ setLoading(false);
+ return () => {
+ window.removeEventListener("beforeunload", onUnload);
+ publish({ type: CollabEventType.PresenceLeave, payload: selfUserSnapshot });
+ };
+ }, [publish, tabId]);
+
+ useEffect(() => {
+ const id = setInterval(() => {
+ const selfUserSnapshot = selfUserRef.current!;
+ publish({ type: CollabEventType.PresencePing, payload: { ...selfUserSnapshot, lastActive: Date.now() } });
+ }, PRESENCE_PING_INTERVAL_MS);
+ return () => clearInterval(id);
+ }, [publish]);
+
+ useEffect(() => {
+ const id = setInterval(() => {
+ const now = Date.now();
+ setUsers(prev => prev.filter(u => now - u.lastActive < STALE_USER_MS || u.userId === userId));
+ }, 5000);
+ return () => clearInterval(id);
+ }, [userId]);
+
+ useEffect(() => {
+ const id = setInterval(() => {
+ const now = Date.now();
+ let expiredCount = 0;
+ setMessages(prev => prev.filter(m => {
+ const keep = !m.expiresAt || m.expiresAt > now;
+ if (!keep) expiredCount++;
+ return keep;
+ }));
+ if (expiredCount > 0) pushFeed({ ts: now, kind: "chat:expire", text: `${expiredCount} message${expiredCount>1?"s":""} expired`, actorId: userId, actorName: username });
+ }, EXPIRY_CLEANUP_INTERVAL_MS);
+ return () => clearInterval(id);
+ }, [pushFeed]);
+
+ const markTyping = useMemo(() => lodashDebounce(() => {
+ setTyping(prev => ({ ...prev, [userId]: Date.now() }));
+ publish({ type: CollabEventType.Typing, payload: { userId, ts: Date.now() } });
+ }, TYPING_DEBOUNCE_MS), [publish, userId]);
+
+ const sendMessage = useCallback((text: string, opts?: { expiresInMs?: number }) => {
+ const now = Date.now();
+ const msg: ChatMessage = { id: nanoid(), userId, username, text: text.trim(), timestamp: now, expiresAt: opts?.expiresInMs ? now + opts.expiresInMs : undefined };
+ setMessages(prev => [...prev, msg]);
+ pushFeed({ ts: Date.now(), kind: "chat:send", text: `${msg.username}: ${msg.text.slice(0,40)}`, actorId: msg.userId, actorName: msg.username });
+ publish({ type: CollabEventType.ChatSend, payload: msg });
+ }, [publish, userId, username, pushFeed]);
+
+ const deleteMessage = useCallback((id: string) => {
+ const target = messages.find(m => m.id === id);
+ if (!target || target.userId !== userId) return;
+ setMessages(prev => prev.map(m => m.id === id ? { ...m, deleted: true } : m));
+ publish({ type: CollabEventType.ChatDelete, payload: { id, userId } });
+ }, [messages, publish, userId]);
+
+ const updateCounter = useCallback((delta: number) => {
+ const now = Date.now();
+ setCounter(prev => {
+ const next = { value: (prev?.value ?? 0) + delta, lastActorId: userId, lastActorName: username, lastUpdatedAt: now } as CounterState;
+ pushFeed({ ts: Date.now(), kind: "counter:update", text: `counter -> ${next.value}`, actorId: userId, actorName: username });
+ publish({ type: CollabEventType.CounterUpdate, payload: next });
+ return next;
+ });
+ }, [publish, userId, username, pushFeed]);
+
+ const updateFocus = useCallback((element: string, cursorPos?: number) => {
+ if (!element) return;
+ const now = Date.now();
+ if ((updateFocus as any).last && now - (updateFocus as any).last < FOCUS_THROTTLE_MS) return;
+ (updateFocus as any).last = now;
+ const payload = { userId, element, cursorPos, ts: now };
+ setFocus((prev: FocusState) => ({ ...prev, [userId]: payload }));
+ publish({ type: CollabEventType.FocusUpdate, payload });
+ }, [publish, userId]);
+
+ const typingUsers = useMemo(() => {
+ const now = Date.now();
+ const ids = Object.entries(typing).filter(([uid, ts]) => uid !== userId && now - ts < TYPING_VISIBLE_WINDOW_MS).map(([uid]) => uid);
+ const map = new Map(users.map(u => [u.userId, u.username] as const));
+ return ids.map(id => ({ userId: id, username: map.get(id) ?? id }));
+ }, [typing, users, userId]);
+
+ const toggleTheme = useCallback(() => {
+ setTheme(prev => {
+ const next: Theme = prev === "light" ? "dark" : "light";
+ try { localStorage.setItem(THEME_STORAGE_KEY, next); } catch {}
+ publish({ type: CollabEventType.ThemeUpdate, payload: { theme: next } });
+ return next;
+ });
+ }, [publish]);
+
+ useEffect(() => { if (typeof document !== "undefined") document.documentElement.dataset.theme = theme; }, [theme]);
+
+ useEffect(() => {
+ try {
+ const stored = localStorage.getItem(THEME_STORAGE_KEY) as Theme | null;
+ if (stored && stored !== theme) {
+ setTheme(stored);
+ }
+ } catch { /* ignore */ }
+ }, []);
+
+ return {
+ loading,
+ users: sortUsers(users, userId),
+ messages,
+ counter,
+ typingUsers,
+ theme,
+ currentUser: selfUser,
+ sendMessage,
+ deleteMessage,
+ updateCounter,
+ markTyping,
+ toggleTheme,
+ feed,
+ focus,
+ updateFocus,
+ } as const;
+};
diff --git a/lib/types/events.ts b/lib/types/events.ts
new file mode 100644
index 0000000..72acfe2
--- /dev/null
+++ b/lib/types/events.ts
@@ -0,0 +1,46 @@
+import type { ChatMessage, CounterState, Theme, User } from "./session";
+
+export enum CollabEventType {
+ PresenceJoin = "presence-join",
+ PresenceLeave = "presence-leave",
+ PresencePing = "presence-ping",
+ ChatSend = "chat-send",
+ ChatDelete = "chat-delete",
+ Typing = "typing",
+ CounterUpdate = "counter-update",
+ RehydrateRequest = "rehydrate-request",
+ RehydrateResponse = "rehydrate-response",
+ ThemeUpdate = "theme-update",
+ FocusUpdate = "focus-update",
+}
+
+export type PresenceJoinEvent = { type: CollabEventType.PresenceJoin; payload: User };
+export type PresenceLeaveEvent = { type: CollabEventType.PresenceLeave; payload: User };
+export type PresencePingEvent = { type: CollabEventType.PresencePing; payload: User };
+export type ChatSendEvent = { type: CollabEventType.ChatSend; payload: ChatMessage };
+export type ChatDeleteEvent = { type: CollabEventType.ChatDelete; payload: { id: string; userId: string } };
+export type TypingEvent = { type: CollabEventType.Typing; payload: { userId: string; ts: number } };
+export type CounterUpdateEvent = { type: CollabEventType.CounterUpdate; payload: CounterState };
+export type RehydrateRequestEvent = { type: CollabEventType.RehydrateRequest; payload: { fromTabId: string } };
+export type RehydrateResponseEvent = {
+ type: CollabEventType.RehydrateResponse;
+ payload: { toTabId: string; users: User[]; messages: ChatMessage[]; counter: CounterState; theme: Theme };
+};
+export type ThemeUpdateEvent = { type: CollabEventType.ThemeUpdate; payload: { theme: Theme } };
+export type FocusUpdateEvent = { type: CollabEventType.FocusUpdate; payload: { userId: string; element: string; cursorPos?: number; ts: number } };
+
+export type CollabEvent =
+ | PresenceJoinEvent
+ | PresenceLeaveEvent
+ | PresencePingEvent
+ | ChatSendEvent
+ | ChatDeleteEvent
+ | TypingEvent
+ | CounterUpdateEvent
+ | RehydrateRequestEvent
+ | RehydrateResponseEvent
+ | ThemeUpdateEvent
+ | FocusUpdateEvent;
+
+export const isCollabEventType = (value: unknown): value is CollabEventType =>
+ typeof value === "string" && Object.values(CollabEventType).includes(value as CollabEventType);
diff --git a/lib/types/session.ts b/lib/types/session.ts
new file mode 100644
index 0000000..6ddec6d
--- /dev/null
+++ b/lib/types/session.ts
@@ -0,0 +1,32 @@
+export type User = {
+ userId: string;
+ username: string;
+ tabId: string;
+ lastActive: number;
+ avatarColor?: string;
+};
+
+export type ChatMessage = {
+ id: string;
+ userId: string;
+ username: string;
+ text: string;
+ timestamp: number;
+ expiresAt?: number;
+ deleted?: boolean;
+};
+
+export type CounterState = {
+ value: number;
+ lastActorId?: string;
+ lastActorName?: string;
+ lastUpdatedAt?: number;
+};
+
+export type TypingState = Record;
+
+export type FocusState = Record; // focus/cursor indicators
+
+export type FeedItem = { id: string; ts: number; kind: string; text: string; actorId?: string; actorName?: string }; // activity feed entry
+
+export type Theme = "light" | "dark";
diff --git a/lib/utils/identity.ts b/lib/utils/identity.ts
new file mode 100644
index 0000000..27e663f
--- /dev/null
+++ b/lib/utils/identity.ts
@@ -0,0 +1,43 @@
+const ADJECTIVES = [
+ "brave",
+ "calm",
+ "eager",
+ "fancy",
+ "gentle",
+ "jolly",
+ "kind",
+ "lively",
+ "merry",
+ "nice",
+] as const;
+
+const ANIMALS = [
+ "panda",
+ "tiger",
+ "otter",
+ "eagle",
+ "koala",
+ "lion",
+ "dolphin",
+ "fox",
+ "wolf",
+ "owl",
+] as const;
+
+export const randomUsername = (seed: string): string => {
+ let h = 0;
+ for (let i = 0; i < seed.length; i++) h = (h * 31 + seed.charCodeAt(i)) >>> 0;
+ const adj = ADJECTIVES[h % ADJECTIVES.length];
+ const ani = ANIMALS[((h >> 5) >>> 0) % ANIMALS.length];
+ const num = (h % 1000).toString().padStart(3, "0");
+ return `${adj}-${ani}-${num}`;
+};
+
+export const avatarColorFromUserId = (seed: string): string => {
+ let h = 0;
+ for (let i = 0; i < seed.length; i++) h = (h * 31 + seed.charCodeAt(i)) >>> 0;
+ const hue = h % 360;
+ const sat = 65 + (h % 10); // vary saturation slightly
+ const light = 50 + (h % 5); // vary lightness slightly
+ return `hsl(${hue}deg ${sat}% ${light}%)`;
+};
diff --git a/lib/utils/index.ts b/lib/utils/index.ts
new file mode 100644
index 0000000..d397d18
--- /dev/null
+++ b/lib/utils/index.ts
@@ -0,0 +1,5 @@
+import { type ClassValue } from "clsx";
+import { clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
diff --git a/lib/utils/merge.ts b/lib/utils/merge.ts
new file mode 100644
index 0000000..d941fad
--- /dev/null
+++ b/lib/utils/merge.ts
@@ -0,0 +1,55 @@
+import {
+ defaultTo,
+ groupBy,
+ keyBy,
+ maxBy,
+ mergeWith,
+ orderBy,
+ values as objectValues,
+} from "lodash-es";
+import type { ChatMessage, CounterState, User } from "@/lib/types/session";
+
+export const mergeUsers = (a: User[], b: User[]): User[] => {
+ const combined = [...a, ...b];
+ const groups = groupBy(combined, (u) => `${u.userId}:${u.tabId}`);
+ return Object.values(groups)
+ .map((arr) => maxBy(arr, (u) => defaultTo(u.lastActive, 0))!)
+ .filter(Boolean) as User[];
+};
+
+export const sortUsers = (arr: User[], currentUserId?: string): User[] => {
+ if (!currentUserId) {
+ return orderBy(arr, [(u) => defaultTo(u.lastActive, 0)], ["desc"]);
+ }
+ return orderBy(
+ arr,
+ [
+ (u) => (u.userId === currentUserId ? 1 : 0),
+ (u) => defaultTo(u.lastActive, 0),
+ ],
+ ["desc", "desc"]
+ );
+};
+
+export const mergeMessages = (a: ChatMessage[], b: ChatMessage[]): ChatMessage[] => {
+ const aBy = keyBy(a, "id");
+ const bBy = keyBy(b, "id");
+ const merged = mergeWith({}, aBy, bBy, (va: ChatMessage, vb: ChatMessage) => {
+ if (!va) return vb;
+ if (!vb) return va;
+ const ta = defaultTo(va.timestamp, 0);
+ const tb = defaultTo(vb.timestamp, 0);
+ if ((va.deleted ?? false) || (vb.deleted ?? false)) {
+ const newer = tb > ta ? vb : va;
+ return { ...newer, deleted: true } as ChatMessage;
+ }
+ return tb > ta ? vb : va;
+ }) as Record;
+ return orderBy(objectValues(merged), [(m: ChatMessage) => m.timestamp], ["asc"]);
+};
+
+export const chooseNewerCounter = (a: CounterState, b: CounterState): CounterState => {
+ const at = defaultTo(a.lastUpdatedAt, 0);
+ const bt = defaultTo(b.lastUpdatedAt, 0);
+ return bt > at ? b : a;
+};
diff --git a/package.json b/package.json
index 500bff1..39d8ad0 100644
--- a/package.json
+++ b/package.json
@@ -10,9 +10,18 @@
"test": "jest"
},
"dependencies": {
- "next": "15.2.0",
+ "@types/lodash-es": "^4.17.12",
+ "lodash-es": "^4.17.21",
+ "next": "15.4.7",
+ "react-hook-form": "^7.54.2",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-broadcast-sync": "^1.6.0",
+ "react-dom": "^19.0.0",
+ "nanoid": "^5.0.7",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.469.0",
+ "tailwind-merge": "^2.5.5"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.0",
@@ -28,7 +37,10 @@
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"ts-node": "^10.9.2",
- "typescript": "^5.8.2"
+ "typescript": "5.7.3",
+ "autoprefixer": "^10.4.20",
+ "postcss": "^8.4.49",
+ "tailwindcss": "^3.4.17"
},
"packageManager": "pnpm@10.5.2+sha512.da9dc28cd3ff40d0592188235ab25d3202add8a207afbedc682220e4a0029ffbff4562102b9e6e46b4e3f9e8bd53e6d05de48544b0c57d4b0179e22c76d1199b"
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 17fb403..aab86af 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,15 +8,42 @@ importers:
.:
dependencies:
+ '@types/lodash-es':
+ specifier: ^4.17.12
+ version: 4.17.12
+ class-variance-authority:
+ specifier: ^0.7.0
+ version: 0.7.1
+ clsx:
+ specifier: ^2.1.1
+ version: 2.1.1
+ lodash-es:
+ specifier: ^4.17.21
+ version: 4.17.21
+ lucide-react:
+ specifier: ^0.469.0
+ version: 0.469.0(react@19.0.0)
+ nanoid:
+ specifier: ^5.0.7
+ version: 5.1.6
next:
- specifier: 15.2.0
- version: 15.2.0(@babel/core@7.26.9)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ specifier: 15.4.7
+ version: 15.4.7(@babel/core@7.26.9)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react:
specifier: ^19.0.0
version: 19.0.0
+ react-broadcast-sync:
+ specifier: ^1.6.0
+ version: 1.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react-dom:
specifier: ^19.0.0
version: 19.0.0(react@19.0.0)
+ react-hook-form:
+ specifier: ^7.54.2
+ version: 7.66.1(react@19.0.0)
+ tailwind-merge:
+ specifier: ^2.5.5
+ version: 2.6.0
devDependencies:
'@eslint/eslintrc':
specifier: ^3.3.0
@@ -42,30 +69,43 @@ importers:
'@types/react-dom':
specifier: ^19.0.4
version: 19.0.4(@types/react@19.0.10)
+ autoprefixer:
+ specifier: ^10.4.20
+ version: 10.4.22(postcss@8.5.6)
eslint:
specifier: ^9.21.0
- version: 9.21.0
+ version: 9.21.0(jiti@1.21.7)
eslint-config-next:
specifier: 15.2.0
- version: 15.2.0(eslint@9.21.0)(typescript@5.8.2)
+ version: 15.2.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.7.3)
jest:
specifier: ^29.7.0
- version: 29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2))
+ version: 29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.7.3))
jest-environment-jsdom:
specifier: ^29.7.0
version: 29.7.0
+ postcss:
+ specifier: ^8.4.49
+ version: 8.5.6
+ tailwindcss:
+ specifier: ^3.4.17
+ version: 3.4.18
ts-node:
specifier: ^10.9.2
- version: 10.9.2(@types/node@22.13.8)(typescript@5.8.2)
+ version: 10.9.2(@types/node@22.13.8)(typescript@5.7.3)
typescript:
- specifier: ^5.8.2
- version: 5.8.2
+ specifier: 5.7.3
+ version: 5.7.3
packages:
'@adobe/css-tools@4.4.2':
resolution: {integrity: sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==}
+ '@alloc/quick-lru@5.2.0':
+ resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
+ engines: {node: '>=10'}
+
'@ampproject/remapping@2.3.0':
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
@@ -239,8 +279,8 @@ packages:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
- '@emnapi/runtime@1.3.1':
- resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==}
+ '@emnapi/runtime@1.7.1':
+ resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==}
'@eslint-community/eslint-utils@4.4.1':
resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==}
@@ -296,107 +336,139 @@ packages:
resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==}
engines: {node: '>=18.18'}
- '@img/sharp-darwin-arm64@0.33.5':
- resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
+ '@img/colour@1.0.0':
+ resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
+ engines: {node: '>=18'}
+
+ '@img/sharp-darwin-arm64@0.34.5':
+ resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
- '@img/sharp-darwin-x64@0.33.5':
- resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==}
+ '@img/sharp-darwin-x64@0.34.5':
+ resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
- '@img/sharp-libvips-darwin-arm64@1.0.4':
- resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==}
+ '@img/sharp-libvips-darwin-arm64@1.2.4':
+ resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
cpu: [arm64]
os: [darwin]
- '@img/sharp-libvips-darwin-x64@1.0.4':
- resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==}
+ '@img/sharp-libvips-darwin-x64@1.2.4':
+ resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
cpu: [x64]
os: [darwin]
- '@img/sharp-libvips-linux-arm64@1.0.4':
- resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
+ '@img/sharp-libvips-linux-arm64@1.2.4':
+ resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
- '@img/sharp-libvips-linux-arm@1.0.5':
- resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
+ '@img/sharp-libvips-linux-arm@1.2.4':
+ resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
- '@img/sharp-libvips-linux-s390x@1.0.4':
- resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
+ '@img/sharp-libvips-linux-ppc64@1.2.4':
+ resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-riscv64@1.2.4':
+ resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-s390x@1.2.4':
+ resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
- '@img/sharp-libvips-linux-x64@1.0.4':
- resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
+ '@img/sharp-libvips-linux-x64@1.2.4':
+ resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
- '@img/sharp-libvips-linuxmusl-arm64@1.0.4':
- resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
+ '@img/sharp-libvips-linuxmusl-arm64@1.2.4':
+ resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
- '@img/sharp-libvips-linuxmusl-x64@1.0.4':
- resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
+ '@img/sharp-libvips-linuxmusl-x64@1.2.4':
+ resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
- '@img/sharp-linux-arm64@0.33.5':
- resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
+ '@img/sharp-linux-arm64@0.34.5':
+ resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
- '@img/sharp-linux-arm@0.33.5':
- resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
+ '@img/sharp-linux-arm@0.34.5':
+ resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
- '@img/sharp-linux-s390x@0.33.5':
- resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
+ '@img/sharp-linux-ppc64@0.34.5':
+ resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@img/sharp-linux-riscv64@0.34.5':
+ resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@img/sharp-linux-s390x@0.34.5':
+ resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
- '@img/sharp-linux-x64@0.33.5':
- resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
+ '@img/sharp-linux-x64@0.34.5':
+ resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
- '@img/sharp-linuxmusl-arm64@0.33.5':
- resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
+ '@img/sharp-linuxmusl-arm64@0.34.5':
+ resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
- '@img/sharp-linuxmusl-x64@0.33.5':
- resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
+ '@img/sharp-linuxmusl-x64@0.34.5':
+ resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
- '@img/sharp-wasm32@0.33.5':
- resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
+ '@img/sharp-wasm32@0.34.5':
+ resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [wasm32]
- '@img/sharp-win32-ia32@0.33.5':
- resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==}
+ '@img/sharp-win32-arm64@0.34.5':
+ resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@img/sharp-win32-ia32@0.34.5':
+ resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32]
os: [win32]
- '@img/sharp-win32-x64@0.33.5':
- resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==}
+ '@img/sharp-win32-x64@0.34.5':
+ resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
@@ -496,56 +568,56 @@ packages:
'@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
- '@next/env@15.2.0':
- resolution: {integrity: sha512-eMgJu1RBXxxqqnuRJQh5RozhskoNUDHBFybvi+Z+yK9qzKeG7dadhv/Vp1YooSZmCnegf7JxWuapV77necLZNA==}
+ '@next/env@15.4.7':
+ resolution: {integrity: sha512-PrBIpO8oljZGTOe9HH0miix1w5MUiGJ/q83Jge03mHEE0E3pyqzAy2+l5G6aJDbXoobmxPJTVhbCuwlLtjSHwg==}
'@next/eslint-plugin-next@15.2.0':
resolution: {integrity: sha512-jHFUG2OwmAuOASqq253RAEG/5BYcPHn27p1NoWZDCf4OdvdK0yRYWX92YKkL+Mk2s+GyJrmd/GATlL5b2IySpw==}
- '@next/swc-darwin-arm64@15.2.0':
- resolution: {integrity: sha512-rlp22GZwNJjFCyL7h5wz9vtpBVuCt3ZYjFWpEPBGzG712/uL1bbSkS675rVAUCRZ4hjoTJ26Q7IKhr5DfJrHDA==}
+ '@next/swc-darwin-arm64@15.4.7':
+ resolution: {integrity: sha512-2Dkb+VUTp9kHHkSqtws4fDl2Oxms29HcZBwFIda1X7Ztudzy7M6XF9HDS2dq85TmdN47VpuhjE+i6wgnIboVzQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
- '@next/swc-darwin-x64@15.2.0':
- resolution: {integrity: sha512-DiU85EqSHogCz80+sgsx90/ecygfCSGl5P3b4XDRVZpgujBm5lp4ts7YaHru7eVTyZMjHInzKr+w0/7+qDrvMA==}
+ '@next/swc-darwin-x64@15.4.7':
+ resolution: {integrity: sha512-qaMnEozKdWezlmh1OGDVFueFv2z9lWTcLvt7e39QA3YOvZHNpN2rLs/IQLwZaUiw2jSvxW07LxMCWtOqsWFNQg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
- '@next/swc-linux-arm64-gnu@15.2.0':
- resolution: {integrity: sha512-VnpoMaGukiNWVxeqKHwi8MN47yKGyki5q+7ql/7p/3ifuU2341i/gDwGK1rivk0pVYbdv5D8z63uu9yMw0QhpQ==}
+ '@next/swc-linux-arm64-gnu@15.4.7':
+ resolution: {integrity: sha512-ny7lODPE7a15Qms8LZiN9wjNWIeI+iAZOFDOnv2pcHStncUr7cr9lD5XF81mdhrBXLUP9yT9RzlmSWKIazWoDw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
- '@next/swc-linux-arm64-musl@15.2.0':
- resolution: {integrity: sha512-ka97/ssYE5nPH4Qs+8bd8RlYeNeUVBhcnsNUmFM6VWEob4jfN9FTr0NBhXVi1XEJpj3cMfgSRW+LdE3SUZbPrw==}
+ '@next/swc-linux-arm64-musl@15.4.7':
+ resolution: {integrity: sha512-4SaCjlFR/2hGJqZLLWycccy1t+wBrE/vyJWnYaZJhUVHccpGLG5q0C+Xkw4iRzUIkE+/dr90MJRUym3s1+vO8A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
- '@next/swc-linux-x64-gnu@15.2.0':
- resolution: {integrity: sha512-zY1JduE4B3q0k2ZCE+DAF/1efjTXUsKP+VXRtrt/rJCTgDlUyyryx7aOgYXNc1d8gobys/Lof9P9ze8IyRDn7Q==}
+ '@next/swc-linux-x64-gnu@15.4.7':
+ resolution: {integrity: sha512-2uNXjxvONyRidg00VwvlTYDwC9EgCGNzPAPYbttIATZRxmOZ3hllk/YYESzHZb65eyZfBR5g9xgCZjRAl9YYGg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
- '@next/swc-linux-x64-musl@15.2.0':
- resolution: {integrity: sha512-QqvLZpurBD46RhaVaVBepkVQzh8xtlUN00RlG4Iq1sBheNugamUNPuZEH1r9X1YGQo1KqAe1iiShF0acva3jHQ==}
+ '@next/swc-linux-x64-musl@15.4.7':
+ resolution: {integrity: sha512-ceNbPjsFgLscYNGKSu4I6LYaadq2B8tcK116nVuInpHHdAWLWSwVK6CHNvCi0wVS9+TTArIFKJGsEyVD1H+4Kg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
- '@next/swc-win32-arm64-msvc@15.2.0':
- resolution: {integrity: sha512-ODZ0r9WMyylTHAN6pLtvUtQlGXBL9voljv6ujSlcsjOxhtXPI1Ag6AhZK0SE8hEpR1374WZZ5w33ChpJd5fsjw==}
+ '@next/swc-win32-arm64-msvc@15.4.7':
+ resolution: {integrity: sha512-pZyxmY1iHlZJ04LUL7Css8bNvsYAMYOY9JRwFA3HZgpaNKsJSowD09Vg2R9734GxAcLJc2KDQHSCR91uD6/AAw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
- '@next/swc-win32-x64-msvc@15.2.0':
- resolution: {integrity: sha512-8+4Z3Z7xa13NdUuUAcpVNA6o76lNPniBd9Xbo02bwXQXnZgFvEopwY2at5+z7yHl47X9qbZpvwatZ2BRo3EdZw==}
+ '@next/swc-win32-x64-msvc@15.4.7':
+ resolution: {integrity: sha512-HjuwPJ7BeRzgl3KrjKqD2iDng0eQIpIReyhpF5r4yeAHFwWRuAhfW92rWv/r3qeQHEwHsLRzFDvMqRjyM5DI6A==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -581,9 +653,6 @@ packages:
'@sinonjs/fake-timers@10.3.0':
resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==}
- '@swc/counter@0.1.3':
- resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
-
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
@@ -668,6 +737,12 @@ packages:
'@types/json5@0.0.29':
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
+ '@types/lodash-es@4.17.12':
+ resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
+
+ '@types/lodash@4.17.20':
+ resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
+
'@types/node@22.13.8':
resolution: {integrity: sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==}
@@ -782,6 +857,9 @@ packages:
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
engines: {node: '>=10'}
+ any-promise@1.3.0:
+ resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
+
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
@@ -789,6 +867,9 @@ packages:
arg@4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
+ arg@5.0.2:
+ resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
+
argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
@@ -844,6 +925,13 @@ packages:
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+ autoprefixer@10.4.22:
+ resolution: {integrity: sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==}
+ engines: {node: ^10 || ^12 || >=14}
+ hasBin: true
+ peerDependencies:
+ postcss: ^8.1.0
+
available-typed-arrays@1.0.7:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
@@ -884,6 +972,14 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+ baseline-browser-mapping@2.8.30:
+ resolution: {integrity: sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==}
+ hasBin: true
+
+ binary-extensions@2.3.0:
+ resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
+ engines: {node: '>=8'}
+
brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
@@ -899,16 +995,17 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
+ browserslist@4.28.0:
+ resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
bser@2.1.1:
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
- busboy@1.6.0:
- resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
- engines: {node: '>=10.16.0'}
-
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
@@ -925,6 +1022,10 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
+ camelcase-css@2.0.1:
+ resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
+ engines: {node: '>= 6'}
+
camelcase@5.3.1:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
@@ -936,6 +1037,9 @@ packages:
caniuse-lite@1.0.30001701:
resolution: {integrity: sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw==}
+ caniuse-lite@1.0.30001756:
+ resolution: {integrity: sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==}
+
chalk@3.0.0:
resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==}
engines: {node: '>=8'}
@@ -948,6 +1052,10 @@ packages:
resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
engines: {node: '>=10'}
+ chokidar@3.6.0:
+ resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
+ engines: {node: '>= 8.10.0'}
+
ci-info@3.9.0:
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
engines: {node: '>=8'}
@@ -955,6 +1063,9 @@ packages:
cjs-module-lexer@1.4.3:
resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==}
+ class-variance-authority@0.7.1:
+ resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
+
client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
@@ -962,6 +1073,10 @@ packages:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
co@4.6.0:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
@@ -976,17 +1091,14 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
- color-string@1.9.1:
- resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
-
- color@4.2.3:
- resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
- engines: {node: '>=12.5.0'}
-
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
+ commander@4.1.1:
+ resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
+ engines: {node: '>= 6'}
+
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -1008,6 +1120,11 @@ packages:
css.escape@1.5.1:
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
+ cssesc@3.0.0:
+ resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
+ engines: {node: '>=4'}
+ hasBin: true
+
cssom@0.3.8:
resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==}
@@ -1091,14 +1208,17 @@ packages:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
- detect-libc@2.0.3:
- resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
detect-newline@3.1.0:
resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
engines: {node: '>=8'}
+ didyoumean@1.2.2:
+ resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
+
diff-sequences@29.6.3:
resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -1107,6 +1227,9 @@ packages:
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
engines: {node: '>=0.3.1'}
+ dlv@1.1.3:
+ resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
+
doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
@@ -1129,6 +1252,9 @@ packages:
electron-to-chromium@1.5.109:
resolution: {integrity: sha512-AidaH9JETVRr9DIPGfp1kAarm/W6hRJTPuCnkF+2MqhF4KaAgRIcBc8nvjk+YMXZhwfISof/7WG29eS4iGxQLQ==}
+ electron-to-chromium@1.5.259:
+ resolution: {integrity: sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ==}
+
emittery@0.13.1:
resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
engines: {node: '>=12'}
@@ -1394,6 +1520,9 @@ packages:
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
engines: {node: '>= 6'}
+ fraction.js@5.3.4:
+ resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
+
fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
@@ -1566,9 +1695,6 @@ packages:
is-arrayish@0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
- is-arrayish@0.3.2:
- resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
-
is-async-function@2.1.1:
resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
engines: {node: '>= 0.4'}
@@ -1577,6 +1703,10 @@ packages:
resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==}
engines: {node: '>= 0.4'}
+ is-binary-path@2.1.0:
+ resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
+ engines: {node: '>=8'}
+
is-boolean-object@1.2.2:
resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==}
engines: {node: '>= 0.4'}
@@ -1851,6 +1981,10 @@ packages:
node-notifier:
optional: true
+ jiti@1.21.7:
+ resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
+ hasBin: true
+
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -1923,6 +2057,10 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
+ lilconfig@3.1.3:
+ resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
+ engines: {node: '>=14'}
+
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
@@ -1934,6 +2072,9 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
+ lodash-es@4.17.21:
+ resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
+
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
@@ -1947,6 +2088,11 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+ lucide-react@0.469.0:
+ resolution: {integrity: sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw==}
+ peerDependencies:
+ react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
lz-string@1.5.0:
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
hasBin: true
@@ -2005,21 +2151,29 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
- nanoid@3.3.8:
- resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
+ mz@2.7.0:
+ resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
+
+ nanoid@3.3.11:
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ nanoid@5.1.6:
+ resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==}
+ engines: {node: ^18 || >=20}
+ hasBin: true
+
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
- next@15.2.0:
- resolution: {integrity: sha512-VaiM7sZYX8KIAHBrRGSFytKknkrexNfGb8GlG6e93JqueCspuGte8i4ybn8z4ww1x3f2uzY4YpTaBEW4/hvsoQ==}
+ next@15.4.7:
+ resolution: {integrity: sha512-OcqRugwF7n7mC8OSYjvsZhhG1AYSvulor1EIUsIkbbEbf1qoE5EbH36Swj8WhF4cHqmDgkiam3z1c1W0J1Wifg==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
'@opentelemetry/api': ^1.1.0
- '@playwright/test': ^1.41.2
+ '@playwright/test': ^1.51.1
babel-plugin-react-compiler: '*'
react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
@@ -2040,10 +2194,17 @@ packages:
node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
+ node-releases@2.0.27:
+ resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
+
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
+ normalize-range@0.1.2:
+ resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
+ engines: {node: '>=0.10.0'}
+
npm-run-path@4.0.1:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'}
@@ -2055,6 +2216,10 @@ packages:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
+ object-hash@3.0.0:
+ resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
+ engines: {node: '>= 6'}
+
object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
@@ -2155,6 +2320,10 @@ packages:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'}
+ pify@2.3.0:
+ resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
+ engines: {node: '>=0.10.0'}
+
pirates@4.0.6:
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
engines: {node: '>= 6'}
@@ -2167,10 +2336,57 @@ packages:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
+ postcss-import@15.1.0:
+ resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ postcss: ^8.0.0
+
+ postcss-js@4.1.0:
+ resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==}
+ engines: {node: ^12 || ^14 || >= 16}
+ peerDependencies:
+ postcss: ^8.4.21
+
+ postcss-load-config@6.0.1:
+ resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==}
+ engines: {node: '>= 18'}
+ peerDependencies:
+ jiti: '>=1.21.0'
+ postcss: '>=8.0.9'
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+ postcss:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ postcss-nested@6.2.0:
+ resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
+ engines: {node: '>=12.0'}
+ peerDependencies:
+ postcss: ^8.2.14
+
+ postcss-selector-parser@6.1.2:
+ resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
+ engines: {node: '>=4'}
+
+ postcss-value-parser@4.2.0:
+ resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
+
postcss@8.4.31:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
+ postcss@8.5.6:
+ resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
+ engines: {node: ^10 || ^12 || >=14}
+
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -2206,11 +2422,24 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+ react-broadcast-sync@1.6.0:
+ resolution: {integrity: sha512-WJjEm9WZOfI4tSpxi8AjZxcd/0Wt8oYr6sKnTHvksrA5/ipsF4PfojzQPqLPUfGgsZCJ3ICRsmBY2CR7J23fJQ==}
+ engines: {node: '>=20.11.1'}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
react-dom@19.0.0:
resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==}
peerDependencies:
react: ^19.0.0
+ react-hook-form@7.66.1:
+ resolution: {integrity: sha512-2KnjpgG2Rhbi+CIiIBQQ9Df6sMGH5ExNyFl4Hw9qO7pIqMBR8Bvu9RQyjl3JM4vehzCh9soiNUM/xYMswb2EiA==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18 || ^19
+
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@@ -2224,6 +2453,13 @@ packages:
resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
engines: {node: '>=0.10.0'}
+ read-cache@1.0.0:
+ resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
+
+ readdirp@3.6.0:
+ resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
+ engines: {node: '>=8.10.0'}
+
redent@3.0.0:
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
engines: {node: '>=8'}
@@ -2312,6 +2548,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ semver@7.7.3:
+ resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
+ engines: {node: '>=10'}
+ hasBin: true
+
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@@ -2324,8 +2565,8 @@ packages:
resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
engines: {node: '>= 0.4'}
- sharp@0.33.5:
- resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
+ sharp@0.34.5:
+ resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
shebang-command@2.0.0:
@@ -2355,9 +2596,6 @@ packages:
signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
- simple-swizzle@0.2.2:
- resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
-
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
@@ -2386,10 +2624,6 @@ packages:
resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
engines: {node: '>=10'}
- streamsearch@1.1.0:
- resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
- engines: {node: '>=10.0.0'}
-
string-length@4.0.2:
resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==}
engines: {node: '>=10'}
@@ -2458,6 +2692,11 @@ packages:
babel-plugin-macros:
optional: true
+ sucrase@3.35.1:
+ resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ hasBin: true
+
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@@ -2473,6 +2712,14 @@ packages:
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
+ tailwind-merge@2.6.0:
+ resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==}
+
+ tailwindcss@3.4.18:
+ resolution: {integrity: sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==}
+ engines: {node: '>=14.0.0'}
+ hasBin: true
+
tapable@2.2.1:
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
engines: {node: '>=6'}
@@ -2481,6 +2728,13 @@ packages:
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
engines: {node: '>=8'}
+ thenify-all@1.6.0:
+ resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
+ engines: {node: '>=0.8'}
+
+ thenify@3.3.1:
+ resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
+
tinyglobby@0.2.12:
resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==}
engines: {node: '>=12.0.0'}
@@ -2506,6 +2760,9 @@ packages:
peerDependencies:
typescript: '>=4.8.4'
+ ts-interface-checker@0.1.13:
+ resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
+
ts-node@10.9.2:
resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
hasBin: true
@@ -2554,8 +2811,8 @@ packages:
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
engines: {node: '>= 0.4'}
- typescript@5.8.2:
- resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==}
+ typescript@5.7.3:
+ resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==}
engines: {node: '>=14.17'}
hasBin: true
@@ -2576,12 +2833,21 @@ packages:
peerDependencies:
browserslist: '>= 4.21.0'
+ update-browserslist-db@1.1.4:
+ resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
url-parse@1.5.10:
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
+ util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
v8-compile-cache-lib@3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
@@ -2694,6 +2960,8 @@ snapshots:
'@adobe/css-tools@4.4.2': {}
+ '@alloc/quick-lru@5.2.0': {}
+
'@ampproject/remapping@2.3.0':
dependencies:
'@jridgewell/gen-mapping': 0.3.8
@@ -2894,14 +3162,14 @@ snapshots:
dependencies:
'@jridgewell/trace-mapping': 0.3.9
- '@emnapi/runtime@1.3.1':
+ '@emnapi/runtime@1.7.1':
dependencies:
tslib: 2.8.1
optional: true
- '@eslint-community/eslint-utils@4.4.1(eslint@9.21.0)':
+ '@eslint-community/eslint-utils@4.4.1(eslint@9.21.0(jiti@1.21.7))':
dependencies:
- eslint: 9.21.0
+ eslint: 9.21.0(jiti@1.21.7)
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.1': {}
@@ -2954,79 +3222,101 @@ snapshots:
'@humanwhocodes/retry@0.4.2': {}
- '@img/sharp-darwin-arm64@0.33.5':
+ '@img/colour@1.0.0':
+ optional: true
+
+ '@img/sharp-darwin-arm64@0.34.5':
optionalDependencies:
- '@img/sharp-libvips-darwin-arm64': 1.0.4
+ '@img/sharp-libvips-darwin-arm64': 1.2.4
optional: true
- '@img/sharp-darwin-x64@0.33.5':
+ '@img/sharp-darwin-x64@0.34.5':
optionalDependencies:
- '@img/sharp-libvips-darwin-x64': 1.0.4
+ '@img/sharp-libvips-darwin-x64': 1.2.4
+ optional: true
+
+ '@img/sharp-libvips-darwin-arm64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-darwin-x64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-arm64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-arm@1.2.4':
optional: true
- '@img/sharp-libvips-darwin-arm64@1.0.4':
+ '@img/sharp-libvips-linux-ppc64@1.2.4':
optional: true
- '@img/sharp-libvips-darwin-x64@1.0.4':
+ '@img/sharp-libvips-linux-riscv64@1.2.4':
optional: true
- '@img/sharp-libvips-linux-arm64@1.0.4':
+ '@img/sharp-libvips-linux-s390x@1.2.4':
optional: true
- '@img/sharp-libvips-linux-arm@1.0.5':
+ '@img/sharp-libvips-linux-x64@1.2.4':
optional: true
- '@img/sharp-libvips-linux-s390x@1.0.4':
+ '@img/sharp-libvips-linuxmusl-arm64@1.2.4':
optional: true
- '@img/sharp-libvips-linux-x64@1.0.4':
+ '@img/sharp-libvips-linuxmusl-x64@1.2.4':
optional: true
- '@img/sharp-libvips-linuxmusl-arm64@1.0.4':
+ '@img/sharp-linux-arm64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm64': 1.2.4
optional: true
- '@img/sharp-libvips-linuxmusl-x64@1.0.4':
+ '@img/sharp-linux-arm@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm': 1.2.4
optional: true
- '@img/sharp-linux-arm64@0.33.5':
+ '@img/sharp-linux-ppc64@0.34.5':
optionalDependencies:
- '@img/sharp-libvips-linux-arm64': 1.0.4
+ '@img/sharp-libvips-linux-ppc64': 1.2.4
optional: true
- '@img/sharp-linux-arm@0.33.5':
+ '@img/sharp-linux-riscv64@0.34.5':
optionalDependencies:
- '@img/sharp-libvips-linux-arm': 1.0.5
+ '@img/sharp-libvips-linux-riscv64': 1.2.4
optional: true
- '@img/sharp-linux-s390x@0.33.5':
+ '@img/sharp-linux-s390x@0.34.5':
optionalDependencies:
- '@img/sharp-libvips-linux-s390x': 1.0.4
+ '@img/sharp-libvips-linux-s390x': 1.2.4
optional: true
- '@img/sharp-linux-x64@0.33.5':
+ '@img/sharp-linux-x64@0.34.5':
optionalDependencies:
- '@img/sharp-libvips-linux-x64': 1.0.4
+ '@img/sharp-libvips-linux-x64': 1.2.4
optional: true
- '@img/sharp-linuxmusl-arm64@0.33.5':
+ '@img/sharp-linuxmusl-arm64@0.34.5':
optionalDependencies:
- '@img/sharp-libvips-linuxmusl-arm64': 1.0.4
+ '@img/sharp-libvips-linuxmusl-arm64': 1.2.4
optional: true
- '@img/sharp-linuxmusl-x64@0.33.5':
+ '@img/sharp-linuxmusl-x64@0.34.5':
optionalDependencies:
- '@img/sharp-libvips-linuxmusl-x64': 1.0.4
+ '@img/sharp-libvips-linuxmusl-x64': 1.2.4
optional: true
- '@img/sharp-wasm32@0.33.5':
+ '@img/sharp-wasm32@0.34.5':
dependencies:
- '@emnapi/runtime': 1.3.1
+ '@emnapi/runtime': 1.7.1
+ optional: true
+
+ '@img/sharp-win32-arm64@0.34.5':
optional: true
- '@img/sharp-win32-ia32@0.33.5':
+ '@img/sharp-win32-ia32@0.34.5':
optional: true
- '@img/sharp-win32-x64@0.33.5':
+ '@img/sharp-win32-x64@0.34.5':
optional: true
'@istanbuljs/load-nyc-config@1.1.0':
@@ -3048,7 +3338,7 @@ snapshots:
jest-util: 29.7.0
slash: 3.0.0
- '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2))':
+ '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.7.3))':
dependencies:
'@jest/console': 29.7.0
'@jest/reporters': 29.7.0
@@ -3062,7 +3352,7 @@ snapshots:
exit: 0.1.2
graceful-fs: 4.2.11
jest-changed-files: 29.7.0
- jest-config: 29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2))
+ jest-config: 29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.7.3))
jest-haste-map: 29.7.0
jest-message-util: 29.7.0
jest-regex-util: 29.6.3
@@ -3223,34 +3513,34 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
- '@next/env@15.2.0': {}
+ '@next/env@15.4.7': {}
'@next/eslint-plugin-next@15.2.0':
dependencies:
fast-glob: 3.3.1
- '@next/swc-darwin-arm64@15.2.0':
+ '@next/swc-darwin-arm64@15.4.7':
optional: true
- '@next/swc-darwin-x64@15.2.0':
+ '@next/swc-darwin-x64@15.4.7':
optional: true
- '@next/swc-linux-arm64-gnu@15.2.0':
+ '@next/swc-linux-arm64-gnu@15.4.7':
optional: true
- '@next/swc-linux-arm64-musl@15.2.0':
+ '@next/swc-linux-arm64-musl@15.4.7':
optional: true
- '@next/swc-linux-x64-gnu@15.2.0':
+ '@next/swc-linux-x64-gnu@15.4.7':
optional: true
- '@next/swc-linux-x64-musl@15.2.0':
+ '@next/swc-linux-x64-musl@15.4.7':
optional: true
- '@next/swc-win32-arm64-msvc@15.2.0':
+ '@next/swc-win32-arm64-msvc@15.4.7':
optional: true
- '@next/swc-win32-x64-msvc@15.2.0':
+ '@next/swc-win32-x64-msvc@15.4.7':
optional: true
'@nodelib/fs.scandir@2.1.5':
@@ -3281,8 +3571,6 @@ snapshots:
dependencies:
'@sinonjs/commons': 3.0.1
- '@swc/counter@0.1.3': {}
-
'@swc/helpers@0.5.15':
dependencies:
tslib: 2.8.1
@@ -3382,6 +3670,12 @@ snapshots:
'@types/json5@0.0.29': {}
+ '@types/lodash-es@4.17.12':
+ dependencies:
+ '@types/lodash': 4.17.20
+
+ '@types/lodash@4.17.20': {}
+
'@types/node@22.13.8':
dependencies:
undici-types: 6.20.0
@@ -3404,32 +3698,32 @@ snapshots:
dependencies:
'@types/yargs-parser': 21.0.3
- '@typescript-eslint/eslint-plugin@8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2)':
+ '@typescript-eslint/eslint-plugin@8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.7.3))(eslint@9.21.0(jiti@1.21.7))(typescript@5.7.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
- '@typescript-eslint/parser': 8.25.0(eslint@9.21.0)(typescript@5.8.2)
+ '@typescript-eslint/parser': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.7.3)
'@typescript-eslint/scope-manager': 8.25.0
- '@typescript-eslint/type-utils': 8.25.0(eslint@9.21.0)(typescript@5.8.2)
- '@typescript-eslint/utils': 8.25.0(eslint@9.21.0)(typescript@5.8.2)
+ '@typescript-eslint/type-utils': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.7.3)
+ '@typescript-eslint/utils': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.7.3)
'@typescript-eslint/visitor-keys': 8.25.0
- eslint: 9.21.0
+ eslint: 9.21.0(jiti@1.21.7)
graphemer: 1.4.0
ignore: 5.3.2
natural-compare: 1.4.0
- ts-api-utils: 2.0.1(typescript@5.8.2)
- typescript: 5.8.2
+ ts-api-utils: 2.0.1(typescript@5.7.3)
+ typescript: 5.7.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/parser@8.25.0(eslint@9.21.0)(typescript@5.8.2)':
+ '@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.7.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.25.0
'@typescript-eslint/types': 8.25.0
- '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.8.2)
+ '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.7.3)
'@typescript-eslint/visitor-keys': 8.25.0
debug: 4.4.0
- eslint: 9.21.0
- typescript: 5.8.2
+ eslint: 9.21.0(jiti@1.21.7)
+ typescript: 5.7.3
transitivePeerDependencies:
- supports-color
@@ -3438,20 +3732,20 @@ snapshots:
'@typescript-eslint/types': 8.25.0
'@typescript-eslint/visitor-keys': 8.25.0
- '@typescript-eslint/type-utils@8.25.0(eslint@9.21.0)(typescript@5.8.2)':
+ '@typescript-eslint/type-utils@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.7.3)':
dependencies:
- '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.8.2)
- '@typescript-eslint/utils': 8.25.0(eslint@9.21.0)(typescript@5.8.2)
+ '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.7.3)
+ '@typescript-eslint/utils': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.7.3)
debug: 4.4.0
- eslint: 9.21.0
- ts-api-utils: 2.0.1(typescript@5.8.2)
- typescript: 5.8.2
+ eslint: 9.21.0(jiti@1.21.7)
+ ts-api-utils: 2.0.1(typescript@5.7.3)
+ typescript: 5.7.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/types@8.25.0': {}
- '@typescript-eslint/typescript-estree@8.25.0(typescript@5.8.2)':
+ '@typescript-eslint/typescript-estree@8.25.0(typescript@5.7.3)':
dependencies:
'@typescript-eslint/types': 8.25.0
'@typescript-eslint/visitor-keys': 8.25.0
@@ -3460,19 +3754,19 @@ snapshots:
is-glob: 4.0.3
minimatch: 9.0.5
semver: 7.7.1
- ts-api-utils: 2.0.1(typescript@5.8.2)
- typescript: 5.8.2
+ ts-api-utils: 2.0.1(typescript@5.7.3)
+ typescript: 5.7.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/utils@8.25.0(eslint@9.21.0)(typescript@5.8.2)':
+ '@typescript-eslint/utils@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.7.3)':
dependencies:
- '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0)
+ '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0(jiti@1.21.7))
'@typescript-eslint/scope-manager': 8.25.0
'@typescript-eslint/types': 8.25.0
- '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.8.2)
- eslint: 9.21.0
- typescript: 5.8.2
+ '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.7.3)
+ eslint: 9.21.0(jiti@1.21.7)
+ typescript: 5.7.3
transitivePeerDependencies:
- supports-color
@@ -3523,6 +3817,8 @@ snapshots:
ansi-styles@5.2.0: {}
+ any-promise@1.3.0: {}
+
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
@@ -3530,6 +3826,8 @@ snapshots:
arg@4.1.3: {}
+ arg@5.0.2: {}
+
argparse@1.0.10:
dependencies:
sprintf-js: 1.0.3
@@ -3612,6 +3910,16 @@ snapshots:
asynckit@0.4.0: {}
+ autoprefixer@10.4.22(postcss@8.5.6):
+ dependencies:
+ browserslist: 4.28.0
+ caniuse-lite: 1.0.30001756
+ fraction.js: 5.3.4
+ normalize-range: 0.1.2
+ picocolors: 1.1.1
+ postcss: 8.5.6
+ postcss-value-parser: 4.2.0
+
available-typed-arrays@1.0.7:
dependencies:
possible-typed-array-names: 1.1.0
@@ -3677,6 +3985,10 @@ snapshots:
balanced-match@1.0.2: {}
+ baseline-browser-mapping@2.8.30: {}
+
+ binary-extensions@2.3.0: {}
+
brace-expansion@1.1.11:
dependencies:
balanced-match: 1.0.2
@@ -3697,16 +4009,20 @@ snapshots:
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.24.4)
+ browserslist@4.28.0:
+ dependencies:
+ baseline-browser-mapping: 2.8.30
+ caniuse-lite: 1.0.30001756
+ electron-to-chromium: 1.5.259
+ node-releases: 2.0.27
+ update-browserslist-db: 1.1.4(browserslist@4.28.0)
+
bser@2.1.1:
dependencies:
node-int64: 0.4.0
buffer-from@1.1.2: {}
- busboy@1.6.0:
- dependencies:
- streamsearch: 1.1.0
-
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
@@ -3726,12 +4042,16 @@ snapshots:
callsites@3.1.0: {}
+ camelcase-css@2.0.1: {}
+
camelcase@5.3.1: {}
camelcase@6.3.0: {}
caniuse-lite@1.0.30001701: {}
+ caniuse-lite@1.0.30001756: {}
+
chalk@3.0.0:
dependencies:
ansi-styles: 4.3.0
@@ -3744,10 +4064,26 @@ snapshots:
char-regex@1.0.2: {}
+ chokidar@3.6.0:
+ dependencies:
+ anymatch: 3.1.3
+ braces: 3.0.3
+ glob-parent: 5.1.2
+ is-binary-path: 2.1.0
+ is-glob: 4.0.3
+ normalize-path: 3.0.0
+ readdirp: 3.6.0
+ optionalDependencies:
+ fsevents: 2.3.3
+
ci-info@3.9.0: {}
cjs-module-lexer@1.4.3: {}
+ class-variance-authority@0.7.1:
+ dependencies:
+ clsx: 2.1.1
+
client-only@0.0.1: {}
cliui@8.0.1:
@@ -3756,6 +4092,8 @@ snapshots:
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
+ clsx@2.1.1: {}
+
co@4.6.0: {}
collect-v8-coverage@1.0.2: {}
@@ -3766,33 +4104,23 @@ snapshots:
color-name@1.1.4: {}
- color-string@1.9.1:
- dependencies:
- color-name: 1.1.4
- simple-swizzle: 0.2.2
- optional: true
-
- color@4.2.3:
- dependencies:
- color-convert: 2.0.1
- color-string: 1.9.1
- optional: true
-
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
+ commander@4.1.1: {}
+
concat-map@0.0.1: {}
convert-source-map@2.0.0: {}
- create-jest@29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2)):
+ create-jest@29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.7.3)):
dependencies:
'@jest/types': 29.6.3
chalk: 4.1.2
exit: 0.1.2
graceful-fs: 4.2.11
- jest-config: 29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2))
+ jest-config: 29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.7.3))
jest-util: 29.7.0
prompts: 2.4.2
transitivePeerDependencies:
@@ -3811,6 +4139,8 @@ snapshots:
css.escape@1.5.1: {}
+ cssesc@3.0.0: {}
+
cssom@0.3.8: {}
cssom@0.5.0: {}
@@ -3879,15 +4209,19 @@ snapshots:
dequal@2.0.3: {}
- detect-libc@2.0.3:
+ detect-libc@2.1.2:
optional: true
detect-newline@3.1.0: {}
+ didyoumean@1.2.2: {}
+
diff-sequences@29.6.3: {}
diff@4.0.2: {}
+ dlv@1.1.3: {}
+
doctrine@2.1.0:
dependencies:
esutils: 2.0.3
@@ -3908,6 +4242,8 @@ snapshots:
electron-to-chromium@1.5.109: {}
+ electron-to-chromium@1.5.259: {}
+
emittery@0.13.1: {}
emoji-regex@8.0.0: {}
@@ -4037,21 +4373,21 @@ snapshots:
optionalDependencies:
source-map: 0.6.1
- eslint-config-next@15.2.0(eslint@9.21.0)(typescript@5.8.2):
+ eslint-config-next@15.2.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.7.3):
dependencies:
'@next/eslint-plugin-next': 15.2.0
'@rushstack/eslint-patch': 1.10.5
- '@typescript-eslint/eslint-plugin': 8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2)
- '@typescript-eslint/parser': 8.25.0(eslint@9.21.0)(typescript@5.8.2)
- eslint: 9.21.0
+ '@typescript-eslint/eslint-plugin': 8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.7.3))(eslint@9.21.0(jiti@1.21.7))(typescript@5.7.3)
+ '@typescript-eslint/parser': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.7.3)
+ eslint: 9.21.0(jiti@1.21.7)
eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0)
- eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0)
- eslint-plugin-jsx-a11y: 6.10.2(eslint@9.21.0)
- eslint-plugin-react: 7.37.4(eslint@9.21.0)
- eslint-plugin-react-hooks: 5.2.0(eslint@9.21.0)
+ eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@1.21.7))
+ eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.7.3))(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@1.21.7))
+ eslint-plugin-jsx-a11y: 6.10.2(eslint@9.21.0(jiti@1.21.7))
+ eslint-plugin-react: 7.37.4(eslint@9.21.0(jiti@1.21.7))
+ eslint-plugin-react-hooks: 5.2.0(eslint@9.21.0(jiti@1.21.7))
optionalDependencies:
- typescript: 5.8.2
+ typescript: 5.7.3
transitivePeerDependencies:
- eslint-import-resolver-webpack
- eslint-plugin-import-x
@@ -4065,33 +4401,33 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0):
+ eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@1.21.7)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.0
enhanced-resolve: 5.18.1
- eslint: 9.21.0
+ eslint: 9.21.0(jiti@1.21.7)
get-tsconfig: 4.10.0
is-bun-module: 1.3.0
stable-hash: 0.0.4
tinyglobby: 0.2.12
optionalDependencies:
- eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0)
+ eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.7.3))(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@1.21.7))
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0):
+ eslint-module-utils@2.12.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@1.21.7)):
dependencies:
debug: 3.2.7
optionalDependencies:
- '@typescript-eslint/parser': 8.25.0(eslint@9.21.0)(typescript@5.8.2)
- eslint: 9.21.0
+ '@typescript-eslint/parser': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.7.3)
+ eslint: 9.21.0(jiti@1.21.7)
eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0)
+ eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@1.21.7))
transitivePeerDependencies:
- supports-color
- eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0):
+ eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.7.3))(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@1.21.7)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.8
@@ -4100,9 +4436,9 @@ snapshots:
array.prototype.flatmap: 1.3.3
debug: 3.2.7
doctrine: 2.1.0
- eslint: 9.21.0
+ eslint: 9.21.0(jiti@1.21.7)
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0)
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@1.21.7))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -4114,13 +4450,13 @@ snapshots:
string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0
optionalDependencies:
- '@typescript-eslint/parser': 8.25.0(eslint@9.21.0)(typescript@5.8.2)
+ '@typescript-eslint/parser': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.7.3)
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
- supports-color
- eslint-plugin-jsx-a11y@6.10.2(eslint@9.21.0):
+ eslint-plugin-jsx-a11y@6.10.2(eslint@9.21.0(jiti@1.21.7)):
dependencies:
aria-query: 5.3.2
array-includes: 3.1.8
@@ -4130,7 +4466,7 @@ snapshots:
axobject-query: 4.1.0
damerau-levenshtein: 1.0.8
emoji-regex: 9.2.2
- eslint: 9.21.0
+ eslint: 9.21.0(jiti@1.21.7)
hasown: 2.0.2
jsx-ast-utils: 3.3.5
language-tags: 1.0.9
@@ -4139,11 +4475,11 @@ snapshots:
safe-regex-test: 1.1.0
string.prototype.includes: 2.0.1
- eslint-plugin-react-hooks@5.2.0(eslint@9.21.0):
+ eslint-plugin-react-hooks@5.2.0(eslint@9.21.0(jiti@1.21.7)):
dependencies:
- eslint: 9.21.0
+ eslint: 9.21.0(jiti@1.21.7)
- eslint-plugin-react@7.37.4(eslint@9.21.0):
+ eslint-plugin-react@7.37.4(eslint@9.21.0(jiti@1.21.7)):
dependencies:
array-includes: 3.1.8
array.prototype.findlast: 1.2.5
@@ -4151,7 +4487,7 @@ snapshots:
array.prototype.tosorted: 1.1.4
doctrine: 2.1.0
es-iterator-helpers: 1.2.1
- eslint: 9.21.0
+ eslint: 9.21.0(jiti@1.21.7)
estraverse: 5.3.0
hasown: 2.0.2
jsx-ast-utils: 3.3.5
@@ -4174,9 +4510,9 @@ snapshots:
eslint-visitor-keys@4.2.0: {}
- eslint@9.21.0:
+ eslint@9.21.0(jiti@1.21.7):
dependencies:
- '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0)
+ '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0(jiti@1.21.7))
'@eslint-community/regexpp': 4.12.1
'@eslint/config-array': 0.19.2
'@eslint/core': 0.12.0
@@ -4210,6 +4546,8 @@ snapshots:
minimatch: 3.1.2
natural-compare: 1.4.0
optionator: 0.9.4
+ optionalDependencies:
+ jiti: 1.21.7
transitivePeerDependencies:
- supports-color
@@ -4325,6 +4663,8 @@ snapshots:
es-set-tostringtag: 2.1.0
mime-types: 2.1.35
+ fraction.js@5.3.4: {}
+
fs.realpath@1.0.0: {}
fsevents@2.3.3:
@@ -4497,9 +4837,6 @@ snapshots:
is-arrayish@0.2.1: {}
- is-arrayish@0.3.2:
- optional: true
-
is-async-function@2.1.1:
dependencies:
async-function: 1.0.0
@@ -4512,6 +4849,10 @@ snapshots:
dependencies:
has-bigints: 1.1.0
+ is-binary-path@2.1.0:
+ dependencies:
+ binary-extensions: 2.3.0
+
is-boolean-object@1.2.2:
dependencies:
call-bound: 1.0.3
@@ -4697,16 +5038,16 @@ snapshots:
- babel-plugin-macros
- supports-color
- jest-cli@29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2)):
+ jest-cli@29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.7.3)):
dependencies:
- '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2))
+ '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.7.3))
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
chalk: 4.1.2
- create-jest: 29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2))
+ create-jest: 29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.7.3))
exit: 0.1.2
import-local: 3.2.0
- jest-config: 29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2))
+ jest-config: 29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.7.3))
jest-util: 29.7.0
jest-validate: 29.7.0
yargs: 17.7.2
@@ -4716,7 +5057,7 @@ snapshots:
- supports-color
- ts-node
- jest-config@29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2)):
+ jest-config@29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.7.3)):
dependencies:
'@babel/core': 7.26.9
'@jest/test-sequencer': 29.7.0
@@ -4742,7 +5083,7 @@ snapshots:
strip-json-comments: 3.1.1
optionalDependencies:
'@types/node': 22.13.8
- ts-node: 10.9.2(@types/node@22.13.8)(typescript@5.8.2)
+ ts-node: 10.9.2(@types/node@22.13.8)(typescript@5.7.3)
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
@@ -4977,18 +5318,20 @@ snapshots:
merge-stream: 2.0.0
supports-color: 8.1.1
- jest@29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2)):
+ jest@29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.7.3)):
dependencies:
- '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2))
+ '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.7.3))
'@jest/types': 29.6.3
import-local: 3.2.0
- jest-cli: 29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2))
+ jest-cli: 29.7.0(@types/node@22.13.8)(ts-node@10.9.2(@types/node@22.13.8)(typescript@5.7.3))
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
- supports-color
- ts-node
+ jiti@1.21.7: {}
+
js-tokens@4.0.0: {}
js-yaml@3.14.1:
@@ -5075,6 +5418,8 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
+ lilconfig@3.1.3: {}
+
lines-and-columns@1.2.4: {}
locate-path@5.0.0:
@@ -5085,6 +5430,8 @@ snapshots:
dependencies:
p-locate: 5.0.0
+ lodash-es@4.17.21: {}
+
lodash.merge@4.6.2: {}
lodash@4.17.21: {}
@@ -5097,6 +5444,10 @@ snapshots:
dependencies:
yallist: 3.1.1
+ lucide-react@0.469.0(react@19.0.0):
+ dependencies:
+ react: 19.0.0
+
lz-string@1.5.0: {}
make-dir@4.0.0:
@@ -5142,31 +5493,37 @@ snapshots:
ms@2.1.3: {}
- nanoid@3.3.8: {}
+ mz@2.7.0:
+ dependencies:
+ any-promise: 1.3.0
+ object-assign: 4.1.1
+ thenify-all: 1.6.0
+
+ nanoid@3.3.11: {}
+
+ nanoid@5.1.6: {}
natural-compare@1.4.0: {}
- next@15.2.0(@babel/core@7.26.9)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+ next@15.4.7(@babel/core@7.26.9)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
- '@next/env': 15.2.0
- '@swc/counter': 0.1.3
+ '@next/env': 15.4.7
'@swc/helpers': 0.5.15
- busboy: 1.6.0
caniuse-lite: 1.0.30001701
postcss: 8.4.31
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
styled-jsx: 5.1.6(@babel/core@7.26.9)(react@19.0.0)
optionalDependencies:
- '@next/swc-darwin-arm64': 15.2.0
- '@next/swc-darwin-x64': 15.2.0
- '@next/swc-linux-arm64-gnu': 15.2.0
- '@next/swc-linux-arm64-musl': 15.2.0
- '@next/swc-linux-x64-gnu': 15.2.0
- '@next/swc-linux-x64-musl': 15.2.0
- '@next/swc-win32-arm64-msvc': 15.2.0
- '@next/swc-win32-x64-msvc': 15.2.0
- sharp: 0.33.5
+ '@next/swc-darwin-arm64': 15.4.7
+ '@next/swc-darwin-x64': 15.4.7
+ '@next/swc-linux-arm64-gnu': 15.4.7
+ '@next/swc-linux-arm64-musl': 15.4.7
+ '@next/swc-linux-x64-gnu': 15.4.7
+ '@next/swc-linux-x64-musl': 15.4.7
+ '@next/swc-win32-arm64-msvc': 15.4.7
+ '@next/swc-win32-x64-msvc': 15.4.7
+ sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
@@ -5175,8 +5532,12 @@ snapshots:
node-releases@2.0.19: {}
+ node-releases@2.0.27: {}
+
normalize-path@3.0.0: {}
+ normalize-range@0.1.2: {}
+
npm-run-path@4.0.1:
dependencies:
path-key: 3.1.1
@@ -5185,6 +5546,8 @@ snapshots:
object-assign@4.1.1: {}
+ object-hash@3.0.0: {}
+
object-inspect@1.13.4: {}
object-keys@1.1.1: {}
@@ -5294,6 +5657,8 @@ snapshots:
picomatch@4.0.2: {}
+ pify@2.3.0: {}
+
pirates@4.0.6: {}
pkg-dir@4.2.0:
@@ -5302,9 +5667,46 @@ snapshots:
possible-typed-array-names@1.1.0: {}
+ postcss-import@15.1.0(postcss@8.5.6):
+ dependencies:
+ postcss: 8.5.6
+ postcss-value-parser: 4.2.0
+ read-cache: 1.0.0
+ resolve: 1.22.10
+
+ postcss-js@4.1.0(postcss@8.5.6):
+ dependencies:
+ camelcase-css: 2.0.1
+ postcss: 8.5.6
+
+ postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6):
+ dependencies:
+ lilconfig: 3.1.3
+ optionalDependencies:
+ jiti: 1.21.7
+ postcss: 8.5.6
+
+ postcss-nested@6.2.0(postcss@8.5.6):
+ dependencies:
+ postcss: 8.5.6
+ postcss-selector-parser: 6.1.2
+
+ postcss-selector-parser@6.1.2:
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+
+ postcss-value-parser@4.2.0: {}
+
postcss@8.4.31:
dependencies:
- nanoid: 3.3.8
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ postcss@8.5.6:
+ dependencies:
+ nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
@@ -5345,11 +5747,22 @@ snapshots:
queue-microtask@1.2.3: {}
+ react-broadcast-sync@1.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+ dependencies:
+ '@babel/runtime': 7.26.9
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ tslib: 2.8.1
+
react-dom@19.0.0(react@19.0.0):
dependencies:
react: 19.0.0
scheduler: 0.25.0
+ react-hook-form@7.66.1(react@19.0.0):
+ dependencies:
+ react: 19.0.0
+
react-is@16.13.1: {}
react-is@17.0.2: {}
@@ -5358,6 +5771,14 @@ snapshots:
react@19.0.0: {}
+ read-cache@1.0.0:
+ dependencies:
+ pify: 2.3.0
+
+ readdirp@3.6.0:
+ dependencies:
+ picomatch: 2.3.1
+
redent@3.0.0:
dependencies:
indent-string: 4.0.0
@@ -5450,6 +5871,9 @@ snapshots:
semver@7.7.1: {}
+ semver@7.7.3:
+ optional: true
+
set-function-length@1.2.2:
dependencies:
define-data-property: 1.1.4
@@ -5472,31 +5896,36 @@ snapshots:
es-errors: 1.3.0
es-object-atoms: 1.1.1
- sharp@0.33.5:
+ sharp@0.34.5:
dependencies:
- color: 4.2.3
- detect-libc: 2.0.3
- semver: 7.7.1
+ '@img/colour': 1.0.0
+ detect-libc: 2.1.2
+ semver: 7.7.3
optionalDependencies:
- '@img/sharp-darwin-arm64': 0.33.5
- '@img/sharp-darwin-x64': 0.33.5
- '@img/sharp-libvips-darwin-arm64': 1.0.4
- '@img/sharp-libvips-darwin-x64': 1.0.4
- '@img/sharp-libvips-linux-arm': 1.0.5
- '@img/sharp-libvips-linux-arm64': 1.0.4
- '@img/sharp-libvips-linux-s390x': 1.0.4
- '@img/sharp-libvips-linux-x64': 1.0.4
- '@img/sharp-libvips-linuxmusl-arm64': 1.0.4
- '@img/sharp-libvips-linuxmusl-x64': 1.0.4
- '@img/sharp-linux-arm': 0.33.5
- '@img/sharp-linux-arm64': 0.33.5
- '@img/sharp-linux-s390x': 0.33.5
- '@img/sharp-linux-x64': 0.33.5
- '@img/sharp-linuxmusl-arm64': 0.33.5
- '@img/sharp-linuxmusl-x64': 0.33.5
- '@img/sharp-wasm32': 0.33.5
- '@img/sharp-win32-ia32': 0.33.5
- '@img/sharp-win32-x64': 0.33.5
+ '@img/sharp-darwin-arm64': 0.34.5
+ '@img/sharp-darwin-x64': 0.34.5
+ '@img/sharp-libvips-darwin-arm64': 1.2.4
+ '@img/sharp-libvips-darwin-x64': 1.2.4
+ '@img/sharp-libvips-linux-arm': 1.2.4
+ '@img/sharp-libvips-linux-arm64': 1.2.4
+ '@img/sharp-libvips-linux-ppc64': 1.2.4
+ '@img/sharp-libvips-linux-riscv64': 1.2.4
+ '@img/sharp-libvips-linux-s390x': 1.2.4
+ '@img/sharp-libvips-linux-x64': 1.2.4
+ '@img/sharp-libvips-linuxmusl-arm64': 1.2.4
+ '@img/sharp-libvips-linuxmusl-x64': 1.2.4
+ '@img/sharp-linux-arm': 0.34.5
+ '@img/sharp-linux-arm64': 0.34.5
+ '@img/sharp-linux-ppc64': 0.34.5
+ '@img/sharp-linux-riscv64': 0.34.5
+ '@img/sharp-linux-s390x': 0.34.5
+ '@img/sharp-linux-x64': 0.34.5
+ '@img/sharp-linuxmusl-arm64': 0.34.5
+ '@img/sharp-linuxmusl-x64': 0.34.5
+ '@img/sharp-wasm32': 0.34.5
+ '@img/sharp-win32-arm64': 0.34.5
+ '@img/sharp-win32-ia32': 0.34.5
+ '@img/sharp-win32-x64': 0.34.5
optional: true
shebang-command@2.0.0:
@@ -5535,11 +5964,6 @@ snapshots:
signal-exit@3.0.7: {}
- simple-swizzle@0.2.2:
- dependencies:
- is-arrayish: 0.3.2
- optional: true
-
sisteransi@1.0.5: {}
slash@3.0.0: {}
@@ -5561,8 +5985,6 @@ snapshots:
dependencies:
escape-string-regexp: 2.0.0
- streamsearch@1.1.0: {}
-
string-length@4.0.2:
dependencies:
char-regex: 1.0.2
@@ -5647,6 +6069,16 @@ snapshots:
optionalDependencies:
'@babel/core': 7.26.9
+ sucrase@3.35.1:
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.8
+ commander: 4.1.1
+ lines-and-columns: 1.2.4
+ mz: 2.7.0
+ pirates: 4.0.6
+ tinyglobby: 0.2.12
+ ts-interface-checker: 0.1.13
+
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
@@ -5659,6 +6091,36 @@ snapshots:
symbol-tree@3.2.4: {}
+ tailwind-merge@2.6.0: {}
+
+ tailwindcss@3.4.18:
+ dependencies:
+ '@alloc/quick-lru': 5.2.0
+ arg: 5.0.2
+ chokidar: 3.6.0
+ didyoumean: 1.2.2
+ dlv: 1.1.3
+ fast-glob: 3.3.3
+ glob-parent: 6.0.2
+ is-glob: 4.0.3
+ jiti: 1.21.7
+ lilconfig: 3.1.3
+ micromatch: 4.0.8
+ normalize-path: 3.0.0
+ object-hash: 3.0.0
+ picocolors: 1.1.1
+ postcss: 8.5.6
+ postcss-import: 15.1.0(postcss@8.5.6)
+ postcss-js: 4.1.0(postcss@8.5.6)
+ postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)
+ postcss-nested: 6.2.0(postcss@8.5.6)
+ postcss-selector-parser: 6.1.2
+ resolve: 1.22.10
+ sucrase: 3.35.1
+ transitivePeerDependencies:
+ - tsx
+ - yaml
+
tapable@2.2.1: {}
test-exclude@6.0.0:
@@ -5667,6 +6129,14 @@ snapshots:
glob: 7.2.3
minimatch: 3.1.2
+ thenify-all@1.6.0:
+ dependencies:
+ thenify: 3.3.1
+
+ thenify@3.3.1:
+ dependencies:
+ any-promise: 1.3.0
+
tinyglobby@0.2.12:
dependencies:
fdir: 6.4.3(picomatch@4.0.2)
@@ -5689,11 +6159,13 @@ snapshots:
dependencies:
punycode: 2.3.1
- ts-api-utils@2.0.1(typescript@5.8.2):
+ ts-api-utils@2.0.1(typescript@5.7.3):
dependencies:
- typescript: 5.8.2
+ typescript: 5.7.3
- ts-node@10.9.2(@types/node@22.13.8)(typescript@5.8.2):
+ ts-interface-checker@0.1.13: {}
+
+ ts-node@10.9.2(@types/node@22.13.8)(typescript@5.7.3):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.11
@@ -5707,7 +6179,7 @@ snapshots:
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
- typescript: 5.8.2
+ typescript: 5.7.3
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
@@ -5761,7 +6233,7 @@ snapshots:
possible-typed-array-names: 1.1.0
reflect.getprototypeof: 1.0.10
- typescript@5.8.2: {}
+ typescript@5.7.3: {}
unbox-primitive@1.1.0:
dependencies:
@@ -5780,6 +6252,12 @@ snapshots:
escalade: 3.2.0
picocolors: 1.1.1
+ update-browserslist-db@1.1.4(browserslist@4.28.0):
+ dependencies:
+ browserslist: 4.28.0
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
uri-js@4.4.1:
dependencies:
punycode: 2.3.1
@@ -5789,6 +6267,8 @@ snapshots:
querystringify: 2.2.0
requires-port: 1.0.0
+ util-deprecate@1.0.2: {}
+
v8-compile-cache-lib@3.0.1: {}
v8-to-istanbul@9.3.0:
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..12a703d
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..718d6fe
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/tailwind.config.ts b/tailwind.config.ts
new file mode 100644
index 0000000..1c6e67c
--- /dev/null
+++ b/tailwind.config.ts
@@ -0,0 +1,69 @@
+import type { Config } from "tailwindcss";
+
+const config: Config = {
+ darkMode: ["class", '[data-theme="dark"]'],
+ content: [
+ "./app/**/*.{ts,tsx}",
+ "./components/**/*.{ts,tsx}",
+ "./pages/**/*.{ts,tsx}",
+ ],
+ theme: {
+ extend: {
+ colors: {
+ border: "hsl(var(--border))",
+ input: "hsl(var(--input))",
+ ring: "hsl(var(--ring))",
+ background: "hsl(var(--background))",
+ foreground: "hsl(var(--foreground))",
+ primary: {
+ DEFAULT: "hsl(var(--primary))",
+ foreground: "hsl(var(--primary-foreground))",
+ },
+ secondary: {
+ DEFAULT: "hsl(var(--secondary))",
+ foreground: "hsl(var(--secondary-foreground))",
+ },
+ muted: {
+ DEFAULT: "hsl(var(--muted))",
+ foreground: "hsl(var(--muted-foreground))",
+ },
+ accent: {
+ DEFAULT: "hsl(var(--accent))",
+ foreground: "hsl(var(--accent-foreground))",
+ },
+ card: {
+ DEFAULT: "hsl(var(--card))",
+ foreground: "hsl(var(--card-foreground))",
+ },
+ destructive: {
+ DEFAULT: "hsl(var(--destructive))",
+ foreground: "hsl(var(--destructive-foreground))",
+ },
+ success: "hsl(var(--success))",
+ warning: "hsl(var(--warning))",
+ },
+ borderRadius: {
+ lg: "var(--radius)",
+ md: "calc(var(--radius) - 2px)",
+ sm: "calc(var(--radius) - 4px)",
+ },
+ keyframes: {
+ "in": {
+ "0%": { opacity: "0" },
+ "100%": { opacity: "1" },
+ },
+ "slide-in-from-bottom-2": {
+ "0%": { transform: "translateY(0.5rem)", opacity: "0" },
+ "100%": { transform: "translateY(0)", opacity: "1" },
+ },
+ },
+ animation: {
+ "in": "in 0.2s ease-out",
+ "slide-in-from-bottom-2": "slide-in-from-bottom-2 0.3s ease-out",
+ },
+ },
+ },
+ plugins: [],
+};
+
+export default config;