@softize/opus 19.0.0 → 20.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 (142) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/bin/cli.mjs +5 -0
  3. package/bin/lib/check.mjs +198 -10
  4. package/bin/lib/copy.mjs +29 -0
  5. package/bin/lib/mcp.mjs +1 -1
  6. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +5 -0
  7. package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
  8. package/docs/data-products.md +15 -9
  9. package/docs/protocol.md +3 -4
  10. package/docs/releasing.md +2 -2
  11. package/package.json +2 -2
  12. package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
  13. package/registry/templates/app/package.json +1 -1
  14. package/src/core/runtime.ts +96 -8
  15. package/src/core/types.ts +5 -2
  16. package/src/ui/components/patterns/action-form-card.tsx +11 -1
  17. package/src/ui/components/patterns/content-header.tsx +15 -3
  18. package/src/ui/components/patterns/fill-region.ts +150 -0
  19. package/src/ui/components/patterns/list.tsx +89 -78
  20. package/src/ui/components/patterns/page.tsx +20 -4
  21. package/src/ui/components/patterns/presentation.tsx +52 -37
  22. package/src/ui/components/patterns/split.tsx +44 -4
  23. package/src/ui/components/patterns/surface-anatomy.ts +35 -8
  24. package/src/ui/components/patterns/trigger.tsx +12 -2
  25. package/src/ui/components/primitives/alert.tsx +9 -1
  26. package/src/ui/components/primitives/avatar.tsx +7 -3
  27. package/src/ui/components/primitives/carousel.tsx +34 -5
  28. package/src/ui/components/primitives/checkbox.tsx +11 -3
  29. package/src/ui/components/primitives/command.tsx +2 -1
  30. package/src/ui/components/primitives/copyable.tsx +120 -10
  31. package/src/ui/components/primitives/dialog.tsx +41 -10
  32. package/src/ui/components/primitives/drawer.tsx +9 -1
  33. package/src/ui/components/primitives/empty.tsx +1 -1
  34. package/src/ui/components/primitives/icon-picker.tsx +6 -2
  35. package/src/ui/components/primitives/icon-tile.tsx +7 -0
  36. package/src/ui/components/primitives/input-group.tsx +9 -2
  37. package/src/ui/components/primitives/input-otp.tsx +1 -1
  38. package/src/ui/components/primitives/kbd.tsx +1 -1
  39. package/src/ui/components/primitives/label.tsx +3 -0
  40. package/src/ui/components/primitives/menu.tsx +5 -0
  41. package/src/ui/components/primitives/popover.tsx +1 -1
  42. package/src/ui/components/primitives/progress.tsx +9 -1
  43. package/src/ui/components/primitives/slider.tsx +62 -11
  44. package/src/ui/components/primitives/truncate.tsx +10 -1
  45. package/src/ui/docs/content/accordion.md +74 -27
  46. package/src/ui/docs/content/action-form-card.md +24 -15
  47. package/src/ui/docs/content/action-form-dialog.md +17 -7
  48. package/src/ui/docs/content/action-form.md +92 -77
  49. package/src/ui/docs/content/action-list-dialog.md +75 -38
  50. package/src/ui/docs/content/action-list.md +134 -52
  51. package/src/ui/docs/content/action-trigger.md +27 -10
  52. package/src/ui/docs/content/action-view.md +14 -14
  53. package/src/ui/docs/content/actions.md +161 -66
  54. package/src/ui/docs/content/ai.md +43 -44
  55. package/src/ui/docs/content/alert.md +83 -29
  56. package/src/ui/docs/content/ask.md +51 -26
  57. package/src/ui/docs/content/aspect-ratio.md +22 -24
  58. package/src/ui/docs/content/audit.md +23 -19
  59. package/src/ui/docs/content/auth.md +38 -19
  60. package/src/ui/docs/content/avatar.md +54 -5
  61. package/src/ui/docs/content/badge.md +29 -9
  62. package/src/ui/docs/content/breadcrumb.md +21 -8
  63. package/src/ui/docs/content/button.md +73 -43
  64. package/src/ui/docs/content/calendar.md +134 -26
  65. package/src/ui/docs/content/card.md +41 -15
  66. package/src/ui/docs/content/carousel.md +37 -6
  67. package/src/ui/docs/content/chat.md +118 -37
  68. package/src/ui/docs/content/checkbox.md +73 -7
  69. package/src/ui/docs/content/cli.md +94 -55
  70. package/src/ui/docs/content/collapsible.md +43 -19
  71. package/src/ui/docs/content/command.md +30 -8
  72. package/src/ui/docs/content/composer.md +23 -8
  73. package/src/ui/docs/content/content.md +31 -7
  74. package/src/ui/docs/content/copyable.md +39 -8
  75. package/src/ui/docs/content/customization.md +55 -46
  76. package/src/ui/docs/content/cycle.md +33 -18
  77. package/src/ui/docs/content/data-products.md +115 -0
  78. package/src/ui/docs/content/data-state.md +20 -6
  79. package/src/ui/docs/content/data.md +20 -19
  80. package/src/ui/docs/content/detail.md +12 -5
  81. package/src/ui/docs/content/dialog.md +107 -24
  82. package/src/ui/docs/content/dictionary-value.md +4 -4
  83. package/src/ui/docs/content/dock.md +54 -19
  84. package/src/ui/docs/content/dot.md +35 -5
  85. package/src/ui/docs/content/drawer.md +63 -19
  86. package/src/ui/docs/content/empty-value.md +20 -6
  87. package/src/ui/docs/content/empty.md +24 -3
  88. package/src/ui/docs/content/events.md +76 -16
  89. package/src/ui/docs/content/executions.md +9 -9
  90. package/src/ui/docs/content/field.md +21 -6
  91. package/src/ui/docs/content/getting-started.md +44 -34
  92. package/src/ui/docs/content/icon-picker.md +19 -13
  93. package/src/ui/docs/content/icon-tile.md +28 -5
  94. package/src/ui/docs/content/input-otp.md +58 -7
  95. package/src/ui/docs/content/input.md +22 -3
  96. package/src/ui/docs/content/item.md +53 -6
  97. package/src/ui/docs/content/kbd.md +5 -1
  98. package/src/ui/docs/content/label.md +13 -4
  99. package/src/ui/docs/content/log.md +5 -5
  100. package/src/ui/docs/content/markdown.md +5 -4
  101. package/src/ui/docs/content/mcp.md +49 -23
  102. package/src/ui/docs/content/menu.md +95 -10
  103. package/src/ui/docs/content/metric-card.md +12 -4
  104. package/src/ui/docs/content/observability.md +10 -9
  105. package/src/ui/docs/content/page.md +118 -12
  106. package/src/ui/docs/content/pagination.md +13 -5
  107. package/src/ui/docs/content/pane.md +55 -5
  108. package/src/ui/docs/content/popover.md +42 -5
  109. package/src/ui/docs/content/presentation.md +91 -47
  110. package/src/ui/docs/content/progress.md +27 -14
  111. package/src/ui/docs/content/queue.md +39 -21
  112. package/src/ui/docs/content/radio-group.md +26 -4
  113. package/src/ui/docs/content/router.md +29 -10
  114. package/src/ui/docs/content/runtime.md +90 -54
  115. package/src/ui/docs/content/scheduler.md +53 -21
  116. package/src/ui/docs/content/scroll-area.md +42 -35
  117. package/src/ui/docs/content/select.md +78 -33
  118. package/src/ui/docs/content/semantic-context.md +21 -2
  119. package/src/ui/docs/content/separator.md +15 -9
  120. package/src/ui/docs/content/sidebar.md +3 -2
  121. package/src/ui/docs/content/skeleton.md +6 -1
  122. package/src/ui/docs/content/slider.md +37 -9
  123. package/src/ui/docs/content/spinner.md +4 -3
  124. package/src/ui/docs/content/split.md +19 -0
  125. package/src/ui/docs/content/storage.md +8 -9
  126. package/src/ui/docs/content/switch.md +10 -7
  127. package/src/ui/docs/content/table.md +46 -8
  128. package/src/ui/docs/content/tabs.md +14 -4
  129. package/src/ui/docs/content/testing.md +76 -31
  130. package/src/ui/docs/content/textarea.md +5 -4
  131. package/src/ui/docs/content/toast.md +48 -7
  132. package/src/ui/docs/content/toggle.md +3 -3
  133. package/src/ui/docs/content/tokens.md +15 -26
  134. package/src/ui/docs/content/tooltip.md +18 -2
  135. package/src/ui/docs/content/truncate.md +36 -6
  136. package/src/ui/docs/content/ui.md +74 -22
  137. package/src/ui/docs/content/upgrading.md +45 -36
  138. package/src/ui/docs/plugin.ts +36 -2
  139. package/src/ui/docs/registry.tsx +6 -0
  140. package/src/ui/drivers/react.tsx +16 -8
  141. package/src/ui/meta.ts +31 -31
  142. package/src/vite/design.ts +16 -4
@@ -2,7 +2,8 @@
2
2
 
3
3
  Use `Popover` para apresentar conteúdo livre junto a um gatilho, sem abrir um modal.
4
4
  `PopoverHeader` agrupa título e descrição; o restante do painel aceita formulários curtos ou
5
- detalhes.
5
+ detalhes. Para uma lista de ações, use [`Menu`](/ui/menu); para uma dica curta ao passar o ponteiro,
6
+ use [`Tooltip`](/ui/tooltip).
6
7
 
7
8
  ```tsx preview
8
9
  <Popover>
@@ -12,7 +13,7 @@ detalhes.
12
13
  <PopoverContent align="start">
13
14
  <PopoverHeader>
14
15
  <PopoverTitle>Renomear</PopoverTitle>
15
- <PopoverDescription>O slug técnico não muda — só o título.</PopoverDescription>
16
+ <PopoverDescription>O endereço da sessão continua o mesmo; só o título muda.</PopoverDescription>
16
17
  </PopoverHeader>
17
18
  <div className="mt-3 space-y-2">
18
19
  <Label htmlFor="pop-titulo">Título</Label>
@@ -23,9 +24,14 @@ detalhes.
23
24
  </Popover>
24
25
  ```
25
26
 
26
- ## Alinhamento
27
+ O painel mede `18rem` de largura por padrão e abre abaixo do gatilho. Clicar fora ou pressionar
28
+ Esc fecha o painel.
27
29
 
28
- `align` posiciona o painel em relação ao gatilho e `sideOffset` define a distância entre eles.
30
+ ## Posição e largura
31
+
32
+ `side` escolhe o lado preferido, `align` alinha o painel ao gatilho e `sideOffset` define a
33
+ distância entre eles. Quando não há espaço no lado escolhido, o painel abre no lado oposto. Ajuste
34
+ a largura com `className`, como `w-56` para `14rem`.
29
35
 
30
36
  ```tsx preview
31
37
  <Popover>
@@ -40,6 +46,12 @@ detalhes.
40
46
  <PopoverDescription>Alinhado à borda direita do gatilho.</PopoverDescription>
41
47
  </PopoverContent>
42
48
  </Popover>
49
+ <Popover>
50
+ <PopoverTrigger asChild><Button variant="ghost">À direita</Button></PopoverTrigger>
51
+ <PopoverContent side="right" className="w-56">
52
+ <PopoverDescription>Abre ao lado direito do gatilho.</PopoverDescription>
53
+ </PopoverContent>
54
+ </Popover>
43
55
  ```
44
56
 
45
57
  ## Propriedades de Popover
@@ -47,7 +59,15 @@ detalhes.
47
59
  | Propriedade | Tipo | Padrão | Descrição |
48
60
  |---|---|---|---|
49
61
  | `open` | `boolean` | | Estado no modo controlado. |
50
- | `onOpenChange` | `(open: boolean) => void` | | Atualiza o estado para permitir abertura ou fechamento por código. |
62
+ | `onOpenChange` | `(open: boolean) => void` | | Chamado quando o painel abre ou fecha; atualize `open` com o valor recebido. |
63
+ | `defaultOpen` | `boolean` | `false` | Estado inicial no modo não controlado. |
64
+ | `modal` | `boolean` | `false` | Com `true`, prende o foco no painel e bloqueia a interação com o restante da página enquanto ele está aberto. |
65
+
66
+ ## Propriedades de PopoverTrigger
67
+
68
+ | Propriedade | Tipo | Padrão | Descrição |
69
+ |---|---|---|---|
70
+ | `asChild` | `boolean` | `false` | Faz do filho (por exemplo, um `Button`) o gatilho, em vez de renderizar outro `<button>`. |
51
71
 
52
72
  ## Propriedades de PopoverAnchor
53
73
 
@@ -59,5 +79,22 @@ não introduzir um nó extra; sem ele, o `PopoverTrigger` é a âncora.
59
79
 
60
80
  | Propriedade | Tipo | Padrão | Descrição |
61
81
  |---|---|---|---|
82
+ | `side` | `'top' \| 'right' \| 'bottom' \| 'left'` | `'bottom'` | Lado preferido do painel; sem espaço, ele abre no lado oposto. |
62
83
  | `align` | `'start' \| 'center' \| 'end'` | `'center'` | Alinhamento do painel em relação ao gatilho. |
63
84
  | `sideOffset` | `number` | `4` | Distância entre o gatilho e o painel. |
85
+ | `className` | `string` | | Classes do painel. A largura padrão é `w-72` (`18rem`). |
86
+
87
+ ## Propriedades de PopoverHeader
88
+
89
+ `PopoverHeader` empilha `PopoverTitle` e `PopoverDescription` com espaçamento curto. Não tem props
90
+ próprias; aceita as de um elemento `div`.
91
+
92
+ ## Propriedades de PopoverTitle
93
+
94
+ `PopoverTitle` destaca o nome do painel com peso médio. Ele renderiza um `div`, sem nível de
95
+ heading. Não tem props próprias; aceita as de um elemento `div`.
96
+
97
+ ## Propriedades de PopoverDescription
98
+
99
+ `PopoverDescription` apresenta o texto de apoio em cor atenuada. Não tem props próprias; aceita as
100
+ de um elemento `p`.
@@ -1,45 +1,11 @@
1
- `Presentation` preserva a identidade de um recurso quando o contexto pede uma página, um dialog ou
2
- um drawer. A superfície decide quanto espaço ocupar; o recurso continua com a mesma navegação,
3
- cabeçalho, body e rodapé.
4
-
5
- Dialog e Drawer mantêm no cabeçalho a ordem horizontal de navegação ou retorno, título e ações.
6
- O retorno aparece em uma surface modal somente
7
- quando ela foi aberta sobre outro Dialog ou Drawer; a Page ao fundo sustenta o modal, mas não cria
8
- uma etapa de navegação. Formulários modais dimensionam o footer pelo conteúdo, com `Cancelar` em
9
- `ghost` e a ação principal sólida.
10
- Em Page hospedada por `PageShell`, a barra concentra navegação e ações globais. Uma Presentation
11
- de listagem materializa título e comandos de header no `Content` que envolve o `ActionList`; assim
12
- a criação permanece próxima da coleção. Esses comandos usam o tamanho `default`: o que abre outra
13
- Presentation é um `Button` na variante padrão (`solid`), e uma action `simple` vira um
14
- `ActionTrigger` na variante `ghost`. Nas demais Presentations, o título abre o conteúdo em
15
- `PageIntro` e os comandos da superfície seguem a mesma regra.
16
-
17
- Use esse padrão quando uma lista puder abrir um detalhe lateral, quando a mesma edição precisar
18
- funcionar em modal e em rota própria ou quando um fluxo começar compacto e crescer sem ganhar uma
19
- segunda implementação.
20
-
21
- Dentro de `PageShell`, a superfície `page` usa a anatomia real da aplicação: a Presentation projeta
22
- navigation e actions globais por `PageHeader`, mantém a introdução no conteúdo quando existir e
23
- compõe `PageBody` e, quando necessário, `PageFooter`. Para uma action `list`, `PageBody` recebe
24
- `Content + ActionList`; o header da coleção recebe o título e os comandos declarados no placement
25
- `header`. Dialog e Drawer continuam usando o header da própria superfície.
26
- Os comandos da coleção ficam no extremo oposto ao título e permanecem textuais, mesmo quando a
27
- action declara um ícone. A criação usa `Criar recurso`, sem ícone, em um `Button` na variante
28
- padrão (`solid`) e no tamanho `default`.
29
- Não envolva a Presentation em um card para simular a página; valide proporção, rolagem e ações no
30
- shell que efetivamente hospeda a rota.
1
+ Use `Presentation` quando o mesmo recurso precisa aparecer ora como página, ora como dialog, ora
2
+ como drawer: uma lista que abre o detalhe ao lado, uma edição que funciona em modal e em rota
3
+ própria, um fluxo que começa compacto e cresce sem ganhar uma segunda implementação. A superfície
4
+ decide quanto espaço ocupar; o recurso mantém a mesma navegação, cabeçalho, corpo e rodapé.
31
5
 
32
- O exemplo declara a Presentation sobre um contrato de formulário e alterna a superfície. O
6
+ O exemplo declara uma Presentation sobre um contrato de formulário e alterna a superfície. O
33
7
  `DocBrowserActionProvider` simula o cliente de actions; na aplicação, esse papel é do provider real.
34
8
 
35
- Quando o recurso aceita assistência contextual, declare somente o gatilho serializável em
36
- `assistant`. O renderer coloca esse gatilho no cabeçalho e abre o painel à direita do body da
37
- própria Page, Dialog ou Drawer. Cabeçalho e rodapé continuam ocupando toda a largura da superfície;
38
- enquanto o painel está aberto, o gatilho sai do cabeçalho e o próprio painel assume sua identidade
39
- visual. O dado vivo do recurso não entra na definição: a página que o carregou fornece
40
- `assistant.render`, captura identidade, nome e demais dados autorizados e monta o chat. Assim,
41
- contexto de conversa não vira autorização nem dado persistido no manifest.
42
-
43
9
  ```tsx live
44
10
  const workspacePresentation = definePresentation({
45
11
  schemaVersion: 1,
@@ -64,7 +30,7 @@ function Example() {
64
30
  <DocBrowserActionProvider>
65
31
  <div className="space-y-4">
66
32
  <ButtonGroup mode="spaced">
67
- {["page", "dialog", "drawer"].map((value) => (
33
+ {(["page", "dialog", "drawer"] as const).map((value) => (
68
34
  <Button
69
35
  key={value}
70
36
  size="sm"
@@ -115,14 +81,48 @@ function Example() {
115
81
  render(<Example />);
116
82
  ```
117
83
 
84
+ ## Como cada superfície apresenta o recurso
85
+
86
+ Em Dialog e Drawer, o cabeçalho da própria superfície reúne, nesta ordem, o retorno, o título e as
87
+ ações. O retorno só aparece quando o modal foi aberto sobre outro Dialog ou Drawer: a página ao
88
+ fundo sustenta o modal, mas não é uma etapa para onde voltar. Formulários modais dimensionam o
89
+ rodapé pelo conteúdo, com `Cancelar` em `ghost` e a ação principal sólida.
90
+
91
+ Dentro de [`PageShell`](/ui/page), a superfície `page` usa a anatomia real da aplicação. O
92
+ `PageHeader` recebe só a navegação e as ações globais; o título abre o conteúdo em `PageIntro`, e o
93
+ corpo vai para `PageBody`, com `PageFooter` quando há ações de rodapé.
94
+
95
+ Uma Presentation de listagem exibida como página muda essa distribuição para manter a criação perto
96
+ da coleção. O `PageBody` recebe um [`Content`](/ui/content) com a
97
+ [`ActionList`](/ui/action-list), e o cabeçalho desse `Content` recebe o título e os comandos
98
+ declarados com `placement: "header"`, no extremo oposto ao título. Os comandos de cabeçalho usam o
99
+ tamanho `default` e permanecem textuais, mesmo quando a action declara um ícone: o que abre outra
100
+ Presentation é um `Button` sólido, e uma action `simple` vira um
101
+ [`ActionTrigger`](/ui/action-trigger) `ghost`. O rótulo vem do `label` da action, como `Criar workspace`.
102
+
103
+ Não envolva a Presentation em um card para simular a página. Valide proporção, rolagem e ações no
104
+ shell que efetivamente hospeda a rota.
105
+
106
+ ## Assistência contextual
107
+
108
+ Quando o recurso aceita uma conversa sobre ele, declare em `assistant` somente o gatilho
109
+ serializável, como no exemplo acima. O renderer coloca o gatilho no cabeçalho e abre o painel à
110
+ direita do corpo da própria página, dialog ou drawer. Cabeçalho e rodapé continuam ocupando toda a
111
+ largura; enquanto o painel está aberto, o gatilho sai do cabeçalho e o painel assume a identidade
112
+ visual da assistência.
113
+
114
+ O dado vivo do recurso não entra na definição. A página que carregou o recurso fornece
115
+ `assistant.render`, reúne identidade, nome e demais dados autorizados e monta o chat. Assim, o
116
+ contexto da conversa não vira autorização nem dado persistido no manifest.
117
+
118
118
  ## A declaração
119
119
 
120
120
  `definePresentation` cria o artefato serializável. O body recebe uma action `form`, `list` ou
121
- `view`; uma action `simple` aparece como comando no cabeçalho ou no rodapé. Em formulário,
122
- `body.submitLabel` sobrescreve o padrão “Salvar” quando a operação tem outro resultado observável,
123
- como “Renomear” ou “Criar nova versão”. Actions que precisam de
124
- interface abrem outra Presentation. Bindings fornecem valores vindos da rota, do registro, da
125
- seleção, da sessão ou do resultado anterior sem guardar callbacks no JSON.
121
+ `view`, ou um componente React, descrito em “Conteúdo React no body”; uma action `simple` aparece
122
+ como comando no cabeçalho ou no rodapé. Em formulário, `body.submitLabel` sobrescreve o padrão
123
+ “Salvar” quando a operação tem outro resultado observável, como “Renomear” ou “Criar nova versão”.
124
+ Actions que precisam de interface abrem outra Presentation. Bindings fornecem valores vindos da
125
+ rota, do registro, da seleção, da sessão ou do resultado anterior sem guardar callbacks no JSON.
126
126
 
127
127
  Quando a exibição precisa sobreviver a refresh, deep link ou histórico, declare `route` na própria
128
128
  Presentation. O `path` aceita segmentos fixos e parâmetros como `:id`; `surface` determina como a
@@ -154,6 +154,48 @@ const workspaceList = definePresentation({
154
154
  });
155
155
  ```
156
156
 
157
+ ## Conteúdo React no body
158
+
159
+ Uma tela que já existe em React pode adotar a casca da Presentation — rota, superfície, título,
160
+ navegação e ações — sem migrar o miolo para actions declarativas de uma vez. Declare
161
+ `body: { component }` com um identificador estável para esse conteúdo e passe o conteúdo como
162
+ `children` do `Presentation`:
163
+
164
+ ```tsx
165
+ const appearance = definePresentation({
166
+ schemaVersion: 1,
167
+ id: "settings.appearance",
168
+ title: "Aparência",
169
+ route: { path: "/settings/appearance", surface: "page" },
170
+ body: { component: "settings.appearance.content" },
171
+ });
172
+
173
+ <Presentation
174
+ definition={appearance}
175
+ definitions={[appearance]}
176
+ actions={{}}
177
+ invocation={invocation}
178
+ >
179
+ <AppearanceSettings />
180
+ </Presentation>;
181
+ ```
182
+
183
+ O identificador de `component` vai para o manifest; o JSX, não. Uma Presentation com body de
184
+ componente exige `children`, e uma com body de action os recusa: nos dois casos, a renderização
185
+ lança um erro que nomeia a Presentation.
186
+
187
+ Com `heading: "shell"`, o padrão, a casca emite o título da definição. Quando o conteúdo React já
188
+ declara o heading principal da página, como um `Content level={1}`, use `heading: "content"`: dentro
189
+ de `PageShell`, a casca deixa de emitir o `PageIntro` e o documento continua com um único `h1`. Em
190
+ Dialog e Drawer, o título continua no cabeçalho da superfície.
191
+
192
+ Se o conteúdo precisa ocupar a altura da página e controlar a própria rolagem, como uma lista com
193
+ cabeçalho fixo, passe `layout="fill"` ao `Presentation`. Com o padrão `flow`, a página rola como um
194
+ documento. Uma [`ActionList`](/ui/action-list) filha direta do conteúdo React recebe essa altura e
195
+ assume `fill` sozinha.
196
+
197
+ ## Registrar as declarações
198
+
157
199
  Registre as declarações em `presentations` no `opus.config.ts`. `opus gen` valida as referências e
158
200
  projeta o JSON no manifest; a Lens pode então exibir o artefato completo sem reconstruí-lo a partir
159
201
  do JSX.
@@ -161,7 +203,7 @@ do JSX.
161
203
  ## A invocação
162
204
 
163
205
  `definePresentationInvocation` cria o estado vivo de uma exibição. Ele contém somente valores JSON,
164
- a surface atual e a pilha necessária para voltar. A definição continua estática no manifest; input
206
+ a superfície atual e a pilha necessária para voltar. A definição continua estática no manifest; input
165
207
  de rota, sessão e resultado existe apenas na invocação.
166
208
 
167
209
  ```ts
@@ -194,7 +236,7 @@ no manifest e concentre a inspeção na Lens em vez de criar um launcher flutuan
194
236
  | `definition` | `PresentationDefinition` | | Artefato estático que descreve o recurso. |
195
237
  | `definitions` | `PresentationDefinition[]` | | Registry alcançável por navegação, abertura e retorno. |
196
238
  | `actions` | `PresentationActionRegistry` | | Contratos compartilháveis referenciados pelo artefato. |
197
- | `invocation` | `PresentationInvocation` | | Surface, input e pilha da exibição atual. |
239
+ | `invocation` | `PresentationInvocation` | | Superfície, input e pilha da exibição atual. |
198
240
  | `bindingContext` | `PresentationBindingContext` | | Rota, item, seleção, sessão e resultado disponíveis. |
199
241
  | `assistant` | `PresentationAssistantRuntime` | | Painel contextual fornecido pela superfície hospedeira. |
200
242
  | `onInvocationChange` | `(next: PresentationInvocation \| null) => void` | | Recebe navegação, retorno e fechamento. |
@@ -205,6 +247,8 @@ no manifest e concentre a inspeção na Lens em vez de criar um launcher flutuan
205
247
  | `onOpenChange` | `(open: boolean) => void` | | Notifica abertura e fechamento da superfície modal. |
206
248
  | `className` | `string` | | Classes adicionais da superfície. |
207
249
  | `bodyClassName` | `string` | | Classes adicionais do body, aplicadas uma única vez. |
250
+ | `layout` | `'flow' \| 'fill'` | `'flow'` | Em `page`, `fill` limita a altura ao shell e entrega a rolagem ao conteúdo do body; a lista de um body `list` ocupa essa altura e rola só os resultados. Dialog e Drawer ignoram. |
251
+ | `children` | `ReactNode` | | Conteúdo React do body declarado com `body.component`. Obrigatório nesse caso e recusado com `body.action`. |
208
252
 
209
253
  ## Propriedades de PresentationInspector
210
254
 
@@ -1,18 +1,26 @@
1
1
  ## Progresso determinado
2
2
 
3
- Use `Progress` quando a tarefa informar uma porcentagem de conclusão. `value` aceita valores de 0 a
4
- 100 e anima o preenchimento a cada mudança. Sem valor, a barra permanece vazia.
3
+ Use `Progress` quando a tarefa informar quanto já foi concluído. `value` aceita valores de 0 a
4
+ `max`, que por padrão é 100, e anima o preenchimento a cada mudança. Para contar etapas, declare
5
+ `max` com o total: `value={3} max={4}` preenche três quartos da barra. Quando a duração da espera
6
+ não é conhecida, use [`Spinner`](/ui/spinner).
7
+
8
+ A barra é exposta aos leitores de tela como `progressbar`, com o valor atual, mas o texto ao lado
9
+ dela não a nomeia automaticamente. Ligue o rótulo visível à barra com `aria-labelledby` ou, sem
10
+ rótulo visível, passe `aria-label`.
5
11
 
6
12
  ```tsx preview col
7
13
  <div className="w-full max-w-sm space-y-2">
8
14
  <div className="flex items-center justify-between text-sm">
9
- <span>opus check</span>
15
+ <span id="progress-check">opus check</span>
10
16
  <span className="text-muted-foreground">65%</span>
11
17
  </div>
12
- <Progress value={65} />
18
+ <Progress value={65} aria-labelledby="progress-check" />
13
19
  </div>
14
20
  ```
15
21
 
22
+ Sem `value`, ou com `null`, a barra fica vazia e é exposta como progresso indeterminado.
23
+
16
24
  ## Controlado
17
25
 
18
26
  Controle `value` externamente quando o progresso acompanhar uma tarefa em andamento. No exemplo,
@@ -20,15 +28,16 @@ cada clique avança uma etapa da sincronização.
20
28
 
21
29
  ```tsx preview col
22
30
  const [step, setStep] = useState(40)
31
+ const labelId = useId()
23
32
  const advance = (): void => setStep((prev) => Math.min(prev + 20, 100))
24
33
 
25
34
  render(
26
35
  <div className="w-full max-w-sm space-y-3">
27
36
  <div className="flex items-center justify-between text-sm">
28
- <span>Sincronizar Empresa X</span>
37
+ <span id={labelId}>Sincronizar Empresa X</span>
29
38
  <span className="text-muted-foreground">{step}%</span>
30
39
  </div>
31
- <Progress value={step} />
40
+ <Progress value={step} aria-labelledby={labelId} />
32
41
  <Button variant="outline" size="sm" onClick={advance}>
33
42
  Avançar etapa
34
43
  </Button>
@@ -39,18 +48,19 @@ render(
39
48
  ## Altura e contexto
40
49
 
41
50
  `context` diz o que o andamento comunica — `success` para meta atingida, `warning` ou `danger`
42
- para atenção — e tinge trilha e preenchimento com a família semântica. `className` compõe sobre o
43
- padrão para ajustar a altura.
51
+ para atenção — e tinge trilha e preenchimento com a família semântica. A cor reforça o significado,
52
+ mas não o substitui: mantenha o rótulo e o valor legíveis. `className` compõe sobre o padrão para
53
+ ajustar a altura.
44
54
 
45
55
  ```tsx preview col
46
56
  <div className="w-full max-w-sm space-y-4">
47
57
  <div className="space-y-1.5">
48
- <span className="text-sm">Cobertura de testes</span>
49
- <Progress value={92} context="success" className="h-1.5" />
58
+ <span id="progress-coverage" className="text-sm">Cobertura de testes</span>
59
+ <Progress value={92} context="success" className="h-1.5" aria-labelledby="progress-coverage" />
50
60
  </div>
51
61
  <div className="space-y-1.5">
52
- <span className="text-sm">Skills cobertas pelo revisor</span>
53
- <Progress value={45} context="warning" className="h-3" />
62
+ <span id="progress-review" className="text-sm">Skills cobertas pelo revisor</span>
63
+ <Progress value={45} context="warning" className="h-3" aria-labelledby="progress-review" />
54
64
  </div>
55
65
  </div>
56
66
  ```
@@ -59,6 +69,9 @@ padrão para ajustar a altura.
59
69
 
60
70
  | Propriedade | Tipo | Padrão | Descrição |
61
71
  |---|---|---|---|
62
- | `value` | `number \| null` | | O progresso de 0 a 100. O preenchimento anima a cada mudança; null/ausente deixa a barra vazia. |
72
+ | `value` | `number \| null` | | O progresso de 0 a `max`. O preenchimento anima a cada mudança; sem valor, a barra fica vazia e indeterminada. |
73
+ | `max` | `number` | `100` | O valor que representa a conclusão. O preenchimento é a fração `value / max`. |
63
74
  | `context` | `'neutral' \| 'primary' \| 'info' \| 'success' \| 'warning' \| 'danger'` | `'primary'` | O que o andamento comunica; tinge trilha e preenchimento pela família `--context-*`. |
64
- | `className` | `string` | | Compõe sobre o padrão — ajuste a altura (h-1.5/h-3). |
75
+ | `aria-label` | `string` | | Nome acessível da barra quando não há rótulo visível. |
76
+ | `aria-labelledby` | `string` | | `id` do rótulo visível que nomeia a barra. |
77
+ | `className` | `string` | | Compõe sobre o padrão — ajuste a altura (`h-1.5`, `h-3`). |
@@ -5,7 +5,7 @@ title: Filas
5
5
  # Filas
6
6
 
7
7
  Use uma action em background quando a operação não puder manter a requisição aberta. O
8
- `QueueAdapter` enfileira o trabalho e devolve um `JobHandle` para acompanhamento pelo cliente.
8
+ `QueueAdapter` enfileira o trabalho e devolve um `JobHandle` para acompanhar o andamento.
9
9
 
10
10
  ## O contrato
11
11
 
@@ -21,18 +21,21 @@ interface JobHandle<T = unknown> {
21
21
  jobId: string
22
22
  action: string
23
23
  status: 'queued' | 'running' | 'done' | 'failed' | 'cancelled'
24
- progress?: { percent?: number; message?: string }
24
+ progress?: { percent?: number; message?: string; data?: unknown }
25
25
  data?: T
26
26
  error?: ActionError
27
+ enqueuedAt: string
28
+ startedAt?: string
29
+ finishedAt?: string
27
30
  attempts: number
28
31
  trace?: TraceContext
29
32
  }
30
33
  ```
31
34
 
32
- `runtime.execute()` valida a chamada, cria o `JobSpec` e inclui o trace efetivo, request ID e
33
- provenance original. O adapter persiste o envelope; o handle também expõe esse trace. No
34
- worker, `runtime.executeJob()` revalida o envelope e executa um span filho, preservando a
35
- cadeia request → enqueue → job sem aceitar baggage ou campos extras no carrier.
35
+ `runtime.execute()` valida a chamada, cria o `JobSpec` e inclui o trace efetivo, o request ID e a
36
+ provenance original. O adapter persiste esse envelope, e o handle também expõe o trace. No worker,
37
+ `runtime.executeJob()` revalida o envelope e executa um span filho, preservando a cadeia requisição
38
+ → enfileiramento → job sem aceitar baggage ou campos extras.
36
39
 
37
40
  ## Driver
38
41
 
@@ -43,26 +46,41 @@ import { bullmqQueue } from '@softize/opus/queue/bullmq'
43
46
  const queue = bullmqQueue({ queue: new Queue('opus', { connection: { host: 'localhost' } }) })
44
47
  ```
45
48
 
46
- Redis-backed (durável, entre-processos, retries). Peers opcionais (`bullmq`, `ioredis`) só
47
- para quem usa o driver.
49
+ O driver usa Redis por meio do BullMQ, com durabilidade, entrega entre processos e novas tentativas.
50
+ `bullmq` é dependência opcional: só precisa ser instalado por quem usa o driver.
48
51
 
49
- ## No runtime
52
+ ## Declarar a action em background
53
+
54
+ Configure a fila no runtime e declare `background` no binding da action:
50
55
 
51
56
  ```ts
52
57
  const runtime = createRuntime({
53
58
  // …server/data…
54
59
  queue,
55
60
  })
61
+
62
+ export const relatorioGerar = bindAction(relatorioGerarContract, {
63
+ background: { enabled: true, retry: { attempts: 3 }, progress: true },
64
+ handler: async (ctx, input, loaded, progress) => {
65
+ await progress?.report({ percent: 50, message: 'Consultando dados' })
66
+ return gerarRelatorio(input)
67
+ },
68
+ })
56
69
  ```
57
70
 
58
- A action declara `background: { enabled: true }` (+ retry/priority no contrato); chamá-la devolve um
59
- `JobHandle` em vez do resultado, e o cliente pola `status(jobId)` (ou `subscribe`) até
60
- `done`/`failed`.
71
+ Só actions `simple` e `form` executam em background; além de `retry` e `progress`, a configuração
72
+ aceita `queue`, `priority` e `timeout`. Em uma action `list` ou `view`, o tipo do binding aceita
73
+ `background`, mas o runtime o ignora e executa a action na própria requisição. Para evitar essa
74
+ surpresa, o runtime avisa ao registrar a action fora de produção (`runtime.action_field_ignored`), e
75
+ o [`opus check`](/docs/cli) aponta a regra `ignored-by-kind`. Sem `QueueAdapter`, a chamada falha com
76
+ `runtime.queue_required`. Chamar a action devolve um `JobHandle` em vez do resultado. O Opus não
77
+ monta uma rota de acompanhamento: exponha `queue.status(jobId)` ou `queue.subscribe` por uma action
78
+ ou rota do próprio app até o job chegar a `done` ou `failed`.
61
79
 
62
80
  ## Worker BullMQ
63
81
 
64
- O app continua dono do `Worker` e da reidratação de autenticação. O processor chama
65
- `executeJob()` — nunca `execute()`, que criaria outro job:
82
+ O app continua dono do `Worker` e de reconstruir a autenticação de quem enfileirou. O processor
83
+ chama `executeJob()`, e não `execute()`, que criaria outro job:
66
84
 
67
85
  ```ts
68
86
  new Worker('opus', async (job) => {
@@ -76,13 +94,13 @@ new Worker('opus', async (job) => {
76
94
  }, { connection })
77
95
  ```
78
96
 
79
- O runtime confirma que o actor reidratado corresponde a `spec.ctx.userId`; tenant, request,
80
- trace e provenance vêm do envelope. O driver mapeia `attempts`, backoff nativo e prioridade.
81
- Multiplicador exponencial diferente de 2 ou `maxMs` exige `backoffStrategy` custom do worker e
82
- é rejeitado pelo driver em vez de ser silenciosamente ignorado. `timeout` permanece no
83
- envelope para o worker aplicar, pois não é uma opção de execução do `Queue.add`.
97
+ O runtime confirma que o usuário reconstruído corresponde a `spec.ctx.userId`; tenant, request,
98
+ trace e provenance vêm do envelope. O driver traduz `attempts`, o backoff nativo e a prioridade para
99
+ o BullMQ. Um backoff exponencial com multiplicador diferente de 2 ou com `maxMs` exige uma
100
+ `backoffStrategy` própria no worker, e o driver o recusa em vez de ignorá-lo em silêncio. `timeout`
101
+ permanece no envelope para o worker aplicar, porque não é uma opção de execução do `Queue.add`.
84
102
 
85
103
  ## Limites atuais
86
104
 
87
- Driver hoje: `bullmq` (Redis). In-memory para dev e outros backends (SQS, pg-boss…) entram
88
- por reincidência.
105
+ O único driver é o `bullmq`. Uma fila em memória para desenvolvimento e outros backends, como SQS e
106
+ pg-boss, ainda não existem.
@@ -1,8 +1,10 @@
1
1
  ## Escolha única
2
2
 
3
- Use `RadioGroup` quando todas as opções mutuamente exclusivas precisarem permanecer visíveis. Cada
4
- `RadioGroupItem` declara um `value` e deve estar associado a um `Label`. `defaultValue` define a
5
- opção inicial no modo não controlado.
3
+ Use `RadioGroup` quando todas as opções mutuamente exclusivas precisarem permanecer visíveis. Com
4
+ muitas opções, prefira [`Select`](/ui/select); para ligar ou desligar um único item, use
5
+ [`Checkbox`](/ui/checkbox) ou [`Switch`](/ui/switch). Cada `RadioGroupItem` declara um `value` e
6
+ deve estar associado a um `Label`, escrito logo depois do item. `defaultValue` define a opção
7
+ inicial no modo não controlado.
6
8
 
7
9
  ```tsx preview col-start
8
10
  <RadioGroup defaultValue="balanced">
@@ -68,6 +70,25 @@ bloquear todas as opções, aplique `disabled` ao `RadioGroup`.
68
70
  </RadioGroup>
69
71
  ```
70
72
 
73
+ ## Estado inválido
74
+
75
+ Quando a escolha for obrigatória e nenhuma opção estiver marcada, declare `aria-invalid` em cada
76
+ `RadioGroupItem` e explique o que falta em um texto associado ao grupo por `aria-describedby`.
77
+
78
+ ```tsx preview col-start
79
+ <RadioGroup aria-describedby="review-mode-error">
80
+ <div className="flex items-center gap-2">
81
+ <RadioGroupItem id="review-auto" value="auto" aria-invalid />
82
+ <Label htmlFor="review-auto">Revisar automaticamente</Label>
83
+ </div>
84
+ <div className="flex items-center gap-2">
85
+ <RadioGroupItem id="review-manual" value="manual" aria-invalid />
86
+ <Label htmlFor="review-manual">Revisar manualmente</Label>
87
+ </div>
88
+ </RadioGroup>
89
+ <FieldError id="review-mode-error">Escolha como a sessão será revisada.</FieldError>
90
+ ```
91
+
71
92
  ## Propriedades de RadioGroup
72
93
 
73
94
  | Propriedade | Tipo | Padrão | Descrição |
@@ -82,4 +103,5 @@ bloquear todas as opções, aplique `disabled` ao `RadioGroup`.
82
103
  | Propriedade | Tipo | Padrão | Descrição |
83
104
  |---|---|---|---|
84
105
  | `value` | `string` | | Valor que o item atribui ao grupo quando selecionado. |
85
- | `disabled` | `boolean` | `false` | Desabilita somente este item; o `Label` associado acompanha o estado. |
106
+ | `disabled` | `boolean` | `false` | Desabilita somente este item; o `Label` escrito logo depois dele acompanha o estado. |
107
+ | `aria-invalid` | `boolean` | | Aplica a borda e o anel de erro ao item. |
@@ -19,11 +19,13 @@ function App() {
19
19
  O router oferece leitura reativa da URL e `navigate`; o aplicativo decide o que renderizar. Ele não
20
20
  inclui tabela de rotas, parâmetros tipados nem carregamento de dados.
21
21
 
22
- A régua: se a sua tela precisa de casamento de padrão (`/users/:id/posts/:postId`), params tipados ou data loaders, o caso pede uma biblioteca de rotas, não isto. Um app de back-office com uma dúzia de destinos quase nunca precisa.
22
+ Se a tela precisa casar padrões como `/users/:id/posts/:postId`, receber parâmetros tipados ou
23
+ carregar dados por rota, use uma biblioteca de rotas. Um app de back-office com uma dúzia de
24
+ destinos costuma dispensar esses recursos.
23
25
 
24
26
  ## As quatro peças
25
27
 
26
- | | |
28
+ | API | Comportamento |
27
29
  |---|---|
28
30
  | `usePathname()` | O pathname como estado reativo. |
29
31
  | `useSegments()` | Os segmentos, já sem os vazios: `/settings/users` → `['settings','users']`. |
@@ -32,15 +34,23 @@ A régua: se a sua tela precisa de casamento de padrão (`/users/:id/posts/:post
32
34
 
33
35
  ## Comportamentos preservados
34
36
 
35
- **Destino igual é no-op.** `navigate` resolve o destino com `new URL` e compara o `href` inteiro — não faz nada se você já está exatamente lá. Sem isso, clicar duas vezes no mesmo item do menu empilha entradas idênticas e o botão "voltar" não sai do lugar.
37
+ **Navegar para o destino atual não cria entrada.** `navigate` resolve o destino com `new URL` e
38
+ compara o `href` inteiro; se a pessoa já está exatamente lá, nada acontece. Assim, clicar duas vezes
39
+ no mesmo item do menu não empilha entradas idênticas, e o botão Voltar continua saindo da página.
36
40
 
37
- Comparar strings cruas parece bastar e não basta, em duas frentes. O **hash**: estando em `/a#secao`, `navigate('/a')` pareceria destino repetido e a âncora nunca sairia da URL. E o **encoding**: `window.location` devolve `/relatórios` como `/relat%C3%B3rios`, então a comparação crua nunca casa e cada clique empilha — bem no caso pt-BR, e no ``navigate(`/reports?report=${arquivo}`)`` com nome de arquivo acentuado.
41
+ A comparação usa a URL resolvida, e não o texto cru, por dois motivos. O hash: estando em
42
+ `/a#secao`, `navigate('/a')` precisa remover a âncora, e não ser tratado como destino repetido. E a
43
+ codificação: `window.location` devolve `/relatórios` como `/relat%C3%B3rios`, então uma comparação
44
+ crua nunca casaria e cada clique empilharia uma entrada, o que afeta caminhos acentuados e
45
+ chamadas como ``navigate(`/reports?report=${arquivo}`)`` com nome de arquivo acentuado.
38
46
 
39
47
  **A notificação usa um evento do navegador.** Como `pushState` não dispara `popstate`, `navigate`
40
48
  emite o evento explicitamente. Isso mantém listeners existentes e cópias diferentes do pacote
41
49
  sincronizados pela mesma janela.
42
50
 
43
- **`useSyncExternalStore`, não `useState`.** Ler `window.location` dentro de `useState`/`useEffect` sofre *tearing* no modo concurrent: dois componentes podem renderizar o mesmo commit com URLs diferentes. As cópias que este módulo substituiu faziam isso.
51
+ **Todos os componentes leem a mesma URL.** Os hooks usam `useSyncExternalStore`. Ler
52
+ `window.location` dentro de `useState` ou `useEffect` permite que, na renderização concorrente, dois
53
+ componentes mostrem URLs diferentes no mesmo commit.
44
54
 
45
55
  ## `replace`: quando o passo não é história
46
56
 
@@ -48,14 +58,23 @@ sincronizados pela mesma janela.
48
58
  navigate(`/reports?report=${file}`, { replace: true })
49
59
  ```
50
60
 
51
- Use quando a mudança não merece uma volta do botão "voltar" — um rascunho que ganha id no meio do fluxo, um filtro que a pessoa ajusta várias vezes seguidas. O default (`pushState`) é o certo para navegação de verdade.
61
+ Use `replace` quando a mudança não deve virar um passo do botão Voltar: um rascunho que ganha id no
62
+ meio do fluxo, um filtro que a pessoa ajusta várias vezes seguidas. Para uma navegação entre
63
+ destinos, mantenha o padrão, `pushState`.
52
64
 
53
65
  ## SSR
54
66
 
55
- Os hooks têm snapshot de servidor constante (`/` e busca vazia): renderizam sem `window` e reconciliam com a URL real no primeiro render do cliente. Se a sua página precisa do path correto **no HTML servido**, isto não resolve — aí o path tem que vir do framework, por prop.
67
+ No servidor, os hooks usam um valor fixo (`/` e busca vazia): renderizam sem `window` e passam a
68
+ refletir a URL real no primeiro render do cliente. Se a página precisa do caminho correto no HTML
69
+ servido, receba o caminho do framework por prop.
56
70
 
57
- O `navigate` **não** tem essa cortesia: ele toca `window` direto e estoura no servidor. É de propósito — ler a URL sem browser tem resposta razoável, navegar não tem, e engolir a chamada em silêncio esconderia o erro de uso.
71
+ `navigate` acessa `window` diretamente e lança um erro quando chamado no servidor. Ler a URL fora
72
+ do navegador tem uma resposta razoável; navegar não tem, e ignorar a chamada em silêncio esconderia
73
+ o erro de uso.
58
74
 
59
- ## Destino externo não é problema daqui
75
+ ## Validar destinos vindos de fora
60
76
 
61
- `navigate` espera um caminho da própria origem. Validar destino que veio de terceiro — o `?redirect=` de um pós-login, por exemplo — é responsabilidade de quem recebe esse input, e a checagem certa é resolver com `new URL(valor, origin)` e comparar `origin`, não olhar prefixo.
77
+ `navigate` espera um caminho da própria origem e não valida destinos. Quando o destino vem de fora,
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
80
+ texto.