@kirchdev/duxt 0.3.4 → 0.3.5-dev-main.44-5337cc1

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.
Files changed (219) hide show
  1. package/README.md +24 -9
  2. package/app/app.vue +17 -11
  3. package/app/assets/css/duxt.css +47 -6
  4. package/app/components/DuxtAnnouncements.vue +117 -0
  5. package/app/components/DuxtBrunoEntries.vue +62 -0
  6. package/app/components/DuxtCodeBlock.vue +18 -34
  7. package/app/components/DuxtCodeToolbar.vue +14 -0
  8. package/app/components/DuxtCopyButton.vue +33 -0
  9. package/app/components/DuxtCopyPage.vue +32 -30
  10. package/app/components/DuxtFooter.vue +39 -8
  11. package/app/components/DuxtHeader.vue +107 -48
  12. package/app/components/DuxtLandingProse.vue +1 -1
  13. package/app/components/DuxtLinkLabel.vue +36 -0
  14. package/app/components/DuxtLiveWindow.vue +8 -8
  15. package/app/components/DuxtLocale.vue +10 -1
  16. package/app/components/DuxtNavigation.vue +2 -2
  17. package/app/components/DuxtOpenApiClient.vue +285 -90
  18. package/app/components/DuxtOpenApiMedia.vue +30 -18
  19. package/app/components/DuxtOpenApiSchema.vue +10 -7
  20. package/app/components/DuxtPageFeedback.vue +7 -0
  21. package/app/components/DuxtPageInfo.vue +15 -12
  22. package/app/components/DuxtPageNav.vue +41 -27
  23. package/app/components/DuxtPreviewSkeleton.vue +4 -4
  24. package/app/components/DuxtProgress.vue +2 -14
  25. package/app/components/DuxtSearch.vue +230 -90
  26. package/app/components/DuxtSearchInput.vue +70 -0
  27. package/app/components/DuxtSections.vue +11 -1
  28. package/app/components/DuxtSegmented.vue +64 -0
  29. package/app/components/DuxtShortcuts.vue +16 -11
  30. package/app/components/DuxtShortcutsTrigger.vue +49 -0
  31. package/app/components/DuxtSkipLink.vue +1 -1
  32. package/app/components/DuxtToc.vue +16 -10
  33. package/app/components/DuxtTranslationBanner.vue +1 -1
  34. package/app/components/DuxtVersion.vue +53 -12
  35. package/app/components/DuxtVersionBanner.vue +5 -5
  36. package/app/components/content/AccordionItem.vue +2 -2
  37. package/app/components/content/BrunoOverview.vue +260 -0
  38. package/app/components/content/BrunoRequest.vue +141 -0
  39. package/app/components/content/BrunoRequests.vue +104 -0
  40. package/app/components/content/Callout.vue +5 -5
  41. package/app/components/content/ChangelogReleases.vue +161 -40
  42. package/app/components/content/CodeGroup.vue +41 -37
  43. package/app/components/content/Field.vue +1 -1
  44. package/app/components/content/FileTree.vue +1 -1
  45. package/app/components/content/OpenApiOperation.vue +4 -4
  46. package/app/components/content/OpenApiOperations.vue +1 -1
  47. package/app/components/content/OpenApiOverview.vue +1 -1
  48. package/app/components/content/PackageManagers.vue +42 -58
  49. package/app/components/content/PageCards.vue +7 -4
  50. package/app/components/content/Preview.vue +15 -39
  51. package/app/components/content/ProseImg.vue +596 -49
  52. package/app/components/content/Since.vue +1 -1
  53. package/app/components/content/Steps.vue +6 -4
  54. package/app/components/content/TabGroup.vue +13 -12
  55. package/app/components/ui/breadcrumb/BreadcrumbSeparator.vue +1 -1
  56. package/app/components/ui/command/CommandShortcut.vue +1 -1
  57. package/app/components/ui/dialog/DialogContent.vue +2 -2
  58. package/app/components/ui/dialog/DialogHeader.vue +1 -1
  59. package/app/components/ui/dialog/DialogScrollContent.vue +1 -1
  60. package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +2 -2
  61. package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
  62. package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +1 -1
  63. package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +2 -2
  64. package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +1 -1
  65. package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +2 -2
  66. package/app/components/ui/navigation-menu/NavigationMenuContent.vue +1 -1
  67. package/app/components/ui/navigation-menu/NavigationMenuTrigger.vue +1 -1
  68. package/app/components/ui/navigation-menu/NavigationMenuViewport.vue +1 -1
  69. package/app/components/ui/scroll-area/ScrollBar.vue +1 -1
  70. package/app/components/ui/select/SelectItem.vue +2 -2
  71. package/app/components/ui/sheet/SheetContent.vue +29 -3
  72. package/app/components/ui/sidebar/Sidebar.vue +28 -4
  73. package/app/components/ui/sidebar/SidebarGroupAction.vue +1 -1
  74. package/app/components/ui/sidebar/SidebarInset.vue +1 -1
  75. package/app/components/ui/sidebar/SidebarMenuAction.vue +1 -1
  76. package/app/components/ui/sidebar/SidebarMenuBadge.vue +1 -1
  77. package/app/components/ui/sidebar/SidebarMenuSub.vue +1 -1
  78. package/app/components/ui/sidebar/SidebarMenuSubButton.vue +1 -1
  79. package/app/components/ui/sidebar/SidebarProvider.vue +7 -16
  80. package/app/components/ui/sidebar/index.ts +10 -2
  81. package/app/components/ui/sidebar/utils.ts +0 -1
  82. package/app/components/ui/sonner/Sonner.vue +18 -0
  83. package/app/components/ui/tabs/TabsList.vue +13 -3
  84. package/app/components/ui/tabs/TabsTrigger.vue +24 -3
  85. package/app/components/ui/tooltip/TooltipContent.vue +2 -2
  86. package/app/composables/useActiveHeading.ts +3 -25
  87. package/app/composables/useDuxtAnalytics.ts +50 -0
  88. package/app/composables/useDuxtAnimatedHeight.ts +8 -38
  89. package/app/composables/useDuxtAnnouncements.ts +153 -0
  90. package/app/composables/useDuxtCopy.ts +51 -0
  91. package/app/composables/useDuxtDirection.ts +23 -0
  92. package/app/composables/useDuxtNavigation.ts +28 -8
  93. package/app/composables/useDuxtPath.ts +3 -5
  94. package/app/composables/useDuxtSearch.ts +87 -35
  95. package/app/composables/useDuxtSection.ts +15 -1
  96. package/app/composables/useDuxtSectionRow.ts +6 -5
  97. package/app/composables/useDuxtShortcuts.ts +319 -20
  98. package/app/composables/useDuxtVersion.ts +57 -6
  99. package/app/composables/useDuxtViewportMeasure.ts +37 -0
  100. package/app/composables/useFuzzySearch.ts +9 -4
  101. package/app/composables/useRecentPages.ts +14 -17
  102. package/app/error.vue +112 -74
  103. package/app/layouts/changelog.vue +9 -1
  104. package/app/layouts/default.vue +9 -0
  105. package/app/layouts/docs.vue +9 -1
  106. package/app/layouts/reference.vue +9 -1
  107. package/app/middleware/duxt-section-layout.global.ts +1 -3
  108. package/app/pages/[...slug].vue +146 -35
  109. package/app/pages/index.vue +15 -23
  110. package/app/types/duxt.d.ts +312 -4
  111. package/app/utils/analytics.ts +91 -0
  112. package/app/utils/announcements.ts +205 -0
  113. package/app/utils/changelog.ts +0 -28
  114. package/app/utils/code-highlight.ts +3 -1
  115. package/app/utils/contributor-avatar.ts +27 -0
  116. package/app/utils/direction.ts +43 -0
  117. package/app/utils/duxt-config.ts +43 -0
  118. package/app/utils/duxt-text.ts +10 -0
  119. package/app/utils/format-date.ts +28 -0
  120. package/app/utils/generated-sections.ts +68 -12
  121. package/app/utils/generated-toc.ts +14 -7
  122. package/app/utils/locale-path.ts +43 -0
  123. package/app/utils/navigation-tree.ts +51 -1
  124. package/app/utils/nearest-page.ts +8 -0
  125. package/app/utils/openapi.ts +16 -11
  126. package/app/utils/page-controls.ts +215 -0
  127. package/app/utils/pill.ts +61 -0
  128. package/app/utils/prose-image.ts +128 -0
  129. package/app/utils/search-context.ts +110 -0
  130. package/app/utils/search-display.ts +248 -0
  131. package/app/utils/section-scope.ts +65 -4
  132. package/app/utils/stored-json.ts +31 -0
  133. package/app/utils/version-choices.ts +6 -13
  134. package/app/utils/version-paths.ts +21 -0
  135. package/bin/duxt-cache-key.mjs +61 -0
  136. package/bin/duxt-og-cache.mjs +58 -0
  137. package/bin/{duxt-report.mjs → duxt.mjs} +13 -7
  138. package/bruno-model.ts +169 -0
  139. package/bruno-parse.ts +656 -0
  140. package/bruno-zip.ts +195 -0
  141. package/cli.ts +240 -0
  142. package/duxt-app-config.ts +18 -1
  143. package/frontmatter.ts +49 -0
  144. package/git-contributors.ts +322 -0
  145. package/i18n/locales/de/duxt/announcement.json +7 -0
  146. package/i18n/locales/de/duxt/bruno.json +25 -0
  147. package/i18n/locales/de/duxt/changelog.json +1 -1
  148. package/i18n/locales/de/duxt/code.json +2 -1
  149. package/i18n/locales/de/duxt/page.json +9 -0
  150. package/i18n/locales/de/duxt/shortcuts.json +1 -0
  151. package/i18n/locales/de/duxt/theme.json +2 -1
  152. package/i18n/locales/en/duxt/announcement.json +7 -0
  153. package/i18n/locales/en/duxt/bruno.json +25 -0
  154. package/i18n/locales/en/duxt/changelog.json +1 -1
  155. package/i18n/locales/en/duxt/code.json +2 -1
  156. package/i18n/locales/en/duxt/page.json +9 -0
  157. package/i18n/locales/en/duxt/shortcuts.json +1 -0
  158. package/i18n/locales/en/duxt/theme.json +2 -1
  159. package/i18n/locales/es/duxt/announcement.json +7 -0
  160. package/i18n/locales/es/duxt/bruno.json +25 -0
  161. package/i18n/locales/es/duxt/changelog.json +1 -1
  162. package/i18n/locales/es/duxt/code.json +2 -1
  163. package/i18n/locales/es/duxt/page.json +9 -0
  164. package/i18n/locales/es/duxt/shortcuts.json +1 -0
  165. package/i18n/locales/es/duxt/theme.json +2 -1
  166. package/i18n/locales/fr/duxt/announcement.json +7 -0
  167. package/i18n/locales/fr/duxt/bruno.json +25 -0
  168. package/i18n/locales/fr/duxt/changelog.json +1 -1
  169. package/i18n/locales/fr/duxt/code.json +2 -1
  170. package/i18n/locales/fr/duxt/page.json +9 -0
  171. package/i18n/locales/fr/duxt/shortcuts.json +1 -0
  172. package/i18n/locales/fr/duxt/theme.json +2 -1
  173. package/i18n/locales/pt/duxt/announcement.json +7 -0
  174. package/i18n/locales/pt/duxt/bruno.json +25 -0
  175. package/i18n/locales/pt/duxt/changelog.json +1 -1
  176. package/i18n/locales/pt/duxt/code.json +2 -1
  177. package/i18n/locales/pt/duxt/page.json +9 -0
  178. package/i18n/locales/pt/duxt/shortcuts.json +1 -0
  179. package/i18n/locales/pt/duxt/theme.json +2 -1
  180. package/modules/bruno.ts +118 -0
  181. package/modules/config.ts +26 -0
  182. package/modules/devtools.ts +2 -3
  183. package/modules/git-meta.ts +16 -47
  184. package/modules/search-records.ts +132 -0
  185. package/nuxt.config.ts +81 -7
  186. package/og-image-cache.ts +742 -0
  187. package/package.json +42 -10
  188. package/public/devtools/checks.html +2 -2
  189. package/public/devtools/config.html +59 -4
  190. package/public/devtools/pages.html +8 -1
  191. package/public/devtools/paths.html +1 -1
  192. package/public/devtools/sources.html +2 -2
  193. package/public/devtools/versions.html +3 -0
  194. package/report.ts +28 -19
  195. package/search-records.ts +380 -0
  196. package/section-input.ts +142 -0
  197. package/section-markdown.ts +67 -0
  198. package/section-reports.ts +10 -4
  199. package/sections-bruno.ts +571 -0
  200. package/sections-changelog.ts +76 -43
  201. package/sections-openapi.ts +12 -83
  202. package/sections-resolve.ts +174 -8
  203. package/sections.ts +33 -10
  204. package/server/devtools/shell.ts +1 -1
  205. package/server/mcp/tools/list-pages.ts +77 -36
  206. package/server/mcp/tools/list-versions.ts +84 -0
  207. package/server/mcp/tools/read-page.ts +80 -26
  208. package/server/mcp/tools/search-docs.ts +87 -53
  209. package/server/middleware/raw-markdown.ts +1 -1
  210. package/server/routes/llms-full.txt.get.ts +6 -8
  211. package/server/routes/llms.txt.get.ts +1 -1
  212. package/server/utils/duxt-server-text.ts +0 -13
  213. package/server/utils/llms-pages.ts +24 -9
  214. package/server/utils/mcp-docs.ts +577 -0
  215. package/sources-cache.ts +376 -0
  216. package/sources-git.ts +186 -31
  217. package/sources-resolve.ts +59 -1
  218. package/sources.ts +122 -13
  219. package/tfplugindocs.ts +158 -0
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * The visible way into the shortcut sheet.
4
+ *
5
+ * `?` opens the sheet, and a reader can only press `?` if they already know the
6
+ * sheet is there — which is the whole discoverability problem, and one no
7
+ * keystroke can solve for itself. So the keys get a control: an icon beside the
8
+ * search, in the header, on every page.
9
+ *
10
+ * It draws the key it duplicates rather than restating it, and draws nothing
11
+ * where `shortcuts.singleCharacter: false` has unbound `?` — in which case this
12
+ * button is not a shortcut for the sheet, it is the only door to it.
13
+ */
14
+ defineOptions({
15
+ inheritAttrs: false
16
+ });
17
+
18
+ const { open } = useDuxtShortcutSheet();
19
+ const { hint } = useDuxtShortcuts();
20
+ const helpHint = computed(() => hint('help'));
21
+ </script>
22
+
23
+ <template>
24
+ <UiTooltip>
25
+ <UiTooltipTrigger as-child>
26
+ <UiButton
27
+ v-bind="$attrs"
28
+ variant="ghost"
29
+ size="icon"
30
+ :aria-label="$t('duxt.shortcuts.open')"
31
+ :aria-keyshortcuts="helpHint"
32
+ @click="open = !open"
33
+ >
34
+ <Icon name="lucide:keyboard" class="size-4" />
35
+ </UiButton>
36
+ </UiTooltipTrigger>
37
+
38
+ <UiTooltipContent side="bottom" class="flex items-center gap-2">
39
+ {{ $t('duxt.shortcuts.open') }}
40
+ <kbd
41
+ v-if="helpHint"
42
+ aria-hidden="true"
43
+ class="rounded border border-neutral-50/25 px-1.5 font-mono text-[10px]"
44
+ >
45
+ {{ helpHint }}
46
+ </kbd>
47
+ </UiTooltipContent>
48
+ </UiTooltip>
49
+ </template>
@@ -26,7 +26,7 @@ function jump(event: MouseEvent) {
26
26
  <template>
27
27
  <a
28
28
  :href="target"
29
- class="sr-only rounded-md bg-background px-4 py-2 text-sm font-medium ring-2 ring-ring focus:not-sr-only focus:fixed focus:top-3 focus:left-3 focus:z-100"
29
+ class="sr-only rounded-md bg-background px-4 py-2 text-sm font-medium ring-2 ring-ring focus:not-sr-only focus:fixed focus:top-3 focus:start-3 focus:z-100"
30
30
  @click="jump"
31
31
  >
32
32
  {{ $t('duxt.nav.skip') }}
@@ -19,18 +19,18 @@ const ids = computed(() =>
19
19
  ])
20
20
  );
21
21
 
22
- const active = useActiveHeading(ids);
22
+ const active = useActiveHeading(ids, duxt.toc?.scrollOffset);
23
23
  </script>
24
24
 
25
25
  <template>
26
26
  <div class="space-y-8 text-sm">
27
27
  <nav v-if="links?.length" :aria-label="$t('duxt.toc.title')">
28
28
  <p class="mb-3 font-medium">{{ $t('duxt.toc.title') }}</p>
29
- <ul class="space-y-1 border-l">
29
+ <ul class="space-y-1 border-s">
30
30
  <li v-for="link in links" :key="link.id">
31
31
  <a
32
32
  :href="`#${link.id}`"
33
- class="-ml-px block border-l py-1 pl-4 transition-colors"
33
+ class="-ms-px block border-s py-1 ps-4 transition-colors"
34
34
  :class="
35
35
  active === link.id
36
36
  ? 'border-primary text-foreground'
@@ -43,7 +43,7 @@ const active = useActiveHeading(ids);
43
43
  <li v-for="child in link.children" :key="child.id">
44
44
  <a
45
45
  :href="`#${child.id}`"
46
- class="-ml-px block border-l py-1 pl-7 transition-colors"
46
+ class="-ms-px block border-s py-1 ps-7 transition-colors"
47
47
  :class="
48
48
  active === child.id
49
49
  ? 'border-primary text-foreground'
@@ -69,15 +69,21 @@ const active = useActiveHeading(ids);
69
69
  <NuxtLink
70
70
  :to="localeLink(link.to)"
71
71
  :target="link.external ? '_blank' : undefined"
72
- class="flex items-center gap-2 text-muted-foreground transition-colors hover:text-foreground"
72
+ class="flex items-start gap-2 text-muted-foreground transition-colors hover:text-foreground"
73
73
  >
74
- <Icon v-if="link.icon" :name="link.icon" class="size-4" />
75
- {{ link.label }}
74
+ <!-- `mt-0.5` centres the icon on the FIRST line, not on a label
75
+ that wrapped onto two. -->
76
76
  <Icon
77
- v-if="link.external"
78
- name="lucide:arrow-up-right"
79
- class="size-3 opacity-50"
77
+ v-if="link.icon"
78
+ :name="link.icon"
79
+ class="mt-0.5 size-4 shrink-0"
80
80
  />
81
+ <!-- The arrow stays with the label's last word: as a flex sibling
82
+ it was pushed to the row's far edge whenever the label
83
+ wrapped. See DuxtLinkLabel. -->
84
+ <span class="min-w-0">
85
+ <DuxtLinkLabel :label="link.label" :external="link.external" />
86
+ </span>
81
87
  </NuxtLink>
82
88
  </li>
83
89
  </ul>
@@ -44,7 +44,7 @@ const shown = computed(() => {
44
44
  information about the page rather than a warning about its content. -->
45
45
  <UiAlert
46
46
  role="status"
47
- class="mb-6 flex items-start gap-2.5 rounded-md border border-l-2 border-l-sky-500 bg-muted/30 px-3 py-2.5"
47
+ class="mb-6 flex items-start gap-2.5 rounded-md border border-s-2 border-s-sky-500 bg-muted/30 px-3 py-2.5"
48
48
  >
49
49
  <Icon
50
50
  name="lucide:languages"
@@ -35,13 +35,6 @@ const { source } = useDuxtCollection();
35
35
  */
36
36
  const { choices: versions } = useDuxtVersion();
37
37
 
38
- /** A global changelog switches between its source's documentation editions. */
39
- const changelog = computed(
40
- () =>
41
- source.value?.generated?.type === 'changelog' &&
42
- source.value.generated.versioning === 'global'
43
- );
44
-
45
38
  const current = computed(() =>
46
39
  sourceForPath(
47
40
  path.value,
@@ -69,14 +62,62 @@ function caption(version: DuxtLink) {
69
62
  return te(value) ? t(value) : value;
70
63
  }
71
64
 
72
- /** Same page, other version: swap the prefix rather than jumping to its root. */
65
+ /**
66
+ * The pages this one would become in each version, checked where they may not
67
+ * exist.
68
+ *
69
+ * Only inside a per-version CHANGELOG, and only below its overview: a release
70
+ * page carries a release slug, and an older version's changelog has no page
71
+ * for a release cut after it. Those are looked up in the target's own
72
+ * collection; any other page keeps its place exactly as `versionPath` puts it,
73
+ * because every other per-version tree is the same tree at another ref.
74
+ *
75
+ * Nothing is read after the first `await` but plain values captured before it —
76
+ * the rule `tests/async-data-context.test.ts` holds.
77
+ */
78
+ const { data: missing } = useAsyncData(
79
+ () => `duxt-version-missing-${path.value}`,
80
+ async () => {
81
+ const sources = duxt.resolvedSources ?? [];
82
+ const own = source.value;
83
+ if (own?.generated?.type !== 'changelog' || path.value === own.prefix) {
84
+ return [];
85
+ }
86
+
87
+ const gone: string[] = [];
88
+
89
+ for (const version of versions.value) {
90
+ const target = versionPath(
91
+ path.value,
92
+ current.value?.to,
93
+ version.to ?? '/'
94
+ );
95
+ const owner = sourceForPath(target, sources);
96
+ if (!owner || owner.generated?.type !== 'changelog') continue;
97
+
98
+ const hit = await queryCollection(owner.collection as DuxtCollectionArg)
99
+ .path(target)
100
+ .select('path')
101
+ .first();
102
+ if (!hit) gone.push(target);
103
+ }
104
+
105
+ return gone;
106
+ },
107
+ { default: () => [], watch: [path] }
108
+ );
109
+
110
+ /**
111
+ * Same page, other version: swap the prefix rather than jumping to its root —
112
+ * unless the target never published that page, in which case its own root.
113
+ */
73
114
  function pathIn(version: { to?: string }) {
74
115
  return localeLink(
75
- versionPath(
116
+ versionSwitchPath(
76
117
  path.value,
77
118
  current.value?.to,
78
119
  version.to ?? '/',
79
- changelog.value
120
+ new Set(missing.value)
80
121
  )
81
122
  );
82
123
  }
@@ -99,7 +140,7 @@ function pathIn(version: { to?: string }) {
99
140
  <button
100
141
  v-if="props.variant === 'block'"
101
142
  type="button"
102
- class="flex w-full cursor-pointer items-center gap-2 rounded-md border p-2 text-left transition-colors hover:bg-accent"
143
+ class="flex w-full cursor-pointer items-center gap-2 rounded-md border p-2 text-start transition-colors hover:bg-accent"
103
144
  >
104
145
  <span
105
146
  class="flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground"
@@ -160,7 +201,7 @@ function pathIn(version: { to?: string }) {
160
201
  <span class="font-mono text-xs">{{ version.label }}</span>
161
202
  <span
162
203
  v-if="caption(version)"
163
- class="ml-auto text-xs text-muted-foreground"
204
+ class="ms-auto text-xs text-muted-foreground"
164
205
  >
165
206
  {{ caption(version) }}
166
207
  </span>
@@ -41,22 +41,22 @@ const offersUpgrade = computed(() =>
41
41
  */
42
42
  const TONES = {
43
43
  upcoming: {
44
- rule: 'border-l-sky-500',
44
+ rule: 'border-s-sky-500',
45
45
  text: 'text-sky-500',
46
46
  icon: 'lucide:flask-conical'
47
47
  },
48
48
  old: {
49
- rule: 'border-l-amber-500',
49
+ rule: 'border-s-amber-500',
50
50
  text: 'text-amber-500',
51
51
  icon: 'lucide:triangle-alert'
52
52
  },
53
53
  deprecated: {
54
- rule: 'border-l-red-500',
54
+ rule: 'border-s-red-500',
55
55
  text: 'text-red-500',
56
56
  icon: 'lucide:triangle-alert'
57
57
  },
58
58
  eol: {
59
- rule: 'border-l-red-500',
59
+ rule: 'border-s-red-500',
60
60
  text: 'text-red-500',
61
61
  icon: 'lucide:circle-off'
62
62
  }
@@ -74,7 +74,7 @@ const tone = computed(
74
74
  <UiAlert
75
75
  v-if="shouldWarn"
76
76
  role="status"
77
- class="mb-6 flex items-start gap-2.5 rounded-md border border-l-2 bg-muted/30 px-3 py-2.5"
77
+ class="mb-6 flex items-start gap-2.5 rounded-md border border-s-2 bg-muted/30 px-3 py-2.5"
78
78
  :class="tone.rule"
79
79
  >
80
80
  <Icon
@@ -21,13 +21,13 @@ const value = parent?.register(props.label ?? '') ?? 'item-0';
21
21
  <Item :value="value" class="border-b last:border-b-0">
22
22
  <AccordionHeader>
23
23
  <AccordionTrigger
24
- class="group flex w-full cursor-pointer items-center gap-2 px-4 py-3 text-left text-sm font-medium hover:bg-accent/50"
24
+ class="group flex w-full cursor-pointer items-center gap-2 px-4 py-3 text-start text-sm font-medium hover:bg-accent/50"
25
25
  >
26
26
  <Icon v-if="icon" :name="icon" class="size-4 text-muted-foreground" />
27
27
  <span class="min-w-0">{{ label }}</span>
28
28
  <Icon
29
29
  name="lucide:chevron-down"
30
- class="ml-auto size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-180"
30
+ class="ms-auto size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-180"
31
31
  />
32
32
  </AccordionTrigger>
33
33
  </AccordionHeader>
@@ -0,0 +1,260 @@
1
+ <script setup lang="ts">
2
+ import type { DuxtBrunoAuth, DuxtBrunoEntry } from '../../../bruno-model';
3
+
4
+ /**
5
+ * The collection's front page: what is in it, and how to get it.
6
+ *
7
+ * MDC: `::bruno-overview` on the section's index page, with `collection.bru`'s
8
+ * own `docs` block in the slot.
9
+ *
10
+ * THE DOWNLOAD IS THE POINT, and it is why this page is not simply a table of
11
+ * contents. A reference tells you what the requests are; a Bruno collection is
12
+ * a thing you OPEN, and a reader who wants the requests wants the files. The
13
+ * archive is built from the exact version and locale of the page they are on —
14
+ * which is what "Fetch in Bruno" below it cannot offer, since it clones
15
+ * whatever the repository's HEAD is today, and the page says so rather than
16
+ * offering the two as if they were the same thing.
17
+ *
18
+ * The Bruno button is a PLAIN LINK. Bruno publishes a `button.js` that draws
19
+ * one; loading it would put a third-party script on every page of a
20
+ * documentation site to render an anchor this site can render itself.
21
+ */
22
+ const props = defineProps<{
23
+ name?: string;
24
+ version?: string;
25
+ auth?: DuxtBrunoAuth;
26
+ headers?: DuxtBrunoEntry[];
27
+ /** How many environment files the collection has — counted, never read. */
28
+ environments?: number;
29
+ /** Absent for a remote collection: nothing outside can build the archive. */
30
+ download?: string;
31
+ /** Absent unless the site declared a public git URL. */
32
+ fetch?: string;
33
+ groups?: {
34
+ name: string;
35
+ description?: string;
36
+ requests?: number;
37
+ to: string;
38
+ }[];
39
+ requests?: { name: string; method: string; url: string; to: string }[];
40
+ }>();
41
+
42
+ const localeLink = useDuxtLink();
43
+
44
+ /**
45
+ * Every request in the collection, its folders' included.
46
+ *
47
+ * Counted here rather than written into the props, because the figure has to
48
+ * agree with the two lists below it on this very page — and a number the parser
49
+ * sent separately is a number that can disagree with them.
50
+ */
51
+ const total = computed(
52
+ () =>
53
+ (props.requests?.length ?? 0) +
54
+ (props.groups ?? []).reduce((all, group) => all + (group.requests ?? 0), 0)
55
+ );
56
+ </script>
57
+
58
+ <template>
59
+ <div>
60
+ <slot />
61
+
62
+ <div class="not-typeset">
63
+ <!-- ACQUISITION FIRST. Everything below is the table of contents, and a
64
+ reader who came for the collection itself should not have to scroll
65
+ past it. -->
66
+ <section v-if="download || fetch" class="mt-8">
67
+ <h2 class="duxt-label">{{ $t('duxt.bruno.getIt') }}</h2>
68
+
69
+ <div class="mt-3 flex flex-wrap items-center gap-3">
70
+ <UiButton v-if="download" as-child variant="default">
71
+ <a :href="download" download>
72
+ <Icon name="lucide:download" aria-hidden="true" class="size-4" />
73
+ {{ $t('duxt.bruno.download') }}
74
+ </a>
75
+ </UiButton>
76
+
77
+ <!-- `simple-icons`, and the repository's icon rule says exactly why:
78
+ this is a third party AS ITSELF — the button hands the collection
79
+ to Bruno — where the section's own navbar entry, which names a
80
+ part of the documentation, is `lucide`. -->
81
+ <UiButton v-if="fetch" as-child variant="outline">
82
+ <a :href="fetch" rel="noopener noreferrer" target="_blank">
83
+ <Icon
84
+ name="simple-icons:bruno"
85
+ aria-hidden="true"
86
+ class="size-4"
87
+ />
88
+ {{ $t('duxt.bruno.fetch') }}
89
+ </a>
90
+ </UiButton>
91
+ </div>
92
+
93
+ <p v-if="download" class="mt-2 text-sm text-muted-foreground">
94
+ {{ $t('duxt.bruno.downloadHint') }}
95
+ </p>
96
+
97
+ <p v-if="fetch" class="mt-1 text-sm text-muted-foreground">
98
+ {{ $t('duxt.bruno.fetchHint') }}
99
+ </p>
100
+ </section>
101
+
102
+ <dl
103
+ v-if="version || groups?.length || environments"
104
+ class="mt-8 grid grid-cols-2 gap-x-6 gap-y-7 border-b pb-5 sm:grid-cols-4"
105
+ >
106
+ <div v-if="version" class="flex flex-col gap-1">
107
+ <dd
108
+ class="font-mono text-2xl font-semibold tracking-tight tabular-nums sm:text-3xl"
109
+ >
110
+ {{ version }}
111
+ </dd>
112
+ <dt
113
+ class="flex items-center gap-1.5 text-xs font-medium text-primary"
114
+ >
115
+ <Icon name="lucide:tag" class="size-3.5" />
116
+ {{ $t('duxt.bruno.format') }}
117
+ </dt>
118
+ </div>
119
+
120
+ <div class="flex flex-col gap-1">
121
+ <dd
122
+ class="text-2xl font-semibold tracking-tight tabular-nums sm:text-3xl"
123
+ >
124
+ {{ total }}
125
+ </dd>
126
+ <dt
127
+ class="flex items-center gap-1.5 text-xs font-medium text-primary"
128
+ >
129
+ <Icon name="lucide:arrow-left-right" class="size-3.5" />
130
+ {{ $t('duxt.bruno.requests') }}
131
+ </dt>
132
+ </div>
133
+
134
+ <div v-if="groups?.length" class="flex flex-col gap-1">
135
+ <dd
136
+ class="text-2xl font-semibold tracking-tight tabular-nums sm:text-3xl"
137
+ >
138
+ {{ groups.length }}
139
+ </dd>
140
+ <dt
141
+ class="flex items-center gap-1.5 text-xs font-medium text-primary"
142
+ >
143
+ <Icon name="lucide:folder-tree" class="size-3.5" />
144
+ {{ $t('duxt.bruno.folders') }}
145
+ </dt>
146
+ </div>
147
+
148
+ <div v-if="environments" class="flex flex-col gap-1">
149
+ <dd
150
+ class="text-2xl font-semibold tracking-tight tabular-nums sm:text-3xl"
151
+ >
152
+ {{ environments }}
153
+ </dd>
154
+ <dt
155
+ class="flex items-center gap-1.5 text-xs font-medium text-primary"
156
+ >
157
+ <Icon name="lucide:lock" class="size-3.5" />
158
+ {{ $t('duxt.bruno.environments') }}
159
+ </dt>
160
+ </div>
161
+ </dl>
162
+
163
+ <section v-if="groups?.length" class="mt-8">
164
+ <h2 class="duxt-label">{{ $t('duxt.bruno.folders') }}</h2>
165
+
166
+ <ul class="mt-3 -mx-3">
167
+ <li
168
+ v-for="group in groups"
169
+ :key="group.to"
170
+ class="border-t border-border/60 first:border-t-0"
171
+ >
172
+ <NuxtLink
173
+ :to="localeLink(group.to) ?? group.to"
174
+ class="group flex items-start gap-3 rounded-lg px-3 py-3 transition-colors hover:bg-muted/40"
175
+ >
176
+ <span class="min-w-0 flex-1">
177
+ <span class="flex flex-wrap items-baseline gap-x-3">
178
+ <span class="font-medium">{{ group.name }}</span>
179
+ <span class="text-xs text-muted-foreground tabular-nums">
180
+ {{ group.requests }}
181
+ </span>
182
+ </span>
183
+
184
+ <span
185
+ v-if="group.description"
186
+ class="mt-1 line-clamp-2 block text-sm text-muted-foreground"
187
+ >
188
+ {{ group.description }}
189
+ </span>
190
+ </span>
191
+
192
+ <Icon
193
+ name="lucide:arrow-right"
194
+ aria-hidden="true"
195
+ 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"
196
+ />
197
+ </NuxtLink>
198
+ </li>
199
+ </ul>
200
+ </section>
201
+
202
+ <section v-if="requests?.length" class="mt-8">
203
+ <h2 class="duxt-label">{{ $t('duxt.bruno.requests') }}</h2>
204
+
205
+ <ul class="mt-3 -mx-3">
206
+ <li
207
+ v-for="entry in requests"
208
+ :key="entry.to"
209
+ class="border-t border-border/60 first:border-t-0"
210
+ >
211
+ <NuxtLink
212
+ :to="localeLink(entry.to) ?? entry.to"
213
+ class="flex flex-wrap items-center gap-3 rounded-lg px-3 py-3 transition-colors hover:bg-muted/40"
214
+ >
215
+ <DuxtOpenApiMethod :method="entry.method" class="w-16" />
216
+
217
+ <span class="font-medium sm:me-auto">{{ entry.name }}</span>
218
+
219
+ <!-- The auto margin is on the NAME above, not here: this is a
220
+ `<code>`, which the stylesheet pins to left-to-right, and a
221
+ `ms-auto` resolved against that would push the URL back to
222
+ the start of a right-to-left row. -->
223
+ <code
224
+ class="w-full font-mono text-xs break-all text-muted-foreground sm:w-auto"
225
+ >
226
+ {{ entry.url }}
227
+ </code>
228
+ </NuxtLink>
229
+ </li>
230
+ </ul>
231
+ </section>
232
+
233
+ <section v-if="auth || headers?.length" class="mt-8">
234
+ <h2 class="duxt-label">{{ $t('duxt.bruno.shared') }}</h2>
235
+
236
+ <p v-if="auth" class="mt-2 text-sm text-muted-foreground">
237
+ {{ $t('duxt.bruno.auth') }}:
238
+ <code class="font-mono">{{ auth.mode }}</code>
239
+ </p>
240
+
241
+ <div v-if="headers?.length" class="mt-3">
242
+ <DuxtBrunoEntries :entries="headers" />
243
+ </div>
244
+ </section>
245
+
246
+ <!-- STATED, ALWAYS. A reader has to know that what they are looking at is
247
+ less than the collection: the environments hold the hosts and the
248
+ keys, and the scripts and tests are code. None of it is on this site,
249
+ and saying so is what stops the page reading as the whole truth. -->
250
+ <p class="mt-8 flex items-start gap-2 text-sm text-muted-foreground">
251
+ <Icon
252
+ name="lucide:shield"
253
+ aria-hidden="true"
254
+ class="mt-0.5 size-4 shrink-0"
255
+ />
256
+ <span>{{ $t('duxt.bruno.notPublished') }}</span>
257
+ </p>
258
+ </div>
259
+ </div>
260
+ </template>
@@ -0,0 +1,141 @@
1
+ <script setup lang="ts">
2
+ import type { DuxtBrunoRequest } from '../../../bruno-model';
3
+ import type {
4
+ DuxtOpenApiOperation,
5
+ DuxtOpenApiServer
6
+ } from '../../../openapi-model';
7
+
8
+ /**
9
+ * One Bruno request, as the two columns of the reference.
10
+ *
11
+ * MDC: the `bruno` section type writes `::bruno-request` into every request
12
+ * page, with the structure as props and the file's own `docs` block in the
13
+ * slot — Markdown, because that is what a `docs` block is.
14
+ *
15
+ * THE SAME TWO COLUMNS `OpenApiOperation` DRAWS, and that is the answer this
16
+ * type was filed to find: a second API section is not a second rendering path.
17
+ * The left column is what the collection says; the right is the existing try-it
18
+ * client, handed an operation the parser adapted at build time.
19
+ *
20
+ * WHAT IS NOT HERE is the honest half. A Bruno collection has no response
21
+ * schemas, no reusable components and no field descriptions, so the page has no
22
+ * "Responses" section and no schema tree — and does not invent one. Scripts and
23
+ * tests are reported as PRESENT and never shown: they are code written against
24
+ * Bruno's runtime, and a documentation site neither prints nor runs it.
25
+ */
26
+ defineProps<{
27
+ request: DuxtBrunoRequest;
28
+ /** Present only where the site opted into a try-it client. */
29
+ operation?: DuxtOpenApiOperation;
30
+ servers?: DuxtOpenApiServer[];
31
+ }>();
32
+ </script>
33
+
34
+ <template>
35
+ <!-- The same grid `OpenApiOperation` uses, and deliberately the same numbers:
36
+ the two page kinds sit in one section row, and a reader moving between
37
+ them should not feel the column move. -->
38
+ <div
39
+ class="grid gap-8 xl:grid-cols-[minmax(0,1fr)_23rem] 2xl:grid-cols-[minmax(0,1fr)_30rem]"
40
+ >
41
+ <div class="min-w-0">
42
+ <div class="not-typeset flex flex-wrap items-center gap-2">
43
+ <DuxtOpenApiMethod :method="request.method" class="h-6 px-2" />
44
+
45
+ <code class="font-mono text-sm break-all">{{ request.url }}</code>
46
+
47
+ <!-- The url gets the same badge the entries table gives a withheld
48
+ value, and for the same reason: a blank `?api_key=` reads as a
49
+ parameter the collection forgot to fill in unless the page says
50
+ it was taken out. -->
51
+ <UiBadge v-if="request.urlRedacted" variant="secondary">
52
+ {{ $t('duxt.bruno.withheld') }}
53
+ </UiBadge>
54
+ </div>
55
+
56
+ <div class="mt-4">
57
+ <slot />
58
+ </div>
59
+
60
+ <div class="not-typeset">
61
+ <section v-if="request.auth" class="mt-8">
62
+ <h2 class="duxt-label">{{ $t('duxt.bruno.auth') }}</h2>
63
+
64
+ <p class="mt-2 text-sm text-muted-foreground">
65
+ <code class="font-mono">{{ request.auth.mode }}</code>
66
+ </p>
67
+ </section>
68
+
69
+ <section v-if="request.params?.length" class="mt-8">
70
+ <h2 class="duxt-label">{{ $t('duxt.bruno.parameters') }}</h2>
71
+
72
+ <div class="mt-3">
73
+ <DuxtBrunoEntries :entries="request.params" />
74
+ </div>
75
+ </section>
76
+
77
+ <section v-if="request.headers?.length" class="mt-8">
78
+ <h2 class="duxt-label">{{ $t('duxt.bruno.headers') }}</h2>
79
+
80
+ <div class="mt-3">
81
+ <DuxtBrunoEntries :entries="request.headers" />
82
+ </div>
83
+ </section>
84
+
85
+ <section v-if="request.body" class="mt-8">
86
+ <h2 class="duxt-label">
87
+ {{ $t('duxt.bruno.body') }}
88
+ <span class="ms-1 font-mono text-xs text-muted-foreground">
89
+ {{ request.body.type }}
90
+ </span>
91
+ </h2>
92
+
93
+ <div class="mt-3">
94
+ <DuxtCodeBlock
95
+ v-if="request.body.text"
96
+ :code="request.body.text"
97
+ :language="request.body.language ?? 'text'"
98
+ />
99
+
100
+ <DuxtBrunoEntries v-else :entries="request.body.entries" />
101
+ </div>
102
+
103
+ <div v-if="request.body.variables" class="mt-3">
104
+ <DuxtCodeBlock :code="request.body.variables" language="json" />
105
+ </div>
106
+ </section>
107
+
108
+ <!-- SAID, NOT SHOWN. A reader has to know the page is not the whole
109
+ request — a collection whose pre-request script signs a token
110
+ behaves differently from what is printed above — and the code is
111
+ neither published nor executed. The download is where it lives. -->
112
+ <p
113
+ v-if="request.script || request.tests"
114
+ class="mt-8 flex items-start gap-2 text-sm text-muted-foreground"
115
+ >
116
+ <Icon
117
+ name="lucide:file-code"
118
+ aria-hidden="true"
119
+ class="mt-0.5 size-4 shrink-0"
120
+ />
121
+ <span>{{ $t('duxt.bruno.hasCode') }}</span>
122
+ </p>
123
+ </div>
124
+ </div>
125
+
126
+ <!-- Only where the site opted in. Without a declared public base URL the
127
+ page is static, which is the decision rather than a gap: a Bruno
128
+ collection points at whatever its author could reach, and a send button
129
+ aimed at somebody's staging host is a failure the page cannot explain. -->
130
+ <div
131
+ v-if="operation"
132
+ class="not-typeset min-w-0 scroll-mt-[var(--duxt-header-offset)]"
133
+ >
134
+ <div
135
+ 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"
136
+ >
137
+ <DuxtOpenApiClient :operation="operation" :servers="servers" />
138
+ </div>
139
+ </div>
140
+ </div>
141
+ </template>