@softize/opus 25.6.0 → 26.0.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.
- package/CHANGELOG.md +108 -1
- package/docs/adr/0017-ui-assumes-a-fixed-desktop-layout.md +19 -0
- package/docs/adr/0057-doc-browser-permite-busca-no-chrome-do-consumidor.md +152 -0
- package/docs/adr/0058-mensagem-da-ia-aceita-imagem-e-o-chat-recebe-anexos.md +201 -0
- package/docs/adr/README.md +12 -0
- package/package.json +1 -1
- package/src/ai/drivers/anthropic.ts +49 -9
- package/src/ai/index.ts +5 -2
- package/src/core/ai-message.ts +20 -0
- package/src/core/index.ts +5 -0
- package/src/core/types.ts +25 -2
- package/src/ui/components/patterns/sidebar.tsx +32 -11
- package/src/ui/components/primitives/chat.tsx +179 -22
- package/src/ui/components/primitives/composer.tsx +397 -7
- package/src/ui/docs/DocBrowser.tsx +404 -69
- package/src/ui/docs/DocCatalog.tsx +255 -0
- package/src/ui/docs/DocHeader.tsx +74 -0
- package/src/ui/docs/browser.tsx +10 -1
- package/src/ui/docs/changelog.tsx +16 -41
- package/src/ui/docs/content/actions.md +3 -3
- package/src/ui/docs/content/ai.md +46 -0
- package/src/ui/docs/content/chat.md +97 -2
- package/src/ui/docs/content/composer.md +144 -1
- package/src/ui/docs/content/customization.md +5 -1
- package/src/ui/docs/content/documentation-portal.md +190 -0
- package/src/ui/docs/content/getting-started.md +129 -87
- package/src/ui/docs/content/installation.md +94 -0
- package/src/ui/docs/content/mcp.md +2 -2
- package/src/ui/docs/content/overview.md +58 -0
- package/src/ui/docs/content/sidebar.md +4 -0
- package/src/ui/docs/index.ts +8 -1
- package/src/ui/docs/markdown.tsx +5 -3
- package/src/ui/docs/records.ts +144 -0
- package/src/ui/docs/registry.tsx +149 -59
- package/src/ui/docs/types.ts +2 -0
- package/src/ui/lib/prepare-image.ts +386 -0
- package/src/ui/meta.ts +2 -2
- package/src/ui/react.tsx +18 -1
|
@@ -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 |
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
97
|
-
section.groups.map((group
|
|
98
|
-
key:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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={
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
424
|
+
label={section.label}
|
|
425
|
+
expanded={sectionExpanded}
|
|
426
|
+
onToggle={
|
|
427
|
+
forceExpanded ? undefined : () => toggleGroup(sectionBranch)
|
|
428
|
+
}
|
|
163
429
|
onClick={() => {
|
|
164
|
-
if (!forceExpanded) toggleGroup(
|
|
430
|
+
if (!forceExpanded) toggleGroup(sectionBranch);
|
|
165
431
|
}}
|
|
166
432
|
/>
|
|
167
|
-
{
|
|
168
|
-
<SidebarTreeGroup>
|
|
169
|
-
{group.pages.map((page) => renderPage(page))}
|
|
170
|
-
</SidebarTreeGroup>
|
|
433
|
+
{sectionExpanded && (
|
|
434
|
+
<SidebarTreeGroup>{groups}</SidebarTreeGroup>
|
|
171
435
|
)}
|
|
172
|
-
|
|
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 [
|
|
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=
|
|
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={
|
|
235
|
-
onChange={(event) =>
|
|
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
|
|
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 ??
|
|
578
|
+
? (searchOptions.emptyMessage ??
|
|
579
|
+
"Nenhum documento encontrado. Tente outro termo.")
|
|
249
580
|
: ""}
|
|
250
581
|
</p>
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
264
|
-
|
|
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
|
);
|