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
9 changes: 9 additions & 0 deletions .changeset/navbar-narrow-overflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@gemstack/the-framework": patch
---

Fix the navbar overflowing the page at narrow widths.

Below a narrow viewport the top nav pushed the whole document wider than the screen, so the page scrolled sideways and slid the app off-screen. The cause was the nav's fixed clusters: the brand mark plus wordmark on one side and the "New session" button plus the icon buttons on the other were both `shrink-0`, so together with the project picker they could not fit a phone-width viewport.

Below `sm` the nav now folds down to what fits: the brand keeps its mark (still the link home) but drops the "The Framework" wordmark, the "New session" button collapses to its `+` icon (still labelled for a screen reader), and the project picker caps narrower and truncates a long name. At `sm` and up everything is exactly as before. Verified by driving a real browser at 375px and 420px (no page scroll) and at 1200px (labels back); jsdom cannot see this class of layout bug.
12 changes: 12 additions & 0 deletions packages/framework-dashboard/components/BrandLink.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ describe('BrandLink', () => {
expect(onNavigate).not.toHaveBeenCalled()
})

test('folds the wordmark away below sm so the nav fits a narrow viewport (#980)', () => {
// jsdom has no layout engine and does not apply the utility CSS, so the actual hide can only
// be proved by driving a real browser (done for the PR). This just pins that the wordmark keeps
// its responsive classes, so a future tidy-up cannot silently bring the overflow back. The mark
// stays visible either way, and it is still the link home (#909).
const { container } = render(<BrandLink working={false} onNavigate={vi.fn()} />)
const wordmark = screen.getByText('The Framework')
expect(wordmark.className).toContain('hidden')
expect(wordmark.className).toContain('sm:inline')
expect(container.querySelector('svg')).not.toBeNull() // the mark is not hidden
})

test('carries the working state through to the mark', () => {
const { container, rerender } = render(<BrandLink working onNavigate={vi.fn()} />)
expect(container.querySelector('path')?.getAttribute('fill')).toBe('url(#hexknot-0)')
Expand Down
4 changes: 3 additions & 1 deletion packages/framework-dashboard/components/BrandLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ export function BrandLink({ working, onNavigate }: { working: boolean; onNavigat
className="flex shrink-0 items-center gap-3 rounded-md transition-opacity hover:opacity-80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-primary)]"
>
<Logo className="h-5 w-auto shrink-0" working={working} />
<span className="shrink-0 font-semibold">The Framework</span>
{/* Below sm the wordmark folds away so the nav fits a narrow viewport (#980); the mark stays,
and it is still the link home (#909). */}
<span className="hidden shrink-0 font-semibold sm:inline">The Framework</span>
</a>
)
}
4 changes: 3 additions & 1 deletion packages/framework-dashboard/components/ProjectPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ export function ProjectPicker({
// which does not say it is a picker.
aria-label={`Project: ${label}`}
title="Select a project"
className={cn(buttonVariants({ variant: 'outline', size: 'sm' }), 'max-w-56 gap-1.5 font-normal')}
// Capped narrower below sm so a long project name cannot push the nav past a narrow
// viewport (#980); the label truncates. Full width returns at sm.
className={cn(buttonVariants({ variant: 'outline', size: 'sm' }), 'max-w-40 gap-1.5 font-normal sm:max-w-56')}
>
<span className="truncate">{label}</span>
{interventionCount > 0 && selectedId !== null && (
Expand Down
10 changes: 7 additions & 3 deletions packages/framework-dashboard/pages/index/+Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { useDocumentTitle } from '../../lib/document-title.js'
import { useWorking } from '../../lib/use-working.js'
import { useFavicon } from '../../lib/favicon.js'
import { useDaemonHealth } from '../../lib/use-daemon-health.js'
import { TriangleAlert, Settings } from 'lucide-react'
import { TriangleAlert, Settings, Plus } from 'lucide-react'

/** Stable, so `files` keeps one identity while no project is selected. */
const EMPTY_FILES: string[] = []
Expand Down Expand Up @@ -306,14 +306,18 @@ export default function Page() {
<div className="flex shrink-0 items-center gap-1">
{/* Which daemon this dashboard is talking to (#1052) — obvious once you can hop devices. */}
<ConnectionIndicator />
{/* Below sm the label folds to the icon alone so the nav fits a narrow viewport (#980);
the aria-label keeps it named for a screen reader either way. */}
<Button
variant="outline"
size="sm"
onClick={newSession}
disabled={!projectId}
title={projectId ? undefined : 'Select a project first'}
title={projectId ? 'New session' : 'Select a project first'}
aria-label="New session"
>
New session
<Plus className="h-4 w-4 shrink-0 sm:hidden" aria-hidden />
<span className="hidden sm:inline">New session</span>
</Button>
<ThemeToggle />
<NotificationsMenu />
Expand Down
Loading