@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,884 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Standard documentation page layout.
|
|
4
|
+
*
|
|
5
|
+
* Uses the same sidebar component system as the demo app:
|
|
6
|
+
* collapsible sidebar, dark mode toggle, frosted glass header,
|
|
7
|
+
* TOC sidebar, pagination footer.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* <DocsLayout
|
|
11
|
+
* siteName="My Docs"
|
|
12
|
+
* title="Getting Started"
|
|
13
|
+
* nav={navItems}
|
|
14
|
+
* headings={headings}
|
|
15
|
+
* >
|
|
16
|
+
* <article>...</article>
|
|
17
|
+
* </DocsLayout>
|
|
18
|
+
*/
|
|
19
|
+
import SidebarProvider from "@dogsbay/ui/sidebar/SidebarProvider.astro";
|
|
20
|
+
import Sidebar from "@dogsbay/ui/sidebar/Sidebar.astro";
|
|
21
|
+
import SidebarHeader from "@dogsbay/ui/sidebar/SidebarHeader.astro";
|
|
22
|
+
import SidebarContent from "@dogsbay/ui/sidebar/SidebarContent.astro";
|
|
23
|
+
import SidebarTrigger from "@dogsbay/ui/sidebar/SidebarTrigger.astro";
|
|
24
|
+
import SidebarInset from "@dogsbay/ui/sidebar/SidebarInset.astro";
|
|
25
|
+
import SidebarRail from "@dogsbay/ui/sidebar/SidebarRail.astro";
|
|
26
|
+
import SidebarGroup from "@dogsbay/ui/sidebar/SidebarGroup.astro";
|
|
27
|
+
import SidebarGroupLabel from "@dogsbay/ui/sidebar/SidebarGroupLabel.astro";
|
|
28
|
+
import SidebarGroupContent from "@dogsbay/ui/sidebar/SidebarGroupContent.astro";
|
|
29
|
+
import SidebarMenu from "@dogsbay/ui/sidebar/SidebarMenu.astro";
|
|
30
|
+
import SidebarMenuItem from "@dogsbay/ui/sidebar/SidebarMenuItem.astro";
|
|
31
|
+
import SidebarMenuButton from "@dogsbay/ui/sidebar/SidebarMenuButton.astro";
|
|
32
|
+
import SidebarSeparator from "@dogsbay/ui/sidebar/SidebarSeparator.astro";
|
|
33
|
+
import SidebarNavTree from "@dogsbay/ui/sidebar/SidebarNavTree.astro";
|
|
34
|
+
import Separator from "@dogsbay/ui/separator/Separator.astro";
|
|
35
|
+
import ThemeToggle from "@dogsbay/ui/theme-toggle/ThemeToggle.astro";
|
|
36
|
+
import DocsToc from "./DocsToc.astro";
|
|
37
|
+
import DocsFooter from "./DocsFooter.astro";
|
|
38
|
+
import SearchDialog from "./SearchDialog.astro";
|
|
39
|
+
import TagList from "./TagList.astro";
|
|
40
|
+
import StatusBadge from "./StatusBadge.astro";
|
|
41
|
+
import TypeBadge from "./TypeBadge.astro";
|
|
42
|
+
import PageActions from "./PageActions.astro";
|
|
43
|
+
import VersionSwitcher from "./VersionSwitcher.astro";
|
|
44
|
+
import LocaleSwitcher from "./LocaleSwitcher.astro";
|
|
45
|
+
import { filterNavByAxis } from "./nav-filter.js";
|
|
46
|
+
import type { LlmProviderName } from "./llm-actions.js";
|
|
47
|
+
import { jsonLdTypeFor, normalizeCustomJsonLd } from "./json-ld.js";
|
|
48
|
+
import { resolveTagKeywords } from "./tag-list-data.js";
|
|
49
|
+
|
|
50
|
+
interface NavItem {
|
|
51
|
+
label: string;
|
|
52
|
+
href?: string;
|
|
53
|
+
icon?: string;
|
|
54
|
+
children?: NavItem[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface NavGroup {
|
|
58
|
+
label: string;
|
|
59
|
+
items: NavItem[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface Heading {
|
|
63
|
+
depth: number;
|
|
64
|
+
slug: string;
|
|
65
|
+
text: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface PaginationLink {
|
|
69
|
+
label: string;
|
|
70
|
+
href: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Closed palette of tag-chip colors. Mirrors `@dogsbay/types`'s
|
|
75
|
+
* `TagPaletteName`; restated here so this component stays
|
|
76
|
+
* standalone-importable. Keep in sync.
|
|
77
|
+
*/
|
|
78
|
+
type TagPaletteName =
|
|
79
|
+
| "blue"
|
|
80
|
+
| "amber"
|
|
81
|
+
| "emerald"
|
|
82
|
+
| "violet"
|
|
83
|
+
| "rose"
|
|
84
|
+
| "slate";
|
|
85
|
+
|
|
86
|
+
interface Props {
|
|
87
|
+
/** Site name shown in header and sidebar logo */
|
|
88
|
+
siteName: string;
|
|
89
|
+
/** Page title for <title> tag */
|
|
90
|
+
title: string;
|
|
91
|
+
/** Home URL — also used as the base for the canonical link if absolute */
|
|
92
|
+
siteUrl?: string;
|
|
93
|
+
/** Navigation tree — flat array of items or grouped sections */
|
|
94
|
+
nav: NavItem[];
|
|
95
|
+
/** Optional grouped nav (label + items). If provided, renders sections with labels. */
|
|
96
|
+
navGroups?: NavGroup[];
|
|
97
|
+
/** Page headings for TOC */
|
|
98
|
+
headings?: Heading[];
|
|
99
|
+
/** Prev page link */
|
|
100
|
+
prev?: PaginationLink;
|
|
101
|
+
/** Next page link */
|
|
102
|
+
next?: PaginationLink;
|
|
103
|
+
/** GitHub/GitLab repo URL */
|
|
104
|
+
repoUrl?: string;
|
|
105
|
+
/** Edit URL for this page */
|
|
106
|
+
editUrl?: string;
|
|
107
|
+
/** Last updated date */
|
|
108
|
+
lastUpdated?: string;
|
|
109
|
+
/** Page description — also used as og:description / twitter:description */
|
|
110
|
+
description?: string;
|
|
111
|
+
/** Site-wide description used as fallback when page `description` is not set */
|
|
112
|
+
siteDescription?: string;
|
|
113
|
+
/** Copyright text (HTML allowed) */
|
|
114
|
+
copyright?: string;
|
|
115
|
+
/** Favicon path (default: "/favicon.ico"). Set to false to disable. */
|
|
116
|
+
favicon?: string | false;
|
|
117
|
+
/** Per-page OG image URL. Overrides defaultOgImage. */
|
|
118
|
+
ogImage?: string;
|
|
119
|
+
/** Site-level default OG image used when ogImage is not set */
|
|
120
|
+
defaultOgImage?: string;
|
|
121
|
+
/** OG content type. Default: "article" (use "website" for landing pages). */
|
|
122
|
+
ogType?: "article" | "website";
|
|
123
|
+
/** Canonical URL for this page. Auto-computed from siteUrl + pathname when absolute siteUrl is provided. */
|
|
124
|
+
canonicalUrl?: string;
|
|
125
|
+
/** Twitter / X handle including the leading "@" */
|
|
126
|
+
twitterHandle?: string;
|
|
127
|
+
/** Theme color hint for browsers (hex string) */
|
|
128
|
+
themeColor?: string;
|
|
129
|
+
/**
|
|
130
|
+
* Emit `<meta name="robots" content="noindex, nofollow">` when
|
|
131
|
+
* true. Tells external search engines (Google, Bing) to skip
|
|
132
|
+
* this page. Has NO effect on in-site Pagefind search — for
|
|
133
|
+
* that, use `excludeFromSearch`. The two are independent: a
|
|
134
|
+
* page can be excluded from external SEs but still appear in
|
|
135
|
+
* Pagefind (e.g. duplicate / old content readers might still
|
|
136
|
+
* want to find when they're already on the site), or vice
|
|
137
|
+
* versa.
|
|
138
|
+
*/
|
|
139
|
+
noindex?: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Exclude this page from in-site Pagefind search results.
|
|
142
|
+
*
|
|
143
|
+
* Two-part wiring required because Pagefind resolves
|
|
144
|
+
* `data-pagefind-body` (declares the indexable region) BEFORE
|
|
145
|
+
* checking for ignore attributes elsewhere — the body-level
|
|
146
|
+
* ignore is dead code when a `data-pagefind-body` element
|
|
147
|
+
* exists deeper in the tree. So when this prop is set, the
|
|
148
|
+
* layout:
|
|
149
|
+
*
|
|
150
|
+
* 1. Adds `data-pagefind-ignore` to `<body>`.
|
|
151
|
+
* 2. **Omits** `data-pagefind-body` from `<main>`. Pagefind
|
|
152
|
+
* falls back to body-scope, sees the ignore, skips the page.
|
|
153
|
+
*
|
|
154
|
+
* The auto-emitted taxonomy index / term routes pass this to
|
|
155
|
+
* keep navigation pages out of search-result clutter — a
|
|
156
|
+
* search for "auth" should return the actual auth tutorial,
|
|
157
|
+
* not the `/tags/concept/auth/` listing.
|
|
158
|
+
*/
|
|
159
|
+
excludeFromSearch?: boolean;
|
|
160
|
+
/**
|
|
161
|
+
* Plausible analytics tracking domain. When set, a single deferred
|
|
162
|
+
* <script> tag is emitted in <head>. Cookie-less; no consent banner
|
|
163
|
+
* required for EU users.
|
|
164
|
+
*/
|
|
165
|
+
plausibleDomain?: string;
|
|
166
|
+
/**
|
|
167
|
+
* Override Plausible script URL. Default:
|
|
168
|
+
* "https://plausible.io/js/script.js". Use for self-hosted or
|
|
169
|
+
* proxied installations.
|
|
170
|
+
*/
|
|
171
|
+
plausibleScriptUrl?: string;
|
|
172
|
+
/**
|
|
173
|
+
* Disable the built-in Pagefind search dialog. Default: false.
|
|
174
|
+
* Set to true on sites that don't generate a Pagefind index.
|
|
175
|
+
*/
|
|
176
|
+
hideSearch?: boolean;
|
|
177
|
+
/**
|
|
178
|
+
* Pagefind index path. Default: "/pagefind/". Override for sites with
|
|
179
|
+
* a non-root base path (e.g. "/docs/pagefind/").
|
|
180
|
+
*/
|
|
181
|
+
pagefindUrl?: string;
|
|
182
|
+
/**
|
|
183
|
+
* Whether the site emits per-page `.md` mirror endpoints. When true,
|
|
184
|
+
* the page advertises `<link rel="alternate" type="text/markdown">`
|
|
185
|
+
* pointing at the mirror — agents that respect link relations can
|
|
186
|
+
* fetch the markdown variant without content negotiation.
|
|
187
|
+
*/
|
|
188
|
+
mdMirror?: boolean;
|
|
189
|
+
/**
|
|
190
|
+
* Page tags from `meta.tags`. Renders a TagList chip strip below the
|
|
191
|
+
* page title when non-empty. Slash-nested values link into the
|
|
192
|
+
* matching taxonomy term page (e.g. `api/rest` → `/tags/api/rest/`).
|
|
193
|
+
*/
|
|
194
|
+
tags?: string[];
|
|
195
|
+
/**
|
|
196
|
+
* Path prefix for tag term pages. Default `/tags`. Match the
|
|
197
|
+
* `taxonomies.tags.indexPath` declared in `dogsbay.config.yml`.
|
|
198
|
+
*/
|
|
199
|
+
tagsIndexPath?: string;
|
|
200
|
+
/**
|
|
201
|
+
* Per-prefix display config for tag chips, sourced from
|
|
202
|
+
* `taxonomies.tags.prefixes` in `dogsbay.config.yml`. Keyed by
|
|
203
|
+
* the top-level segment of slash-nested tags (`concept`,
|
|
204
|
+
* `difficulty`, etc.). When set, `<TagList>` renders the tag as
|
|
205
|
+
* a two-part `<Label>: <Leaf>` chip in the prefix's palette
|
|
206
|
+
* color. Tags whose prefix has no entry render the flat single-
|
|
207
|
+
* color chip. See plans/tag-display-config.md.
|
|
208
|
+
*/
|
|
209
|
+
tagPrefixes?: Record<string, { label?: string; color?: TagPaletteName }>;
|
|
210
|
+
/**
|
|
211
|
+
* Per-tag leaf-label overrides (slug → display string). URLs
|
|
212
|
+
* stay slug-based; only chip text changes. Sourced from
|
|
213
|
+
* `taxonomies.tags.labels`.
|
|
214
|
+
*/
|
|
215
|
+
tagLabels?: Record<string, string>;
|
|
216
|
+
/**
|
|
217
|
+
* Page lifecycle status. Renders a StatusBadge for non-stable
|
|
218
|
+
* states. Drafts/deprecated get strong colors.
|
|
219
|
+
*/
|
|
220
|
+
status?: "draft" | "preview" | "stable" | "deprecated";
|
|
221
|
+
/**
|
|
222
|
+
* Page type from `meta.type` (open string). Renders a TypeBadge.
|
|
223
|
+
* Diátaxis values get conventional colors; others use a neutral
|
|
224
|
+
* fallback.
|
|
225
|
+
*
|
|
226
|
+
* Also drives Schema.org `@type` selection for the auto-emitted
|
|
227
|
+
* JSON-LD: tutorials and how-tos render as `HowTo`, references
|
|
228
|
+
* as `TechArticle`, courses as `Course`. Pages without a
|
|
229
|
+
* recognised type fall back to `Article`.
|
|
230
|
+
*/
|
|
231
|
+
pageType?: string;
|
|
232
|
+
/**
|
|
233
|
+
* Site- or page-specific structured data appended to the
|
|
234
|
+
* `<head>`. Each value is wrapped in its own
|
|
235
|
+
* `<script type="application/ld+json">` block alongside the
|
|
236
|
+
* auto-emitted Article / HowTo / etc. Lets writers layer in
|
|
237
|
+
* `Course`, `Person`, `Organization`, `BreadcrumbList`, etc.
|
|
238
|
+
* without forking the layout.
|
|
239
|
+
*
|
|
240
|
+
* When a page wants to **replace** the auto-emitted JSON-LD,
|
|
241
|
+
* pass `ogType: "website"` (suppresses the Article emit) and
|
|
242
|
+
* provide your own block via `customJsonLd`.
|
|
243
|
+
*/
|
|
244
|
+
customJsonLd?: Record<string, unknown> | Record<string, unknown>[];
|
|
245
|
+
/**
|
|
246
|
+
* Audience facets from `meta.audience`. Surfaced as hidden
|
|
247
|
+
* `data-pagefind-filter="audience:<value>"` elements at the top
|
|
248
|
+
* of `<body>` so Pagefind picks them up during body indexing.
|
|
249
|
+
* Not rendered in the meta strip (would crowd the chip area);
|
|
250
|
+
* use a custom layout if you want them visible.
|
|
251
|
+
*/
|
|
252
|
+
audience?: string[];
|
|
253
|
+
/**
|
|
254
|
+
* Category segments from `meta.category` (or auto-derived from
|
|
255
|
+
* path). Surfaced as hidden `data-pagefind-filter="category:<value>"`
|
|
256
|
+
* elements in `<body>`.
|
|
257
|
+
*/
|
|
258
|
+
category?: string[];
|
|
259
|
+
/**
|
|
260
|
+
* Map of taxonomy name → index path for declared taxonomies.
|
|
261
|
+
* Used to wire links from built-in field badges (TypeBadge,
|
|
262
|
+
* StatusBadge) to their browse destination, when one exists.
|
|
263
|
+
* A field absent from this map renders as a plain badge with no
|
|
264
|
+
* link. Sourced from `taxonomies` declarations in
|
|
265
|
+
* `dogsbay.config.yml` and emitted to `site.json` by
|
|
266
|
+
* `format-astro`.
|
|
267
|
+
*/
|
|
268
|
+
taxonomyIndexPaths?: Record<string, string>;
|
|
269
|
+
/**
|
|
270
|
+
* Per-taxonomy display config (`prefixes` + `labels`), keyed
|
|
271
|
+
* by taxonomy name. Forwarded to `<SearchDialog>` so facet
|
|
272
|
+
* checkboxes show human labels instead of slugs. Same data
|
|
273
|
+
* the chip components consume, just shaped per-taxonomy.
|
|
274
|
+
*/
|
|
275
|
+
taxonomyDisplay?: Record<string, {
|
|
276
|
+
prefixes?: Record<string, { label?: string; color?: TagPaletteName }>;
|
|
277
|
+
labels?: Record<string, string>;
|
|
278
|
+
}>;
|
|
279
|
+
/**
|
|
280
|
+
* When `true`, render `<h1>{title}</h1>` at the top of `<main>`
|
|
281
|
+
* (above the meta strip) — used when the markdown body doesn't
|
|
282
|
+
* already start with a level-1 heading. format-astro computes
|
|
283
|
+
* this per page via `detectLeadingNodes`. Default `false` so
|
|
284
|
+
* existing consumers keep their current rendering until they
|
|
285
|
+
* opt in. See plans/auto-lede.md.
|
|
286
|
+
*/
|
|
287
|
+
autoH1?: boolean;
|
|
288
|
+
/**
|
|
289
|
+
* When `true`, render `<p>{description}</p>` below the auto H1
|
|
290
|
+
* (or above the meta strip when there's no H1) — used when the
|
|
291
|
+
* markdown body doesn't already start with a leading paragraph.
|
|
292
|
+
* Reads `description` from props. Default `false`.
|
|
293
|
+
*/
|
|
294
|
+
autoLede?: boolean;
|
|
295
|
+
/**
|
|
296
|
+
* Multi-source axis metadata for the current page. Stamped by
|
|
297
|
+
* the loader when ≥1 axis is active. Consumed by
|
|
298
|
+
* VersionSwitcher to compute the logical key
|
|
299
|
+
* (`<namespace>/<originalSlug>`) used to look up alternates
|
|
300
|
+
* in `switcherMap`.
|
|
301
|
+
*
|
|
302
|
+
* Undefined for pages outside the multi-source pipeline (root
|
|
303
|
+
* landing pages, custom pages added by hand).
|
|
304
|
+
*/
|
|
305
|
+
multiSource?: {
|
|
306
|
+
namespace?: string;
|
|
307
|
+
version?: string;
|
|
308
|
+
locale?: string;
|
|
309
|
+
originalSlug: string;
|
|
310
|
+
};
|
|
311
|
+
/**
|
|
312
|
+
* Switcher data emitted by format-astro's `emitSwitcherMap`.
|
|
313
|
+
* The VersionSwitcher renders nothing when `versions.length <
|
|
314
|
+
* 2` (single-version site) or when the current page has no
|
|
315
|
+
* `multiSource.version`. Pass the raw JSON imported from
|
|
316
|
+
* `@/data/switcherMap.json`.
|
|
317
|
+
*/
|
|
318
|
+
switcherMap?: {
|
|
319
|
+
versions: Array<{ id: string; label?: string; eol?: boolean; default?: boolean }>;
|
|
320
|
+
byLogicalKey: Record<string, Record<string, string>>;
|
|
321
|
+
};
|
|
322
|
+
/**
|
|
323
|
+
* URL prefix for the switcher's per-version landing fallback
|
|
324
|
+
* (`<basePath>/<version>/`). Defaults to "/docs".
|
|
325
|
+
*/
|
|
326
|
+
basePath?: string;
|
|
327
|
+
/**
|
|
328
|
+
* Per-page LLM action UI. When set and `enabled !== false`, renders
|
|
329
|
+
* the PageActions cluster (Copy markdown + Open in Claude/ChatGPT/
|
|
330
|
+
* Perplexity/Gemini dropdown). See plans/llm-page-actions.md.
|
|
331
|
+
*
|
|
332
|
+
* `markdownBody` is the page's markdown source (for the Copy
|
|
333
|
+
* button). `mdUrl` is the absolute URL of the .md mirror endpoint.
|
|
334
|
+
* Both come from format-astro at emit time.
|
|
335
|
+
*/
|
|
336
|
+
llmActions?: {
|
|
337
|
+
enabled?: boolean;
|
|
338
|
+
providers?: LlmProviderName[];
|
|
339
|
+
placement?: "header" | "inline" | "both";
|
|
340
|
+
copyButton?: boolean;
|
|
341
|
+
promptTemplate?: string;
|
|
342
|
+
footerLink?: boolean;
|
|
343
|
+
/** Markdown body to copy. */
|
|
344
|
+
markdownBody?: string;
|
|
345
|
+
/** Absolute or relative URL to the page's .md mirror. */
|
|
346
|
+
mdUrl?: string;
|
|
347
|
+
};
|
|
348
|
+
/**
|
|
349
|
+
* When `true`, drop the prose-width column cap (~48rem) and the
|
|
350
|
+
* right-hand TOC sidebar so the slot content can use the full
|
|
351
|
+
* width of the inset. Used by pages that already structure their
|
|
352
|
+
* own internal columns — OpenAPI endpoint pages composing
|
|
353
|
+
* `<ApiLayout>`, generated reference pages with side-by-side
|
|
354
|
+
* description + code panels, etc.
|
|
355
|
+
*
|
|
356
|
+
* format-astro sets this automatically when a page's tree is
|
|
357
|
+
* dominated by an `endpoint` TreeNode; manual consumers can opt
|
|
358
|
+
* in per-page.
|
|
359
|
+
*/
|
|
360
|
+
wideLayout?: boolean;
|
|
361
|
+
class?: string;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const {
|
|
365
|
+
siteName,
|
|
366
|
+
title,
|
|
367
|
+
siteUrl = "/",
|
|
368
|
+
nav,
|
|
369
|
+
navGroups,
|
|
370
|
+
headings = [],
|
|
371
|
+
prev,
|
|
372
|
+
next,
|
|
373
|
+
repoUrl,
|
|
374
|
+
description,
|
|
375
|
+
siteDescription,
|
|
376
|
+
editUrl,
|
|
377
|
+
lastUpdated,
|
|
378
|
+
copyright,
|
|
379
|
+
favicon = "/favicon.ico",
|
|
380
|
+
ogImage,
|
|
381
|
+
defaultOgImage,
|
|
382
|
+
ogType = "article",
|
|
383
|
+
canonicalUrl,
|
|
384
|
+
twitterHandle,
|
|
385
|
+
themeColor,
|
|
386
|
+
noindex,
|
|
387
|
+
excludeFromSearch,
|
|
388
|
+
plausibleDomain,
|
|
389
|
+
plausibleScriptUrl,
|
|
390
|
+
hideSearch = false,
|
|
391
|
+
pagefindUrl = "/pagefind/",
|
|
392
|
+
mdMirror = false,
|
|
393
|
+
tags,
|
|
394
|
+
tagsIndexPath = "/tags",
|
|
395
|
+
tagPrefixes,
|
|
396
|
+
tagLabels,
|
|
397
|
+
status,
|
|
398
|
+
pageType,
|
|
399
|
+
audience,
|
|
400
|
+
category,
|
|
401
|
+
taxonomyIndexPaths,
|
|
402
|
+
taxonomyDisplay,
|
|
403
|
+
autoH1,
|
|
404
|
+
autoLede,
|
|
405
|
+
llmActions,
|
|
406
|
+
customJsonLd,
|
|
407
|
+
multiSource,
|
|
408
|
+
switcherMap,
|
|
409
|
+
basePath,
|
|
410
|
+
wideLayout = false,
|
|
411
|
+
class: className,
|
|
412
|
+
} = Astro.props;
|
|
413
|
+
|
|
414
|
+
// Resolve LLM action visibility + placement once. The component
|
|
415
|
+
// guards against missing markdownBody / mdUrl internally, but we
|
|
416
|
+
// also gate at the layout level so the slots stay empty when
|
|
417
|
+
// disabled — keeps the markup lean for the common no-config case.
|
|
418
|
+
const llmActionsEnabled =
|
|
419
|
+
!!llmActions
|
|
420
|
+
&& llmActions.enabled !== false
|
|
421
|
+
&& typeof llmActions.mdUrl === "string"
|
|
422
|
+
&& llmActions.mdUrl.length > 0;
|
|
423
|
+
const llmActionsPlacement = llmActions?.placement ?? "header";
|
|
424
|
+
const showLlmActionsHeader =
|
|
425
|
+
llmActionsEnabled
|
|
426
|
+
&& (llmActionsPlacement === "header" || llmActionsPlacement === "both");
|
|
427
|
+
const showLlmActionsInline =
|
|
428
|
+
llmActionsEnabled
|
|
429
|
+
&& (llmActionsPlacement === "inline" || llmActionsPlacement === "both");
|
|
430
|
+
const llmFooterLink = !!llmActions && llmActions.footerLink !== false;
|
|
431
|
+
|
|
432
|
+
// Compute href targets for the type / status badges. A field is
|
|
433
|
+
// linkable only when (a) the user declared a `taxonomies.<field>`
|
|
434
|
+
// block (so an indexPath flows through `taxonomyIndexPaths`) and
|
|
435
|
+
// (b) the page has a value for it. Otherwise the badge stays
|
|
436
|
+
// unlinked.
|
|
437
|
+
const typeBadgeHref = (pageType && taxonomyIndexPaths?.type)
|
|
438
|
+
? `${taxonomyIndexPaths.type.replace(/\/$/, "")}/${pageType}/`
|
|
439
|
+
: undefined;
|
|
440
|
+
const statusBadgeHref = (status && status !== "stable" && taxonomyIndexPaths?.status)
|
|
441
|
+
? `${taxonomyIndexPaths.status.replace(/\/$/, "")}/${status}/`
|
|
442
|
+
: undefined;
|
|
443
|
+
|
|
444
|
+
const hasMetaStrip = (
|
|
445
|
+
(Array.isArray(tags) && tags.length > 0) ||
|
|
446
|
+
(status && status !== "stable") ||
|
|
447
|
+
(typeof pageType === "string" && pageType.length > 0)
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
const currentPath = Astro.url.pathname.replace(/\/$/, "") || "/";
|
|
451
|
+
|
|
452
|
+
// SEO computation
|
|
453
|
+
const metaDescription = description ?? siteDescription;
|
|
454
|
+
const metaOgImage = ogImage ?? defaultOgImage;
|
|
455
|
+
const isAbsoluteSiteUrl = /^https?:\/\//.test(siteUrl);
|
|
456
|
+
const computedCanonical = canonicalUrl
|
|
457
|
+
?? (isAbsoluteSiteUrl
|
|
458
|
+
? siteUrl.replace(/\/$/, "") + Astro.url.pathname
|
|
459
|
+
: undefined);
|
|
460
|
+
|
|
461
|
+
// Markdown mirror — append `.md` to the current path for the alternate link
|
|
462
|
+
const mdMirrorHref = mdMirror
|
|
463
|
+
? (Astro.url.pathname.replace(/\/$/, "") || "") + ".md"
|
|
464
|
+
: undefined;
|
|
465
|
+
|
|
466
|
+
// Tag keywords for HTML meta + JSON-LD. Slug-based identifiers
|
|
467
|
+
// (`difficulty/1`, `concept/a11y`) mean nothing to a search-engine
|
|
468
|
+
// indexer or social-card preview; the human label is the unit
|
|
469
|
+
// machines should consume. The resolver applies `tagLabels`
|
|
470
|
+
// overrides when set, falls back to the leaf segment otherwise.
|
|
471
|
+
// Empty list when no tags — meta tags + JSON-LD silently elide.
|
|
472
|
+
const tagKeywords = resolveTagKeywords(tags, tagLabels);
|
|
473
|
+
|
|
474
|
+
// JSON-LD block. Shipped when ogType is "article" (the DocsLayout
|
|
475
|
+
// default) and at least one keyword is set, so empty-frontmatter
|
|
476
|
+
// pages don't ship a near-empty JSON blob. Schema.org's `keywords`
|
|
477
|
+
// is a comma-separated string by convention.
|
|
478
|
+
//
|
|
479
|
+
// `@type` is selected from `pageType` (sourced from `meta.type`):
|
|
480
|
+
// tutorial / how-to → HowTo; reference → TechArticle; course →
|
|
481
|
+
// Course; everything else → Article. Matches the conventions
|
|
482
|
+
// search engines key off for educational / tutorial / reference
|
|
483
|
+
// SERP rendering. See `json-ld.ts` for the full mapping table.
|
|
484
|
+
const articleJsonLd = (ogType === "article" && tagKeywords.length > 0)
|
|
485
|
+
? {
|
|
486
|
+
"@context": "https://schema.org",
|
|
487
|
+
"@type": jsonLdTypeFor(pageType),
|
|
488
|
+
headline: title,
|
|
489
|
+
keywords: tagKeywords.join(", "),
|
|
490
|
+
...(metaDescription ? { description: metaDescription } : {}),
|
|
491
|
+
...(metaOgImage ? { image: metaOgImage } : {}),
|
|
492
|
+
...(computedCanonical ? { url: computedCanonical } : {}),
|
|
493
|
+
}
|
|
494
|
+
: undefined;
|
|
495
|
+
|
|
496
|
+
// `customJsonLd` accepts either a single object or an array.
|
|
497
|
+
// Normalize to an array so we can iterate uniformly. Empty array
|
|
498
|
+
// when not set so the template can `.map` safely.
|
|
499
|
+
const customJsonLdBlocks = normalizeCustomJsonLd(customJsonLd);
|
|
500
|
+
|
|
501
|
+
// Default icon for the site logo
|
|
502
|
+
const siteIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"/></svg>';
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
<!doctype html>
|
|
506
|
+
<html lang="en">
|
|
507
|
+
<head>
|
|
508
|
+
<meta charset="UTF-8" />
|
|
509
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
510
|
+
<title>{title} | {siteName}</title>
|
|
511
|
+
|
|
512
|
+
{metaDescription && <meta name="description" content={metaDescription} />}
|
|
513
|
+
{favicon !== false && <link rel="icon" href={favicon} />}
|
|
514
|
+
{themeColor && <meta name="theme-color" content={themeColor} />}
|
|
515
|
+
{/* External search engine directive — orthogonal to in-site
|
|
516
|
+
Pagefind exclusion. */}
|
|
517
|
+
{noindex && <meta name="robots" content="noindex, nofollow" />}
|
|
518
|
+
|
|
519
|
+
{/* In-site Pagefind exclusion is wired via two coordinated
|
|
520
|
+
attributes on <body> and <main> below — see the prop docs
|
|
521
|
+
on `excludeFromSearch` for why both halves are needed. No
|
|
522
|
+
meta-tag signal here: <meta name="pagefind-exclude"> is
|
|
523
|
+
not in Pagefind's documented surface (the `data-pagefind-*`
|
|
524
|
+
attributes are), so emitting it would be cargo-culting. */}
|
|
525
|
+
|
|
526
|
+
{/* Pagefind facet filters live inside <main data-pagefind-body>
|
|
527
|
+
below — keeps them inside the indexed scope while excluding
|
|
528
|
+
sidebar / header / TOC text from per-page excerpts. See main. */}
|
|
529
|
+
|
|
530
|
+
{computedCanonical && <link rel="canonical" href={computedCanonical} />}
|
|
531
|
+
{mdMirrorHref && <link rel="alternate" type="text/markdown" href={mdMirrorHref} />}
|
|
532
|
+
|
|
533
|
+
{/* Open Graph */}
|
|
534
|
+
<meta property="og:type" content={ogType} />
|
|
535
|
+
<meta property="og:title" content={title} />
|
|
536
|
+
{metaDescription && <meta property="og:description" content={metaDescription} />}
|
|
537
|
+
<meta property="og:site_name" content={siteName} />
|
|
538
|
+
{computedCanonical && <meta property="og:url" content={computedCanonical} />}
|
|
539
|
+
{metaOgImage && <meta property="og:image" content={metaOgImage} />}
|
|
540
|
+
{/* OpenGraph article tags — one per resolved keyword. Used by
|
|
541
|
+
FB / LinkedIn / news aggregators to surface topic context.
|
|
542
|
+
Skipped when ogType is not "article" or no tags are set. */}
|
|
543
|
+
{ogType === "article" && tagKeywords.map((keyword) => (
|
|
544
|
+
<meta property="article:tag" content={keyword} />
|
|
545
|
+
))}
|
|
546
|
+
|
|
547
|
+
{/* JSON-LD primary block — Article / HowTo / TechArticle /
|
|
548
|
+
Course depending on `pageType`. Emits keywords as a
|
|
549
|
+
Schema.org keywords string; Google reads this for topical
|
|
550
|
+
categorization in rich results. */}
|
|
551
|
+
{articleJsonLd && (
|
|
552
|
+
<script type="application/ld+json" set:html={JSON.stringify(articleJsonLd)} />
|
|
553
|
+
)}
|
|
554
|
+
|
|
555
|
+
{/* Per-page / per-site custom structured data. Layered on
|
|
556
|
+
top of the auto-emitted block so writers can describe
|
|
557
|
+
Course / Person / Organization / BreadcrumbList without
|
|
558
|
+
forking the layout. */}
|
|
559
|
+
{customJsonLdBlocks.map((block) => (
|
|
560
|
+
<script type="application/ld+json" set:html={JSON.stringify(block)} />
|
|
561
|
+
))}
|
|
562
|
+
|
|
563
|
+
{/* Twitter / X */}
|
|
564
|
+
<meta name="twitter:card" content={metaOgImage ? "summary_large_image" : "summary"} />
|
|
565
|
+
<meta name="twitter:title" content={title} />
|
|
566
|
+
{metaDescription && <meta name="twitter:description" content={metaDescription} />}
|
|
567
|
+
{metaOgImage && <meta name="twitter:image" content={metaOgImage} />}
|
|
568
|
+
{twitterHandle && <meta name="twitter:site" content={twitterHandle} />}
|
|
569
|
+
|
|
570
|
+
{plausibleDomain && (
|
|
571
|
+
<script
|
|
572
|
+
is:inline
|
|
573
|
+
defer
|
|
574
|
+
data-domain={plausibleDomain}
|
|
575
|
+
src={plausibleScriptUrl ?? "https://plausible.io/js/script.js"}
|
|
576
|
+
></script>
|
|
577
|
+
)}
|
|
578
|
+
|
|
579
|
+
<script is:inline>
|
|
580
|
+
if (localStorage.getItem("theme") === "dark") {
|
|
581
|
+
document.documentElement.classList.add("dark");
|
|
582
|
+
}
|
|
583
|
+
</script>
|
|
584
|
+
<slot name="head" />
|
|
585
|
+
</head>
|
|
586
|
+
<body
|
|
587
|
+
class="bg-background text-foreground antialiased"
|
|
588
|
+
data-pagefind-ignore={excludeFromSearch ? "" : undefined}
|
|
589
|
+
>
|
|
590
|
+
<SidebarProvider>
|
|
591
|
+
<Sidebar collapsible="icon">
|
|
592
|
+
<SidebarHeader>
|
|
593
|
+
<SidebarMenu>
|
|
594
|
+
<SidebarMenuItem>
|
|
595
|
+
<SidebarMenuButton size="lg" href={siteUrl} isActive={currentPath === siteUrl || currentPath === "/"}>
|
|
596
|
+
<div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
|
597
|
+
<Fragment set:html={siteIcon} />
|
|
598
|
+
</div>
|
|
599
|
+
<div class="grid flex-1 text-left text-sm leading-tight">
|
|
600
|
+
<span class="truncate font-semibold">{siteName}</span>
|
|
601
|
+
<span class="truncate text-xs text-sidebar-foreground/70">Documentation</span>
|
|
602
|
+
</div>
|
|
603
|
+
</SidebarMenuButton>
|
|
604
|
+
</SidebarMenuItem>
|
|
605
|
+
</SidebarMenu>
|
|
606
|
+
</SidebarHeader>
|
|
607
|
+
|
|
608
|
+
<SidebarSeparator />
|
|
609
|
+
|
|
610
|
+
<SidebarContent>
|
|
611
|
+
{navGroups ? (
|
|
612
|
+
navGroups.map(group => (
|
|
613
|
+
<SidebarGroup>
|
|
614
|
+
<SidebarGroupLabel>{group.label}</SidebarGroupLabel>
|
|
615
|
+
<SidebarGroupContent>
|
|
616
|
+
<SidebarNavTree
|
|
617
|
+
items={filterNavByAxis(group.items, {
|
|
618
|
+
basePath: basePath ?? "/docs",
|
|
619
|
+
version: multiSource?.version,
|
|
620
|
+
locale: multiSource?.locale,
|
|
621
|
+
})}
|
|
622
|
+
currentPath={currentPath}
|
|
623
|
+
/>
|
|
624
|
+
</SidebarGroupContent>
|
|
625
|
+
</SidebarGroup>
|
|
626
|
+
))
|
|
627
|
+
) : (
|
|
628
|
+
<SidebarGroup>
|
|
629
|
+
<SidebarGroupContent>
|
|
630
|
+
<SidebarNavTree
|
|
631
|
+
items={filterNavByAxis(nav, {
|
|
632
|
+
basePath: basePath ?? "/docs",
|
|
633
|
+
version: multiSource?.version,
|
|
634
|
+
locale: multiSource?.locale,
|
|
635
|
+
})}
|
|
636
|
+
currentPath={currentPath}
|
|
637
|
+
/>
|
|
638
|
+
</SidebarGroupContent>
|
|
639
|
+
</SidebarGroup>
|
|
640
|
+
)}
|
|
641
|
+
</SidebarContent>
|
|
642
|
+
|
|
643
|
+
<SidebarRail />
|
|
644
|
+
</Sidebar>
|
|
645
|
+
|
|
646
|
+
<SidebarInset>
|
|
647
|
+
<header class="sticky top-0 z-40 flex h-12 shrink-0 items-center gap-2 border-b bg-background/95 px-4 backdrop-blur-sm supports-[backdrop-filter]:bg-background/60">
|
|
648
|
+
<SidebarTrigger class="-ml-1" />
|
|
649
|
+
<Separator orientation="vertical" class="mr-2 h-4" />
|
|
650
|
+
<span class="text-sm text-muted-foreground" data-page-title>{title}</span>
|
|
651
|
+
<div class="ml-auto flex items-center gap-2">
|
|
652
|
+
<slot name="header" />
|
|
653
|
+
{switcherMap && multiSource && (
|
|
654
|
+
<LocaleSwitcher
|
|
655
|
+
switcherMap={switcherMap}
|
|
656
|
+
multiSource={multiSource}
|
|
657
|
+
basePath={basePath ?? "/docs"}
|
|
658
|
+
/>
|
|
659
|
+
)}
|
|
660
|
+
{switcherMap && multiSource && (
|
|
661
|
+
<VersionSwitcher
|
|
662
|
+
switcherMap={switcherMap}
|
|
663
|
+
multiSource={multiSource}
|
|
664
|
+
basePath={basePath ?? "/docs"}
|
|
665
|
+
/>
|
|
666
|
+
)}
|
|
667
|
+
{showLlmActionsHeader && (
|
|
668
|
+
<PageActions
|
|
669
|
+
markdownBody={llmActions!.markdownBody}
|
|
670
|
+
mdUrl={llmActions!.mdUrl!}
|
|
671
|
+
providers={llmActions!.providers}
|
|
672
|
+
copyButton={llmActions!.copyButton}
|
|
673
|
+
promptTemplate={llmActions!.promptTemplate}
|
|
674
|
+
/>
|
|
675
|
+
)}
|
|
676
|
+
{!hideSearch && (
|
|
677
|
+
<SearchDialog
|
|
678
|
+
pagefindUrl={pagefindUrl}
|
|
679
|
+
taxonomyDisplay={taxonomyDisplay}
|
|
680
|
+
/>
|
|
681
|
+
)}
|
|
682
|
+
{repoUrl && (
|
|
683
|
+
<a href={repoUrl} target="_blank" rel="noopener" class="rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-foreground" aria-label="Source repository">
|
|
684
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
|
|
685
|
+
</a>
|
|
686
|
+
)}
|
|
687
|
+
<ThemeToggle />
|
|
688
|
+
</div>
|
|
689
|
+
</header>
|
|
690
|
+
|
|
691
|
+
<div class:list={["flex min-w-0", className]}>
|
|
692
|
+
<main
|
|
693
|
+
class="min-w-0 flex-1 px-6 py-8 lg:px-12"
|
|
694
|
+
data-pagefind-body={excludeFromSearch ? undefined : ""}
|
|
695
|
+
>
|
|
696
|
+
{/*
|
|
697
|
+
Pagefind facet filter elements. Placed inside the
|
|
698
|
+
`data-pagefind-body` scope so Pagefind discovers
|
|
699
|
+
them while indexing. (When `excludeFromSearch` is
|
|
700
|
+
true the body element is omitted, but so is the
|
|
701
|
+
entire indexing of this page — these elements are
|
|
702
|
+
effectively dead too, which is correct for excluded
|
|
703
|
+
pages.) Moved here from the page-level <body>
|
|
704
|
+
after we discovered every page's excerpt was
|
|
705
|
+
just sidebar nav text (see DocsLayout history). The
|
|
706
|
+
data-pagefind-body attribute on <main> tells Pagefind
|
|
707
|
+
to ignore everything outside this element (sidebar,
|
|
708
|
+
header, TOC, footer), so per-page excerpts come from
|
|
709
|
+
the actual page content. Filter elements live in here
|
|
710
|
+
now to stay inside that scope.
|
|
711
|
+
*/}
|
|
712
|
+
{Array.isArray(tags) && tags.map((tag) => (
|
|
713
|
+
<div hidden data-pagefind-filter={`tag:${tag}`}></div>
|
|
714
|
+
))}
|
|
715
|
+
{Array.isArray(audience) && audience.map((value) => (
|
|
716
|
+
<div hidden data-pagefind-filter={`audience:${value}`}></div>
|
|
717
|
+
))}
|
|
718
|
+
{Array.isArray(category) && category.map((value) => (
|
|
719
|
+
<div hidden data-pagefind-filter={`category:${value}`}></div>
|
|
720
|
+
))}
|
|
721
|
+
{status && <div hidden data-pagefind-filter={`status:${status}`}></div>}
|
|
722
|
+
{pageType && <div hidden data-pagefind-filter={`type:${pageType}`}></div>}
|
|
723
|
+
|
|
724
|
+
<div class:list={["mx-auto", wideLayout ? "max-w-7xl" : "max-w-3xl"]}>
|
|
725
|
+
{/*
|
|
726
|
+
Inline page-actions cluster — sits below breadcrumb
|
|
727
|
+
/ above auto-H1 so it reads as "actions for this
|
|
728
|
+
page" rather than "actions for the site". Header
|
|
729
|
+
placement is configured separately; both can be
|
|
730
|
+
active for sites that want sticky-header + inline.
|
|
731
|
+
See plans/llm-page-actions.md.
|
|
732
|
+
*/}
|
|
733
|
+
{showLlmActionsInline && (
|
|
734
|
+
<div class="mb-4 flex justify-end" data-page-actions-inline>
|
|
735
|
+
<PageActions
|
|
736
|
+
markdownBody={llmActions!.markdownBody}
|
|
737
|
+
mdUrl={llmActions!.mdUrl!}
|
|
738
|
+
providers={llmActions!.providers}
|
|
739
|
+
copyButton={llmActions!.copyButton}
|
|
740
|
+
promptTemplate={llmActions!.promptTemplate}
|
|
741
|
+
/>
|
|
742
|
+
</div>
|
|
743
|
+
)}
|
|
744
|
+
|
|
745
|
+
{/*
|
|
746
|
+
Auto-rendered H1 + lede paragraph. format-astro
|
|
747
|
+
computes whether to inject these per page via
|
|
748
|
+
`detectLeadingNodes(page.tree)` — true when the
|
|
749
|
+
markdown body doesn't already provide them. Sites
|
|
750
|
+
with markdown-first authoring (`# Title\nLede.`)
|
|
751
|
+
see no change. See plans/auto-lede.md.
|
|
752
|
+
|
|
753
|
+
Order: H1 → lede → meta strip → page body. Reader
|
|
754
|
+
gets context (title, summary) before classification
|
|
755
|
+
(chips), then content.
|
|
756
|
+
*/}
|
|
757
|
+
{autoH1 && (
|
|
758
|
+
<h1 class="text-3xl font-bold tracking-tight mb-2">
|
|
759
|
+
{title}
|
|
760
|
+
</h1>
|
|
761
|
+
)}
|
|
762
|
+
{autoLede && description && (
|
|
763
|
+
<p class="text-lg text-muted-foreground mb-6">
|
|
764
|
+
{description}
|
|
765
|
+
</p>
|
|
766
|
+
)}
|
|
767
|
+
|
|
768
|
+
{hasMetaStrip && (
|
|
769
|
+
<div
|
|
770
|
+
class="not-prose mb-6 flex flex-wrap items-center gap-2"
|
|
771
|
+
data-page-meta-strip
|
|
772
|
+
data-pagefind-ignore
|
|
773
|
+
>
|
|
774
|
+
{/*
|
|
775
|
+
data-pagefind-ignore: chip text ("How-to",
|
|
776
|
+
"Concept: Accessibility", etc.) is decorative —
|
|
777
|
+
the same data lives in `data-pagefind-filter`
|
|
778
|
+
elements above, structured for facets. Without
|
|
779
|
+
this attribute, every page's search-result
|
|
780
|
+
excerpt picked up "how to Concept:Accessibility"
|
|
781
|
+
as its leading text, drowning out actual prose.
|
|
782
|
+
*/}
|
|
783
|
+
{pageType && <TypeBadge type={pageType} href={typeBadgeHref} />}
|
|
784
|
+
{status && <StatusBadge status={status} href={statusBadgeHref} />}
|
|
785
|
+
{tags && tags.length > 0 && (
|
|
786
|
+
<TagList
|
|
787
|
+
tags={tags}
|
|
788
|
+
indexPath={tagsIndexPath}
|
|
789
|
+
prefixes={tagPrefixes}
|
|
790
|
+
labels={tagLabels}
|
|
791
|
+
/>
|
|
792
|
+
)}
|
|
793
|
+
</div>
|
|
794
|
+
)}
|
|
795
|
+
|
|
796
|
+
<slot />
|
|
797
|
+
|
|
798
|
+
<DocsFooter
|
|
799
|
+
editUrl={editUrl}
|
|
800
|
+
lastUpdated={lastUpdated}
|
|
801
|
+
prev={prev}
|
|
802
|
+
next={next}
|
|
803
|
+
copyright={copyright}
|
|
804
|
+
llmsLink={llmFooterLink}
|
|
805
|
+
/>
|
|
806
|
+
</div>
|
|
807
|
+
</main>
|
|
808
|
+
|
|
809
|
+
{headings.length > 0 && !wideLayout && (
|
|
810
|
+
<aside class="sticky top-12 hidden h-[calc(100vh-3rem)] w-56 shrink-0 overflow-y-auto border-l p-4 lg:block">
|
|
811
|
+
<DocsToc headings={headings} />
|
|
812
|
+
</aside>
|
|
813
|
+
)}
|
|
814
|
+
</div>
|
|
815
|
+
</SidebarInset>
|
|
816
|
+
</SidebarProvider>
|
|
817
|
+
</body>
|
|
818
|
+
</html>
|
|
819
|
+
|
|
820
|
+
<script>
|
|
821
|
+
import "@dogsbay/ui/sidebar/sidebar.ts";
|
|
822
|
+
|
|
823
|
+
function updateActiveNav() {
|
|
824
|
+
const path = window.location.pathname.replace(/\/$/, "") || "/";
|
|
825
|
+
|
|
826
|
+
document.querySelectorAll("[data-nav-href]").forEach((el) => {
|
|
827
|
+
const href = el.getAttribute("data-nav-href");
|
|
828
|
+
const isActive = href === path;
|
|
829
|
+
if (isActive) {
|
|
830
|
+
el.setAttribute("data-active", "");
|
|
831
|
+
el.classList.add("bg-sidebar-accent", "font-medium", "text-sidebar-accent-foreground");
|
|
832
|
+
} else {
|
|
833
|
+
el.removeAttribute("data-active");
|
|
834
|
+
el.classList.remove("bg-sidebar-accent", "font-medium", "text-sidebar-accent-foreground");
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
// Open ancestor details for active item
|
|
839
|
+
const activeLink = document.querySelector(`[data-nav-href="${path}"]`);
|
|
840
|
+
if (activeLink) {
|
|
841
|
+
let parent = activeLink.parentElement;
|
|
842
|
+
while (parent) {
|
|
843
|
+
if (parent.tagName === "DETAILS" && !parent.hasAttribute("open")) {
|
|
844
|
+
parent.setAttribute("open", "");
|
|
845
|
+
}
|
|
846
|
+
parent = parent.parentElement;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Update page title in header
|
|
851
|
+
const titleEl = document.querySelector("[data-page-title]");
|
|
852
|
+
if (titleEl) {
|
|
853
|
+
titleEl.textContent = document.title.split(" | ")[0];
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Handle branch items with href — clicking label navigates, chevron toggles
|
|
858
|
+
function setupBranchLinks() {
|
|
859
|
+
document.querySelectorAll("summary[data-nav-href]").forEach((summary) => {
|
|
860
|
+
summary.addEventListener("click", (e) => {
|
|
861
|
+
const target = e.target as HTMLElement;
|
|
862
|
+
if (target.closest("[data-chevron]")) return;
|
|
863
|
+
|
|
864
|
+
const href = summary.getAttribute("data-nav-href");
|
|
865
|
+
if (!href) return;
|
|
866
|
+
const path = window.location.pathname.replace(/\/$/, "") || "/";
|
|
867
|
+
if (path === href) return;
|
|
868
|
+
|
|
869
|
+
e.preventDefault();
|
|
870
|
+
const details = summary.closest("details");
|
|
871
|
+
if (details) details.open = true;
|
|
872
|
+
window.location.href = href;
|
|
873
|
+
});
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
setupBranchLinks();
|
|
878
|
+
updateActiveNav();
|
|
879
|
+
|
|
880
|
+
document.addEventListener("astro:after-swap", () => {
|
|
881
|
+
setupBranchLinks();
|
|
882
|
+
updateActiveNav();
|
|
883
|
+
});
|
|
884
|
+
</script>
|