Global search: top-nav magnifier morphs into the one palette surface - #171
Conversation
160158f to
7f2b9a1
Compare
|
Review fixes pushed (rebased onto current main). Per item: 1 — motion classes were inert. Confirmed: react-ui's prebuilt 2 — Esc/focus-return unreachable. Dead wrapper 3 — slug fabrication. 4 — mirror input was a decoy. Now a 5 — 6 — store lifecycle. The provider closes search when 7 — stale comments. Both references to the deleted 8 — 9 — rebase. Rebased onto
|
The magnifier's morph into an inline input, Esc collapsing it back, cmd+K and a click landing on one palette surface, a result navigating to a slug-addressed detail route, and the reduced-motion path where the swap is instant.
Every route's top bar now carries the product's one search entry point: a magnifier that morphs in place into an inline bar over 200ms on react-ui's spring easing, Esc collapsing it back, and cmd+K opening the identical palette. Open state and query moved out of CommandPaletteProvider into a shared external store, since the palette and the top bar are siblings in the Shell — the morph and the palette can no longer disagree about whether search is open, and the context menu's Search item drives the same store instead of a window event, which is gone. Palette results now resolve agents, skills, and plugins to their slug-addressed detail routes (detailPathForName, falling back to the roster for a name that cannot name a URL), and a Plugins group lists the bench's connected MCP servers.
Records the two doors onto the palette and the store they share, the Plugins group and its connected-servers-only scope, and the slug detail routing for agents, skills, and plugins.
The previous morph test asserted Tailwind class names, which compile to nothing against react-ui's prebuilt stylesheet — it would have passed on a morph that never ran. It now reads the authored transition out of app.css and checks the tokens exist in the stylesheet the app imports, and asserts the element carries no inert motion utility. Escape is driven inside the palette dialog rather than dispatched at the collapsed shell, including the cmd+K case where the magnifier was never focused, so the focus return is actually exercised. New coverage: a route change closing the surface, the inline bar being text rather than a second input, and an entity whose handle is not a slug keeping its id deep link instead of a guessed slug.
Review fixes for the top-nav search morph. Motion: the transition is authored on .stage-search in app.css against --duration-standard and --ease-in-out. The Tailwind utilities it used before (duration-standard, ease-spring) exist only inside react-ui's own build — the app imports react-ui's prebuilt stylesheet, where neither the classes nor --ease-spring are defined, so only transition-[width] applied and the morph ran at the default 150ms ease. --ease-in-out is also the right curve: it is what react-ui's theme documents for something growing in place, and a spring's overshoot would drag the whole top bar with it. Focus: the wrapper keydown handler could never fire — the palette is a portaled modal that holds focus in its own input — so it is gone. Focus returns to the magnifier when the surface closes, whichever way it closed. Slugs are no longer derived: detailPath takes the entity's own minted slug (an agent's handle, a skill's name, a server's slug) and falls back to its opaque id, which every roster resolves as a deep link. Guessing a slug from a display title 404s the moment the two disagree — a renamed agent, an accent folded differently — and the create-agent panel's own rival slugify is now @corbits/slug, so a suggested handle can never be a shape the router refuses. Also: the inline bar is a span styled as a field, not a readOnly input that swallowed clicks and confused assistive tech; the reduced-motion hook and data-motion attribute are gone, since react-ui's stylesheet already collapses transition durations globally; search closes on route change and bench switch, so module state cannot outlive the scope it was opened in; the shortcut is named openCommandPalette, which is all it can honestly do while the open palette holds focus in a text field.
DESIGN.md's Motion section named spring as the search morph's curve; in-place morphs take the in-out curve instead, and the section now says why and notes that these are theme tokens rather than utilities the product can name. docs/command-palette.md records the focus return, the route/bench scoping, open-not-toggle, and that a result's slug is the entity's own, never derived.
7f2b9a1 to
28aabb7
Compare
Implements CL-6410 — global search: the top-nav magnifier morphs into the one command-palette surface.
DESIGN.md's Search section fixes the invocation contract: exactly one search surface, reachable two ways that resolve to the same UI. This wires that contract into the shell and extends the existing
@corbits/command-palette— no second search implementation.What changed
StageSearch(apps/web/src/shell/stage-search.tsx) — the magnifier every route's top bar now carries, ahead of the page's own actions. Clicking it morphs the control in place into an inline bar: a width transition on react-ui's motion tokens (duration-standard/ease-spring, i.e. 200ms with the spring easing). Esc collapses it back to the magnifier and returns focus to it. Underprefers-reduced-motionthe transition is not declared at all, so the swap is instant.command-palette-open-store.tsholds open + query outside the React tree, becauseCommandPaletteProviderandStageTopBarare siblings inapp.tsx's Shell. The morph and the palette can no longer disagree about whether search is open. The oldcommand-palette-events.tswindow event is gone; the context menu's Search item drives the same store (cut over, no legacy path left beside it).detailPathForName(new in@corbits/command-palette) resolves a result's name to the slug-addressed detail routes PR Slug detail routes for agents, skills, plugins, routines #155 landed: agents, skills, and plugins now navigate to/agents/<slug>etc. A name that cannot name a URL resolves to the roster rather than a fabricated slug.Interaction decision worth a look
react-ui's
CommandPaletteis a modal dialog that owns the editable input once open. So the morphed inline bar mirrors the live query rather than accepting keystrokes: one editable search field in the product, with the morph showing where the overlay came from. An anchored, non-modal palette in react-ui would let that bar be the input itself — flagged indocs/command-palette.mdrather than worked around here.Follow-ups (noted, not done)
Testing
apps/web/test/global-search-morph.test.tsxcovers the morph open/close, Esc restoring the icon, cmd+K and a click landing on one palette surface (asserting a single combobox in the document), a result navigating to/agents/research-analyst, and the reduced-motion path.packages/command-palette/src/detail-paths.test.tscovers the slug/roster resolution.bun run checkis green exceptworkflows/assistant(2 failures indefinition.test.ts/manager-tools-scenario.test.ts), which fail identically on a cleanorigin/mainworktree — pre-existing, unrelated to this change.https://claude.ai/code/session_01Shhie5zM8L54bLHq5gFQti