@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.
Files changed (286) hide show
  1. package/CHANGELOG.md +1616 -0
  2. package/LICENSE +21 -0
  3. package/README.md +113 -0
  4. package/bin/cli.mjs +528 -0
  5. package/bin/lib/check.mjs +307 -0
  6. package/bin/lib/components.mjs +151 -0
  7. package/bin/lib/create.mjs +208 -0
  8. package/bin/lib/db-check-runner.mjs +86 -0
  9. package/bin/lib/db-migrate-runner.mjs +89 -0
  10. package/bin/lib/db-scaffold-runner.mjs +84 -0
  11. package/bin/lib/db.mjs +261 -0
  12. package/bin/lib/docs-include.mjs +48 -0
  13. package/bin/lib/gen-dicts.mjs +134 -0
  14. package/bin/lib/gen-docs.mjs +288 -0
  15. package/bin/lib/gen-manifest.mjs +102 -0
  16. package/bin/lib/gen-openapi.mjs +195 -0
  17. package/bin/lib/gen-runner.mjs +472 -0
  18. package/bin/lib/gen-stubs.mjs +463 -0
  19. package/bin/lib/gen.mjs +311 -0
  20. package/bin/lib/init.mjs +514 -0
  21. package/bin/lib/introspect.mjs +107 -0
  22. package/bin/lib/mcp.mjs +85 -0
  23. package/bin/lib/postinstall.mjs +56 -0
  24. package/docs/chat-event-protocol.md +85 -0
  25. package/docs/code-style.md +16 -0
  26. package/docs/data-layer.md +246 -0
  27. package/docs/ownership-vs-shadcn-lock.md +102 -0
  28. package/docs/protocol.md +2053 -0
  29. package/docs/releasing.md +110 -0
  30. package/docs/shellnav.md +131 -0
  31. package/package.json +338 -0
  32. package/registry/hooks/hooks.json +26 -0
  33. package/registry/hooks/link-memory-on-start.mjs +46 -0
  34. package/registry/hooks/opus-check-on-stop.mjs +114 -0
  35. package/registry/skills/create-action/SKILL.md +49 -0
  36. package/registry/skills/create-action/scaffold.mjs +122 -0
  37. package/registry/templates/app/_gitignore +3 -0
  38. package/registry/templates/app/_npmrc +1 -0
  39. package/registry/templates/app/_opus/_gitignore +5 -0
  40. package/registry/templates/app/_prettierrc.json +6 -0
  41. package/registry/templates/app/index.html +13 -0
  42. package/registry/templates/app/opus.config.ts +16 -0
  43. package/registry/templates/app/package.json +43 -0
  44. package/registry/templates/app/pnpm-workspace.yaml +11 -0
  45. package/registry/templates/app/public/favicon.svg +4 -0
  46. package/registry/templates/app/src/App.tsx +37 -0
  47. package/registry/templates/app/src/domains/tasks/actions/list.test.ts +34 -0
  48. package/registry/templates/app/src/domains/tasks/actions/list.ts +33 -0
  49. package/registry/templates/app/src/domains/tasks/index.ts +13 -0
  50. package/registry/templates/app/src/index.css +18 -0
  51. package/registry/templates/app/src/main.tsx +25 -0
  52. package/registry/templates/app/tsconfig.json +20 -0
  53. package/registry/templates/app/vite.config.ts +46 -0
  54. package/registry/templates/monorepo/_gitignore +3 -0
  55. package/registry/templates/monorepo/_npmrc +1 -0
  56. package/registry/templates/monorepo/package.json +9 -0
  57. package/registry/templates/monorepo/pnpm-workspace.yaml +14 -0
  58. package/src/ai/ask.ts +64 -0
  59. package/src/ai/drivers/anthropic.ts +309 -0
  60. package/src/ai/index.ts +17 -0
  61. package/src/audit/drivers/console.ts +117 -0
  62. package/src/audit/drivers/pg.ts +172 -0
  63. package/src/audit/index.ts +51 -0
  64. package/src/auth/drivers/better-auth.ts +103 -0
  65. package/src/auth/drivers/jwt.ts +188 -0
  66. package/src/auth/index.ts +9 -0
  67. package/src/client/drivers/fetch.ts +202 -0
  68. package/src/client/index.ts +22 -0
  69. package/src/core/actions.ts +110 -0
  70. package/src/core/audit.ts +239 -0
  71. package/src/core/contracts.ts +137 -0
  72. package/src/core/domain.ts +310 -0
  73. package/src/core/errors.ts +181 -0
  74. package/src/core/index.ts +174 -0
  75. package/src/core/logical-type.ts +31 -0
  76. package/src/core/reactions.ts +81 -0
  77. package/src/core/runtime.ts +1167 -0
  78. package/src/core/schedules.ts +41 -0
  79. package/src/core/types.ts +1356 -0
  80. package/src/data/drivers/kysely.ts +389 -0
  81. package/src/data/index.ts +10 -0
  82. package/src/data/readonly-pool.ts +160 -0
  83. package/src/dsl/eval.ts +136 -0
  84. package/src/dsl/index.ts +29 -0
  85. package/src/dsl/kysely.ts +230 -0
  86. package/src/dsl/loads.ts +123 -0
  87. package/src/dsl/parser.ts +423 -0
  88. package/src/dsl/types.ts +113 -0
  89. package/src/events/drivers/mitt.ts +70 -0
  90. package/src/events/index.ts +9 -0
  91. package/src/log/drivers/pino.ts +57 -0
  92. package/src/log/index.ts +9 -0
  93. package/src/mcp/index.ts +62 -0
  94. package/src/queue/drivers/bullmq.ts +190 -0
  95. package/src/queue/index.ts +9 -0
  96. package/src/scheduler/drivers/node-cron.ts +93 -0
  97. package/src/scheduler/every.ts +45 -0
  98. package/src/scheduler/index.ts +9 -0
  99. package/src/schema/drivers/zod.ts +765 -0
  100. package/src/schema/entity.ts +439 -0
  101. package/src/schema/format/locale.ts +144 -0
  102. package/src/schema/index.ts +65 -0
  103. package/src/schema/openapi.ts +302 -0
  104. package/src/schema/scaffold.ts +160 -0
  105. package/src/server/drivers/fastify.ts +224 -0
  106. package/src/server/drivers/node.ts +386 -0
  107. package/src/server/index.ts +142 -0
  108. package/src/storage/drivers/fs.ts +90 -0
  109. package/src/storage/drivers/s3.ts +117 -0
  110. package/src/storage/index.ts +27 -0
  111. package/src/testing/fake.ts +298 -0
  112. package/src/testing/index.ts +324 -0
  113. package/src/ui/components/patterns/action-form-card.tsx +48 -0
  114. package/src/ui/components/patterns/action-list-dialog.tsx +93 -0
  115. package/src/ui/components/patterns/app-shell.tsx +227 -0
  116. package/src/ui/components/patterns/confirm.tsx +226 -0
  117. package/src/ui/components/patterns/data-state.tsx +75 -0
  118. package/src/ui/components/patterns/form-dialog.tsx +64 -0
  119. package/src/ui/components/patterns/form.tsx +584 -0
  120. package/src/ui/components/patterns/list.tsx +1488 -0
  121. package/src/ui/components/patterns/page.tsx +46 -0
  122. package/src/ui/components/patterns/section-shell.tsx +246 -0
  123. package/src/ui/components/patterns/shell-nav.tsx +150 -0
  124. package/src/ui/components/patterns/sidebar.tsx +89 -0
  125. package/src/ui/components/patterns/split.tsx +93 -0
  126. package/src/ui/components/patterns/trigger.tsx +196 -0
  127. package/src/ui/components/patterns/view.tsx +84 -0
  128. package/src/ui/components/primitives/accordion.tsx +64 -0
  129. package/src/ui/components/primitives/alert-dialog.tsx +190 -0
  130. package/src/ui/components/primitives/alert.tsx +116 -0
  131. package/src/ui/components/primitives/aspect-ratio.tsx +9 -0
  132. package/src/ui/components/primitives/avatar.tsx +107 -0
  133. package/src/ui/components/primitives/badge.tsx +37 -0
  134. package/src/ui/components/primitives/breadcrumb.tsx +109 -0
  135. package/src/ui/components/primitives/button-group.tsx +83 -0
  136. package/src/ui/components/primitives/button.tsx +102 -0
  137. package/src/ui/components/primitives/calendar.tsx +218 -0
  138. package/src/ui/components/primitives/card.tsx +56 -0
  139. package/src/ui/components/primitives/carousel.tsx +239 -0
  140. package/src/ui/components/primitives/chat.tsx +407 -0
  141. package/src/ui/components/primitives/checkbox.tsx +30 -0
  142. package/src/ui/components/primitives/collapsible.tsx +31 -0
  143. package/src/ui/components/primitives/command.tsx +182 -0
  144. package/src/ui/components/primitives/composer.tsx +121 -0
  145. package/src/ui/components/primitives/copyable.tsx +50 -0
  146. package/src/ui/components/primitives/dialog.tsx +147 -0
  147. package/src/ui/components/primitives/drawer.tsx +141 -0
  148. package/src/ui/components/primitives/empty.tsx +104 -0
  149. package/src/ui/components/primitives/field.tsx +246 -0
  150. package/src/ui/components/primitives/icon-picker.tsx +180 -0
  151. package/src/ui/components/primitives/input-group.tsx +168 -0
  152. package/src/ui/components/primitives/input-otp.tsx +75 -0
  153. package/src/ui/components/primitives/input.tsx +72 -0
  154. package/src/ui/components/primitives/item.tsx +193 -0
  155. package/src/ui/components/primitives/kbd.tsx +28 -0
  156. package/src/ui/components/primitives/label.tsx +22 -0
  157. package/src/ui/components/primitives/markdown.tsx +35 -0
  158. package/src/ui/components/primitives/menu.tsx +255 -0
  159. package/src/ui/components/primitives/pagination.tsx +127 -0
  160. package/src/ui/components/primitives/popover.tsx +87 -0
  161. package/src/ui/components/primitives/progress.tsx +29 -0
  162. package/src/ui/components/primitives/radio-group.tsx +43 -0
  163. package/src/ui/components/primitives/resizable.tsx +51 -0
  164. package/src/ui/components/primitives/scroll-area.tsx +56 -0
  165. package/src/ui/components/primitives/select.tsx +479 -0
  166. package/src/ui/components/primitives/separator.tsx +26 -0
  167. package/src/ui/components/primitives/skeleton.tsx +13 -0
  168. package/src/ui/components/primitives/slider.tsx +61 -0
  169. package/src/ui/components/primitives/sonner.tsx +46 -0
  170. package/src/ui/components/primitives/spinner.tsx +29 -0
  171. package/src/ui/components/primitives/switch.tsx +33 -0
  172. package/src/ui/components/primitives/table.tsx +114 -0
  173. package/src/ui/components/primitives/tabs.tsx +104 -0
  174. package/src/ui/components/primitives/textarea.tsx +18 -0
  175. package/src/ui/components/primitives/toggle-group.tsx +81 -0
  176. package/src/ui/components/primitives/toggle.tsx +45 -0
  177. package/src/ui/components/primitives/tooltip.tsx +55 -0
  178. package/src/ui/components/primitives/truncate.tsx +49 -0
  179. package/src/ui/docs/DocBrowser.tsx +90 -0
  180. package/src/ui/docs/changelog.tsx +80 -0
  181. package/src/ui/docs/content/accordion.md +86 -0
  182. package/src/ui/docs/content/action-form-card.md +24 -0
  183. package/src/ui/docs/content/action-form-dialog.md +30 -0
  184. package/src/ui/docs/content/action-form.md +125 -0
  185. package/src/ui/docs/content/action-list-dialog.md +68 -0
  186. package/src/ui/docs/content/action-list.md +194 -0
  187. package/src/ui/docs/content/action-trigger.md +72 -0
  188. package/src/ui/docs/content/action-view.md +47 -0
  189. package/src/ui/docs/content/actions.md +138 -0
  190. package/src/ui/docs/content/ai.md +112 -0
  191. package/src/ui/docs/content/alert-dialog.md +73 -0
  192. package/src/ui/docs/content/alert.md +69 -0
  193. package/src/ui/docs/content/app-shell.md +155 -0
  194. package/src/ui/docs/content/aspect-ratio.md +66 -0
  195. package/src/ui/docs/content/audit.md +84 -0
  196. package/src/ui/docs/content/auth.md +70 -0
  197. package/src/ui/docs/content/avatar.md +94 -0
  198. package/src/ui/docs/content/badge.md +48 -0
  199. package/src/ui/docs/content/breadcrumb.md +87 -0
  200. package/src/ui/docs/content/button-group.md +71 -0
  201. package/src/ui/docs/content/button.md +60 -0
  202. package/src/ui/docs/content/calendar.md +62 -0
  203. package/src/ui/docs/content/card.md +49 -0
  204. package/src/ui/docs/content/carousel.md +85 -0
  205. package/src/ui/docs/content/chat.md +69 -0
  206. package/src/ui/docs/content/checkbox.md +75 -0
  207. package/src/ui/docs/content/cli.md +58 -0
  208. package/src/ui/docs/content/collapsible.md +64 -0
  209. package/src/ui/docs/content/command.md +56 -0
  210. package/src/ui/docs/content/composer.md +50 -0
  211. package/src/ui/docs/content/confirm.md +120 -0
  212. package/src/ui/docs/content/copyable.md +30 -0
  213. package/src/ui/docs/content/customization.md +110 -0
  214. package/src/ui/docs/content/cycle.md +34 -0
  215. package/src/ui/docs/content/data-state.md +47 -0
  216. package/src/ui/docs/content/data.md +99 -0
  217. package/src/ui/docs/content/dialog.md +60 -0
  218. package/src/ui/docs/content/drawer.md +55 -0
  219. package/src/ui/docs/content/empty.md +66 -0
  220. package/src/ui/docs/content/events.md +61 -0
  221. package/src/ui/docs/content/field.md +58 -0
  222. package/src/ui/docs/content/getting-started.md +109 -0
  223. package/src/ui/docs/content/icon-picker.md +51 -0
  224. package/src/ui/docs/content/input-group.md +78 -0
  225. package/src/ui/docs/content/input-otp.md +72 -0
  226. package/src/ui/docs/content/input.md +78 -0
  227. package/src/ui/docs/content/item.md +84 -0
  228. package/src/ui/docs/content/kbd.md +62 -0
  229. package/src/ui/docs/content/label.md +32 -0
  230. package/src/ui/docs/content/log.md +55 -0
  231. package/src/ui/docs/content/markdown.md +41 -0
  232. package/src/ui/docs/content/mcp.md +44 -0
  233. package/src/ui/docs/content/menu.md +114 -0
  234. package/src/ui/docs/content/microcopy.md +83 -0
  235. package/src/ui/docs/content/page.md +34 -0
  236. package/src/ui/docs/content/pagination.md +99 -0
  237. package/src/ui/docs/content/popover.md +49 -0
  238. package/src/ui/docs/content/progress.md +69 -0
  239. package/src/ui/docs/content/queue.md +62 -0
  240. package/src/ui/docs/content/radio-group.md +77 -0
  241. package/src/ui/docs/content/resizable.md +86 -0
  242. package/src/ui/docs/content/router.md +56 -0
  243. package/src/ui/docs/content/runtime.md +77 -0
  244. package/src/ui/docs/content/scheduler.md +66 -0
  245. package/src/ui/docs/content/scroll-area.md +89 -0
  246. package/src/ui/docs/content/section-shell.md +121 -0
  247. package/src/ui/docs/content/select.md +342 -0
  248. package/src/ui/docs/content/separator.md +33 -0
  249. package/src/ui/docs/content/sidebar.md +38 -0
  250. package/src/ui/docs/content/skeleton.md +34 -0
  251. package/src/ui/docs/content/slider.md +64 -0
  252. package/src/ui/docs/content/spinner.md +37 -0
  253. package/src/ui/docs/content/split.md +33 -0
  254. package/src/ui/docs/content/storage.md +69 -0
  255. package/src/ui/docs/content/switch.md +69 -0
  256. package/src/ui/docs/content/table.md +102 -0
  257. package/src/ui/docs/content/tabs.md +94 -0
  258. package/src/ui/docs/content/testing.md +89 -0
  259. package/src/ui/docs/content/textarea.md +30 -0
  260. package/src/ui/docs/content/toast.md +67 -0
  261. package/src/ui/docs/content/toggle-group.md +81 -0
  262. package/src/ui/docs/content/toggle.md +72 -0
  263. package/src/ui/docs/content/tokens.md +171 -0
  264. package/src/ui/docs/content/tooltip.md +50 -0
  265. package/src/ui/docs/content/truncate.md +37 -0
  266. package/src/ui/docs/content/ui.md +40 -0
  267. package/src/ui/docs/content/upgrading.md +48 -0
  268. package/src/ui/docs/doc-client.tsx +214 -0
  269. package/src/ui/docs/doc.tsx +301 -0
  270. package/src/ui/docs/folder.tsx +149 -0
  271. package/src/ui/docs/index.ts +21 -0
  272. package/src/ui/docs/markdown.tsx +130 -0
  273. package/src/ui/docs/md-raw.d.ts +4 -0
  274. package/src/ui/docs/plugin.ts +104 -0
  275. package/src/ui/docs/registry.tsx +424 -0
  276. package/src/ui/docs/standalone.tsx +107 -0
  277. package/src/ui/drivers/react.tsx +627 -0
  278. package/src/ui/index.ts +92 -0
  279. package/src/ui/lib/cn.ts +10 -0
  280. package/src/ui/lib/zod-pt-br.ts +38 -0
  281. package/src/ui/meta.ts +412 -0
  282. package/src/ui/react.tsx +235 -0
  283. package/src/ui/router.ts +96 -0
  284. package/src/ui/theme.css +234 -0
  285. package/src/vite/design.ts +652 -0
  286. 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
+ }
@@ -0,0 +1,4 @@
1
+ declare module '*.md?raw' {
2
+ const src: string
3
+ export default src
4
+ }