feat(gui-v2): port SidebarNav to TypeScript

Ports crewli-starter's sidebar nav into the SPA as production TS:
V2NavGroup/V2NavItem types, a pure toV2NavGroups adapter wrapped by
useV2Nav(items) (composables zone can't import @/navigation, so the
v1 nav array is passed in — the layout supplies orgNavItems in Task 7),
a pure isNavItemActive helper, and SidebarNav.vue (props-only,
router-driven nav, route-based active state, collapsed mode, main.css
translated to Tailwind inline). 16 unit tests. Icon import is
allowed via the components-foundation bridge (no eslint-disable).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 19:23:10 +02:00
parent 4e9eeb99c4
commit 8a8e419ed1
6 changed files with 397 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
import { describe, expect, it } from 'vitest'
import { toV2NavGroups } from '@/composables/useV2Nav'
import type { V2NavItem } from '@/types/v2/nav'
// Hand-built fixture — does NOT depend on the real orgNavItems contents.
const fixture = [
{ title: 'Alpha', to: { name: 'alpha' }, icon: { icon: 'tabler-home' } },
{ title: 'Beta', to: { name: 'beta' }, icon: { icon: 'tabler-bell' } },
{ heading: 'Group One' },
{ title: 'Gamma', to: { name: 'gamma' }, icon: { icon: 'tabler-star' }, count: 5 },
{ heading: 'Group Two' },
{ title: 'Delta', to: { name: 'delta' }, icon: { icon: 'tabler-settings' } },
] as const
describe('toV2NavGroups', () => {
it('places items before the first heading into a leading group with label ""', () => {
const groups = toV2NavGroups(fixture)
expect(groups[0].label).toBe('')
expect(groups[0].items).toHaveLength(2)
})
it('starts a new group when a heading entry is encountered', () => {
const groups = toV2NavGroups(fixture)
expect(groups).toHaveLength(3)
expect(groups[1].label).toBe('Group One')
expect(groups[2].label).toBe('Group Two')
})
it('maps item fields correctly: id, label, icon, to', () => {
const groups = toV2NavGroups(fixture)
const alpha = groups[0].items[0] as V2NavItem
expect(alpha.id).toBe('alpha')
expect(alpha.label).toBe('Alpha')
expect(alpha.icon).toBe('tabler-home')
expect(alpha.to).toEqual({ name: 'alpha' })
})
it('passes count through when present', () => {
const groups = toV2NavGroups(fixture)
const gamma = groups[1].items[0] as V2NavItem
expect(gamma.count).toBe(5)
})
it('leaves count undefined when absent', () => {
const groups = toV2NavGroups(fixture)
const alpha = groups[0].items[0] as V2NavItem
expect(alpha.count).toBeUndefined()
})
it('id is derived from the route name (kebab already for dotted names)', () => {
const groups = toV2NavGroups(fixture)
const delta = groups[2].items[0] as V2NavItem
expect(delta.id).toBe('delta')
})
it('returns empty groups array for an empty input', () => {
expect(toV2NavGroups([])).toEqual([])
})
it('items-only input (no headings) returns a single leading group', () => {
const onlyItems = [
{ title: 'A', to: { name: 'a' }, icon: { icon: 'tabler-a' } },
] as const
const groups = toV2NavGroups(onlyItems)
expect(groups).toHaveLength(1)
expect(groups[0].label).toBe('')
expect(groups[0].items).toHaveLength(1)
})
})