@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.
- package/CHANGELOG.md +34 -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/README.md +12 -0
- package/package.json +1 -1
- package/src/ui/components/patterns/sidebar.tsx +32 -11
- 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/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
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { useEffect, type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import { Search } from "lucide-react";
|
|
3
|
+
import { Input } from "../components/primitives/input.tsx";
|
|
4
|
+
import { Badge } from "../components/primitives/badge.tsx";
|
|
5
|
+
import {
|
|
6
|
+
Alert,
|
|
7
|
+
AlertHeader,
|
|
8
|
+
AlertTitle,
|
|
9
|
+
AlertDescription,
|
|
10
|
+
} from "../components/primitives/alert.tsx";
|
|
11
|
+
import { Markdown, markdownIt } from "../components/primitives/markdown.tsx";
|
|
12
|
+
import {
|
|
13
|
+
filterDocumentationRecords,
|
|
14
|
+
type DocumentationRecord,
|
|
15
|
+
} from "./records.ts";
|
|
16
|
+
import { navigate } from "../router.ts";
|
|
17
|
+
|
|
18
|
+
function follow(
|
|
19
|
+
event: React.MouseEvent<HTMLAnchorElement>,
|
|
20
|
+
href: string,
|
|
21
|
+
): void {
|
|
22
|
+
if (
|
|
23
|
+
event.button !== 0 ||
|
|
24
|
+
event.metaKey ||
|
|
25
|
+
event.ctrlKey ||
|
|
26
|
+
event.shiftKey ||
|
|
27
|
+
event.altKey
|
|
28
|
+
)
|
|
29
|
+
return;
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
navigate(href);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function DocumentationLink({
|
|
35
|
+
href,
|
|
36
|
+
children,
|
|
37
|
+
}: {
|
|
38
|
+
href: string;
|
|
39
|
+
children: ReactNode;
|
|
40
|
+
}): ReactElement {
|
|
41
|
+
return (
|
|
42
|
+
<a
|
|
43
|
+
href={href}
|
|
44
|
+
onClick={(event) => follow(event, href)}
|
|
45
|
+
className="underline-offset-4 hover:underline"
|
|
46
|
+
>
|
|
47
|
+
{children}
|
|
48
|
+
</a>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** O resumo orienta a escolha; links e formatação continuam no documento completo. */
|
|
53
|
+
function summaryText(source: string): string {
|
|
54
|
+
return (markdownIt.parseInline(source, {})[0]?.children ?? [])
|
|
55
|
+
.map((token) =>
|
|
56
|
+
token.type === "softbreak" || token.type === "hardbreak"
|
|
57
|
+
? " "
|
|
58
|
+
: token.content,
|
|
59
|
+
)
|
|
60
|
+
.join("");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface DocCatalogProps {
|
|
64
|
+
title: string;
|
|
65
|
+
description: string;
|
|
66
|
+
records: readonly DocumentationRecord[];
|
|
67
|
+
/** Consulta local do catálogo, independente da busca documental do cabeçalho. */
|
|
68
|
+
query: string;
|
|
69
|
+
onQueryChange: (query: string) => void;
|
|
70
|
+
kind: "decisions" | "releases";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Leitura dos arquivos publicados, com busca textual e sem controles de coleção. */
|
|
74
|
+
export function DocCatalog({
|
|
75
|
+
title,
|
|
76
|
+
description,
|
|
77
|
+
records,
|
|
78
|
+
query,
|
|
79
|
+
onQueryChange,
|
|
80
|
+
kind,
|
|
81
|
+
}: DocCatalogProps): ReactElement {
|
|
82
|
+
const filtered = filterDocumentationRecords(records, query).sort(
|
|
83
|
+
(left, right) =>
|
|
84
|
+
kind === "decisions"
|
|
85
|
+
? Number(right.id) - Number(left.id)
|
|
86
|
+
: right.id.localeCompare(left.id, "en", { numeric: true }),
|
|
87
|
+
);
|
|
88
|
+
return (
|
|
89
|
+
<div data-slot="doc-catalog" className="h-full overflow-y-auto">
|
|
90
|
+
<section className="mx-auto w-full max-w-3xl px-4 py-8 md:px-8 md:py-12">
|
|
91
|
+
<h1 className="text-3xl font-semibold tracking-tight">{title}</h1>
|
|
92
|
+
<p className="mt-3 text-base text-muted-foreground">{description}</p>
|
|
93
|
+
<div className="mt-6 max-w-[28rem]">
|
|
94
|
+
<Input
|
|
95
|
+
type="search"
|
|
96
|
+
icon={<Search aria-hidden="true" />}
|
|
97
|
+
aria-label={
|
|
98
|
+
kind === "decisions" ? "Buscar nas ADRs" : "Buscar no changelog"
|
|
99
|
+
}
|
|
100
|
+
placeholder={
|
|
101
|
+
kind === "decisions" ? "Buscar nas ADRs…" : "Buscar no changelog…"
|
|
102
|
+
}
|
|
103
|
+
value={query}
|
|
104
|
+
onChange={(event) => onQueryChange(event.target.value)}
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
<p
|
|
108
|
+
role="status"
|
|
109
|
+
aria-live="polite"
|
|
110
|
+
aria-atomic="true"
|
|
111
|
+
className={
|
|
112
|
+
query.trim() ? "mt-5 text-sm text-muted-foreground" : "sr-only"
|
|
113
|
+
}
|
|
114
|
+
>
|
|
115
|
+
{filtered.length} de {records.length} registros
|
|
116
|
+
</p>
|
|
117
|
+
<div className="mt-10 space-y-10">
|
|
118
|
+
{filtered.map((record) => (
|
|
119
|
+
<article
|
|
120
|
+
key={record.href}
|
|
121
|
+
data-slot="doc-catalog-entry"
|
|
122
|
+
className="border-t border-border/60 pt-6"
|
|
123
|
+
>
|
|
124
|
+
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-2">
|
|
125
|
+
<h2 className="text-xl font-semibold tracking-tight">
|
|
126
|
+
<DocumentationLink href={record.href}>
|
|
127
|
+
{kind === "decisions"
|
|
128
|
+
? `ADR ${record.id} — ${record.title}`
|
|
129
|
+
: record.id}
|
|
130
|
+
</DocumentationLink>
|
|
131
|
+
</h2>
|
|
132
|
+
{kind === "releases" && (
|
|
133
|
+
<span className="text-sm text-muted-foreground">
|
|
134
|
+
{record.date ?? "Data não informada"}
|
|
135
|
+
</span>
|
|
136
|
+
)}
|
|
137
|
+
{kind === "releases" && record.breaking !== undefined && (
|
|
138
|
+
<Badge context="danger" variant="solid">
|
|
139
|
+
Incompatível
|
|
140
|
+
</Badge>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
{kind === "decisions" ? (
|
|
144
|
+
<>
|
|
145
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
146
|
+
{record.date ?? "Data não informada"} ·{" "}
|
|
147
|
+
{record.status ?? "Status não informado"}
|
|
148
|
+
</p>
|
|
149
|
+
{record.summary && (
|
|
150
|
+
<p className="mt-3 text-sm leading-7">
|
|
151
|
+
{summaryText(record.summary)}
|
|
152
|
+
</p>
|
|
153
|
+
)}
|
|
154
|
+
</>
|
|
155
|
+
) : (
|
|
156
|
+
<>
|
|
157
|
+
<div className="mt-3">
|
|
158
|
+
<Markdown
|
|
159
|
+
content={record.content ?? record.body}
|
|
160
|
+
resolveHref={(href) =>
|
|
161
|
+
href.startsWith("#") ? `${record.href}${href}` : href
|
|
162
|
+
}
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
{record.breaking !== undefined && (
|
|
166
|
+
<Alert context="danger" role="note" className="mt-4">
|
|
167
|
+
<AlertHeader>
|
|
168
|
+
<AlertTitle>Breaking: cuidados ao atualizar</AlertTitle>
|
|
169
|
+
<AlertDescription>
|
|
170
|
+
<Markdown
|
|
171
|
+
content={record.breaking}
|
|
172
|
+
resolveHref={(href) =>
|
|
173
|
+
href.startsWith("#") ? `${record.href}${href}` : href
|
|
174
|
+
}
|
|
175
|
+
/>
|
|
176
|
+
</AlertDescription>
|
|
177
|
+
</AlertHeader>
|
|
178
|
+
</Alert>
|
|
179
|
+
)}
|
|
180
|
+
{record.afterBreaking && (
|
|
181
|
+
<div className="mt-3">
|
|
182
|
+
<Markdown
|
|
183
|
+
content={record.afterBreaking}
|
|
184
|
+
resolveHref={(href) =>
|
|
185
|
+
href.startsWith("#") ? `${record.href}${href}` : href
|
|
186
|
+
}
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
190
|
+
</>
|
|
191
|
+
)}
|
|
192
|
+
</article>
|
|
193
|
+
))}
|
|
194
|
+
{filtered.length === 0 && (
|
|
195
|
+
<p className="border-t border-border/60 pt-6 text-sm text-muted-foreground">
|
|
196
|
+
Nenhum registro encontrado. Tente outro termo.
|
|
197
|
+
</p>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
</section>
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface DocRecordDetailProps {
|
|
206
|
+
catalogLabel: string;
|
|
207
|
+
children: ReactNode;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function DocRecordDetail({
|
|
211
|
+
catalogLabel,
|
|
212
|
+
children,
|
|
213
|
+
}: DocRecordDetailProps): ReactElement {
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
let frame: number | undefined;
|
|
216
|
+
let locationKey: string | undefined;
|
|
217
|
+
const scrollToFragment = () => {
|
|
218
|
+
const nextKey = window.location.pathname + window.location.hash;
|
|
219
|
+
if (nextKey === locationKey) return;
|
|
220
|
+
locationKey = nextKey;
|
|
221
|
+
if (frame !== undefined) cancelAnimationFrame(frame);
|
|
222
|
+
if (!window.location.hash) return;
|
|
223
|
+
frame = requestAnimationFrame(() => {
|
|
224
|
+
let id = window.location.hash.slice(1);
|
|
225
|
+
try {
|
|
226
|
+
id = decodeURIComponent(id);
|
|
227
|
+
} catch {
|
|
228
|
+
/* Um fragmento inválido não interrompe a leitura. */
|
|
229
|
+
}
|
|
230
|
+
document.getElementById(id)?.scrollIntoView({ block: "start" });
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
scrollToFragment();
|
|
234
|
+
window.addEventListener("popstate", scrollToFragment);
|
|
235
|
+
window.addEventListener("hashchange", scrollToFragment);
|
|
236
|
+
return () => {
|
|
237
|
+
if (frame !== undefined) cancelAnimationFrame(frame);
|
|
238
|
+
window.removeEventListener("popstate", scrollToFragment);
|
|
239
|
+
window.removeEventListener("hashchange", scrollToFragment);
|
|
240
|
+
};
|
|
241
|
+
}, []);
|
|
242
|
+
return (
|
|
243
|
+
<div data-slot="doc-record-detail" className="h-full overflow-y-auto">
|
|
244
|
+
<div className="mx-auto max-w-3xl px-4 py-8 md:px-8">
|
|
245
|
+
<p
|
|
246
|
+
data-slot="doc-record-context"
|
|
247
|
+
className="mb-3 text-sm text-muted-foreground"
|
|
248
|
+
>
|
|
249
|
+
{catalogLabel}
|
|
250
|
+
</p>
|
|
251
|
+
<article>{children}</article>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ComponentProps,
|
|
3
|
+
CSSProperties,
|
|
4
|
+
ReactElement,
|
|
5
|
+
ReactNode,
|
|
6
|
+
} from "react";
|
|
7
|
+
import { cn } from "../lib/cn.ts";
|
|
8
|
+
|
|
9
|
+
export interface DocHeaderProps extends Omit<
|
|
10
|
+
ComponentProps<"header">,
|
|
11
|
+
"children"
|
|
12
|
+
> {
|
|
13
|
+
brand: ReactNode;
|
|
14
|
+
search?: ReactNode;
|
|
15
|
+
actions?: ReactNode;
|
|
16
|
+
navigation?: ReactNode;
|
|
17
|
+
/** Largura da coluna de marca, normalmente igual à lateral. Default: 17rem. */
|
|
18
|
+
brandWidth?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Topo documental: controles alinhados e navegação global em uma linha própria. */
|
|
22
|
+
export function DocHeader({
|
|
23
|
+
brand,
|
|
24
|
+
search,
|
|
25
|
+
actions,
|
|
26
|
+
navigation,
|
|
27
|
+
brandWidth = "17rem",
|
|
28
|
+
className,
|
|
29
|
+
style,
|
|
30
|
+
...props
|
|
31
|
+
}: DocHeaderProps): ReactElement {
|
|
32
|
+
return (
|
|
33
|
+
<header
|
|
34
|
+
data-slot="doc-header"
|
|
35
|
+
className={cn("shrink-0 border-b border-border bg-background", className)}
|
|
36
|
+
style={
|
|
37
|
+
{ "--doc-header-brand-width": brandWidth, ...style } as CSSProperties
|
|
38
|
+
}
|
|
39
|
+
{...props}
|
|
40
|
+
>
|
|
41
|
+
<div
|
|
42
|
+
data-slot="doc-header-controls"
|
|
43
|
+
className="grid min-h-[4.25rem] grid-cols-[minmax(0,1fr)_auto] items-center gap-x-8 gap-y-4 px-4 py-3 md:px-8 lg:grid-cols-[var(--doc-header-brand-width)_minmax(0,1fr)_var(--doc-header-brand-width)]"
|
|
44
|
+
>
|
|
45
|
+
<div data-slot="doc-header-brand" className="min-w-0">
|
|
46
|
+
{brand}
|
|
47
|
+
</div>
|
|
48
|
+
<div
|
|
49
|
+
data-slot="doc-header-search"
|
|
50
|
+
className={cn(
|
|
51
|
+
"col-span-2 row-start-2 min-w-0 lg:col-span-1 lg:col-start-2 lg:row-start-1",
|
|
52
|
+
search == null && "hidden lg:block",
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
<div className="mx-auto w-full max-w-[22rem]">{search}</div>
|
|
56
|
+
</div>
|
|
57
|
+
<div
|
|
58
|
+
data-slot="doc-header-actions"
|
|
59
|
+
className="col-start-2 row-start-1 flex items-center justify-end gap-1 lg:col-start-3"
|
|
60
|
+
>
|
|
61
|
+
{actions}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
{navigation != null && (
|
|
65
|
+
<div
|
|
66
|
+
data-slot="doc-header-navigation"
|
|
67
|
+
className="overflow-x-auto px-4 md:px-8"
|
|
68
|
+
>
|
|
69
|
+
{navigation}
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
</header>
|
|
73
|
+
);
|
|
74
|
+
}
|
package/src/ui/docs/browser.tsx
CHANGED
|
@@ -3,7 +3,16 @@
|
|
|
3
3
|
* Opus, o compilador de exemplos TSX nem os componentes usados apenas por esses exemplos.
|
|
4
4
|
*/
|
|
5
5
|
export { DocBrowser } from './DocBrowser.tsx'
|
|
6
|
-
export
|
|
6
|
+
export { DocHeader } from './DocHeader.tsx'
|
|
7
|
+
export { DocCatalog, DocRecordDetail } from './DocCatalog.tsx'
|
|
8
|
+
export type { DocCatalogProps, DocRecordDetailProps } from './DocCatalog.tsx'
|
|
9
|
+
export { decisionRecord, releaseRecords, parseChangelog, filterDocumentationRecords } from './records.ts'
|
|
10
|
+
export type { DocumentationRecord } from './records.ts'
|
|
11
|
+
export type { DocHeaderProps } from './DocHeader.tsx'
|
|
12
|
+
export type {
|
|
13
|
+
DocBrowserProps,
|
|
14
|
+
DocBrowserSearchOptions,
|
|
15
|
+
} from './DocBrowser.tsx'
|
|
7
16
|
export { docSectionsFromFolder } from './folder.tsx'
|
|
8
17
|
export type { FolderOptions, FolderPage } from './folder.tsx'
|
|
9
18
|
export {
|
|
@@ -5,43 +5,13 @@
|
|
|
5
5
|
* O parser é convenção-fina sobre o markdown (`## <versão> — <data>` + `### Breaking`);
|
|
6
6
|
* o corpo de cada trecho segue renderizado pelo DocMarkdown (fidelidade total).
|
|
7
7
|
*/
|
|
8
|
-
import { Badge } from '../react.tsx'
|
|
8
|
+
import { Alert, AlertHeader, AlertTitle, AlertDescription, Badge } from '../react.tsx'
|
|
9
9
|
import { DocMarkdown } from './markdown.tsx'
|
|
10
10
|
// A fonte é o CHANGELOG DO PACOTE (raiz) — a mesma que viaja em node_modules.
|
|
11
11
|
import changelogMd from '../../../CHANGELOG.md?raw'
|
|
12
|
-
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
date?: string
|
|
16
|
-
body: string
|
|
17
|
-
breaking?: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface ParsedChangelog {
|
|
21
|
-
intro: string
|
|
22
|
-
versions: ChangelogVersion[]
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Parseia o changelog: intro (antes do 1º `## `) + versões (`## x — data`, `### Breaking`). */
|
|
26
|
-
export function parseChangelog(source: string): ParsedChangelog {
|
|
27
|
-
const chunks = `\n${source.trim()}`.split(/\n## /)
|
|
28
|
-
const intro = chunks[0]?.trim() ?? ''
|
|
29
|
-
const versions: ChangelogVersion[] = []
|
|
30
|
-
for (const chunk of chunks.slice(1)) {
|
|
31
|
-
const nl = chunk.indexOf('\n')
|
|
32
|
-
const header = (nl === -1 ? chunk : chunk.slice(0, nl)).trim()
|
|
33
|
-
const rest = nl === -1 ? '' : chunk.slice(nl + 1)
|
|
34
|
-
const [version, date] = header.split(/\s+—\s+/)
|
|
35
|
-
const [body, breaking] = rest.split(/\n### Breaking\s*\n/)
|
|
36
|
-
versions.push({
|
|
37
|
-
version: (version ?? header).trim(),
|
|
38
|
-
...(date !== undefined ? { date: date.trim() } : {}),
|
|
39
|
-
body: (body ?? '').trim(),
|
|
40
|
-
...(breaking !== undefined ? { breaking: breaking.trim() } : {}),
|
|
41
|
-
})
|
|
42
|
-
}
|
|
43
|
-
return { intro, versions }
|
|
44
|
-
}
|
|
12
|
+
import { parseChangelog } from './records.ts'
|
|
13
|
+
export { parseChangelog } from './records.ts'
|
|
14
|
+
export type { ChangelogVersion, ParsedChangelog } from './records.ts'
|
|
45
15
|
|
|
46
16
|
export function ChangelogView({ source }: { source: string }): React.ReactElement {
|
|
47
17
|
const { intro, versions } = parseChangelog(source)
|
|
@@ -56,16 +26,21 @@ export function ChangelogView({ source }: { source: string }): React.ReactElemen
|
|
|
56
26
|
{v.date !== undefined && <span className="text-sm text-muted-foreground">{v.date}</span>}
|
|
57
27
|
{v.breaking !== undefined && <Badge context="danger" variant="solid">Incompatível</Badge>}
|
|
58
28
|
</div>
|
|
59
|
-
{v.body.length > 0 && (
|
|
29
|
+
{(v.beforeBreaking ?? v.body).length > 0 && (
|
|
60
30
|
<div className="mt-3">
|
|
61
|
-
<DocMarkdown content={v.body} />
|
|
31
|
+
<DocMarkdown content={v.beforeBreaking ?? v.body} />
|
|
62
32
|
</div>
|
|
63
33
|
)}
|
|
64
34
|
{v.breaking !== undefined && (
|
|
65
|
-
<
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
35
|
+
<Alert context="danger" role="note" className="mt-4">
|
|
36
|
+
<AlertHeader>
|
|
37
|
+
<AlertTitle>Breaking: cuidados ao atualizar</AlertTitle>
|
|
38
|
+
<AlertDescription><DocMarkdown content={v.breaking} /></AlertDescription>
|
|
39
|
+
</AlertHeader>
|
|
40
|
+
</Alert>
|
|
41
|
+
)}
|
|
42
|
+
{v.afterBreaking && (
|
|
43
|
+
<div className="mt-3"><DocMarkdown content={v.afterBreaking} /></div>
|
|
69
44
|
)}
|
|
70
45
|
</section>
|
|
71
46
|
))}
|
|
@@ -74,7 +49,7 @@ export function ChangelogView({ source }: { source: string }): React.ReactElemen
|
|
|
74
49
|
)
|
|
75
50
|
}
|
|
76
51
|
|
|
77
|
-
/** O changelog do Opus pronto
|
|
52
|
+
/** O changelog do Opus pronto para montar no portal ou na documentação embutida. */
|
|
78
53
|
export function OpusChangelog(): React.ReactElement {
|
|
79
54
|
return <ChangelogView source={changelogMd} />
|
|
80
55
|
}
|
|
@@ -222,7 +222,7 @@ receber só `{ id }`: o `id`, do tipo texto, é o único campo obrigatório da e
|
|
|
222
222
|
se houver, têm valor padrão ou são opcionais. Os caminhos usam ponto para descer em objetos; um
|
|
223
223
|
caminho que passa por uma lista vale para cada item, e um campo intermediário `null` aparece como
|
|
224
224
|
`null`. Declare em `context` só o que orienta a conversa: o exemplo deixa fora contato, documento e
|
|
225
|
-
endereço, e o nome já vai como `title`. Um caminho que termina
|
|
225
|
+
endereço, e o nome já vai como `title`. Um caminho que termina em um objeto ou em uma lista leva tudo o
|
|
226
226
|
que está abaixo dele, inclusive campos acrescentados depois à saída; para conferir num teste o que
|
|
227
227
|
cada caminho alcança, use `assistantContextLeaves`, de `@softize/opus/assistant-schema`, sobre o
|
|
228
228
|
schema de saída do manifest. Ela devolve, por caminho, as folhas em `leaves` e, em `open`, os pontos
|
|
@@ -272,7 +272,7 @@ caracteres. Um caminho que falta na saída em execução é omitido com o motivo
|
|
|
272
272
|
aviso no log, sem falhar a leitura. Quem recebe a saída por outro canal, como o MCP, aplica a mesma
|
|
273
273
|
projeção com `projectAssistantContext(assistant, output)`.
|
|
274
274
|
|
|
275
|
-
Entregue o dado ao modelo como bloco delimitado,
|
|
275
|
+
Entregue o dado ao modelo como bloco delimitado, em uma mensagem de dado, nunca no prompt de sistema.
|
|
276
276
|
`resourceDataBlock`, de `@softize/opus/ai`, monta o bloco com o tipo, o id, o título, os dados e,
|
|
277
277
|
quando houve corte ou ausência, `truncated` e `omitted`, e escapa dentro dos valores o que poderia
|
|
278
278
|
fechar o delimitador. Quando o modelo chama a action dona como ferramenta, recebe a saída completa
|
|
@@ -326,7 +326,7 @@ de uma action que não existe, ou dois cenários para a mesma action, impedem o
|
|
|
326
326
|
subir, e o erro lista os nomes.
|
|
327
327
|
|
|
328
328
|
O Opus não decide onde os cenários ficam nem se entram no repositório. Eles não fazem parte da
|
|
329
|
-
aplicação: podem ficar
|
|
329
|
+
aplicação: podem ficar em uma pasta ignorada pelo git e ser escritos durante a sessão de design,
|
|
330
330
|
quando forem necessários.
|
|
331
331
|
|
|
332
332
|
Dados de design não pertencem ao contrato. Quando uma resposta específica importar para a revisão
|
|
@@ -168,7 +168,7 @@ componente da feature em vez de copiá-lo.
|
|
|
168
168
|
|
|
169
169
|
## Largura mínima e densidade
|
|
170
170
|
|
|
171
|
-
|
|
171
|
+
A UI das aplicações oferece suporte a partir de `64rem` e não muda a composição conforme a largura da viewport.
|
|
172
172
|
Props de tamanho, como `size="sm"`, escolhem a densidade do componente; não representam
|
|
173
173
|
breakpoints. Overflow e limites contra a janela continuam protegendo o conteúdo.
|
|
174
174
|
|
|
@@ -176,6 +176,10 @@ Não adicione em `className` uma adaptação isolada por viewport ou container.
|
|
|
176
176
|
futura precisa começar pelo shell e formar um contrato comum para navegação, splits, tabelas e
|
|
177
177
|
componentes.
|
|
178
178
|
|
|
179
|
+
O [portal de documentação](/docs/documentation-portal) tem uma exceção própria para adaptar
|
|
180
|
+
cabeçalho, leitura editorial e catálogos em telas estreitas. Essa exceção não altera o contrato
|
|
181
|
+
dos componentes demonstrados nos exemplos vivos nem autoriza adaptações isoladas na aplicação.
|
|
182
|
+
|
|
179
183
|
## O que fica fora das alavancas
|
|
180
184
|
|
|
181
185
|
Alguns detalhes fazem parte da identidade visual do Opus e são iguais em todo projeto: a faixa do
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Publicação de documentação
|
|
2
|
+
|
|
3
|
+
Quando uma aplicação precisa publicar seus próprios Markdown, use a superfície leve
|
|
4
|
+
`@softize/opus/ui/docs/browser`. Ela oferece cabeçalho, navegação e leitura sem incluir o catálogo
|
|
5
|
+
do Opus nem o compilador de exemplos vivos.
|
|
6
|
+
|
|
7
|
+
## Componha o portal
|
|
8
|
+
|
|
9
|
+
O aplicativo escolhe a marca, as seções, as rotas e as ações. `DocHeader` alinha os controles no
|
|
10
|
+
topo; `DocBrowser` pesquisa as páginas e organiza a leitura. O valor da busca é compartilhado,
|
|
11
|
+
mas o campo aparece somente no cabeçalho:
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { useState } from 'react'
|
|
15
|
+
import { DocBrowser, DocHeader, type DocSection } from '@softize/opus/ui/docs/browser'
|
|
16
|
+
import { Input } from '@softize/opus/ui/react'
|
|
17
|
+
|
|
18
|
+
export function Documentation({ sections }: { sections: DocSection[] }) {
|
|
19
|
+
const [query, setQuery] = useState('')
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex h-dvh flex-col">
|
|
22
|
+
<DocHeader
|
|
23
|
+
brand={<a href="/docs">Meu produto</a>}
|
|
24
|
+
search={<Input type="search" aria-label="Buscar na documentação"
|
|
25
|
+
value={query} onChange={(event) => setQuery(event.target.value)} />}
|
|
26
|
+
/>
|
|
27
|
+
<main className="min-h-0 flex-1">
|
|
28
|
+
<DocBrowser sections={sections} sidebarSize="17rem" editorial sidebarInitiallyCollapsed
|
|
29
|
+
search={{ placement: 'external', value: query }} />
|
|
30
|
+
</main>
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Marca, busca e ações usam o mesmo alinhamento vertical. O campo tem até `22rem` e fica centralizado
|
|
37
|
+
no cabeçalho, com colunas de mesma largura nas extremidades. A navegação superior é opcional; quando
|
|
38
|
+
for necessária, ocupa uma linha própria. Em telas menores que `64rem`, a busca ganha sua própria linha. Não é necessário
|
|
39
|
+
copiar esse grid no aplicativo. Links e controles mantêm seu comportamento nativo: o aplicativo
|
|
40
|
+
continua responsável por navegar, trocar tema ou abrir o repositório.
|
|
41
|
+
|
|
42
|
+
## Declare um percurso de leitura
|
|
43
|
+
|
|
44
|
+
As `sections` organizam grupos e páginas na ordem declarada. Use essa ordem para aproximar
|
|
45
|
+
conceitos relacionados, não para reproduzir automaticamente a árvore de arquivos. Cada página
|
|
46
|
+
tem `slug`, `title` e `render`; `searchText` acrescenta conteúdo à busca e `navigationTitle` permite
|
|
47
|
+
um rótulo lateral curto sem mudar o título completo.
|
|
48
|
+
|
|
49
|
+
Com `editorial`, a leitura recebe contexto, sumário dos títulos renderizados e links de página
|
|
50
|
+
anterior e seguinte. `additionalPages` preserva destinos diretos que não entram nessa sequência.
|
|
51
|
+
A busca documental do topo mantém seu rótulo, valor e função ao navegar para catálogos e detalhes;
|
|
52
|
+
continua pesquisando os guias na lateral. Consultas locais ficam no conteúdo da própria página e
|
|
53
|
+
não substituem nem alteram esse campo.
|
|
54
|
+
|
|
55
|
+
O portal organiza a descoberta; não é uma fronteira de autorização. Só envie ao navegador fontes
|
|
56
|
+
que a pessoa possa consultar, inclusive aquelas disponíveis por URL direta.
|
|
57
|
+
|
|
58
|
+
A adaptação do cabeçalho, da leitura editorial e dos catálogos à largura da tela é uma exceção
|
|
59
|
+
restrita ao portal documental. A UI das aplicações e os componentes dos exemplos vivos preservam
|
|
60
|
+
o contrato desktop a partir de `64rem`. O aplicativo organiza a moldura do portal em telas estreitas;
|
|
61
|
+
`DocBrowser` não muda sozinho a orientação de `Split` nem transforma os exemplos em layouts móveis.
|
|
62
|
+
|
|
63
|
+
## Consulte decisões e entregas
|
|
64
|
+
|
|
65
|
+
ADRs e changelog mantêm o mapa dos guias, sem acrescentar uma árvore para cada registro. Use `sidebarLinks` para oferecer entradas
|
|
66
|
+
no mesmo nível dos grupos, após os capítulos, sem incluí-las na sequência anterior/próxima.
|
|
67
|
+
`DocCatalog` recebe registros derivados dos Markdown e uma consulta local independente do cabeçalho.
|
|
68
|
+
Passe o catálogo em `content` para preservar a lateral:
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { useEffect, useState } from 'react'
|
|
72
|
+
import { DocBrowser, DocHeader, DocCatalog, DocRecordDetail, releaseRecords, type DocSection } from '@softize/opus/ui/docs/browser'
|
|
73
|
+
import { Input, Markdown, usePathname } from '@softize/opus/ui/react'
|
|
74
|
+
|
|
75
|
+
export function DocumentationPortal({ source, sections }: {
|
|
76
|
+
source: string; sections: DocSection[]
|
|
77
|
+
}) {
|
|
78
|
+
const path = usePathname()
|
|
79
|
+
const [documentationQuery, setDocumentationQuery] = useState('')
|
|
80
|
+
const [catalogQuery, setCatalogQuery] = useState('')
|
|
81
|
+
const isChangelog = path === '/changelog' || path.startsWith('/changelog/')
|
|
82
|
+
const records = releaseRecords(source, '/changelog')
|
|
83
|
+
const record = records.find((entry) => entry.href === path)
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (!isChangelog) setCatalogQuery('')
|
|
86
|
+
}, [isChangelog])
|
|
87
|
+
return <div className="flex h-dvh flex-col">
|
|
88
|
+
<DocHeader brand={<a href="/docs">Meu produto</a>}
|
|
89
|
+
search={<Input type="search" aria-label="Buscar na documentação"
|
|
90
|
+
value={documentationQuery} onChange={(event) => setDocumentationQuery(event.target.value)} />} />
|
|
91
|
+
<main className="min-h-0 flex-1">
|
|
92
|
+
<DocBrowser sections={sections} sidebarInitiallyCollapsed
|
|
93
|
+
search={{ placement: 'external', value: documentationQuery }}
|
|
94
|
+
sidebarLinks={[{ label: 'Changelog', href: '/changelog', active: isChangelog }]}
|
|
95
|
+
content={isChangelog ? (
|
|
96
|
+
record ? <DocRecordDetail catalogLabel="Changelog"><Markdown content={record.body} /></DocRecordDetail>
|
|
97
|
+
: path === '/changelog' ? <DocCatalog title="Changelog" description="Mudanças por versão."
|
|
98
|
+
kind="releases" records={records} query={catalogQuery} onQueryChange={setCatalogQuery} />
|
|
99
|
+
: <DocRecordDetail catalogLabel="Changelog"><Markdown content="# Registro não encontrado" /></DocRecordDetail>
|
|
100
|
+
) : undefined} />
|
|
101
|
+
</main>
|
|
102
|
+
</div>
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
O catálogo oferece busca textual sobre o conteúdo completo e apresenta os registros mais recentes
|
|
107
|
+
primeiro, sem tabela, filtros, controles de ordenação ou paginação. Decisões mostram título, data,
|
|
108
|
+
estado declarado e o primeiro parágrafo do contexto. Changelog retoma a leitura das versões com
|
|
109
|
+
texto integral e cuidados de atualização em destaque.
|
|
110
|
+
|
|
111
|
+
Não faz chamadas de rede: o aplicativo fornece os arquivos descobertos no build. Esse
|
|
112
|
+
índice estático não substitui `ActionList` em coleções de recursos servidas por uma action.
|
|
113
|
+
A consulta local pesquisa somente os registros; a busca do cabeçalho continua alimentando a árvore.
|
|
114
|
+
O host controla os dois valores separadamente. Preserve a consulta local ao abrir um registro e
|
|
115
|
+
voltar, e limpe-a ao trocar de catálogo; não limpe a consulta documental pela navegação.
|
|
116
|
+
|
|
117
|
+
`decisionRecord(source, href)` extrai número, título, data e status declarados no cabeçalho de uma
|
|
118
|
+
ADR. A lista apresenta o estado declarado; ressalvas e andamento continuam no texto integral.
|
|
119
|
+
Ausência de data ou status aparece como não informada; títulos que não identificam uma ADR
|
|
120
|
+
não geram registros. `releaseRecords(source, basePath)` preserva a convenção `## versão — data` do
|
|
121
|
+
changelog e suas instruções de incompatibilidade. Histórico dentro de uma versão não vira uma
|
|
122
|
+
release inventada.
|
|
123
|
+
|
|
124
|
+
`content`, `breaking` e `afterBreaking` preservam a sequência da versão no resumo: texto inicial,
|
|
125
|
+
cuidados de atualização e seções seguintes. O detalhe usa `body`, com o Markdown integral.
|
|
126
|
+
|
|
127
|
+
O aplicativo resolve a URL do registro e passa seu conteúdo para `DocRecordDetail`, também no
|
|
128
|
+
slot `content`. O registro mostra `catalogLabel` antes do título, sem breadcrumb. A navegação fica
|
|
129
|
+
na lateral; `active` identifica o catálogo aberto. Preserve a mesma instância de `DocBrowser`
|
|
130
|
+
entre guias, catálogo e registro para manter os ramos abertos pela pessoa.
|
|
131
|
+
|
|
132
|
+
Quando recebe `content`, o navegador não escolhe uma página dos guias, não aplica seus redirects
|
|
133
|
+
e não atualiza o título da aba. O aplicativo resolve a rota e o título desse conteúdo. O catálogo
|
|
134
|
+
não define as rotas nem a autorização do aplicativo.
|
|
135
|
+
|
|
136
|
+
## Propriedades de DocHeader
|
|
137
|
+
|
|
138
|
+
| Propriedade | Tipo | Padrão | Uso |
|
|
139
|
+
| --- | --- | --- | --- |
|
|
140
|
+
| `brand` | `ReactNode` | Obrigatória | Marca ou link de entrada. |
|
|
141
|
+
| `navigation` | `ReactNode` | — | Navegação opcional; sem ela, não há segunda linha de abas. |
|
|
142
|
+
| `search` | `ReactNode` | — | Campo de busca controlado pelo aplicativo. |
|
|
143
|
+
| `actions` | `ReactNode` | — | Controles como tema e repositório. |
|
|
144
|
+
| `brandWidth` | `string` | `17rem` | Coluna da marca, normalmente igual à lateral. |
|
|
145
|
+
| `className`, `style` e atributos de `header` | Atributos HTML | — | Identificação e ajustes locais sem substituir a composição. |
|
|
146
|
+
|
|
147
|
+
## Propriedades de DocBrowser
|
|
148
|
+
|
|
149
|
+
| Propriedade | Tipo | Padrão | Uso |
|
|
150
|
+
| --- | --- | --- | --- |
|
|
151
|
+
| `sections` | `DocSection[]` | Obrigatória na entrada leve | Seções, grupos e páginas na ordem de leitura. |
|
|
152
|
+
| `basePath` | `string` | `/docs` | Prefixo das rotas. |
|
|
153
|
+
| `path` | `string` | Rota compartilhada | Caminho controlado por um host externo. |
|
|
154
|
+
| `redirects` | `Record<string, string>` | `{}` | Slugs anteriores e seus destinos canônicos. |
|
|
155
|
+
| `documentTitle` | `(title: string) => string` | — | Título da aba a partir da página ativa. |
|
|
156
|
+
| `search` | `boolean` ou `DocBrowserSearchOptions` | `false` | Busca lateral ou valor externo; consulta título, rótulo, badge e `searchText`. |
|
|
157
|
+
| `sidebarSize` | `PaneSize` | `14rem` | Largura inicial da navegação. |
|
|
158
|
+
| `sidebar` | `boolean` | `true` | Exibe a árvore; `false` mantém uma leitura simples sem lateral. |
|
|
159
|
+
| `editorial` | `boolean` | `false` | Contexto, sumário e sequência, quando a lateral está presente. |
|
|
160
|
+
| `navigationLabel` | `string` | `Documentação` | Nome da área no contexto editorial; por exemplo, `UI` para o catálogo de componentes. |
|
|
161
|
+
| `additionalPages` | `DocEntry[]` | `[]` | Destinos diretos fora da árvore e da sequência. |
|
|
162
|
+
| `content` | `ReactNode` | — | Conteúdo do host no lugar da página documental, mantendo a lateral. O host resolve rota e título; `null` deixa a leitura vazia. |
|
|
163
|
+
| `sidebarLinks` | `{ label: string; href: string; active?: boolean }[]` | `[]` | Destinos globais fora dos capítulos; `active` marca o catálogo atual. |
|
|
164
|
+
| `sidebarInitiallyCollapsed` | `boolean` | `false` | Seções e grupos nomeados começam recolhidos, inclusive em links diretos. A busca revela resultados temporariamente sem alterar os ramos abertos pela pessoa. |
|
|
165
|
+
|
|
166
|
+
A entrada histórica `@softize/opus/ui/docs` inclui o catálogo e oferece suas páginas quando
|
|
167
|
+
`sections` é omitida. Para conteúdo próprio, prefira a entrada leve do exemplo.
|
|
168
|
+
|
|
169
|
+
`DocBrowserSearchOptions` é exportado pelas duas entradas documentais. Com `placement: 'sidebar'`
|
|
170
|
+
(ou sem `placement`), o navegador mantém a consulta local. Com `placement: 'external'`, `value`
|
|
171
|
+
é obrigatório e o host fornece o campo. Nos dois modos, `label`, `placeholder` e `emptyMessage`
|
|
172
|
+
são opcionais; os dois primeiros só afetam o campo lateral.
|
|
173
|
+
|
|
174
|
+
## Propriedades de DocCatalog
|
|
175
|
+
|
|
176
|
+
| Propriedade | Tipo | Padrão | Uso |
|
|
177
|
+
| --- | --- | --- | --- |
|
|
178
|
+
| `title` | `string` | Obrigatória | Título do catálogo. |
|
|
179
|
+
| `description` | `string` | Obrigatória | Contexto para a consulta. |
|
|
180
|
+
| `records` | `readonly DocumentationRecord[]` | Obrigatória | Índice estático com identidade, título, URL, corpo e texto pesquisável. Data, status, `summary`, `content`, `breaking` e `afterBreaking` são opcionais: contexto da ADR e partes do resumo da versão. |
|
|
181
|
+
| `query` | `string` | Obrigatória | Valor do campo local, independente da busca documental do topo. |
|
|
182
|
+
| `onQueryChange` | `(query: string) => void` | Obrigatória | Atualiza a consulta controlada pelo host quando a pessoa escreve no campo local. |
|
|
183
|
+
| `kind` | `'decisions' \| 'releases'` | Obrigatória | Lista de decisões com contexto ou leitura contínua das versões. |
|
|
184
|
+
|
|
185
|
+
## Propriedades de DocRecordDetail
|
|
186
|
+
|
|
187
|
+
| Propriedade | Tipo | Padrão | Uso |
|
|
188
|
+
| --- | --- | --- | --- |
|
|
189
|
+
| `catalogLabel` | `string` | Obrigatória | Nome do catálogo antes do título, sem breadcrumb. |
|
|
190
|
+
| `children` | `ReactNode` | Obrigatória | Conteúdo completo do registro. |
|