@docsector/docsector-reader 4.4.6 → 4.5.0
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 +11 -0
- package/bin/docsector.js +1 -1
- package/docsector.config.js +1 -0
- package/package.json +1 -1
- package/src/components/DBlockSourceCode.vue +14 -2
- package/src/components/DFooter.vue +43 -0
- package/src/components/DFooterHost.vue +22 -0
- package/src/components/DFooterOutlet.vue +33 -0
- package/src/components/DPage.vue +79 -18
- package/src/composables/anchor-scroll-state.js +35 -0
- package/src/composables/site-footer-outlet.js +15 -0
- package/src/composables/useNavigator.js +29 -3
- package/src/index.js +6 -2
- package/src/layouts/DefaultLayout.vue +65 -27
- package/src/layouts/SystemLayout.vue +7 -0
- package/src/page-layout.js +156 -0
- package/src/pages/404Page.vue +5 -0
- package/src/pages/full/layout.overview.en-US.md +49 -0
- package/src/pages/full/layout.overview.pt-BR.md +49 -0
- package/src/pages/full.book.js +13 -0
- package/src/pages/full.index.js +36 -0
- package/src/quasar.factory.js +4 -1
- package/src/router/routes.js +5 -4
package/README.md
CHANGED
|
@@ -59,7 +59,10 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
|
|
|
59
59
|
- 🔎 **Search** — Menu search across all documentation content and tags
|
|
60
60
|
- 💬 **Assistant Chat UX Enhancements** — Long conversations keep focus on recent messages, load earlier history progressively, deduplicate repeated sources, preserve the assistant panel open state across reloads, include per-message copy actions, and show a floating quick return to the bottom
|
|
61
61
|
- 📱 **Responsive** — Mobile-friendly with collapsible sidebar and drawers
|
|
62
|
+
- 🏷️ **Clickable Header Branding** — The configured `branding.logo` and `branding.name` render as a home link in the global header, aligned left on desktop with a compact mobile treatment
|
|
62
63
|
- 📚 **Book Tabs with Per-State Colors** — Define `*.book.js` tabs with icons, order, and `color.active` / `color.inactive`
|
|
64
|
+
- 📐 **Book Layout Presets** — Configure books with the default documentation chrome or a `fullwidth` layout that keeps the header and book tabs while removing the sidebar, subpage toolbar, and Table of Contents
|
|
65
|
+
- 🦶 **Global Branding Footer** — Built-in `Powered by Docsector` footer renders across documentation and system pages, while respecting each page's own scroll container for full-width layout integration without double scrollbars
|
|
63
66
|
- 🔀 **Internal Shortcut Pages** — Route entries can redirect with `config.link.to`, keeping localized titles while inheriting icon/status from the destination page
|
|
64
67
|
- 📐 **Responsive Subpage Toolbar** — Subpage actions align with the content column on desktop and dock to the bottom on mobile
|
|
65
68
|
- ⬆️ **Reading Progress Back to Top** — Documentation subpages can show a floating back-to-top control with circular reading progress that stays above the mobile subpage toolbar
|
|
@@ -70,6 +73,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
|
|
|
70
73
|
- 📊 **Translation Progress** — Automatic translation percentage based on header coverage
|
|
71
74
|
- 🌐 **Accurate Available Translations** — Locale availability counter now uses actual localized page source presence, avoiding false negatives when metadata is equal
|
|
72
75
|
- 🏠 **Markdown Home at Root** — Homepage is rendered from `src/pages/Homepage.{lang}.md` directly at `/`
|
|
76
|
+
- 🧱 **Configurable Homepage Layout** — Set `homePage.layout` to `default` or `fullwidth`; fullwidth keeps the header and book tabs while removing the sidebar, subpage toolbar, Table of Contents, and homepage footer
|
|
73
77
|
- 🌍 **Remote README as Home** — Optional build-time remote README source for homepage with automatic local fallback and automatic primary-title handoff when the remote README already provides the project heading
|
|
74
78
|
- 🔗 **GitHub-Compatible Heading Anchors** — Markdown headings use GitHub-style slugs so standard README Table of Contents links work inside Docsector
|
|
75
79
|
- 🧬 **Scaffolded Homepage Override Wiring** — New consumer projects automatically wire `virtual:docsector-homepage-override` into i18n message building
|
|
@@ -476,6 +480,9 @@ You can configure Docsector Reader to use a remote README as homepage content.
|
|
|
476
480
|
|
|
477
481
|
- Fetch happens at build-time.
|
|
478
482
|
- The same README content is used for all configured languages.
|
|
483
|
+
- The homepage uses the `default` layout unless `homePage.layout` is set to `fullwidth`.
|
|
484
|
+
- `homePage.layout` accepts only `default` and `fullwidth`.
|
|
485
|
+
- The `fullwidth` homepage layout keeps the global header and book tabs, but removes the sidebar, subpage toolbar, Table of Contents, and homepage footer.
|
|
479
486
|
- When the remote README resolves successfully, Docsector hides the autogenerated homepage title and uses the README's own primary heading in the rendered content.
|
|
480
487
|
- If fetch fails, it falls back to local `src/pages/Homepage.{lang}.md` by default and keeps the usual autogenerated homepage title.
|
|
481
488
|
- Standard GitHub-style heading links and README Table of Contents fragments keep working in the rendered homepage.
|
|
@@ -488,6 +495,7 @@ export default {
|
|
|
488
495
|
|
|
489
496
|
homePage: {
|
|
490
497
|
source: 'remote-readme',
|
|
498
|
+
layout: 'fullwidth',
|
|
491
499
|
remoteReadmeUrl: 'https://raw.githubusercontent.com/your-org/your-repo/main/README.md',
|
|
492
500
|
timeoutMs: 8000,
|
|
493
501
|
fallbackToLocal: true
|
|
@@ -1053,6 +1061,7 @@ export default defineBook({
|
|
|
1053
1061
|
label: 'Guide',
|
|
1054
1062
|
icon: 'school',
|
|
1055
1063
|
order: 2,
|
|
1064
|
+
layout: 'default',
|
|
1056
1065
|
color: {
|
|
1057
1066
|
active: 'white',
|
|
1058
1067
|
inactive: 'secondary'
|
|
@@ -1090,6 +1099,8 @@ Notes:
|
|
|
1090
1099
|
- `color.active` and `color.inactive` control the tab text color for each state.
|
|
1091
1100
|
- Color values accept Quasar tokens (`secondary`, `red-6`), CSS variables (`--brand-color` or `var(--brand-color)`), and plain CSS colors (`white`, `#fff`, `rgb(...)`).
|
|
1092
1101
|
- Legacy `color: 'secondary'` still works, but the object form is the recommended API.
|
|
1102
|
+
- `layout: 'default'` keeps the standard documentation chrome with sidebar, subpage toolbar, and Table of Contents.
|
|
1103
|
+
- `layout: 'fullwidth'` keeps the global header and book tabs, but removes the book sidebar, subpage toolbar, and Table of Contents for pages in that book.
|
|
1093
1104
|
- Tabs are ordered by `order`.
|
|
1094
1105
|
|
|
1095
1106
|
---
|
package/bin/docsector.js
CHANGED
|
@@ -24,7 +24,7 @@ const packageRoot = resolve(__dirname, '..')
|
|
|
24
24
|
const args = process.argv.slice(2)
|
|
25
25
|
const command = args[0]
|
|
26
26
|
|
|
27
|
-
const VERSION = '4.
|
|
27
|
+
const VERSION = '4.5.0'
|
|
28
28
|
const AUTHORING_SKILL_NAME = 'docsector-documentation-authoring'
|
|
29
29
|
const AUTHORING_SKILL_DESCRIPTION = 'Author Docsector documentation with Markdown, custom blocks, MCP, and WebMCP.'
|
|
30
30
|
const AUTHORING_SKILL_PUBLIC_PATH = `/.well-known/agent-skills/${AUTHORING_SKILL_NAME}/SKILL.md`
|
package/docsector.config.js
CHANGED
|
@@ -117,6 +117,7 @@ export default {
|
|
|
117
117
|
// @ Home page source
|
|
118
118
|
homePage: {
|
|
119
119
|
source: 'remote-readme',
|
|
120
|
+
layout: 'default',
|
|
120
121
|
remoteReadmeUrl: 'https://raw.githubusercontent.com/docsector/docsector-reader/main/README.md',
|
|
121
122
|
timeoutMs: 8000,
|
|
122
123
|
fallbackToLocal: true
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docsector/docsector-reader",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"description": "A documentation rendering engine built with Vue 3, Quasar v2 and Vite. Transform Markdown into beautiful, navigable documentation sites.",
|
|
5
5
|
"productName": "Docsector Reader",
|
|
6
6
|
"author": "Rodrigo de Araujo Vieira",
|
|
@@ -54,6 +54,7 @@ const copyBtnIcon = ref('content_copy')
|
|
|
54
54
|
const codeRef = ref(null)
|
|
55
55
|
const activeTab = ref(0)
|
|
56
56
|
const lineAnchorTopOffset = 34
|
|
57
|
+
const lineAnchorScrollRetryDelay = 500
|
|
57
58
|
|
|
58
59
|
const coloring = computed(() => $q.dark.isActive ? 'dark' : 'white')
|
|
59
60
|
const anchor = computed(() => printToLetter(props.index + 1))
|
|
@@ -171,6 +172,13 @@ function buildLineHref(line) {
|
|
|
171
172
|
return resolveSourceCodeLineHref(router, route.path, route.query, buildLineAnchorId(line))
|
|
172
173
|
}
|
|
173
174
|
|
|
175
|
+
function scrollToLineAnchor(hash) {
|
|
176
|
+
scrollToAnchor(hash, false)
|
|
177
|
+
window.setTimeout(() => {
|
|
178
|
+
scrollToAnchor(hash, false)
|
|
179
|
+
}, lineAnchorScrollRetryDelay)
|
|
180
|
+
}
|
|
181
|
+
|
|
174
182
|
async function navigateToLineAnchor(event, line) {
|
|
175
183
|
if (!shouldHandleSourceCodeLineActivation(event)) {
|
|
176
184
|
return
|
|
@@ -181,10 +189,14 @@ async function navigateToLineAnchor(event, line) {
|
|
|
181
189
|
event.preventDefault()
|
|
182
190
|
|
|
183
191
|
if (route.hash === hash) {
|
|
184
|
-
|
|
192
|
+
scrollToLineAnchor(hash)
|
|
185
193
|
return
|
|
186
194
|
}
|
|
187
195
|
|
|
196
|
+
window.setTimeout(() => {
|
|
197
|
+
scrollToAnchor(hash, false)
|
|
198
|
+
}, lineAnchorScrollRetryDelay)
|
|
199
|
+
|
|
188
200
|
await router.push({
|
|
189
201
|
path: route.path,
|
|
190
202
|
query: route.query,
|
|
@@ -192,7 +204,7 @@ async function navigateToLineAnchor(event, line) {
|
|
|
192
204
|
})
|
|
193
205
|
|
|
194
206
|
await nextTick()
|
|
195
|
-
|
|
207
|
+
scrollToLineAnchor(hash)
|
|
196
208
|
}
|
|
197
209
|
|
|
198
210
|
function printToLetter(number) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<footer class="d-footer" role="contentinfo">
|
|
3
|
+
<div class="d-footer__content">
|
|
4
|
+
<span class="d-footer__label">Powered by</span>
|
|
5
|
+
<span class="d-footer__brand">Docsector</span>
|
|
6
|
+
</div>
|
|
7
|
+
</footer>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script setup>
|
|
11
|
+
defineOptions({ name: 'DFooter' })
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<style lang="sass">
|
|
15
|
+
.d-footer
|
|
16
|
+
width: 100%
|
|
17
|
+
background: var(--q-primary, #655529)
|
|
18
|
+
color: #ffffff
|
|
19
|
+
|
|
20
|
+
.d-footer__content
|
|
21
|
+
max-width: 1200px
|
|
22
|
+
margin: 0 auto
|
|
23
|
+
padding: 14px 24px
|
|
24
|
+
display: flex
|
|
25
|
+
align-items: center
|
|
26
|
+
justify-content: center
|
|
27
|
+
gap: 6px
|
|
28
|
+
text-align: center
|
|
29
|
+
font-size: 0.95rem
|
|
30
|
+
line-height: 1.4
|
|
31
|
+
|
|
32
|
+
.d-footer__label
|
|
33
|
+
opacity: 0.84
|
|
34
|
+
|
|
35
|
+
.d-footer__brand
|
|
36
|
+
font-weight: 700
|
|
37
|
+
|
|
38
|
+
@media (max-width: 599px)
|
|
39
|
+
.d-footer
|
|
40
|
+
.d-footer__content
|
|
41
|
+
padding: 12px 16px
|
|
42
|
+
font-size: 0.9rem
|
|
43
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<teleport v-if="ready && siteFooterOutletElement" :to="siteFooterOutletElement">
|
|
3
|
+
<d-footer />
|
|
4
|
+
</teleport>
|
|
5
|
+
|
|
6
|
+
<d-footer v-else-if="ready" />
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script setup>
|
|
10
|
+
import { onMounted, ref } from 'vue'
|
|
11
|
+
|
|
12
|
+
import DFooter from './DFooter.vue'
|
|
13
|
+
import { siteFooterOutletElement } from '../composables/site-footer-outlet'
|
|
14
|
+
|
|
15
|
+
defineOptions({ name: 'DFooterHost' })
|
|
16
|
+
|
|
17
|
+
const ready = ref(false)
|
|
18
|
+
|
|
19
|
+
onMounted(() => {
|
|
20
|
+
ready.value = true
|
|
21
|
+
})
|
|
22
|
+
</script>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div ref="outlet" class="d-footer-outlet"></div>
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup>
|
|
6
|
+
import { onMounted, onBeforeUnmount, ref } from 'vue'
|
|
7
|
+
|
|
8
|
+
import { registerSiteFooterOutlet, unregisterSiteFooterOutlet } from '../composables/site-footer-outlet'
|
|
9
|
+
|
|
10
|
+
defineOptions({ name: 'DFooterOutlet' })
|
|
11
|
+
|
|
12
|
+
const outlet = ref(null)
|
|
13
|
+
|
|
14
|
+
onMounted(() => {
|
|
15
|
+
registerSiteFooterOutlet(outlet.value)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
onBeforeUnmount(() => {
|
|
19
|
+
unregisterSiteFooterOutlet(outlet.value)
|
|
20
|
+
})
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<style lang="sass">
|
|
24
|
+
.d-footer-outlet
|
|
25
|
+
width: calc(100% + (2 * var(--d-footer-outlet-padding-x, 0px)))
|
|
26
|
+
margin-left: calc(-1 * var(--d-footer-outlet-padding-x, 0px))
|
|
27
|
+
margin-right: calc(-1 * var(--d-footer-outlet-padding-x, 0px))
|
|
28
|
+
margin-bottom: calc(-1 * var(--d-footer-outlet-padding-bottom, 0px))
|
|
29
|
+
|
|
30
|
+
> .d-footer
|
|
31
|
+
box-sizing: border-box
|
|
32
|
+
padding-bottom: var(--d-footer-outlet-padding-bottom, 0px)
|
|
33
|
+
</style>
|
package/src/components/DPage.vue
CHANGED
|
@@ -8,11 +8,14 @@ import docsectorConfig from 'docsector.config.js'
|
|
|
8
8
|
|
|
9
9
|
import useNavigator from '../composables/useNavigator'
|
|
10
10
|
import { getReadingProgressState } from '../composables/useReadingProgress'
|
|
11
|
+
import { ANCHOR_SCROLL_EXTRA_BOTTOM_PROPERTY } from '../composables/anchor-scroll-state'
|
|
11
12
|
import { normalizeAiAssistantConfig } from '../ai-assistant/config'
|
|
12
13
|
import { getAssistantRightRailState } from '../ai-assistant/layout'
|
|
14
|
+
import { resolveRoutePageLayout } from '../page-layout'
|
|
13
15
|
|
|
14
16
|
import DPageAnchor from './DPageAnchor.vue'
|
|
15
17
|
import DAssistantPanel from './DAssistantPanel.vue'
|
|
18
|
+
import DFooterOutlet from './DFooterOutlet.vue'
|
|
16
19
|
import DPageMeta from './DPageMeta.vue'
|
|
17
20
|
|
|
18
21
|
const store = useStore()
|
|
@@ -21,7 +24,7 @@ const route = useRoute()
|
|
|
21
24
|
const $q = useQuasar()
|
|
22
25
|
const { locale } = useI18n()
|
|
23
26
|
|
|
24
|
-
const { scrolling, navigate } = useNavigator()
|
|
27
|
+
const { scrolling, navigate, anchor: scrollToAnchor } = useNavigator()
|
|
25
28
|
|
|
26
29
|
const props = defineProps({
|
|
27
30
|
disableNav: {
|
|
@@ -41,8 +44,14 @@ const pageMinHeight = ref('calc(100vh - 86px)')
|
|
|
41
44
|
const submenuHeight = ref('36px')
|
|
42
45
|
const pageBottomInset = ref('0px')
|
|
43
46
|
const readingProgress = ref(getReadingProgressState())
|
|
47
|
+
const routeHashScrollTimeout = ref(null)
|
|
44
48
|
const assistantConfig = normalizeAiAssistantConfig(docsectorConfig)
|
|
45
49
|
const assistantEnabled = assistantConfig.enabled === true
|
|
50
|
+
const pageLayout = computed(() => resolveRoutePageLayout(route))
|
|
51
|
+
const showSubmenu = computed(() => pageLayout.value.submenu)
|
|
52
|
+
const showToc = computed(() => pageLayout.value.toc)
|
|
53
|
+
const showPageMeta = computed(() => !props.disableNav && pageLayout.value.footer)
|
|
54
|
+
const isFullwidthContent = computed(() => pageLayout.value.contentWidth === 'fullwidth')
|
|
46
55
|
|
|
47
56
|
const getPageScrollContainer = () => {
|
|
48
57
|
return pageScrollArea.value?.$el?.querySelector('.q-scrollarea__container') || null
|
|
@@ -67,19 +76,19 @@ const updatePageMinHeight = () => {
|
|
|
67
76
|
const pageContainerEl = pageContainer.value?.$el || pageContainer.value
|
|
68
77
|
const submenuEl = submenu.value?.$el || submenu.value
|
|
69
78
|
|
|
70
|
-
if (!pageContainerEl
|
|
79
|
+
if (!pageContainerEl) {
|
|
71
80
|
return
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
const pageContainerStyles = window.getComputedStyle(pageContainerEl)
|
|
75
84
|
const headerHeight = Number.parseFloat(pageContainerStyles.paddingTop) || 0
|
|
76
|
-
const measuredSubmenuHeight = submenuEl
|
|
85
|
+
const measuredSubmenuHeight = showSubmenu.value ? (submenuEl?.offsetHeight || 0) : 0
|
|
77
86
|
const isMobile = $q.screen.lt.md
|
|
78
|
-
const totalOffset = Math.max(0, Math.round(headerHeight + (isMobile ? 0 : measuredSubmenuHeight)))
|
|
87
|
+
const totalOffset = Math.max(0, Math.round(headerHeight + (isMobile || !showSubmenu.value ? 0 : measuredSubmenuHeight)))
|
|
79
88
|
|
|
80
89
|
pageMinHeight.value = `calc(100vh - ${totalOffset}px)`
|
|
81
|
-
submenuHeight.value = `${Math.max(36, Math.round(measuredSubmenuHeight))}px`
|
|
82
|
-
pageBottomInset.value = isMobile ? submenuHeight.value : '0px'
|
|
90
|
+
submenuHeight.value = `${showSubmenu.value ? Math.max(36, Math.round(measuredSubmenuHeight)) : 0}px`
|
|
91
|
+
pageBottomInset.value = isMobile && showSubmenu.value ? submenuHeight.value : '0px'
|
|
83
92
|
syncReadingProgress()
|
|
84
93
|
}
|
|
85
94
|
|
|
@@ -91,6 +100,22 @@ const schedulePageMinHeightUpdate = () => {
|
|
|
91
100
|
})
|
|
92
101
|
}
|
|
93
102
|
|
|
103
|
+
const scheduleRouteHashScroll = () => {
|
|
104
|
+
if (routeHashScrollTimeout.value) {
|
|
105
|
+
clearTimeout(routeHashScrollTimeout.value)
|
|
106
|
+
routeHashScrollTimeout.value = null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (showToc.value || !route.hash) {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
routeHashScrollTimeout.value = setTimeout(() => {
|
|
114
|
+
scrollToAnchor(route.hash, false)
|
|
115
|
+
routeHashScrollTimeout.value = null
|
|
116
|
+
}, 500)
|
|
117
|
+
}
|
|
118
|
+
|
|
94
119
|
const overview = computed(() => route.matched[0].path)
|
|
95
120
|
const showcase = computed(() => {
|
|
96
121
|
const showcase = route.matched[0].meta.subpages.showcase
|
|
@@ -123,7 +148,7 @@ const shouldShowBackToTopControl = computed(() => {
|
|
|
123
148
|
return props.showBackToTopControl && readingProgress.value.hasOverflow && readingProgress.value.isVisible
|
|
124
149
|
})
|
|
125
150
|
const rightRailState = computed(() => getAssistantRightRailState({
|
|
126
|
-
tocOpen: layoutMeta.value,
|
|
151
|
+
tocOpen: showToc.value && layoutMeta.value,
|
|
127
152
|
assistantOpen: assistantEnabled && layoutAssistant.value,
|
|
128
153
|
screenWidth: $q.screen.width,
|
|
129
154
|
assistantWidth: assistantWidth.value,
|
|
@@ -137,8 +162,8 @@ const mobileAssistantOpen = computed({
|
|
|
137
162
|
set: (value) => { layoutAssistant.value = value }
|
|
138
163
|
})
|
|
139
164
|
const mobileTocOpen = computed({
|
|
140
|
-
get: () => rightRailState.value.isMobile && layoutMeta.value,
|
|
141
|
-
set: (value) => { layoutMeta.value = value }
|
|
165
|
+
get: () => showToc.value && rightRailState.value.isMobile && layoutMeta.value,
|
|
166
|
+
set: (value) => { layoutMeta.value = showToc.value && value }
|
|
142
167
|
})
|
|
143
168
|
const backToTopRightOffset = computed(() => {
|
|
144
169
|
return rightRailState.value.backToTopRightOffset
|
|
@@ -157,6 +182,7 @@ const currentPageTitle = computed(() => {
|
|
|
157
182
|
})
|
|
158
183
|
|
|
159
184
|
const toggleSectionsTree = () => {
|
|
185
|
+
if (!showToc.value) return
|
|
160
186
|
layoutMeta.value = !layoutMeta.value
|
|
161
187
|
}
|
|
162
188
|
|
|
@@ -218,6 +244,8 @@ const subroute = (to) => {
|
|
|
218
244
|
}
|
|
219
245
|
|
|
220
246
|
const resetPageScroll = () => {
|
|
247
|
+
getPageScrollContainer()?.style.removeProperty(ANCHOR_SCROLL_EXTRA_BOTTOM_PROPERTY)
|
|
248
|
+
|
|
221
249
|
if (pageScrollArea.value !== null) {
|
|
222
250
|
pageScrollArea.value.setScrollPosition('vertical', 0, 0)
|
|
223
251
|
}
|
|
@@ -331,12 +359,18 @@ onMounted(() => {
|
|
|
331
359
|
window.addEventListener('resize', schedulePageMinHeightUpdate)
|
|
332
360
|
nextTick(() => {
|
|
333
361
|
schedulePageMinHeightUpdate()
|
|
362
|
+
scheduleRouteHashScroll()
|
|
334
363
|
})
|
|
335
364
|
|
|
336
365
|
router.beforeEach((to, from, next) => {
|
|
337
|
-
|
|
366
|
+
const sameEffectivePage = isSameEffectivePage(from, to)
|
|
367
|
+
const hashOnlyNavigation = sameEffectivePage && to.hash !== ''
|
|
338
368
|
|
|
339
|
-
if (
|
|
369
|
+
if (!hashOnlyNavigation) {
|
|
370
|
+
resetPageScroll()
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (to.hash === '' && !sameEffectivePage) {
|
|
340
374
|
store.commit('page/resetAnchor')
|
|
341
375
|
store.commit('page/resetAnchors')
|
|
342
376
|
store.commit('page/resetNodes')
|
|
@@ -349,12 +383,17 @@ onMounted(() => {
|
|
|
349
383
|
onBeforeUnmount(() => {
|
|
350
384
|
window.removeEventListener('keydown', handleMainScrollKeys)
|
|
351
385
|
window.removeEventListener('resize', schedulePageMinHeightUpdate)
|
|
386
|
+
|
|
387
|
+
if (routeHashScrollTimeout.value) {
|
|
388
|
+
clearTimeout(routeHashScrollTimeout.value)
|
|
389
|
+
}
|
|
352
390
|
})
|
|
353
391
|
|
|
354
392
|
watch(() => route.fullPath, () => {
|
|
355
393
|
nextTick(() => {
|
|
356
394
|
schedulePageMinHeightUpdate()
|
|
357
395
|
syncReadingProgress(0)
|
|
396
|
+
scheduleRouteHashScroll()
|
|
358
397
|
})
|
|
359
398
|
})
|
|
360
399
|
</script>
|
|
@@ -368,8 +407,13 @@ watch(() => route.fullPath, () => {
|
|
|
368
407
|
'--d-submenu-height': submenuHeight,
|
|
369
408
|
'--d-page-bottom-inset': pageBottomInset
|
|
370
409
|
}"
|
|
410
|
+
:class="[
|
|
411
|
+
`d-page-layout--${pageLayout.mode}`,
|
|
412
|
+
isFullwidthContent ? 'd-page-layout--fullwidth-content' : 'd-page-layout--contained-content'
|
|
413
|
+
]"
|
|
371
414
|
>
|
|
372
415
|
<q-toolbar
|
|
416
|
+
v-if="showSubmenu"
|
|
373
417
|
id="submenu"
|
|
374
418
|
ref="submenu"
|
|
375
419
|
class="bg-grey-8 text-white"
|
|
@@ -401,7 +445,7 @@ watch(() => route.fullPath, () => {
|
|
|
401
445
|
/>
|
|
402
446
|
</q-btn-group>
|
|
403
447
|
</q-toolbar-title>
|
|
404
|
-
<q-btn class="d-submenu__toggle" :class="layoutMeta ? 'active' : null" @click="toggleSectionsTree" icon="account_tree">
|
|
448
|
+
<q-btn v-if="showToc" class="d-submenu__toggle" :class="layoutMeta ? 'active' : null" @click="toggleSectionsTree" icon="account_tree">
|
|
405
449
|
<q-tooltip>{{ $t('page.edit.anchor') }}</q-tooltip>
|
|
406
450
|
</q-btn>
|
|
407
451
|
</div>
|
|
@@ -409,10 +453,11 @@ watch(() => route.fullPath, () => {
|
|
|
409
453
|
|
|
410
454
|
<q-page id="page">
|
|
411
455
|
<q-scroll-area class="content" :class="main" ref="pageScrollArea">
|
|
412
|
-
<div id="scroll-container" @click="handleContentAnchorClick">
|
|
456
|
+
<div id="scroll-container" :class="isFullwidthContent ? 'd-scroll-container--fullwidth' : null" @click="handleContentAnchorClick">
|
|
413
457
|
<slot />
|
|
414
458
|
</div>
|
|
415
|
-
<d-page-meta v-if="
|
|
459
|
+
<d-page-meta v-if="showPageMeta" />
|
|
460
|
+
<d-footer-outlet />
|
|
416
461
|
<q-scroll-observer @scroll="handlePageScroll" :debounce="300" />
|
|
417
462
|
</q-scroll-area>
|
|
418
463
|
</q-page>
|
|
@@ -456,10 +501,10 @@ watch(() => route.fullPath, () => {
|
|
|
456
501
|
@update:model-value="setRightRailOpen"
|
|
457
502
|
>
|
|
458
503
|
<div class="d-right-rail">
|
|
459
|
-
<div v-if="rightRailState.showToc" class="d-right-rail__toc" :style="{ width: `${rightRailState.tocWidth}px` }">
|
|
504
|
+
<div v-if="showToc && rightRailState.showToc" class="d-right-rail__toc" :style="{ width: `${rightRailState.tocWidth}px` }">
|
|
460
505
|
<d-page-anchor id="anchor" />
|
|
461
506
|
</div>
|
|
462
|
-
<q-separator v-if="rightRailState.showToc && rightRailState.showAssistant" vertical />
|
|
507
|
+
<q-separator v-if="showToc && rightRailState.showToc && rightRailState.showAssistant" vertical />
|
|
463
508
|
<d-assistant-panel
|
|
464
509
|
v-if="rightRailState.showAssistant"
|
|
465
510
|
class="d-right-rail__assistant"
|
|
@@ -475,6 +520,7 @@ watch(() => route.fullPath, () => {
|
|
|
475
520
|
</q-drawer>
|
|
476
521
|
|
|
477
522
|
<q-dialog
|
|
523
|
+
v-if="showToc"
|
|
478
524
|
v-model="mobileTocOpen"
|
|
479
525
|
position="right"
|
|
480
526
|
square
|
|
@@ -519,12 +565,16 @@ watch(() => route.fullPath, () => {
|
|
|
519
565
|
min-height: var(--d-page-min-height, calc(100vh - 86px))
|
|
520
566
|
|
|
521
567
|
.content > div.scroll > div.q-scrollarea__content
|
|
568
|
+
--d-page-content-padding-x: 15px
|
|
569
|
+
--d-page-content-padding-bottom: calc(15px + var(--d-page-bottom-inset, 0px) + var(--d-anchor-scroll-extra-bottom, 0px) + env(safe-area-inset-bottom, 0px))
|
|
570
|
+
--d-footer-outlet-padding-x: var(--d-page-content-padding-x)
|
|
571
|
+
--d-footer-outlet-padding-bottom: var(--d-page-content-padding-bottom)
|
|
522
572
|
max-width: 100%
|
|
523
573
|
box-sizing: border-box
|
|
524
574
|
|
|
525
575
|
.content:not(.no-padding) > div.scroll > div.q-scrollarea__content
|
|
526
|
-
padding:
|
|
527
|
-
padding-bottom:
|
|
576
|
+
padding: var(--d-page-content-padding-x)
|
|
577
|
+
padding-bottom: var(--d-page-content-padding-bottom)
|
|
528
578
|
|
|
529
579
|
#page
|
|
530
580
|
min-height: var(--d-page-min-height, calc(100vh - 86px)) !important
|
|
@@ -598,6 +648,17 @@ body.body--dark
|
|
|
598
648
|
max-width: 1200px
|
|
599
649
|
margin: auto
|
|
600
650
|
|
|
651
|
+
#page-container.d-page-layout--fullwidth-content
|
|
652
|
+
#scroll-container,
|
|
653
|
+
#d-page-meta
|
|
654
|
+
max-width: none
|
|
655
|
+
|
|
656
|
+
#d-page-meta > .row,
|
|
657
|
+
#d-page-meta > #d-page-nav
|
|
658
|
+
padding-left: 15px
|
|
659
|
+
padding-right: 15px
|
|
660
|
+
box-sizing: border-box
|
|
661
|
+
|
|
601
662
|
#submenu
|
|
602
663
|
min-height: 36px
|
|
603
664
|
padding: 0
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const ANCHOR_SCROLL_EXTRA_BOTTOM_PROPERTY = '--d-anchor-scroll-extra-bottom'
|
|
2
|
+
|
|
3
|
+
const toFiniteNumber = (value) => {
|
|
4
|
+
const normalized = Number(value)
|
|
5
|
+
return Number.isFinite(normalized) ? normalized : 0
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getAnchorScrollMaxTop ({ scrollHeight = 0, clientHeight = 0 } = {}) {
|
|
9
|
+
return Math.max(0, toFiniteNumber(scrollHeight) - toFiniteNumber(clientHeight))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getAnchorScrollExtraBottom ({ offsetTop = 0, scrollHeight = 0, clientHeight = 0, currentExtraBottom = 0 } = {}) {
|
|
13
|
+
const baseScrollHeight = Math.max(0, toFiniteNumber(scrollHeight) - Math.max(0, toFiniteNumber(currentExtraBottom)))
|
|
14
|
+
const maxScrollTop = getAnchorScrollMaxTop({ scrollHeight: baseScrollHeight, clientHeight })
|
|
15
|
+
return Math.ceil(Math.max(0, toFiniteNumber(offsetTop) - maxScrollTop))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function setAnchorScrollExtraBottom (target, value = 0) {
|
|
19
|
+
if (typeof HTMLElement === 'undefined') {
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!(target instanceof HTMLElement)) {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const extraBottom = Math.max(0, Math.ceil(toFiniteNumber(value)))
|
|
28
|
+
|
|
29
|
+
if (extraBottom > 0) {
|
|
30
|
+
target.style.setProperty(ANCHOR_SCROLL_EXTRA_BOTTOM_PROPERTY, `${extraBottom}px`)
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
target.style.removeProperty(ANCHOR_SCROLL_EXTRA_BOTTOM_PROPERTY)
|
|
35
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { shallowRef } from 'vue'
|
|
2
|
+
|
|
3
|
+
export const siteFooterOutletElement = shallowRef(null)
|
|
4
|
+
|
|
5
|
+
export function registerSiteFooterOutlet (element) {
|
|
6
|
+
if (element instanceof HTMLElement) {
|
|
7
|
+
siteFooterOutletElement.value = element
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function unregisterSiteFooterOutlet (element) {
|
|
12
|
+
if (siteFooterOutletElement.value === element) {
|
|
13
|
+
siteFooterOutletElement.value = null
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -4,6 +4,7 @@ import { useRouter, useRoute } from 'vue-router'
|
|
|
4
4
|
import { ref } from 'vue'
|
|
5
5
|
|
|
6
6
|
import { DEFAULT_ACTIVE_ANCHOR_OFFSET, getActiveAnchorId } from './useActiveAnchor'
|
|
7
|
+
import { ANCHOR_SCROLL_EXTRA_BOTTOM_PROPERTY, getAnchorScrollExtraBottom, setAnchorScrollExtraBottom } from './anchor-scroll-state'
|
|
7
8
|
|
|
8
9
|
export default function useNavigator() {
|
|
9
10
|
const store = useStore()
|
|
@@ -49,7 +50,26 @@ export default function useNavigator() {
|
|
|
49
50
|
return 0
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
const
|
|
53
|
+
const syncAnchorScrollExtraBottom = (scrollTarget, offsetTop) => {
|
|
54
|
+
if (!(scrollTarget instanceof HTMLElement)) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const extraBottom = getAnchorScrollExtraBottom({
|
|
59
|
+
offsetTop,
|
|
60
|
+
scrollHeight: scrollTarget.scrollHeight,
|
|
61
|
+
clientHeight: scrollTarget.clientHeight,
|
|
62
|
+
currentExtraBottom: Number.parseFloat(scrollTarget.style.getPropertyValue(ANCHOR_SCROLL_EXTRA_BOTTOM_PROPERTY) || '')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
setAnchorScrollExtraBottom(scrollTarget, extraBottom)
|
|
66
|
+
|
|
67
|
+
if (extraBottom > 0) {
|
|
68
|
+
void scrollTarget.scrollHeight
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const resolveAnchorScrollState = (anchorEl, { syncExtraBottom = true } = {}) => {
|
|
53
73
|
if (!(anchorEl instanceof HTMLElement)) {
|
|
54
74
|
return null
|
|
55
75
|
}
|
|
@@ -61,9 +81,15 @@ export default function useNavigator() {
|
|
|
61
81
|
const targetRectTop = getScrollTargetRectTop(scrollTarget)
|
|
62
82
|
const anchorTopOffset = getAnchorTopOffset(anchorEl)
|
|
63
83
|
|
|
84
|
+
const offsetTop = Math.max(0, ((anchorRect.top - targetRectTop) + scrollTop) - anchorTopOffset)
|
|
85
|
+
|
|
86
|
+
if (syncExtraBottom) {
|
|
87
|
+
syncAnchorScrollExtraBottom(scrollTarget, offsetTop)
|
|
88
|
+
}
|
|
89
|
+
|
|
64
90
|
return {
|
|
65
91
|
scrollTarget,
|
|
66
|
-
offsetTop
|
|
92
|
+
offsetTop
|
|
67
93
|
}
|
|
68
94
|
}
|
|
69
95
|
|
|
@@ -155,7 +181,7 @@ export default function useNavigator() {
|
|
|
155
181
|
const Anchor = document.getElementById(domAnchorId)
|
|
156
182
|
|
|
157
183
|
if (Anchor !== null && typeof Anchor === 'object') {
|
|
158
|
-
return resolveAnchorScrollState(Anchor)?.offsetTop
|
|
184
|
+
return resolveAnchorScrollState(Anchor, { syncExtraBottom: false })?.offsetTop
|
|
159
185
|
}
|
|
160
186
|
|
|
161
187
|
return undefined
|
package/src/index.js
CHANGED
|
@@ -226,6 +226,7 @@ export function createDocsector (config = {}) {
|
|
|
226
226
|
|
|
227
227
|
homePage: {
|
|
228
228
|
source: 'local',
|
|
229
|
+
layout: 'default',
|
|
229
230
|
remoteReadmeUrl: null,
|
|
230
231
|
timeoutMs: 8000,
|
|
231
232
|
fallbackToLocal: true,
|
|
@@ -237,10 +238,12 @@ export function createDocsector (config = {}) {
|
|
|
237
238
|
/**
|
|
238
239
|
* Define a Docsector book entry for the pages registry.
|
|
239
240
|
*
|
|
240
|
-
* @param {Object} config - Book configuration (id, label, icon, order, color)
|
|
241
|
+
* @param {Object} config - Book configuration (id, label, icon, order, color, layout)
|
|
241
242
|
* @param {Object|string} [config.color] - Tab text color settings
|
|
242
243
|
* @param {string} [config.color.active] - Active tab text color token (Quasar color key, CSS var, or CSS color)
|
|
243
244
|
* @param {string} [config.color.inactive] - Inactive tab text color token (Quasar color key, CSS var, or CSS color)
|
|
245
|
+
* @param {string|Object} [config.layout] - Page layout preset for pages in the book (`default` or `fullwidth`)
|
|
246
|
+
* @param {string|Object} [config.layouts] - Alias for `layout`, useful when passing explicit layout flags
|
|
244
247
|
* @returns {Object} Normalized book definition
|
|
245
248
|
*/
|
|
246
249
|
export function defineBook (config = {}) {
|
|
@@ -251,7 +254,8 @@ export function defineBook (config = {}) {
|
|
|
251
254
|
return {
|
|
252
255
|
...config,
|
|
253
256
|
...(resolvedId ? { id: resolvedId } : {}),
|
|
254
|
-
...(resolvedLabel ? { label: resolvedLabel } : {})
|
|
257
|
+
...(resolvedLabel ? { label: resolvedLabel } : {}),
|
|
258
|
+
...(config.layouts === undefined && config.layout !== undefined ? { layouts: config.layout } : {})
|
|
255
259
|
}
|
|
256
260
|
}
|
|
257
261
|
|
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<q-layout view="lHh LpR lFf">
|
|
3
|
-
<q-header class="d-header left-btn" elevated>
|
|
3
|
+
<q-header class="d-header" :class="showSidebar ? 'left-btn' : 'd-header--no-sidebar'" elevated>
|
|
4
4
|
<q-toolbar color="primary">
|
|
5
|
-
<q-btn class="filled" square icon="menu" aria-label="Toggle Menu" @click="toogleMenu" />
|
|
6
|
-
<q-toolbar-title
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
<q-btn v-if="showSidebar" class="filled" square icon="menu" aria-label="Toggle Menu" @click="toogleMenu" />
|
|
6
|
+
<q-toolbar-title
|
|
7
|
+
class="d-header__brand-slot row no-wrap items-stretch self-stretch q-pa-none"
|
|
8
|
+
:class="$q.screen.lt.sm ? 'justify-start' : 'justify-center'"
|
|
9
|
+
>
|
|
10
|
+
<q-btn
|
|
11
|
+
class="filled d-header__brand"
|
|
12
|
+
:class="$q.screen.lt.sm ? 'q-px-sm' : 'q-px-md'"
|
|
13
|
+
align="left"
|
|
14
|
+
no-caps
|
|
15
|
+
stretch
|
|
16
|
+
to="/"
|
|
17
|
+
:aria-label="brandAriaLabel"
|
|
18
|
+
>
|
|
19
|
+
<img
|
|
20
|
+
v-if="branding.logo"
|
|
21
|
+
:src="branding.logo"
|
|
22
|
+
:alt="brandName"
|
|
23
|
+
height="26"
|
|
24
|
+
class="d-header__brand-logo q-mr-sm"
|
|
25
|
+
/>
|
|
26
|
+
<span class="d-header__brand-text col column justify-center no-wrap">
|
|
27
|
+
<span class="d-header__brand-name ellipsis text-left">{{ brandName }}</span>
|
|
28
|
+
<span
|
|
29
|
+
v-if="brandVersion"
|
|
30
|
+
class="d-header__brand-version text-caption ellipsis text-left"
|
|
31
|
+
>{{ brandVersion }}</span>
|
|
32
|
+
</span>
|
|
33
|
+
</q-btn>
|
|
17
34
|
</q-toolbar-title>
|
|
18
35
|
<q-btn
|
|
19
36
|
v-if="assistantEnabled"
|
|
@@ -53,55 +70,58 @@
|
|
|
53
70
|
</q-tabs>
|
|
54
71
|
</q-header>
|
|
55
72
|
|
|
56
|
-
<q-drawer elevated show-if-above side="left" v-model="layout.menu">
|
|
73
|
+
<q-drawer v-if="showSidebar" elevated show-if-above side="left" v-model="layout.menu">
|
|
57
74
|
<d-menu />
|
|
58
75
|
</q-drawer>
|
|
59
76
|
|
|
60
77
|
<router-view />
|
|
78
|
+
|
|
79
|
+
<d-footer-host />
|
|
61
80
|
</q-layout>
|
|
62
81
|
</template>
|
|
63
82
|
|
|
64
83
|
<script setup>
|
|
65
|
-
import { ref, computed } from 'vue'
|
|
84
|
+
import { ref, computed, watch } from 'vue'
|
|
66
85
|
import { useRoute, useRouter } from 'vue-router'
|
|
67
86
|
import { useStore } from 'vuex'
|
|
68
87
|
import { useI18n } from 'vue-i18n'
|
|
69
|
-
import { useMeta, colors } from 'quasar'
|
|
88
|
+
import { useMeta, colors, useQuasar } from 'quasar'
|
|
70
89
|
|
|
71
90
|
import DMenu from '../components/DMenu.vue'
|
|
91
|
+
import DFooterHost from '../components/DFooterHost.vue'
|
|
72
92
|
import docsectorConfig from 'docsector.config.js'
|
|
73
93
|
import { normalizeAiAssistantConfig } from '../ai-assistant/config'
|
|
74
94
|
import { allBooks, booksByVersion } from 'virtual:docsector-books'
|
|
75
|
-
import {
|
|
95
|
+
import { resolveRoutePageLayout } from '../page-layout'
|
|
76
96
|
|
|
77
97
|
defineOptions({ name: 'LayoutDefault' })
|
|
78
98
|
|
|
79
99
|
const branding = docsectorConfig.branding || {}
|
|
100
|
+
const brandName = branding.name || 'Docsector'
|
|
101
|
+
const brandVersion = typeof branding.version === 'string' ? branding.version.trim() : ''
|
|
102
|
+
const brandAriaLabel = `Open ${brandName} home`
|
|
80
103
|
const assistantConfig = normalizeAiAssistantConfig(docsectorConfig)
|
|
81
104
|
const assistantEnabled = assistantConfig.enabled === true
|
|
82
105
|
|
|
83
106
|
const route = useRoute()
|
|
84
107
|
const router = useRouter()
|
|
85
108
|
const store = useStore()
|
|
109
|
+
const $q = useQuasar()
|
|
86
110
|
const { t, locale } = useI18n()
|
|
87
111
|
|
|
88
112
|
const layout = ref({
|
|
89
113
|
menu: false
|
|
90
114
|
})
|
|
91
115
|
|
|
116
|
+
const pageLayout = computed(() => resolveRoutePageLayout(route))
|
|
117
|
+
const showSidebar = computed(() => pageLayout.value.sidebar)
|
|
92
118
|
const assistantOpen = computed(() => store.state.layout.assistant)
|
|
93
119
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const headerTitleText = computed(() => {
|
|
99
|
-
if (store.state.i18n.base) {
|
|
100
|
-
return t(pageTitleI18nPath(store.state.i18n.base))
|
|
101
|
-
} else {
|
|
102
|
-
return t(`menu.${route.matched[1].meta.menu}`)
|
|
120
|
+
watch(showSidebar, (value) => {
|
|
121
|
+
if (!value) {
|
|
122
|
+
layout.value.menu = false
|
|
103
123
|
}
|
|
104
|
-
})
|
|
124
|
+
}, { immediate: true })
|
|
105
125
|
|
|
106
126
|
const defaultBookTabColors = Object.freeze({
|
|
107
127
|
active: 'white',
|
|
@@ -362,6 +382,24 @@ store.commit('page/resetAnchors')
|
|
|
362
382
|
&.left-btn
|
|
363
383
|
.q-toolbar
|
|
364
384
|
padding: 0
|
|
385
|
+
.d-header__brand
|
|
386
|
+
min-width: 0
|
|
387
|
+
max-width: 100%
|
|
388
|
+
.d-header__brand-logo
|
|
389
|
+
display: block
|
|
390
|
+
flex-shrink: 0
|
|
391
|
+
.d-header__brand-text
|
|
392
|
+
min-width: 0
|
|
393
|
+
line-height: 1
|
|
394
|
+
.d-header__brand-name
|
|
395
|
+
display: block
|
|
396
|
+
line-height: 0.95rem
|
|
397
|
+
.d-header__brand-version
|
|
398
|
+
display: block
|
|
399
|
+
margin-top: -2px
|
|
400
|
+
font-size: 10px
|
|
401
|
+
line-height: 0.75rem
|
|
402
|
+
opacity: 0.8
|
|
365
403
|
.q-tabs
|
|
366
404
|
margin-top: 2px
|
|
367
405
|
.d-book-tabs-separator
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
export const PAGE_LAYOUT_DEFAULT = 'default'
|
|
2
|
+
export const PAGE_LAYOUT_FULLWIDTH = 'fullwidth'
|
|
3
|
+
|
|
4
|
+
const PAGE_LAYOUT_PRESETS = Object.freeze({
|
|
5
|
+
[PAGE_LAYOUT_DEFAULT]: Object.freeze({
|
|
6
|
+
mode: PAGE_LAYOUT_DEFAULT,
|
|
7
|
+
sidebar: true,
|
|
8
|
+
submenu: true,
|
|
9
|
+
toc: true,
|
|
10
|
+
footer: true,
|
|
11
|
+
contentWidth: 'contained'
|
|
12
|
+
}),
|
|
13
|
+
[PAGE_LAYOUT_FULLWIDTH]: Object.freeze({
|
|
14
|
+
mode: PAGE_LAYOUT_FULLWIDTH,
|
|
15
|
+
sidebar: false,
|
|
16
|
+
submenu: false,
|
|
17
|
+
toc: false,
|
|
18
|
+
footer: true,
|
|
19
|
+
contentWidth: 'fullwidth'
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export const HOME_PAGE_FULLWIDTH_LAYOUT = Object.freeze({
|
|
24
|
+
...PAGE_LAYOUT_PRESETS[PAGE_LAYOUT_FULLWIDTH],
|
|
25
|
+
footer: false
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const normalizeLayoutMode = (value) => {
|
|
29
|
+
const normalized = String(value || '')
|
|
30
|
+
.trim()
|
|
31
|
+
.toLowerCase()
|
|
32
|
+
.replace(/[\s_-]+/g, '-')
|
|
33
|
+
|
|
34
|
+
if (normalized === PAGE_LAYOUT_FULLWIDTH) {
|
|
35
|
+
return PAGE_LAYOUT_FULLWIDTH
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return PAGE_LAYOUT_DEFAULT
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const normalizeBoolean = (value, fallback) => {
|
|
42
|
+
if (typeof value === 'boolean') return value
|
|
43
|
+
if (value === undefined || value === null) return fallback
|
|
44
|
+
|
|
45
|
+
const normalized = String(value).trim().toLowerCase()
|
|
46
|
+
if (['true', '1', 'yes', 'on'].includes(normalized)) return true
|
|
47
|
+
if (['false', '0', 'no', 'off'].includes(normalized)) return false
|
|
48
|
+
|
|
49
|
+
return fallback
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const firstDefined = (source, keys) => {
|
|
53
|
+
for (const key of keys) {
|
|
54
|
+
if (source?.[key] !== undefined) {
|
|
55
|
+
return source[key]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return undefined
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const normalizeContentWidth = (value, fallback) => {
|
|
63
|
+
if (value === undefined || value === null) return fallback
|
|
64
|
+
|
|
65
|
+
const normalized = String(value).trim().toLowerCase().replace(/[\s_-]+/g, '-')
|
|
66
|
+
if (['full', 'fullwidth', 'full-width', 'wide', 'fluid'].includes(normalized)) {
|
|
67
|
+
return 'fullwidth'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return 'contained'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const toLayoutPatch = (source) => {
|
|
74
|
+
if (typeof source === 'string') {
|
|
75
|
+
return { mode: normalizeLayoutMode(source) }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!source || typeof source !== 'object' || Array.isArray(source)) {
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const patch = {}
|
|
83
|
+
const mode = firstDefined(source, ['mode', 'layout', 'preset', 'name', 'type'])
|
|
84
|
+
if (mode !== undefined) {
|
|
85
|
+
patch.mode = normalizeLayoutMode(mode)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const sidebar = firstDefined(source, ['sidebar', 'menu', 'left'])
|
|
89
|
+
const submenu = firstDefined(source, ['submenu', 'subpage', 'subpageBar'])
|
|
90
|
+
const toc = firstDefined(source, ['toc', 'tableOfContents', 'anchors', 'meta'])
|
|
91
|
+
const footer = firstDefined(source, ['footer', 'nav', 'navigationFooter'])
|
|
92
|
+
const contentWidth = firstDefined(source, ['contentWidth', 'width'])
|
|
93
|
+
|
|
94
|
+
if (sidebar !== undefined) patch.sidebar = normalizeBoolean(sidebar, true)
|
|
95
|
+
if (submenu !== undefined) patch.submenu = normalizeBoolean(submenu, true)
|
|
96
|
+
if (toc !== undefined) patch.toc = normalizeBoolean(toc, true)
|
|
97
|
+
if (footer !== undefined) patch.footer = normalizeBoolean(footer, true)
|
|
98
|
+
if (contentWidth !== undefined) patch.contentWidth = normalizeContentWidth(contentWidth, 'contained')
|
|
99
|
+
|
|
100
|
+
return patch
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function resolvePageLayout (...sources) {
|
|
104
|
+
let layout = { ...PAGE_LAYOUT_PRESETS[PAGE_LAYOUT_DEFAULT] }
|
|
105
|
+
|
|
106
|
+
for (const source of sources) {
|
|
107
|
+
const patch = toLayoutPatch(source)
|
|
108
|
+
if (!patch) continue
|
|
109
|
+
|
|
110
|
+
if (patch.mode) {
|
|
111
|
+
layout = { ...PAGE_LAYOUT_PRESETS[patch.mode] }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
layout = {
|
|
115
|
+
...layout,
|
|
116
|
+
...patch,
|
|
117
|
+
mode: patch.mode || layout.mode,
|
|
118
|
+
contentWidth: normalizeContentWidth(patch.contentWidth, layout.contentWidth)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return layout
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function isHomeRoute (route = {}) {
|
|
126
|
+
const meta = route?.meta || route?.matched?.[0]?.meta || {}
|
|
127
|
+
return meta.book === 'home' || meta.type === 'home'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function resolveHomePageLayout (homePage = {}) {
|
|
131
|
+
const mode = normalizeLayoutMode(homePage?.layout)
|
|
132
|
+
|
|
133
|
+
if (mode === PAGE_LAYOUT_FULLWIDTH) {
|
|
134
|
+
return { ...HOME_PAGE_FULLWIDTH_LAYOUT }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return resolvePageLayout(PAGE_LAYOUT_DEFAULT)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function resolveRoutePageLayout (route = {}) {
|
|
141
|
+
const sources = []
|
|
142
|
+
|
|
143
|
+
if (Array.isArray(route?.matched)) {
|
|
144
|
+
for (const record of route.matched) {
|
|
145
|
+
const meta = record?.meta || {}
|
|
146
|
+
if (meta.layout !== undefined) sources.push(meta.layout)
|
|
147
|
+
if (meta.layouts !== undefined) sources.push(meta.layouts)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const meta = route?.meta || {}
|
|
152
|
+
if (meta.layout !== undefined) sources.push(meta.layout)
|
|
153
|
+
if (meta.layouts !== undefined) sources.push(meta.layouts)
|
|
154
|
+
|
|
155
|
+
return resolvePageLayout(...sources)
|
|
156
|
+
}
|
package/src/pages/404Page.vue
CHANGED
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
<strong>(404)</strong>
|
|
10
10
|
</p>
|
|
11
11
|
<q-btn color="secondary" style="width:200px;" @click="$router.push('/')">Go home</q-btn>
|
|
12
|
+
<d-footer-outlet />
|
|
12
13
|
</q-scroll-area>
|
|
13
14
|
</q-page>
|
|
14
15
|
</q-page-container>
|
|
15
16
|
</template>
|
|
17
|
+
|
|
18
|
+
<script setup>
|
|
19
|
+
import DFooterOutlet from 'components/DFooterOutlet.vue'
|
|
20
|
+
</script>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
## Overview
|
|
2
|
+
|
|
3
|
+
The Full book is a working example of a book-level layout opt-in. It uses `layout: 'fullwidth'` in `src/pages/full.book.js`, so every page registered in `src/pages/full.index.js` inherits the fullwidth page chrome.
|
|
4
|
+
|
|
5
|
+
This page should render with the global header and book tabs, but without the left sidebar, subpage toolbar, Table of Contents toggle, or Table of Contents rail.
|
|
6
|
+
|
|
7
|
+
## Book Configuration
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
import { defineBook } from '../index.js'
|
|
11
|
+
|
|
12
|
+
export default defineBook({
|
|
13
|
+
id: 'full',
|
|
14
|
+
label: 'Full',
|
|
15
|
+
icon: 'fullscreen',
|
|
16
|
+
order: 3,
|
|
17
|
+
layout: 'fullwidth'
|
|
18
|
+
})
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The `layout` field accepts two values:
|
|
22
|
+
|
|
23
|
+
- `default` keeps the standard documentation chrome.
|
|
24
|
+
- `fullwidth` keeps the global header and book tabs, then removes the book sidebar, subpage toolbar, and Table of Contents for pages in that book.
|
|
25
|
+
|
|
26
|
+
## When to Use It
|
|
27
|
+
|
|
28
|
+
Use a fullwidth book when a section needs more freedom than a reference page, such as:
|
|
29
|
+
|
|
30
|
+
- Product overview pages
|
|
31
|
+
- Landing-style documentation sections
|
|
32
|
+
- Rich Vue-driven pages
|
|
33
|
+
- Visual guides with wide screenshots or diagrams
|
|
34
|
+
- Documentation experiences that should not be constrained by a sidebar and Table of Contents
|
|
35
|
+
|
|
36
|
+
## Homepage Comparison
|
|
37
|
+
|
|
38
|
+
Homepage fullwidth is configured separately in `docsector.config.js`:
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
export default {
|
|
42
|
+
homePage: {
|
|
43
|
+
source: 'local',
|
|
44
|
+
layout: 'fullwidth'
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
That setting is opt-in for the homepage only. A book-level `layout: 'fullwidth'` is opt-in for every page in that book.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
## Visão Geral
|
|
2
|
+
|
|
3
|
+
O book Full é um exemplo funcional de opt-in de layout no nível do book. Ele usa `layout: 'fullwidth'` em `src/pages/full.book.js`, então toda página registrada em `src/pages/full.index.js` herda o chrome de página fullwidth.
|
|
4
|
+
|
|
5
|
+
Esta página deve renderizar com o header global e as abas de books, mas sem a sidebar esquerda, sem a toolbar de subpáginas, sem o botão de Table of Contents e sem a área lateral do Table of Contents.
|
|
6
|
+
|
|
7
|
+
## Configuração do Book
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
import { defineBook } from '../index.js'
|
|
11
|
+
|
|
12
|
+
export default defineBook({
|
|
13
|
+
id: 'full',
|
|
14
|
+
label: 'Full',
|
|
15
|
+
icon: 'fullscreen',
|
|
16
|
+
order: 3,
|
|
17
|
+
layout: 'fullwidth'
|
|
18
|
+
})
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
O campo `layout` aceita dois valores:
|
|
22
|
+
|
|
23
|
+
- `default` mantém o chrome padrão de documentação.
|
|
24
|
+
- `fullwidth` mantém o header global e as abas de books, depois remove a sidebar do book, a toolbar de subpáginas e o Table of Contents das páginas desse book.
|
|
25
|
+
|
|
26
|
+
## Quando Usar
|
|
27
|
+
|
|
28
|
+
Use um book fullwidth quando uma seção precisa de mais liberdade do que uma página de referência, como:
|
|
29
|
+
|
|
30
|
+
- Páginas de visão geral de produto
|
|
31
|
+
- Seções de documentação com estilo de landing page
|
|
32
|
+
- Páginas ricas movidas por Vue
|
|
33
|
+
- Guias visuais com screenshots ou diagramas largos
|
|
34
|
+
- Experiências de documentação que não devem ficar limitadas por uma sidebar e um Table of Contents
|
|
35
|
+
|
|
36
|
+
## Comparação com a Homepage
|
|
37
|
+
|
|
38
|
+
O fullwidth da homepage é configurado separadamente em `docsector.config.js`:
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
export default {
|
|
42
|
+
homePage: {
|
|
43
|
+
source: 'local',
|
|
44
|
+
layout: 'fullwidth'
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Essa configuração é opt-in apenas para a homepage. Um `layout: 'fullwidth'` no nível do book é opt-in para todas as páginas desse book.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
'/layout': {
|
|
3
|
+
config: {
|
|
4
|
+
icon: 'fullscreen',
|
|
5
|
+
status: 'new',
|
|
6
|
+
version: 'v4.5.0',
|
|
7
|
+
meta: {
|
|
8
|
+
description: {
|
|
9
|
+
'en-US': 'Fullwidth Layout — Documentation of Docsector Reader',
|
|
10
|
+
'pt-BR': 'Fullwidth Layout — Documentation of Docsector Reader'
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
book: 'full',
|
|
14
|
+
menu: {
|
|
15
|
+
header: {
|
|
16
|
+
icon: 'fullscreen',
|
|
17
|
+
label: 'Fullwidth'
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
subpages: {
|
|
21
|
+
showcase: false,
|
|
22
|
+
vs: false
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
data: {
|
|
26
|
+
'en-US': { title: 'Fullwidth Layout' },
|
|
27
|
+
'pt-BR': { title: 'Fullwidth Layout' }
|
|
28
|
+
},
|
|
29
|
+
metadata: {
|
|
30
|
+
tags: {
|
|
31
|
+
'en-US': 'fullwidth layout book sidebar table of contents home page landing page custom docs',
|
|
32
|
+
'pt-BR': 'fullwidth layout book sidebar table of contents home page landing page custom docs'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/quasar.factory.js
CHANGED
|
@@ -144,6 +144,7 @@ function getPagesRegistryFilesForRoot (root) {
|
|
|
144
144
|
function normalizeBookConfig (rawConfig = {}, fallbackId = 'manual', index = 0) {
|
|
145
145
|
const resolvedId = rawConfig.id || fallbackId || `book-${index + 1}`
|
|
146
146
|
const label = rawConfig.label || (resolvedId.charAt(0).toUpperCase() + resolvedId.slice(1))
|
|
147
|
+
const layouts = rawConfig.layouts ?? rawConfig.layout
|
|
147
148
|
|
|
148
149
|
return {
|
|
149
150
|
...rawConfig,
|
|
@@ -151,7 +152,8 @@ function normalizeBookConfig (rawConfig = {}, fallbackId = 'manual', index = 0)
|
|
|
151
152
|
label,
|
|
152
153
|
icon: rawConfig.icon || 'menu_book',
|
|
153
154
|
order: rawConfig.order ?? (index + 1),
|
|
154
|
-
color: normalizeBookColorConfig(rawConfig.color)
|
|
155
|
+
color: normalizeBookColorConfig(rawConfig.color),
|
|
156
|
+
...(layouts !== undefined ? { layouts } : {})
|
|
155
157
|
}
|
|
156
158
|
}
|
|
157
159
|
|
|
@@ -704,6 +706,7 @@ export const booksByVersion = entries.reduce((accumulator, entry, index) => {
|
|
|
704
706
|
icon: config.icon || 'menu_book',
|
|
705
707
|
order: config.order ?? (index + 1),
|
|
706
708
|
color: normalizeBookColor(config.color),
|
|
709
|
+
...(config.layouts !== undefined || config.layout !== undefined ? { layouts: config.layouts ?? config.layout } : {}),
|
|
707
710
|
version: version.id,
|
|
708
711
|
versionPrefix: version.routePrefix
|
|
709
712
|
}
|
package/src/router/routes.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { pageEntries, versions } from 'virtual:docsector-books'
|
|
2
2
|
import boot from 'pages/boot'
|
|
3
|
+
import docsectorConfig from 'docsector.config.js'
|
|
4
|
+
import { resolveHomePageLayout, resolvePageLayout } from '../page-layout'
|
|
3
5
|
|
|
4
6
|
const normalizeInternalLink = (linkTo) => {
|
|
5
7
|
const normalized = String(linkTo || '').trim()
|
|
@@ -89,6 +91,7 @@ for (const entry of pageEntries || []) {
|
|
|
89
91
|
|
|
90
92
|
const topPage = config.book ?? config.type ?? entry?.book ?? 'manual'
|
|
91
93
|
const routePath = normalizeRoutePath(`${entry?.versionPrefix || ''}/${topPage}${path}`)
|
|
94
|
+
const layouts = resolvePageLayout(entry?.bookConfig?.layouts ?? entry?.bookConfig?.layout, config.layouts ?? config.layout)
|
|
92
95
|
const hasInternalLink = typeof config?.link?.to === 'string' && config.link.to.trim().length > 0
|
|
93
96
|
const internalLinkTo = hasInternalLink ? normalizeVersionedInternalLink(config.link.to, entry?.versionPrefix || '') : null
|
|
94
97
|
const linkedConfig = hasInternalLink
|
|
@@ -167,6 +170,7 @@ for (const entry of pageEntries || []) {
|
|
|
167
170
|
sourceRoot: entry.sourceRoot || '',
|
|
168
171
|
sourcePathBase: buildSourcePathBase(entry, topPage, path),
|
|
169
172
|
pagePath: path,
|
|
173
|
+
layouts,
|
|
170
174
|
i18nSegments: entry.i18nSegments || [topPage, ...String(path).replace(/^\//, '').split('/').filter(Boolean)],
|
|
171
175
|
menuGroupPath: String(path).replace(/^\//, '').split('/').filter(Boolean)[0] || '',
|
|
172
176
|
unversionedPath: entry.unversionedPath || `/${topPage}${path}`
|
|
@@ -201,10 +205,7 @@ const routes = [
|
|
|
201
205
|
}
|
|
202
206
|
},
|
|
203
207
|
meta: boot.meta,
|
|
204
|
-
layouts:
|
|
205
|
-
footer: false,
|
|
206
|
-
submenu: false
|
|
207
|
-
},
|
|
208
|
+
layouts: resolveHomePageLayout(docsectorConfig.homePage),
|
|
208
209
|
pages: {}
|
|
209
210
|
},
|
|
210
211
|
children: [
|