@docsector/docsector-reader 3.0.0 → 3.1.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 CHANGED
@@ -54,6 +54,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
54
54
  - 📚 **Book Tabs with Per-State Colors** — Define `*.book.js` tabs with icons, order, and `color.active` / `color.inactive`
55
55
  - 🔀 **Internal Shortcut Pages** — Route entries can redirect with `config.link.to`, keeping localized titles while inheriting icon/status from the destination page
56
56
  - 📐 **Responsive Subpage Toolbar** — Subpage actions align with the content column on desktop and dock to the bottom on mobile
57
+ - ⬆️ **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
57
58
  - 🏷️ **Status Badges** — Mark pages as `done`, `draft`, `empty`, or `new` with visual indicators
58
59
  - ✏️ **Edit on GitHub** — Direct links to edit pages on your repository
59
60
  - 🧭 **Robust Edit Link Mapping** — Normalizes route paths (including trailing slashes) into `page.subpage.locale.md` source files for reliable GitHub edit URLs
package/bin/docsector.js CHANGED
@@ -23,7 +23,7 @@ const packageRoot = resolve(__dirname, '..')
23
23
  const args = process.argv.slice(2)
24
24
  const command = args[0]
25
25
 
26
- const VERSION = '3.0.0'
26
+ const VERSION = '3.1.0'
27
27
 
28
28
  const HELP = `
29
29
  Docsector Reader v${VERSION}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docsector/docsector-reader",
3
- "version": "3.0.0",
3
+ "version": "3.1.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",
@@ -5,6 +5,7 @@ import { useRoute, useRouter } from 'vue-router'
5
5
  import { useQuasar } from 'quasar'
6
6
 
7
7
  import useNavigator from '../composables/useNavigator'
8
+ import { getReadingProgressState } from '../composables/useReadingProgress'
8
9
 
9
10
  import DPageAnchor from './DPageAnchor.vue'
10
11
  import DPageMeta from './DPageMeta.vue'
@@ -20,6 +21,10 @@ const props = defineProps({
20
21
  disableNav: {
21
22
  type: Boolean,
22
23
  default: false
24
+ },
25
+ showBackToTopControl: {
26
+ type: Boolean,
27
+ default: false
23
28
  }
24
29
  })
25
30
 
@@ -29,6 +34,26 @@ const submenu = ref(null)
29
34
  const pageMinHeight = ref('calc(100vh - 86px)')
30
35
  const submenuHeight = ref('36px')
31
36
  const pageBottomInset = ref('0px')
37
+ const readingProgress = ref(getReadingProgressState())
38
+
39
+ const getPageScrollContainer = () => {
40
+ return pageScrollArea.value?.$el?.querySelector('.q-scrollarea__container') || null
41
+ }
42
+
43
+ const syncReadingProgress = (scrollTop = null) => {
44
+ const container = getPageScrollContainer()
45
+
46
+ if (!container) {
47
+ readingProgress.value = getReadingProgressState()
48
+ return
49
+ }
50
+
51
+ readingProgress.value = getReadingProgressState({
52
+ scrollTop: scrollTop ?? container.scrollTop,
53
+ scrollHeight: container.scrollHeight,
54
+ clientHeight: container.clientHeight
55
+ })
56
+ }
32
57
 
33
58
  const updatePageMinHeight = () => {
34
59
  const pageContainerEl = pageContainer.value?.$el || pageContainer.value
@@ -47,6 +72,7 @@ const updatePageMinHeight = () => {
47
72
  pageMinHeight.value = `calc(100vh - ${totalOffset}px)`
48
73
  submenuHeight.value = `${Math.max(36, Math.round(measuredSubmenuHeight))}px`
49
74
  pageBottomInset.value = isMobile ? submenuHeight.value : '0px'
75
+ syncReadingProgress()
50
76
  }
51
77
 
52
78
  const schedulePageMinHeightUpdate = () => {
@@ -80,6 +106,12 @@ const main = computed(() => {
80
106
  return 'overview'
81
107
  }
82
108
  })
109
+ const shouldShowBackToTopControl = computed(() => {
110
+ return props.showBackToTopControl && readingProgress.value.hasOverflow && readingProgress.value.isVisible
111
+ })
112
+ const backToTopRightOffset = computed(() => {
113
+ return layoutMeta.value && !$q.screen.lt.md ? '332px' : '24px'
114
+ })
83
115
 
84
116
  const toggleSectionsTree = () => {
85
117
  layoutMeta.value = !layoutMeta.value
@@ -133,10 +165,7 @@ const resetPageScroll = () => {
133
165
  if (pageScrollArea.value !== null) {
134
166
  pageScrollArea.value.setScrollPosition('vertical', 0, 0)
135
167
  }
136
- }
137
-
138
- const getPageScrollContainer = () => {
139
- return pageScrollArea.value?.$el?.querySelector('.q-scrollarea__container') || null
168
+ syncReadingProgress(0)
140
169
  }
141
170
 
142
171
  const isEditableTarget = (target) => {
@@ -208,6 +237,15 @@ const handleMainScrollKeys = (event) => {
208
237
  container.scrollTop = nextTop
209
238
  }
210
239
 
240
+ const handlePageScroll = (scrollState) => {
241
+ scrolling(scrollState)
242
+ syncReadingProgress(scrollState?.position?.top)
243
+ }
244
+
245
+ const scrollToTop = () => {
246
+ navigate(0)
247
+ }
248
+
211
249
  onMounted(() => {
212
250
  window.addEventListener('keydown', handleMainScrollKeys)
213
251
  window.addEventListener('resize', schedulePageMinHeightUpdate)
@@ -236,6 +274,7 @@ onBeforeUnmount(() => {
236
274
  watch(() => route.fullPath, () => {
237
275
  nextTick(() => {
238
276
  schedulePageMinHeightUpdate()
277
+ syncReadingProgress(0)
239
278
  })
240
279
  })
241
280
  </script>
@@ -292,10 +331,38 @@ watch(() => route.fullPath, () => {
292
331
  <slot />
293
332
  </div>
294
333
  <d-page-meta v-if="!disableNav" />
295
- <q-scroll-observer @scroll="scrolling" :debounce="300" />
334
+ <q-scroll-observer @scroll="handlePageScroll" :debounce="300" />
296
335
  </q-scroll-area>
297
336
  </q-page>
298
337
 
338
+ <div
339
+ v-if="shouldShowBackToTopControl"
340
+ class="d-back-to-top"
341
+ :style="{ '--d-back-to-top-right': backToTopRightOffset }"
342
+ >
343
+ <q-circular-progress
344
+ class="d-back-to-top__progress"
345
+ :value="readingProgress.progressPercent"
346
+ size="58px"
347
+ :thickness="0.16"
348
+ color="primary"
349
+ track-color="grey-5"
350
+ />
351
+ <q-btn
352
+ class="d-back-to-top__button"
353
+ round
354
+ dense
355
+ unelevated
356
+ color="dark"
357
+ text-color="white"
358
+ icon="north"
359
+ :aria-label="$t('system.backToTop')"
360
+ @click="scrollToTop"
361
+ >
362
+ <q-tooltip anchor="top middle" self="bottom middle" :offset="[10, 10]">{{ $t('system.backToTop') }}</q-tooltip>
363
+ </q-btn>
364
+ </div>
365
+
299
366
  <q-drawer elevated show-if-above side="right" v-model="layoutMeta">
300
367
  <d-page-anchor id="anchor" />
301
368
  </q-drawer>
@@ -321,6 +388,25 @@ watch(() => route.fullPath, () => {
321
388
  #page
322
389
  min-height: var(--d-page-min-height, calc(100vh - 86px)) !important
323
390
 
391
+ .d-back-to-top
392
+ position: fixed
393
+ right: var(--d-back-to-top-right, 24px)
394
+ bottom: calc(24px + var(--d-page-bottom-inset, 0px) + env(safe-area-inset-bottom, 0px))
395
+ width: 58px
396
+ height: 58px
397
+ z-index: 1200
398
+ filter: drop-shadow(0 8px 18px rgba(0,0,0,0.2))
399
+
400
+ .d-back-to-top__progress,
401
+ .d-back-to-top__button
402
+ position: absolute
403
+ inset: 0
404
+
405
+ .d-back-to-top__button
406
+ margin: auto
407
+ width: 40px
408
+ height: 40px
409
+
324
410
  #scroll-container
325
411
  width: 100%
326
412
  max-width: 1200px
@@ -425,4 +511,12 @@ body.mobile.body--dark
425
511
  body.mobile
426
512
  .q-drawer--right
427
513
  background: rgba(255, 255, 255, 0.7)
514
+
515
+ body.body--light
516
+ .d-back-to-top__progress
517
+ color: var(--q-primary)
518
+
519
+ body.body--dark
520
+ .d-back-to-top__progress
521
+ color: #58d1a8
428
522
  </style>
@@ -22,7 +22,7 @@ const id = computed(() => {
22
22
  </script>
23
23
 
24
24
  <template>
25
- <d-page>
25
+ <d-page show-back-to-top-control>
26
26
  <header>
27
27
  <d-page-bar />
28
28
  <hr />
@@ -0,0 +1,46 @@
1
+ export const DEFAULT_READING_PROGRESS_THRESHOLD = 0.12
2
+
3
+ const toFiniteNumber = (value) => {
4
+ const normalized = Number(value)
5
+ return Number.isFinite(normalized) ? normalized : 0
6
+ }
7
+
8
+ const clamp = (value, min, max) => {
9
+ return Math.min(max, Math.max(min, value))
10
+ }
11
+
12
+ export function getReadingProgressState({
13
+ scrollTop = 0,
14
+ scrollHeight = 0,
15
+ clientHeight = 0,
16
+ visibilityThreshold = DEFAULT_READING_PROGRESS_THRESHOLD
17
+ } = {}) {
18
+ const normalizedClientHeight = Math.max(0, toFiniteNumber(clientHeight))
19
+ const normalizedScrollHeight = Math.max(0, toFiniteNumber(scrollHeight))
20
+ const maxScrollTop = Math.max(0, normalizedScrollHeight - normalizedClientHeight)
21
+
22
+ if (maxScrollTop === 0) {
23
+ return {
24
+ hasOverflow: false,
25
+ maxScrollTop: 0,
26
+ scrollTop: 0,
27
+ progressPercent: 0,
28
+ visibleOffset: 0,
29
+ isVisible: false
30
+ }
31
+ }
32
+
33
+ const normalizedScrollTop = clamp(toFiniteNumber(scrollTop), 0, maxScrollTop)
34
+ const normalizedThreshold = clamp(toFiniteNumber(visibilityThreshold), 0, 1)
35
+ const visibleOffset = Math.round(maxScrollTop * normalizedThreshold)
36
+ const progressPercent = Math.round((normalizedScrollTop / maxScrollTop) * 100)
37
+
38
+ return {
39
+ hasOverflow: true,
40
+ maxScrollTop,
41
+ scrollTop: normalizedScrollTop,
42
+ progressPercent,
43
+ visibleOffset,
44
+ isVisible: normalizedScrollTop >= visibleOffset && normalizedScrollTop > 0
45
+ }
46
+ }
@@ -9,6 +9,7 @@ Every documentation page is rendered inside a `DPage` instance.
9
9
  | Prop | Type | Default | Description |
10
10
  |------|------|---------|-------------|
11
11
  | `disableNav` | `Boolean` | `false` | Hides the DPageMeta navigation footer |
12
+ | `showBackToTopControl` | `Boolean` | `false` | Enables the floating back-to-top control with circular reading progress |
12
13
 
13
14
  ## Template Structure
14
15
 
@@ -46,6 +47,8 @@ DPage reads the route's `meta.subpages` configuration to determine which tabs to
46
47
 
47
48
  DPage resets the scroll position on route changes via `router.beforeEach`. The scroll observer monitors vertical scroll position and updates the anchor selection via the `useNavigator` composable.
48
49
 
50
+ When `showBackToTopControl` is enabled, DPage also derives reading progress from the same scroll container. The floating control stays hidden at the top, appears after a small amount of scroll, shows circular progress, and returns to anchor `0` when clicked.
51
+
49
52
  ## Store Integration
50
53
 
51
54
  DPage reads from and writes to several Vuex store modules:
@@ -9,6 +9,7 @@ Toda página de documentação é renderizada dentro de uma instância de `DPage
9
9
  | Prop | Tipo | Padrão | Descrição |
10
10
  |------|------|--------|-----------|
11
11
  | `disableNav` | `Boolean` | `false` | Oculta o rodapé de navegação DPageMeta |
12
+ | `showBackToTopControl` | `Boolean` | `false` | Habilita o controle flutuante de voltar ao topo com progresso circular de leitura |
12
13
 
13
14
  ## Estrutura do Template
14
15
 
@@ -46,6 +47,8 @@ DPage lê a configuração `meta.subpages` da rota para determinar quais abas ex
46
47
 
47
48
  DPage reseta a posição de scroll nas mudanças de rota via `router.beforeEach`. O observador de scroll monitora a posição vertical e atualiza a seleção de âncora via composable `useNavigator`.
48
49
 
50
+ Quando `showBackToTopControl` está habilitada, DPage também deriva o progresso de leitura a partir da mesma área de scroll. O controle flutuante fica oculto no topo, aparece após uma pequena rolagem, exibe progresso circular e retorna para a âncora `0` ao ser clicado.
51
+
49
52
  ## Integração com Store
50
53
 
51
54
  DPage lê e escreve em vários módulos da Vuex store:
@@ -9,7 +9,7 @@ DSubpage generates a deterministic numeric ID from the current route path using
9
9
  ## Template
10
10
 
11
11
  ```html
12
- <d-page>
12
+ <d-page show-back-to-top-control>
13
13
  <header>
14
14
  <d-h1 :id="0" />
15
15
  </header>
@@ -54,3 +54,7 @@ DSubpage is automatically used by the router for all documentation pages. You do
54
54
  - `DSubpage` **uses** `DPage` as its container
55
55
  - `DPage` handles layout (scroll, toolbar, drawer)
56
56
  - `DSubpage` handles content composition (H1 + sections)
57
+
58
+ ## Built-in Back to Top Control
59
+
60
+ Routed documentation subpages enable DPage's floating back-to-top control automatically. The control is only shown when the content actually overflows, becomes visible after the reader scrolls a little, and visualizes the current reading progress with a circular indicator.
@@ -9,7 +9,7 @@ DSubpage gera um ID numérico determinístico a partir do caminho da rota atual
9
9
  ## Template
10
10
 
11
11
  ```html
12
- <d-page>
12
+ <d-page show-back-to-top-control>
13
13
  <header>
14
14
  <d-h1 :id="0" />
15
15
  </header>
@@ -54,3 +54,7 @@ DSubpage é usado automaticamente pelo roteador para todas as páginas de docume
54
54
  - `DSubpage` **usa** `DPage` como container
55
55
  - `DPage` cuida do layout (scroll, toolbar, drawer)
56
56
  - `DSubpage` cuida da composição de conteúdo (H1 + seções)
57
+
58
+ ## Controle Integrado de Voltar ao Topo
59
+
60
+ As sub-páginas de documentação roteadas habilitam automaticamente o controle flutuante de voltar ao topo do DPage. O controle só é exibido quando o conteúdo realmente tem overflow, fica visível após uma pequena rolagem do leitor e mostra o progresso atual de leitura com um indicador circular.