@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { useResizeObserver } from '@vueuse/core';
|
|
3
|
+
import { TabsContent, TabsRoot } from 'reka-ui';
|
|
3
4
|
import type {
|
|
4
5
|
DuxtOpenApiOperation,
|
|
5
6
|
DuxtOpenApiSecurity,
|
|
@@ -67,7 +68,7 @@ const split = computed(() => props.layout === 'split');
|
|
|
67
68
|
|
|
68
69
|
const id = useId();
|
|
69
70
|
const { t } = useI18n();
|
|
70
|
-
const
|
|
71
|
+
const analytics = useDuxtAnalytics();
|
|
71
72
|
|
|
72
73
|
const parameters = computed(() => props.operation.parameters ?? []);
|
|
73
74
|
const bodies = computed(() => props.operation.requestBody?.content ?? []);
|
|
@@ -98,6 +99,12 @@ const variables = ref<Record<string, string>>({});
|
|
|
98
99
|
|
|
99
100
|
const base = computed(() => openApiServerUrl(server.value, variables.value));
|
|
100
101
|
|
|
102
|
+
// Read before `values` below, which calls it while the component is set up.
|
|
103
|
+
// Declared further down it was in its temporal dead zone at that point, and
|
|
104
|
+
// every operation page that renders the client answered a 500 with
|
|
105
|
+
// "Cannot access 'duxt' before initialization".
|
|
106
|
+
const duxt = useDuxtConfig();
|
|
107
|
+
|
|
101
108
|
/* ------------------------------------------------------------- parameters */
|
|
102
109
|
|
|
103
110
|
/**
|
|
@@ -114,7 +121,12 @@ const values = ref<Record<string, string>>(
|
|
|
114
121
|
// what is sent say the same thing about every parameter.
|
|
115
122
|
.filter((parameter) => parameter.in !== 'cookie')
|
|
116
123
|
.map((parameter) => {
|
|
117
|
-
const derived = openApiExampleValue(
|
|
124
|
+
const derived = openApiExampleValue(
|
|
125
|
+
parameter.schema,
|
|
126
|
+
'request',
|
|
127
|
+
0,
|
|
128
|
+
duxt.openapi?.exampleDepth
|
|
129
|
+
);
|
|
118
130
|
const example = parameter.examples?.[0]?.value ?? derived;
|
|
119
131
|
|
|
120
132
|
return [
|
|
@@ -168,8 +180,6 @@ const bodyKeys = computed(() => openApiBodyKeys(media.value?.schema));
|
|
|
168
180
|
* property of one endpoint: somebody who edits the body as JSON means it on
|
|
169
181
|
* the next one too. Falls back to the form the moment it cannot serve.
|
|
170
182
|
*/
|
|
171
|
-
const duxt = useDuxtConfig();
|
|
172
|
-
|
|
173
183
|
const storedMode = useDuxtChoice('request-body-view');
|
|
174
184
|
|
|
175
185
|
const bodyMode = computed({
|
|
@@ -244,7 +254,12 @@ const body = ref(
|
|
|
244
254
|
bodies.value.length
|
|
245
255
|
? openApiJson(
|
|
246
256
|
bodies.value[0]!.examples?.[0]?.value ??
|
|
247
|
-
openApiExampleValue(
|
|
257
|
+
openApiExampleValue(
|
|
258
|
+
bodies.value[0]!.schema,
|
|
259
|
+
'request',
|
|
260
|
+
0,
|
|
261
|
+
duxt.openapi?.exampleDepth
|
|
262
|
+
)
|
|
248
263
|
)
|
|
249
264
|
: ''
|
|
250
265
|
);
|
|
@@ -530,17 +545,65 @@ const { data: sampleHtml } = await useAsyncData(
|
|
|
530
545
|
{ watch: [shown] }
|
|
531
546
|
);
|
|
532
547
|
|
|
533
|
-
|
|
548
|
+
/**
|
|
549
|
+
* A CROSSFADE between two samples, not a cut.
|
|
550
|
+
*
|
|
551
|
+
* The sample is a single block whose markup is replaced, so without help the
|
|
552
|
+
* new one simply appeared — cut off by a card that had not yet grown into it.
|
|
553
|
+
* The markup being replaced is kept for the length of the fade, laid over the
|
|
554
|
+
* top of the new one and faded out while the new one fades in; nothing is ever
|
|
555
|
+
* empty.
|
|
556
|
+
*
|
|
557
|
+
* Only for a change of language or client. The sample is also rewritten on
|
|
558
|
+
* every keystroke in the form above, and a fade per character typed is a
|
|
559
|
+
* flicker, not a transition.
|
|
560
|
+
*/
|
|
561
|
+
const leavingSample = ref<string>();
|
|
562
|
+
const enteringSample = ref(false);
|
|
563
|
+
let switchingSample = false;
|
|
564
|
+
let leavingTimer: ReturnType<typeof setTimeout> | undefined;
|
|
565
|
+
|
|
566
|
+
watch([group, sample], () => {
|
|
567
|
+
switchingSample = true;
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
watch(sampleHtml, (next, previous) => {
|
|
571
|
+
if (!switchingSample || !previous || next === previous) return;
|
|
572
|
+
switchingSample = false;
|
|
573
|
+
|
|
574
|
+
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
|
575
|
+
|
|
576
|
+
clearTimeout(leavingTimer);
|
|
577
|
+
leavingSample.value = previous;
|
|
578
|
+
enteringSample.value = true;
|
|
579
|
+
|
|
580
|
+
requestAnimationFrame(() =>
|
|
581
|
+
requestAnimationFrame(() => {
|
|
582
|
+
enteringSample.value = false;
|
|
583
|
+
})
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
leavingTimer = setTimeout(() => {
|
|
587
|
+
leavingSample.value = undefined;
|
|
588
|
+
}, 250);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
onBeforeUnmount(() => clearTimeout(leavingTimer));
|
|
592
|
+
|
|
593
|
+
const { copied: copiedSample, copy } = useDuxtCopy();
|
|
534
594
|
|
|
535
595
|
async function copySample() {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
596
|
+
if (!(await copy(shown.value.code))) return;
|
|
597
|
+
|
|
598
|
+
// WHICH sample, never the sample. The code is the reader's own request
|
|
599
|
+
// written out — server, parameters, token and all — so only the two pieces
|
|
600
|
+
// of declared configuration travel.
|
|
601
|
+
analytics.track({
|
|
602
|
+
name: 'copy',
|
|
603
|
+
kind: 'request-sample',
|
|
604
|
+
sample: shown.value.id,
|
|
605
|
+
language: shown.value.language
|
|
606
|
+
});
|
|
544
607
|
}
|
|
545
608
|
|
|
546
609
|
/* ------------------------------------------------------------------- send */
|
|
@@ -560,6 +623,36 @@ const sending = ref(false);
|
|
|
560
623
|
const result = ref<Result>();
|
|
561
624
|
const failure = ref<string>();
|
|
562
625
|
|
|
626
|
+
/**
|
|
627
|
+
* The request that just settled, described entirely by the DOCUMENT.
|
|
628
|
+
*
|
|
629
|
+
* Everything here is declared in the OpenAPI file — the operation's id, its
|
|
630
|
+
* method, its path template — and nothing is the request that was actually
|
|
631
|
+
* made. Not the server the reader picked, not the URL it produced, not a
|
|
632
|
+
* header, not the token in it, not the body, not the response. That is the same
|
|
633
|
+
* promise the docblock at the top of this file makes about where credentials
|
|
634
|
+
* go, kept from the other side: a component that holds a token in its own state
|
|
635
|
+
* and then posts it to a site's analytics has not kept it anywhere.
|
|
636
|
+
*
|
|
637
|
+
* The duration is the request's own, taken before the spinner's floor is
|
|
638
|
+
* applied — `MINIMUM_WAIT` is a thing about the button, not about the API.
|
|
639
|
+
*/
|
|
640
|
+
function report(
|
|
641
|
+
outcome: 'response' | 'failed',
|
|
642
|
+
duration: number,
|
|
643
|
+
statusClass?: DuxtAnalyticsStatusClass
|
|
644
|
+
) {
|
|
645
|
+
analytics.track({
|
|
646
|
+
name: 'api-request',
|
|
647
|
+
operation: props.operation.operationId,
|
|
648
|
+
method: props.operation.method,
|
|
649
|
+
path: props.operation.path,
|
|
650
|
+
outcome,
|
|
651
|
+
statusClass,
|
|
652
|
+
duration
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
563
656
|
async function send() {
|
|
564
657
|
sending.value = true;
|
|
565
658
|
// THE PREVIOUS ANSWER STAYS UNTIL THIS ONE ARRIVES. Clearing it here emptied
|
|
@@ -578,15 +671,18 @@ async function send() {
|
|
|
578
671
|
});
|
|
579
672
|
|
|
580
673
|
const text = await response.text();
|
|
674
|
+
const duration = Math.round(performance.now() - started);
|
|
581
675
|
|
|
582
676
|
failure.value = undefined;
|
|
583
677
|
result.value = {
|
|
584
678
|
status: response.status,
|
|
585
679
|
statusText: response.statusText,
|
|
586
|
-
duration
|
|
680
|
+
duration,
|
|
587
681
|
headers: [...response.headers.entries()],
|
|
588
682
|
body: pretty(text)
|
|
589
683
|
};
|
|
684
|
+
|
|
685
|
+
report('response', duration, duxtStatusClass(response.status));
|
|
590
686
|
} catch (error) {
|
|
591
687
|
// `fetch` rejects with a bare TypeError for a CORS refusal, a DNS failure
|
|
592
688
|
// and an offline browser alike — the browser deliberately tells the page
|
|
@@ -597,6 +693,10 @@ async function send() {
|
|
|
597
693
|
error instanceof Error && error.name !== 'TypeError'
|
|
598
694
|
? error.message
|
|
599
695
|
: t('duxt.openapi.client.blocked');
|
|
696
|
+
|
|
697
|
+
// No status and no class: nothing came back to have one. The message is not
|
|
698
|
+
// reported either — for a CORS refusal it is this component's own guess.
|
|
699
|
+
report('failed', Math.round(performance.now() - started));
|
|
600
700
|
} finally {
|
|
601
701
|
// A FLOOR UNDER THE WAIT. The demo endpoint answers in a few milliseconds,
|
|
602
702
|
// so the spinner appeared and vanished inside one frame — which reads as
|
|
@@ -625,6 +725,83 @@ const sampleBody = useTemplateRef<HTMLElement>('sampleBody');
|
|
|
625
725
|
|
|
626
726
|
useDuxtAnimatedHeight(sampleShell, sampleBody, () => split.value);
|
|
627
727
|
|
|
728
|
+
/**
|
|
729
|
+
* THE PROSE BESIDE THE FORM STAYS WHERE IT IS when an answer arrives.
|
|
730
|
+
*
|
|
731
|
+
* The row centres its two halves, so a card that grew by a response pulled the
|
|
732
|
+
* text beside it down by half that height — the paragraph the reader had just
|
|
733
|
+
* read slid away while they looked at the answer. Centring against the FORM
|
|
734
|
+
* alone keeps the first paint exactly as it was and leaves the prose put: the
|
|
735
|
+
* offset is the card's height minus the response row's, which is the same
|
|
736
|
+
* number at every frame of that row opening.
|
|
737
|
+
*
|
|
738
|
+
* Only while the halves stand side by side; stacked, there is nothing to
|
|
739
|
+
* centre against. Until mounted the row's own `items-center` places it, and
|
|
740
|
+
* that is the same position, so hydration moves nothing.
|
|
741
|
+
*/
|
|
742
|
+
const formProse = useTemplateRef<HTMLElement>('formProse');
|
|
743
|
+
const formCard = useTemplateRef<HTMLElement>('formCard');
|
|
744
|
+
const responseRow = useTemplateRef<HTMLElement>('responseRow');
|
|
745
|
+
const proseOffset = ref<number>();
|
|
746
|
+
|
|
747
|
+
useResizeObserver([formCard, formProse], () => {
|
|
748
|
+
const prose = formProse.value;
|
|
749
|
+
const card = formCard.value;
|
|
750
|
+
const row = responseRow.value;
|
|
751
|
+
if (!split.value || !prose || !card || !row) return;
|
|
752
|
+
|
|
753
|
+
proseOffset.value =
|
|
754
|
+
gridColumns(card) > 1
|
|
755
|
+
? Math.max(
|
|
756
|
+
0,
|
|
757
|
+
(card.offsetHeight - row.offsetHeight - prose.offsetHeight) / 2
|
|
758
|
+
)
|
|
759
|
+
: undefined;
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* THE SAME FOR THE PROSE BESIDE THE SAMPLES, which moves for a different
|
|
764
|
+
* reason: the card changes height with every language, and a row centring its
|
|
765
|
+
* halves nudged the text up and down on each tab click.
|
|
766
|
+
*
|
|
767
|
+
* Here there is no part of the card to subtract, so the text is centred once
|
|
768
|
+
* against the column as it first stands — and again only when the WIDTH
|
|
769
|
+
* changes, which is when its own lines rewrap. A tab click changes neither.
|
|
770
|
+
*/
|
|
771
|
+
const sampleProse = useTemplateRef<HTMLElement>('sampleProse');
|
|
772
|
+
const sampleColumn = useTemplateRef<HTMLElement>('sampleColumn');
|
|
773
|
+
const sampleProseOffset = ref<number>();
|
|
774
|
+
|
|
775
|
+
/** The column's height when the text last rewrapped, and the width it did at. */
|
|
776
|
+
let sampleBaseline = 0;
|
|
777
|
+
let sampleWidth: number | undefined;
|
|
778
|
+
|
|
779
|
+
useResizeObserver(sampleProse, () => {
|
|
780
|
+
const prose = sampleProse.value;
|
|
781
|
+
const column = sampleColumn.value;
|
|
782
|
+
if (!split.value || !prose || !column) return;
|
|
783
|
+
|
|
784
|
+
if (prose.offsetWidth !== sampleWidth) {
|
|
785
|
+
sampleWidth = prose.offsetWidth;
|
|
786
|
+
sampleBaseline = column.offsetHeight;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
sampleProseOffset.value =
|
|
790
|
+
gridColumns(column) > 1
|
|
791
|
+
? Math.max(0, (sampleBaseline - prose.offsetHeight) / 2)
|
|
792
|
+
: undefined;
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* How many columns the grid holding `element` lays out — one when the halves
|
|
797
|
+
* stack, which is when neither piece of prose has anything to centre against.
|
|
798
|
+
*/
|
|
799
|
+
function gridColumns(element: HTMLElement): number {
|
|
800
|
+
return getComputedStyle(element.parentElement ?? element)
|
|
801
|
+
.gridTemplateColumns.split(' ')
|
|
802
|
+
.filter(Boolean).length;
|
|
803
|
+
}
|
|
804
|
+
|
|
628
805
|
/** A JSON body, indented; anything else exactly as it arrived. */
|
|
629
806
|
function pretty(text: string): string {
|
|
630
807
|
try {
|
|
@@ -661,7 +838,19 @@ function pretty(text: string): string {
|
|
|
661
838
|
split ? 'grid items-center gap-8 lg:grid-cols-2 lg:gap-16' : 'contents'
|
|
662
839
|
"
|
|
663
840
|
>
|
|
664
|
-
<div
|
|
841
|
+
<div
|
|
842
|
+
v-if="split"
|
|
843
|
+
ref="formProse"
|
|
844
|
+
:class="[
|
|
845
|
+
reverse ? 'lg:order-2' : 'lg:order-1',
|
|
846
|
+
proseOffset === undefined ? '' : 'self-start'
|
|
847
|
+
]"
|
|
848
|
+
:style="
|
|
849
|
+
proseOffset === undefined
|
|
850
|
+
? undefined
|
|
851
|
+
: { marginTop: `${proseOffset}px` }
|
|
852
|
+
"
|
|
853
|
+
>
|
|
665
854
|
<slot name="beside-form" />
|
|
666
855
|
</div>
|
|
667
856
|
|
|
@@ -669,6 +858,7 @@ function pretty(text: string): string {
|
|
|
669
858
|
centred in its row grows in both directions when an answer arrives,
|
|
670
859
|
and the form the reader is looking at moves up under their cursor. -->
|
|
671
860
|
<div
|
|
861
|
+
ref="formCard"
|
|
672
862
|
:class="
|
|
673
863
|
split
|
|
674
864
|
? [
|
|
@@ -906,11 +1096,11 @@ function pretty(text: string): string {
|
|
|
906
1096
|
</label>
|
|
907
1097
|
|
|
908
1098
|
<!-- Beside the label, not beside the toggle. It belongs to the JSON
|
|
909
|
-
view, so it comes and goes with it — and
|
|
910
|
-
nothing: the toggle is pinned to the
|
|
911
|
-
the control the reader just clicked cannot move out
|
|
912
|
-
the pointer, which is what happened when the two
|
|
913
|
-
end of this row. -->
|
|
1099
|
+
view, so it comes and goes with it — and at the START of the row
|
|
1100
|
+
that costs nothing: the toggle is pinned to the far END by
|
|
1101
|
+
`ms-auto`, so the control the reader just clicked cannot move out
|
|
1102
|
+
from under the pointer, which is what happened when the two
|
|
1103
|
+
shared the end of this row. -->
|
|
914
1104
|
<UiButton
|
|
915
1105
|
v-if="mode === 'json'"
|
|
916
1106
|
type="button"
|
|
@@ -926,26 +1116,18 @@ function pretty(text: string): string {
|
|
|
926
1116
|
the schema cannot describe as a form has one view, and a strip
|
|
927
1117
|
offering a tab that falls straight back is a control that lies
|
|
928
1118
|
about what it does. -->
|
|
929
|
-
<
|
|
1119
|
+
<DuxtSegmented
|
|
930
1120
|
v-if="formable"
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
? 'bg-accent text-foreground'
|
|
942
|
-
: 'text-muted-foreground hover:text-foreground'
|
|
943
|
-
"
|
|
944
|
-
@click="bodyMode = view"
|
|
945
|
-
>
|
|
946
|
-
{{ $t(`duxt.openapi.client.${view}`) }}
|
|
947
|
-
</button>
|
|
948
|
-
</div>
|
|
1121
|
+
:model-value="mode"
|
|
1122
|
+
:options="[
|
|
1123
|
+
{ value: 'form', label: $t('duxt.openapi.client.form') },
|
|
1124
|
+
{ value: 'json', label: $t('duxt.openapi.client.json') }
|
|
1125
|
+
]"
|
|
1126
|
+
size="sm"
|
|
1127
|
+
tone="flat"
|
|
1128
|
+
class="ms-auto gap-0.5 rounded-md border p-0.5"
|
|
1129
|
+
@update:model-value="bodyMode = $event"
|
|
1130
|
+
/>
|
|
949
1131
|
</div>
|
|
950
1132
|
|
|
951
1133
|
<UiSelect v-if="bodies.length > 1" v-model="bodyValue">
|
|
@@ -1072,6 +1254,7 @@ function pretty(text: string): string {
|
|
|
1072
1254
|
in advance; the inner `overflow-hidden` is what lets the row be
|
|
1073
1255
|
shorter than its content on the way. -->
|
|
1074
1256
|
<div
|
|
1257
|
+
ref="responseRow"
|
|
1075
1258
|
class="grid motion-safe:transition-all motion-safe:duration-300 motion-safe:ease-out"
|
|
1076
1259
|
:class="
|
|
1077
1260
|
failure || result
|
|
@@ -1094,7 +1277,7 @@ function pretty(text: string): string {
|
|
|
1094
1277
|
<span class="text-muted-foreground">
|
|
1095
1278
|
{{ result.statusText }}
|
|
1096
1279
|
</span>
|
|
1097
|
-
<span class="
|
|
1280
|
+
<span class="ms-auto font-mono text-xs text-muted-foreground">
|
|
1098
1281
|
{{ result.duration }} ms
|
|
1099
1282
|
</span>
|
|
1100
1283
|
</div>
|
|
@@ -1146,7 +1329,19 @@ function pretty(text: string): string {
|
|
|
1146
1329
|
split ? 'grid items-center gap-8 lg:grid-cols-2 lg:gap-16' : 'contents'
|
|
1147
1330
|
"
|
|
1148
1331
|
>
|
|
1149
|
-
<div
|
|
1332
|
+
<div
|
|
1333
|
+
v-if="split"
|
|
1334
|
+
ref="sampleProse"
|
|
1335
|
+
:class="[
|
|
1336
|
+
reverse ? 'lg:order-1' : 'lg:order-2',
|
|
1337
|
+
sampleProseOffset === undefined ? '' : 'self-start'
|
|
1338
|
+
]"
|
|
1339
|
+
:style="
|
|
1340
|
+
sampleProseOffset === undefined
|
|
1341
|
+
? undefined
|
|
1342
|
+
: { marginTop: `${sampleProseOffset}px` }
|
|
1343
|
+
"
|
|
1344
|
+
>
|
|
1150
1345
|
<slot name="beside-samples" />
|
|
1151
1346
|
</div>
|
|
1152
1347
|
|
|
@@ -1159,6 +1354,7 @@ function pretty(text: string): string {
|
|
|
1159
1354
|
in both directions at once, and its tab strip — the control the
|
|
1160
1355
|
reader just clicked — moved up under their cursor. -->
|
|
1161
1356
|
<div
|
|
1357
|
+
ref="sampleColumn"
|
|
1162
1358
|
:class="
|
|
1163
1359
|
split
|
|
1164
1360
|
? ['min-w-0 self-start', reverse ? 'lg:order-2' : 'lg:order-1']
|
|
@@ -1195,39 +1391,30 @@ function pretty(text: string): string {
|
|
|
1195
1391
|
button inside one as `aria-required-children`. The header row is
|
|
1196
1392
|
the flex container instead, so it still sits where every other
|
|
1197
1393
|
card on the site puts it. -->
|
|
1198
|
-
<
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
class="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto"
|
|
1394
|
+
<DuxtCodeToolbar>
|
|
1395
|
+
<UiTabsList
|
|
1396
|
+
variant="bare"
|
|
1397
|
+
class="min-w-0 flex-1 overflow-x-auto"
|
|
1203
1398
|
:aria-label="$t('duxt.openapi.client.samples') as string"
|
|
1204
1399
|
>
|
|
1205
|
-
<
|
|
1400
|
+
<UiTabsTrigger
|
|
1206
1401
|
v-for="entry in groups"
|
|
1207
1402
|
:key="entry"
|
|
1208
1403
|
:value="entry"
|
|
1209
|
-
|
|
1404
|
+
variant="pill"
|
|
1405
|
+
class="font-mono"
|
|
1210
1406
|
>
|
|
1211
1407
|
<Icon :name="groupIcon(entry)" class="size-3.5" />
|
|
1212
1408
|
{{ entry }}
|
|
1213
|
-
</
|
|
1214
|
-
</
|
|
1215
|
-
|
|
1216
|
-
<
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
class="ml-auto size-7 hover:bg-accent hover:text-foreground"
|
|
1220
|
-
:aria-label="
|
|
1221
|
-
copiedSample ? $t('duxt.code.copied') : $t('duxt.code.copy')
|
|
1222
|
-
"
|
|
1409
|
+
</UiTabsTrigger>
|
|
1410
|
+
</UiTabsList>
|
|
1411
|
+
|
|
1412
|
+
<DuxtCopyButton
|
|
1413
|
+
:copied="copiedSample"
|
|
1414
|
+
class="ms-auto"
|
|
1223
1415
|
@click="copySample"
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
:name="copiedSample ? 'lucide:check' : 'lucide:copy'"
|
|
1227
|
-
class="size-3.5"
|
|
1228
|
-
/>
|
|
1229
|
-
</UiButton>
|
|
1230
|
-
</div>
|
|
1416
|
+
/>
|
|
1417
|
+
</DuxtCodeToolbar>
|
|
1231
1418
|
|
|
1232
1419
|
<!-- THE CLIENTS OF THE ACTIVE LANGUAGE, in a row of their own.
|
|
1233
1420
|
Beside the tabs they were a control the reader had to open to learn
|
|
@@ -1240,28 +1427,20 @@ function pretty(text: string): string {
|
|
|
1240
1427
|
`ui/radio-group` here to implement it. A group of pressed buttons
|
|
1241
1428
|
promises only what it does — every one reachable by Tab, every one
|
|
1242
1429
|
saying whether it is on. -->
|
|
1243
|
-
<
|
|
1430
|
+
<DuxtSegmented
|
|
1244
1431
|
v-if="clients.length > 1 && clients.length < 6"
|
|
1245
|
-
|
|
1246
|
-
:
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
? 'bg-background text-foreground shadow-sm'
|
|
1258
|
-
: 'text-muted-foreground hover:bg-accent hover:text-foreground'
|
|
1259
|
-
"
|
|
1260
|
-
@click="sample = entry.id"
|
|
1261
|
-
>
|
|
1262
|
-
{{ entry.label }}
|
|
1263
|
-
</button>
|
|
1264
|
-
</div>
|
|
1432
|
+
v-model="sample"
|
|
1433
|
+
:options="
|
|
1434
|
+
clients.map((entry) => ({
|
|
1435
|
+
value: entry.id,
|
|
1436
|
+
label: entry.label
|
|
1437
|
+
}))
|
|
1438
|
+
"
|
|
1439
|
+
:label="$t('duxt.openapi.client.sampleClient') as string"
|
|
1440
|
+
size="sm"
|
|
1441
|
+
mono
|
|
1442
|
+
class="overflow-x-auto border-b bg-muted/20 px-2 py-1.5"
|
|
1443
|
+
/>
|
|
1265
1444
|
|
|
1266
1445
|
<!-- Six is where a row stops being a row. A language with that many
|
|
1267
1446
|
clients is a site that configured them, and a select carries any
|
|
@@ -1314,14 +1493,30 @@ function pretty(text: string): string {
|
|
|
1314
1493
|
The floor stays for the panel layout, where there is no row to
|
|
1315
1494
|
reserve anything: on an operation page the card is in a column
|
|
1316
1495
|
that scrolls as a whole. -->
|
|
1317
|
-
<TabsContent :value="group">
|
|
1496
|
+
<TabsContent :value="group" class="relative">
|
|
1318
1497
|
<!-- eslint-disable-next-line vue/no-v-html -- Shiki's own output over
|
|
1319
1498
|
a string this component built; nothing a reader typed reaches it
|
|
1320
1499
|
unescaped. -->
|
|
1500
|
+
<!-- eslint-disable-next-line vue/no-v-html -- the sample that was
|
|
1501
|
+
showing, kept only for the length of the fade. -->
|
|
1502
|
+
<div
|
|
1503
|
+
v-if="leavingSample"
|
|
1504
|
+
aria-hidden="true"
|
|
1505
|
+
class="duxt-code-body duxt-code-body-sm pointer-events-none absolute inset-x-0 top-0 transition-opacity duration-200 ease-out"
|
|
1506
|
+
:class="enteringSample ? 'opacity-100' : 'opacity-0'"
|
|
1507
|
+
v-html="leavingSample"
|
|
1508
|
+
/>
|
|
1321
1509
|
<div
|
|
1322
1510
|
v-if="sampleHtml"
|
|
1323
1511
|
class="duxt-code-body duxt-code-body-sm"
|
|
1324
|
-
:class="
|
|
1512
|
+
:class="[
|
|
1513
|
+
split ? '' : 'min-h-56',
|
|
1514
|
+
leavingSample
|
|
1515
|
+
? enteringSample
|
|
1516
|
+
? 'opacity-0'
|
|
1517
|
+
: 'opacity-100 transition-opacity duration-200 ease-out'
|
|
1518
|
+
: ''
|
|
1519
|
+
]"
|
|
1325
1520
|
v-html="sampleHtml"
|
|
1326
1521
|
/>
|
|
1327
1522
|
<pre
|
|
@@ -22,15 +22,29 @@ const props = defineProps<{
|
|
|
22
22
|
|
|
23
23
|
const types = computed(() => props.content ?? []);
|
|
24
24
|
const current = ref(0);
|
|
25
|
+
const duxt = useDuxtConfig();
|
|
25
26
|
|
|
26
27
|
const media = computed(() => types.value[current.value] ?? types.value[0]);
|
|
27
28
|
|
|
29
|
+
/** The index as the string a pill group keys its options by. */
|
|
30
|
+
const currentValue = computed({
|
|
31
|
+
get: () => String(current.value),
|
|
32
|
+
set: (value: string) => {
|
|
33
|
+
current.value = Number(value);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
28
37
|
/** The examples the document wrote, and — where it wrote none — a derived one. */
|
|
29
38
|
const examples = computed(() => {
|
|
30
39
|
const written = media.value?.examples ?? [];
|
|
31
40
|
if (written.length) return written;
|
|
32
41
|
|
|
33
|
-
const derived = openApiExampleValue(
|
|
42
|
+
const derived = openApiExampleValue(
|
|
43
|
+
media.value?.schema,
|
|
44
|
+
props.direction,
|
|
45
|
+
0,
|
|
46
|
+
duxt.openapi?.exampleDepth
|
|
47
|
+
);
|
|
34
48
|
|
|
35
49
|
return derived === null ? [] : [{ name: 'example', value: derived }];
|
|
36
50
|
});
|
|
@@ -38,23 +52,21 @@ const examples = computed(() => {
|
|
|
38
52
|
|
|
39
53
|
<template>
|
|
40
54
|
<div v-if="types.length" class="min-w-0">
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
index
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
</button>
|
|
57
|
-
</div>
|
|
55
|
+
<!-- `flat`: this strip sits on the page itself, where a raised pill's
|
|
56
|
+
shadow has nothing to lift it off. -->
|
|
57
|
+
<DuxtSegmented
|
|
58
|
+
v-if="types.length > 1"
|
|
59
|
+
v-model="currentValue"
|
|
60
|
+
:options="
|
|
61
|
+
types.map((entry, index) => ({
|
|
62
|
+
value: String(index),
|
|
63
|
+
label: entry.type
|
|
64
|
+
}))
|
|
65
|
+
"
|
|
66
|
+
tone="flat"
|
|
67
|
+
mono
|
|
68
|
+
class="mb-3 flex-wrap gap-1.5"
|
|
69
|
+
/>
|
|
58
70
|
|
|
59
71
|
<p v-else class="mb-2 font-mono text-xs text-muted-foreground">
|
|
60
72
|
{{ media?.type }}
|