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:
95
apps/app/src/composables/useV2Nav.ts
Normal file
95
apps/app/src/composables/useV2Nav.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { computed } from 'vue'
|
||||
import type { ComputedRef } from 'vue'
|
||||
import type { V2NavGroup, V2NavItem } from '@/types/v2/nav'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Local discriminated union for v1 nav entries (no `any`)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface V1NavHeading {
|
||||
heading: string
|
||||
}
|
||||
|
||||
interface V1NavLink {
|
||||
title: string
|
||||
to: { name: string }
|
||||
icon: { icon: string }
|
||||
count?: number
|
||||
}
|
||||
|
||||
export type V1NavEntry = V1NavHeading | V1NavLink
|
||||
|
||||
function isHeading(entry: V1NavEntry): entry is V1NavHeading {
|
||||
return 'heading' in entry
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pure adapter — exported so it can be unit-tested without mounting
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Folds a flat v1 nav array into V2NavGroup[].
|
||||
*
|
||||
* - A `{ heading }` entry closes the current group and opens a new one.
|
||||
* - Items before the first heading are placed in a leading group with label ''.
|
||||
* - A pure function with no side-effects.
|
||||
*/
|
||||
export function toV2NavGroups(items: readonly V1NavEntry[]): V2NavGroup[] {
|
||||
if (items.length === 0)
|
||||
return []
|
||||
|
||||
const groups: V2NavGroup[] = []
|
||||
let current: V2NavGroup | null = null
|
||||
|
||||
for (const entry of items) {
|
||||
if (isHeading(entry)) {
|
||||
// Close current group (if any) and start a new named group.
|
||||
if (current !== null)
|
||||
groups.push(current)
|
||||
|
||||
current = { label: entry.heading, items: [] }
|
||||
}
|
||||
else {
|
||||
// Ensure there is a current group (leading ungrouped section).
|
||||
if (current === null)
|
||||
current = { label: '', items: [] }
|
||||
|
||||
const navItem: V2NavItem = {
|
||||
id: entry.to.name,
|
||||
label: entry.title,
|
||||
icon: entry.icon.icon,
|
||||
to: { name: entry.to.name },
|
||||
...(entry.count !== undefined ? { count: entry.count } : {}),
|
||||
}
|
||||
|
||||
current.items.push(navItem)
|
||||
}
|
||||
}
|
||||
|
||||
// Flush the last open group.
|
||||
if (current !== null)
|
||||
groups.push(current)
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Composable
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Wraps toV2NavGroups in a computed ref.
|
||||
*
|
||||
* Accepts the raw v1 nav items as a parameter so the composable (composables
|
||||
* boundary zone) does not need to import from @/navigation (navigation zone).
|
||||
* Call-sites in layouts/pages — which ARE allowed to import navigation —
|
||||
* pass orgNavItems directly:
|
||||
*
|
||||
* import { orgNavItems } from '@/navigation/vertical'
|
||||
* const { groups } = useV2Nav(orgNavItems)
|
||||
*/
|
||||
export function useV2Nav(items: readonly V1NavEntry[]): { groups: ComputedRef<V2NavGroup[]> } {
|
||||
const groups = computed(() => toV2NavGroups(items))
|
||||
|
||||
return { groups }
|
||||
}
|
||||
Reference in New Issue
Block a user