@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,212 @@
1
+ <template>
2
+ <div class="search-box">
3
+ <v-text-field
4
+ v-model="searchQuery"
5
+ prepend-inner-icon="mdi-magnify"
6
+ :append-inner-icon="searchQuery ? 'mdi-close' : undefined"
7
+ placeholder="Search pages..."
8
+ variant="outlined"
9
+ density="compact"
10
+ hide-details
11
+ class="search-input"
12
+ @click:append-inner="clearSearch"
13
+ @update:model-value="handleSearch"
14
+ />
15
+
16
+ <!-- Search Results -->
17
+ <div v-if="searchQuery && searchResults.length > 0" class="search-results">
18
+ <v-list density="compact">
19
+ <v-tooltip
20
+ v-for="result in searchResults"
21
+ :key="result.path"
22
+ :text="result.description"
23
+ :disabled="!result.description"
24
+ :location="tooltip.location.value"
25
+ :max-width="tooltip.maxWidth"
26
+ >
27
+ <template #activator="{ props: tooltipProps }">
28
+ <v-list-item
29
+ v-bind="tooltipProps"
30
+ class="search-result-item"
31
+ @click="handleSelect(result.path)"
32
+ >
33
+ <v-list-item-title class="result-title">
34
+ {{ result.title }}
35
+ </v-list-item-title>
36
+ <v-list-item-subtitle class="result-breadcrumb">
37
+ {{ result.breadcrumb }}
38
+ </v-list-item-subtitle>
39
+ </v-list-item>
40
+ </template>
41
+ </v-tooltip>
42
+ </v-list>
43
+ </div>
44
+
45
+ <!-- No Results -->
46
+ <div v-else-if="searchQuery && searchResults.length === 0" class="no-results">
47
+ <v-list-item>
48
+ <v-list-item-title class="text-center text-caption">
49
+ No results found for "{{ searchQuery }}"
50
+ </v-list-item-title>
51
+ </v-list-item>
52
+ </div>
53
+ </div>
54
+ </template>
55
+
56
+ <script setup lang="ts">
57
+ import type { SearchableFields } from '~/composables/useSearchRelevance'
58
+ import type { SearchIndexEntry } from '~/../../scripts/generate-indices'
59
+
60
+ interface SearchResult {
61
+ path: string
62
+ title: string
63
+ breadcrumb: string
64
+ description?: string
65
+ }
66
+
67
+ const emit = defineEmits<{
68
+ select: [path: string]
69
+ clear: []
70
+ 'search-active': [active: boolean]
71
+ }>()
72
+
73
+ const searchQuery = ref('')
74
+ const searchResults = ref<SearchResult[]>([])
75
+
76
+ // Shared tooltip configuration
77
+ const tooltip = useTooltipConfig()
78
+
79
+ // Relevance scoring
80
+ const { sortByRelevance } = useSearchRelevance()
81
+
82
+ // Search index fetcher
83
+ const { loadSearchIndex } = useSearchIndex()
84
+
85
+ /**
86
+ * Check if page matches search query
87
+ */
88
+ function pageMatchesQuery(page: SearchIndexEntry, queryLower: string): boolean {
89
+ // Check title, description, excerpt
90
+ const textMatch = (
91
+ page.title.toLowerCase().includes(queryLower) ||
92
+ page.description?.toLowerCase().includes(queryLower) ||
93
+ page.excerpt?.toLowerCase().includes(queryLower)
94
+ )
95
+
96
+ // Check keywords array
97
+ const keywordsMatch = page.keywords?.some((keyword: string) =>
98
+ keyword.toLowerCase().includes(queryLower)
99
+ )
100
+
101
+ return textMatch || keywordsMatch || false
102
+ }
103
+
104
+ /**
105
+ * Handle search input
106
+ */
107
+ async function handleSearch(query: string) {
108
+ if (!query || query.trim() === '') {
109
+ searchResults.value = []
110
+ emit('search-active', false)
111
+ return
112
+ }
113
+
114
+ emit('search-active', true)
115
+
116
+ try {
117
+ // Load pre-built search index (cached after first load)
118
+ const allPages = await loadSearchIndex()
119
+ const queryLower = query.toLowerCase()
120
+
121
+ // Filter pages that match query
122
+ const matchingPages = allPages.filter(page =>
123
+ pageMatchesQuery(page, queryLower)
124
+ )
125
+
126
+ // Sort by relevance score (SearchIndexEntry is compatible with SearchableFields)
127
+ const sortedPages = sortByRelevance(matchingPages, query, (page) => page.title)
128
+
129
+ // Map top 50 results to SearchResult interface
130
+ searchResults.value = sortedPages
131
+ .slice(0, 50)
132
+ .map((page) => {
133
+ const segments = page.path.split('/').filter(Boolean)
134
+ return {
135
+ path: page.path,
136
+ title: page.title,
137
+ breadcrumb: segments.length > 0
138
+ ? segments.join('/')
139
+ : '/',
140
+ description: page.description
141
+ }
142
+ })
143
+ } catch (error) {
144
+ console.error('Search error:', error)
145
+ searchResults.value = []
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Clear search
151
+ */
152
+ function clearSearch() {
153
+ searchQuery.value = ''
154
+ searchResults.value = []
155
+ emit('clear')
156
+ emit('search-active', false)
157
+ }
158
+
159
+ /**
160
+ * Handle result selection
161
+ */
162
+ function handleSelect(path: string) {
163
+ emit('select', path)
164
+ clearSearch()
165
+ }
166
+ </script>
167
+
168
+ <style scoped>
169
+ .search-box {
170
+ position: sticky;
171
+ top: 0;
172
+ z-index: 10;
173
+ padding: 0.75rem;
174
+ }
175
+
176
+ .search-results {
177
+ max-height: calc(100vh - 12.5rem);
178
+ overflow-y: auto;
179
+ margin-top: 0.5rem;
180
+ background-color: rgb(var(--v-theme-surface-rail));
181
+ border-radius: 0.5rem;
182
+ border: 1px solid rgb(var(--v-theme-outline-bars));
183
+ }
184
+
185
+ .search-result-item {
186
+ cursor: pointer;
187
+ border-radius: 0.25rem;
188
+ margin: 0.25rem;
189
+ }
190
+
191
+ .search-result-item:hover {
192
+ background-color: rgba(var(--v-theme-on-selectable), 0.04);
193
+ }
194
+
195
+ .result-title {
196
+ font-weight: 500;
197
+ color: rgb(var(--v-theme-on-selectable));
198
+ }
199
+
200
+ .result-breadcrumb {
201
+ font-size: 0.75rem;
202
+ color: rgb(var(--v-theme-on-selectable));
203
+ opacity: 0.6;
204
+ }
205
+
206
+ .no-results {
207
+ margin-top: 0.5rem;
208
+ padding: 1rem;
209
+ color: rgb(var(--v-theme-on-surface-rail));
210
+ opacity: 0.6;
211
+ }
212
+ </style>
@@ -0,0 +1,75 @@
1
+ <template>
2
+ <div
3
+ class="toc-item"
4
+ :class="{ 'is-active': isActive }"
5
+ :style="{ paddingLeft: `${item.level * 16}px` }"
6
+ @click="handleClick"
7
+ >
8
+ <span class="toc-text">{{ item.text }}</span>
9
+ </div>
10
+ </template>
11
+
12
+ <script setup lang="ts">
13
+ import type { TocItem as TocItemType } from '~/composables/useTableOfContents'
14
+
15
+ const props = defineProps<{
16
+ item: TocItemType
17
+ isActive: boolean
18
+ }>()
19
+
20
+ const emit = defineEmits<{
21
+ click: [id: string]
22
+ }>()
23
+
24
+ function handleClick() {
25
+ emit('click', props.item.id)
26
+ }
27
+ </script>
28
+
29
+ <style scoped>
30
+ .toc-item {
31
+ min-height: 36px;
32
+ display: flex;
33
+ align-items: center;
34
+ padding: 6px 12px;
35
+ cursor: pointer;
36
+ border-radius: 4px;
37
+ transition: background-color 0.15s ease, padding-left 0.15s ease;
38
+ margin: 2px 0;
39
+ }
40
+
41
+ .toc-item:hover {
42
+ background-color: rgba(var(--v-theme-on-selectable), 0.04);
43
+ }
44
+
45
+ .toc-item.is-active {
46
+ background-color: rgb(var(--v-theme-selected));
47
+ }
48
+
49
+ .toc-text {
50
+ font-size: 0.813rem;
51
+ line-height: 1.4;
52
+ color: rgb(var(--v-theme-on-selectable));
53
+ white-space: nowrap;
54
+ overflow: hidden;
55
+ text-overflow: ellipsis;
56
+ transition: color 0.15s ease;
57
+ }
58
+
59
+ .toc-item:hover .toc-text {
60
+ color: rgb(var(--v-theme-on-selectable));
61
+ text-decoration: underline;
62
+ }
63
+
64
+ .toc-item.is-active .toc-text {
65
+ color: rgb(var(--v-theme-on-selected));
66
+ font-weight: 600;
67
+ }
68
+
69
+ /* Touch target size for mobile */
70
+ @media (max-width: 599px) {
71
+ .toc-item {
72
+ min-height: 44px;
73
+ }
74
+ }
75
+ </style>
@@ -0,0 +1,264 @@
1
+ <template>
2
+ <div class="tree-node-wrapper">
3
+ <!-- Separator rendering -->
4
+ <v-divider
5
+ v-if="node.isSeparator"
6
+ :style="{ marginLeft: `${depth * 1.25}rem`, marginTop: '0.5rem', marginBottom: '0.5rem', marginRight: '0.5rem' }"
7
+ />
8
+
9
+ <!-- Header rendering (non-clickable section label) -->
10
+ <div
11
+ v-else-if="node.isHeader"
12
+ class="tree-header"
13
+ :style="{ paddingLeft: `${depth * 1.25 + 1.75}rem` }"
14
+ >
15
+ <span class="header-title">{{ node.title }}</span>
16
+ </div>
17
+
18
+ <div
19
+ v-else
20
+ class="tree-node"
21
+ :class="{
22
+ 'is-active': isActive,
23
+ 'is-parent': hasChildren,
24
+ 'is-expanded': isExpanded
25
+ }"
26
+ :style="{ paddingLeft: `${depth * 1.25}rem` }"
27
+ >
28
+ <!-- Expand/collapse chevron for parent nodes -->
29
+ <v-btn
30
+ v-if="hasChildren"
31
+ icon
32
+ size="x-small"
33
+ variant="text"
34
+ class="chevron-button"
35
+ @click="handleToggle"
36
+ >
37
+ <v-icon
38
+ :icon="isExpanded ? 'mdi-chevron-down' : 'mdi-chevron-right'"
39
+ size="16"
40
+ />
41
+ </v-btn>
42
+
43
+ <!-- Active indicator dot -->
44
+ <span v-else class="leaf-indicator" />
45
+
46
+ <!-- Node title with optional tooltip -->
47
+ <v-tooltip
48
+ v-if="node.description"
49
+ :text="node.description"
50
+ :location="tooltip.location.value"
51
+ :max-width="tooltip.maxWidth"
52
+ >
53
+ <template #activator="{ props: tooltipProps }">
54
+ <span
55
+ v-bind="tooltipProps"
56
+ class="node-title"
57
+ :class="{ 'is-parent-title': hasChildren }"
58
+ @click="handleSelect"
59
+ >
60
+ {{ node.title }}
61
+ <v-icon
62
+ v-if="node.isExternal"
63
+ icon="mdi-open-in-new"
64
+ size="14"
65
+ class="external-icon"
66
+ />
67
+ </span>
68
+ </template>
69
+ </v-tooltip>
70
+ <span
71
+ v-else
72
+ class="node-title"
73
+ :class="{ 'is-parent-title': hasChildren }"
74
+ @click="handleSelect"
75
+ >
76
+ {{ node.title }}
77
+ <v-icon
78
+ v-if="node.isExternal"
79
+ icon="mdi-open-in-new"
80
+ size="14"
81
+ class="external-icon"
82
+ />
83
+ </span>
84
+ </div>
85
+
86
+ <!-- Recursively render children -->
87
+ <div v-if="isExpanded && hasChildren" class="tree-children">
88
+ <TreeNode
89
+ v-for="child in node.children"
90
+ :key="child.id"
91
+ :node="child"
92
+ :active-path="activePath"
93
+ :expanded-ids="expandedIds"
94
+ :depth="depth + 1"
95
+ @toggle="$emit('toggle', $event)"
96
+ @select="$emit('select', $event)"
97
+ />
98
+ </div>
99
+ </div>
100
+ </template>
101
+
102
+ <script setup lang="ts">
103
+ import type { TreeNode as TreeNodeType } from '~/composables/useNavigationTree'
104
+
105
+ const props = defineProps<{
106
+ node: TreeNodeType
107
+ activePath: string
108
+ expandedIds: Set<string>
109
+ depth: number
110
+ }>()
111
+
112
+ const emit = defineEmits<{
113
+ toggle: [id: string]
114
+ select: [path: string]
115
+ }>()
116
+
117
+ const hasChildren = computed(() => {
118
+ return props.node.children && props.node.children.length > 0
119
+ })
120
+
121
+ const isExpanded = computed(() => props.expandedIds.has(props.node.id))
122
+ const isActive = computed(() => {
123
+ // Only primary menu items can be highlighted
124
+ // Aliases (custom titled links) should never be highlighted
125
+ return props.node.isPrimary === true && props.node.path === props.activePath
126
+ })
127
+
128
+ // Shared tooltip configuration
129
+ const tooltip = useTooltipConfig()
130
+
131
+ function handleToggle() {
132
+ emit('toggle', props.node.id)
133
+ }
134
+
135
+ function handleSelect() {
136
+ // Handle external URLs differently
137
+ if (props.node.isExternal && props.node.externalUrl) {
138
+ window.open(props.node.externalUrl, '_blank', 'noopener,noreferrer')
139
+ return
140
+ }
141
+
142
+ // For parent nodes: toggle expansion AND navigate
143
+ if (hasChildren.value) {
144
+ emit('toggle', props.node.id)
145
+ }
146
+
147
+ // Navigate to the page
148
+ emit('select', props.node.path)
149
+ }
150
+ </script>
151
+
152
+ <style scoped>
153
+ .tree-node-wrapper {
154
+ width: 100%;
155
+ }
156
+
157
+ .tree-node {
158
+ display: flex;
159
+ align-items: center;
160
+ min-height: 2.75rem;
161
+ padding: 0.25rem 0.5rem;
162
+ cursor: pointer;
163
+ transition: background-color 0.15s ease;
164
+ border-radius: 0.25rem;
165
+ margin: 0.125rem 0;
166
+ }
167
+
168
+ .tree-node:hover {
169
+ background-color: rgba(var(--v-theme-on-selectable), 0.04);
170
+ }
171
+
172
+ .tree-node.is-active {
173
+ background-color: rgb(var(--v-theme-selected));
174
+ color: rgb(var(--v-theme-on-selected));
175
+ }
176
+
177
+ .tree-header {
178
+ display: flex;
179
+ align-items: center;
180
+ min-height: 2rem;
181
+ padding: 0.75rem 0.5rem 0.25rem 0.5rem;
182
+ margin-top: 0.5rem;
183
+ }
184
+
185
+ .header-title {
186
+ font-size: 0.85rem;
187
+ font-weight: 600;
188
+ color: rgb(var(--v-theme-on-surface-rail));
189
+ text-transform: uppercase;
190
+ letter-spacing: 0.05rem;
191
+ white-space: nowrap;
192
+ overflow: hidden;
193
+ text-overflow: ellipsis;
194
+ }
195
+
196
+ .chevron-button {
197
+ margin-right: 0.25rem;
198
+ flex-shrink: 0;
199
+ }
200
+
201
+ .tree-node.is-active :deep(.chevron-button .v-icon) {
202
+ color: rgb(var(--v-theme-on-selected));
203
+ }
204
+
205
+ .leaf-indicator {
206
+ width: 1.5rem;
207
+ height: 1.5rem;
208
+ margin-right: 0.25rem;
209
+ flex-shrink: 0;
210
+ display: flex;
211
+ align-items: center;
212
+ justify-content: center;
213
+ }
214
+
215
+ .leaf-indicator::before {
216
+ content: '';
217
+ width: 0.375rem;
218
+ height: 0.375rem;
219
+ border-radius: 50%;
220
+ background-color: transparent;
221
+ transition: background-color 0.15s ease;
222
+ }
223
+
224
+ .tree-node.is-active .leaf-indicator::before {
225
+ background-color: rgb(var(--v-theme-on-selected));
226
+ }
227
+
228
+ .node-title {
229
+ flex: 1;
230
+ font-size: 0.875rem;
231
+ font-weight: 400;
232
+ color: rgb(var(--v-theme-on-selectable));
233
+ white-space: nowrap;
234
+ overflow: hidden;
235
+ text-overflow: ellipsis;
236
+ transition: color 0.15s ease;
237
+ }
238
+
239
+ .node-title.is-parent-title {
240
+ font-weight: 500;
241
+ }
242
+
243
+ .tree-node.is-active .node-title {
244
+ font-weight: 600;
245
+ color: rgb(var(--v-theme-on-selected));
246
+ }
247
+
248
+ .tree-children {
249
+ width: 100%;
250
+ }
251
+
252
+ .external-icon {
253
+ margin-left: 0.25rem;
254
+ opacity: 0.6;
255
+ vertical-align: middle;
256
+ }
257
+
258
+ /* Touch target size for mobile */
259
+ @media (max-width: 599px) {
260
+ .tree-node {
261
+ min-height: 3rem;
262
+ }
263
+ }
264
+ </style>
@@ -0,0 +1,42 @@
1
+ <template>
2
+ <div :class="['markdown-alert', `markdown-alert-${type}`]">
3
+ <p class="markdown-alert-title">
4
+ <v-icon :icon="icon" size="small" class="mr-2" />
5
+ {{ title }}
6
+ </p>
7
+ <div class="alert-content">
8
+ <slot />
9
+ </div>
10
+ </div>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ const props = defineProps<{
15
+ type: 'note' | 'tip' | 'important' | 'warning' | 'caution'
16
+ }>()
17
+
18
+ const iconMap = {
19
+ note: 'mdi-information',
20
+ tip: 'mdi-lightbulb',
21
+ important: 'mdi-alert-circle',
22
+ warning: 'mdi-alert',
23
+ caution: 'mdi-alert-octagon'
24
+ }
25
+
26
+ const titleMap = {
27
+ note: 'Note',
28
+ tip: 'Tip',
29
+ important: 'Important',
30
+ warning: 'Warning',
31
+ caution: 'Caution'
32
+ }
33
+
34
+ const icon = computed(() => iconMap[props.type] || iconMap.note)
35
+ const title = computed(() => titleMap[props.type] || titleMap.note)
36
+ </script>
37
+
38
+ <style scoped>
39
+ .alert-content :deep(p:last-child) {
40
+ margin-bottom: 0;
41
+ }
42
+ </style>
@@ -0,0 +1,103 @@
1
+ <template>
2
+ <div class="mermaid-container">
3
+ <div v-if="!svg" class="mermaid-loading">
4
+ <v-progress-circular indeterminate size="24"></v-progress-circular>
5
+ <span>Rendering diagram...</span>
6
+ </div>
7
+ <div v-html="svg" ref="container"></div>
8
+ </div>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ import { ref, onMounted, watch } from 'vue'
13
+
14
+ const props = defineProps({
15
+ code: {
16
+ type: String,
17
+ required: true
18
+ }
19
+ })
20
+
21
+ const svg = ref('')
22
+ const container = ref<HTMLElement | null>(null)
23
+
24
+ const renderDiagram = async () => {
25
+ if (process.server) return
26
+
27
+ try {
28
+ const mermaid = (await import('mermaid')).default
29
+ mermaid.initialize({
30
+ startOnLoad: false,
31
+ theme: 'base',
32
+ themeVariables: {
33
+ primaryColor: '#6200ee',
34
+ primaryTextColor: '#fff',
35
+ primaryBorderColor: '#6200ee',
36
+ lineColor: '#6200ee',
37
+ secondaryColor: '#03dac6',
38
+ tertiaryColor: '#f5f5f5'
39
+ },
40
+ securityLevel: 'loose',
41
+ fontFamily: 'Noto Sans, sans-serif'
42
+ })
43
+
44
+ const id = `mermaid-${Math.random().toString(36).substr(2, 9)}`
45
+ const { svg: renderedSvg } = await mermaid.render(id, props.code)
46
+ svg.value = renderedSvg
47
+ } catch (error) {
48
+ console.error('Mermaid rendering failed:', error)
49
+ svg.value = `<div class="error">Failed to render diagram: ${error}</div>`
50
+ }
51
+ }
52
+
53
+ onMounted(() => {
54
+ renderDiagram()
55
+ })
56
+
57
+ watch(() => props.code, () => {
58
+ renderDiagram()
59
+ })
60
+ </script>
61
+
62
+ <style scoped>
63
+ .mermaid-container {
64
+ display: flex;
65
+ justify-content: center;
66
+ margin: 1.5rem 0;
67
+ padding: 1rem;
68
+ background-color: rgb(var(--v-theme-surface));
69
+ border-radius: 12px;
70
+ overflow-x: auto;
71
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
72
+ border: 1px solid rgba(var(--v-theme-on-surface), 0.05);
73
+ }
74
+
75
+ .mermaid-loading {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 12px;
79
+ color: rgb(var(--v-theme-on-surface-variant));
80
+ font-style: italic;
81
+ }
82
+
83
+ :deep(svg) {
84
+ max-width: 100%;
85
+ height: auto;
86
+ }
87
+
88
+ .error {
89
+ color: var(--v-theme-error);
90
+ padding: 1rem;
91
+ font-family: monospace;
92
+ }
93
+
94
+ @media print {
95
+ .mermaid-container {
96
+ background: white !important;
97
+ border: 1px solid #ccc !important;
98
+ box-shadow: none !important;
99
+ margin: 1rem 0 !important;
100
+ padding: 0 !important;
101
+ }
102
+ }
103
+ </style>