@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
@@ -8,12 +8,14 @@
8
8
  get no marker at all. And the list PADDING: typeset gives every `ol` a
9
9
  `padding-inline-start` and every `li` one of its own, which pushed the
10
10
  items — and with them the markers, positioned relative to each item —
11
- clear of the rail they are supposed to sit on. `pl-0` on both puts the
11
+ clear of the rail they are supposed to sit on. `ps-0` on both puts the
12
12
  item's edge back at the container's own padding, which is what
13
- `-left-9` is measured against: 1.5rem in, 2.25rem back out, half a
14
- marker wide — centred on the line. -->
13
+ `-start-9` is measured against: 1.5rem in, 2.25rem back out, half a
14
+ marker wide — centred on the line. The logical spellings matter twice
15
+ over here: typeset's own indent is `padding-inline-start`, so a `pl-0`
16
+ reset would miss it entirely in a right-to-left page. -->
15
17
  <div
16
- class="my-6 border-l pl-6 [counter-reset:step] [&>ol]:ml-0 [&>ol]:list-none [&>ol]:pl-0 [&>ol>li]:relative [&>ol>li]:my-6 [&>ol>li]:pl-0 [&>ol>li]:[counter-increment:step] [&>ol>li]:before:absolute [&>ol>li]:before:-left-9 [&>ol>li]:before:flex [&>ol>li]:before:size-6 [&>ol>li]:before:items-center [&>ol>li]:before:justify-center [&>ol>li]:before:rounded-full [&>ol>li]:before:bg-muted [&>ol>li]:before:text-xs [&>ol>li]:before:font-medium [&>ol>li]:before:text-foreground [&>ol>li]:before:content-[counter(step)]"
18
+ class="my-6 border-s ps-6 [counter-reset:step] [&>ol]:ms-0 [&>ol]:list-none [&>ol]:ps-0 [&>ol>li]:relative [&>ol>li]:my-6 [&>ol>li]:ps-0 [&>ol>li]:[counter-increment:step] [&>ol>li]:before:absolute [&>ol>li]:before:-start-9 [&>ol>li]:before:flex [&>ol>li]:before:size-6 [&>ol>li]:before:items-center [&>ol>li]:before:justify-center [&>ol>li]:before:rounded-full [&>ol>li]:before:bg-muted [&>ol>li]:before:text-xs [&>ol>li]:before:font-medium [&>ol>li]:before:text-foreground [&>ol>li]:before:content-[counter(step)]"
17
19
  >
18
20
  <slot />
19
21
  </div>
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { TabsList, TabsRoot, TabsTrigger } from 'reka-ui';
2
+ import { TabsRoot } from 'reka-ui';
3
3
 
4
4
  /**
5
5
  * `::tab-group` with `:::tab{label="…"}` children.
@@ -11,11 +11,11 @@ import { TabsList, TabsRoot, TabsTrigger } from 'reka-ui';
11
11
  * A content component may not take a name the UI kit owns. `::code-group` next
12
12
  * to it reads the same way, which is the consolation.
13
13
  *
14
- * Built on reka-ui's primitives directly rather than on the shadcn `Tabs`
15
- * wrapper, because the labels are not known until the children have registered
16
- * themselves: MDC hands them in as slot content, not as a prop. A provide/inject
17
- * pair collects them, which is also what makes a `:::tab` work at any depth
18
- * inside the block.
14
+ * The root is reka-ui's own, because the labels are not known until the
15
+ * children have registered themselves: MDC hands them in as slot content, not as
16
+ * a prop. A provide/inject pair collects them, which is also what makes a
17
+ * `:::tab` work at any depth inside the block. The strip is the UI kit's, in its
18
+ * `underline` look.
19
19
  *
20
20
  * Keyboard navigation, roving focus and the ARIA roles come from reka-ui — the
21
21
  * reason not to hand-roll this out of divs.
@@ -39,19 +39,20 @@ provide<DuxtTabGroupContext>('duxt-tabs', {
39
39
 
40
40
  <template>
41
41
  <TabsRoot v-model="active" class="my-6">
42
- <TabsList
43
- class="flex gap-1 border-b"
42
+ <UiTabsList
43
+ variant="bare"
44
+ class="border-b"
44
45
  :aria-label="$t('duxt.page.tabs') as string"
45
46
  >
46
- <TabsTrigger
47
+ <UiTabsTrigger
47
48
  v-for="tab in tabs"
48
49
  :key="tab.value"
49
50
  :value="tab.value"
50
- class="-mb-px cursor-pointer border-b-2 border-transparent px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground data-[state=active]:border-primary data-[state=active]:text-foreground"
51
+ variant="underline"
51
52
  >
52
53
  {{ tab.label }}
53
- </TabsTrigger>
54
- </TabsList>
54
+ </UiTabsTrigger>
55
+ </UiTabsList>
55
56
 
56
57
  <slot />
57
58
  </TabsRoot>
@@ -15,7 +15,7 @@ const props = defineProps<{
15
15
  :class="cn('[&>svg]:size-3.5', props.class)"
16
16
  >
17
17
  <slot>
18
- <Icon name="lucide:chevron-right" />
18
+ <Icon name="lucide:chevron-right" class="rtl:-scale-x-100" />
19
19
  </slot>
20
20
  </li>
21
21
  </template>
@@ -11,7 +11,7 @@ const props = defineProps<{
11
11
  <span
12
12
  data-slot="command-shortcut"
13
13
  :class="
14
- cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)
14
+ cn('text-muted-foreground ms-auto text-xs tracking-widest', props.class)
15
15
  "
16
16
  >
17
17
  <slot />
@@ -28,7 +28,7 @@ const props = withDefaults(
28
28
  );
29
29
  const emits = defineEmits<DialogContentEmits>();
30
30
 
31
- const delegatedProps = reactiveOmit(props, 'class');
31
+ const delegatedProps = reactiveOmit(props, 'class', 'showCloseButton');
32
32
 
33
33
  const forwarded = useForwardPropsEmits(delegatedProps, emits);
34
34
  </script>
@@ -51,7 +51,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
51
51
  <DialogClose
52
52
  v-if="showCloseButton"
53
53
  data-slot="dialog-close"
54
- class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
54
+ class="ring-offset-background focus-visible:ring-ring absolute top-3 end-3 inline-flex size-7 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
55
55
  >
56
56
  <Icon name="lucide:x" />
57
57
  <span class="sr-only">Close</span>
@@ -10,7 +10,7 @@ const props = defineProps<{
10
10
  <template>
11
11
  <div
12
12
  data-slot="dialog-header"
13
- :class="cn('flex flex-col gap-2 text-center sm:text-left', props.class)"
13
+ :class="cn('flex flex-col gap-2 text-center sm:text-start', props.class)"
14
14
  >
15
15
  <slot />
16
16
  </div>
@@ -54,7 +54,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
54
54
  <slot />
55
55
 
56
56
  <DialogClose
57
- class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary"
57
+ class="absolute top-4 end-4 p-0.5 transition-colors rounded-md hover:bg-secondary"
58
58
  >
59
59
  <Icon name="lucide:x" class="w-4 h-4" />
60
60
  <span class="sr-only">Close</span>
@@ -28,13 +28,13 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
28
28
  v-bind="forwarded"
29
29
  :class="
30
30
  cn(
31
- `focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
31
+ `focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer items-center gap-2 rounded-sm py-1.5 pe-2 ps-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
32
32
  props.class
33
33
  )
34
34
  "
35
35
  >
36
36
  <span
37
- class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"
37
+ class="pointer-events-none absolute start-2 flex size-3.5 items-center justify-center"
38
38
  >
39
39
  <DropdownMenuItemIndicator>
40
40
  <slot name="indicator-icon">
@@ -31,7 +31,7 @@ const forwardedProps = useForwardProps(delegatedProps);
31
31
  v-bind="forwardedProps"
32
32
  :class="
33
33
  cn(
34
- `relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground cursor-pointer data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!`,
34
+ `relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground cursor-pointer data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:ps-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!`,
35
35
  props.class
36
36
  )
37
37
  "
@@ -19,7 +19,7 @@ const forwardedProps = useForwardProps(delegatedProps);
19
19
  :data-inset="inset ? '' : undefined"
20
20
  v-bind="forwardedProps"
21
21
  :class="
22
- cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', props.class)
22
+ cn('px-2 py-1.5 text-sm font-medium data-[inset]:ps-8', props.class)
23
23
  "
24
24
  >
25
25
  <slot />
@@ -29,13 +29,13 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
29
29
  v-bind="forwarded"
30
30
  :class="
31
31
  cn(
32
- `focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
32
+ `focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer items-center gap-2 rounded-sm py-1.5 pe-2 ps-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
33
33
  props.class
34
34
  )
35
35
  "
36
36
  >
37
37
  <span
38
- class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"
38
+ class="pointer-events-none absolute start-2 flex size-3.5 items-center justify-center"
39
39
  >
40
40
  <DropdownMenuItemIndicator>
41
41
  <slot name="indicator-icon">
@@ -11,7 +11,7 @@ const props = defineProps<{
11
11
  <span
12
12
  data-slot="dropdown-menu-shortcut"
13
13
  :class="
14
- cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)
14
+ cn('text-muted-foreground ms-auto text-xs tracking-widest', props.class)
15
15
  "
16
16
  >
17
17
  <slot />
@@ -23,12 +23,12 @@ const forwardedProps = useForwardProps(delegatedProps);
23
23
  :data-inset="inset ? '' : undefined"
24
24
  :class="
25
25
  cn(
26
- `relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!`,
26
+ `relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:ps-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!`,
27
27
  props.class
28
28
  )
29
29
  "
30
30
  >
31
31
  <slot />
32
- <Icon name="lucide:chevron-right" class="ml-auto size-4" />
32
+ <Icon name="lucide:chevron-right" class="ms-auto size-4 rtl:-scale-x-100" />
33
33
  </DropdownMenuSubTrigger>
34
34
  </template>
@@ -24,7 +24,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
24
24
  v-bind="forwarded"
25
25
  :class="
26
26
  cn(
27
- 'data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto',
27
+ 'data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out ltr:data-[motion=from-end]:slide-in-from-right-52 ltr:data-[motion=from-start]:slide-in-from-left-52 ltr:data-[motion=to-end]:slide-out-to-right-52 ltr:data-[motion=to-start]:slide-out-to-left-52 rtl:data-[motion=from-end]:slide-in-from-left-52 rtl:data-[motion=from-start]:slide-in-from-right-52 rtl:data-[motion=to-end]:slide-out-to-left-52 rtl:data-[motion=to-start]:slide-out-to-right-52 top-0 start-0 w-full p-2 pe-2.5 md:absolute md:w-auto',
28
28
  'group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none',
29
29
  props.class
30
30
  )
@@ -24,7 +24,7 @@ const forwardedProps = useForwardProps(delegatedProps);
24
24
  <slot />
25
25
  <Icon
26
26
  name="lucide:chevron-down"
27
- class="relative top-px ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
27
+ class="relative top-px ms-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
28
28
  aria-hidden="true"
29
29
  />
30
30
  </NavigationMenuTrigger>
@@ -15,7 +15,7 @@ const forwardedProps = useForwardProps(delegatedProps);
15
15
  </script>
16
16
 
17
17
  <template>
18
- <div class="absolute top-full left-0 isolate z-50 flex justify-center">
18
+ <div class="absolute top-full start-0 isolate z-50 flex justify-center">
19
19
  <NavigationMenuViewport
20
20
  data-slot="navigation-menu-viewport"
21
21
  v-bind="forwardedProps"
@@ -23,7 +23,7 @@ const delegatedProps = reactiveOmit(props, 'class');
23
23
  cn(
24
24
  'flex touch-none p-px transition-colors select-none',
25
25
  orientation === 'vertical' &&
26
- 'h-full w-2.5 border-l border-l-transparent',
26
+ 'h-full w-2.5 border-s border-s-transparent',
27
27
  orientation === 'horizontal' &&
28
28
  'h-2.5 flex-col border-t border-t-transparent',
29
29
  props.class
@@ -24,12 +24,12 @@ const forwardedProps = useForwardProps(delegatedProps);
24
24
  v-bind="forwardedProps"
25
25
  :class="
26
26
  cn(
27
- 'relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
27
+ 'relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 ps-2 pe-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
28
28
  props.class
29
29
  )
30
30
  "
31
31
  >
32
- <span class="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
32
+ <span class="absolute end-2 flex h-3.5 w-3.5 items-center justify-center">
33
33
  <SelectItemIndicator>
34
34
  <Icon name="lucide:check" class="size-4" />
35
35
  </SelectItemIndicator>
@@ -11,9 +11,28 @@ import {
11
11
  import { cn } from '@duxt/lib/utils';
12
12
  import SheetOverlay from './SheetOverlay.vue';
13
13
 
14
+ /**
15
+ * `start` and `end` are ADDED beside `left` and `right`, never instead of them.
16
+ *
17
+ * A consumer asking for `side="left"` means the left of the screen and should
18
+ * keep getting it — a drawer pinned to a fixed edge is a legitimate thing to
19
+ * want, and silently redefining the word would move somebody's sheet the first
20
+ * time they shipped a right-to-left locale. `start` and `end` are the ones that
21
+ * follow the reader, and they are what this layer's own navigation uses.
22
+ *
23
+ * THE DEFAULT IS LOGICAL, the same answer `SidebarProps.side` gives. The two
24
+ * props ask one question and it would be strange to answer it twice: a sheet
25
+ * opened with no side named is not a sheet somebody pinned to an edge, it is
26
+ * one nobody thought about, and the theme's default for a thing nobody thought
27
+ * about is to follow the reader. `end` resolves to `right` in every locale this
28
+ * layer ships, so nothing moves today; what changes is the consumer who writes
29
+ * a bare `<UiSheetContent>` and later declares `dir: 'rtl'`, for whom the panel
30
+ * arrives on the side the page starts from rather than needing a second edit.
31
+ * Naming `right` explicitly is still how you pin it to the screen's right.
32
+ */
14
33
  interface SheetContentProps extends DialogContentProps {
15
34
  class?: HTMLAttributes['class'];
16
- side?: 'top' | 'right' | 'bottom' | 'left';
35
+ side?: 'top' | 'right' | 'bottom' | 'left' | 'start' | 'end';
17
36
  }
18
37
 
19
38
  defineOptions({
@@ -21,7 +40,7 @@ defineOptions({
21
40
  });
22
41
 
23
42
  const props = withDefaults(defineProps<SheetContentProps>(), {
24
- side: 'right'
43
+ side: 'end'
25
44
  });
26
45
  const emits = defineEmits<DialogContentEmits>();
27
46
 
@@ -46,6 +65,13 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
46
65
  'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
47
66
  side === 'bottom' &&
48
67
  'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
68
+ // The logical pair. Everything but the slide is expressible
69
+ // logically; `slide-in-from-*` names a physical edge and has no
70
+ // logical spelling at all, so each direction states its own.
71
+ side === 'start' &&
72
+ 'ltr:data-[state=closed]:slide-out-to-left ltr:data-[state=open]:slide-in-from-left rtl:data-[state=closed]:slide-out-to-right rtl:data-[state=open]:slide-in-from-right inset-y-0 start-0 h-full w-3/4 border-e sm:max-w-sm',
73
+ side === 'end' &&
74
+ 'ltr:data-[state=closed]:slide-out-to-right ltr:data-[state=open]:slide-in-from-right rtl:data-[state=closed]:slide-out-to-left rtl:data-[state=open]:slide-in-from-left inset-y-0 end-0 h-full w-3/4 border-s sm:max-w-sm',
49
75
  props.class
50
76
  )
51
77
  "
@@ -58,7 +84,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
58
84
  the 24px target size axe asks for. Same ghost treatment as every
59
85
  other icon button in the header, so it reads as one. -->
60
86
  <DialogClose
61
- class="ring-offset-background focus:ring-ring absolute top-3 right-3 flex size-8 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"
87
+ class="ring-offset-background focus:ring-ring absolute top-3 end-3 flex size-8 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"
62
88
  >
63
89
  <Icon name="lucide:x" class="size-5" />
64
90
  <span class="sr-only">Close</span>
@@ -5,6 +5,7 @@ import { Sheet, SheetContent } from '@duxt/components/ui/sheet';
5
5
  import SheetDescription from '@duxt/components/ui/sheet/SheetDescription.vue';
6
6
  import SheetHeader from '@duxt/components/ui/sheet/SheetHeader.vue';
7
7
  import SheetTitle from '@duxt/components/ui/sheet/SheetTitle.vue';
8
+ import { useDirection } from 'reka-ui';
8
9
  import { SIDEBAR_WIDTH_MOBILE, useSidebar } from './utils';
9
10
 
10
11
  defineOptions({
@@ -12,12 +13,35 @@ defineOptions({
12
13
  });
13
14
 
14
15
  const props = withDefaults(defineProps<SidebarProps>(), {
15
- side: 'left',
16
+ side: 'start',
16
17
  variant: 'sidebar',
17
18
  collapsible: 'offcanvas'
18
19
  });
19
20
 
20
21
  const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
22
+
23
+ /**
24
+ * The logical side, resolved to a physical one ONCE.
25
+ *
26
+ * Everything below this line — `data-side`, the rail's `[data-side=left]`
27
+ * selectors, the border and offset classes — is physical and stays physical,
28
+ * because it describes where the panel actually is on the screen. Turning
29
+ * `start` into `left` here is what lets all of it keep working while the prop
30
+ * a consumer writes follows the reader.
31
+ *
32
+ * reka's `useDirection` rather than the i18n locale: this is a ui primitive,
33
+ * and the `ConfigProvider` the app shell mounts is the one place the direction
34
+ * is already published to components that must not know about i18n.
35
+ */
36
+ const direction = useDirection();
37
+
38
+ const resolvedSide = computed(() => {
39
+ if (props.side === 'start')
40
+ return direction.value === 'rtl' ? 'right' : 'left';
41
+ if (props.side === 'end') return direction.value === 'rtl' ? 'left' : 'right';
42
+
43
+ return props.side;
44
+ });
21
45
  </script>
22
46
 
23
47
  <template>
@@ -45,7 +69,7 @@ const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
45
69
  data-sidebar="sidebar"
46
70
  data-slot="sidebar"
47
71
  data-mobile="true"
48
- :side="side"
72
+ :side="resolvedSide"
49
73
  class="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
50
74
  :style="{
51
75
  '--sidebar-width': SIDEBAR_WIDTH_MOBILE
@@ -68,7 +92,7 @@ const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
68
92
  :data-state="state"
69
93
  :data-collapsible="state === 'collapsed' ? collapsible : ''"
70
94
  :data-variant="variant"
71
- :data-side="side"
95
+ :data-side="resolvedSide"
72
96
  >
73
97
  <!-- This is what handles the sidebar gap on desktop -->
74
98
  <div
@@ -87,7 +111,7 @@ const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
87
111
  :class="
88
112
  cn(
89
113
  'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
90
- side === 'left'
114
+ resolvedSide === 'left'
91
115
  ? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
92
116
  : 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
93
117
  // Adjust the padding for floating and inset variants.
@@ -19,7 +19,7 @@ const props = defineProps<
19
19
  :as-child="asChild"
20
20
  :class="
21
21
  cn(
22
- 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
22
+ 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 end-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
23
23
  'after:absolute after:-inset-2 md:after:hidden',
24
24
  'group-data-[collapsible=icon]:hidden',
25
25
  props.class
@@ -13,7 +13,7 @@ const props = defineProps<{
13
13
  :class="
14
14
  cn(
15
15
  'bg-background relative flex w-full flex-1 flex-col',
16
- 'md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2',
16
+ 'md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ms-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ms-2',
17
17
  props.class
18
18
  )
19
19
  "
@@ -23,7 +23,7 @@ const props = withDefaults(
23
23
  data-sidebar="menu-action"
24
24
  :class="
25
25
  cn(
26
- 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
26
+ 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 end-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
27
27
  'after:absolute after:-inset-2 md:after:hidden',
28
28
  'peer-data-[size=sm]/menu-button:top-1',
29
29
  'peer-data-[size=default]/menu-button:top-1.5',
@@ -13,7 +13,7 @@ const props = defineProps<{
13
13
  data-sidebar="menu-badge"
14
14
  :class="
15
15
  cn(
16
- 'text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none',
16
+ 'text-sidebar-foreground pointer-events-none absolute end-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none',
17
17
  'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
18
18
  'peer-data-[size=sm]/menu-button:top-1',
19
19
  'peer-data-[size=default]/menu-button:top-1.5',
@@ -13,7 +13,7 @@ const props = defineProps<{
13
13
  data-sidebar="menu-badge"
14
14
  :class="
15
15
  cn(
16
- 'border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5',
16
+ 'border-sidebar-border mx-3.5 flex min-w-0 ltr:translate-x-px rtl:-translate-x-px flex-col gap-1 border-s px-2.5 py-0.5',
17
17
  'group-data-[collapsible=icon]:hidden',
18
18
  props.class
19
19
  )
@@ -29,7 +29,7 @@ const props = withDefaults(
29
29
  :data-active="isActive"
30
30
  :class="
31
31
  cn(
32
- 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
32
+ 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 ltr:-translate-x-px rtl:translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
33
33
  'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
34
34
  size === 'sm' && 'text-xs',
35
35
  size === 'md' && 'text-sm',
@@ -1,11 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import type { HTMLAttributes, Ref } from 'vue';
3
- import {
4
- defaultDocument,
5
- useEventListener,
6
- useMediaQuery,
7
- useVModel
8
- } from '@vueuse/core';
3
+ import { defaultDocument, useMediaQuery, useVModel } from '@vueuse/core';
9
4
  import { TooltipProvider } from 'reka-ui';
10
5
  import { computed, ref } from 'vue';
11
6
  import { cn } from '@duxt/lib/utils';
@@ -13,7 +8,6 @@ import {
13
8
  provideSidebarContext,
14
9
  SIDEBAR_COOKIE_MAX_AGE,
15
10
  SIDEBAR_COOKIE_NAME,
16
- SIDEBAR_KEYBOARD_SHORTCUT,
17
11
  SIDEBAR_WIDTH,
18
12
  SIDEBAR_WIDTH_ICON
19
13
  } from './utils';
@@ -62,15 +56,12 @@ function toggleSidebar() {
62
56
  : setOpen(!open.value);
63
57
  }
64
58
 
65
- useEventListener('keydown', (event: KeyboardEvent) => {
66
- if (
67
- event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
68
- (event.metaKey || event.ctrlKey)
69
- ) {
70
- event.preventDefault();
71
- toggleSidebar();
72
- }
73
- });
59
+ // Upstream binds `⌘/Ctrl+B` here. It is gone deliberately: a global key
60
+ // registered by a primitive is one `useDuxtShortcuts()` does not know about, so
61
+ // the sheet cannot list it, the documentation cannot describe it and
62
+ // `shortcuts.singleCharacter` cannot switch it off — a shortcut that works and
63
+ // that nobody can find. `SidebarTrigger` is how the sidebar is toggled; a site
64
+ // that wants a key for it declares one the same way the layer does.
74
65
 
75
66
  // We add a state so that we can do data-state="expanded" or "collapsed".
76
67
  // This makes it easier to style the sidebar with Tailwind classes.
@@ -3,7 +3,15 @@ import type { HTMLAttributes } from 'vue';
3
3
  import { cva } from 'class-variance-authority';
4
4
 
5
5
  export interface SidebarProps {
6
- side?: 'left' | 'right';
6
+ /**
7
+ * `start`/`end` follow the reader; `left`/`right` pin to the screen.
8
+ *
9
+ * The default moved from `left` to `start`, which is the same edge in a
10
+ * left-to-right page and the mirrored one in a right-to-left page — so an
11
+ * existing site sees no change and a translated one stops being wrong. The
12
+ * physical pair stays for a drawer that genuinely means a fixed edge.
13
+ */
14
+ side?: 'left' | 'right' | 'start' | 'end';
7
15
  variant?: 'sidebar' | 'floating' | 'inset';
8
16
  collapsible?: 'offcanvas' | 'icon' | 'none';
9
17
  class?: HTMLAttributes['class'];
@@ -36,7 +44,7 @@ export { default as SidebarTrigger } from './SidebarTrigger.vue';
36
44
  export { useSidebar } from './utils';
37
45
 
38
46
  export const sidebarMenuButtonVariants = cva(
39
- 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
47
+ 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-start text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pe-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
40
48
  {
41
49
  variants: {
42
50
  variant: {
@@ -6,7 +6,6 @@ export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
6
6
  export const SIDEBAR_WIDTH = '16rem';
7
7
  export const SIDEBAR_WIDTH_MOBILE = '18rem';
8
8
  export const SIDEBAR_WIDTH_ICON = '3rem';
9
- export const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
10
9
 
11
10
  export const [useSidebar, provideSidebarContext] = createContext<{
12
11
  state: ComputedRef<'expanded' | 'collapsed'>;
@@ -11,16 +11,34 @@ const {
11
11
  closeButton = true,
12
12
  duration = 4000,
13
13
  position = 'bottom-right',
14
+ dir,
14
15
  ...forwarded
15
16
  } = defineProps<ToasterProps>();
16
17
 
18
+ // 4000 ms is part of this layer-owned component's notification behaviour.
19
+ // A site needing different notification UX overrides the component instead of
20
+ // taking on a one-purpose configuration key.
21
+
17
22
  const theme = computed(() => (colorMode.value === 'dark' ? 'dark' : 'light'));
23
+
24
+ /**
25
+ * Sonner reads neither the document nor the reka provider — same as the theme
26
+ * above, it has to be told. Left out, a toast in a right-to-left page keeps its
27
+ * close button and its icon on the western side of the card.
28
+ *
29
+ * Pulled out of `forwarded` rather than bound beside it, so a caller passing
30
+ * `dir` explicitly still wins; the locale is the default, not an override.
31
+ */
32
+ const locale = useDuxtDirection();
33
+
34
+ const direction = computed(() => dir ?? locale.value);
18
35
  </script>
19
36
 
20
37
  <template>
21
38
  <Sonner
22
39
  class="toaster group"
23
40
  :theme="theme"
41
+ :dir="direction"
24
42
  :close-button="closeButton"
25
43
  :duration="duration"
26
44
  :position="position"
@@ -5,11 +5,19 @@ import { reactiveOmit } from '@vueuse/core';
5
5
  import { TabsList } from 'reka-ui';
6
6
  import { cn } from '@duxt/lib/utils';
7
7
 
8
+ /**
9
+ * `variant="bare"` is the strip inside a code card's toolbar: no track of its
10
+ * own, because the toolbar already is one. `default` is shadcn's segmented
11
+ * track, kept for a consumer who wants it.
12
+ */
8
13
  const props = defineProps<
9
- TabsListProps & { class?: HTMLAttributes['class'] }
14
+ TabsListProps & {
15
+ class?: HTMLAttributes['class'];
16
+ variant?: 'default' | 'bare';
17
+ }
10
18
  >();
11
19
 
12
- const delegatedProps = reactiveOmit(props, 'class');
20
+ const delegatedProps = reactiveOmit(props, 'class', 'variant');
13
21
  </script>
14
22
 
15
23
  <template>
@@ -18,7 +26,9 @@ const delegatedProps = reactiveOmit(props, 'class');
18
26
  v-bind="delegatedProps"
19
27
  :class="
20
28
  cn(
21
- 'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-0.75',
29
+ props.variant === 'bare'
30
+ ? 'flex items-center gap-1'
31
+ : 'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-0.75',
22
32
  props.class
23
33
  )
24
34
  "