@docsector/docsector-reader 1.7.1 → 2.0.1

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/.eslintrc.cjs CHANGED
@@ -1,6 +1,8 @@
1
1
  module.exports = {
2
2
  root: true,
3
3
 
4
+ ignorePatterns: ['docsector.config.js'],
5
+
4
6
  parserOptions: {
5
7
  ecmaVersion: 2022,
6
8
  sourceType: 'module'
package/README.md CHANGED
@@ -50,6 +50,9 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
50
50
  - 🔎 **Search** — Menu search across all documentation content and tags
51
51
  - 🌐 **WebMCP Browser Tools** — Registers in-page tools for browser agents with `registerTool` and optional `provideContext` fallback
52
52
  - 📱 **Responsive** — Mobile-friendly with collapsible sidebar and drawers
53
+ - 📚 **Book Tabs with Per-State Colors** — Define `*.book.js` tabs with icons, order, and `color.active` / `color.inactive`
54
+ - 🔀 **Internal Shortcut Pages** — Route entries can redirect with `config.link.to`, keeping localized titles while inheriting icon/status from the destination page
55
+ - 📐 **Responsive Subpage Toolbar** — Subpage actions align with the content column on desktop and dock to the bottom on mobile
53
56
  - 🏷️ **Status Badges** — Mark pages as `done`, `draft`, or `empty` with visual indicators
54
57
  - ✏️ **Edit on GitHub** — Direct links to edit pages on your repository
55
58
  - 🧭 **Robust Edit Link Mapping** — Normalizes route paths (including trailing slashes) into `page.subpage.locale.md` source files for reliable GitHub edit URLs
@@ -94,7 +97,7 @@ When `mcp` is configured, `docsector build` generates:
94
97
 
95
98
  | File | Purpose |
96
99
  |---|---|
97
- | `dist/spa/mcp-pages.json` | Page index (title, path, type) for search |
100
+ | `dist/spa/mcp-pages.json` | Page index (title, path, book) for search |
98
101
  | `functions/mcp.js` | Cloudflare Pages Function implementing MCP |
99
102
  | `dist/spa/_routes.json` | Routes `/mcp` to the function |
100
103
  | `dist/spa/_headers` | CORS headers for MCP endpoint |
@@ -775,11 +778,13 @@ const langModules = import.meta.glob('./languages/*.hjson', { eager: true })
775
778
  const mdModules = import.meta.glob('../pages/**/*.md', { eager: true, query: '?raw', import: 'default' })
776
779
 
777
780
  import boot from 'pages/boot'
778
- import pages from 'pages'
781
+ import { books } from 'virtual:docsector-books'
779
782
 
780
- export default buildMessages({ langModules, mdModules, pages, boot, homePageOverride })
783
+ export default buildMessages({ langModules, mdModules, books, boot, homePageOverride })
781
784
  ```
782
785
 
786
+ > `books` is the preferred source because it preserves per-book registries and avoids path collisions when two books reuse the same route key.
787
+
783
788
  ### Language files
784
789
 
785
790
  Place HJSON locale files in `src/i18n/languages/`:
@@ -815,7 +820,10 @@ my-docs/
815
820
  ├── package.json
816
821
  ├── src/
817
822
  │ ├── pages/
818
- │ │ ├── index.js # Page registry (routes + metadata)
823
+ │ │ ├── manual.book.js # Manual tab metadata (icon, order, active/inactive colors)
824
+ │ │ ├── manual.index.js # Manual page registry (routes + metadata)
825
+ │ │ ├── guide.book.js # Guide tab metadata (icon, order, active/inactive colors)
826
+ │ │ ├── guide.index.js # Guide page registry (routes + metadata)
819
827
  │ │ ├── boot.js # Boot page data
820
828
  │ │ ├── guide/ # Guide pages (.md files)
821
829
  │ │ └── manual/ # Manual pages (.md files)
@@ -835,17 +843,54 @@ my-docs/
835
843
 
836
844
  ---
837
845
 
846
+ ## ⚠️ Migrating from 1.x to 2.0
847
+
848
+ - Split the legacy `src/pages/index.js` registry into per-book files such as `src/pages/manual.book.js`, `src/pages/manual.index.js`, `src/pages/guide.book.js`, and `src/pages/guide.index.js`.
849
+ - Update i18n wiring to import `books` from `virtual:docsector-books` and pass it to `buildMessages({ ... })`.
850
+ - Rename `config.type` to `config.book` in page definitions. Legacy fallback still works, but `config.book` is the supported API moving forward.
851
+
852
+ ---
853
+
854
+ ## 📚 Defining Books (Tabs)
855
+
856
+ Each documentation tab is defined by a `*.book.js` file paired with a matching `*.index.js` registry.
857
+
858
+ ```javascript
859
+ import { defineBook } from '@docsector/docsector-reader'
860
+
861
+ export default defineBook({
862
+ id: 'guide',
863
+ label: 'Guide',
864
+ icon: 'school',
865
+ order: 2,
866
+ color: {
867
+ active: 'white',
868
+ inactive: 'secondary'
869
+ }
870
+ })
871
+ ```
872
+
873
+ Notes:
874
+
875
+ - `color.active` and `color.inactive` control the tab text color for each state.
876
+ - Color values accept Quasar tokens (`secondary`, `red-6`), CSS variables (`--brand-color` or `var(--brand-color)`), and plain CSS colors (`white`, `#fff`, `rgb(...)`).
877
+ - Legacy `color: 'secondary'` still works, but the object form is the recommended API.
878
+ - Tabs are ordered by `order`.
879
+
880
+ ---
881
+
838
882
  ## 📄 Adding Pages
839
883
 
840
- 1️⃣ Register in `src/pages/index.js`:
884
+ 1️⃣ Register in `src/pages/manual.index.js` (or `src/pages/guide.index.js`):
841
885
 
842
886
  ```javascript
887
+ import { definePage } from '@docsector/docsector-reader'
888
+
843
889
  export default {
844
- '/manual/my-section/my-page': {
890
+ '/my-section/my-page': definePage({
845
891
  config: {
846
892
  icon: 'description',
847
893
  status: 'done', // 'done' | 'draft' | 'empty'
848
- type: 'manual', // 'guide' | 'manual'
849
894
  menu: {
850
895
  header: { label: '.my-section', icon: 'category' }
851
896
  },
@@ -855,10 +900,16 @@ export default {
855
900
  'en-US': { title: 'My Page' },
856
901
  'pt-BR': { title: 'Minha Página' }
857
902
  }
858
- }
903
+ })
859
904
  }
860
905
  ```
861
906
 
907
+ Notes:
908
+
909
+ - In `manual.index.js`, route keys are relative to the `manual` book (for example `'/my-section/my-page'` becomes `/manual/my-section/my-page/...`).
910
+ - You only need to set `config.book` when overriding the inferred book from the registry file.
911
+ - When `showcase` or `vs` are enabled, the subpage toolbar aligns with the content width on desktop and becomes a bottom action bar on mobile.
912
+
862
913
  2️⃣ Create Markdown files:
863
914
 
864
915
  ```
@@ -866,6 +917,34 @@ src/pages/manual/my-section/my-page.overview.en-US.md
866
917
  src/pages/manual/my-section/my-page.overview.pt-BR.md
867
918
  ```
868
919
 
920
+ ### Internal Links / Menu Shortcuts
921
+
922
+ Use `config.link.to` when an entry should appear in menus but redirect immediately to another internal page.
923
+
924
+ ```javascript
925
+ import { definePage } from '@docsector/docsector-reader'
926
+
927
+ export default {
928
+ '/getting-started': definePage({
929
+ config: {
930
+ link: {
931
+ to: '/guide/getting-started/overview/'
932
+ }
933
+ },
934
+ data: {
935
+ 'en-US': { title: 'Getting started' },
936
+ 'pt-BR': { title: 'Começando' }
937
+ }
938
+ })
939
+ }
940
+ ```
941
+
942
+ Notes:
943
+
944
+ - For shortcut pages, `link.to` and `data` are enough.
945
+ - `icon` and `status` automatically fall back to the destination page when omitted.
946
+ - Internal links redirect directly to the target route instead of rendering `overview` / `showcase` / `vs` locally.
947
+
869
948
  ### GitHub-Style Alert Example
870
949
 
871
950
  ```markdown
@@ -899,9 +978,12 @@ docsector help # Show help
899
978
 
900
979
  | Import path | Export | Description |
901
980
  |---|---|---|
981
+ | `@docsector/docsector-reader` | `createDocsector()` | Main helper for `docsector.config.js` objects |
982
+ | `@docsector/docsector-reader` | `defineBook()` | Define `*.book.js` tab metadata with active/inactive colors |
983
+ | `@docsector/docsector-reader` | `definePage()` | Define page registry entries, including internal shortcut pages |
902
984
  | `@docsector/docsector-reader/quasar-factory` | `createQuasarConfig()` | Config factory for consumer projects |
903
985
  | `@docsector/docsector-reader/quasar-factory` | `configure()` | No-op wrapper (avoids needing `quasar` dep) |
904
- | `@docsector/docsector-reader/i18n` | `buildMessages()` | Build i18n messages from globs + pages |
986
+ | `@docsector/docsector-reader/i18n` | `buildMessages()` | Build i18n messages from globs + book/page registries |
905
987
  | `@docsector/docsector-reader/i18n` | `filter()` | Filter i18n messages by locale |
906
988
 
907
989
  ---
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 = '1.7.1'
26
+ const VERSION = '2.0.1'
27
27
 
28
28
  const HELP = `
29
29
  Docsector Reader v${VERSION}
@@ -71,7 +71,7 @@ function getTemplatePackageJson (name) {
71
71
  serve: 'docsector serve'
72
72
  },
73
73
  dependencies: {
74
- '@docsector/docsector-reader': '^0.6.0',
74
+ '@docsector/docsector-reader': `^${VERSION}`,
75
75
  '@quasar/extras': '^1.16.12',
76
76
  'quasar': '^2.16.6',
77
77
  'vue': '^3.5.13',
@@ -273,7 +273,7 @@ const mdModules = import.meta.glob('../pages/**/*.md', { eager: true, query: '?r
273
273
 
274
274
  // @ Import pages
275
275
  import boot from 'pages/boot'
276
- import pages from 'pages'
276
+ import { allPages as pages } from 'virtual:docsector-books'
277
277
 
278
278
  export default buildMessages({ langModules, mdModules, pages, boot, homePageOverride })
279
279
  `
@@ -383,14 +383,24 @@ const TEMPLATE_I18N_HJSON = `\
383
383
  }
384
384
  `
385
385
 
386
+ const TEMPLATE_PAGES_GUIDE_BOOK = `\
387
+ export default {
388
+ id: 'guide',
389
+ label: 'Guide',
390
+ icon: 'school',
391
+ order: 1,
392
+ color: 'secondary'
393
+ }
394
+ `
395
+
386
396
  const TEMPLATE_PAGES_INDEX = `\
387
397
  /**
388
- * Pages Registry
398
+ * Guide Pages Registry
389
399
  *
390
- * Define your documentation pages here. Each key is a URL path,
391
- * and each value configures the page's type, icon, status, and titles.
400
+ * Define guide pages here. Each key is a URL path,
401
+ * and each value configures the page's book, icon, status, and titles.
392
402
  *
393
- * config.type: top-level route prefix — 'manual', 'guide', etc.
403
+ * config.book: top-level route prefix — 'guide', 'manual', etc.
394
404
  * config.status: 'done' | 'draft' | 'empty'
395
405
  * config.meta.description: string or localized object for SEO/social description
396
406
  * config.icon: Material Design icon name
@@ -410,7 +420,7 @@ export default {
410
420
  'en-US': 'Get started quickly with setup and project structure.'
411
421
  }
412
422
  },
413
- type: 'guide',
423
+ book: 'guide',
414
424
  menu: {
415
425
  header: {
416
426
  icon: 'school',
@@ -952,7 +962,8 @@ Here's an overview of the project files:
952
962
  | \`docsector.config.js\` | Branding, links, languages, and GitHub config |
953
963
  | \`quasar.config.js\` | Quasar/Vite build configuration (via factory) |
954
964
  | \`.markdownlint.json\` | Markdown lint rules (allows Docsector custom tags) |
955
- | \`src/pages/index.js\` | Page registry defines all documentation pages |
965
+ | \`src/pages/guide.book.js\` | Guide book definition (tab metadata) |
966
+ | \`src/pages/guide.index.js\` | Guide page registry (routes + metadata) |
956
967
  | \`src/pages/boot.js\` | Boot metadata for the home page |
957
968
  | \`src/pages/Homepage.en-US.md\` | Home page content in Markdown |
958
969
  | \`src/pages/404Page.vue\` | Not found page |
@@ -963,8 +974,9 @@ Here's an overview of the project files:
963
974
 
964
975
  ## Adding a Page
965
976
 
966
- 1. Register the page in \`src/pages/index.js\`
967
- 2. Create the Markdown file at \`src/pages/{type}/{path}.overview.{lang}.md\`
977
+ 1. Register the page in \`src/pages/guide.index.js\`
978
+ 2. Set \`config.book\` (for example: \`'guide'\`)
979
+ 3. Create the Markdown file at \`src/pages/{book}/{path}.overview.{lang}.md\`
968
980
  3. The page will automatically appear in the sidebar navigation
969
981
 
970
982
  ## Customization
@@ -1082,7 +1094,8 @@ function initProject (name) {
1082
1094
  ['src/css/app.sass', TEMPLATE_CSS_STUB],
1083
1095
  ['src/i18n/index.js', TEMPLATE_I18N_INDEX],
1084
1096
  ['src/i18n/languages/en-US.hjson', TEMPLATE_I18N_HJSON],
1085
- ['src/pages/index.js', TEMPLATE_PAGES_INDEX],
1097
+ ['src/pages/guide.book.js', TEMPLATE_PAGES_GUIDE_BOOK],
1098
+ ['src/pages/guide.index.js', TEMPLATE_PAGES_INDEX],
1086
1099
  ['src/pages/boot.js', TEMPLATE_PAGES_BOOT],
1087
1100
  ['src/pages/Homepage.en-US.md', TEMPLATE_HOMEPAGE_MD],
1088
1101
  ['src/pages/404Page.vue', TEMPLATE_404_PAGE],
@@ -1120,7 +1133,8 @@ function initProject (name) {
1120
1133
  console.log(' │ └── languages/')
1121
1134
  console.log(' │ └── en-US.hjson')
1122
1135
  console.log(' └── pages/')
1123
- console.log(' ├── index.js')
1136
+ console.log(' ├── guide.book.js')
1137
+ console.log(' ├── guide.index.js')
1124
1138
  console.log(' ├── boot.js')
1125
1139
  console.log(' ├── Homepage.en-US.md')
1126
1140
  console.log(' ├── 404Page.vue')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docsector/docsector-reader",
3
- "version": "1.7.1",
3
+ "version": "2.0.1",
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",
@@ -4,6 +4,7 @@ import { useStore } from 'vuex'
4
4
  import { useI18n } from "vue-i18n";
5
5
 
6
6
  import useNavigator from '../composables/useNavigator'
7
+ import { pageTitleI18nPath } from '../i18n/path'
7
8
 
8
9
  const props = defineProps({
9
10
  id: {
@@ -22,7 +23,7 @@ const heading = computed(() => {
22
23
 
23
24
  let h = ''
24
25
  if (base && absolute) {
25
- h = t(`_.${base}._`)
26
+ h = t(pageTitleI18nPath(base))
26
27
  }
27
28
 
28
29
  return h
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
2
+ import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
3
3
  import { useRoute, useRouter } from 'vue-router'
4
4
  import { useQuasar, scroll, openURL } from 'quasar'
5
5
  import { useI18n } from 'vue-i18n'
@@ -7,6 +7,8 @@ import { useI18n } from 'vue-i18n'
7
7
  import tags from '@docsector/tags'
8
8
  import DMenuItem from './DMenuItem.vue'
9
9
  import docsectorConfig from 'docsector.config.js'
10
+ import { allBooks } from 'virtual:docsector-books'
11
+ import { namespacedLabelI18nPath, routeSubpageSourceI18nPath } from '../i18n/path'
10
12
 
11
13
  const $q = useQuasar()
12
14
  const $route = useRoute()
@@ -29,6 +31,27 @@ const subpage = computed(() => {
29
31
  return child.substring(parent.length)
30
32
  })
31
33
 
34
+ const defaultBookId = computed(() => {
35
+ const sortedBooks = [...(allBooks || [])]
36
+ .filter(book => book && typeof book.id === 'string' && book.id.length > 0)
37
+ .sort((a, b) => {
38
+ const orderA = Number.isFinite(a.order) ? a.order : Number.MAX_SAFE_INTEGER
39
+ const orderB = Number.isFinite(b.order) ? b.order : Number.MAX_SAFE_INTEGER
40
+ return orderA - orderB
41
+ })
42
+
43
+ return sortedBooks[0]?.id || null
44
+ })
45
+
46
+ const currentBookId = computed(() => {
47
+ const routeBook = $route.matched?.[0]?.meta?.book ?? $route.meta?.book ?? null
48
+ if (routeBook && routeBook !== 'home') {
49
+ return routeBook
50
+ }
51
+
52
+ return defaultBookId.value
53
+ })
54
+
32
55
  const searchTerm = (term) => {
33
56
  if (term.length > 1) {
34
57
  term = term.toLowerCase()
@@ -76,7 +99,7 @@ const searchTermInI18nTexts = (route, term, locale) => {
76
99
  let source = null
77
100
  let found = false
78
101
  for (const subpage of subpages) {
79
- const path = `_${route.replace(/_$/, '').replace(/\//g, '.')}.${subpage}.source`
102
+ const path = routeSubpageSourceI18nPath(route, subpage)
80
103
  const msgExists = te(path, locale)
81
104
  if (msgExists) {
82
105
  source = tm(path, locale)
@@ -99,7 +122,8 @@ const clearSearchTerm = () => {
99
122
  const getMenuItemHeaderLabel = (meta) => {
100
123
  const label = meta.menu.header.label
101
124
  if (label[0] === '.') { // Node path
102
- const path = `_.${meta.type}${label}._`
125
+ const book = meta.book ?? meta.type ?? 'manual'
126
+ const path = namespacedLabelI18nPath(book, label)
103
127
  return t(path)
104
128
  }
105
129
  return label // String raw
@@ -156,38 +180,54 @@ onBeforeUnmount(() => {
156
180
  }
157
181
  })
158
182
 
159
- // # Events
160
- // Create
161
- const routes = $router.options.routes.slice(0, -2) // Delete last 2 routes
162
- const itemsArray = []
163
-
164
- let nodeBasepath = ''
165
- let nodeIndex = 0
166
- for (const [index, route] of routes.entries()) {
167
- const item = Object.freeze({
168
- path: route.path,
169
- meta: route.meta
183
+ const buildMenuItems = () => {
184
+ const routes = ($router.options.routes || []).slice(0, -2) // Delete last 2 routes
185
+ const activeBook = currentBookId.value
186
+
187
+ const filteredRoutes = routes.filter(route => {
188
+ const routeBook = route?.meta?.book ?? route?.meta?.type
189
+ if (!activeBook) return true
190
+ return routeBook === activeBook
170
191
  })
171
- // # Route
172
- const basepath = route.path.split('/')[2]
173
- const header = route.meta.menu.header
174
-
175
- if (header !== undefined && basepath !== nodeBasepath) {
176
- nodeBasepath = basepath
177
- nodeIndex = index
178
- itemsArray[index] = []
179
- } else if (header === undefined && basepath !== nodeBasepath) {
180
- nodeBasepath = ''
181
- }
182
192
 
183
- if (nodeBasepath !== '') {
184
- itemsArray[nodeIndex].push(item)
185
- } else {
186
- itemsArray.push(item)
193
+ const itemsArray = []
194
+
195
+ let nodeBasepath = ''
196
+ let nodeIndex = 0
197
+ for (const [index, route] of filteredRoutes.entries()) {
198
+ const item = Object.freeze({
199
+ path: route.path,
200
+ meta: route.meta
201
+ })
202
+
203
+ // # Route
204
+ const basepath = route.path.split('/')[2]
205
+ const header = route.meta?.menu?.header
206
+
207
+ if (header !== undefined && basepath !== nodeBasepath) {
208
+ nodeBasepath = basepath
209
+ nodeIndex = index
210
+ itemsArray[index] = []
211
+ } else if (header === undefined && basepath !== nodeBasepath) {
212
+ nodeBasepath = ''
213
+ }
214
+
215
+ if (nodeBasepath !== '') {
216
+ itemsArray[nodeIndex].push(item)
217
+ } else {
218
+ itemsArray.push(item)
219
+ }
187
220
  }
221
+
222
+ return Object.freeze(itemsArray.filter(item => item !== undefined))
223
+ }
224
+
225
+ const rebuildItems = () => {
226
+ items.value = buildMenuItems()
188
227
  }
189
228
 
190
- items.value = Object.freeze(itemsArray.filter(item => item !== undefined))
229
+ rebuildItems()
230
+ watch(currentBookId, rebuildItems)
191
231
  </script>
192
232
 
193
233
  <template>
@@ -4,6 +4,8 @@ import { useRoute } from 'vue-router'
4
4
  import { useQuasar } from 'quasar'
5
5
  import { useI18n } from 'vue-i18n'
6
6
 
7
+ import { namespacedLabelI18nPath, routeTitleI18nPath } from '../i18n/path'
8
+
7
9
  const props = defineProps({
8
10
  items: {
9
11
  type: Number,
@@ -36,13 +38,17 @@ const getMenuItemHeaderBackground = () => {
36
38
  }
37
39
 
38
40
  const getMenuItemLabel = (item, index) => {
39
- const path = `_${item.path.replace(/_$/, '').replace(/\//g, '.')}._`
40
- return t(path)
41
+ return t(routeTitleI18nPath(item.path))
41
42
  }
42
43
 
43
44
  const getMenuItemSubheader = (meta) => {
44
- const subheader = meta.menu.subheader
45
- const path = `_.${meta.type}${subheader}._`
45
+ const subheader = meta.menu?.subheader
46
+ if (!subheader) {
47
+ return ''
48
+ }
49
+
50
+ const book = meta.book ?? meta.type ?? 'manual'
51
+ const path = namespacedLabelI18nPath(book, subheader)
46
52
 
47
53
  return t(path)
48
54
  }
@@ -95,7 +101,7 @@ const isMenuItemActive = (path) => {
95
101
 
96
102
  <template>
97
103
  <!-- Menu Separator - Subheader -->
98
- <q-item-section v-if="subitem.meta.menu.subheader">
104
+ <q-item-section v-if="subitem.meta.menu?.subheader">
99
105
  <q-item-label class="label subheader" header>
100
106
  {{ getMenuItemSubheader(subitem.meta) }}
101
107
  </q-item-label>
@@ -123,7 +129,7 @@ const isMenuItemActive = (path) => {
123
129
  </q-item>
124
130
 
125
131
  <!-- Menu Separator -->
126
- <li v-if="subitem.meta.menu.separator" role="listitem">
132
+ <li v-if="subitem.meta.menu?.separator" role="listitem">
127
133
  <q-separator
128
134
  :class="'separator' + (subitem.meta.menu.separator === true ? '' : subitem.meta.menu.separator)"
129
135
  role="separator"