diff --git a/app/components/monaco-editor.tsx b/app/components/monaco-editor.tsx index d5537ce..ef08506 100644 --- a/app/components/monaco-editor.tsx +++ b/app/components/monaco-editor.tsx @@ -117,7 +117,7 @@ export default function MonacoEditor({ snippetSuggestions: "top", emptySelectionClipboard: false, copyWithSyntaxHighlighting: true, - multiCursorModifier: "alt", + multiCursorModifier: "ctrlCmd", accessibilitySupport: "auto", quickSuggestions: { other: true, diff --git a/app/page.tsx b/app/page.tsx index d3c5c18..827fe07 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -7,7 +7,7 @@ const safeBase64Decode = (str: string) => decodeURIComponent(escape(atob(str))); import type React from "react" -import { useState, useEffect, useRef } from "react" +import { useState, useEffect, useRef, useMemo } from "react" import { Button } from "@/components/ui/button" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" @@ -24,8 +24,13 @@ import { Moon, Link as LinkIcon, Timer, + Search, + ChevronsUp, + ChevronsDown, + MousePointerClick, } from "lucide-react" import { toast } from "sonner" +import { CommandPalette, type Command } from "@/components/ui/command-palette" @@ -1045,17 +1050,6 @@ document.head.appendChild(floatStyle);`, }, }, - { - id: "interactive-card", - name: "Interactive Card", - description: "Animated card component", - icon: , - content: { - html: `Card

Interactive Card

Active

Hover over me!

42Projects
1.2kUsers
`, - css: `body{margin:0;min-height:100vh;background:linear-gradient(135deg,#1e3c72,#2a5298);font-family:'Segoe UI',sans-serif;display:flex;align-items:center;justify-content:center}.card{width:350px;background:rgba(255,255,255,0.1);backdrop-filter:blur(10px);border-radius:20px;padding:2rem;color:white;border:1px solid rgba(255,255,255,0.2);transition:all 0.3s ease;cursor:pointer}.card:hover{transform:translateY(-10px);box-shadow:0 20px 40px rgba(0,0,0,0.3)}.card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1.5rem}.status{background:#4ade80;padding:.25rem .75rem;border-radius:20px;font-size:.8rem}.stats{display:flex;gap:2rem;margin-bottom:1.5rem}.stat-number{display:block;font-size:2rem;font-weight:bold;color:#4ade80}.card-footer button{width:100%;padding:.75rem;background:#4ade80;color:#1f2937;border:none;border-radius:10px;cursor:pointer;font-weight:600}`, - javascript: `function handleAction(){const card=document.getElementById('card');card.style.animation='pulse 0.6s';setTimeout(()=>{alert('Action!');card.style.animation=''},600)}const s=document.createElement('style');s.textContent='@keyframes pulse{0%{transform:scale(1)}50%{transform:scale(1.05)}100%{transform:scale(1)}}';document.head.appendChild(s)`, - }, - }, { id: "todo-app", name: "Todo App", @@ -1101,7 +1095,13 @@ export default function CodeEditor() { const [activeTab, setActiveTab] = useState("html") const [theme, setTheme] = useState<"light" | "dark">("light") + const [paletteOpen, setPaletteOpen] = useState(false) const previewRef = useRef(null) + // Points to the editor of the currently visible tab (Radix unmounts inactive tabs). + const activeEditorRef = useRef<{ + focus: () => void + getAction: (id: string) => { run: () => void | Promise } | null + } | null>(null) useEffect(() => { const savedTheme = localStorage.getItem("theme") as "light" | "dark" | null @@ -1187,8 +1187,113 @@ export default function CodeEditor() { localStorage.setItem("theme", next) } + // Global Ctrl/Cmd+K to toggle the command palette. Capture phase so it beats + // Monaco's own keybindings while an editor is focused. + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { + e.preventDefault() + e.stopPropagation() + setPaletteOpen((o) => !o) + } + } + window.addEventListener("keydown", onKeyDown, true) + return () => window.removeEventListener("keydown", onKeyDown, true) + }, []) + + const commands = useMemo(() => { + const tabCmd = ( + id: string, + label: string, + tab: keyof CodeContent, + icon: React.ReactNode, + ): Command => ({ + id, + label, + group: "Editor", + icon, + keywords: tab, + perform: () => setActiveTab(tab), + }) + + const cursorCmd = ( + id: string, + label: string, + description: string, + actionId: string, + icon: React.ReactNode, + ): Command => ({ + id, + label, + description, + group: "Multi-cursor", + icon, + keywords: "cursor multi multiple selection caret", + perform: () => { + const ed = activeEditorRef.current + if (!ed) return + ed.focus() + ed.getAction(actionId)?.run() + }, + }) + + return [ + tabCmd("go-html", "Go to HTML", "html", ), + tabCmd("go-css", "Go to CSS", "css", ), + tabCmd("go-js", "Go to JavaScript", "javascript", ), + + cursorCmd("mc-above", "Add cursor above", "Ctrl+Alt+↑", "editor.action.insertCursorAbove", ), + cursorCmd("mc-below", "Add cursor below", "Ctrl+Alt+↓", "editor.action.insertCursorBelow", ), + cursorCmd("mc-next", "Add next occurrence to selection", "Ctrl+D", "editor.action.addSelectionToNextFindMatch", ), + cursorCmd("mc-all", "Select all occurrences", "Ctrl+Shift+L", "editor.action.selectHighlights", ), + + { + id: "download", + label: "Download project", + description: "ZIP", + group: "Actions", + icon: , + keywords: "export zip save", + perform: () => { + void downloadCode() + }, + }, + { + id: "share", + label: "Copy shareable link", + group: "Actions", + icon: , + keywords: "url copy", + perform: () => { + void copyShareLink() + }, + }, + { + id: "theme", + label: "Switch theme", + description: theme === "light" ? "Dark" : "Light", + group: "Actions", + icon: theme === "light" ? : , + keywords: "dark light mode appearance", + perform: () => toggleTheme(), + }, + ...templates.map( + (t): Command => ({ + id: `tpl-${t.id}`, + label: t.name, + group: "Templates", + icon: , + keywords: "template load example", + perform: () => loadTemplate(t.id), + }), + ), + ] + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [theme]) + return ( +
@@ -1206,6 +1311,19 @@ export default function CodeEditor() {
+ @@ -1224,13 +1342,13 @@ export default function CodeEditor() {
- handleCodeChange("html", v)} theme={theme} /> + handleCodeChange("html", v)} theme={theme} onEditorReady={(ed) => { if (ed) activeEditorRef.current = ed }} /> - handleCodeChange("css", v)} theme={theme} /> + handleCodeChange("css", v)} theme={theme} onEditorReady={(ed) => { if (ed) activeEditorRef.current = ed }} /> - handleCodeChange("javascript", v)} theme={theme} /> + handleCodeChange("javascript", v)} theme={theme} onEditorReady={(ed) => { if (ed) activeEditorRef.current = ed }} />