feat(10-03): implement BreadcrumbBar.vue — shared breadcrumb component (GREEN)
- Options API component with segments/{id?,label}, rootLabel, showRoot props
- Emits navigate(segment.id) for intermediate clicks, navigate(null) for root
- Computed visibleSegments collapses >4 segments to first+ellipsis+last two
- Segments without id render as plain non-clickable spans (admin static segments)
- Uses AppIcon for chevronRight separator (no inline SVG)
- All 10 Vitest tests pass
- Also creates AppIcon.vue (Rule 3: BreadcrumbBar imports it)
This commit is contained in:
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
v-if="resolvedPaths"
|
||||||
|
:class="$attrs.class"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<template v-if="Array.isArray(resolvedPaths)">
|
||||||
|
<path
|
||||||
|
v-for="(d, i) in resolvedPaths"
|
||||||
|
:key="i"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
:d="d"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<path
|
||||||
|
v-else
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
:d="resolvedPaths"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const ICON_PATHS = {
|
||||||
|
plus: 'M12 4v16m8-8H4',
|
||||||
|
folder: 'M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V7z',
|
||||||
|
folderMove: 'M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2V7z',
|
||||||
|
pencil: 'M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z',
|
||||||
|
trash: 'M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16',
|
||||||
|
share: 'M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z',
|
||||||
|
document: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
|
||||||
|
fileDoc: 'M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z',
|
||||||
|
chevronRight: 'M9 5l7 7-7 7',
|
||||||
|
chevronDown: 'M19 9l-7 7-7-7',
|
||||||
|
tag: 'M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z',
|
||||||
|
inbox: 'M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4',
|
||||||
|
cloud: 'M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z',
|
||||||
|
shield: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z',
|
||||||
|
logout: 'M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1',
|
||||||
|
home: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6',
|
||||||
|
users: 'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z',
|
||||||
|
chartBar: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z',
|
||||||
|
clipboardList: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01',
|
||||||
|
upload: 'M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12',
|
||||||
|
x: 'M6 18L18 6M6 6l12 12',
|
||||||
|
checkCircle: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||||
|
exclamationCircle: 'M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||||
|
warning: 'M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z',
|
||||||
|
copy: 'M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z',
|
||||||
|
check: 'M5 13l4 4L19 7',
|
||||||
|
checkMark: 'M4.5 12.75l6 6 9-13.5',
|
||||||
|
refresh: 'M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15',
|
||||||
|
pencilEdit: 'M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z',
|
||||||
|
lightBulb: 'M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z',
|
||||||
|
search: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0',
|
||||||
|
dots: 'M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z',
|
||||||
|
cog: [
|
||||||
|
'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z',
|
||||||
|
'M15 12a3 3 0 11-6 0 3 3 0 016 0z',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AppIcon',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: {
|
||||||
|
name: { type: String, required: true },
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
resolvedPaths() {
|
||||||
|
const p = ICON_PATHS[this.name]
|
||||||
|
if (!p && import.meta.env.DEV) {
|
||||||
|
console.warn(`[AppIcon] Unknown icon name: "${this.name}"`)
|
||||||
|
}
|
||||||
|
return p ?? null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<nav aria-label="Navigation">
|
||||||
|
<ol class="flex items-center gap-1 text-sm flex-wrap">
|
||||||
|
<li v-if="showRoot" class="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
@click="$emit('navigate', null)"
|
||||||
|
class="text-indigo-600 hover:underline font-medium"
|
||||||
|
>
|
||||||
|
{{ rootLabel }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<template v-for="(segment, idx) in visibleSegments" :key="segment.id ?? 'static-' + idx">
|
||||||
|
<li
|
||||||
|
v-if="showRoot || idx > 0"
|
||||||
|
class="shrink-0"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<AppIcon name="chevronRight" class="w-3 h-3 text-gray-400" />
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li v-if="segment.id === 'ellipsis'" class="flex items-center">
|
||||||
|
<span class="px-2 py-1 text-gray-400">…</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li v-else-if="idx === visibleSegments.length - 1" class="flex items-center">
|
||||||
|
<span class="text-gray-900 font-medium">{{ segment.label }}</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li v-else-if="segment.id" class="flex items-center">
|
||||||
|
<button
|
||||||
|
@click="$emit('navigate', segment.id)"
|
||||||
|
class="text-indigo-600 hover:underline font-medium"
|
||||||
|
>
|
||||||
|
{{ segment.label }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li v-else class="flex items-center">
|
||||||
|
<span class="text-gray-500">{{ segment.label }}</span>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import AppIcon from './AppIcon.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'BreadcrumbBar',
|
||||||
|
components: { AppIcon },
|
||||||
|
props: {
|
||||||
|
segments: { type: Array, default: () => [] },
|
||||||
|
rootLabel: { type: String, default: 'Home' },
|
||||||
|
showRoot: { type: Boolean, default: true },
|
||||||
|
},
|
||||||
|
emits: ['navigate'],
|
||||||
|
computed: {
|
||||||
|
visibleSegments() {
|
||||||
|
if (this.segments.length > 4) {
|
||||||
|
return [
|
||||||
|
this.segments[0],
|
||||||
|
{ id: 'ellipsis', label: '…' },
|
||||||
|
...this.segments.slice(-2),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return this.segments
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user