- 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>
138 lines
5.5 KiB
Vue
138 lines
5.5 KiB
Vue
<script setup lang="ts">
|
|
import { useRoute } from 'vue-router'
|
|
import type { RouteLocationRaw } from 'unplugin-vue-router'
|
|
import Icon from '@/components/Icon.vue'
|
|
import { isNavItemActive } from '@/components-v2/layout/sidebarNavActive'
|
|
import type { V2NavGroup, V2NavItem } from '@/types/v2/nav'
|
|
|
|
defineProps<{
|
|
groups: V2NavGroup[]
|
|
collapsed: boolean
|
|
}>()
|
|
|
|
const route = useRoute()
|
|
|
|
function checkActive(item: V2NavItem): boolean {
|
|
return isNavItemActive(item, route.name)
|
|
}
|
|
|
|
// Cast V2NavItem.to (vue-router generic RouteLocationRaw) to the typed version
|
|
// expected by RouterLinkTyped. V2NavItem.to values are always named-route objects
|
|
// from the RouteNamedMap so the cast is sound.
|
|
function itemTo(item: V2NavItem): RouteLocationRaw {
|
|
return item.to as RouteLocationRaw
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<!--
|
|
.nav → flex-1 min-h-0 overflow-y-auto py-3.5 px-2.5 [scrollbar-width:thin]
|
|
.nav-group → mt-[18px] (only from the second group onward, via CSS sibling selector →
|
|
Tailwind cannot express `+ .nav-group { margin-top }` on a class without
|
|
a group-based trick; using first:mt-0 / not-first:mt-[18px] via the
|
|
:not(:first-child) pseudo equivalent — `[&:not(:first-child)]:mt-[18px]`)
|
|
.nav-label → text-[11px] font-semibold text-surface-500 dark:text-surface-400
|
|
uppercase tracking-[0.06em] px-2.5 pb-1.5 whitespace-nowrap overflow-hidden
|
|
.nav-item → flex items-center gap-3 py-[9px] px-2.5 rounded-md
|
|
text-surface-600 dark:text-surface-300 text-[13.5px] font-medium
|
|
transition-[background,color] duration-150 whitespace-nowrap relative
|
|
cursor-pointer w-full text-start min-w-0
|
|
.nav-item:hover → hover:bg-surface-100 dark:hover:bg-surface-800 hover:text-surface-900 dark:hover:text-surface-0
|
|
.nav-item.active → bg-primary-50 dark:bg-primary-950 text-primary-600 dark:text-primary-400 font-semibold
|
|
.nav-item.active::before → the left accent bar — inexpressible in Tailwind without a plugin
|
|
(position:absolute left:-10px, custom width:3px, height:18px,
|
|
background:primary, transform:translateY(-50%)). Using <style scoped>.
|
|
.nav-item .iconify → text-[18px] flex-shrink-0 (handled by Icon's own sizing via :size)
|
|
.count → ms-auto text-[11px] font-semibold bg-surface-100 dark:bg-surface-800
|
|
text-surface-500 dark:text-surface-400 px-1.5 py-px rounded-full
|
|
.nav-item.active .count → bg-primary-600 dark:bg-primary-500 text-white
|
|
-->
|
|
<nav class="flex-1 min-h-0 overflow-y-auto py-3.5 px-2.5 [scrollbar-width:thin]">
|
|
<div
|
|
v-for="(group, gi) in groups"
|
|
:key="gi"
|
|
class="[&:not(:first-child)]:mt-[18px]"
|
|
>
|
|
<!-- Group label: hidden in collapsed mode -->
|
|
<div
|
|
v-if="group.label && !collapsed"
|
|
class="text-[11px] font-semibold text-surface-500 dark:text-surface-400 uppercase tracking-[0.06em] px-2.5 pb-1.5 whitespace-nowrap overflow-hidden"
|
|
>
|
|
{{ group.label }}
|
|
</div>
|
|
|
|
<RouterLink
|
|
v-for="item in group.items"
|
|
:key="item.id"
|
|
v-slot="{ href, navigate }"
|
|
:to="itemTo(item)"
|
|
custom
|
|
>
|
|
<a
|
|
:href="href"
|
|
class="flex items-center gap-3 py-[9px] rounded-md text-[13.5px] font-medium transition-[background,color] duration-150 whitespace-nowrap relative cursor-pointer w-full text-start min-w-0"
|
|
:class="[
|
|
collapsed ? 'justify-center px-0' : 'px-2.5',
|
|
checkActive(item)
|
|
? 'nav-item-active bg-primary-50 dark:bg-primary-950 text-primary-600 dark:text-primary-400 font-semibold'
|
|
: 'text-surface-600 dark:text-surface-300 hover:bg-surface-100 dark:hover:bg-surface-800 hover:text-surface-900 dark:hover:text-surface-0',
|
|
]"
|
|
:aria-current="checkActive(item) ? 'page' : undefined"
|
|
:aria-label="item.label"
|
|
:title="collapsed ? item.label : undefined"
|
|
@click="navigate"
|
|
>
|
|
<Icon
|
|
:name="item.icon"
|
|
:size="18"
|
|
class="flex-shrink-0"
|
|
/>
|
|
|
|
<!-- Text label: hidden in collapsed mode -->
|
|
<span
|
|
v-if="!collapsed"
|
|
class="overflow-hidden text-ellipsis min-w-0"
|
|
>
|
|
{{ item.label }}
|
|
</span>
|
|
|
|
<!-- Count badge: hidden in collapsed mode -->
|
|
<span
|
|
v-if="item.count != null && !collapsed"
|
|
class="ms-auto text-[11px] font-semibold px-1.5 py-px rounded-full"
|
|
:class="[
|
|
checkActive(item)
|
|
? 'bg-primary-600 dark:bg-primary-500 text-white'
|
|
: 'bg-surface-100 dark:bg-surface-800 text-surface-500 dark:text-surface-400',
|
|
]"
|
|
>
|
|
{{ item.count }}
|
|
</span>
|
|
</a>
|
|
</RouterLink>
|
|
</div>
|
|
</nav>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/*
|
|
* The active left-accent bar (.nav-item.active::before in crewli-starter main.css).
|
|
* The bar sits at left:-10px — outside the row boundary. The parent <nav> is an
|
|
* overflow-y-auto scroll container which clips cross-axis overflow, so a Tailwind
|
|
* before: utility would be clipped by that parent overflow context. A <style scoped>
|
|
* pseudo-element keeps the exception self-contained (customization order: Tailwind →
|
|
* pt API → Aura → <style scoped> last resort with comment).
|
|
*/
|
|
.nav-item-active::before {
|
|
content: "";
|
|
position: absolute;
|
|
left: -10px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 3px;
|
|
height: 18px;
|
|
background: var(--p-primary-500, theme('colors.primary.500', #6366f1));
|
|
border-radius: 0 3px 3px 0;
|
|
}
|
|
</style>
|