@life-and-dev/mdsite 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +228 -0
- package/bin/mdsite.js +3 -0
- package/dist/commands/commands.test.d.ts +1 -0
- package/dist/commands/commands.test.js +302 -0
- package/dist/commands/commands.test.js.map +1 -0
- package/dist/commands/generate.d.ts +1 -0
- package/dist/commands/generate.js +20 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +16 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/prepare.d.ts +1 -0
- package/dist/commands/prepare.js +111 -0
- package/dist/commands/prepare.js.map +1 -0
- package/dist/commands/prepare.test.d.ts +1 -0
- package/dist/commands/prepare.test.js +131 -0
- package/dist/commands/prepare.test.js.map +1 -0
- package/dist/commands/preview.d.ts +5 -0
- package/dist/commands/preview.js +62 -0
- package/dist/commands/preview.js.map +1 -0
- package/dist/commands/start.d.ts +5 -0
- package/dist/commands/start.js +50 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/stop.d.ts +1 -0
- package/dist/commands/stop.js +26 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/commands/workflows.test.d.ts +1 -0
- package/dist/commands/workflows.test.js +183 -0
- package/dist/commands/workflows.test.js.map +1 -0
- package/dist/config/default-mdsite-config.d.ts +2 -0
- package/dist/config/default-mdsite-config.js +82 -0
- package/dist/config/default-mdsite-config.js.map +1 -0
- package/dist/config/default-mdsite-config.test.d.ts +1 -0
- package/dist/config/default-mdsite-config.test.js +42 -0
- package/dist/config/default-mdsite-config.test.js.map +1 -0
- package/dist/config/mdsite-config.d.ts +42 -0
- package/dist/config/mdsite-config.js +74 -0
- package/dist/config/mdsite-config.js.map +1 -0
- package/dist/config/mdsite-config.test.d.ts +1 -0
- package/dist/config/mdsite-config.test.js +115 -0
- package/dist/config/mdsite-config.test.js.map +1 -0
- package/dist/config/menu.d.ts +3 -0
- package/dist/config/menu.js +58 -0
- package/dist/config/menu.js.map +1 -0
- package/dist/config/menu.test.d.ts +1 -0
- package/dist/config/menu.test.js +53 -0
- package/dist/config/menu.test.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +99 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +133 -0
- package/dist/index.test.js.map +1 -0
- package/dist/process/child-process.d.ts +9 -0
- package/dist/process/child-process.js +142 -0
- package/dist/process/child-process.js.map +1 -0
- package/dist/process/child-process.test.d.ts +1 -0
- package/dist/process/child-process.test.js +131 -0
- package/dist/process/child-process.test.js.map +1 -0
- package/dist/process/runtime-state.d.ts +16 -0
- package/dist/process/runtime-state.js +42 -0
- package/dist/process/runtime-state.js.map +1 -0
- package/dist/process/runtime-state.test.d.ts +1 -0
- package/dist/process/runtime-state.test.js +57 -0
- package/dist/process/runtime-state.test.js.map +1 -0
- package/dist/renderer/mdsite-nuxt.d.ts +22 -0
- package/dist/renderer/mdsite-nuxt.js +194 -0
- package/dist/renderer/mdsite-nuxt.js.map +1 -0
- package/dist/renderer/mdsite-nuxt.test.d.ts +1 -0
- package/dist/renderer/mdsite-nuxt.test.js +226 -0
- package/dist/renderer/mdsite-nuxt.test.js.map +1 -0
- package/mdsite-nuxt/app/app.vue +7 -0
- package/mdsite-nuxt/app/assets/css/bible-tooltips.css +87 -0
- package/mdsite-nuxt/app/assets/css/markdown.css +195 -0
- package/mdsite-nuxt/app/assets/css/print.css +133 -0
- package/mdsite-nuxt/app/components/AppBar.vue +138 -0
- package/mdsite-nuxt/app/components/AppFooter.vue +88 -0
- package/mdsite-nuxt/app/components/AppNavigation.vue +162 -0
- package/mdsite-nuxt/app/components/AppTableOfContents.vue +149 -0
- package/mdsite-nuxt/app/components/BreadcrumbNav.vue +139 -0
- package/mdsite-nuxt/app/components/NavigationTree.vue +169 -0
- package/mdsite-nuxt/app/components/SearchBox.vue +212 -0
- package/mdsite-nuxt/app/components/TocItem.vue +75 -0
- package/mdsite-nuxt/app/components/TreeNode.vue +264 -0
- package/mdsite-nuxt/app/components/content/MarkdownAlert.vue +42 -0
- package/mdsite-nuxt/app/components/content/Mermaid.vue +103 -0
- package/mdsite-nuxt/app/components/content/ProseA.vue +57 -0
- package/mdsite-nuxt/app/components/content/ProseBlockquote.vue +14 -0
- package/mdsite-nuxt/app/components/content/ProsePre.vue +49 -0
- package/mdsite-nuxt/app/components/content/ProseTable.vue +143 -0
- package/mdsite-nuxt/app/composables/useAppTheme.ts +89 -0
- package/mdsite-nuxt/app/composables/useBibleTooltips.ts +21 -0
- package/mdsite-nuxt/app/composables/useBreadcrumbs.ts +70 -0
- package/mdsite-nuxt/app/composables/useContentPostProcessing.ts +89 -0
- package/mdsite-nuxt/app/composables/useNavigationTree.ts +174 -0
- package/mdsite-nuxt/app/composables/useSearchIndex.ts +47 -0
- package/mdsite-nuxt/app/composables/useSearchRelevance.test.ts +255 -0
- package/mdsite-nuxt/app/composables/useSearchRelevance.ts +207 -0
- package/mdsite-nuxt/app/composables/useSidebarState.ts +62 -0
- package/mdsite-nuxt/app/composables/useSiteConfig.ts +37 -0
- package/mdsite-nuxt/app/composables/useSourceEdit.ts +69 -0
- package/mdsite-nuxt/app/composables/useTableOfContents.ts +141 -0
- package/mdsite-nuxt/app/composables/useTableParser.ts +106 -0
- package/mdsite-nuxt/app/composables/useTooltipConfig.ts +20 -0
- package/mdsite-nuxt/app/config/themes.ts +17 -0
- package/mdsite-nuxt/app/layouts/default.vue +376 -0
- package/mdsite-nuxt/app/pages/[...slug].vue +56 -0
- package/mdsite-nuxt/app/pages/index.vue +54 -0
- package/mdsite-nuxt/app/plugins/bible-tooltips.client.ts +401 -0
- package/mdsite-nuxt/app/plugins/bible-tooltips.test.ts +284 -0
- package/mdsite-nuxt/app/types/content.d.ts +10 -0
- package/mdsite-nuxt/app/types/table.ts +57 -0
- package/mdsite-nuxt/app/utils/bible-book-names.ts +37 -0
- package/mdsite-nuxt/app/utils/bible-verse-utils.test.ts +148 -0
- package/mdsite-nuxt/app/utils/bible-verse-utils.ts +236 -0
- package/mdsite-nuxt/content.config.ts +18 -0
- package/mdsite-nuxt/example.config.yml +67 -0
- package/mdsite-nuxt/nuxt.config.ts +247 -0
- package/mdsite-nuxt/package-lock.json +15922 -0
- package/mdsite-nuxt/package.json +50 -0
- package/mdsite-nuxt/playwright.config.js +47 -0
- package/mdsite-nuxt/public/robots.txt +2 -0
- package/mdsite-nuxt/scripts/generate-favicons.ts +192 -0
- package/mdsite-nuxt/scripts/generate-indices.ts +508 -0
- package/mdsite-nuxt/scripts/renderer-hooks.test.ts +277 -0
- package/mdsite-nuxt/scripts/renderer-hooks.ts +141 -0
- package/mdsite-nuxt/scripts/start.test.ts +164 -0
- package/mdsite-nuxt/scripts/start.ts +55 -0
- package/mdsite-nuxt/scripts/sync-content.ts +504 -0
- package/mdsite-nuxt/tsconfig.json +18 -0
- package/mdsite-nuxt/utils/mdsite-config.ts +240 -0
- package/mdsite-nuxt/vitest.config.ts +7 -0
- package/package.json +36 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="navigation-sidebar" ref="sidebarContainer">
|
|
3
|
+
<!-- Search Box (sticky at top) - only on desktop -->
|
|
4
|
+
<SearchBox
|
|
5
|
+
v-if="showSearch"
|
|
6
|
+
class="search-sticky"
|
|
7
|
+
@select="handleSearchSelect"
|
|
8
|
+
@clear="showTree = true"
|
|
9
|
+
@search-active="handleSearchActive"
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
<v-divider v-if="showSearch" />
|
|
13
|
+
|
|
14
|
+
<!-- Navigation Tree or Search Results -->
|
|
15
|
+
<div v-if="showTree" class="tree-container" ref="treeContainer">
|
|
16
|
+
<NavigationTree
|
|
17
|
+
v-if="navigationTree"
|
|
18
|
+
:tree="navigationTree"
|
|
19
|
+
:active-path="route.path"
|
|
20
|
+
@select="handleNavSelect"
|
|
21
|
+
/>
|
|
22
|
+
<div v-else class="loading-state">
|
|
23
|
+
<v-progress-circular indeterminate size="32" />
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<!-- Fade gradient for overflow -->
|
|
28
|
+
<div v-if="showFade" class="fade-gradient" />
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script setup lang="ts">
|
|
33
|
+
import { useNavigationTree } from '~/composables/useNavigationTree'
|
|
34
|
+
|
|
35
|
+
const props = defineProps<{
|
|
36
|
+
showSearch?: boolean
|
|
37
|
+
}>()
|
|
38
|
+
|
|
39
|
+
const emit = defineEmits<{
|
|
40
|
+
select: [path: string]
|
|
41
|
+
}>()
|
|
42
|
+
|
|
43
|
+
const route = useRoute()
|
|
44
|
+
|
|
45
|
+
// Navigation tree state
|
|
46
|
+
const { tree: navigationTree, loadTree } = useNavigationTree()
|
|
47
|
+
|
|
48
|
+
// UI state
|
|
49
|
+
const showTree = ref(true)
|
|
50
|
+
const showFade = ref(false)
|
|
51
|
+
const sidebarContainer = ref<HTMLElement>()
|
|
52
|
+
const treeContainer = ref<HTMLElement>()
|
|
53
|
+
|
|
54
|
+
// Load navigation tree on mount
|
|
55
|
+
onMounted(async () => {
|
|
56
|
+
await loadTree()
|
|
57
|
+
checkOverflow()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Handle search selection
|
|
62
|
+
*/
|
|
63
|
+
function handleSearchSelect(path: string) {
|
|
64
|
+
showTree.value = true
|
|
65
|
+
emit('select', path)
|
|
66
|
+
navigateTo(path)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Handle search active state
|
|
71
|
+
*/
|
|
72
|
+
function handleSearchActive(active: boolean) {
|
|
73
|
+
showTree.value = !active
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Handle navigation selection
|
|
78
|
+
*/
|
|
79
|
+
function handleNavSelect(path: string) {
|
|
80
|
+
emit('select', path)
|
|
81
|
+
navigateTo(path)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if content overflows and show fade gradient
|
|
86
|
+
*/
|
|
87
|
+
function checkOverflow() {
|
|
88
|
+
if (!treeContainer.value) return
|
|
89
|
+
|
|
90
|
+
const hasOverflow = treeContainer.value.scrollHeight > treeContainer.value.clientHeight
|
|
91
|
+
showFade.value = hasOverflow
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Set up ResizeObserver to detect overflow
|
|
95
|
+
onMounted(() => {
|
|
96
|
+
if (!treeContainer.value) return
|
|
97
|
+
|
|
98
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
99
|
+
checkOverflow()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
resizeObserver.observe(treeContainer.value)
|
|
103
|
+
|
|
104
|
+
onUnmounted(() => {
|
|
105
|
+
resizeObserver.disconnect()
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<style scoped>
|
|
111
|
+
.navigation-sidebar {
|
|
112
|
+
height: 100%;
|
|
113
|
+
display: flex;
|
|
114
|
+
flex-direction: column;
|
|
115
|
+
position: relative;
|
|
116
|
+
overflow: hidden;
|
|
117
|
+
background-color: rgb(var(--v-theme-surface-rail));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.search-sticky {
|
|
121
|
+
flex-shrink: 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.tree-container {
|
|
125
|
+
flex: 1;
|
|
126
|
+
overflow-y: auto;
|
|
127
|
+
overflow-x: hidden;
|
|
128
|
+
position: relative;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Hide scrollbar but keep functionality */
|
|
132
|
+
.tree-container::-webkit-scrollbar {
|
|
133
|
+
display: none;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.tree-container {
|
|
137
|
+
-ms-overflow-style: none;
|
|
138
|
+
scrollbar-width: none;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.loading-state {
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
justify-content: center;
|
|
145
|
+
padding: 32px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.fade-gradient {
|
|
149
|
+
position: absolute;
|
|
150
|
+
bottom: 0;
|
|
151
|
+
left: 0;
|
|
152
|
+
right: 0;
|
|
153
|
+
height: 60px;
|
|
154
|
+
background: linear-gradient(
|
|
155
|
+
to bottom,
|
|
156
|
+
transparent,
|
|
157
|
+
rgb(var(--v-theme-surface-rail))
|
|
158
|
+
);
|
|
159
|
+
pointer-events: none;
|
|
160
|
+
z-index: 10;
|
|
161
|
+
}
|
|
162
|
+
</style>
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="toc-sidebar" ref="tocContainer">
|
|
3
|
+
<!-- Header -->
|
|
4
|
+
<h3 v-if="showHeader" class="toc-header">On This Page</h3>
|
|
5
|
+
<v-divider v-if="showHeader" class="mb-2" />
|
|
6
|
+
|
|
7
|
+
<!-- TOC Items -->
|
|
8
|
+
<nav v-if="tocItems.length >= 2" class="toc-nav">
|
|
9
|
+
<TocItem
|
|
10
|
+
v-for="item in tocItems"
|
|
11
|
+
:key="item.id"
|
|
12
|
+
:item="item"
|
|
13
|
+
:is-active="activeId === item.id"
|
|
14
|
+
@click="handleItemClick(item.id)"
|
|
15
|
+
/>
|
|
16
|
+
</nav>
|
|
17
|
+
|
|
18
|
+
<!-- Empty state -->
|
|
19
|
+
<div v-else class="toc-empty">
|
|
20
|
+
<p class="text-caption text-center">No table of contents available</p>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<!-- Fade gradient for overflow -->
|
|
24
|
+
<div v-if="showFade" class="fade-gradient" />
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<script setup lang="ts">
|
|
29
|
+
import type { TocItem as TocItemType } from '~/composables/useTableOfContents'
|
|
30
|
+
|
|
31
|
+
const props = withDefaults(
|
|
32
|
+
defineProps<{
|
|
33
|
+
tocItems: TocItemType[]
|
|
34
|
+
activeId: string
|
|
35
|
+
showHeader?: boolean
|
|
36
|
+
}>(),
|
|
37
|
+
{
|
|
38
|
+
showHeader: true
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const emit = defineEmits<{
|
|
43
|
+
'item-click': [id: string]
|
|
44
|
+
}>()
|
|
45
|
+
|
|
46
|
+
const { scrollToHeading } = useTableOfContents()
|
|
47
|
+
|
|
48
|
+
const tocContainer = ref<HTMLElement>()
|
|
49
|
+
const showFade = ref(false)
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Handle TOC item click
|
|
53
|
+
*/
|
|
54
|
+
function handleItemClick(id: string) {
|
|
55
|
+
scrollToHeading(id)
|
|
56
|
+
emit('item-click', id)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if content overflows
|
|
61
|
+
*/
|
|
62
|
+
function checkOverflow() {
|
|
63
|
+
if (!tocContainer.value) return
|
|
64
|
+
|
|
65
|
+
const hasOverflow = tocContainer.value.scrollHeight > tocContainer.value.clientHeight
|
|
66
|
+
showFade.value = hasOverflow
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Set up ResizeObserver to detect overflow
|
|
70
|
+
onMounted(() => {
|
|
71
|
+
if (!tocContainer.value) return
|
|
72
|
+
|
|
73
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
74
|
+
checkOverflow()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
resizeObserver.observe(tocContainer.value)
|
|
78
|
+
|
|
79
|
+
onUnmounted(() => {
|
|
80
|
+
resizeObserver.disconnect()
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Check overflow when items change
|
|
85
|
+
watch(() => props.tocItems, () => {
|
|
86
|
+
nextTick(() => checkOverflow())
|
|
87
|
+
})
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<style scoped>
|
|
91
|
+
.toc-sidebar {
|
|
92
|
+
height: 100%;
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
position: relative;
|
|
96
|
+
overflow: hidden;
|
|
97
|
+
background-color: rgb(var(--v-theme-surface-rail));
|
|
98
|
+
padding: 16px 12px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.toc-header {
|
|
102
|
+
font-size: 0.875rem;
|
|
103
|
+
font-weight: 600;
|
|
104
|
+
color: rgb(var(--v-theme-on-surface-rail));
|
|
105
|
+
margin-bottom: 8px;
|
|
106
|
+
padding: 0 4px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.toc-nav {
|
|
110
|
+
flex: 1;
|
|
111
|
+
overflow-y: auto;
|
|
112
|
+
overflow-x: hidden;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Hide scrollbar but keep functionality */
|
|
116
|
+
.toc-nav::-webkit-scrollbar {
|
|
117
|
+
display: none;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.toc-nav {
|
|
121
|
+
-ms-overflow-style: none;
|
|
122
|
+
scrollbar-width: none;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.toc-empty {
|
|
126
|
+
flex: 1;
|
|
127
|
+
display: flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
justify-content: center;
|
|
130
|
+
padding: 32px 16px;
|
|
131
|
+
color: rgb(var(--v-theme-on-surface-rail));
|
|
132
|
+
opacity: 0.6;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.fade-gradient {
|
|
136
|
+
position: absolute;
|
|
137
|
+
bottom: 0;
|
|
138
|
+
left: 0;
|
|
139
|
+
right: 0;
|
|
140
|
+
height: 60px;
|
|
141
|
+
background: linear-gradient(
|
|
142
|
+
to bottom,
|
|
143
|
+
transparent,
|
|
144
|
+
rgb(var(--v-theme-surface-rail))
|
|
145
|
+
);
|
|
146
|
+
pointer-events: none;
|
|
147
|
+
z-index: 10;
|
|
148
|
+
}
|
|
149
|
+
</style>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<nav class="breadcrumb-nav" aria-label="Breadcrumb">
|
|
3
|
+
<ol class="breadcrumb-list">
|
|
4
|
+
<li
|
|
5
|
+
v-for="(item, index) in breadcrumbs"
|
|
6
|
+
:key="item.path"
|
|
7
|
+
class="breadcrumb-item"
|
|
8
|
+
>
|
|
9
|
+
<v-tooltip
|
|
10
|
+
v-if="item.title === '...'"
|
|
11
|
+
text="Navigate up to parent section"
|
|
12
|
+
location="bottom"
|
|
13
|
+
>
|
|
14
|
+
<template #activator="{ props: tooltipProps }">
|
|
15
|
+
<a
|
|
16
|
+
v-bind="tooltipProps"
|
|
17
|
+
class="breadcrumb-link"
|
|
18
|
+
:class="{ 'is-ellipsis': item.title === '...' }"
|
|
19
|
+
@click.prevent="router.push(item.path)"
|
|
20
|
+
>
|
|
21
|
+
{{ item.title }}
|
|
22
|
+
</a>
|
|
23
|
+
</template>
|
|
24
|
+
</v-tooltip>
|
|
25
|
+
<a
|
|
26
|
+
v-else
|
|
27
|
+
class="breadcrumb-link"
|
|
28
|
+
:class="{ 'is-current': index === breadcrumbs.length - 1 }"
|
|
29
|
+
@click.prevent="handleClick(item, index)"
|
|
30
|
+
>
|
|
31
|
+
{{ item.title }}
|
|
32
|
+
</a>
|
|
33
|
+
<span
|
|
34
|
+
v-if="index < breadcrumbs.length - 1"
|
|
35
|
+
class="breadcrumb-separator"
|
|
36
|
+
aria-hidden="true"
|
|
37
|
+
>
|
|
38
|
+
>
|
|
39
|
+
</span>
|
|
40
|
+
</li>
|
|
41
|
+
</ol>
|
|
42
|
+
</nav>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<script setup lang="ts">
|
|
46
|
+
import type { BreadcrumbItem } from '~/composables/useBreadcrumbs'
|
|
47
|
+
|
|
48
|
+
const props = defineProps<{
|
|
49
|
+
breadcrumbs: BreadcrumbItem[]
|
|
50
|
+
}>()
|
|
51
|
+
|
|
52
|
+
const router = useRouter()
|
|
53
|
+
|
|
54
|
+
function handleClick(item: BreadcrumbItem, index: number) {
|
|
55
|
+
// Don't navigate if it's the current page (last item)
|
|
56
|
+
if (index !== props.breadcrumbs.length - 1) {
|
|
57
|
+
router.push(item.path)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<style scoped>
|
|
63
|
+
.breadcrumb-nav {
|
|
64
|
+
flex: 1;
|
|
65
|
+
min-width: 0;
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.breadcrumb-list {
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
list-style: none;
|
|
74
|
+
margin: 0;
|
|
75
|
+
padding: 0;
|
|
76
|
+
flex-wrap: nowrap;
|
|
77
|
+
overflow: hidden;
|
|
78
|
+
max-width: 100%;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.breadcrumb-item {
|
|
82
|
+
display: flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
flex-shrink: 1;
|
|
85
|
+
min-width: 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.breadcrumb-link {
|
|
89
|
+
color: rgb(var(--v-theme-on-surface));
|
|
90
|
+
text-decoration: none;
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
padding: 4px 8px;
|
|
93
|
+
border-radius: 4px;
|
|
94
|
+
transition: background-color 0.15s ease, color 0.15s ease;
|
|
95
|
+
white-space: nowrap;
|
|
96
|
+
overflow: hidden;
|
|
97
|
+
text-overflow: ellipsis;
|
|
98
|
+
font-size: 0.875rem;
|
|
99
|
+
font-weight: 500;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.breadcrumb-link:hover {
|
|
103
|
+
background-color: rgba(var(--v-theme-on-selectable), 0.04);
|
|
104
|
+
color: rgb(var(--v-theme-primary));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.breadcrumb-link.is-current {
|
|
108
|
+
color: rgb(var(--v-theme-on-surface));
|
|
109
|
+
opacity: 0.6;
|
|
110
|
+
cursor: default;
|
|
111
|
+
pointer-events: none;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.breadcrumb-link.is-ellipsis {
|
|
115
|
+
font-weight: 600;
|
|
116
|
+
min-width: auto;
|
|
117
|
+
flex-shrink: 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.breadcrumb-separator {
|
|
121
|
+
margin: 0 4px;
|
|
122
|
+
color: rgb(var(--v-theme-on-surface));
|
|
123
|
+
opacity: 0.4;
|
|
124
|
+
font-size: 0.75rem;
|
|
125
|
+
flex-shrink: 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Responsive adjustments */
|
|
129
|
+
@media (max-width: 599px) {
|
|
130
|
+
.breadcrumb-link {
|
|
131
|
+
font-size: 0.75rem;
|
|
132
|
+
padding: 2px 6px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.breadcrumb-separator {
|
|
136
|
+
margin: 0 2px;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
</style>
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="navigation-tree">
|
|
3
|
+
<!-- Render root's children (skip root itself) -->
|
|
4
|
+
<TreeNode
|
|
5
|
+
v-for="node in rootChildren"
|
|
6
|
+
:key="node.id"
|
|
7
|
+
:node="node"
|
|
8
|
+
:active-path="activePath"
|
|
9
|
+
:expanded-ids="expandedIds"
|
|
10
|
+
:depth="0"
|
|
11
|
+
@toggle="handleToggle"
|
|
12
|
+
@select="handleSelect"
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import type { TreeNode } from '~/composables/useNavigationTree'
|
|
19
|
+
|
|
20
|
+
const props = defineProps<{
|
|
21
|
+
tree: TreeNode | null
|
|
22
|
+
activePath: string
|
|
23
|
+
}>()
|
|
24
|
+
|
|
25
|
+
const emit = defineEmits<{
|
|
26
|
+
select: [path: string]
|
|
27
|
+
}>()
|
|
28
|
+
|
|
29
|
+
// Expanded node IDs
|
|
30
|
+
const expandedIds = ref<Set<string>>(new Set())
|
|
31
|
+
|
|
32
|
+
// Get root's children (skip rendering root itself)
|
|
33
|
+
const rootChildren = computed(() => {
|
|
34
|
+
if (!props.tree) return []
|
|
35
|
+
return props.tree.children || []
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Auto-expand path to active node on mount and when activePath changes
|
|
40
|
+
*/
|
|
41
|
+
watch(() => props.activePath, (newPath) => {
|
|
42
|
+
if (newPath && props.tree) {
|
|
43
|
+
expandPathToActive(newPath)
|
|
44
|
+
}
|
|
45
|
+
}, { immediate: true })
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Expand all ancestors of the active node
|
|
49
|
+
*/
|
|
50
|
+
function expandPathToActive(path: string) {
|
|
51
|
+
if (!props.tree) return
|
|
52
|
+
|
|
53
|
+
// Special case: Homepage (/) - collapse all menus
|
|
54
|
+
if (path === '/') {
|
|
55
|
+
expandedIds.value.clear()
|
|
56
|
+
expandedIds.value = new Set(expandedIds.value)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const node = findNodeByPath(path, props.tree)
|
|
61
|
+
if (!node) return
|
|
62
|
+
|
|
63
|
+
// Walk up the tree and expand all ancestors
|
|
64
|
+
let current = node.parent
|
|
65
|
+
while (current && current.id !== 'root') {
|
|
66
|
+
expandedIds.value.add(current.id)
|
|
67
|
+
current = current.parent
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Force Vue to detect Set mutations by creating new Set instance
|
|
71
|
+
expandedIds.value = new Set(expandedIds.value)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Find node by path in tree
|
|
76
|
+
*/
|
|
77
|
+
function findNodeByPath(path: string, node: TreeNode): TreeNode | null {
|
|
78
|
+
if (node.path === path) return node
|
|
79
|
+
|
|
80
|
+
for (const child of node.children || []) {
|
|
81
|
+
const found = findNodeByPath(path, child)
|
|
82
|
+
if (found) return found
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Handle toggle expand/collapse
|
|
90
|
+
*/
|
|
91
|
+
function handleToggle(nodeId: string) {
|
|
92
|
+
if (expandedIds.value.has(nodeId)) {
|
|
93
|
+
// Collapse
|
|
94
|
+
expandedIds.value.delete(nodeId)
|
|
95
|
+
// Force Vue to detect Set mutation by creating new Set instance
|
|
96
|
+
expandedIds.value = new Set(expandedIds.value)
|
|
97
|
+
} else {
|
|
98
|
+
// Expand - collapse siblings at same level (Option B behavior)
|
|
99
|
+
const node = findNodeById(nodeId, props.tree)
|
|
100
|
+
if (node && node.parent) {
|
|
101
|
+
// Collapse siblings
|
|
102
|
+
for (const sibling of node.parent.children || []) {
|
|
103
|
+
if (sibling.id !== nodeId && sibling.id !== node.id) {
|
|
104
|
+
expandedIds.value.delete(sibling.id)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Expand this node
|
|
110
|
+
expandedIds.value.add(nodeId)
|
|
111
|
+
// Force Vue to detect Set mutation by creating new Set instance
|
|
112
|
+
expandedIds.value = new Set(expandedIds.value)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Find node by ID
|
|
118
|
+
*/
|
|
119
|
+
function findNodeById(id: string, node: TreeNode | null): TreeNode | null {
|
|
120
|
+
if (!node) return null
|
|
121
|
+
if (node.id === id) return node
|
|
122
|
+
|
|
123
|
+
for (const child of node.children || []) {
|
|
124
|
+
const found = findNodeById(id, child)
|
|
125
|
+
if (found) return found
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Handle node selection
|
|
133
|
+
*/
|
|
134
|
+
function handleSelect(path: string) {
|
|
135
|
+
emit('select', path)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Scroll active node into view
|
|
140
|
+
*/
|
|
141
|
+
function scrollActiveIntoView() {
|
|
142
|
+
nextTick(() => {
|
|
143
|
+
const activeElement = document.querySelector('.tree-node.is-active')
|
|
144
|
+
if (activeElement) {
|
|
145
|
+
activeElement.scrollIntoView({
|
|
146
|
+
behavior: 'smooth',
|
|
147
|
+
block: 'nearest'
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Scroll active node into view when path changes
|
|
154
|
+
watch(() => props.activePath, () => {
|
|
155
|
+
scrollActiveIntoView()
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Initial scroll on mount
|
|
159
|
+
onMounted(() => {
|
|
160
|
+
scrollActiveIntoView()
|
|
161
|
+
})
|
|
162
|
+
</script>
|
|
163
|
+
|
|
164
|
+
<style scoped>
|
|
165
|
+
.navigation-tree {
|
|
166
|
+
width: 100%;
|
|
167
|
+
padding: 8px 4px;
|
|
168
|
+
}
|
|
169
|
+
</style>
|