@softize/opus 25.6.0 → 25.7.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.
@@ -12,11 +12,19 @@
12
12
  * Nos dois, navegar emite `popstate`: host que guarda o path por conta própria acompanha.
13
13
  * `basePath` adapta o prefixo das URLs (`/docs` no Maestro, `/__docs` nos apps).
14
14
  */
15
- import { useEffect, useMemo, useState } from "react";
16
- import { Search } from "lucide-react";
17
- import type { DocEntry, DocSection } from "./types.ts";
18
- import { Pane, Split } from "../components/patterns/split.tsx";
15
+ import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
16
+ import { ArrowLeft, ArrowRight, Search } from "lucide-react";
17
+ import type { DocEntry, DocGroup, DocSection } from "./types.ts";
18
+ import { Pane, Split, type PaneSize } from "../components/patterns/split.tsx";
19
19
  import { Input } from "../components/primitives/input.tsx";
20
+ import {
21
+ Breadcrumb,
22
+ BreadcrumbList,
23
+ BreadcrumbItem,
24
+ BreadcrumbLink,
25
+ BreadcrumbSeparator,
26
+ BreadcrumbPage,
27
+ } from "../components/primitives/breadcrumb.tsx";
20
28
  import {
21
29
  Sidebar,
22
30
  SidebarGroupLabel,
@@ -35,6 +43,16 @@ function findPage(sections: DocSection[], slug: string): DocEntry | undefined {
35
43
  return undefined;
36
44
  }
37
45
 
46
+ interface DocBrowserSearchCopy {
47
+ label?: string;
48
+ placeholder?: string;
49
+ emptyMessage?: string;
50
+ }
51
+
52
+ export type DocBrowserSearchOptions =
53
+ | (DocBrowserSearchCopy & { placement?: "sidebar" })
54
+ | (DocBrowserSearchCopy & { placement: "external"; value: string });
55
+
38
56
  export interface DocBrowserProps {
39
57
  /** Prefixo das rotas da doc. Default `/docs` (Maestro); `/__docs` na rota dev dos apps. */
40
58
  basePath?: string;
@@ -48,7 +66,202 @@ export interface DocBrowserProps {
48
66
  * Ausente = não mexe no title. */
49
67
  documentTitle?: (pageTitle: string) => string;
50
68
  /** Busca local por título, badge e `searchText` das páginas. Omitida = sem campo de busca. */
51
- search?: boolean | { label?: string; placeholder?: string; emptyMessage?: string };
69
+ search?: boolean | DocBrowserSearchOptions;
70
+ /** Largura inicial da navegação. Preserva `14rem` quando omitida. */
71
+ sidebarSize?: PaneSize;
72
+ /** Exibe a navegação lateral. Default `true`; destinos globais de uma página podem omiti-la. */
73
+ sidebar?: boolean;
74
+ /** Compõe leitura com contexto, sumário das seções e continuidade entre páginas. */
75
+ editorial?: boolean;
76
+ /** Nome da área no contexto da leitura editorial. */
77
+ navigationLabel?: string;
78
+ /** Páginas acessíveis por URL direta, mas fora da árvore e da sequência de leitura. */
79
+ additionalPages?: DocEntry[];
80
+ /** Substitui a leitura, mantendo a árvore; o host resolve conteúdo, título e rota. */
81
+ content?: ReactNode;
82
+ /** Destinos fora dos capítulos; não participam da sequência de leitura. */
83
+ sidebarLinks?: Array<{ label: string; href: string; active?: boolean }>;
84
+ /** Seções e grupos começam recolhidos; a busca revela resultados sem mudar a escolha manual. */
85
+ sidebarInitiallyCollapsed?: boolean;
86
+ }
87
+
88
+ function documentNavigation(
89
+ event: React.MouseEvent<HTMLAnchorElement>,
90
+ href: string,
91
+ ): void {
92
+ if (
93
+ event.button !== 0 ||
94
+ event.metaKey ||
95
+ event.ctrlKey ||
96
+ event.shiftKey ||
97
+ event.altKey
98
+ )
99
+ return;
100
+ event.preventDefault();
101
+ navigate(href);
102
+ }
103
+
104
+ function EditorialDocument({
105
+ page,
106
+ sections,
107
+ basePath,
108
+ navigationLabel,
109
+ }: {
110
+ page: DocEntry;
111
+ sections: DocSection[];
112
+ basePath: string;
113
+ navigationLabel: string;
114
+ }): React.ReactElement {
115
+ const articleRef = useRef<HTMLElement>(null);
116
+ const [headings, setHeadings] = useState<
117
+ Array<{ id: string; title: string }>
118
+ >([]);
119
+ const entries = sections.flatMap((section) =>
120
+ section.groups.flatMap((group) =>
121
+ group.pages.map((entry) => ({
122
+ entry,
123
+ context: group.label || section.label,
124
+ })),
125
+ ),
126
+ );
127
+ const index = entries.findIndex(({ entry }) => entry.slug === page.slug);
128
+ const previous = index > 0 ? entries[index - 1]?.entry : undefined;
129
+ const next = index >= 0 ? entries[index + 1]?.entry : undefined;
130
+ const context = entries[index]?.context;
131
+
132
+ useEffect(() => {
133
+ const article = articleRef.current;
134
+ if (!article) return;
135
+ const collect = (): void => {
136
+ const nextHeadings = Array.from(
137
+ article.querySelectorAll<HTMLHeadingElement>("h2[id]"),
138
+ ).map((heading) => ({
139
+ id: heading.id,
140
+ title: heading.textContent ?? "",
141
+ }));
142
+ // Blocos renderizados podem renovar o DOM sem mudar o sumário. Não realimentar esse ciclo.
143
+ setHeadings((current) =>
144
+ current.length === nextHeadings.length &&
145
+ current.every(
146
+ (heading, index) =>
147
+ heading.id === nextHeadings[index]?.id &&
148
+ heading.title === nextHeadings[index]?.title,
149
+ )
150
+ ? current
151
+ : nextHeadings,
152
+ );
153
+ };
154
+ collect();
155
+ const observer = new MutationObserver(collect);
156
+ observer.observe(article, { childList: true, subtree: true });
157
+ if (window.location.hash) {
158
+ const heading = Array.from(
159
+ article.querySelectorAll<HTMLElement>("[id]"),
160
+ ).find(
161
+ (element) =>
162
+ `#${encodeURIComponent(element.id)}` === window.location.hash ||
163
+ `#${element.id}` === window.location.hash,
164
+ );
165
+ heading?.scrollIntoView?.();
166
+ }
167
+ return () => observer.disconnect();
168
+ }, [page.slug]);
169
+
170
+ const chapterLink = (
171
+ entry: DocEntry,
172
+ direction: "previous" | "next",
173
+ ): React.ReactElement => {
174
+ const href = `${basePath}/${entry.slug}`;
175
+ return (
176
+ <a
177
+ href={href}
178
+ onClick={(event) => documentNavigation(event, href)}
179
+ className={`group flex flex-col gap-2 rounded-lg border border-border p-5 text-sm transition-colors hover:border-primary/40 hover:bg-muted/30 ${direction === "next" ? "text-right" : ""}`}
180
+ >
181
+ <span className="text-xs text-muted-foreground">
182
+ {direction === "previous" ? "Leitura anterior" : "Próxima leitura"}
183
+ </span>
184
+ <span
185
+ className={`flex items-center gap-2 font-medium ${direction === "next" ? "justify-end" : ""}`}
186
+ >
187
+ {direction === "previous" && (
188
+ <ArrowLeft aria-hidden="true" className="size-4" />
189
+ )}
190
+ {entry.title}
191
+ {direction === "next" && (
192
+ <ArrowRight aria-hidden="true" className="size-4" />
193
+ )}
194
+ </span>
195
+ </a>
196
+ );
197
+ };
198
+
199
+ return (
200
+ <div
201
+ data-slot="doc-editorial"
202
+ className="mx-auto grid max-w-[78rem] grid-cols-1 gap-12 px-8 py-10 xl:grid-cols-[minmax(0,1fr)_12rem] xl:px-12"
203
+ >
204
+ <div className="min-w-0">
205
+ <Breadcrumb className="mb-8">
206
+ <BreadcrumbList>
207
+ <BreadcrumbItem>
208
+ <BreadcrumbLink
209
+ href={basePath}
210
+ onClick={(event) => documentNavigation(event, basePath)}
211
+ >
212
+ {navigationLabel}
213
+ </BreadcrumbLink>
214
+ </BreadcrumbItem>
215
+ {context && (
216
+ <>
217
+ <BreadcrumbSeparator />
218
+ <BreadcrumbItem>
219
+ <BreadcrumbPage>{context}</BreadcrumbPage>
220
+ </BreadcrumbItem>
221
+ </>
222
+ )}
223
+ </BreadcrumbList>
224
+ </Breadcrumb>
225
+ <article
226
+ ref={articleRef}
227
+ aria-label={page.title}
228
+ className="min-w-0 [&_h1]:text-[2.25rem] [&_h1]:leading-tight [&_h1]:tracking-tight [&_h2]:mt-12 [&_h2]:scroll-mt-8 [&_h2]:text-xl [&_h3]:scroll-mt-8 [&_[data-slot=markdown]]:text-base [&_[data-slot=markdown]]:leading-[1.8] [&_pre]:text-sm [&_table]:text-sm"
229
+ >
230
+ {page.render()}
231
+ </article>
232
+ {(previous || next) && (
233
+ <nav
234
+ aria-label="Sequência de leitura"
235
+ className="mt-14 grid grid-cols-2 gap-4 border-t border-border pt-8"
236
+ >
237
+ {previous ? chapterLink(previous, "previous") : <span />}
238
+ {next && chapterLink(next, "next")}
239
+ </nav>
240
+ )}
241
+ </div>
242
+ {headings.length > 0 && (
243
+ <aside className="hidden xl:block">
244
+ <nav
245
+ aria-label="Nesta página"
246
+ className="sticky top-8 border-l border-border pl-5"
247
+ >
248
+ <p className="mb-4 text-xs font-semibold text-foreground">
249
+ Nesta página
250
+ </p>
251
+ <ul className="space-y-3 text-xs leading-relaxed text-muted-foreground">
252
+ {headings.map((heading) => (
253
+ <li key={heading.id}>
254
+ <a href={`#${heading.id}`} className="hover:text-foreground">
255
+ {heading.title}
256
+ </a>
257
+ </li>
258
+ ))}
259
+ </ul>
260
+ </nav>
261
+ </aside>
262
+ )}
263
+ </div>
264
+ );
52
265
  }
53
266
 
54
267
  function searchable(value: string): string {
@@ -58,14 +271,31 @@ function searchable(value: string): string {
58
271
  .toLocaleLowerCase("pt-BR");
59
272
  }
60
273
 
61
- function filterSections(sections: DocSection[], query: string): DocSection[] {
274
+ type SidebarSection = Omit<DocSection, "groups"> & {
275
+ navigationKey: string;
276
+ groups: Array<DocGroup & { navigationKey: string }>;
277
+ };
278
+
279
+ function filterSections(
280
+ sections: DocSection[],
281
+ query: string,
282
+ ): SidebarSection[] {
62
283
  const terms = searchable(query).split(/\s+/).filter(Boolean);
63
- if (terms.length === 0) return sections;
64
- return sections.flatMap((section) => {
284
+ // Identidade vem do manifesto completo, antes de remover ramos pela busca.
285
+ const indexedSections = sections.map((section, sectionIndex) => ({
286
+ ...section,
287
+ navigationKey: `section:${sectionIndex}`,
288
+ groups: section.groups.map((group, groupIndex) => ({
289
+ ...group,
290
+ navigationKey: `group:${sectionIndex}:${groupIndex}`,
291
+ })),
292
+ }));
293
+ if (terms.length === 0) return indexedSections;
294
+ return indexedSections.flatMap((section) => {
65
295
  const groups = section.groups.flatMap((group) => {
66
296
  const pages = group.pages.filter((page) => {
67
297
  const text = searchable(
68
- `${page.title}\n${page.badge ?? ""}\n${page.searchText ?? ""}`,
298
+ `${page.title}\n${page.navigationTitle ?? ""}\n${page.badge ?? ""}\n${page.searchText ?? ""}`,
69
299
  );
70
300
  return terms.every((term) => text.includes(term));
71
301
  });
@@ -80,22 +310,27 @@ function DocSidebarTree({
80
310
  activeSlug,
81
311
  onSelect,
82
312
  forceExpanded = false,
313
+ initiallyCollapsed = false,
314
+ links = [],
83
315
  }: {
84
- sections: DocSection[];
316
+ sections: SidebarSection[];
85
317
  activeSlug?: string;
86
318
  onSelect: (slug: string) => void;
87
319
  forceExpanded?: boolean;
320
+ initiallyCollapsed?: boolean;
321
+ links?: DocBrowserProps["sidebarLinks"];
88
322
  }): React.ReactElement {
89
- const [closedGroups, setClosedGroups] = useState<Set<string>>(
323
+ // O conjunto guarda os ramos alterados pela pessoa em relação ao estado inicial.
324
+ const [toggledBranches, setToggledBranches] = useState<Set<string>>(
90
325
  () => new Set(),
91
326
  );
92
327
 
93
328
  useEffect(() => {
94
- if (forceExpanded) return;
329
+ if (forceExpanded || initiallyCollapsed) return;
95
330
  const activeGroup = sections
96
- .flatMap((section, sectionIndex) =>
97
- section.groups.map((group, groupIndex) => ({
98
- key: `${sectionIndex}:${groupIndex}`,
331
+ .flatMap((section) =>
332
+ section.groups.map((group) => ({
333
+ key: group.navigationKey,
99
334
  group,
100
335
  })),
101
336
  )
@@ -104,16 +339,16 @@ function DocSidebarTree({
104
339
  );
105
340
 
106
341
  if (activeGroup === undefined) return;
107
- setClosedGroups((current) => {
342
+ setToggledBranches((current) => {
108
343
  if (!current.has(activeGroup.key)) return current;
109
344
  const next = new Set(current);
110
345
  next.delete(activeGroup.key);
111
346
  return next;
112
347
  });
113
- }, [activeSlug, forceExpanded, sections]);
348
+ }, [activeSlug, forceExpanded, sections, initiallyCollapsed]);
114
349
 
115
350
  const toggleGroup = (key: string): void => {
116
- setClosedGroups((current) => {
351
+ setToggledBranches((current) => {
117
352
  const next = new Set(current);
118
353
  if (next.has(key)) next.delete(key);
119
354
  else next.add(key);
@@ -124,7 +359,7 @@ function DocSidebarTree({
124
359
  const renderPage = (page: DocEntry): React.ReactElement => (
125
360
  <SidebarItem
126
361
  key={page.slug}
127
- label={page.title}
362
+ label={page.navigationTitle ?? page.title}
128
363
  badge={
129
364
  page.badge === undefined ? undefined : (
130
365
  <span className="rounded border border-border/60 px-1 font-mono text-[0.625rem] leading-tight text-muted-foreground/60">
@@ -138,42 +373,92 @@ function DocSidebarTree({
138
373
  );
139
374
 
140
375
  return (
141
- <nav aria-label="Navegação da documentação" className="space-y-4 p-2">
142
- {sections.map((section, sectionIndex) => (
143
- <div key={`${section.label}:${sectionIndex}`} className="space-y-0.5">
144
- {section.label.trim() !== "" && (
145
- <SidebarGroupLabel>{section.label}</SidebarGroupLabel>
146
- )}
147
- {section.groups.map((group, groupIndex) => {
148
- const key = `${sectionIndex}:${groupIndex}`;
149
- if (group.pages.length === 0) return null;
150
- if ((group.label ?? "").trim() === "") {
151
- return group.pages.map((page) => renderPage(page));
152
- }
376
+ <nav
377
+ aria-label="Navegação da documentação"
378
+ hidden={sections.length === 0 && links.length === 0}
379
+ className={initiallyCollapsed ? "space-y-0.5 p-2" : "space-y-4 p-2"}
380
+ >
381
+ {sections.map((section) => {
382
+ const sectionBranch = section.navigationKey;
383
+ const collapsibleSection =
384
+ initiallyCollapsed && section.label.trim() !== "";
385
+ const sectionExpanded =
386
+ forceExpanded || toggledBranches.has(sectionBranch);
387
+ const groups = section.groups.map((group) => {
388
+ const key = group.navigationKey;
389
+ if (group.pages.length === 0) return null;
390
+ if ((group.label ?? "").trim() === "") {
391
+ return group.pages.map((page) => renderPage(page));
392
+ }
153
393
 
154
- const expanded = forceExpanded || !closedGroups.has(key);
155
- return (
156
- <div key={key} className="space-y-0.5">
394
+ const expanded =
395
+ forceExpanded ||
396
+ (initiallyCollapsed
397
+ ? toggledBranches.has(key)
398
+ : !toggledBranches.has(key));
399
+ return (
400
+ <div key={key} className="space-y-0.5">
401
+ <SidebarItem
402
+ label={group.label!}
403
+ icon={group.icon}
404
+ expanded={expanded}
405
+ onToggle={forceExpanded ? undefined : () => toggleGroup(key)}
406
+ // Durante a busca, o grupo é contexto dos resultados e permanece aberto.
407
+ onClick={() => {
408
+ if (!forceExpanded) toggleGroup(key);
409
+ }}
410
+ />
411
+ {expanded && (
412
+ <SidebarTreeGroup>
413
+ {group.pages.map((page) => renderPage(page))}
414
+ </SidebarTreeGroup>
415
+ )}
416
+ </div>
417
+ );
418
+ });
419
+ return (
420
+ <div key={sectionBranch} className="space-y-0.5">
421
+ {collapsibleSection ? (
422
+ <>
157
423
  <SidebarItem
158
- label={group.label!}
159
- icon={group.icon}
160
- expanded={expanded}
161
- onToggle={forceExpanded ? undefined : () => toggleGroup(key)}
162
- // Durante a busca, o grupo é contexto dos resultados e permanece aberto.
424
+ label={section.label}
425
+ expanded={sectionExpanded}
426
+ onToggle={
427
+ forceExpanded ? undefined : () => toggleGroup(sectionBranch)
428
+ }
163
429
  onClick={() => {
164
- if (!forceExpanded) toggleGroup(key);
430
+ if (!forceExpanded) toggleGroup(sectionBranch);
165
431
  }}
166
432
  />
167
- {expanded && (
168
- <SidebarTreeGroup>
169
- {group.pages.map((page) => renderPage(page))}
170
- </SidebarTreeGroup>
433
+ {sectionExpanded && (
434
+ <SidebarTreeGroup>{groups}</SidebarTreeGroup>
171
435
  )}
172
- </div>
173
- );
174
- })}
436
+ </>
437
+ ) : (
438
+ <>
439
+ {section.label.trim() !== "" && (
440
+ <SidebarGroupLabel>{section.label}</SidebarGroupLabel>
441
+ )}
442
+ {groups}
443
+ </>
444
+ )}
445
+ </div>
446
+ );
447
+ })}
448
+ {links.length > 0 && (
449
+ <div className="space-y-0.5">
450
+ {links.map((link) => (
451
+ <SidebarItem
452
+ key={link.href}
453
+ href={link.href}
454
+ active={link.active}
455
+ onClick={() => navigate(link.href)}
456
+ label={link.label}
457
+ icon={initiallyCollapsed ? <span aria-hidden="true" /> : undefined}
458
+ />
459
+ ))}
175
460
  </div>
176
- ))}
461
+ )}
177
462
  </nav>
178
463
  );
179
464
  }
@@ -185,8 +470,16 @@ export function DocBrowser({
185
470
  redirects = {},
186
471
  documentTitle,
187
472
  search = false,
473
+ sidebarSize = "14rem",
474
+ sidebar = true,
475
+ editorial = false,
476
+ navigationLabel = "Documentação",
477
+ additionalPages = [],
478
+ content,
479
+ sidebarLinks = [],
480
+ sidebarInitiallyCollapsed = false,
188
481
  }: DocBrowserProps): React.ReactElement {
189
- const [query, setQuery] = useState("");
482
+ const [sidebarQuery, setSidebarQuery] = useState("");
190
483
  // Standalone: o path vem do router da casa (`usePathname` — pathname É o estado).
191
484
  // Controlado: o host manda, e este valor é ignorado. O hook roda sempre (regra dos
192
485
  // hooks); o que muda é quem vence no `??`.
@@ -199,30 +492,65 @@ export function DocBrowser({
199
492
  const rel = path.startsWith(basePath) ? path.slice(basePath.length) : "";
200
493
  const requestedSlug = rel.split("/").filter(Boolean).join("/") || defaultSlug;
201
494
  const slug = redirects[requestedSlug] ?? requestedSlug;
202
- const page = findPage(sections, slug) ?? findPage(sections, defaultSlug);
203
- const visibleSections = useMemo(() => filterSections(sections, query), [query, sections]);
204
495
  const searchOptions = typeof search === "object" ? search : {};
496
+ const query =
497
+ searchOptions.placement === "external" ? searchOptions.value : sidebarQuery;
498
+ const visibleSections = useMemo(
499
+ () => filterSections(sections, query),
500
+ [query, sections],
501
+ );
502
+ const hasCustomContent = content !== undefined;
503
+ const page = hasCustomContent
504
+ ? undefined
505
+ : (findPage(sections, slug) ??
506
+ additionalPages.find((entry) => entry.slug === slug) ??
507
+ findPage(sections, defaultSlug));
205
508
 
206
509
  // `navigate` emite `popstate`, então o host é notificado nos dois modos — inclusive o
207
510
  // que ainda escuta o evento na unha, que era como este componente avisava antes.
208
511
  const go = (to: string): void => navigate(to);
209
512
 
210
513
  useEffect(() => {
211
- if (requestedSlug !== slug)
514
+ if (!hasCustomContent && requestedSlug !== slug)
212
515
  navigate(`${basePath}/${slug}`, { replace: true });
213
- }, [basePath, requestedSlug, slug]);
516
+ }, [basePath, requestedSlug, slug, hasCustomContent]);
214
517
 
215
518
  // O title da aba acompanha a página atual (quando o host fornece o formato).
216
519
  useEffect(() => {
217
520
  if (page && documentTitle) document.title = documentTitle(page.title);
218
521
  }, [page, documentTitle]);
219
522
 
523
+ if (!sidebar) {
524
+ return (
525
+ <div className="h-full overflow-y-auto">
526
+ {hasCustomContent ? (
527
+ content
528
+ ) : (
529
+ <div className="mx-auto max-w-3xl px-8 py-8">{page?.render()}</div>
530
+ )}
531
+ </div>
532
+ );
533
+ }
534
+
535
+ const readingContent = hasCustomContent ? (
536
+ content
537
+ ) : editorial && page ? (
538
+ <EditorialDocument
539
+ page={page}
540
+ sections={sections}
541
+ basePath={basePath}
542
+ navigationLabel={navigationLabel}
543
+ />
544
+ ) : (
545
+ <div className="mx-auto max-w-3xl px-8 py-8">{page?.render()}</div>
546
+ );
547
+
220
548
  return (
221
549
  <Split className="h-full">
222
- <Pane initialSize="14rem">
550
+ <Pane initialSize={sidebarSize}>
223
551
  <Sidebar className="w-full">
224
552
  <div className="flex min-h-0 flex-1 flex-col">
225
- {search !== false && (
553
+ {search !== false && searchOptions.placement !== "external" && (
226
554
  <div className="shrink-0 border-b border-border p-3">
227
555
  <Input
228
556
  type="search"
@@ -231,37 +559,44 @@ export function DocBrowser({
231
559
  placeholder={
232
560
  searchOptions.placeholder ?? "Buscar na documentação…"
233
561
  }
234
- value={query}
235
- onChange={(event) => setQuery(event.target.value)}
562
+ value={sidebarQuery}
563
+ onChange={(event) => setSidebarQuery(event.target.value)}
236
564
  />
237
565
  </div>
238
566
  )}
239
567
  <div className="min-h-0 flex-1 overflow-y-auto px-3 py-6">
240
568
  <p
241
569
  className={
242
- visibleSections.length === 0 ? "px-2 text-sm text-muted-foreground" : "sr-only"
570
+ visibleSections.length === 0
571
+ ? "px-2 text-sm text-muted-foreground"
572
+ : "sr-only"
243
573
  }
244
574
  role={search === false ? undefined : "status"}
245
575
  aria-atomic={search === false ? undefined : "true"}
246
576
  >
247
577
  {visibleSections.length === 0
248
- ? searchOptions.emptyMessage ?? "Nenhum documento encontrado. Tente outro termo."
578
+ ? (searchOptions.emptyMessage ??
579
+ "Nenhum documento encontrado. Tente outro termo.")
249
580
  : ""}
250
581
  </p>
251
- {visibleSections.length > 0 && (
252
- <DocSidebarTree
253
- sections={visibleSections}
254
- activeSlug={page?.slug}
255
- forceExpanded={query.trim() !== ""}
256
- onSelect={(nextSlug) => go(`${basePath}/${nextSlug}`)}
257
- />
258
- )}
582
+ <DocSidebarTree
583
+ sections={visibleSections}
584
+ activeSlug={page?.slug}
585
+ forceExpanded={query.trim() !== ""}
586
+ initiallyCollapsed={sidebarInitiallyCollapsed}
587
+ links={sidebarLinks}
588
+ onSelect={(nextSlug) => go(`${basePath}/${nextSlug}`)}
589
+ />
259
590
  </div>
260
591
  </div>
261
592
  </Sidebar>
262
593
  </Pane>
263
- <Pane key={page?.slug} grow className="overflow-y-auto">
264
- <div className="mx-auto max-w-3xl px-8 py-8">{page?.render()}</div>
594
+ <Pane
595
+ key={hasCustomContent ? path : page?.slug}
596
+ grow
597
+ className="overflow-y-auto"
598
+ >
599
+ {readingContent}
265
600
  </Pane>
266
601
  </Split>
267
602
  );