@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.
- package/README.md +24 -9
- package/app/app.vue +17 -11
- package/app/assets/css/duxt.css +47 -6
- package/app/components/DuxtAnnouncements.vue +117 -0
- package/app/components/DuxtBrunoEntries.vue +62 -0
- package/app/components/DuxtCodeBlock.vue +18 -34
- package/app/components/DuxtCodeToolbar.vue +14 -0
- package/app/components/DuxtCopyButton.vue +33 -0
- package/app/components/DuxtCopyPage.vue +32 -30
- package/app/components/DuxtFooter.vue +39 -8
- package/app/components/DuxtHeader.vue +107 -48
- package/app/components/DuxtLandingProse.vue +1 -1
- package/app/components/DuxtLinkLabel.vue +36 -0
- package/app/components/DuxtLiveWindow.vue +8 -8
- package/app/components/DuxtLocale.vue +10 -1
- package/app/components/DuxtNavigation.vue +2 -2
- package/app/components/DuxtOpenApiClient.vue +285 -90
- package/app/components/DuxtOpenApiMedia.vue +30 -18
- package/app/components/DuxtOpenApiSchema.vue +10 -7
- package/app/components/DuxtPageFeedback.vue +7 -0
- package/app/components/DuxtPageInfo.vue +15 -12
- package/app/components/DuxtPageNav.vue +41 -27
- package/app/components/DuxtPreviewSkeleton.vue +4 -4
- package/app/components/DuxtProgress.vue +2 -14
- package/app/components/DuxtSearch.vue +230 -90
- package/app/components/DuxtSearchInput.vue +70 -0
- package/app/components/DuxtSections.vue +11 -1
- package/app/components/DuxtSegmented.vue +64 -0
- package/app/components/DuxtShortcuts.vue +16 -11
- package/app/components/DuxtShortcutsTrigger.vue +49 -0
- package/app/components/DuxtSkipLink.vue +1 -1
- package/app/components/DuxtToc.vue +16 -10
- package/app/components/DuxtTranslationBanner.vue +1 -1
- package/app/components/DuxtVersion.vue +53 -12
- package/app/components/DuxtVersionBanner.vue +5 -5
- package/app/components/content/AccordionItem.vue +2 -2
- package/app/components/content/BrunoOverview.vue +260 -0
- package/app/components/content/BrunoRequest.vue +141 -0
- package/app/components/content/BrunoRequests.vue +104 -0
- package/app/components/content/Callout.vue +5 -5
- package/app/components/content/ChangelogReleases.vue +161 -40
- package/app/components/content/CodeGroup.vue +41 -37
- package/app/components/content/Field.vue +1 -1
- package/app/components/content/FileTree.vue +1 -1
- package/app/components/content/OpenApiOperation.vue +4 -4
- package/app/components/content/OpenApiOperations.vue +1 -1
- package/app/components/content/OpenApiOverview.vue +1 -1
- package/app/components/content/PackageManagers.vue +42 -58
- package/app/components/content/PageCards.vue +7 -4
- package/app/components/content/Preview.vue +15 -39
- package/app/components/content/ProseImg.vue +596 -49
- package/app/components/content/Since.vue +1 -1
- package/app/components/content/Steps.vue +6 -4
- package/app/components/content/TabGroup.vue +13 -12
- package/app/components/ui/breadcrumb/BreadcrumbSeparator.vue +1 -1
- package/app/components/ui/command/CommandShortcut.vue +1 -1
- package/app/components/ui/dialog/DialogContent.vue +2 -2
- package/app/components/ui/dialog/DialogHeader.vue +1 -1
- package/app/components/ui/dialog/DialogScrollContent.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +2 -2
- package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +2 -2
- package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +2 -2
- package/app/components/ui/navigation-menu/NavigationMenuContent.vue +1 -1
- package/app/components/ui/navigation-menu/NavigationMenuTrigger.vue +1 -1
- package/app/components/ui/navigation-menu/NavigationMenuViewport.vue +1 -1
- package/app/components/ui/scroll-area/ScrollBar.vue +1 -1
- package/app/components/ui/select/SelectItem.vue +2 -2
- package/app/components/ui/sheet/SheetContent.vue +29 -3
- package/app/components/ui/sidebar/Sidebar.vue +28 -4
- package/app/components/ui/sidebar/SidebarGroupAction.vue +1 -1
- package/app/components/ui/sidebar/SidebarInset.vue +1 -1
- package/app/components/ui/sidebar/SidebarMenuAction.vue +1 -1
- package/app/components/ui/sidebar/SidebarMenuBadge.vue +1 -1
- package/app/components/ui/sidebar/SidebarMenuSub.vue +1 -1
- package/app/components/ui/sidebar/SidebarMenuSubButton.vue +1 -1
- package/app/components/ui/sidebar/SidebarProvider.vue +7 -16
- package/app/components/ui/sidebar/index.ts +10 -2
- package/app/components/ui/sidebar/utils.ts +0 -1
- package/app/components/ui/sonner/Sonner.vue +18 -0
- package/app/components/ui/tabs/TabsList.vue +13 -3
- package/app/components/ui/tabs/TabsTrigger.vue +24 -3
- package/app/components/ui/tooltip/TooltipContent.vue +2 -2
- package/app/composables/useActiveHeading.ts +3 -25
- package/app/composables/useDuxtAnalytics.ts +50 -0
- package/app/composables/useDuxtAnimatedHeight.ts +8 -38
- package/app/composables/useDuxtAnnouncements.ts +153 -0
- package/app/composables/useDuxtCopy.ts +51 -0
- package/app/composables/useDuxtDirection.ts +23 -0
- package/app/composables/useDuxtNavigation.ts +28 -8
- package/app/composables/useDuxtPath.ts +3 -5
- package/app/composables/useDuxtSearch.ts +87 -35
- package/app/composables/useDuxtSection.ts +15 -1
- package/app/composables/useDuxtSectionRow.ts +6 -5
- package/app/composables/useDuxtShortcuts.ts +319 -20
- package/app/composables/useDuxtVersion.ts +57 -6
- package/app/composables/useDuxtViewportMeasure.ts +37 -0
- package/app/composables/useFuzzySearch.ts +9 -4
- package/app/composables/useRecentPages.ts +14 -17
- package/app/error.vue +112 -74
- package/app/layouts/changelog.vue +9 -1
- package/app/layouts/default.vue +9 -0
- package/app/layouts/docs.vue +9 -1
- package/app/layouts/reference.vue +9 -1
- package/app/middleware/duxt-section-layout.global.ts +1 -3
- package/app/pages/[...slug].vue +146 -35
- package/app/pages/index.vue +15 -23
- package/app/types/duxt.d.ts +312 -4
- package/app/utils/analytics.ts +91 -0
- package/app/utils/announcements.ts +205 -0
- package/app/utils/changelog.ts +0 -28
- package/app/utils/code-highlight.ts +3 -1
- package/app/utils/contributor-avatar.ts +27 -0
- package/app/utils/direction.ts +43 -0
- package/app/utils/duxt-config.ts +43 -0
- package/app/utils/duxt-text.ts +10 -0
- package/app/utils/format-date.ts +28 -0
- package/app/utils/generated-sections.ts +68 -12
- package/app/utils/generated-toc.ts +14 -7
- package/app/utils/locale-path.ts +43 -0
- package/app/utils/navigation-tree.ts +51 -1
- package/app/utils/nearest-page.ts +8 -0
- package/app/utils/openapi.ts +16 -11
- package/app/utils/page-controls.ts +215 -0
- package/app/utils/pill.ts +61 -0
- package/app/utils/prose-image.ts +128 -0
- package/app/utils/search-context.ts +110 -0
- package/app/utils/search-display.ts +248 -0
- package/app/utils/section-scope.ts +65 -4
- package/app/utils/stored-json.ts +31 -0
- package/app/utils/version-choices.ts +6 -13
- package/app/utils/version-paths.ts +21 -0
- package/bin/duxt-cache-key.mjs +61 -0
- package/bin/duxt-og-cache.mjs +58 -0
- package/bin/{duxt-report.mjs → duxt.mjs} +13 -7
- package/bruno-model.ts +169 -0
- package/bruno-parse.ts +656 -0
- package/bruno-zip.ts +195 -0
- package/cli.ts +240 -0
- package/duxt-app-config.ts +18 -1
- package/frontmatter.ts +49 -0
- package/git-contributors.ts +322 -0
- package/i18n/locales/de/duxt/announcement.json +7 -0
- package/i18n/locales/de/duxt/bruno.json +25 -0
- package/i18n/locales/de/duxt/changelog.json +1 -1
- package/i18n/locales/de/duxt/code.json +2 -1
- package/i18n/locales/de/duxt/page.json +9 -0
- package/i18n/locales/de/duxt/shortcuts.json +1 -0
- package/i18n/locales/de/duxt/theme.json +2 -1
- package/i18n/locales/en/duxt/announcement.json +7 -0
- package/i18n/locales/en/duxt/bruno.json +25 -0
- package/i18n/locales/en/duxt/changelog.json +1 -1
- package/i18n/locales/en/duxt/code.json +2 -1
- package/i18n/locales/en/duxt/page.json +9 -0
- package/i18n/locales/en/duxt/shortcuts.json +1 -0
- package/i18n/locales/en/duxt/theme.json +2 -1
- package/i18n/locales/es/duxt/announcement.json +7 -0
- package/i18n/locales/es/duxt/bruno.json +25 -0
- package/i18n/locales/es/duxt/changelog.json +1 -1
- package/i18n/locales/es/duxt/code.json +2 -1
- package/i18n/locales/es/duxt/page.json +9 -0
- package/i18n/locales/es/duxt/shortcuts.json +1 -0
- package/i18n/locales/es/duxt/theme.json +2 -1
- package/i18n/locales/fr/duxt/announcement.json +7 -0
- package/i18n/locales/fr/duxt/bruno.json +25 -0
- package/i18n/locales/fr/duxt/changelog.json +1 -1
- package/i18n/locales/fr/duxt/code.json +2 -1
- package/i18n/locales/fr/duxt/page.json +9 -0
- package/i18n/locales/fr/duxt/shortcuts.json +1 -0
- package/i18n/locales/fr/duxt/theme.json +2 -1
- package/i18n/locales/pt/duxt/announcement.json +7 -0
- package/i18n/locales/pt/duxt/bruno.json +25 -0
- package/i18n/locales/pt/duxt/changelog.json +1 -1
- package/i18n/locales/pt/duxt/code.json +2 -1
- package/i18n/locales/pt/duxt/page.json +9 -0
- package/i18n/locales/pt/duxt/shortcuts.json +1 -0
- package/i18n/locales/pt/duxt/theme.json +2 -1
- package/modules/bruno.ts +118 -0
- package/modules/config.ts +26 -0
- package/modules/devtools.ts +2 -3
- package/modules/git-meta.ts +16 -47
- package/modules/search-records.ts +132 -0
- package/nuxt.config.ts +81 -7
- package/og-image-cache.ts +742 -0
- package/package.json +42 -10
- package/public/devtools/checks.html +2 -2
- package/public/devtools/config.html +59 -4
- package/public/devtools/pages.html +8 -1
- package/public/devtools/paths.html +1 -1
- package/public/devtools/sources.html +2 -2
- package/public/devtools/versions.html +3 -0
- package/report.ts +28 -19
- package/search-records.ts +380 -0
- package/section-input.ts +142 -0
- package/section-markdown.ts +67 -0
- package/section-reports.ts +10 -4
- package/sections-bruno.ts +571 -0
- package/sections-changelog.ts +76 -43
- package/sections-openapi.ts +12 -83
- package/sections-resolve.ts +174 -8
- package/sections.ts +33 -10
- package/server/devtools/shell.ts +1 -1
- package/server/mcp/tools/list-pages.ts +77 -36
- package/server/mcp/tools/list-versions.ts +84 -0
- package/server/mcp/tools/read-page.ts +80 -26
- package/server/mcp/tools/search-docs.ts +87 -53
- package/server/middleware/raw-markdown.ts +1 -1
- package/server/routes/llms-full.txt.get.ts +6 -8
- package/server/routes/llms.txt.get.ts +1 -1
- package/server/utils/duxt-server-text.ts +0 -13
- package/server/utils/llms-pages.ts +24 -9
- package/server/utils/mcp-docs.ts +577 -0
- package/sources-cache.ts +376 -0
- package/sources-git.ts +186 -31
- package/sources-resolve.ts +59 -1
- package/sources.ts +122 -13
- package/tfplugindocs.ts +158 -0
package/README.md
CHANGED
|
@@ -45,32 +45,38 @@ export default defineAppConfig({
|
|
|
45
45
|
title: 'Acme',
|
|
46
46
|
sources: [
|
|
47
47
|
{ path: 'docs', slug: 'acme' }, // this repository
|
|
48
|
-
{
|
|
48
|
+
{
|
|
49
|
+
repo: 'acme/api',
|
|
50
|
+
path: 'docs',
|
|
51
|
+
releases: { select: 'minor' }, // newest tag per minor line
|
|
52
|
+
refs: [{ branch: 'main', status: 'upcoming' }],
|
|
53
|
+
},
|
|
49
54
|
],
|
|
50
|
-
sourceOptions: { defaultRef: 'v2.0.0' },
|
|
51
55
|
},
|
|
52
56
|
})
|
|
53
57
|
```
|
|
54
58
|
|
|
55
|
-
|
|
59
|
+
The local folder and every selected release or branch become collections, the URL prefixes that serve them, and a version switcher — none of which you write. Prefer a fixed list? Tags are explicit: `{ tag: 'v2.0.0' }`; a bare string names a branch.
|
|
56
60
|
|
|
57
61
|
## ✨ Features
|
|
58
62
|
|
|
59
63
|
- **📦 Extend, don't scaffold** — a Nuxt layer: theme, pages, components and `app.config` defaults arrive with `extends` and are overridden file by file where you disagree.
|
|
60
|
-
- **🗂️ Sources as a list** — one compact entry per source instead of one Content collection per version × repository.
|
|
64
|
+
- **🗂️ Sources and releases as a list** — one compact entry per source; select every release, or the newest per minor or major line, instead of maintaining one Content collection per version × repository.
|
|
61
65
|
- **🧭 Version switcher and URL scheme** — `/[repo]/[version]/[...slug]`, collapsing cleanly when there is one source and no versions, with a lifecycle per version that decides the banner a reader gets.
|
|
62
66
|
- **🌿 Git-native sourcing** — branches, tags, private repositories and hash-based caching come straight from Content v3's own `repository` support; `'latest'` resolves to the newest semver tag at build time.
|
|
63
|
-
-
|
|
67
|
+
- **📚 Generated reference** — publish OpenAPI documents, Bruno collections and tfplugindocs trees beside authored Markdown; OpenAPI and Bruno can power the built-in request client.
|
|
68
|
+
- **🤖 Machine-readable by default** — `llms.txt`, the default-version corpus in `llms-full.txt`, every page as Markdown and a real MCP server at `/mcp`, all reading the same collections as the site.
|
|
64
69
|
- **🎨 shadcn-vue theme, owned not vendored** — Tailwind 4 with the components in `app/components/ui/`, MDC components in `app/components/content/`.
|
|
65
|
-
- **🔍 Search, TOC, breadcrumb, prev/next** —
|
|
70
|
+
- **🔍 Search, TOC, breadcrumb, prev/next** — ⌘/Ctrl+K fuzzy search across every source, active-heading tracking, section landing pages, feedback and "Edit this page" links.
|
|
66
71
|
- **🧯 A build that fails loudly** — a validator walks the parse cache for URL collisions, empty collections, broken links and missing titles, because every bug this layer actually had was a silent one.
|
|
72
|
+
- **♻️ Cacheable build work** — remote Content downloads and rendered OG images expose stable cache keys and a warm-build report; see [Cache build work](docs/3.guides/12.cache-build-work.md).
|
|
67
73
|
|
|
68
74
|
<details>
|
|
69
75
|
<summary>Full feature list</summary>
|
|
70
76
|
|
|
71
77
|
### Sources & versions
|
|
72
78
|
|
|
73
|
-
- **🗂️ Sources as a list** — one compact entry per source instead of one Content collection per version × repository. The single unversioned folder is the default and needs no config at all.
|
|
79
|
+
- **🗂️ Sources as a list** — one compact entry per source instead of one Content collection per version × repository. The single unversioned folder is the default and needs no config at all; `releases` discovers all releases or the newest per minor or major line.
|
|
74
80
|
- **🧭 Version switcher and URL scheme** — `/[repo]/[version]/[...slug]`, collapsing cleanly when there is one source and no versions, with a lifecycle per version (`upcoming`, `current`, `maintained`, `deprecated`, `eol`) that decides the banner a reader gets.
|
|
75
81
|
- **🌿 Git-native sourcing** — branches, tags, private repositories and hash-based caching come straight from Content v3's own `repository` support; `'latest'` resolves to the newest semver tag at build time.
|
|
76
82
|
- **🔁 Redirects from frontmatter** — `redirectFrom` on a moved page becomes route rules under every repository, version and locale prefix it is served at.
|
|
@@ -78,12 +84,20 @@ Two repositories and three refs become five collections, the URL prefixes that s
|
|
|
78
84
|
### Theme & reading experience
|
|
79
85
|
|
|
80
86
|
- **🎨 shadcn-vue theme, owned not vendored** — Tailwind 4 with the components in `app/components/ui/`, MDC components (`::callout`, `::steps`, `::code-group`, file trees, Mermaid) in `app/components/content/`.
|
|
81
|
-
- **🔍 Search, TOC, breadcrumb, prev/next** —
|
|
87
|
+
- **🔍 Search, TOC, breadcrumb, prev/next** — ⌘/Ctrl+K fuzzy search across every source, active-heading tracking, section landing pages, feedback and "Edit this page" links.
|
|
82
88
|
- **🌍 Seven locales out of the box** — `en-GB`, `en-US`, `de-DE`, `es-ES`, `fr-FR`, `pt-PT` and `pt-BR`; a site picks which of them it serves, and its own strings take a literal, a key or a per-locale record.
|
|
89
|
+
- **🖼️ Responsive images** — Nuxt Image produces responsive sources and a persistent zoom affordance while SVG and GIF pass through unchanged.
|
|
90
|
+
|
|
91
|
+
### Generated reference
|
|
92
|
+
|
|
93
|
+
- **📄 OpenAPI** — turn a specification into navigable operation and schema pages, with an optional request client.
|
|
94
|
+
- **🧰 Bruno** — publish a request collection as reference pages and a downloadable redacted collection, with opt-in try-it controls.
|
|
95
|
+
- **🔌 tfplugindocs** — read a Terraform provider's generated documentation as a structured source with provider-aware navigation.
|
|
96
|
+
- **📜 Changelogs** — render a global or version-aligned release history, including the contributors in each release.
|
|
83
97
|
|
|
84
98
|
### Machine-readable & SEO
|
|
85
99
|
|
|
86
|
-
- **🤖 Machine-readable by default** — `llms.txt`, `llms-full.txt
|
|
100
|
+
- **🤖 Machine-readable by default** — `llms.txt`, the default-version corpus in `llms-full.txt`, every page as Markdown and a real MCP server at `/mcp` (`list_versions`, `list_pages`, `search_docs`, `read_page`), all reading the same collections as the site.
|
|
87
101
|
- **📈 SEO and feeds included** — sitemap, robots, generated OG images, and an optional `/rss.xml` over a section you nominate.
|
|
88
102
|
|
|
89
103
|
### Layer & build
|
|
@@ -91,6 +105,7 @@ Two repositories and three refs become five collections, the URL prefixes that s
|
|
|
91
105
|
- **📦 Extend, don't scaffold** — a Nuxt layer: theme, pages, components and `app.config` defaults arrive with `extends` and are overridden file by file where you disagree.
|
|
92
106
|
- **🧯 A build that fails loudly** — a validator walks the parse cache for URL collisions, empty collections, broken links and missing titles, because every bug this layer actually had was a silent one.
|
|
93
107
|
- **🔎 DevTools tab** — the resolved sources, collections, prefixes, message catalogues and download cache, in dev, where guessing used to be the only option.
|
|
108
|
+
- **♻️ Cacheable build work** — `duxt-cache-key` identifies remote Content downloads and `duxt-og-cache` identifies reusable rendered cards; the [cache guide](docs/3.guides/12.cache-build-work.md) wires both into CI.
|
|
94
109
|
|
|
95
110
|
</details>
|
|
96
111
|
|
package/app/app.vue
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { ConfigProvider } from 'reka-ui';
|
|
3
|
+
|
|
2
4
|
/**
|
|
3
5
|
* The locale's head tags.
|
|
4
6
|
*
|
|
@@ -19,18 +21,11 @@
|
|
|
19
21
|
*/
|
|
20
22
|
const { locale, locales } = useI18n();
|
|
21
23
|
const duxt = useDuxtConfig();
|
|
24
|
+
const direction = useDuxtDirection();
|
|
22
25
|
|
|
23
26
|
const baseUrl = (useRuntimeConfig().public as { i18n?: { baseUrl?: string } })
|
|
24
27
|
.i18n?.baseUrl;
|
|
25
28
|
|
|
26
|
-
const direction = computed(() => {
|
|
27
|
-
const current = locales.value.find(
|
|
28
|
-
(entry) => (typeof entry === 'string' ? entry : entry.code) === locale.value
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
return (typeof current === 'object' && current?.dir) || 'ltr';
|
|
32
|
-
});
|
|
33
|
-
|
|
34
29
|
useHead(() => ({
|
|
35
30
|
htmlAttrs: { lang: locale.value, dir: direction.value },
|
|
36
31
|
|
|
@@ -116,7 +111,18 @@ useSchemaOrg([
|
|
|
116
111
|
navigation; the focus half of the same problem is useDuxtPageFocus. -->
|
|
117
112
|
<NuxtRouteAnnouncer />
|
|
118
113
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
114
|
+
<!-- reka-ui reads its direction from HERE and from nowhere else: its
|
|
115
|
+
`useDirection` injects this context and falls back to `ltr` without ever
|
|
116
|
+
consulting the document, so `<html dir>` alone leaves every menu, select,
|
|
117
|
+
tooltip and scroll area laid out left-to-right under a right-to-left
|
|
118
|
+
page. Renderless — it draws no element and changes no landmark. -->
|
|
119
|
+
<ConfigProvider :dir="direction">
|
|
120
|
+
<!-- One provider for every tooltip in the layer, including controls such as
|
|
121
|
+
DuxtLocale when a consumer renders them outside the stock header. -->
|
|
122
|
+
<UiTooltipProvider>
|
|
123
|
+
<NuxtLayout>
|
|
124
|
+
<NuxtPage />
|
|
125
|
+
</NuxtLayout>
|
|
126
|
+
</UiTooltipProvider>
|
|
127
|
+
</ConfigProvider>
|
|
122
128
|
</template>
|
package/app/assets/css/duxt.css
CHANGED
|
@@ -15,6 +15,38 @@
|
|
|
15
15
|
sets it with classSuffix: '' so the two agree. */
|
|
16
16
|
@custom-variant dark (&:is(.dark *));
|
|
17
17
|
|
|
18
|
+
/*
|
|
19
|
+
* TECHNICAL CONTENT IS LEFT-TO-RIGHT, whichever way the interface runs.
|
|
20
|
+
*
|
|
21
|
+
* A right-to-left locale turns the chrome round — the sidebar, the table of
|
|
22
|
+
* contents, the nesting rules, every `ms-`/`ps-`/`border-s` in this theme. It
|
|
23
|
+
* must not turn round a shell command, a file path, a URL or a JSON body:
|
|
24
|
+
* those are syntax, not prose, and the bidirectional algorithm reorders their
|
|
25
|
+
* punctuation into something that no longer parses. `cd ../src` rendered
|
|
26
|
+
* right-to-left is not a translation of `cd ../src`.
|
|
27
|
+
*
|
|
28
|
+
* `unicode-bidi: isolate` rather than `direction: ltr` alone, because the run
|
|
29
|
+
* has to be sealed off from the sentence AROUND it as well: an inline `code`
|
|
30
|
+
* sitting mid-paragraph inherits the paragraph's ordering otherwise, and a
|
|
31
|
+
* trailing bracket migrates to the wrong end of the word.
|
|
32
|
+
*
|
|
33
|
+
* NOT THE `[dir=rtl]` OVERRIDE SHEET THIS PROJECT REJECTED. That idea was a
|
|
34
|
+
* second, invisible layout system running beside the utility classes — every
|
|
35
|
+
* component having to remember it exists. This is the opposite: four element
|
|
36
|
+
* selectors saying what kind of content they hold, changing nothing in a
|
|
37
|
+
* left-to-right page, and needing no component to know about it at all.
|
|
38
|
+
*
|
|
39
|
+
* `:where()` throughout, so the whole thing carries no specificity and a
|
|
40
|
+
* consumer overrides it with a bare `code { … }`. The `[dir]` guard is the
|
|
41
|
+
* narrower escape hatch: a block that genuinely holds right-to-left text — a
|
|
42
|
+
* transliteration, a string literal in Arabic — says so on the element and is
|
|
43
|
+
* left alone.
|
|
44
|
+
*/
|
|
45
|
+
:where(code, pre, kbd, samp):where(:not([dir])) {
|
|
46
|
+
direction: ltr;
|
|
47
|
+
unicode-bidi: isolate;
|
|
48
|
+
}
|
|
49
|
+
|
|
18
50
|
/*
|
|
19
51
|
* The neutral base palette, in oklch. A consumer overrides a token by
|
|
20
52
|
* redefining it in its own stylesheet — no fork of this file needed.
|
|
@@ -246,7 +278,7 @@
|
|
|
246
278
|
}
|
|
247
279
|
|
|
248
280
|
.typeset :where(.duxt-anchor) {
|
|
249
|
-
@apply absolute -
|
|
281
|
+
@apply absolute -start-5 font-normal text-muted-foreground opacity-0 transition-opacity;
|
|
250
282
|
}
|
|
251
283
|
|
|
252
284
|
/* THE MARKER AS THE LINK ITSELF, where the heading already contains one and
|
|
@@ -256,7 +288,7 @@
|
|
|
256
288
|
while the words beside it are the same link and not at all once they are
|
|
257
289
|
somebody else's. */
|
|
258
290
|
.typeset :where(.duxt-anchor-only) {
|
|
259
|
-
@apply relative
|
|
291
|
+
@apply relative start-auto me-2 inline no-underline opacity-60 transition-opacity;
|
|
260
292
|
}
|
|
261
293
|
|
|
262
294
|
.typeset :where(.duxt-anchor-only:hover, .duxt-anchor-only:focus-visible) {
|
|
@@ -354,14 +386,14 @@ details[open] > .duxt-schema-row .duxt-schema-tile .duxt-schema-minus {
|
|
|
354
386
|
because there the indent is what says it is nested. */
|
|
355
387
|
.typeset :where(.duxt-release > ul) {
|
|
356
388
|
margin: 0;
|
|
357
|
-
padding-
|
|
389
|
+
padding-inline-start: 0;
|
|
358
390
|
list-style: none;
|
|
359
391
|
}
|
|
360
392
|
|
|
361
393
|
.typeset :where(.duxt-release > ul > li) {
|
|
362
394
|
position: relative;
|
|
363
395
|
margin: 0;
|
|
364
|
-
padding-
|
|
396
|
+
padding-inline-start: 1.05rem;
|
|
365
397
|
line-height: 1.55;
|
|
366
398
|
}
|
|
367
399
|
|
|
@@ -373,7 +405,7 @@ details[open] > .duxt-schema-row .duxt-schema-tile .duxt-schema-minus {
|
|
|
373
405
|
content: '';
|
|
374
406
|
position: absolute;
|
|
375
407
|
top: 0.6em;
|
|
376
|
-
|
|
408
|
+
inset-inline-start: 0;
|
|
377
409
|
height: 0.3125rem;
|
|
378
410
|
width: 0.3125rem;
|
|
379
411
|
border-radius: 999px;
|
|
@@ -524,7 +556,16 @@ pre.shiki,
|
|
|
524
556
|
|
|
525
557
|
/* Line numbers, opt-in per fence with ```ts line-numbers.
|
|
526
558
|
A CSS counter rather than markup: the numbers are decoration, and a reader
|
|
527
|
-
copying the block must not get them in the clipboard.
|
|
559
|
+
copying the block must not get them in the clipboard.
|
|
560
|
+
|
|
561
|
+
PHYSICAL ON PURPOSE, and the only two declarations in this file that are.
|
|
562
|
+
`margin-right` and `text-align: right` sit inside `pre`/`code`, which the
|
|
563
|
+
`unicode-bidi: isolate` rule at the top of this file pins left-to-right —
|
|
564
|
+
a shell command is syntax, not prose. A gutter that followed the reader
|
|
565
|
+
would land on the opposite side of the run it numbers, and the digits
|
|
566
|
+
right-align towards that run for the same reason they do in an editor.
|
|
567
|
+
Declared with this reason in `tests/direction-utilities.test.ts`; every
|
|
568
|
+
other side-naming rule here is logical. */
|
|
528
569
|
.duxt-line-numbers code {
|
|
529
570
|
counter-reset: duxt-line;
|
|
530
571
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The site's own notices — a release, a migration window, an outage.
|
|
4
|
+
*
|
|
5
|
+
* NOT the version and translation banners beside it: those describe the page a
|
|
6
|
+
* reader is on and the layer decides when to draw them. This one says nothing
|
|
7
|
+
* until a consumer writes `duxt.announcements`, and then says exactly what they
|
|
8
|
+
* wrote.
|
|
9
|
+
*
|
|
10
|
+
* CLIENT-ONLY, and it has to be. Two of the three things this draws depend on
|
|
11
|
+
* the reader rather than on the build: whether they have already closed the
|
|
12
|
+
* notice, which lives in their browser, and whether the notice is inside its
|
|
13
|
+
* window, which is their clock. Every page here is prerendered, so a banner
|
|
14
|
+
* rendered at build time would carry a decision taken hours or weeks earlier —
|
|
15
|
+
* an announcement that expired overnight would still be in the HTML, and one a
|
|
16
|
+
* reader dismissed would flash back on every navigation before the script
|
|
17
|
+
* caught up.
|
|
18
|
+
*/
|
|
19
|
+
const props = defineProps<{ placement: DuxtAnnouncementPlacement }>();
|
|
20
|
+
|
|
21
|
+
const { visible, dismiss } = useDuxtAnnouncements(props.placement);
|
|
22
|
+
const localeLink = useDuxtLink();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The two full-width placements belong to the header; the third to the page.
|
|
26
|
+
*
|
|
27
|
+
* `above-header` and `below-header` run to the browser edge and align their
|
|
28
|
+
* contents with the header above or below them, so the sentence starts where
|
|
29
|
+
* the site's name does. `above-content` sits in the document column with the
|
|
30
|
+
* version and translation banners, and takes their shape — a reader learns what
|
|
31
|
+
* a bordered notice over a page means once.
|
|
32
|
+
*/
|
|
33
|
+
const edge = computed(() => props.placement !== 'above-content');
|
|
34
|
+
|
|
35
|
+
const strip = useTemplateRef<HTMLElement>('strip');
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Dismiss, and then put focus somewhere a keyboard reader can carry on from.
|
|
39
|
+
*
|
|
40
|
+
* The button that was just activated leaves the DOM with the row it sat in, and
|
|
41
|
+
* a focused element that disappears drops focus onto `<body>` — which sends a
|
|
42
|
+
* screen reader back to the top of the document, several notices above where
|
|
43
|
+
* the reader actually was. So focus moves to whatever is left: the strip itself
|
|
44
|
+
* while it still holds an announcement, and the page's own main region once the
|
|
45
|
+
* last one is closed.
|
|
46
|
+
*/
|
|
47
|
+
async function close(key: string) {
|
|
48
|
+
dismiss(key);
|
|
49
|
+
await nextTick();
|
|
50
|
+
(strip.value ?? document.getElementById('duxt-main'))?.focus();
|
|
51
|
+
}
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<ClientOnly>
|
|
56
|
+
<!-- `role="status"` because the strip ARRIVES: it is drawn after hydration,
|
|
57
|
+
so a reader already reading the page is told once, politely, rather
|
|
58
|
+
than having a banner appear silently above them. -->
|
|
59
|
+
<div
|
|
60
|
+
v-if="visible.length"
|
|
61
|
+
ref="strip"
|
|
62
|
+
role="status"
|
|
63
|
+
tabindex="-1"
|
|
64
|
+
class="outline-none"
|
|
65
|
+
:class="
|
|
66
|
+
edge
|
|
67
|
+
? 'w-full divide-y border-b bg-muted/40'
|
|
68
|
+
: 'mb-6 divide-y rounded-md border bg-muted/30'
|
|
69
|
+
"
|
|
70
|
+
>
|
|
71
|
+
<div
|
|
72
|
+
v-for="entry in visible"
|
|
73
|
+
:key="entry.key"
|
|
74
|
+
:class="
|
|
75
|
+
edge
|
|
76
|
+
? 'mx-auto flex max-w-[90rem] items-start gap-3 px-4 py-2.5 lg:px-8'
|
|
77
|
+
: 'flex items-start gap-3 px-3 py-2.5'
|
|
78
|
+
"
|
|
79
|
+
>
|
|
80
|
+
<Icon
|
|
81
|
+
name="lucide:megaphone"
|
|
82
|
+
class="mt-0.5 size-4 shrink-0 text-muted-foreground"
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
<!-- `min-w-0` and `flex-wrap`, because the sentence is the part that
|
|
86
|
+
grows: a German or Portuguese translation of an English notice runs
|
|
87
|
+
half as long again, and on a phone it has to wrap under itself
|
|
88
|
+
rather than push the close button off the row. -->
|
|
89
|
+
<p
|
|
90
|
+
class="flex min-w-0 flex-1 flex-wrap items-baseline gap-x-2 gap-y-1 text-sm text-foreground text-pretty"
|
|
91
|
+
>
|
|
92
|
+
<span>{{ entry.text }}</span>
|
|
93
|
+
|
|
94
|
+
<NuxtLink
|
|
95
|
+
v-if="entry.link?.to"
|
|
96
|
+
:to="localeLink(entry.link.to)"
|
|
97
|
+
:target="entry.link.external ? '_blank' : undefined"
|
|
98
|
+
:rel="entry.link.external ? 'noopener' : undefined"
|
|
99
|
+
class="font-medium underline underline-offset-4"
|
|
100
|
+
>
|
|
101
|
+
{{ entry.link.label }}
|
|
102
|
+
</NuxtLink>
|
|
103
|
+
</p>
|
|
104
|
+
|
|
105
|
+
<UiButton
|
|
106
|
+
variant="ghost"
|
|
107
|
+
size="icon"
|
|
108
|
+
class="-my-1 -me-2 size-8 shrink-0 text-muted-foreground hover:text-foreground"
|
|
109
|
+
:aria-label="$t('duxt.announcement.dismiss')"
|
|
110
|
+
@click="close(entry.key)"
|
|
111
|
+
>
|
|
112
|
+
<Icon name="lucide:x" class="size-4" />
|
|
113
|
+
</UiButton>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</ClientOnly>
|
|
117
|
+
</template>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { DuxtBrunoEntry } from '../../bruno-model';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A dictionary block — headers, query parameters, a form body — as a table.
|
|
6
|
+
*
|
|
7
|
+
* Two columns and no third, because a Bruno collection has no third to give: it
|
|
8
|
+
* carries a name and a value, and no type, no description and no statement of
|
|
9
|
+
* whether the field is required. A table with an empty "Description" column
|
|
10
|
+
* would be this type pretending to be `openapi`, which is the pretence the
|
|
11
|
+
* issue behind it rejected conversion for.
|
|
12
|
+
*
|
|
13
|
+
* TWO THINGS ARE MARKED. A `~` entry is switched off in the collection and is
|
|
14
|
+
* shown struck through rather than dropped — a reference that hid it would
|
|
15
|
+
* describe a request the file does not make. A withheld value shows the badge
|
|
16
|
+
* instead of the value: the name is the useful half (the endpoint wants an
|
|
17
|
+
* `Authorization` header) and the value was a live credential.
|
|
18
|
+
*/
|
|
19
|
+
defineProps<{ entries?: DuxtBrunoEntry[] }>();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<table v-if="entries?.length" class="w-full text-start text-sm">
|
|
24
|
+
<thead>
|
|
25
|
+
<tr class="border-b border-border/60">
|
|
26
|
+
<th scope="col" class="py-2 pe-4 font-medium">
|
|
27
|
+
{{ $t('duxt.bruno.name') }}
|
|
28
|
+
</th>
|
|
29
|
+
<th scope="col" class="py-2 font-medium">
|
|
30
|
+
{{ $t('duxt.bruno.value') }}
|
|
31
|
+
</th>
|
|
32
|
+
</tr>
|
|
33
|
+
</thead>
|
|
34
|
+
|
|
35
|
+
<tbody>
|
|
36
|
+
<tr
|
|
37
|
+
v-for="entry in entries"
|
|
38
|
+
:key="`${entry.name}-${entry.value}`"
|
|
39
|
+
class="border-b border-border/40 last:border-b-0"
|
|
40
|
+
>
|
|
41
|
+
<td class="py-2 pe-4 align-top font-mono text-xs break-all">
|
|
42
|
+
<span :class="entry.disabled && 'text-muted-foreground line-through'">
|
|
43
|
+
{{ entry.name }}
|
|
44
|
+
</span>
|
|
45
|
+
</td>
|
|
46
|
+
|
|
47
|
+
<td class="py-2 align-top font-mono text-xs break-all">
|
|
48
|
+
<UiBadge v-if="entry.redacted" variant="secondary">
|
|
49
|
+
{{ $t('duxt.bruno.withheld') }}
|
|
50
|
+
</UiBadge>
|
|
51
|
+
|
|
52
|
+
<span
|
|
53
|
+
v-else
|
|
54
|
+
:class="entry.disabled && 'text-muted-foreground line-through'"
|
|
55
|
+
>
|
|
56
|
+
{{ entry.value }}
|
|
57
|
+
</span>
|
|
58
|
+
</td>
|
|
59
|
+
</tr>
|
|
60
|
+
</tbody>
|
|
61
|
+
</table>
|
|
62
|
+
</template>
|
|
@@ -64,32 +64,31 @@ const icon = computed(() =>
|
|
|
64
64
|
|
|
65
65
|
const label = computed(() => props.filename ?? props.language);
|
|
66
66
|
|
|
67
|
-
const copied = ref(false);
|
|
68
|
-
const notify = useDuxtToast();
|
|
69
|
-
const { t } = useI18n();
|
|
70
67
|
const root = useTemplateRef<HTMLElement>('root');
|
|
68
|
+
const analytics = useDuxtAnalytics();
|
|
69
|
+
const { copied, copy: copyText } = useDuxtCopy();
|
|
71
70
|
|
|
72
71
|
async function copy() {
|
|
73
72
|
const text =
|
|
74
73
|
props.code ?? root.value?.querySelector('code')?.textContent ?? '';
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
setTimeout(() => (copied.value = false), 2000);
|
|
81
|
-
} catch {
|
|
82
|
-
notify.error(
|
|
83
|
-
'Could not copy',
|
|
84
|
-
'The clipboard is unavailable in this context.'
|
|
85
|
-
);
|
|
86
|
-
// Clipboard is unavailable over plain HTTP; a failed copy stays silent.
|
|
75
|
+
// After the write, so a copy the clipboard refused is not reported as one —
|
|
76
|
+
// and the language only: the copied TEXT never travels.
|
|
77
|
+
if (await copyText(text)) {
|
|
78
|
+
analytics.track({ name: 'copy', kind: 'code', language: props.language });
|
|
87
79
|
}
|
|
88
80
|
}
|
|
89
81
|
</script>
|
|
90
82
|
|
|
91
83
|
<template>
|
|
84
|
+
<!-- `dir="ltr"` on the block, not on the `pre` alone: the header carries a
|
|
85
|
+
file name and the floating button sits over the code, and all three are
|
|
86
|
+
one technical surface. Inside the island the logical utilities below
|
|
87
|
+
resolve left-to-right, so they stay logical rather than turning physical
|
|
88
|
+
— the direction is declared once, in one place, instead of being spelled
|
|
89
|
+
out again in every class. -->
|
|
92
90
|
<div
|
|
91
|
+
dir="ltr"
|
|
93
92
|
class="duxt-code group relative my-6 overflow-hidden rounded-lg border bg-card"
|
|
94
93
|
>
|
|
95
94
|
<div
|
|
@@ -99,31 +98,16 @@ async function copy() {
|
|
|
99
98
|
<Icon :name="icon" class="size-4 shrink-0" />
|
|
100
99
|
<span class="truncate font-mono">{{ label }}</span>
|
|
101
100
|
|
|
102
|
-
<
|
|
103
|
-
variant="ghost"
|
|
104
|
-
size="icon"
|
|
105
|
-
class="ml-auto size-7 hover:bg-accent hover:text-foreground"
|
|
106
|
-
:aria-label="copied ? $t('duxt.code.copied') : $t('duxt.code.copy')"
|
|
107
|
-
@click="copy"
|
|
108
|
-
>
|
|
109
|
-
<Icon
|
|
110
|
-
:name="copied ? 'lucide:check' : 'lucide:copy'"
|
|
111
|
-
class="size-3.5"
|
|
112
|
-
/>
|
|
113
|
-
</UiButton>
|
|
101
|
+
<DuxtCopyButton :copied="copied" class="ms-auto" @click="copy" />
|
|
114
102
|
</div>
|
|
115
103
|
|
|
116
|
-
<
|
|
104
|
+
<DuxtCopyButton
|
|
117
105
|
v-else-if="header"
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
class="duxt-code-copy absolute top-2 right-2 size-7 opacity-0 transition-opacity group-hover:opacity-100 hover:bg-accent focus-visible:opacity-100"
|
|
106
|
+
:copied="copied"
|
|
107
|
+
class="duxt-code-copy absolute top-2 end-2 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100"
|
|
121
108
|
:class="{ 'opacity-100': copied }"
|
|
122
|
-
:aria-label="copied ? $t('duxt.code.copied') : $t('duxt.code.copy')"
|
|
123
109
|
@click="copy"
|
|
124
|
-
|
|
125
|
-
<Icon :name="copied ? 'lucide:check' : 'lucide:copy'" class="size-3.5" />
|
|
126
|
-
</UiButton>
|
|
110
|
+
/>
|
|
127
111
|
|
|
128
112
|
<div ref="root">
|
|
129
113
|
<slot v-if="hasBody" />
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- The bar across the top of a code card: tabs or pills on one side, the
|
|
3
|
+
copy button on the other. Four cards draw it — the code group, the
|
|
4
|
+
preview, the package managers and the request samples — and a reader
|
|
5
|
+
learns what that bar does once only if it is the same bar each time.
|
|
6
|
+
|
|
7
|
+
A plain box, never the tablist itself: `role="tablist"` may only contain
|
|
8
|
+
tabs, and the copy button beside them is not one. -->
|
|
9
|
+
<div
|
|
10
|
+
class="flex min-h-11 items-center gap-1 border-b bg-muted/40 px-2 py-1.5"
|
|
11
|
+
>
|
|
12
|
+
<slot />
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'vue';
|
|
3
|
+
import { cn } from '@duxt/lib/utils';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The icon button beside a block of code: a copy mark that turns into a check.
|
|
7
|
+
*
|
|
8
|
+
* Its state comes from `useDuxtCopy` in the component that owns the text —
|
|
9
|
+
* this only draws it, because what gets copied is the owner's to know (a fence's
|
|
10
|
+
* source, the tab that is showing, the manager that is picked).
|
|
11
|
+
*/
|
|
12
|
+
const props = defineProps<{
|
|
13
|
+
copied: boolean;
|
|
14
|
+
/** The accessible name before copying. Defaults to "Copy code". */
|
|
15
|
+
label?: string;
|
|
16
|
+
class?: HTMLAttributes['class'];
|
|
17
|
+
}>();
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<UiButton
|
|
22
|
+
variant="ghost"
|
|
23
|
+
size="icon"
|
|
24
|
+
:class="
|
|
25
|
+
cn('size-7 shrink-0 hover:bg-accent hover:text-foreground', props.class)
|
|
26
|
+
"
|
|
27
|
+
:aria-label="
|
|
28
|
+
copied ? $t('duxt.code.copied') : (label ?? $t('duxt.code.copy'))
|
|
29
|
+
"
|
|
30
|
+
>
|
|
31
|
+
<Icon :name="copied ? 'lucide:check' : 'lucide:copy'" class="size-3.5" />
|
|
32
|
+
</UiButton>
|
|
33
|
+
</template>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { stripFrontmatter } from '../../frontmatter';
|
|
3
|
+
|
|
2
4
|
/**
|
|
3
5
|
* The top action: hand this page to a model, or read its source.
|
|
4
6
|
*
|
|
@@ -20,14 +22,19 @@ const props = defineProps<{
|
|
|
20
22
|
}>();
|
|
21
23
|
|
|
22
24
|
const { t } = useI18n();
|
|
23
|
-
const notify = useDuxtToast();
|
|
24
25
|
const localeLink = useDuxtLink();
|
|
26
|
+
const duxt = useDuxtConfig();
|
|
25
27
|
|
|
26
|
-
const copied =
|
|
28
|
+
const { copied, copy: copyText } = useDuxtCopy({
|
|
29
|
+
// The button's own label turns into "Copied"; a toast would say it twice.
|
|
30
|
+
toast: false,
|
|
31
|
+
failureKey: 'duxt.page.copyFailed'
|
|
32
|
+
});
|
|
27
33
|
|
|
28
34
|
const markdownPath = computed(
|
|
29
35
|
() => `${localeLink(props.path) ?? props.path}.md`
|
|
30
36
|
);
|
|
37
|
+
const models = computed(() => duxt.copy?.models ?? []);
|
|
31
38
|
|
|
32
39
|
/** The prompt both models get: fetch the source, then answer about it. */
|
|
33
40
|
function prompt() {
|
|
@@ -36,21 +43,10 @@ function prompt() {
|
|
|
36
43
|
return t('duxt.page.copy.prompt', { url, title: props.title ?? '' });
|
|
37
44
|
}
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
const raw = props.rawbody;
|
|
41
|
-
if (!raw) return;
|
|
42
|
-
|
|
46
|
+
function copy() {
|
|
43
47
|
// Without the frontmatter: what is copied should read as the page, not as
|
|
44
48
|
// the file. Same call `llms-full.txt` makes over the same field.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
await navigator.clipboard.writeText(text);
|
|
49
|
-
copied.value = true;
|
|
50
|
-
setTimeout(() => (copied.value = false), 2000);
|
|
51
|
-
} catch {
|
|
52
|
-
notify.error(t('duxt.page.copyFailed'));
|
|
53
|
-
}
|
|
49
|
+
return copyText(props.rawbody && stripFrontmatter(props.rawbody));
|
|
54
50
|
}
|
|
55
51
|
|
|
56
52
|
function open(base: string) {
|
|
@@ -63,11 +59,18 @@ function open(base: string) {
|
|
|
63
59
|
</script>
|
|
64
60
|
|
|
65
61
|
<template>
|
|
66
|
-
|
|
62
|
+
<!-- `data-duxt-copy-page` is how `check:overflow` finds this control on a
|
|
63
|
+
built page. It is not styling and not behaviour: matching on the utility
|
|
64
|
+
classes instead would make any restyle silently stop checking anything,
|
|
65
|
+
and matching on the button's own words would tie a gate to a translation
|
|
66
|
+
string. `shrink-0` is deliberate — this holds a dropdown trigger and a
|
|
67
|
+
link, and a squeezed control is worse than a wrapped one — so the row
|
|
68
|
+
that carries it is the thing that has to give (see `[...slug].vue`). -->
|
|
69
|
+
<div v-if="rawbody" data-duxt-copy-page class="flex shrink-0 items-stretch">
|
|
67
70
|
<UiButton
|
|
68
71
|
variant="outline"
|
|
69
72
|
size="sm"
|
|
70
|
-
class="gap-1.5 rounded-
|
|
73
|
+
class="gap-1.5 rounded-e-none border-e-0"
|
|
71
74
|
@click="copy"
|
|
72
75
|
>
|
|
73
76
|
<Icon :name="copied ? 'lucide:check' : 'lucide:copy'" class="size-3.5" />
|
|
@@ -79,7 +82,7 @@ function open(base: string) {
|
|
|
79
82
|
<UiButton
|
|
80
83
|
variant="outline"
|
|
81
84
|
size="sm"
|
|
82
|
-
class="rounded-
|
|
85
|
+
class="rounded-s-none px-1.5"
|
|
83
86
|
:aria-label="$t('duxt.page.copy.more')"
|
|
84
87
|
>
|
|
85
88
|
<Icon name="lucide:chevron-down" class="size-3.5 opacity-60" />
|
|
@@ -94,22 +97,21 @@ function open(base: string) {
|
|
|
94
97
|
</a>
|
|
95
98
|
</UiDropdownMenuItem>
|
|
96
99
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
<Icon name="simple-icons:openai" class="size-4" />
|
|
104
|
-
{{ $t('duxt.page.copy.chatgpt') }}
|
|
105
|
-
</UiDropdownMenuItem>
|
|
100
|
+
<!--
|
|
101
|
+
Only when there is a second group to divide: `copy.models` is a
|
|
102
|
+
consumer-owned list, and an empty one would otherwise leave a rule
|
|
103
|
+
drawn under the last item with nothing beneath it.
|
|
104
|
+
-->
|
|
105
|
+
<UiDropdownMenuSeparator v-if="models.length" />
|
|
106
106
|
|
|
107
107
|
<UiDropdownMenuItem
|
|
108
|
+
v-for="model in models"
|
|
109
|
+
:key="model.url"
|
|
108
110
|
class="gap-2"
|
|
109
|
-
@select="open(
|
|
111
|
+
@select="open(model.url)"
|
|
110
112
|
>
|
|
111
|
-
<Icon name="
|
|
112
|
-
{{
|
|
113
|
+
<Icon :name="model.icon" class="size-4" />
|
|
114
|
+
{{ model.label }}
|
|
113
115
|
</UiDropdownMenuItem>
|
|
114
116
|
</UiDropdownMenuContent>
|
|
115
117
|
</UiDropdownMenu>
|