@docsector/docsector-reader 2.0.2 → 2.0.4

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
@@ -47,6 +47,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
47
47
  - 🌍 **Internationalization (i18n)** — Multi-language support with HJSON locale files and per-page translations
48
48
  - 🌗 **Dark/Light Mode** — Automatic theme switching with Quasar Dark Plugin
49
49
  - 🔗 **Anchor Navigation** — Right-side Table of Contents tree with scroll tracking and auto-scroll to active section
50
+ - 🖱️ **Active Menu Item UX** — Active menu entries keep pointer cursor, clear URL hash without redundant navigation, and prevent accidental label text selection
50
51
  - 🔎 **Search** — Menu search across all documentation content and tags
51
52
  - 🌐 **WebMCP Browser Tools** — Registers in-page tools for browser agents with `registerTool` and optional `provideContext` fallback
52
53
  - 📱 **Responsive** — Mobile-friendly with collapsible sidebar and drawers
@@ -58,6 +59,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
58
59
  - 🧭 **Robust Edit Link Mapping** — Normalizes route paths (including trailing slashes) into `page.subpage.locale.md` source files for reliable GitHub edit URLs
59
60
  - 📅 **Last Updated Date** — Automatic per-page "last updated" date from git commit history, locale-formatted
60
61
  - 📊 **Translation Progress** — Automatic translation percentage based on header coverage
62
+ - 🌐 **Accurate Available Translations** — Locale availability counter now uses actual localized page source presence, avoiding false negatives when metadata is equal
61
63
  - 🧠 **Markdown Negotiation** — Responds with Markdown when clients send `Accept: text/markdown`, while keeping HTML as browser default
62
64
  - 🔐 **Web Bot Auth** — Can publish a signed HTTP message signatures directory and includes helpers to sign outbound bot requests
63
65
  - 🧭 **Content Signals** — Injects `Content-Signal` policy in `robots.txt` with deterministic, idempotent build output
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 = '2.0.2'
26
+ const VERSION = '2.0.4'
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": "2.0.2",
3
+ "version": "2.0.4",
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,12 +1,15 @@
1
1
  <script setup>
2
2
  // defineProps is a compiler macro in <script setup>, no import needed
3
- import { useRoute } from 'vue-router'
4
- import { useQuasar } from 'quasar'
3
+ import { useRoute, useRouter } from 'vue-router'
5
4
  import { useI18n } from 'vue-i18n'
6
5
 
7
6
  import { namespacedLabelI18nPath, routeTitleI18nPath } from '../i18n/path'
8
7
 
9
- const props = defineProps({
8
+ const $route = useRoute()
9
+ const $router = useRouter()
10
+ const { t } = useI18n()
11
+
12
+ defineProps({
10
13
  items: {
11
14
  type: Number,
12
15
  required: true
@@ -24,24 +27,20 @@ const props = defineProps({
24
27
  required: true
25
28
  },
26
29
  founds: {
27
- type: [Boolean, Array],
30
+ type: [Boolean, Array, Object],
28
31
  required: true
29
32
  }
30
33
  })
31
34
 
32
- const $q = useQuasar()
33
- const $route = useRoute()
34
- const { t } = useI18n()
35
-
36
- const getMenuItemHeaderBackground = () => {
37
- return $q.dark.isActive ? 'background-color: #1D1D1D !important' : 'background-color: #f5f5f5 !important'
38
- }
35
+ const getMenuItemLabel = (item) => {
36
+ if (!item?.path) {
37
+ return ''
38
+ }
39
39
 
40
- const getMenuItemLabel = (item, index) => {
41
40
  return t(routeTitleI18nPath(item.path))
42
41
  }
43
42
 
44
- const getMenuItemSubheader = (meta) => {
43
+ const getMenuItemSubheader = (meta = {}) => {
45
44
  const subheader = meta.menu?.subheader
46
45
  if (!subheader) {
47
46
  return ''
@@ -97,28 +96,54 @@ const normalizePath = (path) => {
97
96
  const isMenuItemActive = (path) => {
98
97
  return normalizePath(path) === normalizePath($route.path)
99
98
  }
99
+
100
+ const getMenuItemTargetPath = (path) => {
101
+ return `${path}/overview/`
102
+ }
103
+
104
+ const getMenuItemTo = (path) => {
105
+ return getMenuItemTargetPath(path)
106
+ }
107
+
108
+ const onMenuItemClick = (event, path, currentSubpage) => {
109
+ const currentPath = `${path}${currentSubpage}`
110
+ if (!isMenuItemActive(currentPath)) {
111
+ return
112
+ }
113
+
114
+ event?.preventDefault?.()
115
+ event?.stopPropagation?.()
116
+
117
+ if ($route.hash) {
118
+ $router.replace({ path: $route.path, hash: '' })
119
+ }
120
+ }
100
121
  </script>
101
122
 
102
123
  <template>
103
124
  <!-- Menu Separator - Subheader -->
104
- <q-item-section v-if="subitem.meta.menu?.subheader">
125
+ <q-item-section v-if="subitem?.meta?.menu?.subheader">
105
126
  <q-item-label class="label subheader" header>
106
127
  {{ getMenuItemSubheader(subitem.meta) }}
107
128
  </q-item-label>
108
129
  </q-item-section>
109
130
 
110
131
  <q-item
111
- :to="subitem.path + '/overview/'"
132
+ v-if="subitem?.path"
133
+ :to="getMenuItemTo(subitem.path)"
112
134
  :active="isMenuItemActive(subitem.path + subpage)"
135
+ :class="{ 'd-menu-item--active': isMenuItemActive(subitem.path + subpage) }"
136
+ clickable
137
+ @click="onMenuItemClick($event, subitem.path, subpage)"
113
138
  v-show="founds[subitem.path] || !founds"
114
139
  >
115
140
  <q-item-section side>
116
- <q-icon v-if="subitem.meta.icon" :name="subitem.meta.icon" />
141
+ <q-icon v-if="subitem?.meta?.icon" :name="subitem.meta.icon" />
117
142
  </q-item-section>
118
143
  <q-item-section>
119
- {{ getMenuItemLabel(subitem, subindex) }}
144
+ {{ getMenuItemLabel(subitem) }}
120
145
  </q-item-section>
121
- <q-item-section class="page-status" v-if="subitem.meta.status !== 'done'" side>
146
+ <q-item-section class="page-status" v-if="subitem?.meta && subitem.meta.status !== 'done'" side>
122
147
  <q-badge
123
148
  :text-color="getPageStatusTextColor(subitem.meta.status)"
124
149
  :color="getPageStatusColor(subitem.meta.status)"
@@ -129,10 +154,17 @@ const isMenuItemActive = (path) => {
129
154
  </q-item>
130
155
 
131
156
  <!-- Menu Separator -->
132
- <li v-if="subitem.meta.menu?.separator" role="listitem">
157
+ <li v-if="subitem?.meta?.menu?.separator" role="listitem">
133
158
  <q-separator
134
159
  :class="'separator' + (subitem.meta.menu.separator === true ? '' : subitem.meta.menu.separator)"
135
160
  role="separator"
136
161
  />
137
162
  </li>
138
163
  </template>
164
+
165
+ <style lang="sass">
166
+ .d-menu-item--active
167
+ cursor: pointer
168
+ user-select: none
169
+ -webkit-user-select: none
170
+ </style>
@@ -94,6 +94,20 @@ const pActive = (relative) => {
94
94
  return null
95
95
  }
96
96
 
97
+ const normalizeRoutePath = (path) => {
98
+ const normalized = String(path || '').trim()
99
+ if (normalized === '' || normalized === '/') {
100
+ return '/'
101
+ }
102
+
103
+ const sanitized = normalized.replace(/\/+$/, '')
104
+ return sanitized === '' ? '/' : sanitized
105
+ }
106
+
107
+ const isSameEffectivePage = (from, to) => {
108
+ return normalizeRoutePath(from?.path) === normalizeRoutePath(to?.path)
109
+ }
110
+
97
111
  const subroute = (to) => {
98
112
  const base = '/' + store.state.page.base
99
113
  const relative = store.state.page.relative
@@ -204,7 +218,7 @@ onMounted(() => {
204
218
  router.beforeEach((to, from, next) => {
205
219
  resetPageScroll()
206
220
 
207
- if (to.hash === '' && from.path !== to.path) {
221
+ if (to.hash === '' && !isSameEffectivePage(from, to)) {
208
222
  store.commit('page/resetAnchor')
209
223
  store.commit('page/resetAnchors')
210
224
  store.commit('page/resetNodes')
@@ -107,23 +107,25 @@ const progress = computed(() => {
107
107
 
108
108
  const languages = computed(() => {
109
109
  const i18nPathAbsolute = store.state.i18n.absolute
110
- const translations = pageValueI18nPath(i18nPathAbsolute, '_translations')
111
110
  const i18nLocales = availableLocales
112
- let fallbackLastUpdated = null
111
+ const sourcePath = pageValueI18nPath(i18nPathAbsolute, 'source')
113
112
 
114
- if (te(translations, 'en-US')) {
115
- fallbackLastUpdated = tm(translations, 'en-US')
113
+ if (!i18nPathAbsolute || i18nLocales.length === 0) {
114
+ return `0/${i18nLocales.length}`
116
115
  }
117
116
 
118
117
  let i18nLocalesAvailable = 0
119
- if (fallbackLastUpdated) {
120
- for (let i = 0; i < i18nLocales.length; i++) {
121
- if (t(translations, i18nLocales[i]) !== fallbackLastUpdated) {
122
- i18nLocalesAvailable++
123
- }
118
+ for (let i = 0; i < i18nLocales.length; i++) {
119
+ const currentLocale = i18nLocales[i]
120
+
121
+ if (!te(sourcePath, currentLocale)) {
122
+ continue
123
+ }
124
+
125
+ const source = tm(sourcePath, currentLocale)
126
+ if (typeof source === 'string' && source.trim().length > 0) {
127
+ i18nLocalesAvailable++
124
128
  }
125
- } else {
126
- i18nLocalesAvailable = 1
127
129
  }
128
130
 
129
131
  return `${i18nLocalesAvailable}/${i18nLocales.length}`
@@ -294,11 +294,25 @@ function onBookTabChange (bookId) {
294
294
  }
295
295
  }
296
296
 
297
+ const normalizeRoutePath = (path) => {
298
+ const normalized = String(path || '').trim()
299
+ if (normalized === '' || normalized === '/') {
300
+ return '/'
301
+ }
302
+
303
+ const sanitized = normalized.replace(/\/+$/, '')
304
+ return sanitized === '' ? '/' : sanitized
305
+ }
306
+
307
+ const isSameEffectivePage = (from, to) => {
308
+ return normalizeRoutePath(from?.path) === normalizeRoutePath(to?.path)
309
+ }
310
+
297
311
  // --- created logic (runs at setup time) ---
298
312
  store.dispatch('app/configureLanguage', route.matched)
299
313
 
300
314
  router.afterEach((to, from) => {
301
- if (!to.hash || (from.path !== to.path)) {
315
+ if (!isSameEffectivePage(from, to)) {
302
316
  store.dispatch('app/configureLanguage', to.matched)
303
317
  }
304
318
  })
@@ -7,6 +7,6 @@ export default defineBook({
7
7
  order: 2,
8
8
  color: {
9
9
  active: 'white',
10
- inactive: 'secondary'
10
+ inactive: 'white'
11
11
  }
12
12
  })
@@ -7,6 +7,6 @@ export default defineBook({
7
7
  order: 1,
8
8
  color: {
9
9
  active: 'white',
10
- inactive: 'primary'
10
+ inactive: 'white'
11
11
  }
12
12
  })