@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,57 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<NuxtLink :to="cleanHref" :target="target">
|
|
3
|
+
<slot />
|
|
4
|
+
</NuxtLink>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
/**
|
|
9
|
+
* Custom ProseA component for Nuxt Content
|
|
10
|
+
*
|
|
11
|
+
* Strips .md extensions from internal links for web routes.
|
|
12
|
+
* This allows markdown files to have .md extensions (for IDE preview)
|
|
13
|
+
* while web routes remain clean (without .md).
|
|
14
|
+
*
|
|
15
|
+
* Example:
|
|
16
|
+
* - Markdown: [link](/docs/history.md)
|
|
17
|
+
* - Rendered: <a href="/docs/history">link</a>
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
interface ProseAProps {
|
|
21
|
+
href?: string
|
|
22
|
+
target?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const props = defineProps<ProseAProps>()
|
|
26
|
+
|
|
27
|
+
// Strip /content/{domain}/ prefix and .md extension from href for web routes
|
|
28
|
+
// External URLs (http/https) are passed through unchanged
|
|
29
|
+
const cleanHref = computed(() => {
|
|
30
|
+
if (!props.href) return props.href
|
|
31
|
+
|
|
32
|
+
// Don't modify external URLs
|
|
33
|
+
if (props.href.startsWith('http://') || props.href.startsWith('https://')) {
|
|
34
|
+
return props.href
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Don't modify anchor-only links
|
|
38
|
+
if (props.href.startsWith('#')) {
|
|
39
|
+
return props.href
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let cleaned = props.href
|
|
43
|
+
|
|
44
|
+
// Strip /content/{domain}/ prefix from absolute paths (for web routing)
|
|
45
|
+
// Example: /content/example/docs/history.md → /docs/history.md
|
|
46
|
+
// This allows IDE to navigate with full paths while web uses clean URLs
|
|
47
|
+
cleaned = cleaned.replace(/^\/content\/[^/]+/, '')
|
|
48
|
+
|
|
49
|
+
// Strip .md extension from internal links (before any fragment/query)
|
|
50
|
+
// Example: /docs/history.md → /docs/history
|
|
51
|
+
// Example: /docs/history.md#section → /docs/history#section
|
|
52
|
+
// Example: ../parent.md → ../parent
|
|
53
|
+
cleaned = cleaned.replace(/\.md(#|\?|$)/, '$1')
|
|
54
|
+
|
|
55
|
+
return cleaned
|
|
56
|
+
})
|
|
57
|
+
</script>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card class="my-4">
|
|
3
|
+
<v-card-text>
|
|
4
|
+
<div class="blockquote-content">
|
|
5
|
+
<slot />
|
|
6
|
+
</div>
|
|
7
|
+
</v-card-text>
|
|
8
|
+
</v-card>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script setup lang="ts">
|
|
12
|
+
// Custom blockquote rendering as VCard for Material Design styling
|
|
13
|
+
// Preserves the beautiful card-based blockquote design
|
|
14
|
+
</script>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Mermaid v-if="language === 'mermaid'" :code="code" />
|
|
3
|
+
<pre v-else :class="$attrs.class"><slot /></pre>
|
|
4
|
+
</template>
|
|
5
|
+
|
|
6
|
+
<script setup lang="ts">
|
|
7
|
+
const props = defineProps({
|
|
8
|
+
code: {
|
|
9
|
+
type: String,
|
|
10
|
+
default: ''
|
|
11
|
+
},
|
|
12
|
+
language: {
|
|
13
|
+
type: String,
|
|
14
|
+
default: null
|
|
15
|
+
},
|
|
16
|
+
filename: {
|
|
17
|
+
type: String,
|
|
18
|
+
default: null
|
|
19
|
+
},
|
|
20
|
+
highlights: {
|
|
21
|
+
type: Array,
|
|
22
|
+
default: () => []
|
|
23
|
+
},
|
|
24
|
+
meta: {
|
|
25
|
+
type: String,
|
|
26
|
+
default: null
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<style scoped>
|
|
32
|
+
pre {
|
|
33
|
+
border-radius: 1rem;
|
|
34
|
+
position: relative;
|
|
35
|
+
padding: 1rem;
|
|
36
|
+
overflow-x: auto;
|
|
37
|
+
background-color: #ddd;
|
|
38
|
+
border: 2px solid rgb(var(--v-theme-outline));
|
|
39
|
+
border-radius: 1rem !important;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@media print {
|
|
43
|
+
pre {
|
|
44
|
+
background-color: white !important;
|
|
45
|
+
border-radius: 1rem !important;
|
|
46
|
+
border: 2px solid #ccc !important;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
</style>
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="prose-table-wrapper">
|
|
3
|
+
<!-- Hidden original table for SSR and parsing -->
|
|
4
|
+
<div ref="tableContainerRef" class="original-table" :style="{ display: isClient && headers.length > 0 ? 'none' : 'block' }">
|
|
5
|
+
<table>
|
|
6
|
+
<slot />
|
|
7
|
+
</table>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<!-- v-data-table rendered client-side -->
|
|
11
|
+
<v-data-table
|
|
12
|
+
v-if="isClient && headers.length > 0"
|
|
13
|
+
:headers="headers"
|
|
14
|
+
:items="items"
|
|
15
|
+
:items-per-page="-1"
|
|
16
|
+
:mobile-breakpoint="600"
|
|
17
|
+
class="my-4"
|
|
18
|
+
density="comfortable"
|
|
19
|
+
hide-default-footer
|
|
20
|
+
>
|
|
21
|
+
<!-- Custom cell rendering to support HTML content -->
|
|
22
|
+
<template v-for="header in headers" :key="header.key" #[`item.${header.key}`]="{ value }">
|
|
23
|
+
<!-- eslint-disable vue/no-v-html -->
|
|
24
|
+
<span v-html="value" />
|
|
25
|
+
<!-- eslint-enable vue/no-v-html -->
|
|
26
|
+
</template>
|
|
27
|
+
</v-data-table>
|
|
28
|
+
</div>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<script setup lang="ts">
|
|
32
|
+
import { ref, onMounted } from 'vue'
|
|
33
|
+
import { useTableParser } from '~/composables/useTableParser'
|
|
34
|
+
import type { TableHeader, TableItem } from '~/types/table'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Custom ProseTable component for Nuxt Content
|
|
38
|
+
*
|
|
39
|
+
* Renders markdown tables as Vuetify v-data-table components with:
|
|
40
|
+
* - Material Design 3 styling
|
|
41
|
+
* - Sorting capabilities (click column headers)
|
|
42
|
+
* - Responsive mobile layout (cards on small screens)
|
|
43
|
+
* - All rows displayed (no pagination)
|
|
44
|
+
*
|
|
45
|
+
* SSR Strategy:
|
|
46
|
+
* - Server renders original <table> for SEO and accessibility
|
|
47
|
+
* - Client parses table and renders v-data-table on mount
|
|
48
|
+
* - Original table hidden once v-data-table is ready
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
const tableContainerRef = ref<HTMLDivElement | null>(null)
|
|
52
|
+
const headers = ref<TableHeader[]>([])
|
|
53
|
+
const items = ref<TableItem[]>([])
|
|
54
|
+
const isClient = ref(false)
|
|
55
|
+
|
|
56
|
+
const { parseTable } = useTableParser()
|
|
57
|
+
|
|
58
|
+
onMounted(() => {
|
|
59
|
+
isClient.value = true
|
|
60
|
+
|
|
61
|
+
// Wait for next tick to ensure slot content is fully rendered
|
|
62
|
+
nextTick(() => {
|
|
63
|
+
if (!tableContainerRef.value) return
|
|
64
|
+
|
|
65
|
+
// Find the actual <table> element in the rendered slot
|
|
66
|
+
const tableElement = tableContainerRef.value.querySelector('table')
|
|
67
|
+
|
|
68
|
+
if (!tableElement) {
|
|
69
|
+
console.warn('[ProseTable] No <table> element found in slot content')
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Parse the table structure
|
|
74
|
+
const parsed = parseTable(tableElement)
|
|
75
|
+
|
|
76
|
+
if (parsed.headers.length === 0) {
|
|
77
|
+
console.warn('[ProseTable] No headers found in table. Falling back to original table rendering.')
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Update reactive data
|
|
82
|
+
headers.value = parsed.headers
|
|
83
|
+
items.value = parsed.items
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<style scoped>
|
|
89
|
+
.prose-table-wrapper {
|
|
90
|
+
margin: 1rem 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Style original table with basic formatting when v-data-table isn't available */
|
|
94
|
+
.original-table table {
|
|
95
|
+
width: 100%;
|
|
96
|
+
border-collapse: collapse;
|
|
97
|
+
margin: 1rem 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.original-table th,
|
|
101
|
+
.original-table td {
|
|
102
|
+
padding: 0.75rem 1rem;
|
|
103
|
+
text-align: left;
|
|
104
|
+
border-bottom: 1px solid rgb(var(--v-theme-outline));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.original-table th {
|
|
108
|
+
font-weight: 500;
|
|
109
|
+
background-color: rgb(var(--v-theme-surface-container));
|
|
110
|
+
color: rgb(var(--v-theme-on-surface-variant));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.original-table tr:hover {
|
|
114
|
+
background-color: rgba(var(--v-theme-on-surface), 0.04);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* Style lists and line breaks within table cells */
|
|
118
|
+
.prose-table-wrapper :deep(.v-data-table) ul,
|
|
119
|
+
.prose-table-wrapper :deep(.v-data-table) ol {
|
|
120
|
+
margin: 0.25rem 0;
|
|
121
|
+
padding-left: 1.25rem;
|
|
122
|
+
list-style-position: outside;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.prose-table-wrapper :deep(.v-data-table) ul {
|
|
126
|
+
list-style-type: disc;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.prose-table-wrapper :deep(.v-data-table) ol {
|
|
130
|
+
list-style-type: decimal;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.prose-table-wrapper :deep(.v-data-table) li {
|
|
134
|
+
margin: 0.25rem 0;
|
|
135
|
+
line-height: 1.5;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.prose-table-wrapper :deep(.v-data-table) br {
|
|
139
|
+
display: block;
|
|
140
|
+
content: "";
|
|
141
|
+
margin-top: 0.375rem;
|
|
142
|
+
}
|
|
143
|
+
</style>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useTheme as useVuetifyTheme } from 'vuetify'
|
|
2
|
+
|
|
3
|
+
export type ThemeMode = 'light' | 'dark'
|
|
4
|
+
|
|
5
|
+
const THEME_STORAGE_KEY = 'theme-preference'
|
|
6
|
+
|
|
7
|
+
export const useAppTheme = () => {
|
|
8
|
+
const vuetifyTheme = useVuetifyTheme()
|
|
9
|
+
|
|
10
|
+
// Get the current theme preference from localStorage
|
|
11
|
+
const getStoredTheme = (): ThemeMode | null => {
|
|
12
|
+
if (import.meta.client) {
|
|
13
|
+
const stored = localStorage.getItem(THEME_STORAGE_KEY) as ThemeMode
|
|
14
|
+
return stored && ['light', 'dark'].includes(stored) ? stored : null
|
|
15
|
+
}
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Get system theme preference
|
|
20
|
+
const getSystemTheme = (): ThemeMode => {
|
|
21
|
+
if (import.meta.client && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
22
|
+
return 'dark'
|
|
23
|
+
}
|
|
24
|
+
return 'light'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Store theme preference in localStorage
|
|
28
|
+
const setStoredTheme = (theme: ThemeMode) => {
|
|
29
|
+
if (import.meta.client) {
|
|
30
|
+
localStorage.setItem(THEME_STORAGE_KEY, theme)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Reactive theme preference
|
|
35
|
+
const themePreference = ref<ThemeMode>('light')
|
|
36
|
+
|
|
37
|
+
// Apply theme to Vuetify
|
|
38
|
+
const applyTheme = (theme: ThemeMode) => {
|
|
39
|
+
// Use the new Vuetify 3.7+ API: theme.change()
|
|
40
|
+
if (vuetifyTheme && typeof (vuetifyTheme as any).change === 'function') {
|
|
41
|
+
(vuetifyTheme as any).change(theme)
|
|
42
|
+
} else if (vuetifyTheme.global) {
|
|
43
|
+
// Fallback for older versions
|
|
44
|
+
vuetifyTheme.global.name.value = theme
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Set theme preference and apply it
|
|
49
|
+
const setTheme = (theme: ThemeMode) => {
|
|
50
|
+
themePreference.value = theme
|
|
51
|
+
setStoredTheme(theme)
|
|
52
|
+
applyTheme(theme)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Toggle between light and dark
|
|
56
|
+
const toggleTheme = () => {
|
|
57
|
+
const newTheme: ThemeMode = themePreference.value === 'dark' ? 'light' : 'dark'
|
|
58
|
+
setTheme(newTheme)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Initialize theme on mount
|
|
62
|
+
onMounted(() => {
|
|
63
|
+
const storedTheme = getStoredTheme()
|
|
64
|
+
const initialTheme = storedTheme || getSystemTheme()
|
|
65
|
+
|
|
66
|
+
themePreference.value = initialTheme
|
|
67
|
+
applyTheme(initialTheme)
|
|
68
|
+
|
|
69
|
+
// Listen for system theme changes if no user preference
|
|
70
|
+
if (import.meta.client && !storedTheme) {
|
|
71
|
+
const media = window.matchMedia('(prefers-color-scheme: dark)')
|
|
72
|
+
media.addEventListener('change', (e) => {
|
|
73
|
+
// Only react to system changes if user hasn't manually set a valid preference (optional logic, but matches typical behavior)
|
|
74
|
+
// For now, let's just update if no stored theme
|
|
75
|
+
if (!getStoredTheme()) {
|
|
76
|
+
const newSystemTheme = e.matches ? 'dark' : 'light'
|
|
77
|
+
themePreference.value = newSystemTheme
|
|
78
|
+
applyTheme(newSystemTheme)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
themePreference: readonly(themePreference),
|
|
86
|
+
setTheme,
|
|
87
|
+
toggleTheme,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable for managing Bible tooltip scanning on page content
|
|
3
|
+
* Automatically scans content when it becomes available and re-scans on changes
|
|
4
|
+
*/
|
|
5
|
+
export function useBibleTooltips(pageRef: Ref<any>) {
|
|
6
|
+
const { $bibleTooltips } = useNuxtApp()
|
|
7
|
+
|
|
8
|
+
onMounted(() => {
|
|
9
|
+
// Initial scan if page is already loaded
|
|
10
|
+
if (pageRef.value) {
|
|
11
|
+
nextTick(() => $bibleTooltips.scan())
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Watch for content changes and re-scan
|
|
15
|
+
watch(pageRef, (newPage) => {
|
|
16
|
+
if (newPage) {
|
|
17
|
+
nextTick(() => $bibleTooltips.scan())
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export interface BreadcrumbItem {
|
|
2
|
+
title: string
|
|
3
|
+
path: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract fallback title from path
|
|
8
|
+
*/
|
|
9
|
+
function getFallbackTitle(pagePath: string, siteName?: string): string {
|
|
10
|
+
if (pagePath === '/') return siteName || 'Home'
|
|
11
|
+
return pagePath.split('/').filter(Boolean).pop() || 'Page'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate breadcrumb array from a given path
|
|
16
|
+
* @param path - Route path (e.g., '/church/history/messianic')
|
|
17
|
+
* @returns Array of breadcrumb items
|
|
18
|
+
*/
|
|
19
|
+
export async function generateBreadcrumbs(path: string): Promise<BreadcrumbItem[]> {
|
|
20
|
+
const segments = path.split('/').filter(Boolean)
|
|
21
|
+
const { siteName } = useSiteConfig()
|
|
22
|
+
|
|
23
|
+
// Build full path array for querying (includes root)
|
|
24
|
+
const paths: string[] = ['/']
|
|
25
|
+
for (let i = 0; i < segments.length; i++) {
|
|
26
|
+
paths.push('/' + segments.slice(0, i + 1).join('/'))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Query all pages to get titles
|
|
30
|
+
const pageTitles = new Map<string, string>()
|
|
31
|
+
|
|
32
|
+
for (const pagePath of paths) {
|
|
33
|
+
try {
|
|
34
|
+
const page = await queryCollection('content').path(pagePath).first()
|
|
35
|
+
pageTitles.set(pagePath, page?.title || getFallbackTitle(pagePath, siteName))
|
|
36
|
+
} catch {
|
|
37
|
+
pageTitles.set(pagePath, getFallbackTitle(pagePath, siteName))
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Build breadcrumb items based on path depth
|
|
42
|
+
const breadcrumbs: BreadcrumbItem[] = []
|
|
43
|
+
|
|
44
|
+
if (segments.length <= 3) {
|
|
45
|
+
// Show root + all segments
|
|
46
|
+
for (const currentPath of paths) {
|
|
47
|
+
breadcrumbs.push({
|
|
48
|
+
title: pageTitles.get(currentPath) || getFallbackTitle(currentPath, siteName),
|
|
49
|
+
path: currentPath
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
// Show ellipsis + last 3 segments
|
|
54
|
+
// For /a/b/c/d: show ... > b > c > d, ellipsis links to /a
|
|
55
|
+
const firstVisibleIndex = segments.length - 3
|
|
56
|
+
const ellipsisPath = '/' + segments.slice(0, firstVisibleIndex).join('/')
|
|
57
|
+
|
|
58
|
+
breadcrumbs.push({ title: '...', path: ellipsisPath })
|
|
59
|
+
|
|
60
|
+
for (let i = firstVisibleIndex; i < segments.length; i++) {
|
|
61
|
+
const segmentPath = '/' + segments.slice(0, i + 1).join('/')
|
|
62
|
+
breadcrumbs.push({
|
|
63
|
+
title: pageTitles.get(segmentPath) || segments[i] || 'Page',
|
|
64
|
+
path: segmentPath
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return breadcrumbs
|
|
70
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-process content after ContentRenderer finishes rendering
|
|
3
|
+
* Uses Vue's onUpdated lifecycle hook to detect when ContentRenderer completes rendering
|
|
4
|
+
*
|
|
5
|
+
* NOTE: Bible verses are now wrapped in <span class="bible-ref"> at BUILD TIME
|
|
6
|
+
* via the content transformer (see content.config.ts and app/transformers/bible-verses.ts)
|
|
7
|
+
* This composable only attaches event listeners to pre-existing spans.
|
|
8
|
+
*
|
|
9
|
+
* Strategy:
|
|
10
|
+
* 1. Watch for page data arrival
|
|
11
|
+
* 2. Use onUpdated() to run processing after ContentRenderer re-renders
|
|
12
|
+
* 3. Guard flag prevents duplicate processing
|
|
13
|
+
* 4. Let useTableOfContents handle "< 2 headings" check via DOM
|
|
14
|
+
*
|
|
15
|
+
* This follows the standard Vue pattern for "run code after component updates"
|
|
16
|
+
*/
|
|
17
|
+
export function useContentPostProcessing(pageRef: Ref<any>) {
|
|
18
|
+
const { $bibleTooltips } = useNuxtApp()
|
|
19
|
+
const layoutGenerateTOC = inject<() => void>('generateTOC')
|
|
20
|
+
|
|
21
|
+
// Track if we've already processed the current page
|
|
22
|
+
const processedPageId = ref<string | null>(null)
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Process content: attach event listeners to Bible verses and generate TOC
|
|
26
|
+
* Bible verses are already wrapped in spans at build time
|
|
27
|
+
* Only runs once per page load
|
|
28
|
+
*/
|
|
29
|
+
function processContent() {
|
|
30
|
+
const currentPage = pageRef.value
|
|
31
|
+
if (!currentPage) return
|
|
32
|
+
|
|
33
|
+
// Prevent duplicate processing of the same page
|
|
34
|
+
const pageId = currentPage.id || currentPage._path || currentPage.path
|
|
35
|
+
if (processedPageId.value === pageId) return
|
|
36
|
+
|
|
37
|
+
// Verify content actually exists in DOM (guard against spurious onUpdated calls)
|
|
38
|
+
const contentContainer = document.querySelector('.content-body, article')
|
|
39
|
+
if (!contentContainer) return
|
|
40
|
+
|
|
41
|
+
const hasContent = contentContainer.querySelector('h1, h2, h3, p')
|
|
42
|
+
if (!hasContent) return
|
|
43
|
+
|
|
44
|
+
// Mark as processed BEFORE running operations to prevent re-entry
|
|
45
|
+
processedPageId.value = pageId
|
|
46
|
+
|
|
47
|
+
// Scan for Bible verse references (scoped to article content only)
|
|
48
|
+
$bibleTooltips.scan(contentContainer as HTMLElement)
|
|
49
|
+
|
|
50
|
+
// Generate TOC (useTableOfContents will handle the "< 2 headings" check)
|
|
51
|
+
if (layoutGenerateTOC) {
|
|
52
|
+
layoutGenerateTOC()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Reset processed flag when new page data arrives
|
|
58
|
+
* This allows onUpdated to process the new content
|
|
59
|
+
*/
|
|
60
|
+
watch(pageRef, (newPage) => {
|
|
61
|
+
if (!newPage) return
|
|
62
|
+
|
|
63
|
+
// Reset processed flag when new page data arrives
|
|
64
|
+
processedPageId.value = null
|
|
65
|
+
}, { immediate: true })
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Vue lifecycle hook: runs after component DOM updates
|
|
69
|
+
* This fires after ContentRenderer finishes re-rendering with new data
|
|
70
|
+
*
|
|
71
|
+
* IMPORTANT: Add nextTick to ensure all child component DOM updates are committed
|
|
72
|
+
* ContentRenderer might still be updating when onUpdated fires
|
|
73
|
+
*/
|
|
74
|
+
onUpdated(() => {
|
|
75
|
+
nextTick(() => {
|
|
76
|
+
processContent()
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Also process on initial mount (handles direct page load)
|
|
82
|
+
*/
|
|
83
|
+
onMounted(() => {
|
|
84
|
+
// Wait for hydration to complete
|
|
85
|
+
nextTick(() => {
|
|
86
|
+
processContent()
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
}
|