@softize/opus 8.6.6
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 +1616 -0
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/bin/cli.mjs +528 -0
- package/bin/lib/check.mjs +307 -0
- package/bin/lib/components.mjs +151 -0
- package/bin/lib/create.mjs +208 -0
- package/bin/lib/db-check-runner.mjs +86 -0
- package/bin/lib/db-migrate-runner.mjs +89 -0
- package/bin/lib/db-scaffold-runner.mjs +84 -0
- package/bin/lib/db.mjs +261 -0
- package/bin/lib/docs-include.mjs +48 -0
- package/bin/lib/gen-dicts.mjs +134 -0
- package/bin/lib/gen-docs.mjs +288 -0
- package/bin/lib/gen-manifest.mjs +102 -0
- package/bin/lib/gen-openapi.mjs +195 -0
- package/bin/lib/gen-runner.mjs +472 -0
- package/bin/lib/gen-stubs.mjs +463 -0
- package/bin/lib/gen.mjs +311 -0
- package/bin/lib/init.mjs +514 -0
- package/bin/lib/introspect.mjs +107 -0
- package/bin/lib/mcp.mjs +85 -0
- package/bin/lib/postinstall.mjs +56 -0
- package/docs/chat-event-protocol.md +85 -0
- package/docs/code-style.md +16 -0
- package/docs/data-layer.md +246 -0
- package/docs/ownership-vs-shadcn-lock.md +102 -0
- package/docs/protocol.md +2053 -0
- package/docs/releasing.md +110 -0
- package/docs/shellnav.md +131 -0
- package/package.json +338 -0
- package/registry/hooks/hooks.json +26 -0
- package/registry/hooks/link-memory-on-start.mjs +46 -0
- package/registry/hooks/opus-check-on-stop.mjs +114 -0
- package/registry/skills/create-action/SKILL.md +49 -0
- package/registry/skills/create-action/scaffold.mjs +122 -0
- package/registry/templates/app/_gitignore +3 -0
- package/registry/templates/app/_npmrc +1 -0
- package/registry/templates/app/_opus/_gitignore +5 -0
- package/registry/templates/app/_prettierrc.json +6 -0
- package/registry/templates/app/index.html +13 -0
- package/registry/templates/app/opus.config.ts +16 -0
- package/registry/templates/app/package.json +43 -0
- package/registry/templates/app/pnpm-workspace.yaml +11 -0
- package/registry/templates/app/public/favicon.svg +4 -0
- package/registry/templates/app/src/App.tsx +37 -0
- package/registry/templates/app/src/domains/tasks/actions/list.test.ts +34 -0
- package/registry/templates/app/src/domains/tasks/actions/list.ts +33 -0
- package/registry/templates/app/src/domains/tasks/index.ts +13 -0
- package/registry/templates/app/src/index.css +18 -0
- package/registry/templates/app/src/main.tsx +25 -0
- package/registry/templates/app/tsconfig.json +20 -0
- package/registry/templates/app/vite.config.ts +46 -0
- package/registry/templates/monorepo/_gitignore +3 -0
- package/registry/templates/monorepo/_npmrc +1 -0
- package/registry/templates/monorepo/package.json +9 -0
- package/registry/templates/monorepo/pnpm-workspace.yaml +14 -0
- package/src/ai/ask.ts +64 -0
- package/src/ai/drivers/anthropic.ts +309 -0
- package/src/ai/index.ts +17 -0
- package/src/audit/drivers/console.ts +117 -0
- package/src/audit/drivers/pg.ts +172 -0
- package/src/audit/index.ts +51 -0
- package/src/auth/drivers/better-auth.ts +103 -0
- package/src/auth/drivers/jwt.ts +188 -0
- package/src/auth/index.ts +9 -0
- package/src/client/drivers/fetch.ts +202 -0
- package/src/client/index.ts +22 -0
- package/src/core/actions.ts +110 -0
- package/src/core/audit.ts +239 -0
- package/src/core/contracts.ts +137 -0
- package/src/core/domain.ts +310 -0
- package/src/core/errors.ts +181 -0
- package/src/core/index.ts +174 -0
- package/src/core/logical-type.ts +31 -0
- package/src/core/reactions.ts +81 -0
- package/src/core/runtime.ts +1167 -0
- package/src/core/schedules.ts +41 -0
- package/src/core/types.ts +1356 -0
- package/src/data/drivers/kysely.ts +389 -0
- package/src/data/index.ts +10 -0
- package/src/data/readonly-pool.ts +160 -0
- package/src/dsl/eval.ts +136 -0
- package/src/dsl/index.ts +29 -0
- package/src/dsl/kysely.ts +230 -0
- package/src/dsl/loads.ts +123 -0
- package/src/dsl/parser.ts +423 -0
- package/src/dsl/types.ts +113 -0
- package/src/events/drivers/mitt.ts +70 -0
- package/src/events/index.ts +9 -0
- package/src/log/drivers/pino.ts +57 -0
- package/src/log/index.ts +9 -0
- package/src/mcp/index.ts +62 -0
- package/src/queue/drivers/bullmq.ts +190 -0
- package/src/queue/index.ts +9 -0
- package/src/scheduler/drivers/node-cron.ts +93 -0
- package/src/scheduler/every.ts +45 -0
- package/src/scheduler/index.ts +9 -0
- package/src/schema/drivers/zod.ts +765 -0
- package/src/schema/entity.ts +439 -0
- package/src/schema/format/locale.ts +144 -0
- package/src/schema/index.ts +65 -0
- package/src/schema/openapi.ts +302 -0
- package/src/schema/scaffold.ts +160 -0
- package/src/server/drivers/fastify.ts +224 -0
- package/src/server/drivers/node.ts +386 -0
- package/src/server/index.ts +142 -0
- package/src/storage/drivers/fs.ts +90 -0
- package/src/storage/drivers/s3.ts +117 -0
- package/src/storage/index.ts +27 -0
- package/src/testing/fake.ts +298 -0
- package/src/testing/index.ts +324 -0
- package/src/ui/components/patterns/action-form-card.tsx +48 -0
- package/src/ui/components/patterns/action-list-dialog.tsx +93 -0
- package/src/ui/components/patterns/app-shell.tsx +227 -0
- package/src/ui/components/patterns/confirm.tsx +226 -0
- package/src/ui/components/patterns/data-state.tsx +75 -0
- package/src/ui/components/patterns/form-dialog.tsx +64 -0
- package/src/ui/components/patterns/form.tsx +584 -0
- package/src/ui/components/patterns/list.tsx +1488 -0
- package/src/ui/components/patterns/page.tsx +46 -0
- package/src/ui/components/patterns/section-shell.tsx +246 -0
- package/src/ui/components/patterns/shell-nav.tsx +150 -0
- package/src/ui/components/patterns/sidebar.tsx +89 -0
- package/src/ui/components/patterns/split.tsx +93 -0
- package/src/ui/components/patterns/trigger.tsx +196 -0
- package/src/ui/components/patterns/view.tsx +84 -0
- package/src/ui/components/primitives/accordion.tsx +64 -0
- package/src/ui/components/primitives/alert-dialog.tsx +190 -0
- package/src/ui/components/primitives/alert.tsx +116 -0
- package/src/ui/components/primitives/aspect-ratio.tsx +9 -0
- package/src/ui/components/primitives/avatar.tsx +107 -0
- package/src/ui/components/primitives/badge.tsx +37 -0
- package/src/ui/components/primitives/breadcrumb.tsx +109 -0
- package/src/ui/components/primitives/button-group.tsx +83 -0
- package/src/ui/components/primitives/button.tsx +102 -0
- package/src/ui/components/primitives/calendar.tsx +218 -0
- package/src/ui/components/primitives/card.tsx +56 -0
- package/src/ui/components/primitives/carousel.tsx +239 -0
- package/src/ui/components/primitives/chat.tsx +407 -0
- package/src/ui/components/primitives/checkbox.tsx +30 -0
- package/src/ui/components/primitives/collapsible.tsx +31 -0
- package/src/ui/components/primitives/command.tsx +182 -0
- package/src/ui/components/primitives/composer.tsx +121 -0
- package/src/ui/components/primitives/copyable.tsx +50 -0
- package/src/ui/components/primitives/dialog.tsx +147 -0
- package/src/ui/components/primitives/drawer.tsx +141 -0
- package/src/ui/components/primitives/empty.tsx +104 -0
- package/src/ui/components/primitives/field.tsx +246 -0
- package/src/ui/components/primitives/icon-picker.tsx +180 -0
- package/src/ui/components/primitives/input-group.tsx +168 -0
- package/src/ui/components/primitives/input-otp.tsx +75 -0
- package/src/ui/components/primitives/input.tsx +72 -0
- package/src/ui/components/primitives/item.tsx +193 -0
- package/src/ui/components/primitives/kbd.tsx +28 -0
- package/src/ui/components/primitives/label.tsx +22 -0
- package/src/ui/components/primitives/markdown.tsx +35 -0
- package/src/ui/components/primitives/menu.tsx +255 -0
- package/src/ui/components/primitives/pagination.tsx +127 -0
- package/src/ui/components/primitives/popover.tsx +87 -0
- package/src/ui/components/primitives/progress.tsx +29 -0
- package/src/ui/components/primitives/radio-group.tsx +43 -0
- package/src/ui/components/primitives/resizable.tsx +51 -0
- package/src/ui/components/primitives/scroll-area.tsx +56 -0
- package/src/ui/components/primitives/select.tsx +479 -0
- package/src/ui/components/primitives/separator.tsx +26 -0
- package/src/ui/components/primitives/skeleton.tsx +13 -0
- package/src/ui/components/primitives/slider.tsx +61 -0
- package/src/ui/components/primitives/sonner.tsx +46 -0
- package/src/ui/components/primitives/spinner.tsx +29 -0
- package/src/ui/components/primitives/switch.tsx +33 -0
- package/src/ui/components/primitives/table.tsx +114 -0
- package/src/ui/components/primitives/tabs.tsx +104 -0
- package/src/ui/components/primitives/textarea.tsx +18 -0
- package/src/ui/components/primitives/toggle-group.tsx +81 -0
- package/src/ui/components/primitives/toggle.tsx +45 -0
- package/src/ui/components/primitives/tooltip.tsx +55 -0
- package/src/ui/components/primitives/truncate.tsx +49 -0
- package/src/ui/docs/DocBrowser.tsx +90 -0
- package/src/ui/docs/changelog.tsx +80 -0
- package/src/ui/docs/content/accordion.md +86 -0
- package/src/ui/docs/content/action-form-card.md +24 -0
- package/src/ui/docs/content/action-form-dialog.md +30 -0
- package/src/ui/docs/content/action-form.md +125 -0
- package/src/ui/docs/content/action-list-dialog.md +68 -0
- package/src/ui/docs/content/action-list.md +194 -0
- package/src/ui/docs/content/action-trigger.md +72 -0
- package/src/ui/docs/content/action-view.md +47 -0
- package/src/ui/docs/content/actions.md +138 -0
- package/src/ui/docs/content/ai.md +112 -0
- package/src/ui/docs/content/alert-dialog.md +73 -0
- package/src/ui/docs/content/alert.md +69 -0
- package/src/ui/docs/content/app-shell.md +155 -0
- package/src/ui/docs/content/aspect-ratio.md +66 -0
- package/src/ui/docs/content/audit.md +84 -0
- package/src/ui/docs/content/auth.md +70 -0
- package/src/ui/docs/content/avatar.md +94 -0
- package/src/ui/docs/content/badge.md +48 -0
- package/src/ui/docs/content/breadcrumb.md +87 -0
- package/src/ui/docs/content/button-group.md +71 -0
- package/src/ui/docs/content/button.md +60 -0
- package/src/ui/docs/content/calendar.md +62 -0
- package/src/ui/docs/content/card.md +49 -0
- package/src/ui/docs/content/carousel.md +85 -0
- package/src/ui/docs/content/chat.md +69 -0
- package/src/ui/docs/content/checkbox.md +75 -0
- package/src/ui/docs/content/cli.md +58 -0
- package/src/ui/docs/content/collapsible.md +64 -0
- package/src/ui/docs/content/command.md +56 -0
- package/src/ui/docs/content/composer.md +50 -0
- package/src/ui/docs/content/confirm.md +120 -0
- package/src/ui/docs/content/copyable.md +30 -0
- package/src/ui/docs/content/customization.md +110 -0
- package/src/ui/docs/content/cycle.md +34 -0
- package/src/ui/docs/content/data-state.md +47 -0
- package/src/ui/docs/content/data.md +99 -0
- package/src/ui/docs/content/dialog.md +60 -0
- package/src/ui/docs/content/drawer.md +55 -0
- package/src/ui/docs/content/empty.md +66 -0
- package/src/ui/docs/content/events.md +61 -0
- package/src/ui/docs/content/field.md +58 -0
- package/src/ui/docs/content/getting-started.md +109 -0
- package/src/ui/docs/content/icon-picker.md +51 -0
- package/src/ui/docs/content/input-group.md +78 -0
- package/src/ui/docs/content/input-otp.md +72 -0
- package/src/ui/docs/content/input.md +78 -0
- package/src/ui/docs/content/item.md +84 -0
- package/src/ui/docs/content/kbd.md +62 -0
- package/src/ui/docs/content/label.md +32 -0
- package/src/ui/docs/content/log.md +55 -0
- package/src/ui/docs/content/markdown.md +41 -0
- package/src/ui/docs/content/mcp.md +44 -0
- package/src/ui/docs/content/menu.md +114 -0
- package/src/ui/docs/content/microcopy.md +83 -0
- package/src/ui/docs/content/page.md +34 -0
- package/src/ui/docs/content/pagination.md +99 -0
- package/src/ui/docs/content/popover.md +49 -0
- package/src/ui/docs/content/progress.md +69 -0
- package/src/ui/docs/content/queue.md +62 -0
- package/src/ui/docs/content/radio-group.md +77 -0
- package/src/ui/docs/content/resizable.md +86 -0
- package/src/ui/docs/content/router.md +56 -0
- package/src/ui/docs/content/runtime.md +77 -0
- package/src/ui/docs/content/scheduler.md +66 -0
- package/src/ui/docs/content/scroll-area.md +89 -0
- package/src/ui/docs/content/section-shell.md +121 -0
- package/src/ui/docs/content/select.md +342 -0
- package/src/ui/docs/content/separator.md +33 -0
- package/src/ui/docs/content/sidebar.md +38 -0
- package/src/ui/docs/content/skeleton.md +34 -0
- package/src/ui/docs/content/slider.md +64 -0
- package/src/ui/docs/content/spinner.md +37 -0
- package/src/ui/docs/content/split.md +33 -0
- package/src/ui/docs/content/storage.md +69 -0
- package/src/ui/docs/content/switch.md +69 -0
- package/src/ui/docs/content/table.md +102 -0
- package/src/ui/docs/content/tabs.md +94 -0
- package/src/ui/docs/content/testing.md +89 -0
- package/src/ui/docs/content/textarea.md +30 -0
- package/src/ui/docs/content/toast.md +67 -0
- package/src/ui/docs/content/toggle-group.md +81 -0
- package/src/ui/docs/content/toggle.md +72 -0
- package/src/ui/docs/content/tokens.md +171 -0
- package/src/ui/docs/content/tooltip.md +50 -0
- package/src/ui/docs/content/truncate.md +37 -0
- package/src/ui/docs/content/ui.md +40 -0
- package/src/ui/docs/content/upgrading.md +48 -0
- package/src/ui/docs/doc-client.tsx +214 -0
- package/src/ui/docs/doc.tsx +301 -0
- package/src/ui/docs/folder.tsx +149 -0
- package/src/ui/docs/index.ts +21 -0
- package/src/ui/docs/markdown.tsx +130 -0
- package/src/ui/docs/md-raw.d.ts +4 -0
- package/src/ui/docs/plugin.ts +104 -0
- package/src/ui/docs/registry.tsx +424 -0
- package/src/ui/docs/standalone.tsx +107 -0
- package/src/ui/drivers/react.tsx +627 -0
- package/src/ui/index.ts +92 -0
- package/src/ui/lib/cn.ts +10 -0
- package/src/ui/lib/zod-pt-br.ts +38 -0
- package/src/ui/meta.ts +412 -0
- package/src/ui/react.tsx +235 -0
- package/src/ui/router.ts +96 -0
- package/src/ui/theme.css +234 -0
- package/src/vite/design.ts +652 -0
- package/src/vite/index.ts +8 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* doc.tsx — blocos de construção das páginas da doc de UI (DocBrowser).
|
|
3
|
+
*
|
|
4
|
+
* Toda página compõe a partir daqui: DocPage (cabeçalho com meta), Example
|
|
5
|
+
* (exemplo vivo + snippet), CodeBlock e PropsTable. O `meta` vem de
|
|
6
|
+
* `@softize/opus/ui/meta` — whenToUse/ancestry têm UMA fonte (o
|
|
7
|
+
* próprio componente); a doc só renderiza.
|
|
8
|
+
*/
|
|
9
|
+
import { useLayoutEffect, useRef, useState } from 'react'
|
|
10
|
+
import { Check, ChevronDown, Copy } from 'lucide-react'
|
|
11
|
+
import { Highlight, type PrismTheme } from 'prism-react-renderer'
|
|
12
|
+
import type { ComponentMeta } from '../index.ts'
|
|
13
|
+
import {
|
|
14
|
+
Badge,
|
|
15
|
+
Table,
|
|
16
|
+
TableBody,
|
|
17
|
+
TableCell,
|
|
18
|
+
TableHead,
|
|
19
|
+
TableHeader,
|
|
20
|
+
TableRow,
|
|
21
|
+
cn,
|
|
22
|
+
} from '../react.tsx'
|
|
23
|
+
|
|
24
|
+
/** Prosa curta com `código` inline: trecho entre crases vira chip <code> — o
|
|
25
|
+
* whenToUse/lead são texto plano (não passam pelo DocMarkdown). */
|
|
26
|
+
function InlineMd({ text }: { text: string }): React.ReactElement {
|
|
27
|
+
const parts = text.split('`')
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
{parts.map((part, i) =>
|
|
31
|
+
i % 2 === 1 ? (
|
|
32
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
33
|
+
<code key={i} className="rounded-[calc(var(--radius)-4px)] bg-muted px-1.5 py-0.5 font-mono text-[0.85em]">
|
|
34
|
+
{part}
|
|
35
|
+
</code>
|
|
36
|
+
) : (
|
|
37
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
38
|
+
<span key={i}>{part}</span>
|
|
39
|
+
),
|
|
40
|
+
)}
|
|
41
|
+
</>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Página da doc: título, badge de origem (ancestry) e o whenToUse do meta como
|
|
47
|
+
* lead. Páginas conceituais (sem meta) passam só `lead`.
|
|
48
|
+
*/
|
|
49
|
+
export function DocPage({
|
|
50
|
+
title,
|
|
51
|
+
meta,
|
|
52
|
+
lead,
|
|
53
|
+
children,
|
|
54
|
+
}: {
|
|
55
|
+
title: string
|
|
56
|
+
meta?: ComponentMeta
|
|
57
|
+
lead?: string
|
|
58
|
+
children: React.ReactNode
|
|
59
|
+
}): React.ReactElement {
|
|
60
|
+
return (
|
|
61
|
+
<article className="space-y-10">
|
|
62
|
+
<header className="space-y-3">
|
|
63
|
+
<div className="flex items-center gap-3">
|
|
64
|
+
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
|
|
65
|
+
{meta && (
|
|
66
|
+
<Badge variant="outline">{meta.ancestry === 'shadcn' ? 'shadcn' : 'Nativo do Opus'}</Badge>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
{(lead ?? meta?.whenToUse) !== undefined && (
|
|
70
|
+
<p className="max-w-2xl text-sm leading-relaxed break-words text-muted-foreground">
|
|
71
|
+
<InlineMd text={lead ?? meta?.whenToUse ?? ''} />
|
|
72
|
+
</p>
|
|
73
|
+
)}
|
|
74
|
+
{meta?.deprecated !== undefined && (
|
|
75
|
+
<div className="max-w-2xl rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm leading-relaxed text-foreground">
|
|
76
|
+
<span className="font-medium">Deprecated{meta.deprecated.since !== undefined ? ` desde ${meta.deprecated.since}` : ''}.</span>{' '}
|
|
77
|
+
Use <InlineMd text={meta.deprecated.alternative} />.
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
</header>
|
|
81
|
+
{children}
|
|
82
|
+
</article>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Seção nomeada dentro de uma página (agrupa exemplos ou prosa). */
|
|
87
|
+
export function DocSection({
|
|
88
|
+
title,
|
|
89
|
+
hint,
|
|
90
|
+
children,
|
|
91
|
+
}: {
|
|
92
|
+
title: string
|
|
93
|
+
hint?: string
|
|
94
|
+
children: React.ReactNode
|
|
95
|
+
}): React.ReactElement {
|
|
96
|
+
return (
|
|
97
|
+
<section className="space-y-4">
|
|
98
|
+
<div className="space-y-1">
|
|
99
|
+
<h2 className="text-base font-semibold tracking-tight">{title}</h2>
|
|
100
|
+
{hint !== undefined && (
|
|
101
|
+
<p className="max-w-2xl text-sm leading-relaxed text-muted-foreground">
|
|
102
|
+
<InlineMd text={hint} />
|
|
103
|
+
</p>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
{children}
|
|
107
|
+
</section>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Exemplo vivo: preview renderizado com o tema da casa + o snippet de como se
|
|
113
|
+
* escreve. O snippet é a fonte de cópia — o preview, a expectativa visual.
|
|
114
|
+
*/
|
|
115
|
+
export function Example({
|
|
116
|
+
title,
|
|
117
|
+
hint,
|
|
118
|
+
code,
|
|
119
|
+
children,
|
|
120
|
+
previewClassName,
|
|
121
|
+
lang = 'tsx',
|
|
122
|
+
}: {
|
|
123
|
+
title?: string
|
|
124
|
+
hint?: string
|
|
125
|
+
code: string
|
|
126
|
+
children: React.ReactNode
|
|
127
|
+
/** Layout do palco do preview (padrão: linha com wrap, itens centrados). */
|
|
128
|
+
previewClassName?: string
|
|
129
|
+
/** Linguagem do snippet pro highlight (default `tsx`). */
|
|
130
|
+
lang?: string
|
|
131
|
+
}): React.ReactElement {
|
|
132
|
+
return (
|
|
133
|
+
<div className="space-y-2">
|
|
134
|
+
{title !== undefined && (
|
|
135
|
+
<div className="space-y-0.5">
|
|
136
|
+
<h3 className="font-medium leading-relaxed">{title}</h3>
|
|
137
|
+
{hint !== undefined && <p className="text-sm leading-relaxed text-muted-foreground">{hint}</p>}
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
<div className="overflow-hidden rounded-lg border border-border">
|
|
141
|
+
<div className={cn('flex flex-wrap items-center gap-3 bg-background p-6', previewClassName)}>
|
|
142
|
+
{children}
|
|
143
|
+
</div>
|
|
144
|
+
<CodeBlock code={code} lang={lang} className="rounded-none border-x-0 border-b-0" />
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Tema do prism mapeado nos CSS vars da casa (theme.css) — o cascade troca light/dark sozinho.
|
|
151
|
+
const codeTheme: PrismTheme = {
|
|
152
|
+
plain: { color: 'var(--code-fg)', backgroundColor: 'transparent' },
|
|
153
|
+
styles: [
|
|
154
|
+
{ types: ['comment', 'prolog', 'doctype', 'cdata'], style: { color: 'var(--code-comment)', fontStyle: 'italic' } },
|
|
155
|
+
{ types: ['keyword', 'selector', 'changed', 'operator', 'atrule'], style: { color: 'var(--code-keyword)' } },
|
|
156
|
+
{ types: ['string', 'char', 'inserted', 'attr-value', 'url'], style: { color: 'var(--code-string)' } },
|
|
157
|
+
{ types: ['function', 'function-variable'], style: { color: 'var(--code-function)' } },
|
|
158
|
+
{ types: ['number', 'boolean', 'constant', 'symbol'], style: { color: 'var(--code-number)' } },
|
|
159
|
+
{ types: ['class-name', 'tag', 'builtin', 'maybe-class-name'], style: { color: 'var(--code-class)' } },
|
|
160
|
+
{ types: ['attr-name', 'property', 'variable'], style: { color: 'var(--code-property)' } },
|
|
161
|
+
{ types: ['punctuation'], style: { color: 'var(--code-punctuation)' } },
|
|
162
|
+
],
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Altura do peek colapsado (px). Bate com `max-h-44` (11rem) — o conteúdo acima disto colapsa. */
|
|
166
|
+
const PEEK_PX = 176
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Bloco de código com highlight (prism-react-renderer, tema da casa via CSS vars), gutter de
|
|
170
|
+
* número de linha e copiar. Código que TRANSBORDA o peek (medido, não por contagem de linha)
|
|
171
|
+
* colapsa num peek com fade + botão "Ver código"; o que cabe mostra inteiro. `collapsible={false}`
|
|
172
|
+
* força aberto.
|
|
173
|
+
*/
|
|
174
|
+
export function CodeBlock({
|
|
175
|
+
code,
|
|
176
|
+
className,
|
|
177
|
+
lang = 'tsx',
|
|
178
|
+
collapsible = true,
|
|
179
|
+
}: {
|
|
180
|
+
code: string
|
|
181
|
+
className?: string
|
|
182
|
+
/** Linguagem do highlight (prism). Default `tsx`; passe `bash`/`css`/`json` quando couber. */
|
|
183
|
+
lang?: string
|
|
184
|
+
/** Permite o colapso (peek + Ver código) quando o código transborda o peek. Default `true`. */
|
|
185
|
+
collapsible?: boolean
|
|
186
|
+
}): React.ReactElement {
|
|
187
|
+
const [copied, setCopied] = useState(false)
|
|
188
|
+
const [expanded, setExpanded] = useState(false)
|
|
189
|
+
const [overflows, setOverflows] = useState(false)
|
|
190
|
+
const contentRef = useRef<HTMLDivElement>(null)
|
|
191
|
+
const clean = code.replace(/\n$/, '')
|
|
192
|
+
|
|
193
|
+
// Colapsa só quando o código realmente passa da altura do peek — sem limiar de linha mágico
|
|
194
|
+
// (scrollHeight é a altura plena mesmo com o overflow-hidden aplicado). Re-mede via
|
|
195
|
+
// ResizeObserver: o mount sozinho pega o layout antes de assentar (fonte/flex) e mede errado
|
|
196
|
+
// de forma inconsistente — o observer corrige quando a altura final se estabelece.
|
|
197
|
+
useLayoutEffect(() => {
|
|
198
|
+
const el = contentRef.current
|
|
199
|
+
if (!el) return
|
|
200
|
+
const measure = (): void => setOverflows(el.scrollHeight > PEEK_PX)
|
|
201
|
+
measure()
|
|
202
|
+
const ro = new ResizeObserver(measure)
|
|
203
|
+
ro.observe(el)
|
|
204
|
+
return () => ro.disconnect()
|
|
205
|
+
}, [clean])
|
|
206
|
+
|
|
207
|
+
const canCollapse = collapsible && overflows
|
|
208
|
+
const collapsed = canCollapse && !expanded
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
// overflow-hidden: os filhos (fade incluso) são CLIPADOS no raio do contêiner — sem
|
|
212
|
+
// isso, o overlay desenhava um segundo arco nos cantos (a "borda dobrada").
|
|
213
|
+
<div className={cn('group relative overflow-hidden rounded-lg border border-border bg-muted/40', className)}>
|
|
214
|
+
<button
|
|
215
|
+
onClick={() => {
|
|
216
|
+
void navigator.clipboard.writeText(clean)
|
|
217
|
+
setCopied(true)
|
|
218
|
+
setTimeout(() => setCopied(false), 1500)
|
|
219
|
+
}}
|
|
220
|
+
title="Copiar código"
|
|
221
|
+
className="absolute right-2 top-2 z-20 rounded-md p-1.5 text-muted-foreground opacity-0 transition-opacity hover:bg-muted hover:text-foreground group-hover:opacity-100"
|
|
222
|
+
>
|
|
223
|
+
{copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
|
|
224
|
+
</button>
|
|
225
|
+
|
|
226
|
+
<div ref={contentRef} className={cn('relative', collapsed && 'max-h-44 overflow-hidden')}>
|
|
227
|
+
<Highlight code={clean} language={lang} theme={codeTheme}>
|
|
228
|
+
{({ tokens, getLineProps, getTokenProps }) => (
|
|
229
|
+
<pre className="overflow-x-auto py-4 font-mono text-xs leading-7">
|
|
230
|
+
{tokens.map((line, i) => {
|
|
231
|
+
const { key, ...lineProps } = getLineProps({ line })
|
|
232
|
+
return (
|
|
233
|
+
<div key={i} {...lineProps} className={cn(lineProps.className, 'flex')}>
|
|
234
|
+
<span className="w-12 shrink-0 select-none pr-4 text-right tabular-nums text-muted-foreground/40">
|
|
235
|
+
{i + 1}
|
|
236
|
+
</span>
|
|
237
|
+
<span className="flex-1 pr-4">
|
|
238
|
+
{line.map((token, k) => {
|
|
239
|
+
const { key: tk, ...tokenProps } = getTokenProps({ token })
|
|
240
|
+
return <span key={k} {...tokenProps} />
|
|
241
|
+
})}
|
|
242
|
+
</span>
|
|
243
|
+
</div>
|
|
244
|
+
)
|
|
245
|
+
})}
|
|
246
|
+
</pre>
|
|
247
|
+
)}
|
|
248
|
+
</Highlight>
|
|
249
|
+
|
|
250
|
+
{collapsed && (
|
|
251
|
+
// O fade NÃO fecha opaco — o código segue levemente visível; o clip do
|
|
252
|
+
// contêiner (overflow-hidden) cuida dos cantos.
|
|
253
|
+
<div className="absolute inset-x-0 bottom-0 z-10 flex items-end justify-center bg-gradient-to-t from-background/90 via-background/55 to-transparent pb-1.5 pt-16">
|
|
254
|
+
<button
|
|
255
|
+
aria-label="Ver código"
|
|
256
|
+
onClick={() => setExpanded(true)}
|
|
257
|
+
className="text-muted-foreground transition-colors hover:text-foreground"
|
|
258
|
+
>
|
|
259
|
+
<ChevronDown className="h-5 w-5" />
|
|
260
|
+
</button>
|
|
261
|
+
</div>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export interface PropRow {
|
|
269
|
+
prop: string
|
|
270
|
+
type: string
|
|
271
|
+
default?: string
|
|
272
|
+
description: string
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Tabela de props — só as que o consumidor decide (não o spread de DOM props). */
|
|
276
|
+
export function PropsTable({ rows }: { rows: PropRow[] }): React.ReactElement {
|
|
277
|
+
return (
|
|
278
|
+
<div className="overflow-hidden rounded-lg border border-border">
|
|
279
|
+
<Table>
|
|
280
|
+
<TableHeader>
|
|
281
|
+
<TableRow>
|
|
282
|
+
<TableHead className="w-[18%]">Prop</TableHead>
|
|
283
|
+
<TableHead className="w-[30%]">Tipo</TableHead>
|
|
284
|
+
<TableHead className="w-[12%]">Padrão</TableHead>
|
|
285
|
+
<TableHead>Descrição</TableHead>
|
|
286
|
+
</TableRow>
|
|
287
|
+
</TableHeader>
|
|
288
|
+
<TableBody>
|
|
289
|
+
{rows.map((r) => (
|
|
290
|
+
<TableRow key={r.prop}>
|
|
291
|
+
<TableCell className="font-mono text-xs">{r.prop}</TableCell>
|
|
292
|
+
<TableCell className="whitespace-normal font-mono text-xs text-muted-foreground">{r.type}</TableCell>
|
|
293
|
+
<TableCell className="font-mono text-xs text-muted-foreground">{r.default ?? '—'}</TableCell>
|
|
294
|
+
<TableCell className="whitespace-normal text-xs">{r.description}</TableCell>
|
|
295
|
+
</TableRow>
|
|
296
|
+
))}
|
|
297
|
+
</TableBody>
|
|
298
|
+
</Table>
|
|
299
|
+
</div>
|
|
300
|
+
)
|
|
301
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* folder — transforma a pasta `docs/` de um projeto em `DocSection[]` pro DocBrowser.
|
|
3
|
+
* Recebe o resultado de um glob eager do Vite (caminho → conteúdo do .md) e monta a nav
|
|
4
|
+
* espelhando a árvore de arquivos:
|
|
5
|
+
*
|
|
6
|
+
* docs/getting-started.md → seção raiz · página "Getting started"
|
|
7
|
+
* docs/guias/deploy.md → seção "Guias" · página (slug aninhado `guias/deploy`)
|
|
8
|
+
* docs/guias/ci/build.md → seção "Guias" · grupo "ci" · página `guias/ci/build`
|
|
9
|
+
*
|
|
10
|
+
* Convenção zero-config (frontmatter só SOBRESCREVE):
|
|
11
|
+
* - seção = 1ª pasta (`section:` sobrescreve; arquivo na raiz → `rootLabel`)
|
|
12
|
+
* - grupo = 2ª pasta em diante (`group:` sobrescreve)
|
|
13
|
+
* - título = `title:` → 1º `# H1` → nome do arquivo humanizado
|
|
14
|
+
* - ordem = `order:` → prefixo numérico do nome (`01-deploy.md`) → alfabética
|
|
15
|
+
* - slug = caminho sem prefixo numérico/extensão (aninhado, estável pra deep link)
|
|
16
|
+
*
|
|
17
|
+
* Frontmatter: parser mínimo browser-safe — este transform roda no bundle do app, e o
|
|
18
|
+
* gray-matter usa Buffer/js-yaml (inóspito no browser). Cobre title/section/group/order;
|
|
19
|
+
* YAML rico fica pra depois. Uso: ver `opusDocs({ source })` em ./plugin.ts.
|
|
20
|
+
*/
|
|
21
|
+
import { DocMarkdown } from './markdown.tsx'
|
|
22
|
+
import type { DocEntry, DocGroup, DocSection } from './registry.tsx'
|
|
23
|
+
|
|
24
|
+
interface FrontMatter {
|
|
25
|
+
title?: string
|
|
26
|
+
section?: string
|
|
27
|
+
group?: string
|
|
28
|
+
order?: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Extrai o bloco `--- … ---` do topo (subset `key: value`) + o corpo restante. */
|
|
32
|
+
function parseFrontMatter(raw: string): { data: FrontMatter; body: string } {
|
|
33
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(raw)
|
|
34
|
+
if (!m) return { data: {}, body: raw }
|
|
35
|
+
const kv: Record<string, string> = {}
|
|
36
|
+
for (const line of (m[1] ?? '').split(/\r?\n/)) {
|
|
37
|
+
const pair = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line.trim())
|
|
38
|
+
if (pair) kv[pair[1]!] = (pair[2] ?? '').replace(/^['"]|['"]$/g, '').trim()
|
|
39
|
+
}
|
|
40
|
+
const n = kv.order !== undefined ? Number(kv.order) : NaN
|
|
41
|
+
return {
|
|
42
|
+
data: { title: kv.title, section: kv.section, group: kv.group, order: Number.isFinite(n) ? n : undefined },
|
|
43
|
+
body: raw.slice(m[0].length),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** `01-getting-started` → { order: 1, name: 'getting-started' }. Sem prefixo → order ausente. */
|
|
48
|
+
function splitOrder(seg: string): { order?: number; name: string } {
|
|
49
|
+
const m = /^(\d+)[-_](.+)$/.exec(seg)
|
|
50
|
+
return m ? { order: Number(m[1]), name: m[2]! } : { name: seg }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** `getting-started` → "Getting started"; `guias` → "Guias". */
|
|
54
|
+
function humanize(seg: string): string {
|
|
55
|
+
const s = seg.replace(/[-_]+/g, ' ').trim()
|
|
56
|
+
return s.length === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 1º `# H1` do markdown, se houver — vira título quando não há frontmatter. */
|
|
60
|
+
function firstH1(body: string): string | undefined {
|
|
61
|
+
const m = /^#\s+(.+)$/m.exec(body)
|
|
62
|
+
return m ? m[1]!.trim() : undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function escapeRe(s: string): string {
|
|
66
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface FolderOptions {
|
|
70
|
+
/** Pasta-fonte usada no glob (removida do início das chaves). Ex.: 'docs'. */
|
|
71
|
+
root?: string
|
|
72
|
+
/** Rótulo da seção para arquivos na raiz da pasta. Default "Documentação". */
|
|
73
|
+
rootLabel?: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const LAST = 1e9
|
|
77
|
+
|
|
78
|
+
interface Row {
|
|
79
|
+
slug: string
|
|
80
|
+
title: string
|
|
81
|
+
body: string
|
|
82
|
+
section: string
|
|
83
|
+
group?: string
|
|
84
|
+
secOrder: number
|
|
85
|
+
grpOrder: number
|
|
86
|
+
order: number
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Constrói `DocSection[]` a partir do glob eager do Vite (chaves = caminhos, valores = o
|
|
91
|
+
* conteúdo raw do .md). Cada página renderiza `<Markdown>` do corpo do arquivo.
|
|
92
|
+
*/
|
|
93
|
+
export function docSectionsFromFolder(files: Record<string, string>, options: FolderOptions = {}): DocSection[] {
|
|
94
|
+
const rootLabel = options.rootLabel ?? 'Documentação'
|
|
95
|
+
const stripRe = options.root
|
|
96
|
+
? new RegExp('^/?' + escapeRe(options.root.replace(/^\/+|\/+$/g, '')) + '/')
|
|
97
|
+
: /^\/?[^/]+\//
|
|
98
|
+
const rows: Row[] = []
|
|
99
|
+
|
|
100
|
+
for (const [path, raw] of Object.entries(files)) {
|
|
101
|
+
const rel = path.replace(stripRe, '').replace(/\.mdx?$/, '')
|
|
102
|
+
const segs = rel.split('/').filter(Boolean)
|
|
103
|
+
if (segs.length === 0) continue
|
|
104
|
+
const { data, body } = parseFrontMatter(raw)
|
|
105
|
+
const file = splitOrder(segs[segs.length - 1]!)
|
|
106
|
+
const dirs = segs.slice(0, -1).map(splitOrder)
|
|
107
|
+
rows.push({
|
|
108
|
+
slug: [...dirs.map((d) => d.name), file.name].join('/'),
|
|
109
|
+
title: data.title ?? firstH1(body) ?? humanize(file.name),
|
|
110
|
+
body,
|
|
111
|
+
section: data.section ?? (dirs[0] ? humanize(dirs[0].name) : rootLabel),
|
|
112
|
+
group: data.group ?? (dirs.length > 1 ? humanize(dirs.slice(1).map((d) => d.name).join('/')) : undefined),
|
|
113
|
+
secOrder: dirs.length === 0 ? -1 : dirs[0]!.order ?? LAST,
|
|
114
|
+
grpOrder: dirs.length > 1 ? dirs[1]!.order ?? LAST : -1,
|
|
115
|
+
order: data.order ?? file.order ?? LAST,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Agrega seção → grupo → páginas, guardando a MENOR ordem vista de cada nível.
|
|
120
|
+
const sections = new Map<string, { order: number; groups: Map<string, { label?: string; order: number; rows: Row[] }> }>()
|
|
121
|
+
for (const r of rows) {
|
|
122
|
+
const sec = sections.get(r.section) ?? { order: r.secOrder, groups: new Map() }
|
|
123
|
+
sec.order = Math.min(sec.order, r.secOrder)
|
|
124
|
+
const gkey = r.group ?? ''
|
|
125
|
+
const grp = sec.groups.get(gkey) ?? { label: r.group, order: r.grpOrder, rows: [] }
|
|
126
|
+
grp.order = Math.min(grp.order, r.grpOrder)
|
|
127
|
+
grp.rows.push(r)
|
|
128
|
+
sec.groups.set(gkey, grp)
|
|
129
|
+
sections.set(r.section, sec)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const byOrder = <T,>(o: (t: T) => number, l: (t: T) => string) => (a: T, b: T) => o(a) - o(b) || l(a).localeCompare(l(b))
|
|
133
|
+
|
|
134
|
+
return [...sections.entries()]
|
|
135
|
+
.sort(byOrder(([, s]) => s.order, ([label]) => label))
|
|
136
|
+
.map(([label, s]): DocSection => ({
|
|
137
|
+
label,
|
|
138
|
+
groups: [...s.groups.values()]
|
|
139
|
+
.sort(byOrder((g) => g.order, (g) => g.label ?? ''))
|
|
140
|
+
.map(
|
|
141
|
+
(g): DocGroup => ({
|
|
142
|
+
label: g.label,
|
|
143
|
+
pages: g.rows
|
|
144
|
+
.sort(byOrder((r) => r.order, (r) => r.title))
|
|
145
|
+
.map((r): DocEntry => ({ slug: r.slug, title: r.title, render: () => <DocMarkdown content={r.body} /> })),
|
|
146
|
+
}),
|
|
147
|
+
),
|
|
148
|
+
}))
|
|
149
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/ui/docs — a doc de UI da casa (o "DocBrowser"), reusável por qualquer app.
|
|
3
|
+
*
|
|
4
|
+
* Consumo:
|
|
5
|
+
* - Embutida num shell (Maestro): `<DocBrowser basePath="/docs" path={path} />` (modo controlado).
|
|
6
|
+
* - Rota dev `/__docs` de um app: o plugin `opusDocs()` (vite) usa `mountDocs` de
|
|
7
|
+
* `@softize/opus/ui/docs/standalone` — não passa por aqui.
|
|
8
|
+
*/
|
|
9
|
+
export { DocBrowser } from './DocBrowser.tsx'
|
|
10
|
+
export type { DocBrowserProps } from './DocBrowser.tsx'
|
|
11
|
+
export { DOC_SECTIONS, PROTOCOL_SECTIONS, UI_SECTIONS } from './registry.tsx'
|
|
12
|
+
export type { DocEntry, DocGroup, DocSection } from './registry.tsx'
|
|
13
|
+
export { docSectionsFromFolder } from './folder.tsx'
|
|
14
|
+
export type { FolderOptions } from './folder.tsx'
|
|
15
|
+
// Primitivos de página reusáveis (o mesmo Example/CodeBlock das páginas TSX do DocBrowser).
|
|
16
|
+
export { Example, CodeBlock, DocPage } from './doc.tsx'
|
|
17
|
+
// Renderer de markdown do DocBrowser: prosa + fence ```tsx preview vira componente vivo.
|
|
18
|
+
export { DocMarkdown } from './markdown.tsx'
|
|
19
|
+
export type { DocMarkdownProps } from './markdown.tsx'
|
|
20
|
+
// O changelog do pacote, pronto pra montar (o site o usa como página do menu superior).
|
|
21
|
+
export { OpusChangelog, ChangelogView } from './changelog.tsx'
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocMarkdown — o renderer de markdown do DocBrowser. Prosa/tabelas via markdown-it; e
|
|
3
|
+
* fence-dispatch pelos TOKENS do markdown-it (robusto — crase inline na prosa NÃO confunde,
|
|
4
|
+
* ao contrário de um split por regex):
|
|
5
|
+
*
|
|
6
|
+
* ```tsx preview → componente RODANDO (sucrase transpila o jsx → createElement, avaliado
|
|
7
|
+
* com o `scope`), dentro do <Example> (preview + snippet, tema da casa)
|
|
8
|
+
* ```<lang> → <CodeBlock> (highlight + copiar)
|
|
9
|
+
* (prosa/tabelas) → markdown-it
|
|
10
|
+
*
|
|
11
|
+
* `scope` (default: o opus/ui) = os componentes disponíveis nos blocos `tsx preview`. O
|
|
12
|
+
* `new Function` é aceitável porque o markdown é conteúdo do projeto (confiável), não de terceiro.
|
|
13
|
+
* Só fences de nível 0 viram preview/CodeBlock; fences aninhados caem no HTML do markdown-it.
|
|
14
|
+
*/
|
|
15
|
+
import * as React from 'react'
|
|
16
|
+
import { markdownIt as md } from '../components/primitives/markdown.tsx'
|
|
17
|
+
import { transform } from 'sucrase'
|
|
18
|
+
import { CodeBlock, Example } from './doc.tsx'
|
|
19
|
+
import * as opusUi from '../react.tsx'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
// Scope default estável (computado uma vez) — evita re-eval do LiveBlock a cada render. Os blocos
|
|
23
|
+
// `tsx preview` enxergam o opus/ui (componentes) + os hooks do React (bare `useState`… além de
|
|
24
|
+
// `React.useState`). Ícones (lucide) NÃO entram aqui de propósito: quem monta a doc injeta a paleta
|
|
25
|
+
// pelo `scope` (o registry passa o lucide inteiro) — assim o renderer não arrasta o lucide pro
|
|
26
|
+
// bundle de TODO consumidor do DocMarkdown; só a superfície que exibe o catálogo paga por ele.
|
|
27
|
+
const DEFAULT_SCOPE: Record<string, unknown> = {
|
|
28
|
+
...opusUi,
|
|
29
|
+
useState: React.useState,
|
|
30
|
+
useEffect: React.useEffect,
|
|
31
|
+
useRef: React.useRef,
|
|
32
|
+
useMemo: React.useMemo,
|
|
33
|
+
useCallback: React.useCallback,
|
|
34
|
+
useId: React.useId,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function LiveBlock({
|
|
38
|
+
code,
|
|
39
|
+
scope,
|
|
40
|
+
previewClassName,
|
|
41
|
+
}: {
|
|
42
|
+
code: string
|
|
43
|
+
scope: Record<string, unknown>
|
|
44
|
+
previewClassName?: string
|
|
45
|
+
}): React.ReactElement {
|
|
46
|
+
const result = React.useMemo<{ Comp?: () => React.ReactNode; error?: string }>(() => {
|
|
47
|
+
try {
|
|
48
|
+
// Exemplo com ESTADO: se o snippet chama `render(<…/>)`, ele roda como CORPO de um
|
|
49
|
+
// componente (os hooks rodam no lugar certo). Bare JSX (uma OU várias irmãs) → embrulho
|
|
50
|
+
// num Fragment antes do render() — várias expressões soltas não passam como um argumento só.
|
|
51
|
+
const hasRender = /\brender\s*\(/.test(code)
|
|
52
|
+
const body = `let __el = null; const render = (e) => { __el = e; };\n${hasRender ? code.trim() : `render(<>${code.trim()}</>)`}\nreturn __el;`
|
|
53
|
+
const js = transform(body, { transforms: ['jsx', 'typescript'], jsxRuntime: 'classic' }).code
|
|
54
|
+
// Só passa pro new Function os identificadores que o snippet realmente cita (o scope tem
|
|
55
|
+
// ~1500 nomes com o lucide) e que são identificador válido (ex.: 'default' fica de fora).
|
|
56
|
+
// React entra SEMPRE — o JSX clássico compila pra React.createElement.
|
|
57
|
+
const entries = Object.entries({ React, ...scope }).filter(
|
|
58
|
+
([k]) => /^[A-Za-z_$][\w$]*$/.test(k) && k !== 'default' && (k === 'React' || code.includes(k)),
|
|
59
|
+
)
|
|
60
|
+
const fn = new Function(...entries.map(([k]) => k), js)
|
|
61
|
+
const args = entries.map(([, v]) => v)
|
|
62
|
+
// Componente estável (memoizado por [code, scope]): React o chama no render, então os
|
|
63
|
+
// hooks do snippet executam durante um render de verdade — não num eval solto.
|
|
64
|
+
const Comp = (): React.ReactNode => fn(...args) as React.ReactNode
|
|
65
|
+
return { Comp }
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return { error: e instanceof Error ? e.message : String(e) }
|
|
68
|
+
}
|
|
69
|
+
}, [code, scope])
|
|
70
|
+
const Comp = result.Comp
|
|
71
|
+
return (
|
|
72
|
+
<Example code={code.trim()} previewClassName={previewClassName}>
|
|
73
|
+
{result.error ? <span className="text-sm text-red-500">{result.error}</span> : Comp ? <Comp /> : null}
|
|
74
|
+
</Example>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface DocMarkdownProps {
|
|
79
|
+
content: string
|
|
80
|
+
/** ESTENDE o scope dos blocos ```tsx preview (opus/ui + lucide + hooks). Ex.: um pattern injeta
|
|
81
|
+
* o palco doc-client. Omitido = só o default. */
|
|
82
|
+
scope?: Record<string, unknown>
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function DocMarkdown({ content, scope }: DocMarkdownProps): React.ReactElement {
|
|
86
|
+
// O `scope` ESTENDE o default (não substitui): o pattern injeta o palco doc-client
|
|
87
|
+
// (DocBrowserActionProvider + contratos) sem perder o opus/ui + lucide + hooks. Memoiza pra
|
|
88
|
+
// não re-avaliar os LiveBlocks a cada render (o scope injetado é estável no chamador).
|
|
89
|
+
const liveScope = React.useMemo(() => (scope ? { ...DEFAULT_SCOPE, ...scope } : DEFAULT_SCOPE), [scope])
|
|
90
|
+
// Ignora o frontmatter (---\n…\n---) — a nav já tem o título e o corpo tem o H1. Sem isso,
|
|
91
|
+
// o markdown-it renderiza "title: …" como texto (é robusto pros dois caminhos: registry e folder).
|
|
92
|
+
const body = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '')
|
|
93
|
+
const tokens = md.parse(body, {})
|
|
94
|
+
const parts: React.ReactNode[] = []
|
|
95
|
+
let buffer: typeof tokens = []
|
|
96
|
+
let key = 0
|
|
97
|
+
const flush = (): void => {
|
|
98
|
+
if (buffer.length === 0) return
|
|
99
|
+
parts.push(<div key={key++} dangerouslySetInnerHTML={{ __html: md.renderer.render(buffer, md.options, {}) }} />)
|
|
100
|
+
buffer = []
|
|
101
|
+
}
|
|
102
|
+
for (const t of tokens) {
|
|
103
|
+
if (t.type === 'fence' && t.level === 0) {
|
|
104
|
+
flush()
|
|
105
|
+
const info = (t.info ?? '').trim()
|
|
106
|
+
const infoTokens = info.split(/\s+/)
|
|
107
|
+
const lang = infoTokens[0] || 'tsx'
|
|
108
|
+
const flags = infoTokens.slice(1)
|
|
109
|
+
// Palco do preview via flags no info (ex.: `tsx preview col md`): `col`/`col-start` = layout
|
|
110
|
+
// em coluna (bloco: Alert, AspectRatio, grupo vertical); um token de largura (`md`, `2xl`…) =
|
|
111
|
+
// cap de `max-w-*` (form/campo que não deve esticar). Viram o previewClassName do Example.
|
|
112
|
+
const dir = flags.includes('col-start') ? 'flex-col items-start' : flags.includes('col') ? 'flex-col items-stretch' : ''
|
|
113
|
+
const widthTok = flags.find((f) => /^(sm|md|lg|xl|2xl|3xl|4xl)$/.test(f))
|
|
114
|
+
const previewClassName = [dir, widthTok ? `max-w-${widthTok}` : ''].filter(Boolean).join(' ') || undefined
|
|
115
|
+
parts.push(
|
|
116
|
+
<div key={key++} className="not-prose">
|
|
117
|
+
{/\b(preview|live)\b/.test(info) ? (
|
|
118
|
+
<LiveBlock code={t.content} scope={liveScope} previewClassName={previewClassName} />
|
|
119
|
+
) : (
|
|
120
|
+
<CodeBlock code={t.content.replace(/\n$/, '')} lang={lang} />
|
|
121
|
+
)}
|
|
122
|
+
</div>,
|
|
123
|
+
)
|
|
124
|
+
} else {
|
|
125
|
+
buffer.push(t)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
flush()
|
|
129
|
+
return <div className="prose prose-sm max-w-none space-y-4">{parts}</div>
|
|
130
|
+
}
|