- FIX 1: Replace <button @click="router.push"> with <RouterLink custom> + <a> for real link semantics (middle-click, ⌘-click, screen-reader); custom isNavItemActive prefix-match stays the active source of truth; adds :aria-current="page" on active items; drops useRouter/router.push. RouterLink to prop cast via itemTo() helper (RouteLocationRaw from unplugin-vue-router) to satisfy typed RouterLinkTyped<RouteNamedMap>. - FIX 2: Align .nav-item comment to actual template values (py-[9px] rounded-md, not CSS vars); replace inaccurate Tailwind v3/v4 before: composability justification in <style scoped> with the real reason (accent bar at left:-10px is clipped by the overflow-y-auto nav). - FIX 3: text-left → text-start (logical property, RTL-safe). - FIX 4: Document id=route-name assumption in useV2Nav.ts with a one-line comment at the id: assignment. - FIX 5: Reword misleading "dotted names" spec description to state the real invariant (id = v1 route name, already kebab-case). - FIX 6: Add 2 tests — useV2Nav wrapper .value equality, and consecutive-headings edge case (empty-items group produced). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
96 lines
2.9 KiB
TypeScript
96 lines
2.9 KiB
TypeScript
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, // v1 route names are already kebab-case; no normalisation needed
|
|
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 }
|
|
}
|