@docsector/docsector-reader 4.4.4 → 4.4.6

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
@@ -57,7 +57,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
57
57
  - 🔗 **Anchor Navigation** — Right-side source-ordered Table of Contents tree with stable scroll tracking, resize-safe drawer state, auto-scroll to the active section, and active-heading resolution based on the last heading that crossed the content threshold
58
58
  - 🖱️ **Active Menu Item UX** — Active menu entries keep pointer cursor, clear URL hash without redundant navigation, and prevent accidental label text selection
59
59
  - 🔎 **Search** — Menu search across all documentation content and tags
60
- - 💬 **Assistant Chat UX Enhancements** — Long conversations keep focus on recent messages, load earlier history progressively, deduplicate repeated sources, include per-message copy actions, and show a floating quick return to the bottom
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
62
  - 📚 **Book Tabs with Per-State Colors** — Define `*.book.js` tabs with icons, order, and `color.active` / `color.inactive`
63
63
  - 🔀 **Internal Shortcut Pages** — Route entries can redirect with `config.link.to`, keeping localized titles while inheriting icon/status from the destination page
@@ -298,7 +298,7 @@ Check `checks.discovery.webMcp.status` equals `"pass"`.
298
298
 
299
299
  Docsector Reader can add an opt-in assistant panel for documentation Q&A. Users open it from the global header while reading pages and subpages; it is not a dedicated documentation route. The drawer posts to a same-origin Cloudflare Pages Function, and that function calls Cloudflare AI Search so secrets, rate-limit strategy, provider errors, and future auth stay server-side.
300
300
 
301
- The panel is disabled by default. When enabled, desktop pages get a dedicated right-side assistant rail that can sit beside the table of contents on wide screens. Mobile uses a fullscreen dialog. Conversations restore at the latest message, reveal earlier history progressively in long chats, deduplicate repeated source links, and provide per-message copy actions.
301
+ The panel is disabled by default. When enabled, desktop pages get a dedicated right-side assistant rail that can sit beside the table of contents on wide screens. Mobile uses a fullscreen dialog. Conversations restore at the latest message, reveal earlier history progressively in long chats, deduplicate repeated source links, preserve the panel open state across page reloads, and provide per-message copy actions.
302
302
 
303
303
  ### Configure
304
304
 
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.4.4'
27
+ const VERSION = '4.4.6'
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docsector/docsector-reader",
3
- "version": "4.4.4",
3
+ "version": "4.4.6",
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",
@@ -1,10 +1,16 @@
1
1
  <script setup>
2
- import { ref, computed, watch } from 'vue'
2
+ import { ref, computed, watch, nextTick } from 'vue'
3
+ import { useRoute, useRouter } from 'vue-router'
3
4
  import { useQuasar } from 'quasar'
4
- import { useStore } from 'vuex'
5
5
  import Prism from './code-block-highlighting'
6
+ import {
7
+ buildSourceCodeLineAnchorId,
8
+ resolveSourceCodeLineHref,
9
+ shouldHandleSourceCodeLineActivation
10
+ } from './source-code-anchor'
6
11
  import { countRenderedCodeLines } from './source-code-lines'
7
12
  import { looksLikeFileName, resolveFileIconUrl } from '../composables/useFileIcon'
13
+ import useNavigator from '../composables/useNavigator'
8
14
 
9
15
  defineOptions({
10
16
  name: 'DBlockSourceCode'
@@ -38,15 +44,17 @@ const props = defineProps({
38
44
  })
39
45
 
40
46
  const $q = useQuasar()
41
- const store = useStore()
47
+ const route = useRoute()
48
+ const router = useRouter()
49
+ const { anchor: scrollToAnchor } = useNavigator()
42
50
 
43
51
  const copyBtnDisabled = ref(false)
44
52
  const copyBtnColor = ref(null)
45
53
  const copyBtnIcon = ref('content_copy')
46
54
  const codeRef = ref(null)
47
55
  const activeTab = ref(0)
56
+ const lineAnchorTopOffset = 34
48
57
 
49
- const href = computed(() => `${store.state.page.absolute}#${anchor.value}`)
50
58
  const coloring = computed(() => $q.dark.isActive ? 'dark' : 'white')
51
59
  const anchor = computed(() => printToLetter(props.index + 1))
52
60
 
@@ -155,6 +163,38 @@ function copyCode() {
155
163
  }
156
164
  }
157
165
 
166
+ function buildLineAnchorId(line) {
167
+ return buildSourceCodeLineAnchorId(anchor.value, line)
168
+ }
169
+
170
+ function buildLineHref(line) {
171
+ return resolveSourceCodeLineHref(router, route.path, route.query, buildLineAnchorId(line))
172
+ }
173
+
174
+ async function navigateToLineAnchor(event, line) {
175
+ if (!shouldHandleSourceCodeLineActivation(event)) {
176
+ return
177
+ }
178
+
179
+ const hash = `#${buildLineAnchorId(line)}`
180
+
181
+ event.preventDefault()
182
+
183
+ if (route.hash === hash) {
184
+ scrollToAnchor(hash, false)
185
+ return
186
+ }
187
+
188
+ await router.push({
189
+ path: route.path,
190
+ query: route.query,
191
+ hash
192
+ })
193
+
194
+ await nextTick()
195
+ scrollToAnchor(hash, false)
196
+ }
197
+
158
198
  function printToLetter(number) {
159
199
  const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
160
200
  let result = ''
@@ -245,9 +285,9 @@ function printToLetter(number) {
245
285
  <div class="code" :class="coloring">
246
286
  <div class="lines" v-if="lines && lines > 1">
247
287
  <template v-for="(line, index) in lines" :key="index">
248
- <a class="line" :href="href+line">
288
+ <a class="line" :href="buildLineHref(line)" @click="navigateToLineAnchor($event, line)">
249
289
  <i class="fa fa-link" aria-hidden="true" data-hidden="true"></i>
250
- <span :id="`${anchor}${line}`">{{ line }}</span>
290
+ <span :id="buildLineAnchorId(line)" :data-anchor-offset-top="lineAnchorTopOffset">{{ line }}</span>
251
291
  </a>
252
292
  </template>
253
293
  </div>
@@ -0,0 +1,15 @@
1
+ export function buildSourceCodeLineAnchorId(anchorPrefix, line) {
2
+ return `${anchorPrefix}${line}`
3
+ }
4
+
5
+ export function resolveSourceCodeLineHref(router, routePath, routeQuery, anchorId) {
6
+ return router.resolve({
7
+ path: routePath,
8
+ query: routeQuery,
9
+ hash: `#${anchorId}`
10
+ }).href
11
+ }
12
+
13
+ export function shouldHandleSourceCodeLineActivation(event) {
14
+ return !(event.defaultPrevented || event.button !== 0 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
15
+ }
@@ -11,6 +11,62 @@ export default function useNavigator() {
11
11
  const route = useRoute()
12
12
  const selected = ref(null)
13
13
 
14
+ const normalizeScrollTarget = (target) => {
15
+ if (target === document.body || target === document.documentElement) {
16
+ return window
17
+ }
18
+
19
+ return target
20
+ }
21
+
22
+ const getScrollTargetTop = (target) => {
23
+ if (target === window) {
24
+ return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0
25
+ }
26
+
27
+ return target?.scrollTop || 0
28
+ }
29
+
30
+ const getScrollTargetRectTop = (target) => {
31
+ if (target === window) {
32
+ return 0
33
+ }
34
+
35
+ return target?.getBoundingClientRect?.().top || 0
36
+ }
37
+
38
+ const getAnchorTopOffset = (anchorEl) => {
39
+ if (!(anchorEl instanceof HTMLElement)) {
40
+ return 0
41
+ }
42
+
43
+ const explicitOffset = Number.parseFloat(anchorEl.dataset.anchorOffsetTop || '')
44
+
45
+ if (Number.isFinite(explicitOffset)) {
46
+ return Math.max(0, explicitOffset)
47
+ }
48
+
49
+ return 0
50
+ }
51
+
52
+ const resolveAnchorScrollState = (anchorEl) => {
53
+ if (!(anchorEl instanceof HTMLElement)) {
54
+ return null
55
+ }
56
+
57
+ const pageScrollContainer = anchorEl.closest('.q-scrollarea__container')
58
+ const scrollTarget = normalizeScrollTarget(pageScrollContainer || scroll.getScrollTarget(anchorEl))
59
+ const anchorRect = anchorEl.getBoundingClientRect()
60
+ const scrollTop = getScrollTargetTop(scrollTarget)
61
+ const targetRectTop = getScrollTargetRectTop(scrollTarget)
62
+ const anchorTopOffset = getAnchorTopOffset(anchorEl)
63
+
64
+ return {
65
+ scrollTarget,
66
+ offsetTop: Math.max(0, ((anchorRect.top - targetRectTop) + scrollTop) - anchorTopOffset)
67
+ }
68
+ }
69
+
14
70
  const normalizeDomAnchorId = (id) => {
15
71
  if (id === null || id === undefined || id === false) {
16
72
  return ''
@@ -62,10 +118,11 @@ export default function useNavigator() {
62
118
  const Anchor = document.getElementById(anchorId)
63
119
 
64
120
  if (Anchor !== null && typeof Anchor === 'object') {
65
- const ScrollTarget = scroll.getScrollTarget(Anchor)
66
- const AnchorOffsetTop = Anchor.offsetTop
121
+ const scrollState = resolveAnchorScrollState(Anchor)
67
122
 
68
- scroll.setVerticalScrollPosition(ScrollTarget, AnchorOffsetTop, 300)
123
+ if (scrollState !== null) {
124
+ scroll.setVerticalScrollPosition(scrollState.scrollTarget, scrollState.offsetTop, 300)
125
+ }
69
126
 
70
127
  setTimeout(() => {
71
128
  store.commit('page/setScrolling', true)
@@ -98,7 +155,7 @@ export default function useNavigator() {
98
155
  const Anchor = document.getElementById(domAnchorId)
99
156
 
100
157
  if (Anchor !== null && typeof Anchor === 'object') {
101
- return Anchor.offsetTop
158
+ return resolveAnchorScrollState(Anchor)?.offsetTop
102
159
  }
103
160
 
104
161
  return undefined
@@ -1,7 +1,35 @@
1
- export default {
2
- namespaced: true,
1
+ export const LAYOUT_ASSISTANT_STORAGE_KEY = 'docsector.layout.assistant.v1'
2
+
3
+ function getStorage (storage = null) {
4
+ if (storage) return storage
5
+ if (typeof window === 'undefined') return null
6
+ return window.localStorage || null
7
+ }
8
+
9
+ export function loadPersistedAssistantLayout ({ storage = null, key = LAYOUT_ASSISTANT_STORAGE_KEY } = {}) {
10
+ const target = getStorage(storage)
11
+ if (!target) return false
12
+
13
+ try {
14
+ return target.getItem(key) === 'true'
15
+ } catch {
16
+ return false
17
+ }
18
+ }
19
+
20
+ export function savePersistedAssistantLayout (value, { storage = null, key = LAYOUT_ASSISTANT_STORAGE_KEY } = {}) {
21
+ const target = getStorage(storage)
22
+ if (!target) return
3
23
 
4
- state: {
24
+ try {
25
+ target.setItem(key, value ? 'true' : 'false')
26
+ } catch {
27
+ // Ignore storage failures so layout keeps working in memory.
28
+ }
29
+ }
30
+
31
+ export function createLayoutState () {
32
+ return {
5
33
  header: true,
6
34
  footer: true,
7
35
  left: false,
@@ -30,9 +58,15 @@ export default {
30
58
  scrolling: true,
31
59
  meta: true,
32
60
  metaToggle: false,
33
- assistant: false,
61
+ assistant: loadPersistedAssistantLayout(),
34
62
  assistantWidth: 380
35
- },
63
+ }
64
+ }
65
+
66
+ export default {
67
+ namespaced: true,
68
+
69
+ state: createLayoutState,
36
70
  getters: {
37
71
  view (state) {
38
72
  const
@@ -122,7 +156,8 @@ export default {
122
156
  state.metaToggle = val
123
157
  },
124
158
  setAssistant (state, val) {
125
- state.assistant = val
159
+ state.assistant = Boolean(val)
160
+ savePersistedAssistantLayout(state.assistant)
126
161
  },
127
162
  setAssistantWidth (state, val) {
128
163
  state.assistantWidth = val