@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.
Files changed (133) hide show
  1. package/README.md +228 -0
  2. package/bin/mdsite.js +3 -0
  3. package/dist/commands/commands.test.d.ts +1 -0
  4. package/dist/commands/commands.test.js +302 -0
  5. package/dist/commands/commands.test.js.map +1 -0
  6. package/dist/commands/generate.d.ts +1 -0
  7. package/dist/commands/generate.js +20 -0
  8. package/dist/commands/generate.js.map +1 -0
  9. package/dist/commands/init.d.ts +1 -0
  10. package/dist/commands/init.js +16 -0
  11. package/dist/commands/init.js.map +1 -0
  12. package/dist/commands/prepare.d.ts +1 -0
  13. package/dist/commands/prepare.js +111 -0
  14. package/dist/commands/prepare.js.map +1 -0
  15. package/dist/commands/prepare.test.d.ts +1 -0
  16. package/dist/commands/prepare.test.js +131 -0
  17. package/dist/commands/prepare.test.js.map +1 -0
  18. package/dist/commands/preview.d.ts +5 -0
  19. package/dist/commands/preview.js +62 -0
  20. package/dist/commands/preview.js.map +1 -0
  21. package/dist/commands/start.d.ts +5 -0
  22. package/dist/commands/start.js +50 -0
  23. package/dist/commands/start.js.map +1 -0
  24. package/dist/commands/stop.d.ts +1 -0
  25. package/dist/commands/stop.js +26 -0
  26. package/dist/commands/stop.js.map +1 -0
  27. package/dist/commands/workflows.test.d.ts +1 -0
  28. package/dist/commands/workflows.test.js +183 -0
  29. package/dist/commands/workflows.test.js.map +1 -0
  30. package/dist/config/default-mdsite-config.d.ts +2 -0
  31. package/dist/config/default-mdsite-config.js +82 -0
  32. package/dist/config/default-mdsite-config.js.map +1 -0
  33. package/dist/config/default-mdsite-config.test.d.ts +1 -0
  34. package/dist/config/default-mdsite-config.test.js +42 -0
  35. package/dist/config/default-mdsite-config.test.js.map +1 -0
  36. package/dist/config/mdsite-config.d.ts +42 -0
  37. package/dist/config/mdsite-config.js +74 -0
  38. package/dist/config/mdsite-config.js.map +1 -0
  39. package/dist/config/mdsite-config.test.d.ts +1 -0
  40. package/dist/config/mdsite-config.test.js +115 -0
  41. package/dist/config/mdsite-config.test.js.map +1 -0
  42. package/dist/config/menu.d.ts +3 -0
  43. package/dist/config/menu.js +58 -0
  44. package/dist/config/menu.js.map +1 -0
  45. package/dist/config/menu.test.d.ts +1 -0
  46. package/dist/config/menu.test.js +53 -0
  47. package/dist/config/menu.test.js.map +1 -0
  48. package/dist/index.d.ts +2 -0
  49. package/dist/index.js +99 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/index.test.d.ts +1 -0
  52. package/dist/index.test.js +133 -0
  53. package/dist/index.test.js.map +1 -0
  54. package/dist/process/child-process.d.ts +9 -0
  55. package/dist/process/child-process.js +142 -0
  56. package/dist/process/child-process.js.map +1 -0
  57. package/dist/process/child-process.test.d.ts +1 -0
  58. package/dist/process/child-process.test.js +131 -0
  59. package/dist/process/child-process.test.js.map +1 -0
  60. package/dist/process/runtime-state.d.ts +16 -0
  61. package/dist/process/runtime-state.js +42 -0
  62. package/dist/process/runtime-state.js.map +1 -0
  63. package/dist/process/runtime-state.test.d.ts +1 -0
  64. package/dist/process/runtime-state.test.js +57 -0
  65. package/dist/process/runtime-state.test.js.map +1 -0
  66. package/dist/renderer/mdsite-nuxt.d.ts +22 -0
  67. package/dist/renderer/mdsite-nuxt.js +194 -0
  68. package/dist/renderer/mdsite-nuxt.js.map +1 -0
  69. package/dist/renderer/mdsite-nuxt.test.d.ts +1 -0
  70. package/dist/renderer/mdsite-nuxt.test.js +226 -0
  71. package/dist/renderer/mdsite-nuxt.test.js.map +1 -0
  72. package/mdsite-nuxt/app/app.vue +7 -0
  73. package/mdsite-nuxt/app/assets/css/bible-tooltips.css +87 -0
  74. package/mdsite-nuxt/app/assets/css/markdown.css +195 -0
  75. package/mdsite-nuxt/app/assets/css/print.css +133 -0
  76. package/mdsite-nuxt/app/components/AppBar.vue +138 -0
  77. package/mdsite-nuxt/app/components/AppFooter.vue +88 -0
  78. package/mdsite-nuxt/app/components/AppNavigation.vue +162 -0
  79. package/mdsite-nuxt/app/components/AppTableOfContents.vue +149 -0
  80. package/mdsite-nuxt/app/components/BreadcrumbNav.vue +139 -0
  81. package/mdsite-nuxt/app/components/NavigationTree.vue +169 -0
  82. package/mdsite-nuxt/app/components/SearchBox.vue +212 -0
  83. package/mdsite-nuxt/app/components/TocItem.vue +75 -0
  84. package/mdsite-nuxt/app/components/TreeNode.vue +264 -0
  85. package/mdsite-nuxt/app/components/content/MarkdownAlert.vue +42 -0
  86. package/mdsite-nuxt/app/components/content/Mermaid.vue +103 -0
  87. package/mdsite-nuxt/app/components/content/ProseA.vue +57 -0
  88. package/mdsite-nuxt/app/components/content/ProseBlockquote.vue +14 -0
  89. package/mdsite-nuxt/app/components/content/ProsePre.vue +49 -0
  90. package/mdsite-nuxt/app/components/content/ProseTable.vue +143 -0
  91. package/mdsite-nuxt/app/composables/useAppTheme.ts +89 -0
  92. package/mdsite-nuxt/app/composables/useBibleTooltips.ts +21 -0
  93. package/mdsite-nuxt/app/composables/useBreadcrumbs.ts +70 -0
  94. package/mdsite-nuxt/app/composables/useContentPostProcessing.ts +89 -0
  95. package/mdsite-nuxt/app/composables/useNavigationTree.ts +174 -0
  96. package/mdsite-nuxt/app/composables/useSearchIndex.ts +47 -0
  97. package/mdsite-nuxt/app/composables/useSearchRelevance.test.ts +255 -0
  98. package/mdsite-nuxt/app/composables/useSearchRelevance.ts +207 -0
  99. package/mdsite-nuxt/app/composables/useSidebarState.ts +62 -0
  100. package/mdsite-nuxt/app/composables/useSiteConfig.ts +37 -0
  101. package/mdsite-nuxt/app/composables/useSourceEdit.ts +69 -0
  102. package/mdsite-nuxt/app/composables/useTableOfContents.ts +141 -0
  103. package/mdsite-nuxt/app/composables/useTableParser.ts +106 -0
  104. package/mdsite-nuxt/app/composables/useTooltipConfig.ts +20 -0
  105. package/mdsite-nuxt/app/config/themes.ts +17 -0
  106. package/mdsite-nuxt/app/layouts/default.vue +376 -0
  107. package/mdsite-nuxt/app/pages/[...slug].vue +56 -0
  108. package/mdsite-nuxt/app/pages/index.vue +54 -0
  109. package/mdsite-nuxt/app/plugins/bible-tooltips.client.ts +401 -0
  110. package/mdsite-nuxt/app/plugins/bible-tooltips.test.ts +284 -0
  111. package/mdsite-nuxt/app/types/content.d.ts +10 -0
  112. package/mdsite-nuxt/app/types/table.ts +57 -0
  113. package/mdsite-nuxt/app/utils/bible-book-names.ts +37 -0
  114. package/mdsite-nuxt/app/utils/bible-verse-utils.test.ts +148 -0
  115. package/mdsite-nuxt/app/utils/bible-verse-utils.ts +236 -0
  116. package/mdsite-nuxt/content.config.ts +18 -0
  117. package/mdsite-nuxt/example.config.yml +67 -0
  118. package/mdsite-nuxt/nuxt.config.ts +247 -0
  119. package/mdsite-nuxt/package-lock.json +15922 -0
  120. package/mdsite-nuxt/package.json +50 -0
  121. package/mdsite-nuxt/playwright.config.js +47 -0
  122. package/mdsite-nuxt/public/robots.txt +2 -0
  123. package/mdsite-nuxt/scripts/generate-favicons.ts +192 -0
  124. package/mdsite-nuxt/scripts/generate-indices.ts +508 -0
  125. package/mdsite-nuxt/scripts/renderer-hooks.test.ts +277 -0
  126. package/mdsite-nuxt/scripts/renderer-hooks.ts +141 -0
  127. package/mdsite-nuxt/scripts/start.test.ts +164 -0
  128. package/mdsite-nuxt/scripts/start.ts +55 -0
  129. package/mdsite-nuxt/scripts/sync-content.ts +504 -0
  130. package/mdsite-nuxt/tsconfig.json +18 -0
  131. package/mdsite-nuxt/utils/mdsite-config.ts +240 -0
  132. package/mdsite-nuxt/vitest.config.ts +7 -0
  133. 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>