@softize/opus 25.5.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 +50 -1
- package/docs/adr/0017-ui-assumes-a-fixed-desktop-layout.md +19 -0
- package/docs/adr/0056-doc-browser-separa-runtime-leve-do-catalogo.md +8 -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 +5 -1
- package/src/ui/components/patterns/sidebar.tsx +32 -11
- package/src/ui/components/primitives/markdown.tsx +2 -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 +17 -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/markdown.md +41 -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/folder.tsx +2 -1
- package/src/ui/docs/index.ts +8 -1
- package/src/ui/docs/links.ts +102 -0
- 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,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. |
|
|
@@ -4,123 +4,165 @@ title: Primeiros passos
|
|
|
4
4
|
|
|
5
5
|
# Primeiros passos
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
as ferramentas usam essas definições sem exigir schemas e tipos paralelos.
|
|
7
|
+
Vamos calcular o preço de uma assinatura a partir do tamanho da equipe. Ao terminar, você terá
|
|
8
|
+
uma operação com entrada validada, um handler no servidor e um teste que confirma a regra.
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
Neste exemplo, cada pessoa custa R$ 29 por mês. A consulta é pública, não grava dados e não
|
|
11
|
+
precisa de banco. Isso permite acompanhar o caminho completo de uma action antes de configurar
|
|
12
|
+
um servidor HTTP ou uma interface.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
formulários e documentação. No Opus, entidades, dicionários e contratos de action descrevem essas
|
|
15
|
-
regras uma vez. A API e a web consomem a mesma definição, enquanto runtime, adapters, UI, CLI
|
|
16
|
-
e verificações ajudam a mantê-la consistente. Cada projeto acrescenta seu domínio sobre essa
|
|
17
|
-
base versionada.
|
|
14
|
+
## Prepare o projeto
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Para um projeto novo, use `opus create`: ele gera o app com os pré-requisitos do protocolo, da UI e
|
|
22
|
-
do preview já configurados, e os gates passam desde o início.
|
|
23
|
-
|
|
24
|
-
O Opus e a Base ficam no npm público: a instalação não exige token nem configuração de escopo.
|
|
25
|
-
Crie o projeto e materialize as duas camadas antes dos gates:
|
|
16
|
+
Se você já trabalha em um projeto Opus, siga para o contrato. Para criar um projeto novo, use um
|
|
17
|
+
ambiente com Node.js e pnpm instalados:
|
|
26
18
|
|
|
27
19
|
```bash
|
|
28
20
|
pnpm dlx @softize/opus create meu-app
|
|
29
|
-
cd meu-app
|
|
21
|
+
cd meu-app
|
|
22
|
+
git init
|
|
23
|
+
pnpm install
|
|
24
|
+
pnpm run setup
|
|
25
|
+
pnpm test
|
|
30
26
|
```
|
|
31
27
|
|
|
32
|
-
O
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
O projeto inclui TypeScript, Vite, React, um domínio de tarefas e os comandos de teste e
|
|
29
|
+
verificação. `setup` materializa as instruções e os artefatos do Opus e da Base usados pelo
|
|
30
|
+
projeto. Para adotar em um aplicativo existente, criar um monorepo ou resolver uma falha de
|
|
31
|
+
instalação, consulte [Instalação](/docs/installation).
|
|
32
|
+
|
|
33
|
+
## Declare o contrato
|
|
34
|
+
|
|
35
|
+
O contrato diz o que a operação recebe, o que devolve e quem pode chamá-la. Ele fica em um arquivo
|
|
36
|
+
que cliente e servidor conseguem importar, sem banco, segredo ou driver de servidor.
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
uma versão anterior. Se o CLI responder com “Comando desconhecido”, informe a versão explicitamente
|
|
40
|
-
com `pnpm dlx @softize/opus@<versão> create meu-app` ou desative a quarentena apenas nessa chamada
|
|
41
|
-
com `pnpm --config.minimum-release-age=0 dlx …`. Dentro do projeto, o workspace exclui Opus e Base
|
|
42
|
-
da quarentena para preservar o par validado.
|
|
38
|
+
Crie `src/domains/subscription/actions/quote.contract.ts`:
|
|
43
39
|
|
|
44
|
-
|
|
40
|
+
```ts
|
|
41
|
+
import { defineContract } from '@softize/opus/core'
|
|
42
|
+
import { z } from 'zod'
|
|
43
|
+
|
|
44
|
+
export const subscriptionQuoteContract = defineContract({
|
|
45
|
+
name: 'subscription.quote',
|
|
46
|
+
kind: 'simple',
|
|
47
|
+
summary: 'Calcula a mensalidade de uma equipe',
|
|
48
|
+
description: 'A assinatura custa R$ 29 por pessoa por mês, com pelo menos uma pessoa.',
|
|
49
|
+
input: z.object({ seats: z.number().int().positive() }),
|
|
50
|
+
output: z.object({ monthlyTotalInCents: z.number().int().nonnegative() }),
|
|
51
|
+
effect: 'read',
|
|
52
|
+
personalData: false,
|
|
53
|
+
public: true,
|
|
54
|
+
})
|
|
55
|
+
```
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
porta;
|
|
50
|
-
- `opus.json`, com a versão adotada, e as instruções, skills e hooks gerenciados.
|
|
57
|
+
`seats` aceita somente um inteiro positivo. O valor devolvido usa centavos: três pessoas custam
|
|
58
|
+
`8700`, ou R$ 87. `public: true` permite consultar o preço sem login; quando esse campo é omitido,
|
|
59
|
+
a action exige uma pessoa autenticada.
|
|
51
60
|
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
O nome `subscription.quote` identifica a operação. O `kind: 'simple'` a define como uma chamada
|
|
62
|
+
sem formulário, coleção ou página de detalhe. Os outros tipos estão em
|
|
63
|
+
[Actions e contratos](/docs/actions).
|
|
54
64
|
|
|
55
|
-
|
|
56
|
-
detecta o `pnpm-workspace.yaml` e gera só os arquivos do app, sem um workspace aninhado. O que a raiz
|
|
57
|
-
precisa ter, como o glob de packages, `allowBuilds` e as exclusões de quarentena para Opus e Base,
|
|
58
|
-
aparece como aviso, sem alterar os arquivos da raiz:
|
|
65
|
+
## Ligue a regra ao contrato
|
|
59
66
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
O handler recebe a entrada validada e calcula a resposta. `bindAction` liga esse código ao
|
|
68
|
+
contrato sem colocar a implementação no módulo que o cliente importa.
|
|
69
|
+
|
|
70
|
+
Crie `src/domains/subscription/actions/quote.ts`:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { bindAction } from '@softize/opus/core'
|
|
74
|
+
import { subscriptionQuoteContract } from './quote.contract.ts'
|
|
75
|
+
|
|
76
|
+
export const subscriptionQuote = bindAction(subscriptionQuoteContract, {
|
|
77
|
+
handler: async (_ctx, input) => ({
|
|
78
|
+
monthlyTotalInCents: input.seats * 2900,
|
|
79
|
+
}),
|
|
80
|
+
})
|
|
64
81
|
```
|
|
65
82
|
|
|
66
|
-
|
|
83
|
+
O contrato permanece compartilhável. O binding fica do lado do servidor, onde também poderão
|
|
84
|
+
entrar banco, integrações e outras dependências da regra.
|
|
67
85
|
|
|
68
|
-
|
|
69
|
-
`opus setup` roda em cada app e grava o marcador, o inventário de copy e as projeções específicas do
|
|
70
|
-
SDK. `base setup` materializa na raiz do repositório o método geral, a memória, os agentes e a
|
|
71
|
-
revisão. Os componentes vêm de `@softize/opus/ui/react`, e o tema, de um arquivo CSS.
|
|
86
|
+
## Confira o resultado e o limite da entrada
|
|
72
87
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
`runAction` executa a action pela fronteira do contrato. Assim, o teste verifica tanto a conta
|
|
89
|
+
quanto a validação que ocorre antes do handler.
|
|
90
|
+
|
|
91
|
+
Crie `src/domains/subscription/actions/quote.test.ts`:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { runAction } from '@softize/opus/testing'
|
|
95
|
+
import { expect, it } from 'vitest'
|
|
96
|
+
import { subscriptionQuote } from './quote.ts'
|
|
97
|
+
|
|
98
|
+
it('calcula a assinatura de três pessoas', async () => {
|
|
99
|
+
const { output } = await runAction(subscriptionQuote, { seats: 3 })
|
|
100
|
+
expect(output).toEqual({ monthlyTotalInCents: 8700 })
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('recusa uma equipe sem pessoas', async () => {
|
|
104
|
+
await expect(runAction(subscriptionQuote, { seats: 0 })).rejects.toMatchObject({
|
|
105
|
+
code: 'validation.invalid_input',
|
|
106
|
+
})
|
|
107
|
+
})
|
|
79
108
|
```
|
|
80
109
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
110
|
+
Execute `pnpm test`. Os dois testes devem passar. Não foi necessário criar um servidor, simular
|
|
111
|
+
login ou configurar banco: o teste usa a operação ligada ao seu handler.
|
|
112
|
+
|
|
113
|
+
## Inclua a action no domínio
|
|
114
|
+
|
|
115
|
+
Um domínio reúne as operações e os dados de um assunto do produto. Registre a nova action em
|
|
116
|
+
`src/domains/subscription/index.ts`:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { defineDomain } from '@softize/opus/core'
|
|
120
|
+
import { subscriptionQuote } from './actions/quote.ts'
|
|
121
|
+
|
|
122
|
+
export const subscriptionDomain = defineDomain({
|
|
123
|
+
name: 'subscription',
|
|
124
|
+
description: 'Assinaturas de equipes e cálculo de mensalidade.',
|
|
125
|
+
actions: { subscriptionQuote },
|
|
126
|
+
})
|
|
85
127
|
```
|
|
86
128
|
|
|
87
|
-
|
|
129
|
+
No `opus.config.ts`, importe `subscriptionDomain` e acrescente-o ao array `domains` que já existe.
|
|
130
|
+
Essa lista informa à CLI quais domínios entram no manifest gerado pelo projeto.
|
|
88
131
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
interface estão em [Como consumir](/ui/como-consumir).
|
|
132
|
+
Para executar a action no runtime, registre o mesmo domínio. Este trecho pode rodar em um
|
|
133
|
+
arquivo do servidor ou em um teste de integração:
|
|
92
134
|
|
|
93
135
|
```ts
|
|
94
136
|
import { createRuntime } from '@softize/opus/core'
|
|
95
|
-
import {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
auth: jwtAuth({ secret: process.env.JWT_SECRET!, mapUser: (p) => ({ id: String(p.sub) }) }),
|
|
105
|
-
audit: [pgAudit({ pool })], // trilha de auditoria
|
|
106
|
-
config: { env, i18n: { defaultLocale: 'pt-BR' } },
|
|
137
|
+
import { subscriptionDomain } from './domains/subscription/index.ts'
|
|
138
|
+
|
|
139
|
+
const runtime = createRuntime()
|
|
140
|
+
runtime.register([subscriptionDomain])
|
|
141
|
+
|
|
142
|
+
const result = await runtime.execute('subscription.quote', { seats: 3 }, {
|
|
143
|
+
user: null,
|
|
144
|
+
tenantId: null,
|
|
145
|
+
can: () => false,
|
|
107
146
|
})
|
|
108
147
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
148
|
+
if (result.ok) {
|
|
149
|
+
console.log(result.data) // { monthlyTotalInCents: 8700 }
|
|
150
|
+
}
|
|
112
151
|
```
|
|
113
152
|
|
|
114
|
-
|
|
153
|
+
O runtime devolve um resultado com `ok` para distinguir sucesso de falha. A consulta continua
|
|
154
|
+
pública mesmo sem usuário. Uma action que não foi registrada não pode ser executada.
|
|
155
|
+
|
|
156
|
+
## Continue a partir do que você precisa construir
|
|
115
157
|
|
|
116
|
-
|
|
158
|
+
Você já declarou, executou e testou uma action. O próximo passo depende de onde a operação será
|
|
159
|
+
usada:
|
|
117
160
|
|
|
118
|
-
|
|
119
|
-
|
|
161
|
+
- Para publicar a operação por HTTP e acrescentar banco ou autenticação, siga para
|
|
162
|
+
[Runtime e adapters](/docs/runtime).
|
|
163
|
+
- Para criar um formulário, uma coleção ou um detalhe, veja os tipos em
|
|
164
|
+
[Actions e contratos](/docs/actions) e os [componentes de UI](/ui/como-consumir).
|
|
165
|
+
- Para cobrir permissões e efeitos da operação, avance para [Testar actions](/docs/testing).
|
|
120
166
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
manifest.
|
|
124
|
-
- Antes de entregar, rode `opus check` (convenções das actions e da UI), `opus copy --check` e
|
|
125
|
-
`base copy check` (inventário e política de copy) e, com banco, `opus db check` (divergência entre
|
|
126
|
-
entidades e banco).
|
|
167
|
+
Antes de entregar uma mudança, execute `pnpm exec opus check`, `pnpm exec opus copy --check`
|
|
168
|
+
e os gates do seu repositório. Consulte [CLI](/docs/cli) para entender o que cada comando verifica.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Instalação
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Instalação
|
|
6
|
+
|
|
7
|
+
Use `opus create` para começar um projeto com TypeScript, React, testes e as verificações do
|
|
8
|
+
Opus configurados. Se o aplicativo já existe, instale o pacote e execute o setup para acrescentar
|
|
9
|
+
os artefatos do SDK ao repositório.
|
|
10
|
+
|
|
11
|
+
Os comandos desta página usam pnpm. Os pacotes Opus e Base são públicos e não exigem token de
|
|
12
|
+
registro.
|
|
13
|
+
|
|
14
|
+
## Criar um aplicativo
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm dlx @softize/opus create meu-app
|
|
18
|
+
cd meu-app
|
|
19
|
+
git init
|
|
20
|
+
pnpm install
|
|
21
|
+
pnpm run setup
|
|
22
|
+
pnpm test
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
O aplicativo inclui um domínio de tarefas, `opus.config.ts`, Vite, React, o tema do Opus e os
|
|
26
|
+
comandos de teste e verificação. O dev server respeita a variável `PORT`. `opus.json` registra a
|
|
27
|
+
versão do SDK adotada pelo app; `setup` materializa as instruções, skills e hooks gerenciados.
|
|
28
|
+
|
|
29
|
+
Quando os testes passarem, continue em [Primeiros passos](/docs/getting-started).
|
|
30
|
+
|
|
31
|
+
## Criar um monorepo
|
|
32
|
+
|
|
33
|
+
Crie a raiz do workspace antes dos aplicativos:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pnpm dlx @softize/opus create meu-cliente --monorepo
|
|
37
|
+
cd meu-cliente
|
|
38
|
+
git init
|
|
39
|
+
pnpm dlx @softize/opus create apps/portal
|
|
40
|
+
pnpm install
|
|
41
|
+
pnpm --dir apps/portal run setup
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Dentro de um workspace pnpm, `create` gera somente os arquivos do aplicativo. Ele detecta o
|
|
45
|
+
`pnpm-workspace.yaml` da raiz e evita criar um workspace aninhado.
|
|
46
|
+
|
|
47
|
+
Se a raiz já existir, o comando avisa quando faltar configuração para incluir o app em
|
|
48
|
+
`packages`, permitir o postinstall com `allowBuilds` ou excluir Opus e Base da quarentena de
|
|
49
|
+
versões. Esses avisos não alteram os arquivos da raiz.
|
|
50
|
+
|
|
51
|
+
## Adotar em um projeto existente
|
|
52
|
+
|
|
53
|
+
Na pasta do aplicativo, instale o SDK e execute o setup do Opus. Execute o setup da Base na raiz
|
|
54
|
+
do repositório:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pnpm add @softize/opus
|
|
58
|
+
pnpm add -D @softize/base
|
|
59
|
+
pnpm exec opus setup
|
|
60
|
+
pnpm exec base setup
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Os dois setups são idempotentes. O Opus grava o marcador `opus.json`, o inventário de copy e as
|
|
64
|
+
projeções específicas do SDK. A Base materializa o método geral de trabalho, a memória e os
|
|
65
|
+
agentes. Em um monorepo, cada aplicativo mantém seu próprio marcador e sua configuração Opus.
|
|
66
|
+
|
|
67
|
+
Para usar a UI em um aplicativo com Tailwind v4, importe o tema e inclua os componentes do
|
|
68
|
+
pacote no scan do Tailwind. Este exemplo considera um CSS dentro de `src/`:
|
|
69
|
+
|
|
70
|
+
```css
|
|
71
|
+
@import '@softize/opus/ui/theme.css';
|
|
72
|
+
@source '../node_modules/@softize/opus/src/ui/**/*.{ts,tsx}';
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Os componentes são importados de `@softize/opus/ui/react`. A configuração dos providers e da
|
|
76
|
+
execução de actions está em [Como consumir](/ui/como-consumir).
|
|
77
|
+
|
|
78
|
+
## Resolver uma falha antes da criação
|
|
79
|
+
|
|
80
|
+
Se o pnpm informar que o diretório global de binários não está no `PATH`, execute `pnpm setup`
|
|
81
|
+
e abra uma nova sessão do terminal. Quando o setup apontar `ERR_PNPM_BAD_SHELL_SECTION`,
|
|
82
|
+
`pnpm setup --force` substitui o bloco antigo do pnpm no arquivo de inicialização do shell.
|
|
83
|
+
|
|
84
|
+
Uma política de quarentena do pnpm pode resolver uma versão anterior durante as primeiras
|
|
85
|
+
horas de uma release. Se `create` responder com “Comando desconhecido”, informe a versão que
|
|
86
|
+
você quer usar:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pnpm dlx @softize/opus@<versão> create meu-app
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Outra opção é desativar a quarentena somente nessa chamada com
|
|
93
|
+
`pnpm --config.minimum-release-age=0 dlx @softize/opus create meu-app`.
|
|
94
|
+
O workspace gerado exclui Opus e Base dessa quarentena para manter o par adotado pelo template.
|
|
@@ -34,6 +34,45 @@ const sample = [
|
|
|
34
34
|
render(<Markdown content={sample} />)
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
## Documentos navegáveis
|
|
38
|
+
|
|
39
|
+
Ao apresentar uma pasta de documentação, use `headingIds` para gerar âncoras e `resolveHref` para
|
|
40
|
+
transformar os links internos na rota da aplicação. Os utilitários de
|
|
41
|
+
`@softize/opus/ui/docs/links` mantêm os dois lados na mesma gramática sem decidir a URL final.
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import {
|
|
45
|
+
documentationSlugFromSourcePath,
|
|
46
|
+
resolveDocumentationLinkTarget,
|
|
47
|
+
} from '@softize/opus/ui/docs/links'
|
|
48
|
+
|
|
49
|
+
const sourcePath = 'guides/current.md'
|
|
50
|
+
|
|
51
|
+
<Markdown
|
|
52
|
+
content={content}
|
|
53
|
+
headingIds
|
|
54
|
+
resolveHref={(href) => {
|
|
55
|
+
const target = resolveDocumentationLinkTarget(sourcePath, href)
|
|
56
|
+
if (target === null) return href
|
|
57
|
+
const slug = documentationSlugFromSourcePath(target.sourcePath)
|
|
58
|
+
return `/docs/project/${slug}${target.anchor === undefined ? '' : `#${target.anchor}`}`
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Passe `sourcePath` relativo à raiz documental. Um link absoluto iniciado por `/docs/` volta a essa
|
|
64
|
+
raiz; `docs/` sem a barra inicial continua sendo uma subpasta comum. Protocolos, arquivos que não
|
|
65
|
+
são Markdown e caminhos que escapam da raiz retornam `null`, para a aplicação preservar ou tratar o
|
|
66
|
+
endereço original.
|
|
67
|
+
|
|
68
|
+
## Referência de links documentais
|
|
69
|
+
|
|
70
|
+
| Função | Resultado |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `resolveDocumentationLinkTarget(currentSourcePath, href)` | Devolve o caminho `.md` ou `.mdx` relativo à raiz e a âncora normalizada. Decodifica segmentos percentuais válidos e devolve `null` quando o endereço não pertence ao acervo. |
|
|
73
|
+
| `documentationSlugFromSourcePath(sourcePath)` | Remove a extensão e os prefixos numéricos de ordenação de cada segmento. Preserva a hierarquia restante. |
|
|
74
|
+
| `documentationAnchor(fragment)` | Decodifica e normaliza um fragmento URI para comparação com o ID do título. |
|
|
75
|
+
| `documentationHeadingAnchor(text)` | Normaliza o texto visível de um título sem interpretar sequências percentuais como URI. |
|
|
37
76
|
|
|
38
77
|
## Propriedades de Markdown
|
|
39
78
|
|
|
@@ -41,3 +80,5 @@ render(<Markdown content={sample} />)
|
|
|
41
80
|
|---|---|---|---|
|
|
42
81
|
| `content` | `string` | obrigatório | O texto em Markdown (CommonMark e tabelas do GFM, via markdown-it). HTML no texto é escapado, não interpretado, por isso o componente serve para texto escrito por pessoas ou gerado por modelos. Endereços soltos, como `https://…`, viram links. |
|
|
43
82
|
| `className` | `string` | | Classes do contêiner. |
|
|
83
|
+
| `resolveHref` | `(href: string) => string` | | Reescreve cada link antes da validação e da renderização. Use para transformar caminhos documentais em rotas da aplicação. |
|
|
84
|
+
| `headingIds` | `boolean` | `false` | Gera IDs normalizados e únicos nos títulos. Ative em documentos navegáveis; mantenha desativado em mensagens comuns. |
|
|
@@ -57,8 +57,8 @@ entrada. Em `_meta`, cada tool traz os
|
|
|
57
57
|
identificadores e `com.softize.opus/data-product-labels`, o nome legível de cada um.
|
|
58
58
|
|
|
59
59
|
Quando a action declara o que faz com os dados, a tool traz também as anotações padrão do MCP:
|
|
60
|
-
`readOnlyHint` vem de `effect`.
|
|
61
|
-
partir de `ai.destructive`, porque o MCP trata a ausência
|
|
60
|
+
`readOnlyHint` vem de `effect`. Em uma action que escreve, `destructiveHint` vai sempre explícito, a
|
|
61
|
+
partir de `ai.destructive`, porque o MCP trata a ausência em uma escrita como destrutiva.
|
|
62
62
|
Em `_meta`, `com.softize.opus/effect` repete o efeito e `com.softize.opus/personal-data` diz se a
|
|
63
63
|
resposta contém dado pessoal. Na action dona de um tipo de recurso, `com.softize.opus/assistant`
|
|
64
64
|
traz o assistente declarado, para o host conhecê-lo pelo catálogo que já consulta e aplicar à saída
|