feat(gui-v2): port AppTopbar + useBreadcrumb to TypeScript
- useBreadcrumb composable: pure toBreadcrumbItems() helper + thin useRoute() wrapper; route-driven, no prop coupling - AppTopbar: hamburger→setMobileOpen, theme/density toggles→shell store, PrimeVue Breadcrumb/OverlayBadge/Popover/Avatar/Menu; replaces all manual document.mousedown listeners with PrimeVue built-in dismissal; notifications stubbed (useNotificationStore is a toast queue, not a feed — TODO TECH-WS-GUI-REDESIGN); sign-out→authStore.logout() - Unit tests: 10 breadcrumb + 6 AppTopbar assertions (16 total, all pass) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
91
apps/app/src/composables/useBreadcrumb.ts
Normal file
91
apps/app/src/composables/useBreadcrumb.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import type { ComputedRef } from 'vue'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** A single breadcrumb item. `to` is absent for the current (last) item. */
|
||||
export interface BreadcrumbItem {
|
||||
label: string
|
||||
to?: RouteLocationRaw
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal shape we need from a matched route record.
|
||||
* Using a structural type keeps the pure helper testable without
|
||||
* requiring full RouteLocationMatched fixtures in unit tests.
|
||||
* RouteLocationMatched is structurally assignable to this interface so
|
||||
* route.matched (RouteLocationMatched[]) passes the type-checker.
|
||||
*/
|
||||
export interface BreadcrumbRouteRecord {
|
||||
meta?: Record<string, unknown> & {
|
||||
breadcrumb?: string
|
||||
title?: string
|
||||
}
|
||||
name?: string | symbol | null
|
||||
path?: string
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pure helper — testable without a live router
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Convert an array of matched route records into breadcrumb items.
|
||||
*
|
||||
* Rules:
|
||||
* - Records without `meta.breadcrumb` OR `meta.title` are filtered out.
|
||||
* - Label = `meta.breadcrumb` if set, else `meta.title`.
|
||||
* - The last item is current — no `to` link.
|
||||
* - All preceding items carry a `to` pointing at their `path` (or `name`
|
||||
* when path is absent).
|
||||
*/
|
||||
export function toBreadcrumbItems(matched: readonly BreadcrumbRouteRecord[]): BreadcrumbItem[] {
|
||||
const eligible = matched.filter(
|
||||
r => r.meta?.breadcrumb !== undefined || r.meta?.title !== undefined,
|
||||
)
|
||||
|
||||
return eligible.map((record, index): BreadcrumbItem => {
|
||||
const label = (record.meta?.breadcrumb ?? record.meta?.title) as string
|
||||
const isLast = index === eligible.length - 1
|
||||
|
||||
if (isLast)
|
||||
return { label }
|
||||
|
||||
// Prefer path; fall back to name when path is absent/empty
|
||||
const to: RouteLocationRaw
|
||||
= record.path
|
||||
? record.path
|
||||
: { name: record.name as string }
|
||||
|
||||
return { label, to }
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Composable — thin wrapper around useRoute()
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface UseBreadcrumbReturn {
|
||||
items: ComputedRef<BreadcrumbItem[]>
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable that derives breadcrumb items from the current route's
|
||||
* `matched` array. Reactive — updates automatically on navigation.
|
||||
*
|
||||
* Composables zone may NOT import from @/navigation — do not add
|
||||
* route-level nav helpers here.
|
||||
*/
|
||||
export function useBreadcrumb(): UseBreadcrumbReturn {
|
||||
const route = useRoute()
|
||||
|
||||
const items = computed<BreadcrumbItem[]>(() =>
|
||||
toBreadcrumbItems(route.matched),
|
||||
)
|
||||
|
||||
return { items }
|
||||
}
|
||||
Reference in New Issue
Block a user