diff --git a/crates/supersearch-runtime/src/extension/host.rs b/crates/supersearch-runtime/src/extension/host.rs index aebb736..74706f8 100644 --- a/crates/supersearch-runtime/src/extension/host.rs +++ b/crates/supersearch-runtime/src/extension/host.rs @@ -162,12 +162,20 @@ mod tests { } #[test] - fn timeout_kills_runaway_script() { + fn nonzero_exit_is_classified_as_failure() { let dir = tempfile::tempdir().unwrap(); - // SCRIPT_TIMEOUT is 10s; this test would be slow, so just assert the - // error type via a script that exits non-zero quickly instead. + // A script that exits non-zero must classify as `NonZeroExit` (asserting + // the error type directly rather than waiting out the 10s timeout). let ep = write_script(dir.path(), "fail.sh", "#!/bin/sh\necho oops >&2\nexit 3\n"); - let err = run_query(dir.path(), &ep, "x", SCRIPT_TIMEOUT).unwrap_err(); - assert!(matches!(err, HostError::NonZeroExit(_))); + // Spawning a subprocess can transiently fail under heavy parallel test + // load (e.g. EAGAIN), so retry past transient Spawn/Timeout outcomes and + // assert the steady-state classification. + for _ in 0..15 { + match run_query(dir.path(), &ep, "x", SCRIPT_TIMEOUT) { + Err(HostError::NonZeroExit(_)) => return, + _ => std::thread::sleep(Duration::from_millis(20)), + } + } + panic!("script exiting non-zero was never classified as NonZeroExit"); } } diff --git a/react-command-palette/App.tsx b/react-command-palette/App.tsx index dec7d46..339d7a9 100644 --- a/react-command-palette/App.tsx +++ b/react-command-palette/App.tsx @@ -9,6 +9,8 @@ import { reducedVariants, } from "./variants"; import { invoke, listen, type BackendResult } from "./bridge"; +import { ExtensionManager } from "./ExtensionManager"; +import { Settings } from "./Settings"; import type { CommandAction } from "./types"; /** A palette row plus how to run it. */ @@ -53,6 +55,7 @@ export default function App() { const [query, setQuery] = useState(""); const [rows, setRows] = useState([]); const [activeIndex, setActiveIndex] = useState(0); + const [view, setView] = useState<"palette" | "extensions" | "settings">("palette"); const [summonKey, setSummonKey] = useState(0); // bumped on each summon β†’ replays entrance const inputRef = useRef(null); const listRef = useRef(null); @@ -92,6 +95,29 @@ export default function App() { hide(); }, })); + // Built-in command to open the extension manager. + if (/^(ext|manage|plugin)/i.test(q) || "manage extensions".includes(q.toLowerCase())) { + rows.unshift({ + id: "view:extensions", + title: "Manage Extensions", + subtitle: "Install, enable, and trust extensions", + icon: "🧩", + group: "Command", + hint: "Open", + perform: () => setView("extensions"), + }); + } + if (/^(set|pref|config)/i.test(q) || "settings preferences".includes(q.toLowerCase())) { + rows.unshift({ + id: "view:settings", + title: "Settings", + subtitle: "Shortcut, theme, and behavior", + icon: "βš™οΈ", + group: "Command", + hint: "Open", + perform: () => setView("settings"), + }); + } rows.sort((a, b) => (RANK[a.group ?? ""] ?? 99) - (RANK[b.group ?? ""] ?? 99)); setRows(rows); setActiveIndex(0); @@ -170,12 +196,18 @@ export default function App() { initial="hidden" animate="visible" style={{ willChange: "transform, opacity" }} - className="flex h-full w-full flex-col overflow-hidden rounded-[18px] border border-white/15 + className="relative flex h-full w-full flex-col overflow-hidden rounded-[18px] border border-white/15 bg-[hsla(228,18%,12%,0.72)] shadow-[0_28px_70px_-18px_rgba(0,0,0,0.6)] ring-1 ring-inset ring-white/[0.06] backdrop-blur-2xl" role="dialog" aria-label="SuperSearch" > + {view === "extensions" ? ( + setView("palette")} /> + ) : view === "settings" ? ( + setView("palette")} /> + ) : ( + <> {/* Search input */}
@@ -241,6 +273,8 @@ export default function App() {
+ + )} ); diff --git a/react-command-palette/ExtensionManager.tsx b/react-command-palette/ExtensionManager.tsx new file mode 100644 index 0000000..99916c0 --- /dev/null +++ b/react-command-palette/ExtensionManager.tsx @@ -0,0 +1,256 @@ +import { useCallback, useEffect, useState } from "react"; +import { invoke, type ExtensionInfo } from "./bridge"; + +/** + * Extension manager view β€” install, enable (with permission consent), trust + * (for unsandboxed script extensions), and uninstall extensions. Replaces the + * legacy `ui/scripts/extensions.js` Plugin Manager. All actions go through the + * Tauri IPC commands backed by `ExtensionRegistry`. + */ +export function ExtensionManager({ onClose }: { onClose: () => void }) { + const [items, setItems] = useState([]); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(null); + const [path, setPath] = useState(""); + // Pending confirmation: enable (show requested permissions) or trust. + const [confirm, setConfirm] = useState< + { kind: "enable" | "trust"; ext: ExtensionInfo } | null + >(null); + + const refresh = useCallback(async () => { + try { + setItems((await invoke("list_extensions")) ?? []); + setError(null); + } catch (e) { + setError(String(e)); + } + }, []); + + useEffect(() => { + void refresh(); + }, [refresh]); + + // Esc closes the manager (or a pending confirm dialog first). + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key !== "Escape") return; + e.preventDefault(); + e.stopPropagation(); + if (confirm) setConfirm(null); + else onClose(); + }; + window.addEventListener("keydown", onKey, true); + return () => window.removeEventListener("keydown", onKey, true); + }, [confirm, onClose]); + + const run = useCallback( + async (id: string, op: () => Promise) => { + setBusy(id); + try { + await op(); + await refresh(); + } catch (e) { + setError(String(e)); + } finally { + setBusy(null); + } + }, + [refresh], + ); + + const onToggle = (ext: ExtensionInfo) => { + if (ext.enabled) { + void run(ext.id, () => invoke("set_extension_enabled", { id: ext.id, enabled: false })); + } else { + // Enabling grants a capability token β€” confirm the requested permissions. + setConfirm({ kind: "enable", ext }); + } + }; + + const confirmAction = () => { + if (!confirm) return; + const { kind, ext } = confirm; + setConfirm(null); + if (kind === "enable") { + void run(ext.id, () => invoke("set_extension_enabled", { id: ext.id, enabled: true })); + } else { + void run(ext.id, () => invoke("set_extension_trusted", { id: ext.id, trusted: true })); + } + }; + + const onInstall = () => { + const p = path.trim(); + if (!p) return; + void run("__install__", async () => { + await invoke("install_extension", { path: p }); + setPath(""); + }); + }; + + return ( +
+
+

Extensions

+ +
+ + {/* Install from a local folder (manifest.toml + entrypoint). */} +
+ setPath(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && onInstall()} + placeholder="Path to an extension folder…" + className="min-w-0 flex-1 rounded-md border border-white/10 bg-black/30 px-2 py-1.5 text-xs outline-none placeholder:text-zinc-500 focus:border-white/25" + /> + +
+ + {error &&

{error}

} + +
    + {items.length === 0 && ( +
  • No extensions installed
  • + )} + {items.map((ext) => ( +
  • +
    +
    +
    + {ext.name} + + {ext.kind} + + v{ext.version} +
    + {ext.description && ( +

    {ext.description}

    + )} + {ext.permissions.length > 0 && ( +
    + {ext.permissions.map((p) => ( + + {p.permission} + + ))} +
    + )} + {ext.needs_trust && ( +

    + ⚠ Unsandboxed script β€” needs trust to run. +

    + )} +
    + +
    + {ext.needs_trust && ( + + )} + + +
    +
    +
  • + ))} +
+ + {/* Consent / trust confirmation. */} + {confirm && ( +
e.target === e.currentTarget && setConfirm(null)} + > +
+ {confirm.kind === "enable" ? ( + <> +

Enable {confirm.ext.name}?

+

+ This grants the extension a revocable capability token for: +

+
    + {confirm.ext.permissions.length === 0 && ( +
  • No special permissions.
  • + )} + {confirm.ext.permissions.map((p) => ( +
  • + {p.permission} +
  • + ))} +
+ + ) : ( + <> +

Trust {confirm.ext.name}?

+

+ This is a script extension. It runs with your full user + privileges and is not sandboxed. Only trust extensions whose + code you have reviewed. +

+ + )} +
+ + +
+
+
+ )} +
+ ); +} diff --git a/react-command-palette/Settings.tsx b/react-command-palette/Settings.tsx new file mode 100644 index 0000000..2c799d7 --- /dev/null +++ b/react-command-palette/Settings.tsx @@ -0,0 +1,106 @@ +import { useCallback, useEffect, useState } from "react"; +import { invoke, type AppSettings } from "./bridge"; + +const DEFAULTS: AppSettings = { toggle_shortcut: "Alt+Space", hide_on_blur: true, theme: "dark" }; + +/** + * Settings view β€” the global summon hotkey, hide-on-blur, and theme. Persisted + * via `get_settings`/`update_settings`. Replaces the legacy `ui/scripts/settings.js`. + */ +export function Settings({ onClose }: { onClose: () => void }) { + const [s, setS] = useState(DEFAULTS); + const [status, setStatus] = useState(null); + + useEffect(() => { + void (async () => { + try { + const loaded = await invoke("get_settings"); + if (loaded) setS({ ...DEFAULTS, ...loaded }); + } catch { + /* keep defaults */ + } + })(); + }, []); + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault(); + e.stopPropagation(); + onClose(); + } + }; + window.addEventListener("keydown", onKey, true); + return () => window.removeEventListener("keydown", onKey, true); + }, [onClose]); + + const save = useCallback(async () => { + const next: AppSettings = { + toggle_shortcut: s.toggle_shortcut.trim() || DEFAULTS.toggle_shortcut, + hide_on_blur: s.hide_on_blur, + theme: s.theme, + }; + try { + await invoke("update_settings", { settings: next }); + setStatus("Saved"); + } catch (e) { + setStatus(`Failed: ${e}`); + } + }, [s]); + + return ( +
+
+

Settings

+ +
+ + + + + + + +
+ + {status && {status}} +
+
+ ); +} diff --git a/react-command-palette/bridge.ts b/react-command-palette/bridge.ts index 82854bb..c674175 100644 --- a/react-command-palette/bridge.ts +++ b/react-command-palette/bridge.ts @@ -33,6 +33,35 @@ export interface ExtensionHit { action: unknown | null; } +/** Persisted app settings (mirrors the Rust settings store). */ +export interface AppSettings { + toggle_shortcut: string; + hide_on_blur: boolean; + theme: "dark" | "light"; +} + +/** A requested permission, rendered in the consent dialog. */ +export interface PermissionInfo { + permission: string; + justification: string; +} + +/** Installed-extension summary (mirrors ExtensionInfo). */ +export interface ExtensionInfo { + id: string; + name: string; + version: string; + author?: string | null; + description?: string | null; + kind: "script" | "wasm"; + enabled: boolean; + /** User has trusted this (unsandboxed) script extension to run. */ + trusted: boolean; + /** A script that still needs an explicit trust grant before it will run. */ + needs_trust: boolean; + permissions: PermissionInfo[]; +} + export async function invoke(cmd: string, args?: Record): Promise { if (isTauri) return tauriInvoke(cmd, args); return mock(cmd, args) as T; @@ -73,6 +102,22 @@ function mock(cmd: string, args?: Record): unknown { return null; case "get_settings": return { toggle_shortcut: "Alt+Space", hide_on_blur: true, theme: "dark" }; + case "list_extensions": + return [ + { + id: "ddg", name: "DuckDuckGo", version: "1.0.0", author: "demo", + description: "Instant answers", kind: "script", enabled: true, + trusted: false, needs_trust: true, + permissions: [{ permission: "NetworkConnect", justification: "fetch answers" }], + }, + ]; + case "install_extension": + return "installed-id"; + case "set_extension_enabled": + case "set_extension_trusted": + case "uninstall_extension": + case "update_settings": + return null; default: return null; } diff --git a/ui/index.html b/ui/index.html deleted file mode 100644 index 57921fc..0000000 --- a/ui/index.html +++ /dev/null @@ -1,232 +0,0 @@ - - - - - - - SuperSearch - - - - - - - -
- -
- - - - - -
- -
-
- - -
-
- - - - - - - - -
- - - - - - diff --git a/ui/scripts/agent.js b/ui/scripts/agent.js deleted file mode 100644 index 718fbf6..0000000 --- a/ui/scripts/agent.js +++ /dev/null @@ -1,188 +0,0 @@ -/** - * SuperSearch β€” Agent Console Module - * - * Handles AI agent interactions: - * - Sends natural language queries to the agent backend - * - Renders workflow plans and execution steps - * - Streams runtime events for real-time updates - */ - -import { Bridge } from './bridge.js'; - -/** @type {HTMLElement|null} */ -let agentPanel = null; - -/** @type {boolean} */ -let isProcessing = false; - -/** - * Initialize the agent module. - * @param {HTMLElement} panel - The agent panel DOM element - */ -export function init(panel) { - agentPanel = panel; -} - -/** - * Check if a query should be handled by the agent. - * @param {string} query - * @returns {Promise} - */ -export async function isAgentQuery(query) { - if (!query || query.trim().length < 2) return false; - try { - return await Bridge.invoke('agent_check', { query: query.trim() }); - } catch { - return false; - } -} - -/** - * Execute an agent query and render results. - * @param {string} query - * @returns {Promise} - */ -export async function executeQuery(query) { - if (isProcessing) return null; - isProcessing = true; - - renderProcessing(query); - - try { - const response = await Bridge.invoke('agent_query', { query: query.trim() }); - renderResponse(response); - isProcessing = false; - return response; - } catch (err) { - renderError(query, err); - isProcessing = false; - return null; - } -} - -/** - * Render a processing/thinking state. - * @param {string} query - */ -function renderProcessing(query) { - if (!agentPanel) return; - agentPanel.innerHTML = ` -
-
- πŸ€– - Processing… -
-
-
${escapeHtml(query)}
-
- - - -
-
-
- `; - agentPanel.classList.add('active'); -} - -/** - * Render a completed agent response. - * @param {object} response - */ -function renderResponse(response) { - if (!agentPanel) return; - - const stepsHtml = response.steps.map((step, i) => ` -
-
- ${step.success ? 'βœ“' : 'βœ—'} -
-
-
${escapeHtml(step.label)}
- ${step.output ? `
${escapeHtml(truncate(step.output, 200))}
` : ''} - ${step.error ? `
${escapeHtml(step.error)}
` : ''} -
-
- `).join(''); - - agentPanel.innerHTML = ` -
-
- ${response.success ? 'βœ“' : '⚠'} - ${escapeHtml(response.intent)} - ${response.duration_ms}ms -
-
-
${escapeHtml(response.summary)}
- ${response.steps.length > 0 ? ` -
-
- Workflow β€” ${response.plan_description} - ${response.total_steps} step${response.total_steps !== 1 ? 's' : ''} -
- ${stepsHtml} -
- ` : ''} -
-
- `; - agentPanel.classList.add('active'); - - // Trigger entrance animation. - agentPanel.style.animation = 'none'; - agentPanel.offsetHeight; - agentPanel.style.animation = ''; -} - -/** - * Render an error state. - * @param {string} query - * @param {*} err - */ -function renderError(query, err) { - if (!agentPanel) return; - agentPanel.innerHTML = ` -
-
- βœ— - Agent Error -
-
-
${escapeHtml(query)}
-
${escapeHtml(String(err))}
-
-
- `; - agentPanel.classList.add('active'); -} - -/** - * Hide the agent panel. - */ -export function hide() { - if (agentPanel) { - agentPanel.classList.remove('active'); - agentPanel.innerHTML = ''; - } - isProcessing = false; -} - -/** - * Whether the agent is currently processing. - */ -export function busy() { - return isProcessing; -} - -/** Escape HTML */ -function escapeHtml(str) { - const div = document.createElement('div'); - div.textContent = str; - return div.innerHTML; -} - -/** Truncate string */ -function truncate(str, max) { - if (str.length <= max) return str; - return str.substring(0, max) + '…'; -} diff --git a/ui/scripts/app.js b/ui/scripts/app.js deleted file mode 100644 index cf69cca..0000000 --- a/ui/scripts/app.js +++ /dev/null @@ -1,442 +0,0 @@ -/** - * SuperSearch β€” Application Controller - * - * Main orchestrator that wires all modules together: - * - Bridge (Tauri IPC) - * - Search (query engine) - * - Keyboard (navigation) - * - Preview (detail panel) - * - Agent (AI console) - * - Telemetry (kernel metrics) - */ - -import { Bridge } from './bridge.js'; -import * as Search from './search.js'; -import * as Keyboard from './keyboard.js'; -import * as Preview from './preview.js'; -import * as Agent from './agent.js'; -import * as Extensions from './extensions.js'; -import * as Settings from './settings.js'; - -/** @type {number} Currently selected result index */ -let selectedIndex = -1; - -/** @type {SearchResult[]} Current results */ -let currentResults = []; - -/** @type {boolean} Whether the preview panel is visible (off by default β€” Raycast-style full-width results) */ -let previewVisible = false; - -/** @type {number|null} Telemetry polling interval */ -let telemetryInterval = null; - -/** @type {boolean} Whether agent mode is active */ -let agentMode = false; - -/** - * Initialize the application. - */ -export function init() { - // DOM references - const searchInput = document.getElementById('search-input'); - const resultsPanel = document.getElementById('results-panel'); - const previewPanel = document.getElementById('preview-panel'); - const contentArea = document.getElementById('content-area'); - const agentPanel = document.getElementById('agent-panel'); - const modeBadge = document.getElementById('mode-badge'); - - if (!searchInput || !resultsPanel || !previewPanel || !contentArea || !agentPanel) { - console.error('[App] Required DOM elements not found'); - return; - } - - // Raycast-style full-width single column by default; Tab reveals the preview. - contentArea.classList.add('no-preview'); - previewPanel.style.display = 'none'; - - // Initialize modules - Preview.init(previewPanel); - Agent.init(agentPanel); - Extensions.init(); - Settings.init(); - Keyboard.init(); - - // Wire search input - const paletteContainer = document.getElementById('palette-container'); - searchInput.addEventListener('input', (e) => { - const query = e.target.value; - - if (query.trim().length === 0) { - if (paletteContainer) paletteContainer.style.display = 'none'; - } else { - if (paletteContainer) paletteContainer.style.display = 'flex'; - } - - // Hide agent panel when user types - if (agentMode) { - agentMode = false; - agentPanel.classList.remove('active'); - contentArea.style.display = ''; - if (modeBadge) modeBadge.textContent = 'βŒ₯ Space'; - } - - Search.search(query); - }); - - // Wire search results - Search.onResults((results) => { - // Order into Raycast-style contiguous category groups (so visual order == - // selection order), highest-score within each group. - currentResults = orderResults(results); - selectedIndex = currentResults.length > 0 ? 0 : -1; - renderResults(resultsPanel); - updatePreview(); - }); - - // Wire keyboard navigation - Keyboard.registerCallbacks({ - onNavigate: (direction) => { - if (Extensions.isOpen() || Settings.isOpen()) return; - if (currentResults.length === 0) return; - selectedIndex = Math.max(0, Math.min(currentResults.length - 1, selectedIndex + direction)); - // Glide the highlight (no list rebuild) for a smooth, Spotlight-like move. - positionHighlight(resultsPanel, true); - updatePreview(); - }, - - onExecute: (withMeta) => { - if (Extensions.isOpen() || Settings.isOpen()) return; - if (selectedIndex >= 0 && selectedIndex < currentResults.length) { - const result = currentResults[selectedIndex]; - executeResult(result, withMeta, contentArea, agentPanel, modeBadge); - } - }, - - onDismiss: () => { - // Overlays intercept Esc first. - if (Settings.isOpen()) { - Settings.close(); - return; - } - if (Extensions.isOpen()) { - Extensions.close(); - return; - } - if (agentMode) { - // Exit agent mode first - agentMode = false; - Agent.hide(); - contentArea.style.display = ''; - if (modeBadge) modeBadge.textContent = 'βŒ₯ Space'; - return; - } - Bridge.invoke('hide_window').catch((error) => { - console.error('[App] Hide window failed:', error); - }); - }, - - onTogglePreview: () => { - previewVisible = !previewVisible; - contentArea.classList.toggle('no-preview', !previewVisible); - previewPanel.style.display = previewVisible ? '' : 'none'; - }, - }); - - // Composer toolbar actions - const submit = () => { - if (selectedIndex >= 0 && selectedIndex < currentResults.length) { - executeResult(currentResults[selectedIndex], false, contentArea, agentPanel, modeBadge); - } else if (searchInput.value.trim()) { - enterAgent(searchInput.value.trim()); - } - }; - document.getElementById('submit-btn')?.addEventListener('click', submit); - document.getElementById('deepsearch-btn')?.addEventListener('click', () => { - const q = searchInput.value.trim(); - if (q) enterAgent(q); - }); - document.getElementById('websearch-btn')?.addEventListener('click', () => { - const q = searchInput.value.trim(); - if (q) enterAgent(`search the web for ${q}`); - }); - document.getElementById('attach-btn')?.addEventListener('click', async () => { - const path = await Bridge.pickDirectory(); - if (path) { searchInput.value = path; searchInput.dispatchEvent(new Event('input', { bubbles: true })); } - }); - - // Auto-focus search input - searchInput.focus(); - - // When the palette is summoned via the global hotkey (Option+Space), the - // backend emits `supersearch://reset`. Clear stale state and refocus so the - // user always lands on an empty, ready prompt β€” like Spotlight. - Bridge.listen('supersearch://reset', () => { - // Replay the Raycast open animation each time the palette is summoned. - const palette = document.querySelector('.palette-container'); - if (palette) { - palette.classList.remove('rc-animate'); - void palette.offsetWidth; // force reflow so the animation restarts - palette.classList.add('rc-animate'); - palette.style.display = 'none'; // hide it on reset until typed - } - searchInput.value = ''; - currentResults = []; - selectedIndex = -1; - if (agentMode) { - agentMode = false; - Agent.hide(); - agentPanel.classList.remove('active'); - contentArea.style.display = ''; - if (modeBadge) modeBadge.textContent = 'βŒ₯ Space'; - } - Search.search(''); - renderResults(resultsPanel); - updatePreview(); - searchInput.focus(); - }); - - // Start telemetry polling - startTelemetry(); - - // Render initial empty state - renderResults(resultsPanel); - - console.log(`[App] SuperSearch initialized (Tauri: ${Bridge.isTauri})`); -} - -/** - * Execute a search result. - */ -async function executeResult(result, withMeta, contentArea, agentPanel, modeBadge) { - try { - if (result._ext) { - // Extension result β€” run its action through the gate-checked IPC. If it - // has no action, there's nothing to execute (informational row). - if (result._ext.action) { - const execution = await Bridge.invoke('execute_extension_action', { - id: result._ext.id, - action: result._ext.action, - }); - Preview.renderExecution(execution || { - title: result.title, category: 'Extension', detail: 'βœ“ Done', backend: `ext:${result._ext.id}`, - }); - } - return; - } - if (result.id.startsWith('agent:')) { - // Switch to agent mode - agentMode = true; - contentArea.style.display = 'none'; - if (modeBadge) modeBadge.textContent = 'πŸ€– Agent'; - - const query = result.id.substring(6); - await Agent.executeQuery(query); - } else { - // Regular action execution - const execution = await Bridge.invoke('execute_action', { - request: { - action_id: result.id, - with_meta: withMeta, - } - }); - Preview.renderExecution(execution); - } - } catch (error) { - console.error('[App] Execute failed:', error); - } -} - -/** - * Enter agent mode and run a natural-language query (used by the composer - * submit / Deep search / Search buttons). - * @param {string} query - */ -async function enterAgent(query) { - agentMode = true; - const contentArea = document.getElementById('content-area'); - const modeBadge = document.getElementById('mode-badge'); - if (contentArea) contentArea.style.display = 'none'; - if (modeBadge) modeBadge.textContent = 'πŸ€– Agent'; - try { - await Agent.executeQuery(query); - } catch (err) { - console.error('[App] Agent failed:', err); - } -} - -/** - * Render search results into the results panel. - * @param {HTMLElement} panel - */ -function renderResults(panel) { - requestAnimationFrame(() => { - if (currentResults.length === 0) { - panel.innerHTML = ` -
- ✨ - Ask anything, or search apps, files & commands -
- `; - return; - } - - // Sliding selection highlight (a single element that glides between rows). - let lastLabel = null; - const html = ['']; - currentResults.forEach((result, i) => { - const label = sectionLabel(result.category); - if (label !== lastLabel) { - lastLabel = label; - html.push(`
${escapeHtml(label)}
`); - } - html.push(` -
-
${result.icon}
-
-
${escapeHtml(result.title)}
- ${result.subtitle ? `
${escapeHtml(result.subtitle)}
` : ''} -
-
- Search ${escapeHtml(result.title)} - tab -
-
`); - }); - panel.innerHTML = html.join(''); - - // Click / double-click β€” move the highlight (no rebuild), execute on dbl. - panel.querySelectorAll('.result-item').forEach(el => { - el.addEventListener('click', () => { - selectedIndex = parseInt(el.dataset.index, 10); - positionHighlight(panel, true); - updatePreview(); - }); - el.addEventListener('dblclick', () => { - selectedIndex = parseInt(el.dataset.index, 10); - const result = currentResults[selectedIndex]; - if (result) { - executeResult(result, false, - document.getElementById('content-area'), - document.getElementById('agent-panel'), - document.getElementById('mode-badge')); - } - }); - }); - - // Place the highlight on the first row without animating (fresh list). - positionHighlight(panel, false); - }); -} - -/** - * Move the sliding selection highlight to the active row and sync the active - * class / aria. When `animate` is false the move is instant (used on re-render). - * @param {HTMLElement} panel - * @param {boolean} animate - */ -function positionHighlight(panel, animate) { - const hl = panel.querySelector('.selection-highlight'); - panel.querySelectorAll('.result-item').forEach(r => { - r.classList.remove('result-item--active'); - r.setAttribute('aria-selected', 'false'); - }); - if (!hl) return; - if (selectedIndex < 0) { hl.style.opacity = '0'; return; } - const active = panel.querySelector(`.result-item[data-index="${selectedIndex}"]`); - if (!active) { hl.style.opacity = '0'; return; } - - active.classList.add('result-item--active'); - active.setAttribute('aria-selected', 'true'); - - if (!animate) hl.classList.add('no-anim'); - hl.style.transform = `translateY(${active.offsetTop}px)`; - hl.style.height = `${active.offsetHeight}px`; - hl.style.opacity = '1'; - if (!animate) { void hl.offsetWidth; hl.classList.remove('no-anim'); } - - active.scrollIntoView({ block: 'nearest' }); -} - -/** - * Update the preview panel with the currently selected result. - */ -function updatePreview() { - if (!previewVisible) return; - const result = selectedIndex >= 0 ? currentResults[selectedIndex] : null; - Preview.render(result); -} - -/** - * Start telemetry polling β€” updates the telemetry strip every 500ms. - */ -function startTelemetry() { - updateTelemetry(); - telemetryInterval = setInterval(updateTelemetry, 500); -} - -/** - * Fetch and render kernel telemetry. - */ -async function updateTelemetry() { - try { - const data = await Bridge.invoke('get_telemetry'); - if (!data) return; - - setTelemetryValue('telemetry-scheduler', `${data.scheduler_ticks} ticks`); - setTelemetryValue('telemetry-capabilities', `${data.capabilities_active} active`); - setTelemetryValue('telemetry-uptime', formatUptime(data.uptime_seconds)); - setTelemetryValue('telemetry-boot', `${data.boot_time_ms}ms`); - } catch (err) { - // Silently ignore telemetry failures - } -} - -function setTelemetryValue(id, value) { - const el = document.getElementById(id); - if (el) el.textContent = value; -} - -function formatUptime(seconds) { - if (seconds < 60) return `${Math.floor(seconds)}s`; - if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${Math.floor(seconds % 60)}s`; - return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`; -} - -function escapeHtml(str) { - const div = document.createElement('div'); - div.textContent = str == null ? '' : String(str); - return div.innerHTML; -} - -/** Category display priority (lower = higher in the list), Raycast-style. */ -const CATEGORY_RANK = { Agent: 0, Command: 1, Application: 2, Extension: 3, System: 4, Folder: 5, File: 6 }; - -/** Human section header for a result category. */ -function sectionLabel(category) { - switch (category) { - case 'Agent': return 'AI Agent'; - case 'Command': return 'Commands'; - case 'Application': return 'Applications'; - case 'Extension': return 'Extensions'; - case 'System': return 'System'; - case 'File': case 'Folder': return 'Files'; - default: return category || 'Results'; - } -} - -/** Order results into contiguous category groups (stable; keeps score order within a group). */ -function orderResults(results) { - return [...(results || [])].sort((a, b) => { - const ra = CATEGORY_RANK[a.category] ?? 99; - const rb = CATEGORY_RANK[b.category] ?? 99; - return ra - rb; // stable sort preserves the incoming score order within a category - }); -} - -// Auto-initialize -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', init); -} else { - init(); -} diff --git a/ui/scripts/bridge.js b/ui/scripts/bridge.js deleted file mode 100644 index 72dddcd..0000000 --- a/ui/scripts/bridge.js +++ /dev/null @@ -1,244 +0,0 @@ -/** - * SuperSearch β€” Tauri IPC Bridge - * - * Abstraction over window.__TAURI__ for WebView ↔ Kernel communication. - * Falls back to realistic mock data when running outside Tauri (browser dev). - */ - -const IS_TAURI = typeof window !== 'undefined' && window.__TAURI__ != null; - -let mockUptime = 0; -let mockTicks = 0; - -const MOCK_APPS = [ - { id: 'app:/Applications/Google Chrome.app', title: 'Google Chrome', subtitle: '/Applications/Google Chrome.app', category: 'Application', icon: 'πŸ“±', score: 0.9 }, - { id: 'app:/Applications/Visual Studio Code.app', title: 'Visual Studio Code', subtitle: '/Applications/Visual Studio Code.app', category: 'Application', icon: 'πŸ“±', score: 0.85 }, - { id: 'app:/Applications/Slack.app', title: 'Slack', subtitle: '/Applications/Slack.app', category: 'Application', icon: 'πŸ“±', score: 0.8 }, - { id: 'app:/Applications/Spotify.app', title: 'Spotify', subtitle: '/Applications/Spotify.app', category: 'Application', icon: 'πŸ“±', score: 0.75 }, - { id: 'app:/System/Applications/Terminal.app', title: 'Terminal', subtitle: '/System/Applications/Terminal.app', category: 'Application', icon: 'πŸ“±', score: 0.7 }, - { id: 'app:/System/Applications/Calculator.app', title: 'Calculator', subtitle: '/System/Applications/Calculator.app', category: 'Application', icon: 'πŸ“±', score: 0.65 }, - { id: 'app:/Applications/Figma.app', title: 'Figma', subtitle: '/Applications/Figma.app', category: 'Application', icon: 'πŸ“±', score: 0.6 }, - { id: 'app:/Applications/Discord.app', title: 'Discord', subtitle: '/Applications/Discord.app', category: 'Application', icon: 'πŸ“±', score: 0.55 }, -]; - -const MOCK_SYS_COMMANDS = [ - { id: 'sys:lock', title: 'Lock Screen', subtitle: 'Lock the screen immediately', category: 'System', icon: 'πŸ”’', score: 0.0 }, - { id: 'sys:screenshot', title: 'Screenshot', subtitle: 'Capture a screenshot', category: 'System', icon: 'πŸ“Έ', score: 0.0 }, - { id: 'sys:dnd', title: 'Do Not Disturb', subtitle: 'Toggle Do Not Disturb mode', category: 'System', icon: 'πŸ”•', score: 0.0 }, - { id: 'sys:dark_mode', title: 'Toggle Dark Mode', subtitle: 'Switch between light and dark appearance', category: 'System', icon: 'πŸŒ—', score: 0.0 }, - { id: 'sys:clipboard', title: 'Show Clipboard', subtitle: 'View current clipboard contents', category: 'System', icon: 'πŸ“‹', score: 0.0 }, - { id: 'sys:running_apps', title: 'Running Apps', subtitle: 'List all currently running applications', category: 'System', icon: 'πŸ“Š', score: 0.0 }, - { id: 'sys:system_info', title: 'System Info', subtitle: 'View system information', category: 'System', icon: 'ℹ️', score: 0.0 }, -]; - -// In-memory extension list for browser-dev mock mode. -let MOCK_EXTENSIONS = [ - { - id: 'ddg', name: 'DuckDuckGo Search', version: '1.0.0', author: 'SuperSearch', - description: 'Open a DuckDuckGo web search for your query', kind: 'script', enabled: false, - permissions: [{ permission: 'NetworkConnect', justification: 'Open search results in your default browser' }], - }, -]; - -// Mutable settings for browser-dev mock mode. -let MOCK_SETTINGS = { toggle_shortcut: 'Alt+Space', hide_on_blur: true, theme: 'dark' }; - -// Simple agent intent detection for mock mode -const AGENT_PATTERNS = [ - /^open\s+/i, /^launch\s+/i, /^start\s+/i, /^quit\s+/i, /^close\s+/i, - /^find\s+/i, /^search\s+/i, /^switch to\s+/i, /^focus\s+/i, - /^lock/i, /^screenshot/i, /^clipboard/i, /^running/i, /^what's running/i, - /^volume/i, /^mute/i, /^brightness/i, /^dark mode/i, /^dnd/i, - /^sleep/i, /^empty trash/i, /^battery/i, /^disk/i, /^uptime/i, - /^https?:\/\//i, /^www\./i, -]; - -export const Bridge = { - isTauri: IS_TAURI, - - async invoke(cmd, args = {}) { - if (IS_TAURI) { - return window.__TAURI__.core.invoke(cmd, args); - } - - // Mock fallback for browser development - switch (cmd) { - case 'search_query': { - const q = (args.query || '').toLowerCase(); - if (!q) return []; - - let results = []; - - // App search - const appResults = MOCK_APPS.filter(a => - a.title.toLowerCase().includes(q) - ).map(a => ({ ...a, score: a.title.toLowerCase().startsWith(q) ? 0.95 : 0.7 })); - results.push(...appResults); - - // System commands - const sysResults = MOCK_SYS_COMMANDS.filter(c => - c.title.toLowerCase().includes(q) || c.subtitle.toLowerCase().includes(q) - ).map(c => ({ ...c, score: 0.6 })); - results.push(...sysResults); - - // Agent detection - if (AGENT_PATTERNS.some(p => p.test(args.query || ''))) { - results.unshift({ - id: `agent:${args.query}`, - title: `⚑ ${args.query}`, - subtitle: 'Execute with AI Agent', - category: 'Agent', - icon: 'πŸ€–', - score: 1.5, - }); - } - - results.sort((a, b) => b.score - a.score); - return results.slice(0, 12); - } - - case 'execute_action': { - const actionId = args.action_id || ''; - if (actionId.startsWith('agent:')) { - const query = actionId.substring(6); - return { - action_id: actionId, - acknowledged: true, - title: 'Agent Execution', - category: 'Agent', - detail: `Mock agent executed: "${query}"`, - backend: 'mock-agent', - }; - } - if (actionId.startsWith('app:')) { - return { - action_id: actionId, - acknowledged: true, - title: 'Launch App', - category: 'Application', - detail: `βœ“ Launched ${actionId.split('/').pop().replace('.app', '')}`, - backend: 'mock-os', - }; - } - return { - action_id: actionId, - acknowledged: true, - title: 'Action Executed', - category: 'System', - detail: `Mock executed: ${actionId}`, - backend: 'mock-backend', - }; - } - - case 'agent_query': { - const q = args.query || ''; - await new Promise(r => setTimeout(r, 300)); // Simulate processing - return { - query: q, - intent: `Mock Intent: ${q}`, - plan_description: q, - total_steps: 1, - steps: [{ - label: q, - success: true, - output: `βœ“ Mock executed: ${q}`, - error: null, - }], - success: true, - summary: `βœ“ ${q} completed (mock)`, - duration_ms: 42, - }; - } - - case 'agent_check': { - const q = (args.query || '').trim(); - return AGENT_PATTERNS.some(p => p.test(q)); - } - - case 'hide_window': - return true; - - case 'list_extensions': - return MOCK_EXTENSIONS.map((e) => ({ ...e })); - - case 'install_extension': { - const id = (args.path || '').split('/').filter(Boolean).pop() || `ext-${Date.now()}`; - if (!MOCK_EXTENSIONS.some((e) => e.id === id)) { - MOCK_EXTENSIONS.push({ - id, name: id, version: '0.0.0', author: null, description: 'Installed (mock)', - kind: 'script', enabled: false, permissions: [], - }); - } - return id; - } - - case 'uninstall_extension': - MOCK_EXTENSIONS = MOCK_EXTENSIONS.filter((e) => e.id !== args.id); - return null; - - case 'set_extension_enabled': { - const ext = MOCK_EXTENSIONS.find((e) => e.id === args.id); - if (ext) ext.enabled = !!args.enabled; - return null; - } - - case 'query_extensions': { - const q = (args.query || '').trim(); - if (!q) return []; - return MOCK_EXTENSIONS.filter((e) => e.enabled).map((e) => ({ - extension_id: e.id, - title: `${e.name}: ${q}`, - subtitle: 'Extension result (mock)', - action: { type: 'open_url', url: `https://duckduckgo.com/?q=${encodeURIComponent(q)}` }, - })); - } - - case 'execute_extension_action': - return null; - - case 'get_settings': - return { ...MOCK_SETTINGS }; - - case 'update_settings': - MOCK_SETTINGS = { ...MOCK_SETTINGS, ...(args.settings || {}) }; - return null; - - case 'get_telemetry': - mockUptime += 0.5; - mockTicks += Math.floor(Math.random() * 3); - return { - scheduler_ticks: mockTicks, - scheduler_idle: true, - capabilities_active: 3 + Math.floor(Math.random() * 3), - capabilities_total: 5, - uptime_seconds: mockUptime, - boot_time_ms: 12, - }; - - default: - console.warn(`[Bridge] Unknown command: ${cmd}`); - return null; - } - }, - - async listen(event, callback) { - if (IS_TAURI) { - return window.__TAURI__.event.listen(event, callback); - } - return () => {}; - }, - - /** - * Open a native folder picker, returning the selected path or null. - * Falls back to a prompt in browser-dev or if the dialog API is absent. - * @returns {Promise} - */ - async pickDirectory() { - if (IS_TAURI && window.__TAURI__.dialog?.open) { - const selected = await window.__TAURI__.dialog.open({ directory: true, multiple: false }); - return typeof selected === 'string' ? selected : null; - } - const path = typeof prompt === 'function' ? prompt('Extension folder path:') : null; - return path && path.trim() ? path.trim() : null; - }, -}; diff --git a/ui/scripts/extensions.js b/ui/scripts/extensions.js deleted file mode 100644 index 6b6953d..0000000 --- a/ui/scripts/extensions.js +++ /dev/null @@ -1,198 +0,0 @@ -/** - * SuperSearch β€” Extension Manager - * - * Drives the Plugin Manager overlay: list installed extensions, install from a - * folder, enable (with a permission-consent step), disable, and uninstall. - * All actions go through the Tauri IPC commands backed by ExtensionRegistry. - */ - -import { Bridge } from './bridge.js'; - -let overlay, listEl, consentEl, consentTitle, consentPerms, consentCancel, consentConfirm; -let initialized = false; - -/** Resolver for the in-flight consent prompt, if any. */ -let consentResolve = null; - -/** - * Wire up DOM references and event handlers. Call once on startup. - */ -export function init() { - overlay = document.getElementById('extensions-overlay'); - listEl = document.getElementById('ext-list'); - consentEl = document.getElementById('ext-consent'); - consentTitle = document.getElementById('ext-consent-title'); - consentPerms = document.getElementById('ext-consent-perms'); - consentCancel = document.getElementById('ext-consent-cancel'); - consentConfirm = document.getElementById('ext-consent-confirm'); - - if (!overlay) return; - - document.getElementById('extensions-btn')?.addEventListener('click', open); - document.getElementById('ext-close-btn')?.addEventListener('click', close); - document.getElementById('ext-install-btn')?.addEventListener('click', installFlow); - - // Click the dimmed backdrop (but not the panel) to dismiss. - overlay.addEventListener('mousedown', (e) => { - if (e.target === overlay) close(); - }); - - consentCancel?.addEventListener('click', () => resolveConsent(false)); - consentConfirm?.addEventListener('click', () => resolveConsent(true)); - - initialized = true; -} - -/** @returns {boolean} whether the manager overlay is currently open. */ -export function isOpen() { - return initialized && overlay && !overlay.hidden; -} - -/** Open the manager and refresh the list. */ -export async function open() { - if (!initialized) return; - overlay.hidden = false; - await refresh(); -} - -/** Close the manager (and any open consent prompt). */ -export function close() { - if (!initialized) return; - resolveConsent(false); - overlay.hidden = true; -} - -/** Fetch the installed extensions and render the list. */ -async function refresh() { - let extensions = []; - try { - extensions = (await Bridge.invoke('list_extensions')) || []; - } catch (err) { - console.error('[Extensions] list failed:', err); - } - render(extensions); -} - -function render(extensions) { - if (extensions.length === 0) { - listEl.innerHTML = ` -
- 🧩 - No extensions installed - Click β€œInstall…” to add one from a folder. -
`; - return; - } - - listEl.innerHTML = extensions.map((ext) => { - const perms = (ext.permissions || []) - .map((p) => `${escapeHtml(p.permission)}`) - .join(''); - const author = ext.author ? ` Β· ${escapeHtml(ext.author)}` : ''; - return ` -
-
-
- ${escapeHtml(ext.name)} - v${escapeHtml(ext.version)}${author} Β· ${escapeHtml(ext.kind)} -
- ${ext.description ? `
${escapeHtml(ext.description)}
` : ''} - ${perms ? `
${perms}
` : ''} -
-
- - -
-
`; - }).join(''); - - listEl.querySelectorAll('.ext-card').forEach((card) => { - const id = card.dataset.id; - const ext = extensions.find((e) => e.id === id); - card.querySelector('[data-action="toggle"]')?.addEventListener('change', (e) => { - toggleExtension(ext, e.target.checked); - }); - card.querySelector('[data-action="uninstall"]')?.addEventListener('click', () => { - uninstallExtension(ext); - }); - }); -} - -/** Enable (with consent) or disable an extension. */ -async function toggleExtension(ext, enabled) { - try { - if (enabled && (ext.permissions || []).length > 0) { - const granted = await requestConsent(ext); - if (!granted) { - await refresh(); // revert the toggle UI - return; - } - } - await Bridge.invoke('set_extension_enabled', { id: ext.id, enabled }); - } catch (err) { - console.error('[Extensions] toggle failed:', err); - } - await refresh(); -} - -async function uninstallExtension(ext) { - if (!confirm(`Uninstall β€œ${ext.name}”? This removes its files.`)) return; - try { - await Bridge.invoke('uninstall_extension', { id: ext.id }); - } catch (err) { - console.error('[Extensions] uninstall failed:', err); - alert(`Uninstall failed: ${err}`); - } - await refresh(); -} - -/** Pick a folder and install the extension it contains. */ -async function installFlow() { - let dir; - try { - dir = await Bridge.pickDirectory(); - } catch (err) { - console.error('[Extensions] folder pick failed:', err); - return; - } - if (!dir) return; - try { - await Bridge.invoke('install_extension', { path: dir }); - } catch (err) { - console.error('[Extensions] install failed:', err); - alert(`Install failed: ${err}`); - } - await refresh(); -} - -/** - * Show the permission-consent modal and resolve to true if the user allows. - * @returns {Promise} - */ -function requestConsent(ext) { - consentTitle.textContent = `Enable β€œ${ext.name}”`; - consentPerms.innerHTML = (ext.permissions || []).map((p) => ` - `).join(''); - consentEl.hidden = false; - return new Promise((resolve) => { consentResolve = resolve; }); -} - -function resolveConsent(value) { - if (consentResolve) { - consentResolve(value); - consentResolve = null; - } - if (consentEl) consentEl.hidden = true; -} - -function escapeHtml(str) { - const div = document.createElement('div'); - div.textContent = str == null ? '' : String(str); - return div.innerHTML; -} diff --git a/ui/scripts/keyboard.js b/ui/scripts/keyboard.js deleted file mode 100644 index 924da82..0000000 --- a/ui/scripts/keyboard.js +++ /dev/null @@ -1,76 +0,0 @@ -/** - * SuperSearch β€” Keyboard Navigation - * - * Manages keyboard shortcuts for the command palette. - * All bindings use `keydown` with `event.key`. - */ - -/** @type {function|null} */ -let onNavigateCallback = null; -/** @type {function|null} */ -let onExecuteCallback = null; -/** @type {function|null} */ -let onDismissCallback = null; -/** @type {function|null} */ -let onTogglePreviewCallback = null; - -/** - * Register navigation callbacks. - */ -export function registerCallbacks({ onNavigate, onExecute, onDismiss, onTogglePreview }) { - onNavigateCallback = onNavigate || null; - onExecuteCallback = onExecute || null; - onDismissCallback = onDismiss || null; - onTogglePreviewCallback = onTogglePreview || null; -} - -/** - * Initialize keyboard event listeners. - * Call once on app startup. - */ -export function init() { - document.addEventListener('keydown', handleKeyDown); -} - -/** - * Teardown keyboard listeners. - */ -export function destroy() { - document.removeEventListener('keydown', handleKeyDown); -} - -/** - * Handle keydown events. - * @param {KeyboardEvent} e - */ -function handleKeyDown(e) { - switch (e.key) { - case 'ArrowDown': - e.preventDefault(); - if (onNavigateCallback) onNavigateCallback(1); - break; - - case 'ArrowUp': - e.preventDefault(); - if (onNavigateCallback) onNavigateCallback(-1); - break; - - case 'Enter': - e.preventDefault(); - if (onExecuteCallback) onExecuteCallback(e.metaKey); - break; - - case 'Escape': - e.preventDefault(); - if (onDismissCallback) onDismissCallback(); - break; - - case 'Tab': - e.preventDefault(); - if (onTogglePreviewCallback) onTogglePreviewCallback(); - break; - - default: - break; - } -} diff --git a/ui/scripts/preview.js b/ui/scripts/preview.js deleted file mode 100644 index 49163f7..0000000 --- a/ui/scripts/preview.js +++ /dev/null @@ -1,142 +0,0 @@ -/** - * SuperSearch β€” Preview Panel - * - * Renders contextual preview for the selected search result. - */ - -/** @type {HTMLElement|null} */ -let previewElement = null; - -/** - * Initialize the preview module. - * @param {HTMLElement} el - The preview panel DOM element - */ -export function init(el) { - previewElement = el; - renderEmpty(); -} - -/** - * Render preview for a search result. - * @param {SearchResult|null} result - */ -export function render(result) { - if (!previewElement) return; - - if (!result) { - renderEmpty(); - return; - } - - const meta = getResultMeta(result); - - previewElement.innerHTML = ` -
- ${result.icon} - ${escapeHtml(result.title)} -
-
-

${escapeHtml(result.subtitle)}

-
-
- ${meta.map(([label, value]) => ` -
- ${escapeHtml(label)} - ${escapeHtml(value)} -
- `).join('')} -
- `; - - // Re-trigger animation - previewElement.style.animation = 'none'; - previewElement.offsetHeight; // force reflow - previewElement.style.animation = ''; -} - -/** - * Render a backend execution response in the preview panel. - * @param {{action_id: string, acknowledged: boolean, title: string, category: string, detail: string, backend: string}} execution - */ -export function renderExecution(execution) { - if (!previewElement) return; - - previewElement.innerHTML = ` -
- ↡ - ${escapeHtml(execution.title || 'Action executed')} -
-
-

${escapeHtml(execution.detail || 'The backend handled the request.')}

-
-
-
- Action - ${escapeHtml(execution.action_id)} -
-
- Category - ${escapeHtml(execution.category || 'Action')} -
-
- Backend - ${escapeHtml(execution.backend || 'rust-runtime')} -
-
- Status - ${execution.acknowledged ? 'Acknowledged' : 'Pending'} -
-
- `; - - previewElement.style.animation = 'none'; - previewElement.offsetHeight; - previewElement.style.animation = ''; -} - -/** - * Render the empty state for the preview panel. - */ -function renderEmpty() { - if (!previewElement) return; - previewElement.innerHTML = ` -
- β—‡ - Select a result to preview details -
- `; -} - -/** - * Extract metadata key-value pairs from a result. - * @param {SearchResult} result - * @returns {[string, string][]} - */ -function getResultMeta(result) { - const meta = [ - ['Category', result.category], - ['ID', result.id], - ]; - - if (result.id.startsWith('cmd:')) { - meta.push(['Type', 'Kernel Command']); - meta.push(['Priority', 'UserBlocking']); - } else if (result.id.startsWith('act:')) { - meta.push(['Type', 'Action']); - meta.push(['Priority', 'Default']); - } - - meta.push(['Relevance', `${Math.round(result.score * 100)}%`]); - return meta; -} - -/** - * Escape HTML to prevent XSS. - * @param {string} str - * @returns {string} - */ -function escapeHtml(str) { - const div = document.createElement('div'); - div.textContent = str; - return div.innerHTML; -} diff --git a/ui/scripts/search.js b/ui/scripts/search.js deleted file mode 100644 index dff7392..0000000 --- a/ui/scripts/search.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * SuperSearch β€” Search Engine Client - * - * Debounced search with result caching and IPC integration. - */ - -import { Bridge } from './bridge.js'; - -/** @type {number|null} */ -let debounceTimer = null; - -/** @type {SearchResult[]} */ -let cachedResults = []; - -/** @type {string} */ -let lastQuery = ''; - -/** @type {function|null} */ -let onResultsCallback = null; - -/** - * Register a callback for search results. - * @param {function(SearchResult[]): void} callback - */ -export function onResults(callback) { - onResultsCallback = callback; -} - -/** - * Perform a debounced search. - * Skips queries shorter than 1 character and debounces at 50ms. - * @param {string} query - Search query string - */ -export function search(query) { - const trimmed = query.trim(); - - // Clear results for empty queries immediately - if (!trimmed) { - clearTimeout(debounceTimer); - lastQuery = ''; - cachedResults = []; - if (onResultsCallback) onResultsCallback([]); - return; - } - - // Skip if query hasn't changed - if (trimmed === lastQuery) return; - - // Debounce - clearTimeout(debounceTimer); - debounceTimer = setTimeout(async () => { - lastQuery = trimmed; - try { - // Native results and enabled-extension results are fetched in parallel - // and merged. Extension hits carry an `_ext` payload (source id + action) - // so execution can route through execute_extension_action (gate-checked). - const [native, extHits] = await Promise.all([ - Bridge.invoke('search_query', { query: trimmed }), - Bridge.invoke('query_extensions', { query: trimmed }).catch(() => []), - ]); - - const extResults = (extHits || []).map((h, i) => ({ - id: `ext:${h.extension_id}:${i}`, - title: h.title, - subtitle: h.subtitle || '', - category: 'Extension', - icon: '🧩', - score: 1.2, - _ext: { id: h.extension_id, action: h.action || null }, - })); - - const merged = [...(native || []), ...extResults]; - merged.sort((a, b) => (b.score || 0) - (a.score || 0)); - cachedResults = merged; - if (onResultsCallback) onResultsCallback(cachedResults); - } catch (err) { - console.error('[Search] Query failed:', err); - cachedResults = []; - if (onResultsCallback) onResultsCallback([]); - } - }, 50); -} - -/** - * Get the currently cached results. - * @returns {SearchResult[]} - */ -export function getResults() { - return cachedResults; -} - -/** - * Clear the search state. - */ -export function clear() { - clearTimeout(debounceTimer); - lastQuery = ''; - cachedResults = []; - if (onResultsCallback) onResultsCallback([]); -} diff --git a/ui/scripts/settings.js b/ui/scripts/settings.js deleted file mode 100644 index 4785789..0000000 --- a/ui/scripts/settings.js +++ /dev/null @@ -1,87 +0,0 @@ -/** - * SuperSearch β€” Settings panel - * - * Reads/writes persisted settings via IPC (get_settings/update_settings): - * the global hotkey, hide-on-blur, and theme. Applies the theme immediately. - */ - -import { Bridge } from './bridge.js'; - -let overlay, hotkeyInput, blurInput, themeSelect, statusEl; -let initialized = false; -let current = null; - -export function init() { - overlay = document.getElementById('settings-overlay'); - hotkeyInput = document.getElementById('settings-hotkey'); - blurInput = document.getElementById('settings-blur'); - themeSelect = document.getElementById('settings-theme'); - statusEl = document.getElementById('settings-status'); - if (!overlay) return; - - document.getElementById('settings-btn')?.addEventListener('click', open); - document.getElementById('settings-close-btn')?.addEventListener('click', close); - document.getElementById('settings-save-btn')?.addEventListener('click', save); - overlay.addEventListener('mousedown', (e) => { if (e.target === overlay) close(); }); - - // Apply the persisted theme on startup (before the panel is ever opened). - loadAndApplyTheme(); - initialized = true; -} - -export function isOpen() { - return initialized && overlay && !overlay.hidden; -} - -export async function open() { - if (!initialized) return; - await refresh(); - overlay.hidden = false; - if (statusEl) statusEl.textContent = ''; - hotkeyInput?.focus(); -} - -export function close() { - if (initialized) overlay.hidden = true; -} - -async function refresh() { - try { - current = (await Bridge.invoke('get_settings')) || {}; - } catch (err) { - console.error('[Settings] load failed:', err); - current = {}; - } - if (hotkeyInput) hotkeyInput.value = current.toggle_shortcut || 'Alt+Space'; - if (blurInput) blurInput.checked = current.hide_on_blur !== false; - if (themeSelect) themeSelect.value = current.theme || 'dark'; -} - -async function save() { - const next = { - toggle_shortcut: (hotkeyInput?.value || 'Alt+Space').trim() || 'Alt+Space', - hide_on_blur: !!blurInput?.checked, - theme: themeSelect?.value || 'dark', - }; - try { - await Bridge.invoke('update_settings', { settings: next }); - current = next; - applyTheme(next.theme); - if (statusEl) statusEl.textContent = 'Saved βœ“'; - setTimeout(() => { if (statusEl) statusEl.textContent = ''; }, 1500); - } catch (err) { - console.error('[Settings] save failed:', err); - if (statusEl) statusEl.textContent = `Error: ${err}`; - } -} - -async function loadAndApplyTheme() { - try { - const s = (await Bridge.invoke('get_settings')) || {}; - applyTheme(s.theme || 'dark'); - } catch (_) { /* ignore */ } -} - -function applyTheme(theme) { - document.body.dataset.theme = theme; -} diff --git a/ui/styles/animations.css b/ui/styles/animations.css deleted file mode 100644 index 9c6b7b4..0000000 --- a/ui/styles/animations.css +++ /dev/null @@ -1,215 +0,0 @@ -/* ============================================================ - SuperSearch β€” Animations & Micro-interactions - ============================================================ */ - -/* ---- Palette entrance ---- */ -@keyframes palette-enter { - from { - opacity: 0; - transform: translateY(12px) scale(0.98); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -.palette-container { - animation: palette-enter var(--duration-slow) var(--ease-out) both; -} - -/* ---- Telemetry dot pulse ---- */ -@keyframes pulse-dot { - 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.5; transform: scale(0.85); } -} - -/* ---- Result item staggered entrance ---- */ -@keyframes result-slide-in { - from { - opacity: 0; - transform: translateX(-6px); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -.result-item { - animation: result-slide-in var(--duration-normal) var(--ease-out) both; -} - -.result-item:nth-child(1) { animation-delay: 0ms; } -.result-item:nth-child(2) { animation-delay: 30ms; } -.result-item:nth-child(3) { animation-delay: 60ms; } -.result-item:nth-child(4) { animation-delay: 90ms; } -.result-item:nth-child(5) { animation-delay: 120ms; } -.result-item:nth-child(6) { animation-delay: 150ms; } -.result-item:nth-child(7) { animation-delay: 180ms; } -.result-item:nth-child(8) { animation-delay: 210ms; } -.result-item:nth-child(9) { animation-delay: 240ms; } -.result-item:nth-child(10) { animation-delay: 270ms; } -.result-item:nth-child(11) { animation-delay: 300ms; } -.result-item:nth-child(12) { animation-delay: 330ms; } - -/* ---- Active result glow ---- */ -@keyframes active-glow { - 0%, 100% { box-shadow: inset 0 0 0 0 transparent; } - 50% { box-shadow: inset 0 0 20px -8px hsla(220, 85%, 60%, 0.1); } -} - -.result-item--active { - animation: active-glow 3s ease-in-out infinite, result-slide-in var(--duration-normal) var(--ease-out) both; -} - -/* ---- Preview panel slide-in ---- */ -@keyframes preview-slide-in { - from { - opacity: 0; - transform: translateX(10px); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -.preview-panel { - animation: preview-slide-in var(--duration-normal) var(--ease-out) both; -} - -/* ---- Search icon focus bounce ---- */ -@keyframes icon-bounce { - 0%, 100% { transform: translateY(0); } - 40% { transform: translateY(-2px); } - 60% { transform: translateY(1px); } -} - -.search-bar:focus-within .search-bar__icon { - animation: icon-bounce 0.4s var(--ease-out); -} - -/* ---- Smooth content transitions ---- */ -.telemetry-item__value { - transition: color var(--duration-fast) var(--ease-out); -} - -/* ---- Shortcut key press effect ---- */ -.shortcut__key:active { - transform: scale(0.92); - background: var(--surface-active); -} - -.shortcut__key { - transition: transform var(--duration-fast) var(--ease-out), - background var(--duration-fast) var(--ease-out); -} - -/* ---- Result item icon hover ---- */ -.result-item:hover .result-item__icon { - border-color: hsla(220, 85%, 60%, 0.3); - transition: border-color var(--duration-fast) var(--ease-out); -} - -/* ---- Gradient text for accent elements ---- */ -.gradient-text { - background: var(--accent-gradient); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -/* ---- Typing dots animation ---- */ -@keyframes typing-bounce { - 0%, 80%, 100% { - transform: scale(0.4); - opacity: 0.3; - } - 40% { - transform: scale(1); - opacity: 1; - } -} - -/* ---- Agent panel entrance ---- */ -@keyframes agent-slide-up { - from { - opacity: 0; - transform: translateY(8px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.agent-panel.active { - animation: agent-slide-up var(--duration-normal) var(--ease-out) both; -} - -/* ---- Agent step entrance ---- */ -@keyframes step-check-in { - from { - opacity: 0; - transform: translateX(-4px); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -.agent-step { - animation: step-check-in var(--duration-normal) var(--ease-out) both; -} - -.agent-step:nth-child(1) { animation-delay: 50ms; } -.agent-step:nth-child(2) { animation-delay: 100ms; } -.agent-step:nth-child(3) { animation-delay: 150ms; } -.agent-step:nth-child(4) { animation-delay: 200ms; } -.agent-step:nth-child(5) { animation-delay: 250ms; } - -/* ---- Agent response success pulse ---- */ -@keyframes success-border-pulse { - 0%, 100% { border-color: hsla(145, 70%, 50%, 0.3); } - 50% { border-color: hsla(145, 70%, 50%, 0.5); } -} - -.agent-response--success { - animation: success-border-pulse 2s ease-in-out 1; -} - -/* ---- Agent mode badge glow ---- */ -@keyframes badge-glow { - 0%, 100% { box-shadow: 0 0 0 0 transparent; } - 50% { box-shadow: 0 0 8px -2px hsla(270, 80%, 65%, 0.4); } -} - -/* ---- Reduced motion ---- */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - } -} - -/* ============================================================================ - Raycast open animation (replays on each summon via .rc-animate) - ============================================================================ */ -@keyframes rc-pop { - from { opacity: 0; transform: scale(0.985) translateY(-6px); } - to { opacity: 1; transform: none; } -} -.palette-container { animation: rc-pop 170ms cubic-bezier(0.16, 1, 0.3, 1); transform-origin: top center; } -.palette-container.rc-animate { animation: rc-pop 170ms cubic-bezier(0.16, 1, 0.3, 1); } - -/* Rows intentionally do NOT animate per-render: the list rebuilds on every - keystroke/selection change, so a per-row fade would flicker. The window - open animation (rc-pop) provides the entrance; selection is a fast - background transition handled in main.css. */ - -@media (prefers-reduced-motion: reduce) { - .palette-container, .palette-container.rc-animate { animation: none !important; } -} diff --git a/ui/styles/main.css b/ui/styles/main.css deleted file mode 100644 index 480f81a..0000000 --- a/ui/styles/main.css +++ /dev/null @@ -1,1493 +0,0 @@ -/* ============================================================ - SuperSearch β€” Core Design System - ============================================================ - Architecture: @layer reset, base, tokens, layout, components, utilities - Color: Dark mode glassmorphism with violetβ†’cyan accent gradient - Typography: Inter via Google Fonts - ============================================================ */ - -@layer reset, base, tokens, layout, components, utilities; - -/* ---- Google Font ---- */ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); - -/* ============================================================ - Reset Layer - ============================================================ */ -@layer reset { - *, *::before, *::after { - box-sizing: border-box; - margin: 0; - padding: 0; - } - - html { - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - text-rendering: optimizeLegibility; - } -} - -/* ============================================================ - Design Tokens - ============================================================ */ -@layer tokens { - :root { - /* β€”β€” Surface palette β€”β€” */ - --surface-base: hsl(225, 15%, 8%); - --surface-raised: hsl(225, 14%, 11%); - --surface-overlay: hsla(225, 18%, 14%, 0.72); - --surface-hover: hsla(225, 16%, 18%, 0.6); - --surface-active: hsla(225, 16%, 22%, 0.7); - - /* β€”β€” Accent gradient β€”β€” */ - --accent-violet: hsl(258, 80%, 65%); - --accent-blue: hsl(220, 85%, 60%); - --accent-cyan: hsl(185, 75%, 55%); - --accent-gradient: linear-gradient(135deg, var(--accent-violet), var(--accent-blue), var(--accent-cyan)); - - /* β€”β€” Semantic colors β€”β€” */ - --color-success: hsl(145, 70%, 50%); - --color-error: hsl(0, 75%, 60%); - --color-warning: hsl(40, 85%, 55%); - --color-agent: hsl(270, 80%, 65%); - - /* β€”β€” Text β€”β€” */ - --text-primary: hsl(0, 0%, 93%); - --text-secondary: hsl(225, 10%, 62%); - --text-tertiary: hsl(225, 10%, 42%); - --text-accent: var(--accent-cyan); - - /* β€”β€” Borders β€”β€” */ - --border-subtle: hsla(225, 15%, 30%, 0.35); - --border-focus: var(--accent-blue); - - /* β€”β€” Radii β€”β€” */ - --radius-sm: 6px; - --radius-md: 10px; - --radius-lg: 16px; - --radius-xl: 20px; - - /* β€”β€” Spacing scale (4px base) β€”β€” */ - --space-1: 4px; - --space-2: 8px; - --space-3: 12px; - --space-4: 16px; - --space-5: 20px; - --space-6: 24px; - --space-8: 32px; - - /* β€”β€” Typography β€”β€” */ - --font-sans: 'Inter', system-ui, -apple-system, sans-serif; - --font-mono: 'SF Mono', 'Fira Code', monospace; - - --text-xs: 0.6875rem; /* 11px */ - --text-sm: 0.75rem; /* 12px */ - --text-base: 0.8125rem; /* 13px */ - --text-md: 0.875rem; /* 14px */ - --text-lg: 1rem; /* 16px */ - - /* β€”β€” Shadows β€”β€” */ - --shadow-palette: 0 25px 60px -12px hsla(0, 0%, 0%, 0.6), - 0 0 0 1px var(--border-subtle); - - /* β€”β€” Transitions β€”β€” */ - --ease-out: cubic-bezier(0.16, 1, 0.3, 1); - --duration-fast: 120ms; - --duration-normal: 200ms; - --duration-slow: 350ms; - - /* β€”β€” Glass β€”β€” */ - --glass-blur: 40px; - --glass-saturation: 1.8; - } -} - -/* ============================================================ - Base Layer - ============================================================ */ -@layer base { - html, body { - height: 100%; - width: 100%; - overflow: hidden; - background: transparent; - color: var(--text-primary); - font-family: var(--font-sans); - font-size: var(--text-base); - line-height: 1.5; - font-weight: 400; - user-select: none; - } - - body { - display: flex; - align-items: flex-start; - justify-content: center; - padding-block-start: 10vh; - } - - /* Scrollbar theming */ - ::-webkit-scrollbar { width: 6px; } - ::-webkit-scrollbar-track { background: transparent; } - ::-webkit-scrollbar-thumb { - background: hsla(225, 15%, 30%, 0.4); - border-radius: 3px; - } - ::-webkit-scrollbar-thumb:hover { - background: hsla(225, 15%, 40%, 0.6); - } -} - -/* ============================================================ - Layout Layer - ============================================================ */ -@layer layout { - .app-layout { - width: min(720px, 92vw); - display: flex; - flex-direction: column; - gap: var(--space-4); - } - - .top-row { - display: flex; - gap: var(--space-3); - align-items: center; - } - - .palette-container { - max-height: calc(520px - 56px - var(--space-4)); - display: flex; - flex-direction: column; - background: var(--surface-overlay); - backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); - -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); - border-radius: var(--radius-xl); - box-shadow: var(--shadow-palette); - border: 1px solid var(--border-subtle); - overflow: hidden; - position: relative; - } - - .content-area { - display: grid; - grid-template-columns: 1fr 1fr; - flex: 1; - min-height: 0; - overflow: hidden; - } - - .content-area.no-preview { - grid-template-columns: 1fr; - } -} - -/* ============================================================ - Components Layer - ============================================================ */ -@layer components { - - /* ---- Search Pill & Buttons ---- */ - .search-pill { - flex: 1; - height: 56px; - display: flex; - align-items: center; - gap: var(--space-3); - padding: 0 var(--space-5); - background: var(--surface-overlay); - backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); - -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); - border-radius: 28px; - box-shadow: var(--shadow-palette); - border: 1px solid var(--border-subtle); - -webkit-app-region: drag; - } - - .search-pill__icon { - width: 20px; - height: 20px; - color: var(--text-secondary); - flex-shrink: 0; - transition: color var(--duration-fast) var(--ease-out); - } - - .search-pill:focus-within .search-pill__icon { - color: var(--text-accent); - } - - .search-pill__input { - flex: 1; - background: none; - border: none; - outline: none; - font-family: var(--font-sans); - font-size: 1.1rem; - font-weight: 300; - color: var(--text-primary); - caret-color: var(--accent-cyan); - letter-spacing: -0.01em; - -webkit-app-region: no-drag; - } - - .search-pill__input::placeholder { - color: var(--text-tertiary); - font-weight: 300; - } - - .search-pill__badge { - display: flex; - align-items: center; - gap: var(--space-1); - padding: 2px var(--space-2); - background: var(--surface-hover); - border-radius: var(--radius-sm); - font-size: var(--text-xs); - font-weight: 500; - color: var(--text-secondary); - flex-shrink: 0; - border: 1px solid var(--border-subtle); - transition: all var(--duration-fast) var(--ease-out); - } - - .floating-actions { - display: flex; - gap: var(--space-3); - } - - .floating-btn { - width: 56px; - height: 56px; - border-radius: 50%; - background: var(--surface-overlay); - backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); - -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); - border: 1px solid var(--border-subtle); - box-shadow: var(--shadow-palette); - display: flex; - align-items: center; - justify-content: center; - color: var(--text-secondary); - cursor: pointer; - transition: all var(--duration-fast) var(--ease-out); - -webkit-app-region: no-drag; - } - - .floating-btn:hover { - background: var(--surface-hover); - color: var(--text-primary); - } - - /* ---- Telemetry Strip ---- */ - .telemetry-strip { - display: flex; - align-items: center; - gap: var(--space-5); - padding: var(--space-2) var(--space-5); - border-block-end: 1px solid var(--border-subtle); - flex-shrink: 0; - overflow-x: auto; - overflow-y: hidden; - } - - .telemetry-item { - display: flex; - align-items: center; - gap: var(--space-2); - white-space: nowrap; - } - - .telemetry-item__dot { - width: 6px; - height: 6px; - border-radius: 50%; - flex-shrink: 0; - animation: pulse-dot 2.5s ease-in-out infinite; - } - - .telemetry-item__dot--green { background: var(--color-success); } - .telemetry-item__dot--blue { background: var(--accent-blue); } - .telemetry-item__dot--violet { background: var(--accent-violet); } - .telemetry-item__dot--cyan { background: var(--accent-cyan); } - - .telemetry-item__label { - font-size: var(--text-xs); - font-weight: 500; - color: var(--text-secondary); - } - - .telemetry-item__value { - font-size: var(--text-xs); - font-weight: 600; - color: var(--text-primary); - font-family: var(--font-mono); - } - - /* ---- Results Panel ---- */ - .results-panel { - overflow-y: auto; - padding: var(--space-2) 0; - contain: layout style; - } - - .results-empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: var(--space-8) var(--space-6); - gap: var(--space-3); - color: var(--text-tertiary); - } - - .results-empty__icon { - font-size: 2rem; - opacity: 0.5; - } - - .results-empty__text { - font-size: var(--text-sm); - font-weight: 400; - text-align: center; - max-width: 200px; - } - - .result-item { - display: flex; - align-items: center; - gap: var(--space-3); - padding: var(--space-2) var(--space-5); - cursor: pointer; - transition: background var(--duration-fast) var(--ease-out); - position: relative; - } - - .result-item:hover:not(.result-item--active) { - background: var(--surface-hover); - } - - .result-item--active { - background: var(--surface-active); - } - - .result-item--active::before { - content: ''; - position: absolute; - inset-inline-start: 0; - inset-block-start: 50%; - transform: translateY(-50%); - width: 3px; - height: 60%; - border-radius: 0 2px 2px 0; - background: var(--accent-gradient); - } - - /* Agent result styling */ - .result-item--agent { - background: hsla(270, 50%, 20%, 0.2); - border-left: 2px solid var(--color-agent); - } - - .result-item--agent .result-item__category { - background: hsla(270, 60%, 40%, 0.3); - color: var(--color-agent); - } - - .result-item__icon { - width: 28px; - height: 28px; - display: flex; - align-items: center; - justify-content: center; - font-size: 1rem; - background: var(--surface-hover); - border-radius: var(--radius-sm); - flex-shrink: 0; - border: 1px solid var(--border-subtle); - } - - .result-item__content { - flex: 1; - min-width: 0; - } - - .result-item__title { - font-size: var(--text-base); - font-weight: 500; - color: var(--text-primary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .result-item__subtitle { - font-size: var(--text-xs); - color: var(--text-secondary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - margin-block-start: 1px; - } - - .result-item__category { - font-size: var(--text-xs); - font-weight: 500; - color: var(--text-tertiary); - padding: 1px var(--space-2); - background: var(--surface-hover); - border-radius: var(--radius-sm); - flex-shrink: 0; - } - - /* ---- Preview Panel ---- */ - .preview-panel { - border-inline-start: 1px solid var(--border-subtle); - padding: var(--space-5); - overflow-y: auto; - display: flex; - flex-direction: column; - gap: var(--space-4); - } - - .preview-panel__empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100%; - color: var(--text-tertiary); - gap: var(--space-3); - } - - .preview-panel__empty-icon { - font-size: 2rem; - opacity: 0.4; - } - - .preview-panel__empty-text { - font-size: var(--text-sm); - text-align: center; - } - - .preview-header { - display: flex; - align-items: center; - gap: var(--space-3); - } - - .preview-header__icon { - font-size: 1.5rem; - } - - .preview-header__title { - font-size: var(--text-md); - font-weight: 600; - color: var(--text-primary); - } - - .preview-body { - display: flex; - flex-direction: column; - gap: var(--space-3); - } - - .preview-body__description { - font-size: var(--text-sm); - color: var(--text-secondary); - line-height: 1.6; - } - - .preview-meta { - display: flex; - flex-direction: column; - gap: var(--space-2); - padding: var(--space-3); - background: var(--surface-raised); - border-radius: var(--radius-md); - border: 1px solid var(--border-subtle); - } - - .preview-meta__row { - display: flex; - justify-content: space-between; - align-items: center; - padding: var(--space-1) var(--space-2); - border-radius: var(--radius-sm); - margin: -2px calc(-1 * var(--space-2)); - transition: background var(--duration-fast) var(--ease-out); - } - - .preview-meta__row:hover { - background: var(--surface-hover); - } - - .preview-meta__label { - font-size: var(--text-xs); - color: var(--text-tertiary); - font-weight: 500; - } - - .preview-meta__value { - font-size: var(--text-xs); - color: var(--text-primary); - font-weight: 500; - font-family: var(--font-mono); - } - - /* ---- Agent Panel ---- */ - .agent-panel { - display: none; - flex-direction: column; - padding: var(--space-4) var(--space-5); - overflow-y: auto; - max-height: 280px; - border-block-start: 1px solid var(--border-subtle); - } - - .agent-panel.active { - display: flex; - } - - .agent-response { - display: flex; - flex-direction: column; - gap: var(--space-3); - padding: var(--space-4); - background: var(--surface-raised); - border-radius: var(--radius-md); - border: 1px solid var(--border-subtle); - } - - .agent-response--success { - border-color: hsla(145, 70%, 50%, 0.3); - } - - .agent-response--error { - border-color: hsla(0, 75%, 60%, 0.3); - } - - .agent-response--processing { - border-color: hsla(270, 80%, 65%, 0.3); - } - - .agent-header { - display: flex; - align-items: center; - gap: var(--space-2); - } - - .agent-header__icon { - font-size: 1.2rem; - } - - .agent-header__title { - font-size: var(--text-md); - font-weight: 600; - color: var(--text-primary); - flex: 1; - } - - .agent-header__timing { - font-size: var(--text-xs); - font-family: var(--font-mono); - color: var(--text-tertiary); - padding: 1px var(--space-2); - background: var(--surface-hover); - border-radius: var(--radius-sm); - } - - .agent-body { - display: flex; - flex-direction: column; - gap: var(--space-3); - } - - .agent-query { - font-size: var(--text-sm); - color: var(--text-secondary); - font-style: italic; - } - - .agent-summary { - font-size: var(--text-sm); - color: var(--text-primary); - line-height: 1.6; - white-space: pre-wrap; - word-break: break-word; - } - - /* ---- Agent Steps (Workflow Visualization) ---- */ - .agent-steps { - display: flex; - flex-direction: column; - gap: var(--space-2); - margin-block-start: var(--space-2); - } - - .agent-steps__header { - font-size: var(--text-xs); - font-weight: 600; - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 0.04em; - display: flex; - align-items: center; - gap: var(--space-2); - } - - .agent-steps__count { - font-weight: 500; - color: var(--text-tertiary); - font-family: var(--font-mono); - } - - .agent-step { - display: flex; - align-items: flex-start; - gap: var(--space-3); - padding: var(--space-2) var(--space-3); - background: var(--surface-overlay); - border-radius: var(--radius-sm); - border: 1px solid var(--border-subtle); - } - - .agent-step--success { - border-left: 2px solid var(--color-success); - } - - .agent-step--error { - border-left: 2px solid var(--color-error); - } - - .agent-step__indicator { - width: 20px; - height: 20px; - display: flex; - align-items: center; - justify-content: center; - font-size: var(--text-xs); - font-weight: 700; - border-radius: 50%; - flex-shrink: 0; - } - - .agent-step--success .agent-step__indicator { - background: hsla(145, 70%, 50%, 0.15); - color: var(--color-success); - } - - .agent-step--error .agent-step__indicator { - background: hsla(0, 75%, 60%, 0.15); - color: var(--color-error); - } - - .agent-step__content { - flex: 1; - min-width: 0; - } - - .agent-step__label { - font-size: var(--text-sm); - font-weight: 500; - color: var(--text-primary); - } - - .agent-step__output { - font-size: var(--text-xs); - color: var(--text-secondary); - font-family: var(--font-mono); - margin-block-start: 2px; - white-space: pre-wrap; - word-break: break-all; - max-height: 60px; - overflow-y: auto; - } - - .agent-step__error { - font-size: var(--text-xs); - color: var(--color-error); - margin-block-start: 2px; - } - - /* ---- Typing Animation ---- */ - .agent-thinking { - display: flex; - align-items: center; - gap: 4px; - padding: var(--space-2) 0; - } - - .typing-dot { - width: 6px; - height: 6px; - border-radius: 50%; - background: var(--color-agent); - animation: typing-bounce 1.4s infinite ease-in-out both; - } - - .typing-dot:nth-child(1) { animation-delay: 0s; } - .typing-dot:nth-child(2) { animation-delay: 0.16s; } - .typing-dot:nth-child(3) { animation-delay: 0.32s; } - - /* ---- Keyboard Shortcut Bar ---- */ - .shortcut-bar { - display: flex; - align-items: center; - gap: var(--space-5); - padding: var(--space-2) var(--space-5); - border-block-start: 1px solid var(--border-subtle); - flex-shrink: 0; - overflow-x: auto; - } - - .shortcut { - display: flex; - align-items: center; - gap: var(--space-1); - white-space: nowrap; - } - - .shortcut__key { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 18px; - height: 18px; - padding: 0 var(--space-1); - font-size: 10px; - font-weight: 600; - font-family: var(--font-sans); - color: var(--text-secondary); - background: var(--surface-raised); - border: 1px solid var(--border-subtle); - border-radius: 3px; - line-height: 1; - } - - .shortcut__label { - font-size: var(--text-xs); - color: var(--text-tertiary); - font-weight: 400; - } - - /* ---- Status indicator ---- */ - .status-indicator { - display: flex; - align-items: center; - gap: var(--space-1); - margin-inline-start: auto; - } - - .status-indicator__dot { - width: 5px; - height: 5px; - border-radius: 50%; - background: var(--color-success); - animation: pulse-dot 2s ease-in-out infinite; - } - - .status-indicator__label { - font-size: var(--text-xs); - color: var(--text-tertiary); - font-weight: 400; - } -} - -/* ============================================================ - Utilities - ============================================================ */ -@layer utilities { - .visually-hidden { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; - } -} - -/* ============================================================= */ -/* Extension Manager */ -/* ============================================================= */ - -.title-bar__action { - position: absolute; - right: 8px; - top: 50%; - transform: translateY(-50%); - -webkit-app-region: no-drag; - background: transparent; - border: none; - cursor: pointer; - font-size: 13px; - line-height: 1; - padding: 4px 6px; - border-radius: var(--radius-sm); - opacity: 0.7; - transition: opacity var(--duration-fast) var(--ease-out), - background var(--duration-fast) var(--ease-out); -} -.title-bar__action:hover { - opacity: 1; - background: var(--surface-hover); -} -.title-bar__action--left { right: auto; left: 8px; } - -/* Settings panel */ -.ext-panel--narrow { width: min(440px, 92%); } -.settings-body { padding: 14px 16px; display: flex; flex-direction: column; gap: 12px; } -.settings-row { display: flex; flex-direction: column; gap: 6px; } -.settings-row--inline { flex-direction: row; align-items: center; justify-content: space-between; } -.settings-row__label { font-size: var(--text-base); color: var(--text-primary); } -.settings-input { - font-size: var(--text-base); - font-family: var(--font-mono); - color: var(--text-primary); - background: var(--surface-overlay); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - padding: 8px 10px; - outline: none; -} -.settings-input:focus { border-color: var(--border-focus); } -.settings-input--select { font-family: var(--font-sans); } -.settings-hint { margin: -4px 0 0; font-size: var(--text-xs); color: var(--text-tertiary); } -.settings-hint code { font-family: var(--font-mono); color: var(--text-secondary); } -.settings-footer { - display: flex; align-items: center; justify-content: flex-end; gap: 12px; - padding: 12px 16px; border-top: 1px solid var(--border-subtle); -} -.settings-status { font-size: var(--text-sm); color: var(--color-success); margin-right: auto; } - -.ext-overlay { - position: absolute; - inset: 0; - z-index: 50; - display: flex; - align-items: center; - justify-content: center; - background: hsla(225, 30%, 4%, 0.55); - backdrop-filter: blur(6px); -} -.ext-overlay[hidden] { display: none; } - -.ext-panel { - width: min(560px, 92%); - max-height: 80%; - display: flex; - flex-direction: column; - background: var(--surface-raised); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-palette); - overflow: hidden; -} - -.ext-panel__header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 14px 16px; - border-bottom: 1px solid var(--border-subtle); -} -.ext-panel__title { - margin: 0; - font-size: var(--text-md); - font-weight: 600; - color: var(--text-primary); -} -.ext-panel__actions { display: flex; gap: 8px; align-items: center; } - -.ext-list { - overflow-y: auto; - padding: 10px; - display: flex; - flex-direction: column; - gap: 8px; -} - -.ext-card { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 12px; - padding: 12px; - background: var(--surface-overlay); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-md); -} -.ext-card__main { min-width: 0; flex: 1; } -.ext-card__head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; } -.ext-card__name { font-size: var(--text-base); font-weight: 600; color: var(--text-primary); } -.ext-card__meta { font-size: var(--text-xs); color: var(--text-tertiary); } -.ext-card__desc { margin-top: 4px; font-size: var(--text-sm); color: var(--text-secondary); } -.ext-card__perms { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 4px; } -.ext-perm { - font-size: var(--text-xs); - font-family: var(--font-mono); - color: var(--accent-cyan); - background: hsla(185, 75%, 55%, 0.12); - border: 1px solid hsla(185, 75%, 55%, 0.25); - border-radius: var(--radius-sm); - padding: 1px 6px; -} -.ext-card__controls { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; } - -/* Toggle switch */ -.ext-toggle { display: inline-flex; cursor: pointer; } -.ext-toggle__input { position: absolute; opacity: 0; width: 0; height: 0; } -.ext-toggle__track { - width: 36px; height: 20px; - background: var(--surface-active); - border-radius: 999px; - position: relative; - transition: background var(--duration-fast) var(--ease-out); -} -.ext-toggle__thumb { - position: absolute; top: 2px; left: 2px; - width: 16px; height: 16px; - background: var(--text-secondary); - border-radius: 50%; - transition: transform var(--duration-fast) var(--ease-out), - background var(--duration-fast) var(--ease-out); -} -.ext-toggle__input:checked + .ext-toggle__track { background: var(--accent-blue); } -.ext-toggle__input:checked + .ext-toggle__track .ext-toggle__thumb { - transform: translateX(16px); - background: white; -} - -/* Buttons */ -.ext-btn { - -webkit-app-region: no-drag; - font-size: var(--text-sm); - font-family: var(--font-sans); - color: var(--text-secondary); - background: var(--surface-hover); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - padding: 6px 12px; - cursor: pointer; - transition: background var(--duration-fast) var(--ease-out), - color var(--duration-fast) var(--ease-out); -} -.ext-btn:hover { background: var(--surface-active); color: var(--text-primary); } -.ext-btn--primary { background: var(--accent-blue); color: white; border-color: transparent; } -.ext-btn--primary:hover { filter: brightness(1.1); color: white; } -.ext-btn--danger:hover { background: hsla(0, 75%, 60%, 0.18); color: var(--color-error); } -.ext-btn--icon { padding: 6px 9px; } -.ext-btn--sm { padding: 4px 9px; font-size: var(--text-xs); } - -.ext-empty { - display: flex; - flex-direction: column; - align-items: center; - gap: 6px; - padding: 40px 16px; - text-align: center; -} -.ext-empty__icon { font-size: 28px; } -.ext-empty__text { font-size: var(--text-base); color: var(--text-secondary); } -.ext-empty__hint { font-size: var(--text-xs); color: var(--text-tertiary); } - -/* Consent modal */ -.ext-consent { - position: absolute; - inset: 0; - z-index: 60; - display: flex; - align-items: center; - justify-content: center; - background: hsla(225, 30%, 4%, 0.6); -} -.ext-consent[hidden] { display: none; } -.ext-consent__box { - width: min(420px, 88%); - background: var(--surface-raised); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-palette); - padding: 18px; -} -.ext-consent__title { margin: 0 0 4px; font-size: var(--text-md); color: var(--text-primary); } -.ext-consent__sub { margin: 0 0 10px; font-size: var(--text-sm); color: var(--text-secondary); } -.ext-consent__perms { list-style: none; margin: 0 0 16px; padding: 0; display: flex; flex-direction: column; gap: 8px; } -.ext-consent__perm { - display: flex; flex-direction: column; gap: 2px; - padding: 8px 10px; - background: var(--surface-overlay); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); -} -.ext-consent__perm-name { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--accent-cyan); } -.ext-consent__perm-why { font-size: var(--text-xs); color: var(--text-secondary); } -.ext-consent__actions { display: flex; justify-content: flex-end; gap: 8px; } - -/* ============================================================================ - RAYCAST REDESIGN (unlayered β€” overrides everything above by cascade order) - Modern, sleek, minimal: dark translucent window, large search, soft selection - highlight, full-width single-column results, compact footer. - ============================================================================ */ - -:root { - /* Raycast-like dark palette */ - --rc-bg: rgba(24, 24, 27, 0.72); - --rc-bg-solid: #18181b; - --rc-border: rgba(255, 255, 255, 0.09); - --rc-hairline: rgba(255, 255, 255, 0.07); - --rc-text: rgba(255, 255, 255, 0.92); - --rc-text-dim: rgba(255, 255, 255, 0.5); - --rc-text-faint: rgba(255, 255, 255, 0.34); - --rc-sel: rgba(255, 255, 255, 0.09); - --rc-accent: #ff6363; /* Raycast-ish coral accent */ - --rc-radius: 12px; - --rc-font: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif; -} - -body { - background: transparent; - font-family: var(--rc-font); -} - -/* β€”β€” Window β€”β€” */ -.palette-container { - background: var(--rc-bg); - -webkit-backdrop-filter: blur(34px) saturate(1.7); - backdrop-filter: blur(34px) saturate(1.7); - border: 1px solid var(--rc-border); - border-radius: var(--rc-radius); - box-shadow: 0 24px 70px -12px rgba(0, 0, 0, 0.65), 0 0 0 0.5px rgba(0,0,0,0.4); - overflow: hidden; - display: flex; - flex-direction: column; - height: 100vh; -} - -/* β€”β€” Drag bar: invisible, just a thin drag region with corner actions β€”β€” */ -.title-bar { - height: 0; - min-height: 0; - padding: 0; - -webkit-app-region: drag; -} -.title-bar__label { display: none; } -.title-bar__action { - position: absolute; - top: 14px; - z-index: 5; - -webkit-app-region: no-drag; - background: transparent; - border: none; - cursor: pointer; - font-size: 13px; - line-height: 1; - padding: 4px; - border-radius: 6px; - opacity: 0.45; - transition: opacity 120ms ease, background 120ms ease; -} -.title-bar__action:hover { opacity: 0.95; background: var(--rc-sel); } -.title-bar__action { right: 12px; } -.title-bar__action--left { left: auto; right: 40px; } - -/* β€”β€” Search bar β€”β€” */ -.search-bar { - display: flex; - align-items: center; - gap: 12px; - height: 58px; - padding: 0 18px; - border-bottom: 1px solid var(--rc-hairline); - background: transparent; - flex-shrink: 0; -} -.search-bar__icon { - width: 20px; height: 20px; - color: var(--rc-text-faint); - flex-shrink: 0; -} -.search-bar__input { - flex: 1; - background: transparent; - border: none; - outline: none; - color: var(--rc-text); - font-family: var(--rc-font); - font-size: 19px; - font-weight: 400; - letter-spacing: -0.01em; - caret-color: var(--rc-accent); -} -.search-bar__input::placeholder { color: var(--rc-text-faint); } -.search-bar__badge { - font-size: 11px; - color: var(--rc-text-faint); - background: var(--rc-sel); - border: 1px solid var(--rc-hairline); - border-radius: 6px; - padding: 3px 7px; - font-family: var(--rc-font); -} - -/* β€”β€” Telemetry strip: removed for Raycast minimalism β€”β€” */ -.telemetry-strip { display: none !important; } - -/* β€”β€” Content / results (full-width single column by default) β€”β€” */ -.content-area { - display: flex; - flex: 1; - min-height: 0; - overflow: hidden; -} -.results-panel { - flex: 1 1 auto; - overflow-y: auto; - padding: 8px; - display: flex; - flex-direction: column; - gap: 1px; -} -.preview-panel { - flex: 0 0 44%; - border-left: 1px solid var(--rc-hairline); - overflow-y: auto; -} -.content-area.no-preview .preview-panel { display: none; } - -/* Agent console only occupies space when active (keeps the footer pinned). */ -.agent-panel:not(.active) { display: none; } -.agent-panel.active { flex: 1; min-height: 0; overflow-y: auto; } - -/* Section header (Raycast-style grouping) */ -.results-section__header { - font-size: 11px; - font-weight: 600; - letter-spacing: 0.04em; - text-transform: uppercase; - color: var(--rc-text-faint); - padding: 10px 10px 4px; - user-select: none; -} - -/* β€”β€” Result rows β€”β€” */ -.result-item { - display: flex; - align-items: center; - gap: 11px; - padding: 8px 10px; - border-radius: 8px; - border: none; - cursor: default; - background: transparent; - transition: background 90ms ease; -} -.result-item:hover { background: rgba(255, 255, 255, 0.045); } -.result-item--active, -.result-item--active:hover { background: var(--rc-sel); box-shadow: none; border-left: 0; } -/* Kill the legacy left-accent pseudo-element so selection is a clean fill. */ -.result-item::before, .result-item::after { content: none !important; display: none !important; } -.result-item__icon { - width: 22px; - height: 22px; - display: flex; - align-items: center; - justify-content: center; - font-size: 17px; - flex-shrink: 0; -} -.result-item__content { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; } -.result-item__title { - font-size: 14px; - font-weight: 450; - color: var(--rc-text); - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - letter-spacing: -0.01em; -} -.result-item__subtitle { - font-size: 12px; - color: var(--rc-text-faint); - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; -} -.result-item__category { - font-size: 12px; - color: var(--rc-text-faint); - flex-shrink: 0; - padding-left: 8px; - opacity: 0; - transition: opacity 90ms ease; -} -.result-item--active .result-item__category { opacity: 1; } -.result-item--agent .result-item__icon { filter: none; } - -/* Empty state */ -.results-empty { - display: flex; flex-direction: column; align-items: center; justify-content: center; - gap: 8px; padding: 56px 16px; color: var(--rc-text-faint); margin: auto; -} -.results-empty__icon { font-size: 26px; opacity: 0.5; } -.results-empty__text { font-size: 13px; } - -/* β€”β€” Footer β€”β€” */ -.shortcut-bar { - display: flex; - align-items: center; - gap: 14px; - height: 40px; - padding: 0 12px 0 14px; - border-top: 1px solid var(--rc-hairline); - background: rgba(0, 0, 0, 0.18); - flex-shrink: 0; -} -.shortcut-bar::before { - content: "⌘ SuperSearch"; - font-size: 12px; - font-weight: 500; - color: var(--rc-text-dim); - margin-right: auto; - letter-spacing: -0.01em; -} -.shortcut { display: flex; align-items: center; gap: 5px; } -.shortcut__key { - font-size: 11px; - color: var(--rc-text-dim); - background: var(--rc-sel); - border: 1px solid var(--rc-hairline); - border-radius: 5px; - min-width: 18px; - height: 18px; - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0 5px; - font-family: var(--rc-font); -} -.shortcut__label { font-size: 12px; color: var(--rc-text-faint); } - -/* β€”β€” Scrollbar (thin, Raycast-like) β€”β€” */ -.results-panel::-webkit-scrollbar, .preview-panel::-webkit-scrollbar { width: 8px; } -.results-panel::-webkit-scrollbar-thumb, .preview-panel::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.12); - border-radius: 4px; - border: 2px solid transparent; - background-clip: padding-box; -} -.results-panel::-webkit-scrollbar-track { background: transparent; } - -/* β€”β€” Mode badge when in agent mode β€”β€” */ -#mode-badge { white-space: nowrap; } - -/* ============================================================================ - GLASS COMPOSER (latest override β€” vivid blue frosted glass + pill toolbar) - ============================================================================ */ - -.palette-container { - background: - linear-gradient(135deg, - hsla(222, 70%, 32%, 0.62) 0%, - hsla(214, 80%, 42%, 0.50) 46%, - hsla(196, 85%, 55%, 0.42) 100%); - -webkit-backdrop-filter: blur(40px) saturate(1.7); - backdrop-filter: blur(40px) saturate(1.7); - border: 1px solid rgba(255, 255, 255, 0.22); - border-radius: 26px; - box-shadow: - 0 32px 80px -22px hsla(220, 80%, 8%, 0.7), - inset 0 1px 0 rgba(255, 255, 255, 0.20), - inset 0 0 0 0.5px rgba(255, 255, 255, 0.06); -} - -/* Corner action buttons (settings / extensions) β€” subtle glass */ -.title-bar__action { color: #fff; opacity: 0.55; } -.title-bar__action:hover { opacity: 1; background: rgba(255, 255, 255, 0.16); } - -/* β€”β€” Big "Ask anything…" input β€”β€” */ -.search-bar { - height: 72px; - padding: 0 24px; - border-bottom: none; - gap: 0; -} -.search-bar__icon { display: none; } -.search-bar__badge { display: none; } -.search-bar__input { - font-size: 26px; - font-weight: 500; - color: #fff; - letter-spacing: -0.015em; - caret-color: #8ff09d; -} -.search-bar__input::placeholder { color: rgba(255, 255, 255, 0.5); font-weight: 500; } - -/* β€”β€” Results on glass β€”β€” */ -.results-panel { position: relative; padding: 6px 14px; } -.results-section__header { color: rgba(255, 255, 255, 0.55); padding: 8px 8px 4px; } -.result-item { padding: 12px 16px; z-index: 1; display: flex; align-items: center; gap: 16px; height: 64px; } -.result-item:hover { background: transparent; } -.result-item--active, .result-item--active:hover { background: transparent; } /* moving highlight handles it */ -.result-item__title { color: #fff; font-weight: 500; font-size: 1.15rem; } -.result-item__subtitle { color: rgba(255, 255, 255, 0.55); } - -.result-item__icon { - width: 36px !important; - height: 36px !important; - border-radius: 10px !important; - background: transparent !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; - font-size: 20px !important; - border: none !important; -} - -.result-item__action-hint { - margin-left: auto; - display: none; - align-items: center; - gap: 8px; - color: rgba(255,255,255,0.4); - font-size: 0.85rem; - z-index: 2; -} -.result-item--active .result-item__action-hint { - display: flex; -} -.result-item__action-key { - background: rgba(255,255,255,0.15); - padding: 2px 6px; - border-radius: 4px; - font-size: 0.75rem; - color: #fff; -} -.results-empty, .results-empty__text { color: rgba(255, 255, 255, 0.6); } - -/* Sliding selection highlight (Spotlight-smooth) */ -.selection-highlight { - position: absolute; - left: 8px; - right: 8px; - top: 0; - height: 0; - border-radius: 14px; - background: rgba(255, 255, 255, 0.12); - box-shadow: none; - pointer-events: none; - opacity: 0; - z-index: 0; - transition: transform 165ms cubic-bezier(0.22, 1, 0.36, 1), - height 165ms cubic-bezier(0.22, 1, 0.36, 1), - opacity 120ms ease; -} -.selection-highlight.no-anim { transition: none; } - -/* β€”β€” Hide the keyboard hint footer; the composer toolbar replaces it β€”β€” */ -.shortcut-bar { display: none !important; } - -/* β€”β€” Composer toolbar (pills + voice + green submit) β€”β€” */ -.composer-toolbar { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 16px 16px; - flex-shrink: 0; -} -.ctool-spacer { flex: 1; } -.ctool-pill, .ctool-btn { - -webkit-app-region: no-drag; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - height: 42px; - border-radius: 999px; - background: rgba(255, 255, 255, 0.12); - border: 1px solid rgba(255, 255, 255, 0.16); - color: rgba(255, 255, 255, 0.94); - font-family: var(--rc-font); - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: background 130ms ease, transform 90ms ease, box-shadow 130ms ease; -} -.ctool-pill { padding: 0 18px; } -.ctool-btn { width: 42px; } -.ctool-pill svg, .ctool-btn svg { width: 18px; height: 18px; } -.ctool-pill:hover, .ctool-btn:hover { background: rgba(255, 255, 255, 0.2); } -.ctool-pill:active, .ctool-btn:active { transform: scale(0.95); } -.ctool-btn--submit { - background: linear-gradient(180deg, #8ff09d, #5fd97a); - border-color: transparent; - color: #0a2c14; - box-shadow: 0 4px 14px -2px hsla(140, 70%, 45%, 0.5); -} -.ctool-btn--submit:hover { background: linear-gradient(180deg, #9bf6a8, #6ee389); } -.ctool-btn--submit svg { stroke-width: 2.4; } - -/* ============================================================================ - SPOTLIGHT-STYLE RESULTS (tall airy rows, big titles, selected-row accessory) - ============================================================================ */ - -/* Flat list (no section labels) like Spotlight. */ -.results-section__header { display: none !important; } - -.results-panel { padding: 8px 12px; } - -.result-item { - min-height: 60px; /* tall airy rows; flex-shrink:0 stops them collapsing */ - flex-shrink: 0; - padding: 0 14px; - gap: 15px; - border-radius: 12px; -} -.result-item__icon { - width: 38px; - height: 38px; - border-radius: 9px; - font-size: 26px; -} -.result-item__title { - font-size: 20px; - font-weight: 500; - letter-spacing: -0.012em; -} -/* Spotlight shows title-only rows. */ -.result-item__subtitle { display: none; } - -/* Right-side accessory: action hint + key chip, only on the selected row. */ -.result-item__category { display: none; } /* replaced by the action hint */ -.result-item__action-hint { - display: inline-flex; - align-items: center; - gap: 10px; - flex-shrink: 0; - padding-left: 12px; - opacity: 0; - transition: opacity 120ms ease; - color: rgba(255, 255, 255, 0.6); - font-size: 15px; - white-space: nowrap; -} -.result-item--active .result-item__action-hint { opacity: 1; } -.result-item__action-key { - font-size: 12px; - color: rgba(255, 255, 255, 0.85); - background: rgba(255, 255, 255, 0.16); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 6px; - padding: 3px 9px; - min-width: 24px; - text-align: center; - font-family: var(--rc-font); -} - -/* Larger, slightly brighter sliding highlight to match the tall rows. */ -.selection-highlight { - left: 10px; - right: 10px; - border-radius: 12px; - background: rgba(255, 255, 255, 0.14); -}