@dogsbay/docs-layout 0.2.0-beta.0
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/package.json +59 -0
- package/src/DocsFooter.astro +96 -0
- package/src/DocsHeader.astro +41 -0
- package/src/DocsLayout.astro +884 -0
- package/src/DocsNav.astro +62 -0
- package/src/DocsSidebar.astro +35 -0
- package/src/DocsToc.astro +50 -0
- package/src/LocaleSwitcher.astro +87 -0
- package/src/PageActions.astro +281 -0
- package/src/SearchDialog.astro +529 -0
- package/src/StatusBadge.astro +79 -0
- package/src/TagList.astro +124 -0
- package/src/TaxonomyIndex.astro +148 -0
- package/src/TaxonomyTerm.astro +181 -0
- package/src/TypeBadge.astro +63 -0
- package/src/VersionSwitcher.astro +86 -0
- package/src/json-ld.ts +55 -0
- package/src/llm-actions.ts +128 -0
- package/src/markdown-negotiation.ts +76 -0
- package/src/nav-filter.ts +166 -0
- package/src/pagination.ts +39 -0
- package/src/search-facets.ts +232 -0
- package/src/switcher.ts +138 -0
- package/src/tag-list-data.ts +202 -0
- package/src/toc-kind.css +52 -0
- package/src/version-redirect.ts +147 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Recursive navigation tree renderer.
|
|
4
|
+
* Accepts any nav structure: { label, href?, children? }
|
|
5
|
+
* Used by all importers (MkDocs, AsciiDoc, Docusaurus, etc.)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface NavItem {
|
|
9
|
+
label: string;
|
|
10
|
+
href?: string;
|
|
11
|
+
children?: NavItem[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
items: NavItem[];
|
|
16
|
+
currentPath: string;
|
|
17
|
+
depth?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { items, currentPath, depth = 0 } = Astro.props;
|
|
21
|
+
|
|
22
|
+
function hasActive(item: NavItem, path: string): boolean {
|
|
23
|
+
if (item.href && path === item.href) return true;
|
|
24
|
+
return item.children?.some(c => hasActive(c, path)) ?? false;
|
|
25
|
+
}
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
<ul class:list={[depth === 0 ? "space-y-1" : "mt-1 space-y-0.5 pl-3 border-l border-border"]}>
|
|
29
|
+
{items.map(item => (
|
|
30
|
+
<li>
|
|
31
|
+
{item.children && item.children.length > 0 ? (
|
|
32
|
+
<details open={hasActive(item, currentPath)}>
|
|
33
|
+
<summary class:list={[
|
|
34
|
+
"flex cursor-pointer list-none items-center gap-1 rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-accent [&::-webkit-details-marker]:hidden",
|
|
35
|
+
depth === 0 ? "font-semibold" : "font-medium",
|
|
36
|
+
]}>
|
|
37
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0 transition-transform [[open]>&]:rotate-90"><polyline points="9 18 15 12 9 6"/></svg>
|
|
38
|
+
{item.href ? (
|
|
39
|
+
<a href={item.href} class="flex-1 text-foreground no-underline">{item.label}</a>
|
|
40
|
+
) : (
|
|
41
|
+
<span class="flex-1">{item.label}</span>
|
|
42
|
+
)}
|
|
43
|
+
</summary>
|
|
44
|
+
<Astro.self items={item.children} currentPath={currentPath} depth={depth + 1} />
|
|
45
|
+
</details>
|
|
46
|
+
) : item.href ? (
|
|
47
|
+
<a
|
|
48
|
+
href={item.href}
|
|
49
|
+
class:list={[
|
|
50
|
+
"block rounded-md px-2 py-1.5 text-sm no-underline transition-colors hover:bg-accent",
|
|
51
|
+
currentPath === item.href ? "bg-accent font-medium text-foreground" : "text-muted-foreground",
|
|
52
|
+
]}
|
|
53
|
+
>{item.label}</a>
|
|
54
|
+
) : (
|
|
55
|
+
<span class:list={[
|
|
56
|
+
"block px-2 py-1.5 text-sm",
|
|
57
|
+
depth === 0 ? "font-semibold text-foreground" : "text-muted-foreground",
|
|
58
|
+
]}>{item.label}</span>
|
|
59
|
+
)}
|
|
60
|
+
</li>
|
|
61
|
+
))}
|
|
62
|
+
</ul>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Documentation sidebar wrapper.
|
|
4
|
+
* Fixed on desktop, toggleable overlay on mobile.
|
|
5
|
+
* Contains the nav tree (DocsNav) and optional slot for extras.
|
|
6
|
+
*/
|
|
7
|
+
import DocsNav from "./DocsNav.astro";
|
|
8
|
+
|
|
9
|
+
interface NavItem {
|
|
10
|
+
label: string;
|
|
11
|
+
href?: string;
|
|
12
|
+
children?: NavItem[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
items: NavItem[];
|
|
17
|
+
currentPath: string;
|
|
18
|
+
class?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { items, currentPath, class: className } = Astro.props;
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<aside
|
|
25
|
+
data-docs-sidebar
|
|
26
|
+
class:list={[
|
|
27
|
+
"fixed inset-y-14 left-0 z-30 hidden w-64 shrink-0 overflow-y-auto border-r bg-background p-4",
|
|
28
|
+
"lg:sticky lg:top-14 lg:block lg:h-[calc(100vh-3.5rem)]",
|
|
29
|
+
className,
|
|
30
|
+
]}
|
|
31
|
+
>
|
|
32
|
+
<slot name="before-nav" />
|
|
33
|
+
<DocsNav items={items} currentPath={currentPath} />
|
|
34
|
+
<slot name="after-nav" />
|
|
35
|
+
</aside>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Table of contents sidebar.
|
|
4
|
+
* Renders heading links, optionally with scroll tracking (client JS).
|
|
5
|
+
*/
|
|
6
|
+
import "./toc-kind.css";
|
|
7
|
+
|
|
8
|
+
interface Heading {
|
|
9
|
+
depth: number;
|
|
10
|
+
slug: string;
|
|
11
|
+
text: string;
|
|
12
|
+
kind?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
headings: Heading[];
|
|
17
|
+
minDepth?: number;
|
|
18
|
+
maxDepth?: number;
|
|
19
|
+
title?: string;
|
|
20
|
+
class?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
headings,
|
|
25
|
+
minDepth = 2,
|
|
26
|
+
maxDepth = 3,
|
|
27
|
+
title = "On this page",
|
|
28
|
+
class: className,
|
|
29
|
+
} = Astro.props;
|
|
30
|
+
|
|
31
|
+
const filtered = headings.filter(h => h.depth >= minDepth && h.depth <= maxDepth);
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
{filtered.length > 0 && (
|
|
35
|
+
<nav class:list={["text-sm", className]} aria-label="Table of contents">
|
|
36
|
+
<div class="text-xs font-semibold uppercase text-muted-foreground">{title}</div>
|
|
37
|
+
<ul class="mt-2 space-y-1">
|
|
38
|
+
{filtered.map(h => (
|
|
39
|
+
<li>
|
|
40
|
+
<a
|
|
41
|
+
href={`#${h.slug}`}
|
|
42
|
+
class="block py-0.5 text-muted-foreground transition-colors hover:text-foreground"
|
|
43
|
+
style={`padding-left: ${(h.depth - minDepth) * 0.75}rem`}
|
|
44
|
+
data-toc-link={h.slug}
|
|
45
|
+
>{h.kind && <span class={`toc-kind toc-kind-${h.kind}`}>{h.kind}</span>}{h.text}</a>
|
|
46
|
+
</li>
|
|
47
|
+
))}
|
|
48
|
+
</ul>
|
|
49
|
+
</nav>
|
|
50
|
+
)}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* LocaleSwitcher — dropdown letting readers switch between
|
|
4
|
+
* declared locales of a docs page. Mirror of VersionSwitcher;
|
|
5
|
+
* shared decision logic in `./switcher.ts`.
|
|
6
|
+
*
|
|
7
|
+
* When the current page has no equivalent in the target locale,
|
|
8
|
+
* the dropdown link falls back to the locale's landing page
|
|
9
|
+
* (`<basePath>/<locale>/`). The default-locale redirect (PR 5c)
|
|
10
|
+
* + missing-translation fallback further smooth the UX.
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
type SwitcherMap,
|
|
14
|
+
type MultiSourceMeta,
|
|
15
|
+
buildSwitcherRows,
|
|
16
|
+
fallbackLandingUrl,
|
|
17
|
+
shouldRenderSwitcher,
|
|
18
|
+
} from "./switcher.js";
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
switcherMap: SwitcherMap;
|
|
22
|
+
multiSource?: MultiSourceMeta;
|
|
23
|
+
basePath?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { switcherMap, multiSource, basePath = "/docs" } = Astro.props;
|
|
27
|
+
const visible = shouldRenderSwitcher("locale", switcherMap, multiSource);
|
|
28
|
+
const rows = visible
|
|
29
|
+
? buildSwitcherRows({ axis: "locale", switcherMap, multiSource: multiSource! })
|
|
30
|
+
: [];
|
|
31
|
+
const currentRow = rows.find((r) => r.isCurrent);
|
|
32
|
+
const currentLabel = currentRow?.entry.label ?? currentRow?.entry.id ?? "Locale";
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
{visible && (
|
|
36
|
+
<details class="locale-switcher relative">
|
|
37
|
+
<summary
|
|
38
|
+
class="flex cursor-pointer items-center gap-1 rounded-md border border-border px-3 py-1.5 text-sm hover:bg-accent"
|
|
39
|
+
aria-label="Switch language"
|
|
40
|
+
>
|
|
41
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
|
42
|
+
<span class="font-medium">{currentLabel}</span>
|
|
43
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="ml-1 transition-transform"><polyline points="6 9 12 15 18 9"/></svg>
|
|
44
|
+
</summary>
|
|
45
|
+
<ul class="absolute right-0 z-50 mt-1 min-w-[10rem] rounded-md border border-border bg-popover p-1 text-sm shadow-md">
|
|
46
|
+
{rows.map((row) => {
|
|
47
|
+
const href = row.url ?? fallbackLandingUrl(basePath, row.entry.id);
|
|
48
|
+
const isFallback = row.url === null && !row.isCurrent;
|
|
49
|
+
return (
|
|
50
|
+
<li>
|
|
51
|
+
<a
|
|
52
|
+
href={href}
|
|
53
|
+
hreflang={row.entry.id}
|
|
54
|
+
aria-current={row.isCurrent ? "true" : undefined}
|
|
55
|
+
class:list={[
|
|
56
|
+
"flex items-center gap-2 rounded-sm px-2 py-1.5 no-underline",
|
|
57
|
+
row.isCurrent
|
|
58
|
+
? "bg-accent font-medium text-foreground"
|
|
59
|
+
: "text-foreground hover:bg-accent",
|
|
60
|
+
]}
|
|
61
|
+
>
|
|
62
|
+
<span class="flex-1">{row.entry.label ?? row.entry.id}</span>
|
|
63
|
+
{row.entry.default && !row.isCurrent && (
|
|
64
|
+
<span class="text-[10px] text-muted-foreground">default</span>
|
|
65
|
+
)}
|
|
66
|
+
{isFallback && (
|
|
67
|
+
<span title="Page not translated" class="text-[10px] text-muted-foreground">→</span>
|
|
68
|
+
)}
|
|
69
|
+
</a>
|
|
70
|
+
</li>
|
|
71
|
+
);
|
|
72
|
+
})}
|
|
73
|
+
</ul>
|
|
74
|
+
</details>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
<style>
|
|
78
|
+
.locale-switcher > summary {
|
|
79
|
+
list-style: none;
|
|
80
|
+
}
|
|
81
|
+
.locale-switcher > summary::-webkit-details-marker {
|
|
82
|
+
display: none;
|
|
83
|
+
}
|
|
84
|
+
.locale-switcher[open] > summary > svg:last-child {
|
|
85
|
+
transform: rotate(180deg);
|
|
86
|
+
}
|
|
87
|
+
</style>
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Per-page LLM action cluster.
|
|
4
|
+
*
|
|
5
|
+
* Single dropdown trigger ("Copy or open ▾") containing every
|
|
6
|
+
* action labeled in plain text. The cluster is excluded from
|
|
7
|
+
* Pagefind indexing so its labels don't pollute search excerpts.
|
|
8
|
+
*
|
|
9
|
+
* Layout decision: one labeled dropdown beats a copy icon + a
|
|
10
|
+
* separate "Open in" dropdown. An icon-only copy button is
|
|
11
|
+
* ambiguous; a single dropdown with everything labeled is
|
|
12
|
+
* discoverable in one click. Costs a second click for the most
|
|
13
|
+
* common action (Copy) but the discoverability win is worth it.
|
|
14
|
+
*
|
|
15
|
+
* Items in order:
|
|
16
|
+
* 1. Copy markdown — clipboard write (button-like menu item)
|
|
17
|
+
* 2. Open markdown — navigates to .md mirror
|
|
18
|
+
* 3. Open in <Provider> ×N — provider deep links
|
|
19
|
+
*
|
|
20
|
+
* Provider deep links are URL-only — the provider's LLM fetches
|
|
21
|
+
* the .md mirror directly. See plans/llm-page-actions.md.
|
|
22
|
+
*
|
|
23
|
+
* Composes existing primitives from `@dogsbay/ui`:
|
|
24
|
+
* - DropdownMenu* — accessible dropdown with keyboard nav
|
|
25
|
+
*
|
|
26
|
+
* Items carry `data-part="item"` so the existing dropdown JS
|
|
27
|
+
* picks them up for arrow-key navigation. Provider items are
|
|
28
|
+
* real <a> tags (cmd/middle-click works natively); the Copy
|
|
29
|
+
* item is a <button> so it doesn't navigate.
|
|
30
|
+
*/
|
|
31
|
+
import DropdownMenu from "@dogsbay/ui/dropdown-menu/DropdownMenu.astro";
|
|
32
|
+
import DropdownMenuTrigger from "@dogsbay/ui/dropdown-menu/DropdownMenuTrigger.astro";
|
|
33
|
+
import DropdownMenuContent from "@dogsbay/ui/dropdown-menu/DropdownMenuContent.astro";
|
|
34
|
+
import DropdownMenuSeparator from "@dogsbay/ui/dropdown-menu/DropdownMenuSeparator.astro";
|
|
35
|
+
import {
|
|
36
|
+
DEFAULT_PROMPT_TEMPLATE,
|
|
37
|
+
providerDeepLink,
|
|
38
|
+
resolveProviders,
|
|
39
|
+
type LlmProviderName,
|
|
40
|
+
} from "./llm-actions.js";
|
|
41
|
+
|
|
42
|
+
interface Props {
|
|
43
|
+
/**
|
|
44
|
+
* Plain-text markdown body of the current page. Embedded into the
|
|
45
|
+
* Copy menu item via `data-copy-text`. When omitted, the Copy
|
|
46
|
+
* item is hidden.
|
|
47
|
+
*/
|
|
48
|
+
markdownBody?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Path or URL to the .md mirror of the current page. Used for
|
|
51
|
+
* the "Open markdown" item and as the `{url}` substitution in
|
|
52
|
+
* the provider prompt template. Should be absolute when a
|
|
53
|
+
* `siteUrl` is configured (so providers can fetch); falls back
|
|
54
|
+
* to a relative path when not — the build-time warning catches
|
|
55
|
+
* the misconfiguration.
|
|
56
|
+
*/
|
|
57
|
+
mdUrl: string;
|
|
58
|
+
/** Provider list, in render order. */
|
|
59
|
+
providers?: LlmProviderName[];
|
|
60
|
+
/**
|
|
61
|
+
* Show the "Copy markdown" item in the dropdown. Default: true.
|
|
62
|
+
* Disable for sites that want only the navigation / open-in
|
|
63
|
+
* actions. The standalone copy-icon-button is gone — copy now
|
|
64
|
+
* lives inside the dropdown alongside the other actions.
|
|
65
|
+
*/
|
|
66
|
+
copyButton?: boolean;
|
|
67
|
+
/** Prompt template; `{url}` is replaced by `mdUrl`. */
|
|
68
|
+
promptTemplate?: string;
|
|
69
|
+
/** Optional class for layout-side styling. */
|
|
70
|
+
class?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const {
|
|
74
|
+
markdownBody,
|
|
75
|
+
mdUrl,
|
|
76
|
+
providers,
|
|
77
|
+
copyButton = true,
|
|
78
|
+
promptTemplate = DEFAULT_PROMPT_TEMPLATE,
|
|
79
|
+
class: className,
|
|
80
|
+
} = Astro.props;
|
|
81
|
+
|
|
82
|
+
const resolvedProviders = resolveProviders(providers);
|
|
83
|
+
const showCopyItem = copyButton && typeof markdownBody === "string" && markdownBody.length > 0;
|
|
84
|
+
const showOpenItem = mdUrl.length > 0;
|
|
85
|
+
const hasProviders = resolvedProviders.length > 0;
|
|
86
|
+
const showCluster = showCopyItem || showOpenItem || hasProviders;
|
|
87
|
+
|
|
88
|
+
// Tailwind classes shared by every menu item — styled to look
|
|
89
|
+
// like DropdownMenuItem without going through the <div
|
|
90
|
+
// role="menuitem"> component (we want real <a> / <button> tags
|
|
91
|
+
// so cmd-click + native button semantics work).
|
|
92
|
+
const itemClass =
|
|
93
|
+
"relative flex w-full cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm text-foreground outline-none focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground no-underline";
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
{showCluster && (
|
|
97
|
+
<div
|
|
98
|
+
class:list={["flex items-center", className]}
|
|
99
|
+
data-component="page-actions"
|
|
100
|
+
data-pagefind-ignore
|
|
101
|
+
>
|
|
102
|
+
<DropdownMenu>
|
|
103
|
+
<DropdownMenuTrigger
|
|
104
|
+
class="inline-flex h-8 items-center gap-1 rounded-md border border-border bg-background px-3 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
105
|
+
aria-label="Copy or open page actions"
|
|
106
|
+
>
|
|
107
|
+
<span>Copy or open</span>
|
|
108
|
+
<svg
|
|
109
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
110
|
+
width="14"
|
|
111
|
+
height="14"
|
|
112
|
+
viewBox="0 0 24 24"
|
|
113
|
+
fill="none"
|
|
114
|
+
stroke="currentColor"
|
|
115
|
+
stroke-width="2"
|
|
116
|
+
stroke-linecap="round"
|
|
117
|
+
stroke-linejoin="round"
|
|
118
|
+
aria-hidden="true"
|
|
119
|
+
>
|
|
120
|
+
<polyline points="6 9 12 15 18 9"></polyline>
|
|
121
|
+
</svg>
|
|
122
|
+
</DropdownMenuTrigger>
|
|
123
|
+
<DropdownMenuContent align="end" class="min-w-[12rem]">
|
|
124
|
+
{showCopyItem && (
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
class={itemClass}
|
|
128
|
+
data-part="item"
|
|
129
|
+
role="menuitem"
|
|
130
|
+
data-action="copy-markdown"
|
|
131
|
+
data-copy-text={markdownBody!}
|
|
132
|
+
>
|
|
133
|
+
<svg
|
|
134
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
135
|
+
width="14"
|
|
136
|
+
height="14"
|
|
137
|
+
viewBox="0 0 24 24"
|
|
138
|
+
fill="none"
|
|
139
|
+
stroke="currentColor"
|
|
140
|
+
stroke-width="2"
|
|
141
|
+
stroke-linecap="round"
|
|
142
|
+
stroke-linejoin="round"
|
|
143
|
+
data-copy-icon
|
|
144
|
+
aria-hidden="true"
|
|
145
|
+
>
|
|
146
|
+
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect>
|
|
147
|
+
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path>
|
|
148
|
+
</svg>
|
|
149
|
+
<svg
|
|
150
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
151
|
+
width="14"
|
|
152
|
+
height="14"
|
|
153
|
+
viewBox="0 0 24 24"
|
|
154
|
+
fill="none"
|
|
155
|
+
stroke="currentColor"
|
|
156
|
+
stroke-width="2"
|
|
157
|
+
stroke-linecap="round"
|
|
158
|
+
stroke-linejoin="round"
|
|
159
|
+
data-check-icon
|
|
160
|
+
class="hidden"
|
|
161
|
+
aria-hidden="true"
|
|
162
|
+
>
|
|
163
|
+
<polyline points="20 6 9 17 4 12"></polyline>
|
|
164
|
+
</svg>
|
|
165
|
+
<span data-copy-label>Copy markdown</span>
|
|
166
|
+
</button>
|
|
167
|
+
)}
|
|
168
|
+
{showOpenItem && (
|
|
169
|
+
<a
|
|
170
|
+
href={mdUrl}
|
|
171
|
+
class={itemClass}
|
|
172
|
+
data-part="item"
|
|
173
|
+
role="menuitem"
|
|
174
|
+
data-action="open-markdown"
|
|
175
|
+
>
|
|
176
|
+
<svg
|
|
177
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
178
|
+
width="14"
|
|
179
|
+
height="14"
|
|
180
|
+
viewBox="0 0 24 24"
|
|
181
|
+
fill="none"
|
|
182
|
+
stroke="currentColor"
|
|
183
|
+
stroke-width="2"
|
|
184
|
+
stroke-linecap="round"
|
|
185
|
+
stroke-linejoin="round"
|
|
186
|
+
aria-hidden="true"
|
|
187
|
+
>
|
|
188
|
+
<path d="M14 3h7v7"></path>
|
|
189
|
+
<path d="M10 14L21 3"></path>
|
|
190
|
+
<path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5"></path>
|
|
191
|
+
</svg>
|
|
192
|
+
<span>Open markdown</span>
|
|
193
|
+
</a>
|
|
194
|
+
)}
|
|
195
|
+
{hasProviders && (showCopyItem || showOpenItem) && <DropdownMenuSeparator />}
|
|
196
|
+
{resolvedProviders.map((provider) => (
|
|
197
|
+
<a
|
|
198
|
+
href={providerDeepLink(provider.name, mdUrl, promptTemplate)}
|
|
199
|
+
class={itemClass}
|
|
200
|
+
data-part="item"
|
|
201
|
+
role="menuitem"
|
|
202
|
+
data-provider={provider.name}
|
|
203
|
+
target="_blank"
|
|
204
|
+
rel="noopener noreferrer"
|
|
205
|
+
aria-label={`${provider.label} — opens in new tab`}
|
|
206
|
+
>
|
|
207
|
+
<span>{provider.label}</span>
|
|
208
|
+
<svg
|
|
209
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
210
|
+
width="12"
|
|
211
|
+
height="12"
|
|
212
|
+
viewBox="0 0 24 24"
|
|
213
|
+
fill="none"
|
|
214
|
+
stroke="currentColor"
|
|
215
|
+
stroke-width="2"
|
|
216
|
+
stroke-linecap="round"
|
|
217
|
+
stroke-linejoin="round"
|
|
218
|
+
class="ml-auto opacity-60"
|
|
219
|
+
aria-hidden="true"
|
|
220
|
+
>
|
|
221
|
+
<path d="M7 17L17 7"></path>
|
|
222
|
+
<polyline points="7 7 17 7 17 17"></polyline>
|
|
223
|
+
</svg>
|
|
224
|
+
</a>
|
|
225
|
+
))}
|
|
226
|
+
</DropdownMenuContent>
|
|
227
|
+
</DropdownMenu>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
|
|
231
|
+
<script>
|
|
232
|
+
import "@dogsbay/ui/dropdown-menu/dropdown-menu.ts";
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Wire up the in-dropdown "Copy markdown" item. Clipboard write
|
|
236
|
+
* with checkmark feedback for two seconds; same UX as the
|
|
237
|
+
* standalone CopyButton component, inlined so we don't need a
|
|
238
|
+
* separate component just for one menu item.
|
|
239
|
+
*/
|
|
240
|
+
function setupPageActionsCopy(): void {
|
|
241
|
+
document.querySelectorAll<HTMLElement>(
|
|
242
|
+
"[data-component='page-actions'] [data-action='copy-markdown']",
|
|
243
|
+
).forEach((btn) => {
|
|
244
|
+
// @ts-expect-error — guard against double-binding on
|
|
245
|
+
// astro:after-swap re-runs.
|
|
246
|
+
if (btn.__copyBound) return;
|
|
247
|
+
// @ts-expect-error
|
|
248
|
+
btn.__copyBound = true;
|
|
249
|
+
|
|
250
|
+
btn.addEventListener("click", async (e) => {
|
|
251
|
+
e.preventDefault();
|
|
252
|
+
const text = btn.getAttribute("data-copy-text") ?? "";
|
|
253
|
+
try {
|
|
254
|
+
await navigator.clipboard.writeText(text);
|
|
255
|
+
} catch {
|
|
256
|
+
// No clipboard access (insecure context, denied permission, etc.)
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const copyIcon = btn.querySelector<HTMLElement>("[data-copy-icon]");
|
|
260
|
+
const checkIcon = btn.querySelector<HTMLElement>("[data-check-icon]");
|
|
261
|
+
const label = btn.querySelector<HTMLElement>("[data-copy-label]");
|
|
262
|
+
const original = label?.textContent ?? "Copy markdown";
|
|
263
|
+
if (copyIcon && checkIcon) {
|
|
264
|
+
copyIcon.classList.add("hidden");
|
|
265
|
+
checkIcon.classList.remove("hidden");
|
|
266
|
+
}
|
|
267
|
+
if (label) label.textContent = "Copied!";
|
|
268
|
+
setTimeout(() => {
|
|
269
|
+
if (copyIcon && checkIcon) {
|
|
270
|
+
copyIcon.classList.remove("hidden");
|
|
271
|
+
checkIcon.classList.add("hidden");
|
|
272
|
+
}
|
|
273
|
+
if (label) label.textContent = original;
|
|
274
|
+
}, 2000);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
setupPageActionsCopy();
|
|
280
|
+
document.addEventListener("astro:after-swap", setupPageActionsCopy);
|
|
281
|
+
</script>
|