/* Command palette ⌘K. */ import { LitElement, html, css } from 'lit'; import { customElement, state, query } from 'lit/decorators.js'; import { toast } from './nv-toast'; import { openModal } from './nv-modal'; import { getClient, theme, expectedWitness, witnessHex, witnessVerified, pushLog, running, } from '../store/appStore'; interface Cmd { ico: string; label: string; kbd?: string; run: () => void; } @customElement('nv-palette') export class NvPalette extends LitElement { @state() private open = false; @state() private filter = ''; @state() private idx = 0; @query('#palette-input') private inputEl!: HTMLInputElement; static styles = css` :host { position: fixed; inset: 0; z-index: 220; background: rgba(0,0,0,0.5); opacity: 0; pointer-events: none; transition: opacity 0.15s; display: flex; justify-content: center; padding-top: 12vh; backdrop-filter: blur(4px); } :host([open]) { opacity: 1; pointer-events: auto; } .palette { width: min(560px, 92vw); background: var(--bg-1); border: 1px solid var(--line-2); border-radius: var(--radius); box-shadow: 0 30px 80px -20px rgba(0,0,0,0.7); overflow: hidden; display: flex; flex-direction: column; max-height: 60vh; } .input { padding: 14px 16px; border-bottom: 1px solid var(--line); } input { width: 100%; background: transparent; border: none; outline: none; color: var(--ink); font-size: 14px; font-family: inherit; } .list { flex: 1; overflow-y: auto; padding: 4px; } .item { display: flex; align-items: center; gap: 10px; padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 12.5px; } .item.active { background: var(--bg-3); } .item .ico { width: 20px; text-align: center; color: var(--accent); } .item .lbl { flex: 1; } .item .kbd { font-family: var(--mono); font-size: 10.5px; color: var(--ink-3); padding: 1px 5px; background: var(--bg-3); border-radius: 4px; } `; private cmds: Cmd[] = [ { ico: '▶', label: 'Run pipeline', kbd: 'Space', run: async () => { await getClient()?.run(); running.value = true; toast('Pipeline running', '▶'); } }, { ico: '❚', label: 'Pause pipeline', run: async () => { await getClient()?.pause(); running.value = false; toast('Paused', '❚❚'); } }, { ico: '+', label: 'New scene…', kbd: '⌘N', run: () => openModal({ title: 'New scene', body: `
Build a fresh magnetic scene. The dashboard generates the JSON and pushes it to the running pipeline (or you can copy the JSON for offline use).
`, buttons: [ { label: 'Cancel', variant: 'ghost' }, { label: 'Create', variant: 'primary', onClick: async () => { const root = document.querySelector('nv-app')?.shadowRoot?.querySelector('nv-modal')?.shadowRoot; if (!root) return; const name = (root.querySelectorClears the frame stream and rewinds t to 0.
⌘K / Ctrl KSpace⌘R⌘,⌘/\`1 · 2 · 3Esc/nvsim is a deterministic, byte-reproducible forward simulator for nitrogen-vacancy diamond magnetometry.
This dashboard runs nvsim as WASM in a Web Worker. Same (scene, config, seed) → byte-identical SHA-256 witness across runs and machines.
License: MIT OR Apache-2.0 · See ADR-089, ADR-092.
`, buttons: [{ label: 'Close', variant: 'primary' }], }) }, ]; override connectedCallback(): void { super.connectedCallback(); window.addEventListener('keydown', this.onKey); window.addEventListener('nv-palette', this.onOpen as EventListener); } override disconnectedCallback(): void { super.disconnectedCallback(); window.removeEventListener('keydown', this.onKey); window.removeEventListener('nv-palette', this.onOpen as EventListener); } private onKey = (e: KeyboardEvent): void => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); this.openPal(); } else if (e.key === 'Escape' && this.open) { this.closePal(); } else if (this.open) { if (e.key === 'ArrowDown') { this.idx = Math.min(this.cmds.length - 1, this.idx + 1); e.preventDefault(); } else if (e.key === 'ArrowUp') { this.idx = Math.max(0, this.idx - 1); e.preventDefault(); } else if (e.key === 'Enter') { this.runIdx(); e.preventDefault(); } } }; private onOpen = (): void => this.openPal(); private openPal(): void { this.open = true; this.setAttribute('open', ''); this.filter = ''; this.idx = 0; setTimeout(() => this.inputEl?.focus(), 0); } private closePal(): void { this.open = false; this.removeAttribute('open'); } private filtered(): Cmd[] { if (!this.filter.trim()) return this.cmds; const q = this.filter.toLowerCase(); return this.cmds.filter((c) => c.label.toLowerCase().includes(q)); } private runIdx(): void { const f = this.filtered(); const c = f[this.idx]; if (c) { c.run(); this.closePal(); } } override render() { const items = this.filtered(); return html`