added whiteboard
This commit is contained in:
parent
03dd3571a3
commit
f982a558d3
82
.clinerules/Nextjs-Developer.md
Normal file
82
.clinerules/Nextjs-Developer.md
Normal file
@ -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.
|
||||
152
.clinerules/Nextjs-Forms-Developer.md
Normal file
152
.clinerules/Nextjs-Forms-Developer.md
Normal file
@ -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.
|
||||
166
.clinerules/Nextjs-Realtime-Developer.md
Normal file
166
.clinerules/Nextjs-Realtime-Developer.md
Normal file
@ -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.
|
||||
180
app/components/doom/DoomOverlay.tsx
Normal file
180
app/components/doom/DoomOverlay.tsx
Normal file
@ -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<string | null>(null);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
|
||||
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<HTMLDivElement>) => {
|
||||
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<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSelectedZipUrl(null);
|
||||
setLoadError(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={isOpen} onClose={close} title="Doom Emulator">
|
||||
<div className="p-6">
|
||||
{!selectedZipUrl ? (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
<h3 className="text-xl font-semibold text-neutral-100 mb-2">
|
||||
Running on a Potato
|
||||
</h3>
|
||||
<p className="text-neutral-400 mb-6">
|
||||
Choose how you'd like to run Doom:
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
onClick={handleRemoteLoad}
|
||||
className="w-full px-4 py-3 rounded-lg glass text-neutral-200 font-medium transition-colors hover:opacity-95 focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)]"
|
||||
>
|
||||
Load Demo from Internet
|
||||
</button>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-white/20"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-4 bg-transparent text-neutral-400">or</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
className="border-2 border-dashed border-white/20 rounded-lg p-8 text-center hover:border-white/30 transition-colors"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="text-neutral-400">
|
||||
<p className="mb-2">Drag and drop a .jsdos file here</p>
|
||||
<p className="text-sm">or</p>
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".jsdos"
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
id="file-input"
|
||||
/>
|
||||
<label
|
||||
htmlFor="file-input"
|
||||
className="inline-block px-4 py-2 rounded-lg glass text-neutral-200 font-medium cursor-pointer transition-colors hover:opacity-95"
|
||||
>
|
||||
Browse Files
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loadError && (
|
||||
<div className="p-4 glass text-red-400 rounded-lg text-sm">
|
||||
{loadError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-neutral-500 space-y-1">
|
||||
<p>
|
||||
You can create .jsdos files using the{" "}
|
||||
<a
|
||||
href="https://js-dos.com/tools"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[color:var(--accent)] hover:underline"
|
||||
>
|
||||
js-dos tools
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
The demo requires internet connection and may take a moment to load.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-sm text-neutral-400">
|
||||
{selectedZipUrl === remoteDemoUrl
|
||||
? "Running demo from internet"
|
||||
: "Running local file"}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="px-3 py-1 text-sm rounded-md border border-white/10 text-neutral-300 hover:bg-white/5 transition-colors"
|
||||
>
|
||||
Change Source
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<JsDosPlayer zipUrl={selectedZipUrl} className="w-full" />
|
||||
|
||||
<div className="text-xs text-neutral-500">
|
||||
<p>Use keyboard controls to play. ESC to exit fullscreen mode.</p>
|
||||
<p>Close this modal to stop the emulator.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
129
app/components/doom/JsDosPlayer.tsx
Normal file
129
app/components/doom/JsDosPlayer.tsx
Normal file
@ -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<HTMLDivElement>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const dosInstanceRef = useRef<DosInstance | null>(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 (
|
||||
<div className={`flex items-center justify-center p-8 text-neutral-400 ${className}`}>
|
||||
<p>No archive URL provided</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={`flex flex-col items-center justify-center p-8 text-neutral-400 ${className}`}>
|
||||
<p className="text-red-400 mb-4">Error: {error}</p>
|
||||
<p className="text-sm">Please check the archive URL or try a different file.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`}>
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/50 z-10">
|
||||
<div className="text-neutral-200">Loading emulator...</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="w-full h-full min-h-[400px] bg-black rounded"
|
||||
style={{ minHeight: "400px" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -24,7 +24,7 @@ function NavLink({ href, label }: NavItem) {
|
||||
<Link
|
||||
href={href}
|
||||
className={cn(
|
||||
'px-2 py-1 text-sm rounded-md transition-colors',
|
||||
'px-2 py-1 text-sm rounded-md transition-colors text-neutral-800 dark:text-neutral-200',
|
||||
'hover:bg-black/[0.04] dark:hover:bg-white/5',
|
||||
isActive && 'text-foreground underline underline-offset-4'
|
||||
)}
|
||||
@ -42,7 +42,7 @@ export function Navbar() {
|
||||
<div className="font-semibold tracking-tight">
|
||||
<Link
|
||||
href="/"
|
||||
className="px-2 py-1 rounded-md hover:bg-black/[0.04] dark:hover:bg-white/5"
|
||||
className="px-2 py-1 rounded-md text-neutral-800 dark:text-neutral-200 hover:bg-black/[0.04] dark:hover:bg-white/5"
|
||||
aria-label="Home"
|
||||
>
|
||||
N
|
||||
@ -58,7 +58,7 @@ export function Navbar() {
|
||||
<div className="flex items-center gap-2" aria-label="Utilities">
|
||||
<Link
|
||||
href="/rss"
|
||||
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"
|
||||
>
|
||||
RSS
|
||||
</Link>
|
||||
@ -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
|
||||
</a>
|
||||
|
||||
118
app/components/overlay/Modal.tsx
Normal file
118
app/components/overlay/Modal.tsx
Normal file
@ -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<HTMLDivElement>(null);
|
||||
const previousFocusRef = useRef<HTMLElement | null>(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 (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-[9999] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
>
|
||||
<motion.div
|
||||
ref={modalRef}
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
className={`relative w-full max-w-4xl max-h-[90vh] rounded-lg shadow-2xl glass-strong glass-refract overflow-hidden ${className}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{title && (
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10">
|
||||
<h2 className="text-lg font-semibold text-neutral-100">{title}</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 rounded-full hover:bg-white/5 transition-colors"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="overflow-auto max-h-[calc(90vh-4rem)]">
|
||||
{children}
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@ -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<string>('')
|
||||
const { lenis } = useScrollContext()
|
||||
const { open: openDoom } = useDoomOverlay()
|
||||
const { open: openWhiteboard } = useWhiteboard()
|
||||
|
||||
useEffect(() => {
|
||||
const observers: IntersectionObserver[] = []
|
||||
@ -46,7 +50,7 @@ export default function SidebarMenu() {
|
||||
|
||||
return (
|
||||
<aside className="sticky top-24 h-fit">
|
||||
<p className="mb-2 text-xs font-medium uppercase tracking-wider text-neutral-600 dark:text-neutral-500">
|
||||
<p className="mb-2 text-xs font-medium uppercase tracking-wider text-neutral-800 dark:text-neutral-300">
|
||||
Menu
|
||||
</p>
|
||||
<nav aria-label="Section menu" className="flex flex-col gap-1">
|
||||
@ -66,7 +70,7 @@ export default function SidebarMenu() {
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'px-3 py-2 text-sm rounded-md transition-colors',
|
||||
'px-3 py-2 text-sm rounded-md transition-colors text-neutral-800 dark:text-neutral-200',
|
||||
'hover:bg-black/[0.04] dark:hover:bg-white/5',
|
||||
isActive && 'bg-black/[0.06] dark:bg-white/10'
|
||||
)}
|
||||
@ -76,6 +80,35 @@ export default function SidebarMenu() {
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
|
||||
<button
|
||||
onClick={(e) => {
|
||||
// Prevent the opening click from bubbling to the modal backdrop and instantly closing it
|
||||
e.stopPropagation();
|
||||
openDoom();
|
||||
}}
|
||||
className={cn(
|
||||
'px-3 py-2 text-sm rounded-md transition-colors text-left w-full',
|
||||
'hover:bg-black/[0.04] dark:hover:bg-white/5',
|
||||
'text-neutral-900 dark:text-neutral-100'
|
||||
)}
|
||||
>
|
||||
running on a potato
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openWhiteboard();
|
||||
}}
|
||||
className={cn(
|
||||
'px-3 py-2 text-sm rounded-md transition-colors text-left w-full',
|
||||
'hover:bg-black/[0.04] dark:hover:bg-white/5',
|
||||
'text-neutral-900 dark:text-neutral-100'
|
||||
)}
|
||||
aria-label="Open whiteboard"
|
||||
>
|
||||
Whiteboard
|
||||
</button>
|
||||
</nav>
|
||||
</aside>
|
||||
)
|
||||
|
||||
190
app/components/whiteboard/WhiteboardCanvas.tsx
Normal file
190
app/components/whiteboard/WhiteboardCanvas.tsx
Normal file
@ -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<HTMLDivElement | null>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const ctxRef = useRef<CanvasRenderingContext2D | null>(null);
|
||||
const [isDrawing, setIsDrawing] = useState(false);
|
||||
const [lastPoint, setLastPoint] = useState<Point | null>(null);
|
||||
const [strokeColor, setStrokeColor] = useState<string>("#111111");
|
||||
const [strokeWidth, setStrokeWidth] = useState<number>(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<HTMLCanvasElement>): 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<HTMLCanvasElement>) => {
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
setIsDrawing(true);
|
||||
const p = getRelativePoint(e);
|
||||
setLastPoint(p);
|
||||
};
|
||||
|
||||
const handlePointerMove = (e: React.PointerEvent<HTMLCanvasElement>) => {
|
||||
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<HTMLCanvasElement>) => {
|
||||
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 (
|
||||
<div ref={containerRef} className="w-full h-[70vh] min-h-[360px]">
|
||||
<div className="flex items-center gap-2 pb-3">
|
||||
<label className="text-sm" htmlFor="stroke-color">
|
||||
Color
|
||||
</label>
|
||||
<input
|
||||
id="stroke-color"
|
||||
aria-label="Stroke color"
|
||||
type="color"
|
||||
value={strokeColor}
|
||||
onChange={(e) => setStrokeColor(e.target.value)}
|
||||
className="h-8 w-10 rounded border border-black/10 dark:border-white/10 bg-transparent"
|
||||
/>
|
||||
<label className="text-sm pl-2" htmlFor="stroke-width">
|
||||
Size
|
||||
</label>
|
||||
<input
|
||||
id="stroke-width"
|
||||
aria-label="Stroke width"
|
||||
type="range"
|
||||
min={1}
|
||||
max={20}
|
||||
value={strokeWidth}
|
||||
onChange={(e) => setStrokeWidth(Number(e.target.value))}
|
||||
className="w-32 accent-[color:var(--accent)]"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClear}
|
||||
className={cn(
|
||||
"px-3 py-1.5 text-sm rounded-md transition-colors",
|
||||
"bg-black/80 text-white hover:bg-black",
|
||||
"dark:bg-white/10 dark:text-white dark:hover:bg-white/20"
|
||||
)}
|
||||
aria-label="Clear canvas"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDownload}
|
||||
className={cn(
|
||||
"px-3 py-1.5 text-sm rounded-md transition-colors",
|
||||
"bg-[color:var(--accent)] text-black hover:brightness-110"
|
||||
)}
|
||||
aria-label="Download image"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className={cn(
|
||||
"w-full h-[calc(70vh-3rem)] min-h-[300px] rounded-md",
|
||||
"bg-white dark:bg-neutral-900 border border-black/10 dark:border-white/10 touch-none"
|
||||
)}
|
||||
role="img"
|
||||
aria-label="Whiteboard drawing area"
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
onPointerLeave={handlePointerLeave}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WhiteboardCanvas;
|
||||
124
app/globals.css
124
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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({
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<MotionConfigProvider>
|
||||
<LenisProvider>
|
||||
<Navbar />
|
||||
<DotBackground />
|
||||
<main className="flex-auto min-w-0 mt-6 flex flex-col px-2 md:px-0">
|
||||
{children}
|
||||
<Footer />
|
||||
</main>
|
||||
</LenisProvider>
|
||||
</MotionConfigProvider>
|
||||
<DoomOverlayProvider>
|
||||
<MotionConfigProvider>
|
||||
<WhiteboardProvider>
|
||||
<LenisProvider>
|
||||
<Navbar />
|
||||
<DotBackground />
|
||||
<main className="flex-auto min-w-0 mt-6 flex flex-col px-2 md:px-0">
|
||||
{children}
|
||||
<Footer />
|
||||
</main>
|
||||
<DoomOverlay />
|
||||
</LenisProvider>
|
||||
</WhiteboardProvider>
|
||||
</MotionConfigProvider>
|
||||
</DoomOverlayProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@ -82,13 +82,13 @@ export default function Home() {
|
||||
>
|
||||
Instagram
|
||||
</a>
|
||||
<span className="ml-1 text-xs text-neutral-500">– I hate Instagram</span>
|
||||
<span className="ml-1 text-xs text-neutral-600 dark:text-neutral-400">– I hate Instagram</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{/* Listening */}
|
||||
<div className="pt-2">
|
||||
<h3 className="mb-2 text-xs font-medium uppercase tracking-wider text-neutral-600 dark:text-neutral-500">
|
||||
<h3 className="mb-2 text-xs font-medium uppercase tracking-wider text-neutral-800 dark:text-neutral-300">
|
||||
Listening
|
||||
</h3>
|
||||
<div className="spotify-card">
|
||||
|
||||
54
app/providers/DoomOverlayProvider.tsx
Normal file
54
app/providers/DoomOverlayProvider.tsx
Normal file
@ -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<DoomOverlayContextValue | undefined>(
|
||||
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 (
|
||||
<DoomOverlayContext.Provider value={value}>
|
||||
{children}
|
||||
</DoomOverlayContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useDoomOverlay(): DoomOverlayContextValue {
|
||||
const context = useContext(DoomOverlayContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useDoomOverlay must be used within a DoomOverlayProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
54
app/providers/WhiteboardProvider.tsx
Normal file
54
app/providers/WhiteboardProvider.tsx
Normal file
@ -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<WhiteboardContextType | null>(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 (
|
||||
<WhiteboardContext.Provider value={{ open, close, isOpen }}>
|
||||
{children}
|
||||
<Sheet open={isOpen} onOpenChange={setIsOpen}>
|
||||
<SheetContent side="right" className="w-full sm:max-w-[560px] p-0">
|
||||
<SheetHeader className="p-4 border-b">
|
||||
<SheetTitle>Whiteboard</SheetTitle>
|
||||
<SheetDescription>
|
||||
Sketch ideas. Use mouse or touch to draw.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="p-4">
|
||||
<WhiteboardCanvas />
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</WhiteboardContext.Provider>
|
||||
);
|
||||
}
|
||||
22
components.json
Normal file
22
components.json
Normal file
@ -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": {}
|
||||
}
|
||||
@ -33,7 +33,7 @@ export function CardTitle({
|
||||
return (
|
||||
<h3
|
||||
className={cn(
|
||||
'text-sm font-medium uppercase tracking-wider text-neutral-600 dark:text-neutral-400',
|
||||
'text-sm font-medium uppercase tracking-wider text-neutral-900 dark:text-neutral-100',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@ -47,7 +47,7 @@ export function CardDescription({
|
||||
}: React.HTMLAttributes<HTMLParagraphElement>) {
|
||||
return (
|
||||
<p
|
||||
className={cn('text-sm text-neutral-600 dark:text-neutral-400', className)}
|
||||
className={cn('text-sm text-neutral-700 dark:text-neutral-300', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
143
components/ui/dialog.tsx
Normal file
143
components/ui/dialog.tsx
Normal file
@ -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<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
139
components/ui/sheet.tsx
Normal file
139
components/ui/sheet.tsx
Normal file
@ -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<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||
}
|
||||
|
||||
function SheetTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||
}
|
||||
|
||||
function SheetClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||
}
|
||||
|
||||
function SheetPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||
}
|
||||
|
||||
function SheetOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||
return (
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left"
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
side === "right" &&
|
||||
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||
side === "left" &&
|
||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||
side === "top" &&
|
||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||
side === "bottom" &&
|
||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-header"
|
||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
|
||||
518
package-lock.json
generated
518
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
13
types/js-dos.d.ts
vendored
Normal file
13
types/js-dos.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
declare module "js-dos" {
|
||||
export interface DosInstance {
|
||||
run(url: string): Promise<void>;
|
||||
stop(): void;
|
||||
}
|
||||
|
||||
export function Dos(container: HTMLElement): DosInstance;
|
||||
}
|
||||
|
||||
declare module "js-dos/css/js-dos.css" {
|
||||
const css: string;
|
||||
export default css;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user