Skip to content
Open
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
3 changes: 2 additions & 1 deletion src/components/studio/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,13 @@
import { translate as announceT } from '@/lib/i18n/messages/announce'
import { useAnnouncer } from '@/lib/a11y/announcer'
import { ShortcutsTrigger } from './shortcuts-dialog'
import { COMMAND_PALETTE_SHORTCUT_LABEL } from '@/lib/studio/platform'

interface NavItem {
id: ViewId
label: string
icon: typeof Home
shortcut?: string

Check notice on line 38 in src/components/studio/sidebar.tsx

View check run for this annotation

nexus-check / GitNexus

Changed symbol: NavItem

`NavItem` (Interface) 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 38 in src/components/studio/sidebar.tsx

View check run for this annotation

nexus-check / GitNexus

Changed symbol: NavItem

`NavItem` (Interface) 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.
experimental?: boolean
}

Expand Down Expand Up @@ -174,46 +175,46 @@
</nav>
)
}

/** Sidebar with brand, search trigger, navigation, and footer controls. */
export const Sidebar = () => {
const setCommandOpen = useStudioStore((s) => s.setCommandOpen)
const rightPanelOpen = useStudioStore((s) => s.rightPanelOpen)
const setRightPanelOpen = useStudioStore((s) => s.setRightPanelOpen)
const { theme, setTheme } = useTheme()

return (
<aside className="hidden h-full w-[248px] shrink-0 flex-col border-r border-border bg-sidebar text-sidebar-foreground lg:flex">
{/* Brand */}
<div className="flex items-center gap-2.5 px-5 pt-5 pb-4">
<img src="/logo.svg" alt="" className="h-9 w-9" />
<div className="flex flex-col">
<span className="font-serif text-[15px] font-semibold leading-tight tracking-tight">
Knowledge Studio
</span>
<a
href={`${RELEASES_BASE_URL}/tag/v${packageJson.version}`}
target="_blank"
rel="noreferrer"
aria-label={`Knowledge Studio v${packageJson.version} release page`}
className="inline-flex min-h-[44px] items-center text-caption uppercase tracking-[0.14em] text-ink-faint transition-colors hover:text-saffron focus-ring"
>
Local-first · v{packageJson.version}
</a>
</div>
</div>

{/* Search trigger */}
<div className="px-3 pb-3">
<button
onClick={() => { setCommandOpen(true) }}
className="group flex w-full items-center gap-2 rounded-lg border border-border bg-background/50 px-3 py-2 text-left text-[13px] text-ink-mute transition-colors hover:border-saffron/40 hover:text-ink-soft focus-ring min-h-[44px]"
aria-label="Open command palette"
>
<Search className="h-3.5 w-3.5" />
<span className="flex-1">Search…</span>
<kbd className="rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-caption text-ink-faint">
⌘K
{COMMAND_PALETTE_SHORTCUT_LABEL}

Check warning on line 217 in src/components/studio/sidebar.tsx

View check run for this annotation

nexus-check / GitNexus

Platform-specific shortcut label differs during server rendering and hydration

`COMMAND_PALETTE_SHORTCUT_LABEL` is evaluated in `platform.ts` as `Ctrl+K` when `navigator` is unavailable, but evaluates as `⌘K` in a Mac browser. `Sidebar` renders this constant at this line, and `AppShell` renders `Sidebar` directly (`app-shell.tsx:267`); the app entry renders `AppShell` (`page.tsx:7`) without a client-only/SSR-disabled wrapper. Thus a Mac receives different server and initial client text for this element, causing a hydration mismatch instead of matching markup.

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.

🟡 Warning — Platform-specific shortcut label differs during server rendering and hydration

COMMAND_PALETTE_SHORTCUT_LABEL is evaluated in platform.ts as Ctrl+K when navigator is unavailable, but evaluates as ⌘K in a Mac browser. Sidebar renders this constant at this line, and AppShell renders Sidebar directly (app-shell.tsx:267); the app entry renders AppShell (page.tsx:7) without a client-only/SSR-disabled wrapper. Thus a Mac receives different server and initial client text for this element, causing a hydration mismatch instead of matching markup.

(the same fix applies at src/components/studio/topbar.tsx:128, src/lib/studio/platform.ts:2)

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/studio/sidebar.tsx, line 217:

<comment>'COMMAND_PALETTE_SHORTCUT_LABEL' is evaluated in 'platform.ts' as 'Ctrl+K' when 'navigator' is unavailable, but evaluates as '⌘K' in a Mac browser. 'Sidebar' renders this constant at this line, and 'AppShell' renders 'Sidebar' directly ('app-shell.tsx:267'); the app entry renders 'AppShell' ('page.tsx:7') without a client-only/SSR-disabled wrapper. Thus a Mac receives different server and initial client text for this element, causing a hydration mismatch instead of matching markup.</comment>

<context>Enclosing symbol: Sidebar.</context>

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

</kbd>
</button>
</div> {/* Nav groups */}
Expand Down
3 changes: 2 additions & 1 deletion src/components/studio/topbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { translate as timelineT } from '@/lib/i18n/messages/timeline'
import { Menu, Plus, Search, X } from 'lucide-react'
import type { KeyboardEvent } from 'react'
import { COMMAND_PALETTE_SHORTCUT_LABEL } from '@/lib/studio/platform'

/** View title metadata keyed by ViewId (bounded Map retrieval — no dynamic indexing). */
const VIEW_TITLES: ReadonlyMap<ViewId, { title: string; subtitle: string }> = new Map([
Expand All @@ -24,126 +25,126 @@
/** Bounded lookup; falls back to the home title for unknown view ids. */
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) {
e.preventDefault()
setSearchQuery('')
}
}

// Open the drawer directly on the Search tab — fixes mobile search reachability
// (research pain point #35).
const openMobileSearch = () => {
setMobilePanelView('search')
setMobileDrawerOpen(true)
}

return (
<header
className="flex h-16 shrink-0 items-center gap-2 border-b border-border bg-background/80 px-3 backdrop-blur-sm sm:gap-3 sm:px-5"
style={{ height: 'var(--header-height, 4rem)' }}
>
{/* Mobile menu + search triggers — visible only below lg (matches the
desktop Sidebar's `hidden lg:flex`). The menu opens the drawer on
the Navigate tab; the search icon opens it on the Search tab. */}
<button
onClick={() => setMobileDrawerOpen(true)}
className="flex min-h-[44px] min-w-[44px] flex-shrink-0 items-center justify-center rounded-md p-2 text-ink-soft hover:bg-muted focus-ring lg:hidden"
aria-label="Open menu"
>
<Menu className="h-5 w-5" />
</button>
<button
onClick={openMobileSearch}
className="flex min-h-[44px] min-w-[44px] flex-shrink-0 items-center justify-center rounded-md p-2 text-ink-soft hover:bg-muted focus-ring lg:hidden"
aria-label="Search knowledge base"
>
<Search className="h-5 w-5" />
</button>

{/* Title / subtitle column — min-w-0 + flex-1 + truncate so it never overflows */}
<div className="flex min-w-0 flex-1 flex-col">
<h1 className="truncate font-serif text-base font-semibold leading-tight tracking-tight text-ink sm:text-xl">
{meta.title}
</h1>
<p className="hidden truncate text-[12px] text-ink-mute sm:block">
{meta.subtitle}
</p>
</div>

{/* Inline quick-filter input — visible at lg+ (≥ 1024px) */}
<div className="relative hidden w-60 flex-shrink-0 items-center lg:flex">
<Search
className="pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-ink-faint"
aria-hidden
/>
<input
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-20 text-[13px] text-ink placeholder:text-ink-faint transition-colors focus:border-saffron focus:outline-none focus:ring-2 focus:ring-saffron/40"
/>
{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"
>
<X className="h-3.5 w-3.5" />
</button>
) : null}
<button
type="button"
onClick={() => setCommandOpen(true)}
aria-label="Open command palette"
className="absolute right-1.5 top-1/2 flex min-h-[44px] min-w-[44px] items-center justify-center -translate-y-1/2 rounded border border-border bg-muted px-1.5 font-mono text-caption text-ink-faint transition-colors hover:bg-muted/70 hover:text-ink-soft focus-ring overflow-hidden"
>
⌘K
{COMMAND_PALETTE_SHORTCUT_LABEL}
</button>
</div>

{/* Offline-ready badge — hidden on mobile (< 768px) */}
<div className="hidden flex-shrink-0 items-center gap-1.5 rounded-full border border-saffron/30 bg-saffron-soft px-2.5 py-1 text-label font-medium text-saffron-deep md:flex">
<span className="h-1.5 w-1.5 rounded-full bg-saffron" />
Offline ready
</div>

{/* New entity button — icon-only on mobile, "New" on md–lg, "New entity" on wide (≥ 1100px) */}
<button
onClick={startNew}
aria-label="New entity"
className="flex flex-shrink-0 min-h-[44px] items-center gap-1.5 rounded-md bg-primary px-2.5 text-[12px] font-semibold text-primary-foreground shadow-sm transition-all hover:opacity-90 press-scale focus-ring sm:px-3"
>
<Plus className="h-3.5 w-3.5" />
<span className="hidden sm:inline wide:hidden">New</span>
<span className="hidden wide:inline">New entity</span>
</button>
</header>

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

View check run for this annotation

nexus-check / GitNexus

Changed symbol: Topbar

`Topbar` (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 148 in src/components/studio/topbar.tsx

View check run for this annotation

nexus-check / GitNexus

Changed symbol: Topbar

`Topbar` (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.
)
}
4 changes: 4 additions & 0 deletions src/lib/studio/platform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const IS_MAC =
typeof navigator !== 'undefined' && /mac/i.test(navigator.platform)

export const COMMAND_PALETTE_SHORTCUT_LABEL = IS_MAC ? '⌘K' : 'Ctrl+K'
Loading