@softize/opus 28.0.0 → 30.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/bin/cli.mjs +68 -3
- package/bin/lib/check.mjs +86 -10
- package/bin/lib/copy.mjs +505 -67
- package/bin/lib/materialize.mjs +99 -1
- package/docs/adr/0028-schedules-run-actions-as-the-system-by-action-opt-in.md +1 -1
- package/docs/adr/0037-design-data-lives-outside-the-contract.md +1 -1
- package/docs/adr/0045-surface-replaces-content-and-card.md +3 -0
- package/docs/adr/0061-claude-md-imports-agents-md.md +41 -0
- package/docs/adr/0062-listas-contidas-e-texto-flexivel.md +68 -0
- package/docs/adr/0063-metadados-das-superficies.md +44 -0
- package/docs/adr/README.md +6 -0
- package/docs/code-style.md +47 -8
- package/docs/protocol.md +1 -1
- package/docs/releasing.md +7 -0
- package/package.json +1 -1
- package/registry/instructions/opus.md +3 -1
- package/registry/skills/build-opus-ui/SKILL.md +6 -4
- package/registry/skills/build-opus-ui/references/evaluations.md +10 -1
- package/registry/skills/build-opus-ui/references/ui-patterns.md +10 -2
- package/registry/skills/maintain-opus-docs/SKILL.md +2 -1
- package/registry/skills/maintain-opus-docs/references/editorial-standard.md +9 -0
- package/registry/skills/maintain-opus-docs/references/evaluations.md +8 -0
- package/registry/templates/app/pnpm-workspace.yaml +1 -1
- package/src/core/system-actor.ts +1 -1
- package/src/core/types.ts +1 -1
- package/src/ui/components/patterns/list.tsx +19 -10
- package/src/ui/components/patterns/page.tsx +27 -0
- package/src/ui/components/patterns/structural-header.tsx +25 -3
- package/src/ui/components/patterns/surface.tsx +36 -8
- package/src/ui/components/primitives/dialog.tsx +74 -42
- package/src/ui/components/primitives/drawer.tsx +69 -42
- package/src/ui/components/primitives/table.tsx +3 -3
- package/src/ui/docs/content/action-list.md +15 -8
- package/src/ui/docs/content/actions.md +1 -1
- package/src/ui/docs/content/auth.md +3 -3
- package/src/ui/docs/content/cli.md +6 -0
- package/src/ui/docs/content/communication.md +10 -8
- package/src/ui/docs/content/dialog.md +39 -0
- package/src/ui/docs/content/drawer.md +36 -0
- package/src/ui/docs/content/events.md +13 -13
- package/src/ui/docs/content/installation.md +4 -0
- package/src/ui/docs/content/log.md +1 -1
- package/src/ui/docs/content/page.md +30 -1
- package/src/ui/docs/content/queue.md +2 -2
- package/src/ui/docs/content/router.md +1 -1
- package/src/ui/docs/content/scheduler.md +15 -15
- package/src/ui/docs/content/surface.md +41 -8
- package/src/ui/docs/content/table.md +7 -0
- package/src/ui/docs/content/testing.md +4 -4
- package/src/ui/react.tsx +4 -0
- package/src/vite/design.ts +1 -1
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
|
|
16
16
|
import {
|
|
17
17
|
StructuralHeader,
|
|
18
|
+
structuralMetaClasses,
|
|
18
19
|
structuralHeaderClasses,
|
|
19
20
|
validateSurfaceIntro,
|
|
20
21
|
} from "../patterns/structural-header.tsx";
|
|
@@ -50,6 +51,7 @@ const DrawerContentContext = React.createContext<DrawerContentContextValue>({
|
|
|
50
51
|
closeSize: "sm",
|
|
51
52
|
});
|
|
52
53
|
|
|
54
|
+
const DrawerMetaContext = React.createContext(false);
|
|
53
55
|
const DrawerIntroContext = React.createContext(false);
|
|
54
56
|
|
|
55
57
|
/** Estado de aberto que a pilha de modais da camada flutuante acompanha (ADR 0052). */
|
|
@@ -257,33 +259,36 @@ function DrawerHeader({
|
|
|
257
259
|
const { showCloseButton, closeDisabled, closeSize } =
|
|
258
260
|
React.useContext(DrawerContentContext);
|
|
259
261
|
return (
|
|
260
|
-
<
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
"
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
262
|
+
<DrawerMetaContext.Provider value>
|
|
263
|
+
<StructuralHeader
|
|
264
|
+
name="DrawerHeader"
|
|
265
|
+
slot="drawer"
|
|
266
|
+
slots={{
|
|
267
|
+
leading: DrawerNavigation,
|
|
268
|
+
media: DrawerMedia,
|
|
269
|
+
title: DrawerTitle,
|
|
270
|
+
description: DrawerDescription,
|
|
271
|
+
meta: DrawerMeta,
|
|
272
|
+
actions: DrawerActions,
|
|
273
|
+
}}
|
|
274
|
+
leadingPlacement="inline"
|
|
275
|
+
titleRequired={false}
|
|
276
|
+
rowClassName={surfaceHeaderRowClasses("drawer")}
|
|
277
|
+
className={cn(
|
|
278
|
+
"text-left",
|
|
279
|
+
surfaceRegionClasses("drawer", layout, "header"),
|
|
280
|
+
className,
|
|
281
|
+
)}
|
|
282
|
+
trailing={
|
|
283
|
+
showCloseButton ? (
|
|
284
|
+
<DrawerCloseButton disabled={closeDisabled} size={closeSize} />
|
|
285
|
+
) : undefined
|
|
286
|
+
}
|
|
287
|
+
{...props}
|
|
288
|
+
>
|
|
289
|
+
{children}
|
|
290
|
+
</StructuralHeader>
|
|
291
|
+
</DrawerMetaContext.Provider>
|
|
287
292
|
);
|
|
288
293
|
}
|
|
289
294
|
|
|
@@ -325,25 +330,47 @@ function DrawerIntro({
|
|
|
325
330
|
}: React.ComponentProps<"div">) {
|
|
326
331
|
return (
|
|
327
332
|
<DrawerIntroContext.Provider value>
|
|
328
|
-
<
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
333
|
+
<DrawerMetaContext.Provider value>
|
|
334
|
+
<StructuralHeader
|
|
335
|
+
name="DrawerIntro"
|
|
336
|
+
slot="drawer-intro"
|
|
337
|
+
slots={{
|
|
338
|
+
title: DrawerTitle,
|
|
339
|
+
description: DrawerDescription,
|
|
340
|
+
meta: DrawerMeta,
|
|
341
|
+
actions: DrawerIntroActions,
|
|
342
|
+
}}
|
|
343
|
+
leadingPlacement="inline"
|
|
344
|
+
titleRequired
|
|
345
|
+
className={cn("text-left", className)}
|
|
346
|
+
{...props}
|
|
347
|
+
>
|
|
348
|
+
{children}
|
|
349
|
+
</StructuralHeader>
|
|
350
|
+
</DrawerMetaContext.Provider>
|
|
343
351
|
</DrawerIntroContext.Provider>
|
|
344
352
|
);
|
|
345
353
|
}
|
|
346
354
|
|
|
355
|
+
/** Badges, datas e autoria abaixo do título e da orientação. */
|
|
356
|
+
export function DrawerMeta({
|
|
357
|
+
className,
|
|
358
|
+
...props
|
|
359
|
+
}: React.ComponentProps<"div">) {
|
|
360
|
+
const inside = React.useContext(DrawerMetaContext);
|
|
361
|
+
if (!inside)
|
|
362
|
+
throw new Error(
|
|
363
|
+
"DrawerMeta deve estar dentro de DrawerHeader ou DrawerIntro.",
|
|
364
|
+
);
|
|
365
|
+
return (
|
|
366
|
+
<div
|
|
367
|
+
data-slot="drawer-meta"
|
|
368
|
+
className={cn(structuralMetaClasses, className)}
|
|
369
|
+
{...props}
|
|
370
|
+
/>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
347
374
|
/** Ações relacionadas ao título do conteúdo, separadas das ações de chrome do header. */
|
|
348
375
|
function DrawerIntroActions({
|
|
349
376
|
className,
|
|
@@ -24,7 +24,7 @@ function Table({
|
|
|
24
24
|
data-slot="table-container"
|
|
25
25
|
data-variant={variant}
|
|
26
26
|
className={cn(
|
|
27
|
-
"relative w-full overflow-x-auto",
|
|
27
|
+
"relative w-full min-w-0 max-w-full overflow-x-auto",
|
|
28
28
|
variant === "framed" && "rounded-lg border border-border",
|
|
29
29
|
containerClassName,
|
|
30
30
|
)}
|
|
@@ -95,7 +95,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|
|
95
95
|
<th
|
|
96
96
|
data-slot="table-head"
|
|
97
97
|
className={cn(
|
|
98
|
-
"h-10 px-3 text-left align-middle font-medium whitespace-
|
|
98
|
+
"h-10 px-3 text-left align-middle font-medium whitespace-normal text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[0.125rem]",
|
|
99
99
|
className,
|
|
100
100
|
)}
|
|
101
101
|
{...props}
|
|
@@ -108,7 +108,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
|
|
108
108
|
<td
|
|
109
109
|
data-slot="table-cell"
|
|
110
110
|
className={cn(
|
|
111
|
-
"px-3 py-2 align-middle whitespace-
|
|
111
|
+
"px-3 py-2 align-middle whitespace-normal wrap-break-word [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[0.125rem]",
|
|
112
112
|
className,
|
|
113
113
|
)}
|
|
114
114
|
{...props}
|
|
@@ -10,13 +10,20 @@ limites visuais da coleção; use [`Table`](/ui/table) diretamente quando a comp
|
|
|
10
10
|
variante `framed`. Carregamento, erro e vazio usam o mesmo [`DataState`](/ui/data-state) das
|
|
11
11
|
demais coleções.
|
|
12
12
|
|
|
13
|
-
A
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
A tabela usa a largura disponível e permite quebra em células textuais. Colunas com `fit`
|
|
14
|
+
preservam valores compactos sem quebra. A largura das colunas de dados considera cabeçalhos e
|
|
15
|
+
valores, sem comprimir todas as colunas `fit` para ampliar a primeira. Os cabeçalhos quebram
|
|
16
|
+
quando falta espaço; seleção, ações de linha e células contendo apenas um switch
|
|
17
|
+
(direto ou envolvido por TooltipTrigger) mantêm a largura compacta.
|
|
18
|
+
Se identificadores sem espaços ou larguras mínimas explícitas excederem o espaço, a rolagem
|
|
19
|
+
fica nos resultados, sem alargar a toolbar ou o rodapé. Em `layout="fill"`, o cabeçalho continua fixo nessa região.
|
|
20
|
+
|
|
21
|
+
A barra distribui os controles `inline` em mais de uma linha quando necessário, preservando sua
|
|
22
|
+
ordem. Ela não move filtros automaticamente para o modal. Seletores de valor único acompanham o
|
|
23
|
+
valor escolhido até `20rem` e truncam o excedente; a busca varia entre `8rem` e `16rem`.
|
|
24
|
+
Declare como `advanced` os filtros que não precisam ficar sempre à vista, como descreve a seção
|
|
25
|
+
“Onde cada filtro aparece”. Recarregar e exibição formam um `ButtonGroup` espaçado;
|
|
26
|
+
`toolbarActions` acrescenta, depois deles, operações ligadas ao recorte atual.
|
|
20
27
|
|
|
21
28
|
```tsx preview col
|
|
22
29
|
render(
|
|
@@ -459,7 +466,7 @@ restaurá-lo.
|
|
|
459
466
|
/>
|
|
460
467
|
```
|
|
461
468
|
|
|
462
|
-
Convenção compacta (defaults omitidos): `?q=…&sort=
|
|
469
|
+
Convenção compacta (defaults omitidos): `?q=…&sort=key:dir&status=…&view=board&cols=a,b,c&limit=25`.
|
|
463
470
|
|
|
464
471
|
## Composição dos itens
|
|
465
472
|
|
|
@@ -323,7 +323,7 @@ A resposta é tipada pela entrada e pela saída do contrato; em `list`, é a pá
|
|
|
323
323
|
plugin onde os cenários estão, com caminhos ou globs:
|
|
324
324
|
`opusDesign({ scenarios: 'design/scenarios/**/*.ts' })`. Nos globs, `*` casa parte do nome dentro
|
|
325
325
|
de uma pasta e `**` casa qualquer número de pastas, inclusive nenhuma: o padrão do exemplo carrega
|
|
326
|
-
tanto `design/scenarios/
|
|
326
|
+
tanto `design/scenarios/orders.ts` quanto `design/scenarios/orders/list.ts`. O cenário vence a
|
|
327
327
|
resposta gerada. Um cenário de uma action que não existe, ou dois cenários para a mesma action,
|
|
328
328
|
impedem o runtime de design de subir, e o erro lista os nomes.
|
|
329
329
|
|
|
@@ -64,8 +64,8 @@ const runtime = createRuntime({
|
|
|
64
64
|
})
|
|
65
65
|
|
|
66
66
|
// Na action: `authorize` decide antes do handler, com o contexto já resolvido.
|
|
67
|
-
export const
|
|
68
|
-
authorize: (ctx) => ctx.can('
|
|
67
|
+
export const invoiceIssue = bindAction(invoiceIssueContract, {
|
|
68
|
+
authorize: (ctx) => ctx.can('invoice:issue'),
|
|
69
69
|
handler: async (ctx, input) => repo.create({ ...input, tenantId: ctx.tenantId }),
|
|
70
70
|
})
|
|
71
71
|
```
|
|
@@ -95,7 +95,7 @@ Quando a decisão depende de dado que só o handler conhece, verifique no própr
|
|
|
95
95
|
```ts
|
|
96
96
|
import { error } from '@softize/opus/core'
|
|
97
97
|
|
|
98
|
-
if (!(await ctx.can('
|
|
98
|
+
if (!(await ctx.can('invoice:cancel', invoice))) {
|
|
99
99
|
throw error({ code: 'auth.forbidden', category: 'authorization', message: 'Acesso negado' })
|
|
100
100
|
}
|
|
101
101
|
```
|
|
@@ -82,6 +82,7 @@ opus gen # manifest / openapi / docs / stubs
|
|
|
82
82
|
opus gen --config ./apps/api/opus.config.ts # config fora do diretório atual
|
|
83
83
|
opus gen --output ./custom/gen # pasta de saída (padrão: a do config, ou ./.gen)
|
|
84
84
|
opus copy # inventário semântico de copy dos contratos
|
|
85
|
+
opus copy --reanchor # ancora pelo conteúdo as declarações antigas do base.json
|
|
85
86
|
opus introspect # modelo da estrutura: actions, reactions, schedules e ligações
|
|
86
87
|
opus introspect --json
|
|
87
88
|
```
|
|
@@ -92,6 +93,11 @@ copy da `@softize/base` valida. O caminho vem de `base.json` (`copy.inventory`;
|
|
|
92
93
|
versionado. `--check` compara sem escrever e falha quando o inventário está ausente,
|
|
93
94
|
desatualizado ou contém copy dinâmica não declarada.
|
|
94
95
|
|
|
96
|
+
Declarações de `copy.dynamic`, `copy.exemptions` e `copy.transforms` são ancoradas pelo conteúdo
|
|
97
|
+
(`expression` ou `text`), e não pela linha. `opus copy --reanchor` converte as declarações antigas,
|
|
98
|
+
ancoradas por `line`, grava o `base.json` e lista para revisão as que mudaram de linha ou
|
|
99
|
+
continuam por linha; o detalhe está em `docs/code-style.md` do pacote.
|
|
100
|
+
|
|
95
101
|
`opus introspect` lê o código sem executá-lo e só reconhece `defineAction`, `defineReaction`,
|
|
96
102
|
`defineSchedule` e `defineDataProduct`. Actions do par `defineContract` e `bindAction`, reactions do
|
|
97
103
|
par `defineReactionContract` e `bindReaction` e schedules declarados sem `defineSchedule` ficam fora
|
|
@@ -68,7 +68,7 @@ declaração adicional:
|
|
|
68
68
|
respostas (`alert`, `prompt`, `choose`) entram no inventário como se fossem props de um
|
|
69
69
|
`ActionFormDialog`; o campo do diagnóstico é `dialog.confirm().title`,
|
|
70
70
|
`dialog.choose().actions[0].label` etc. Constantes locais resolvem normalmente. Uma função
|
|
71
|
-
que devolve template (`body: describe(
|
|
71
|
+
que devolve template (`body: describe(name)`) gera o diagnóstico `content` habitual e
|
|
72
72
|
se declara em `copy.dynamic` como `message-template`. Opções construídas em tempo de execução
|
|
73
73
|
ou mutadas depois de declaradas reprovam como estrutura, porque nenhum texto pode ser
|
|
74
74
|
atribuído a elas.
|
|
@@ -80,7 +80,7 @@ declaração adicional:
|
|
|
80
80
|
mesmo arquivo, cada literal do array vira um texto na linha em que foi escrito, em vez de um
|
|
81
81
|
diagnóstico sobre a prop. `filter`, `slice`, `toSorted` e `toReversed` podem ficar entre o
|
|
82
82
|
array e a iteração. O limite é deliberado: o array precisa ser literal e declarado no mesmo
|
|
83
|
-
arquivo; o elemento só pode ser lido (`item.
|
|
83
|
+
arquivo; o elemento só pode ser lido (`item.key`), nunca passado adiante, espalhado ou
|
|
84
84
|
atribuído; o callback não pode receber o próprio array como terceiro parâmetro; e nenhum
|
|
85
85
|
elemento pode ter a chave dinâmica. Arrays importados, derivados por spread ou por
|
|
86
86
|
concatenação continuam gerando o diagnóstico da prop.
|
|
@@ -97,13 +97,15 @@ visual que o protocolo não representa com segurança.
|
|
|
97
97
|
é copy editorial estável; `label` e `confirm.*` são as superfícies editoriais cobertas.
|
|
98
98
|
|
|
99
99
|
Quando uma prop mapeada recebe dados da aplicação, declare a superfície em
|
|
100
|
-
`base.json.copy.dynamic` em vez de excluir o arquivo inteiro. O seletor usa o caminho,
|
|
101
|
-
|
|
100
|
+
`base.json.copy.dynamic` em vez de excluir o arquivo inteiro. O seletor usa o caminho, o campo
|
|
101
|
+
e o trecho de código exibidos pelo diagnóstico, sem a linha: a declaração continua válida quando
|
|
102
|
+
o arquivo muda acima dela. `runtime-data` registra valores como nomes,
|
|
102
103
|
contagens e opções vindas de API sem transformá-los em copy editorial. `message-template`
|
|
103
104
|
acrescenta ao inventário um texto-modelo com papel semântico, para que a parte autoral de uma
|
|
104
105
|
mensagem interpolada continue passando pela política da Base. As declarações não alteram o
|
|
105
|
-
que o React renderiza; um seletor que deixa de
|
|
106
|
-
|
|
106
|
+
que o React renderiza; um seletor que deixa de encontrar o trecho reprova o gate e precisa ser
|
|
107
|
+
revisto. Declarações antigas, ancoradas por `line`, são convertidas com `opus copy --reanchor`.
|
|
108
|
+
Diagnósticos estruturais ou de transformação visual não podem ser
|
|
107
109
|
dispensados por `copy.dynamic`; ausência de conteúdo também continua bloqueante.
|
|
108
110
|
|
|
109
111
|
```json
|
|
@@ -112,7 +114,7 @@ dispensados por `copy.dynamic`; ausência de conteúdo também continua bloquean
|
|
|
112
114
|
"dynamic": [
|
|
113
115
|
{
|
|
114
116
|
"source": "src/sessions.tsx",
|
|
115
|
-
"
|
|
117
|
+
"expression": "sessionOptions",
|
|
116
118
|
"field": "<Select>.options",
|
|
117
119
|
"kind": "runtime-data",
|
|
118
120
|
"reason": "As opções são nomes de sessões retornados pela aplicação.",
|
|
@@ -120,7 +122,7 @@ dispensados por `copy.dynamic`; ausência de conteúdo também continua bloquean
|
|
|
120
122
|
},
|
|
121
123
|
{
|
|
122
124
|
"source": "src/sessions.tsx",
|
|
123
|
-
"
|
|
125
|
+
"expression": "`Excluir ${session.name}?`",
|
|
124
126
|
"field": "dialog.confirm().body",
|
|
125
127
|
"kind": "message-template",
|
|
126
128
|
"role": "dialog-body",
|
|
@@ -460,6 +460,32 @@ botão. O texto informa o efeito real, como `Excluir`, `Revogar acesso` ou `Arqu
|
|
|
460
460
|
age onde a pessoa está. O modo `alert` cobre a camada até a resposta.
|
|
461
461
|
- `DialogHost` apresenta uma solicitação por vez para que cada resposta tenha um alvo inequívoco.
|
|
462
462
|
|
|
463
|
+
## Metadados junto à identidade
|
|
464
|
+
|
|
465
|
+
Use `DialogMeta` para badges, autoria e datas relacionados ao registro. A faixa fica
|
|
466
|
+
abaixo da linha de título e ações, com espaço próprio; `DialogDescription` continua
|
|
467
|
+
reservado à orientação textual breve. Os itens quebram em várias linhas quando necessário.
|
|
468
|
+
|
|
469
|
+
```tsx
|
|
470
|
+
<Dialog defaultOpen>
|
|
471
|
+
<DialogContent>
|
|
472
|
+
<DialogHeader />
|
|
473
|
+
<DialogBody>
|
|
474
|
+
<DialogIntro>
|
|
475
|
+
<DialogTitle>Manual de atendimento</DialogTitle>
|
|
476
|
+
<DialogDescription>Orientações para a equipe.</DialogDescription>
|
|
477
|
+
<DialogMeta><Badge>Publicado</Badge><span>Por Ana</span></DialogMeta>
|
|
478
|
+
</DialogIntro>
|
|
479
|
+
<p>Conteúdo do manual.</p>
|
|
480
|
+
</DialogBody>
|
|
481
|
+
</DialogContent>
|
|
482
|
+
</Dialog>
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
A composição aceita um único `DialogMeta` e exige um `DialogTitle`. Na forma explícita, coloque-o em `DialogIntro` ou no `DialogHeader` compacto. Não coloque o slot solto no corpo.
|
|
486
|
+
|
|
487
|
+
O modo `alert` conserva sua composição especializada e não recebe `DialogMeta`.
|
|
488
|
+
|
|
463
489
|
## Propriedades de Dialog
|
|
464
490
|
|
|
465
491
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
@@ -668,3 +694,16 @@ Use `flush` quando o conteúdo controlar seus próprios recuos.
|
|
|
668
694
|
|
|
669
695
|
Na intro, `DialogTitle` usa a mesma escala de `PageTitle` (`1.875rem`). No header
|
|
670
696
|
compacto, o título conserva a escala de `1.125rem`. O nível semântico não altera essas medidas.
|
|
697
|
+
|
|
698
|
+
## Propriedades de DialogMeta
|
|
699
|
+
|
|
700
|
+
| Propriedade | Tipo | Descrição |
|
|
701
|
+
| --- | --- | --- |
|
|
702
|
+
| `children` | `ReactNode` | Badges, datas, autoria ou outros metadados do registro. |
|
|
703
|
+
| `className` | `string` | Ajusta a faixa de metadados. |
|
|
704
|
+
|
|
705
|
+
Aceita atributos nativos de `div`. O intervalo superior é de 0,75rem; os itens têm
|
|
706
|
+
0,75rem entre colunas e 0,5rem entre linhas. O componente não insere separadores nem
|
|
707
|
+
carrega dados. Omitir o slot não reserva espaço. Metadados não substituem orientações
|
|
708
|
+
ou condições necessárias à decisão e não são anunciados como descrição do modal.
|
|
709
|
+
`DialogMeta` não se aplica a `mode="alert"`; informações necessárias à resposta ficam no corpo do alerta.
|
|
@@ -190,6 +190,30 @@ de continuar, use [`Dialog mode="alert"`](/ui/dialog), que não fecha pelo cliqu
|
|
|
190
190
|
- O botão de fechar tem o nome acessível `Fechar`. Botões só com ícone no cabeçalho precisam de
|
|
191
191
|
`aria-label`.
|
|
192
192
|
|
|
193
|
+
## Metadados junto à identidade
|
|
194
|
+
|
|
195
|
+
Use `DrawerMeta` para badges, autoria e datas relacionados ao registro. A faixa fica
|
|
196
|
+
abaixo da linha de título e ações, com espaço próprio; `DrawerDescription` continua
|
|
197
|
+
reservado à orientação textual breve. Os itens quebram em várias linhas quando necessário.
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
<Drawer defaultOpen>
|
|
201
|
+
<DrawerContent>
|
|
202
|
+
<DrawerHeader />
|
|
203
|
+
<DrawerBody>
|
|
204
|
+
<DrawerIntro>
|
|
205
|
+
<DrawerTitle>Manual de atendimento</DrawerTitle>
|
|
206
|
+
<DrawerDescription>Orientações para a equipe.</DrawerDescription>
|
|
207
|
+
<DrawerMeta><Badge>Publicado</Badge><span>Por Ana</span></DrawerMeta>
|
|
208
|
+
</DrawerIntro>
|
|
209
|
+
<p>Conteúdo do manual.</p>
|
|
210
|
+
</DrawerBody>
|
|
211
|
+
</DrawerContent>
|
|
212
|
+
</Drawer>
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
A composição aceita um único `DrawerMeta` e exige um `DrawerTitle`. Na forma explícita, coloque-o em `DrawerIntro` ou no `DrawerHeader` compacto. Não coloque o slot solto no corpo.
|
|
216
|
+
|
|
193
217
|
## Propriedades de Drawer
|
|
194
218
|
|
|
195
219
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
@@ -335,3 +359,15 @@ Use `flush` quando o conteúdo controlar seus próprios recuos.
|
|
|
335
359
|
|
|
336
360
|
Na intro, `DrawerTitle` usa a mesma escala de `PageTitle` (`1.875rem`). No header
|
|
337
361
|
compacto, o título conserva a escala de `1.125rem`. O nível semântico não altera essas medidas.
|
|
362
|
+
|
|
363
|
+
## Propriedades de DrawerMeta
|
|
364
|
+
|
|
365
|
+
| Propriedade | Tipo | Descrição |
|
|
366
|
+
| --- | --- | --- |
|
|
367
|
+
| `children` | `ReactNode` | Badges, datas, autoria ou outros metadados do registro. |
|
|
368
|
+
| `className` | `string` | Ajusta a faixa de metadados. |
|
|
369
|
+
|
|
370
|
+
Aceita atributos nativos de `div`. O intervalo superior é de 0,75rem; os itens têm
|
|
371
|
+
0,75rem entre colunas e 0,5rem entre linhas. O componente não insere separadores nem
|
|
372
|
+
carrega dados. Omitir o slot não reserva espaço. Metadados não substituem orientações
|
|
373
|
+
ou condições necessárias à decisão e não são anunciados como descrição do modal.
|
|
@@ -17,7 +17,7 @@ interface EventBusAdapter {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
interface DomainEvent<T = unknown> {
|
|
20
|
-
type: string // '
|
|
20
|
+
type: string // 'invoice.issued'
|
|
21
21
|
id: string
|
|
22
22
|
timestamp: string
|
|
23
23
|
actor: { id: string | null; type?: 'user' | 'system' | 'integration' }
|
|
@@ -39,7 +39,7 @@ const bus = mittEvents() // no mesmo processo do runtime
|
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
O `mitt` entrega pelo nome exato do evento ou por `*`, que recebe todos. Padrões parciais, como
|
|
42
|
-
`
|
|
42
|
+
`invoice.*`, não são reconhecidos: assine `*` e filtre no handler.
|
|
43
43
|
|
|
44
44
|
## Emitir de uma action
|
|
45
45
|
|
|
@@ -51,13 +51,13 @@ const runtime = createRuntime({
|
|
|
51
51
|
eventBus: bus,
|
|
52
52
|
})
|
|
53
53
|
|
|
54
|
-
export const
|
|
55
|
-
emits: ['
|
|
54
|
+
export const invoiceIssue = bindAction(invoiceIssueContract, {
|
|
55
|
+
emits: ['invoice.issued'],
|
|
56
56
|
handler: async (ctx, input) => {
|
|
57
|
-
const
|
|
57
|
+
const invoice = await repo.create(input)
|
|
58
58
|
// O runtime preenche id, timestamp, actor, tenant, source e trace.
|
|
59
|
-
await ctx.emit('
|
|
60
|
-
return
|
|
59
|
+
await ctx.emit('invoice.issued', { id: invoice.id, amount: invoice.amount })
|
|
60
|
+
return invoice
|
|
61
61
|
},
|
|
62
62
|
})
|
|
63
63
|
```
|
|
@@ -86,21 +86,21 @@ no código compartilhado e o efeito, no servidor:
|
|
|
86
86
|
import { bindReaction, defineReactionContract } from '@softize/opus/core'
|
|
87
87
|
|
|
88
88
|
// shared/ — qual evento e o que a reação significa.
|
|
89
|
-
export const
|
|
90
|
-
name: '
|
|
91
|
-
on: '
|
|
89
|
+
export const invoiceNotifyFinance = defineReactionContract<{ id: string; amount: number }>({
|
|
90
|
+
name: 'invoice.notify-finance',
|
|
91
|
+
on: 'invoice.issued',
|
|
92
92
|
description: 'Avisa o financeiro quando uma nota é emitida.',
|
|
93
93
|
})
|
|
94
94
|
|
|
95
95
|
// api/ — o efeito.
|
|
96
|
-
export const
|
|
96
|
+
export const invoiceNotifyFinanceImpl = bindReaction(invoiceNotifyFinance, {
|
|
97
97
|
handler: async (ctx, event) => {
|
|
98
|
-
ctx.log.info('nota emitida', { id: event.data.id,
|
|
98
|
+
ctx.log.info('nota emitida', { id: event.data.id, amount: event.data.amount })
|
|
99
99
|
},
|
|
100
100
|
})
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
-
Registre a reaction com as actions, em `defineDomain({ reactions: [
|
|
103
|
+
Registre a reaction com as actions, em `defineDomain({ reactions: [invoiceNotifyFinanceImpl] })`
|
|
104
104
|
ou em `runtime.register`. Com reactions registradas, o `start()` exige um `EventBusAdapter` e falha
|
|
105
105
|
com `runtime.eventbus_required` quando ele não existe. `defineReaction` declara tudo num objeto só,
|
|
106
106
|
quando não há separação entre cliente e servidor.
|
|
@@ -25,6 +25,10 @@ pnpm test
|
|
|
25
25
|
O aplicativo inclui um domínio de tarefas, `opus.config.ts`, Vite, React, o tema do Opus e os
|
|
26
26
|
comandos de teste e verificação. O dev server respeita a variável `PORT`. `opus.json` registra a
|
|
27
27
|
versão do SDK adotada pelo app; `setup` materializa as instruções, skills e hooks gerenciados.
|
|
28
|
+
As instruções ficam no `AGENTS.md`, que qualquer agente lê; o `CLAUDE.md` contém só a linha
|
|
29
|
+
`@AGENTS.md`, que as importa para o Claude Code. Escreva as regras do seu projeto no `AGENTS.md`:
|
|
30
|
+
o `setup` recusa um `CLAUDE.md` com instruções que não estão no `AGENTS.md` e pede que elas sejam
|
|
31
|
+
movidas para lá.
|
|
28
32
|
|
|
29
33
|
Quando os testes passarem, continue em [Primeiros passos](/docs/getting-started).
|
|
30
34
|
|
|
@@ -44,7 +44,7 @@ const runtime = createRuntime({
|
|
|
44
44
|
|
|
45
45
|
// No handler: ctx.log já traz o request ID e a action, adicionados com child().
|
|
46
46
|
handler: async (ctx, input) => {
|
|
47
|
-
ctx.log.info('emitindo nota', {
|
|
47
|
+
ctx.log.info('emitindo nota', { amount: input.amount })
|
|
48
48
|
return repo.create(input)
|
|
49
49
|
}
|
|
50
50
|
```
|
|
@@ -360,11 +360,28 @@ Não use `PageState` para uma falha parcial. Se outra parte da página continua
|
|
|
360
360
|
estado na própria seção com [`DataState`](/ui/data-state), [`ActionView`](/ui/action-view),
|
|
361
361
|
[`ActionList`](/ui/action-list) ou [`Alert`](/ui/alert).
|
|
362
362
|
|
|
363
|
+
## Metadados junto à identidade
|
|
364
|
+
|
|
365
|
+
Use `PageMeta` para badges, autoria e datas relacionados ao registro. A faixa fica
|
|
366
|
+
abaixo da linha de título e ações, com espaço próprio; `PageDescription` continua
|
|
367
|
+
reservado à orientação textual breve. Os itens quebram em várias linhas quando necessário.
|
|
368
|
+
|
|
369
|
+
```tsx preview col
|
|
370
|
+
render(
|
|
371
|
+
<Page title="Manual de atendimento" description="Orientações para a equipe." meta={<><Badge>Publicado</Badge><span>Por Ana</span></>}>
|
|
372
|
+
<p>Conteúdo do manual.</p>
|
|
373
|
+
</Page>
|
|
374
|
+
);
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
A composição aceita um único `PageMeta` e exige um `PageTitle`. Na forma explícita, coloque-o em `PageIntro` ou no `PageHeader` compacto. Não coloque o slot solto no corpo.
|
|
378
|
+
|
|
363
379
|
## Propriedades de Page
|
|
364
380
|
|
|
365
381
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
366
382
|
| ------------- | -------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
367
383
|
| `description` | `ReactNode` | | Orientação opcional sob o título na forma curta. |
|
|
384
|
+
| `meta` | `ReactNode` | | Forma curta: metadados sob o título e a orientação. Exige `title`. |
|
|
368
385
|
| `title` | `ReactNode` | | O h1 da página. |
|
|
369
386
|
| `actions` | `ReactNode` | | Ações contextuais no extremo oposto do cabeçalho. |
|
|
370
387
|
| `layout` | `'flow' \| 'fill'` | `'flow'` | Ocupação de altura, não anatomia: `fill` limita a página ao shell para que uma região interna role e leva essa altura até uma `ActionList` composta em `PageBody`. |
|
|
@@ -503,7 +520,7 @@ Aceita atributos HTML de `div` e exige um `PageTitle`. Recebe opcionalmente `Pag
|
|
|
503
520
|
|
|
504
521
|
| Propriedade | Tipo | Descrição |
|
|
505
522
|
| ----------- | ----------------------------- | ------------------------------------------------------------------------ |
|
|
506
|
-
| `children` | `ReactNode` | Um `PageTitle` e, opcionalmente, `PageDescription` e `PageIntroActions`. |
|
|
523
|
+
| `children` | `ReactNode` | Um `PageTitle` e, opcionalmente, `PageDescription`, `PageMeta` e `PageIntroActions`. |
|
|
507
524
|
| `className` | `string` | Classes adicionais da introdução. |
|
|
508
525
|
| demais | Atributos de `HTMLDivElement` | Repassados à região introdutória. |
|
|
509
526
|
|
|
@@ -536,3 +553,15 @@ controla a moldura externa; em PageBody, controla o viewport.
|
|
|
536
553
|
PageShell permite rolagem de contingência quando a rota renderiza um fallback ou um
|
|
537
554
|
wrapper sem altura definida. Para manter o header fixo, a rota deve preservar a cadeia
|
|
538
555
|
flexível de altura até Page. Uma Page diretamente no shell já segue esse contrato.
|
|
556
|
+
|
|
557
|
+
## Propriedades de PageMeta
|
|
558
|
+
|
|
559
|
+
| Propriedade | Tipo | Descrição |
|
|
560
|
+
| --- | --- | --- |
|
|
561
|
+
| `children` | `ReactNode` | Badges, datas, autoria ou outros metadados do registro. |
|
|
562
|
+
| `className` | `string` | Ajusta a faixa de metadados. |
|
|
563
|
+
|
|
564
|
+
Aceita atributos nativos de `div`. O intervalo superior é de 0,75rem; os itens têm
|
|
565
|
+
0,75rem entre colunas e 0,5rem entre linhas. O componente não insere separadores nem
|
|
566
|
+
carrega dados. Omitir o slot não reserva espaço. Metadados não substituem orientações
|
|
567
|
+
ou condições necessárias à decisão e não substituem a descrição textual da região.
|
|
@@ -59,11 +59,11 @@ const runtime = createRuntime({
|
|
|
59
59
|
queue,
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
-
export const
|
|
62
|
+
export const reportGenerate = bindAction(reportGenerateContract, {
|
|
63
63
|
background: { enabled: true, retry: { attempts: 3 }, progress: true },
|
|
64
64
|
handler: async (ctx, input, loaded, progress) => {
|
|
65
65
|
await progress?.report({ percent: 50, message: 'Consultando dados' })
|
|
66
|
-
return
|
|
66
|
+
return generateReport(input)
|
|
67
67
|
},
|
|
68
68
|
})
|
|
69
69
|
```
|
|
@@ -76,5 +76,5 @@ o erro de uso.
|
|
|
76
76
|
|
|
77
77
|
`navigate` espera um caminho da própria origem e não valida destinos. Quando o destino vem de fora,
|
|
78
78
|
como o `?redirect=` de um pós-login, valide-o antes de navegar: resolva com
|
|
79
|
-
`new URL(
|
|
79
|
+
`new URL(target, origin)` e compare o `origin` do resultado, em vez de conferir apenas o prefixo do
|
|
80
80
|
texto.
|
|
@@ -47,26 +47,26 @@ Declare o schedule com `defineSchedule` e registre-o no domínio, ao lado da act
|
|
|
47
47
|
|
|
48
48
|
```ts
|
|
49
49
|
import { createRuntime, defineDomain, defineSchedule } from '@softize/opus/core'
|
|
50
|
-
import {
|
|
50
|
+
import { ledgerClose } from './ledger-close.ts' // a action 'ledger.close', preparada abaixo
|
|
51
51
|
|
|
52
|
-
const
|
|
53
|
-
name: '
|
|
54
|
-
action: '
|
|
52
|
+
const closeDay = defineSchedule({
|
|
53
|
+
name: 'ledger.close-day',
|
|
54
|
+
action: 'ledger.close',
|
|
55
55
|
cron: '0 23 * * *',
|
|
56
56
|
timezone: 'America/Sao_Paulo',
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
-
export const
|
|
60
|
-
name: '
|
|
61
|
-
actions: {
|
|
62
|
-
schedules: [
|
|
59
|
+
export const ledgerDomain = defineDomain({
|
|
60
|
+
name: 'ledger',
|
|
61
|
+
actions: { ledgerClose },
|
|
62
|
+
schedules: [closeDay],
|
|
63
63
|
})
|
|
64
64
|
|
|
65
65
|
const runtime = createRuntime({
|
|
66
66
|
// …server/data…
|
|
67
67
|
scheduler,
|
|
68
68
|
})
|
|
69
|
-
runtime.register([
|
|
69
|
+
runtime.register([ledgerDomain])
|
|
70
70
|
await runtime.start()
|
|
71
71
|
```
|
|
72
72
|
|
|
@@ -91,18 +91,18 @@ recusa o sistema até que o contrato dela aceite essa execução com `automatabl
|
|
|
91
91
|
```ts
|
|
92
92
|
import { defineContract, isSystemActor } from '@softize/opus/core'
|
|
93
93
|
|
|
94
|
-
export const
|
|
95
|
-
name: '
|
|
94
|
+
export const ledgerCloseContract = defineContract({
|
|
95
|
+
name: 'ledger.close',
|
|
96
96
|
kind: 'simple',
|
|
97
97
|
input: z.object({}),
|
|
98
|
-
output: z.object({
|
|
98
|
+
output: z.object({ closedEntries: z.number() }),
|
|
99
99
|
automatable: true,
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
-
export const
|
|
102
|
+
export const ledgerClose = bindAction(ledgerCloseContract, {
|
|
103
103
|
// O sistema não tem permissões: aceite-o de forma explícita ao lado da regra das pessoas.
|
|
104
|
-
authorize: (ctx) => isSystemActor(ctx) || ctx.can('
|
|
105
|
-
handler: async (ctx) => ({
|
|
104
|
+
authorize: (ctx) => isSystemActor(ctx) || ctx.can('ledger:close'),
|
|
105
|
+
handler: async (ctx) => ({ closedEntries: await ledger.closeDay() }),
|
|
106
106
|
})
|
|
107
107
|
```
|
|
108
108
|
|