+
e.stopPropagation()}
+ >
+
+
+
setQuery(e.target.value)}
+ onKeyDown={onInputKey}
+ placeholder={placeholder}
+ aria-label="Search query"
+ aria-controls={listboxId}
+ aria-activedescendant={
+ results[selectedIndex] ? `cp-opt-${selectedIndex}` : undefined
+ }
+ aria-autocomplete="list"
+ className="cp-input"
+ autoComplete="off"
+ autoCorrect="off"
+ spellCheck={false}
+ />
+
+
+
+ {loading &&
}
+ {errored &&
}
+ {showInitial &&
}
+ {showEmpty && (
+
+ )}
+ {!loading && !errored && results.length > 0 && (
+ <>
+
+ Results
+
+ · {results.length} {results.length === 1 ? 'match' : 'matches'}
+
+
+
+
+
+
,
+ document.body,
+ );
+}
+
+// ─── State components ─────────────────────────────────────────────────────────
+
+function SkeletonRows() {
+ return (
+
+ {[0, 1, 2].map((i) => (
+ -
+
+
+
+
+ ))}
+
+ );
+}
+
+function InitialState({ recents }: { recents: RecentDoc[] }) {
+ if (recents.length === 0) {
+ return (
+
+
Start typing to search the vault
+
+ Results stream as you type. Press esc to close.
+
+
+ );
+ }
+ return (
+ <>
+
+ Recent
+ · {recents.length} indexed
+
+
+ >
+ );
+}
+
+function EmptyState({
+ query,
+ emptyActions,
+ recentSearches,
+}: {
+ query: string;
+ emptyActions: EmptyAction[];
+ recentSearches: RecentSearch[];
+}) {
+ return (
+
+
+ No matches for {query} in this vault.
+
+
+ Try a shorter query, switch scopes, or jump to the index.
+
+ {emptyActions.length > 0 && (
+
+ {emptyActions.map((a) => (
+
+ {a.label}
+ {a.kbd && {a.kbd}}
+
+ ))}
+
+ )}
+ {recentSearches.length > 0 && (
+
+
Recent searches
+ {recentSearches.map((row) => (
+
+
+
{row.q}
+
{row.ago}
+
+ ))}
+
+ )}
+
+ );
+}
+
+function ErrorState({ onRetry }: { onRetry: () => void }) {
+ return (
+
+
Search is unavailable right now
+
+ The vault index didn’t respond.{' '}
+
+
+
+ );
+}
+
+// ─── Styles ───────────────────────────────────────────────────────────────────
+// Source preserved from docs-ai-vault-search verbatim. `:global(...)` wrappers
+// dropped (now plain global CSS, all class names are `cp-*` prefixed).
+
+const COMMAND_PALETTE_CSS = `
+.cp-result-snippet mark,
+.cp-result-title mark {
+ background: color-mix(in srgb, var(--cf-primary) 22%, transparent);
+ color: color-mix(in srgb, var(--cf-primary) 35%, var(--cf-fg));
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--cf-primary) 30%, transparent);
+ padding: 1px 5px 0;
+ border-radius: 3px;
+ font-weight: 600;
+ line-height: inherit;
+ vertical-align: baseline;
+ box-decoration-break: clone;
+ -webkit-box-decoration-break: clone;
+}
+[data-theme="light"] .cp-result-snippet mark,
+[data-theme="light"] .cp-result-title mark {
+ background: color-mix(in srgb, var(--cf-primary) 14%, transparent);
+ color: color-mix(in srgb, var(--cf-primary) 70%, #0c1729);
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--cf-primary) 28%, transparent);
+}
+
+.cp-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 200;
+ background:
+ radial-gradient(ellipse at center,
+ color-mix(in srgb, var(--cf-bg) 78%, #000) 0%,
+ color-mix(in srgb, var(--cf-bg) 92%, #000) 80%);
+ backdrop-filter: blur(28px) saturate(140%);
+ -webkit-backdrop-filter: blur(28px) saturate(140%);
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+ padding-top: 14vh;
+ padding-inline: 1rem;
+ animation: cp-overlay-in 140ms ease-out;
+}
+[data-theme="light"] .cp-overlay {
+ background:
+ radial-gradient(ellipse at center,
+ rgba(15, 23, 42, 0.45) 0%,
+ rgba(15, 23, 42, 0.62) 80%);
+}
+@keyframes cp-overlay-in { from { opacity: 0; } to { opacity: 1; } }
+
+.cp-surface {
+ width: min(40rem, 100%);
+ background:
+ linear-gradient(180deg,
+ color-mix(in srgb, var(--cf-bg) 90%, white) 0%,
+ var(--cf-bg) 100%);
+ border: 1px solid rgba(255, 255, 255, 0.10);
+ border-radius: 14px;
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.08),
+ inset 0 -1px 0 rgba(0, 0, 0, 0.4),
+ 0 0 0 1px rgba(0, 0, 0, 0.55),
+ 0 4px 8px rgba(0, 0, 0, 0.25),
+ 0 12px 24px rgba(0, 0, 0, 0.35),
+ 0 32px 64px -8px rgba(0, 0, 0, 0.55),
+ 0 64px 120px -24px rgba(0, 0, 0, 0.6);
+ overflow: hidden;
+ font-family: var(--font-sans);
+ animation: cp-surface-in 180ms cubic-bezier(0.16, 1, 0.3, 1);
+ display: flex;
+ flex-direction: column;
+ max-height: 72vh;
+}
+[data-theme="light"] .cp-surface {
+ background:
+ linear-gradient(180deg,
+ #ffffff 0%,
+ color-mix(in srgb, var(--cf-bg) 96%, #000) 100%);
+ border-color: rgba(15, 23, 42, 0.08);
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.6),
+ 0 0 0 1px rgba(15, 23, 42, 0.05),
+ 0 4px 8px rgba(15, 23, 42, 0.06),
+ 0 12px 24px rgba(15, 23, 42, 0.10),
+ 0 32px 64px -8px rgba(15, 23, 42, 0.18),
+ 0 64px 120px -24px rgba(15, 23, 42, 0.22);
+}
+@keyframes cp-surface-in {
+ from { opacity: 0; transform: translateY(-6px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+@media (max-width: 767px) {
+ .cp-overlay-sheet, .cp-overlay-auto {
+ align-items: flex-end;
+ padding-top: 0;
+ padding-inline: 0;
+ }
+ .cp-overlay-sheet .cp-surface, .cp-overlay-auto .cp-surface {
+ width: 100%;
+ max-height: 88vh;
+ border-radius: 16px 16px 0 0;
+ border-bottom: 0;
+ animation: cp-sheet-in 220ms cubic-bezier(0.16, 1, 0.3, 1);
+ }
+}
+@keyframes cp-sheet-in {
+ from { transform: translateY(100%); }
+ to { transform: translateY(0); }
+}
+
+.cp-input-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 0 18px;
+ height: 56px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.07);
+ flex-shrink: 0;
+}
+[data-theme="light"] .cp-input-row {
+ border-bottom-color: rgba(15, 23, 42, 0.08);
+}
+.cp-input-icon {
+ color: var(--cf-muted);
+ flex-shrink: 0;
+ width: 16px;
+ height: 16px;
+}
+.cp-input {
+ flex: 1;
+ min-width: 0;
+ background: transparent;
+ border: 0;
+ outline: none;
+ color: var(--cf-fg);
+ font-size: 16px;
+ line-height: 1;
+ font-family: var(--font-sans);
+ font-weight: 500;
+ letter-spacing: -0.01em;
+ caret-color: var(--cf-primary);
+ padding: 0;
+}
+.cp-input::placeholder {
+ color: color-mix(in srgb, var(--cf-muted) 70%, transparent);
+ opacity: 1;
+ font-weight: 400;
+}
+
+.cp-results-wrap {
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ min-height: 0;
+ scrollbar-width: thin;
+}
+.cp-results-meta {
+ padding: 14px 20px 8px;
+ display: flex;
+ align-items: baseline;
+ gap: 10px;
+ font-family: var(--font-sans);
+ font-weight: 600;
+ font-size: 10.5px;
+ line-height: 1;
+ color: color-mix(in srgb, var(--cf-muted) 75%, transparent);
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+}
+.cp-results-meta-label { color: color-mix(in srgb, var(--cf-muted) 90%, var(--cf-fg)); }
+.cp-results-meta-count {
+ font-family: var(--font-mono);
+ font-weight: 500;
+ font-size: 10.5px;
+ color: color-mix(in srgb, var(--cf-muted) 65%, transparent);
+ letter-spacing: 0;
+ text-transform: none;
+}
+.cp-results {
+ list-style: none;
+ margin: 0;
+ padding: 4px 8px 10px;
+}
+.cp-result {
+ position: relative;
+ display: grid;
+ grid-template-columns: 26px 1fr auto;
+ column-gap: 12px;
+ align-items: start;
+ padding: 11px 14px;
+ color: var(--cf-fg);
+ text-decoration: none;
+ cursor: pointer;
+ border-radius: 8px;
+ transition: background 80ms ease-out;
+ min-height: 44px;
+}
+.cp-result:hover { background: rgba(255, 255, 255, 0.025); }
+[data-theme="light"] .cp-result:hover { background: rgba(15, 23, 42, 0.03); }
+.cp-result[data-selected] {
+ background:
+ linear-gradient(90deg,
+ color-mix(in srgb, var(--cf-primary) 16%, transparent) 0%,
+ color-mix(in srgb, var(--cf-primary) 6%, transparent) 60%,
+ transparent 100%);
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--cf-primary) 16%, transparent);
+}
+.cp-result[data-selected]::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 10px;
+ bottom: 10px;
+ width: 3px;
+ border-radius: 0 2px 2px 0;
+ background:
+ linear-gradient(180deg,
+ color-mix(in srgb, var(--cf-primary) 75%, white) 0%,
+ var(--cf-primary) 100%);
+ box-shadow: 0 0 8px color-mix(in srgb, var(--cf-primary) 45%, transparent);
+}
+.cp-result-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 26px;
+ height: 26px;
+ border-radius: 6px;
+ background: rgba(255, 255, 255, 0.03);
+ border: 1px solid rgba(255, 255, 255, 0.04);
+ color: var(--cf-muted);
+ margin-top: 1px;
+ flex-shrink: 0;
+}
+[data-theme="light"] .cp-result-icon {
+ background: rgba(15, 23, 42, 0.025);
+ border-color: rgba(15, 23, 42, 0.06);
+}
+.cp-result[data-selected] .cp-result-icon {
+ background: color-mix(in srgb, var(--cf-primary) 18%, transparent);
+ border-color: color-mix(in srgb, var(--cf-primary) 32%, transparent);
+ color: color-mix(in srgb, var(--cf-primary) 30%, var(--cf-fg));
+}
+.cp-result-icon svg { width: 14px; height: 14px; }
+.cp-result-main { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
+.cp-result-title {
+ font-family: var(--font-sans);
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 1.35;
+ letter-spacing: -0.012em;
+ color: color-mix(in srgb, var(--cf-fg) 96%, white);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.cp-result[data-selected] .cp-result-title { color: #ffffff; }
+[data-theme="light"] .cp-result[data-selected] .cp-result-title { color: var(--cf-fg); }
+.cp-result-path {
+ font-family: var(--font-mono);
+ font-weight: 400;
+ font-size: 11px;
+ line-height: 1.3;
+ color: color-mix(in srgb, var(--cf-muted) 50%, transparent);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ letter-spacing: 0;
+}
+.cp-result-snippet {
+ font-family: var(--font-sans);
+ font-weight: 400;
+ font-size: 12.5px;
+ line-height: 1.45;
+ color: color-mix(in srgb, var(--cf-fg) 68%, transparent);
+ margin-top: 3px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.cp-result[data-selected] .cp-result-snippet {
+ color: color-mix(in srgb, var(--cf-fg) 88%, white);
+}
+[data-theme="light"] .cp-result[data-selected] .cp-result-snippet {
+ color: color-mix(in srgb, var(--cf-fg) 90%, transparent);
+}
+.cp-result-aside {
+ color: var(--cf-muted);
+ align-self: center;
+}
+
+.cp-kbd {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-family: var(--font-mono);
+ font-weight: 600;
+ font-size: 10.5px;
+ line-height: 1;
+ border: 1px solid rgba(255, 255, 255, 0.10);
+ border-radius: 4px;
+ padding: 0 5px;
+ color: var(--cf-fg);
+ background:
+ linear-gradient(180deg,
+ rgba(255, 255, 255, 0.07) 0%,
+ rgba(255, 255, 255, 0.035) 100%);
+ min-width: 20px;
+ height: 20px;
+ letter-spacing: 0;
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.06),
+ 0 1px 0 rgba(0, 0, 0, 0.3);
+}
+[data-theme="light"] .cp-kbd {
+ background: linear-gradient(180deg, #ffffff 0%, rgba(15, 23, 42, 0.04) 100%);
+ border-color: rgba(15, 23, 42, 0.12);
+ color: var(--cf-fg);
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.5),
+ 0 1px 0 rgba(15, 23, 42, 0.04);
+}
+.cp-kbd-enter {
+ min-width: 26px;
+ height: 22px;
+ padding: 0 8px;
+ border-radius: 5px;
+ border-color: color-mix(in srgb, var(--cf-primary) 40%, transparent);
+ color: color-mix(in srgb, var(--cf-primary) 30%, var(--cf-fg));
+ background:
+ linear-gradient(180deg,
+ color-mix(in srgb, var(--cf-primary) 22%, transparent),
+ color-mix(in srgb, var(--cf-primary) 10%, transparent));
+ font-weight: 600;
+ font-size: 11px;
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.08),
+ 0 1px 0 rgba(0, 0, 0, 0.25);
+}
+
+.cp-footer {
+ display: flex;
+ align-items: center;
+ height: 40px;
+ padding: 0 14px;
+ background: rgba(255, 255, 255, 0.015);
+ border-top: 1px solid rgba(255, 255, 255, 0.07);
+ gap: 0;
+ font-family: var(--font-sans);
+ font-weight: 500;
+ font-size: 11px;
+ line-height: 1;
+ color: color-mix(in srgb, var(--cf-muted) 85%, var(--cf-fg));
+ flex-shrink: 0;
+}
+[data-theme="light"] .cp-footer {
+ background: rgba(15, 23, 42, 0.02);
+ border-top-color: rgba(15, 23, 42, 0.08);
+}
+.cp-footer-hint {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 0 10px;
+}
+.cp-footer-hint:first-child { padding-left: 4px; }
+.cp-footer-hint-label {
+ font-family: var(--font-sans);
+ font-weight: 400;
+ font-size: 11.5px;
+ color: color-mix(in srgb, var(--cf-muted) 65%, transparent);
+}
+.cp-footer-hint + .cp-footer-hint::before {
+ content: "";
+ width: 1px;
+ height: 14px;
+ background: rgba(255, 255, 255, 0.10);
+ display: inline-block;
+ margin-right: 10px;
+ margin-left: -2px;
+}
+[data-theme="light"] .cp-footer-hint + .cp-footer-hint::before {
+ background: rgba(15, 23, 42, 0.10);
+}
+.cp-footer-hint .cp-kbd { margin: 0; }
+.cp-footer-scope {
+ margin-left: auto;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ height: 22px;
+ padding: 0 10px;
+ color: color-mix(in srgb, var(--cf-muted) 70%, transparent);
+ font-family: var(--font-sans);
+ font-weight: 500;
+ font-size: 11px;
+ letter-spacing: -0.005em;
+}
+.cp-scope-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--cf-primary);
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--cf-primary) 18%, transparent);
+}
+.cp-scope-label {
+ font-family: var(--font-mono);
+ font-weight: 600;
+ font-size: 10.5px;
+ color: color-mix(in srgb, var(--cf-muted) 30%, var(--cf-fg));
+ letter-spacing: 0;
+}
+.cp-scope-count {
+ color: color-mix(in srgb, var(--cf-muted) 70%, transparent);
+ font-family: var(--font-mono);
+ font-size: 10.5px;
+ padding-left: 6px;
+ margin-left: 2px;
+ border-left: 1px solid rgba(255, 255, 255, 0.10);
+}
+[data-theme="light"] .cp-scope-count {
+ border-left-color: rgba(15, 23, 42, 0.10);
+}
+@media (max-width: 767px) {
+ .cp-footer {
+ flex-wrap: wrap;
+ height: auto;
+ padding: 8px 14px 10px;
+ row-gap: 6px;
+ }
+ .cp-footer-scope { margin-left: 0; width: max-content; }
+}
+
+.cp-skeleton-list { padding: 10px 8px; }
+.cp-skeleton { display: grid; gap: 6px; padding: 12px 14px; }
+.cp-skeleton-title,
+.cp-skeleton-path,
+.cp-skeleton-snippet {
+ background: var(--cf-card);
+ border-radius: 4px;
+ height: 14px;
+ animation: cp-shimmer 1.4s ease-in-out infinite;
+}
+.cp-skeleton-title { width: 60%; height: 16px; }
+.cp-skeleton-path { width: 35%; height: 11px; }
+.cp-skeleton-snippet { width: 90%; height: 14px; }
+@keyframes cp-shimmer {
+ 0%, 100% { opacity: 0.5; }
+ 50% { opacity: 0.9; }
+}
+
+.cp-state {
+ padding: 2.25rem 1.25rem;
+ text-align: center;
+}
+.cp-state-initial { padding-top: 3rem; }
+.cp-state-title {
+ font-family: var(--font-sans);
+ font-weight: 500;
+ font-size: 0.95rem;
+ color: var(--cf-fg);
+ letter-spacing: -0.005em;
+}
+.cp-state-body {
+ margin-top: 6px;
+ font-size: 0.85rem;
+ color: var(--cf-muted);
+ line-height: 1.55;
+}
+.cp-state-retry {
+ background: none;
+ border: 0;
+ color: var(--cf-primary);
+ cursor: pointer;
+ font: inherit;
+ padding: 0;
+ margin-left: 4px;
+ text-decoration: underline;
+ text-underline-offset: 2px;
+}
+.cp-state-retry:hover { color: var(--cf-fg); }
+
+.cp-recent-list { list-style: none; margin: 0; padding: 0 0 4px; }
+.cp-recent-date {
+ font-family: var(--font-mono);
+ font-size: 11.5px;
+ color: color-mix(in srgb, var(--cf-muted) 60%, transparent);
+}
+.cp-recent-list .cp-result {
+ min-height: 44px;
+ padding: 7px 16px 7px 18px;
+ align-items: center;
+}
+.cp-recent-list .cp-result-icon { margin-top: 0; }
+
+.cp-empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+ padding: 88px 32px 24px;
+ text-align: center;
+ gap: 14px;
+}
+.cp-empty-line1 {
+ font-family: var(--font-sans);
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 1.4;
+ color: var(--cf-fg);
+ letter-spacing: -0.005em;
+}
+.cp-empty-q {
+ font-family: var(--font-mono);
+ font-weight: 500;
+ color: color-mix(in srgb, var(--cf-primary) 65%, var(--cf-fg));
+ background: color-mix(in srgb, var(--cf-primary) 18%, transparent);
+ padding: 0 6px;
+ border-radius: 3px;
+ margin: 0 1px;
+}
+.cp-empty-hint {
+ font-family: var(--font-sans);
+ font-weight: 400;
+ font-size: 12.5px;
+ line-height: 1.5;
+ color: var(--cf-muted);
+ max-width: 360px;
+}
+.cp-empty-chips {
+ margin-top: 6px;
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ justify-content: center;
+}
+.cp-empty-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ height: 24px;
+ padding: 0 10px;
+ border-radius: 4px;
+ background: rgba(255, 255, 255, 0.03);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ color: var(--cf-muted);
+ font-family: var(--font-sans);
+ font-weight: 500;
+ font-size: 11.5px;
+ line-height: 1;
+ letter-spacing: -0.005em;
+}
+[data-theme="light"] .cp-empty-chip {
+ background: rgba(0, 0, 0, 0.02);
+ border-color: rgba(15, 23, 42, 0.08);
+}
+.cp-chip-kbd {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 16px;
+ height: 16px;
+ padding: 0 4px;
+ margin-left: 8px;
+ border-radius: 3px;
+ background: rgba(255, 255, 255, 0.06);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ font-family: var(--font-mono);
+ font-weight: 500;
+ font-size: 10px;
+ line-height: 1;
+ color: var(--cf-fg);
+}
+[data-theme="light"] .cp-chip-kbd {
+ background: rgba(0, 0, 0, 0.06);
+ border-color: rgba(15, 23, 42, 0.08);
+}
+.cp-empty-recent {
+ margin-top: 28px;
+ width: 100%;
+ max-width: 380px;
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
+ padding-top: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+[data-theme="light"] .cp-empty-recent {
+ border-top-color: rgba(15, 23, 42, 0.08);
+}
+.cp-empty-recent-label {
+ font-family: var(--font-sans);
+ font-weight: 500;
+ font-size: 10.5px;
+ line-height: 1;
+ color: color-mix(in srgb, var(--cf-muted) 70%, transparent);
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ margin-bottom: 8px;
+ text-align: left;
+}
+.cp-empty-recent-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 6px 4px;
+ font-family: var(--font-sans);
+ font-weight: 400;
+ font-size: 12.5px;
+ line-height: 1.3;
+ color: var(--cf-muted);
+ text-align: left;
+}
+.cp-empty-recent-row svg {
+ color: color-mix(in srgb, var(--cf-muted) 70%, transparent);
+ flex-shrink: 0;
+}
+.cp-empty-recent-q { flex: 1; }
+.cp-empty-recent-ago {
+ font-family: var(--font-mono);
+ font-size: 11.5px;
+ color: color-mix(in srgb, var(--cf-muted) 60%, transparent);
+ margin-left: auto;
+}
+
+.cp-trigger {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ height: 32px;
+ padding: 0 10px 0 12px;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 8px;
+ color: var(--cf-muted);
+ font-family: var(--font-sans);
+ font-size: 13px;
+ cursor: pointer;
+ transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
+ min-width: 44px;
+ min-height: 44px;
+}
+.cp-trigger:hover {
+ background: rgba(255, 255, 255, 0.06);
+ border-color: rgba(255, 255, 255, 0.14);
+ color: var(--cf-fg);
+}
+[data-theme="light"] .cp-trigger {
+ background: rgba(15, 23, 42, 0.03);
+ border-color: rgba(15, 23, 42, 0.08);
+}
+[data-theme="light"] .cp-trigger:hover {
+ background: rgba(15, 23, 42, 0.06);
+ border-color: rgba(15, 23, 42, 0.16);
+}
+.cp-trigger-label {
+ font-weight: 500;
+}
+.cp-trigger-kbd {
+ display: inline-flex;
+ align-items: center;
+ gap: 1px;
+ height: 18px;
+ padding: 0 5px;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 4px;
+ font-family: var(--font-mono);
+ font-size: 10.5px;
+ font-weight: 600;
+ color: var(--cf-muted);
+ background: rgba(255, 255, 255, 0.03);
+}
+[data-theme="light"] .cp-trigger-kbd {
+ background: rgba(15, 23, 42, 0.04);
+ border-color: rgba(15, 23, 42, 0.10);
+}
+.cp-kbd-meta { font-size: 11px; margin-right: 1px; }
+`;
+
+export default CommandPalette;
diff --git a/src/index.ts b/src/index.ts
index 42539eb..263737c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -548,6 +548,23 @@ export type {
ScheduleFallback,
} from "./components/docs";
+// CommandPalette — generalized vault/search palette (2026-05-20)
+export {
+ CommandPalette,
+ CommandPaletteTrigger,
+} from "./components/command-palette/CommandPalette";
+export type {
+ CommandPaletteProps,
+ CommandPaletteTriggerProps,
+ SearchHit,
+ SearchResponse,
+ SearchFn,
+ DocRole,
+ RecentDoc,
+ RecentSearch,
+ EmptyAction,
+} from "./components/command-palette/CommandPalette";
+
// LinkPreview — wikilink hover popover (2026-05-20)
// docs.cofoundy.dev: vault navigation without full-page repaint.
// Provider mounts once around the doc content tree; same-origin anchors
diff --git a/src/stories/command-palette/CommandPalette.stories.tsx b/src/stories/command-palette/CommandPalette.stories.tsx
new file mode 100644
index 0000000..df14285
--- /dev/null
+++ b/src/stories/command-palette/CommandPalette.stories.tsx
@@ -0,0 +1,220 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { useState } from 'react';
+import {
+ CommandPalette,
+ type SearchHit,
+ type SearchResponse,
+ type SearchFn,
+} from '../../components/command-palette/CommandPalette';
+
+const VIEWPORT_MOBILE = { defaultViewport: 'mobile1' };
+
+const FIXTURE: SearchHit[] = [
+ {
+ project: 'cofoundy',
+ slug: 'engineering/brand-validator-guide',
+ role: 'team',
+ title: 'Brand Validator Guide',
+ snippet:
+ '
violates the allowlist when colors fall outside brand tokens. Nearest match suggestion lands in stderr.',
+ score: 0.42,
+ url: '/team/cofoundy/engineering/brand-validator-guide',
+ },
+ {
+ project: 'handbook',
+ slug: 'positioning/voice-and-persona',
+ role: 'team',
+ title: 'Voice & Persona',
+ snippet:
+ 'Cofoundy is Sage-Caregiver with Hero notes. Earns trust through clarity. Owns the constraint instead of citing authority.',
+ score: 0.71,
+ url: '/team/handbook/positioning/voice-and-persona',
+ },
+ {
+ project: 'cofoundy-gtm',
+ slug: 'decisions/channel-stack',
+ role: 'team',
+ title: 'Channel Stack — Decisions',
+ snippet:
+ 'WhatsApp first, then email; Discord for ops; Slack rejected for client comms. Rationale: register fit + read latency.',
+ score: 0.95,
+ url: '/team/cofoundy-gtm/decisions/channel-stack',
+ },
+ {
+ project: 'client-xgodel',
+ slug: 'meetings/kickoff-recap-2026-05-12',
+ role: 'client',
+ title: 'Kickoff Recap — XGodel',
+ snippet:
+ 'Phase 1 scope locked at landing + brand. Approval gate at concept C. Next checkpoint: shader config Friday.',
+ score: 1.12,
+ url: '/client/client-xgodel/meetings/kickoff-recap-2026-05-12',
+ },
+ {
+ project: 'core',
+ slug: 'finance/pricing-q2',
+ role: 'team',
+ title: 'Pricing — Q2 review',
+ snippet:
+ 'Tier ladder revised. Partner-tier unchanged. Consultant rates indexed to USD floor.',
+ score: 1.38,
+ url: '/team/core/finance/pricing-q2',
+ },
+];
+
+const makeResponse = (query: string, hits: SearchHit[]): SearchResponse => ({
+ query,
+ hits,
+ total: hits.length,
+ took_ms: 4,
+});
+
+const instantSearch =
+ (hits: SearchHit[]): SearchFn =>
+ async (q) => {
+ const needle = q.trim().toLowerCase();
+ if (!needle) return makeResponse(q, hits);
+ const filtered = hits.filter(
+ (h) =>
+ h.title.toLowerCase().includes(needle) ||
+ h.url.toLowerCase().includes(needle) ||
+ h.snippet.toLowerCase().includes(needle),
+ );
+ return makeResponse(q, filtered);
+ };
+
+const slowSearch =
+ (hits: SearchHit[], delayMs = 1500): SearchFn =>
+ (q, signal) =>
+ new Promise((resolve, reject) => {
+ const t = setTimeout(() => resolve(makeResponse(q, hits)), delayMs);
+ signal.addEventListener('abort', () => {
+ clearTimeout(t);
+ reject(new DOMException('aborted', 'AbortError'));
+ });
+ });
+
+const emptySearch: SearchFn = async (q) => makeResponse(q, []);
+
+const erroringSearch: SearchFn = async () => {
+ throw new Error('upstream search index unavailable');
+};
+
+function Harness(props: Parameters[0]) {
+ const [open, setOpen] = useState(true);
+ return (
+ {
+ // eslint-disable-next-line no-console
+ console.log('[CommandPalette] navigate →', url);
+ }}
+ />
+ );
+}
+
+const meta: Meta = {
+ title: 'Command Palette/CommandPalette',
+ component: CommandPalette,
+ parameters: { layout: 'fullscreen' },
+ tags: ['autodocs'],
+};
+export default meta;
+
+type Story = StoryObj;
+
+export const Idle: Story = {
+ render: () => {}} searchFn={instantSearch(FIXTURE)} scope="team" />,
+};
+
+export const Loading: Story = {
+ render: () => (
+ {}}
+ searchFn={slowSearch(FIXTURE, 60_000)}
+ debounceMs={0}
+ scope="team"
+ />
+ ),
+};
+
+export const Results: Story = {
+ render: () => {
+ const [open, setOpen] = useState(true);
+ return (
+
+ );
+ },
+};
+
+export const NoResults: Story = {
+ render: () => {
+ const [open, setOpen] = useState(true);
+ const fn: SearchFn = async (q) => makeResponse(q || 'asdfasdf', []);
+ return (
+
+ );
+ },
+};
+
+export const Error_: Story = {
+ name: 'Error',
+ render: () => {}} searchFn={erroringSearch} debounceMs={0} scope="team" />,
+};
+
+export const WithRecents: Story = {
+ render: () => (
+ {}}
+ searchFn={instantSearch(FIXTURE)}
+ scope="team"
+ recents={[
+ { title: 'Onboarding playbook v2', path: 'vault / team / onboarding.md', date: '2d ago' },
+ { title: 'Q2 priorities', path: 'vault / strategy / q2-priorities.md', date: '3d ago' },
+ { title: 'Pets — kickoff brief', path: 'clients / pets / kickoff.md', date: '4d ago' },
+ ]}
+ />
+ ),
+};
+
+export const MobileBaseline: Story = {
+ parameters: { viewport: VIEWPORT_MOBILE },
+ render: () => (
+ {}}
+ searchFn={instantSearch(FIXTURE)}
+ scope="team"
+ mobileVariant="sheet"
+ />
+ ),
+};