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`,
- 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 }} />