Skip to content

feat(command-palette): publish + 24 fixes → @cofoundy/ui 0.6.0#13

Merged
A-PachecoT merged 2 commits into
mainfrom
feat/cmdk-publish
May 22, 2026
Merged

feat(command-palette): publish + 24 fixes → @cofoundy/ui 0.6.0#13
A-PachecoT merged 2 commits into
mainfrom
feat/cmdk-publish

Conversation

@A-PachecoT

Copy link
Copy Markdown
Contributor

Summary

Ships CommandPalette, CommandPaletteTrigger, and useCommandPaletteHotkeys from docs-ai-vault-search to @cofoundy/ui — plus 24 follow-up fixes from a deep audit of the original publish.

This is the single bundled publish-prep PR before consumers wire it in. Bumps to 0.6.0.

What's new

Public exports

  • <CommandPalette> + <CommandPaletteTrigger> (themed, mobile sheet variant)
  • useCommandPaletteHotkeys({open, setOpen}) — Cmd+K / / / Esc binding
  • Types: CommandPaletteProps, SearchHit, SearchResponse, SearchFn, DocRole, RecentDoc, RecentSearch, EmptyAction
  • Telemetry: onSearch?(query, hits, took_ms?), onSelect?(hit, idx, 'click' | 'enter')
  • Safety: trustSnippetHtml?: boolean opt-in (default = sanitize allow-only-<mark>)
  • Ergonomics: minQueryLength?: number (default 2)

Fixes (audit findings — 24 total)

Functional / UX (8)

  • Reset query/results on close (regression vs source hook)
  • InitialState: drop fake "selected" on first recent + fix ARIA
  • Remove onMouseEnter selection hijack (kept keyboard sticky)
  • 2-line clamp on snippet so FTS5 <mark> survives long-title truncation
  • Debounce hygiene: minQueryLength + stale-results dim + working retry nonce
  • emptyActions chips render as buttons when onSelect supplied
  • Clear (×) button inside input
  • Cmd/Ctrl + Enter (and Cmd-click) opens hit in new tab

A11y (6)

  • Focus trap on dialog (Tab cycles inside)
  • aria-controls only references the listbox when it's mounted
  • Option IDs namespaced via useId so two palettes don't collide
  • Focus restore guarded with isConnected
  • prefers-reduced-motion fallback for all 4 animations
  • searchFn dev warning on silent breakage

Boundary / safety (5)

  • SearchHit.role now optional, rendered as a small role badge if present
  • Same-origin guard: external hits auto-open with _blank + noopener,noreferrer
  • Snippet sanitizer (<mark>-only) is the default; opt out with trustSnippetHtml
  • CSS variable defaults via :where(...) so the component renders standalone
  • Trigger pill min-height: 44px gated behind @media (pointer: coarse)

Performance + infra (3)

  • Singleton CSS injection (one <style id="cp-styles"> to <head>, not re-injected per mount)
  • Refcounted body scroll-lock — composes with sibling modals
  • Retry button now actually re-fires via retryNonce (the old setQuery((q) => q) was a no-op)

Tests + stories (2)

  • 21 tests total (7 baseline + 14 new) covering XSS sanitization, trust opt-out, telemetry callbacks, emptyActions click, Cmd+Enter new-tab, role badge, recents click-through, ID namespacing, dev warn, and the three hotkeys
  • Stories cleanup: removed console.log in Harness; MobileBaseline story verified

Verification

  • pnpm vitest run src/__tests__/components/command-palette/21/21 green
  • pnpm vitest run (full suite) → 471/472 green (the 1 failure is the pre-existing ChatInput baseline, documented in the original publish commit msg, unaffected by this PR)
  • pnpm tsc --noEmit → clean on new code (pre-existing hero-shader errors unaffected, also documented)

Test plan

  • Storybook smoke at pnpm storybook — Idle / Loading / Results / NoResults / Error / WithRecents / MobileBaseline render cleanly in light + dark
  • Manual Cmd+K test inside a host app (deferred to T27 docs-ai consumption PR)
  • Visual diff vs docs-ai-vault-search baseline (deferred to T35 SSOT regen in docs-ai)

Follow-up after merge

  • T27-T35 in docs-ai: refactor Header.tsx to consume @cofoundy/ui CommandPalette + hook, delete the local copy, regen visual SSOT.

Library-side fixes before consumer wiring. All 7 existing tests still pass.

Functional:
  - Reset query/results/selectedIndex on close (regression vs source hook)
  - InitialState recents: drop fake "selected" on first row, fix ARIA
  - Remove onMouseEnter selection hijack (kept keyboard sticky)
  - Snippet now 2-line clamps so <mark> isn't truncated past viewport
  - minQueryLength prop (default 2) + stale-results indicator during debounce
  - retryNonce wires the error-state Retry button to actually re-fire
  - emptyActions extended with `onSelect` — chips render as buttons when wired
  - Clear (×) button in input

A11y:
  - Cmd/Ctrl+Enter (and Cmd-click) opens hit in a new tab
  - Focus trap on the dialog (Tab cycles inside)
  - aria-controls only points at the listbox when it's mounted
  - Option IDs namespaced via useId so two palettes don't collide
  - Focus restore guarded with isConnected
  - prefers-reduced-motion fallback for all 4 animations

Library boundary:
  - Dev warning when searchFn is missing instead of silent no-op
  - SearchHit.role now optional, rendered as a small role badge if present
  - Same-origin guard: external URLs auto-open in a new tab with noopener
  - Snippet sanitizer (allow-only-<mark>) is the default; opt-in trustSnippetHtml
  - CSS variable defaults via :where(...) so the component renders safely standalone
  - Trigger pill min-height: 44px moved behind @media (pointer: coarse)

Tests touched to keep passing with the new defaults:
  - Harness sets minQueryLength={1} so single-char fixtures still fire fetches
Library prep for downstream consumers. 472 tests run, 471 pass — the only
failure is the pre-existing ChatInput baseline (unrelated, documented).

Public surface additions:
  - useCommandPaletteHotkeys({open, setOpen}) — Cmd+K / / / Esc binding
  - onSearch?(query, hits, took_ms?) telemetry
  - onSelect?(hit, idx, 'click' | 'enter') telemetry
  - trustSnippetHtml prop — opt-in raw HTML, default sanitize

Infrastructure refactor (no API change):
  - Singleton CSS injection — one <style id="cp-styles"> appended to <head>
    on first mount instead of re-injected per-palette-open.
  - Refcounted body scroll-lock — composes safely with sibling modals.

Tests: 21 total (7 baseline + 14 new). Covers min-query gate, XSS sanitization,
trustSnippetHtml opt-out, clear button, telemetry callbacks, emptyActions click,
Cmd+Enter new-tab, role badge, recents click-through, ID namespacing, dev warn,
and the three hotkeys (Cmd+K / / / Esc).

Stories: removed console.log in Harness, MobileBaseline story present.

Version: 0.5.2 → 0.6.0 (minor — additive public surface, no breaking change).
@A-PachecoT A-PachecoT force-pushed the feat/cmdk-publish branch from 2d7c1a5 to c400098 Compare May 22, 2026 17:21
@A-PachecoT A-PachecoT merged commit aa081da into main May 22, 2026
1 of 2 checks passed
@A-PachecoT A-PachecoT deleted the feat/cmdk-publish branch May 22, 2026 17:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant