From f45f1434b1a81c6b199b1153d7c600d4c2061e53 Mon Sep 17 00:00:00 2001
From: Bissbert <43237892+Bissbert@users.noreply.github.com>
Date: Thu, 16 Jul 2026 20:36:35 +0200
Subject: [PATCH] feat(docs): persistent sidebar + in-page TOC, consistent
prev/next, corrected API references
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- added a persistent grouped left sidebar to all 12 docs pages (nav.ts single source of truth, active-page highlight, sticky rail on desktop, mobile disclosure)
- in-page TOC with scroll-spy on long reference pages (CDL spec, cli-options, components, 4 API pages) via a heading-scanning DocsTOC (desktop sticky rail + mobile bottom sheet)
- consistent PageNav prev/next on every page in reading order (previously only on API pages)
- new DocsLayout wraps every page
- corrected API-doc drift against real package sources: crystal-renderer 1.0.1→2.0.0, mineral-database 2.2.0→2.3.0 + 140→216 presets + valid preset ids (quartz-prism/diamond-octahedron), crystal-geometry field rename + radians units + newly-documented public API, cdl-parser stale repr/type examples
- CDL Examples page reworked for the playground-disabled state (copy-to-clipboard per snippet, syntax-reference links into the spec, honest "coming back" callout) with the playground path preserved to auto-restore when re-enabled
- both sticky rails measured pinned through deep scroll in light+dark
- 502 tests, browser-verified
---
src/components/docs/CopyCodeButton.astro | 134 ++++++++
src/components/docs/DocsLayout.astro | 93 ++++++
src/components/docs/DocsSidebar.astro | 113 +++++++
src/components/docs/DocsTOC.astro | 365 ++++++++++++++++++++++
src/components/docs/index.ts | 3 +
src/lib/docs/nav.ts | 138 ++++++++
src/pages/docs/api/cdl-parser.astro | 75 ++---
src/pages/docs/api/crystal-geometry.astro | 149 ++++++---
src/pages/docs/api/crystal-renderer.astro | 24 +-
src/pages/docs/api/mineral-database.astro | 61 ++--
src/pages/docs/cdl-examples.astro | 102 ++++--
src/pages/docs/cdl.astro | 17 +-
src/pages/docs/cli-options.astro | 17 +-
src/pages/docs/cli.astro | 16 +-
src/pages/docs/components.astro | 11 +-
src/pages/docs/index.astro | 53 +---
src/pages/docs/installation.astro | 16 +-
src/pages/docs/quickstart.astro | 16 +-
18 files changed, 1125 insertions(+), 278 deletions(-)
create mode 100644 src/components/docs/CopyCodeButton.astro
create mode 100644 src/components/docs/DocsLayout.astro
create mode 100644 src/components/docs/DocsSidebar.astro
create mode 100644 src/components/docs/DocsTOC.astro
create mode 100644 src/lib/docs/nav.ts
diff --git a/src/components/docs/CopyCodeButton.astro b/src/components/docs/CopyCodeButton.astro
new file mode 100644
index 0000000..c3fc92d
--- /dev/null
+++ b/src/components/docs/CopyCodeButton.astro
@@ -0,0 +1,134 @@
+---
+/**
+ * CopyCodeButton - framework-free "copy to clipboard" affordance for inline
+ * CDL/code snippets.
+ *
+ * Renders a small icon button that copies its `code` prop to the clipboard
+ * and swaps to a checkmark + "Copied" state briefly on success. Follows the
+ * same pattern as ThemeToggle.astro: a single delegated click listener bound
+ * once per page (guarded by a `window` flag), so this can be rendered dozens
+ * of times on one page (e.g. one per example card) without adding dozens of
+ * listeners.
+ */
+interface Props {
+ code: string;
+ label?: string;
+ class?: string;
+}
+
+const { code, label = 'Copy CDL to clipboard', class: className = '' } = Astro.props;
+---
+
+
+
+
+
+
diff --git a/src/components/docs/DocsLayout.astro b/src/components/docs/DocsLayout.astro
new file mode 100644
index 0000000..7011a5a
--- /dev/null
+++ b/src/components/docs/DocsLayout.astro
@@ -0,0 +1,93 @@
+---
+/**
+ * DocsLayout - shared chrome for every page under `/docs`.
+ *
+ * Wraps `BaseLayout` and adds the persistent `DocsSidebar`, an automatic
+ * `Breadcrumb` + `PageNav` (both derived from the single nav tree in
+ * `src/lib/docs/nav.ts` for the current `Astro.url.pathname`), and
+ * optionally `DocsTOC` for long reference pages.
+ *
+ * Usage - replace a page's `` +
+ * its own ``/``/`` with:
+ *
+ *
+ * ...page content, unchanged...
+ *
+ *
+ * Pass `showToc` for the long reference pages (CDL spec, CLI options,
+ * Components, the 4 API pages) to render `DocsTOC` in a third rail at xl+.
+ * Pass `wide` for pages whose content is a card grid / component gallery
+ * rather than a prose article (index, components) so the content column
+ * isn't capped to reading width.
+ *
+ * Layout/responsive approach:
+ * - Mobile (`
+ * disclosure inline above the content; `DocsTOC` (if present) renders
+ * its own floating button + bottom sheet, independent of scroll
+ * position.
+ * - lg+: two-column CSS grid, `[240px_minmax(0,1fr)]` - sidebar rail then
+ * content. Both the sidebar's and (at xl+, see below) the TOC's grid
+ * cells get `lg:self-stretch`/`xl:self-stretch` so the cell spans the
+ * full row height. This is required for `position: sticky` to have any
+ * travel room - a cell that shrink-wraps its sticky child's own height
+ * gives the sticky rule nothing to do, which was the exact bug just
+ * fixed on `/learn`. Verified by measuring the sidebar's
+ * `getBoundingClientRect()` after a ~1500px scroll (see verification
+ * notes in the PR/commit for this change).
+ * - xl+ (only when `showToc`): a third `240px` column is added for
+ * `DocsTOC`, so long pages become sidebar | content | TOC.
+ */
+import BaseLayout from '../../layouts/BaseLayout.astro';
+import DocsSidebar from './DocsSidebar.astro';
+import DocsTOC from './DocsTOC.astro';
+import Breadcrumb from './Breadcrumb.astro';
+import PageNav from './PageNav.astro';
+import { Container } from '../ui-astro';
+import { getDocsBreadcrumb, getDocsPageNav } from '../../lib/docs/nav';
+
+interface Props {
+ title: string;
+ description?: string;
+ /** Render the in-page "On this page" TOC rail (long reference pages only). */
+ showToc?: boolean;
+ /** Card-grid/gallery pages that want the full content-column width instead of a reading-width cap. */
+ wide?: boolean;
+}
+
+const { title, description, showToc = false, wide = false } = Astro.props;
+
+const pathname = Astro.url.pathname;
+const breadcrumbItems = getDocsBreadcrumb(pathname);
+const { prev, next } = getDocsPageNav(pathname);
+---
+
+
+
+
+
+
+
+
+
+ {breadcrumbItems && }
+
+
+
+
+
+
+
+
+ {showToc && (
+
+
+
+ )}
+
+
+
diff --git a/src/components/docs/DocsSidebar.astro b/src/components/docs/DocsSidebar.astro
new file mode 100644
index 0000000..c35bfe4
--- /dev/null
+++ b/src/components/docs/DocsSidebar.astro
@@ -0,0 +1,113 @@
+---
+/**
+ * DocsSidebar - persistent cross-page navigation for the /docs section.
+ *
+ * Renders the grouped nav tree from `src/lib/docs/nav.ts` (single source of
+ * truth, shared with the flat prev/next ordering and breadcrumb lookup).
+ *
+ * Two variants, each responsible for hiding itself at the "wrong" breakpoint
+ * (mirrors the pattern in `src/components/learn/TableOfContents.astro`):
+ * - Mobile (` "Documentation menu" disclosure sitting
+ * inline at the top of the content column. A `` (rather than
+ * the floating-button+dialog pattern also used on this site for
+ * `DocsTOC`) was chosen deliberately: this is a full page tree, not a
+ * handful of in-page anchors, and long docs pages already get a floating
+ * "On this page" button from `DocsTOC` — a second floating trigger on
+ * the same screen would be redundant and visually competing. A simple
+ * inline disclosure avoids that collision entirely.
+ * - Desktop (lg+): a sticky aside rail. `DocsLayout.astro` places this
+ * component's wrapper in a grid cell with `lg:self-stretch` so the cell
+ * spans the full row height - without that, `position: sticky` has no
+ * room to travel and the rail scrolls off with the page (the bug fixed
+ * in `learn/TableOfContents.astro`; do not reintroduce it here).
+ */
+import { docsNav, isDocsNavActive } from '../../lib/docs/nav';
+
+const pathname = Astro.url.pathname;
+---
+
+
+
+
+
+
+ Documentation menu
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/docs/DocsTOC.astro b/src/components/docs/DocsTOC.astro
new file mode 100644
index 0000000..8e1f0e6
--- /dev/null
+++ b/src/components/docs/DocsTOC.astro
@@ -0,0 +1,365 @@
+---
+/**
+ * DocsTOC - "On this page" navigation for long, hand-authored docs pages.
+ *
+ * Unlike `learn/TableOfContents.astro` (which receives structured `sections`
+ * data as a prop), docs pages are plain `.astro` files with hand-written
+ * `
`/`
` headings and no shared content schema. So this component
+ * builds its list entirely client-side: a script scans `contentSelector`
+ * for headings, assigns a stable slug id to any heading missing one, and
+ * renders the list + scroll-spy from that scan. There is no server-rendered
+ * fallback list - see the "why no build-time list" note in the script below.
+ *
+ * Rendered only by `DocsLayout.astro` when `showToc` is true (the long
+ * reference pages: CDL spec, CLI options, Components, and the 4 API pages).
+ *
+ * Mirrors the two-variant responsive split used by `learn/TableOfContents.astro`:
+ * - Desktop (lg+): sticky right rail with scroll-spy.
+ * - Mobile (`
+ * bottom sheet. This is deliberately the *same* mobile pattern as the
+ * learn TOC (not the `` pattern `DocsSidebar` uses for the docs
+ * nav tree) — the two mobile controls serve different purposes (jump to
+ * a heading vs. cross-page navigation) and don't overlap on screen, so
+ * reusing the proven learn pattern here keeps mobile TOC behaviour
+ * consistent across the whole site.
+ *
+ * Both variants are hidden (via a shared `.docs-toc-root` wrapper) until the
+ * scan finds at least 2 headings, so pages with too few headings to be worth
+ * a TOC render nothing.
+ *
+ * `#docs-toc-root` carries `xl:h-full` deliberately: unlike `DocsSidebar`
+ * (whose top-level elements are the ``/`
Describes an aggregate arrangement of multiple crystal individuals
- using the ~ operator.
+ using the ~ operator. AggregateSpec is itself a
+ FormNode that wraps the aggregated form — desc.forms[0]
+ is the AggregateSpec, not an attribute on CrystalForm.