@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.
Files changed (52) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/bin/cli.mjs +68 -3
  3. package/bin/lib/check.mjs +86 -10
  4. package/bin/lib/copy.mjs +505 -67
  5. package/bin/lib/materialize.mjs +99 -1
  6. package/docs/adr/0028-schedules-run-actions-as-the-system-by-action-opt-in.md +1 -1
  7. package/docs/adr/0037-design-data-lives-outside-the-contract.md +1 -1
  8. package/docs/adr/0045-surface-replaces-content-and-card.md +3 -0
  9. package/docs/adr/0061-claude-md-imports-agents-md.md +41 -0
  10. package/docs/adr/0062-listas-contidas-e-texto-flexivel.md +68 -0
  11. package/docs/adr/0063-metadados-das-superficies.md +44 -0
  12. package/docs/adr/README.md +6 -0
  13. package/docs/code-style.md +47 -8
  14. package/docs/protocol.md +1 -1
  15. package/docs/releasing.md +7 -0
  16. package/package.json +1 -1
  17. package/registry/instructions/opus.md +3 -1
  18. package/registry/skills/build-opus-ui/SKILL.md +6 -4
  19. package/registry/skills/build-opus-ui/references/evaluations.md +10 -1
  20. package/registry/skills/build-opus-ui/references/ui-patterns.md +10 -2
  21. package/registry/skills/maintain-opus-docs/SKILL.md +2 -1
  22. package/registry/skills/maintain-opus-docs/references/editorial-standard.md +9 -0
  23. package/registry/skills/maintain-opus-docs/references/evaluations.md +8 -0
  24. package/registry/templates/app/pnpm-workspace.yaml +1 -1
  25. package/src/core/system-actor.ts +1 -1
  26. package/src/core/types.ts +1 -1
  27. package/src/ui/components/patterns/list.tsx +19 -10
  28. package/src/ui/components/patterns/page.tsx +27 -0
  29. package/src/ui/components/patterns/structural-header.tsx +25 -3
  30. package/src/ui/components/patterns/surface.tsx +36 -8
  31. package/src/ui/components/primitives/dialog.tsx +74 -42
  32. package/src/ui/components/primitives/drawer.tsx +69 -42
  33. package/src/ui/components/primitives/table.tsx +3 -3
  34. package/src/ui/docs/content/action-list.md +15 -8
  35. package/src/ui/docs/content/actions.md +1 -1
  36. package/src/ui/docs/content/auth.md +3 -3
  37. package/src/ui/docs/content/cli.md +6 -0
  38. package/src/ui/docs/content/communication.md +10 -8
  39. package/src/ui/docs/content/dialog.md +39 -0
  40. package/src/ui/docs/content/drawer.md +36 -0
  41. package/src/ui/docs/content/events.md +13 -13
  42. package/src/ui/docs/content/installation.md +4 -0
  43. package/src/ui/docs/content/log.md +1 -1
  44. package/src/ui/docs/content/page.md +30 -1
  45. package/src/ui/docs/content/queue.md +2 -2
  46. package/src/ui/docs/content/router.md +1 -1
  47. package/src/ui/docs/content/scheduler.md +15 -15
  48. package/src/ui/docs/content/surface.md +41 -8
  49. package/src/ui/docs/content/table.md +7 -0
  50. package/src/ui/docs/content/testing.md +4 -4
  51. package/src/ui/react.tsx +4 -0
  52. 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
- <StructuralHeader
261
- name="DrawerHeader"
262
- slot="drawer"
263
- slots={{
264
- leading: DrawerNavigation,
265
- media: DrawerMedia,
266
- title: DrawerTitle,
267
- description: DrawerDescription,
268
- actions: DrawerActions,
269
- }}
270
- leadingPlacement="inline"
271
- titleRequired={false}
272
- rowClassName={surfaceHeaderRowClasses("drawer")}
273
- className={cn(
274
- "text-left",
275
- surfaceRegionClasses("drawer", layout, "header"),
276
- className,
277
- )}
278
- trailing={
279
- showCloseButton ? (
280
- <DrawerCloseButton disabled={closeDisabled} size={closeSize} />
281
- ) : undefined
282
- }
283
- {...props}
284
- >
285
- {children}
286
- </StructuralHeader>
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
- <StructuralHeader
329
- name="DrawerIntro"
330
- slot="drawer-intro"
331
- slots={{
332
- title: DrawerTitle,
333
- description: DrawerDescription,
334
- actions: DrawerIntroActions,
335
- }}
336
- leadingPlacement="inline"
337
- titleRequired
338
- className={cn("text-left", className)}
339
- {...props}
340
- >
341
- {children}
342
- </StructuralHeader>
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-nowrap text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[0.125rem]",
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-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[0.125rem]",
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 barra mantém em uma única linha os controles declarados como `inline`: ela não quebra e não move
14
- filtros para o modal por falta de espaço. Seletores de valor único acompanham o valor escolhido até
15
- `20rem` e truncam o excedente; a busca varia entre `8rem` e `16rem` conforme a largura
16
- disponível. Quando a barra ficar longa demais, declare como `advanced` os filtros que não precisam
17
- ficar sempre à vista, como descreve a seção “Onde cada filtro aparece”. Recarregar e
18
- exibição formam um `ButtonGroup` espaçado; `toolbarActions` acrescenta, depois deles, operações
19
- ligadas ao recorte atual.
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=chave:dir&status=…&view=board&cols=a,b,c&limit=25`.
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/pedidos.ts` quanto `design/scenarios/pedidos/lista.ts`. O cenário vence a
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 notaEmitir = bindAction(notaEmitirContract, {
68
- authorize: (ctx) => ctx.can('nota.emitir'),
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('nota.cancelar', nota))) {
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(nome)`) gera o diagnóstico `content` habitual e
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.chave`), nunca passado adiante, espalhado ou
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, a
101
- linha e o campo exibido pelo diagnóstico. `runtime-data` registra valores como nomes,
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 corresponder a um único diagnóstico reprova o
106
- gate e precisa ser revisto. Diagnósticos estruturais ou de transformação visual não podem ser
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
- "line": 42,
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
- "line": 58,
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 // 'nota.emitida'
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
- `nota.*`, não são reconhecidos: assine `*` e filtre no handler.
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 notaEmitir = bindAction(notaEmitirContract, {
55
- emits: ['nota.emitida'],
54
+ export const invoiceIssue = bindAction(invoiceIssueContract, {
55
+ emits: ['invoice.issued'],
56
56
  handler: async (ctx, input) => {
57
- const nota = await repo.create(input)
57
+ const invoice = await repo.create(input)
58
58
  // O runtime preenche id, timestamp, actor, tenant, source e trace.
59
- await ctx.emit('nota.emitida', { id: nota.id, valor: nota.valor })
60
- return nota
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 notaAvisarFinanceiro = defineReactionContract<{ id: string; valor: number }>({
90
- name: 'nota.avisar-financeiro',
91
- on: 'nota.emitida',
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 notaAvisarFinanceiroImpl = bindReaction(notaAvisarFinanceiro, {
96
+ export const invoiceNotifyFinanceImpl = bindReaction(invoiceNotifyFinance, {
97
97
  handler: async (ctx, event) => {
98
- ctx.log.info('nota emitida', { id: event.data.id, valor: event.data.valor })
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: [notaAvisarFinanceiroImpl] })`
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', { valor: input.valor })
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 relatorioGerar = bindAction(relatorioGerarContract, {
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 gerarRelatorio(input)
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(valor, origin)` e compare o `origin` do resultado, em vez de conferir apenas o prefixo do
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 { caixaFechar } from './caixa-fechar.ts' // a action 'caixa.fechar', preparada abaixo
50
+ import { ledgerClose } from './ledger-close.ts' // a action 'ledger.close', preparada abaixo
51
51
 
52
- const fecharDia = defineSchedule({
53
- name: 'caixa.fechar-dia',
54
- action: 'caixa.fechar',
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 caixaDomain = defineDomain({
60
- name: 'caixa',
61
- actions: { caixaFechar },
62
- schedules: [fecharDia],
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([caixaDomain])
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 caixaFecharContract = defineContract({
95
- name: 'caixa.fechar',
94
+ export const ledgerCloseContract = defineContract({
95
+ name: 'ledger.close',
96
96
  kind: 'simple',
97
97
  input: z.object({}),
98
- output: z.object({ fechados: z.number() }),
98
+ output: z.object({ closedEntries: z.number() }),
99
99
  automatable: true,
100
100
  })
101
101
 
102
- export const caixaFechar = bindAction(caixaFecharContract, {
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('caixa:fechar'),
105
- handler: async (ctx) => ({ fechados: await caixa.fecharDia() }),
104
+ authorize: (ctx) => isSystemActor(ctx) || ctx.can('ledger:close'),
105
+ handler: async (ctx) => ({ closedEntries: await ledger.closeDay() }),
106
106
  })
107
107
  ```
108
108