@docubook/flame 1.7.2 → 2.0.0-alpha.1

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.
Files changed (67) hide show
  1. package/.docu/components/Context.tsx +1 -1
  2. package/.docu/components/Pagination.tsx +9 -1
  3. package/.docu/components/Search.tsx +6 -6
  4. package/.docu/components/Sidebar.tsx +37 -8
  5. package/.docu/components/Theme.tsx +2 -2
  6. package/.docu/components/Toc.tsx +52 -6
  7. package/.docu/components/Typography.tsx +1 -1
  8. package/.docu/components/home/Hero.tsx +4 -4
  9. package/.docu/components/registry.ts +1 -1
  10. package/.docu/lib/build.deno.js +3 -3
  11. package/.docu/lib/{build.impl-TSIF3F7O.js → build.impl-VXB4KL4D.js} +3 -3
  12. package/.docu/lib/build.node.js +3 -3
  13. package/.docu/lib/{chunk-SPHVBXRR.js → chunk-7PRQ3RQB.js} +6 -6
  14. package/.docu/lib/{chunk-DITXUPUV.js → chunk-A6FIEG3H.js} +61 -10
  15. package/.docu/lib/{chunk-PJIJEPNR.js → chunk-JRERMREW.js} +5 -4
  16. package/.docu/lib/{chunk-KMDGSD57.js → chunk-LZDEWK25.js} +3 -3
  17. package/.docu/lib/{chunk-HRO7ONJQ.js → chunk-MQWWCO6O.js} +70 -27
  18. package/.docu/lib/{chunk-B6LGUADD.js → chunk-TRT6WQZG.js} +346 -183
  19. package/.docu/lib/chunk-UISOJ4RW.js +114 -0
  20. package/.docu/lib/clean.js +1 -1
  21. package/.docu/lib/deploy.deno.js +1 -1
  22. package/.docu/lib/deploy.node.js +1 -1
  23. package/.docu/lib/preview.deno.js +5 -3
  24. package/.docu/lib/preview.node.js +5 -3
  25. package/.docu/lib/server.deno.js +6 -4
  26. package/.docu/lib/server.node.js +6 -4
  27. package/.docu/node/build.impl.ts +75 -26
  28. package/.docu/node/build.ts +70 -17
  29. package/.docu/node/client.ts +64 -27
  30. package/.docu/node/deploy.shared.ts +5 -5
  31. package/.docu/node/deploy.ts +1 -1
  32. package/.docu/node/html.shared.ts +7 -2
  33. package/.docu/node/html.ts +4 -2
  34. package/.docu/node/hydrate.node.ts +69 -4
  35. package/.docu/node/hydrate.ts +50 -1
  36. package/.docu/node/mdx-manifest.d.ts +5 -0
  37. package/.docu/node/mdx.ts +103 -17
  38. package/.docu/node/preview.deno.ts +1 -1
  39. package/.docu/node/preview.impl.ts +3 -3
  40. package/.docu/node/preview.node.ts +1 -1
  41. package/.docu/node/preview.ts +2 -2
  42. package/.docu/node/route.ts +57 -1
  43. package/.docu/node/runtime/bun.ts +28 -0
  44. package/.docu/node/runtime/deno.ts +34 -0
  45. package/.docu/node/runtime/index.ts +4 -0
  46. package/.docu/node/runtime/node.ts +107 -0
  47. package/.docu/node/runtime/types.ts +19 -0
  48. package/.docu/node/search-indexer.ts +3 -2
  49. package/.docu/node/seo.ts +2 -2
  50. package/.docu/node/server-routes.ts +32 -13
  51. package/.docu/node/server.deno.ts +1 -1
  52. package/.docu/node/server.impl.ts +49 -3
  53. package/.docu/node/server.node.ts +1 -1
  54. package/.docu/node/server.ts +45 -1
  55. package/.docu/pages/docs/[[...slug]].tsx +13 -4
  56. package/.docu/pages/index.tsx +1 -1
  57. package/.docu/styles/globals.css +38 -20
  58. package/README.md +7 -48
  59. package/bin/cli.js +25 -7
  60. package/package.json +7 -6
  61. package/template/README.md +7 -48
  62. package/template/docs/getting-started/configuration.mdx +16 -4
  63. package/template/docs/getting-started/overview.mdx +34 -42
  64. package/template/docs/guide/components.mdx +46 -228
  65. package/template/docs/guide/routing.mdx +8 -6
  66. package/template/docs/index.mdx +9 -8
  67. package/template/docu.json +4 -4
@@ -76,7 +76,7 @@ export function Context({ className }: ContextProps) {
76
76
  onClick={() => navigate(contextPath)}
77
77
  className={cn(
78
78
  "relative flex cursor-pointer items-center gap-2 rounded px-2 py-2 text-sm",
79
- "text-left outline-none transition-colors",
79
+ "text-left transition-colors outline-none",
80
80
  isActive
81
81
  ? "bg-primary/20 text-primary"
82
82
  : "hover:bg-base-200 text-base-content/80 hover:text-base-content"
@@ -29,7 +29,15 @@ export default function Pagination({
29
29
  return (
30
30
  <PaginationDocs
31
31
  prev={prev ? { href: docsHtmlHref(`/docs${prev.href}`), title: prev.title } : undefined}
32
- next={next ? { href: docsHtmlHref(`/docs${next.href}`), title: next.title } : undefined}
32
+ next={
33
+ next
34
+ ? {
35
+ href: docsHtmlHref(`/docs${next.href}`),
36
+ title: next.title,
37
+ description: next.description,
38
+ }
39
+ : undefined
40
+ }
33
41
  className={className}
34
42
  prevIcon={prevIcon}
35
43
  nextIcon={nextIcon}
@@ -154,7 +154,7 @@ export default function Search({ className }: SearchProps) {
154
154
  boxClassName="w-[calc(100%-2rem)] max-w-[640px] p-0 mx-auto relative z-10"
155
155
  >
156
156
  <div onKeyDown={handleKeyDown}>
157
- <div className="px-4 pb-2 pt-4">
157
+ <div className="px-4 pt-4 pb-2">
158
158
  <div className="border-base-300 flex items-center gap-3 rounded-lg border px-4 py-2.5">
159
159
  <SearchIcon className="text-primary h-5 w-5 shrink-0" />
160
160
  <input
@@ -163,7 +163,7 @@ export default function Search({ className }: SearchProps) {
163
163
  onChange={handleQueryChange}
164
164
  placeholder="Search documentation..."
165
165
  autoFocus
166
- className="placeholder:text-base-content/40 h-8 w-full border-none bg-transparent text-base outline-none ring-0 focus:outline-none focus:ring-0"
166
+ className="placeholder:text-base-content/40 h-8 w-full border-none bg-transparent text-base ring-0 outline-none focus:ring-0 focus:outline-none"
167
167
  aria-label="Search documentation"
168
168
  />
169
169
  {query && (
@@ -187,7 +187,7 @@ export default function Search({ className }: SearchProps) {
187
187
  </div>
188
188
  </div>
189
189
 
190
- <div className="max-h-[400px] overflow-y-auto px-3 py-2">
190
+ <div className="max-h-100 overflow-y-auto px-3 py-2">
191
191
  {query.trim().length >= 2 && results.length === 0 && (
192
192
  <p className="text-base-content/50 py-8 text-center text-sm">
193
193
  No results for &ldquo;<span className="text-primary">{query}</span>&rdquo;
@@ -248,10 +248,10 @@ function SearchTrigger({ onClick }: { onClick: () => void }) {
248
248
  <SearchIcon className="h-4 w-4 shrink-0" />
249
249
  <span className="flex-1 text-left">Search</span>
250
250
  <div className="flex items-center gap-0.5">
251
- <Kbd className="bg-primary text-[12px text-base-300 dark:text-foreground rounded-md p-1.5 font-medium">
251
+ <Kbd className="bg-primary text-primary-foreground rounded-md p-1.5 text-[12px] font-medium">
252
252
  <FnKey.Cmd />
253
253
  </Kbd>
254
- <Kbd className="bg-primary text-base-300 dark:text-foreground rounded-md p-1.5 text-[12px] font-medium">
254
+ <Kbd className="bg-primary text-primary-foreground rounded-md p-1.5 text-[12px] font-medium">
255
255
  K
256
256
  </Kbd>
257
257
  </div>
@@ -289,7 +289,7 @@ function GroupedResults({
289
289
  <div className="flex flex-col gap-1">
290
290
  {groups.map((group) => (
291
291
  <div key={group.section}>
292
- <div className="text-base-content/50 px-2 pb-1 pt-3 text-xs font-semibold">
292
+ <div className="text-base-content/50 px-2 pt-3 pb-1 text-xs font-semibold">
293
293
  {group.section}
294
294
  </div>
295
295
  {group.items.map(({ result, globalIndex }) => {
@@ -92,13 +92,21 @@ export function MobileBar({
92
92
  useEffect(() => {
93
93
  if (!tocs.length) return;
94
94
 
95
+ // Match the CSS scroll-margin-top (scroll-mt-54 = 216px) that anchor
96
+ // jumps rest at, so the highlight agrees with where a jump lands.
97
+ const firstEl = document.getElementById(tocs[0].href.slice(1));
98
+ const offset = firstEl ? parseFloat(getComputedStyle(firstEl).scrollMarginTop) || 100 : 100;
99
+
95
100
  const handleScroll = () => {
96
101
  let currentId: string | null = null;
97
102
  for (const toc of tocs) {
98
103
  const id = toc.href.slice(1);
99
104
  const el = document.getElementById(id);
100
105
  if (!el) continue;
101
- if (el.getBoundingClientRect().top <= 100) {
106
+ // Round: smooth anchor scrolls land at sub-pixel positions (e.g.
107
+ // 216.4px), which would otherwise fail the <= offset comparison and
108
+ // leave the previous heading highlighted.
109
+ if (Math.round(el.getBoundingClientRect().top) <= offset) {
102
110
  currentId = id;
103
111
  } else {
104
112
  break;
@@ -109,7 +117,13 @@ export function MobileBar({
109
117
 
110
118
  handleScroll();
111
119
  window.addEventListener("scroll", handleScroll, { passive: true });
112
- return () => window.removeEventListener("scroll", handleScroll);
120
+ // Smooth anchor scrolls may not emit a trailing scroll event at the
121
+ // target position — recompute once the animation actually settles.
122
+ window.addEventListener("scrollend", handleScroll, { passive: true });
123
+ return () => {
124
+ window.removeEventListener("scroll", handleScroll);
125
+ window.removeEventListener("scrollend", handleScroll);
126
+ };
113
127
  }, [tocs]);
114
128
 
115
129
  useEffect(() => {
@@ -192,7 +206,10 @@ export function MobileBar({
192
206
  <MobileTocList
193
207
  tocs={tocs}
194
208
  activeId={activeId}
195
- onSelect={() => setTocExpanded(false)}
209
+ onSelect={(id) => {
210
+ setTocExpanded(false);
211
+ setActiveId(id);
212
+ }}
196
213
  />
197
214
  ) : (
198
215
  <div className="flex flex-col items-center py-6 text-center">
@@ -216,7 +233,7 @@ function MobileTocList({
216
233
  }: {
217
234
  tocs: TocItem[];
218
235
  activeId: string | null;
219
- onSelect: () => void;
236
+ onSelect: (id: string) => void;
220
237
  }) {
221
238
  return (
222
239
  <ul className="flex flex-col gap-0.5">
@@ -230,9 +247,21 @@ function MobileTocList({
230
247
  href={toc.href}
231
248
  onClick={(e) => {
232
249
  e.preventDefault();
233
- onSelect();
234
- const el = document.getElementById(id);
235
- if (el) el.scrollIntoView({ behavior: "smooth" });
250
+ onSelect(id);
251
+ history.replaceState(null, "", toc.href);
252
+ const targetId = id;
253
+ // Wait for the bar collapse re-render so the measurement is
254
+ // accurate, then jump with the same offset the observer uses
255
+ // (scrollIntoView ignores scroll-margin-top on iOS Safari).
256
+ requestAnimationFrame(() => {
257
+ const el = document.getElementById(targetId);
258
+ if (!el) return;
259
+ const margin = parseFloat(getComputedStyle(el).scrollMarginTop) || 100;
260
+ window.scrollTo({
261
+ top: el.getBoundingClientRect().top + window.scrollY - margin,
262
+ behavior: "smooth",
263
+ });
264
+ });
236
265
  }}
237
266
  className={cn(
238
267
  "block rounded px-2 py-1.5 text-sm transition-colors",
@@ -276,7 +305,7 @@ function MobileDrawer({
276
305
  return (
277
306
  <div className="fixed inset-0 z-[100]">
278
307
  <div className="absolute inset-0 bg-black/40" onClick={onClose} aria-hidden="true" />
279
- <div className="bg-base-100 absolute right-0 top-0 flex h-full w-80 max-w-[85vw] flex-col shadow-xl">
308
+ <div className="bg-base-100 absolute top-0 right-0 flex h-full w-80 max-w-[85vw] flex-col shadow-xl">
280
309
  <div className="border-base-200 flex items-center justify-between border-b px-4 py-3">
281
310
  <div className="flex items-center gap-2">
282
311
  <GitHubLink repoUrl={repoUrl} />
@@ -24,7 +24,7 @@ export function ThemeToggle({ className }: ThemeToggleProps) {
24
24
  >
25
25
  <Sun
26
26
  className={`h-4 w-4 transition-colors ${
27
- !isDark ? "text-base-300" : "text-foreground"
27
+ !isDark ? "text-primary-foreground" : "text-foreground"
28
28
  }`}
29
29
  />
30
30
  </button>
@@ -38,7 +38,7 @@ export function ThemeToggle({ className }: ThemeToggleProps) {
38
38
  >
39
39
  <Moon
40
40
  className={`h-4 w-4 transition-colors ${
41
- isDark ? "text-foreground" : "text-base-content/60"
41
+ isDark ? "text-primary-foreground" : "text-base-content/60"
42
42
  }`}
43
43
  />
44
44
  </button>
@@ -20,13 +20,29 @@ export default function Toc({ tocs }: TocProps) {
20
20
  activeIdRef.current = activeId;
21
21
  }, [activeId]);
22
22
 
23
+ // Single source of truth for the anchor offset — used by BOTH the scroll
24
+ // observer and the TOC click scroll so they can never disagree. Reads the
25
+ // CSS scroll-margin-top (prose-headings:scroll-mt-4 desktop /
26
+ // scroll-mt-16 mobile) so a change in one place updates both, and the
27
+ // heading lands exactly where the browser would put a native anchor jump.
28
+ const getAnchorOffset = useCallback((): number => {
29
+ if (typeof window === "undefined") return 100;
30
+ const first = tocs[0] ? document.getElementById(tocs[0].href.slice(1)) : null;
31
+ const margin = first ? parseFloat(getComputedStyle(first).scrollMarginTop) : 0;
32
+ return margin || 100;
33
+ }, [tocs]);
34
+
23
35
  useEffect(() => {
24
- if (typeof document === "undefined" || !tocs.length) return;
36
+ // Desktop-only TOC — on mobile this island is display:none but still
37
+ // mounted; letting its observer run would hijack the URL hash while the
38
+ // mobile bar owns TOC behavior there.
39
+ if (typeof window === "undefined" || window.innerWidth < 1024) return;
40
+ if (!tocs.length) return;
25
41
 
26
42
  const isDesktop = window.innerWidth >= 1024;
27
43
  const container = isDesktop ? document.getElementById("scroll-container") : null;
28
44
  const scrollTarget = container || window;
29
- const offset = isDesktop ? 80 : 100;
45
+ const offset = getAnchorOffset();
30
46
 
31
47
  const handleScroll = () => {
32
48
  if (clickedIdRef.current) return;
@@ -72,7 +88,7 @@ export default function Toc({ tocs }: TocProps) {
72
88
  scrollTarget.removeEventListener("scroll", listener);
73
89
  if (throttleTimer) clearTimeout(throttleTimer);
74
90
  };
75
- }, [tocs]);
91
+ }, [tocs, getAnchorOffset]);
76
92
 
77
93
  const handleLinkClick = useCallback((id: string) => {
78
94
  clickedIdRef.current = id;
@@ -101,7 +117,7 @@ export default function Toc({ tocs }: TocProps) {
101
117
 
102
118
  <div className="relative">
103
119
  <div className="relative text-sm">
104
- <div className="bg-base-300 absolute left-0 top-0 h-full w-px" />
120
+ <div className="bg-base-300 absolute top-0 left-0 h-full w-px" />
105
121
 
106
122
  <div className="flex flex-col">
107
123
  {tocs.map(({ href, level, text }) => {
@@ -143,7 +159,37 @@ export default function Toc({ tocs }: TocProps) {
143
159
  e.preventDefault();
144
160
  handleLinkClick(id);
145
161
  const el = document.getElementById(id);
146
- if (el) el.scrollIntoView({ behavior: "smooth" });
162
+ if (el) {
163
+ // Manual scroll with the same offset the observer uses —
164
+ // scrollIntoView ignores scroll-margin-top on iOS Safari
165
+ // window-scroll, landing the heading under the sticky
166
+ // mobile bar.
167
+ const offset = getAnchorOffset();
168
+ const scroller =
169
+ window.innerWidth >= 1024
170
+ ? document.getElementById("scroll-container")
171
+ : null;
172
+ const elTop = el.getBoundingClientRect().top;
173
+ if (scroller) {
174
+ // Container scroll space: element offset within the
175
+ // container minus the anchor offset (the container
176
+ // itself may sit below the viewport top, e.g. under
177
+ // the navbar).
178
+ scroller.scrollTo({
179
+ top:
180
+ elTop -
181
+ scroller.getBoundingClientRect().top +
182
+ scroller.scrollTop -
183
+ offset,
184
+ behavior: "smooth",
185
+ });
186
+ } else {
187
+ window.scrollTo({
188
+ top: elTop + window.scrollY - offset,
189
+ behavior: "smooth",
190
+ });
191
+ }
192
+ }
147
193
  }}
148
194
  className={cn(
149
195
  "flex flex-1 items-center py-2 transition-all duration-200",
@@ -153,7 +199,7 @@ export default function Toc({ tocs }: TocProps) {
153
199
  )}
154
200
  style={{ paddingLeft: `${levelPadding + 6}px` }}
155
201
  >
156
- <span className="line-clamp-2 break-words text-sm">{text}</span>
202
+ <span className="line-clamp-2 text-sm break-words">{text}</span>
157
203
  </a>
158
204
  </div>
159
205
  );
@@ -2,7 +2,7 @@ import { PropsWithChildren } from "react";
2
2
 
3
3
  export function Typography({ children }: PropsWithChildren) {
4
4
  return (
5
- <div className="prose prose-zinc dark:prose-invert prose-code:font-code dark:prose-code:bg-stone-900/25 prose-code:bg-stone-50 prose-pre:bg-background max-lg:prose-headings:scroll-mt-54 prose-headings:scroll-mt-4 prose-code:text-sm prose-code:leading-6 dark:prose-code:text-white prose-code:text-stone-800 prose-code:p-1 prose-code:rounded-md prose-code:border prose-img:rounded-md prose-img:border prose-code:before:content-none prose-code:after:content-none prose-code:px-1.5 prose-code:overflow-x-auto prose-img:my-3 prose-h2:my-4 prose-h2:mt-8 max-w-full! w-full pt-2">
5
+ <div className="prose prose-zinc dark:prose-invert prose-code:font-code dark:prose-code:bg-stone-900/25 prose-code:bg-stone-50 prose-pre:bg-background max-lg:prose-headings:scroll-mt-16 prose-headings:scroll-mt-4 prose-code:text-sm prose-code:leading-6 dark:prose-code:text-white prose-code:text-stone-800 prose-code:p-1 prose-code:rounded-md prose-code:border prose-img:rounded-md prose-img:border prose-code:before:content-none prose-code:after:content-none prose-code:px-1.5 prose-code:overflow-x-auto prose-img:my-3 prose-h2:my-4 prose-h2:mt-8 w-full max-w-full! pt-2">
6
6
  {children}
7
7
  </div>
8
8
  );
@@ -34,8 +34,8 @@ function renderActionButtonIcon(iconName: string | undefined, className?: string
34
34
 
35
35
  function ActionButton({ action }: ActionButtonProps) {
36
36
  const themeClasses = {
37
- primary: "bg-primary text-primary-content hover:bg-primary/90",
38
- secondary: "bg-secondary text-secondary-content hover:bg-secondary/90",
37
+ primary: "bg-primary text-primary-foreground hover:bg-primary/90",
38
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/90",
39
39
  ghost: "bg-transparent text-muted-foreground border border-base-300 hover:bg-base-200",
40
40
  };
41
41
 
@@ -64,11 +64,11 @@ export function Hero({ hero, className }: HeroProps) {
64
64
  <div className={cn("mx-auto max-w-4xl px-6 py-32 sm:py-44", className)}>
65
65
  <div className="text-center">
66
66
  {tagline && <p className="text-primary mb-4 text-lg font-semibold">{tagline}</p>}
67
- <h1 className="text-balance text-5xl font-semibold tracking-tight sm:text-7xl">
67
+ <h1 className="text-5xl font-semibold tracking-tight text-balance sm:text-7xl">
68
68
  {headline}
69
69
  </h1>
70
70
  {description && (
71
- <p className="text-muted-foreground mt-8 text-pretty text-lg sm:text-xl">{description}</p>
71
+ <p className="text-muted-foreground mt-8 text-lg text-pretty sm:text-xl">{description}</p>
72
72
  )}
73
73
  {actions && actions.length > 0 && (
74
74
  <div className="mt-10 flex flex-wrap items-center justify-center gap-4">
@@ -1,5 +1,5 @@
1
1
  import type { ComponentType } from "react";
2
- import { createMdxComponents } from "@docubook/mdx-content";
2
+ import { createMdxComponents } from "@docubook/markdown";
3
3
  import { Dropdown, DropdownItem, DropdownLink } from "@docubook/ui-react/dropdown";
4
4
  import { PaginationDocs } from "@docubook/ui-react/pagination";
5
5
  import { Toggle, ToggleGroup } from "@docubook/ui-react/toggle";
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  runBuildCli
3
- } from "./chunk-HRO7ONJQ.js";
4
- import "./chunk-B6LGUADD.js";
3
+ } from "./chunk-MQWWCO6O.js";
4
+ import "./chunk-TRT6WQZG.js";
5
5
  import "./chunk-EOK6KATZ.js";
6
- import "./chunk-PJIJEPNR.js";
6
+ import "./chunk-JRERMREW.js";
7
7
  import "./chunk-4IQXHHPF.js";
8
8
 
9
9
  // .docu/node/build.deno.ts
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  runBuild,
3
3
  runBuildCli
4
- } from "./chunk-HRO7ONJQ.js";
5
- import "./chunk-B6LGUADD.js";
4
+ } from "./chunk-MQWWCO6O.js";
5
+ import "./chunk-TRT6WQZG.js";
6
6
  import "./chunk-EOK6KATZ.js";
7
- import "./chunk-PJIJEPNR.js";
7
+ import "./chunk-JRERMREW.js";
8
8
  import "./chunk-4IQXHHPF.js";
9
9
  export {
10
10
  runBuild,
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  runBuildCli
3
- } from "./chunk-HRO7ONJQ.js";
4
- import "./chunk-B6LGUADD.js";
3
+ } from "./chunk-MQWWCO6O.js";
4
+ import "./chunk-TRT6WQZG.js";
5
5
  import "./chunk-EOK6KATZ.js";
6
- import "./chunk-PJIJEPNR.js";
6
+ import "./chunk-JRERMREW.js";
7
7
  import "./chunk-4IQXHHPF.js";
8
8
 
9
9
  // .docu/node/build.node.ts
@@ -28,7 +28,7 @@ var NGINX_CONF = `server {
28
28
  add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
29
29
  add_header Referrer-Policy "strict-origin-when-cross-origin" always;
30
30
  add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
31
- add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'" always;
31
+ add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'" always;
32
32
 
33
33
  location /assets/ {
34
34
  expires 1y;
@@ -38,7 +38,7 @@ var NGINX_CONF = `server {
38
38
  add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
39
39
  add_header Referrer-Policy "strict-origin-when-cross-origin" always;
40
40
  add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
41
- add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'" always;
41
+ add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'" always;
42
42
  }
43
43
 
44
44
  location /docs/assets/ {
@@ -49,7 +49,7 @@ var NGINX_CONF = `server {
49
49
  add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
50
50
  add_header Referrer-Policy "strict-origin-when-cross-origin" always;
51
51
  add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
52
- add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'" always;
52
+ add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'" always;
53
53
  }
54
54
 
55
55
  location / {
@@ -113,7 +113,7 @@ var HEADERS_FILE = `/*
113
113
  Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
114
114
  Referrer-Policy: strict-origin-when-cross-origin
115
115
  Permissions-Policy: camera=(), microphone=(), geolocation=()
116
- Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'
116
+ Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' data:; connect-src 'self' https:; frame-src https://www.youtube-nocookie.com; frame-ancestors 'none'
117
117
 
118
118
  /assets/*
119
119
  Cache-Control: public, max-age=31536000, immutable
@@ -137,7 +137,7 @@ async function runBuild() {
137
137
  process.env.FLAME_BUILD_SILENT = "1";
138
138
  process.env.LOG_LEVEL = "error";
139
139
  }
140
- const { runBuildCli } = await import("./build.impl-TSIF3F7O.js");
140
+ const { runBuildCli } = await import("./build.impl-VXB4KL4D.js");
141
141
  await runBuildCli();
142
142
  }
143
143
  async function writeDockerFiles() {
@@ -145,7 +145,7 @@ async function writeDockerFiles() {
145
145
  if (!existsSync(join(dockerDir, "Dockerfile"))) {
146
146
  await writeFile(
147
147
  join(dockerDir, "Dockerfile"),
148
- `FROM ghcr.io/docubook/flame-builder:${FLAME_MAJOR} AS builder
148
+ `FROM ghcr.io/docubook/flame:${FLAME_MAJOR} AS builder
149
149
  ENV NODE_ENV=production
150
150
  WORKDIR /app
151
151
  COPY . .
@@ -9,12 +9,13 @@ import {
9
9
  compileMdx,
10
10
  computeInlineThemeCss,
11
11
  errorHtml,
12
+ frontmatterField,
12
13
  generateSearchIndex,
13
14
  hmrScript,
14
15
  htmlShell,
15
16
  initSentry,
16
17
  loadPlugins
17
- } from "./chunk-B6LGUADD.js";
18
+ } from "./chunk-TRT6WQZG.js";
18
19
  import {
19
20
  SECURITY_HEADERS,
20
21
  generateNonce,
@@ -27,7 +28,7 @@ import {
27
28
  } from "./chunk-EOK6KATZ.js";
28
29
  import {
29
30
  logger
30
- } from "./chunk-PJIJEPNR.js";
31
+ } from "./chunk-JRERMREW.js";
31
32
  import {
32
33
  DIST_DIR,
33
34
  DOCS_DIR,
@@ -36,10 +37,12 @@ import {
36
37
  } from "./chunk-4IQXHHPF.js";
37
38
 
38
39
  // .docu/node/server.impl.ts
39
- import { watch } from "node:fs";
40
+ import { watch, existsSync } from "node:fs";
41
+ import { dirname, join } from "node:path";
42
+ import { createRequire } from "node:module";
40
43
 
41
44
  // .docu/node/server-routes.ts
42
- import { readFile } from "node:fs/promises";
45
+ import { readFile, stat } from "node:fs/promises";
43
46
  import { resolve } from "node:path";
44
47
  import { readFileSync, statSync } from "node:fs";
45
48
  import React from "react";
@@ -57,10 +60,15 @@ function createHtmlResponse(title, description, body, status, state, depth = 0)
57
60
  nonce,
58
61
  extraScripts: hmrScript(nonce),
59
62
  themeCss: state.inlineThemeCss,
60
- depth
63
+ depth,
64
+ // 404 pages can be requested at arbitrary depths (e.g. a noLink section
65
+ // path typed in the address bar) — relative asset paths would resolve
66
+ // against the wrong directory and break CSS/JS.
67
+ absoluteAssets: status === 404
61
68
  });
62
69
  return htmlResponse(html, nonce, status, true);
63
70
  }
71
+ var docsCache = /* @__PURE__ */ new Map();
64
72
  async function getDocsForSlug(slug, state) {
65
73
  if (!isSlugSafe(slug, DOCS_DIR)) return null;
66
74
  const paths = [
@@ -87,6 +95,11 @@ async function getDocsForSlug(slug, state) {
87
95
  }
88
96
  if (!filePath || !raw) return null;
89
97
  const relPath = filePath.replace(PROJECT_ROOT + "/", "");
98
+ const mtimeMs = (await stat(filePath)).mtimeMs;
99
+ const cached = docsCache.get(relPath);
100
+ if (cached && cached.mtimeMs === mtimeMs) {
101
+ return cached.doc;
102
+ }
90
103
  let content = raw;
91
104
  if (state.builder) {
92
105
  const transformed = await state.builder.runOnLoad(relPath, content);
@@ -105,7 +118,7 @@ async function getDocsForSlug(slug, state) {
105
118
  content
106
119
  });
107
120
  }
108
- return {
121
+ const doc = {
109
122
  content: result.content,
110
123
  compiledSource: result.compiledSource,
111
124
  frontmatter,
@@ -113,10 +126,12 @@ async function getDocsForSlug(slug, state) {
113
126
  filePath: relPath,
114
127
  resolvedContent: content
115
128
  };
129
+ docsCache.set(relPath, { mtimeMs, doc });
130
+ return doc;
116
131
  }
117
132
  async function renderDocsServerPage(doc, slug, pathname, state) {
118
- const title = (typeof doc.frontmatter.title === "string" ? doc.frontmatter.title : "") || slug.join("/") || "Docs";
119
- const description = typeof doc.frontmatter.description === "string" ? doc.frontmatter.description : "";
133
+ const title = frontmatterField(doc.frontmatter, "title") || slug.join("/") || "Docs";
134
+ const description = frontmatterField(doc.frontmatter, "description");
120
135
  const page = React.createElement(
121
136
  DocsLayout,
122
137
  { repoUrl: state.docuConfig.repo?.url, pathname },
@@ -124,8 +139,9 @@ async function renderDocsServerPage(doc, slug, pathname, state) {
124
139
  slug,
125
140
  title,
126
141
  description,
127
- date: doc.frontmatter.date,
128
- content: doc.content,
142
+ date: frontmatterField(doc.frontmatter, "date") || void 0,
143
+ // Same root-relative SSR as build — see build.ts.
144
+ content: renderToString(doc.content),
129
145
  tocs: doc.tocs,
130
146
  filePath: doc.filePath,
131
147
  repoUrl: state.docuConfig.repo?.url,
@@ -283,6 +299,16 @@ async function runServer(adapter) {
283
299
  builder
284
300
  };
285
301
  const hmrClients = /* @__PURE__ */ new Set();
302
+ const require2 = createRequire(import.meta.url);
303
+ function resolveMdxContentSrc() {
304
+ try {
305
+ const pkgDir = dirname(require2.resolve("@docubook/markdown/package.json"));
306
+ const src = join(pkgDir, "src");
307
+ return existsSync(src) ? src : null;
308
+ } catch {
309
+ return null;
310
+ }
311
+ }
286
312
  let hmrTimeout = null;
287
313
  const watcher = watch(DOCS_DIR, { recursive: true }, (_event, filename) => {
288
314
  if (!filename || !filename.endsWith(".mdx") && !filename.endsWith(".md")) return;
@@ -297,12 +323,37 @@ async function runServer(adapter) {
297
323
  }
298
324
  }, 300);
299
325
  });
326
+ let srcHmrTimeout = null;
327
+ const mdxContentSrc = resolveMdxContentSrc();
328
+ const srcWatcher = mdxContentSrc ? watch(mdxContentSrc, { recursive: true }, (_event, filename) => {
329
+ if (!filename || !/\.(ts|tsx|js|jsx|css)$/.test(filename)) return;
330
+ if (srcHmrTimeout) clearTimeout(srcHmrTimeout);
331
+ srcHmrTimeout = setTimeout(async () => {
332
+ try {
333
+ state.assetManifest = await buildClientBundle();
334
+ logger.warn("[hmr] client bundle rebuilt (component change)");
335
+ } catch (e) {
336
+ logger.warn(
337
+ `[hmr] bundle rebuild failed: ${e instanceof Error ? e.message : String(e)}`
338
+ );
339
+ }
340
+ for (const client of [...hmrClients]) {
341
+ try {
342
+ client.enqueue(new TextEncoder().encode("data: reload\n\n"));
343
+ } catch {
344
+ hmrClients.delete(client);
345
+ }
346
+ }
347
+ }, 300);
348
+ }) : null;
300
349
  process.on("SIGINT", () => {
301
350
  watcher.close();
351
+ srcWatcher?.close();
302
352
  process.exit(0);
303
353
  });
304
354
  process.on("SIGTERM", () => {
305
355
  watcher.close();
356
+ srcWatcher?.close();
306
357
  process.exit(0);
307
358
  });
308
359
  let handle = null;
@@ -5,7 +5,7 @@ import {
5
5
  // package.json
6
6
  var package_default = {
7
7
  name: "@docubook/flame",
8
- version: "1.7.2",
8
+ version: "2.0.0-alpha.1",
9
9
  description: "A blazing-fast React + MDX framework powered by Bun, built for modern documentation experiences.",
10
10
  type: "module",
11
11
  bin: {
@@ -62,10 +62,10 @@ var package_default = {
62
62
  license: "MIT",
63
63
  dependencies: {
64
64
  "@docubook/core": "workspace:^",
65
- "@docubook/mdx-content": "workspace:^",
66
- "@docubook/runt": "workspace:^",
65
+ "@docubook/markdown": "workspace:^",
67
66
  "@docubook/themes-colors": "workspace:^",
68
67
  "@docubook/ui-react": "workspace:^",
68
+ "@mdx-js/react": "^3.0.1",
69
69
  "@tailwindcss/cli": "4.3.0",
70
70
  "@tailwindcss/typography": "0.5.16",
71
71
  "bun-plugin-tailwind": "^0.1.2",
@@ -74,7 +74,8 @@ var package_default = {
74
74
  "lucide-react": "^1.14.0",
75
75
  react: "^19.2.7",
76
76
  "react-dom": "^19.2.7",
77
- unified: "^11.0.0"
77
+ unified: "^11.0.0",
78
+ zod: "^4.4.3"
78
79
  },
79
80
  peerDependencies: {
80
81
  "@sentry/bun": "^10.0.0"
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-EOK6KATZ.js";
8
8
  import {
9
9
  logger
10
- } from "./chunk-PJIJEPNR.js";
10
+ } from "./chunk-JRERMREW.js";
11
11
  import {
12
12
  DIST_DIR
13
13
  } from "./chunk-4IQXHHPF.js";
@@ -58,7 +58,7 @@ async function runPreview(adapter) {
58
58
  headers: {
59
59
  "Content-Type": "text/html",
60
60
  ...SECURITY_HEADERS,
61
- "Content-Security-Policy": cspHeader(nonce, true)
61
+ "Content-Security-Policy": cspHeader(nonce)
62
62
  }
63
63
  });
64
64
  }
@@ -75,7 +75,7 @@ async function runPreview(adapter) {
75
75
  headers: {
76
76
  "Content-Type": "text/html",
77
77
  ...SECURITY_HEADERS,
78
- "Content-Security-Policy": cspHeader(nonce, true)
78
+ "Content-Security-Policy": cspHeader(nonce)
79
79
  }
80
80
  });
81
81
  }