- {DOCS_SECTIONS.map((section) => (
-
-
-
- {section.title}
-
-
- {section.items.map((item) => (
-
-
-
-
-
{item.label}
-
- {item.content}
-
-
- ))}
-
-
- ))}
+
+
+
+
+ Getting Started
+
+
+
+
+ Web Utils is a suite of carefully crafted utilities running entirely in your browser. No data is sent to external servers, ensuring complete privacy and security for your sensitive code and tokens.
+
+
+ Simply select a tool from the sidebar to begin.
+
+
+
+
+
+
+
+ Code Editors
+
+
+
+
+ All text-based tools use the Monaco Editor (the core of VS Code). This provides you with powerful features like:
+
+
+ Syntax highlighting & validation
+ Minimap for large files
+ Multiple cursors
+ Command Palette (F1)
+
+
+
+
+
+
+
+ Formatting
+
+
+
+
+ Need to format an ugly JSON or un-minified HTML block? Most editors have a "Format" button near the top right, or you can right-click anywhere in the editor and select "Format Document" .
+
+
+
+
+
+
+
+ Keyboard Shortcuts
+
+
+
+
+
+ Command Palette
+ Ctrl + K
+
+
+ Toggle Sidebar
+ Ctrl + B
+
+
+ Format Document
+ Shift + Alt + F
+
+
+
+
- {/* Footer info */}
-
-
Ready to get started?
-
- Join thousands of developers using Web Utils to edit and preview their code more efficiently.
-
-
-
- Launch App
-
-
- Star on GitHub
-
+ {/* Learn Section */}
+
+
+
+ How Things Work
+
+
+
+
+
+
+ Blockchain Technology
+
+
+
+
+ At its core, a blockchain is a distributed ledger . Imagine a digital spreadsheet that is copied thousands of times across a network of computers. This network is designed to regularly update this spreadsheet.
+
+
+
+
1. Blocks
+
Data is bundled into blocks. Each block has a unique hash and the hash of the previous block.
+
+
+
2. Hashing
+
A cryptographic fingerprint. If any data in the block changes, the hash changes completely.
+
+
+
3. Consensus
+
The network must agree on the validity of a block before it is added to the chain.
+
+
+
+ This structure makes the blockchain immutable —once a block is added, it cannot be changed without changing all subsequent blocks, which would require the consensus of the entire network.
+
+
+
-
+
+
+
);
}
diff --git a/app/draw/page.tsx b/app/draw/page.tsx
new file mode 100644
index 0000000..5b79415
--- /dev/null
+++ b/app/draw/page.tsx
@@ -0,0 +1,5 @@
+import { DrawPage } from "@/components/draw/draw-page";
+
+export default function Page() {
+ return
;
+}
diff --git a/app/dummy/page.tsx b/app/dummy/page.tsx
new file mode 100644
index 0000000..2a391d0
--- /dev/null
+++ b/app/dummy/page.tsx
@@ -0,0 +1,5 @@
+import { DummyFilePage } from "@/components/dummy/dummy-page";
+
+export default function Page() {
+ return
;
+}
diff --git a/app/editor/page.tsx b/app/editor/page.tsx
index 6fb6743..66a53ee 100644
--- a/app/editor/page.tsx
+++ b/app/editor/page.tsx
@@ -1,15 +1,14 @@
"use client";
-import { EditorContainer } from "@/components/editor/editor-container";
-import { DEFAULT_CONTENT } from "@/data/default-content";
+import { WorkspaceContainer } from "@/components/workspace/workspace-container";
export default function EditorPage() {
return (
-
diff --git a/app/globals.css b/app/globals.css
index 170a52e..4d0a255 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -135,18 +135,33 @@
}
.custom-scrollbar::-webkit-scrollbar-thumb {
- background: rgba(0, 0, 0, 0.05);
+ background: rgba(0, 0, 0, 0.03);
border-radius: 10px;
}
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
- background: rgba(255, 255, 255, 0.05);
+ background: rgba(255, 255, 255, 0.03);
}
.custom-scrollbar:hover::-webkit-scrollbar-thumb {
- background: rgba(0, 0, 0, 0.1);
+ background: rgba(0, 0, 0, 0.08);
}
.dark .custom-scrollbar:hover::-webkit-scrollbar-thumb {
- background: rgba(255, 255, 255, 0.1);
+ background: rgba(255, 255, 255, 0.08);
}
+
+/* Global Smooth Transitions */
+.smooth-layout {
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+/* Monaco Editor Overrides */
+.monaco-editor, .monaco-editor .margin, .monaco-editor-background {
+ background-color: transparent !important;
+}
+
+.monaco-editor {
+ padding-top: 8px;
+}
+
diff --git a/app/ide/page.tsx b/app/ide/page.tsx
index 165d23b..d5ec12d 100644
--- a/app/ide/page.tsx
+++ b/app/ide/page.tsx
@@ -1,59 +1,11 @@
"use client";
-import React from 'react';
-import { Blocks, Rocket, Hammer, Construction } from 'lucide-react';
+import { WorkspaceContainer } from "@/components/workspace/workspace-container";
-export default function IDEPage() {
+export default function IdePage() {
return (
-
- {/* Background Decorative Elements */}
-
-
-
-
-
-
- Under Development
-
-
-
- The Next-Gen
- Cloud IDE
-
-
-
- Building a browser-based client WASM type build IDE with real-time collaboration,
- WASM-powered runtimes, and deep integration.
-
-
-
-
-
-
Fast Runtime
-
WASM Powered
-
-
-
-
Extensions
-
Rich Ecosystem
-
-
-
-
Cloud Build
-
Instant Deploy
-
-
-
-
-
- Get Early Access
-
-
- Star Project
-
-
-
-
+
+
);
}
diff --git a/app/layout.tsx b/app/layout.tsx
index db3e5c8..638866f 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,8 +1,8 @@
-import type { Metadata } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
+import type {Metadata} from "next";
+import {Geist, Geist_Mono} from "next/font/google";
import "./globals.css";
-import { ThemeProvider } from "@/components/layout/theme-provider";
-import { Navbar } from "@/components/layout/navbar";
+import {ThemeProvider} from "@/components/layout/theme-provider";
+import {ClientLayout} from "@/components/layout/client-layout";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -16,31 +16,30 @@ const geistMono = Geist_Mono({
export const metadata: Metadata = {
title: "Web Utils | Universal Code Previewer",
- description: "A professional tool for editing and previewing HTML, JSON, YAML, and React code with ease.",
+ description:
+ "A professional tool for editing and previewing HTML, JSON, YAML, and React code with ease.",
};
export default function RootLayout({
children,
-}: Readonly<{
+ }: {
children: React.ReactNode;
-}>) {
+}) {
return (
-
+
-
+ {/* CLIENT PART MOVED HERE */}
+ {children}
);
-}
+}
\ No newline at end of file
diff --git a/app/page.tsx b/app/page.tsx
index 92943c4..a562f13 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,15 +1,155 @@
"use client";
-import { Hero } from "@/components/landing/hero";
-import { Features } from "@/components/landing/features";
-import { Footer } from "@/components/landing/footer";
-
-export default function Home() {
- return (
-
-
-
-
-
- );
+import React, {useState, useEffect, useRef} from 'react';
+import {Search, Copy, Check} from 'lucide-react';
+import {Button} from '@/components/ui/button';
+import {Input} from '@/components/ui/input';
+import {AdsCard} from '@/components/shared/ads-card';
+import {type Category, type Tool, TOOL_CATEGORIES, TOOLS} from '@/lib/constants/tools';
+import {Card, CardDescription, CardHeader, CardTitle} from '@/components/ui/card';
+import Footer from "@/components/common/Footer";
+
+export default function ToolsListingPage() {
+ const [searchQuery, setSearchQuery] = useState("");
+ const [activeCategory, setActiveCategory] = useState("all");
+ const [liveEpoch, setLiveEpoch] = useState(() => Math.floor(Date.now() / 1000));
+ const [copied, setCopied] = useState(false);
+ const searchInputRef = useRef
(null);
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setLiveEpoch(Math.floor(Date.now() / 1000));
+ }, 1000);
+ return () => clearInterval(interval);
+ }, []);
+
+ useEffect(() => {
+ const down = (e: KeyboardEvent) => {
+ if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
+ e.preventDefault();
+ searchInputRef.current?.focus();
+ }
+ };
+ document.addEventListener("keydown", down);
+ return () => document.removeEventListener("keydown", down);
+ }, []);
+
+
+ const filteredTools = TOOLS.filter((tool: Tool) => {
+ const matchesSearch = tool.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ tool.description.toLowerCase().includes(searchQuery.toLowerCase());
+ const matchesCategory = activeCategory === 'all' || tool.category === activeCategory;
+ return matchesSearch && matchesCategory;
+ });
+
+ const handleCopyEpoch = () => {
+ navigator.clipboard.writeText(String(liveEpoch));
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ Developer Tools
+
+
+ A clean, minimalist suite of developer utilities and converters.
+
+
+
+
+
+
+
+ {liveEpoch}
+
+
+ {new Date(liveEpoch * 1000).toUTCString().split(' ').slice(0, 5).join(' ')} UTC
+
+
+
+ {copied ? : }
+
+
+
+
+ {/* Search and Filters */}
+
+
+
+
setSearchQuery(e.target.value)}
+ />
+
+
+ ⌘ K
+
+
+
+
+ setActiveCategory('all')}
+ >
+ All
+
+ {TOOL_CATEGORIES.map((cat: Category) => (
+ setActiveCategory(cat.id)}
+ >
+ {cat.label}
+
+ ))}
+
+
+
+ {/* Unified Tools Grid */}
+
+ {filteredTools.map((tool: Tool) => (
+
window.open(tool.href, '_self')}
+ >
+
+
+
+
+ {tool.name}
+ {tool.description}
+
+
+ {tool.status}
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+ );
}
diff --git a/app/settings/page.tsx b/app/settings/page.tsx
new file mode 100644
index 0000000..f1b9dc1
--- /dev/null
+++ b/app/settings/page.tsx
@@ -0,0 +1,244 @@
+"use client";
+
+import React from 'react';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Settings, Moon, Sun, Monitor, Type, Code, Clock, Globe, Timer } from 'lucide-react';
+import { useTheme } from 'next-themes';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Separator } from '@/components/ui/separator';
+import Footer from "@/components/common/Footer";
+
+import { useLocalStorage } from '@/hooks/use-local-storage';
+
+export default function SettingsPage() {
+ const { theme, setTheme } = useTheme();
+ const [fontSize, setFontSize] = useLocalStorage('editorFontSize', 14);
+ const [tabSize, setTabSize] = useLocalStorage('editorTabSize', 4);
+ const [timeZone, setTimeZone] = useLocalStorage('timeZone', 'UTC');
+ const [timeFormat, setTimeFormat] = useLocalStorage('timeFormat', 'seconds');
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ Settings
+
+
+ Manage your preferences, workspace configuration, and themes.
+
+
+
+
+ {/* Appearance */}
+
+
+ Appearance
+ Customize the look and feel of Web Utils.
+
+
+
+ setTheme('light')}
+ >
+ Light
+
+ setTheme('dark')}
+ >
+ Dark
+
+ setTheme('system')}
+ >
+ System
+
+
+
+
+
+ {/* Editor Preferences */}
+
+
+ Editor Configuration
+ Default settings for Monaco Editor workspaces.
+
+
+
+
+ Default Font Size
+
+ setFontSize(Number(e.target.value))}
+ className="w-full bg-muted/50 border-transparent focus-visible:ring-indigo-500"
+ />
+
+
+
+
+ Tab Size
+
+ setTabSize(Number(e.target.value))}
+ className="w-full bg-muted/50 border-transparent focus-visible:ring-indigo-500"
+ />
+
+
+
+
+ {/* Time & Date Preferences */}
+
+
+
+
+ Time & Date Configuration
+
+ Default preferences for epoch conversion and time display.
+
+
+
+
+
+ Default Time Zone
+
+
+ setTimeZone('UTC')}
+ >
+ UTC / GMT
+
+ setTimeZone('Local')}
+ >
+ Local Time
+
+
+
Sets the primary display format for human-readable dates.
+
+
+
+
+ Default Epoch Format
+
+
+ setTimeFormat('seconds')}
+ >
+ Seconds (10 digits)
+
+ setTimeFormat('millis')}
+ >
+ Millis (13 digits)
+
+
+
Default precision when using "Now" or generating timestamps.
+
+
+
+
+
+ {/* Data Management */}
+
+
+ Data Management
+ Export or import your application settings and workspace data.
+
+
+ {
+ const data = { ...localStorage };
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `web-utils-settings-${new Date().toISOString().split('T')[0]}.json`;
+ a.click();
+ }}
+ >
+ Export Settings
+
+
+
+ {
+ const file = e.target.files?.[0];
+ if (file) {
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ try {
+ const data = JSON.parse(event.target?.result as string);
+ Object.entries(data).forEach(([key, value]) => {
+ localStorage.setItem(key, value as string);
+ });
+ window.location.reload();
+ } catch {
+ alert('Invalid settings file');
+ }
+ };
+ reader.readAsText(file);
+ }
+ }}
+ />
+ document.getElementById('import-settings')?.click()}
+ >
+ Import Settings
+
+
+
+ {
+ if (confirm('Are you sure you want to reset all settings? This will reload the page.')) {
+ localStorage.clear();
+ window.location.reload();
+ }
+ }}
+ >
+ Reset All
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/time/page.tsx b/app/time/page.tsx
new file mode 100644
index 0000000..d2653d7
--- /dev/null
+++ b/app/time/page.tsx
@@ -0,0 +1,5 @@
+import { TimePage } from "@/components/time";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/view/[type]/page.tsx b/app/view/[type]/page.tsx
index 2685141..24657ce 100644
--- a/app/view/[type]/page.tsx
+++ b/app/view/[type]/page.tsx
@@ -1,8 +1,6 @@
-import { SidebarProvider } from "@/components/ui/sidebar";
-import { AppSidebar } from "@/components/layout/app-sidebar";
-import { ViewerContainer } from "@/components/view/viewer-container";
-import { Format } from "@/types";
-import { DEFAULT_CONTENT } from "@/data/default-content";
+import {ViewerContainer} from "@/components/view/viewer-container";
+import {Format} from "@/types";
+import {DEFAULT_CONTENT} from "@/data/default-content";
export function generateStaticParams() {
return [
@@ -23,16 +21,13 @@ export default async function ViewPage({ params }: { params: Promise<{ type: str
const content = (DEFAULT_CONTENT as Record)[format] || "";
return (
-
-
-
+
+
+
+
+
);
}
diff --git a/app/view/page.tsx b/app/view/page.tsx
index c93f5bf..5fba31b 100644
--- a/app/view/page.tsx
+++ b/app/view/page.tsx
@@ -1,22 +1,13 @@
-"use client";
-
-import { SidebarProvider } from "@/components/ui/sidebar";
-import { AppSidebar } from "@/components/layout/app-sidebar";
import { ViewerContainer } from "@/components/view/viewer-container";
import { DEFAULT_CONTENT } from "@/data/default-content";
export default function ViewPage() {
return (
-
-
-
+
+
+
);
}
diff --git a/components/common/Footer.tsx b/components/common/Footer.tsx
new file mode 100644
index 0000000..a245e3f
--- /dev/null
+++ b/components/common/Footer.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { Github } from 'lucide-react';
+import Link from 'next/link';
+
+export default function Footer() {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/components/common/Navbar.tsx b/components/common/Navbar.tsx
new file mode 100644
index 0000000..bf48733
--- /dev/null
+++ b/components/common/Navbar.tsx
@@ -0,0 +1,30 @@
+import React from "react";
+import { Button } from "@/components/ui/button";
+
+function Navbar() {
+ return (
+
+
+
+ {/* Left section */}
+
+
+ Web Utils
+
+
+ Developer utilities & converters
+
+
+
+ {/* Right section */}
+
+ Docs
+ GitHub
+
+
+
+
+ );
+}
+
+export default Navbar;
\ No newline at end of file
diff --git a/components/crypto/blockchain-page.tsx b/components/crypto/blockchain-page.tsx
new file mode 100644
index 0000000..51078c5
--- /dev/null
+++ b/components/crypto/blockchain-page.tsx
@@ -0,0 +1,226 @@
+"use client";
+
+import React, { useState } from 'react';
+import {
+ Bitcoin,
+ Search,
+ ArrowUpRight,
+ ArrowDownLeft,
+ Blocks,
+ Wallet,
+ Activity,
+ ExternalLink,
+ Cpu,
+ Globe,
+ Zap
+} from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Separator } from '@/components/ui/separator';
+
+interface Transaction {
+ type: 'in' | 'out';
+ id: string;
+ date: string;
+ amount: string;
+ status: string;
+}
+
+interface SearchResult {
+ type: 'transaction' | 'address';
+ hash: string;
+ balance: string;
+ totalReceived: string;
+ totalSent: string;
+ txCount: number;
+ transactions: Transaction[];
+ value?: string;
+}
+
+export function BlockchainPage() {
+ const [query, setQuery] = useState('');
+ const [isSearching, setIsSearching] = useState(false);
+ const [result, setResult] = useState(null);
+
+ const handleSearch = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!query.trim()) return;
+
+ setIsSearching(true);
+ // Simulate API call
+ setTimeout(() => {
+ setResult({
+ type: query.length > 42 ? 'transaction' : 'address',
+ hash: query,
+ balance: '1.245 BTC',
+ totalReceived: '4.567 BTC',
+ totalSent: '3.322 BTC',
+ txCount: 42,
+ transactions: [
+ { type: 'in', id: 'f418...d2c1', date: '2 mins ago', amount: '+0.042 BTC', status: 'CONFIRMED' },
+ { type: 'out', id: 'a12b...e34f', date: '1 hour ago', amount: '-0.015 BTC', status: 'CONFIRMED' },
+ { type: 'in', id: 'c98d...b76a', date: '5 hours ago', amount: '+1.120 BTC', status: 'CONFIRMED' },
+ ],
+ value: '$78,452.12'
+ });
+ setIsSearching(false);
+ }, 800);
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+ Blockchain Explorer
+
+
+ Real-time simulation of Bitcoin and Ethereum network data.
+
+
+
+
+
+
+ {!result && !isSearching && (
+
+ {[
+ { label: 'Current Block', value: '835,214', icon: Blocks, color: 'text-indigo-500' },
+ { label: 'Avg Fee', value: '12 sat/vB', icon: Zap, color: 'text-amber-500' },
+ { label: 'Total Nodes', value: '14,231', icon: Globe, color: 'text-emerald-500' },
+ { label: 'Hashrate', value: '612.4 EH/s', icon: Activity, color: 'text-rose-500' }
+ ].map((stat, i) => (
+
+
+
+
+
+
+
{stat.label}
+
{stat.value}
+
+
+
+ ))}
+
+ )}
+
+ {isSearching && (
+
+
+
Scanning Network...
+
+ )}
+
+ {result && !isSearching && (
+
+
+
+
+
+ {result.type}
+
+
+
+
+
+
+ {result.hash}
+
+
+
+
+
Final Balance
+
+ {result.balance}
+ {result.value}
+
+
+
+
+
+
+
+
Received
+
{result.totalReceived}
+
+
+
Sent
+
{result.totalSent}
+
+
+
Txs
+
{result.txCount}
+
+
+
+ View on Explorer
+
+
+
+
+
+
+
+ Recent Transactions
+
+
+
+
+ {result.transactions.map((tx, i) => (
+
+
+
+ {tx.type === 'in' ?
:
}
+
+
+
+
+
{tx.amount}
+
{tx.status}
+
+
+ ))}
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
Real-time Simulation
+
+ This explorer provides a simulated environment for testing blockchain queries.
+ In a production environment, this would connect to the Bitcoin Core or Ethereum JSON-RPC APIs.
+
+
+
+
+
+
+ );
+}
diff --git a/components/crypto/uuid-page.tsx b/components/crypto/uuid-page.tsx
new file mode 100644
index 0000000..7b31add
--- /dev/null
+++ b/components/crypto/uuid-page.tsx
@@ -0,0 +1,138 @@
+"use client";
+
+import React, { useState, useEffect } from 'react';
+import {
+ Shield,
+ RefreshCw,
+ Copy,
+ Check,
+ Settings2,
+ Zap,
+ Hash,
+ Binary
+} from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Separator } from '@/components/ui/separator';
+
+export function UuidPage() {
+ const [uuids, setUuids] = useState([]);
+ const [count, setCount] = useState(5);
+ const [copiedIndex, setCopiedIndex] = useState(null);
+ const hasInitialized = React.useRef(false);
+
+ const generateUuids = React.useCallback(() => {
+ const newUuids = Array.from({ length: count }, () => {
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
+ return crypto.randomUUID();
+ }
+ // Fallback for older browsers
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+ const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
+ return v.toString(16);
+ });
+ });
+ setUuids(newUuids);
+ }, [count]);
+
+ useEffect(() => {
+ if (!hasInitialized.current) {
+ queueMicrotask(() => {
+ generateUuids();
+ });
+ hasInitialized.current = true;
+ }
+ }, [generateUuids]);
+
+ const handleCopy = (text: string, index: number) => {
+ navigator.clipboard.writeText(text);
+ setCopiedIndex(index);
+ setTimeout(() => setCopiedIndex(null), 2000);
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+ UUID v4 Generator
+
+
+ Generate cryptographically strong unique identifiers instantly.
+
+
+
+ Generate New
+
+
+
+
+ {/* Controls */}
+
+
+
+ Configuration
+
+
+
+
+
Batch Size
+
+ {[1, 5, 10, 20, 50].map(n => (
+ setCount(n)}
+ >
+ {n}
+
+ ))}
+
+
+
+
+
+
+ V4 (Random) Algorithm
+
+
+
+ 128-bit identifiers
+
+
+
+ 36 characters total
+
+
+
+
+
+ {/* Results List */}
+
+
+ {uuids.map((uuid, i) => (
+
+
+ {uuid}
+
+ handleCopy(uuid, i)}
+ >
+ {copiedIndex === i ? : }
+
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/components/docs/DocsPage.tsx b/components/docs/DocsPage.tsx
new file mode 100644
index 0000000..8c62af0
--- /dev/null
+++ b/components/docs/DocsPage.tsx
@@ -0,0 +1,130 @@
+"use client";
+
+import React from 'react';
+import {
+ Book,
+ Zap,
+ Shield,
+ Globe,
+ Braces,
+ Box,
+ Terminal,
+ Cpu
+} from 'lucide-react';
+
+import { Footer } from "@/components/landing/footer";
+
+const DOCS_SECTIONS = [
+ {
+ title: "Getting Started",
+ items: [
+ {
+ icon: Zap,
+ label: "Introduction",
+ content: "Web Utils is a high-performance, universal file editor and code previewer designed for modern developers. It allows you to instantly edit, visualize, and format multiple data structures with zero setup."
+ },
+ {
+ icon: Terminal,
+ label: "Quick Start",
+ content: "Navigate to the /view route, paste your code into the editor, and watch as it instantly renders in the preview pane. Use the sidebar to switch between output formats."
+ }
+ ]
+ },
+ {
+ title: "Supported Formats",
+ items: [
+ {
+ icon: Globe,
+ label: "HTML & Bootstrap",
+ content: "Full support for HTML5 and Bootstrap 5.3. Renders code in a safe, sandboxed iframe environment."
+ },
+ {
+ icon: Braces,
+ label: "JSON & YAML",
+ content: "Automatic syntax validation and beautification for structured data. Error highlighting for invalid syntax."
+ },
+ {
+ icon: Box,
+ label: "React (JSX/TSX)",
+ content: "Premium syntax highlighting for React components. Perfect for reviewing component structures."
+ }
+ ]
+ },
+ {
+ title: "Advanced Features",
+ items: [
+ {
+ icon: Cpu,
+ label: "Performance",
+ content: "Built on Next.js 15 for lightning-fast route transitions and optimized rendering cycles."
+ },
+ {
+ icon: Shield,
+ label: "Security",
+ content: "All previews are sandboxed to prevent script execution from impacting the main application window."
+ }
+ ]
+ }
+];
+
+export function DocsPage() {
+ return (
+
+
+
+
+ < Book className="size-3" />
+ Documentation
+
+
+ Everything you need to scale your workflow.
+
+
+ A comprehensive guide to using and extending the Web Utils platform.
+
+
+
+
+ {DOCS_SECTIONS.map((section) => (
+
+
+
+ {section.title}
+
+
+ {section.items.map((item) => (
+
+
+
+
+
{item.label}
+
+ {item.content}
+
+
+ ))}
+
+
+ ))}
+
+
+ {/* Footer info */}
+
+
Ready to get started?
+
+ Join thousands of developers using Web Utils to edit and preview their code more efficiently.
+
+
+
+ Launch App
+
+
+ Star on GitHub
+
+
+
+
+
+
+ );
+}
diff --git a/components/docs/index.ts b/components/docs/index.ts
new file mode 100644
index 0000000..bf3ef9b
--- /dev/null
+++ b/components/docs/index.ts
@@ -0,0 +1 @@
+export * from "./DocsPage";
diff --git a/components/draw/action-menu.tsx b/components/draw/action-menu.tsx
new file mode 100644
index 0000000..215e1f6
--- /dev/null
+++ b/components/draw/action-menu.tsx
@@ -0,0 +1,45 @@
+"use client";
+
+import React from 'react';
+import { MoreHorizontal, Save, FileDown, Share2, Settings } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Separator } from '@/components/ui/separator';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger
+} from '@/components/ui/dropdown-menu';
+
+interface ActionMenuProps {
+ handleDownload: () => void;
+}
+
+export function ActionMenu({ handleDownload }: ActionMenuProps) {
+ return (
+
+
+
+
+
+
+
+
+
+ Save to...
+
+
+ Export image... Ctrl+Shift+E
+
+
+
+ Live collaboration...
+
+
+ Help & Settings
+
+
+
+
+ );
+}
diff --git a/components/draw/draw-page.tsx b/components/draw/draw-page.tsx
new file mode 100644
index 0000000..0752393
--- /dev/null
+++ b/components/draw/draw-page.tsx
@@ -0,0 +1,504 @@
+"use client";
+
+import React, { useRef, useState, useEffect, useLayoutEffect, useCallback } from 'react';
+import Script from 'next/script';
+
+import { Element, ElementType, RoughCanvas } from './types';
+import { Toolbar } from './toolbar';
+import { StylePanel } from './style-panel';
+import { ZoomControls } from './zoom-controls';
+import { ActionMenu } from './action-menu';
+
+// Type-safe access to global roughjs
+interface RoughWindow extends Window {
+ rough: {
+ canvas: (canvas: HTMLCanvasElement) => RoughCanvas;
+ }
+}
+
+// Define drawElement outside to ensure it's not re-created and is hoisted
+function drawElement(rc: RoughCanvas, ctx: CanvasRenderingContext2D, element: Element) {
+ const generator = rc.generator;
+ const options = {
+ stroke: element.color,
+ strokeWidth: element.strokeWidth,
+ roughness: 1.2,
+ bowing: 1.5,
+ seed: element.id + 1,
+ };
+
+ switch (element.type) {
+ case 'line': rc.draw(generator.line(element.x1, element.y1, element.x2, element.y2, options)); break;
+ case 'rectangle': rc.draw(generator.rectangle(element.x1, element.y1, element.x2 - element.x1, element.y2 - element.y1, options)); break;
+ case 'diamond': {
+ const midX = (element.x1 + element.x2) / 2;
+ const midY = (element.y1 + element.y2) / 2;
+ rc.draw(generator.polygon([[midX, element.y1], [element.x2, midY], [midX, element.y2], [element.x1, midY]], options));
+ break;
+ }
+ case 'circle': {
+ const width = element.x2 - element.x1;
+ const height = element.y2 - element.y1;
+ rc.draw(generator.ellipse(element.x1 + width / 2, element.y1 + height / 2, width, height, options));
+ break;
+ }
+ case 'arrow': {
+ const headlen = 15;
+ const angle = Math.atan2(element.y2 - element.y1, element.x2 - element.x1);
+ rc.draw(generator.line(element.x1, element.y1, element.x2, element.y2, options));
+ rc.draw(generator.line(element.x2, element.y2, element.x2 - headlen * Math.cos(angle - Math.PI / 6), element.y2 - headlen * Math.sin(angle - Math.PI / 6), options));
+ rc.draw(generator.line(element.x2, element.y2, element.x2 - headlen * Math.cos(angle + Math.PI / 6), element.y2 - headlen * Math.sin(angle + Math.PI / 6), options));
+ break;
+ }
+ case 'freehand':
+ if (element.points && element.points.length > 1) {
+ const stroke = element.points.map(p => [p.x, p.y] as [number, number]);
+ rc.draw(generator.curve(stroke, options));
+ }
+ break;
+ case 'text':
+ ctx.font = `${32 * element.strokeWidth}px 'Outfit', sans-serif`;
+ ctx.fillStyle = element.color;
+ ctx.textBaseline = 'top';
+ const lines = (element.text || '').split('\n');
+ lines.forEach((line, i) => {
+ ctx.fillText(line, element.x1, element.y1 + i * (38 * element.strokeWidth));
+ });
+ break;
+ }
+}
+
+export function DrawPage() {
+ const canvasRef = useRef(null);
+ const [elements, setElements] = useState([]);
+ const [action, setAction] = useState<'none' | 'drawing' | 'moving' | 'resizing' | 'panning' | 'selecting'>('none');
+ const [tool, setTool] = useState('freehand');
+ const [color, setColor] = useState('#1e1e1e');
+ const [strokeWidth, setStrokeWidth] = useState(2);
+ const [roughCanvas, setRoughCanvas] = useState(null);
+ const [history, setHistory] = useState([]);
+ const [redoStack, setRedoStack] = useState([]);
+ const [selectedElementIds, setSelectedElementIds] = useState([]);
+ const [offset, setOffset] = useState({ x: 0, y: 0 });
+ const [scale, setScale] = useState(1);
+ const [startPanPos, setStartPanPos] = useState({ x: 0, y: 0 });
+ const [selectionBox, setSelectionBox] = useState<{ x1: number, y1: number, x2: number, y2: number } | null>(null);
+ const [isLocked, setIsLocked] = useState(false);
+ const [editingElement, setEditingElement] = useState(null);
+
+ const getMousePos = useCallback((e: React.MouseEvent | MouseEvent) => {
+ if (!canvasRef.current) return { x: 0, y: 0 };
+ const rect = canvasRef.current.getBoundingClientRect();
+ return {
+ x: (e.clientX - rect.left - offset.x) / scale,
+ y: (e.clientY - rect.top - offset.y) / scale
+ };
+ }, [offset, scale]);
+
+ const onScriptLoad = () => {
+ if (typeof window !== 'undefined') {
+ const roughWindow = window as unknown as RoughWindow;
+ if (roughWindow.rough) {
+ const canvas = canvasRef.current;
+ if (canvas) setRoughCanvas(roughWindow.rough.canvas(canvas));
+ }
+ }
+ };
+
+ useEffect(() => {
+ if (typeof window !== 'undefined') {
+ const roughWindow = window as unknown as RoughWindow;
+ if (roughWindow.rough && canvasRef.current && !roughCanvas) {
+ setRoughCanvas(roughWindow.rough.canvas(canvasRef.current));
+ }
+ }
+ }, [roughCanvas]);
+
+ useLayoutEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas || !roughCanvas) return;
+
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return;
+
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.save();
+ ctx.translate(offset.x, offset.y);
+ ctx.scale(scale, scale);
+
+ const sortedElements = [...elements].sort((a, b) => a.zIndex - b.zIndex);
+ sortedElements.forEach((element) => {
+ drawElement(roughCanvas, ctx, element);
+
+ if (selectedElementIds.includes(element.id)) {
+ let minX = Math.min(element.x1, element.x2);
+ let maxX = Math.max(element.x1, element.x2);
+ let minY = Math.min(element.y1, element.y2);
+ let maxY = Math.max(element.y1, element.y2);
+
+ if (element.type === 'freehand' && element.points && element.points.length > 0) {
+ minX = Math.min(...element.points.map(p => p.x));
+ maxX = Math.max(...element.points.map(p => p.x));
+ minY = Math.min(...element.points.map(p => p.y));
+ maxY = Math.max(...element.points.map(p => p.y));
+ }
+
+ ctx.strokeStyle = '#3b82f6';
+ ctx.setLineDash([5, 5]);
+ ctx.lineWidth = 1;
+ ctx.strokeRect(minX - 5, minY - 5, (maxX - minX) + 10, (maxY - minY) + 10);
+ ctx.setLineDash([]);
+
+ // Draw handles
+ ctx.fillStyle = 'white';
+ ctx.strokeStyle = '#3b82f6';
+ ctx.lineWidth = 1.5;
+ const handleSize = 8 / scale;
+
+ [
+ [minX, minY], [maxX, minY], [minX, maxY], [maxX, maxY],
+ [(minX + maxX) / 2, minY], [(minX + maxX) / 2, maxY],
+ [minX, (minY + maxY) / 2], [maxX, (minY + maxY) / 2]
+ ].forEach(([x, y]) => {
+ ctx.beginPath();
+ ctx.rect(x - handleSize / 2, y - handleSize / 2, handleSize, handleSize);
+ ctx.fill();
+ ctx.stroke();
+ });
+ }
+ });
+
+ if (selectionBox) {
+ ctx.strokeStyle = '#3b82f6';
+ ctx.lineWidth = 1 / scale;
+ ctx.strokeRect(
+ selectionBox.x1,
+ selectionBox.y1,
+ selectionBox.x2 - selectionBox.x1,
+ selectionBox.y2 - selectionBox.y1
+ );
+ ctx.fillStyle = 'rgba(59, 130, 246, 0.1)';
+ ctx.fillRect(
+ selectionBox.x1,
+ selectionBox.y1,
+ selectionBox.x2 - selectionBox.x1,
+ selectionBox.y2 - selectionBox.y1
+ );
+ }
+
+ ctx.restore();
+ }, [elements, roughCanvas, selectedElementIds, selectionBox, offset, scale]);
+
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Delete' || e.key === 'Backspace') {
+ if (selectedElementIds.length > 0 && !editingElement) {
+ setHistory(prev => [...prev, elements]);
+ setElements(prev => prev.filter(el => !selectedElementIds.includes(el.id)));
+ setSelectedElementIds([]);
+ }
+ }
+ };
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [selectedElementIds, elements, editingElement]);
+
+ const handleMouseDown = (e: React.MouseEvent) => {
+ if (isLocked) return;
+ const { x, y } = getMousePos(e);
+
+ if (tool === 'hand') {
+ setAction('panning');
+ setStartPanPos({ x: e.clientX, y: e.clientY });
+ return;
+ }
+
+ if (tool === 'selection') {
+ // Check if clicking on an element
+ const clickedElement = [...elements].reverse().find(el => {
+ const minX = Math.min(el.x1, el.x2) - 5;
+ const maxX = Math.max(el.x1, el.x2) + 5;
+ const minY = Math.min(el.y1, el.y2) - 5;
+ const maxY = Math.max(el.y1, el.y2) + 5;
+ return x >= minX && x <= maxX && y >= minY && y <= maxY;
+ });
+
+ if (clickedElement) {
+ if (e.shiftKey) {
+ setSelectedElementIds(prev =>
+ prev.includes(clickedElement.id)
+ ? prev.filter(id => id !== clickedElement.id)
+ : [...prev, clickedElement.id]
+ );
+ } else if (!selectedElementIds.includes(clickedElement.id)) {
+ setSelectedElementIds([clickedElement.id]);
+ }
+ setAction('moving');
+ setStartPanPos({ x: x, y: y });
+ } else {
+ setSelectedElementIds([]);
+ setAction('selecting');
+ setSelectionBox({ x1: x, y1: y, x2: x, y2: y });
+ }
+ return;
+ }
+
+ setAction('drawing');
+ const id = Date.now();
+ const newElement: Element = {
+ id,
+ type: tool,
+ x1: x,
+ y1: y,
+ x2: x,
+ y2: y,
+ color,
+ strokeWidth,
+ zIndex: elements.length,
+ points: tool === 'freehand' ? [{ x, y }] : undefined,
+ text: tool === 'text' ? '' : undefined
+ };
+ setElements(prev => [...prev, newElement]);
+ setHistory(prev => [...prev, elements]);
+ if (tool === 'text') {
+ setEditingElement(newElement);
+ setAction('none');
+ }
+ };
+
+ const handleMouseMove = (e: React.MouseEvent) => {
+ const { x, y } = getMousePos(e);
+
+ if (action === 'panning') {
+ const dx = e.clientX - startPanPos.x;
+ const dy = e.clientY - startPanPos.y;
+ setOffset(prev => ({ x: prev.x + dx, y: prev.y + dy }));
+ setStartPanPos({ x: e.clientX, y: e.clientY });
+ return;
+ }
+
+ if (action === 'drawing') {
+ const index = elements.length - 1;
+ const updatedElements = [...elements];
+ const element = updatedElements[index];
+ element.x2 = x;
+ element.y2 = y;
+ if (element.type === 'freehand' && element.points) {
+ element.points.push({ x, y });
+ }
+ setElements(updatedElements);
+ } else if (action === 'moving') {
+ const dx = x - startPanPos.x;
+ const dy = y - startPanPos.y;
+ setElements(prev => prev.map(el => {
+ if (selectedElementIds.includes(el.id)) {
+ const newEl = { ...el, x1: el.x1 + dx, y1: el.y1 + dy, x2: el.x2 + dx, y2: el.y2 + dy };
+ if (newEl.type === 'freehand' && newEl.points) {
+ newEl.points = newEl.points.map(p => ({ x: p.x + dx, y: p.y + dy }));
+ }
+ return newEl;
+ }
+ return el;
+ }));
+ setStartPanPos({ x, y });
+ } else if (action === 'selecting' && selectionBox) {
+ setSelectionBox({ ...selectionBox, x2: x, y2: y });
+ }
+ };
+
+ const handleDoubleClick = (e: React.MouseEvent) => {
+ const { x, y } = getMousePos(e);
+ const clickedElement = [...elements].reverse().find(el => {
+ const minX = Math.min(el.x1, el.x2) - 5;
+ const maxX = Math.max(el.x1, el.x2) + 5;
+ const minY = Math.min(el.y1, el.y2) - 5;
+ const maxY = Math.max(el.y1, el.y2) + 5;
+ return x >= minX && x <= maxX && y >= minY && y <= maxY;
+ });
+
+ if (clickedElement && clickedElement.type === 'text') {
+ setEditingElement(clickedElement);
+ } else if (!clickedElement) {
+ // Create new text element
+ const id = Date.now();
+ const newElement: Element = {
+ id,
+ type: 'text',
+ x1: x,
+ y1: y,
+ x2: x,
+ y2: y,
+ color,
+ strokeWidth,
+ zIndex: elements.length,
+ text: ''
+ };
+ setElements(prev => [...prev, newElement]);
+ setEditingElement(newElement);
+ }
+ };
+
+ const deleteSelected = useCallback(() => {
+ if (selectedElementIds.length > 0) {
+ setHistory(prev => [...prev, elements]);
+ setElements(prev => prev.filter(el => !selectedElementIds.includes(el.id)));
+ setSelectedElementIds([]);
+ }
+ }, [selectedElementIds, elements]);
+
+ const handleMouseUp = () => {
+ if (action === 'selecting' && selectionBox) {
+ const minX = Math.min(selectionBox.x1, selectionBox.x2);
+ const maxX = Math.max(selectionBox.x1, selectionBox.x2);
+ const minY = Math.min(selectionBox.y1, selectionBox.y2);
+ const maxY = Math.max(selectionBox.y1, selectionBox.y2);
+
+ const newlySelected = elements.filter(el => {
+ const elMinX = Math.min(el.x1, el.x2);
+ const elMaxX = Math.max(el.x1, el.x2);
+ const elMinY = Math.min(el.y1, el.y2);
+ const elMaxY = Math.max(el.y1, el.y2);
+ return elMinX >= minX && elMaxX <= maxX && elMinY >= minY && elMaxY <= maxY;
+ });
+ setSelectedElementIds(newlySelected.map(el => el.id));
+ }
+ setAction('none');
+ setSelectionBox(null);
+ };
+
+ const handleWheel = (e: React.WheelEvent) => {
+ if (e.ctrlKey || e.shiftKey) {
+ e.preventDefault();
+ const delta = e.deltaY > 0 ? 0.9 : 1.1;
+ setScale(prev => Math.max(0.1, Math.min(10, prev * delta)));
+ } else {
+ setOffset(prev => ({ x: prev.x - e.deltaX, y: prev.y - e.deltaY }));
+ }
+ };
+
+ const handleUndo = () => { if (history.length) { setRedoStack(p => [...p, elements]); setElements(history[history.length - 1]); setHistory(h => h.slice(0, -1)); } };
+ const handleRedo = () => { if (redoStack.length) { setHistory(h => [...h, elements]); setElements(redoStack[redoStack.length - 1]); setRedoStack(r => r.slice(0, -1)); } };
+ const handleDownload = () => { const canvas = canvasRef.current; if (canvas) { const a = document.createElement('a'); a.download = 'sketch.png'; a.href = canvas.toDataURL(); a.click(); } };
+
+ const updateElement = (id: number, updates: Partial) => {
+ setElements(prev => prev.map(el => el.id === id ? { ...el, ...updates } : el));
+ };
+
+ const bringToFront = () => {
+ const maxZ = Math.max(...elements.map(e => e.zIndex), 0);
+ setElements(prev => prev.map(e => selectedElementIds.includes(e.id) ? { ...e, zIndex: maxZ + 1 } : e));
+ };
+
+ const sendToBack = () => {
+ const minZ = Math.min(...elements.map(e => e.zIndex), 0);
+ setElements(prev => prev.map(e => selectedElementIds.includes(e.id) ? { ...e, zIndex: minZ - 1 } : e));
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ setScale(s => Math.min(10, s * 1.1))}
+ zoomOut={() => setScale(s => Math.max(0.1, s * 0.9))}
+ resetZoom={() => { setScale(1); setOffset({ x: 0, y: 0 }); }}
+ handleUndo={handleUndo}
+ handleRedo={handleRedo}
+ canUndo={history.length > 0}
+ canRedo={redoStack.length > 0}
+ />
+
+
+ {selectedElementIds.length > 0 && (
+
+ {
+ setColor(c);
+ selectedElementIds.forEach(id => updateElement(id, { color: c }));
+ }}
+ strokeWidth={strokeWidth}
+ setStrokeWidth={(w) => {
+ setStrokeWidth(w);
+ selectedElementIds.forEach(id => updateElement(id, { strokeWidth: w }));
+ }}
+ handleClear={() => { setElements([]); setHistory([]); setRedoStack([]); }}
+ updateElement={updateElement}
+ deleteSelected={deleteSelected}
+ bringToFront={bringToFront}
+ sendToBack={sendToBack}
+ />
+
+ )}
+
+
+
+
+ {editingElement && (() => {
+ const currentEl = elements.find(el => el.id === editingElement.id);
+ if (!currentEl) return null;
+ return (
+
+
+ );
+}
diff --git a/components/draw/style-panel.tsx b/components/draw/style-panel.tsx
new file mode 100644
index 0000000..4953cf2
--- /dev/null
+++ b/components/draw/style-panel.tsx
@@ -0,0 +1,184 @@
+"use client";
+
+import React, { useRef } from 'react';
+import { Palette, Trash2, Plus, Layers, MousePointer2, Type } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Separator } from '@/components/ui/separator';
+import { ScrollArea } from '@/components/ui/scroll-area';
+import { Element } from './types';
+
+export interface StylePanelProps {
+ elements: Element[];
+ selectedElementIds: number[];
+ setSelectedElementIds: (ids: number[]) => void;
+ color: string;
+ setColor: (color: string) => void;
+ strokeWidth: number;
+ setStrokeWidth: (width: number) => void;
+ handleClear: () => void;
+ updateElement: (id: number, updates: Partial) => void;
+ deleteSelected: () => void;
+ bringToFront: () => void;
+ sendToBack: () => void;
+}
+
+const DEFAULT_COLORS = [
+ { name: 'Black', value: '#1e1e1e' },
+ { name: 'Red', value: '#ff4d4d' },
+ { name: 'Green', value: '#4dff4d' },
+ { name: 'Blue', value: '#4d4dff' },
+ { name: 'Yellow', value: '#ffff4d' },
+ { name: 'Purple', value: '#800080' },
+ { name: 'Cyan', value: '#00ffff' },
+];
+
+export function StylePanel({
+ elements, selectedElementIds, setSelectedElementIds, color, setColor, strokeWidth, setStrokeWidth, handleClear, updateElement, deleteSelected, bringToFront, sendToBack
+}: StylePanelProps) {
+ const colorInputRef = useRef(null);
+ const isCustomColor = !DEFAULT_COLORS.some(c => c.value === color);
+
+ const handleDragStart = (e: React.MouseEvent) => {
+ if (e.target instanceof HTMLButtonElement || e.target instanceof HTMLInputElement || (e.target as HTMLElement).closest('button')) return;
+ const el = e.currentTarget as HTMLElement;
+ const startX = e.clientX - el.offsetLeft;
+ const startY = e.clientY - el.offsetTop;
+ const onMouseMove = (moveEvent: MouseEvent) => {
+ const newLeft = moveEvent.clientX - startX;
+ const newTop = moveEvent.clientY - startY;
+ el.style.left = `${Math.max(0, Math.min(window.innerWidth - el.offsetWidth, newLeft))}px`;
+ el.style.top = `${Math.max(0, Math.min(window.innerHeight - el.offsetHeight, newTop))}px`;
+ el.style.right = 'auto';
+ };
+ const onMouseUp = () => {
+ document.removeEventListener('mousemove', onMouseMove);
+ document.removeEventListener('mouseup', onMouseUp);
+ };
+ document.addEventListener('mousemove', onMouseMove);
+ document.addEventListener('mouseup', onMouseUp);
+ };
+
+ const isSelected = (id: number) => selectedElementIds.includes(id);
+ const selectedEl = selectedElementIds.length === 1 ? elements.find(el => el.id === selectedElementIds[0]) : null;
+
+ return (
+
+ {/* Color Palette */}
+
+
+ Color
+
+
+ {DEFAULT_COLORS.map(c => (
+
setColor(c.value)}
+ />
+ ))}
+
+
colorInputRef.current?.click()}
+ >
+ {isCustomColor ? null : }
+
+
setColor(e.target.value)}
+ />
+
+
+
+
+
+
+ {/* Thickness */}
+
+
Thickness
+
+ {[1, 2, 4].map(w => (
+ setStrokeWidth(w)}
+ >
+ {w === 1 ? 'Thin' : w === 2 ? 'Bold' : 'Extra'}
+
+ ))}
+
+
+
+
+
+ {/* Text Editor (Visible when one text node is selected) */}
+ {selectedEl?.type === 'text' && (
+
+
+ Text Content
+
+
+ )}
+
+
+
+ {/* Elements List */}
+
+
+ Nodes ({elements.length})
+
+
+
+ {[...elements].reverse().map(el => (
+
setSelectedElementIds(isSelected(el.id) ? selectedElementIds.filter(id => id !== el.id) : [...selectedElementIds, el.id])}
+ className={`flex items-center gap-2 px-2 py-1.5 rounded-md text-[10px] transition-all ${isSelected(el.id) ? 'bg-primary text-white' : 'hover:bg-zinc-200 dark:hover:bg-zinc-800 text-muted-foreground'}`}
+ >
+
+ {el.type}
+ {isSelected(el.id) && }
+
+ ))}
+ {elements.length === 0 &&
Canvas is empty
}
+
+
+
+
+
+
+ {/* Layering */}
+
+ Send Back
+ Bring Front
+
+
+ {/* Actions */}
+
+
+ Delete Selected
+
+
+
+ Clear All
+
+
+
+ );
+}
diff --git a/components/draw/toolbar.tsx b/components/draw/toolbar.tsx
new file mode 100644
index 0000000..8c06a24
--- /dev/null
+++ b/components/draw/toolbar.tsx
@@ -0,0 +1,174 @@
+"use client";
+
+import React from 'react';
+import {
+ Eraser,
+ Square,
+ Circle,
+ Minus,
+ MousePointer2,
+ PenTool,
+ ArrowRight,
+ Diamond,
+ Hand,
+ Type,
+ Image as ImageIcon,
+ Lock,
+ Unlock,
+ PlusCircle
+} from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Separator } from '@/components/ui/separator';
+import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
+import { ElementType } from './types';
+
+interface ToolbarProps {
+ tool: ElementType;
+ setTool: (tool: ElementType) => void;
+ isLocked: boolean;
+ setIsLocked: (locked: boolean) => void;
+}
+
+export function Toolbar({ tool, setTool, isLocked, setIsLocked }: ToolbarProps) {
+ return (
+
+
+ setIsLocked(!isLocked)}
+ >
+ {isLocked ? : }
+
+
+
+
+
+
v && setTool(v as ElementType)}
+ className="flex items-center gap-0.5"
+ >
+
+
+
+
+
+
+ Hand (H)
+
+
+
+
+
+
+ 1
+
+
+ Selection (V)
+
+
+
+
+
+
+ 2
+
+
+ Rectangle (R)
+
+
+
+
+
+
+ 3
+
+
+ Diamond (D)
+
+
+
+
+
+
+ 4
+
+
+ Circle (O)
+
+
+
+
+
+
+ 5
+
+
+ Arrow (A)
+
+
+
+
+
+
+ 6
+
+
+ Line (L)
+
+
+
+
+
+
+ 7
+
+
+ Draw (P)
+
+
+
+
+
+
+ 8
+
+
+ Text (T)
+
+
+
+
+
+
+ 9
+
+
+ Image
+
+
+
+
+
+
+ 0
+
+
+ Eraser (E)
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/draw/types.ts b/components/draw/types.ts
new file mode 100644
index 0000000..b76c0ba
--- /dev/null
+++ b/components/draw/types.ts
@@ -0,0 +1,43 @@
+
+
+export type ElementType = 'selection' | 'hand' | 'rectangle' | 'diamond' | 'circle' | 'arrow' | 'line' | 'freehand' | 'eraser' | 'text' | 'image';
+
+export interface Element {
+ id: number;
+ type: ElementType;
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+ points?: { x: number, y: number }[];
+ color: string;
+ strokeWidth: number;
+ zIndex: number;
+ text?: string;
+}
+
+export interface Point {
+ x: number;
+ y: number;
+}
+
+export interface RoughOptions {
+ stroke?: string;
+ strokeWidth?: number;
+ roughness?: number;
+ bowing?: number;
+ seed?: number;
+}
+
+export interface RoughGenerator {
+ line: (x1: number, y1: number, x2: number, y2: number, options: RoughOptions) => unknown;
+ rectangle: (x: number, y: number, width: number, height: number, options: RoughOptions) => unknown;
+ polygon: (pts: [number, number][], options: RoughOptions) => unknown;
+ ellipse: (x: number, y: number, width: number, height: number, options: RoughOptions) => unknown;
+ curve: (pts: [number, number][], options: RoughOptions) => unknown;
+}
+
+export interface RoughCanvas {
+ draw: (drawable: unknown) => void;
+ generator: RoughGenerator;
+}
diff --git a/components/draw/zoom-controls.tsx b/components/draw/zoom-controls.tsx
new file mode 100644
index 0000000..1101c09
--- /dev/null
+++ b/components/draw/zoom-controls.tsx
@@ -0,0 +1,45 @@
+"use client";
+
+import React from 'react';
+import { MinusCircle, Plus, Undo2, Redo2 } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+
+interface ZoomControlsProps {
+ scale: number;
+ zoomIn: () => void;
+ zoomOut: () => void;
+ resetZoom: () => void;
+ handleUndo: () => void;
+ handleRedo: () => void;
+ canUndo: boolean;
+ canRedo: boolean;
+}
+
+export function ZoomControls({
+ scale, zoomIn, zoomOut, resetZoom, handleUndo, handleRedo, canUndo, canRedo
+}: ZoomControlsProps) {
+ return (
+
+
+
+
+
+
+ {Math.round(scale * 100)}%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/dummy/dummy-page.tsx b/components/dummy/dummy-page.tsx
new file mode 100644
index 0000000..14d7138
--- /dev/null
+++ b/components/dummy/dummy-page.tsx
@@ -0,0 +1,168 @@
+"use client";
+
+import React, { useState } from 'react';
+import {
+ FilePlus,
+ Download,
+ Info,
+ FileText,
+ Binary,
+ Database,
+ Cpu
+} from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import { Separator } from '@/components/ui/separator';
+import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
+
+export function DummyFilePage() {
+ const [size, setSize] = useState(1);
+ const [unit, setUnit] = useState<'KB' | 'MB' | 'GB'>('MB');
+ const [extension, setExtension] = useState('bin');
+ const [fileName, setFileName] = useState('test-file');
+ const [isGenerating, setIsGenerating] = useState(false);
+
+ const generateFile = () => {
+ setIsGenerating(true);
+ setTimeout(() => {
+ const multiplier = unit === 'KB' ? 1024 : unit === 'MB' ? 1024 * 1024 : 1024 * 1024 * 1024;
+ const byteSize = size * multiplier;
+
+ // For large files, we use a more efficient way if possible,
+ // but for browser-side, we'll create a blob from an arraybuffer
+ try {
+ const buffer = new Uint8Array(byteSize);
+ const blob = new Blob([buffer], { type: 'application/octet-stream' });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `${fileName}.${extension}`;
+ link.click();
+ URL.revokeObjectURL(url);
+ } catch {
+ alert("File too large for browser memory. Try a smaller size.");
+ }
+ setIsGenerating(false);
+ }, 500);
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+ Dummy File Generator
+
+
+ Create placeholder files for testing uploads, storage, or network performance.
+
+
+
+
+
+
+
+ Configuration
+
+
+
+
+
+
File Size
+
+ setSize(parseInt(e.target.value) || 1)}
+ className="h-12 text-lg font-bold w-32"
+ />
+ setUnit(v as 'KB' | 'MB' | 'GB')}
+ className="flex-1"
+ >
+
+ KB
+ MB
+ GB
+
+
+
+
+
+
+
+ {isGenerating ? (
+
+ ) : (
+
+ )}
+ {isGenerating ? 'Generating...' : 'Generate & Download'}
+
+
+
+
+
+
+
+
+ Info
+
+
+
+
+ This tool creates a file of the specified size filled with null bytes (0x00).
+
+
+
+
+ Simulate log files
+
+
+
+ Test binary uploads
+
+
+
+ Stress test storage
+
+
+
+
+ Note: Generating very large files (GB+) may cause your browser to hang temporarily as it allocates memory.
+
+
+
+
+
+
+ );
+}
diff --git a/components/editor/EditorPage.tsx b/components/editor/EditorPage.tsx
new file mode 100644
index 0000000..166a4df
--- /dev/null
+++ b/components/editor/EditorPage.tsx
@@ -0,0 +1,17 @@
+"use client";
+
+import { EditorContainer } from "./editor-container";
+import { DEFAULT_CONTENT } from "@/data/default-content";
+
+export function EditorPage() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/components/editor/editor-container.tsx b/components/editor/editor-container.tsx
index 2d53167..b0a5234 100644
--- a/components/editor/editor-container.tsx
+++ b/components/editor/editor-container.tsx
@@ -1,28 +1,31 @@
"use client";
-import React, { useState, useRef } from 'react';
-import { useRouter } from 'next/navigation';
+import React, { useState, useRef, useMemo } from 'react';
import { Button } from "@/components/ui/button";
+import Editor, { OnMount } from '@monaco-editor/react';
+import { useTheme } from 'next-themes';
import {
Save,
- Check,
FileEdit,
- RotateCcw,
- FileType,
-
- Download,
- Terminal,
Plus,
Table as TableIcon,
- Code,
- ChevronDown,
Trash,
- Eye,
-
+ Copy as CopyIcon,
+ Settings,
+ Type as TypeIcon,
+ Code2,
+ PanelLeftClose,
+ PanelLeftOpen,
+ Maximize2,
+ ChevronDown,
+ Layout as LayoutIcon,
+ AlignLeft,
+ Download
} from "lucide-react";
-import { CodeViewer } from '@/components/shared/code-viewer';
+import ReactMarkdown from 'react-markdown';
+import remarkGfm from 'remark-gfm';
import { TableViewer } from '@/components/shared/table-viewer';
-import { Format, ContainerProps } from '@/types';
+import { ContainerProps, Format } from '@/types';
import yaml from 'js-yaml';
import {
DropdownMenu,
@@ -30,394 +33,400 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
- DropdownMenuLabel,
- DropdownMenuRadioGroup,
- DropdownMenuRadioItem,
} from "@/components/ui/dropdown-menu";
-
-import { PREVIEWABLE_FORMATS, ALL_FORMATS, getLanguage } from '@/lib/formats';
+import { useEditor } from '@/lib/hooks/use-editor';
+import { useLocalStorage } from '@/hooks/use-local-storage';
+import { ALL_FORMATS, getLanguage } from '@/lib/formats';
+import { Separator } from '@/components/ui/separator';
+import {
+ ResizableHandle,
+ ResizablePanel,
+ ResizablePanelGroup,
+} from "@/components/ui/resizable";
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import { HTMLViewer } from '@/components/shared/html-viewer';
+import { CodeViewer } from '@/components/shared/code-viewer';
export function EditorContainer({ initialContent, initialFormat }: ContainerProps) {
- const router = useRouter();
- const [content, setContent] = useState(initialContent);
- const [format, setFormat] = useState(initialFormat);
- const [fileName, setFileName] = useState(`index.${getLanguage(initialFormat)}`);
- const [isSaved, setIsSaved] = useState(true);
- const [copied, setCopied] = useState(false);
- const [wordWrap, setWordWrap] = useState(true);
- const [autoFormat, setAutoFormat] = useState(false);
- const [viewMode, setViewMode] = useState<'code' | 'table'>('code');
- const [cursorPos, setCursorPos] = useState({ line: 1, col: 1 });
- const codeViewerRef = useRef(null);
+ const { resolvedTheme } = useTheme();
+ const { content, setContent, format, setFormat, isSaved, setIsSaved } = useEditor({
+ initialContent,
+ initialFormat,
+ });
- const handleScroll = (e: React.UIEvent) => {
- if (codeViewerRef.current) {
- codeViewerRef.current.scrollTop = e.currentTarget.scrollTop;
- codeViewerRef.current.scrollLeft = e.currentTarget.scrollLeft;
- }
+ const [fileName, setFileName] = useState(`index.${getLanguage(initialFormat)}`);
+
+ // Editor Settings from LocalStorage
+ const [prefFontSize, setPrefFontSize] = useLocalStorage('editorFontSize', 14);
+ const [prefTabSize, setPrefTabSize] = useLocalStorage('editorTabSize', 4);
+ const [prefWordWrap, setPrefWordWrap] = useLocalStorage('editorWordWrap', 'on');
+
+ const handleWordWrapToggle = () => {
+ const newVal = prefWordWrap === 'on' ? 'off' : 'on';
+ setPrefWordWrap(newVal);
};
- const updateCursorPosition = (e: React.UIEvent | React.KeyboardEvent) => {
- const target = e.currentTarget as HTMLTextAreaElement;
- const textBeforeCursor = target.value.substring(0, target.selectionStart);
- const lines = textBeforeCursor.split("\n");
- setCursorPos({
- line: lines.length,
- col: lines[lines.length - 1].length + 1
+ const [cursorPos, setCursorPos] = useState({ line: 1, col: 1 });
+ const [showPreview, setShowPreview] = useState(true);
+ const [isFullscreen, setIsFullscreen] = useState(false);
+ const editorRef = useRef(null); // eslint-disable-line @typescript-eslint/no-explicit-any
+
+ const handleEditorDidMount: OnMount = (editor) => {
+ editorRef.current = editor;
+ editor.onDidChangeCursorPosition((e) => {
+ setCursorPos({ line: e.position.lineNumber, col: e.position.column });
});
};
- const [prevInitialContent, setPrevInitialContent] = useState(initialContent);
- const [prevInitialFormat, setPrevInitialFormat] = useState(initialFormat);
-
- // Sync to sessionStorage automatically for the Viewer to pick up
- React.useEffect(() => {
- if (typeof window !== 'undefined') {
- sessionStorage.setItem(`web-viewer-content-${format}`, content);
- }
- }, [content, format]);
-
- if (initialContent !== prevInitialContent || initialFormat !== prevInitialFormat) {
- setPrevInitialContent(initialContent);
- setPrevInitialFormat(initialFormat);
- setContent(initialContent);
- setFormat(initialFormat);
-
- if (initialFormat !== prevInitialFormat) {
- const namePart = fileName.substring(0, fileName.lastIndexOf('.')) || fileName;
- setFileName(`${namePart}.${getLanguage(initialFormat)}`);
- }
- }
+ const handleSave = () => {
+ const blob = new Blob([content], { type: 'text/plain' });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = fileName;
+ link.click();
+ URL.revokeObjectURL(url);
+ setIsSaved(true);
+ };
const handleCopy = () => {
navigator.clipboard.writeText(content);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
};
- const handleFormat = () => {
+ const handleFormatChange = (newFormat: Format) => {
+ setFormat(newFormat);
+ setFileName(prev => {
+ const parts = prev.split('.');
+ if (parts.length > 1) parts.pop();
+ return [...parts, getLanguage(newFormat)].join('.');
+ });
+ };
+
+ const handleAutoFormat = async () => {
try {
if (format === 'json') {
- const parsed = JSON.parse(content);
- setContent(JSON.stringify(parsed, null, 2));
+ setContent(JSON.stringify(JSON.parse(content), null, prefTabSize));
} else if (format === 'yaml') {
- const parsed = yaml.load(content);
- setContent(yaml.dump(parsed));
+ setContent(yaml.dump(yaml.load(content)));
}
- setIsSaved(false);
- } catch (e) {
- console.error(e);
+ } catch {
+ console.error("Format error");
}
};
- const handleSave = () => {
- setIsSaved(true);
- const element = document.createElement("a");
- const file = new Blob([content], { type: 'text/plain' });
- element.href = URL.createObjectURL(file);
- // If fileName already has an extension, use it; otherwise append current format
- const finalName = fileName.includes('.') ? fileName : `${fileName}.${getLanguage(format)}`;
- element.download = finalName;
- document.body.appendChild(element);
- element.click();
- document.body.removeChild(element);
- };
-
const handleNewFile = () => {
setContent("");
- setFileName("untitled");
setIsSaved(true);
- setViewMode('code');
};
const handleNewTable = () => {
- const defaultTable = JSON.stringify([
- { id: "1", name: "Sample Item", price: "$10.00" },
- { id: "2", name: "Another Item", price: "$20.00" }
- ], null, 2);
- setContent(defaultTable);
- setFormat('json');
- setFileName("table.json");
- setIsSaved(false);
- setViewMode('table');
+ const csvHeader = "id,name,value\n1,example,data";
+ setContent(csvHeader);
+ setFormat("csv");
+ setFileName("table.csv");
};
- const handleTableDataChange = (newData: Record[]) => {
- setContent(JSON.stringify(newData, null, 2));
- setIsSaved(false);
- };
-
- const isTabularData = () => {
- try {
- const parsed = JSON.parse(content);
- return Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === 'object';
- } catch {
- return false;
+ const tableRows = useMemo[] | null>(() => {
+ if (format === 'csv' || format === 'json' || format === 'yaml') {
+ try {
+ if (format === 'csv') {
+ const lines = content.trim().split('\n');
+ if (lines.length < 2) return null;
+ const headers = lines[0].split(',');
+ return lines.slice(1).map(line => {
+ const values = line.split(',');
+ return headers.reduce((obj, header, i) => {
+ obj[header.trim()] = values[i]?.trim();
+ return obj;
+ }, {} as Record);
+ });
+ }
+ if (format === 'json') {
+ const data = JSON.parse(content);
+ if (Array.isArray(data) && data.length > 0) {
+ return data as Record[];
+ }
+ }
+ } catch {
+ return null;
+ }
}
- };
-
- const isPreviewable = PREVIEWABLE_FORMATS.includes(format.toLowerCase() as Format);
+ return null;
+ }, [content, format]);
- const handlePreview = () => {
- if (isPreviewable) {
- router.push(`/view/${format}`);
+ const handleTableDataChange = (newData: Record[]) => {
+ if (format === 'csv') {
+ if (newData.length === 0) return;
+ const headers = Object.keys(newData[0]);
+ const csv = [
+ headers.join(','),
+ ...newData.map(row => headers.map(h => row[h]).join(','))
+ ].join('\n');
+ setContent(csv);
+ } else if (format === 'json') {
+ setContent(JSON.stringify(newData, null, prefTabSize));
}
};
+ const monacoWordWrap = (prefWordWrap === 'on' || prefWordWrap === 'off') ? prefWordWrap : 'off';
+
return (
-
- {/* Notepad-style Ribbon Toolbar */}
-
- {/* Menu Bar */}
-
+
+ {/* Header / Standard Toolbar */}
+
+
-
- File
+
+ {format}
+
-
-
- New File
-
-
- New Table
-
-
-
- Save to System
-
-
- setContent("")} className="gap-2 text-xs text-red-500">
- Clear Content
-
+
+ {ALL_FORMATS.slice(0, 10).map((fmt) => (
+ handleFormatChange(fmt as Format)} className="text-[10px] font-bold uppercase">
+ {fmt}
+
+ ))}
-
-
-
- Options
-
-
-
- Editor Settings
- setWordWrap(!wordWrap)} className="justify-between text-xs">
- Word Wrap
- {wordWrap ? "ON" : "OFF"}
-
- setAutoFormat(!autoFormat)} className="justify-between text-xs">
- Auto-Format
- {autoFormat ? "ON" : "OFF"}
-
-
- Language / Format
- {
- setFormat(v as Format);
- const namePart = fileName.substring(0, fileName.lastIndexOf('.')) || fileName;
- setFileName(`${namePart}.${getLanguage(v as string)}`);
- }}>
- {ALL_FORMATS.slice(0, 10).map((fmt) => (
-
- {fmt}
-
- ))}
-
-
-
- Custom Format
- {
- if (e.target.value) setFormat(e.target.value.toLowerCase() as Format);
- }}
- onKeyDown={(e) => {
- if (e.key === 'Enter') setFormat(e.currentTarget.value.toLowerCase() as Format);
- }}
- />
-
-
-
+
+
+
+
+
+
+
setFileName(e.target.value)}
+ className="bg-transparent border-none outline-none text-[11px] font-bold text-foreground min-w-[120px] placeholder:text-muted-foreground/50"
+ spellCheck={false}
+ />
+ {!isSaved &&
}
+
+
-
- Format Code
+
+
setShowPreview(!showPreview)}
+ >
+ {showPreview ? : }
-
- {copied ? : null}
- {copied ? "Copied" : "Copy"}
+ setIsFullscreen(!isFullscreen)}>
+
- {isPreviewable && (
-
- View in Viewer
-
- )}
-
+
- {/* Quick Actions Toolbar */}
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ Font Size
+
+
+ setPrefFontSize(Number(e.target.value))}
+ className="flex-1 h-1 bg-muted rounded-full appearance-none cursor-pointer accent-primary"
+ />
+ {prefFontSize}
+
-
-
setFileName(e.target.value)}
- className="bg-transparent border-none outline-none text-sm font-bold text-zinc-700 dark:text-zinc-200 w-auto min-w-[120px]"
- style={{ width: `${Math.max(fileName.length, 10)}ch` }}
- spellCheck={false}
- />
+
+
+ Tab Size
+
+
+ {[2, 4, 8].map(size => (
+ setPrefTabSize(size)}
+ >
+ {size}
+
+ ))}
+
- {!isSaved &&
}
-
-
- {isTabularData() && (
-
-
setViewMode('code')}
- className={`h-7 px-3 rounded-md text-[11px] gap-1.5 transition-all ${viewMode === 'code' ? 'bg-white dark:bg-zinc-700 shadow-sm text-indigo-600' : 'text-zinc-500'}`}
+
+
+ Word Wrap
+
+
- Code
+ {prefWordWrap === 'on' ? 'Enabled' : 'Disabled'}
-
setViewMode('table')}
- className={`h-7 px-3 rounded-md text-[11px] gap-1.5 transition-all ${viewMode === 'table' ? 'bg-white dark:bg-zinc-700 shadow-sm text-indigo-600' : 'text-zinc-500'}`}
- >
- Table
+
+
+
+
+ New File
+
+
+ New Table
+
+
+ Save
+
+
setContent("")} className="w-full justify-start h-8 text-[10px] font-bold uppercase gap-2 text-destructive">
+ Clear
- )}
-
-
-
- {isPreviewable && (
-
- Preview
-
- )}
-
- Download
-
-
+
+
- {/* Main Editor Area */}
+ {/* Main Split Canvas */}
-
- {viewMode === 'code' ? (
- <>
- {/* Line Numbers Column */}
-
- {content.split('\n').map((_: string, i: number) => (
-
{i + 1}
- ))}
+
+
+
+
+
+
+ Source
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
setContent("")} className="size-7 text-muted-foreground hover:text-destructive" title="Clear">
+
+
+
-
-
- >
- ) : (
-
+
+
+ {showPreview && (
+ <>
+
+
+
+
+
+
+
+
+ Live View
+ {tableRows && Table View }
+ Raw Output
+
+
+
+
+
+ {format === 'html' ? (
+
+ ) : format === 'markdown' ? (
+
+ {content}
+
+ ) : (
+
+ )}
+
+ {tableRows && (
+
+
+
+ )}
+
+
+ {content}
+
+
+
+
+
+
+
+ >
)}
-
+
- {/* Editor Footer / Status Bar */}
-
+ {/* Footer / Status Bar */}
+
-
- UTF-8
-
-
Spaces: 4
-
-
-
-
- {format}
-
-
- {ALL_FORMATS.slice(0, 10).map((fmt) => (
- setFormat(fmt)} className="text-[10px] uppercase font-bold">
- {fmt}
-
- ))}
-
-
-
-
-
-
-
{content.length} characters
-
+
+
{isSaved ? 'Synced' : 'Modified'}
+
+
{content.length} characters
-
-
Line {cursorPos.line}, Col {cursorPos.col}
-
-
- Notepad+ v1.0
-
+
+ {format}
+
+ UTF-8
-
-
);
}
diff --git a/components/editor/index.ts b/components/editor/index.ts
new file mode 100644
index 0000000..5d49c4a
--- /dev/null
+++ b/components/editor/index.ts
@@ -0,0 +1 @@
+export * from "./EditorPage";
diff --git a/components/features/json/format.ts b/components/features/json/format.ts
new file mode 100644
index 0000000..501465f
--- /dev/null
+++ b/components/features/json/format.ts
@@ -0,0 +1,33 @@
+/**
+ * JSON formatting and validation logic
+ */
+
+export function formatJson(jsonString: string, tabSize: number = 2): string {
+ try {
+ const obj = JSON.parse(jsonString);
+ return JSON.stringify(obj, null, tabSize);
+ } catch (error) {
+ console.error(`Error`,error);
+ throw new Error("Invalid JSON string");
+ }
+}
+
+export function validateJson(jsonString: string): { isValid: boolean; error?: string } {
+ try {
+ JSON.parse(jsonString);
+ return { isValid: true };
+ } catch (error) {
+ console.error(`Error`,error);
+ return { isValid: false, error: error instanceof Error ? error.message : "Unknown error" };
+ }
+}
+
+export function minifyJson(jsonString: string): string {
+ try {
+ const obj = JSON.parse(jsonString);
+ return JSON.stringify(obj);
+ } catch (error : unknown) {
+ console.error(`Error`,error);
+ throw new Error("Invalid JSON string");
+ }
+}
diff --git a/components/features/time/epoch.ts b/components/features/time/epoch.ts
new file mode 100644
index 0000000..8b9de9c
--- /dev/null
+++ b/components/features/time/epoch.ts
@@ -0,0 +1,33 @@
+/**
+ * Epoch and Date conversion logic
+ */
+
+export function epochToDate(epoch: number): Date {
+ // Detect if seconds or milliseconds
+ const ms = epoch < 10000000000 ? epoch * 1000 : epoch;
+ return new Date(ms);
+}
+
+export function dateToEpoch(date: Date, unit: 's' | 'ms' = 's'): number {
+ const ms = date.getTime();
+ return unit === 's' ? Math.floor(ms / 1000) : ms;
+}
+
+export function formatRelative(date: Date): string {
+ const now = new Date();
+ const diff = now.getTime() - date.getTime();
+ const seconds = Math.floor(diff / 1000);
+ const minutes = Math.floor(seconds / 60);
+ const hours = Math.floor(minutes / 60);
+ const days = Math.floor(hours / 24);
+
+ if (days > 0) return `${days} day${days > 1 ? 's' : ''} ago`;
+ if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
+ if (minutes > 0) return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
+ return "just now";
+}
+
+export function isValidEpoch(val: unknown): boolean {
+ const num = Number(val);
+ return !isNaN(num) && num > 0;
+}
diff --git a/components/features/time/timezone.ts b/components/features/time/timezone.ts
new file mode 100644
index 0000000..660ef8d
--- /dev/null
+++ b/components/features/time/timezone.ts
@@ -0,0 +1,20 @@
+/**
+ * Timezone conversion logic
+ */
+
+export function convertTimezone(date: Date, toTz: string): string {
+ return date.toLocaleString('en-US', { timeZone: toTz });
+}
+
+export const COMMON_TIMEZONES = [
+ { label: "UTC", value: "UTC" },
+ { label: "India (IST)", value: "Asia/Kolkata" },
+ { label: "New York (EST/EDT)", value: "America/New_York" },
+ { label: "London (GMT/BST)", value: "Europe/London" },
+ { label: "Tokyo (JST)", value: "Asia/Tokyo" },
+ { label: "Pacific (PST/PDT)", value: "America/Los_Angeles" }
+];
+
+export function getSystemTimezone(): string {
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
+}
diff --git a/components/ide/IDEPage.tsx b/components/ide/IDEPage.tsx
new file mode 100644
index 0000000..88cd5c3
--- /dev/null
+++ b/components/ide/IDEPage.tsx
@@ -0,0 +1,59 @@
+"use client";
+
+import React from 'react';
+import { Blocks, Rocket, Hammer, Construction } from 'lucide-react';
+
+export function IDEPage() {
+ return (
+
+ {/* Background Decorative Elements */}
+
+
+
+
+
+
+ Under Development
+
+
+
+ The Next-Gen
+ Cloud IDE
+
+
+
+ Building a browser-based client WASM type build IDE with real-time collaboration,
+ WASM-powered runtimes, and deep integration.
+
+
+
+
+
+
Fast Runtime
+
WASM Powered
+
+
+
+
Extensions
+
Rich Ecosystem
+
+
+
+
Cloud Build
+
Instant Deploy
+
+
+
+
+
+ Get Early Access
+
+
+ Star Project
+
+
+
+
+
+ );
+}
diff --git a/components/ide/index.ts b/components/ide/index.ts
new file mode 100644
index 0000000..a406606
--- /dev/null
+++ b/components/ide/index.ts
@@ -0,0 +1 @@
+export * from "./IDEPage";
diff --git a/components/json/tree-viewer.tsx b/components/json/tree-viewer.tsx
new file mode 100644
index 0000000..58963f1
--- /dev/null
+++ b/components/json/tree-viewer.tsx
@@ -0,0 +1,124 @@
+ "use client";
+
+import React, { useState } from 'react';
+import {
+ ChevronRight,
+ ChevronDown,
+ Copy,
+} from 'lucide-react';
+import { cn } from "@/lib/utils";
+import { Badge } from "@/components/ui/badge";
+import { toast } from "sonner";
+
+const JsonNode = ({ data, depth = 0, path = "", label }: { data: unknown, depth?: number, path: string, label?: string }) => {
+ const [isExpanded, setIsExpanded] = useState(depth < 2);
+ const type = typeof data;
+ const isObject = data !== null && type === 'object';
+ const isArray = Array.isArray(data);
+
+ const toggle = () => setIsExpanded(!isExpanded);
+
+ const copyPath = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ navigator.clipboard.writeText(path);
+ toast.success(`Path copied: ${path}`);
+ };
+
+ if (isObject) {
+ const objData = data as Record
;
+ const keys = Object.keys(objData);
+ const isEmpty = keys.length === 0;
+
+ return (
+
+
+
+ {!isEmpty && (
+ isExpanded ?
:
+ )}
+ {isEmpty &&
}
+
+
+ {label && (
+
{label}:
+ )}
+
+
+ {isArray ? `Array[${keys.length}]` : `Object{${keys.length}}`}
+
+
+
+
+
+
+
+ {isExpanded && !isEmpty && (
+
+ {keys.map(key => (
+
+ ))}
+
+ )}
+
+ );
+ }
+
+ const numValue = Number(data);
+
+ // Primitive values
+ return (
+
+
+ {label && (
+
{label}:
+ )}
+
+ {type === 'string' ? `"${data}"` : String(data)}
+
+
+ {/* Special "Smart Detection" for timestamps (Epoch) */}
+ {type === 'number' && numValue > 1000000000 && numValue < 3000000000 && (
+
+ Epoch Detected
+
+ )}
+
+
+
+
+
+ );
+};
+
+export function JsonTreeViewer({ data }: { data: unknown }) {
+ if (!data) return No JSON data to display
;
+
+ return (
+
+
+
+ );
+}
diff --git a/components/landing/hero.tsx b/components/landing/hero.tsx
index daeef00..a9696e7 100644
--- a/components/landing/hero.tsx
+++ b/components/landing/hero.tsx
@@ -2,14 +2,15 @@
import React from 'react';
import Link from "next/link";
-import { Button } from "@/components/ui/button";
-import { ArrowRight, Zap } from "lucide-react";
+import {Button} from "@/components/ui/button";
+import {ArrowRight, Zap} from "lucide-react";
export function Hero() {
return (
{/* Background Glows */}
-
+
@@ -17,7 +18,7 @@ export function Hero() {
The next-gen code toolkit
-
+
Precision Editing.
Instant Visualization.
@@ -43,9 +44,11 @@ export function Hero() {
{/* Hero Visual - Professional Code Preview */}
-
+
-
+
{/* Fake Browser Toolbar */}
@@ -60,7 +63,7 @@ export function Hero() {
-
+
{/* Editor Side */}
diff --git a/components/layout/app-sidebar.tsx b/components/layout/app-sidebar.tsx
index 2c138bc..b822f76 100644
--- a/components/layout/app-sidebar.tsx
+++ b/components/layout/app-sidebar.tsx
@@ -6,20 +6,15 @@ import {
HelpCircle,
ChevronLeft,
ChevronRight,
- FileEdit,
- Braces,
- Globe,
- Box,
- FileCode,
- StickyNote,
- Table,
- Image
+ Command,
+ Search
} from "lucide-react";
import {
Sidebar,
SidebarContent,
SidebarFooter,
+ SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
@@ -29,80 +24,159 @@ import {
useSidebar,
} from "@/components/ui/sidebar";
import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { TOOLS, TOOL_CATEGORIES, type Tool, type Category } from "@/lib/constants/tools";
export function AppSidebar() {
+ const pathname = usePathname();
const { toggleSidebar, state } = useSidebar();
+ const [searchQuery, setSearchQuery] = React.useState("");
+ const searchInputRef = React.useRef
(null);
+
+ React.useEffect(() => {
+ const down = (e: KeyboardEvent) => {
+ if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
+ e.preventDefault();
+ searchInputRef.current?.focus();
+ }
+ };
+ document.addEventListener("keydown", down);
+ return () => document.removeEventListener("keydown", down);
+ }, []);
+
const isCollapsed = state === "collapsed";
return (
{/* Toggle Button "On the Line" - Placed Between Header and Content */}
-
-
- {isCollapsed ? (
-
- ) : (
-
- )}
-
+
+
+
+
+ {isCollapsed ? (
+
+ ) : (
+
+ )}
+
+
+
+ Toggle Sidebar
+
+ ⌘ B
+
+
+
+
+
+
+
+
setSearchQuery(e.target.value)}
+ />
+
+
+ ⌘ K
+
+
+
+
+
-
- Formats
-
-
- {[
- { id: "html", label: "HTML Preview", icon: Globe, tooltip: "HTML View" },
- { id: "json", label: "JSON Tool", icon: Braces, tooltip: "JSON View" },
- { id: "yaml", label: "YAML View", icon: StickyNote, tooltip: "YAML View" },
- { id: "react", label: "React Code", icon: Box, tooltip: "React Code" },
- { id: "markdown", label: "Markdown View", icon: FileEdit, tooltip: "Markdown View" },
- { id: "xml", label: "XML Viewer", icon: FileCode, tooltip: "XML View" },
- { id: "svg", label: "SVG Render", icon: Image, tooltip: "SVG Render" },
- { id: "csv", label: "CSV Viewer", icon: Table, tooltip: "CSV View" },
- ].map((item) => (
-
-
-
-
- {item.label}
-
-
-
- ))}
-
-
-
+ {TOOL_CATEGORIES.map((cat: Category) => {
+ const categoryTools = TOOLS.filter(
+ (tool: Tool) =>
+ tool.category === cat.id &&
+ (tool.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ tool.description.toLowerCase().includes(searchQuery.toLowerCase()))
+ );
+ if (categoryTools.length === 0) return null;
+
+ return (
+
+
+ {cat.label}
+
+
+
+ {categoryTools.map((tool: Tool) => (
+
+
+
+
+
+ {tool.name}
+ {['draw-tool', 'dummy-file', 'blockchain-tool', 'uuid-generator'].includes(tool.id) && (
+ NEW
+ )}
+
+
+
+
+ ))}
+
+
+
+ );
+ })}
-
-
-
+
+
+
-
-
+
+
Documentation
-
-
-
- Settings
-
+
+
+
+
+ Settings
+
+
diff --git a/components/layout/client-layout.tsx b/components/layout/client-layout.tsx
new file mode 100644
index 0000000..9ba7b0b
--- /dev/null
+++ b/components/layout/client-layout.tsx
@@ -0,0 +1,51 @@
+"use client";
+
+import React, { useState } from "react";
+import { usePathname } from "next/navigation";
+import {SidebarProvider} from "@/components/ui/sidebar";
+import {SplashScreen} from "@/components/layout/splash-screen";
+import {Navbar} from "@/components/layout/navbar";
+import {AppSidebar} from "@/components/layout/app-sidebar";
+
+export function ClientLayout({ children }: { children: React.ReactNode }) {
+ const pathname = usePathname();
+ const isHomePage = pathname === "/";
+ const [showSplash, setShowSplash] = useState(() => {
+ if (typeof window !== "undefined") {
+ return !localStorage.getItem("hasSeenSplash_v1");
+ }
+ return true;
+ });
+
+ const handleSplashComplete = () => {
+ setShowSplash(false);
+ localStorage.setItem("hasSeenSplash_v1", "true");
+ };
+
+ return (
+
+ {showSplash && }
+
+
+
+
+
+ {isHomePage && (
+
+ )}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/layout/command-menu.tsx b/components/layout/command-menu.tsx
new file mode 100644
index 0000000..64440ea
--- /dev/null
+++ b/components/layout/command-menu.tsx
@@ -0,0 +1,75 @@
+"use client";
+
+import * as React from "react";
+import {
+ Settings,
+ User,
+} from "lucide-react";
+import { useRouter } from "next/navigation";
+
+import {
+ CommandDialog,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+ CommandSeparator,
+ CommandShortcut,
+} from "@/components/ui/command";
+import { TOOLS, type Tool } from "@/lib/constants/tools";
+
+export function CommandMenu() {
+ const [open, setOpen] = React.useState(false);
+ const router = useRouter();
+
+ React.useEffect(() => {
+ const down = (e: KeyboardEvent) => {
+ if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
+ e.preventDefault();
+ setOpen((open) => !open);
+ }
+ };
+
+ document.addEventListener("keydown", down);
+ return () => document.removeEventListener("keydown", down);
+ }, []);
+
+ const runCommand = React.useCallback((command: () => void) => {
+ setOpen(false);
+ command();
+ }, []);
+
+ return (
+
+
+
+ No results found.
+
+ {TOOLS.map((tool: Tool) => (
+ runCommand(() => router.push(tool.href))}
+ >
+
+ {tool.name}
+
+ ))}
+
+
+
+ runCommand(() => {})}>
+
+ Profile
+ ⌘P
+
+ runCommand(() => {})}>
+
+ Settings
+ ⌘S
+
+
+
+
+ );
+}
diff --git a/components/layout/ide-workspace.tsx b/components/layout/ide-workspace.tsx
new file mode 100644
index 0000000..f2dc342
--- /dev/null
+++ b/components/layout/ide-workspace.tsx
@@ -0,0 +1,269 @@
+"use client";
+
+import React, { useState } from 'react';
+import Editor from '@monaco-editor/react';
+import {
+ Panel,
+ Group as PanelGroup,
+ Separator as PanelResizeHandle
+} from 'react-resizable-panels';
+import {
+ Play,
+ Save,
+ RotateCcw,
+ Eye,
+ Braces,
+ Clock,
+ Terminal as TerminalIcon,
+ Globe
+} from 'lucide-react';
+import { Button } from "@/components/ui/button";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { JsonTreeViewer } from "@/components/json/tree-viewer";
+import { useTheme } from "next-themes";
+
+export function IdeWorkspace() {
+ const [code, setCode] = useState('{\n "name": "Web Utils Pro",\n "status": "Ready",\n "timestamp": 1711206600\n}');
+ const [language, setLanguage] = useState('json');
+ const { theme } = useTheme();
+
+ const [terminalInput, setTerminalInput] = useState('');
+ const [terminalOutput, setTerminalOutput] = useState([
+ 'system: ide workspace activated',
+ 'system: terminal ready'
+ ]);
+
+ const handleTerminalSubmit = () => {
+ if (!terminalInput.trim()) return;
+
+ const cmd = terminalInput.toLowerCase();
+ const newOutput = [...terminalOutput, `$${terminalInput}`];
+
+ switch(cmd) {
+ case 'help':
+ newOutput.push('available commands:');
+ newOutput.push(' - clear: clear the terminal');
+ newOutput.push(' - json-format: format the editor content');
+ newOutput.push(' - time: show current epoch');
+ newOutput.push(' - build: simulate build process');
+ break;
+ case 'clear':
+ setTerminalOutput([]);
+ setTerminalInput('');
+ return;
+ case 'json-format':
+ try {
+ const formatted = JSON.stringify(JSON.parse(code), null, 2);
+ setCode(formatted);
+ newOutput.push('success: json formatted');
+ } catch(e: unknown) {
+ console.error('Error : ', e);
+ newOutput.push(`error: invalid json `);
+ }
+ break;
+ case 'time':
+ newOutput.push(`current epoch: ${Math.floor(Date.now() / 1000)}`);
+ break;
+ case 'build':
+ newOutput.push('build: starting optimization...');
+ newOutput.push('build: generated 18 static pages');
+ newOutput.push('build: success in 234ms');
+ break;
+ default:
+ newOutput.push(`error: command not found: ${cmd}`);
+ }
+
+ setTerminalOutput(newOutput);
+ setTerminalInput('');
+ };
+
+ const handleEditorChange = (value: string | undefined) => {
+ if (value) setCode(value);
+ };
+
+ let parsedJson = null;
+ try {
+ parsedJson = JSON.parse(code);
+ } catch (e: unknown) {
+ console.error('Error : ', e);
+ }
+
+ return (
+
+ {/* IDE Toolbar */}
+
+
+
+ setLanguage(e.target.value)}
+ className="bg-transparent text-xs font-bold text-indigo-600 dark:text-indigo-400 outline-none cursor-pointer"
+ >
+ scratchpad.json
+ index.html
+ main.js
+
+
+
+
+
+
+ Reset
+
+
+ Save
+
+
+ Run
+
+
+
+
+ {/* Main Workspace */}
+
+ {/* Editor Panel */}
+
+
+
+
+
+
+
+
+ {/* Right Panel (Tools & Preview) */}
+
+
+
+
+
+
+ Preview
+
+
+ JSON Tree
+
+
+ Time
+
+
+ Terminal
+
+
+ Console
+
+
+
+
+
+
+ {language === 'html' ? (
+
+
+
+ ) : (
+
+
+
+ HTML Preview Available
+
+
+ )}
+
+
+
+
+
+
+
+
Selected Timestamp
+
1711206600
+
+
+
+
Local Time
+
2024-03-23 20:30:00
+
+
+
UTC Time
+
2024-03-23 15:00:00
+
+
+
+
+
+
+
+
+
+
web-utils-shell v1.0
+
+
type 'help' to see available commands
+
+
+ {terminalOutput.map((line, i) => (
+
+ {line.startsWith('>') ? (
+ {line.substring(0, 1)}
+ ) : line.startsWith('$') ? (
+ $
+ ) : null}
+ {line.startsWith('>') || line.startsWith('$') ? line.substring(1) : line}
+
+ ))}
+
+
+
+ $
+ setTerminalInput(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') handleTerminalSubmit();
+ }}
+ placeholder="run command..."
+ />
+
+
+
+
+
+
[System] Workspace initialized successfully.
+
[Info] Loaded language: {language}
+
_
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/layout/navbar.tsx b/components/layout/navbar.tsx
index 377e02b..38f3f83 100644
--- a/components/layout/navbar.tsx
+++ b/components/layout/navbar.tsx
@@ -1,144 +1,51 @@
"use client";
-import React, { useState } from 'react';
-import { Moon, Sun, Layout, Github, Menu, X, FileEdit, Code2, Home } from 'lucide-react';
-import { useTheme } from 'next-themes';
-import { Button } from '@/components/ui/button';
+import React from 'react';
+import {Github, Moon, Sun, Command} from 'lucide-react';
+import {useTheme} from 'next-themes';
+import {Button} from '@/components/ui/button';
import Link from 'next/link';
-import { usePathname } from 'next/navigation';
+
+import { useSidebar } from "@/components/ui/sidebar";
export function Navbar() {
const { theme, setTheme } = useTheme();
- const [isMenuOpen, setIsMenuOpen] = useState(false);
- const pathname = usePathname();
- const navLinks = [
- { href: "/", label: "Home", icon: Home },
- { href: "/documentation", label: "Docs" },
- ];
-
- const tools = [
- { id: "editor", label: "Editor", href: "/editor", icon: FileEdit },
- { id: "viewer", label: "Viewer", href: "/view", icon: Layout },
- { id: "ide", label: "IDE", href: "/ide", icon: Code2 }
- ];
-
- const activeTool = pathname.includes("editor") ? "editor"
- : pathname.includes("ide") ? "ide"
- : pathname.includes("view") ? "viewer"
- : null;
+ const { state } = useSidebar();
return (
- <>
-
-
-
-
-
+
+
+
+
+
+
-
- Web Utils
-
+
Web Utils
- {/* Desktop Menu */}
-
- {navLinks.map((link) => (
-
- {link.label}
-
- ))}
-
-
-
+
-
- {tools.map((tool) => (
-
-
-
- {tool.label}
-
-
- ))}
-
-
+
+
+
+
+
+ GitHub
+
+
setTheme(theme === 'dark' ? 'light' : 'dark')}
- className="rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800"
>
-
-
+
+
Toggle theme
-
-
- {/* Mobile Controls */}
-
- setTheme(theme === 'dark' ? 'light' : 'dark')}
- className="rounded-full"
- >
-
-
-
- setIsMenuOpen(!isMenuOpen)}
- className="text-zinc-600 dark:text-zinc-400"
- >
- {isMenuOpen ? : }
-
-
+
-
- {/* Mobile Menu Dropdown */}
- {isMenuOpen && (
-
- {tools.map((tool) => (
-
setIsMenuOpen(false)}
- className={`flex items-center gap-3 p-3 rounded-xl text-base font-semibold transition-colors ${activeTool === tool.id
- ? "bg-indigo-50 dark:bg-indigo-900/20 text-indigo-600 dark:text-indigo-400"
- : "text-zinc-600 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-zinc-900"
- }`}
- >
-
-
{tool.label}
-
- ))}
-
-
- GitHub
-
-
- )}
-
- >
+
+
);
}
diff --git a/components/layout/splash-screen.tsx b/components/layout/splash-screen.tsx
new file mode 100644
index 0000000..31815fb
--- /dev/null
+++ b/components/layout/splash-screen.tsx
@@ -0,0 +1,87 @@
+"use client";
+
+import React from 'react';
+import {Command} from 'lucide-react';
+import gsap from 'gsap';
+import {useGSAP} from '@gsap/react';
+
+export function SplashScreen({ onCompleteAction }: { onCompleteAction: () => void }) {
+ const container = React.useRef
(null);
+ const logoRef = React.useRef(null);
+ const textRef = React.useRef(null);
+
+ useGSAP(() => {
+ const tl = gsap.timeline({
+ onComplete: () => {
+ gsap.to(container.current, {
+ opacity: 0,
+ duration: 0.3,
+ ease: "power2.inOut",
+ onComplete: onCompleteAction
+ });
+ }
+ });
+
+ tl.from(logoRef.current, {
+ scale: 0.5,
+ opacity: 0,
+ duration: 0.4,
+ ease: "back.out(1.7)"
+ })
+ .from(textRef.current, {
+ y: 20,
+ opacity: 0,
+ duration: 0.3,
+ ease: "power3.out"
+ }, "-=0.2")
+ .to(logoRef.current, {
+ rotation: 360,
+ duration: 0.6,
+ ease: "power1.inOut"
+ }, "-=0.1")
+ .to(container.current, {
+ delay: 0.1,
+ duration: 0.2
+ });
+
+ }, { scope: container });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ Web Utils
+
+
+ The Developer Workspace
+
+
+ {/* Progress bar simulation correctly placed under text */}
+
+
+
+
+ );
+}
diff --git a/components/layout/tool-shell.tsx b/components/layout/tool-shell.tsx
new file mode 100644
index 0000000..23fa0ad
--- /dev/null
+++ b/components/layout/tool-shell.tsx
@@ -0,0 +1,86 @@
+"use client";
+
+import React from 'react';
+import Link from 'next/link';
+import {
+ ChevronRight,
+ Home,
+} from "lucide-react";
+
+interface BreadcrumbItem {
+ label: string;
+ href?: string;
+}
+
+interface ToolShellProps {
+ title: string;
+ description: string;
+ breadcrumbs: BreadcrumbItem[];
+ children: React.ReactNode;
+ sidebar?: React.ReactNode;
+}
+
+export function ToolShell({
+ title,
+ description,
+ breadcrumbs,
+ children,
+ sidebar
+}: ToolShellProps) {
+ return (
+
+ {/* Tool Header & Breadcrumbs */}
+
+
+ {/* Breadcrumbs */}
+
+
+ Home
+
+ {breadcrumbs.map((item, i) => (
+
+
+ {item.href ? (
+
+ {item.label}
+
+ ) : (
+ {item.label}
+ )}
+
+ ))}
+
+
+ {/* Title Area */}
+
+
+ {title}
+
+
+ {description}
+
+
+
+
+
+ {/* Main Layout Area */}
+
+
+ {/* Parameters Sidebar (Optional) */}
+ {sidebar && (
+
+ )}
+
+ {/* Content Canvas */}
+
+
+ {children}
+
+
+
+
+
+ );
+}
diff --git a/components/shared/ads-card.tsx b/components/shared/ads-card.tsx
new file mode 100644
index 0000000..510728a
--- /dev/null
+++ b/components/shared/ads-card.tsx
@@ -0,0 +1,56 @@
+ "use client";
+
+import React from 'react';
+import { ExternalLink, Sparkles } from 'lucide-react';
+import { cn } from "@/lib/utils";
+
+interface AdsCardProps {
+ variant?: 'sidebar' | 'horizontal' | 'native';
+ className?: string;
+}
+
+export function AdsCard({ variant = 'sidebar', className }: AdsCardProps) {
+ return (
+
+ {/* Glossy overlay */}
+
+
+
+
+
+
+
+ {variant === 'horizontal' ? "Level up your workflow with Pro Features" : "Turbocharge your IDE"}
+
+
+ Join 10k+ developers using our advanced toolchain every day.
+
+
+
+ {variant === 'horizontal' && (
+
+ Upgrade Now
+
+ )}
+
+
+ );
+}
diff --git a/components/shared/code-viewer.tsx b/components/shared/code-viewer.tsx
index 7cdd434..1ccd705 100644
--- a/components/shared/code-viewer.tsx
+++ b/components/shared/code-viewer.tsx
@@ -1,8 +1,9 @@
"use client";
import React, { forwardRef } from 'react';
-import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
-import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
+import Editor from '@monaco-editor/react';
+import { useTheme } from 'next-themes';
+import { useLocalStorage } from '@/hooks/use-local-storage';
interface CodeViewerProps {
content: string;
@@ -12,39 +13,38 @@ interface CodeViewerProps {
wrapLines?: boolean;
}
-export const CodeViewer = forwardRef(({ content, language, className, showLineNumbers = true, wrapLines = true }, ref) => {
+export const CodeViewer = forwardRef(({ content, language, className, wrapLines = true }, ref) => {
+ const { resolvedTheme } = useTheme();
+ const [prefFontSize] = useLocalStorage('editorFontSize', 14);
+ const [prefTabSize] = useLocalStorage('editorTabSize', 4);
+
return (
-
-
+
- {content}
-
+ />
);
});
diff --git a/components/shared/html-viewer.tsx b/components/shared/html-viewer.tsx
index 1f948b2..df5e0cf 100644
--- a/components/shared/html-viewer.tsx
+++ b/components/shared/html-viewer.tsx
@@ -9,7 +9,7 @@ interface HTMLViewerProps {
enableJS?: boolean;
}
-export function HTMLViewer({ content, useBootstrap = true, useTailwind = true, enableJS = true }: HTMLViewerProps) {
+export function HTMLViewer({ content, useBootstrap = false, useTailwind = false, enableJS = true }: HTMLViewerProps) {
const iframeRef = useRef(null);
useEffect(() => {
@@ -26,7 +26,7 @@ export function HTMLViewer({ content, useBootstrap = true, useTailwind = true, e
${useBootstrap ? ' ' : ''}
${useTailwind ? '' : ''}
@@ -43,7 +43,7 @@ export function HTMLViewer({ content, useBootstrap = true, useTailwind = true, e
return (
diff --git a/components/time/TimePage.tsx b/components/time/TimePage.tsx
new file mode 100644
index 0000000..a97061a
--- /dev/null
+++ b/components/time/TimePage.tsx
@@ -0,0 +1,12 @@
+"use client";
+
+
+import { EpochConverter } from "./epoch-converter";
+
+export function TimePage() {
+ return (
+
+
+
+ );
+}
diff --git a/components/time/epoch-converter.tsx b/components/time/epoch-converter.tsx
new file mode 100644
index 0000000..fd743a0
--- /dev/null
+++ b/components/time/epoch-converter.tsx
@@ -0,0 +1,384 @@
+"use client";
+
+import React, { useState, useEffect, useCallback } from "react";
+import {
+ Clock,
+ Calendar,
+ Timer,
+ Globe,
+ Copy,
+ Check,
+ RefreshCw,
+ Zap,
+ Table as TableIcon,
+ ArrowRight
+} from "lucide-react";
+import {
+ ResizableHandle,
+ ResizablePanel,
+ ResizablePanelGroup
+} from "@/components/ui/resizable";
+import { Button } from "@/components/ui/button";
+import {
+ formatRelativeTime,
+ getDayOfYear,
+ getWeekNumber,
+ toLocalDatetimeString
+} from "@/lib/time-utils";
+import { cn } from "@/lib/utils";
+import { useLocalStorage } from "@/hooks/use-local-storage";
+
+type CopiedField = string | null;
+
+interface CopyButtonProps {
+ value: string;
+ field: string;
+ className?: string;
+ copied: string | null;
+ onCopy: (text: string, field: string) => void;
+}
+
+const CopyButton = ({ value, field, className, copied, onCopy }: CopyButtonProps) => (
+ onCopy(value, field)}
+ className={cn("flex items-center justify-center size-8 rounded-lg hover:bg-zinc-200 dark:hover:bg-zinc-800 text-zinc-400 hover:text-primary transition-all active:scale-90", className)}
+ title="Copy"
+ >
+ {copied === field ? (
+
+ ) : (
+
+ )}
+
+);
+
+interface TableRowProps {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ icon: any;
+ label: string;
+ value: string;
+ field: string;
+ mono?: boolean;
+ isPreferred?: boolean;
+ copied: string | null;
+ onCopy: (text: string, field: string) => void;
+}
+
+const TableRow = ({ icon: Icon, label, value, field, mono = true, isPreferred = false, copied, onCopy }: TableRowProps) => (
+
+
+
+
+ {label}
+
+
+
+ {value}
+
+
+
+
+
+);
+
+export function EpochConverter() {
+ // Settings
+ const [prefTimeZone] = useLocalStorage('timeZone', 'UTC');
+ const [prefTimeFormat] = useLocalStorage('timeFormat', 'seconds');
+
+ // State
+ const [epochInput, setEpochInput] = useState("");
+ const [dateInput, setDateInput] = useState("");
+ const [liveEpoch, setLiveEpoch] = useState(() => Math.floor(Date.now() / 1000));
+ const [copied, setCopied] = useState(null);
+
+ // Live clock effect
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setLiveEpoch(Math.floor(Date.now() / 1000));
+ }, 1000);
+ return () => clearInterval(interval);
+ }, []);
+
+ const copyToClipboard = useCallback((text: string, field: string) => {
+ navigator.clipboard.writeText(text);
+ setCopied(field);
+ setTimeout(() => setCopied(null), 1500);
+ }, []);
+
+ const copyAsTable = (date: Date, field: string) => {
+ const table = `| Format | Value |\n| --- | --- |\n| GMT / UTC | ${date.toUTCString()} |\n| Local Time | ${date.toLocaleString()} |\n| ISO 8601 | ${date.toISOString()} |\n| Relative | ${formatRelativeTime(date)} |`;
+ navigator.clipboard.writeText(table);
+ setCopied(field);
+ setTimeout(() => setCopied(null), 1500);
+ };
+
+ // Derived from epochInput
+ const { parsedDate, epochError, isMillis } = React.useMemo(() => {
+ if (!epochInput.trim()) {
+ return { parsedDate: null, epochError: "", isMillis: false };
+ }
+ const num = Number(epochInput.trim());
+ if (isNaN(num)) {
+ return { parsedDate: null, epochError: "Enter a valid number", isMillis: false };
+ }
+ const isMs = epochInput.trim().length > 10;
+ const ms = isMs ? num : num * 1000;
+ const d = new Date(ms);
+ if (isNaN(d.getTime())) {
+ return { parsedDate: null, epochError: "Invalid timestamp", isMillis: isMs };
+ }
+ return { parsedDate: d, epochError: "", isMillis: isMs };
+ }, [epochInput]);
+
+ // Derived from dateInput
+ const { dateEpochSeconds, dateEpochMillis } = React.useMemo(() => {
+ if (!dateInput) {
+ return { dateEpochSeconds: null, dateEpochMillis: null };
+ }
+ const d = new Date(dateInput);
+ if (isNaN(d.getTime())) {
+ return { dateEpochSeconds: null, dateEpochMillis: null };
+ }
+ return {
+ dateEpochSeconds: Math.floor(d.getTime() / 1000),
+ dateEpochMillis: d.getTime()
+ };
+ }, [dateInput]);
+
+ const setNow = () => {
+ const now = Date.now();
+ const value = prefTimeFormat === 'millis' ? now : Math.floor(now / 1000);
+ setEpochInput(String(value));
+ };
+
+ const setDateNow = () => {
+ setDateInput(toLocalDatetimeString(new Date()));
+ };
+
+
+ return (
+
+ {/* Header section with Live Epoch */}
+
+
+
+
+
+
Time & Date
+
+
Professional epoch converter and time utility
+
+
+
+
copyToClipboard(String(liveEpoch), "live")}
+ >
+
+
+
+ {liveEpoch}
+
+
+ {new Date(liveEpoch * 1000).toUTCString().split(' ').slice(0, 5).join(' ')} UTC
+
+
+
+ {copied === "live" ? : }
+
+
+
+
+
+
+
+
+
+ {/* Epoch to Date */}
+
+
+
+
+
+
+
+ {parsedDate && (
+
copyAsTable(parsedDate, "table-epoch")}
+ >
+ {copied === "table-epoch" ? : }
+ Copy Table
+
+ )}
+
+
+
+
+
setEpochInput(e.target.value)}
+ placeholder="Enter epoch (e.g. 1711206600)"
+ className="w-full h-10 px-4 rounded-lg bg-muted/20 border border-transparent focus:border-primary/30 outline-none transition-all font-mono text-xs placeholder:text-muted-foreground/40 shadow-inner"
+ />
+ {isMillis && epochInput && !epochError && (
+
+
+ MS
+
+ )}
+
+
+ Now
+
+
+ {epochError &&
{epochError}
}
+
+ {parsedDate ? (
+
+
+
+
+ Day of Year
+ {getDayOfYear(parsedDate)}
+
+
+ Week Number
+ {getWeekNumber(parsedDate)}
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+ {/* Date to Epoch */}
+
+
+
+
+
+
+
+ {dateEpochSeconds && (
+
copyAsTable(new Date(dateEpochSeconds * 1000), "table-date")}
+ >
+ {copied === "table-date" ? : }
+ Copy Table
+
+ )}
+
+
+
+ setDateInput(e.target.value)}
+ className="flex-1 h-10 px-4 rounded-lg bg-muted/20 border border-transparent focus:border-primary/30 outline-none transition-all font-mono text-xs [color-scheme:light] dark:[color-scheme:dark] shadow-inner"
+ />
+
+ Now
+
+
+
+ {dateEpochSeconds ? (
+
+
+
+
+
+
+ Verification
+
+
+
+ UTC Date
+ {new Date(dateEpochSeconds * 1000).toUTCString()}
+
+
+ ISO Format
+ {new Date(dateEpochSeconds * 1000).toISOString()}
+
+
+
+
+ ) : (
+
+ )}
+
+
+
+ {/* Extra Utilities Box */}
+
+
+
+ Quick Tips
+
+
+
+ •
+ Automatic millisecond detection for inputs > 10 digits.
+
+
+ •
+ Epoch time starts from January 1st, 1970 (UTC).
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/time/index.ts b/components/time/index.ts
new file mode 100644
index 0000000..fd93f64
--- /dev/null
+++ b/components/time/index.ts
@@ -0,0 +1 @@
+export * from "./TimePage";
diff --git a/components/ui/resizable.tsx b/components/ui/resizable.tsx
index 29c67a0..af76aa6 100644
--- a/components/ui/resizable.tsx
+++ b/components/ui/resizable.tsx
@@ -6,10 +6,15 @@ import { Group, Panel, Separator } from "react-resizable-panels"
import { cn } from "@/lib/utils"
+export interface ResizablePanelGroupProps extends React.ComponentProps {
+ className?: string
+ direction: "horizontal" | "vertical"
+}
+
function ResizablePanelGroup({
className,
...props
-}: React.ComponentProps) {
+}: ResizablePanelGroupProps) {
return (
{
+ className?: string
+}
+
function ResizablePanel({
className,
...props
-}: React.ComponentProps) {
+}: ResizablePanelProps) {
return (
+
+
+ );
+}
diff --git a/components/view/index.ts b/components/view/index.ts
new file mode 100644
index 0000000..8db8c1c
--- /dev/null
+++ b/components/view/index.ts
@@ -0,0 +1 @@
+export * from "./ViewPage";
diff --git a/components/view/viewer-container.tsx b/components/view/viewer-container.tsx
index 5c0416e..1aacf3a 100644
--- a/components/view/viewer-container.tsx
+++ b/components/view/viewer-container.tsx
@@ -1,11 +1,11 @@
"use client";
-import React, { useState, useRef } from 'react';
+import React, { useState, useMemo } from 'react';
import { useRouter } from 'next/navigation';
import { HTMLViewer } from '@/components/shared/html-viewer';
import { CodeViewer } from '@/components/shared/code-viewer';
import { TableViewer } from '@/components/shared/table-viewer';
-import { Format, ContainerProps } from '@/types';
+import { ContainerProps } from '@/types';
import yaml from 'js-yaml';
import * as prettier from 'prettier/standalone';
import * as prettierPluginHtml from 'prettier/plugins/html';
@@ -15,25 +15,25 @@ import * as prettierPluginEstree from 'prettier/plugins/estree';
import * as prettierPluginMarkdown from 'prettier/plugins/markdown';
import { format as formatSql } from 'sql-formatter';
import { Button } from "@/components/ui/button";
+import Editor from '@monaco-editor/react';
+import { useTheme } from 'next-themes';
import {
Code2,
Eye,
- Layout,
Table as TableIcon,
Maximize2,
ExternalLink,
PanelLeftClose,
PanelLeftOpen,
+ Settings,
AlignLeft,
Copy,
FileEdit,
ChevronDown,
- Zap,
- Layers,
Download,
Trash2,
- ListOrdered,
- Type
+ Type,
+ Layout as LayoutIcon
} from "lucide-react";
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
@@ -43,57 +43,41 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
-
+import {
+ ResizableHandle,
+ ResizablePanel,
+ ResizablePanelGroup,
+} from "@/components/ui/resizable";
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import { useEditor } from '@/lib/hooks/use-editor';
+import { useLocalStorage } from '@/hooks/use-local-storage';
import { ALL_FORMATS, PREVIEWABLE_FORMATS, getLanguage } from '@/lib/formats';
+import { Separator } from '@/components/ui/separator';
export function ViewerContainer({ initialContent, initialFormat }: ContainerProps) {
const router = useRouter();
- const [content, setContent] = useState(initialContent);
- const [format, setFormat] = useState(initialFormat);
+ const { resolvedTheme } = useTheme();
+ const { content, setContent, format, setFormat } = useEditor({
+ initialContent,
+ initialFormat,
+ });
+
const [activeTab, setActiveTab] = useState("preview");
const [isFullscreen, setIsFullscreen] = useState(false);
const [useBootstrap, setUseBootstrap] = useState(true);
const [useTailwind, setUseTailwind] = useState(true);
- const [enableJS, setEnableJS] = useState(true);
const [showEditor, setShowEditor] = useState(true);
- const [showLineNumbers, setShowLineNumbers] = useState(true);
- const [wordWrap, setWordWrap] = useState(false);
- const codeViewerRef = useRef(null);
- const textareaRef = useRef(null);
-
- const [prevInitialContent, setPrevInitialContent] = useState(initialContent);
- const [prevInitialFormat, setPrevInitialFormat] = useState(initialFormat);
-
- React.useEffect(() => {
- if (typeof window !== 'undefined') {
- const saved = sessionStorage.getItem(`web-viewer-content-${format}`);
- if (saved) {
- setContent(saved);
- }
- }
- }, [format]);
-
- if (initialContent !== prevInitialContent || initialFormat !== prevInitialFormat) {
- setPrevInitialContent(initialContent);
- setPrevInitialFormat(initialFormat);
-
- // Only update content from props if we don't have something in sessionStorage for this format
- let hasSaved = false;
- if (typeof window !== 'undefined') {
- hasSaved = !!sessionStorage.getItem(`web-viewer-content-${initialFormat}`);
- }
-
- if (!hasSaved) {
- setContent(initialContent);
- }
- setFormat(initialFormat);
- }
+
+ // Editor Settings from LocalStorage
+ const [prefFontSize, setPrefFontSize] = useLocalStorage('editorFontSize', 14);
+ const [prefTabSize, setPrefTabSize] = useLocalStorage('editorTabSize', 4);
+ const [prefWordWrap, setPrefWordWrap] = useLocalStorage('editorWordWrap', 'off');
+
+ const [fileName, setFileName] = useState(`view.${getLanguage(initialFormat)}`);
- const handleScroll = (e: React.UIEvent) => {
- if (codeViewerRef.current) {
- codeViewerRef.current.scrollTop = e.currentTarget.scrollTop;
- codeViewerRef.current.scrollLeft = e.currentTarget.scrollLeft;
- }
+ const handleWordWrapToggle = () => {
+ const newVal = prefWordWrap === "on" ? "off" : "on";
+ setPrefWordWrap(newVal);
};
const handleClear = () => {
@@ -103,19 +87,18 @@ export function ViewerContainer({ initialContent, initialFormat }: ContainerProp
};
const handleDownload = () => {
- const extension = format === 'react' ? 'tsx' : format;
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
- a.download = `file.${extension}`;
+ a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
- const handleFormat = async () => {
+ const handleAutoFormat = async () => {
try {
if (format === 'json') {
const parsed = JSON.parse(content);
@@ -132,10 +115,9 @@ export function ViewerContainer({ initialContent, initialFormat }: ContainerProp
'javascript': 'babel',
'typescript': 'babel-ts',
'react': 'babel-ts',
- 'markdown': 'markdown',
- 'xml': 'html', // Prettier doesn't have a native XML plugin without extra deps, but HTML often works for basics
+ 'markdown': 'babel',
+ 'xml': 'html',
};
-
const parser = parserMap[format];
if (parser) {
const formatted = await prettier.format(content, {
@@ -155,56 +137,50 @@ export function ViewerContainer({ initialContent, initialFormat }: ContainerProp
}
}
} catch (e) {
- console.error('Formatting error:', e);
+ console.error(e);
}
};
- let formattedContent = content;
- try {
- if (format === 'json') formattedContent = JSON.stringify(JSON.parse(content), null, 2);
- else if (format === 'yaml') formattedContent = yaml.dump(yaml.load(content));
- } catch (e) {
- console.error(e);
- formattedContent = content;
- }
-
- let tableData: Record[] | null = null;
- try {
- if (format === 'json') {
- const parsed = JSON.parse(content);
- tableData = Array.isArray(parsed) ? parsed : null;
- }
- if (format === 'csv') {
- const lines = content.trim().split('\n');
- if (lines.length >= 2) {
- const headers = lines[0].split(',');
- tableData = lines.slice(1).map((line: string) => {
- const values = line.split(',');
- return headers.reduce((acc: Record, header: string, i: number) => ({ ...acc, [header]: values[i] }), {});
- });
- }
+ const formattedContent = useMemo(() => {
+ try {
+ if (format === 'json') return JSON.stringify(JSON.parse(content), null, 2);
+ if (format === 'yaml') return yaml.dump(yaml.load(content));
+ } catch {
+ return content;
}
- } catch (e) {
- console.error(e);
- tableData = null;
- }
+ return content;
+ }, [content, format]);
- const handleTableDataChange = (newData: Record[]) => {
- if (format === 'json') setContent(JSON.stringify(newData, null, 2));
- else if (format === 'csv' && newData.length > 0) {
- const headers = Object.keys(newData[0]);
- setContent([headers.join(','), ...newData.map(row => headers.map(h => row[h]).join(','))].join('\n'));
+ const tableData = useMemo(() => {
+ try {
+ if (format === 'json') {
+ const parsed = JSON.parse(content);
+ return Array.isArray(parsed) ? parsed : null;
+ }
+ if (format === 'csv') {
+ const lines = content.trim().split('\n');
+ if (lines.length >= 2) {
+ const headers = lines[0].split(',');
+ return lines.slice(1).map((line: string) => {
+ const values = line.split(',');
+ return headers.reduce((acc: Record, header: string, i: number) => ({ ...acc, [header]: values[i] }), {});
+ });
+ }
+ }
+ } catch {
+ return null;
}
- };
+ return null;
+ }, [content, format]);
- const canEditTable = (format === 'json' && Array.isArray(tableData)) || format === 'csv';
+ const canPreviewTable = (format === 'json' && Array.isArray(tableData)) || format === 'csv';
const openFullPage = () => {
const htmlContent = `
- Preview - Web Utils Pro
+ Preview - Web Utils
@@ -222,239 +198,257 @@ export function ViewerContainer({ initialContent, initialFormat }: ContainerProp
window.open(url, '_blank');
};
- const jumpToEditor = () => {
- router.push('/editor');
- };
-
return (
-
- {/* Top Toolbar */}
-
-
-
-
-
-
- {format}
-
-
-
-
- {ALL_FORMATS.slice(0, 10).map((fmt) => (
- setFormat(fmt)} className="text-[10px] font-bold uppercase tracking-wider">
- {fmt}
-
- ))}
-
-
-
-
+
+ {/* Minimalist Top Toolbar */}
+
+
+
+
+
+ {format}
+
+
+
+
+ {ALL_FORMATS.slice(0, 10).map((fmt) => (
+ setFormat(fmt)} className="text-[10px] font-bold uppercase">
+ {fmt}
+
+ ))}
+
+
-
- Jump to Editor
-
-
+
-
setShowEditor(!showEditor)}
- title={showEditor ? "Close Source Code" : "Open Source Code"}
- >
- {showEditor ? : }
-
-
setIsFullscreen(!isFullscreen)}>
-
-
+
+
+
+
setFileName(e.target.value)}
+ className="bg-transparent border-none outline-none text-[11px] font-bold text-foreground min-w-[120px] placeholder:text-muted-foreground/50"
+ spellCheck={false}
+ />
-
- {/* Split Editor/Preview */}
-
- {/* Left: Syntax Highlighted Editor */}
- {showEditor && (
-
-
-
-
- Live Editor
-
-
- {format === 'html' && (
-
-
setUseBootstrap(!useBootstrap)}
- className={`flex items-center gap-1.5 px-2 py-1 rounded-md text-[10px] font-bold uppercase tracking-wider transition-all border ${useBootstrap ? "bg-indigo-50 dark:bg-indigo-900/20 text-indigo-600 border-indigo-200 dark:border-indigo-800" : "text-zinc-400 border-transparent hover:bg-zinc-100 dark:hover:bg-zinc-800"}`}
- >
- Bootstrap
-
-
setUseTailwind(!useTailwind)}
- className={`flex items-center gap-1.5 px-2 py-1 rounded-md text-[10px] font-bold uppercase tracking-wider transition-all border ${useTailwind ? "bg-cyan-50 dark:bg-cyan-900/20 text-cyan-600 border-cyan-200 dark:border-cyan-800" : "text-zinc-400 border-transparent hover:bg-zinc-100 dark:hover:bg-zinc-800"}`}
- >
- Tailwind
-
-
-
setEnableJS(!enableJS)}
- className={`flex items-center gap-1.5 px-2 py-1 rounded-md text-[10px] font-bold uppercase tracking-wider transition-all border ${enableJS ? "bg-yellow-50 dark:bg-yellow-900/20 text-yellow-600 border-yellow-200 dark:border-yellow-800" : "text-zinc-400 border-transparent hover:bg-zinc-100 dark:hover:bg-zinc-800"}`}
- >
- JavaScript
-
-
- )}
-
+
+
setShowEditor(!showEditor)}
+ >
+ {showEditor ? : }
+
+
+
+
+
+
+
+
+
+
+
+ Font Size
+
- setShowLineNumbers(!showLineNumbers)}
- className={`flex items-center gap-1.5 px-2 py-1 rounded-md text-[10px] font-bold uppercase tracking-wider transition-all border ${showLineNumbers ? "bg-indigo-50 dark:bg-indigo-900/20 text-indigo-600 border-indigo-200 dark:border-indigo-800" : "text-zinc-400 border-transparent hover:bg-zinc-100 dark:hover:bg-zinc-800"}`}
- title="Toggle Line Numbers"
- >
- Lines
-
- setWordWrap(!wordWrap)}
- className={`flex items-center gap-1.5 px-2 py-1 rounded-md text-[10px] font-bold uppercase tracking-wider transition-all border ${wordWrap ? "bg-indigo-50 dark:bg-indigo-900/20 text-indigo-600 border-indigo-200 dark:border-indigo-800" : "text-zinc-400 border-transparent hover:bg-zinc-100 dark:hover:bg-zinc-800"}`}
- title="Toggle Word Wrap"
- >
- Wrap
-
+ setPrefFontSize(Number(e.target.value))}
+ className="flex-1 h-1 bg-muted rounded-full appearance-none cursor-pointer accent-primary"
+ />
+ {prefFontSize}
-
-
-
-
- Format
-
-
navigator.clipboard.writeText(content)}
- className="flex items-center gap-1 px-2 py-1 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded text-zinc-400 hover:text-indigo-500 transition-colors"
- title="Copy Code"
- >
-
- Copy
-
-
-
- Save
-
-
-
- Clear
-
+
+
+
+ Tab Size
+
+
+ {[2, 4, 8].map(size => (
+ setPrefTabSize(size)}
+ >
+ {size}
+
+ ))}
-
-
-
-
-
-
- )}
-
- {/* Right: Correct Preview */}
-
-
-
-
setActiveTab("preview")} className={`px-4 py-1.5 rounded-md text-[10px] font-bold uppercase tracking-wider transition-all flex items-center gap-2 ${activeTab === "preview" ? "bg-white dark:bg-zinc-700 text-indigo-600 shadow-sm" : "text-zinc-500"}`}>
- Preview
-
+
+
+ Word Wrap
+
+
+ {prefWordWrap === 'on' ? 'Enabled' : 'Disabled'}
+
+
+
+
- {canEditTable && (
-
setActiveTab("interactive")} className={`px-4 py-1.5 rounded-md text-[10px] font-bold uppercase tracking-wider transition-all flex items-center gap-2 ${activeTab === "interactive" ? "bg-white dark:bg-zinc-700 text-indigo-600 shadow-sm" : "text-zinc-500"}`}>
- Data Table
-
- )}
-
-
-
- Full Page
-
-
-
+
setIsFullscreen(!isFullscreen)}>
+
+
+
+
-
-
- {activeTab === "preview" && (
-
- {format === 'html' &&
}
- {format === 'json' &&
}
- {format === 'markdown' && (
-
-
{content}
+ {/* Split UI with Resizable Panels */}
+
+
+ {showEditor && (
+ <>
+
+
+
+
+
+ Source
- )}
- {format === 'svg' && (
-
- )}
- {format === 'csv' &&
}
- {!PREVIEWABLE_FORMATS.includes(format) && (
-
-
-
No Preview Format Available
+
+
router.push('/editor')} className="size-7" title="Open in Editor">
+
+
+
+
+
+
+
+
+
+
+
navigator.clipboard.writeText(content)} className="size-7">
+
+
+
+
+
+
+
+
- )}
+
+
+ setContent(value || "")}
+ theme={resolvedTheme === 'dark' ? 'vs-dark' : 'light'}
+ options={{
+ minimap: { enabled: false },
+ fontSize: prefFontSize,
+ tabSize: prefTabSize,
+ wordWrap: prefWordWrap as "on" | "off",
+ automaticLayout: true,
+ padding: { top: 16 },
+ lineNumbersMinChars: 3,
+ scrollBeyondLastLine: false,
+ }}
+ />
+
- )}
- {activeTab === "interactive" &&
}
+
+
+ >
+ )}
+
+
+
+
+
+
+
+ Preview
+
+ {canPreviewTable && (
+
+ Data
+
+ )}
+
+
+
+ {format === 'html' && (
+
+ setUseBootstrap(!useBootstrap)}
+ >
+ BS
+
+ setUseTailwind(!useTailwind)}
+ >
+ TW
+
+
+ )}
+
+ Full Page
+
+
+
+
+
+
+ {format === 'html' &&
}
+ {format === 'json' &&
}
+ {format === 'markdown' && (
+
+ {content}
+
+ )}
+ {format === 'svg' && (
+
+ )}
+ {format === 'csv' &&
{
+ const headers = Object.keys(newData[0]);
+ setContent([headers.join(','), ...newData.map(row => headers.map(h => row[h]).join(','))].join('\n'));
+ }} /> }
+ {!PREVIEWABLE_FORMATS.includes(format) && (
+
+ )}
+
+
+
+
+ {
+ if (format === 'json') setContent(JSON.stringify(newData, null, 2));
+ else if (format === 'csv') {
+ const headers = Object.keys(newData[0]);
+ setContent([headers.join(','), ...newData.map(row => headers.map(h => row[h]).join(','))].join('\n'));
+ }
+ }} />
+
+
-
-
+
+
-
-
);
}
diff --git a/components/workspace/workspace-container.tsx b/components/workspace/workspace-container.tsx
new file mode 100644
index 0000000..5cf9d6f
--- /dev/null
+++ b/components/workspace/workspace-container.tsx
@@ -0,0 +1,478 @@
+"use client";
+
+import React, { useState, useRef, useMemo } from 'react';
+import { Button } from "@/components/ui/button";
+import Editor, { OnMount } from '@monaco-editor/react';
+import { useTheme } from 'next-themes';
+import {
+ FileEdit,
+ Terminal as TerminalIcon,
+ Table as TableIcon,
+ Code,
+ Eye,
+ Copy as CopyIcon,
+ Braces,
+ Layout,
+ ChevronDown,
+ Maximize2,
+ PanelLeftClose,
+ PanelLeftOpen,
+ Settings,
+ Type as TypeIcon,
+ Code2,
+ Download,
+ Trash
+} from "lucide-react";
+import { TableViewer } from '@/components/shared/table-viewer';
+import { HTMLViewer } from '@/components/shared/html-viewer';
+import { CodeViewer } from '@/components/shared/code-viewer';
+import { JsonTreeViewer } from '@/components/json/tree-viewer';
+import { ContainerProps, Format } from '@/types';
+import yaml from 'js-yaml';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {
+ ResizableHandle,
+ ResizablePanel,
+ ResizablePanelGroup,
+} from "@/components/ui/resizable";
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import { useEditor } from '@/lib/hooks/use-editor';
+import { useLocalStorage } from '@/hooks/use-local-storage';
+import { ALL_FORMATS, getLanguage } from '@/lib/formats';
+import { Separator } from '@/components/ui/separator';
+import { format as formatSql } from 'sql-formatter';
+import * as prettier from 'prettier/standalone';
+import * as prettierPluginHtml from 'prettier/plugins/html';
+import * as prettierPluginPostcss from 'prettier/plugins/postcss';
+import * as prettierPluginBabel from 'prettier/plugins/babel';
+import * as prettierPluginEstree from 'prettier/plugins/estree';
+import * as prettierPluginMarkdown from 'prettier/plugins/markdown';
+
+export function WorkspaceContainer({ initialContent, initialFormat }: ContainerProps) {
+ const { resolvedTheme } = useTheme();
+ const { content, setContent, format, setFormat, isSaved, setIsSaved } = useEditor({
+ initialContent,
+ initialFormat,
+ });
+
+ // Shell State
+ const [terminalInput, setTerminalInput] = useState('');
+ const [terminalOutput, setTerminalOutput] = useState
([
+ 'system: workspace activated',
+ 'system: terminal ready'
+ ]);
+
+ // UI State
+ const [fileName, setFileName] = useState(`index.${getLanguage(initialFormat)}`);
+
+
+ // Editor Settings from LocalStorage
+ const [prefFontSize, setPrefFontSize] = useLocalStorage('editorFontSize', 14);
+ const [prefTabSize, setPrefTabSize] = useLocalStorage('editorTabSize', 4);
+ const [prefWordWrap, setPrefWordWrap] = useLocalStorage('editorWordWrap', 'on');
+
+ const [wordWrap, setWordWrap] = useState<"on" | "off">(prefWordWrap as "on" | "off");
+
+ // Sync local wordWrap with preference when preference changes
+ React.useEffect(() => {
+ setWordWrap(prefWordWrap as "on" | "off");
+ }, [prefWordWrap]);
+
+ const handleWordWrapToggle = () => {
+ const newVal = wordWrap === "on" ? "off" : "on";
+ setWordWrap(newVal);
+ setPrefWordWrap(newVal);
+ };
+
+ const [showEditor, setShowEditor] = useState(true);
+ const [isFullscreen, setIsFullscreen] = useState(false);
+ const [activeTab, setActiveTab] = useState("preview");
+ const [cursorPos, setCursorPos] = useState({ line: 1, col: 1 });
+ const editorRef = useRef(null); // eslint-disable-line @typescript-eslint/no-explicit-any
+
+ // Preview Settings
+ const [useBootstrap] = useState(true);
+ const [useTailwind] = useState(true);
+
+ const handleEditorDidMount: OnMount = (editor) => {
+ editorRef.current = editor;
+ editor.onDidChangeCursorPosition((e) => {
+ setCursorPos({
+ line: e.position.lineNumber,
+ col: e.position.column
+ });
+ });
+ };
+
+ const handleFormatChange = (newFormat: Format) => {
+ setFormat(newFormat);
+ const namePart = fileName.substring(0, fileName.lastIndexOf('.')) || fileName;
+ setFileName(`${namePart}.${getLanguage(newFormat)}`);
+ };
+
+ const handleSave = () => {
+ const blob = new Blob([content], { type: 'text/plain' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = fileName;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ setIsSaved?.(true);
+ };
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(content);
+ };
+
+ const handleAutoFormat = async () => {
+ try {
+ if (format === 'json') {
+ const parsed = JSON.parse(content);
+ setContent(JSON.stringify(parsed, null, 2));
+ } else if (format === 'yaml') {
+ const parsed = yaml.load(content);
+ setContent(yaml.dump(parsed));
+ } else if (format === 'sql') {
+ setContent(formatSql(content));
+ } else {
+ const parserMap: Record = {
+ 'html': 'html',
+ 'css': 'css',
+ 'javascript': 'babel',
+ 'typescript': 'babel-ts',
+ 'react': 'babel-ts',
+ 'markdown': 'markdown',
+ 'xml': 'html',
+ };
+ const parser = parserMap[format];
+ if (parser) {
+ const formatted = await prettier.format(content, {
+ parser,
+ plugins: [
+ prettierPluginHtml,
+ prettierPluginPostcss,
+ prettierPluginBabel,
+ prettierPluginEstree,
+ prettierPluginMarkdown
+ ],
+ semi: true,
+ singleQuote: true,
+ tabWidth: 2,
+ });
+ setContent(formatted);
+ }
+ }
+ setIsSaved(true);
+ setTerminalOutput(prev => [...prev, 'system: code formatted successfully']);
+ } catch (e) {
+ console.error(e);
+ setTerminalOutput(prev => [...prev, `error: format failed - ${e instanceof Error ? e.message : 'unknown'}`]);
+ }
+ };
+
+ const handleTerminalSubmit = () => {
+ if (!terminalInput.trim()) return;
+ const cmd = terminalInput.toLowerCase().trim();
+ const newOutput = [...terminalOutput, `$ ${terminalInput}`];
+
+ switch(cmd) {
+ case 'help':
+ newOutput.push('available commands: clear, format, time, stats, info');
+ break;
+ case 'clear':
+ setTerminalOutput([]);
+ setTerminalInput('');
+ return;
+ case 'format':
+ handleAutoFormat();
+ break;
+ case 'time':
+ newOutput.push(`epoch: ${Math.floor(Date.now() / 1000)}`);
+ break;
+ case 'stats':
+ newOutput.push(`chars: ${content.length}`);
+ newOutput.push(`lines: ${content.split('\n').length}`);
+ break;
+ case 'info':
+ newOutput.push(`format: ${format}`);
+ newOutput.push(`file: ${fileName}`);
+ break;
+ default:
+ newOutput.push(`error: command not found: ${cmd}`);
+ }
+ setTerminalOutput(newOutput);
+ setTerminalInput('');
+ };
+
+ const isTabularData = useMemo(() => {
+ try {
+ const parsed = JSON.parse(content);
+ return Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === 'object';
+ } catch {
+ return false;
+ }
+ }, [content]);
+
+ const parsedJson = useMemo(() => {
+ try { return JSON.parse(content); } catch { return null; }
+ }, [content]);
+
+ return (
+
+ {/* Unified Workspace Toolbar */}
+
+
+
+
+
+
+
setFileName(e.target.value)}
+ className="bg-transparent border-none outline-none text-sm font-semibold text-foreground min-w-[140px]"
+ spellCheck={false}
+ />
+ {!isSaved &&
}
+
+
+
+
+
+
+
+ {format}
+
+
+
+
+ {ALL_FORMATS.slice(0, 10).map((fmt) => (
+ handleFormatChange(fmt as Format)} className="text-xs uppercase">
+ {fmt}
+
+ ))}
+
+
+
+
+
+
setShowEditor(!showEditor)}
+ >
+ {showEditor ? : }
+
+
setIsFullscreen(!isFullscreen)}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Font Size
+
+
+ setPrefFontSize(Number(e.target.value))}
+ className="flex-1 h-1 bg-muted rounded-full appearance-none cursor-pointer accent-primary"
+ />
+ {prefFontSize}
+
+
+
+
+ Tab Size
+
+
+ {[2, 4, 8].map(size => (
+ setPrefTabSize(size)}
+ >
+ {size}
+
+ ))}
+
+
+
+
+
+
+
+ {/* Main Split Canvas */}
+
+
+ {showEditor && (
+ <>
+
+
+
+
+
+ Source
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setContent("")} className="size-7 text-muted-foreground hover:text-destructive" title="Clear">
+
+
+
+
+
+
setContent(value || "")}
+ onMount={handleEditorDidMount}
+ theme={resolvedTheme === 'dark' ? 'vs-dark' : 'light'}
+ options={{
+ minimap: { enabled: false },
+ fontSize: prefFontSize,
+ tabSize: prefTabSize,
+ wordWrap: wordWrap,
+ automaticLayout: true,
+ padding: { top: 16 },
+ lineNumbersMinChars: 3,
+ scrollBeyondLastLine: false,
+ }}
+ />
+ {/* Subtle Editor Footer */}
+
+ Line {cursorPos.line}, Col {cursorPos.col}
+ {content.length} characters
+
+
+
+
+
+ >
+ )}
+
+
+
+
+
+
+
+ Preview
+
+
+ Source
+
+ {isTabularData && (
+
+ Data
+
+ )}
+ {format === 'json' && (
+
+ Tree
+
+ )}
+
+ Shell
+
+
+
+
+
+
+ {format === 'html' ? (
+
+ ) : format === 'markdown' ? (
+
+
+
+ ) : format === 'svg' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+ {
+ setContent(JSON.stringify(newData, null, 2));
+ setIsSaved(false);
+ }} />
+
+
+
+
+
+
+
+
+ {terminalOutput.map((line, i) => (
+
+ {line}
+
+ ))}
+
+
+ $
+ setTerminalInput(e.target.value)}
+ onKeyDown={(e) => e.key === 'Enter' && handleTerminalSubmit()}
+ placeholder="type 'help'..."
+ />
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/hooks/use-local-storage.ts b/hooks/use-local-storage.ts
new file mode 100644
index 0000000..64fbe4b
--- /dev/null
+++ b/hooks/use-local-storage.ts
@@ -0,0 +1,37 @@
+import { useState } from "react";
+
+export function useLocalStorage(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {
+ // State to store our value
+ // Pass initial state function to useState so logic is only executed once
+ const [storedValue, setStoredValue] = useState(() => {
+ if (typeof window === "undefined") {
+ return initialValue;
+ }
+ try {
+ const item = window.localStorage.getItem(key);
+ return item ? JSON.parse(item) : initialValue;
+ } catch (error) {
+ console.error(error);
+ return initialValue;
+ }
+ });
+
+ // Return a wrapped version of useState's setter function that
+ // persists the new value to localStorage.
+ const setValue = (value: T | ((val: T) => T)) => {
+ try {
+ // Allow value to be a function so we have same API as useState
+ const valueToStore = value instanceof Function ? value(storedValue) : value;
+ // Save state
+ setStoredValue(valueToStore);
+ // Save to local storage
+ if (typeof window !== "undefined") {
+ window.localStorage.setItem(key, JSON.stringify(valueToStore));
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ return [storedValue, setValue];
+}
diff --git a/lib/constants/tools.ts b/lib/constants/tools.ts
new file mode 100644
index 0000000..1528dd7
--- /dev/null
+++ b/lib/constants/tools.ts
@@ -0,0 +1,204 @@
+import {
+ Box,
+ Braces,
+ Clock,
+ FileCode,
+ FileEdit,
+ Globe,
+ Image,
+ type LucideIcon,
+ Rocket,
+ Shield,
+ StickyNote,
+ Table,
+ PenTool,
+ FilePlus,
+ Bitcoin
+} from 'lucide-react';
+
+export interface Tool {
+ id: string;
+ name: string;
+ description: string;
+ href: string;
+ category: string;
+ status: 'Available' | 'Coming Soon' | 'Beta';
+ icon: LucideIcon;
+}
+
+export interface Category {
+ id: string;
+ label: string;
+ description: string;
+}
+
+export const TOOL_CATEGORIES: Category[] = [
+ {
+ id: "core",
+ label: "Core Workspace",
+ description: "Primary development environments for coding and previewing"
+ },
+ {
+ id: "json",
+ label: "JSON Suite",
+ description: "A comprehensive set of tools to manipulate and format JSON data"
+ },
+ {
+ id: "formats",
+ label: "Document Formats",
+ description: "Format-specific viewers for HTML, XML, CSV, etc."
+ },
+ {
+ id: "time",
+ label: "Time & Date",
+ description: "Comprehensive epoch, timezone, and duration utilities"
+ },
+ {
+ id: "crypto",
+ label: "Cryptography",
+ description: "Hashing, encryption, and secure text processing"
+ },
+ {
+ id: "media",
+ label: "Media & Assets",
+ description: "Converters and optimizers for images, videos, and fonts"
+ }
+];
+
+export const TOOLS: Tool[] = [
+ {
+ id: "editor",
+ name: "Code Editor",
+ description: "Professional Monaco-based editor with syntax highlighting, auto-save, and formatting",
+ href: "/editor",
+ category: "core",
+ status: "Available",
+ icon: FileCode
+ },
+ {
+ id: "ide",
+ name: "Power IDE",
+ description: "Integrated development environment with real-time feedback and advanced debugging",
+ href: "/ide",
+ category: "core",
+ status: "Available",
+ icon: Rocket
+ },
+ {
+ id: "html-preview",
+ name: "HTML Preview",
+ description: "Live preview and editor for HTML documents",
+ href: "/view/html",
+ category: "formats",
+ status: "Available",
+ icon: Globe
+ },
+ {
+ id: "json-formatter",
+ name: "JSON Formatter",
+ description: "Clean up and validate messy JSON strings instantly",
+ href: "/view/json",
+ category: "json",
+ status: "Available",
+ icon: Braces
+ },
+ {
+ id: "yaml-view",
+ name: "YAML Viewer",
+ description: "Validate and visualize YAML configuration files",
+ href: "/view/yaml",
+ category: "formats",
+ status: "Available",
+ icon: StickyNote
+ },
+ {
+ id: "react-code",
+ name: "React Playground",
+ description: "Write and preview React components in the browser",
+ href: "/view/react",
+ category: "core",
+ status: "Available",
+ icon: Box
+ },
+ {
+ id: "markdown-view",
+ name: "Markdown Editor",
+ description: "Write and preview Markdown with GitHub-flavored support",
+ href: "/view/markdown",
+ category: "formats",
+ status: "Available",
+ icon: FileEdit
+ },
+ {
+ id: "xml-viewer",
+ name: "XML Viewer",
+ description: "Format and inspect XML documents",
+ href: "/view/xml",
+ category: "formats",
+ status: "Available",
+ icon: FileCode
+ },
+ {
+ id: "svg-render",
+ name: "SVG Renderer",
+ description: "Live preview for SVG graphics and icons",
+ href: "/view/svg",
+ category: "media",
+ status: "Available",
+ icon: Image
+ },
+ {
+ id: "csv-viewer",
+ name: "CSV Viewer",
+ description: "View and filter comma-separated values as tables",
+ href: "/view/csv",
+ category: "formats",
+ status: "Available",
+ icon: Table
+ },
+ {
+ id: "epoch-converter",
+ name: "Epoch Converter",
+ description: "Convert between Unix timestamps and human-readable dates",
+ href: "/time",
+ category: "time",
+ status: "Available",
+ icon: Clock
+ },
+ {
+ id: "uuid-generator",
+ name: "UUID Generator",
+ description: "Generate v4 UUIDs for your applications and tests",
+ href: "/crypto",
+ category: "crypto",
+ status: "Available",
+ icon: Shield
+ },
+ {
+ id: "draw-tool",
+ name: "Quick Draw",
+ description: "Simple canvas-based sketching and drawing tool for quick ideas",
+ href: "/draw",
+ category: "media",
+ status: "Available",
+ icon: PenTool
+ },
+ {
+ id: "dummy-file",
+ name: "Dummy File Generator",
+ description: "Create placeholder files of any size or type for testing",
+ href: "/dummy",
+ category: "core",
+ status: "Available",
+ icon: FilePlus
+ },
+ {
+ id: "blockchain-tool",
+ name: "Blockchain Inspector",
+ description: "Analyze wallet addresses, transactions, and block data across major chains",
+ href: "/crypto/blockchain",
+ category: "crypto",
+ status: "Available",
+ icon: Bitcoin
+ }
+];
diff --git a/lib/hooks/use-editor.ts b/lib/hooks/use-editor.ts
new file mode 100644
index 0000000..650c372
--- /dev/null
+++ b/lib/hooks/use-editor.ts
@@ -0,0 +1,56 @@
+import { useState, useEffect, useCallback } from 'react';
+import { Format } from '@/types';
+
+interface UseEditorOptions {
+ initialContent: string;
+ initialFormat: Format;
+ debounceMs?: number;
+}
+
+export function useEditor({ initialContent, initialFormat, debounceMs = 500 }: UseEditorOptions) {
+ const [content, setContent] = useState(initialContent);
+ const [format, setFormat] = useState(initialFormat);
+ const [isSaved, setIsSaved] = useState(true);
+
+ // Sync to props without useEffect to avoid cascading renders
+ const [prevInitialContent, setPrevInitialContent] = useState(initialContent);
+ const [prevInitialFormat, setPrevInitialFormat] = useState(initialFormat);
+
+ if (initialContent !== prevInitialContent || initialFormat !== prevInitialFormat) {
+ setPrevInitialContent(initialContent);
+ setPrevInitialFormat(initialFormat);
+ setContent(initialContent);
+ setFormat(initialFormat);
+ setIsSaved(true);
+ }
+
+ // Debounced sessionStorage persistence
+ useEffect(() => {
+ if (typeof window === 'undefined') return;
+
+ const timer = setTimeout(() => {
+ sessionStorage.setItem(`web-viewer-content-${format}`, content);
+ }, debounceMs);
+
+ return () => clearTimeout(timer);
+ }, [content, format, debounceMs]);
+
+ const updateContent = useCallback((newContent: string) => {
+ setContent(newContent);
+ setIsSaved(false);
+ }, []);
+
+ const updateFormat = useCallback((newFormat: Format) => {
+ setFormat(newFormat);
+ setIsSaved(false);
+ }, []);
+
+ return {
+ content,
+ setContent: updateContent,
+ format,
+ setFormat: updateFormat,
+ isSaved,
+ setIsSaved
+ };
+}
diff --git a/lib/time-utils.ts b/lib/time-utils.ts
new file mode 100644
index 0000000..c7798d4
--- /dev/null
+++ b/lib/time-utils.ts
@@ -0,0 +1,47 @@
+export function formatRelativeTime(date: Date): string {
+ const now = new Date();
+ const diffMs = now.getTime() - date.getTime();
+ const absDiff = Math.abs(diffMs);
+ const isFuture = diffMs < 0;
+ const prefix = isFuture ? "in " : "";
+ const suffix = isFuture ? "" : " ago";
+
+ const seconds = Math.floor(absDiff / 1000);
+ const minutes = Math.floor(seconds / 60);
+ const hours = Math.floor(minutes / 60);
+ const days = Math.floor(hours / 24);
+ const months = Math.floor(days / 30);
+ const years = Math.floor(days / 365);
+
+ if (seconds < 60) return `${prefix}${seconds} second${seconds !== 1 ? "s" : ""}${suffix}`;
+ if (minutes < 60) return `${prefix}${minutes} minute${minutes !== 1 ? "s" : ""}${suffix}`;
+ if (hours < 24) return `${prefix}${hours} hour${hours !== 1 ? "s" : ""}${suffix}`;
+ if (days < 30) return `${prefix}${days} day${days !== 1 ? "s" : ""}${suffix}`;
+ if (months < 12) return `${prefix}${months} month${months !== 1 ? "s" : ""}${suffix}`;
+ return `${prefix}${years} year${years !== 1 ? "s" : ""}${suffix}`;
+}
+
+export function getDayOfYear(date: Date): number {
+ const start = new Date(date.getFullYear(), 0, 0);
+ const diff = date.getTime() - start.getTime();
+ const oneDay = 1000 * 60 * 60 * 24;
+ return Math.floor(diff / oneDay);
+}
+
+export function getWeekNumber(date: Date): number {
+ const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
+ const dayNum = d.getUTCDay() || 7;
+ d.setUTCDate(d.getUTCDate() + 4 - dayNum);
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
+ return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
+}
+
+export function toLocalDatetimeString(date: Date): string {
+ const y = date.getFullYear();
+ const m = String(date.getMonth() + 1).padStart(2, "0");
+ const d = String(date.getDate()).padStart(2, "0");
+ const h = String(date.getHours()).padStart(2, "0");
+ const min = String(date.getMinutes()).padStart(2, "0");
+ const s = String(date.getSeconds()).padStart(2, "0");
+ return `${y}-${m}-${d}T${h}:${min}:${s}`;
+}
diff --git a/next.config.ts b/next.config.ts
index e9ffa30..e5540e8 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,7 +1,10 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
- /* config options here */
+ output: 'export',
+ images: {
+ unoptimized: true,
+ },
};
export default nextConfig;
diff --git a/package-lock.json b/package-lock.json
index d0ebea6..1a89663 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,7 +8,9 @@
"name": "web-utils",
"version": "0.1.0",
"dependencies": {
+ "@gsap/react": "^2.1.2",
"@hookform/resolvers": "^5.2.2",
+ "@monaco-editor/react": "^4.7.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.8",
@@ -42,10 +44,11 @@
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
+ "gsap": "^3.15.0",
"input-otp": "^1.4.2",
"js-yaml": "^4.1.1",
"lucide-react": "^0.563.0",
- "next": "16.1.4",
+ "next": "^16.2.3",
"next-themes": "^0.4.6",
"prettier": "^3.8.1",
"react": "19.2.3",
@@ -64,15 +67,20 @@
"zod": "^4.3.6"
},
"devDependencies": {
+ "@eslint/js": "^9.21.0",
"@tailwindcss/postcss": "^4",
+ "@types/hast": "^3.0.4",
+ "@types/mdast": "^4.0.4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
- "eslint": "^9",
- "eslint-config-next": "16.1.4",
+ "@types/unist": "^3.0.3",
+ "eslint": "^9.21.0",
+ "eslint-config-next": "^16.1.4",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
- "typescript": "^5"
+ "typescript": "^5.9.3",
+ "typescript-eslint": "^8.58.2"
}
},
"node_modules/@alloc/quick-lru": {
@@ -119,7 +127,6 @@
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.28.6",
@@ -420,15 +427,15 @@
}
},
"node_modules/@eslint/config-array": {
- "version": "0.21.1",
- "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
- "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
+ "version": "0.21.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
+ "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/object-schema": "^2.1.7",
"debug": "^4.3.1",
- "minimatch": "^3.1.2"
+ "minimatch": "^3.1.5"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -461,20 +468,20 @@
}
},
"node_modules/@eslint/eslintrc": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
- "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
+ "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ajv": "^6.12.4",
+ "ajv": "^6.14.0",
"debug": "^4.3.2",
"espree": "^10.0.1",
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.1",
- "minimatch": "^3.1.2",
+ "minimatch": "^3.1.5",
"strip-json-comments": "^3.1.1"
},
"engines": {
@@ -485,9 +492,9 @@
}
},
"node_modules/@eslint/js": {
- "version": "9.39.2",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
- "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
+ "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -559,6 +566,16 @@
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
+ "node_modules/@gsap/react": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@gsap/react/-/react-2.1.2.tgz",
+ "integrity": "sha512-JqliybO1837UcgH2hVOM4VO+38APk3ECNrsuSM4MuXp+rbf+/2IG2K1YJiqfTcXQHH7XlA0m3ykniFYstfq0Iw==",
+ "license": "SEE LICENSE AT https://gsap.com/standard-license",
+ "peerDependencies": {
+ "gsap": "^3.12.5",
+ "react": ">=17"
+ }
+ },
"node_modules/@hookform/resolvers": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz",
@@ -1139,6 +1156,29 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@monaco-editor/loader": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz",
+ "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==",
+ "license": "MIT",
+ "dependencies": {
+ "state-local": "^1.0.6"
+ }
+ },
+ "node_modules/@monaco-editor/react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz",
+ "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@monaco-editor/loader": "^1.5.0"
+ },
+ "peerDependencies": {
+ "monaco-editor": ">= 0.25.0 < 1",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
@@ -1153,9 +1193,9 @@
}
},
"node_modules/@next/env": {
- "version": "16.1.4",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.4.tgz",
- "integrity": "sha512-gkrXnZyxPUy0Gg6SrPQPccbNVLSP3vmW8LU5dwEttEEC1RwDivk8w4O+sZIjFvPrSICXyhQDCG+y3VmjlJf+9A==",
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.3.tgz",
+ "integrity": "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@@ -1169,9 +1209,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "16.1.4",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.4.tgz",
- "integrity": "sha512-T8atLKuvk13XQUdVLCv1ZzMPgLPW0+DWWbHSQXs0/3TjPrKNxTmUIhOEaoEyl3Z82k8h/gEtqyuoZGv6+Ugawg==",
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.3.tgz",
+ "integrity": "sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg==",
"cpu": [
"arm64"
],
@@ -1185,9 +1225,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "16.1.4",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.4.tgz",
- "integrity": "sha512-AKC/qVjUGUQDSPI6gESTx0xOnOPQ5gttogNS3o6bA83yiaSZJek0Am5yXy82F1KcZCx3DdOwdGPZpQCluonuxg==",
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.3.tgz",
+ "integrity": "sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ==",
"cpu": [
"x64"
],
@@ -1201,9 +1241,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "16.1.4",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.4.tgz",
- "integrity": "sha512-POQ65+pnYOkZNdngWfMEt7r53bzWiKkVNbjpmCt1Zb3V6lxJNXSsjwRuTQ8P/kguxDC8LRkqaL3vvsFrce4dMQ==",
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.3.tgz",
+ "integrity": "sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q==",
"cpu": [
"arm64"
],
@@ -1217,9 +1257,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "16.1.4",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.4.tgz",
- "integrity": "sha512-3Wm0zGYVCs6qDFAiSSDL+Z+r46EdtCv/2l+UlIdMbAq9hPJBvGu/rZOeuvCaIUjbArkmXac8HnTyQPJFzFWA0Q==",
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.3.tgz",
+ "integrity": "sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==",
"cpu": [
"arm64"
],
@@ -1233,9 +1273,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "version": "16.1.4",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.4.tgz",
- "integrity": "sha512-lWAYAezFinaJiD5Gv8HDidtsZdT3CDaCeqoPoJjeB57OqzvMajpIhlZFce5sCAH6VuX4mdkxCRqecCJFwfm2nQ==",
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.3.tgz",
+ "integrity": "sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ==",
"cpu": [
"x64"
],
@@ -1249,9 +1289,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "version": "16.1.4",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.4.tgz",
- "integrity": "sha512-fHaIpT7x4gA6VQbdEpYUXRGyge/YbRrkG6DXM60XiBqDM2g2NcrsQaIuj375egnGFkJow4RHacgBOEsHfGbiUw==",
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.3.tgz",
+ "integrity": "sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw==",
"cpu": [
"x64"
],
@@ -1265,9 +1305,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "16.1.4",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.4.tgz",
- "integrity": "sha512-MCrXxrTSE7jPN1NyXJr39E+aNFBrQZtO154LoCz7n99FuKqJDekgxipoodLNWdQP7/DZ5tKMc/efybx1l159hw==",
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.3.tgz",
+ "integrity": "sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==",
"cpu": [
"arm64"
],
@@ -1281,9 +1321,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "16.1.4",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.4.tgz",
- "integrity": "sha512-JSVlm9MDhmTXw/sO2PE/MRj+G6XOSMZB+BcZ0a7d6KwVFZVpkHcb2okyoYFBaco6LeiL53BBklRlOrDDbOeE5w==",
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.3.tgz",
+ "integrity": "sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw==",
"cpu": [
"x64"
],
@@ -3409,7 +3449,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz",
"integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -3420,7 +3459,6 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -3434,6 +3472,14 @@
"@types/react": "*"
}
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true
+ },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -3441,20 +3487,20 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.53.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz",
- "integrity": "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==",
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz",
+ "integrity": "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
- "@typescript-eslint/scope-manager": "8.53.1",
- "@typescript-eslint/type-utils": "8.53.1",
- "@typescript-eslint/utils": "8.53.1",
- "@typescript-eslint/visitor-keys": "8.53.1",
+ "@typescript-eslint/scope-manager": "8.58.2",
+ "@typescript-eslint/type-utils": "8.58.2",
+ "@typescript-eslint/utils": "8.58.2",
+ "@typescript-eslint/visitor-keys": "8.58.2",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
- "ts-api-utils": "^2.4.0"
+ "ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3464,9 +3510,9 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^8.53.1",
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <6.0.0"
+ "@typescript-eslint/parser": "^8.58.2",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
@@ -3480,17 +3526,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.53.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.1.tgz",
- "integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==",
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.2.tgz",
+ "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
- "@typescript-eslint/scope-manager": "8.53.1",
- "@typescript-eslint/types": "8.53.1",
- "@typescript-eslint/typescript-estree": "8.53.1",
- "@typescript-eslint/visitor-keys": "8.53.1",
+ "@typescript-eslint/scope-manager": "8.58.2",
+ "@typescript-eslint/types": "8.58.2",
+ "@typescript-eslint/typescript-estree": "8.58.2",
+ "@typescript-eslint/visitor-keys": "8.58.2",
"debug": "^4.4.3"
},
"engines": {
@@ -3501,19 +3546,19 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <6.0.0"
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/project-service": {
- "version": "8.53.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.1.tgz",
- "integrity": "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==",
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.2.tgz",
+ "integrity": "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.53.1",
- "@typescript-eslint/types": "^8.53.1",
+ "@typescript-eslint/tsconfig-utils": "^8.58.2",
+ "@typescript-eslint/types": "^8.58.2",
"debug": "^4.4.3"
},
"engines": {
@@ -3524,18 +3569,18 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "typescript": ">=4.8.4 <6.0.0"
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.53.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.1.tgz",
- "integrity": "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==",
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz",
+ "integrity": "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.53.1",
- "@typescript-eslint/visitor-keys": "8.53.1"
+ "@typescript-eslint/types": "8.58.2",
+ "@typescript-eslint/visitor-keys": "8.58.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3546,9 +3591,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.53.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.1.tgz",
- "integrity": "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==",
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz",
+ "integrity": "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3559,21 +3604,21 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "typescript": ">=4.8.4 <6.0.0"
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.53.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.1.tgz",
- "integrity": "sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w==",
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz",
+ "integrity": "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.53.1",
- "@typescript-eslint/typescript-estree": "8.53.1",
- "@typescript-eslint/utils": "8.53.1",
+ "@typescript-eslint/types": "8.58.2",
+ "@typescript-eslint/typescript-estree": "8.58.2",
+ "@typescript-eslint/utils": "8.58.2",
"debug": "^4.4.3",
- "ts-api-utils": "^2.4.0"
+ "ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3583,14 +3628,14 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <6.0.0"
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.53.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.1.tgz",
- "integrity": "sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==",
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz",
+ "integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3602,21 +3647,21 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.53.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.1.tgz",
- "integrity": "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==",
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz",
+ "integrity": "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.53.1",
- "@typescript-eslint/tsconfig-utils": "8.53.1",
- "@typescript-eslint/types": "8.53.1",
- "@typescript-eslint/visitor-keys": "8.53.1",
+ "@typescript-eslint/project-service": "8.58.2",
+ "@typescript-eslint/tsconfig-utils": "8.58.2",
+ "@typescript-eslint/types": "8.58.2",
+ "@typescript-eslint/visitor-keys": "8.58.2",
"debug": "^4.4.3",
- "minimatch": "^9.0.5",
+ "minimatch": "^10.2.2",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
- "ts-api-utils": "^2.4.0"
+ "ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3626,39 +3671,52 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "typescript": ">=4.8.4 <6.0.0"
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+ "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "balanced-match": "^1.0.0"
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dev": true,
- "license": "ISC",
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "brace-expansion": "^2.0.1"
+ "brace-expansion": "^5.0.5"
},
"engines": {
- "node": ">=16 || 14 >=14.17"
+ "node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
- "version": "7.7.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
- "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -3669,16 +3727,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.53.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.1.tgz",
- "integrity": "sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg==",
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz",
+ "integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
- "@typescript-eslint/scope-manager": "8.53.1",
- "@typescript-eslint/types": "8.53.1",
- "@typescript-eslint/typescript-estree": "8.53.1"
+ "@typescript-eslint/scope-manager": "8.58.2",
+ "@typescript-eslint/types": "8.58.2",
+ "@typescript-eslint/typescript-estree": "8.58.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3688,19 +3746,19 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <6.0.0"
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.53.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.1.tgz",
- "integrity": "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==",
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz",
+ "integrity": "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.53.1",
- "eslint-visitor-keys": "^4.2.1"
+ "@typescript-eslint/types": "8.58.2",
+ "eslint-visitor-keys": "^5.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3710,6 +3768,19 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/@ungap/structured-clone": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
@@ -3986,12 +4057,11 @@
]
},
"node_modules/acorn": {
- "version": "8.15.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
- "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4010,9 +4080,9 @@
}
},
"node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4301,18 +4371,21 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.9.18",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz",
- "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==",
+ "version": "2.10.19",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz",
+ "integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==",
"license": "Apache-2.0",
"bin": {
- "baseline-browser-mapping": "dist/cli.js"
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
}
},
"node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
+ "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4353,7 +4426,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -4973,6 +5045,16 @@
"csstype": "^3.0.2"
}
},
+ "node_modules/dompurify": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
+ "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "peer": true,
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -4999,8 +5081,7 @@
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/embla-carousel-react": {
"version": "8.6.0",
@@ -5246,26 +5327,25 @@
}
},
"node_modules/eslint": {
- "version": "9.39.2",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
- "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
+ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
- "@eslint/config-array": "^0.21.1",
+ "@eslint/config-array": "^0.21.2",
"@eslint/config-helpers": "^0.4.2",
"@eslint/core": "^0.17.0",
- "@eslint/eslintrc": "^3.3.1",
- "@eslint/js": "9.39.2",
+ "@eslint/eslintrc": "^3.3.5",
+ "@eslint/js": "9.39.4",
"@eslint/plugin-kit": "^0.4.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2",
"@types/estree": "^1.0.6",
- "ajv": "^6.12.4",
+ "ajv": "^6.14.0",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.6",
"debug": "^4.3.2",
@@ -5284,7 +5364,7 @@
"is-glob": "^4.0.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"lodash.merge": "^4.6.2",
- "minimatch": "^3.1.2",
+ "minimatch": "^3.1.5",
"natural-compare": "^1.4.0",
"optionator": "^0.9.3"
},
@@ -5437,7 +5517,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -5834,9 +5913,9 @@
}
},
"node_modules/flatted": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
- "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true,
"license": "ISC"
},
@@ -6067,6 +6146,12 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/gsap": {
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz",
+ "integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==",
+ "license": "Standard 'no charge' license: https://gsap.com/standard-license."
+ },
"node_modules/has-bigints": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
@@ -7266,9 +7351,9 @@
}
},
"node_modules/lodash": {
- "version": "4.17.23",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
- "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash.merge": {
@@ -7353,6 +7438,19 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/marked": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
+ "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -8233,9 +8331,9 @@
}
},
"node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -8255,6 +8353,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/monaco-editor": {
+ "version": "0.55.1",
+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
+ "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "dompurify": "3.2.7",
+ "marked": "14.0.0"
+ }
+ },
"node_modules/moo": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
@@ -8331,14 +8440,14 @@
}
},
"node_modules/next": {
- "version": "16.1.4",
- "resolved": "https://registry.npmjs.org/next/-/next-16.1.4.tgz",
- "integrity": "sha512-gKSecROqisnV7Buen5BfjmXAm7Xlpx9o2ueVQRo5DxQcjC8d330dOM1xiGWc2k3Dcnz0In3VybyRPOsudwgiqQ==",
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/next/-/next-16.2.3.tgz",
+ "integrity": "sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==",
"license": "MIT",
"dependencies": {
- "@next/env": "16.1.4",
+ "@next/env": "16.2.3",
"@swc/helpers": "0.5.15",
- "baseline-browser-mapping": "^2.8.3",
+ "baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
@@ -8350,15 +8459,15 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
- "@next/swc-darwin-arm64": "16.1.4",
- "@next/swc-darwin-x64": "16.1.4",
- "@next/swc-linux-arm64-gnu": "16.1.4",
- "@next/swc-linux-arm64-musl": "16.1.4",
- "@next/swc-linux-x64-gnu": "16.1.4",
- "@next/swc-linux-x64-musl": "16.1.4",
- "@next/swc-win32-arm64-msvc": "16.1.4",
- "@next/swc-win32-x64-msvc": "16.1.4",
- "sharp": "^0.34.4"
+ "@next/swc-darwin-arm64": "16.2.3",
+ "@next/swc-darwin-x64": "16.2.3",
+ "@next/swc-linux-arm64-gnu": "16.2.3",
+ "@next/swc-linux-arm64-musl": "16.2.3",
+ "@next/swc-linux-x64-gnu": "16.2.3",
+ "@next/swc-linux-x64-musl": "16.2.3",
+ "@next/swc-win32-arm64-msvc": "16.2.3",
+ "@next/swc-win32-x64-msvc": "16.2.3",
+ "sharp": "^0.34.5"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
@@ -8690,9 +8799,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -8851,7 +8960,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -8882,7 +8990,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -8895,7 +9002,6 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz",
"integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -9645,6 +9751,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/state-local": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
+ "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
+ "license": "MIT"
+ },
"node_modules/stop-iteration-iterator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@@ -9949,12 +10061,11 @@
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -9996,9 +10107,9 @@
}
},
"node_modules/ts-api-utils": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
- "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -10147,7 +10258,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -10157,16 +10267,16 @@
}
},
"node_modules/typescript-eslint": {
- "version": "8.53.1",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.1.tgz",
- "integrity": "sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==",
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.2.tgz",
+ "integrity": "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/eslint-plugin": "8.53.1",
- "@typescript-eslint/parser": "8.53.1",
- "@typescript-eslint/typescript-estree": "8.53.1",
- "@typescript-eslint/utils": "8.53.1"
+ "@typescript-eslint/eslint-plugin": "8.58.2",
+ "@typescript-eslint/parser": "8.58.2",
+ "@typescript-eslint/typescript-estree": "8.58.2",
+ "@typescript-eslint/utils": "8.58.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -10176,8 +10286,8 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <6.0.0"
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/unbox-primitive": {
@@ -10624,7 +10734,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/package.json b/package.json
index e27390e..c14965f 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,9 @@
"lint": "eslint"
},
"dependencies": {
+ "@gsap/react": "^2.1.2",
"@hookform/resolvers": "^5.2.2",
+ "@monaco-editor/react": "^4.7.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.8",
@@ -43,10 +45,11 @@
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
+ "gsap": "^3.15.0",
"input-otp": "^1.4.2",
"js-yaml": "^4.1.1",
"lucide-react": "^0.563.0",
- "next": "16.1.4",
+ "next": "^16.2.3",
"next-themes": "^0.4.6",
"prettier": "^3.8.1",
"react": "19.2.3",
@@ -65,14 +68,19 @@
"zod": "^4.3.6"
},
"devDependencies": {
+ "@eslint/js": "^9.21.0",
"@tailwindcss/postcss": "^4",
+ "@types/hast": "^3.0.4",
+ "@types/mdast": "^4.0.4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
- "eslint": "^9",
- "eslint-config-next": "16.1.4",
+ "@types/unist": "^3.0.3",
+ "eslint": "^9.21.0",
+ "eslint-config-next": "^16.1.4",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
- "typescript": "^5"
+ "typescript": "^5.9.3",
+ "typescript-eslint": "^8.58.2"
}
}
diff --git a/scratch/check-panels.ts b/scratch/check-panels.ts
new file mode 100644
index 0000000..f6b9b9e
--- /dev/null
+++ b/scratch/check-panels.ts
@@ -0,0 +1,2 @@
+import * as panels from 'react-resizable-panels';
+console.log(Object.keys(panels));
diff --git a/types/index.ts b/types/index.ts
index 1c52358..b0aa013 100644
--- a/types/index.ts
+++ b/types/index.ts
@@ -1,2 +1,2 @@
export * from "./format";
-export * from "./container";
+export * from "./props/container";
diff --git a/types/container.ts b/types/props/container.ts
similarity index 71%
rename from types/container.ts
rename to types/props/container.ts
index ec612e1..3ea674b 100644
--- a/types/container.ts
+++ b/types/props/container.ts
@@ -1,4 +1,4 @@
-import { Format } from "./format";
+import { Format } from "../format";
export interface ContainerProps {
initialContent: string;
diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml
new file mode 100644
index 0000000..154f739
--- /dev/null
+++ b/wasm/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "wasm"
+version = "0.1.0"
+edition = "2024"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+wasm-bindgen = "0.2"
diff --git a/wasm/loader.ts b/wasm/loader.ts
new file mode 100644
index 0000000..e69de29
diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs
new file mode 100644
index 0000000..f3bb77c
--- /dev/null
+++ b/wasm/src/lib.rs
@@ -0,0 +1,11 @@
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen]
+pub fn greet(name: &str) -> String {
+ format!("Hello, {}! This is from WASM.", name)
+}
+
+#[wasm_bindgen]
+pub fn add(a: i32, b: i32) -> i32 {
+ a + b
+}