@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
@@ -35,9 +35,12 @@ const props = withDefaults(
35
35
  );
36
36
 
37
37
  const DEPTH = 8;
38
+ const duxt = useDuxtConfig();
38
39
 
39
40
  const schema = computed(() => props.schema);
40
- const deep = computed(() => props.depth >= DEPTH);
41
+ const deep = computed(
42
+ () => props.depth >= (duxt.openapi?.schemaDepth ?? DEPTH)
43
+ );
41
44
 
42
45
  const type = computed(() => openApiTypeLabel(schema.value));
43
46
  const constraints = computed(() => openApiConstraints(schema.value));
@@ -224,7 +227,7 @@ const open = computed(() => size.value < MANY);
224
227
  said. -->
225
228
  <span
226
229
  v-if="collapsible && size >= MANY"
227
- class="ml-auto rounded-md bg-muted px-1.5 font-mono text-[0.6875rem] text-muted-foreground tabular-nums"
230
+ class="ms-auto rounded-md bg-muted px-1.5 font-mono text-[0.6875rem] text-muted-foreground tabular-nums"
228
231
  :aria-label="$t('duxt.openapi.fields', { count: size })"
229
232
  >
230
233
  {{ size }}
@@ -268,7 +271,7 @@ const open = computed(() => size.value < MANY);
268
271
  <template v-else-if="!schema?.circular">
269
272
  <ul
270
273
  v-if="schema?.properties?.length"
271
- class="mt-3 space-y-3 border-l pl-4"
274
+ class="mt-3 space-y-3 border-s ps-4"
272
275
  >
273
276
  <li v-for="property in schema.properties" :key="property.name">
274
277
  <DuxtOpenApiSchema
@@ -281,7 +284,7 @@ const open = computed(() => size.value < MANY);
281
284
  </li>
282
285
  </ul>
283
286
 
284
- <div v-if="items" class="mt-3 border-l pl-4">
287
+ <div v-if="items" class="mt-3 border-s ps-4">
285
288
  <p class="mb-1 text-xs font-medium text-muted-foreground">
286
289
  {{ $t('duxt.openapi.items') }}
287
290
  </p>
@@ -292,7 +295,7 @@ const open = computed(() => size.value < MANY);
292
295
  />
293
296
  </div>
294
297
 
295
- <div v-if="additional" class="mt-3 border-l pl-4">
298
+ <div v-if="additional" class="mt-3 border-s ps-4">
296
299
  <p class="mb-1 text-xs font-medium text-muted-foreground">
297
300
  {{ $t('duxt.openapi.additionalProperties') }}
298
301
  </p>
@@ -313,7 +316,7 @@ const open = computed(() => size.value < MANY);
313
316
  <div
314
317
  v-for="[kind, list] in branches"
315
318
  :key="kind"
316
- class="mt-3 border-l pl-4"
319
+ class="mt-3 border-s ps-4"
317
320
  >
318
321
  <p class="mb-1 text-xs font-medium text-muted-foreground">
319
322
  {{ $t(`duxt.openapi.composition.${kind}`) }}
@@ -329,7 +332,7 @@ const open = computed(() => size.value < MANY);
329
332
  </ul>
330
333
  </div>
331
334
 
332
- <div v-if="schema?.not" class="mt-3 border-l pl-4">
335
+ <div v-if="schema?.not" class="mt-3 border-s ps-4">
333
336
  <p class="mb-1 text-xs font-medium text-muted-foreground">
334
337
  {{ $t('duxt.openapi.composition.not') }}
335
338
  </p>
@@ -11,9 +11,15 @@
11
11
  * A consumer that wants the answer kept listens for it:
12
12
  *
13
13
  * <DuxtPageFeedback @feedback="(helpful) => track(helpful)" />
14
+ *
15
+ * A site that has wired `duxt.analytics.track` gets the same answer as a
16
+ * `feedback` event without wrapping anything — and the event above stays, so
17
+ * this component is usable either way and neither route needs the other.
14
18
  */
15
19
  const emit = defineEmits<{ feedback: [helpful: boolean] }>();
16
20
 
21
+ const analytics = useDuxtAnalytics();
22
+
17
23
  const answered = ref<boolean | undefined>();
18
24
 
19
25
  // A new page is a new question. Without this the answer given on one page
@@ -26,6 +32,7 @@ watch(
26
32
 
27
33
  function answer(helpful: boolean) {
28
34
  answered.value = helpful;
35
+ analytics.track({ name: 'feedback', helpful });
29
36
  emit('feedback', helpful);
30
37
  }
31
38
  </script>
@@ -44,6 +44,7 @@ const page = computed(() => props.page);
44
44
 
45
45
  const { source } = useDuxtCollection();
46
46
  const { locale, t } = useI18n();
47
+ const duxt = useDuxtConfig();
47
48
 
48
49
  const file = computed(() => {
49
50
  // A GENERATED SECTION has no file per page: every page in it was split out of
@@ -80,7 +81,9 @@ const link = computed(() =>
80
81
  * the browser, which is a hydration mismatch as well as a wrong date. The
81
82
  * commit date IS a moment and is left alone.
82
83
  */
83
- const released = computed(() => changelogDate(page.value?.date, locale.value));
84
+ const released = computed(() =>
85
+ formatDate(page.value?.date, locale.value, { calendarDay: true })
86
+ );
84
87
 
85
88
  const updated = computed(() => {
86
89
  // The page's own date wins. Both lines would be true and only one is useful:
@@ -89,18 +92,18 @@ const updated = computed(() => {
89
92
  // one's and at worst years off.
90
93
  if (page.value?.date) return undefined;
91
94
 
92
- const value = page.value?.lastUpdated;
93
- if (!value) return undefined;
94
-
95
- const date = new Date(value);
96
- if (Number.isNaN(date.getTime())) return undefined;
97
-
98
- return new Intl.DateTimeFormat(locale.value, { dateStyle: 'medium' }).format(
99
- date
100
- );
95
+ return formatDate(page.value?.lastUpdated, locale.value);
101
96
  });
102
97
 
103
98
  const contributors = computed(() => page.value?.contributors ?? []);
99
+ // `DuxtText` also permits a record of strings, so the resolved-config mapped
100
+ // type narrows this all-optional object too far. It remains an object at
101
+ // runtime; only its template is configurable.
102
+ const contributorConfig = duxt.contributors as unknown as
103
+ | { avatarUrl?: string }
104
+ | undefined;
105
+ const avatar = (username: string) =>
106
+ contributorAvatar(contributorConfig?.avatarUrl, username);
104
107
 
105
108
  /**
106
109
  * What this page lets a reader DO with where it came from.
@@ -171,8 +174,8 @@ const notes = computed(() => {
171
174
  class="flex items-center gap-1.5"
172
175
  >
173
176
  <img
174
- v-if="person.username"
175
- :src="`https://github.com/${person.username}.png?size=40`"
177
+ v-if="person.username && avatar(person.username)"
178
+ :src="avatar(person.username)"
176
179
  alt=""
177
180
  width="18"
178
181
  height="18"
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import type { ContentNavigationItem } from '@nuxt/content';
2
+ import { flattenedNavigationPages } from '../utils/navigation-tree';
3
3
  // Previous and next within the current section only. Content's own
4
4
  // surroundings query walks the whole collection in one flat order, so the last
5
5
  // page of one section would offer a "next" that lands in another — crossing a
@@ -12,19 +12,7 @@ const { items } = useDuxtSection(navigation);
12
12
  const localeLink = useDuxtLink();
13
13
 
14
14
  /** The section's pages in reading order, groups flattened into their children. */
15
- const pages = computed(() => {
16
- const flat: ContentNavigationItem[] = [];
17
-
18
- const walk = (entries: ContentNavigationItem[]) => {
19
- for (const entry of entries) {
20
- if (entry.page !== false) flat.push(entry);
21
- if (entry.children?.length) walk(entry.children);
22
- }
23
- };
24
-
25
- walk(items.value);
26
- return flat;
27
- });
15
+ const pages = computed(() => flattenedNavigationPages(items.value));
28
16
 
29
17
  const index = computed(() =>
30
18
  pages.value.findIndex((page) => page.path === props.path)
@@ -44,17 +32,29 @@ const next = computed(() =>
44
32
  * chance to disagree with what the links say.
45
33
  */
46
34
  const router = useRouter();
35
+ const { hint, on } = useDuxtShortcuts();
47
36
 
48
- onDuxtShortcut(
49
- (event) =>
50
- (event.key === '[' || event.key === ']') &&
51
- !event.metaKey &&
52
- !event.ctrlKey,
53
- (event) => {
54
- const target = event.key === '[' ? previous.value : next.value;
55
- if (target?.path) void router.push(localeLink(target.path)!);
56
- }
57
- );
37
+ /**
38
+ * And they say so, on the two links they move between.
39
+ *
40
+ * These were the least findable keys in the theme: the search button at least
41
+ * carried `⌘K`, while `[` and `]` appeared nowhere a reader would look — only
42
+ * in a sheet they had to already know `?` to open. The cap goes on the link that
43
+ * does the same thing, which is where a reader is already looking when they
44
+ * want it.
45
+ *
46
+ * Nothing is drawn where the policy unbound the keys, and nothing is read out:
47
+ * `aria-hidden` keeps the link's accessible name the page it leads to, since
48
+ * "Next page ] Collections" is a worse thing to hear than the title, and the
49
+ * sheet announces the binding properly.
50
+ */
51
+ const previousHint = computed(() => hint('previous'));
52
+ const nextHint = computed(() => hint('next'));
53
+
54
+ on(['previous', 'next'], (action) => {
55
+ const target = action === 'previous' ? previous.value : next.value;
56
+ if (target?.path) void router.push(localeLink(target.path)!);
57
+ });
58
58
  </script>
59
59
 
60
60
  <template>
@@ -69,8 +69,15 @@ onDuxtShortcut(
69
69
  class="flex flex-col gap-1 rounded-lg border p-4 transition-colors hover:bg-accent"
70
70
  >
71
71
  <span class="flex items-center gap-1 text-xs text-muted-foreground">
72
- <Icon name="lucide:arrow-left" class="size-3.5" />
72
+ <Icon name="lucide:arrow-left" class="size-3.5 rtl:-scale-x-100" />
73
73
  {{ $t('duxt.nav.previous') }}
74
+ <kbd
75
+ v-if="previousHint"
76
+ aria-hidden="true"
77
+ class="ms-auto rounded border bg-muted px-1.5 font-mono text-[10px]"
78
+ >
79
+ {{ previousHint }}
80
+ </kbd>
74
81
  </span>
75
82
  <span class="font-medium">{{ previous.title }}</span>
76
83
  </NuxtLink>
@@ -79,13 +86,20 @@ onDuxtShortcut(
79
86
  <NuxtLink
80
87
  v-if="next"
81
88
  :to="localeLink(next.path)"
82
- class="flex flex-col gap-1 rounded-lg border p-4 text-right transition-colors hover:bg-accent"
89
+ class="flex flex-col gap-1 rounded-lg border p-4 text-end transition-colors hover:bg-accent"
83
90
  >
84
91
  <span
85
92
  class="flex items-center justify-end gap-1 text-xs text-muted-foreground"
86
93
  >
94
+ <kbd
95
+ v-if="nextHint"
96
+ aria-hidden="true"
97
+ class="me-auto rounded border bg-muted px-1.5 font-mono text-[10px]"
98
+ >
99
+ {{ nextHint }}
100
+ </kbd>
87
101
  {{ $t('duxt.nav.next') }}
88
- <Icon name="lucide:arrow-right" class="size-3.5" />
102
+ <Icon name="lucide:arrow-right" class="size-3.5 rtl:-scale-x-100" />
89
103
  </span>
90
104
  <span class="font-medium">{{ next.title }}</span>
91
105
  </NuxtLink>
@@ -42,7 +42,7 @@ withDefaults(
42
42
  /**
43
43
  * `docs` — a Markdown page: two sidebars, prose, a code card.
44
44
  * `api` — an operation list: a run of method-and-path rows.
45
- * `text` — a plain-text artefact such as `llms.txt`: no chrome at all,
45
+ * `text` — a plain-text artefact such as `llms.txt`: no layout at all,
46
46
  * because the browser renders one in a bare monospace document.
47
47
  */
48
48
  variant?: DuxtSkeletonVariant;
@@ -81,7 +81,7 @@ withDefaults(
81
81
  <!-- The navbar row, then a clear gap, then the version pill, the search
82
82
  box at its real `w-56` and the three icons. Measured off the page
83
83
  this stands in for, not sketched. -->
84
- <div class="ml-auto hidden items-center gap-6 @2xl:flex">
84
+ <div class="ms-auto hidden items-center gap-6 @2xl:flex">
85
85
  <div
86
86
  v-for="width in [32, 62, 50, 42]"
87
87
  :key="width"
@@ -90,7 +90,7 @@ withDefaults(
90
90
  />
91
91
  </div>
92
92
 
93
- <div class="ml-6 flex items-center gap-6">
93
+ <div class="ms-6 flex items-center gap-6">
94
94
  <div class="h-5 w-12 rounded-full bg-muted/60" />
95
95
  <div class="h-8 w-56 rounded-md border bg-muted/30 @max-2xl:w-24" />
96
96
  <div class="hidden gap-6 @2xl:flex">
@@ -181,7 +181,7 @@ withDefaults(
181
181
  :style="{ width: `${row.path}px` }"
182
182
  />
183
183
  <div
184
- class="ml-auto h-2.5 rounded bg-muted/40"
184
+ class="ms-auto h-2.5 rounded bg-muted/40"
185
185
  :style="{ width: `${row.summary}px` }"
186
186
  />
187
187
  </div>
@@ -8,27 +8,15 @@
8
8
  * reader cannot already ask the document.
9
9
  *
10
10
  * Measured against the document, not against a scroll container, so it is
11
- * correct on the docs layout and on the landing page alike. Passive listener:
12
- * this must never be the thing that makes scrolling stutter.
11
+ * correct on the docs layout and on the landing page alike.
13
12
  */
14
13
  const progress = ref(0);
15
14
 
16
- function measure() {
15
+ useDuxtViewportMeasure(() => {
17
16
  const doc = document.documentElement;
18
17
  const scrollable = doc.scrollHeight - doc.clientHeight;
19
18
 
20
19
  progress.value = scrollable > 0 ? (doc.scrollTop / scrollable) * 100 : 0;
21
- }
22
-
23
- onMounted(() => {
24
- measure();
25
- window.addEventListener('scroll', measure, { passive: true });
26
- window.addEventListener('resize', measure, { passive: true });
27
-
28
- onBeforeUnmount(() => {
29
- window.removeEventListener('scroll', measure);
30
- window.removeEventListener('resize', measure);
31
- });
32
20
  });
33
21
  </script>
34
22