@softize/opus 24.1.0 → 25.1.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 (44) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/README.md +1 -1
  3. package/bin/cli.mjs +9 -0
  4. package/bin/lib/check.mjs +390 -1
  5. package/bin/lib/copy.mjs +39 -2
  6. package/bin/lib/gen-runner.mjs +25 -12
  7. package/bin/lib/mcp.mjs +1 -1
  8. package/docs/adr/0051-soft-delete-declares-retention-and-purge.md +224 -0
  9. package/docs/adr/0052-floating-layer-coexists-with-modal-surfaces.md +668 -0
  10. package/docs/data-layer.md +31 -3
  11. package/docs/protocol.md +98 -2
  12. package/docs/releasing.md +8 -2
  13. package/docs/stacking-scale.md +53 -0
  14. package/package.json +6 -1
  15. package/src/ai/drivers/anthropic.ts +3 -0
  16. package/src/core/domain.ts +189 -7
  17. package/src/core/index.ts +19 -2
  18. package/src/core/retention.ts +493 -0
  19. package/src/core/runtime.ts +93 -29
  20. package/src/core/system-actor.ts +33 -0
  21. package/src/core/types.ts +12 -0
  22. package/src/data/drivers/kysely.ts +47 -5
  23. package/src/data/index.ts +24 -6
  24. package/src/mcp/index.ts +3 -2
  25. package/src/schema/entity.ts +30 -9
  26. package/src/schema/index.ts +4 -0
  27. package/src/schema/openapi.ts +4 -1
  28. package/src/ui/components/primitives/dialog.tsx +285 -26
  29. package/src/ui/components/primitives/drawer.tsx +104 -13
  30. package/src/ui/components/primitives/floating-layer.tsx +79 -0
  31. package/src/ui/components/primitives/menu.tsx +17 -2
  32. package/src/ui/components/primitives/popover.tsx +6 -1
  33. package/src/ui/components/primitives/tooltip.tsx +6 -1
  34. package/src/ui/docs/content/cli.md +5 -3
  35. package/src/ui/docs/content/data.md +42 -0
  36. package/src/ui/docs/content/dialog.md +3 -0
  37. package/src/ui/docs/content/drawer.md +3 -0
  38. package/src/ui/docs/content/floating-layer.md +178 -0
  39. package/src/ui/docs/content/scheduler.md +4 -0
  40. package/src/ui/docs/registry.tsx +6 -0
  41. package/src/ui/lib/dismiss-on-iframe.ts +4 -1
  42. package/src/ui/lib/modal-layers.ts +547 -0
  43. package/src/ui/meta.ts +6 -0
  44. package/src/ui/react.tsx +4 -0
@@ -5,6 +5,7 @@ import { DropdownMenu as MenuPrimitive } from "radix-ui";
5
5
 
6
6
  import { useDismissOnIframeFocus } from "../../lib/dismiss-on-iframe.ts";
7
7
  import { cn } from "../../lib/cn.ts";
8
+ import { layerModalMarker, useLayerPortal } from "../../lib/modal-layers.ts";
8
9
 
9
10
  function Menu({ ...props }: React.ComponentProps<typeof MenuPrimitive.Root>) {
10
11
  return <MenuPrimitive.Root data-slot="menu" {...props} />;
@@ -16,9 +17,17 @@ function Menu({ ...props }: React.ComponentProps<typeof MenuPrimitive.Root>) {
16
17
  * próxima versão major.
17
18
  */
18
19
  function MenuPortal({
20
+ container,
19
21
  ...props
20
22
  }: React.ComponentProps<typeof MenuPrimitive.Portal>) {
21
- return <MenuPrimitive.Portal data-slot="menu-portal" {...props} />;
23
+ const layer = useLayerPortal();
24
+ return (
25
+ <MenuPrimitive.Portal
26
+ data-slot="menu-portal"
27
+ container={container ?? layer.container}
28
+ {...props}
29
+ />
30
+ );
22
31
  }
23
32
 
24
33
  function MenuTrigger({
@@ -35,14 +44,18 @@ function MenuContent({
35
44
  // Mesma razão do Popover: clique dentro de iframe não chega aqui fora.
36
45
  const ref = React.useRef<HTMLDivElement>(null);
37
46
  useDismissOnIframeFocus(ref);
47
+ // Aberto de dentro de uma camada flutuante, vai para o hospedeiro dela (ADR 0052).
48
+ const layer = useLayerPortal();
38
49
  return (
39
- <MenuPrimitive.Portal>
50
+ <MenuPrimitive.Portal container={layer.container}>
40
51
  <MenuPrimitive.Content
41
52
  ref={ref}
42
53
  data-slot="menu-content"
54
+ {...layerModalMarker(layer)}
43
55
  sideOffset={sideOffset}
44
56
  className={cn(
45
57
  "z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[10rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md will-change-[transform,opacity] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
58
+ layer.zIndexClass,
46
59
  className,
47
60
  )}
48
61
  {...props}
@@ -215,11 +228,13 @@ function MenuSubContent({
215
228
  className,
216
229
  ...props
217
230
  }: React.ComponentProps<typeof MenuPrimitive.SubContent>) {
231
+ const layer = useLayerPortal();
218
232
  return (
219
233
  <MenuPrimitive.SubContent
220
234
  data-slot="menu-sub-content"
221
235
  className={cn(
222
236
  "z-50 min-w-[10rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md will-change-[transform,opacity] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
237
+ layer.zIndexClass,
223
238
  className,
224
239
  )}
225
240
  {...props}
@@ -2,6 +2,7 @@ import * as React from "react"
2
2
  import { Popover as PopoverPrimitive } from "radix-ui"
3
3
 
4
4
  import { useDismissOnIframeFocus } from "../../lib/dismiss-on-iframe.ts"
5
+ import { layerModalMarker, useLayerPortal } from "../../lib/modal-layers.ts"
5
6
  import { cn } from '../../lib/cn.ts'
6
7
 
7
8
  function Popover({
@@ -26,15 +27,19 @@ function PopoverContent({
26
27
  // pendurado sobre o conteúdo. Na cabine, o preview É um iframe.
27
28
  const ref = React.useRef<HTMLDivElement>(null)
28
29
  useDismissOnIframeFocus(ref)
30
+ // Aberto de dentro de uma camada flutuante, vai para o hospedeiro dela (ADR 0052).
31
+ const layer = useLayerPortal()
29
32
  return (
30
- <PopoverPrimitive.Portal>
33
+ <PopoverPrimitive.Portal container={layer.container}>
31
34
  <PopoverPrimitive.Content
32
35
  ref={ref}
33
36
  data-slot="popover-content"
37
+ {...layerModalMarker(layer)}
34
38
  align={align}
35
39
  sideOffset={sideOffset}
36
40
  className={cn(
37
41
  "z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border border-border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
42
+ layer.zIndexClass,
38
43
  className
39
44
  )}
40
45
  {...props}
@@ -2,6 +2,7 @@ import * as React from "react"
2
2
  import { Tooltip as TooltipPrimitive } from "radix-ui"
3
3
 
4
4
  import { cn } from '../../lib/cn.ts'
5
+ import { layerModalMarker, useLayerPortal } from "../../lib/modal-layers.ts"
5
6
 
6
7
  function TooltipProvider({
7
8
  delayDuration = 0,
@@ -34,13 +35,17 @@ function TooltipContent({
34
35
  children,
35
36
  ...props
36
37
  }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
38
+ // Aberto de dentro de uma camada flutuante, vai para o hospedeiro dela (ADR 0052).
39
+ const layer = useLayerPortal()
37
40
  return (
38
- <TooltipPrimitive.Portal>
41
+ <TooltipPrimitive.Portal container={layer.container}>
39
42
  <TooltipPrimitive.Content
40
43
  data-slot="tooltip-content"
44
+ {...layerModalMarker(layer)}
41
45
  sideOffset={sideOffset}
42
46
  className={cn(
43
47
  "z-50 w-fit max-w-sm origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-pretty text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
48
+ layer.zIndexClass,
44
49
  className
45
50
  )}
46
51
  {...props}
@@ -20,11 +20,13 @@ opus db check # drift entidade ↔ banco (read-only)
20
20
  opus seed check # bindings, dependências, ciclos e scripts paralelos de seed
21
21
  ```
22
22
 
23
- `opus check` lê o código-fonte sem executá-lo e aplica regras em três grupos: actions
23
+ `opus check` lê o código-fonte sem executá-lo e aplica regras em quatro grupos: actions
24
24
  (`action-name`, `kind`, `field-order`, `export`, `requires-sem-authorize`, `ignored-by-kind`), UI
25
- (`ui-structure`, `ui-structure-split`, `ui-semantic-api`, `removed-ui-token`, `unpaired-ui-surface`) e
25
+ (`ui-structure`, `ui-structure-split`, `ui-semantic-api`, `removed-ui-token`, `unpaired-ui-surface`),
26
26
  [Produtos de Dados](/docs/data-products) (`data-product-export`, `-id`, `-duplicate`, `-version`,
27
- `-interfaces`, `-interface`, `-entities` e `-entity`, todas com o prefixo `data-product`). Um projeto
27
+ `-interfaces`, `-interface`, `-entities` e `-entity`, todas com o prefixo `data-product`) e
28
+ [exclusão lógica](/docs/data) (`soft-delete-retention`, que cobra a retenção de cada entidade e a
29
+ ligação com o domínio). Um projeto
28
30
  marcado com `opus.json` e ainda sem actions passa sem achados; sem o marcador, nenhuma action
29
31
  encontrada é falha, porque um gate que não verificou nada não é aprovação. `opus check --help` descreve cada regra.
30
32
 
@@ -30,6 +30,48 @@ export const SkillEntity = defineEntity({
30
30
  })
31
31
  ```
32
32
 
33
+ ## Exclusão lógica e expurgo
34
+
35
+ Use `softDelete` quando um registro excluído precisa continuar no banco por um tempo, como
36
+ histórico ou para atender uma regra de guarda. `kyselyRepo` passa a marcar `deletedAt` em
37
+ `remove`, e `findById`, `all` e `list` deixam os excluídos de fora.
38
+
39
+ A declaração também diz quando o registro excluído sai de vez. Informe o prazo em `retainDays`, o
40
+ fuso da rotina e os textos da action que o Opus gera para expurgar:
41
+
42
+ ```ts
43
+ export const ReportEntity = defineEntity({
44
+ name: 'report',
45
+ fields: { title: t.string() },
46
+ softDelete: {
47
+ retainDays: 90,
48
+ timezone: 'America/Sao_Paulo',
49
+ label: 'Expurgar relatórios excluídos',
50
+ description: 'Apaga de vez os relatórios excluídos há mais de 90 dias.',
51
+ },
52
+ })
53
+ ```
54
+
55
+ O domínio que lista a entidade em `entities` ganha a action `report.purge-deleted` e a rotina
56
+ diária `report.purge-deleted.daily`, às 3h30 no fuso declarado. Só o sistema executa a action,
57
+ que fica fora do REST e da IA, e ela nunca expurga antes do prazo. Com `kyselyData` no runtime, o
58
+ handler padrão apaga as linhas em lotes, das mais antigas para as mais novas; a rotina exige um
59
+ [agendador](/docs/scheduler). Cada lote que apaga algo registra `opus.purge` no log com os ids
60
+ removidos, e a execução termina com `opus.purge.total`, que traz a quantidade.
61
+
62
+ Antes de declarar `retainDays`, confira as chaves estrangeiras que apontam para a tabela. Uma chave
63
+ restritiva faz cada execução falhar no lote da linha travada, que está entre as mais antigas, e o
64
+ expurgo fica parado até a chave ganhar cascata ou um `bindPurge` apagar os dependentes primeiro.
65
+
66
+ Quando apagar a linha não basta, como ao remover arquivos em outro serviço, registre um handler
67
+ próprio com `bindPurge`, de `@softize/opus/data`, em `purges` no mesmo domínio. O handler recebe o
68
+ corte e só apaga o que foi excluído antes dele: é isso que garante o prazo. Para guardar o
69
+ registro excluído para sempre, declare o motivo: `softDelete: { retain: 'forever', reason }`.
70
+
71
+ O [`opus check`](/docs/cli) reprova `softDelete: true`, que não diz por quanto tempo o registro
72
+ fica guardado. Opções, nomes gerados e erros estão no `docs/protocol.md` do pacote, §15, "Retenção
73
+ e expurgo".
74
+
33
75
  ## Código em camelCase, banco em snake_case
34
76
 
35
77
  O schema tipado do Kysely e todas as consultas usam camelCase (`workspaceId`, `ghRepo`). O banco
@@ -441,6 +441,9 @@ botão. O texto informa o efeito real, como `Excluir`, `Revogar acesso` ou `Arqu
441
441
  - O comportamento selecionado por `mode` determina o papel de acessibilidade; `role` não configura
442
442
  o comportamento.
443
443
  - O foco permanece dentro do diálogo aberto e retorna ao gatilho após o fechamento.
444
+ - No modo padrão, uma [`FloatingLayer`](/ui/floating-layer), como o painel de um assistente,
445
+ continua utilizável com o diálogo aberto: o Tab passa por ela, o foco não é puxado de volta e o Esc
446
+ age onde a pessoa está. O modo `alert` cobre a camada até a resposta.
444
447
  - `DialogHost` apresenta uma solicitação por vez para que cada resposta tenha um alvo inequívoco.
445
448
 
446
449
  ## Propriedades de Dialog
@@ -170,6 +170,9 @@ de continuar, use [`Dialog mode="alert"`](/ui/dialog), que não fecha pelo cliqu
170
170
  - Com `modal` ativo, o padrão, o foco permanece dentro do painel e o restante da página fica
171
171
  inacessível até o fechamento.
172
172
  - Ao fechar, o foco retorna ao gatilho.
173
+ - Uma [`FloatingLayer`](/ui/floating-layer), como o painel de um assistente, continua utilizável
174
+ com o painel lateral aberto: o Tab passa por ela, o foco não é puxado de volta e o Esc age onde a
175
+ pessoa está. Ela recebe o lado do Drawer em `data-modal-side` para sair da frente dele.
173
176
  - O botão de fechar tem o nome acessível `Fechar`. Botões só com ícone no cabeçalho precisam de
174
177
  `aria-label`.
175
178
 
@@ -0,0 +1,178 @@
1
+ Use `FloatingLayer` para uma região que acompanha a aplicação inteira e precisa continuar utilizável
2
+ por cima de qualquer `Dialog` ou `Drawer`, como o botão e o painel de um assistente. Quem está
3
+ preenchendo um formulário em um modal pode abrir o painel, perguntar e voltar ao formulário, que
4
+ continua aberto e com o que já foi digitado.
5
+
6
+ No exemplo, abra o formulário, digite um nome e use o assistente no canto inferior direito. O modal
7
+ não fecha, e fechar o painel devolve o foco ao campo em que você estava.
8
+
9
+ ```tsx preview
10
+ function AssistantDemo() {
11
+ const [editing, setEditing] = useState(false);
12
+ const [assistant, setAssistant] = useState(false);
13
+
14
+ return (
15
+ <>
16
+ <Dialog
17
+ open={editing}
18
+ onOpenChange={(open) => {
19
+ setEditing(open);
20
+ if (!open) setAssistant(false);
21
+ }}
22
+ >
23
+ <DialogTrigger asChild>
24
+ <Button variant="outline">Editar cliente</Button>
25
+ </DialogTrigger>
26
+ <DialogContent>
27
+ <DialogHeader>
28
+ <DialogTitle>Editar cliente</DialogTitle>
29
+ </DialogHeader>
30
+ <DialogBody>
31
+ <Input aria-label="Nome do cliente" placeholder="Nome do cliente" />
32
+ </DialogBody>
33
+ <DialogFooter>
34
+ <DialogClose asChild>
35
+ <Button>Salvar</Button>
36
+ </DialogClose>
37
+ </DialogFooter>
38
+ </DialogContent>
39
+ </Dialog>
40
+ {editing && (
41
+ <FloatingLayer onEscapeKeyDown={() => setAssistant(false)}>
42
+ {assistant ? (
43
+ <aside
44
+ aria-label="Assistente"
45
+ className="absolute right-4 bottom-4 flex w-80 flex-col gap-3 rounded-xl border border-border bg-background p-4 shadow-lg"
46
+ >
47
+ <p className="text-sm">Pergunte sobre o cliente que você está editando.</p>
48
+ <Textarea aria-label="Mensagem" placeholder="Sua pergunta" />
49
+ <Button variant="ghost" onClick={() => setAssistant(false)}>
50
+ Fechar o assistente
51
+ </Button>
52
+ </aside>
53
+ ) : (
54
+ <Button className="absolute right-4 bottom-4" onClick={() => setAssistant(true)}>
55
+ Abrir o assistente
56
+ </Button>
57
+ )}
58
+ </FloatingLayer>
59
+ )}
60
+ </>
61
+ );
62
+ }
63
+
64
+ render(<AssistantDemo />);
65
+ ```
66
+
67
+ O exemplo monta a camada só enquanto o formulário está aberto para não deixar um botão fixo nesta
68
+ página. Em uma aplicação, a camada costuma ficar montada o tempo todo, na raiz.
69
+
70
+ ## Posicionar os filhos
71
+
72
+ `FloatingLayer` porta os filhos para um hospedeiro próprio, `div[data-slot="floating-layer"]`, no
73
+ fim do `body`. O hospedeiro ocupa a janela inteira, fica acima dos modais e não recebe clique; cada
74
+ filho direto recebe. Por isso, posicione cada filho com `absolute` contra a janela, como `right-4
75
+ bottom-4`, e não espere que ele siga a moldura da página.
76
+
77
+ Com um modal aberto, o hospedeiro informa qual `Dialog` ou `Drawer` está no topo. Diálogos que exigem
78
+ resposta não entram nesse sinal, porque cobrem a camada enquanto estão abertos:
79
+
80
+ - `data-modal` vale `none`, `dialog` ou `drawer`;
81
+ - `data-modal-side` traz o lado do Drawer: `left`, `right`, `top` ou `bottom`.
82
+
83
+ O Opus não desloca nem redimensiona modais por causa da camada. Use esses atributos para tirar o
84
+ painel de cima do que a pessoa precisa ver. Um Drawer abre, por padrão, do lado direito, o mesmo
85
+ canto do botão do exemplo; com a variante `in-data-*` do Tailwind, o painel troca de lado enquanto
86
+ esse Drawer estiver aberto:
87
+
88
+ ```tsx
89
+ <aside
90
+ aria-label="Assistente"
91
+ className="absolute right-4 bottom-4 in-data-[modal-side=right]:right-auto in-data-[modal-side=right]:left-4"
92
+ >
93
+ …
94
+ </aside>
95
+ ```
96
+
97
+ Podem existir várias camadas ao mesmo tempo. A que monta depois fica por cima.
98
+
99
+ ## O que muda enquanto um modal está aberto
100
+
101
+ Sem nenhum modal aberto, a camada é uma região comum da página. Com um `Dialog` ou `Drawer` aberto,
102
+ o modal passa a tratar a camada como parte de dentro dele:
103
+
104
+ | Aspecto | Comportamento |
105
+ | --------------- | ------------------------------------------------------------------------------------------------------ |
106
+ | Clique | A camada recebe clique e não fecha o modal, mesmo que o elemento clicado suma no próprio clique. |
107
+ | Véu | Clicar no véu continua fechando o modal, respeitando `closeDisabled`. |
108
+ | Foco | O foco pode ir para a camada por clique ou por código e não é puxado de volta. |
109
+ | Tab | Depois do último elemento do modal vem a camada; depois da camada, o primeiro elemento do modal. |
110
+ | Rolagem | A camada rola; a página atrás continua travada. |
111
+ | Leitor de tela | O modal e a camada ficam visíveis; o resto da página continua escondido. |
112
+
113
+ Quando o painel fecha com o foco dentro dele, ou a camada inteira sai da tela, o foco volta ao
114
+ elemento do modal que a pessoa usava antes de entrar na camada. Se esse elemento não existir mais,
115
+ vai para o primeiro elemento do modal. No sentido inverso, um modal que fecha enquanto a pessoa
116
+ digita na camada não leva o foco de volta ao botão que o abriu: o foco fica na camada.
117
+
118
+ Os patterns construídos sobre `Dialog` e `Drawer`, como [`ActionFormDialog`](/ui/action-form-dialog),
119
+ [`ActionListDialog`](/ui/action-list-dialog) e [`Presentation`](/ui/presentation), ganham esse
120
+ comportamento sem nenhuma configuração.
121
+
122
+ ## Esc
123
+
124
+ O Esc age onde a pessoa está. Com o foco na camada, ou depois de um clique em uma área do painel que
125
+ não recebe foco, nenhum modal fecha e o Opus chama `onEscapeKeyDown` uma vez por tecla. Com o foco
126
+ no modal, o Esc fecha o modal, como sempre. Sem modal aberto, `onEscapeKeyDown` também é chamado
127
+ quando a pessoa está na camada.
128
+
129
+ Trate o Esc da camada só por `onEscapeKeyDown`. Para impedir que o modal feche, o Opus marca o evento
130
+ com `preventDefault` antes de ele chegar aos elementos da camada; um `onKeyDown` próprio para Esc no
131
+ painel trataria a tecla duas vezes ou leria um evento já marcado.
132
+
133
+ Um popover ou menu aberto dentro da camada fecha primeiro, e só o Esc seguinte chega a ela. O mesmo
134
+ vale, no sentido inverso, para um popover aberto dentro do modal: ele fecha antes, mesmo com a
135
+ pessoa na camada.
136
+
137
+ ## Conteúdo aberto de dentro da camada
138
+
139
+ `Popover`, `Menu`, `Tooltip`, `Select`, `Dialog` e `Drawer` abertos de dentro da camada vão para o
140
+ hospedeiro dela. Assim, o menu de uma conversa abre por cima do painel que o abriu, e não por baixo
141
+ dele.
142
+
143
+ Um `Dialog` ou `Drawer` aberto de dentro da camada é modal para tudo, inclusive para a própria
144
+ camada: prende o foco, fecha pelo véu e pelo Esc e não chama o `onEscapeKeyDown` da camada.
145
+
146
+ ## Diálogos de resposta obrigatória
147
+
148
+ `Dialog mode="alert"` e as confirmações de [`dialog.confirm`](/ui/dialog) exigem uma resposta antes
149
+ de qualquer outra coisa. Enquanto um deles está aberto, o véu dele cobre a camada, o foco não chega
150
+ a ela e o leitor de tela a esconde. Isso vale também para um `Dialog` aberto de dentro do alerta.
151
+ Ao responder, a camada volta a funcionar com o modal que estava aberto antes.
152
+
153
+ ## Rolagem no toque
154
+
155
+ Num arrasto de toque, a rolagem que chega ao fim de uma área do painel pode passar a rolar a página
156
+ atrás do modal. Aplique `overscroll-contain` nas áreas roláveis da camada, como o histórico de uma
157
+ conversa. A propriedade só age no contêiner que rola, por isso o Opus não consegue aplicá-la por
158
+ você.
159
+
160
+ ## Acessibilidade
161
+
162
+ - O hospedeiro não tem papel ARIA. Dê semântica aos filhos, como um `aside` com `aria-label` para o
163
+ painel e um nome claro para o botão que o abre.
164
+ - O hospedeiro carrega `aria-live="off"`, o valor padrão do atributo: a camada não anuncia mudanças
165
+ por conta própria, e regiões vivas de dentro dela, como a de um [`Ask`](/ui/ask), mantêm o próprio
166
+ comportamento. O atributo é o que mantém a camada visível ao leitor de tela com um modal aberto.
167
+ - O foco e o Tab descritos acima são verificados em Chromium por testes automatizados. Safari,
168
+ Firefox e a leitura com VoiceOver ainda não foram verificados: o roteiro manual da ADR 0052 roda
169
+ na adoção pela Grand Brasil, em Safari com VoiceOver e no Firefox, antes de ela chegar à versão
170
+ principal da plataforma. Leitores de tela do Windows, como o NVDA, ficam fora dessa verificação.
171
+
172
+ ## Propriedades de FloatingLayer
173
+
174
+ | Propriedade | Tipo | Padrão | Descrição |
175
+ | ----------------- | -------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
176
+ | `children` | `ReactNode` | | Elementos da camada. Cada filho direto recebe clique e se posiciona contra a janela. |
177
+ | `className` | `string` | | Classes do hospedeiro. A posição de cada filho continua sendo sua. |
178
+ | `onEscapeKeyDown` | `(event: KeyboardEvent) => void` | | Chamado uma vez por tecla quando a pessoa aperta Esc na camada, com ou sem modal aberto. Nenhum modal fecha nesse caso, e o Opus não fecha a camada. |
@@ -79,6 +79,10 @@ seção). Com schedules registrados e sem `scheduler`, o `start()` falha com
79
79
  `runtime.scheduler_required`. Um schedule com `enabled: false` não é registrado nem
80
80
  conferido, então pode apontar para uma action que ainda não existe no ambiente.
81
81
 
82
+ Uma entidade com exclusão lógica e `retainDays` traz a própria rotina de expurgo, que o domínio
83
+ registra junto com a action, como `report.purge-deleted.daily`. Ela também exige o `scheduler`.
84
+ Veja "Exclusão lógica e expurgo" na página [Camada de dados](/docs/data).
85
+
82
86
  ## Preparar a action agendada
83
87
 
84
88
  O schedule executa a action como o sistema, sem uma pessoa por trás. Uma action que exige usuário
@@ -78,6 +78,7 @@ import detailMd from "./content/detail.md?raw";
78
78
  import dictionaryValueMd from "./content/dictionary-value.md?raw";
79
79
  import emptyValueMd from "./content/empty-value.md?raw";
80
80
  import drawerMd from "./content/drawer.md?raw";
81
+ import floatingLayerMd from "./content/floating-layer.md?raw";
81
82
  import menuMd from "./content/menu.md?raw";
82
83
  import emptyMd from "./content/empty.md?raw";
83
84
  import fieldMd from "./content/field.md?raw";
@@ -793,6 +794,11 @@ export const UI_SECTIONS: DocSection[] = [
793
794
  title: "Drawer",
794
795
  render: comp("Drawer", "drawer", drawerMd),
795
796
  },
797
+ {
798
+ slug: "floating-layer",
799
+ title: "Floating Layer",
800
+ render: comp("Floating Layer", "floating-layer", floatingLayerMd),
801
+ },
796
802
  { slug: "menu", title: "Menu", render: comp("Menu", "menu", menuMd) },
797
803
  {
798
804
  slug: "popover",
@@ -1,13 +1,16 @@
1
1
  import * as React from 'react'
2
2
 
3
3
  /**
4
- * Fecha a camada flutuante quando o clique cai DENTRO de um iframe.
4
+ * Fecha um popover ou menu aberto quando o clique cai DENTRO de um iframe.
5
5
  *
6
6
  * O navegador entrega o clique ao documento do iframe, e o documento de fora nunca fica
7
7
  * sabendo. Como toda dispensa por "clique fora" escuta eventos do documento de fora, o
8
8
  * popover/menu simplesmente NÃO fecha — fica pendurado sobre o conteúdo, e a pessoa
9
9
  * precisa achar um pedaço de página livre pra clicar.
10
10
  *
11
+ * Não confundir com a camada flutuante (`FloatingLayer`, ADR 0052), a região que convive com
12
+ * Dialog e Drawer modais; aqui o assunto são sobreposições transitórias presas a um gatilho.
13
+ *
11
14
  * Não é caso de borda pra nós: a cabine embute o preview num iframe, então metade da tela
12
15
  * é justamente a área onde clicar não fecha nada.
13
16
  *