@kirchdev/duxt 0.3.4-dev-main.41-23ded41 → 0.3.5-dev-main.43-3310f4b
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 +24 -9
- package/app/app.vue +17 -11
- package/app/assets/css/duxt.css +47 -6
- package/app/components/DuxtAnnouncements.vue +117 -0
- package/app/components/DuxtBrunoEntries.vue +62 -0
- package/app/components/DuxtCodeBlock.vue +18 -34
- package/app/components/DuxtCodeToolbar.vue +14 -0
- package/app/components/DuxtCopyButton.vue +33 -0
- package/app/components/DuxtCopyPage.vue +32 -30
- package/app/components/DuxtFooter.vue +39 -8
- package/app/components/DuxtHeader.vue +107 -48
- package/app/components/DuxtLandingProse.vue +1 -1
- package/app/components/DuxtLinkLabel.vue +36 -0
- package/app/components/DuxtLiveWindow.vue +8 -8
- package/app/components/DuxtLocale.vue +10 -1
- package/app/components/DuxtNavigation.vue +2 -2
- package/app/components/DuxtOpenApiClient.vue +285 -90
- package/app/components/DuxtOpenApiMedia.vue +30 -18
- package/app/components/DuxtOpenApiSchema.vue +10 -7
- package/app/components/DuxtPageFeedback.vue +7 -0
- package/app/components/DuxtPageInfo.vue +15 -12
- package/app/components/DuxtPageNav.vue +41 -27
- package/app/components/DuxtPreviewSkeleton.vue +4 -4
- package/app/components/DuxtProgress.vue +2 -14
- package/app/components/DuxtSearch.vue +230 -90
- package/app/components/DuxtSearchInput.vue +70 -0
- package/app/components/DuxtSections.vue +11 -1
- package/app/components/DuxtSegmented.vue +64 -0
- package/app/components/DuxtShortcuts.vue +16 -11
- package/app/components/DuxtShortcutsTrigger.vue +49 -0
- package/app/components/DuxtSkipLink.vue +1 -1
- package/app/components/DuxtToc.vue +16 -10
- package/app/components/DuxtTranslationBanner.vue +1 -1
- package/app/components/DuxtVersion.vue +53 -12
- package/app/components/DuxtVersionBanner.vue +5 -5
- package/app/components/content/AccordionItem.vue +2 -2
- package/app/components/content/BrunoOverview.vue +260 -0
- package/app/components/content/BrunoRequest.vue +141 -0
- package/app/components/content/BrunoRequests.vue +104 -0
- package/app/components/content/Callout.vue +5 -5
- package/app/components/content/ChangelogReleases.vue +161 -40
- package/app/components/content/CodeGroup.vue +41 -37
- package/app/components/content/Field.vue +1 -1
- package/app/components/content/FileTree.vue +1 -1
- package/app/components/content/OpenApiOperation.vue +4 -4
- package/app/components/content/OpenApiOperations.vue +1 -1
- package/app/components/content/OpenApiOverview.vue +1 -1
- package/app/components/content/PackageManagers.vue +42 -58
- package/app/components/content/PageCards.vue +7 -4
- package/app/components/content/Preview.vue +15 -39
- package/app/components/content/ProseImg.vue +596 -49
- package/app/components/content/Since.vue +1 -1
- package/app/components/content/Steps.vue +6 -4
- package/app/components/content/TabGroup.vue +13 -12
- package/app/components/ui/breadcrumb/BreadcrumbSeparator.vue +1 -1
- package/app/components/ui/command/CommandShortcut.vue +1 -1
- package/app/components/ui/dialog/DialogContent.vue +2 -2
- package/app/components/ui/dialog/DialogHeader.vue +1 -1
- package/app/components/ui/dialog/DialogScrollContent.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +2 -2
- package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +2 -2
- package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +2 -2
- package/app/components/ui/navigation-menu/NavigationMenuContent.vue +1 -1
- package/app/components/ui/navigation-menu/NavigationMenuTrigger.vue +1 -1
- package/app/components/ui/navigation-menu/NavigationMenuViewport.vue +1 -1
- package/app/components/ui/scroll-area/ScrollBar.vue +1 -1
- package/app/components/ui/select/SelectItem.vue +2 -2
- package/app/components/ui/sheet/SheetContent.vue +29 -3
- package/app/components/ui/sidebar/Sidebar.vue +28 -4
- package/app/components/ui/sidebar/SidebarGroupAction.vue +1 -1
- package/app/components/ui/sidebar/SidebarInset.vue +1 -1
- package/app/components/ui/sidebar/SidebarMenuAction.vue +1 -1
- package/app/components/ui/sidebar/SidebarMenuBadge.vue +1 -1
- package/app/components/ui/sidebar/SidebarMenuSub.vue +1 -1
- package/app/components/ui/sidebar/SidebarMenuSubButton.vue +1 -1
- package/app/components/ui/sidebar/SidebarProvider.vue +7 -16
- package/app/components/ui/sidebar/index.ts +10 -2
- package/app/components/ui/sidebar/utils.ts +0 -1
- package/app/components/ui/sonner/Sonner.vue +18 -0
- package/app/components/ui/tabs/TabsList.vue +13 -3
- package/app/components/ui/tabs/TabsTrigger.vue +24 -3
- package/app/components/ui/tooltip/TooltipContent.vue +2 -2
- package/app/composables/useActiveHeading.ts +3 -25
- package/app/composables/useDuxtAnalytics.ts +50 -0
- package/app/composables/useDuxtAnimatedHeight.ts +8 -38
- package/app/composables/useDuxtAnnouncements.ts +153 -0
- package/app/composables/useDuxtCopy.ts +51 -0
- package/app/composables/useDuxtDirection.ts +23 -0
- package/app/composables/useDuxtNavigation.ts +28 -8
- package/app/composables/useDuxtPath.ts +3 -5
- package/app/composables/useDuxtSearch.ts +87 -35
- package/app/composables/useDuxtSection.ts +15 -1
- package/app/composables/useDuxtSectionRow.ts +6 -5
- package/app/composables/useDuxtShortcuts.ts +319 -20
- package/app/composables/useDuxtVersion.ts +57 -6
- package/app/composables/useDuxtViewportMeasure.ts +37 -0
- package/app/composables/useFuzzySearch.ts +9 -4
- package/app/composables/useRecentPages.ts +14 -17
- package/app/error.vue +112 -74
- package/app/layouts/changelog.vue +9 -1
- package/app/layouts/default.vue +9 -0
- package/app/layouts/docs.vue +9 -1
- package/app/layouts/reference.vue +9 -1
- package/app/middleware/duxt-section-layout.global.ts +1 -3
- package/app/pages/[...slug].vue +146 -35
- package/app/pages/index.vue +15 -23
- package/app/types/duxt.d.ts +312 -4
- package/app/utils/analytics.ts +91 -0
- package/app/utils/announcements.ts +205 -0
- package/app/utils/changelog.ts +0 -28
- package/app/utils/code-highlight.ts +3 -1
- package/app/utils/contributor-avatar.ts +27 -0
- package/app/utils/direction.ts +43 -0
- package/app/utils/duxt-config.ts +43 -0
- package/app/utils/duxt-text.ts +10 -0
- package/app/utils/format-date.ts +28 -0
- package/app/utils/generated-sections.ts +68 -12
- package/app/utils/generated-toc.ts +14 -7
- package/app/utils/locale-path.ts +43 -0
- package/app/utils/navigation-tree.ts +51 -1
- package/app/utils/nearest-page.ts +8 -0
- package/app/utils/openapi.ts +16 -11
- package/app/utils/page-controls.ts +215 -0
- package/app/utils/pill.ts +61 -0
- package/app/utils/prose-image.ts +128 -0
- package/app/utils/search-context.ts +110 -0
- package/app/utils/search-display.ts +248 -0
- package/app/utils/section-scope.ts +65 -4
- package/app/utils/stored-json.ts +31 -0
- package/app/utils/version-choices.ts +6 -13
- package/app/utils/version-paths.ts +21 -0
- package/bin/duxt-cache-key.mjs +61 -0
- package/bin/duxt-og-cache.mjs +58 -0
- package/bin/{duxt-report.mjs → duxt.mjs} +13 -7
- package/bruno-model.ts +169 -0
- package/bruno-parse.ts +656 -0
- package/bruno-zip.ts +195 -0
- package/cli.ts +240 -0
- package/duxt-app-config.ts +18 -1
- package/frontmatter.ts +49 -0
- package/git-contributors.ts +322 -0
- package/i18n/locales/de/duxt/announcement.json +7 -0
- package/i18n/locales/de/duxt/bruno.json +25 -0
- package/i18n/locales/de/duxt/changelog.json +1 -1
- package/i18n/locales/de/duxt/code.json +2 -1
- package/i18n/locales/de/duxt/page.json +9 -0
- package/i18n/locales/de/duxt/shortcuts.json +1 -0
- package/i18n/locales/de/duxt/theme.json +2 -1
- package/i18n/locales/en/duxt/announcement.json +7 -0
- package/i18n/locales/en/duxt/bruno.json +25 -0
- package/i18n/locales/en/duxt/changelog.json +1 -1
- package/i18n/locales/en/duxt/code.json +2 -1
- package/i18n/locales/en/duxt/page.json +9 -0
- package/i18n/locales/en/duxt/shortcuts.json +1 -0
- package/i18n/locales/en/duxt/theme.json +2 -1
- package/i18n/locales/es/duxt/announcement.json +7 -0
- package/i18n/locales/es/duxt/bruno.json +25 -0
- package/i18n/locales/es/duxt/changelog.json +1 -1
- package/i18n/locales/es/duxt/code.json +2 -1
- package/i18n/locales/es/duxt/page.json +9 -0
- package/i18n/locales/es/duxt/shortcuts.json +1 -0
- package/i18n/locales/es/duxt/theme.json +2 -1
- package/i18n/locales/fr/duxt/announcement.json +7 -0
- package/i18n/locales/fr/duxt/bruno.json +25 -0
- package/i18n/locales/fr/duxt/changelog.json +1 -1
- package/i18n/locales/fr/duxt/code.json +2 -1
- package/i18n/locales/fr/duxt/page.json +9 -0
- package/i18n/locales/fr/duxt/shortcuts.json +1 -0
- package/i18n/locales/fr/duxt/theme.json +2 -1
- package/i18n/locales/pt/duxt/announcement.json +7 -0
- package/i18n/locales/pt/duxt/bruno.json +25 -0
- package/i18n/locales/pt/duxt/changelog.json +1 -1
- package/i18n/locales/pt/duxt/code.json +2 -1
- package/i18n/locales/pt/duxt/page.json +9 -0
- package/i18n/locales/pt/duxt/shortcuts.json +1 -0
- package/i18n/locales/pt/duxt/theme.json +2 -1
- package/modules/bruno.ts +118 -0
- package/modules/config.ts +26 -0
- package/modules/devtools.ts +2 -3
- package/modules/git-meta.ts +16 -47
- package/modules/search-records.ts +132 -0
- package/nuxt.config.ts +81 -7
- package/og-image-cache.ts +742 -0
- package/package.json +41 -9
- package/public/devtools/checks.html +2 -2
- package/public/devtools/config.html +59 -4
- package/public/devtools/pages.html +8 -1
- package/public/devtools/paths.html +1 -1
- package/public/devtools/sources.html +2 -2
- package/public/devtools/versions.html +3 -0
- package/report.ts +28 -19
- package/search-records.ts +380 -0
- package/section-input.ts +142 -0
- package/section-markdown.ts +67 -0
- package/section-reports.ts +10 -4
- package/sections-bruno.ts +571 -0
- package/sections-changelog.ts +76 -43
- package/sections-openapi.ts +12 -83
- package/sections-resolve.ts +174 -8
- package/sections.ts +33 -10
- package/server/devtools/shell.ts +1 -1
- package/server/mcp/tools/list-pages.ts +77 -36
- package/server/mcp/tools/list-versions.ts +84 -0
- package/server/mcp/tools/read-page.ts +80 -26
- package/server/mcp/tools/search-docs.ts +87 -53
- package/server/middleware/raw-markdown.ts +1 -1
- package/server/routes/llms-full.txt.get.ts +6 -8
- package/server/routes/llms.txt.get.ts +1 -1
- package/server/utils/duxt-server-text.ts +0 -13
- package/server/utils/llms-pages.ts +24 -9
- package/server/utils/mcp-docs.ts +577 -0
- package/sources-cache.ts +376 -0
- package/sources-git.ts +186 -31
- package/sources-resolve.ts +59 -1
- package/sources.ts +122 -13
- package/tfplugindocs.ts +158 -0
|
@@ -105,14 +105,65 @@ export function useDuxtVersion() {
|
|
|
105
105
|
() => isPreferred.value && status.value !== 'eol'
|
|
106
106
|
);
|
|
107
107
|
|
|
108
|
-
/**
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Whether the same page is MISSING from the preferred version.
|
|
110
|
+
*
|
|
111
|
+
* Only a changelog release page can be: every version publishes its own
|
|
112
|
+
* changelog, and a branch's may carry a release the default version has not
|
|
113
|
+
* shipped. Swapping the prefix alone then pointed the banner's link and the
|
|
114
|
+
* canonical at a page that does not exist. Every other per-version tree is the
|
|
115
|
+
* same tree at another ref, so nothing else is looked up.
|
|
116
|
+
*
|
|
117
|
+
* `useAsyncData`, so the server waits for it and the canonical in the
|
|
118
|
+
* prerendered HTML is already right. Keyed per path, so the banner and the
|
|
119
|
+
* page share one lookup. Nothing after the `await` touches a composable.
|
|
120
|
+
*/
|
|
121
|
+
const { data: preferredMissing } = useAsyncData(
|
|
122
|
+
() => `duxt-preferred-missing-${path.value}`,
|
|
123
|
+
async () => {
|
|
124
|
+
const own = current.value;
|
|
125
|
+
const target = preferred.value;
|
|
126
|
+
if (
|
|
127
|
+
!own ||
|
|
128
|
+
!target ||
|
|
129
|
+
own === target ||
|
|
130
|
+
own.generated?.type !== 'changelog' ||
|
|
131
|
+
path.value === own.prefix
|
|
132
|
+
) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const candidate = versionPath(
|
|
137
|
+
path.value,
|
|
138
|
+
own.prefix,
|
|
139
|
+
target.prefix || '/'
|
|
140
|
+
);
|
|
141
|
+
const hit = await queryCollection(target.collection as DuxtCollectionArg)
|
|
142
|
+
.path(candidate)
|
|
143
|
+
.select('path')
|
|
144
|
+
.first();
|
|
145
|
+
|
|
146
|
+
return !hit;
|
|
147
|
+
},
|
|
148
|
+
{ default: () => false, watch: [path] }
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The same page in the preferred version, for the banner and the canonical —
|
|
153
|
+
* or that version's own root where it never published the page. The same rule
|
|
154
|
+
* the switcher follows, see `versionSwitchPath`.
|
|
155
|
+
*/
|
|
156
|
+
const preferredPath = computed(() => {
|
|
157
|
+
const to = preferred.value?.prefix || '/';
|
|
158
|
+
const candidate = versionPath(path.value, current.value?.prefix, to);
|
|
159
|
+
|
|
160
|
+
return versionSwitchPath(
|
|
111
161
|
path.value,
|
|
112
162
|
current.value?.prefix,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
163
|
+
to,
|
|
164
|
+
new Set(preferredMissing.value ? [candidate] : [])
|
|
165
|
+
);
|
|
166
|
+
});
|
|
116
167
|
|
|
117
168
|
const shouldWarn = computed(
|
|
118
169
|
() =>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run a measurement whenever the window scrolls or resizes — at most once per
|
|
3
|
+
* painted frame.
|
|
4
|
+
*
|
|
5
|
+
* The reading-progress bar and the table of contents both answer a question
|
|
6
|
+
* about where the reader is, and both used to wire up the same two listeners by
|
|
7
|
+
* hand; only one of them coalesced into an animation frame, so the other ran
|
|
8
|
+
* its layout read on every scroll event however fast the wheel turned.
|
|
9
|
+
*
|
|
10
|
+
* Passive listeners, because this must never be what makes scrolling stutter.
|
|
11
|
+
* Measured once on mount, since a page can open already scrolled. Returns the
|
|
12
|
+
* scheduler, for a caller that knows of another moment worth measuring.
|
|
13
|
+
*/
|
|
14
|
+
import { useEventListener } from '@vueuse/core';
|
|
15
|
+
|
|
16
|
+
export function useDuxtViewportMeasure(measure: () => void) {
|
|
17
|
+
let frame = 0;
|
|
18
|
+
|
|
19
|
+
function schedule() {
|
|
20
|
+
if (frame) return;
|
|
21
|
+
frame = requestAnimationFrame(() => {
|
|
22
|
+
frame = 0;
|
|
23
|
+
measure();
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// `useEventListener` without a target binds to the window, and to nothing at
|
|
28
|
+
// all during server rendering.
|
|
29
|
+
useEventListener(['scroll', 'resize'], schedule, { passive: true });
|
|
30
|
+
|
|
31
|
+
onMounted(measure);
|
|
32
|
+
onBeforeUnmount(() => {
|
|
33
|
+
if (frame) cancelAnimationFrame(frame);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return schedule;
|
|
37
|
+
}
|
|
@@ -31,7 +31,12 @@ export function useFuzzySearch(
|
|
|
31
31
|
* the approximate pass reading the same one.
|
|
32
32
|
*/
|
|
33
33
|
collection: MaybeRefOrGetter<DuxtCollectionName> = 'docs' as DuxtCollectionName,
|
|
34
|
-
options?: {
|
|
34
|
+
options?: {
|
|
35
|
+
ignoredTags?: string[];
|
|
36
|
+
threshold?: number;
|
|
37
|
+
minMatchCharLength?: number;
|
|
38
|
+
limit?: number;
|
|
39
|
+
}
|
|
35
40
|
) {
|
|
36
41
|
let index: Promise<Fuse<DuxtSearchSection>> | undefined;
|
|
37
42
|
let indexedFor: string | undefined;
|
|
@@ -58,12 +63,12 @@ export function useFuzzySearch(
|
|
|
58
63
|
|
|
59
64
|
// Loose enough to survive a transposed letter, tight enough that a short
|
|
60
65
|
// term does not drag in half the documentation.
|
|
61
|
-
threshold: 0.35,
|
|
62
|
-
minMatchCharLength: 3
|
|
66
|
+
threshold: options?.threshold ?? 0.35,
|
|
67
|
+
minMatchCharLength: options?.minMatchCharLength ?? 3
|
|
63
68
|
});
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
async function search(term: string, limit = 20) {
|
|
71
|
+
async function search(term: string, limit = options?.limit ?? 20) {
|
|
67
72
|
const name = toValue(collection);
|
|
68
73
|
|
|
69
74
|
// Built on first use, and again when the route moves to another collection
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/** Where the list is kept. Namespaced so a consumer's own keys cannot collide. */
|
|
2
2
|
const STORAGE_KEY = 'duxt:recent-pages';
|
|
3
|
-
const LIMIT = 5;
|
|
4
|
-
|
|
5
3
|
export interface RecentPage {
|
|
6
4
|
path: string;
|
|
7
5
|
title: string;
|
|
@@ -15,17 +13,19 @@ export interface RecentPage {
|
|
|
15
13
|
* request. It stays in the browser, identifies nobody, and clearing site data
|
|
16
14
|
* forgets it.
|
|
17
15
|
*/
|
|
18
|
-
export function useRecentPages() {
|
|
16
|
+
export function useRecentPages(limit = 5) {
|
|
19
17
|
const recent = useState<RecentPage[]>('duxt-recent-pages', () => []);
|
|
20
18
|
|
|
21
19
|
function read(): RecentPage[] {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
// Blocked storage, or a value someone else wrote — either way, no history.
|
|
21
|
+
const parsed = readStoredJson(STORAGE_KEY);
|
|
22
|
+
|
|
23
|
+
return Array.isArray(parsed)
|
|
24
|
+
? parsed.filter(
|
|
25
|
+
(entry): entry is RecentPage =>
|
|
26
|
+
typeof entry?.path === 'string' && typeof entry?.title === 'string'
|
|
27
|
+
)
|
|
28
|
+
: [];
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function remember(page: RecentPage) {
|
|
@@ -34,18 +34,15 @@ export function useRecentPages() {
|
|
|
34
34
|
const next = [
|
|
35
35
|
page,
|
|
36
36
|
...read().filter((entry) => entry.path !== page.path)
|
|
37
|
-
].slice(0,
|
|
37
|
+
].slice(0, Math.max(0, limit));
|
|
38
38
|
recent.value = next;
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
} catch {
|
|
43
|
-
// A convenience not worth an error.
|
|
44
|
-
}
|
|
40
|
+
// A convenience not worth an error.
|
|
41
|
+
writeStoredJson(STORAGE_KEY, next);
|
|
45
42
|
}
|
|
46
43
|
|
|
47
44
|
function load() {
|
|
48
|
-
if (import.meta.client) recent.value = read();
|
|
45
|
+
if (import.meta.client) recent.value = read().slice(0, Math.max(0, limit));
|
|
49
46
|
}
|
|
50
47
|
|
|
51
48
|
return { recent, remember, load };
|
package/app/error.vue
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { NuxtError } from '#app';
|
|
3
|
+
import { ConfigProvider } from 'reka-ui';
|
|
3
4
|
|
|
4
5
|
const props = defineProps<{ error: NuxtError }>();
|
|
5
6
|
|
|
@@ -7,16 +8,27 @@ const localeLink = useDuxtLink();
|
|
|
7
8
|
const path = useDuxtPath();
|
|
8
9
|
const duxt = useDuxtConfig();
|
|
9
10
|
const { locale } = useI18n();
|
|
11
|
+
const direction = useDuxtDirection();
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* The error page replaces `app.vue`, so it inherits nothing from it — not the
|
|
13
|
-
* `lang` attribute and not the title template.
|
|
14
|
-
* or the one page a reader reaches by accident is
|
|
15
|
-
* untitled, language-less document.
|
|
15
|
+
* `lang` attribute, not the `dir` one and not the title template. All three
|
|
16
|
+
* have to be set again here, or the one page a reader reaches by accident is
|
|
17
|
+
* the one page with an untitled, language-less, wrong-way-round document.
|
|
18
|
+
*
|
|
19
|
+
* `dir` was the one that was missing, which is the whole shape of the problem:
|
|
20
|
+
* nothing failed, the error page simply stayed left-to-right while the rest of
|
|
21
|
+
* the site turned round.
|
|
16
22
|
*/
|
|
17
23
|
useHead(() => ({
|
|
18
|
-
htmlAttrs: { lang: locale.value },
|
|
19
|
-
|
|
24
|
+
htmlAttrs: { lang: locale.value, dir: direction.value },
|
|
25
|
+
// The status code alone, through the same template `app.vue` sets. After a
|
|
26
|
+
// client-side navigation that template is still mounted, so a title that
|
|
27
|
+
// already carried the site name came out as "404 · duxt · duxt"; rendered on
|
|
28
|
+
// the server it is not, and this page has to bring its own.
|
|
29
|
+
title: String(props.error.statusCode),
|
|
30
|
+
titleTemplate: (title?: string) =>
|
|
31
|
+
title ? `${title} · ${duxt.title}` : duxt.title
|
|
20
32
|
}));
|
|
21
33
|
|
|
22
34
|
/**
|
|
@@ -39,25 +51,20 @@ useSeoMeta({ robots: 'noindex, follow' });
|
|
|
39
51
|
*/
|
|
40
52
|
const { data: navigation } = await useDuxtNavigation();
|
|
41
53
|
|
|
54
|
+
// `flattenedNavigationPages` rather than a walk of its own: it honours
|
|
55
|
+
// `page: false`, and a group that is not a route — a tfplugindocs `subcategory`
|
|
56
|
+
// is the standing example — was being offered here as a near miss, which is a
|
|
57
|
+
// 404 suggesting another 404.
|
|
42
58
|
const suggestions = computed(() => {
|
|
43
59
|
if (props.error.statusCode !== 404) return [];
|
|
44
60
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (Array.isArray(item.children)) {
|
|
53
|
-
walk(item.children as { path?: string; title?: string }[]);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
walk((navigation.value ?? []) as { path?: string; title?: string }[]);
|
|
59
|
-
|
|
60
|
-
return nearestPages(path.value, pages);
|
|
61
|
+
return nearestPages(
|
|
62
|
+
path.value,
|
|
63
|
+
flattenedNavigationPages(navigation.value ?? []).map((item) => ({
|
|
64
|
+
path: item.path,
|
|
65
|
+
title: item.title
|
|
66
|
+
}))
|
|
67
|
+
);
|
|
61
68
|
});
|
|
62
69
|
|
|
63
70
|
// A page missing from one version but present in another is the interesting
|
|
@@ -73,61 +80,92 @@ const elsewhere = computed(
|
|
|
73
80
|
</script>
|
|
74
81
|
|
|
75
82
|
<template>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
<!-- The same provider `app.vue` mounts, for the same reason: this page
|
|
84
|
+
replaces that one, so the reka primitives inside the layout below would
|
|
85
|
+
otherwise be left with the `ltr` default. The tooltip provider likewise:
|
|
86
|
+
the header's icon controls carry tooltips, and without one a client-side
|
|
87
|
+
navigation to this page threw before it could draw. -->
|
|
88
|
+
<ConfigProvider :dir="direction">
|
|
89
|
+
<UiTooltipProvider>
|
|
90
|
+
<NuxtLayout>
|
|
91
|
+
<div
|
|
92
|
+
class="mx-auto flex max-w-2xl flex-col items-center px-4 py-32 text-center"
|
|
93
|
+
>
|
|
94
|
+
<p class="font-mono text-sm text-muted-foreground">
|
|
95
|
+
{{ error.statusCode }}
|
|
96
|
+
</p>
|
|
97
|
+
<h1 class="mt-3 text-3xl font-semibold tracking-tight text-balance">
|
|
98
|
+
{{ error.statusMessage ?? $t('duxt.error.title') }}
|
|
99
|
+
</h1>
|
|
86
100
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
<div v-if="elsewhere.length" class="mt-8 w-full">
|
|
102
|
+
<p class="mb-3 text-sm text-muted-foreground">
|
|
103
|
+
{{ $t('duxt.error.elsewhere') }}
|
|
104
|
+
</p>
|
|
105
|
+
<div class="flex flex-wrap justify-center gap-2">
|
|
106
|
+
<UiButton
|
|
107
|
+
v-for="entry in elsewhere"
|
|
108
|
+
:key="entry.path"
|
|
109
|
+
as-child
|
|
110
|
+
variant="outline"
|
|
111
|
+
size="sm"
|
|
112
|
+
>
|
|
113
|
+
<NuxtLink
|
|
114
|
+
:to="localeLink(entry.path)"
|
|
115
|
+
class="font-mono text-xs"
|
|
116
|
+
>
|
|
117
|
+
{{ entry.version.label }}
|
|
118
|
+
</NuxtLink>
|
|
119
|
+
</UiButton>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div v-if="suggestions.length" class="mt-8 w-full">
|
|
124
|
+
<p class="mb-3 text-sm text-muted-foreground">
|
|
125
|
+
{{ $t('duxt.error.nearest') }}
|
|
126
|
+
</p>
|
|
127
|
+
<!-- ROWS, NOT A CENTRED LINE PER PAGE. Each suggestion used to be a
|
|
128
|
+
title and its path run together on one centred line, so every
|
|
129
|
+
row started somewhere else and the paths wandered from line to
|
|
130
|
+
line. Left-aligned in one column — title over path, the way the
|
|
131
|
+
search dialog lists a page — the list reads down at a glance. -->
|
|
132
|
+
<ul class="mx-auto flex w-full max-w-md flex-col gap-1 text-start">
|
|
133
|
+
<li v-for="page in suggestions" :key="page.path">
|
|
134
|
+
<NuxtLink
|
|
135
|
+
:to="localeLink(page.path)"
|
|
136
|
+
class="group flex items-center gap-3 rounded-md px-3 py-2 transition-colors hover:bg-accent focus-visible:bg-accent focus-visible:outline-none"
|
|
137
|
+
>
|
|
138
|
+
<Icon
|
|
139
|
+
name="lucide:file-text"
|
|
140
|
+
class="size-4 shrink-0 text-muted-foreground"
|
|
141
|
+
/>
|
|
142
|
+
<span class="min-w-0 flex-1">
|
|
143
|
+
<span class="block truncate text-sm font-medium">
|
|
144
|
+
{{ page.title ?? page.path }}
|
|
145
|
+
</span>
|
|
146
|
+
<span
|
|
147
|
+
class="block truncate font-mono text-xs text-muted-foreground"
|
|
148
|
+
>
|
|
149
|
+
{{ page.path }}
|
|
150
|
+
</span>
|
|
151
|
+
</span>
|
|
152
|
+
<Icon
|
|
153
|
+
name="lucide:chevron-right"
|
|
154
|
+
class="size-4 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100 rtl:-scale-x-100"
|
|
155
|
+
/>
|
|
156
|
+
</NuxtLink>
|
|
157
|
+
</li>
|
|
158
|
+
</ul>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<UiButton as-child class="mt-10">
|
|
162
|
+
<NuxtLink :to="localeLink('/')">
|
|
163
|
+
<Icon name="lucide:arrow-left" class="size-4 rtl:-scale-x-100" />
|
|
164
|
+
{{ $t('duxt.error.back') }}
|
|
101
165
|
</NuxtLink>
|
|
102
166
|
</UiButton>
|
|
103
167
|
</div>
|
|
104
|
-
</
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
<p class="mb-3 text-sm text-muted-foreground">
|
|
108
|
-
{{ $t('duxt.error.nearest') }}
|
|
109
|
-
</p>
|
|
110
|
-
<ul class="flex flex-col gap-1 text-sm">
|
|
111
|
-
<li v-for="page in suggestions" :key="page.path">
|
|
112
|
-
<NuxtLink
|
|
113
|
-
:to="localeLink(page.path)"
|
|
114
|
-
class="flex items-center justify-center gap-2 rounded-md px-3 py-1.5 transition-colors hover:bg-accent"
|
|
115
|
-
>
|
|
116
|
-
<span class="font-medium">{{ page.title ?? page.path }}</span>
|
|
117
|
-
<span class="font-mono text-xs text-muted-foreground">{{
|
|
118
|
-
page.path
|
|
119
|
-
}}</span>
|
|
120
|
-
</NuxtLink>
|
|
121
|
-
</li>
|
|
122
|
-
</ul>
|
|
123
|
-
</div>
|
|
124
|
-
|
|
125
|
-
<UiButton as-child class="mt-10">
|
|
126
|
-
<NuxtLink :to="localeLink('/')">
|
|
127
|
-
<Icon name="lucide:arrow-left" class="size-4" />
|
|
128
|
-
{{ $t('duxt.error.back') }}
|
|
129
|
-
</NuxtLink>
|
|
130
|
-
</UiButton>
|
|
131
|
-
</div>
|
|
132
|
-
</NuxtLayout>
|
|
168
|
+
</NuxtLayout>
|
|
169
|
+
</UiTooltipProvider>
|
|
170
|
+
</ConfigProvider>
|
|
133
171
|
</template>
|
|
@@ -50,16 +50,24 @@ const { headerOffset } = useDuxtSectionRow();
|
|
|
50
50
|
<DuxtProgress />
|
|
51
51
|
<DuxtShortcuts />
|
|
52
52
|
|
|
53
|
+
<!-- The site the layer is rendering may have something to say. Both strips
|
|
54
|
+
draw nothing at all until `duxt.announcements` holds one, space
|
|
55
|
+
included; the third placement, `above-content`, belongs to the page
|
|
56
|
+
rather than to the shell. -->
|
|
57
|
+
<DuxtAnnouncements placement="above-header" />
|
|
58
|
+
|
|
53
59
|
<DuxtHeader />
|
|
54
60
|
<DuxtSections />
|
|
55
61
|
|
|
62
|
+
<DuxtAnnouncements placement="below-header" />
|
|
63
|
+
|
|
56
64
|
<div class="mx-auto flex w-full max-w-[90rem] flex-1 gap-8 px-4 lg:px-8">
|
|
57
65
|
<!-- A section with one page has nothing to navigate, exactly as in the
|
|
58
66
|
docs layout: the column would list the page the reader is on. That is
|
|
59
67
|
the whole overview of a changelog with a single release. -->
|
|
60
68
|
<div v-if="items.length > 1" class="hidden w-56 shrink-0 lg:block">
|
|
61
69
|
<div
|
|
62
|
-
class="sticky top-[var(--duxt-header-offset)] max-h-[calc(100vh-var(--duxt-header-offset)-1.5rem)] overflow-y-auto py-8
|
|
70
|
+
class="sticky top-[var(--duxt-header-offset)] max-h-[calc(100vh-var(--duxt-header-offset)-1.5rem)] overflow-y-auto py-8 pe-2"
|
|
63
71
|
>
|
|
64
72
|
<DuxtNavigation
|
|
65
73
|
:items="items"
|
package/app/layouts/default.vue
CHANGED
|
@@ -11,7 +11,16 @@
|
|
|
11
11
|
<DuxtProgress />
|
|
12
12
|
<DuxtShortcuts />
|
|
13
13
|
|
|
14
|
+
<!-- The site the layer is rendering may have something to say. Both strips
|
|
15
|
+
draw nothing at all until `duxt.announcements` holds one, space
|
|
16
|
+
included; the third placement, `above-content`, belongs to the page
|
|
17
|
+
rather than to the shell. -->
|
|
18
|
+
<DuxtAnnouncements placement="above-header" />
|
|
19
|
+
|
|
14
20
|
<DuxtHeader />
|
|
21
|
+
|
|
22
|
+
<DuxtAnnouncements placement="below-header" />
|
|
23
|
+
|
|
15
24
|
<main id="duxt-main" tabindex="-1" class="flex-1 outline-none">
|
|
16
25
|
<slot />
|
|
17
26
|
</main>
|
package/app/layouts/docs.vue
CHANGED
|
@@ -21,12 +21,20 @@ const { headerOffset } = useDuxtSectionRow();
|
|
|
21
21
|
<DuxtProgress />
|
|
22
22
|
<DuxtShortcuts />
|
|
23
23
|
|
|
24
|
+
<!-- The site the layer is rendering may have something to say. Both strips
|
|
25
|
+
draw nothing at all until `duxt.announcements` holds one, space
|
|
26
|
+
included; the third placement, `above-content`, belongs to the page
|
|
27
|
+
rather than to the shell. -->
|
|
28
|
+
<DuxtAnnouncements placement="above-header" />
|
|
29
|
+
|
|
24
30
|
<DuxtHeader />
|
|
25
31
|
|
|
26
32
|
<!-- The section row sits with the docs, not in the global header: the
|
|
27
33
|
landing page has no sections to show. -->
|
|
28
34
|
<DuxtSections />
|
|
29
35
|
|
|
36
|
+
<DuxtAnnouncements placement="below-header" />
|
|
37
|
+
|
|
30
38
|
<!-- Three columns inside one centred container, not a sidebar pinned to the
|
|
31
39
|
window edge: on a wide screen the docs stay a readable block instead of
|
|
32
40
|
drifting apart. -->
|
|
@@ -39,7 +47,7 @@ const { headerOffset } = useDuxtSectionRow();
|
|
|
39
47
|
the content takes the width. -->
|
|
40
48
|
<div v-if="items.length > 1" class="hidden w-56 shrink-0 lg:block">
|
|
41
49
|
<div
|
|
42
|
-
class="sticky top-[var(--duxt-header-offset)] max-h-[calc(100vh-var(--duxt-header-offset)-1.5rem)] overflow-y-auto py-8
|
|
50
|
+
class="sticky top-[var(--duxt-header-offset)] max-h-[calc(100vh-var(--duxt-header-offset)-1.5rem)] overflow-y-auto py-8 pe-2"
|
|
43
51
|
>
|
|
44
52
|
<DuxtNavigation :items="items" :label="$t('duxt.nav.docs')" />
|
|
45
53
|
</div>
|
|
@@ -51,15 +51,23 @@ const { headerOffset } = useDuxtSectionRow();
|
|
|
51
51
|
<DuxtProgress />
|
|
52
52
|
<DuxtShortcuts />
|
|
53
53
|
|
|
54
|
+
<!-- The site the layer is rendering may have something to say. Both strips
|
|
55
|
+
draw nothing at all until `duxt.announcements` holds one, space
|
|
56
|
+
included; the third placement, `above-content`, belongs to the page
|
|
57
|
+
rather than to the shell. -->
|
|
58
|
+
<DuxtAnnouncements placement="above-header" />
|
|
59
|
+
|
|
54
60
|
<DuxtHeader />
|
|
55
61
|
<DuxtSections />
|
|
56
62
|
|
|
63
|
+
<DuxtAnnouncements placement="below-header" />
|
|
64
|
+
|
|
57
65
|
<div class="mx-auto flex w-full max-w-[90rem] flex-1 gap-8 px-4 lg:px-8">
|
|
58
66
|
<!-- A section with one page has nothing to navigate, exactly as in the
|
|
59
67
|
docs layout: the column would list the page the reader is on. -->
|
|
60
68
|
<div v-if="items.length > 1" class="hidden w-64 shrink-0 lg:block">
|
|
61
69
|
<div
|
|
62
|
-
class="sticky top-[var(--duxt-header-offset)] max-h-[calc(100vh-var(--duxt-header-offset)-1.5rem)] overflow-y-auto py-8
|
|
70
|
+
class="sticky top-[var(--duxt-header-offset)] max-h-[calc(100vh-var(--duxt-header-offset)-1.5rem)] overflow-y-auto py-8 pe-2"
|
|
63
71
|
>
|
|
64
72
|
<DuxtNavigation
|
|
65
73
|
:items="items"
|
|
@@ -51,7 +51,5 @@ function localeCodes(): string[] {
|
|
|
51
51
|
| { locales?: { value?: (string | { code: string })[] } }
|
|
52
52
|
| undefined;
|
|
53
53
|
|
|
54
|
-
return (i18n?.locales?.value
|
|
55
|
-
typeof entry === 'string' ? entry : entry.code
|
|
56
|
-
);
|
|
54
|
+
return localeCodesOf(i18n?.locales?.value);
|
|
57
55
|
}
|