Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion src/components/studio/topbar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ vi.mock('lucide-react', () => {
const Icon = ({ className }: { className?: string }) => (
<span data-testid="icon" className={className} />
)
return { Menu: Icon, Plus: Icon, Search: Icon }
return { Menu: Icon, Plus: Icon, Search: Icon, X: Icon }
})

vi.mock('@/lib/utils', () => ({
Expand Down Expand Up @@ -176,4 +176,27 @@ describe('Topbar', () => {
render(<Topbar />)
expect(screen.getByLabelText('Search')).toBeDefined()
})

it('renders input with type="search"', () => {
render(<Topbar />)
const input = screen.getByLabelText('Search') as HTMLInputElement
expect(input.type).toBe('search')
})

it('renders clear search button when searchQuery is non-empty and handles click', () => {
searchQuery = 'test query'
render(<Topbar />)
const clearButton = screen.getByLabelText('Clear quick filter')
expect(clearButton).toBeDefined()
expect(clearButton.getAttribute('title')).toBe('Clear quick filter')
fireEvent.click(clearButton)
expect(mockSetSearchQuery).toHaveBeenCalledWith('')
})

it('clears searchQuery on Escape key in search input when searchQuery is non-empty', () => {
searchQuery = 'test query'
render(<Topbar />)
fireEvent.keyDown(screen.getByLabelText('Search'), { key: 'Escape' })
expect(mockSetSearchQuery).toHaveBeenCalledWith('')
})
})
20 changes: 17 additions & 3 deletions src/components/studio/topbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useStudioStore } from '@/lib/studio/store'
import type { ViewId } from '@/lib/studio/types'
import { translate as timelineT } from '@/lib/i18n/messages/timeline'
import { Menu, Plus, Search } from 'lucide-react'
import { Menu, Plus, Search, X } from 'lucide-react'
import type { KeyboardEvent } from 'react'

/** View title metadata keyed by ViewId (bounded Map retrieval β€” no dynamic indexing). */
Expand All @@ -25,27 +25,30 @@
const getViewMeta = (view: ViewId): { title: string; subtitle: string } =>
VIEW_TITLES.get(view) ?? { title: 'Studio', subtitle: 'Your knowledge base at a glance' }

/** Top header bar with view title, inline search, offline badge, and new entity button. */
export const Topbar = () => {
const currentView = useStudioStore((s) => s.currentView)
const searchQuery = useStudioStore((s) => s.searchQuery)
const startNew = useStudioStore((s) => s.startNew)
const setCommandOpen = useStudioStore((s) => s.setCommandOpen)
const setSearchQuery = useStudioStore((s) => s.setSearchQuery)
const setMobileDrawerOpen = useStudioStore((s) => s.setMobileDrawerOpen)
const setMobilePanelView = useStudioStore((s) => s.setMobilePanelView)
const meta = getViewMeta(currentView)

// Inline input doubles as a quick filter for the Library + right-panel SearchPanel,
// and as a launcher for the command palette (via ⌘K or the kbd chip).
const isLibraryView = currentView === 'library'
const placeholder = isLibraryView ? 'Filter library…' : 'Search…'
const inputAriaLabel = isLibraryView ? 'Filter library' : 'Search'

const handleSearchKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault()
setCommandOpen(true)
} else if (e.key === 'Escape' && searchQuery) {

Check notice on line 49 in src/components/studio/topbar.tsx

View check run for this annotation

NexusCheck / GitNexus

Changed symbol: handleSearchKeyDown

`handleSearchKeyDown` (Function) is directly changed by this PR. PR-wide downstream impact: 1 direct dependent(s), 1 indirect. See the check summary for the impacted-file breakdown.

Check notice on line 49 in src/components/studio/topbar.tsx

View check run for this annotation

NexusCheck / GitNexus

Changed symbol: handleSearchKeyDown

`handleSearchKeyDown` (Function) is directly changed by this PR. PR-wide downstream impact: 1 direct dependent(s), 1 indirect. See the check summary for the impacted-file breakdown.
e.preventDefault()
setSearchQuery('')
}
}

Expand Down Expand Up @@ -96,14 +99,25 @@
aria-hidden
/>
<input
type="text"
type="search"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={handleSearchKeyDown}
placeholder={placeholder}
aria-label={inputAriaLabel}
className="min-h-[44px] w-full rounded-md border border-border bg-background pl-9 pr-12 text-[13px] text-ink placeholder:text-ink-faint transition-colors focus:border-saffron focus:outline-none focus:ring-2 focus:ring-saffron/40"
className="min-h-[44px] w-full rounded-md border border-border bg-background pl-9 pr-20 text-[13px] text-ink placeholder:text-ink-faint transition-colors focus:border-saffron focus:outline-none focus:ring-2 focus:ring-saffron/40"

Check notice on line 108 in src/components/studio/topbar.tsx

View check run for this annotation

NexusCheck / GitNexus

Reserve enough input padding for both overlay buttons

`w-60` makes the input 240px wide. With `pr-20`, its editable content extends to x=160px (240βˆ’80px), but when a query is present the new 44px-wide clear button is positioned from x=144px to x=188px (`right-[3.25rem]` = 52px; see lines 96 and 110–116). Thus 16px of the editable text area lies under the clear button; clicks near the end of a query trigger clear instead of positioning the caret, and text can be obscured on hover.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

βšͺ Nit β€” Reserve enough input padding for both overlay buttons

w-60 makes the input 240px wide. With pr-20, its editable content extends to x=160px (240βˆ’80px), but when a query is present the new 44px-wide clear button is positioned from x=144px to x=188px (right-[3.25rem\] = 52px; see lines 96 and 110–116). Thus 16px of the editable text area lies under the clear button; clicks near the end of a query trigger clear instead of positioning the caret, and text can be obscured on hover.

Suggested fix:

Suggested change
className="min-h-[44px] w-full rounded-md border border-border bg-background pl-9 pr-20 text-[13px] text-ink placeholder:text-ink-faint transition-colors focus:border-saffron focus:outline-none focus:ring-2 focus:ring-saffron/40"
className="min-h-[44px] w-full rounded-md border border-border bg-background pl-9 pr-24 text-[13px] text-ink placeholder:text-ink-faint transition-colors focus:border-saffron focus:outline-none focus:ring-2 focus:ring-saffron/40"
Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At src/components/studio/topbar.tsx, line 108:

<comment>'w-60' makes the input 240px wide. With 'pr-20', its editable content extends to x=160px (240βˆ’80px), but when a query is present the new 44px-wide clear button is positioned from x=144px to x=188px ('right-[3.25rem]' = 52px; see lines 96 and 110–116). Thus 16px of the editable text area lies under the clear button; clicks near the end of a query trigger clear instead of positioning the caret, and text can be obscured on hover.</comment>

<context>Enclosing symbol: Topbar. A verified single-line fix was proposed in the suggestion block above.</context>

Why this matters: GitNexus flagged this from your code graph β€” a caller or contract relies on what changed here. Β· llm-review

/>
{searchQuery ? (
<button
type="button"
onClick={() => setSearchQuery('')}
aria-label="Clear quick filter"
title="Clear quick filter"
className="absolute right-[3.25rem] top-1/2 flex min-h-[44px] min-w-[44px] items-center justify-center -translate-y-1/2 rounded text-ink-faint transition-colors hover:bg-muted hover:text-ink focus-ring"

Check notice on line 116 in src/components/studio/topbar.tsx

View check run for this annotation

NexusCheck / GitNexus

Keep the clear target from overlapping the command-palette target

The new clear button is 44px wide at minimum and placed `right-12` (48px), so in the 240px (`w-60`) container it spans x=148–192. The existing command button is also at least 44px wide and `right-1.5` (6px), spanning x=190–234. Thus the two interactive targets overlap by 2px whenever the clear button is rendered.
>
<X className="h-3.5 w-3.5" />
</button>
) : null}
<button
type="button"
onClick={() => setCommandOpen(true)}
Expand Down
Loading