Files
crewli/apps/app/src/composables/useV2Nav.ts
bert.hausmans 8444ea7443 fix(gui-v2): SidebarNav uses RouterLink (a11y) + review-nit cleanup
- 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>
2026-05-16 19:39:56 +02:00

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 }
}