@dogsbay/docs-layout 0.2.0-beta.84 → 0.2.0-beta.86

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dogsbay/docs-layout",
3
- "version": "0.2.0-beta.84",
3
+ "version": "0.2.0-beta.86",
4
4
  "description": "Standard documentation layout components for Dogsbay",
5
5
  "type": "module",
6
6
  "exports": {
@@ -29,8 +29,8 @@
29
29
  "./json-ld": "./src/json-ld.ts"
30
30
  },
31
31
  "dependencies": {
32
- "@dogsbay/ui": "0.2.0-beta.84",
33
- "@dogsbay/primitives": "0.2.0-beta.84"
32
+ "@dogsbay/ui": "0.2.0-beta.86",
33
+ "@dogsbay/primitives": "0.2.0-beta.86"
34
34
  },
35
35
  "devDependencies": {
36
36
  "vitest": "^3.0.0"
@@ -35,6 +35,7 @@ import DocsNavClient from "./DocsNavClient.astro";
35
35
  import Separator from "@dogsbay/ui/separator/Separator.astro";
36
36
  import ThemeToggle from "@dogsbay/ui/theme-toggle/ThemeToggle.astro";
37
37
  import DocsToc from "./DocsToc.astro";
38
+ import { resolveTocPlacement, type TocMode } from "./toc-placement.js";
38
39
  import DocsFooter from "./DocsFooter.astro";
39
40
  import SearchDialog from "./SearchDialog.astro";
40
41
  import TagList from "./TagList.astro";
@@ -404,6 +405,17 @@ interface Props {
404
405
  * in per-page.
405
406
  */
406
407
  wideLayout?: boolean;
408
+ /**
409
+ * Table-of-contents placement. Default `"top"`.
410
+ * - `"top"` — expandable "On this page" disclosure at the top of the
411
+ * article, identical on desktop and mobile; frees the right rail for the
412
+ * `right-rail` named slot (e.g. an Ask AI panel).
413
+ * - `"popover"` — an "On this page" dropdown in the header.
414
+ * - `"rail"` — the classic right-hand TOC sidebar (pre-`toc` behaviour).
415
+ * - `"off"` — no table of contents.
416
+ * See plans/ask-branch1-placement-toc.md.
417
+ */
418
+ toc?: TocMode;
407
419
  class?: string;
408
420
  }
409
421
 
@@ -414,6 +426,7 @@ const {
414
426
  nav,
415
427
  navGroups,
416
428
  headings = [],
429
+ toc = "top",
417
430
  prev,
418
431
  next,
419
432
  repoUrl,
@@ -512,6 +525,13 @@ const hasMetaStrip = (
512
525
  (typeof pageType === "string" && pageType.length > 0)
513
526
  );
514
527
 
528
+ // TOC placement: which container(s) + the right-rail plugin region render.
529
+ // Logic lives in toc-placement.ts so it's unit-tested; this file just consumes.
530
+ const tocPlacement = resolveTocPlacement(toc, {
531
+ hasHeadings: headings.length > 0,
532
+ wideLayout: !!wideLayout,
533
+ });
534
+
515
535
  const currentPath = Astro.url.pathname.replace(/\/$/, "") || "/";
516
536
 
517
537
  // SEO computation
@@ -766,6 +786,16 @@ const siteIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
766
786
  <span class="text-sm text-muted-foreground" data-page-title>{title}</span>
767
787
  <div class="ml-auto flex items-center gap-2">
768
788
  <slot name="header" />
789
+ {tocPlacement.popoverToc && (
790
+ <details class="dba-toc-popover relative" data-toc-popover>
791
+ <summary class="cursor-pointer list-none rounded-md px-2 py-1.5 text-sm text-muted-foreground hover:bg-accent hover:text-foreground">
792
+ On this page
793
+ </summary>
794
+ <div class="absolute right-0 z-50 mt-1 max-h-[70vh] w-64 overflow-y-auto rounded-md border bg-background p-3 shadow-md">
795
+ <DocsToc headings={headings} title="" />
796
+ </div>
797
+ </details>
798
+ )}
769
799
  {switcherMap && multiSource && (
770
800
  <LocaleSwitcher
771
801
  switcherMap={switcherMap}
@@ -945,6 +975,17 @@ const siteIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
945
975
  </div>
946
976
  )}
947
977
 
978
+ {tocPlacement.topToc && (
979
+ <details class="dba-toc-top not-prose mb-6 rounded-md border" data-toc-top>
980
+ <summary class="cursor-pointer list-none px-3 py-2 text-sm font-medium text-muted-foreground">
981
+ On this page
982
+ </summary>
983
+ <div class="border-t px-3 py-2">
984
+ <DocsToc headings={headings} title="" />
985
+ </div>
986
+ </details>
987
+ )}
988
+
948
989
  <slot />
949
990
 
950
991
  <DocsFooter
@@ -959,17 +1000,41 @@ const siteIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
959
1000
  </div>
960
1001
  </main>
961
1002
 
962
- {headings.length > 0 && !wideLayout && (
1003
+ {/* Classic right-hand TOC only in `toc: rail` mode. */}
1004
+ {tocPlacement.railToc && (
963
1005
  <aside class="sticky top-12 hidden h-[calc(100vh-3rem)] w-56 shrink-0 overflow-y-auto border-l p-4 lg:block">
964
1006
  <DocsToc headings={headings} />
965
1007
  </aside>
966
1008
  )}
1009
+ {/* Right-rail plugin region — host for the `right-rail` named slot
1010
+ (e.g. an Ask AI panel). Rendered in every non-rail mode and not
1011
+ gated on headings, so a plugin can dock on heading-less pages.
1012
+ Hidden when nothing fills it (see the empty-rail style below). */}
1013
+ {tocPlacement.regionRail && (
1014
+ <aside
1015
+ class="dba-right-rail sticky top-12 hidden h-[calc(100vh-3rem)] w-56 shrink-0 overflow-y-auto border-l p-4 lg:block"
1016
+ data-right-rail
1017
+ >
1018
+ <slot name="right-rail" />
1019
+ </aside>
1020
+ )}
967
1021
  </div>
968
1022
  </SidebarInset>
969
1023
  </SidebarProvider>
970
1024
  </body>
971
1025
  </html>
972
1026
 
1027
+ <style>
1028
+ /* The right-rail plugin region renders unconditionally in non-rail TOC
1029
+ modes so plugins (e.g. Ask AI) can dock into it. Until something fills
1030
+ the `right-rail` slot it has no element children — hide it so an empty
1031
+ bordered column doesn't show. (:has matches element children only; an
1032
+ unfilled Astro named slot renders none.) */
1033
+ aside[data-right-rail]:not(:has(*)) {
1034
+ display: none;
1035
+ }
1036
+ </style>
1037
+
973
1038
  <script>
974
1039
  import "@dogsbay/ui/sidebar/sidebar.ts";
975
1040
 
package/src/DocsToc.astro CHANGED
@@ -33,7 +33,7 @@ const filtered = headings.filter(h => h.depth >= minDepth && h.depth <= maxDepth
33
33
 
34
34
  {filtered.length > 0 && (
35
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>
36
+ {title && <div class="text-xs font-semibold uppercase text-muted-foreground">{title}</div>}
37
37
  <ul class="mt-2 space-y-1">
38
38
  {filtered.map(h => (
39
39
  <li>
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Decide which TOC container(s) and right-rail region `DocsLayout` renders,
3
+ * given the `toc` placement mode and per-page facts. Factored out of
4
+ * `DocsLayout.astro` so the branching logic is unit-testable (the .astro
5
+ * file just consumes the result) — same pattern as `nav-filter.ts` etc.
6
+ *
7
+ * See plans/ask-branch1-placement-toc.md.
8
+ */
9
+
10
+ export type TocMode = "top" | "popover" | "rail" | "off";
11
+
12
+ export interface TocPlacement {
13
+ /** Classic right-hand TOC sidebar (the pre-`toc`-option layout). */
14
+ railToc: boolean;
15
+ /** Expandable "On this page" disclosure at the top of the article. */
16
+ topToc: boolean;
17
+ /** "On this page" dropdown in the header. */
18
+ popoverToc: boolean;
19
+ /**
20
+ * The right-rail plugin region (host for the `right-rail` named slot, e.g.
21
+ * an Ask AI panel). Present whenever the rail isn't the classic TOC's, and
22
+ * deliberately NOT gated on headings — so a plugin can dock on heading-less
23
+ * pages. Never shown on wide/API pages (no rail there).
24
+ */
25
+ regionRail: boolean;
26
+ }
27
+
28
+ export interface TocPlacementInput {
29
+ /** Page has at least one heading to list. */
30
+ hasHeadings: boolean;
31
+ /** Wide/API layout — composes its own columns, so no right rail. */
32
+ wideLayout: boolean;
33
+ }
34
+
35
+ /**
36
+ * Resolve the placement flags. A TOC container only renders when there are
37
+ * headings to show; the region rail is independent of headings. `rail` and
38
+ * the region rail are mutually exclusive (they compete for the same column),
39
+ * and both are suppressed on wide pages.
40
+ */
41
+ export function resolveTocPlacement(
42
+ toc: TocMode,
43
+ { hasHeadings, wideLayout }: TocPlacementInput,
44
+ ): TocPlacement {
45
+ return {
46
+ railToc: toc === "rail" && hasHeadings && !wideLayout,
47
+ topToc: toc === "top" && hasHeadings,
48
+ popoverToc: toc === "popover" && hasHeadings,
49
+ regionRail: toc !== "rail" && !wideLayout,
50
+ };
51
+ }