Skip to content

Commit 87bd045

Browse files
authored
Merge pull request #100 from roackb2/codex/web-v2-ui-polish
Polish web v2 navigation and visual tokens
2 parents e2a8acb + c44bb41 commit 87bd045

30 files changed

Lines changed: 989 additions & 112 deletions

components.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@
44
"rsc": false,
55
"tsx": true,
66
"tailwind": {
7-
"css": "src/web/tailwind.css",
7+
"css": "src/web-v2/tailwind.css",
88
"baseColor": "slate",
99
"cssVariables": true,
1010
"prefix": ""
1111
},
1212
"aliases": {
13-
"components": "@/web/components",
14-
"utils": "@/web/lib/utils",
15-
"ui": "@/web/components/ui",
16-
"lib": "@/web/lib",
17-
"hooks": "@/web/hooks"
13+
"components": "@/web-v2/components",
14+
"utils": "@/web-v2/lib/utils",
15+
"ui": "@/web-v2/components/ui",
16+
"lib": "@/web-v2/lib",
17+
"hooks": "@/web-v2/hooks"
1818
},
1919
"iconLibrary": "lucide"
2020
}

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,10 @@
129129
"dependencies": {
130130
"@anthropic-ai/sdk": "^0.54.0",
131131
"@inkjs/ui": "^2.0.0",
132+
"@radix-ui/react-dialog": "^1.1.15",
132133
"@radix-ui/react-popover": "^1.1.15",
133134
"@radix-ui/react-select": "^2.2.6",
135+
"@radix-ui/react-separator": "^1.1.8",
134136
"@radix-ui/react-slot": "^1.2.4",
135137
"@radix-ui/react-toast": "^1.2.15",
136138
"@radix-ui/react-tooltip": "^1.2.8",

skills-lock.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@
1313
"skillPath": "plugins/ui-design/skills/design-system-patterns/SKILL.md",
1414
"computedHash": "5f8c9b187e77fcb31db78da2f7b55f905dcd2357454624cac93f00c766ace346"
1515
},
16+
"shadcn": {
17+
"source": "shadcn/ui",
18+
"sourceType": "github",
19+
"skillPath": "skills/shadcn/SKILL.md",
20+
"computedHash": "80a6226e78f6d1fe464214ae0ef449d49d8ffaa3e7704f011e9b418c678ad4d1"
21+
},
1622
"vercel-composition-patterns": {
1723
"source": "vercel-labs/agent-skills",
1824
"sourceType": "github",

src/__tests__/browser-integration/web-v2/control-plane-v2.spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@ test('loads the web v2 shell sections', async ({ page }) => {
1010
await expect(page.getByRole('link', { name: 'Sessions' })).toHaveAttribute('aria-current', 'page');
1111
});
1212

13+
test('collapses and expands the sidebar', async ({ page }) => {
14+
await page.goto('/sessions');
15+
16+
const sidebar = page.getByRole('complementary', { name: 'Primary navigation' });
17+
await expect(sidebar).toBeVisible();
18+
19+
await page.getByRole('button', { name: 'Collapse sidebar' }).click();
20+
await expect(page.getByRole('button', { name: 'Expand sidebar' })).toHaveAttribute('aria-expanded', 'false');
21+
await expect(sidebar).toHaveCSS('width', '0px');
22+
23+
await page.getByRole('button', { name: 'Expand sidebar' }).click();
24+
await expect(page.getByRole('button', { name: 'Collapse sidebar' })).toHaveAttribute('aria-expanded', 'true');
25+
await expect(sidebar).not.toHaveCSS('width', '0px');
26+
});
27+
1328
test('navigates primary and settings routes without hash routing', async ({ page }) => {
1429
await page.goto('/sessions');
1530

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1+
import {
2+
SidebarContent,
3+
SidebarFooter,
4+
SidebarHeader,
5+
SidebarMenu,
6+
SidebarMenuItem,
7+
} from '@web/components/ui/sidebar';
18
import { useI18n } from '@web/i18n';
2-
import type { AppSurfaceId, NavigationItem } from '@web/layout/types';
3-
import { SettingsMenu } from './SettingsMenu';
4-
import { SidebarLink } from './SidebarLink';
9+
import type { AppRoute } from '@web/layout/routes';
10+
import type { AppSurfaceId } from '@web/layout/types';
11+
import { MainNavigationSection } from './MainNavigationSection';
12+
import { SidebarContentRegion } from './SidebarContentRegion';
13+
import { SidebarSettingsEntry } from './SidebarSettingsEntry';
514

615
interface AppNavigationProps {
716
activeItemId: AppSurfaceId;
8-
items: readonly NavigationItem[];
17+
items: readonly AppRoute[];
918
onOpenSettings: () => void;
1019
}
1120

@@ -16,21 +25,22 @@ export function AppNavigation({ activeItemId, items, onOpenSettings }: AppNaviga
1625

1726
return (
1827
<>
19-
<div className="border-b px-3 py-2 text-sm font-medium text-foreground">Heddle</div>
20-
<nav className="grid gap-1 p-2" aria-label={t('navigation.mainAriaLabel')}>
21-
{items.map((item) => (
22-
<SidebarLink
23-
key={item.id}
24-
active={item.id === activeItemId}
25-
href={item.href}
26-
label={t(item.labelKey)}
27-
/>
28-
))}
29-
</nav>
30-
<div className="min-h-0 flex-1 border-t bg-background" aria-label={t('navigation.sessionListAriaLabel')} />
31-
<div className="border-t p-2">
32-
<SettingsMenu onOpenSettings={onOpenSettings} />
33-
</div>
28+
<SidebarHeader className="v2-panel-divider h-12 justify-center border-b px-2 py-0">
29+
<div className="flex items-center gap-2">
30+
<span className="text-sm font-medium text-foreground">Heddle</span>
31+
</div>
32+
</SidebarHeader>
33+
<MainNavigationSection activeItemId={activeItemId} items={items} />
34+
<SidebarContent>
35+
<SidebarContentRegion ariaLabel={t('navigation.sessionListAriaLabel')} />
36+
</SidebarContent>
37+
<SidebarFooter className="v2-panel-divider border-t p-1.5">
38+
<SidebarMenu>
39+
<SidebarMenuItem>
40+
<SidebarSettingsEntry onOpenSettings={onOpenSettings} />
41+
</SidebarMenuItem>
42+
</SidebarMenu>
43+
</SidebarFooter>
3444
</>
3545
);
3646
}

src/web-v2/components/navigation/LanguageSelect.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function LanguageSelect() {
1818
{t('language.label')}
1919
</label>
2020
<Select value={locale} onValueChange={(value) => setLocale(value as Locale)}>
21-
<SelectTrigger id="web-v2-language-select" className="h-8">
21+
<SelectTrigger id="web-v2-language-select">
2222
<SelectValue />
2323
</SelectTrigger>
2424
<SelectContent>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { SidebarGroup, SidebarGroupContent, SidebarMenu, SidebarMenuItem } from '@web/components/ui/sidebar';
2+
import type { AppRoute } from '@web/layout/routes';
3+
import type { AppSurfaceId } from '@web/layout/types';
4+
import { useI18n } from '@web/i18n';
5+
import { SidebarLink } from './SidebarLink';
6+
7+
interface MainNavigationSectionProps {
8+
activeItemId: AppSurfaceId;
9+
items: readonly AppRoute[];
10+
}
11+
12+
// MainNavigationSection owns the current top-level web-v2 routes. Do not add
13+
// placeholder features here until the matching route and behavior exist.
14+
export function MainNavigationSection({ activeItemId, items }: MainNavigationSectionProps) {
15+
const { t } = useI18n();
16+
17+
return (
18+
<SidebarGroup>
19+
<SidebarGroupContent>
20+
<nav aria-label={t('navigation.mainAriaLabel')}>
21+
<SidebarMenu>
22+
{items.map((item) => (
23+
<SidebarMenuItem key={item.id}>
24+
<SidebarLink
25+
active={item.id === activeItemId}
26+
href={item.href}
27+
label={t(item.labelKey)}
28+
surfaceId={item.id}
29+
/>
30+
</SidebarMenuItem>
31+
))}
32+
</SidebarMenu>
33+
</nav>
34+
</SidebarGroupContent>
35+
</SidebarGroup>
36+
);
37+
}

src/web-v2/components/navigation/SettingsMenu.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import { Settings } from 'lucide-react';
12
import { Button } from '@web/components/ui/button';
23
import {
34
Popover,
45
PopoverContent,
56
PopoverTrigger,
67
} from '@web/components/ui/popover';
8+
import { SidebarMenuButton } from '@web/components/ui/sidebar';
79
import { LanguageSelect } from './LanguageSelect';
810
import { useI18n } from '@web/i18n';
911

@@ -19,19 +21,26 @@ export function SettingsMenu({ onOpenSettings }: SettingsMenuProps) {
1921
return (
2022
<Popover>
2123
<PopoverTrigger asChild>
22-
<Button className="h-8 w-full justify-start px-2" type="button" variant="ghost">
23-
{t('navigation.settings')}
24-
</Button>
24+
<SidebarMenuButton tooltip={t('navigation.settings')}>
25+
<Settings aria-hidden="true" />
26+
<span>{t('navigation.settings')}</span>
27+
</SidebarMenuButton>
2528
</PopoverTrigger>
2629
<PopoverContent
2730
align="start"
28-
className="w-72"
29-
side="right"
31+
className="w-64 p-2"
32+
side="top"
33+
sideOffset={8}
3034
aria-label={t('navigation.settingsMenuAriaLabel')}
3135
>
32-
<div className="grid gap-4">
36+
<div className="grid gap-2">
3337
<LanguageSelect />
34-
<Button className="h-8 justify-start px-2" onClick={onOpenSettings} type="button" variant="ghost">
38+
<Button
39+
className="v2-nav-row w-full"
40+
onClick={onOpenSettings}
41+
type="button"
42+
variant="ghost"
43+
>
3544
{t('navigation.openSettings')}
3645
</Button>
3746
</div>
Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1-
import { Button } from '@web/components/ui/button';
1+
import {
2+
SidebarContent,
3+
SidebarGroup,
4+
SidebarGroupContent,
5+
SidebarHeader,
6+
SidebarMenu,
7+
SidebarMenuButton,
8+
SidebarMenuItem,
9+
} from '@web/components/ui/sidebar';
210
import { useI18n } from '@web/i18n';
3-
import type { NavigationItem, SettingsSectionId } from '@web/layout/types';
11+
import type { SettingsRoute } from '@web/layout/routes';
12+
import type { SettingsSectionId } from '@web/layout/types';
413
import { SidebarLink } from './SidebarLink';
514

615
interface SettingsNavigationProps {
716
activeItemId: SettingsSectionId;
8-
items: readonly NavigationItem[];
17+
items: readonly SettingsRoute[];
918
onBack: () => void;
1019
}
1120

@@ -16,21 +25,41 @@ export function SettingsNavigation({ activeItemId, items, onBack }: SettingsNavi
1625

1726
return (
1827
<>
19-
<div className="border-b p-2">
20-
<Button className="h-8 w-full justify-start px-2 text-muted-foreground" onClick={onBack} type="button" variant="ghost">
21-
{t('navigation.backToApp')}
22-
</Button>
23-
</div>
24-
<nav className="grid gap-1 p-2" aria-label={t('navigation.settingsAriaLabel')}>
25-
{items.map((item) => (
26-
<SidebarLink
27-
key={item.id}
28-
active={item.id === activeItemId}
29-
href={item.href}
30-
label={t(item.labelKey)}
31-
/>
32-
))}
33-
</nav>
28+
<SidebarHeader className="v2-panel-divider h-12 justify-center border-b px-2 py-0">
29+
<div className="flex items-center gap-2">
30+
<span className="text-sm font-medium text-foreground">{t('settings.general')}</span>
31+
</div>
32+
</SidebarHeader>
33+
<SidebarContent>
34+
<SidebarGroup className="v2-panel-divider border-b">
35+
<SidebarGroupContent>
36+
<SidebarMenu>
37+
<SidebarMenuItem>
38+
<SidebarMenuButton onClick={onBack}>
39+
{t('navigation.backToApp')}
40+
</SidebarMenuButton>
41+
</SidebarMenuItem>
42+
</SidebarMenu>
43+
</SidebarGroupContent>
44+
</SidebarGroup>
45+
<SidebarGroup>
46+
<SidebarGroupContent>
47+
<nav aria-label={t('navigation.settingsAriaLabel')}>
48+
<SidebarMenu>
49+
{items.map((item) => (
50+
<SidebarMenuItem key={item.id}>
51+
<SidebarLink
52+
active={item.id === activeItemId}
53+
href={item.href}
54+
label={t(item.labelKey)}
55+
/>
56+
</SidebarMenuItem>
57+
))}
58+
</SidebarMenu>
59+
</nav>
60+
</SidebarGroupContent>
61+
</SidebarGroup>
62+
</SidebarContent>
3463
</>
3564
);
3665
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
interface SidebarContentRegionProps {
2+
ariaLabel: string;
3+
}
4+
5+
// SidebarContentRegion reserves the future session-list area without inventing
6+
// sample sessions or extra product surfaces.
7+
export function SidebarContentRegion({ ariaLabel }: SidebarContentRegionProps) {
8+
return (
9+
<div
10+
className="v2-panel-divider v2-panel-surface min-h-0 flex-1 border-t"
11+
aria-label={ariaLabel}
12+
/>
13+
);
14+
}

0 commit comments

Comments
 (0)