@dreamlake/dockit 0.2.31 → 0.2.33

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.
@@ -0,0 +1,8 @@
1
+ import { ReactNode } from 'react';
2
+ /** Only the entry page is rendered on the server and indexed by Pagefind.
3
+ * Appended MDX modules retain their React components and interactions. */
4
+ export declare function ContinuousReading({ children, path, onActive, }: {
5
+ children: ReactNode;
6
+ path: string;
7
+ onActive: (path: string) => void;
8
+ }): import("react").JSX.Element;
@@ -0,0 +1,162 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { useState, useEffect, useRef, Component } from "react";
3
+ import { usePageContext, PageContextProvider } from "vike-react/usePageContext";
4
+ import { nextReadingPage, pageLoader, ReadingPageContext } from "../lib/reading.js";
5
+ class ArticleBoundary extends Component {
6
+ state = { failed: false };
7
+ static getDerivedStateFromError() {
8
+ return { failed: true };
9
+ }
10
+ render() {
11
+ return this.state.failed ? /* @__PURE__ */ jsxs("p", { role: "status", children: [
12
+ "This page could not be displayed here. ",
13
+ /* @__PURE__ */ jsx("a", { href: this.props.path, children: "Open the page" }),
14
+ "."
15
+ ] }) : this.props.children;
16
+ }
17
+ }
18
+ function ContinuousReading({
19
+ children,
20
+ path,
21
+ onActive
22
+ }) {
23
+ const context = usePageContext();
24
+ const [enabled, setEnabled] = useState(false);
25
+ const [ready, setReady] = useState(false);
26
+ useEffect(() => setReady(true), []);
27
+ const [articles, setArticles] = useState([]);
28
+ const [error, setError] = useState(false);
29
+ const [busy, setBusy] = useState(false);
30
+ const [attempt, setAttempt] = useState(0);
31
+ const sentinel = useRef(null);
32
+ const root = useRef(null);
33
+ const lastPath = articles.at(-1)?.path ?? path;
34
+ const next = nextReadingPage(lastPath);
35
+ useEffect(() => {
36
+ if (!enabled || !next || error) return;
37
+ const target = sentinel.current;
38
+ if (!target || typeof IntersectionObserver === "undefined") return;
39
+ let cancelled = false;
40
+ let started = false;
41
+ const observer = new IntersectionObserver(
42
+ (entries) => {
43
+ if (started || !entries.some((e) => e.isIntersecting)) return;
44
+ started = true;
45
+ setBusy(true);
46
+ pageLoader(next.path)().then((module) => {
47
+ if (!cancelled)
48
+ setArticles((items) => [...items, { path: next.path, Page: module.default }]);
49
+ }).catch(() => {
50
+ if (!cancelled) setError(true);
51
+ }).finally(() => {
52
+ if (!cancelled) setBusy(false);
53
+ });
54
+ },
55
+ { rootMargin: "0px 0px 240px 0px" }
56
+ );
57
+ observer.observe(target);
58
+ return () => {
59
+ cancelled = true;
60
+ observer.disconnect();
61
+ };
62
+ }, [enabled, next?.path, error, attempt]);
63
+ useEffect(() => {
64
+ if (!enabled) {
65
+ onActive(path);
66
+ return;
67
+ }
68
+ let frame = 0;
69
+ const update = () => {
70
+ frame = 0;
71
+ const sections = Array.from(
72
+ root.current?.querySelectorAll("[data-doc-page]") ?? []
73
+ );
74
+ const active = sections.filter((s) => s.getBoundingClientRect().top <= 130).at(-1) ?? sections[0];
75
+ if (active?.dataset.docPage) onActive(active.dataset.docPage);
76
+ };
77
+ const schedule = () => {
78
+ if (!frame) frame = requestAnimationFrame(update);
79
+ };
80
+ update();
81
+ window.addEventListener("scroll", schedule, { passive: true });
82
+ window.addEventListener("resize", schedule);
83
+ return () => {
84
+ cancelAnimationFrame(frame);
85
+ window.removeEventListener("scroll", schedule);
86
+ window.removeEventListener("resize", schedule);
87
+ };
88
+ }, [enabled, articles, onActive, path]);
89
+ return /* @__PURE__ */ jsxs("div", { ref: root, children: [
90
+ /* @__PURE__ */ jsxs("label", { className: "doc-reading-toggle", "data-pagefind-ignore": true, children: [
91
+ /* @__PURE__ */ jsx(
92
+ "input",
93
+ {
94
+ type: "checkbox",
95
+ disabled: !ready,
96
+ checked: enabled,
97
+ onChange: (e) => {
98
+ setEnabled(e.target.checked);
99
+ if (!e.target.checked) {
100
+ setArticles([]);
101
+ setBusy(false);
102
+ setError(false);
103
+ onActive(path);
104
+ }
105
+ }
106
+ }
107
+ ),
108
+ " ",
109
+ "Keep scrolling"
110
+ ] }),
111
+ /* @__PURE__ */ jsx("section", { "data-doc-page": path, children: /* @__PURE__ */ jsx(ReadingPageContext.Provider, { value: { path, prefix: "" }, children }) }),
112
+ articles.map(({ path: articlePath, Page }) => /* @__PURE__ */ jsxs(
113
+ "section",
114
+ {
115
+ "data-doc-page": articlePath,
116
+ className: "doc-reading-page",
117
+ "data-pagefind-ignore": true,
118
+ children: [
119
+ /* @__PURE__ */ jsx("a", { className: "doc-reading-permalink", href: articlePath, children: "Open this page ↗" }),
120
+ /* @__PURE__ */ jsx(ArticleBoundary, { path: articlePath, children: /* @__PURE__ */ jsx(PageContextProvider, { pageContext: { ...context, urlPathname: articlePath }, children: /* @__PURE__ */ jsx(
121
+ ReadingPageContext.Provider,
122
+ {
123
+ value: {
124
+ path: articlePath,
125
+ prefix: `doc-${encodeURIComponent(articlePath)}--`
126
+ },
127
+ children: /* @__PURE__ */ jsx(Page, {})
128
+ }
129
+ ) }) })
130
+ ]
131
+ },
132
+ articlePath
133
+ )),
134
+ enabled && next && /* @__PURE__ */ jsx("div", { ref: sentinel, className: "doc-reading-next", "data-pagefind-ignore": true, children: error ? /* @__PURE__ */ jsxs("p", { role: "status", children: [
135
+ "Could not load ",
136
+ next.title,
137
+ ".",
138
+ " ",
139
+ /* @__PURE__ */ jsx(
140
+ "button",
141
+ {
142
+ type: "button",
143
+ onClick: () => {
144
+ setError(false);
145
+ setAttempt((n) => n + 1);
146
+ },
147
+ children: "Retry"
148
+ }
149
+ ),
150
+ " ",
151
+ "or ",
152
+ /* @__PURE__ */ jsx("a", { href: next.path, children: "open the page" }),
153
+ "."
154
+ ] }) : /* @__PURE__ */ jsxs("p", { role: busy ? "status" : void 0, children: [
155
+ busy ? "Loading " : "Up next: ",
156
+ /* @__PURE__ */ jsx("a", { href: next.path, children: next.title })
157
+ ] }) })
158
+ ] });
159
+ }
160
+ export {
161
+ ContinuousReading
162
+ };
@@ -1,5 +1,5 @@
1
1
  import { jsxs, jsx } from "react/jsx-runtime";
2
- import { usePageContext } from "vike-react/usePageContext";
2
+ import { useDocPath } from "../lib/reading.js";
3
3
  import { getAdjacentPages } from "../lib/navigation.js";
4
4
  import { useHiddenToggle } from "../lib/use-hidden-toggle.js";
5
5
  const LINK = {
@@ -21,7 +21,7 @@ const LABEL = {
21
21
  color: "var(--color-doc-template-muted)"
22
22
  };
23
23
  function DocFooter() {
24
- const { urlPathname } = usePageContext();
24
+ const urlPathname = useDocPath();
25
25
  const [showHidden] = useHiddenToggle();
26
26
  const { prev, next } = getAdjacentPages(urlPathname, { includeHidden: showHidden });
27
27
  if (!prev && !next) return null;
@@ -12,5 +12,10 @@ export interface PreviewProps {
12
12
  defaultTab?: 'preview' | 'source' | 'data';
13
13
  /** Force a height for the preview pane. */
14
14
  height?: number | string;
15
+ /** `split` shows the source and the live render side by side (source on
16
+ * the left), stacking source above preview when the frame is narrower
17
+ * than 560px. The preview pane is then a size container, so a demo's
18
+ * container queries follow the pane's width. Default `tabs`. */
19
+ layout?: 'tabs' | 'split';
15
20
  }
16
- export declare function Preview({ children, source, dataSource, filename, defaultTab, height, }: PreviewProps): import("react").JSX.Element;
21
+ export declare function Preview({ children, source, dataSource, filename, defaultTab, height, layout, }: PreviewProps): import("react").JSX.Element;
@@ -161,9 +161,12 @@ function Preview({
161
161
  dataSource,
162
162
  filename,
163
163
  defaultTab = "preview",
164
- height
164
+ height,
165
+ layout = "tabs"
165
166
  }) {
166
- const [tab, setTab] = useState(defaultTab);
167
+ const split = layout === "split";
168
+ const [selectedTab, setTab] = useState(defaultTab);
169
+ const tab = split ? "source" : selectedTab;
167
170
  const [copied, setCopied] = useState(false);
168
171
  const [fullscreen, setFullscreen] = useState(false);
169
172
  const [showLines, setShowLines] = useLineNumbers();
@@ -187,7 +190,8 @@ function Preview({
187
190
  "div",
188
191
  {
189
192
  "data-line-numbers": showLines ? "on" : "off",
190
- className: "border border-doc-template-faint bg-doc-template-panel",
193
+ ...split ? { "data-preview-layout": "split" } : null,
194
+ className: "dockit-preview border border-doc-template-faint bg-doc-template-panel",
191
195
  style: fullscreen ? {
192
196
  // Full-viewport overlay. `--color-doc-template-bg` is the page
193
197
  // surface token so the backdrop matches the site in both light
@@ -213,7 +217,7 @@ function Preview({
213
217
  background: "color-mix(in srgb, var(--color-doc-template-ink) 3%, transparent)"
214
218
  },
215
219
  children: [
216
- /* @__PURE__ */ jsx("div", { style: { display: "inline-flex", gap: 2 }, role: "tablist", children: ["preview", "source", ...dataSource ? ["data"] : []].map((t) => {
220
+ split ? /* @__PURE__ */ jsx("span", { style: { ...TAB, color: "var(--color-doc-template-muted)", cursor: "default", display: "inline-flex", alignItems: "center" }, children: "Source · Preview" }) : /* @__PURE__ */ jsx("div", { style: { display: "inline-flex", gap: 2 }, role: "tablist", children: ["preview", "source", ...dataSource ? ["data"] : []].map((t) => {
217
221
  const active = t === tab;
218
222
  return /* @__PURE__ */ jsx(
219
223
  "button",
@@ -285,7 +289,31 @@ function Preview({
285
289
  ]
286
290
  }
287
291
  ),
288
- tab === "preview" ? /* @__PURE__ */ jsxs(
292
+ split ? /* @__PURE__ */ jsxs(
293
+ "div",
294
+ {
295
+ className: "dockit-preview-split",
296
+ style: fullscreen ? { flex: 1, minHeight: 0, overflow: "auto" } : void 0,
297
+ children: [
298
+ /* @__PURE__ */ jsx(
299
+ "div",
300
+ {
301
+ className: "dockit-preview-split-source",
302
+ style: { background: "var(--color-doc-template-code)", minWidth: 0 },
303
+ children: /* @__PURE__ */ jsx(HighlightedSource, { code: source, filename })
304
+ }
305
+ ),
306
+ /* @__PURE__ */ jsx(
307
+ "div",
308
+ {
309
+ className: "dockit-preview-split-render",
310
+ style: { padding: 24, minHeight: 80, minWidth: 0, ...height ? { height } : null },
311
+ children
312
+ }
313
+ )
314
+ ]
315
+ }
316
+ ) : tab === "preview" ? /* @__PURE__ */ jsxs(
289
317
  "div",
290
318
  {
291
319
  ...fullscreen ? { "data-preview-fill": "" } : null,
@@ -1,6 +1,6 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useMemo, useState, useEffect } from "react";
3
- import { usePageContext } from "vike-react/usePageContext";
3
+ import { useDocPath } from "../lib/reading.js";
4
4
  import { normalizePath, groupedPages, groupedVisiblePages } from "../lib/navigation.js";
5
5
  import { useHiddenToggle } from "../lib/use-hidden-toggle.js";
6
6
  import { tabForUrl, urlInTab } from "../lib/tabs.js";
@@ -14,7 +14,7 @@ function loadCollapsed() {
14
14
  }
15
15
  }
16
16
  function Sidebar() {
17
- const { urlPathname } = usePageContext();
17
+ const urlPathname = useDocPath();
18
18
  const currentPath = normalizePath(urlPathname);
19
19
  const [showHidden] = useHiddenToggle();
20
20
  const allGroups = showHidden ? groupedPages : groupedVisiblePages;
@@ -1,6 +1,6 @@
1
1
  import { jsxs, jsx } from "react/jsx-runtime";
2
2
  import { useState, useRef, useEffect, useMemo } from "react";
3
- import { usePageContext } from "vike-react/usePageContext";
3
+ import { useDocPath } from "../lib/reading.js";
4
4
  import { normalizePath, findPage } from "../lib/navigation.js";
5
5
  import { siteConfig } from "../site-config.js";
6
6
  import { useHiddenToggle } from "../lib/use-hidden-toggle.js";
@@ -48,10 +48,10 @@ function scrollToHeading(e, id) {
48
48
  const marginTop = parseFloat(cs.scrollMarginTop) || 0;
49
49
  const targetY = target.getBoundingClientRect().top + window.scrollY - marginTop;
50
50
  animateScrollTo(targetY);
51
- history.replaceState(null, "", `#${id}`);
51
+ if (!target.closest("[data-doc-page]")) history.replaceState(history.state, "", `#${id}`);
52
52
  }
53
53
  function TOC() {
54
- const { urlPathname } = usePageContext();
54
+ const urlPathname = useDocPath();
55
55
  const currentPath = normalizePath(urlPathname);
56
56
  const [showDeveloperLinks] = useHiddenToggle();
57
57
  const mdHref = (() => {
@@ -137,7 +137,7 @@ function TOC() {
137
137
  return result;
138
138
  }, [headings, collapsedH2s]);
139
139
  useEffect(() => {
140
- const main = document.querySelector("main.doc-content");
140
+ const main = document.querySelector(`[data-doc-page="${CSS.escape(urlPathname)}"]`) ?? document.querySelector("main.doc-content");
141
141
  if (!main) {
142
142
  setHeadings([]);
143
143
  return;
@@ -315,7 +315,7 @@ function TOC() {
315
315
  const prev = body.querySelector('a[aria-current="location"]');
316
316
  if (prev) prev.removeAttribute("aria-current");
317
317
  if (id) {
318
- const next = body.querySelector(`a[href="#${CSS.escape(id)}"]`);
318
+ const next = body.querySelector(`a[data-toc-target="${CSS.escape(id)}"]`);
319
319
  if (next) next.setAttribute("aria-current", "location");
320
320
  }
321
321
  }
@@ -339,7 +339,7 @@ function TOC() {
339
339
  }, [headings, collapsedH2s]);
340
340
  useEffect(() => {
341
341
  if (!foldVersion || !activeIdRef.current || !bodyRef.current) return;
342
- const link = bodyRef.current.querySelector(`a[href="#${CSS.escape(activeIdRef.current)}"]`);
342
+ const link = bodyRef.current.querySelector(`a[data-toc-target="${CSS.escape(activeIdRef.current)}"]`);
343
343
  if (link) link.scrollIntoView({ block: "nearest", behavior: "auto" });
344
344
  }, [foldVersion]);
345
345
  function applyRailDOM(start, len) {
@@ -550,7 +550,8 @@ function TOC() {
550
550
  children: /* @__PURE__ */ jsxs(
551
551
  "a",
552
552
  {
553
- href: `#${h.id}`,
553
+ href: h.el.closest("[data-doc-page]") ? `${urlPathname}#${h.el.dataset.docAnchor ?? h.id}` : `#${h.id}`,
554
+ "data-toc-target": h.id,
554
555
  onClick: (e) => scrollToHeading(e, h.id),
555
556
  "data-level": h.level,
556
557
  className: "toc-link",
@@ -1,6 +1,6 @@
1
1
  import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
2
  import { useRef, useState, useEffect, useLayoutEffect } from "react";
3
- import { usePageContext } from "vike-react/usePageContext";
3
+ import { useDocPath } from "../lib/reading.js";
4
4
  import { findPage } from "../lib/navigation.js";
5
5
  import { siteConfig } from "../site-config.js";
6
6
  import { useMediaQuery } from "../lib/use-media-query.js";
@@ -36,7 +36,7 @@ const SEARCH_MAX_W = 860;
36
36
  const SEARCH_GUTTER = 24;
37
37
  const SEARCH_INSET = `max(${SEARCH_GUTTER}px, calc((100vw - ${SEARCH_MAX_W}px) / 2))`;
38
38
  function Topbar({ searchOpen, onOpenSearch, onCloseSearch, query, setQuery }) {
39
- const { urlPathname } = usePageContext();
39
+ const urlPathname = useDocPath();
40
40
  const current = findPage(urlPathname);
41
41
  const [showDeveloperLinks] = useHiddenToggle();
42
42
  const hasTabs = TABS.length > 0;
@@ -14,6 +14,9 @@ export declare const mdxComponents: {
14
14
  h1: import('react').FC<Omit<import('react').DetailedHTMLProps<import('react').HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>, "ref">>;
15
15
  h2: import('react').FC<Omit<import('react').DetailedHTMLProps<import('react').HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>, "ref">>;
16
16
  h3: import('react').FC<Omit<import('react').DetailedHTMLProps<import('react').HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>, "ref">>;
17
+ h4: ({ id, ...props }: ComponentPropsWithoutRef<"h4">) => import("react").JSX.Element;
18
+ h5: ({ id, ...props }: ComponentPropsWithoutRef<"h4">) => import("react").JSX.Element;
19
+ h6: ({ id, ...props }: ComponentPropsWithoutRef<"h4">) => import("react").JSX.Element;
17
20
  p: import('react').FC<Omit<import('react').DetailedHTMLProps<import('react').HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>, "ref">>;
18
21
  ul: import('react').FC<Omit<import('react').DetailedHTMLProps<import('react').HTMLAttributes<HTMLUListElement>, HTMLUListElement>, "ref">>;
19
22
  ol: import('react').FC<Omit<import('react').DetailedHTMLProps<import('react').OlHTMLAttributes<HTMLOListElement>, HTMLOListElement>, "ref">>;
@@ -1,4 +1,5 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
+ import { useReadingPage, readingHref } from "../lib/reading.js";
2
3
  import { useContext, createContext, useState } from "react";
3
4
  import { CodeBlock } from "./CodeBlock.js";
4
5
  import { Table } from "./Table.js";
@@ -39,12 +40,23 @@ const CheckIcon = () => /* @__PURE__ */ jsx(
39
40
  children: /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" })
40
41
  }
41
42
  );
42
- function HeadingAnchor({ id, label }) {
43
+ function HeadingAnchor({
44
+ id,
45
+ label,
46
+ originalId
47
+ }) {
48
+ const page = useReadingPage();
43
49
  const [copied, setCopied] = useState(false);
44
50
  if (!id) return null;
45
- const onClick = () => {
51
+ const onClick = (event) => {
52
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0)
53
+ return;
54
+ if (page) {
55
+ event.preventDefault();
56
+ document.getElementById(id)?.scrollIntoView();
57
+ }
46
58
  if (typeof window === "undefined") return;
47
- const url = `${window.location.origin}${window.location.pathname}#${id}`;
59
+ const url = `${window.location.origin}${page?.path ?? window.location.pathname}#${originalId ?? id}`;
48
60
  navigator.clipboard?.writeText(url).catch(() => {
49
61
  });
50
62
  setCopied(true);
@@ -56,7 +68,7 @@ function HeadingAnchor({ id, label }) {
56
68
  return /* @__PURE__ */ jsx(
57
69
  "a",
58
70
  {
59
- href: `#${id}`,
71
+ href: page ? `${page.path}#${originalId ?? id}` : `#${id}`,
60
72
  onClick,
61
73
  "aria-label": copied ? "Link copied to clipboard" : label ? `Copy link to ${label}` : "Copy link to this section",
62
74
  title: copied ? "Copied!" : "Copy link to this section",
@@ -102,98 +114,125 @@ function PreWrapper(props) {
102
114
  }
103
115
  );
104
116
  }
105
- const H2 = ({ id, children, style, ...rest }) => /* @__PURE__ */ jsxs(Fragment, { children: [
106
- id && /* @__PURE__ */ jsx(
107
- "span",
117
+ const H2 = ({ id, children, style, ...rest }) => {
118
+ const page = useReadingPage();
119
+ const originalId = id;
120
+ if (id && page?.prefix) id = page.prefix + id;
121
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
122
+ id && /* @__PURE__ */ jsx(
123
+ "span",
124
+ {
125
+ id,
126
+ "data-doc-anchor": originalId,
127
+ "aria-hidden": true,
128
+ "data-toc-anchor": true,
129
+ "data-toc-level": "2",
130
+ style: {
131
+ display: "block",
132
+ height: 0,
133
+ marginTop: 16,
134
+ scrollMarginTop: 56
135
+ }
136
+ }
137
+ ),
138
+ /* @__PURE__ */ jsxs(
139
+ "h2",
140
+ {
141
+ ...rest,
142
+ className: "flex items-center text-doc-template-ink group -mx-[18px] px-[18px] lg:-mx-[56px] lg:px-[56px]",
143
+ style: {
144
+ position: "sticky",
145
+ top: 40,
146
+ zIndex: 3,
147
+ fontFamily: "var(--font-doc-template-ui)",
148
+ fontSize: 22,
149
+ fontWeight: 600,
150
+ letterSpacing: "-0.018em",
151
+ marginBottom: 12,
152
+ paddingTop: 12,
153
+ paddingBottom: 8,
154
+ gap: 8,
155
+ lineHeight: 1.2,
156
+ // Let the actual page surface show through, including custom themes.
157
+ backgroundColor: "transparent",
158
+ backdropFilter: "saturate(140%) blur(8px)",
159
+ WebkitBackdropFilter: "saturate(140%) blur(8px)",
160
+ ...style
161
+ },
162
+ children: [
163
+ children,
164
+ /* @__PURE__ */ jsx(HeadingAnchor, { originalId, id, label: flatten(children) })
165
+ ]
166
+ }
167
+ )
168
+ ] });
169
+ };
170
+ const H3 = ({ id, children, style, ...rest }) => {
171
+ const page = useReadingPage();
172
+ const originalId = id;
173
+ if (id && page?.prefix) id = page.prefix + id;
174
+ return /* @__PURE__ */ jsxs(
175
+ "h3",
108
176
  {
109
177
  id,
110
- "aria-hidden": true,
111
- "data-toc-anchor": true,
112
- "data-toc-level": "2",
178
+ "data-doc-anchor": originalId,
179
+ ...rest,
180
+ className: "flex items-center text-doc-template-ink group",
113
181
  style: {
114
- display: "block",
115
- height: 0,
116
- marginTop: 16,
117
- scrollMarginTop: 56
118
- }
182
+ fontFamily: "var(--font-doc-template-ui)",
183
+ fontSize: 15,
184
+ fontWeight: 600,
185
+ letterSpacing: "-0.005em",
186
+ margin: "18px 0 6px",
187
+ gap: 8,
188
+ scrollMarginTop: 96,
189
+ ...style
190
+ },
191
+ children: [
192
+ children,
193
+ /* @__PURE__ */ jsx(HeadingAnchor, { originalId, id, label: flatten(children) })
194
+ ]
119
195
  }
120
- ),
121
- /* @__PURE__ */ jsxs(
122
- "h2",
196
+ );
197
+ };
198
+ const H1 = ({ id, children, style, ...rest }) => {
199
+ const page = useReadingPage();
200
+ const originalId = id;
201
+ if (id && page?.prefix) id = page.prefix + id;
202
+ return /* @__PURE__ */ jsxs(
203
+ "h1",
123
204
  {
205
+ id,
206
+ "data-doc-anchor": originalId,
124
207
  ...rest,
125
- className: "flex items-center text-doc-template-ink group -mx-[18px] px-[18px] lg:-mx-[56px] lg:px-[56px]",
208
+ className: "flex items-center text-doc-template-ink group",
126
209
  style: {
127
- position: "sticky",
128
- top: 40,
129
- zIndex: 3,
130
210
  fontFamily: "var(--font-doc-template-ui)",
131
- fontSize: 22,
132
- fontWeight: 600,
133
- letterSpacing: "-0.018em",
134
- marginBottom: 12,
135
- paddingTop: 12,
136
- paddingBottom: 8,
211
+ fontSize: 32,
212
+ fontWeight: 700,
213
+ letterSpacing: "-0.025em",
214
+ margin: "18px 0 10px",
215
+ paddingBottom: 24,
216
+ lineHeight: 1.1,
137
217
  gap: 8,
138
- lineHeight: 1.2,
139
- // Let the actual page surface show through, including custom themes.
140
- backgroundColor: "transparent",
141
- backdropFilter: "saturate(140%) blur(8px)",
142
- WebkitBackdropFilter: "saturate(140%) blur(8px)",
143
218
  ...style
144
219
  },
145
220
  children: [
146
221
  children,
147
- /* @__PURE__ */ jsx(HeadingAnchor, { id, label: flatten(children) })
222
+ /* @__PURE__ */ jsx(HeadingAnchor, { originalId, id, label: flatten(children) })
148
223
  ]
149
224
  }
150
- )
151
- ] });
152
- const H3 = ({ id, children, style, ...rest }) => /* @__PURE__ */ jsxs(
153
- "h3",
154
- {
155
- id,
156
- ...rest,
157
- className: "flex items-center text-doc-template-ink group",
158
- style: {
159
- fontFamily: "var(--font-doc-template-ui)",
160
- fontSize: 15,
161
- fontWeight: 600,
162
- letterSpacing: "-0.005em",
163
- margin: "18px 0 6px",
164
- gap: 8,
165
- scrollMarginTop: 96,
166
- ...style
167
- },
168
- children: [
169
- children,
170
- /* @__PURE__ */ jsx(HeadingAnchor, { id, label: flatten(children) })
171
- ]
172
- }
173
- );
174
- const H1 = ({ id, children, style, ...rest }) => /* @__PURE__ */ jsxs(
175
- "h1",
176
- {
177
- id,
178
- ...rest,
179
- className: "flex items-center text-doc-template-ink group",
180
- style: {
181
- fontFamily: "var(--font-doc-template-ui)",
182
- fontSize: 32,
183
- fontWeight: 700,
184
- letterSpacing: "-0.025em",
185
- margin: "18px 0 10px",
186
- paddingBottom: 24,
187
- lineHeight: 1.1,
188
- gap: 8,
189
- ...style
190
- },
191
- children: [
192
- children,
193
- /* @__PURE__ */ jsx(HeadingAnchor, { id, label: flatten(children) })
194
- ]
195
- }
196
- );
225
+ );
226
+ };
227
+ function scopedHeading(Tag) {
228
+ return function Heading({ id, ...props }) {
229
+ const page = useReadingPage();
230
+ return /* @__PURE__ */ jsx(Tag, { ...props, id: id && page?.prefix ? page.prefix + id : id, "data-doc-anchor": id });
231
+ };
232
+ }
233
+ const H4 = scopedHeading("h4");
234
+ const H5 = scopedHeading("h5");
235
+ const H6 = scopedHeading("h6");
197
236
  const BODY_FONT_SIZE = 14.5;
198
237
  const BODY_LINE_HEIGHT = 1.65;
199
238
  const P = ({ children, style, ...rest }) => /* @__PURE__ */ jsx(
@@ -246,7 +285,11 @@ const OL = ({ children, style, ...rest }) => /* @__PURE__ */ jsx(
246
285
  }
247
286
  );
248
287
  const LI = ({ children, style, ...rest }) => /* @__PURE__ */ jsx("li", { ...rest, style: { marginBottom: 4, ...style }, children });
249
- const Blockquote = ({ children, style, ...rest }) => /* @__PURE__ */ jsx(
288
+ const Blockquote = ({
289
+ children,
290
+ style,
291
+ ...rest
292
+ }) => /* @__PURE__ */ jsx(
250
293
  "blockquote",
251
294
  {
252
295
  ...rest,
@@ -260,19 +303,40 @@ const Blockquote = ({ children, style, ...rest }) => /* @__PURE__ */ jsx(
260
303
  children
261
304
  }
262
305
  );
263
- const A = ({ children, style, ...rest }) => /* @__PURE__ */ jsx(
264
- "a",
265
- {
266
- ...rest,
267
- className: "text-doc-template-accent hover:[border-bottom-color:var(--color-doc-template-accent)]",
268
- style: {
269
- borderBottom: "1px solid color-mix(in srgb, var(--color-doc-template-accent) 35%, transparent)",
270
- textDecoration: "none",
271
- ...style
272
- },
273
- children
274
- }
275
- );
306
+ const A = ({
307
+ children,
308
+ style,
309
+ href,
310
+ onClick,
311
+ ...rest
312
+ }) => {
313
+ const page = useReadingPage();
314
+ const local = page && href?.startsWith("#");
315
+ return /* @__PURE__ */ jsx(
316
+ "a",
317
+ {
318
+ ...rest,
319
+ href: page ? readingHref(href, page.path) : href,
320
+ onClick: (event) => {
321
+ onClick?.(event);
322
+ if (!local || event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0)
323
+ return;
324
+ const target = document.getElementById(page.prefix + decodeURIComponent(href.slice(1)));
325
+ if (target) {
326
+ event.preventDefault();
327
+ target.scrollIntoView();
328
+ }
329
+ },
330
+ className: "text-doc-template-accent hover:[border-bottom-color:var(--color-doc-template-accent)]",
331
+ style: {
332
+ borderBottom: "1px solid color-mix(in srgb, var(--color-doc-template-accent) 35%, transparent)",
333
+ textDecoration: "none",
334
+ ...style
335
+ },
336
+ children
337
+ }
338
+ );
339
+ };
276
340
  const Th = ({ children, style, ...rest }) => /* @__PURE__ */ jsx(
277
341
  "th",
278
342
  {
@@ -299,7 +363,12 @@ const Td = ({ children, style, ...rest }) => /* @__PURE__ */ jsx(
299
363
  children
300
364
  }
301
365
  );
302
- const Code = ({ className, children, style, ...rest }) => {
366
+ const Code = ({
367
+ className,
368
+ children,
369
+ style,
370
+ ...rest
371
+ }) => {
303
372
  const insidePre = useContext(InsidePreContext);
304
373
  const isBlock = insidePre || !!className && /(?:shiki|language-|hljs)/.test(className);
305
374
  if (isBlock) {
@@ -328,6 +397,9 @@ const mdxComponents = {
328
397
  h1: H1,
329
398
  h2: H2,
330
399
  h3: H3,
400
+ h4: H4,
401
+ h5: H5,
402
+ h6: H6,
331
403
  p: P,
332
404
  ul: UL,
333
405
  ol: OL,
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { DocPageLoader } from './lib/reading';
1
2
  import { SiteConfig as _SiteConfig, SearchConfig as _SearchConfig } from './site-config';
2
3
  import { PageFrontmatter as _PageFrontmatter } from './lib/navigation';
3
4
  import { TabEntry as _TabEntry } from './lib/tabs';
@@ -47,6 +48,8 @@ export { Topbar } from './components/Topbar';
47
48
  export { VersionBadge, type VersionBadgeProps } from './components/VersionBadge';
48
49
  export { Layout, useMerge } from './renderer/Layout';
49
50
  export interface InitDocsOptions {
51
+ /** Lazy MDX bodies for opt-in continuous reading. */
52
+ pageLoaders?: Record<string, DocPageLoader>;
50
53
  /** Site branding, URLs, version chips, git hash. */
51
54
  site: Partial<_SiteConfig>;
52
55
  /** Metadata-only glob: `{ eager: true, query: '?frontmatter', import: 'default' }`.
@@ -75,3 +78,4 @@ export interface InitDocsOptions {
75
78
  /** One call wires the whole shell: site config, navigation, search, tabs. */
76
79
  export declare function initDocs(opts: InitDocsOptions): void;
77
80
  export { ContentRow, type ContentRowProps } from './components/ContentRow';
81
+ export { nextReadingPage, pageLoader, type DocPageLoader } from './lib/reading';
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { initPageLoaders } from "./lib/reading.js";
2
+ import { nextReadingPage, pageLoader } from "./lib/reading.js";
1
3
  import { defineSiteConfig } from "./site-config.js";
2
4
  import { siteConfig } from "./site-config.js";
3
5
  import { initNavigation } from "./lib/navigation.js";
@@ -42,6 +44,7 @@ function initDocs(opts) {
42
44
  initNavigation(modules, opts.sectionOrder ?? []);
43
45
  initSearchIndex(opts.rawPages ?? {});
44
46
  initTabs(opts.tabs ?? []);
47
+ initPageLoaders(opts.pageLoaders);
45
48
  }
46
49
  export {
47
50
  Callout,
@@ -84,7 +87,9 @@ export {
84
87
  internalTabs,
85
88
  isExternalTab,
86
89
  mdxComponents,
90
+ nextReadingPage,
87
91
  normalizePath,
92
+ pageLoader,
88
93
  pages,
89
94
  rawMarkdown,
90
95
  search,
@@ -0,0 +1,21 @@
1
+ import { ComponentType } from 'react';
2
+ export type DocPageLoader = () => Promise<{
3
+ default: ComponentType;
4
+ }>;
5
+ export declare function initPageLoaders(modules?: Record<string, DocPageLoader>): void;
6
+ export declare function pageLoader(path: string): DocPageLoader | undefined;
7
+ /** Reading follows visible sidebar order, stays in the current tab, and never
8
+ * enters hidden, noindex, fullscreen, or non-MDX/server-data pages. */
9
+ export declare function nextReadingPage(path: string): import('./navigation').PageMeta | undefined;
10
+ export declare const ReadingPathContext: import('react').Context<string | null>;
11
+ export declare function useDocPath(): string;
12
+ export declare const ReadingPageContext: import('react').Context<{
13
+ path: string;
14
+ prefix: string;
15
+ } | null>;
16
+ export declare function useReadingPage(): {
17
+ path: string;
18
+ prefix: string;
19
+ } | null;
20
+ /** Resolve authored relative links as if this article were opened on its own. */
21
+ export declare function readingHref(href: string | undefined, path: string): string | undefined;
@@ -0,0 +1,46 @@
1
+ import { useContext, createContext } from "react";
2
+ import { usePageContext } from "vike-react/usePageContext";
3
+ import { groupedVisiblePages, normalizePath } from "./navigation.js";
4
+ import { tabForUrl } from "./tabs.js";
5
+ const loaders = /* @__PURE__ */ new Map();
6
+ function initPageLoaders(modules = {}) {
7
+ loaders.clear();
8
+ for (const [file, load] of Object.entries(modules)) {
9
+ const slug = file.replace(/^.*?\bpages\//, "").replace(/\/\+Page\.mdx$/, "");
10
+ loaders.set(slug === "index" ? "/" : `/${slug}`, load);
11
+ }
12
+ }
13
+ function pageLoader(path) {
14
+ return loaders.get(normalizePath(path));
15
+ }
16
+ function nextReadingPage(path) {
17
+ const list = groupedVisiblePages.flatMap((g) => g.items).filter((p) => !p.noindex && !p.fullscreen && tabForUrl(p.path) === tabForUrl(path));
18
+ const index = list.findIndex((p) => p.path === normalizePath(path));
19
+ const next = index >= 0 ? list[index + 1] : void 0;
20
+ return next && pageLoader(next.path) ? next : void 0;
21
+ }
22
+ const ReadingPathContext = createContext(null);
23
+ function useDocPath() {
24
+ const active = useContext(ReadingPathContext);
25
+ const page = usePageContext();
26
+ return active ?? page.urlPathname;
27
+ }
28
+ const ReadingPageContext = createContext(null);
29
+ function useReadingPage() {
30
+ return useContext(ReadingPageContext);
31
+ }
32
+ function readingHref(href, path) {
33
+ if (!href || /^(?:[a-z][a-z\d+.-]*:|\/)/i.test(href)) return href;
34
+ const url = new URL(href, `https://docs.invalid${path}`);
35
+ return url.pathname + url.search + url.hash;
36
+ }
37
+ export {
38
+ ReadingPageContext,
39
+ ReadingPathContext,
40
+ initPageLoaders,
41
+ nextReadingPage,
42
+ pageLoader,
43
+ readingHref,
44
+ useDocPath,
45
+ useReadingPage
46
+ };
@@ -1,5 +1,7 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
- import { useState, useMemo, useContext, createContext, useEffect } from "react";
2
+ import { ContinuousReading } from "../components/ContinuousReading.js";
3
+ import { pageLoader, ReadingPathContext, useDocPath } from "../lib/reading.js";
4
+ import { useState, useEffect, useMemo, useContext, createContext } from "react";
3
5
  import { MDXProvider } from "@mdx-js/react";
4
6
  import { usePageContext } from "vike-react/usePageContext";
5
7
  import { ThemeProvider } from "../components/ThemeProvider.js";
@@ -15,7 +17,7 @@ const SPRING = "cubic-bezier(0.4, 0.7, 0.3, 1)";
15
17
  const MergeContext = createContext(false);
16
18
  const useMerge = () => useContext(MergeContext);
17
19
  function PageBreadcrumb() {
18
- const { urlPathname } = usePageContext();
20
+ const urlPathname = useDocPath();
19
21
  const current = findPage(urlPathname);
20
22
  const merged = useMerge();
21
23
  if (!current) return null;
@@ -78,7 +80,7 @@ function H1MergeWatcher({ onChange }) {
78
80
  return null;
79
81
  }
80
82
  function HeadSync() {
81
- const { urlPathname } = usePageContext();
83
+ const urlPathname = useDocPath();
82
84
  useEffect(() => {
83
85
  if (typeof document === "undefined") return;
84
86
  const current = findPage(urlPathname);
@@ -111,7 +113,10 @@ function HeadSync() {
111
113
  }
112
114
  function Layout({ children }) {
113
115
  const { urlPathname } = usePageContext();
116
+ const [activePath, setActivePath] = useState(urlPathname);
117
+ useEffect(() => setActivePath(urlPathname), [urlPathname]);
114
118
  const current = findPage(urlPathname);
119
+ const continuous = siteConfig.continuousReading && !!pageLoader(urlPathname) && !current?.noindex && !current?.hidden;
115
120
  const isFullscreen = current?.fullscreen === true;
116
121
  const [searchOpen, setSearchOpen] = useState(false);
117
122
  const [searchQuery, setSearchQuery] = useState("");
@@ -124,7 +129,7 @@ function Layout({ children }) {
124
129
  }),
125
130
  []
126
131
  );
127
- return /* @__PURE__ */ jsx(ThemeProvider, { children: /* @__PURE__ */ jsx(MDXProvider, { components: mergedMdxComponents, children: /* @__PURE__ */ jsxs(MergeContext.Provider, { value: merged, children: [
132
+ return /* @__PURE__ */ jsx(ThemeProvider, { children: /* @__PURE__ */ jsx(MDXProvider, { components: mergedMdxComponents, children: /* @__PURE__ */ jsx(ReadingPathContext.Provider, { value: continuous ? activePath : null, children: /* @__PURE__ */ jsxs(MergeContext.Provider, { value: merged, children: [
128
133
  /* @__PURE__ */ jsx(
129
134
  Topbar,
130
135
  {
@@ -151,7 +156,7 @@ function Layout({ children }) {
151
156
  className: isFullscreen ? "doc-content w-full min-w-0 h-full overflow-auto p-4" : "doc-content w-full min-w-0 px-[18px] pb-20 lg:px-[56px] lg:pb-[120px] lg:max-w-[760px]",
152
157
  children: [
153
158
  !isFullscreen && /* @__PURE__ */ jsx(PageBreadcrumb, {}),
154
- children,
159
+ continuous && !isFullscreen ? /* @__PURE__ */ jsx(ContinuousReading, { path: urlPathname, onActive: setActivePath, children }, urlPathname) : children,
155
160
  !isFullscreen && /* @__PURE__ */ jsx("div", { "data-pagefind-ignore": true, children: /* @__PURE__ */ jsx(DocFooter, {}) })
156
161
  ]
157
162
  }
@@ -170,7 +175,7 @@ function Layout({ children }) {
170
175
  ),
171
176
  !isFullscreen && /* @__PURE__ */ jsx(H1MergeWatcher, { onChange: setMerged }),
172
177
  /* @__PURE__ */ jsx(HeadSync, {})
173
- ] }) }) });
178
+ ] }) }) }) });
174
179
  }
175
180
  export {
176
181
  Layout,
@@ -40,6 +40,8 @@ export interface SearchConfig {
40
40
  mergeIndexes?: MergeIndexDef[];
41
41
  }
42
42
  export interface SiteConfig {
43
+ /** Offer a Keep scrolling control. Requires initDocs.pageLoaders. Default false. */
44
+ continuousReading?: boolean;
43
45
  /** Brand name in the top-left of the shell (e.g. "DreamLake"). */
44
46
  brand: string;
45
47
  /** Muted secondary label after the brand (e.g. "Dockit"). */
package/dist/styles.css CHANGED
@@ -468,3 +468,20 @@
468
468
  /* Touch has no hover-away state: leave room so actions never mask code. */
469
469
  [data-code-block]:has(> .dockit-code-controls) > div:last-child { padding-top: 30px; }
470
470
  }
471
+
472
+ /* Preview layout="split": source left, live render right; stacked when the
473
+ frame is narrower than 560px. The render pane is a size container so a
474
+ demo's container queries follow the pane, not the page. */
475
+ .dockit-preview[data-preview-layout='split'] { container: dockit-preview / inline-size; }
476
+ .dockit-preview-split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
477
+ .dockit-preview-split-source { border-right: 1px solid var(--color-doc-template-faint); overflow-x: auto; }
478
+ .dockit-preview-split-render { container-type: inline-size; }
479
+ @container dockit-preview (max-width: 560px) {
480
+ .dockit-preview-split { grid-template-columns: minmax(0, 1fr); }
481
+ .dockit-preview-split-source { border-right: 0; border-bottom: 1px solid var(--color-doc-template-faint); }
482
+ }
483
+
484
+ .doc-reading-toggle { display:flex; align-items:center; gap:8px; font-size:12px; color:var(--color-doc-template-muted); margin:12px 0; }
485
+ .doc-reading-page { border-top:1px solid var(--color-doc-template-faint); margin-top:64px; padding-top:24px; }
486
+ .doc-reading-permalink, .doc-reading-next { font-size:12px; color:var(--color-doc-template-muted); }
487
+ .doc-reading-next { min-height:56px; margin-top:32px; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreamlake/dockit",
3
- "version": "0.2.31",
3
+ "version": "0.2.33",
4
4
  "description": "The DreamLake documentation shell — Vike + React + Tailwind v4 + MDX docs framework with sidebar, TOC, search, and theming",
5
5
  "license": "MIT",
6
6
  "type": "module",