@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
|
@@ -4,14 +4,33 @@ import type { HTMLAttributes } from 'vue';
|
|
|
4
4
|
import { reactiveOmit } from '@vueuse/core';
|
|
5
5
|
import { TabsTrigger, useForwardProps } from 'reka-ui';
|
|
6
6
|
import { cn } from '@duxt/lib/utils';
|
|
7
|
+
import { duxtPill, type DuxtPillSize } from '../../../utils/pill';
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Three looks, one behaviour.
|
|
11
|
+
*
|
|
12
|
+
* `pill` is the site's own — the code group, the preview and the request
|
|
13
|
+
* samples, drawn by `duxtPill` like every other "pick one of these" strip.
|
|
14
|
+
* `underline` is the prose tab group, which sits in running text rather than in
|
|
15
|
+
* a card. `default` is shadcn's, untouched, for a consumer who wants it.
|
|
16
|
+
*/
|
|
8
17
|
const props = defineProps<
|
|
9
|
-
TabsTriggerProps & {
|
|
18
|
+
TabsTriggerProps & {
|
|
19
|
+
class?: HTMLAttributes['class'];
|
|
20
|
+
variant?: 'default' | 'pill' | 'underline';
|
|
21
|
+
size?: DuxtPillSize;
|
|
22
|
+
}
|
|
10
23
|
>();
|
|
11
24
|
|
|
12
|
-
const delegatedProps = reactiveOmit(props, 'class');
|
|
25
|
+
const delegatedProps = reactiveOmit(props, 'class', 'variant', 'size');
|
|
13
26
|
|
|
14
27
|
const forwardedProps = useForwardProps(delegatedProps);
|
|
28
|
+
|
|
29
|
+
const looks = {
|
|
30
|
+
default: `data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-3 focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
|
|
31
|
+
underline:
|
|
32
|
+
'-mb-px cursor-pointer border-b-2 border-transparent px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[state=active]:border-primary data-[state=active]:text-foreground'
|
|
33
|
+
};
|
|
15
34
|
</script>
|
|
16
35
|
|
|
17
36
|
<template>
|
|
@@ -19,7 +38,9 @@ const forwardedProps = useForwardProps(delegatedProps);
|
|
|
19
38
|
data-slot="tabs-trigger"
|
|
20
39
|
:class="
|
|
21
40
|
cn(
|
|
22
|
-
|
|
41
|
+
props.variant === 'pill'
|
|
42
|
+
? duxtPill('tab', { size: props.size })
|
|
43
|
+
: looks[props.variant ?? 'default'],
|
|
23
44
|
props.class
|
|
24
45
|
)
|
|
25
46
|
"
|
|
@@ -34,7 +34,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|
|
34
34
|
v-bind="{ ...forwarded, ...$attrs }"
|
|
35
35
|
:class="
|
|
36
36
|
cn(
|
|
37
|
-
'bg-
|
|
37
|
+
'bg-neutral-900 text-neutral-50 animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance',
|
|
38
38
|
props.class
|
|
39
39
|
)
|
|
40
40
|
"
|
|
@@ -42,7 +42,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|
|
42
42
|
<slot />
|
|
43
43
|
|
|
44
44
|
<TooltipArrow
|
|
45
|
-
class="
|
|
45
|
+
class="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-xs bg-neutral-900 fill-neutral-900"
|
|
46
46
|
/>
|
|
47
47
|
</TooltipContent>
|
|
48
48
|
</TooltipPortal>
|
|
@@ -24,17 +24,10 @@
|
|
|
24
24
|
* `getBoundingClientRect` per heading, coalesced into an animation frame, so at
|
|
25
25
|
* most one pass per painted frame however fast the wheel turns.
|
|
26
26
|
*/
|
|
27
|
-
export function useActiveHeading(ids: Ref<string[]
|
|
27
|
+
export function useActiveHeading(ids: Ref<string[]>, scrollOffset = 96) {
|
|
28
28
|
const active = ref<string>();
|
|
29
29
|
|
|
30
|
-
/** Where a heading counts as reached: reading position, not the top edge. */
|
|
31
|
-
const TRIGGER = 96;
|
|
32
|
-
|
|
33
|
-
let frame = 0;
|
|
34
|
-
|
|
35
30
|
function measure() {
|
|
36
|
-
frame = 0;
|
|
37
|
-
|
|
38
31
|
const headings = ids.value
|
|
39
32
|
.map((id) => document.getElementById(id))
|
|
40
33
|
.filter((element): element is HTMLElement => Boolean(element));
|
|
@@ -58,33 +51,18 @@ export function useActiveHeading(ids: Ref<string[]>) {
|
|
|
58
51
|
|
|
59
52
|
let current = headings[0]!;
|
|
60
53
|
for (const heading of headings) {
|
|
61
|
-
if (heading.getBoundingClientRect().top >
|
|
54
|
+
if (heading.getBoundingClientRect().top > scrollOffset) break;
|
|
62
55
|
current = heading;
|
|
63
56
|
}
|
|
64
57
|
|
|
65
58
|
active.value = current.id;
|
|
66
59
|
}
|
|
67
60
|
|
|
68
|
-
|
|
69
|
-
if (frame) return;
|
|
70
|
-
frame = requestAnimationFrame(measure);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
onMounted(() => {
|
|
74
|
-
measure();
|
|
75
|
-
window.addEventListener('scroll', schedule, { passive: true });
|
|
76
|
-
window.addEventListener('resize', schedule, { passive: true });
|
|
77
|
-
});
|
|
61
|
+
useDuxtViewportMeasure(measure);
|
|
78
62
|
|
|
79
63
|
// A new page's headings exist only after it renders, and the ids change
|
|
80
64
|
// before the DOM does.
|
|
81
65
|
watch(ids, () => nextTick(measure));
|
|
82
66
|
|
|
83
|
-
onBeforeUnmount(() => {
|
|
84
|
-
if (frame) cancelAnimationFrame(frame);
|
|
85
|
-
window.removeEventListener('scroll', schedule);
|
|
86
|
-
window.removeEventListener('resize', schedule);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
67
|
return active;
|
|
90
68
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report one reader interaction to whoever the site said, if it said anyone.
|
|
3
|
+
*
|
|
4
|
+
* OPT-IN IS THE WHOLE FEATURE, and it is worth saying what that buys. duxt
|
|
5
|
+
* ships no provider, no SDK, no adapter and no script: a site that has not
|
|
6
|
+
* written `duxt.analytics.track` downloads not one byte more than before this
|
|
7
|
+
* existed, and every call below reaches a `typeof track !== 'function'` and
|
|
8
|
+
* stops. There is no queue, no buffer, no storage, no cookie and no consent
|
|
9
|
+
* banner, because duxt never holds the data long enough to need any of them —
|
|
10
|
+
* the site's own function is the only thing that ever leaves the page.
|
|
11
|
+
*
|
|
12
|
+
* That puts three decisions on the side that owns them: WHICH provider, WHETHER
|
|
13
|
+
* to transmit at all, and under what consent. A layer cannot answer any of the
|
|
14
|
+
* three for somebody else's readers — the same reason `DuxtPageFeedback` has an
|
|
15
|
+
* event and no backend, and the footer's legal row ships empty (ADR 0005).
|
|
16
|
+
*
|
|
17
|
+
* WHAT IS NEVER SENT, stated once so it can be checked: no API credential, no
|
|
18
|
+
* request or response body, no concrete request URL, no header, no copied text,
|
|
19
|
+
* no rendered search result, no browser or reader identifier, and no query
|
|
20
|
+
* string or fragment off the page's own path. The event types say the rest —
|
|
21
|
+
* they are a closed union, so what duxt can report is readable in one place.
|
|
22
|
+
*
|
|
23
|
+
* CLIENT ONLY. A build and a prerender call nobody: a static render is not a
|
|
24
|
+
* reader, and a site's analytics must not fill up with pages the crawler
|
|
25
|
+
* opened. `duxt.app` prerenders every page it has, which is exactly the case
|
|
26
|
+
* that would otherwise send hundreds of events from a CI box.
|
|
27
|
+
*/
|
|
28
|
+
export function useDuxtAnalytics() {
|
|
29
|
+
const duxt = useDuxtConfig();
|
|
30
|
+
const path = useDuxtPath();
|
|
31
|
+
const { locale } = useI18n();
|
|
32
|
+
// The source the reader is standing in, not the one a hit came out of — a
|
|
33
|
+
// search result carries its own provenance on the event itself.
|
|
34
|
+
const { source } = useDuxtCollection();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Stamp the context on and hand it over. Returns nothing, on purpose:
|
|
38
|
+
* nothing a call site does should depend on whether reporting worked.
|
|
39
|
+
*/
|
|
40
|
+
function track<Event extends DuxtAnalyticsEventInput>(event: Event): void {
|
|
41
|
+
if (import.meta.server) return;
|
|
42
|
+
|
|
43
|
+
emitDuxtAnalytics(duxt.analytics?.track, {
|
|
44
|
+
...event,
|
|
45
|
+
context: duxtAnalyticsContext(path.value, locale.value, source.value)
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { track };
|
|
50
|
+
}
|
|
@@ -45,15 +45,6 @@ const HEIGHT_STEP = 24;
|
|
|
45
45
|
*/
|
|
46
46
|
const SETTLE_MS = 60;
|
|
47
47
|
|
|
48
|
-
/**
|
|
49
|
-
* How long the content takes to come back once the box is on its way.
|
|
50
|
-
*
|
|
51
|
-
* Shorter than the box's own 300ms on purpose: the point is to be readable
|
|
52
|
-
* again well before the box stops moving, not to make the reader wait out two
|
|
53
|
-
* animations in a row.
|
|
54
|
-
*/
|
|
55
|
-
const FADE_MS = 200;
|
|
56
|
-
|
|
57
48
|
export function useDuxtAnimatedHeight(
|
|
58
49
|
shell: Readonly<import('vue').ShallowRef<HTMLElement | null>>,
|
|
59
50
|
body: Readonly<import('vue').ShallowRef<HTMLElement | null>>,
|
|
@@ -69,10 +60,10 @@ export function useDuxtAnimatedHeight(
|
|
|
69
60
|
|
|
70
61
|
// The box carries the border and `box-sizing: border-box`, so its height
|
|
71
62
|
// has to include what that border takes; the content's height does not.
|
|
72
|
-
const
|
|
63
|
+
const borderHeight = box.offsetHeight - box.clientHeight;
|
|
73
64
|
|
|
74
65
|
const step = (height: number) =>
|
|
75
|
-
Math.ceil((height +
|
|
66
|
+
Math.ceil((height + borderHeight) / HEIGHT_STEP) * HEIGHT_STEP;
|
|
76
67
|
|
|
77
68
|
let last = step(content.offsetHeight);
|
|
78
69
|
|
|
@@ -90,28 +81,16 @@ export function useDuxtAnimatedHeight(
|
|
|
90
81
|
|
|
91
82
|
let settle: ReturnType<typeof setTimeout> | undefined;
|
|
92
83
|
|
|
93
|
-
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
94
|
-
|
|
95
84
|
const observer = new ResizeObserver(() => {
|
|
96
85
|
const next = step(content.offsetHeight);
|
|
97
86
|
if (next === last) return;
|
|
98
87
|
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
// there is nothing cut off to see: what a reader watches is one sample
|
|
106
|
-
// replacing another, not a box catching up with its contents.
|
|
107
|
-
//
|
|
108
|
-
// Set with the transition off, so it is instant going out and animated
|
|
109
|
-
// coming back — a fade in both directions would be twice as long a wait
|
|
110
|
-
// for the same effect.
|
|
111
|
-
if (!reduced.matches) {
|
|
112
|
-
content.style.transition = 'none';
|
|
113
|
-
content.style.opacity = '0';
|
|
114
|
-
}
|
|
88
|
+
// NOTHING FADES. The content used to be hidden while the box travelled and
|
|
89
|
+
// faded back in as it arrived, so a taller file was never seen cut off.
|
|
90
|
+
// What a reader saw instead was the block emptying first and filling
|
|
91
|
+
// again — the code gone for the length of the fade on every tab change.
|
|
92
|
+
// Clipped by `overflow-hidden` and revealed as the box opens, the new file
|
|
93
|
+
// is there the whole time.
|
|
115
94
|
|
|
116
95
|
// ONLY THE VALUE IT SETTLES ON, never the ones on the way.
|
|
117
96
|
//
|
|
@@ -131,13 +110,6 @@ export function useDuxtAnimatedHeight(
|
|
|
131
110
|
last = settled;
|
|
132
111
|
box.style.height = `${settled}px`;
|
|
133
112
|
}
|
|
134
|
-
|
|
135
|
-
if (!reduced.matches) {
|
|
136
|
-
requestAnimationFrame(() => {
|
|
137
|
-
content.style.transition = `opacity ${FADE_MS}ms ease-out`;
|
|
138
|
-
content.style.opacity = '1';
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
113
|
}, SETTLE_MS);
|
|
142
114
|
});
|
|
143
115
|
|
|
@@ -148,8 +120,6 @@ export function useDuxtAnimatedHeight(
|
|
|
148
120
|
clearTimeout(settle);
|
|
149
121
|
box.style.height = '';
|
|
150
122
|
box.style.transition = '';
|
|
151
|
-
content.style.opacity = '';
|
|
152
|
-
content.style.transition = '';
|
|
153
123
|
});
|
|
154
124
|
});
|
|
155
125
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/** Where the dismissals are kept. Namespaced so a consumer's keys cannot collide. */
|
|
2
|
+
const STORAGE_KEY = 'duxt:announcements';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The one timer the whole page needs, at module scope.
|
|
6
|
+
*
|
|
7
|
+
* Three placements mean up to three components asking the same question — when
|
|
8
|
+
* does the next window open or close — and three timers firing at the same
|
|
9
|
+
* millisecond to set the same value. Module scope is safe here because every
|
|
10
|
+
* write below sits behind `import.meta.client`: in the browser the module is
|
|
11
|
+
* one app for one page load, and on the server it is never touched at all.
|
|
12
|
+
*/
|
|
13
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
14
|
+
let scheduledFor: number | undefined;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The announcements a placement should draw, and how a reader closes one.
|
|
18
|
+
*
|
|
19
|
+
* TWO configs are read, deliberately. The RESOLVED one is what the banner
|
|
20
|
+
* prints — `useDuxtConfig()` has collapsed each `text` to the reader's own
|
|
21
|
+
* language. The WRITTEN one is what a dismissal is remembered against, because
|
|
22
|
+
* a derived identity has to survive a language change, and a sentence resolved
|
|
23
|
+
* for `de-DE` is a different string from the same sentence resolved for
|
|
24
|
+
* `en-GB`. The two lists are the same list in the same order, so an index
|
|
25
|
+
* addresses the same announcement in both.
|
|
26
|
+
*/
|
|
27
|
+
export function useDuxtAnnouncements(placement: DuxtAnnouncementPlacement) {
|
|
28
|
+
const appConfig = useAppConfig();
|
|
29
|
+
const duxt = useDuxtConfig();
|
|
30
|
+
|
|
31
|
+
/** As the consumer wrote it: every locale of every sentence, still together. */
|
|
32
|
+
const written = computed<DuxtAnnouncement[]>(
|
|
33
|
+
() => mergeDuxtConfig(appConfig.duxt, duxtDefaults).announcements ?? []
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The reader's clock, shared so the three placements never disagree about
|
|
38
|
+
* what time it is mid-render.
|
|
39
|
+
*
|
|
40
|
+
* Seeded at `0` rather than at `Date.now()`: `0` is before every window a
|
|
41
|
+
* consumer would write, so an announcement with a `startsAt` cannot flash
|
|
42
|
+
* during hydration before the real time arrives.
|
|
43
|
+
*/
|
|
44
|
+
const now = useState<number>('duxt-announcements-now', () => 0);
|
|
45
|
+
|
|
46
|
+
/** The keys this reader has closed. Empty until the browser says otherwise. */
|
|
47
|
+
const dismissed = useState<string[]>(
|
|
48
|
+
'duxt-announcements-dismissed',
|
|
49
|
+
() => []
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
function read(): string[] {
|
|
53
|
+
// Blocked storage, or a value someone else wrote — either way, nothing has
|
|
54
|
+
// been dismissed and every due announcement shows.
|
|
55
|
+
const parsed = readStoredJson(STORAGE_KEY);
|
|
56
|
+
|
|
57
|
+
return Array.isArray(parsed)
|
|
58
|
+
? parsed.filter((entry): entry is string => typeof entry === 'string')
|
|
59
|
+
: [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Keep only the keys the config still names.
|
|
64
|
+
*
|
|
65
|
+
* Without it the list grows for the life of the browser profile: every
|
|
66
|
+
* announcement a site ever ran leaves a key behind, and none of them can ever
|
|
67
|
+
* match anything again — a derived key changes with its sentence, and a
|
|
68
|
+
* retired `id` is gone from the config.
|
|
69
|
+
*/
|
|
70
|
+
function persist(keys: string[]) {
|
|
71
|
+
const live = new Set(written.value.map(duxtAnnouncementKey));
|
|
72
|
+
const next = keys.filter((key) => live.has(key));
|
|
73
|
+
|
|
74
|
+
dismissed.value = next;
|
|
75
|
+
|
|
76
|
+
// A dismissal that cannot be remembered still hides the banner for this
|
|
77
|
+
// page. Not worth an error the reader would have to read.
|
|
78
|
+
writeStoredJson(STORAGE_KEY, next);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Wake up exactly once, at the next moment the list changes.
|
|
83
|
+
*
|
|
84
|
+
* Not a poll: an announcement scheduled for next Tuesday costs one sleeping
|
|
85
|
+
* timer rather than a tick every second for six days. `setTimeout` overflows
|
|
86
|
+
* past 2^31 ms and fires immediately, so a longer wait is taken in day-long
|
|
87
|
+
* steps — each one simply re-asks the same question.
|
|
88
|
+
*/
|
|
89
|
+
function schedule() {
|
|
90
|
+
if (!import.meta.client) return;
|
|
91
|
+
|
|
92
|
+
const next = duxtNextAnnouncementChange(written.value, now.value);
|
|
93
|
+
|
|
94
|
+
if (next === undefined) {
|
|
95
|
+
scheduledFor = undefined;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (timer !== undefined && scheduledFor === next) return;
|
|
100
|
+
|
|
101
|
+
const DAY = 86_400_000;
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
scheduledFor = next;
|
|
104
|
+
timer = setTimeout(
|
|
105
|
+
() => {
|
|
106
|
+
timer = undefined;
|
|
107
|
+
scheduledFor = undefined;
|
|
108
|
+
now.value = Date.now();
|
|
109
|
+
schedule();
|
|
110
|
+
},
|
|
111
|
+
Math.min(Math.max(next - Date.now(), 0), DAY)
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
onMounted(() => {
|
|
116
|
+
now.value = Date.now();
|
|
117
|
+
persist(read());
|
|
118
|
+
schedule();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const visible = computed(() => {
|
|
122
|
+
const due = new Set(
|
|
123
|
+
duxtVisibleAnnouncements(written.value, {
|
|
124
|
+
placement,
|
|
125
|
+
now: now.value,
|
|
126
|
+
dismissed: dismissed.value,
|
|
127
|
+
maxVisible: duxt.announcementOptions?.maxVisible
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Paired back to the resolved list by POSITION: `resolveDuxtTexts` maps an
|
|
132
|
+
// array one to one, so index `n` is the same announcement in both — which
|
|
133
|
+
// is what lets the identity come from the written text and the words on
|
|
134
|
+
// screen from the resolved one.
|
|
135
|
+
return written.value.flatMap((announcement, index) =>
|
|
136
|
+
due.has(announcement)
|
|
137
|
+
? [
|
|
138
|
+
{
|
|
139
|
+
key: duxtAnnouncementKey(announcement),
|
|
140
|
+
text: duxt.announcements?.[index]?.text ?? '',
|
|
141
|
+
link: duxt.announcements?.[index]?.link
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
: []
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
function dismiss(key: string) {
|
|
149
|
+
persist([...dismissed.value, key]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { visible, dismiss };
|
|
153
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy a string to the clipboard, and say whether it worked.
|
|
3
|
+
*
|
|
4
|
+
* Every copy control on the site does the same four things — write, confirm
|
|
5
|
+
* with a toast, show a check for two seconds, report a refusal — and seven of
|
|
6
|
+
* them used to spell it out. The spellings had drifted: two reported a failure
|
|
7
|
+
* in English whatever the reader's language, one never confirmed a success at
|
|
8
|
+
* all, and none cleared its timer, so two copies inside two seconds reset the
|
|
9
|
+
* icon early.
|
|
10
|
+
*
|
|
11
|
+
* Returns whether the text reached the clipboard, which is what a caller
|
|
12
|
+
* reporting the copy needs: an analytics event for a copy the clipboard
|
|
13
|
+
* refused would be a copy that never happened.
|
|
14
|
+
*
|
|
15
|
+
* `toast: false` for a control whose own label already changes to "Copied" —
|
|
16
|
+
* a second confirmation in the corner says the same thing twice.
|
|
17
|
+
*/
|
|
18
|
+
export function useDuxtCopy({
|
|
19
|
+
toast = true,
|
|
20
|
+
failureKey = 'duxt.code.copyFailed'
|
|
21
|
+
}: { toast?: boolean; failureKey?: string } = {}) {
|
|
22
|
+
const copied = ref(false);
|
|
23
|
+
const notify = useDuxtToast();
|
|
24
|
+
const { t } = useI18n();
|
|
25
|
+
|
|
26
|
+
let reset: ReturnType<typeof setTimeout> | undefined;
|
|
27
|
+
|
|
28
|
+
async function copy(text: string | undefined): Promise<boolean> {
|
|
29
|
+
if (!text) return false;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
await navigator.clipboard.writeText(text);
|
|
33
|
+
} catch {
|
|
34
|
+
// Unavailable over plain HTTP, or refused by the browser.
|
|
35
|
+
notify.error(t(failureKey));
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
copied.value = true;
|
|
40
|
+
if (toast) notify.success(t('duxt.code.copiedToast'));
|
|
41
|
+
|
|
42
|
+
clearTimeout(reset);
|
|
43
|
+
reset = setTimeout(() => (copied.value = false), 2000);
|
|
44
|
+
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
onScopeDispose(() => clearTimeout(reset));
|
|
49
|
+
|
|
50
|
+
return { copied, copy };
|
|
51
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { localeDirection } from '@duxt/utils/direction';
|
|
2
|
+
import type { DuxtDirection } from '@duxt/utils/direction';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Which way round this page is drawn.
|
|
6
|
+
*
|
|
7
|
+
* Two shells need the answer and neither can borrow it from the other:
|
|
8
|
+
* `error.vue` REPLACES `app.vue` rather than nesting inside it, which is how
|
|
9
|
+
* the error page came to set `lang` and never `dir`. One composable, both
|
|
10
|
+
* shells, and the layer's one statement about direction.
|
|
11
|
+
*
|
|
12
|
+
* WHAT THE ANSWER IS FOR is wider than the `<html>` attribute. reka-ui reads
|
|
13
|
+
* its direction from a `ConfigProvider` and from nothing else — `useDirection`
|
|
14
|
+
* injects that context and defaults to `ltr`, never looking at the document —
|
|
15
|
+
* so a menu, a select and a tooltip stay left-to-right under a right-to-left
|
|
16
|
+
* page unless the same value is handed to the provider. That is the difference
|
|
17
|
+
* between a theme that CAN be mirrored and one that merely says it is.
|
|
18
|
+
*/
|
|
19
|
+
export function useDuxtDirection(): ComputedRef<DuxtDirection> {
|
|
20
|
+
const { locale, locales } = useI18n();
|
|
21
|
+
|
|
22
|
+
return computed(() => localeDirection(locales.value, locale.value));
|
|
23
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ContentNavigationItem } from '@nuxt/content';
|
|
2
|
+
import { tfplugindocsNavigation } from '../../tfplugindocs';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Which collections the navigation is being fetched from.
|
|
@@ -22,7 +23,11 @@ import type { ContentNavigationItem } from '@nuxt/content';
|
|
|
22
23
|
const useNavigationSource = () =>
|
|
23
24
|
useState('duxt-navigation-source', () => ({
|
|
24
25
|
base: 'docs' as DuxtCollectionName,
|
|
25
|
-
translation: 'docs' as DuxtCollectionName
|
|
26
|
+
translation: 'docs' as DuxtCollectionName,
|
|
27
|
+
// The base source's tfplugindocs prefix, or `undefined` for any other
|
|
28
|
+
// flavour. Resolved in `useDuxtNavigation`, never in the handler — see
|
|
29
|
+
// the note there.
|
|
30
|
+
tfplugindocsPrefix: undefined as string | undefined
|
|
26
31
|
}));
|
|
27
32
|
|
|
28
33
|
/**
|
|
@@ -35,19 +40,28 @@ const useNavigationSource = () =>
|
|
|
35
40
|
* see `overlayTranslations`.
|
|
36
41
|
*/
|
|
37
42
|
const handler = async (): Promise<ContentNavigationItem[]> => {
|
|
38
|
-
// Read BEFORE the first await: after it, the route may have moved on
|
|
39
|
-
|
|
43
|
+
// Read BEFORE the first await: after it, the route may have moved on — and
|
|
44
|
+
// the Nuxt instance is gone. NOTHING below the first await may call a
|
|
45
|
+
// composable: `useDuxtConfig()` sat under it once, threw E1001 on every
|
|
46
|
+
// request, and the handler's error left every page without a sidebar.
|
|
47
|
+
// `tests/async-data-context.test.ts` holds that line.
|
|
48
|
+
const { base, translation, tfplugindocsPrefix } = useNavigationSource().value;
|
|
40
49
|
|
|
41
50
|
const tree = await queryCollectionNavigation(base as DuxtCollectionArg, [
|
|
42
51
|
'icon',
|
|
43
|
-
'description'
|
|
52
|
+
'description',
|
|
53
|
+
'subcategory'
|
|
44
54
|
]);
|
|
45
55
|
|
|
46
56
|
// Before the overlay: a folder's title comes from its own index page, and a
|
|
47
57
|
// translated index has to be able to carry that up with it.
|
|
48
58
|
const named = titleFoldersFromIndex(tree);
|
|
59
|
+
const navigable =
|
|
60
|
+
tfplugindocsPrefix === undefined
|
|
61
|
+
? named
|
|
62
|
+
: tfplugindocsNavigation(named, tfplugindocsPrefix);
|
|
49
63
|
|
|
50
|
-
if (translation === base) return
|
|
64
|
+
if (translation === base) return navigable;
|
|
51
65
|
|
|
52
66
|
const translated = await queryCollection(translation as DuxtCollectionArg)
|
|
53
67
|
.select('path', 'title', 'description')
|
|
@@ -55,7 +69,7 @@ const handler = async (): Promise<ContentNavigationItem[]> => {
|
|
|
55
69
|
|
|
56
70
|
return titleFoldersFromIndex(
|
|
57
71
|
overlayTranslations(
|
|
58
|
-
|
|
72
|
+
navigable,
|
|
59
73
|
new Map(translated.map((page) => [page.path, page]))
|
|
60
74
|
)
|
|
61
75
|
);
|
|
@@ -68,14 +82,20 @@ const handler = async (): Promise<ContentNavigationItem[]> => {
|
|
|
68
82
|
* one cache entry, and neither may two languages of one.
|
|
69
83
|
*/
|
|
70
84
|
export function useDuxtNavigation() {
|
|
71
|
-
const { collection, baseCollection } = useDuxtCollection();
|
|
85
|
+
const { collection, baseCollection, sources } = useDuxtCollection();
|
|
72
86
|
const source = useNavigationSource();
|
|
73
87
|
|
|
74
88
|
// Kept in step before every fetch, including the ones `watch` triggers.
|
|
75
89
|
watchEffect(() => {
|
|
90
|
+
const base = sources.value.find(
|
|
91
|
+
(entry) => entry.collection === baseCollection.value
|
|
92
|
+
);
|
|
93
|
+
|
|
76
94
|
source.value = {
|
|
77
95
|
base: baseCollection.value,
|
|
78
|
-
translation: collection.value
|
|
96
|
+
translation: collection.value,
|
|
97
|
+
tfplugindocsPrefix:
|
|
98
|
+
base?.flavor === 'tfplugindocs' ? base.prefix : undefined
|
|
79
99
|
};
|
|
80
100
|
});
|
|
81
101
|
|
|
@@ -8,11 +8,7 @@ export function useDuxtPath() {
|
|
|
8
8
|
const route = useRoute();
|
|
9
9
|
const { locales } = useI18n();
|
|
10
10
|
|
|
11
|
-
const codes = computed(() =>
|
|
12
|
-
locales.value.map((entry) =>
|
|
13
|
-
typeof entry === 'string' ? entry : entry.code
|
|
14
|
-
)
|
|
15
|
-
);
|
|
11
|
+
const codes = computed(() => localeCodesOf(locales.value));
|
|
16
12
|
|
|
17
13
|
return computed(() => stripLocalePrefix(route.path, codes.value));
|
|
18
14
|
}
|
|
@@ -30,6 +26,8 @@ export function useDuxtLink() {
|
|
|
30
26
|
|
|
31
27
|
return (to?: string) => {
|
|
32
28
|
if (!to?.startsWith('/')) return to;
|
|
29
|
+
// A root server route has no locale variant — see `isMachineRoute`.
|
|
30
|
+
if (isMachineRoute(to)) return to;
|
|
33
31
|
|
|
34
32
|
// localePath resolves a route, and a trailing anchor is not part of one.
|
|
35
33
|
// Split it off and put it back, so a sidebar entry pointing at a heading
|