@ampernic/vitepress-theme-alt-docs 0.1.23 → 0.1.24

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.
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { onKeyStroke, useScrollLock } from '@vueuse/core'
3
3
  import { useRouter } from 'vitepress'
4
- import { nextTick, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
4
+ import { computed, nextTick, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
5
5
 
6
6
  // ── Dropdown ──────────────────────────────────────────────────────────────────
7
7
 
@@ -175,17 +175,65 @@ interface ResultData {
175
175
  excerpt: string
176
176
  }
177
177
 
178
+ interface ResultStub { data: () => Promise<ResultData> }
179
+
180
+ const PAGE_SIZE = 10
181
+
178
182
  const query = ref('')
179
183
  const selectedIndex = ref(0)
180
184
  const results = ref<ResultData[]>([])
181
185
  const searching = ref(false)
182
186
 
187
+ // All relevance-ranked hits (lazy stubs); the visible page is loaded on
188
+ // demand so results are never truncated.
189
+ const stubs = shallowRef<ResultStub[]>([])
190
+ const total = ref(0)
191
+ const page = ref(1)
192
+ const pageCount = computed(() => Math.max(1, Math.ceil(total.value / PAGE_SIZE)))
193
+ const pageItems = computed<(number | '…')[]>(() => {
194
+ const n = pageCount.value
195
+ const c = page.value
196
+ if (n <= 7) return Array.from({ length: n }, (_, i) => i + 1)
197
+ const items: (number | '…')[] = [1]
198
+ const lo = Math.max(2, c - 1)
199
+ const hi = Math.min(n - 1, c + 1)
200
+ if (lo > 2) items.push('…')
201
+ for (let i = lo; i <= hi; i++) items.push(i)
202
+ if (hi < n - 1) items.push('…')
203
+ items.push(n)
204
+ return items
205
+ })
206
+
183
207
  let searchTimer: ReturnType<typeof setTimeout> | null = null
208
+ let loadToken = 0
209
+
210
+ async function loadPage() {
211
+ const token = ++loadToken
212
+ const start = (page.value - 1) * PAGE_SIZE
213
+ const data = await Promise.all(
214
+ stubs.value.slice(start, start + PAGE_SIZE).map((r) => r.data()),
215
+ )
216
+ if (token !== loadToken) return
217
+ results.value = data
218
+ selectedIndex.value = 0
219
+ }
220
+
221
+ function goPage(p: number) {
222
+ const next = Math.min(Math.max(1, p), pageCount.value)
223
+ if (next !== page.value) page.value = next
224
+ }
225
+ function nextPage() { goPage(page.value + 1) }
226
+ function prevPage() { goPage(page.value - 1) }
227
+
228
+ watch(page, () => { if (stubs.value.length) loadPage() })
184
229
 
185
230
  watch([query, selectedDistro, selectedVersion], () => {
186
231
  selectedIndex.value = 0
232
+ page.value = 1
187
233
  if (searchTimer) clearTimeout(searchTimer)
188
234
  if (!query.value.trim() || !pagefind.value) {
235
+ stubs.value = []
236
+ total.value = 0
189
237
  results.value = []
190
238
  searching.value = false
191
239
  return
@@ -196,9 +244,11 @@ watch([query, selectedDistro, selectedVersion], () => {
196
244
  if (selectedDistro.value) f.distro = selectedDistro.value
197
245
  if (selectedVersion.value) f.version = selectedVersion.value
198
246
  const search = await pagefind.value.search(query.value, Object.keys(f).length ? { filters: f } : {})
199
- results.value = await Promise.all(
200
- search.results.slice(0, 12).map((r: { data: () => Promise<ResultData> }) => r.data()),
201
- )
247
+ // Keep pagefind's relevance order; paginate instead of truncating.
248
+ stubs.value = search.results as ResultStub[]
249
+ total.value = search.results.length
250
+ page.value = 1
251
+ await loadPage()
202
252
  searching.value = false
203
253
  }, 200)
204
254
  })
@@ -324,28 +374,46 @@ function onOverlayClick(e: MouseEvent) { if (e.target === e.currentTarget) close
324
374
  </div>
325
375
 
326
376
  <!-- Results -->
327
- <ul v-if="results.length" class="gs-results">
328
- <li
329
- v-for="(result, i) in results"
330
- :key="result.url"
331
- class="gs-result"
332
- :class="{ 'is-selected': i === selectedIndex }"
333
- @mouseenter="selectedIndex = i"
334
- @click="go(result.url)"
335
- >
336
- <div class="gs-result-header">
337
- <span class="gs-result-title">{{ result.meta.title }}</span>
338
- <span v-if="parseUrl(result.url)" class="gs-result-badges">
339
- <span class="gs-badge gs-badge--distro">
340
- {{ DISTRO_NAMES[parseUrl(result.url)!.distro] ?? parseUrl(result.url)!.distro }}
377
+ <template v-if="results.length">
378
+ <div class="gs-meta">
379
+ <span>{{ total }} результатов</span>
380
+ <span v-if="pageCount > 1">{{ page }} / {{ pageCount }}</span>
381
+ </div>
382
+ <ul class="gs-results">
383
+ <li
384
+ v-for="(result, i) in results"
385
+ :key="result.url"
386
+ class="gs-result"
387
+ :class="{ 'is-selected': i === selectedIndex }"
388
+ @mouseenter="selectedIndex = i"
389
+ @click="go(result.url)"
390
+ >
391
+ <div class="gs-result-header">
392
+ <span class="gs-result-title">{{ result.meta.title }}</span>
393
+ <span v-if="parseUrl(result.url)" class="gs-result-badges">
394
+ <span class="gs-badge gs-badge--distro">
395
+ {{ DISTRO_NAMES[parseUrl(result.url)!.distro] ?? parseUrl(result.url)!.distro }}
396
+ </span>
397
+ <span class="gs-badge gs-badge--version">{{ parseUrl(result.url)!.version }}</span>
341
398
  </span>
342
- <span class="gs-badge gs-badge--version">{{ parseUrl(result.url)!.version }}</span>
343
- </span>
344
- </div>
345
- <!-- eslint-disable-next-line vue/no-v-html -->
346
- <div class="gs-result-excerpt" v-html="result.excerpt" />
347
- </li>
348
- </ul>
399
+ </div>
400
+ <!-- eslint-disable-next-line vue/no-v-html -->
401
+ <div class="gs-result-excerpt" v-html="result.excerpt" />
402
+ </li>
403
+ </ul>
404
+ <nav v-if="pageCount > 1" class="gs-pagination" aria-label="Pagination">
405
+ <button class="gs-page-btn" :disabled="page === 1" aria-label="Назад" @click="prevPage">‹</button>
406
+ <button
407
+ v-for="(it, idx) in pageItems"
408
+ :key="idx"
409
+ class="gs-page-btn"
410
+ :class="{ 'is-active': it === page }"
411
+ :disabled="it === '…'"
412
+ @click="typeof it === 'number' && goPage(it)"
413
+ >{{ it }}</button>
414
+ <button class="gs-page-btn" :disabled="page === pageCount" aria-label="Вперёд" @click="nextPage">›</button>
415
+ </nav>
416
+ </template>
349
417
 
350
418
  <div v-else-if="indexLoading" class="gs-status">
351
419
  <span class="gs-spinner" />
@@ -541,6 +609,17 @@ function onOverlayClick(e: MouseEvent) { if (e.target === e.currentTarget) close
541
609
  font-weight: 500;
542
610
  }
543
611
 
612
+ .gs-meta {
613
+ display: flex;
614
+ justify-content: space-between;
615
+ gap: 12px;
616
+ padding: 8px 16px;
617
+ border-bottom: 1px solid var(--vp-c-divider);
618
+ font-size: 12px;
619
+ color: var(--vp-c-text-3);
620
+ flex-shrink: 0;
621
+ }
622
+
544
623
  .gs-results {
545
624
  overflow-y: auto;
546
625
  list-style: none;
@@ -549,6 +628,47 @@ function onOverlayClick(e: MouseEvent) { if (e.target === e.currentTarget) close
549
628
  flex: 1;
550
629
  }
551
630
 
631
+ .gs-pagination {
632
+ display: flex;
633
+ flex-wrap: wrap;
634
+ gap: 4px;
635
+ justify-content: center;
636
+ padding: 8px 16px;
637
+ border-top: 1px solid var(--vp-c-divider);
638
+ flex-shrink: 0;
639
+ }
640
+
641
+ .gs-page-btn {
642
+ min-width: 28px;
643
+ height: 28px;
644
+ padding: 0 6px;
645
+ border: 1px solid var(--vp-c-divider);
646
+ border-radius: 6px;
647
+ background: transparent;
648
+ color: var(--vp-c-text-2);
649
+ font-size: 13px;
650
+ cursor: pointer;
651
+ transition: background 0.1s, border-color 0.1s, color 0.1s;
652
+ }
653
+
654
+ .gs-page-btn:hover:not(:disabled):not(.is-active) {
655
+ border-color: var(--vp-c-brand-1);
656
+ color: var(--vp-c-brand-1);
657
+ }
658
+
659
+ .gs-page-btn.is-active {
660
+ background: var(--vp-c-brand-1);
661
+ border-color: var(--vp-c-brand-1);
662
+ color: var(--vp-c-bg);
663
+ font-weight: 600;
664
+ cursor: default;
665
+ }
666
+
667
+ .gs-page-btn:disabled {
668
+ opacity: 0.45;
669
+ cursor: default;
670
+ }
671
+
552
672
  .gs-result {
553
673
  padding: 8px 12px;
554
674
  border-radius: 8px;
@@ -77,9 +77,15 @@ const hasLangPrefix = computed(
77
77
  * mode where the distro name is encoded in the VitePress `base` and absent
78
78
  * from route.path.
79
79
  */
80
+ // A version segment is `\d+.\d+...` OR a platform dir `pN` (p10/p11).
81
+ // Without the pN case, pN distros (alt-admin, Nets-Laboratories) were
82
+ // treated as multi-distro mode and currentProduct became "p11", so the
83
+ // products dropdown showed "P11" instead of the distribution name.
84
+ const isVersionSeg = (s: string) => /^(\d+\.\d+(\.\d+)*|p\d+)$/.test(s)
85
+
80
86
  const isPerDistroMode = computed(() => {
81
87
  const parts = pathParts.value
82
- return parts.length === 0 || /^\d+\.\d+/.test(parts[0])
88
+ return parts.length === 0 || isVersionSeg(parts[0])
83
89
  })
84
90
 
85
91
  const currentProduct = computed<string | null>(() => {
@@ -20,7 +20,13 @@ import { computed, ref, onMounted } from 'vue'
20
20
  import { useRoute, useData } from 'vitepress'
21
21
 
22
22
  interface DistroInfo { versions: string[]; latest: string; title?: string }
23
- interface SectionInfo { name: string; distros: string[] }
23
+ // Sections come in two shapes: a group `{name, distros:[...]}` or a flat
24
+ // top-level item `{name, distro:<slug>}`. Handling only `distros` made
25
+ // `section.distros.filter` throw on flat entries, crashing the whole
26
+ // products sidebar (blank sidebar on every distro landing).
27
+ interface SectionInfo { name: string; distros?: string[]; distro?: string }
28
+ const sectionDistros = (s: SectionInfo): string[] =>
29
+ s.distros ?? (s.distro ? [s.distro] : [])
24
30
  declare const __VERSIONS_DATA__: {
25
31
  distros: { [distroName: string]: DistroInfo }
26
32
  sections?: SectionInfo[]
@@ -59,6 +65,12 @@ const PRODUCT_NAMES: Record<string, string> = {
59
65
  'simply-linux': 'Симпли Линукс',
60
66
  'simply-linux-e2k': 'Симпли Линукс для Эльбрус',
61
67
  'group-policy': 'Групповые политики',
68
+ 'alt-domain-p10': 'Альт Домен (p10)',
69
+ 'alt-admin': 'Администрирование Альт',
70
+ 'alt-platform-practic': 'Альт Платформа Практик',
71
+ 'freeipa-p10': 'FreeIPA (p10)',
72
+ 'freeipa-p11': 'FreeIPA (p11)',
73
+ 'Nets-Laboratories': 'Лабораторные работы',
62
74
  }
63
75
 
64
76
  const productName = (d: string) =>
@@ -75,10 +87,14 @@ const contentPath = computed(() => {
75
87
  return base.length > 1 && p.startsWith(base) ? p.slice(base.length - 1) : p
76
88
  })
77
89
 
78
- // Show only on the landing page (no version segment like /11.0/…)
90
+ // Show ONLY on the distro landing (path is exactly the distro root, no
91
+ // segment). The old heuristic `!/^\d+\.\d+/.test(parts[0])` mis-fired for
92
+ // pN version dirs (p10/p11): every p11 content page looked like a landing,
93
+ // so the products sidebar rendered on top of the doc sidebar (two
94
+ // sidebars on alt-admin etc.). The landing is unambiguously parts.length===0.
79
95
  const isLandingPage = computed(() => {
80
96
  const parts = contentPath.value.split('/').filter(Boolean)
81
- return parts.length === 0 || !/^\d+\.\d+/.test(parts[0])
97
+ return parts.length === 0
82
98
  })
83
99
 
84
100
  // Current distro is encoded in the base in per-distro mode
@@ -92,7 +108,7 @@ interface SidebarGroup { title: string; distros: string[] }
92
108
  const groups = computed((): SidebarGroup[] => {
93
109
  const allDistros = Object.keys(versionsData.distros)
94
110
  const sections = versionsData.sections ?? []
95
- const sectionedSet = new Set(sections.flatMap((s: SectionInfo) => s.distros))
111
+ const sectionedSet = new Set(sections.flatMap(sectionDistros))
96
112
 
97
113
  const unsectioned = allDistros.filter(d => !sectionedSet.has(d))
98
114
  const regular = unsectioned.filter(d => !d.endsWith('-e2k'))
@@ -105,7 +121,7 @@ const groups = computed((): SidebarGroup[] => {
105
121
  }
106
122
 
107
123
  for (const section of sections) {
108
- const distros = section.distros.filter((d: string) => allDistros.includes(d))
124
+ const distros = sectionDistros(section).filter((d: string) => allDistros.includes(d))
109
125
  if (distros.length > 0) {
110
126
  result.push({ title: section.name, distros })
111
127
  }
@@ -22,6 +22,8 @@ const placeholder = computed(() =>
22
22
  <PagefindSearch
23
23
  :filters="filters"
24
24
  :placeholder="placeholder"
25
+ :page-size="10"
26
+ results-label="результатов"
25
27
  @close="emit('close')"
26
28
  />
27
29
  </template>
package/dist/index.mjs CHANGED
@@ -19,16 +19,40 @@ import ADInlineImage from "./components/ADInlineImage.vue";
19
19
  import ADAssetLink from "./components/ADAssetLink.vue";
20
20
  import ADNavBarSearch from "./components/ADNavBarSearch.vue";
21
21
  import ExportButton from "@ampernic/vitepress-plugin-export/components/ExportButton.vue";
22
+ import Breadcrumbs from "@ampernic/vitepress-plugin-breadcrumbs/components/Breadcrumbs.vue";
23
+ import { productName } from "./productNames.mjs";
22
24
  import "./styles/theme.css";
23
25
  import "./styles/custom.css";
24
26
  import "./styles/icons.css";
25
27
  import "./styles/print.css";
28
+ function breadcrumbsHome(ctx) {
29
+ if (!ctx.distro) return false;
30
+ const name = productName(ctx.distro);
31
+ const text = ctx.version && /^\d/.test(ctx.version) ? `${name} ${ctx.version}` : name;
32
+ return { text, link: ctx.landing };
33
+ }
34
+ function breadcrumbsPrefix(ctx) {
35
+ const arr = [{ text: "\u0413\u043B\u0430\u0432\u043D\u0430\u044F", link: "/" }];
36
+ if (ctx.distro && ctx.version && /^\d/.test(ctx.version)) {
37
+ arr.push({ text: productName(ctx.distro), link: `/${ctx.distro}/` });
38
+ }
39
+ return arr;
40
+ }
26
41
  export const Theme = {
27
42
  Layout: () => h(DefaultTheme.Layout, null, {
28
43
  "nav-bar-content-before": () => h(ADNavBarSearch),
29
44
  "nav-bar-content-after": () => h(NolebaseEnhancedReadabilitiesMenu),
30
45
  "nav-screen-content-after": () => h(NolebaseEnhancedReadabilitiesScreenMenu),
31
46
  "sidebar-nav-before": () => h(ADProductsSidebar),
47
+ // Dynamic breadcrumbs above the page content (standalone package).
48
+ // `home` resolver adds a root crumb linking back to the document
49
+ // landing so the full path up to the index is always reachable.
50
+ "doc-before": () => h(Breadcrumbs, {
51
+ home: breadcrumbsHome,
52
+ prefix: breadcrumbsPrefix,
53
+ showOnHome: true,
54
+ hideSingle: false
55
+ }),
32
56
  // Above the outline ("Оглавление"), per request.
33
57
  "aside-outline-before": () => h(ExportButton)
34
58
  }),
@@ -0,0 +1,2 @@
1
+ export declare const PRODUCT_NAMES: Record<string, string>;
2
+ export declare function productName(slug: string): string;
@@ -0,0 +1,29 @@
1
+ export const PRODUCT_NAMES = {
2
+ "alt-domain": "\u0410\u043B\u044C\u0442 \u0414\u043E\u043C\u0435\u043D",
3
+ "alt-education": "\u0410\u043B\u044C\u0442 \u041E\u0431\u0440\u0430\u0437\u043E\u0432\u0430\u043D\u0438\u0435",
4
+ "alt-education-e2k": "\u0410\u043B\u044C\u0442 \u041E\u0431\u0440\u0430\u0437\u043E\u0432\u0430\u043D\u0438\u0435 \u0434\u043B\u044F \u042D\u043B\u044C\u0431\u0440\u0443\u0441",
5
+ "alt-server": "\u0410\u043B\u044C\u0442 \u0421\u0435\u0440\u0432\u0435\u0440",
6
+ "alt-server-e2k": "\u0410\u043B\u044C\u0442 \u0421\u0435\u0440\u0432\u0435\u0440 \u0434\u043B\u044F \u042D\u043B\u044C\u0431\u0440\u0443\u0441",
7
+ "alt-server-v": "\u0410\u043B\u044C\u0442 \u0421\u0435\u0440\u0432\u0435\u0440 \u0412\u0438\u0440\u0442\u0443\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u0438",
8
+ "alt-workstation": "\u0410\u043B\u044C\u0442 \u0420\u0430\u0431\u043E\u0447\u0430\u044F \u0441\u0442\u0430\u043D\u0446\u0438\u044F",
9
+ "alt-workstation-e2k": "\u0410\u043B\u044C\u0442 \u0420\u0430\u0431\u043E\u0447\u0430\u044F \u0441\u0442\u0430\u043D\u0446\u0438\u044F \u0434\u043B\u044F \u042D\u043B\u044C\u0431\u0440\u0443\u0441",
10
+ "alt-kworkstation": "\u0410\u043B\u044C\u0442 \u0420\u0430\u0431\u043E\u0447\u0430\u044F \u0441\u0442\u0430\u043D\u0446\u0438\u044F K",
11
+ "alt-platform": "\u0410\u043B\u044C\u0442 \u041F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0430",
12
+ "alt-mobile": "\u0410\u043B\u044C\u0442 \u041C\u043E\u0431\u0438\u043B\u044C\u043D\u044B\u0439",
13
+ "alt-virtualisation-one": "\u0410\u043B\u044C\u0442 \u0412\u0438\u0440\u0442\u0443\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u044F ONE",
14
+ "alt-virtualization-pve": "\u0410\u043B\u044C\u0442 \u0412\u0438\u0440\u0442\u0443\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u044F PVE",
15
+ "simply-linux": "\u0421\u0438\u043C\u043F\u043B\u0438 \u041B\u0438\u043D\u0443\u043A\u0441",
16
+ "simply-linux-e2k": "\u0421\u0438\u043C\u043F\u043B\u0438 \u041B\u0438\u043D\u0443\u043A\u0441 \u0434\u043B\u044F \u042D\u043B\u044C\u0431\u0440\u0443\u0441",
17
+ "group-policy": "\u0413\u0440\u0443\u043F\u043F\u043E\u0432\u044B\u0435 \u043F\u043E\u043B\u0438\u0442\u0438\u043A\u0438",
18
+ "alt-domain-p10": "\u0410\u043B\u044C\u0442 \u0414\u043E\u043C\u0435\u043D (p10)",
19
+ "alt-admin": "\u0410\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u0410\u043B\u044C\u0442",
20
+ "alt-platform-practic": "\u0410\u043B\u044C\u0442 \u041F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0430 \u041F\u0440\u0430\u043A\u0442\u0438\u043A",
21
+ "freeipa-p10": "FreeIPA (p10)",
22
+ "freeipa-p11": "FreeIPA (p11)",
23
+ "Nets-Laboratories": "\u041B\u0430\u0431\u043E\u0440\u0430\u0442\u043E\u0440\u043D\u044B\u0435 \u0440\u0430\u0431\u043E\u0442\u044B"
24
+ };
25
+ export function productName(slug) {
26
+ return PRODUCT_NAMES[slug] ?? slug.split("-").map(
27
+ (w) => w === "alt" ? "ALT" : w === "e2k" ? "E2K" : w.charAt(0).toUpperCase() + w.slice(1)
28
+ ).join(" ");
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampernic/vitepress-theme-alt-docs",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "description": "Shared VitePress theme for ALT Linux documentation",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "author": "Ampernic",
@@ -27,17 +27,18 @@
27
27
  "peerDependencies": {
28
28
  "vitepress": "^1.0.0",
29
29
  "vue": "^3.0.0",
30
- "@ampernic/vitepress-plugin-export": "^0.1.38"
30
+ "@ampernic/vitepress-plugin-export": "^0.1.39"
31
31
  },
32
32
  "dependencies": {
33
33
  "@alt-gnome/markdown-it-custom-containers": "^1.0.0",
34
34
  "@nolebase/vitepress-plugin-enhanced-readabilities": "^2.14.0",
35
35
  "markdown-it-kbd": "^1.0.0",
36
36
  "vitepress-plugin-tabs": "^0.6.0",
37
- "@ampernic/vitepress-plugin-alt-docs-versioning": "0.1.9",
37
+ "@ampernic/vitepress-plugin-alt-docs-versioning": "0.1.10",
38
+ "@ampernic/vitepress-plugin-breadcrumbs": "0.1.0",
38
39
  "@ampernic/vitepress-plugin-cross-site-router": "0.1.2",
39
- "@ampernic/vitepress-plugin-pagefind": "0.1.8",
40
- "@ampernic/vitepress-plugin-html-image": "0.1.2"
40
+ "@ampernic/vitepress-plugin-html-image": "0.1.2",
41
+ "@ampernic/vitepress-plugin-pagefind": "0.1.9"
41
42
  },
42
43
  "devDependencies": {
43
44
  "builtin-modules": "^3.3.0",