@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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The requests in one folder, as the list a reader scans.
|
|
4
|
+
*
|
|
5
|
+
* MDC: `::bruno-requests` on a folder's index page. The method is a coloured
|
|
6
|
+
* chip and the URL is the line beside it, because "find this request" is the
|
|
7
|
+
* search a reader actually performs — a list of names alone makes them open
|
|
8
|
+
* every one to find the `DELETE`.
|
|
9
|
+
*
|
|
10
|
+
* FOLDERS FIRST, THEN REQUESTS, which is Bruno's own nesting: a folder holds
|
|
11
|
+
* both, and OpenAPI's flat tags have no counterpart for it. Rendering the
|
|
12
|
+
* sub-folders as a second list rather than flattening them is what keeps the
|
|
13
|
+
* page the same shape as the collection on disk.
|
|
14
|
+
*/
|
|
15
|
+
defineProps<{
|
|
16
|
+
requests?: { name: string; method: string; url: string; to: string }[];
|
|
17
|
+
groups?: {
|
|
18
|
+
name: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
requests?: number;
|
|
21
|
+
to: string;
|
|
22
|
+
}[];
|
|
23
|
+
}>();
|
|
24
|
+
|
|
25
|
+
const localeLink = useDuxtLink();
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<div>
|
|
30
|
+
<!-- Outside `not-typeset`: the folder's own `docs` block is Markdown and
|
|
31
|
+
wants the typeset preset; the lists below emphatically do not. -->
|
|
32
|
+
<slot />
|
|
33
|
+
|
|
34
|
+
<section v-if="groups?.length" class="not-typeset mt-6">
|
|
35
|
+
<h2 class="duxt-label">{{ $t('duxt.bruno.folders') }}</h2>
|
|
36
|
+
|
|
37
|
+
<ul class="mt-3 -mx-3">
|
|
38
|
+
<li
|
|
39
|
+
v-for="group in groups"
|
|
40
|
+
:key="group.to"
|
|
41
|
+
class="border-t border-border/60 first:border-t-0"
|
|
42
|
+
>
|
|
43
|
+
<NuxtLink
|
|
44
|
+
:to="localeLink(group.to) ?? group.to"
|
|
45
|
+
class="group flex items-start gap-3 rounded-lg px-3 py-3 transition-colors hover:bg-muted/40"
|
|
46
|
+
>
|
|
47
|
+
<span class="min-w-0 flex-1">
|
|
48
|
+
<span class="flex flex-wrap items-baseline gap-x-3">
|
|
49
|
+
<span class="font-medium">{{ group.name }}</span>
|
|
50
|
+
<span class="text-xs text-muted-foreground tabular-nums">
|
|
51
|
+
{{ group.requests }}
|
|
52
|
+
</span>
|
|
53
|
+
</span>
|
|
54
|
+
|
|
55
|
+
<span
|
|
56
|
+
v-if="group.description"
|
|
57
|
+
class="mt-1 line-clamp-2 block text-sm text-muted-foreground"
|
|
58
|
+
>
|
|
59
|
+
{{ group.description }}
|
|
60
|
+
</span>
|
|
61
|
+
</span>
|
|
62
|
+
|
|
63
|
+
<Icon
|
|
64
|
+
name="lucide:arrow-right"
|
|
65
|
+
aria-hidden="true"
|
|
66
|
+
class="mt-1 size-3.5 shrink-0 text-muted-foreground rtl:-scale-x-100 transition-transform ltr:group-hover:translate-x-0.5 rtl:group-hover:-translate-x-0.5"
|
|
67
|
+
/>
|
|
68
|
+
</NuxtLink>
|
|
69
|
+
</li>
|
|
70
|
+
</ul>
|
|
71
|
+
</section>
|
|
72
|
+
|
|
73
|
+
<section v-if="requests?.length" class="not-typeset mt-6">
|
|
74
|
+
<h2 class="duxt-label">{{ $t('duxt.bruno.requests') }}</h2>
|
|
75
|
+
|
|
76
|
+
<ul class="mt-3 -mx-3">
|
|
77
|
+
<li
|
|
78
|
+
v-for="entry in requests"
|
|
79
|
+
:key="entry.to"
|
|
80
|
+
class="border-t border-border/60 first:border-t-0"
|
|
81
|
+
>
|
|
82
|
+
<NuxtLink
|
|
83
|
+
:to="localeLink(entry.to) ?? entry.to"
|
|
84
|
+
class="flex flex-wrap items-center gap-3 rounded-lg px-3 py-3 transition-colors hover:bg-muted/40"
|
|
85
|
+
>
|
|
86
|
+
<DuxtOpenApiMethod :method="entry.method" class="w-16" />
|
|
87
|
+
|
|
88
|
+
<span class="font-medium sm:me-auto">{{ entry.name }}</span>
|
|
89
|
+
|
|
90
|
+
<!-- The auto margin is on the NAME above, not here: this is a
|
|
91
|
+
`<code>`, which the stylesheet pins to left-to-right, and a
|
|
92
|
+
`ms-auto` resolved against that would push the URL back to the
|
|
93
|
+
start of a right-to-left row. -->
|
|
94
|
+
<code
|
|
95
|
+
class="w-full font-mono text-xs break-all text-muted-foreground sm:w-auto"
|
|
96
|
+
>
|
|
97
|
+
{{ entry.url }}
|
|
98
|
+
</code>
|
|
99
|
+
</NuxtLink>
|
|
100
|
+
</li>
|
|
101
|
+
</ul>
|
|
102
|
+
</section>
|
|
103
|
+
</div>
|
|
104
|
+
</template>
|
|
@@ -12,20 +12,20 @@ const props = withDefaults(
|
|
|
12
12
|
);
|
|
13
13
|
|
|
14
14
|
const variants = {
|
|
15
|
-
info: { icon: 'lucide:info', rule: 'border-
|
|
15
|
+
info: { icon: 'lucide:info', rule: 'border-s-sky-500', text: 'text-sky-500' },
|
|
16
16
|
tip: {
|
|
17
17
|
icon: 'lucide:lightbulb',
|
|
18
|
-
rule: 'border-
|
|
18
|
+
rule: 'border-s-emerald-500',
|
|
19
19
|
text: 'text-emerald-500'
|
|
20
20
|
},
|
|
21
21
|
warning: {
|
|
22
22
|
icon: 'lucide:triangle-alert',
|
|
23
|
-
rule: 'border-
|
|
23
|
+
rule: 'border-s-amber-500',
|
|
24
24
|
text: 'text-amber-500'
|
|
25
25
|
},
|
|
26
26
|
danger: {
|
|
27
27
|
icon: 'lucide:octagon-alert',
|
|
28
|
-
rule: 'border-
|
|
28
|
+
rule: 'border-s-red-500',
|
|
29
29
|
text: 'text-red-500'
|
|
30
30
|
}
|
|
31
31
|
};
|
|
@@ -35,7 +35,7 @@ const variant = computed(() => variants[props.type]);
|
|
|
35
35
|
|
|
36
36
|
<template>
|
|
37
37
|
<UiAlert
|
|
38
|
-
class="my-5 flex items-start gap-2.5 rounded-md border border-
|
|
38
|
+
class="my-5 flex items-start gap-2.5 rounded-md border border-s-2 bg-muted/30 px-3 py-2.5"
|
|
39
39
|
:class="variant.rule"
|
|
40
40
|
>
|
|
41
41
|
<Icon
|
|
@@ -29,6 +29,15 @@ const props = withDefaults(
|
|
|
29
29
|
date?: string;
|
|
30
30
|
to: string;
|
|
31
31
|
groups?: { name: string; count: number }[];
|
|
32
|
+
/**
|
|
33
|
+
* Who wrote the commits this release carries, most commits first.
|
|
34
|
+
*
|
|
35
|
+
* Written by `sections-changelog.ts` out of the repository's own tags,
|
|
36
|
+
* and absent wherever the build could not read them — a downloaded
|
|
37
|
+
* source, a release with no tag yet, a changelog kept by hand. A name
|
|
38
|
+
* and, where git carried one, a handle; never an address.
|
|
39
|
+
*/
|
|
40
|
+
contributors?: { name: string; username?: string }[];
|
|
32
41
|
}[];
|
|
33
42
|
}>(),
|
|
34
43
|
{ releases: () => [] }
|
|
@@ -36,6 +45,18 @@ const props = withDefaults(
|
|
|
36
45
|
|
|
37
46
|
const { locale } = useI18n();
|
|
38
47
|
const localeLink = useDuxtLink();
|
|
48
|
+
const duxt = useDuxtConfig();
|
|
49
|
+
|
|
50
|
+
// `DuxtText` also permits a record of strings, so the resolved-config mapped
|
|
51
|
+
// type narrows this all-optional object too far. It remains an object at
|
|
52
|
+
// runtime; only its template is configurable. The same cast `DuxtPageInfo`
|
|
53
|
+
// makes, over the same key, for the same reason.
|
|
54
|
+
const contributorConfig = duxt.contributors as unknown as
|
|
55
|
+
| { avatarUrl?: string }
|
|
56
|
+
| undefined;
|
|
57
|
+
|
|
58
|
+
const avatar = (username?: string) =>
|
|
59
|
+
contributorAvatar(contributorConfig?.avatarUrl, username);
|
|
39
60
|
|
|
40
61
|
/** Every group name the file used, in the order it first used it. */
|
|
41
62
|
const names = computed(() => {
|
|
@@ -93,10 +114,34 @@ function toggle(name: string) {
|
|
|
93
114
|
* The release day, in the reader's language — shared with the meta row a
|
|
94
115
|
* release page draws, so one release cannot be dated two different ways.
|
|
95
116
|
*/
|
|
96
|
-
const formatted = (value?: string) =>
|
|
117
|
+
const formatted = (value?: string) =>
|
|
118
|
+
formatDate(value, locale.value, { calendarDay: true });
|
|
97
119
|
/** How many groups a row names before it starts counting the rest. */
|
|
98
120
|
const SHOWN_GROUPS = 3;
|
|
99
121
|
|
|
122
|
+
/**
|
|
123
|
+
* How many faces a row shows before it starts counting the rest.
|
|
124
|
+
*
|
|
125
|
+
* Fewer than the groups, because these are pictures rather than words: four
|
|
126
|
+
* overlapping circles read as "a few people" at a glance, where eight read as a
|
|
127
|
+
* second column and push the date off the line.
|
|
128
|
+
*/
|
|
129
|
+
const SHOWN_CONTRIBUTORS = 4;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* One person's identity ACROSS releases.
|
|
133
|
+
*
|
|
134
|
+
* The build dedupes by email, which is the only thing that actually identifies
|
|
135
|
+
* a committer — and then deliberately does not ship it (see
|
|
136
|
+
* `git-contributors.ts`). What is left to count across releases is the handle,
|
|
137
|
+
* or the name where git carried no handle. Two people with the same display
|
|
138
|
+
* name and neither of them on GitHub therefore count as one; they are also
|
|
139
|
+
* indistinguishable to the reader, so the figure says exactly what the page
|
|
140
|
+
* shows.
|
|
141
|
+
*/
|
|
142
|
+
const identity = (person: { name: string; username?: string }) =>
|
|
143
|
+
person.username ?? person.name;
|
|
144
|
+
|
|
100
145
|
/**
|
|
101
146
|
* The whole history in two numbers, over the list rather than in it.
|
|
102
147
|
*
|
|
@@ -118,14 +163,16 @@ const stats = computed(() => ({
|
|
|
118
163
|
all + (release.groups ?? []).reduce((sum, group) => sum + group.count, 0),
|
|
119
164
|
0
|
|
120
165
|
),
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
166
|
+
// WHO, not what kind of. This figure used to count the group names the file
|
|
167
|
+
// used, which described the changelog rather than the project — and a
|
|
168
|
+
// documentation site's first question about a project it is weighing up is
|
|
169
|
+
// how many people are behind it. The kinds are still on the page: they are
|
|
170
|
+
// what the filter offers and what every row names.
|
|
171
|
+
//
|
|
172
|
+
// Counted over what is SHOWN, like every figure here, so filtering to
|
|
173
|
+
// breaking changes answers "who has ever shipped one".
|
|
174
|
+
contributors: new Set(
|
|
175
|
+
shown.value.flatMap((release) => (release.contributors ?? []).map(identity))
|
|
129
176
|
).size,
|
|
130
177
|
// The last day something shipped — the one figure here that is not a count,
|
|
131
178
|
// and the one a reader checks first on a project they are considering.
|
|
@@ -137,8 +184,8 @@ const latest = (release: { to: string }) =>
|
|
|
137
184
|
release.to === props.releases[0]?.to;
|
|
138
185
|
|
|
139
186
|
/**
|
|
140
|
-
* The rows as they are drawn: a release, the groups it NAMES,
|
|
141
|
-
* leaves.
|
|
187
|
+
* The rows as they are drawn: a release, the groups it NAMES, the people it
|
|
188
|
+
* SHOWS, and how many of each it leaves.
|
|
142
189
|
*
|
|
143
190
|
* The whole list was the row before this, and on a release that touched
|
|
144
191
|
* everything that was twelve names over two lines — twelve facts on a page
|
|
@@ -154,6 +201,10 @@ const latest = (release: { to: string }) =>
|
|
|
154
201
|
* A FILTERED row names what it was filtered by first. Otherwise selecting
|
|
155
202
|
* "Tests" leaves a release matching on a group its row does not mention, which
|
|
156
203
|
* reads as a filter that let the wrong page through.
|
|
204
|
+
*
|
|
205
|
+
* THE PEOPLE ARE NOT RE-SORTED. The build already ordered them by how much of
|
|
206
|
+
* the release each one wrote, and nothing on this page filters by person — so
|
|
207
|
+
* the cap takes the front of a list that already means something.
|
|
157
208
|
*/
|
|
158
209
|
const rows = computed(() =>
|
|
159
210
|
shown.value.map((release) => {
|
|
@@ -165,10 +216,14 @@ const rows = computed(() =>
|
|
|
165
216
|
return chosen || b.count - a.count;
|
|
166
217
|
});
|
|
167
218
|
|
|
219
|
+
const people = release.contributors ?? [];
|
|
220
|
+
|
|
168
221
|
return {
|
|
169
222
|
release,
|
|
170
223
|
named: groups.slice(0, SHOWN_GROUPS),
|
|
171
|
-
rest: Math.max(0, groups.length - SHOWN_GROUPS)
|
|
224
|
+
rest: Math.max(0, groups.length - SHOWN_GROUPS),
|
|
225
|
+
people: people.slice(0, SHOWN_CONTRIBUTORS),
|
|
226
|
+
others: Math.max(0, people.length - SHOWN_CONTRIBUTORS)
|
|
172
227
|
};
|
|
173
228
|
})
|
|
174
229
|
);
|
|
@@ -249,7 +304,7 @@ const rows = computed(() =>
|
|
|
249
304
|
/>
|
|
250
305
|
<span class="truncate">{{ changelogLabel(name) }}</span>
|
|
251
306
|
<span
|
|
252
|
-
class="
|
|
307
|
+
class="ms-auto ps-2 text-xs text-muted-foreground tabular-nums"
|
|
253
308
|
>
|
|
254
309
|
{{ totals[name] }}
|
|
255
310
|
</span>
|
|
@@ -369,15 +424,20 @@ const rows = computed(() =>
|
|
|
369
424
|
</dt>
|
|
370
425
|
</div>
|
|
371
426
|
|
|
372
|
-
|
|
427
|
+
<!-- Dropped rather than shown as a nought wherever the build could not
|
|
428
|
+
read a history — a downloaded source, a changelog whose versions
|
|
429
|
+
were never tagged. "0 contributors" is a claim about the project;
|
|
430
|
+
an absent figure is a claim about this build, which is the true
|
|
431
|
+
one. Same rule as the date beside it. -->
|
|
432
|
+
<div v-if="stats.contributors" class="flex flex-col gap-1">
|
|
373
433
|
<dd
|
|
374
434
|
class="text-2xl font-semibold tracking-tight tabular-nums sm:text-3xl"
|
|
375
435
|
>
|
|
376
|
-
{{ stats.
|
|
436
|
+
{{ stats.contributors }}
|
|
377
437
|
</dd>
|
|
378
438
|
<dt class="flex items-center gap-1.5 text-xs font-medium text-primary">
|
|
379
|
-
<Icon name="lucide:
|
|
380
|
-
{{ $t('duxt.changelog.
|
|
439
|
+
<Icon name="lucide:users" class="size-3.5" />
|
|
440
|
+
{{ $t('duxt.changelog.contributors') }}
|
|
381
441
|
</dt>
|
|
382
442
|
</div>
|
|
383
443
|
|
|
@@ -434,7 +494,7 @@ const rows = computed(() =>
|
|
|
434
494
|
move-class="transition-transform duration-200 ease-out motion-reduce:transition-none"
|
|
435
495
|
>
|
|
436
496
|
<li
|
|
437
|
-
v-for="{ release, named, rest } in rows"
|
|
497
|
+
v-for="{ release, named, rest, people, others } in rows"
|
|
438
498
|
:key="release.to"
|
|
439
499
|
class="border-t border-border/60 first:border-t-0"
|
|
440
500
|
>
|
|
@@ -478,40 +538,101 @@ const rows = computed(() =>
|
|
|
478
538
|
</time>
|
|
479
539
|
</span>
|
|
480
540
|
|
|
481
|
-
|
|
482
|
-
|
|
541
|
+
<!-- WHAT the release carried and WHO carried it, in one cell. They
|
|
542
|
+
share the summary column rather than taking a track each: a
|
|
543
|
+
fifth grid column would hold an empty gutter on every release
|
|
544
|
+
whose contributors this build could not read, and the faces
|
|
545
|
+
belong beside the summary they are the other half of. `flex-wrap`
|
|
546
|
+
lets the people drop under the groups on a narrow row instead of
|
|
547
|
+
squeezing the line. -->
|
|
548
|
+
<div
|
|
549
|
+
class="col-span-2 row-start-2 flex flex-wrap items-center gap-x-2 gap-y-1 sm:col-span-1 sm:col-start-2 sm:row-start-1"
|
|
483
550
|
>
|
|
484
|
-
<
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
551
|
+
<p class="text-xs text-muted-foreground">
|
|
552
|
+
<span v-for="(group, index) in named" :key="group.name">
|
|
553
|
+
<!-- The separator is punctuation, not a word: set fainter than
|
|
554
|
+
either side and given room, so the eye breaks the line into
|
|
555
|
+
three facts instead of reading one run of text. -->
|
|
556
|
+
<span
|
|
557
|
+
v-if="index"
|
|
558
|
+
aria-hidden="true"
|
|
559
|
+
class="px-1 text-muted-foreground/50"
|
|
560
|
+
>
|
|
561
|
+
·
|
|
562
|
+
</span>
|
|
563
|
+
{{ changelogLabel(group.name) }}
|
|
564
|
+
<span class="ms-0.5 font-medium text-foreground tabular-nums">
|
|
565
|
+
{{ group.count }}
|
|
566
|
+
</span>
|
|
567
|
+
</span>
|
|
568
|
+
|
|
569
|
+
<template v-if="rest">
|
|
570
|
+
<span aria-hidden="true" class="px-1 text-muted-foreground/50">
|
|
571
|
+
·
|
|
572
|
+
</span>
|
|
573
|
+
{{ $t('duxt.changelog.more', { count: rest }) }}
|
|
574
|
+
</template>
|
|
575
|
+
</p>
|
|
576
|
+
|
|
577
|
+
<!-- FACES, NOT NAMES, and the row is why: four names are a second
|
|
578
|
+
line of prose competing with the summary, where four
|
|
579
|
+
overlapping circles are read as "a few people" without being
|
|
580
|
+
read at all. The name is still THERE for anyone who needs it —
|
|
581
|
+
as the image's own alternative text, so a screen reader hears
|
|
582
|
+
the people rather than "image, image, image", and so the row's
|
|
583
|
+
link says who wrote the release it leads to.
|
|
584
|
+
|
|
585
|
+
They OVERLAP, each ringed in the page's own background, which
|
|
586
|
+
is the one arrangement that says "these belong together" at
|
|
587
|
+
18px without a label. `ring` rather than `border`, so the ring
|
|
588
|
+
sits outside the circle and the picture is not cropped by it. -->
|
|
589
|
+
<span v-if="people.length" class="flex shrink-0 items-center">
|
|
488
590
|
<span
|
|
489
|
-
v-
|
|
490
|
-
|
|
491
|
-
class="
|
|
591
|
+
v-for="person in people"
|
|
592
|
+
:key="identity(person)"
|
|
593
|
+
class="-ms-1.5 first:ms-0"
|
|
492
594
|
>
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
595
|
+
<img
|
|
596
|
+
v-if="avatar(person.username)"
|
|
597
|
+
:src="avatar(person.username)"
|
|
598
|
+
:alt="person.name"
|
|
599
|
+
width="18"
|
|
600
|
+
height="18"
|
|
601
|
+
loading="lazy"
|
|
602
|
+
class="size-[18px] rounded-full bg-muted ring-2 ring-background"
|
|
603
|
+
/>
|
|
604
|
+
<!-- No handle, so no picture: git only carries one in a
|
|
605
|
+
noreply address, and a guessed avatar is somebody else's
|
|
606
|
+
face. The initial is decoration over the name beside it. -->
|
|
607
|
+
<span
|
|
608
|
+
v-else
|
|
609
|
+
class="flex size-[18px] items-center justify-center rounded-full bg-muted text-[9px] font-medium ring-2 ring-background"
|
|
610
|
+
>
|
|
611
|
+
<span aria-hidden="true">
|
|
612
|
+
{{ person.name.slice(0, 1).toUpperCase() }}
|
|
613
|
+
</span>
|
|
614
|
+
<span class="sr-only">{{ person.name }}</span>
|
|
615
|
+
</span>
|
|
498
616
|
</span>
|
|
499
|
-
</span>
|
|
500
617
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
618
|
+
<span
|
|
619
|
+
v-if="others"
|
|
620
|
+
class="-ms-1.5 flex h-[18px] items-center rounded-full bg-muted px-1.5 text-[9px] font-medium text-muted-foreground ring-2 ring-background tabular-nums"
|
|
621
|
+
>
|
|
622
|
+
<span aria-hidden="true">+{{ others }}</span>
|
|
623
|
+
<span class="sr-only">
|
|
624
|
+
{{ $t('duxt.changelog.more', { count: others }) }}
|
|
625
|
+
</span>
|
|
504
626
|
</span>
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
</p>
|
|
627
|
+
</span>
|
|
628
|
+
</div>
|
|
508
629
|
|
|
509
630
|
<!-- The affordance every card and row of this site uses: the arrow
|
|
510
631
|
that steps forward under the pointer. -->
|
|
511
632
|
<Icon
|
|
512
633
|
name="lucide:arrow-right"
|
|
513
634
|
aria-hidden="true"
|
|
514
|
-
class="col-start-2 row-start-1 size-3.5 shrink-0 self-center text-muted-foreground transition-transform group-hover:translate-x-0.5 sm:col-start-4"
|
|
635
|
+
class="col-start-2 row-start-1 size-3.5 shrink-0 self-center text-muted-foreground rtl:-scale-x-100 transition-transform ltr:group-hover:translate-x-0.5 rtl:group-hover:-translate-x-0.5 sm:col-start-4"
|
|
515
636
|
/>
|
|
516
637
|
</NuxtLink>
|
|
517
638
|
</li>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { TabsContent,
|
|
2
|
+
import { TabsContent, TabsRoot } from 'reka-ui';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Several fences, one block, one tab each.
|
|
@@ -29,6 +29,8 @@ interface Entry {
|
|
|
29
29
|
label: string;
|
|
30
30
|
icon: string;
|
|
31
31
|
code: string;
|
|
32
|
+
/** The fence's own language, where it declared one — for the copy event. */
|
|
33
|
+
language?: string;
|
|
32
34
|
node: VNode;
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -56,6 +58,7 @@ const entries = computed<Entry[]>(() => {
|
|
|
56
58
|
props.language ? 'lucide:terminal' : 'lucide:file'
|
|
57
59
|
),
|
|
58
60
|
code: props.code ?? '',
|
|
61
|
+
language: props.language,
|
|
59
62
|
node
|
|
60
63
|
};
|
|
61
64
|
});
|
|
@@ -87,9 +90,8 @@ const shell = useTemplateRef<HTMLElement>('shell');
|
|
|
87
90
|
const body = useTemplateRef<HTMLElement>('body');
|
|
88
91
|
|
|
89
92
|
useDuxtAnimatedHeight(shell, body);
|
|
90
|
-
const copied =
|
|
91
|
-
const
|
|
92
|
-
const { t } = useI18n();
|
|
93
|
+
const { copied, copy: copyText } = useDuxtCopy();
|
|
94
|
+
const analytics = useDuxtAnalytics();
|
|
93
95
|
|
|
94
96
|
async function copy() {
|
|
95
97
|
const text =
|
|
@@ -97,14 +99,15 @@ async function copy() {
|
|
|
97
99
|
root.value?.querySelector('[data-state="active"] code')?.textContent ||
|
|
98
100
|
'';
|
|
99
101
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
if (!(await copyText(text))) return;
|
|
103
|
+
|
|
104
|
+
// Which tab was showing is the interesting part — a group exists because
|
|
105
|
+
// the same thing is spelled several ways, and this says which spelling won.
|
|
106
|
+
analytics.track({
|
|
107
|
+
name: 'copy',
|
|
108
|
+
kind: 'code-group',
|
|
109
|
+
language: current.value?.language
|
|
110
|
+
});
|
|
108
111
|
}
|
|
109
112
|
</script>
|
|
110
113
|
|
|
@@ -119,37 +122,26 @@ async function copy() {
|
|
|
119
122
|
inside it is a control a screen reader is told to treat as a tab and
|
|
120
123
|
cannot. So the row is a plain box and the tablist is the part of it
|
|
121
124
|
that actually holds tabs. -->
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
class="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto"
|
|
125
|
+
<DuxtCodeToolbar>
|
|
126
|
+
<UiTabsList
|
|
127
|
+
variant="bare"
|
|
128
|
+
class="min-w-0 flex-1 overflow-x-auto"
|
|
127
129
|
:aria-label="$t('duxt.page.tabs') as string"
|
|
128
130
|
>
|
|
129
|
-
<
|
|
131
|
+
<UiTabsTrigger
|
|
130
132
|
v-for="entry in entries"
|
|
131
133
|
:key="entry.value"
|
|
132
134
|
:value="entry.value"
|
|
133
|
-
|
|
135
|
+
variant="pill"
|
|
136
|
+
class="font-mono"
|
|
134
137
|
>
|
|
135
138
|
<Icon :name="entry.icon" class="size-3.5" />
|
|
136
139
|
{{ entry.label }}
|
|
137
|
-
</
|
|
138
|
-
</
|
|
139
|
-
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
size="icon"
|
|
143
|
-
class="size-7 shrink-0"
|
|
144
|
-
:aria-label="copied ? $t('duxt.code.copied') : $t('duxt.code.copy')"
|
|
145
|
-
@click="copy"
|
|
146
|
-
>
|
|
147
|
-
<Icon
|
|
148
|
-
:name="copied ? 'lucide:check' : 'lucide:copy'"
|
|
149
|
-
class="size-3.5"
|
|
150
|
-
/>
|
|
151
|
-
</UiButton>
|
|
152
|
-
</div>
|
|
140
|
+
</UiTabsTrigger>
|
|
141
|
+
</UiTabsList>
|
|
142
|
+
|
|
143
|
+
<DuxtCopyButton :copied="copied" @click="copy" />
|
|
144
|
+
</DuxtCodeToolbar>
|
|
153
145
|
|
|
154
146
|
<!-- THE BLOCK FOLLOWS THE FILE THAT IS SHOWING, over 300ms.
|
|
155
147
|
|
|
@@ -166,12 +158,24 @@ async function copy() {
|
|
|
166
158
|
ref="shell"
|
|
167
159
|
class="overflow-hidden motion-safe:transition-[height] motion-safe:duration-300 motion-safe:ease-out"
|
|
168
160
|
>
|
|
169
|
-
<div ref="body">
|
|
161
|
+
<div ref="body" class="relative">
|
|
162
|
+
<!-- EVERY PANEL STAYS MOUNTED, and the switch is a CROSSFADE. Mounted on
|
|
163
|
+
demand, a new panel left the body empty for about 150ms and the box
|
|
164
|
+
folded shut and opened again; hidden outright, the new file was
|
|
165
|
+
simply there, cut off by the box it had not yet grown into.
|
|
166
|
+
|
|
167
|
+
So the panel leaving is lifted out of the flow — absolute, over the
|
|
168
|
+
top of the one arriving — and fades out while the new one fades in.
|
|
169
|
+
Out of the flow it takes no height, so the box still follows the
|
|
170
|
+
file that shows. `invisible` lands when the fade ends, which keeps
|
|
171
|
+
the hidden panels out of the tab order and away from a screen
|
|
172
|
+
reader. -->
|
|
170
173
|
<TabsContent
|
|
171
174
|
v-for="entry in entries"
|
|
172
175
|
:key="entry.value"
|
|
173
176
|
:value="entry.value"
|
|
174
|
-
|
|
177
|
+
force-mount
|
|
178
|
+
class="focus-visible:outline-none motion-safe:transition-[opacity,visibility] motion-safe:duration-200 motion-safe:ease-out data-[state=inactive]:pointer-events-none data-[state=inactive]:invisible data-[state=inactive]:absolute data-[state=inactive]:inset-x-0 data-[state=inactive]:top-0 data-[state=inactive]:opacity-0 [&_.duxt-code]:my-0 [&_.duxt-code]:rounded-none [&_.duxt-code]:border-0"
|
|
175
179
|
>
|
|
176
180
|
<!-- `:header="false"`: the tab above already says what the bar inside
|
|
177
181
|
would, and the copy button now sits beside the tabs. -->
|
|
@@ -65,7 +65,7 @@ const expanded = computed(() => {
|
|
|
65
65
|
v-slot="{ isExpanded }"
|
|
66
66
|
:key="item._id"
|
|
67
67
|
v-bind="item.bind"
|
|
68
|
-
:style="{
|
|
68
|
+
:style="{ paddingInlineStart: `${item.level - 1}rem` }"
|
|
69
69
|
class="flex items-center gap-1.5 rounded px-1 py-[3px] outline-none focus:bg-accent data-[selected]:bg-accent/60"
|
|
70
70
|
>
|
|
71
71
|
<template v-if="isDirectory(item.value)">
|
|
@@ -107,7 +107,7 @@ const clientAnchor = computed(() =>
|
|
|
107
107
|
{{ $t('duxt.openapi.requestBody') }}
|
|
108
108
|
<span
|
|
109
109
|
v-if="operation.requestBody.required"
|
|
110
|
-
class="
|
|
110
|
+
class="ms-1 text-xs font-medium text-destructive"
|
|
111
111
|
>
|
|
112
112
|
{{ $t('duxt.openapi.required') }}
|
|
113
113
|
</span>
|
|
@@ -151,7 +151,7 @@ const clientAnchor = computed(() =>
|
|
|
151
151
|
client sits after the description, which is the order a phone reads.
|
|
152
152
|
|
|
153
153
|
AND IT SCROLLS ITSELF, the way the sidebar and the table of contents
|
|
154
|
-
already do — same expression, `100vh` less the
|
|
154
|
+
already do — same expression, `100vh` less the header above it. Sticky
|
|
155
155
|
alone pinned a panel taller than the viewport, so the request samples
|
|
156
156
|
under the send button were cut off and only came into view once the
|
|
157
157
|
reader had scrolled the whole page to its end: the panel is beside the
|
|
@@ -161,7 +161,7 @@ const clientAnchor = computed(() =>
|
|
|
161
161
|
normal flow and a second scrollbar inside the page would be one nobody
|
|
162
162
|
asked for.
|
|
163
163
|
|
|
164
|
-
AND IT KEEPS A REM OFF THE
|
|
164
|
+
AND IT KEEPS A REM OFF THE HEADER. Pinned at exactly `--duxt-header-offset`
|
|
165
165
|
the card's top border sat against the navbar's bottom border, so the
|
|
166
166
|
two read as one thick line and the panel looked attached to the header
|
|
167
167
|
rather than floating under it. The `max-h` gives the same rem back at
|
|
@@ -171,7 +171,7 @@ const clientAnchor = computed(() =>
|
|
|
171
171
|
class="not-typeset min-w-0 scroll-mt-[var(--duxt-header-offset)]"
|
|
172
172
|
>
|
|
173
173
|
<div
|
|
174
|
-
class="xl:sticky xl:top-[calc(var(--duxt-header-offset)+1rem)] xl:max-h-[calc(100vh-var(--duxt-header-offset)-2.5rem)] xl:overflow-y-auto xl:
|
|
174
|
+
class="xl:sticky xl:top-[calc(var(--duxt-header-offset)+1rem)] xl:max-h-[calc(100vh-var(--duxt-header-offset)-2.5rem)] xl:overflow-y-auto xl:pe-1"
|
|
175
175
|
>
|
|
176
176
|
<DuxtOpenApiClient
|
|
177
177
|
:operation="operation"
|
|
@@ -46,7 +46,7 @@ const localeLink = useDuxtLink();
|
|
|
46
46
|
|
|
47
47
|
<span
|
|
48
48
|
v-if="entry.summary"
|
|
49
|
-
class="w-full text-sm text-muted-foreground sm:
|
|
49
|
+
class="w-full text-sm text-muted-foreground sm:ms-auto sm:w-auto sm:text-end"
|
|
50
50
|
>
|
|
51
51
|
{{ entry.summary }}
|
|
52
52
|
</span>
|
|
@@ -151,7 +151,7 @@ const operations = computed(() =>
|
|
|
151
151
|
<Icon
|
|
152
152
|
name="lucide:arrow-right"
|
|
153
153
|
aria-hidden="true"
|
|
154
|
-
class="mt-1 size-3.5 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5"
|
|
154
|
+
class="mt-1 size-3.5 shrink-0 text-muted-foreground rtl:-scale-x-100 transition-transform ltr:group-hover:translate-x-0.5 rtl:group-hover:-translate-x-0.5"
|
|
155
155
|
/>
|
|
156
156
|
</NuxtLink>
|
|
157
157
|
</li>
|