@dreamlake/dockit 0.1.1 → 0.2.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.
@@ -60,22 +60,36 @@ function CodeBlock({ children, filename, lang }) {
60
60
  },
61
61
  children: [
62
62
  /* @__PURE__ */ jsx("span", { style: { fontWeight: 600, textTransform: "none", letterSpacing: 0 }, children: filename }),
63
- lang && /* @__PURE__ */ jsx("span", { className: "uppercase", style: { fontWeight: 600, letterSpacing: "0.06em" }, children: lang }),
63
+ lang && /* @__PURE__ */ jsx(
64
+ "span",
65
+ {
66
+ className: "uppercase opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity duration-150",
67
+ style: { fontWeight: 600, letterSpacing: "0.06em" },
68
+ children: lang
69
+ }
70
+ ),
64
71
  /* @__PURE__ */ jsx("span", { style: { flex: 1 } }),
65
- /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center", style: { gap: 6 }, children: [
66
- /* @__PURE__ */ jsx(
67
- "button",
68
- {
69
- type: "button",
70
- "aria-pressed": showLines,
71
- title: showLines ? ":set nonu" : ":set nu",
72
- onClick: () => setShowLines(!showLines),
73
- style: headerBtnStyle(showLines),
74
- children: /* @__PURE__ */ jsx(HashIcon, {})
75
- }
76
- ),
77
- /* @__PURE__ */ jsx("button", { type: "button", onClick: copy, style: headerBtnStyle(copied), children: copied ? "Copied" : "Copy" })
78
- ] })
72
+ /* @__PURE__ */ jsxs(
73
+ "span",
74
+ {
75
+ className: "inline-flex items-center opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity duration-150",
76
+ style: { gap: 6 },
77
+ children: [
78
+ /* @__PURE__ */ jsx(
79
+ "button",
80
+ {
81
+ type: "button",
82
+ "aria-pressed": showLines,
83
+ title: showLines ? ":set nonu" : ":set nu",
84
+ onClick: () => setShowLines(!showLines),
85
+ style: headerBtnStyle(showLines),
86
+ children: /* @__PURE__ */ jsx(HashIcon, {})
87
+ }
88
+ ),
89
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: copy, style: headerBtnStyle(copied), children: copied ? "Copied" : "Copy" })
90
+ ]
91
+ }
92
+ )
79
93
  ]
80
94
  }
81
95
  ),
@@ -112,6 +112,48 @@ function HashIcon() {
112
112
  }
113
113
  );
114
114
  }
115
+ function MaximizeIcon() {
116
+ return /* @__PURE__ */ jsxs(
117
+ "svg",
118
+ {
119
+ viewBox: "0 0 24 24",
120
+ fill: "none",
121
+ stroke: "currentColor",
122
+ strokeWidth: "2",
123
+ strokeLinecap: "round",
124
+ strokeLinejoin: "round",
125
+ "aria-hidden": true,
126
+ style: { width: 12, height: 12, display: "block" },
127
+ children: [
128
+ /* @__PURE__ */ jsx("polyline", { points: "15 3 21 3 21 9" }),
129
+ /* @__PURE__ */ jsx("polyline", { points: "9 21 3 21 3 15" }),
130
+ /* @__PURE__ */ jsx("line", { x1: "21", y1: "3", x2: "14", y2: "10" }),
131
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "21", x2: "10", y2: "14" })
132
+ ]
133
+ }
134
+ );
135
+ }
136
+ function MinimizeIcon() {
137
+ return /* @__PURE__ */ jsxs(
138
+ "svg",
139
+ {
140
+ viewBox: "0 0 24 24",
141
+ fill: "none",
142
+ stroke: "currentColor",
143
+ strokeWidth: "2",
144
+ strokeLinecap: "round",
145
+ strokeLinejoin: "round",
146
+ "aria-hidden": true,
147
+ style: { width: 12, height: 12, display: "block" },
148
+ children: [
149
+ /* @__PURE__ */ jsx("polyline", { points: "4 14 10 14 10 20" }),
150
+ /* @__PURE__ */ jsx("polyline", { points: "20 10 14 10 14 4" }),
151
+ /* @__PURE__ */ jsx("line", { x1: "14", y1: "10", x2: "21", y2: "3" }),
152
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "21", x2: "10", y2: "14" })
153
+ ]
154
+ }
155
+ );
156
+ }
115
157
  function Preview({
116
158
  children,
117
159
  source,
@@ -122,8 +164,17 @@ function Preview({
122
164
  }) {
123
165
  const [tab, setTab] = useState(defaultTab);
124
166
  const [copied, setCopied] = useState(false);
167
+ const [fullscreen, setFullscreen] = useState(false);
125
168
  const [showLines, setShowLines] = useLineNumbers();
126
169
  const visibleSource = tab === "data" ? dataSource ?? "" : source;
170
+ useEffect(() => {
171
+ if (!fullscreen) return;
172
+ function onKey(e) {
173
+ if (e.key === "Escape") setFullscreen(false);
174
+ }
175
+ window.addEventListener("keydown", onKey);
176
+ return () => window.removeEventListener("keydown", onKey);
177
+ }, [fullscreen]);
127
178
  function copy() {
128
179
  navigator.clipboard.writeText(visibleSource).then(
129
180
  () => {
@@ -139,7 +190,20 @@ function Preview({
139
190
  {
140
191
  "data-line-numbers": showLines ? "on" : "off",
141
192
  className: "border border-doc-template-faint bg-doc-template-panel",
142
- style: { margin: "18px 0 22px", borderRadius: "var(--radius-doc-template)", overflow: "hidden" },
193
+ style: fullscreen ? {
194
+ // Full-viewport overlay. `--color-doc-template-bg` is the page
195
+ // surface token so the backdrop matches the site in both light
196
+ // and dark themes.
197
+ position: "fixed",
198
+ inset: 0,
199
+ zIndex: 1e3,
200
+ margin: 0,
201
+ borderRadius: 0,
202
+ background: "var(--color-doc-template-bg)",
203
+ display: "flex",
204
+ flexDirection: "column",
205
+ overflow: "hidden"
206
+ } : { margin: "18px 0 22px", borderRadius: "var(--radius-doc-template)", overflow: "hidden" },
143
207
  children: [
144
208
  /* @__PURE__ */ jsxs(
145
209
  "div",
@@ -206,11 +270,54 @@ function Preview({
206
270
  children: copied ? "Copied" : "Copy"
207
271
  }
208
272
  )
209
- ] })
273
+ ] }),
274
+ tab === "preview" && /* @__PURE__ */ jsx("span", { style: { marginLeft: "auto" } }),
275
+ /* @__PURE__ */ jsx(
276
+ "button",
277
+ {
278
+ type: "button",
279
+ "aria-pressed": fullscreen,
280
+ "aria-label": fullscreen ? "Exit fullscreen" : "Enter fullscreen",
281
+ title: fullscreen ? "Exit fullscreen (Esc)" : "Enter fullscreen",
282
+ onClick: () => setFullscreen((f) => !f),
283
+ style: headerBtnStyle(fullscreen),
284
+ children: fullscreen ? /* @__PURE__ */ jsx(MinimizeIcon, {}) : /* @__PURE__ */ jsx(MaximizeIcon, {})
285
+ }
286
+ )
210
287
  ]
211
288
  }
212
289
  ),
213
- tab === "preview" ? /* @__PURE__ */ jsx("div", { style: { padding: 24, minHeight: 80, ...height ? { height } : null }, children }) : /* @__PURE__ */ jsx("div", { style: { background: "var(--color-doc-template-code)" }, children: /* @__PURE__ */ jsx(HighlightedSource, { code: visibleSource, filename }) })
290
+ tab === "preview" ? /* @__PURE__ */ jsxs(
291
+ "div",
292
+ {
293
+ ...fullscreen ? { "data-preview-fill": "" } : null,
294
+ style: fullscreen ? {
295
+ // Full-bleed: drop the padding so a wide demo reaches every
296
+ // edge of the overlay. The `[data-preview-fill]` rule below
297
+ // then stretches the demo's own wrapper to this pane's
298
+ // height, so a specimen with a fixed height grows to fill
299
+ // the viewport instead of leaving dead space below it.
300
+ padding: 0,
301
+ flex: 1,
302
+ minHeight: 0,
303
+ width: "100%",
304
+ overflow: "auto"
305
+ } : { padding: 24, minHeight: 80, ...height ? { height } : null },
306
+ children: [
307
+ fullscreen && /* @__PURE__ */ jsx("style", { children: "[data-preview-fill]>:not(style){height:100%!important}" }),
308
+ children
309
+ ]
310
+ }
311
+ ) : /* @__PURE__ */ jsx(
312
+ "div",
313
+ {
314
+ style: {
315
+ background: "var(--color-doc-template-code)",
316
+ ...fullscreen ? { flex: 1, minHeight: 0, overflow: "auto" } : null
317
+ },
318
+ children: /* @__PURE__ */ jsx(HighlightedSource, { code: visibleSource, filename })
319
+ }
320
+ )
214
321
  ]
215
322
  }
216
323
  );
@@ -1,7 +1,7 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useMemo, useState, useEffect } from "react";
3
3
  import { usePageContext } from "vike-react/usePageContext";
4
- import { groupedPages, groupedVisiblePages } from "../lib/navigation.js";
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";
7
7
  const SIDEBAR_KEY = "sidebar-collapsed";
@@ -15,6 +15,7 @@ function loadCollapsed() {
15
15
  }
16
16
  function Sidebar() {
17
17
  const { urlPathname } = usePageContext();
18
+ const currentPath = normalizePath(urlPathname);
18
19
  const [showHidden] = useHiddenToggle();
19
20
  const allGroups = showHidden ? groupedPages : groupedVisiblePages;
20
21
  const activeTab = tabForUrl(urlPathname);
@@ -36,7 +37,7 @@ function Sidebar() {
36
37
  });
37
38
  }
38
39
  useEffect(() => {
39
- const g = groups.find((g2) => g2.items.some((i) => i.path === urlPathname));
40
+ const g = groups.find((g2) => g2.items.some((i) => i.path === currentPath));
40
41
  if (g?.label && collapsed.has(g.label)) {
41
42
  setCollapsed((prev) => {
42
43
  const next = new Set(prev);
@@ -84,7 +85,7 @@ function Sidebar() {
84
85
  "a",
85
86
  {
86
87
  href: item.path,
87
- "aria-current": item.path === urlPathname ? "page" : void 0,
88
+ "aria-current": item.path === currentPath ? "page" : void 0,
88
89
  className: "sidebar-link",
89
90
  children: [
90
91
  /* @__PURE__ */ jsx("span", { style: { flex: 1, minWidth: 0 }, children: item.title }),
@@ -1,7 +1,7 @@
1
1
  import { jsxs, jsx } from "react/jsx-runtime";
2
2
  import { useState, useRef, useEffect, useMemo } from "react";
3
3
  import { usePageContext } from "vike-react/usePageContext";
4
- import { pages } from "../lib/navigation.js";
4
+ import { normalizePath, findPage } from "../lib/navigation.js";
5
5
  import { siteConfig } from "../site-config.js";
6
6
  const TOC_KEY_PREFIX = "toc-collapsed:";
7
7
  const X0 = 4;
@@ -51,15 +51,16 @@ function scrollToHeading(e, id) {
51
51
  }
52
52
  function TOC() {
53
53
  const { urlPathname } = usePageContext();
54
+ const currentPath = normalizePath(urlPathname);
54
55
  const mdHref = (() => {
55
- const meta = pages.find((p) => p.path === urlPathname);
56
+ const meta = findPage(urlPathname);
56
57
  if (!meta || meta.noindex) return null;
57
- return urlPathname === "/" ? "/index.md" : `${urlPathname}.md`;
58
+ return currentPath === "/" ? "/index.md" : `${currentPath}.md`;
58
59
  })();
59
60
  const editHref = (() => {
60
- const meta = pages.find((p) => p.path === urlPathname);
61
+ const meta = findPage(urlPathname);
61
62
  if (!meta) return null;
62
- const slug = urlPathname === "/" ? "index" : urlPathname.replace(/^\//, "");
63
+ const slug = currentPath === "/" ? "index" : currentPath.replace(/^\//, "");
63
64
  const pagesPath = siteConfig.docsPagesPath ?? "docs/pages";
64
65
  return `${siteConfig.docsRepoUrl}/edit/${siteConfig.docsBranch}/${pagesPath}/${slug}/+Page.mdx`;
65
66
  })();
@@ -138,7 +139,7 @@ function TOC() {
138
139
  setHeadings([]);
139
140
  return;
140
141
  }
141
- const current = pages.find((p) => p.path === urlPathname);
142
+ const current = findPage(urlPathname);
142
143
  const tocLevel = current?.tocLevel ?? 3;
143
144
  const selector = tocLevel >= 3 ? "[data-toc-anchor], h3[id]" : "[data-toc-anchor]";
144
145
  function collect() {
@@ -1,2 +1,5 @@
1
1
  import { JSX } from 'react';
2
+ /** Topbar theme control. The variant comes from `siteConfig.themeToggle`:
3
+ * `'cycle'` (default) renders the single cycling button, `'segmented'`
4
+ * the three-button slider. Both share ThemeProvider state. */
2
5
  export declare function ThemeToggle(): JSX.Element;
@@ -1,6 +1,7 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useTheme } from "./ThemeProvider.js";
3
3
  import { ClientOnly } from "./ClientOnly.js";
4
+ import { siteConfig } from "../site-config.js";
4
5
  const SIZE = 24;
5
6
  const PAD = 2;
6
7
  const SPRING = "cubic-bezier(0.34, 1.45, 0.55, 1)";
@@ -61,11 +62,11 @@ const DarkIcon = () => /* @__PURE__ */ jsx(
61
62
  }
62
63
  );
63
64
  const SEGMENTS = [
64
- { value: "light", label: "Light", Icon: LightIcon },
65
+ { value: "light", label: "Light", Icon: LightIcon, inactiveRotate: -18 },
65
66
  { value: "system", label: "System", Icon: SystemIcon },
66
- { value: "dark", label: "Dark", Icon: DarkIcon }
67
+ { value: "dark", label: "Dark", Icon: DarkIcon, inactiveRotate: 18 }
67
68
  ];
68
- function ToggleInner() {
69
+ function CycleToggle() {
69
70
  const { theme, setTheme } = useTheme();
70
71
  const idx = Math.max(0, SEGMENTS.findIndex((s) => s.value === theme));
71
72
  const current = SEGMENTS[idx];
@@ -101,8 +102,80 @@ function ToggleInner() {
101
102
  }
102
103
  );
103
104
  }
105
+ function SegmentedToggle() {
106
+ const { theme, setTheme } = useTheme();
107
+ const idx = Math.max(0, SEGMENTS.findIndex((s) => s.value === theme));
108
+ return /* @__PURE__ */ jsxs(
109
+ "div",
110
+ {
111
+ role: "group",
112
+ "aria-label": "Theme",
113
+ className: "relative inline-flex items-center bg-doc-template-panel",
114
+ style: { padding: PAD, borderRadius: 999, isolation: "isolate" },
115
+ children: [
116
+ /* @__PURE__ */ jsx(
117
+ "span",
118
+ {
119
+ "aria-hidden": true,
120
+ className: "bg-doc-template-chip",
121
+ style: {
122
+ position: "absolute",
123
+ top: PAD,
124
+ left: PAD,
125
+ width: SIZE,
126
+ height: SIZE,
127
+ borderRadius: 999,
128
+ transform: `translateX(${idx * SIZE}px)`,
129
+ transition: `transform 0.42s ${SPRING}`,
130
+ zIndex: 0,
131
+ willChange: "transform"
132
+ }
133
+ }
134
+ ),
135
+ SEGMENTS.map((seg) => {
136
+ const pressed = seg.value === theme;
137
+ return /* @__PURE__ */ jsx(
138
+ "button",
139
+ {
140
+ type: "button",
141
+ "aria-label": seg.label,
142
+ title: seg.label,
143
+ "aria-pressed": pressed,
144
+ onClick: () => setTheme(seg.value),
145
+ className: pressed ? "inline-flex items-center justify-center bg-transparent border-0 cursor-pointer text-doc-template-ink" : "inline-flex items-center justify-center bg-transparent border-0 cursor-pointer text-doc-template-muted hover:text-doc-template-ink",
146
+ style: {
147
+ position: "relative",
148
+ zIndex: 1,
149
+ width: SIZE,
150
+ height: SIZE,
151
+ borderRadius: 999,
152
+ padding: 0,
153
+ margin: 0,
154
+ transition: "color 0.25s ease"
155
+ },
156
+ children: /* @__PURE__ */ jsx(
157
+ "span",
158
+ {
159
+ style: {
160
+ display: "inline-flex",
161
+ transform: pressed ? "scale(1) rotate(0deg)" : `scale(0.85)${seg.inactiveRotate ? ` rotate(${seg.inactiveRotate}deg)` : ""}`,
162
+ opacity: pressed ? 1 : 0.65,
163
+ transition: `transform 0.42s ${SPRING}, opacity 0.25s ease`
164
+ },
165
+ children: /* @__PURE__ */ jsx(seg.Icon, {})
166
+ }
167
+ )
168
+ },
169
+ seg.value
170
+ );
171
+ })
172
+ ]
173
+ }
174
+ );
175
+ }
104
176
  function ThemeToggle() {
105
- return /* @__PURE__ */ jsx(ClientOnly, { children: /* @__PURE__ */ jsx(ToggleInner, {}) });
177
+ const segmented = siteConfig.themeToggle === "segmented";
178
+ return /* @__PURE__ */ jsx(ClientOnly, { children: segmented ? /* @__PURE__ */ jsx(SegmentedToggle, {}) : /* @__PURE__ */ jsx(CycleToggle, {}) });
106
179
  }
107
180
  export {
108
181
  ThemeToggle
@@ -1,11 +1,12 @@
1
1
  import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
2
  import { useRef, useState, useEffect, useLayoutEffect } from "react";
3
3
  import { usePageContext } from "vike-react/usePageContext";
4
- import { pages } from "../lib/navigation.js";
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";
7
7
  import { useHiddenToggle } from "../lib/use-hidden-toggle.js";
8
8
  import { ThemeToggle } from "./ThemeToggle.js";
9
+ import { VersionBadge } from "./VersionBadge.js";
9
10
  import { useMerge } from "../renderer/Layout.js";
10
11
  import { TabStrip } from "./TabStrip.js";
11
12
  function HiddenRevealChip() {
@@ -45,303 +46,6 @@ function HiddenRevealChip() {
45
46
  }
46
47
  );
47
48
  }
48
- function VersionBadge({
49
- label,
50
- version,
51
- collapsed,
52
- dropdown = false
53
- }) {
54
- const transition = `opacity 0.4s ease, transform 0.4s ${SPRING}, width 0.4s ${SPRING}, margin 0.4s ${SPRING}, padding 0.4s ${SPRING}`;
55
- const [open, setOpen] = useState(false);
56
- const [manifest, setManifest] = useState(null);
57
- const [displayVersion, setDisplayVersion] = useState(version);
58
- const wrapperRef = useRef(null);
59
- useEffect(() => {
60
- if (!dropdown) return;
61
- let cancelled = false;
62
- fetch("/versions.json").then((r) => r.ok ? r.json() : null).then((m) => {
63
- if (!cancelled && m) setManifest(m);
64
- }).catch(() => {
65
- });
66
- return () => {
67
- cancelled = true;
68
- };
69
- }, [dropdown]);
70
- useEffect(() => {
71
- if (typeof window === "undefined") return;
72
- const host = window.location.hostname;
73
- const m = host.match(/^v(\d+(?:-\d+)+(?:[a-z]+\d+)?)\./i);
74
- if (m) {
75
- setDisplayVersion(m[1].replace(/-/g, "."));
76
- } else if (host.startsWith("stable.") && manifest?.stable) {
77
- setDisplayVersion(manifest.stable);
78
- } else if (manifest?.current) {
79
- setDisplayVersion(manifest.current);
80
- }
81
- }, [manifest]);
82
- useEffect(() => {
83
- if (!open) return;
84
- function onDown(e) {
85
- if (wrapperRef.current && !wrapperRef.current.contains(e.target)) setOpen(false);
86
- }
87
- function onKey(e) {
88
- if (e.key === "Escape") setOpen(false);
89
- }
90
- window.addEventListener("mousedown", onDown);
91
- window.addEventListener("keydown", onKey);
92
- return () => {
93
- window.removeEventListener("mousedown", onDown);
94
- window.removeEventListener("keydown", onKey);
95
- };
96
- }, [open]);
97
- const labelSegStyle = {
98
- padding: "2px 6px",
99
- fontWeight: 600,
100
- borderTopLeftRadius: 3,
101
- borderBottomLeftRadius: 3
102
- };
103
- const versionSegStyle = {
104
- padding: "2px 6px",
105
- background: "color-mix(in srgb, var(--color-doc-template-ink) 5%, var(--color-doc-template-bg))",
106
- fontWeight: 600,
107
- borderTopRightRadius: 3,
108
- borderBottomRightRadius: 3,
109
- display: "inline-flex",
110
- alignItems: "center",
111
- gap: dropdown ? 4 : 0,
112
- border: 0,
113
- borderLeft: "1px solid var(--color-doc-template-faint)",
114
- color: "inherit",
115
- fontFamily: "inherit",
116
- fontSize: "inherit",
117
- letterSpacing: "inherit",
118
- cursor: dropdown ? "pointer" : "default"
119
- };
120
- const wrapperBase = {
121
- fontFamily: "var(--font-doc-template-mono)",
122
- fontSize: 10,
123
- fontWeight: 500,
124
- letterSpacing: "0.02em",
125
- marginLeft: 6,
126
- borderRadius: 4,
127
- transition,
128
- ...collapsed ? {
129
- opacity: 0,
130
- transform: "translateX(-4px)",
131
- pointerEvents: "none",
132
- width: 0,
133
- margin: 0,
134
- padding: 0,
135
- overflow: "hidden"
136
- } : {}
137
- };
138
- const chevron = dropdown ? /* @__PURE__ */ jsx(
139
- "svg",
140
- {
141
- width: "8",
142
- height: "8",
143
- viewBox: "0 0 24 24",
144
- fill: "none",
145
- stroke: "currentColor",
146
- strokeWidth: "3",
147
- strokeLinecap: "round",
148
- strokeLinejoin: "round",
149
- "aria-hidden": true,
150
- style: {
151
- opacity: 0.65,
152
- transform: open ? "scaleY(-1)" : "none",
153
- transition: "transform 0.22s ease"
154
- },
155
- children: /* @__PURE__ */ jsx("path", { d: "M6 9l6 6 6-6" })
156
- }
157
- ) : null;
158
- return /* @__PURE__ */ jsxs(
159
- "span",
160
- {
161
- ref: wrapperRef,
162
- className: "hidden md:inline-flex items-stretch relative border border-doc-template-faint",
163
- style: { ...wrapperBase, borderRadius: 4 },
164
- title: dropdown ? void 0 : `${label} ${version}`,
165
- children: [
166
- /* @__PURE__ */ jsx(
167
- "span",
168
- {
169
- className: "inline-flex items-center text-doc-template-muted",
170
- style: labelSegStyle,
171
- children: label
172
- }
173
- ),
174
- dropdown ? /* @__PURE__ */ jsxs(
175
- "button",
176
- {
177
- type: "button",
178
- onClick: () => setOpen((o) => !o),
179
- "aria-haspopup": "listbox",
180
- "aria-expanded": open,
181
- title: `Switch ${label} version (current v${displayVersion})`,
182
- className: "text-doc-template-ink hover:bg-doc-template-search",
183
- style: versionSegStyle,
184
- children: [
185
- "v",
186
- displayVersion,
187
- chevron
188
- ]
189
- }
190
- ) : /* @__PURE__ */ jsxs(
191
- "span",
192
- {
193
- className: "inline-flex items-center text-doc-template-ink",
194
- style: versionSegStyle,
195
- children: [
196
- "v",
197
- displayVersion
198
- ]
199
- }
200
- ),
201
- dropdown && open && manifest && (() => {
202
- const aliasEntries = manifest.aliases ? Object.entries(manifest.aliases) : [];
203
- const versionEntries = manifest.versions ?? [];
204
- const ENTRY_STEP = 0.03;
205
- const ENTRY_BASE = 0.13;
206
- let idx = 0;
207
- return /* @__PURE__ */ jsxs(Fragment, { children: [
208
- /* @__PURE__ */ jsxs(
209
- "svg",
210
- {
211
- width: "14",
212
- height: "11",
213
- viewBox: "0 0 14 11",
214
- "aria-hidden": true,
215
- style: {
216
- position: "absolute",
217
- top: "calc(100% + 4.5px)",
218
- right: 4,
219
- zIndex: 81,
220
- display: "block",
221
- pointerEvents: "none",
222
- animation: "popover-wedge-in 0.24s ease both"
223
- },
224
- children: [
225
- /* @__PURE__ */ jsx("path", { d: "M0 11 L7 1 L14 11", stroke: "var(--color-doc-template-faint)", strokeWidth: "1", fill: "none" }),
226
- /* @__PURE__ */ jsx("path", { d: "M1 11 L7 2 L13 11 Z", fill: "var(--color-doc-template-bg)" })
227
- ]
228
- }
229
- ),
230
- /* @__PURE__ */ jsxs(
231
- "div",
232
- {
233
- role: "listbox",
234
- "aria-label": `Past ${label} versions`,
235
- className: "absolute border border-doc-template-faint bg-doc-template-bg",
236
- style: {
237
- top: "calc(100% + 14px)",
238
- left: 0,
239
- minWidth: 220,
240
- maxHeight: "min(360px, 60vh)",
241
- overflowY: "auto",
242
- borderRadius: 12,
243
- // Drop shadow + a "negative" 0-blur shadow above using the page bg
244
- // color: it paints a bg-colored band into the strip between the
245
- // popover top and the navbar's bottom border, masking the navbar's
246
- // 1px box-shadow in the popover's column so the two don't visually
247
- // collide.
248
- boxShadow: "0 6px 24px rgb(0 0 0 / 0.10), 0 -5px 3px 0 var(--color-doc-template-bg)",
249
- padding: 4,
250
- zIndex: 80,
251
- fontFamily: "var(--font-doc-template-mono)",
252
- fontSize: 11,
253
- animation: "popover-in 0.28s ease both",
254
- animationDelay: "0.04s",
255
- transformOrigin: "top right"
256
- },
257
- children: [
258
- aliasEntries.length > 0 && /* @__PURE__ */ jsx(
259
- "div",
260
- {
261
- style: {
262
- padding: "4px 6px 2px",
263
- opacity: 0.55,
264
- fontSize: 9,
265
- letterSpacing: "0.08em",
266
- textTransform: "uppercase",
267
- animation: "popover-entry-in 0.28s ease both",
268
- animationDelay: `${ENTRY_BASE + idx++ * ENTRY_STEP}s`
269
- },
270
- children: "Aliases"
271
- }
272
- ),
273
- aliasEntries.map(([name, url]) => /* @__PURE__ */ jsxs(
274
- "a",
275
- {
276
- href: url,
277
- className: "block text-doc-template-ink no-underline hover:bg-doc-template-search",
278
- style: {
279
- padding: "5px 8px",
280
- borderRadius: 8,
281
- animation: "popover-entry-in 0.28s ease both",
282
- animationDelay: `${ENTRY_BASE + idx++ * ENTRY_STEP}s`
283
- },
284
- children: [
285
- name,
286
- name === "stable" && manifest.stable && /* @__PURE__ */ jsxs("span", { style: { opacity: 0.5, marginLeft: 6 }, children: [
287
- "v",
288
- manifest.stable
289
- ] })
290
- ]
291
- },
292
- `alias-${name}`
293
- )),
294
- versionEntries.length > 0 && /* @__PURE__ */ jsx(
295
- "div",
296
- {
297
- style: {
298
- padding: "6px 6px 2px",
299
- opacity: 0.55,
300
- fontSize: 9,
301
- letterSpacing: "0.08em",
302
- textTransform: "uppercase",
303
- animation: "popover-entry-in 0.28s ease both",
304
- animationDelay: `${ENTRY_BASE + idx++ * ENTRY_STEP}s`
305
- },
306
- children: "Versions"
307
- }
308
- ),
309
- versionEntries.map((v) => {
310
- const isCurrent = v.version === manifest.current;
311
- return /* @__PURE__ */ jsxs(
312
- "a",
313
- {
314
- href: v.url,
315
- className: "text-doc-template-ink no-underline hover:bg-doc-template-search flex items-baseline justify-between gap-3",
316
- style: {
317
- padding: "5px 8px",
318
- borderRadius: 8,
319
- fontWeight: isCurrent ? 700 : 400,
320
- background: isCurrent ? "color-mix(in srgb, var(--color-doc-template-accent) 12%, transparent)" : void 0,
321
- animation: "popover-entry-in 0.28s ease both",
322
- animationDelay: `${ENTRY_BASE + idx++ * ENTRY_STEP}s`
323
- },
324
- children: [
325
- /* @__PURE__ */ jsxs("span", { children: [
326
- "v",
327
- v.version,
328
- isCurrent && /* @__PURE__ */ jsx("span", { style: { opacity: 0.55, marginLeft: 6 }, children: "current" })
329
- ] }),
330
- v.date && /* @__PURE__ */ jsx("span", { style: { opacity: 0.5, fontVariantNumeric: "tabular-nums", fontSize: 10 }, children: v.date })
331
- ]
332
- },
333
- v.version
334
- );
335
- })
336
- ]
337
- }
338
- )
339
- ] });
340
- })()
341
- ]
342
- }
343
- );
344
- }
345
49
  const SPRING = "cubic-bezier(0.4, 0.7, 0.3, 1)";
346
50
  const EXPAND_EASE = "cubic-bezier(0.16, 1, 0.3, 1)";
347
51
  const EXPAND_MS = 320;
@@ -354,7 +58,7 @@ const SEARCH_GUTTER = 24;
354
58
  const SEARCH_INSET = `max(${SEARCH_GUTTER}px, calc((100vw - ${SEARCH_MAX_W}px) / 2))`;
355
59
  function Topbar({ searchOpen, onOpenSearch, onCloseSearch, query, setQuery }) {
356
60
  const { urlPathname } = usePageContext();
357
- const current = pages.find((p) => p.path === urlPathname);
61
+ const current = findPage(urlPathname);
358
62
  const inputRef = useRef(null);
359
63
  const labelRef = useRef(null);
360
64
  const focusTimerRef = useRef(null);
@@ -0,0 +1,31 @@
1
+ export interface VersionBadgeProps {
2
+ /** Left segment text (e.g. the package name). */
3
+ label: string;
4
+ /** Build-time version shown in the right segment (may be overridden on
5
+ * the client from the hostname / manifest — see below). */
6
+ version: string;
7
+ /** Collapse to width 0 (the Topbar drives this from its merged state).
8
+ * Standalone placements can omit it. */
9
+ collapsed?: boolean;
10
+ /** Turn the version segment into a popover listing the deploys from
11
+ * `/versions.json`. */
12
+ dropdown?: boolean;
13
+ }
14
+ /**
15
+ * Two-segment version chip: `[ label | v<version> ]`.
16
+ *
17
+ * With `dropdown: true` the right segment opens a popover listing alias
18
+ * and past-version doc URLs sourced from a `versions.json` manifest
19
+ * (schema: `{ current, stable, aliases, versions: [{version, url, date}] }`).
20
+ *
21
+ * The manifest is fetched from the production origin (`siteConfig.url`)
22
+ * first, so the dropdown on old, immutable version-branch deploys still
23
+ * lists releases published after they were built; the same-origin copy is
24
+ * the fallback (local dev, or when no `url` is configured). Cross-origin
25
+ * fetching needs a CORS header on `/versions.json` — see the
26
+ * `netlify.example.toml` template.
27
+ *
28
+ * Rendered by the Topbar for each `siteConfig.versionChips` entry, and
29
+ * exported from the library index for standalone placement.
30
+ */
31
+ export declare function VersionBadge({ label, version, collapsed, dropdown, }: VersionBadgeProps): import("react").JSX.Element;
@@ -0,0 +1,315 @@
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { useState, useRef, useEffect } from "react";
3
+ import { siteConfig } from "../site-config.js";
4
+ const SPRING = "cubic-bezier(0.4, 0.7, 0.3, 1)";
5
+ function VersionBadge({
6
+ label,
7
+ version,
8
+ collapsed = false,
9
+ dropdown = false
10
+ }) {
11
+ const transition = `opacity 0.4s ease, transform 0.4s ${SPRING}, width 0.4s ${SPRING}, margin 0.4s ${SPRING}, padding 0.4s ${SPRING}`;
12
+ const [open, setOpen] = useState(false);
13
+ const [manifest, setManifest] = useState(null);
14
+ const [displayVersion, setDisplayVersion] = useState(version);
15
+ const wrapperRef = useRef(null);
16
+ useEffect(() => {
17
+ if (!dropdown) return;
18
+ let cancelled = false;
19
+ const origin = (siteConfig.url || "").replace(/\/+$/, "");
20
+ const sameOrigin = () => fetch("/versions.json").then((r) => r.ok ? r.json() : null);
21
+ const load = origin ? fetch(`${origin}/versions.json`).then((r) => r.ok ? r.json() : Promise.reject(new Error(String(r.status)))).catch(sameOrigin) : sameOrigin();
22
+ load.then((m) => {
23
+ if (!cancelled && m) setManifest(m);
24
+ }).catch(() => {
25
+ });
26
+ return () => {
27
+ cancelled = true;
28
+ };
29
+ }, [dropdown]);
30
+ useEffect(() => {
31
+ if (typeof window === "undefined") return;
32
+ const host = window.location.hostname;
33
+ const m = host.match(/^v(\d+(?:-\d+)+(?:[a-z]+\d+)?)\./i);
34
+ if (m) {
35
+ setDisplayVersion(m[1].replace(/-/g, "."));
36
+ } else if (host.startsWith("stable.") && manifest?.stable) {
37
+ setDisplayVersion(manifest.stable);
38
+ } else if (manifest?.current) {
39
+ setDisplayVersion(manifest.current);
40
+ }
41
+ }, [manifest]);
42
+ useEffect(() => {
43
+ if (!open) return;
44
+ function onDown(e) {
45
+ if (wrapperRef.current && !wrapperRef.current.contains(e.target)) setOpen(false);
46
+ }
47
+ function onKey(e) {
48
+ if (e.key === "Escape") setOpen(false);
49
+ }
50
+ window.addEventListener("mousedown", onDown);
51
+ window.addEventListener("keydown", onKey);
52
+ return () => {
53
+ window.removeEventListener("mousedown", onDown);
54
+ window.removeEventListener("keydown", onKey);
55
+ };
56
+ }, [open]);
57
+ const labelSegStyle = {
58
+ padding: "2px 6px",
59
+ fontWeight: 600,
60
+ borderTopLeftRadius: 3,
61
+ borderBottomLeftRadius: 3
62
+ };
63
+ const versionSegStyle = {
64
+ padding: "2px 6px",
65
+ background: "color-mix(in srgb, var(--color-doc-template-ink) 5%, var(--color-doc-template-bg))",
66
+ fontWeight: 600,
67
+ borderTopRightRadius: 3,
68
+ borderBottomRightRadius: 3,
69
+ display: "inline-flex",
70
+ alignItems: "center",
71
+ gap: dropdown ? 4 : 0,
72
+ border: 0,
73
+ borderLeft: "1px solid var(--color-doc-template-faint)",
74
+ color: "inherit",
75
+ fontFamily: "inherit",
76
+ fontSize: "inherit",
77
+ letterSpacing: "inherit",
78
+ cursor: dropdown ? "pointer" : "default"
79
+ };
80
+ const wrapperBase = {
81
+ fontFamily: "var(--font-doc-template-mono)",
82
+ fontSize: 10,
83
+ fontWeight: 500,
84
+ letterSpacing: "0.02em",
85
+ marginLeft: 6,
86
+ borderRadius: 4,
87
+ transition,
88
+ ...collapsed ? {
89
+ opacity: 0,
90
+ transform: "translateX(-4px)",
91
+ pointerEvents: "none",
92
+ width: 0,
93
+ margin: 0,
94
+ padding: 0,
95
+ overflow: "hidden"
96
+ } : {}
97
+ };
98
+ const chevron = dropdown ? /* @__PURE__ */ jsx(
99
+ "svg",
100
+ {
101
+ width: "8",
102
+ height: "8",
103
+ viewBox: "0 0 24 24",
104
+ fill: "none",
105
+ stroke: "currentColor",
106
+ strokeWidth: "3",
107
+ strokeLinecap: "round",
108
+ strokeLinejoin: "round",
109
+ "aria-hidden": true,
110
+ style: {
111
+ opacity: 0.65,
112
+ transform: open ? "scaleY(-1)" : "none",
113
+ transition: "transform 0.22s ease"
114
+ },
115
+ children: /* @__PURE__ */ jsx("path", { d: "M6 9l6 6 6-6" })
116
+ }
117
+ ) : null;
118
+ return /* @__PURE__ */ jsxs(
119
+ "span",
120
+ {
121
+ ref: wrapperRef,
122
+ className: "hidden md:inline-flex items-stretch relative border border-doc-template-faint",
123
+ style: { ...wrapperBase, borderRadius: 4 },
124
+ title: dropdown ? void 0 : `${label} ${version}`,
125
+ children: [
126
+ /* @__PURE__ */ jsx(
127
+ "span",
128
+ {
129
+ className: "inline-flex items-center text-doc-template-muted",
130
+ style: labelSegStyle,
131
+ children: label
132
+ }
133
+ ),
134
+ dropdown ? /* @__PURE__ */ jsxs(
135
+ "button",
136
+ {
137
+ type: "button",
138
+ onClick: (e) => {
139
+ e.preventDefault();
140
+ e.stopPropagation();
141
+ setOpen((o) => !o);
142
+ },
143
+ "aria-haspopup": "listbox",
144
+ "aria-expanded": open,
145
+ title: `Switch ${label} version (current v${displayVersion})`,
146
+ className: "text-doc-template-ink hover:bg-doc-template-search",
147
+ style: versionSegStyle,
148
+ children: [
149
+ "v",
150
+ displayVersion,
151
+ chevron
152
+ ]
153
+ }
154
+ ) : /* @__PURE__ */ jsxs(
155
+ "span",
156
+ {
157
+ className: "inline-flex items-center text-doc-template-ink",
158
+ style: versionSegStyle,
159
+ children: [
160
+ "v",
161
+ displayVersion
162
+ ]
163
+ }
164
+ ),
165
+ dropdown && open && manifest && (() => {
166
+ const aliasEntries = manifest.aliases ? Object.entries(manifest.aliases) : [];
167
+ const versionEntries = manifest.versions ?? [];
168
+ const ENTRY_STEP = 0.03;
169
+ const ENTRY_BASE = 0.13;
170
+ let idx = 0;
171
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
172
+ /* @__PURE__ */ jsxs(
173
+ "svg",
174
+ {
175
+ width: "14",
176
+ height: "11",
177
+ viewBox: "0 0 14 11",
178
+ "aria-hidden": true,
179
+ style: {
180
+ position: "absolute",
181
+ top: "calc(100% + 4.5px)",
182
+ right: 4,
183
+ zIndex: 81,
184
+ display: "block",
185
+ pointerEvents: "none",
186
+ animation: "popover-wedge-in 0.24s ease both"
187
+ },
188
+ children: [
189
+ /* @__PURE__ */ jsx("path", { d: "M0 11 L7 1 L14 11", stroke: "var(--color-doc-template-faint)", strokeWidth: "1", fill: "none" }),
190
+ /* @__PURE__ */ jsx("path", { d: "M1 11 L7 2 L13 11 Z", fill: "var(--color-doc-template-bg)" })
191
+ ]
192
+ }
193
+ ),
194
+ /* @__PURE__ */ jsxs(
195
+ "div",
196
+ {
197
+ role: "listbox",
198
+ "aria-label": `Past ${label} versions`,
199
+ className: "absolute border border-doc-template-faint bg-doc-template-bg",
200
+ style: {
201
+ top: "calc(100% + 14px)",
202
+ left: 0,
203
+ minWidth: 220,
204
+ maxHeight: "min(360px, 60vh)",
205
+ overflowY: "auto",
206
+ borderRadius: 12,
207
+ // Drop shadow + a "negative" 0-blur shadow above using the page bg
208
+ // color: it paints a bg-colored band into the strip between the
209
+ // popover top and the navbar's bottom border, masking the navbar's
210
+ // 1px box-shadow in the popover's column so the two don't visually
211
+ // collide.
212
+ boxShadow: "0 6px 24px rgb(0 0 0 / 0.10), 0 -5px 3px 0 var(--color-doc-template-bg)",
213
+ padding: 4,
214
+ zIndex: 80,
215
+ fontFamily: "var(--font-doc-template-mono)",
216
+ fontSize: 11,
217
+ animation: "popover-in 0.28s ease both",
218
+ animationDelay: "0.04s",
219
+ transformOrigin: "top right"
220
+ },
221
+ children: [
222
+ aliasEntries.length > 0 && /* @__PURE__ */ jsx(
223
+ "div",
224
+ {
225
+ style: {
226
+ padding: "4px 6px 2px",
227
+ opacity: 0.55,
228
+ fontSize: 9,
229
+ letterSpacing: "0.08em",
230
+ textTransform: "uppercase",
231
+ animation: "popover-entry-in 0.28s ease both",
232
+ animationDelay: `${ENTRY_BASE + idx++ * ENTRY_STEP}s`
233
+ },
234
+ children: "Aliases"
235
+ }
236
+ ),
237
+ aliasEntries.map(([name, url]) => /* @__PURE__ */ jsxs(
238
+ "a",
239
+ {
240
+ href: url,
241
+ className: "block text-doc-template-ink no-underline hover:bg-doc-template-search",
242
+ style: {
243
+ padding: "5px 8px",
244
+ borderRadius: 8,
245
+ animation: "popover-entry-in 0.28s ease both",
246
+ animationDelay: `${ENTRY_BASE + idx++ * ENTRY_STEP}s`
247
+ },
248
+ children: [
249
+ name,
250
+ name === "stable" && manifest.stable && /* @__PURE__ */ jsxs("span", { style: { opacity: 0.5, marginLeft: 6 }, children: [
251
+ "v",
252
+ manifest.stable
253
+ ] }),
254
+ name === "latest" && manifest.current && /* @__PURE__ */ jsxs("span", { style: { opacity: 0.5, marginLeft: 6 }, children: [
255
+ "v",
256
+ manifest.current
257
+ ] })
258
+ ]
259
+ },
260
+ `alias-${name}`
261
+ )),
262
+ versionEntries.length > 0 && /* @__PURE__ */ jsx(
263
+ "div",
264
+ {
265
+ style: {
266
+ padding: "6px 6px 2px",
267
+ opacity: 0.55,
268
+ fontSize: 9,
269
+ letterSpacing: "0.08em",
270
+ textTransform: "uppercase",
271
+ animation: "popover-entry-in 0.28s ease both",
272
+ animationDelay: `${ENTRY_BASE + idx++ * ENTRY_STEP}s`
273
+ },
274
+ children: "Versions"
275
+ }
276
+ ),
277
+ versionEntries.map((v) => {
278
+ const isCurrent = v.version === manifest.current;
279
+ return /* @__PURE__ */ jsxs(
280
+ "a",
281
+ {
282
+ href: v.url,
283
+ className: "text-doc-template-ink no-underline hover:bg-doc-template-search flex items-baseline justify-between gap-3",
284
+ style: {
285
+ padding: "5px 8px",
286
+ borderRadius: 8,
287
+ fontWeight: isCurrent ? 700 : 400,
288
+ background: isCurrent ? "color-mix(in srgb, var(--color-doc-template-accent) 12%, transparent)" : void 0,
289
+ animation: "popover-entry-in 0.28s ease both",
290
+ animationDelay: `${ENTRY_BASE + idx++ * ENTRY_STEP}s`
291
+ },
292
+ children: [
293
+ /* @__PURE__ */ jsxs("span", { children: [
294
+ "v",
295
+ v.version,
296
+ isCurrent && /* @__PURE__ */ jsx("span", { style: { opacity: 0.55, marginLeft: 6 }, children: "current" })
297
+ ] }),
298
+ v.date && /* @__PURE__ */ jsx("span", { style: { opacity: 0.5, fontVariantNumeric: "tabular-nums", fontSize: 10 }, children: v.date })
299
+ ]
300
+ },
301
+ v.version
302
+ );
303
+ })
304
+ ]
305
+ }
306
+ )
307
+ ] });
308
+ })()
309
+ ]
310
+ }
311
+ );
312
+ }
313
+ export {
314
+ VersionBadge
315
+ };
package/dist/index.d.ts CHANGED
@@ -18,7 +18,7 @@ import { TabDef as _TabDef } from './lib/tabs';
18
18
  * })
19
19
  */
20
20
  export { siteConfig, defineSiteConfig, type SiteConfig, type VersionChip, } from './site-config';
21
- export { pages, groupedPages, groupedVisiblePages, initNavigation, getAdjacentPages, type PageMeta, type PageFrontmatter, type PageModule, type NavGroup, } from './lib/navigation';
21
+ export { pages, groupedPages, groupedVisiblePages, initNavigation, getAdjacentPages, normalizePath, findPage, type PageMeta, type PageFrontmatter, type PageModule, type NavGroup, } from './lib/navigation';
22
22
  export { searchIndex, rawMarkdown, initSearchIndex, search, type SearchEntry, type SearchHit, } from './lib/search-index';
23
23
  export { searchPages, defaultResults, type SearchResult, type SubResult, } from './lib/pagefind-search';
24
24
  export { TABS, initTabs, tabForUrl, urlInTab, type TabDef } from './lib/tabs';
@@ -41,6 +41,7 @@ export { ThemeProvider, useTheme, type Theme } from './components/ThemeProvider'
41
41
  export { ThemeToggle } from './components/ThemeToggle';
42
42
  export { TOC } from './components/TOC';
43
43
  export { Topbar } from './components/Topbar';
44
+ export { VersionBadge, type VersionBadgeProps } from './components/VersionBadge';
44
45
  export { Layout, useMerge } from './renderer/Layout';
45
46
  export interface InitDocsOptions {
46
47
  /** Site branding, URLs, version chips, git hash. */
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { defineSiteConfig } from "./site-config.js";
2
2
  import { siteConfig } from "./site-config.js";
3
3
  import { initNavigation } from "./lib/navigation.js";
4
- import { getAdjacentPages, groupedPages, groupedVisiblePages, pages } from "./lib/navigation.js";
4
+ import { findPage, getAdjacentPages, groupedPages, groupedVisiblePages, normalizePath, pages } from "./lib/navigation.js";
5
5
  import { initSearchIndex } from "./lib/search-index.js";
6
6
  import { rawMarkdown, search, searchIndex } from "./lib/search-index.js";
7
7
  import { defaultResults, searchPages } from "./lib/pagefind-search.js";
@@ -26,6 +26,7 @@ import { ThemeProvider, useTheme } from "./components/ThemeProvider.js";
26
26
  import { ThemeToggle } from "./components/ThemeToggle.js";
27
27
  import { TOC } from "./components/TOC.js";
28
28
  import { Topbar } from "./components/Topbar.js";
29
+ import { VersionBadge } from "./components/VersionBadge.js";
29
30
  import { Layout, useMerge } from "./renderer/Layout.js";
30
31
  function initDocs(opts) {
31
32
  defineSiteConfig(opts.site);
@@ -52,9 +53,11 @@ export {
52
53
  ThemeProvider,
53
54
  ThemeToggle,
54
55
  Topbar,
56
+ VersionBadge,
55
57
  defaultResults,
56
58
  defineSiteConfig,
57
59
  docKey,
60
+ findPage,
58
61
  getAdjacentPages,
59
62
  groupedPages,
60
63
  groupedVisiblePages,
@@ -63,6 +66,7 @@ export {
63
66
  initSearchIndex,
64
67
  initTabs,
65
68
  mdxComponents,
69
+ normalizePath,
66
70
  pages,
67
71
  rawMarkdown,
68
72
  search,
@@ -29,6 +29,9 @@ export interface PageFrontmatter {
29
29
  draft?: boolean;
30
30
  hidden?: boolean;
31
31
  noindex?: boolean;
32
+ /** Shorthand for an internal developer note (studio's flag): implies
33
+ * both `hidden: true` and `noindex: true`. */
34
+ dev?: boolean;
32
35
  tocLevel?: 2 | 3;
33
36
  fullscreen?: boolean;
34
37
  }
@@ -54,6 +57,18 @@ export declare const groupedPages: NavGroup[];
54
57
  /** Same shape, but filtered to only-visible pages. Equivalent to
55
58
  * `groupedPages` when no page declares `hidden: true`. */
56
59
  export declare const groupedVisiblePages: NavGroup[];
60
+ /** Normalize a pathname for comparison against `PageMeta.path`: strip any
61
+ * query/hash and drop a trailing slash (except for root `/`). Vike's
62
+ * prerender and client routing can disagree on the trailing slash — e.g.
63
+ * a `/develop` prerender hydrating at `/develop/` — and a raw `===` match
64
+ * then resolves the page on the server but not the client, which renders
65
+ * *different* DOM (breadcrumb, `aria-current`) on each side and trips
66
+ * React hydration. Normalizing both sides keeps SSR and hydration in step. */
67
+ export declare function normalizePath(url: string): string;
68
+ /** Resolve the `PageMeta` for a pathname, tolerant of a trailing slash.
69
+ * Prefer this over `pages.find(p => p.path === urlPathname)` anywhere the
70
+ * result drives rendered output, so server and client agree. */
71
+ export declare function findPage(urlPathname: string): PageMeta | undefined;
57
72
  /**
58
73
  * Populate the navigation singletons from the site's eager page glob:
59
74
  *
@@ -2,6 +2,16 @@ const pages = [];
2
2
  const groupedPages = [];
3
3
  const groupedVisiblePages = [];
4
4
  let sectionOrderList = [];
5
+ function normalizePath(url) {
6
+ if (!url) return "/";
7
+ const clean = url.split("?")[0].split("#")[0];
8
+ if (clean === "/") return "/";
9
+ return clean.replace(/\/+$/, "") || "/";
10
+ }
11
+ function findPage(urlPathname) {
12
+ const norm = normalizePath(urlPathname);
13
+ return pages.find((p) => p.path === norm);
14
+ }
5
15
  function groupPages(items) {
6
16
  const map = /* @__PURE__ */ new Map();
7
17
  for (const p of items) {
@@ -18,6 +28,7 @@ function initNavigation(modules, sectionOrder = []) {
18
28
  const fm = mod.frontmatter ?? {};
19
29
  const dir = filePath.replace(/^.*?\bpages\//, "").replace(/\/\+Page\.mdx$/, "");
20
30
  const path = dir === "index" ? "/" : `/${dir}`;
31
+ const isDev = fm.dev === true;
21
32
  return {
22
33
  path,
23
34
  title: fm.title ?? dir,
@@ -25,8 +36,8 @@ function initNavigation(modules, sectionOrder = []) {
25
36
  order: fm.order ?? 99,
26
37
  description: fm.description,
27
38
  draft: fm.draft === true,
28
- hidden: fm.hidden === true,
29
- noindex: fm.noindex === true,
39
+ hidden: fm.hidden === true || isDev,
40
+ noindex: fm.noindex === true || isDev,
30
41
  tocLevel: fm.tocLevel,
31
42
  fullscreen: fm.fullscreen === true
32
43
  };
@@ -39,9 +50,10 @@ function initNavigation(modules, sectionOrder = []) {
39
50
  groupedVisiblePages.push(...groupPages(pages.filter((p) => !p.hidden)));
40
51
  }
41
52
  function getAdjacentPages(path, opts = {}) {
42
- const here = pages.find((p) => p.path === path);
53
+ const norm = normalizePath(path);
54
+ const here = pages.find((p) => p.path === norm);
43
55
  const list = opts.includeHidden || here?.hidden ? pages : pages.filter((p) => !p.hidden);
44
- const idx = list.findIndex((p) => p.path === path);
56
+ const idx = list.findIndex((p) => p.path === norm);
45
57
  return {
46
58
  current: idx >= 0 ? list[idx] : null,
47
59
  prev: idx > 0 ? list[idx - 1] : null,
@@ -49,9 +61,11 @@ function getAdjacentPages(path, opts = {}) {
49
61
  };
50
62
  }
51
63
  export {
64
+ findPage,
52
65
  getAdjacentPages,
53
66
  groupedPages,
54
67
  groupedVisiblePages,
55
68
  initNavigation,
69
+ normalizePath,
56
70
  pages
57
71
  };
@@ -9,14 +9,14 @@ import { TOC } from "../components/TOC.js";
9
9
  import { DocFooter } from "../components/DocFooter.js";
10
10
  import { SearchPalette } from "../components/SearchPalette.js";
11
11
  import { mdxComponents } from "../components/mdx-components.js";
12
- import { pages } from "../lib/navigation.js";
12
+ import { findPage } from "../lib/navigation.js";
13
13
  import { siteConfig } from "../site-config.js";
14
14
  const SPRING = "cubic-bezier(0.4, 0.7, 0.3, 1)";
15
15
  const MergeContext = createContext(false);
16
16
  const useMerge = () => useContext(MergeContext);
17
17
  function PageBreadcrumb() {
18
18
  const { urlPathname } = usePageContext();
19
- const current = pages.find((p) => p.path === urlPathname);
19
+ const current = findPage(urlPathname);
20
20
  const merged = useMerge();
21
21
  if (!current) return null;
22
22
  return /* @__PURE__ */ jsxs(
@@ -81,7 +81,7 @@ function HeadSync() {
81
81
  const { urlPathname } = usePageContext();
82
82
  useEffect(() => {
83
83
  if (typeof document === "undefined") return;
84
- const current = pages.find((p) => p.path === urlPathname);
84
+ const current = findPage(urlPathname);
85
85
  document.title = current ? `${current.title} — ${siteConfig.brand}` : siteConfig.brand;
86
86
  const desc = current?.description ?? "";
87
87
  let tag = document.querySelector('meta[name="description"]');
@@ -111,7 +111,7 @@ function HeadSync() {
111
111
  }
112
112
  function Layout({ children }) {
113
113
  const { urlPathname } = usePageContext();
114
- const current = pages.find((p) => p.path === urlPathname);
114
+ const current = findPage(urlPathname);
115
115
  const isFullscreen = current?.fullscreen === true;
116
116
  const [searchOpen, setSearchOpen] = useState(false);
117
117
  const [searchQuery, setSearchQuery] = useState("");
@@ -3,14 +3,14 @@ import ReactDOMServer from "react-dom/server";
3
3
  import { Layout } from "./Layout.js";
4
4
  import { PageContextProvider } from "vike-react/usePageContext";
5
5
  import { dangerouslySkipEscape, escapeInject } from "vike/server";
6
- import { pages } from "../lib/navigation.js";
6
+ import { findPage } from "../lib/navigation.js";
7
7
  import { siteConfig } from "../site-config.js";
8
8
  async function onRenderHtml(pageContext) {
9
9
  const { Page, pageProps, urlPathname } = pageContext;
10
10
  const html = ReactDOMServer.renderToString(
11
11
  /* @__PURE__ */ jsx(PageContextProvider, { pageContext, children: /* @__PURE__ */ jsx(Layout, { children: /* @__PURE__ */ jsx(Page, { ...pageProps }) }) })
12
12
  );
13
- const current = pages.find((p) => p.path === urlPathname);
13
+ const current = findPage(urlPathname);
14
14
  const title = current ? `${current.title} — ${siteConfig.brand}` : siteConfig.brand;
15
15
  const description = current?.description ?? "";
16
16
  const mdHref = current && !current.noindex ? current.path === "/" ? "/index.md" : `${current.path}.md` : null;
@@ -55,6 +55,10 @@ export interface SiteConfig {
55
55
  * "Edit this page" link (`<docsRepoUrl>/edit/<docsBranch>/<docsPagesPath>/…`).
56
56
  * Defaults to 'docs/pages'. */
57
57
  docsPagesPath?: string;
58
+ /** Topbar theme control style. `'cycle'` (default) is a single button
59
+ * that steps light → system → dark; `'segmented'` is the three-button
60
+ * slider with a sliding indicator (the uikit/dreamlake docs style). */
61
+ themeToggle?: 'cycle' | 'segmented';
58
62
  }
59
63
  export declare const siteConfig: SiteConfig;
60
64
  /** Merge site-provided values into the singleton (mutates in place). */
@@ -10,7 +10,8 @@ const siteConfig = {
10
10
  summary: "",
11
11
  versionChips: [],
12
12
  gitHash: "",
13
- docsPagesPath: "docs/pages"
13
+ docsPagesPath: "docs/pages",
14
+ themeToggle: "cycle"
14
15
  };
15
16
  function defineSiteConfig(cfg) {
16
17
  Object.assign(siteConfig, cfg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreamlake/dockit",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
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",
@@ -31,3 +31,14 @@ COREPACK_INTEGRITY_KEYS = "0"
31
31
 
32
32
  [context.branch-deploy]
33
33
  # Applies to every non-production branch push.
34
+
35
+ # ─── Version switcher (VersionBadge) ─────────────────────────────────
36
+ # The version-switcher dropdown on old v/* branch deploys fetches the
37
+ # production manifest cross-origin so its list never goes stale — that
38
+ # needs a CORS header on /versions.json.
39
+
40
+ [[headers]]
41
+ for = "/versions.json"
42
+
43
+ [headers.values]
44
+ Access-Control-Allow-Origin = "*"