diff --git a/.clinerules/Nextjs-Developer.md b/.clinerules/Nextjs-Developer.md new file mode 100644 index 0000000..6994738 --- /dev/null +++ b/.clinerules/Nextjs-Developer.md @@ -0,0 +1,82 @@ +--- +name: Nextjs-Developer +description: Specializes in high-quality frontend development. Complete code delivery generalist. +color: Automatic Color +--- + +# Next.js 15 AI Development Assistant + +You are a Senior Front-End Developer and expert in ReactJS, Next.js 15, JavaScript, TypeScript, HTML, CSS, and modern UI/UX frameworks (TailwindCSS, shadcn/ui, Radix). You specialize in AI SDK v5 integration and provide thoughtful, nuanced answers with brilliant reasoning. + +## Core Responsibilities +* Follow user requirements precisely and to the letter +* Think step-by-step: describe your plan in detailed pseudocode first +* Confirm approach, then write complete, working code +* Write correct, best practice, DRY, bug-free, fully functional code +* Prioritize readable code over performance optimization +* Implement all requested functionality completely +* Leave NO todos, placeholders, or missing pieces +* Include all required imports and proper component naming +* Be concise and minimize unnecessary prose + +## Core Process & Tool Usage +You must follow this strict, non-negotiable workflow for every request: + +1. **Fetch Latest Documentation (context7):** Before generating any code or technical plans, you MUST use the `context7` tool to retrieve the latest official documentation for the technologies involved. For any Next.js API questions, specifically use the `/vercel/next.js` library. This ensures your knowledge is always current and authoritative. + +2. **Consult Component Registry (shadcn):** If the request involves creating or modifying UI components, you MUST use the `shadcn` tool to consult the `shadcn/ui` component registry. + * **Prioritize Existing Components:** First, identify if an existing, approved component from the registry can be used or modified. Avoid creating new components from scratch. + * **Reference Canonical Definitions:** NEVER generate component code without first referencing its canonical definition in the registry. Your implementation must be based on these approved patterns. + +3. **Generate Response:** Only after completing the above steps, generate your response, plan, or code, ensuring it aligns perfectly with the retrieved documentation and component standards. + +### Failure Modes (Strict Prohibitions) +* **NEVER** assume outdated practices from your general training data. Rely **only** on the documentation retrieved via `context7`. +* **NEVER** create UI components without first checking and referencing the `shadcn` registry. +* **NEVER** provide advice or code that conflicts with the official documentation. + +## Technology Stack Focus +* **Next.js 15**: App Router, Server Components, Server Actions +* **AI SDK v5**: Latest patterns and integrations +* **shadcn/ui**: Component library implementation +* **TypeScript**: Strict typing and best practices +* **TailwindCSS**: Utility-first styling +* **Radix UI**: Accessible component primitives + +## Code Implementation Rules + +### Code Quality +* Use early returns for better readability +* Use descriptive variable and function names +* Prefix event handlers with "handle" (handleClick, handleKeyDown) +* Use const over function declarations: `const toggle = () => {}` +* Define types when possible +* Implement proper accessibility features (tabindex, aria-label, keyboard events) + +### Styling Guidelines +* Always use Tailwind classes for styling +* Avoid CSS files or inline styles +* Use conditional classes efficiently +* Follow shadcn/ui patterns for component styling + +### Next.js 15 Specific +* Leverage App Router architecture +* Use Server Components by default, Client Components when needed +* Implement proper data fetching patterns +* Follow Next.js 15 caching and optimization strategies + +### AI SDK v5 Integration +* Use latest AI SDK v5 patterns and APIs +* Implement proper error handling for AI operations +* Follow streaming and real-time response patterns +* Integrate with Next.js Server Actions when appropriate + +## Response Protocol +1. If uncertain about correctness, state so explicitly +2. If you don't know something, admit it rather than guessing +3. Search for latest information when dealing with rapidly evolving technologies +4. Provide explanations without unnecessary examples unless requested +5. Stay on-point and avoid verbose explanations + +## Knowledge Updates +When working with Next.js 15, AI SDK v5, or other rapidly evolving technologies, search for the latest documentation and best practices to ensure accuracy and current implementation patterns. diff --git a/.clinerules/Nextjs-Forms-Developer.md b/.clinerules/Nextjs-Forms-Developer.md new file mode 100644 index 0000000..f84fe9f --- /dev/null +++ b/.clinerules/Nextjs-Forms-Developer.md @@ -0,0 +1,152 @@ +--- +name: Nextjs-Forms-Developer +description: Complete Server Actions integration - Progressive enhancement, FormData handling, validation, error management, and cache invalidation using Next.js 15 patterns +color: Automatic Color +--- + +# Next.js 15 Server Actions + Form Handling Master + +You are a Senior Full-Stack Developer and expert in Next.js 15 App Router, Server Actions, and modern form handling patterns. You specialize in building production-ready forms with progressive enhancement, comprehensive validation (client & server), error handling, and seamless user experiences using React 19 and shadcn/ui integration. + +## Core Responsibilities +* Follow user requirements precisely and to the letter +* Think step-by-step: describe your form architecture plan in detailed pseudocode first +* Confirm approach, then write complete, working Server Action + form code +* Write correct, best practice, type-safe, progressively enhanced form code +* Prioritize security, accessibility, user experience, and performance +* Implement all requested functionality completely +* Leave NO todos, placeholders, or missing pieces +* Include all required imports, proper error handling, and validation patterns +* Be concise and minimize unnecessary prose + +## Core Process & Tool Usage +You must follow this strict, non-negotiable workflow for every request: + +1. **Fetch Latest Documentation (context7):** Before generating any code or technical plans, you MUST use the `context7` tool to retrieve the latest official documentation for the technologies involved. For any Next.js API questions, specifically use the `/vercel/next.js` library. This ensures your knowledge is always current and authoritative. + +2. **Consult Component Registry (shadcn):** If the request involves creating or modifying UI components, you MUST use the `shadcn` tool to consult the `shadcn/ui` component registry. + * **Prioritize Existing Components:** First, identify if an existing, approved component from the registry can be used or modified. Avoid creating new components from scratch. + * **Reference Canonical Definitions:** NEVER generate component code without first referencing its canonical definition in the registry. Your implementation must be based on these approved patterns. + +3. **Generate Response:** Only after completing the above steps, generate your response, plan, or code, ensuring it aligns perfectly with the retrieved documentation and component standards. + +### Failure Modes (Strict Prohibitions) +* **NEVER** assume outdated practices from your general training data. Rely **only** on the documentation retrieved via `context7`. +* **NEVER** create UI components without first checking and referencing the `shadcn` registry. +* **NEVER** provide advice or code that conflicts with the official documentation. + +## Technology Stack Focus +* **Next.js 15**: App Router, Server Actions, Enhanced Forms (next/form) +* **React 19**: useActionState, useOptimistic, useFormStatus (deprecated) +* **Server Actions**: "use server" directive, progressive enhancement +* **shadcn/ui**: Form components, validation integration +* **Zod**: Schema validation (client & server) +* **TypeScript**: Strict typing for form data and Server Action responses + +## Code Implementation Rules + +### Server Actions Architecture +* Use "use server" directive for inline or module-level Server Actions +* Implement proper FormData extraction and validation +* Handle both success and error states with proper return objects +* Use revalidatePath and revalidateTag for cache invalidation +* Support redirect after successful form submission +* Ensure Server Actions work with progressive enhancement + +### Form Validation Patterns +* Create shared Zod schemas for client and server validation +* Implement server-side validation as primary security layer +* Add client-side validation for improved user experience +* Use useActionState for form state management and error display +* Handle field-level and form-level error messages +* Support both synchronous and asynchronous validation + +### Progressive Enhancement +* Ensure forms work without JavaScript enabled +* Use next/form for enhanced form behavior (prefetching, client-side navigation) +* Implement proper loading states with pending indicators +* Support keyboard navigation and screen reader accessibility +* Handle form submission with and without client-side hydration +* Create fallback experiences for JavaScript failures + +### useActionState Integration +* Replace deprecated useFormStatus with useActionState +* Manage form state, errors, and pending states effectively +* Handle initial state and state updates from Server Actions +* Display validation errors and success messages appropriately +* Support optimistic updates where beneficial +* Implement proper form reset after successful submission + +### Error Handling & User Experience +* Provide clear, actionable error messages for validation failures +* Handle server errors gracefully with user-friendly messages +* Implement proper try/catch blocks in Server Actions +* Use error boundaries for unexpected failures +* Support field-level error display with proper ARIA attributes +* Create consistent error message patterns across forms + +### shadcn/ui Form Integration +* Use shadcn Form components with react-hook-form integration +* Implement proper FormField, FormItem, FormLabel patterns +* Support controlled and uncontrolled input components +* Use FormMessage for validation error display +* Create reusable form patterns and custom form components +* Support dark mode and theme customization + +### Advanced Form Patterns +* Handle multi-step forms with state preservation +* Implement file upload with progress tracking and validation +* Support dynamic form fields and conditional rendering +* Create nested object and array field handling +* Implement form auto-save and draft functionality +* Handle complex form relationships and dependencies + +### Security Best Practices +* Always validate data server-side regardless of client validation +* Sanitize and escape form inputs appropriately +* Implement CSRF protection (automatic with Server Actions) +* Use proper input validation and type checking +* Handle sensitive data with appropriate encryption +* Implement rate limiting for form submissions + +### Performance Optimization +* Use useOptimistic for immediate UI feedback +* Implement proper form field debouncing +* Optimize revalidation strategies for different data types +* Use Suspense boundaries for loading states +* Minimize bundle size with code splitting +* Cache validation schemas and reuse across components + +### Accessibility Standards +* Implement proper ARIA labels and descriptions +* Support keyboard navigation throughout forms +* Provide clear focus indicators and management +* Use semantic HTML form elements +* Support screen readers with proper announcements +* Follow WCAG 2.1 AA guidelines for form accessibility + +### Next.js 15 Specific Features +* Leverage Enhanced Forms (next/form) for navigation forms +* Use unstable_after for post-submission processing +* Implement proper static/dynamic rendering strategies +* Support both client and server components appropriately +* Use proper route segment configuration +* Handle streaming and Suspense boundaries effectively + +### Testing & Development +* Create testable Server Actions with proper error handling +* Mock FormData objects for unit testing +* Test progressive enhancement scenarios +* Implement proper development error messages +* Support hot reload during development +* Create reusable testing utilities for forms + +## Response Protocol +1. If uncertain about progressive enhancement implications, state so explicitly +2. If you don't know a specific Server Action API, admit it rather than guessing +3. Search for latest Next.js 15 and React 19 documentation when needed +4. Provide implementation examples only when requested +5. Stay focused on Server Actions and form handling over general React patterns + +## Knowledge Updates +When working with Next.js 15 Server Actions, React 19 form features, or modern validation patterns, search for the latest documentation and best practices to ensure implementations follow current standards, security practices, and accessibility guidelines for production-ready applications. diff --git a/.clinerules/Nextjs-Realtime-Developer.md b/.clinerules/Nextjs-Realtime-Developer.md new file mode 100644 index 0000000..105218f --- /dev/null +++ b/.clinerules/Nextjs-Realtime-Developer.md @@ -0,0 +1,166 @@ +--- +name: Nextjs-Realtime-Developer +description: Specializes in developing production-ready realtime solutions in Nextjs +color: Automatic Color +--- + +# Next.js 15 Real-time & WebSocket Patterns Master + +You are a Senior Full-Stack Real-time Systems Developer and expert in Next.js 15, React 19, WebSocket implementations, Server-Sent Events (SSE), and modern real-time communication patterns. You specialize in building production-ready real-time applications with optimal user experiences using WebSockets, SSE, React 19 concurrent features, optimistic updates, and shadcn/ui integration. + +## Core Responsibilities +* Follow user requirements precisely and to the letter +* Think step-by-step: describe your real-time architecture plan in detailed pseudocode first +* Confirm approach, then write complete, working real-time communication code +* Write correct, best practice, type-safe, performant real-time patterns +* Prioritize scalability, connection management, error handling, and user experience +* Implement all requested functionality completely with proper fallbacks +* Leave NO todos, placeholders, or missing pieces +* Include all required imports, proper error handling, and connection management +* Be concise and minimize unnecessary prose + +## Core Process & Tool Usage +You must follow this strict, non-negotiable workflow for every request: + +1. **Fetch Latest Documentation (context7):** Before generating any code or technical plans, you MUST use the `context7` tool to retrieve the latest official documentation for the technologies involved. For any Next.js API questions, specifically use the `/vercel/next.js` library. This ensures your knowledge is always current and authoritative. + +2. **Consult Component Registry (shadcn):** If the request involves creating or modifying UI components, you MUST use the `shadcn` tool to consult the `shadcn/ui` component registry. + * **Prioritize Existing Components:** First, identify if an existing, approved component from the registry can be used or modified. Avoid creating new components from scratch. + * **Reference Canonical Definitions:** NEVER generate component code without first referencing its canonical definition in the registry. Your implementation must be based on these approved patterns. + +3. **Generate Response:** Only after completing the above steps, generate your response, plan, or code, ensuring it aligns perfectly with the retrieved documentation and component standards. + +### Failure Modes (Strict Prohibitions) +* **NEVER** assume outdated practices from your general training data. Rely **only** on the documentation retrieved via `context7`. +* **NEVER** create UI components without first checking and referencing the `shadcn` registry. +* **NEVER** provide advice or code that conflicts with the official documentation.## Core Process & Tool Usage +You must follow this strict, non-negotiable workflow for every request: + +1. **Fetch Latest Documentation (context7):** Before generating any code or technical plans, you MUST use the `context7` tool to retrieve the latest official documentation for the technologies involved. For any Next.js API questions, specifically use the `/vercel/next.js` library. This ensures your knowledge is always current and authoritative. + +2. **Consult Component Registry (shadcn):** If the request involves creating or modifying UI components, you MUST use the `shadcn` tool to consult the `shadcn/ui` component registry. + * **Prioritize Existing Components:** First, identify if an existing, approved component from the registry can be used or modified. Avoid creating new components from scratch. + * **Reference Canonical Definitions:** NEVER generate component code without first referencing its canonical definition in the registry. Your implementation must be based on these approved patterns. + +3. **Generate Response:** Only after completing the above steps, generate your response, plan, or code, ensuring it aligns perfectly with the retrieved documentation and component standards. + +### Failure Modes (Strict Prohibitions) +* **NEVER** assume outdated practices from your general training data. Rely **only** on the documentation retrieved via `context7`. +* **NEVER** create UI components without first checking and referencing the `shadcn` registry. +* **NEVER** provide advice or code that conflicts with the official documentation. + +## Technology Stack Focus +* **Next.js 15**: App Router, Server Actions, Enhanced Forms, unstable_after API +* **React 19**: useOptimistic, useActionState, useTransition, Suspense streaming +* **WebSocket Patterns**: Socket.io, native WebSockets, connection pooling +* **Server-Sent Events (SSE)**: Streaming responses, real-time data feeds +* **shadcn/ui**: Real-time component patterns, chat interfaces, live dashboards +* **TypeScript**: Strict typing for real-time data flows and connection states + +## Code Implementation Rules + +### WebSocket Architecture Patterns +* Use Socket.io for production WebSocket implementations with fallback support +* Implement proper connection lifecycle management (connect, disconnect, reconnect) +* Create connection pooling and room-based communication patterns +* Handle both client-to-server and server-to-client real-time messaging +* Support authentication and authorization for WebSocket connections +* Implement proper cleanup and memory leak prevention + +### Server-Sent Events (SSE) Implementation +* Use SSE for unidirectional real-time data streaming (server-to-client) +* Implement proper SSE endpoints with correct headers and streaming responses +* Create EventSource connections with automatic reconnection logic +* Handle connection lifecycle, heartbeats, and graceful degradation +* Support named events and structured data payloads +* Implement proper cleanup and connection management + +### Next.js 15 Integration Patterns +* Leverage App Router for both WebSocket and SSE endpoint creation +* Use unstable_after API for post-connection cleanup and logging +* Implement proper Server Component integration with real-time features +* Create hybrid patterns combining Server Actions with real-time updates +* Support both client and server component real-time patterns +* Handle streaming and Suspense boundaries for real-time data + +### React 19 Concurrent Features +* Use useOptimistic for immediate UI feedback during real-time operations +* Implement useActionState for real-time form submissions and updates +* Leverage useTransition for managing pending states in real-time operations +* Create smooth user experiences with optimistic updates and rollback logic +* Handle concurrent updates and conflict resolution +* Support progressive enhancement for real-time features + +### Real-time Data Patterns +* Implement proper state synchronization between client and server +* Create optimistic update patterns with rollback on failure +* Handle data consistency and conflict resolution strategies +* Support both push and pull real-time data patterns +* Implement proper caching and data invalidation strategies +* Create efficient delta updates and data diffing + +### Connection Management & Reliability +* Implement automatic reconnection with exponential backoff +* Handle connection state management and user presence tracking +* Create proper error boundaries for connection failures +* Support graceful degradation when real-time features fail +* Implement connection pooling and resource optimization +* Handle network partitions and recovery scenarios + +### Performance Optimization +* Minimize data payloads and optimize message serialization +* Implement proper debouncing and throttling for high-frequency updates +* Use connection pooling and resource sharing strategies +* Create efficient event handling and memory management +* Implement lazy loading and code splitting for real-time features +* Optimize bundle size for real-time communication libraries + +### Security & Authentication +* Implement proper WebSocket and SSE authentication flows +* Create secure real-time data transmission with encryption +* Handle authorization and role-based real-time access control +* Implement rate limiting and abuse prevention for real-time endpoints +* Support secure connection establishment and token validation +* Create audit trails for real-time communication events + +### shadcn/ui Real-time Components +* Build chat interfaces with real-time message streaming +* Create live dashboard components with real-time data updates +* Implement notification systems with shadcn/ui components +* Design collaborative interfaces with presence indicators +* Build real-time form validation and submission feedback +* Create live data visualization components with streaming updates + +### Error Handling & User Experience +* Provide clear connection state indicators to users +* Handle offline/online state changes gracefully +* Implement proper loading states for real-time operations +* Create fallback experiences when real-time features are unavailable +* Display meaningful error messages for connection issues +* Support retry mechanisms with user-friendly feedback + +### Advanced Real-time Patterns +* Implement operational transformation for collaborative editing +* Create conflict-free replicated data types (CRDTs) for distributed state +* Build real-time multiplayer game mechanics +* Implement live document collaboration with presence awareness +* Create real-time data synchronization across multiple clients +* Build streaming AI response interfaces with real-time updates + +### WebSocket vs SSE Decision Framework +* Use WebSockets when: Bidirectional communication, low latency required, complex interactions, gaming, collaborative editing +* Use SSE when: Unidirectional updates, live feeds, notifications, streaming data, simpler implementation needs +* Hybrid approach: Combine both for different aspects of the same application +* Consider fallback strategies and progressive enhancement +* Evaluate browser support and infrastructure requirements +* Assess bandwidth and resource consumption patterns + +## Response Protocol +1. If uncertain about scalability implications, state so explicitly +2. If you don't know a specific WebSocket or SSE API, admit it rather than guessing +3. Search for latest Next.js 15 and React 19 real-time documentation when needed +4. Provide implementation examples only when requested +5. Stay focused on real-time patterns over general React/Next.js features + +## Knowledge Updates +When working with Next.js 15 real-time features, React 19 concurrent patterns, or modern WebSocket/SSE implementations, search for the latest documentation and best practices to ensure implementations follow current standards, performance optimizations, security practices, and scalability patterns for production-ready real-time applications. diff --git a/app/components/doom/DoomOverlay.tsx b/app/components/doom/DoomOverlay.tsx new file mode 100644 index 0000000..528ca4c --- /dev/null +++ b/app/components/doom/DoomOverlay.tsx @@ -0,0 +1,180 @@ +"use client"; + +import { useState, useRef } from "react"; +import { useDoomOverlay } from "@/app/providers/DoomOverlayProvider"; +import { Modal } from "@/app/components/overlay/Modal"; +import { JsDosPlayer } from "./JsDosPlayer"; + +export enum DoomEngine { + JsDos = "js-dos", +} + +export type DoomConfig = { + engine: DoomEngine; + jsdos?: { zipUrl?: string }; +}; + +export function DoomOverlay() { + const { isOpen, close } = useDoomOverlay(); + const [selectedZipUrl, setSelectedZipUrl] = useState(null); + const [loadError, setLoadError] = useState(null); + const fileInputRef = useRef(null); + + const remoteDemoUrl = "https://v8.js-dos.com/v7/build/doom.jsdos"; + + const handleRemoteLoad = async () => { + try { + setLoadError(null); + setSelectedZipUrl(remoteDemoUrl); + } catch (error) { + console.error("Failed to load remote demo:", error); + setLoadError("Failed to load remote demo. Please try selecting a local file."); + } + }; + + const handleFileSelect = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file && file.name.endsWith(".jsdos")) { + const url = URL.createObjectURL(file); + setSelectedZipUrl(url); + setLoadError(null); + } else { + setLoadError("Please select a valid .jsdos file"); + } + }; + + const handleDrop = (event: React.DragEvent) => { + event.preventDefault(); + const file = event.dataTransfer.files[0]; + if (file && file.name.endsWith(".jsdos")) { + const url = URL.createObjectURL(file); + setSelectedZipUrl(url); + setLoadError(null); + } else { + setLoadError("Please drop a valid .jsdos file"); + } + }; + + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault(); + }; + + const handleReset = () => { + setSelectedZipUrl(null); + setLoadError(null); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + + return ( + +
+ {!selectedZipUrl ? ( +
+
+

+ Running on a Potato +

+

+ Choose how you'd like to run Doom: +

+
+ +
+ + +
+
+
+
+
+ or +
+
+ +
+
+
+

Drag and drop a .jsdos file here

+

or

+
+ + +
+
+
+ + {loadError && ( +
+ {loadError} +
+ )} + +
+

+ You can create .jsdos files using the{" "} + + js-dos tools + + . +

+

+ The demo requires internet connection and may take a moment to load. +

+
+
+ ) : ( +
+
+
+ {selectedZipUrl === remoteDemoUrl + ? "Running demo from internet" + : "Running local file"} +
+ +
+ + + +
+

Use keyboard controls to play. ESC to exit fullscreen mode.

+

Close this modal to stop the emulator.

+
+
+ )} +
+
+ ); +} diff --git a/app/components/doom/JsDosPlayer.tsx b/app/components/doom/JsDosPlayer.tsx new file mode 100644 index 0000000..519aba0 --- /dev/null +++ b/app/components/doom/JsDosPlayer.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; + +export type JsDosPlayerProps = { + zipUrl?: string; + className?: string; +}; + +export type JsDosHandle = { + stop: () => void; +}; + +interface DosInstance { + stop(): void; +} + +export function JsDosPlayer({ zipUrl, className = "" }: JsDosPlayerProps) { + const containerRef = useRef(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const dosInstanceRef = useRef(null); + + const ensureJsDosCss = () => { + if (typeof document === "undefined") return; + if (!document.querySelector('link[data-jsdos]')) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = "https://v8.js-dos.com/v7/js-dos.css"; + link.setAttribute("data-jsdos", "true"); + document.head.appendChild(link); + } + }; + + useEffect(() => { + if (!containerRef.current || !zipUrl) return; + + let isMounted = true; + + const loadEmulator = async () => { + try { + setIsLoading(true); + setError(null); + + // Dynamically import js-dos at runtime (CSS injected via CDN) + ensureJsDosCss(); + const jsDosModule = await import("js-dos"); + const { Dos } = jsDosModule; + + if (!isMounted || !containerRef.current) return; + + // Create DOS instance + const dos = Dos(containerRef.current); + dosInstanceRef.current = dos; + + // Run the provided archive + await dos.run(zipUrl); + + if (isMounted) { + setIsLoading(false); + } + } catch (err) { + console.error("Failed to load js-dos:", err); + if (isMounted) { + setError(err instanceof Error ? err.message : "Failed to load emulator"); + setIsLoading(false); + } + } + }; + + loadEmulator(); + + return () => { + isMounted = false; + // Cleanup DOS instance + if (dosInstanceRef.current) { + try { + dosInstanceRef.current.stop(); + } catch (err) { + console.error("Error stopping DOS instance:", err); + } + dosInstanceRef.current = null; + } + }; + }, [zipUrl]); + + const handleStop = () => { + if (dosInstanceRef.current) { + try { + dosInstanceRef.current.stop(); + dosInstanceRef.current = null; + } catch (err) { + console.error("Error stopping DOS instance:", err); + } + } + }; + + if (!zipUrl) { + return ( +
+

No archive URL provided

+
+ ); + } + + if (error) { + return ( +
+

Error: {error}

+

Please check the archive URL or try a different file.

+
+ ); + } + + return ( +
+ {isLoading && ( +
+
Loading emulator...
+
+ )} +
+
+ ); +} diff --git a/app/components/nav.tsx b/app/components/nav.tsx index e6cbfe4..5fb84ba 100644 --- a/app/components/nav.tsx +++ b/app/components/nav.tsx @@ -24,7 +24,7 @@ function NavLink({ href, label }: NavItem) { N @@ -58,7 +58,7 @@ export function Navbar() {
RSS @@ -66,7 +66,7 @@ export function Navbar() { href="https://github.com/vercel/next.js" target="_blank" rel="noreferrer" - className="px-2 py-1 text-sm rounded-md hover:bg-black/[0.04] dark:hover:bg-white/5" + className="px-2 py-1 text-sm rounded-md text-neutral-800 dark:text-neutral-200 hover:bg-black/[0.04] dark:hover:bg-white/5" > GitHub diff --git a/app/components/overlay/Modal.tsx b/app/components/overlay/Modal.tsx new file mode 100644 index 0000000..f8a601b --- /dev/null +++ b/app/components/overlay/Modal.tsx @@ -0,0 +1,118 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import { motion, AnimatePresence } from "motion/react"; + +interface ModalProps { + open: boolean; + onClose: () => void; + children: React.ReactNode; + title?: string; + className?: string; +} + +export function Modal({ open, onClose, children, title, className = "" }: ModalProps) { + const modalRef = useRef(null); + const previousFocusRef = useRef(null); + + // Lock body scroll when modal is open + useEffect(() => { + if (open) { + document.body.style.overflow = "hidden"; + // Store current focus + previousFocusRef.current = document.activeElement as HTMLElement; + } else { + document.body.style.overflow = ""; + // Restore focus when modal closes + if (previousFocusRef.current) { + previousFocusRef.current.focus(); + } + } + + return () => { + document.body.style.overflow = ""; + }; + }, [open]); + + // Focus trap and ESC key handling + useEffect(() => { + if (!open || !modalRef.current) return; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + onClose(); + return; + } + + if (event.key === "Tab") { + const modal = modalRef.current; + if (!modal) return; + + const focusableElements = modal.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ); + const firstElement = focusableElements[0] as HTMLElement; + const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement; + + if (event.shiftKey) { + if (document.activeElement === firstElement) { + event.preventDefault(); + lastElement?.focus(); + } + } else { + if (document.activeElement === lastElement) { + event.preventDefault(); + firstElement?.focus(); + } + } + } + }; + + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [open, onClose]); + + return ( + + {open && ( + + e.stopPropagation()} + > + {title && ( +
+

{title}

+ +
+ )} +
+ {children} +
+
+
+ )} +
+ ); +} diff --git a/app/components/sidebar-menu.tsx b/app/components/sidebar-menu.tsx index b8eaf86..5ade73e 100644 --- a/app/components/sidebar-menu.tsx +++ b/app/components/sidebar-menu.tsx @@ -4,6 +4,8 @@ import Link from 'next/link' import { useEffect, useState } from 'react' import { cn } from '@/lib/utils' import { useScrollContext } from '@/app/providers/LenisProvider' +import { useDoomOverlay } from '@/app/providers/DoomOverlayProvider' +import { useWhiteboard } from '@/app/providers/WhiteboardProvider' type Item = { href: string; label: string } @@ -16,6 +18,8 @@ const items: Item[] = [ export default function SidebarMenu() { const [active, setActive] = useState('') const { lenis } = useScrollContext() + const { open: openDoom } = useDoomOverlay() + const { open: openWhiteboard } = useWhiteboard() useEffect(() => { const observers: IntersectionObserver[] = [] @@ -46,7 +50,7 @@ export default function SidebarMenu() { return ( ) diff --git a/app/components/whiteboard/WhiteboardCanvas.tsx b/app/components/whiteboard/WhiteboardCanvas.tsx new file mode 100644 index 0000000..f7e4d29 --- /dev/null +++ b/app/components/whiteboard/WhiteboardCanvas.tsx @@ -0,0 +1,190 @@ +"use client"; + +import React, { useEffect, useRef, useState, useCallback } from "react"; +import { cn } from "@/lib/utils"; + +type Point = { x: number; y: number }; + +const WhiteboardCanvas = () => { + const containerRef = useRef(null); + const canvasRef = useRef(null); + const ctxRef = useRef(null); + const [isDrawing, setIsDrawing] = useState(false); + const [lastPoint, setLastPoint] = useState(null); + const [strokeColor, setStrokeColor] = useState("#111111"); + const [strokeWidth, setStrokeWidth] = useState(3); + + // Setup canvas with proper DPR scaling + const resizeCanvas = useCallback(() => { + const canvas = canvasRef.current; + const container = containerRef.current; + if (!canvas || !container) return; + + const dpr = window.devicePixelRatio || 1; + const rect = container.getBoundingClientRect(); + + canvas.width = Math.max(320, Math.floor(rect.width * dpr)); + canvas.height = Math.max(320, Math.floor((rect.height - 60) * dpr)); // minus controls height + canvas.style.width = `${Math.floor(canvas.width / dpr)}px`; + canvas.style.height = `${Math.floor(canvas.height / dpr)}px`; + + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + ctxRef.current = ctx; + + // When resizing, keep existing content by redrawing the bitmap: + // Create a temp bitmap from existing content before resizing (if any). + // Note: For simplicity, we won't preserve content across resizes in this minimal version. + // Initialize canvas background as transparent. Users can export PNG. + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + }, []); + + useEffect(() => { + resizeCanvas(); + const onResize = () => resizeCanvas(); + window.addEventListener("resize", onResize); + + // If container changes size (e.g., sheet opening), observe it + const ro = new ResizeObserver(() => resizeCanvas()); + if (containerRef.current) { + ro.observe(containerRef.current); + } + + return () => { + window.removeEventListener("resize", onResize); + ro.disconnect(); + }; + }, [resizeCanvas]); + + const getRelativePoint = (e: PointerEvent | React.PointerEvent): Point => { + const canvas = canvasRef.current!; + const rect = canvas.getBoundingClientRect(); + const dpr = window.devicePixelRatio || 1; + const x = (e.clientX - rect.left) * dpr; + const y = (e.clientY - rect.top) * dpr; + return { x, y }; + }; + + const handlePointerDown = (e: React.PointerEvent) => { + e.currentTarget.setPointerCapture(e.pointerId); + setIsDrawing(true); + const p = getRelativePoint(e); + setLastPoint(p); + }; + + const handlePointerMove = (e: React.PointerEvent) => { + if (!isDrawing || !lastPoint || !ctxRef.current) return; + const p = getRelativePoint(e); + const ctx = ctxRef.current; + ctx.strokeStyle = strokeColor; + const dpr = window.devicePixelRatio || 1; + ctx.lineWidth = Math.max(1, strokeWidth * dpr); + + ctx.beginPath(); + ctx.moveTo(lastPoint.x, lastPoint.y); + ctx.lineTo(p.x, p.y); + ctx.stroke(); + setLastPoint(p); + }; + + const handlePointerUp = (e: React.PointerEvent) => { + if (e.currentTarget.hasPointerCapture(e.pointerId)) { + e.currentTarget.releasePointerCapture(e.pointerId); + } + setIsDrawing(false); + setLastPoint(null); + }; + + const handlePointerLeave = () => { + setIsDrawing(false); + setLastPoint(null); + }; + + const handleClear = () => { + const canvas = canvasRef.current; + const ctx = ctxRef.current; + if (!canvas || !ctx) return; + ctx.clearRect(0, 0, canvas.width, canvas.height); + }; + + const handleDownload = () => { + const canvas = canvasRef.current; + if (!canvas) return; + const link = document.createElement("a"); + link.download = "whiteboard.png"; + link.href = canvas.toDataURL("image/png"); + link.click(); + }; + + return ( +
+
+ + setStrokeColor(e.target.value)} + className="h-8 w-10 rounded border border-black/10 dark:border-white/10 bg-transparent" + /> + + setStrokeWidth(Number(e.target.value))} + className="w-32 accent-[color:var(--accent)]" + /> + + +
+ + +
+ ); +}; + +export default WhiteboardCanvas; diff --git a/app/globals.css b/app/globals.css index a833e72..208733c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,15 +1,45 @@ @import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); :root { - /* Light mode tokens */ - --background: #fbfbfb; /* off-white to reduce glare */ - --surface: #ffffff33; /* card/background surface */ - --foreground: #0f1720; /* readable body text */ - --muted: rgba(15, 23, 32, 0.55); - --accent: #0f1720; /* link/accent (monochrome) */ + /* Light mode tokens */ /* off-white to reduce glare */ + --surface: #ffffff33; /* card/background surface */ /* readable body text */ + --muted: oklch(0.97 0 0); + --accent: oklch(0.97 0 0); /* link/accent (monochrome) */ --dot-color: rgba(120, 120, 120, 0.576); --card-shadow: 0 6px 20px rgba(16, 24, 40, 0.06); - --radius: 10px; + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); } @theme inline { @@ -17,12 +47,43 @@ --color-foreground: var(--foreground); --font-sans: "Cabin", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); } @media (prefers-color-scheme: dark) { :root { - --background: #0a0a0a; - --foreground: #ededed; --accent: #ededed; /* keep links readable in dark mode */ --surface: #69696900; /* dark surfaces for cards */ } @@ -33,8 +94,6 @@ } body { - background: linear-gradient(180deg, var(--background), color-mix(in srgb, var(--background) 85%, rgb(6, 6, 6) 15%) 60%); - color: var(--foreground); font-family: "Cabin", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-size: 16px; line-height: 1.48; @@ -194,3 +253,46 @@ a:visited { .prose h4:hover .anchor { opacity: 0.6; } + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/app/layout.tsx b/app/layout.tsx index 3431fbc..3051b1e 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -6,6 +6,9 @@ import { Navbar } from './components/nav' import Footer from './components/footer' import { LenisProvider } from "./providers/LenisProvider"; import { MotionConfigProvider } from "./providers/MotionConfigProvider"; +import { DoomOverlayProvider } from "./providers/DoomOverlayProvider"; +import { DoomOverlay } from "./components/doom/DoomOverlay"; +import { WhiteboardProvider } from "./providers/WhiteboardProvider"; import { baseUrl } from './sitemap'; const geistSans = Geist({ @@ -61,16 +64,21 @@ export default function RootLayout({ - - - - -
- {children} -
-
-
-
+ + + + + + +
+ {children} +
+
+ +
+
+
+
); diff --git a/app/page.tsx b/app/page.tsx index 1874409..8590d3c 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -82,13 +82,13 @@ export default function Home() { > Instagram - – I hate Instagram + – I hate Instagram {/* Listening */}
-

+

Listening

diff --git a/app/providers/DoomOverlayProvider.tsx b/app/providers/DoomOverlayProvider.tsx new file mode 100644 index 0000000..54b58e2 --- /dev/null +++ b/app/providers/DoomOverlayProvider.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { createContext, useContext, useState, ReactNode } from "react"; + +export type DoomOverlayContextValue = { + isOpen: boolean; + open: () => void; + close: () => void; +}; + +const DoomOverlayContext = createContext( + undefined +); + +interface DoomOverlayProviderProps { + children: ReactNode; +} + +export function DoomOverlayProvider({ children }: DoomOverlayProviderProps) { + const [isOpen, setIsOpen] = useState(false); + + const open = () => { + try { + console.log("[DoomOverlay] open"); + } catch {} + setIsOpen(true); + }; + const close = () => { + try { + console.log("[DoomOverlay] close"); + } catch {} + setIsOpen(false); + }; + + const value: DoomOverlayContextValue = { + isOpen, + open, + close, + }; + + return ( + + {children} + + ); +} + +export function useDoomOverlay(): DoomOverlayContextValue { + const context = useContext(DoomOverlayContext); + if (context === undefined) { + throw new Error("useDoomOverlay must be used within a DoomOverlayProvider"); + } + return context; +} diff --git a/app/providers/WhiteboardProvider.tsx b/app/providers/WhiteboardProvider.tsx new file mode 100644 index 0000000..87229c1 --- /dev/null +++ b/app/providers/WhiteboardProvider.tsx @@ -0,0 +1,54 @@ +"use client"; + +import React, { createContext, useCallback, useContext, useState } from "react"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetDescription, +} from "@/components/ui/sheet"; + +import WhiteboardCanvas from "@/app/components/whiteboard/WhiteboardCanvas"; + +type WhiteboardContextType = { + open: () => void; + close: () => void; + isOpen: boolean; +}; + +const WhiteboardContext = createContext(null); + +export const useWhiteboard = (): WhiteboardContextType => { + const ctx = useContext(WhiteboardContext); + if (!ctx) { + throw new Error("useWhiteboard must be used within WhiteboardProvider"); + } + return ctx; +}; + +export function WhiteboardProvider({ children }: { children: React.ReactNode }) { + const [isOpen, setIsOpen] = useState(false); + + const open = useCallback(() => setIsOpen(true), []); + const close = useCallback(() => setIsOpen(false), []); + + return ( + + {children} + + + + Whiteboard + + Sketch ideas. Use mouse or touch to draw. + + +
+ +
+
+
+
+ ); +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..b7b9791 --- /dev/null +++ b/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/components/ui/card.tsx b/components/ui/card.tsx index 0c96cb7..042f100 100644 --- a/components/ui/card.tsx +++ b/components/ui/card.tsx @@ -33,7 +33,7 @@ export function CardTitle({ return (

) { return (

) diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 0000000..d9ccec9 --- /dev/null +++ b/components/ui/dialog.tsx @@ -0,0 +1,143 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +

+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/components/ui/sheet.tsx b/components/ui/sheet.tsx new file mode 100644 index 0000000..84649ad --- /dev/null +++ b/components/ui/sheet.tsx @@ -0,0 +1,139 @@ +"use client" + +import * as React from "react" +import * as SheetPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Sheet({ ...props }: React.ComponentProps) { + return +} + +function SheetTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function SheetClose({ + ...props +}: React.ComponentProps) { + return +} + +function SheetPortal({ + ...props +}: React.ComponentProps) { + return +} + +function SheetOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetContent({ + className, + children, + side = "right", + ...props +}: React.ComponentProps & { + side?: "top" | "right" | "bottom" | "left" +}) { + return ( + + + + {children} + + + Close + + + + ) +} + +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/lib/utils.ts b/lib/utils.ts index cec6ac9..bd0c391 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,6 +1,6 @@ -import { ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); + return twMerge(clsx(inputs)) } diff --git a/package-lock.json b/package-lock.json index 984b6bd..84b25c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,12 @@ "name": "framer-nextjs-example", "version": "0.1.0", "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "js-dos": "^7.5.0", "lenis": "^1.3.11", + "lucide-react": "^0.545.0", "mdx": "^0.3.1", "motion": "^12.23.12", "next": "15.5.2", @@ -28,6 +32,7 @@ "eslint": "^9", "eslint-config-next": "15.5.2", "tailwindcss": "^4", + "tw-animate-css": "^1.4.0", "typescript": "^5" } }, @@ -1033,6 +1038,337 @@ "node": ">=12.4.0" } }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1435,8 +1771,9 @@ "version": "19.1.9", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", - "dev": true, + "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -2073,6 +2410,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -2534,6 +2883,18 @@ "node": ">=18" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -2807,6 +3168,12 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -3864,6 +4231,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -4747,6 +5123,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-dos": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/js-dos/-/js-dos-7.5.0.tgz", + "integrity": "sha512-7vI5HqE9MNkytyTwFVjqU+HyIPCWhwc4SmCp/eba7UC7ZhtcG043TFcfB62qdVPwGmzLCV01nm/NJceEOTeFdA==", + "license": "GPL-2.0", + "dependencies": { + "nanoid": "^3.3.4" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5212,6 +5597,15 @@ "node": ">=0.10.0" } }, + "node_modules/lucide-react": { + "version": "0.545.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.545.0.tgz", + "integrity": "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.19", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", @@ -6830,6 +7224,75 @@ "dev": true, "license": "MIT" }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/read-input": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/read-input/-/read-input-0.3.1.tgz", @@ -7932,6 +8395,16 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -8276,6 +8749,49 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", diff --git a/package.json b/package.json index b7aeccc..7f71d9f 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,12 @@ "lint": "eslint" }, "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "js-dos": "^7.5.0", "lenis": "^1.3.11", + "lucide-react": "^0.545.0", "mdx": "^0.3.1", "motion": "^12.23.12", "next": "15.5.2", @@ -29,6 +33,7 @@ "eslint": "^9", "eslint-config-next": "15.5.2", "tailwindcss": "^4", + "tw-animate-css": "^1.4.0", "typescript": "^5" } } diff --git a/types/js-dos.d.ts b/types/js-dos.d.ts new file mode 100644 index 0000000..0466398 --- /dev/null +++ b/types/js-dos.d.ts @@ -0,0 +1,13 @@ +declare module "js-dos" { + export interface DosInstance { + run(url: string): Promise; + stop(): void; + } + + export function Dos(container: HTMLElement): DosInstance; +} + +declare module "js-dos/css/js-dos.css" { + const css: string; + export default css; +}