@docsector/docsector-reader 2.0.7 → 2.2.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.
Files changed (32) hide show
  1. package/README.md +19 -7
  2. package/bin/docsector.js +28 -7
  3. package/docsector.config.js +13 -1
  4. package/package.json +1 -1
  5. package/src/components/DMenu.vue +241 -12
  6. package/src/components/DMenuItem.vue +25 -2
  7. package/src/components/DPageBar.vue +35 -5
  8. package/src/components/DPageMeta.vue +40 -26
  9. package/src/i18n/helpers.js +84 -18
  10. package/src/i18n/index.js +5 -3
  11. package/src/i18n/languages/en-US.hjson +14 -0
  12. package/src/i18n/languages/pt-BR.hjson +14 -0
  13. package/src/i18n/path.js +15 -2
  14. package/src/index.js +2 -2
  15. package/src/layouts/DefaultLayout.vue +16 -2
  16. package/src/pages/.old/v0.x/guide/getting-started.overview.en-US.md +7 -0
  17. package/src/pages/.old/v0.x/guide/getting-started.overview.pt-BR.md +7 -0
  18. package/src/pages/.old/v0.x/guide.book.js +12 -0
  19. package/src/pages/.old/v0.x/guide.index.js +28 -0
  20. package/src/pages/guide/configuration.overview.en-US.md +13 -2
  21. package/src/pages/guide/configuration.overview.pt-BR.md +13 -2
  22. package/src/pages/guide/pages-and-routing.overview.en-US.md +6 -2
  23. package/src/pages/guide/pages-and-routing.overview.pt-BR.md +6 -2
  24. package/src/pages/guide.index.js +3 -1
  25. package/src/pages/manual/components/d-menu.overview.en-US.md +6 -2
  26. package/src/pages/manual/components/d-menu.overview.pt-BR.md +6 -2
  27. package/src/pages/manual/components/d-page-meta.overview.en-US.md +1 -0
  28. package/src/pages/manual/components/d-page-meta.overview.pt-BR.md +1 -0
  29. package/src/pages/manual.index.js +2 -1
  30. package/src/quasar.factory.js +648 -91
  31. package/src/router/routes.js +129 -95
  32. package/src/store/App.js +15 -5
package/README.md CHANGED
@@ -54,7 +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
- - 🏷️ **Status Badges** — Mark pages as `done`, `draft`, or `empty` with visual indicators
57
+ - 🏷️ **Status Badges** — Mark pages as `done`, `draft`, `empty`, or `new` with visual indicators
58
58
  - ✏️ **Edit on GitHub** — Direct links to edit pages on your repository
59
59
  - 🧭 **Robust Edit Link Mapping** — Normalizes route paths (including trailing slashes) into `page.subpage.locale.md` source files for reliable GitHub edit URLs
60
60
  - 📅 **Last Updated Date** — Automatic per-page "last updated" date from git commit history, locale-formatted
@@ -68,6 +68,8 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
68
68
  - 🧬 **Scaffolded Homepage Override Wiring** — New consumer projects automatically wire `virtual:docsector-homepage-override` into i18n message building
69
69
  - 🧭 **Quick Links Custom Element** — Use `<d-quick-links>` and `<d-quick-link>` in Markdown to render rich home navigation cards
70
70
  - 🗂️ **API Catalog Well-Known** — Auto-generates `/.well-known/api-catalog` as Linkset JSON for machine-readable API discovery
71
+ - 🗃️ **Multi-Version History** — Archive older major versions under `src/pages/.old/<version>/` and expose them at prefixed routes (e.g. `/v0.x/guide/...`) while keeping the current docs at unprefixed routes
72
+ - 🏷️ **Version Selector Badges** — Every version in the sidebar selector displays a color-coded badge: green for released, orange for draft, red for deprecated; fully customizable via `badge: { label, color, textColor }`
71
73
  - ⚙️ **Single Config File** — Customize branding, links, and languages via `docsector.config.js`
72
74
 
73
75
  ---
@@ -675,7 +677,10 @@ export default {
675
677
  logo: '/images/logo/my-logo.png',
676
678
  name: 'My Project',
677
679
  version: 'v1.0.0',
678
- versions: ['v1.0.0', 'v0.9.0']
680
+ versions: [
681
+ { id: 'v1.0.0', current: true, released: false },
682
+ { id: 'v0.9.0', released: true, status: 'deprecated' }
683
+ ]
679
684
  },
680
685
 
681
686
  links: {
@@ -754,6 +759,10 @@ export default {
754
759
  }
755
760
  ```
756
761
 
762
+ The current version keeps the normal unprefixed routes such as `/guide/getting-started/overview/`. Archived major versions can be placed under `src/pages/.old/<version>/` with the same book/index/Markdown layout, and are exposed with a URL prefix such as `/v0.x/guide/getting-started/overview/`.
763
+
764
+ Every version shows a release badge in the selector. Released versions default to `released`; versions with `released: false` or `status: 'draft'` default to `draft`; versions with `status: 'deprecated'` or `deprecated: true` default to `deprecated` in red. Use `badge: { label, color, textColor }` when you need custom badge copy or colors.
765
+
757
766
  ### MCP (optional)
758
767
 
759
768
  ```javascript
@@ -777,15 +786,17 @@ import { buildMessages } from '@docsector/docsector-reader/i18n'
777
786
  import homePageOverride from 'virtual:docsector-homepage-override'
778
787
 
779
788
  const langModules = import.meta.glob('./languages/*.hjson', { eager: true })
780
- const mdModules = import.meta.glob('../pages/**/*.md', { eager: true, query: '?raw', import: 'default' })
789
+ const currentMdModules = import.meta.glob('../pages/**/*.md', { eager: true, query: '?raw', import: 'default' })
790
+ const oldMdModules = import.meta.glob('../pages/.old/**/*.md', { eager: true, query: '?raw', import: 'default' })
791
+ const mdModules = { ...currentMdModules, ...oldMdModules }
781
792
 
782
793
  import boot from 'pages/boot'
783
- import { books } from 'virtual:docsector-books'
794
+ import { books, pageEntries } from 'virtual:docsector-books'
784
795
 
785
- export default buildMessages({ langModules, mdModules, books, boot, homePageOverride })
796
+ export default buildMessages({ langModules, mdModules, books, pageEntries, boot, homePageOverride })
786
797
  ```
787
798
 
788
- > `books` is the preferred source because it preserves per-book registries and avoids path collisions when two books reuse the same route key.
799
+ > `pageEntries` is the preferred source because it preserves per-book and per-version registries and avoids path collisions when books or archived versions reuse the same route key.
789
800
 
790
801
  ### Language files
791
802
 
@@ -892,7 +903,8 @@ export default {
892
903
  '/my-section/my-page': definePage({
893
904
  config: {
894
905
  icon: 'description',
895
- status: 'done', // 'done' | 'draft' | 'empty'
906
+ status: 'new', // 'done' | 'draft' | 'empty' | 'new'
907
+ version: 'v2.1.0', // Optional: shown as "New in" / "Novo em"
896
908
  menu: {
897
909
  header: { label: '.my-section', icon: 'category' }
898
910
  },
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.7'
26
+ const VERSION = '2.2.0'
27
27
 
28
28
  const HELP = `
29
29
  Docsector Reader v${VERSION}
@@ -105,7 +105,10 @@ export default {
105
105
  branding: {
106
106
  logo: '/images/logo.png',
107
107
  name: 'My Documentation',
108
- version: 'v0.1.0'
108
+ version: 'v0.1.0',
109
+ versions: [
110
+ { id: 'v0.1.0', current: true, released: false }
111
+ ]
109
112
  },
110
113
 
111
114
  // @ Links
@@ -269,13 +272,15 @@ import homePageOverride from 'virtual:docsector-homepage-override'
269
272
  // @ Import language HJSON files (Vite-compatible eager import)
270
273
  const langModules = import.meta.glob('./languages/*.hjson', { eager: true })
271
274
  // @ Import markdown files (Vite-compatible eager import as raw strings)
272
- const mdModules = import.meta.glob('../pages/**/*.md', { eager: true, query: '?raw', import: 'default' })
275
+ const currentMdModules = import.meta.glob('../pages/**/*.md', { eager: true, query: '?raw', import: 'default' })
276
+ const oldMdModules = import.meta.glob('../pages/.old/**/*.md', { eager: true, query: '?raw', import: 'default' })
277
+ const mdModules = { ...currentMdModules, ...oldMdModules }
273
278
 
274
279
  // @ Import pages
275
280
  import boot from 'pages/boot'
276
- import { allPages as pages } from 'virtual:docsector-books'
281
+ import { books, pageEntries } from 'virtual:docsector-books'
277
282
 
278
- export default buildMessages({ langModules, mdModules, pages, boot, homePageOverride })
283
+ export default buildMessages({ langModules, mdModules, books, pageEntries, boot, homePageOverride })
279
284
  `
280
285
 
281
286
  const TEMPLATE_I18N_HJSON = `\
@@ -310,6 +315,7 @@ const TEMPLATE_I18N_HJSON = `\
310
315
  prev: 'Previous page'
311
316
  next: 'Next page'
312
317
  }
318
+ newVersion: 'New in'
313
319
  }
314
320
 
315
321
  // @ Menu
@@ -331,6 +337,19 @@ const TEMPLATE_I18N_HJSON = `\
331
337
  _: 'draft'
332
338
  tooltip: 'This page is under construction.'
333
339
  }
340
+ new: {
341
+ _: 'new'
342
+ tooltip: 'This page is new.'
343
+ tooltipVersion: 'New in {version}'
344
+ }
345
+ }
346
+
347
+ version: {
348
+ status: {
349
+ released: 'released'
350
+ draft: 'draft'
351
+ deprecated: 'deprecated'
352
+ }
334
353
  }
335
354
 
336
355
  settings: 'Settings'
@@ -401,7 +420,8 @@ const TEMPLATE_PAGES_INDEX = `\
401
420
  * and each value configures the page's book, icon, status, and titles.
402
421
  *
403
422
  * config.book: top-level route prefix — 'guide', 'manual', etc.
404
- * config.status: 'done' | 'draft' | 'empty'
423
+ * config.status: 'done' | 'draft' | 'empty' | 'new'
424
+ * config.version: optional version where this page was introduced (e.g. 'v2.1.0')
405
425
  * config.meta.description: string or localized object for SEO/social description
406
426
  * config.icon: Material Design icon name
407
427
  * config.menu: menu display options (header, subheader, separator)
@@ -414,7 +434,8 @@ export default {
414
434
  '/getting-started': {
415
435
  config: {
416
436
  icon: 'flag',
417
- status: 'done',
437
+ status: 'new',
438
+ version: 'v0.1.0',
418
439
  meta: {
419
440
  description: {
420
441
  'en-US': 'Get started quickly with setup and project structure.'
@@ -15,7 +15,19 @@ export default {
15
15
  // Project name displayed in the sidebar
16
16
  name: 'Docsector Reader',
17
17
  // Version label displayed next to the name
18
- version: 'v' + pkg.version
18
+ version: 'v' + pkg.version,
19
+ versions: [
20
+ {
21
+ id: 'v' + pkg.version,
22
+ current: true,
23
+ released: false
24
+ },
25
+ {
26
+ id: 'v0.x',
27
+ released: true,
28
+ status: 'deprecated'
29
+ }
30
+ ]
19
31
  },
20
32
 
21
33
  // @ Links
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docsector/docsector-reader",
3
- "version": "2.0.7",
3
+ "version": "2.2.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",
@@ -7,7 +7,7 @@ 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'
10
+ import { allBooks, booksByVersion, versions } from 'virtual:docsector-books'
11
11
  import { namespacedLabelI18nPath, routeSubpageSourceI18nPath } from '../i18n/path'
12
12
 
13
13
  const $q = useQuasar()
@@ -20,19 +20,110 @@ const links = docsectorConfig.links || {}
20
20
 
21
21
  const term = ref(null)
22
22
  const founds = ref(false)
23
- const version = ref(branding.version || 'v0.x')
24
- const versions = ref(branding.versions || ['v0.x'])
25
23
  const items = ref([])
26
24
  const scrolling = ref(null)
27
25
 
28
26
  const subpage = computed(() => {
29
27
  const parent = $route.matched[0]?.path
30
28
  const child = $route.matched[1]?.path
29
+ if (!parent || !child) return '/overview'
31
30
  return child.substring(parent.length)
32
31
  })
33
32
 
33
+ const activeVersionId = computed(() => {
34
+ return $route.matched?.[0]?.meta?.version ?? versions?.[0]?.id ?? ''
35
+ })
36
+
37
+ const activeBooks = computed(() => {
38
+ if (activeVersionId.value && booksByVersion?.[activeVersionId.value]?.allBooks) {
39
+ return booksByVersion[activeVersionId.value].allBooks
40
+ }
41
+
42
+ return allBooks || []
43
+ })
44
+
45
+ const draftReleaseStatuses = new Set(['draft', 'unreleased', 'preview', 'next'])
46
+
47
+ const versionStatusLabel = (label, releaseStatus) => {
48
+ const normalizedStatus = String(releaseStatus || '').toLowerCase()
49
+ const normalizedLabel = String(label || normalizedStatus).toLowerCase()
50
+ const statusKey = normalizedStatus ? `menu.version.status.${normalizedStatus}` : null
51
+ const labelKey = normalizedLabel ? `menu.version.status.${normalizedLabel}` : null
52
+
53
+ if (statusKey && (!label || normalizedLabel === normalizedStatus) && te(statusKey)) {
54
+ return t(statusKey)
55
+ }
56
+
57
+ if (labelKey && te(labelKey)) {
58
+ return t(labelKey)
59
+ }
60
+
61
+ return label || releaseStatus
62
+ }
63
+
64
+ const normalizeVersionBadge = (item) => {
65
+ const configuredStatus = item.deprecated === true
66
+ ? 'deprecated'
67
+ : (item.releaseStatus || item.status)
68
+ const explicitlyReleased = item.released !== undefined ? item.released !== false : null
69
+ const released = configuredStatus === 'deprecated'
70
+ ? true
71
+ : (explicitlyReleased ?? !draftReleaseStatuses.has(String(configuredStatus || '').toLowerCase()))
72
+ const releaseStatus = configuredStatus || (released ? 'released' : 'draft')
73
+ const rawBadge = item.badge ?? item.releaseBadge
74
+ const deprecated = releaseStatus === 'deprecated'
75
+ const defaultColor = deprecated ? 'negative' : (released ? 'positive' : 'warning')
76
+ const defaultTextColor = (deprecated || released) ? 'white' : 'dark'
77
+
78
+ if (rawBadge === false || rawBadge === null) {
79
+ return { label: versionStatusLabel(releaseStatus, releaseStatus), color: defaultColor, textColor: defaultTextColor }
80
+ }
81
+
82
+ if (typeof rawBadge === 'string') {
83
+ return { label: versionStatusLabel(rawBadge, releaseStatus), color: defaultColor, textColor: defaultTextColor }
84
+ }
85
+
86
+ if (typeof rawBadge === 'object' && rawBadge !== null) {
87
+ const label = rawBadge.label || rawBadge.text || releaseStatus
88
+ if (!label) {
89
+ return null
90
+ }
91
+
92
+ return {
93
+ ...rawBadge,
94
+ label: versionStatusLabel(label, releaseStatus),
95
+ color: rawBadge.color || defaultColor,
96
+ textColor: rawBadge.textColor || defaultTextColor
97
+ }
98
+ }
99
+
100
+ return { label: versionStatusLabel(releaseStatus, releaseStatus), color: defaultColor, textColor: defaultTextColor }
101
+ }
102
+
103
+ const versionOptions = computed(() => {
104
+ return (versions || [])
105
+ .filter(item => item && item.id)
106
+ .map(item => ({
107
+ label: item.label || item.id,
108
+ value: item.id,
109
+ badge: normalizeVersionBadge(item),
110
+ released: item.released !== false,
111
+ deprecated: item.deprecated === true || item.releaseStatus === 'deprecated' || item.status === 'deprecated',
112
+ releaseStatus: item.releaseStatus || item.status || (item.released === false ? 'draft' : 'released')
113
+ }))
114
+ })
115
+
116
+ const activeVersionOption = computed(() => {
117
+ return versionOptions.value.find(item => item.value === activeVersionId.value) || null
118
+ })
119
+
120
+ const version = computed({
121
+ get: () => activeVersionId.value,
122
+ set: (versionId) => onVersionChange(versionId)
123
+ })
124
+
34
125
  const defaultBookId = computed(() => {
35
- const sortedBooks = [...(allBooks || [])]
126
+ const sortedBooks = [...activeBooks.value]
36
127
  .filter(book => book && typeof book.id === 'string' && book.id.length > 0)
37
128
  .sort((a, b) => {
38
129
  const orderA = Number.isFinite(a.order) ? a.order : Number.MAX_SAFE_INTEGER
@@ -52,13 +143,97 @@ const currentBookId = computed(() => {
52
143
  return defaultBookId.value
53
144
  })
54
145
 
146
+ const normalizeRoutePath = (path) => {
147
+ const normalized = String(path || '').trim()
148
+ if (normalized === '' || normalized === '/') {
149
+ return '/'
150
+ }
151
+
152
+ const sanitized = normalized.replace(/\/+$/, '')
153
+ return sanitized === '' ? '/' : sanitized
154
+ }
155
+
156
+ const getTopRoutes = () => {
157
+ return ($router.options.routes || []).slice(0, -2)
158
+ }
159
+
160
+ const routeHasSubpage = (route, subpageName) => {
161
+ return (route.children || []).some(child => child.path === subpageName)
162
+ }
163
+
164
+ const routeToSubpagePath = (route, subpageName) => {
165
+ return `${route.path.replace(/\/$/, '')}/${subpageName}/`
166
+ }
167
+
168
+ const getCurrentSubpageName = () => {
169
+ return String(subpage.value || '/overview').replace(/^\/+|\/+$/g, '') || 'overview'
170
+ }
171
+
172
+ const getFirstRoutePathByVersion = (versionId, preferredBook = null) => {
173
+ const routes = getTopRoutes()
174
+
175
+ for (const preferBook of [preferredBook, null]) {
176
+ for (const route of routes) {
177
+ if (route?.meta?.version !== versionId) continue
178
+ if (preferBook && (route.meta?.book ?? route.meta?.type) !== preferBook) continue
179
+ if (!routeHasSubpage(route, 'overview')) continue
180
+
181
+ const hasInternalLink = typeof route.meta?.link?.to === 'string' && route.meta.link.to.trim().length > 0
182
+ if (hasInternalLink) continue
183
+
184
+ return routeToSubpagePath(route, 'overview')
185
+ }
186
+ }
187
+
188
+ return '/'
189
+ }
190
+
191
+ const getEquivalentRoutePath = (versionId) => {
192
+ const routeMeta = $route.matched?.[0]?.meta || {}
193
+ const book = routeMeta.book ?? routeMeta.type ?? currentBookId.value
194
+ const pagePath = routeMeta.pagePath
195
+ const subpageName = getCurrentSubpageName()
196
+
197
+ if (book && typeof pagePath === 'string') {
198
+ const equivalentRoute = getTopRoutes().find(route => {
199
+ return route?.meta?.version === versionId &&
200
+ (route.meta?.book ?? route.meta?.type) === book &&
201
+ route.meta?.pagePath === pagePath &&
202
+ routeHasSubpage(route, subpageName)
203
+ })
204
+
205
+ if (equivalentRoute) {
206
+ return routeToSubpagePath(equivalentRoute, subpageName)
207
+ }
208
+ }
209
+
210
+ return getFirstRoutePathByVersion(versionId, book)
211
+ }
212
+
213
+ function onVersionChange (versionId) {
214
+ if (!versionId || versionId === activeVersionId.value) return
215
+
216
+ const targetVersion = (versions || []).find(item => item.id === versionId)
217
+ if (!targetVersion) return
218
+
219
+ if (typeof targetVersion.url === 'string' && targetVersion.url.trim().length > 0) {
220
+ openURL(targetVersion.url)
221
+ return
222
+ }
223
+
224
+ const targetPath = getEquivalentRoutePath(versionId)
225
+ if (normalizeRoutePath($route.path) !== normalizeRoutePath(targetPath)) {
226
+ $router.push(targetPath)
227
+ }
228
+ }
229
+
55
230
  const searchTerm = (term) => {
56
231
  if (term.length > 1) {
57
232
  term = term.toLowerCase()
58
233
  const locale = $q.localStorage.getItem('setting.language')
59
234
  founds.value = []
60
235
 
61
- for (const [index, group] of items.value.entries()) {
236
+ for (const group of items.value) {
62
237
  searchTermIterate(group, term, locale)
63
238
  }
64
239
  } else {
@@ -74,13 +249,14 @@ const searchTermIterate = (items, term, locale) => {
74
249
  } else if (typeof items === 'object') {
75
250
  const item = items
76
251
  const path = item.path
252
+ const tagPath = item.meta?.unversionedPath || path
77
253
  founds.value[path] = false
78
254
 
79
255
  // @ search in i18n/tags.hjson
80
256
  if (tags[locale] && Object.keys(tags[locale]).length > 0) {
81
- founds.value[path] = tags[locale][path]?.indexOf(term) !== -1
257
+ founds.value[path] = tags[locale][tagPath]?.indexOf(term) !== -1
82
258
  if (founds.value[path] === false && locale !== 'en-US') {
83
- founds.value[path] = tags['en-US'][path]?.indexOf(term) !== -1
259
+ founds.value[path] = tags['en-US'][tagPath]?.indexOf(term) !== -1
84
260
  }
85
261
  }
86
262
 
@@ -181,11 +357,13 @@ onBeforeUnmount(() => {
181
357
  })
182
358
 
183
359
  const buildMenuItems = () => {
184
- const routes = ($router.options.routes || []).slice(0, -2) // Delete last 2 routes
360
+ const routes = getTopRoutes()
185
361
  const activeBook = currentBookId.value
362
+ const activeVersion = activeVersionId.value
186
363
 
187
364
  const filteredRoutes = routes.filter(route => {
188
365
  const routeBook = route?.meta?.book ?? route?.meta?.type
366
+ if (activeVersion && route?.meta?.version !== activeVersion) return false
189
367
  if (!activeBook) return true
190
368
  return routeBook === activeBook
191
369
  })
@@ -201,7 +379,7 @@ const buildMenuItems = () => {
201
379
  })
202
380
 
203
381
  // # Route
204
- const basepath = route.path.split('/')[2]
382
+ const basepath = route.meta?.menuGroupPath || route.path.split('/')[2]
205
383
  const header = route.meta?.menu?.header
206
384
 
207
385
  if (header !== undefined && basepath !== nodeBasepath) {
@@ -227,7 +405,7 @@ const rebuildItems = () => {
227
405
  }
228
406
 
229
407
  rebuildItems()
230
- watch(currentBookId, rebuildItems)
408
+ watch([currentBookId, activeVersionId], rebuildItems)
231
409
  </script>
232
410
 
233
411
  <template>
@@ -254,10 +432,44 @@ watch(currentBookId, rebuildItems)
254
432
  <div class="text-weight-medium">{{ branding.name || 'Docsector' }}</div>
255
433
  <div class="text-caption q-pt-xs">{{ t('system.documentation') }}</div>
256
434
  <q-select class="q-mr-md"
257
- v-model="version" :options="versions"
435
+ v-model="version" :options="versionOptions"
436
+ emit-value map-options
258
437
  dense options-dense
259
438
  behavior="menu"
260
- />
439
+ >
440
+ <template v-slot:selected>
441
+ <div v-if="activeVersionOption" class="version-select-option">
442
+ <span class="version-select-label">{{ activeVersionOption.label }}</span>
443
+ <q-badge
444
+ v-if="activeVersionOption.badge"
445
+ class="version-select-badge"
446
+ :color="activeVersionOption.badge.color || 'warning'"
447
+ :text-color="activeVersionOption.badge.textColor || 'dark'"
448
+ :outline="activeVersionOption.badge.outline === true"
449
+ >
450
+ {{ activeVersionOption.badge.label }}
451
+ </q-badge>
452
+ </div>
453
+ </template>
454
+ <template v-slot:option="scope">
455
+ <q-item v-bind="scope.itemProps">
456
+ <q-item-section>
457
+ <div class="version-select-option">
458
+ <span class="version-select-label">{{ scope.opt.label }}</span>
459
+ <q-badge
460
+ v-if="scope.opt.badge"
461
+ class="version-select-badge"
462
+ :color="scope.opt.badge.color || 'warning'"
463
+ :text-color="scope.opt.badge.textColor || 'dark'"
464
+ :outline="scope.opt.badge.outline === true"
465
+ >
466
+ {{ scope.opt.badge.label }}
467
+ </q-badge>
468
+ </div>
469
+ </q-item-section>
470
+ </q-item>
471
+ </template>
472
+ </q-select>
261
473
  </div>
262
474
  </div>
263
475
 
@@ -470,6 +682,23 @@ body.body--light
470
682
  width: 30px
471
683
  height: 3px
472
684
 
685
+ .version-select-option
686
+ display: flex
687
+ align-items: center
688
+ gap: 6px
689
+ min-width: 0
690
+ max-width: 100%
691
+
692
+ .version-select-label
693
+ overflow: hidden
694
+ text-overflow: ellipsis
695
+ white-space: nowrap
696
+
697
+ .version-select-badge
698
+ flex: 0 0 auto
699
+ font-size: 10px
700
+ line-height: 1
701
+
473
702
  // Search
474
703
  label[for="search"]
475
704
  z-index: 2
@@ -53,6 +53,10 @@ const getMenuItemSubheader = (meta = {}) => {
53
53
  }
54
54
 
55
55
  const getPageStatusText = (status) => {
56
+ if (status === 'new') {
57
+ return t('menu.status.new._')
58
+ }
59
+
56
60
  if (status === 'draft') {
57
61
  return t('menu.status.draft._')
58
62
  } else {
@@ -61,6 +65,10 @@ const getPageStatusText = (status) => {
61
65
  }
62
66
 
63
67
  const getPageStatusTextColor = (status) => {
68
+ if (status === 'new') {
69
+ return null
70
+ }
71
+
64
72
  if (status === 'draft') {
65
73
  return 'dark'
66
74
  } else {
@@ -68,7 +76,15 @@ const getPageStatusTextColor = (status) => {
68
76
  }
69
77
  }
70
78
 
79
+ const getPageStatusStyle = (status) => {
80
+ return status === 'new' ? { color: '#151515' } : null
81
+ }
82
+
71
83
  const getPageStatusColor = (status) => {
84
+ if (status === 'new') {
85
+ return 'positive'
86
+ }
87
+
72
88
  if (status === 'draft') {
73
89
  return 'orange'
74
90
  } else {
@@ -76,7 +92,13 @@ const getPageStatusColor = (status) => {
76
92
  }
77
93
  }
78
94
 
79
- const getPageStatusTooltip = (status) => {
95
+ const getPageStatusTooltip = (status, pageVersion) => {
96
+ if (status === 'new') {
97
+ return pageVersion
98
+ ? t('menu.status.new.tooltipVersion', { version: pageVersion })
99
+ : t('menu.status.new.tooltip')
100
+ }
101
+
80
102
  if (status === 'draft') {
81
103
  return t('menu.status.draft.tooltip')
82
104
  } else {
@@ -148,8 +170,9 @@ const onMenuItemClick = (event, path, currentSubpage) => {
148
170
  :text-color="getPageStatusTextColor(subitem.meta.status)"
149
171
  :color="getPageStatusColor(subitem.meta.status)"
150
172
  :label="getPageStatusText(subitem.meta.status)"
173
+ :style="getPageStatusStyle(subitem.meta.status)"
151
174
  />
152
- <q-tooltip :hide-delay="3">{{ getPageStatusTooltip(subitem.meta.status) }}</q-tooltip>
175
+ <q-tooltip :hide-delay="3">{{ getPageStatusTooltip(subitem.meta.status, subitem.meta.pageVersion) }}</q-tooltip>
153
176
  </q-item-section>
154
177
  </q-item>
155
178
 
@@ -37,6 +37,11 @@ const subpage = computed(() => {
37
37
  })
38
38
 
39
39
  const fileKey = computed(() => {
40
+ const sourcePathBase = route.matched?.[0]?.meta?.sourcePathBase
41
+ if (typeof sourcePathBase === 'string' && sourcePathBase.length > 0) {
42
+ return `${sourcePathBase}.${subpage.value}.${locale.value}.md`
43
+ }
44
+
40
45
  const base = store.state.page.base
41
46
  if (!base) return ''
42
47
  return `${base}.${subpage.value}.${locale.value}.md`
@@ -56,6 +61,11 @@ const formattedDate = computed(() => {
56
61
  }).format(date)
57
62
  })
58
63
 
64
+ const pageVersion = computed(() => {
65
+ const value = route.meta?.pageVersion
66
+ return typeof value === 'string' ? value.trim() : ''
67
+ })
68
+
59
69
  const rawMarkdown = computed(() => {
60
70
  const absolute = store.state.i18n.absolute
61
71
  if (!absolute) return ''
@@ -187,10 +197,14 @@ const copyPage = () => {
187
197
 
188
198
  <template>
189
199
  <div class="d-page-bar">
190
- <span v-if="formattedDate" class="d-page-bar__date">
191
- {{ t('page.lastUpdated') }}: <br class="d-page-bar__date-break"> {{ formattedDate }}
192
- </span>
193
- <span v-else class="d-page-bar__date"></span>
200
+ <div class="d-page-bar__meta">
201
+ <span v-if="formattedDate" class="d-page-bar__date">
202
+ {{ t('page.lastUpdated') }}: <br class="d-page-bar__date-break"> {{ formattedDate }}
203
+ </span>
204
+ <span v-if="pageVersion" class="d-page-bar__new-in">
205
+ {{ t('page.newVersion') }}: {{ pageVersion }}
206
+ </span>
207
+ </div>
194
208
 
195
209
  <q-btn-dropdown
196
210
  class="d-page-bar__actions"
@@ -338,6 +352,18 @@ const copyPage = () => {
338
352
  font-size: 0.8rem
339
353
  opacity: 0.6
340
354
 
355
+ &__meta
356
+ display: flex
357
+ flex-direction: column
358
+ min-height: 1.5rem
359
+
360
+ &__new-in
361
+ font-size: 0.8rem
362
+ opacity: 0.6
363
+ margin-top: 8px
364
+ padding-top: 8px
365
+ border-top: 1px solid rgba(0, 0, 0, 0.12)
366
+
341
367
  &__date-break
342
368
  display: none
343
369
 
@@ -345,9 +371,13 @@ const copyPage = () => {
345
371
  font-size: 0.75rem
346
372
 
347
373
  body.body--dark
348
- .d-page-bar__date
374
+ .d-page-bar__date,
375
+ .d-page-bar__new-in
349
376
  color: rgba(255, 255, 255, 0.7)
350
377
 
378
+ .d-page-bar__new-in
379
+ border-top-color: rgba(255, 255, 255, 0.18)
380
+
351
381
  @media (max-width: 376px)
352
382
  .d-page-bar__date-break
353
383
  display: block