@softize/opus 19.0.0 → 20.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 (148) hide show
  1. package/CHANGELOG.md +239 -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/0020-icon-tiles-are-explicitly-composed.md +8 -0
  7. package/docs/adr/0021-content-carries-structure-and-semantic-element.md +7 -0
  8. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +19 -1
  9. package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
  10. package/docs/adr/0025-content-frame-treatment-does-not-change-semantics.md +134 -0
  11. package/docs/data-products.md +15 -9
  12. package/docs/protocol.md +3 -4
  13. package/docs/releasing.md +2 -2
  14. package/package.json +2 -2
  15. package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
  16. package/registry/templates/app/package.json +1 -1
  17. package/src/core/runtime.ts +96 -8
  18. package/src/core/types.ts +5 -2
  19. package/src/ui/components/patterns/action-form-card.tsx +11 -1
  20. package/src/ui/components/patterns/content-header.tsx +109 -18
  21. package/src/ui/components/patterns/fill-region.ts +150 -0
  22. package/src/ui/components/patterns/list.tsx +91 -92
  23. package/src/ui/components/patterns/page.tsx +20 -4
  24. package/src/ui/components/patterns/presentation.tsx +52 -37
  25. package/src/ui/components/patterns/split.tsx +44 -4
  26. package/src/ui/components/patterns/surface-anatomy.ts +35 -8
  27. package/src/ui/components/patterns/surface-header.tsx +58 -25
  28. package/src/ui/components/patterns/trigger.tsx +12 -2
  29. package/src/ui/components/primitives/alert.tsx +9 -1
  30. package/src/ui/components/primitives/avatar.tsx +7 -3
  31. package/src/ui/components/primitives/card.tsx +1 -1
  32. package/src/ui/components/primitives/carousel.tsx +34 -5
  33. package/src/ui/components/primitives/checkbox.tsx +11 -3
  34. package/src/ui/components/primitives/command.tsx +2 -1
  35. package/src/ui/components/primitives/copyable.tsx +120 -10
  36. package/src/ui/components/primitives/dialog.tsx +42 -11
  37. package/src/ui/components/primitives/drawer.tsx +10 -2
  38. package/src/ui/components/primitives/empty.tsx +1 -1
  39. package/src/ui/components/primitives/icon-picker.tsx +6 -2
  40. package/src/ui/components/primitives/icon-tile.tsx +7 -0
  41. package/src/ui/components/primitives/input-group.tsx +9 -2
  42. package/src/ui/components/primitives/input-otp.tsx +1 -1
  43. package/src/ui/components/primitives/kbd.tsx +1 -1
  44. package/src/ui/components/primitives/label.tsx +3 -0
  45. package/src/ui/components/primitives/menu.tsx +5 -0
  46. package/src/ui/components/primitives/popover.tsx +1 -1
  47. package/src/ui/components/primitives/progress.tsx +9 -1
  48. package/src/ui/components/primitives/slider.tsx +62 -11
  49. package/src/ui/components/primitives/truncate.tsx +10 -1
  50. package/src/ui/docs/content/accordion.md +74 -27
  51. package/src/ui/docs/content/action-form-card.md +24 -15
  52. package/src/ui/docs/content/action-form-dialog.md +17 -7
  53. package/src/ui/docs/content/action-form.md +92 -77
  54. package/src/ui/docs/content/action-list-dialog.md +75 -38
  55. package/src/ui/docs/content/action-list.md +141 -58
  56. package/src/ui/docs/content/action-trigger.md +27 -10
  57. package/src/ui/docs/content/action-view.md +14 -14
  58. package/src/ui/docs/content/actions.md +161 -66
  59. package/src/ui/docs/content/ai.md +43 -44
  60. package/src/ui/docs/content/alert.md +83 -29
  61. package/src/ui/docs/content/ask.md +51 -26
  62. package/src/ui/docs/content/aspect-ratio.md +22 -24
  63. package/src/ui/docs/content/audit.md +23 -19
  64. package/src/ui/docs/content/auth.md +38 -19
  65. package/src/ui/docs/content/avatar.md +54 -5
  66. package/src/ui/docs/content/badge.md +29 -9
  67. package/src/ui/docs/content/breadcrumb.md +21 -8
  68. package/src/ui/docs/content/button.md +73 -43
  69. package/src/ui/docs/content/calendar.md +134 -26
  70. package/src/ui/docs/content/card.md +41 -15
  71. package/src/ui/docs/content/carousel.md +37 -6
  72. package/src/ui/docs/content/chat.md +118 -37
  73. package/src/ui/docs/content/checkbox.md +73 -7
  74. package/src/ui/docs/content/cli.md +94 -55
  75. package/src/ui/docs/content/collapsible.md +43 -19
  76. package/src/ui/docs/content/command.md +30 -8
  77. package/src/ui/docs/content/composer.md +23 -8
  78. package/src/ui/docs/content/content.md +119 -27
  79. package/src/ui/docs/content/copyable.md +39 -8
  80. package/src/ui/docs/content/customization.md +55 -46
  81. package/src/ui/docs/content/cycle.md +33 -18
  82. package/src/ui/docs/content/data-products.md +115 -0
  83. package/src/ui/docs/content/data-state.md +20 -6
  84. package/src/ui/docs/content/data.md +20 -19
  85. package/src/ui/docs/content/detail.md +12 -5
  86. package/src/ui/docs/content/dialog.md +107 -24
  87. package/src/ui/docs/content/dictionary-value.md +4 -4
  88. package/src/ui/docs/content/dock.md +54 -19
  89. package/src/ui/docs/content/dot.md +35 -5
  90. package/src/ui/docs/content/drawer.md +63 -19
  91. package/src/ui/docs/content/empty-value.md +20 -6
  92. package/src/ui/docs/content/empty.md +24 -3
  93. package/src/ui/docs/content/events.md +76 -16
  94. package/src/ui/docs/content/executions.md +9 -9
  95. package/src/ui/docs/content/field.md +21 -6
  96. package/src/ui/docs/content/getting-started.md +44 -34
  97. package/src/ui/docs/content/icon-picker.md +19 -13
  98. package/src/ui/docs/content/icon-tile.md +38 -15
  99. package/src/ui/docs/content/input-otp.md +58 -7
  100. package/src/ui/docs/content/input.md +22 -3
  101. package/src/ui/docs/content/item.md +53 -6
  102. package/src/ui/docs/content/kbd.md +5 -1
  103. package/src/ui/docs/content/label.md +13 -4
  104. package/src/ui/docs/content/log.md +5 -5
  105. package/src/ui/docs/content/markdown.md +5 -4
  106. package/src/ui/docs/content/mcp.md +49 -23
  107. package/src/ui/docs/content/menu.md +95 -10
  108. package/src/ui/docs/content/metric-card.md +12 -4
  109. package/src/ui/docs/content/observability.md +10 -9
  110. package/src/ui/docs/content/page.md +118 -12
  111. package/src/ui/docs/content/pagination.md +13 -5
  112. package/src/ui/docs/content/pane.md +55 -5
  113. package/src/ui/docs/content/popover.md +42 -5
  114. package/src/ui/docs/content/presentation.md +91 -47
  115. package/src/ui/docs/content/progress.md +27 -14
  116. package/src/ui/docs/content/queue.md +39 -21
  117. package/src/ui/docs/content/radio-group.md +26 -4
  118. package/src/ui/docs/content/router.md +29 -10
  119. package/src/ui/docs/content/runtime.md +90 -54
  120. package/src/ui/docs/content/scheduler.md +53 -21
  121. package/src/ui/docs/content/scroll-area.md +42 -35
  122. package/src/ui/docs/content/select.md +78 -33
  123. package/src/ui/docs/content/semantic-context.md +21 -2
  124. package/src/ui/docs/content/separator.md +15 -9
  125. package/src/ui/docs/content/sidebar.md +3 -2
  126. package/src/ui/docs/content/skeleton.md +6 -1
  127. package/src/ui/docs/content/slider.md +37 -9
  128. package/src/ui/docs/content/spinner.md +4 -3
  129. package/src/ui/docs/content/split.md +19 -0
  130. package/src/ui/docs/content/storage.md +8 -9
  131. package/src/ui/docs/content/switch.md +10 -7
  132. package/src/ui/docs/content/table.md +46 -8
  133. package/src/ui/docs/content/tabs.md +14 -4
  134. package/src/ui/docs/content/testing.md +76 -31
  135. package/src/ui/docs/content/textarea.md +5 -4
  136. package/src/ui/docs/content/toast.md +48 -7
  137. package/src/ui/docs/content/toggle.md +3 -3
  138. package/src/ui/docs/content/tokens.md +15 -26
  139. package/src/ui/docs/content/tooltip.md +18 -2
  140. package/src/ui/docs/content/truncate.md +36 -6
  141. package/src/ui/docs/content/ui.md +74 -22
  142. package/src/ui/docs/content/upgrading.md +45 -36
  143. package/src/ui/docs/plugin.ts +36 -2
  144. package/src/ui/docs/registry.tsx +6 -0
  145. package/src/ui/drivers/react.tsx +16 -8
  146. package/src/ui/meta.ts +32 -32
  147. package/src/ui/react.tsx +1 -0
  148. package/src/vite/design.ts +16 -4
@@ -1,7 +1,9 @@
1
1
  ## Escolha em lista
2
2
 
3
- Use `Select` para escolhas em lista. Declare as opções como dados em `options`; `label` é usado no
4
- campo, na busca e na leitura assistiva. Associe `id` ao `htmlFor` de `Label` em formulários.
3
+ Use `Select` quando a pessoa precisar escolher um valor em uma lista que não cabe, inteira, na tela.
4
+ Para poucas opções que devem ficar visíveis ao mesmo tempo, use [`RadioGroup`](/ui/radio-group).
5
+ Declare as opções como dados em `options`; `label` é usado no campo, na busca e na leitura
6
+ assistiva. Associe `id` ao `htmlFor` de `Label` em formulários.
5
7
 
6
8
  ```tsx preview col md
7
9
  const [role, setRole] = useState('')
@@ -16,7 +18,7 @@ render(
16
18
  placeholder="Selecione o papel"
17
19
  options={[
18
20
  { value: 'designer', label: 'Designer' },
19
- { value: 'developer', label: 'Developer' },
21
+ { value: 'developer', label: 'Desenvolvedor' },
20
22
  { value: 'reviewer', label: 'Revisor' },
21
23
  ]}
22
24
  />
@@ -24,9 +26,10 @@ render(
24
26
  )
25
27
  ```
26
28
 
27
- O gatilho respeita a largura definida pelo contexto. Ao abrir, a lista usa essa largura como mínimo
28
- e pode crescer até `32rem` para preservar rótulos maiores sem alterar o layout do formulário ou da
29
- toolbar. Rótulos que ultrapassarem esse limite continuam truncados como proteção de layout.
29
+ O gatilho respeita a largura definida pelo contexto. Na variante padrão, a lista aberta usa essa
30
+ largura como mínimo e pode crescer até `32rem` para mostrar rótulos maiores sem alterar o layout do
31
+ formulário ou da toolbar. Nas variantes `outline` e `ghost`, a lista tem largura própria, entre
32
+ `14rem` e `20rem`. Rótulos que ultrapassarem o limite são truncados.
30
33
 
31
34
  ## Lista pesquisável
32
35
 
@@ -67,7 +70,7 @@ const [skills, setSkills] = useState<string[]>(['clean-code'])
67
70
 
68
71
  render(
69
72
  <div className="grid gap-2">
70
- <Label htmlFor="agent-skills">Skills do agente</Label>
73
+ <Label htmlFor="agent-skills">Habilidades do agente</Label>
71
74
  <Select
72
75
  multiple
73
76
  searchable
@@ -128,7 +131,7 @@ render(
128
131
  options={[
129
132
  { value: 'joao', label: 'João Campos', group: 'Membros' },
130
133
  { value: 'rafa', label: 'Rafa', group: 'Membros' },
131
- { value: 'developer', label: 'Developer', group: 'Agentes' },
134
+ { value: 'developer', label: 'Desenvolvedor', group: 'Agentes' },
132
135
  { value: 'reviewer', label: 'Revisor', group: 'Agentes' },
133
136
  ]}
134
137
  />,
@@ -144,7 +147,7 @@ Com `onSearch`, o consumidor consulta as opções ao abrir e ao digitar, com deb
144
147
  const ASSIGNEES = [
145
148
  { value: 'joao', label: 'João Campos', hint: 'humano' },
146
149
  { value: 'designer', label: 'Designer', hint: 'IA' },
147
- { value: 'developer', label: 'Developer', hint: 'IA' },
150
+ { value: 'developer', label: 'Desenvolvedor', hint: 'IA' },
148
151
  { value: 'reviewer', label: 'Revisor', hint: 'IA' },
149
152
  ]
150
153
 
@@ -188,7 +191,7 @@ render(
188
191
  onChange={setTask}
189
192
  placeholder="Vincular tarefa"
190
193
  options={[
191
- { value: 'gra-2', label: 'Nova tela anotações', hint: 'GRA-2' },
194
+ { value: 'gra-2', label: 'Nova tela de anotações', hint: 'GRA-2' },
192
195
  { value: 'gra-7', label: 'Filtro por filial', hint: 'GRA-7' },
193
196
  ]}
194
197
  />,
@@ -210,7 +213,7 @@ render(
210
213
  icon={<Ticket />}
211
214
  value={a}
212
215
  onChange={setA}
213
- options={[{ value: 'gra-2', label: 'Nova tela anotações', hint: 'GRA-2' }]}
216
+ options={[{ value: 'gra-2', label: 'Nova tela de anotações', hint: 'GRA-2' }]}
214
217
  />
215
218
  <Button size="sm" variant="outline">Filtrar</Button>
216
219
  </div>,
@@ -220,7 +223,8 @@ render(
220
223
  ## Limpar a seleção
221
224
 
222
225
  `clearable` acrescenta uma ação que limpa o valor sem abrir a lista. No modo simples, o valor volta
223
- a vazio; no múltiplo, volta a `[]`. Os filtros de `ActionList` já habilitam esse comportamento.
226
+ a vazio; no múltiplo, volta a `[]`. Os filtros de [`ActionList`](/ui/action-list) já habilitam
227
+ esse comportamento.
224
228
 
225
229
  ```tsx preview col md
226
230
  function Demo() {
@@ -260,12 +264,12 @@ render(
260
264
  options={[
261
265
  {
262
266
  value: 'developer',
263
- label: 'Developer',
267
+ label: 'Desenvolvedor',
264
268
  content: (
265
269
  <span className="flex items-center gap-2">
266
270
  <Bot className="size-4 text-muted-foreground" />
267
271
  <span>
268
- Developer <span className="text-muted-foreground">· implementa</span>
272
+ Desenvolvedor <span className="text-muted-foreground">· implementa</span>
269
273
  </span>
270
274
  </span>
271
275
  ),
@@ -289,21 +293,22 @@ render(
289
293
 
290
294
  ## Select na barra do Composer
291
295
 
292
- Use `variant="ghost"` para integrar o seletor à barra de ações de `Composer`. `triggerLabel` pode
293
- encurtar somente o texto do gatilho; a lista continua exibindo o `label` completo.
296
+ Use `variant="ghost"` para integrar o seletor à barra de ações de [`Composer`](/ui/composer).
297
+ `triggerLabel` pode encurtar somente o texto do gatilho; a lista continua exibindo o `label`
298
+ completo.
294
299
 
295
300
  ```tsx preview col-start
296
- const [task, setTask] = useState('GB-42')
301
+ const [order, setOrder] = useState('OS-1842')
297
302
 
298
303
  render(
299
304
  <Select
300
305
  variant="ghost"
301
- value={task}
302
- onChange={setTask}
306
+ value={order}
307
+ onChange={setOrder}
303
308
  options={[
304
- { value: '', label: 'Sem task' },
305
- { value: 'GB-42', label: 'Trocar o FAB por botão no header', hint: 'GB-42', triggerLabel: 'GB-42' },
306
- { value: 'GB-51', label: 'Anexo no composer de criação', hint: 'GB-51', triggerLabel: 'GB-51' },
309
+ { value: '', label: 'Sem ordem de serviço' },
310
+ { value: 'OS-1842', label: 'Revisão dos 20.000 km', hint: 'OS-1842', triggerLabel: 'OS-1842' },
311
+ { value: 'OS-1839', label: 'Troca das pastilhas de freio', hint: 'OS-1839', triggerLabel: 'OS-1839' },
307
312
  ]}
308
313
  />,
309
314
  )
@@ -324,7 +329,7 @@ render(
324
329
  onChange={setTask}
325
330
  className="w-72"
326
331
  options={[
327
- { value: 'gra-2', label: 'Nova tela anotações', hint: 'GRA-2' },
332
+ { value: 'gra-2', label: 'Nova tela de anotações', hint: 'GRA-2' },
328
333
  { value: 'gra-7', label: 'Filtro por filial', hint: 'GRA-7' },
329
334
  ]}
330
335
  trailing={
@@ -336,26 +341,66 @@ render(
336
341
  )
337
342
  ```
338
343
 
344
+ ## Estados
345
+
346
+ `disabled` esmaece o controle e impede a abertura da lista. `aria-invalid` aplica o contexto de
347
+ erro; associe a mensagem por `aria-describedby`.
348
+
349
+ ```tsx preview col md
350
+ const [unit, setUnit] = useState('')
351
+
352
+ render(
353
+ <div className="grid gap-4">
354
+ <Field data-invalid="true">
355
+ <FieldLabel htmlFor="unit">Unidade</FieldLabel>
356
+ <Select
357
+ id="unit"
358
+ value={unit}
359
+ onChange={setUnit}
360
+ placeholder="Selecione a unidade"
361
+ aria-invalid
362
+ aria-describedby="unit-error"
363
+ options={[
364
+ { value: 'centro', label: 'Centro' },
365
+ { value: 'norte', label: 'Zona Norte' },
366
+ ]}
367
+ />
368
+ <FieldError id="unit-error">Escolha a unidade responsável.</FieldError>
369
+ </Field>
370
+ <Field data-disabled="true">
371
+ <FieldLabel htmlFor="region">Região</FieldLabel>
372
+ <Select
373
+ id="region"
374
+ value="sudeste"
375
+ onChange={() => undefined}
376
+ disabled
377
+ options={[{ value: 'sudeste', label: 'Sudeste' }]}
378
+ />
379
+ </Field>
380
+ </div>,
381
+ )
382
+ ```
383
+
339
384
  ## Propriedades de Select
340
385
 
341
386
  | Propriedade | Tipo | Padrão | Descrição |
342
387
  |---|---|---|---|
343
388
  | `options` | `SelectOption[]` | | As opções: `{ value, label, hint?, content?, triggerLabel?, group?, disabled? }`. |
344
389
  | `value` | `string \| string[]` | | O selecionado: string no single, string[] no multiple. |
345
- | `onChange` | `(value: string) => void \| (value: string[]) => void` | | Chamado ao escolher (e ao remover chip, no multiple) — a assinatura segue o modo. |
390
+ | `onChange` | `((value: string) => void) \| ((value: string[]) => void)` | | Chamado ao escolher (e ao remover chip, no multiple) — a assinatura segue o modo. |
346
391
  | `native` | `boolean` | `false` | Renderiza o `<select>` do sistema. Exclui busca, seleção múltipla e variantes visuais (o browser é quem desenha a lista). |
347
392
  | `searchable` | `boolean` | `false` | Acrescenta uma linha de busca no topo da lista, que filtra as opções enquanto a pessoa digita. O gatilho continua exibindo a escolha. |
348
- | `multiple` | `boolean` | `false` | Chips removíveis, lista que permanece aberta e Selecionar tudo. |
349
- | `variant` | `'default' \| 'outline' \| 'ghost'` | `'default'` | `outline` = compacto com borda; `ghost` = compacto sem borda nem fundo, para barra do composer. |
350
- | `placeholder` | `string` | `'Selecione…'` | Texto do campo vazio. |
393
+ | `multiple` | `boolean` | `false` | Seleção de vários valores: chips removíveis, lista que permanece aberta e a opção “Selecionar tudo”. |
394
+ | `variant` | `'default' \| 'outline' \| 'ghost'` | `'default'` | `outline` é compacto e mantém a borda; `ghost` é compacto, sem borda nem fundo, para a barra do `Composer`. |
395
+ | `placeholder` | `string` | `'Selecione…'`; sem padrão com `native` | Texto do campo vazio. Com `native`, só existe quando declarado e vira uma opção desabilitada no início da lista. |
351
396
  | `searchPlaceholder` | `string` | `'Buscar…'` | Placeholder da linha de busca dentro da lista. |
352
397
  | `emptyText` | `string` | `'Nada encontrado.'` | Mensagem quando a busca não acha nada. |
353
- | `onSearch` | `(query: string) => void` | | Busca server-side (debounced, ao abrir e ao digitar): desliga o filtro do cmdk — o pai atualiza `options`. |
354
- | `loading` | `boolean` | | Mostra Buscando… enquanto o fetch corre (use com `onSearch`). |
355
- | `clearable` | `boolean` | `false` | X no controle quando há seleção — limpa em um clique. |
356
- | `icon` | `React.ReactNode` | | Ícone leading DENTRO do controle (decorativo) — herda `size-4` e o tom muted. |
357
- | `trailing` | `React.ReactNode` | | Ação custom no FIM do controle (antes do chevron) — o clique não abre a lista. |
358
- | `size` | `'default' \| 'sm'` | `'default'` | Altura: default (h-9, a do Input e do Button) ou sm (h-8) para toolbar densa. |
398
+ | `onSearch` | `(query: string) => void` | | Busca no servidor, chamada ao abrir a lista e depois de uma breve pausa na digitação. Desliga o filtro local; o consumidor atualiza `options` com a resposta. |
399
+ | `loading` | `boolean` | | Mostra “Buscando…” enquanto a consulta de `onSearch` está em andamento. |
400
+ | `clearable` | `boolean` | `false` | Mostra, quando há seleção, uma ação que limpa o valor sem abrir a lista. |
401
+ | `icon` | `React.ReactNode` | | Ícone decorativo no início do controle, em `1rem` e no tom atenuado. |
402
+ | `trailing` | `React.ReactNode` | | Ação própria no fim do controle, antes do chevron; interagir com ela não abre a lista. |
403
+ | `size` | `'default' \| 'sm'` | `'default'` | Altura na escala dos controles: `default` (`2.25rem`, a de `Input` e `Button`) ou `sm` (`2rem`) para toolbars densas. |
359
404
  | `shape` | `'default' \| 'pill'` | `'default'` | Geometria do controle; `pill` arredonda as extremidades e preserva a variante visual. |
360
405
  | `disabled` | `boolean` | `false` | Esmaece e trava o controle. |
361
406
  | `id` | `string` | | Vai para o campo — para parear com o `htmlFor` do Label. |
@@ -41,10 +41,29 @@ aceita o subconjunto coerente com seu papel.
41
41
  </div>
42
42
  ```
43
43
 
44
+ ## Controles sem contexto
45
+
46
+ Alguns controles também têm `variant`, mas não têm `context`. Neles, `variant` não está ligado a um
47
+ significado: descreve o formato do próprio controle.
48
+
49
+ O par `context` + `variant` vale para componentes que comunicam significado, como
50
+ [`Button`](/ui/button), [`Badge`](/ui/badge), [`Alert`](/ui/alert), [`Dot`](/ui/dot) e
51
+ [`IconTile`](/ui/icon-tile). Os controles abaixo não aceitam `context`:
52
+
53
+ | Componente | Valores | O que muda |
54
+ | --- | --- | --- |
55
+ | [`Select`](/ui/select) | `default`, `outline`, `ghost` | `default` é o campo completo; `outline` é compacto com borda, e `ghost`, compacto sem borda nem fundo. |
56
+ | [`Toggle`](/ui/toggle) | `default`, `outline` | `default` não tem borda nem fundo em repouso; o fundo aparece no hover e quando ativo. `outline` acrescenta a borda. |
57
+ | [`TabsList`](/ui/tabs) | `default`, `line` | `default` agrupa as abas em uma superfície preenchida; `line` usa uma linha-base e marca nela a aba ativa. |
58
+
59
+ O mesmo nome pode aparecer nos dois grupos. Num `Badge`, `outline` é um tratamento do significado
60
+ declarado em `context`; num `Select`, é apenas o formato compacto com borda.
61
+
44
62
  ## Dicionários
45
63
 
46
- Status e estágios declaram `context` na entrada. A tela usa `DictionaryValue` ou `ActionList` e não
47
- escolhe a aparência novamente.
64
+ Status e estágios declaram `context` na entrada. A tela usa
65
+ [`DictionaryValue`](/ui/dictionary-value) ou [`ActionList`](/ui/action-list) e não escolhe a
66
+ aparência novamente.
48
67
 
49
68
  ```ts
50
69
  const status = t.dict(
@@ -1,27 +1,32 @@
1
1
  ## Separar blocos empilhados
2
2
 
3
- Use a orientação horizontal entre blocos empilhados. A linha ocupa toda a largura disponível.
3
+ Use `Separator` para marcar a fronteira entre blocos de conteúdo que não pertencem a uma superfície
4
+ com regiões. A orientação horizontal fica entre blocos empilhados e ocupa toda a largura disponível.
4
5
 
5
6
  ```tsx preview col
6
7
  <div>
7
- <h4 className="text-sm font-medium">Empresa X</h4>
8
- <p className="text-sm text-muted-foreground">Workspace do cliente Empresa X.</p>
8
+ <h4 className="text-sm font-medium">Oficina Centro</h4>
9
+ <p className="text-sm text-muted-foreground">Rua das Flores, 120.</p>
9
10
  </div>
10
11
  <Separator />
11
- <p className="text-sm text-muted-foreground">Última sessão há 2 horas.</p>
12
+ <p className="text-sm text-muted-foreground">Próximo horário livre hoje, às 14h.</p>
12
13
  ```
13
14
 
15
+ Não use `Separator` entre cabeçalho, corpo e rodapé de [`Card`](/ui/card), [`Dialog`](/ui/dialog) ou
16
+ [`Drawer`](/ui/drawer). Nessas superfícies, os divisores são derivados das regiões presentes: use
17
+ `layout="sectioned"` no componente raiz.
18
+
14
19
  ## Vertical
15
20
 
16
21
  Use a orientação vertical entre itens lado a lado. A linha herda a altura do contêiner.
17
22
 
18
23
  ```tsx preview
19
24
  <div className="flex h-4 items-center gap-3 text-sm">
20
- <span>2 repositórios</span>
25
+ <span>4 boxes</span>
21
26
  <Separator orientation="vertical" />
22
- <span>3 agentes</span>
27
+ <span>2 consultores</span>
23
28
  <Separator orientation="vertical" />
24
- <span>6 skills</span>
29
+ <span>6 revisões hoje</span>
25
30
  </div>
26
31
  ```
27
32
 
@@ -29,5 +34,6 @@ Use a orientação vertical entre itens lado a lado. A linha herda a altura do c
29
34
 
30
35
  | Propriedade | Tipo | Padrão | Descrição |
31
36
  |---|---|---|---|
32
- | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Direção da linha. Vertical precisa de altura vinda do contêiner. |
33
- | `decorative` | `boolean` | `true` | Decorativa some da árvore de acessibilidade. Use false quando a divisão é semântica. |
37
+ | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Direção da linha. A vertical precisa de altura vinda do contêiner. |
38
+ | `decorative` | `boolean` | `true` | Uma linha decorativa fica fora da árvore de acessibilidade. Use `false` quando a divisão tiver significado e precisar ser anunciada. |
39
+ | `className` | `string` | | Ajusta a linha, como o espaçamento ao redor. |
@@ -258,8 +258,9 @@ O driver de drag-and-drop continua externo e fornece seus atributos por `dragPro
258
258
  | `label` | `string` | Rótulo opcional do grupo. Rótulos vazios não reservam espaço. |
259
259
  | `items` | `SidebarNavItem[]` | Itens diretos do grupo. |
260
260
 
261
- `SidebarNavItem` acrescenta `id` às propriedades de `SidebarItem`. Para mais níveis, componha
262
- `SidebarItem` e `SidebarTreeGroup` diretamente.
261
+ `SidebarNavItem` recebe as propriedades de `SidebarItem`, exceto `active` e `onClick`, e acrescenta
262
+ `id`. O destaque vem de `activeId` e a seleção chega por `onSelect`, ambos no `SidebarNav`. Para mais
263
+ níveis, componha `SidebarItem` e `SidebarTreeGroup` diretamente.
263
264
 
264
265
  ## Propriedades de SidebarItem
265
266
 
@@ -1,7 +1,12 @@
1
1
  ## Linha de lista com avatar
2
2
 
3
3
  Use `Skeleton` para preservar a forma do conteúdo durante o carregamento. Combine círculos para
4
- avatares e barras com dimensões próximas às linhas de texto esperadas.
4
+ avatares e barras com dimensões próximas às linhas de texto esperadas. Quando a forma do conteúdo
5
+ não é conhecida, use [`Spinner`](/ui/spinner); para os estados completos de uma consulta, incluindo
6
+ erro e vazio, use [`DataState`](/ui/data-state).
7
+
8
+ O `Skeleton` é só visual e não anuncia a espera. Quando a região inteira estiver carregando, marque
9
+ o contêiner com `role="status"` e um nome, como `aria-label="Carregando sessões"`.
5
10
 
6
11
  ```tsx preview col
7
12
  <div className="flex items-center gap-3">
@@ -3,52 +3,77 @@
3
3
  Use `Slider` para escolher um valor dentro de uma faixa. `defaultValue` recebe um array; `[n]`
4
4
  representa um único controle. `min`, `max` e `step` delimitam os valores disponíveis.
5
5
 
6
+ Quem recebe o foco e anuncia o valor é o controle arrastável, chamado de thumb. Sem rótulo
7
+ visível, passe `aria-label` ao `Slider`: ele repassa o nome ao thumb.
8
+
6
9
  ```tsx preview col
7
- <Slider defaultValue={[40]} min={0} max={100} step={5} />
10
+ <Slider aria-label="Opacidade" defaultValue={[40]} min={0} max={100} step={5} />
8
11
  ```
9
12
 
10
13
  ## Controlado
11
14
 
12
15
  `onValueChange` recebe o array atualizado. Para um único controle, leia `value[0]`.
13
16
 
17
+ Com um rótulo visível, ligue-o ao `Slider` por `aria-labelledby`. O `htmlFor` do `Label` não
18
+ alcança o thumb, porque ele não é um campo nativo.
19
+
14
20
  ```tsx preview col
15
21
  const [parallelism, setParallelism] = useState([4])
16
22
 
17
23
  render(
18
- <div role="group" aria-labelledby="parallelism-label" className="grid w-full gap-3">
24
+ <div className="grid w-full gap-3">
19
25
  <div className="flex items-center justify-between">
20
26
  <Label id="parallelism-label">Sessões em paralelo</Label>
21
27
  <span className="text-sm text-muted-foreground">{parallelism[0]}</span>
22
28
  </div>
23
- <Slider value={parallelism} onValueChange={setParallelism} min={1} max={8} step={1} />
29
+ <Slider
30
+ aria-labelledby="parallelism-label"
31
+ value={parallelism}
32
+ onValueChange={setParallelism}
33
+ min={1}
34
+ max={8}
35
+ step={1}
36
+ />
24
37
  </div>,
25
38
  )
26
39
  ```
27
40
 
28
41
  ## Intervalo
29
42
 
30
- Dois números em `value` criam um intervalo selecionável entre dois controles.
43
+ Dois números em `value` criam um intervalo selecionável entre dois thumbs, e cada um precisa de um
44
+ nome próprio. Passe os nomes em `thumbLabels`, na ordem dos valores. Sem `thumbLabels`, o nome comum
45
+ se compõe com a posição: com `aria-label`, “Custo, mínimo” e “Custo, máximo”; com
46
+ `aria-labelledby`, o texto do rótulo seguido de “Mínimo” ou “Máximo”. Sem nome algum, o leitor de
47
+ tela anuncia só “Mínimo” e “Máximo”, sem dizer do quê.
31
48
 
32
49
  ```tsx preview col
33
50
  const [budget, setBudget] = useState([20, 60])
34
51
 
35
52
  render(
36
- <div role="group" aria-labelledby="budget-label" className="grid w-full gap-3">
53
+ <div className="grid w-full gap-3">
37
54
  <div className="flex items-center justify-between">
38
- <Label id="budget-label">Custo estimado (US$)</Label>
55
+ <Label>Custo estimado (US$)</Label>
39
56
  <span className="text-sm text-muted-foreground">{budget[0]} – {budget[1]}</span>
40
57
  </div>
41
- <Slider value={budget} onValueChange={setBudget} min={0} max={100} step={5} />
58
+ <Slider
59
+ thumbLabels={['Custo mínimo', 'Custo máximo']}
60
+ value={budget}
61
+ onValueChange={setBudget}
62
+ min={0}
63
+ max={100}
64
+ step={5}
65
+ />
42
66
  </div>,
43
67
  )
44
68
  ```
45
69
 
46
70
  ## Desabilitado
47
71
 
48
- disabled esmaece o trilho e o thumb e bloqueia o arraste — o valor fica congelado no defaultValue.
72
+ `disabled` esmaece o trilho e o thumb e bloqueia o arraste. O valor fica congelado no
73
+ `defaultValue`.
49
74
 
50
75
  ```tsx preview col
51
- <Slider defaultValue={[70]} min={0} max={100} disabled />
76
+ <Slider aria-label="Opacidade" defaultValue={[70]} min={0} max={100} disabled />
52
77
  ```
53
78
 
54
79
  ## Propriedades de Slider
@@ -63,3 +88,6 @@ disabled esmaece o trilho e o thumb e bloqueia o arraste — o valor fica congel
63
88
  | `step` | `number` | `1` | Incremento entre valores possíveis. |
64
89
  | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Direção do trilho — vertical exige altura no container. |
65
90
  | `disabled` | `boolean` | `false` | Esmaece e bloqueia o arraste. |
91
+ | `aria-label` | `string` | | Nome do thumb quando não há rótulo visível. Em intervalo, prefira `thumbLabels`. |
92
+ | `aria-labelledby` | `string` | | `id` do rótulo visível que nomeia o thumb. |
93
+ | `thumbLabels` | `readonly string[]` | | Um nome por thumb, na ordem de `value`. Prevalece sobre `aria-label` e `aria-labelledby`. |
@@ -3,7 +3,7 @@
3
3
  Use `Spinner` quando a duração ou o progresso da espera não forem conhecidos. `size` usa os nomes da
4
4
  escala dos controles e mede o glifo do controle homônimo: `sm` é o ícone de um `Button` `sm`, `lg` o
5
5
  de um `lg`. O ícone é decorativo; quem nomeia a espera é o contêiner (`role="status"`, como fazem
6
- `DataState` e `PageState`) ou o texto ao lado.
6
+ [`DataState`](/ui/data-state) e `PageState`) ou o texto ao lado.
7
7
 
8
8
  ```tsx preview
9
9
  <Spinner size="xs" />
@@ -14,7 +14,7 @@ de um `lg`. O ícone é decorativo; quem nomeia a espera é o contêiner (`role=
14
14
 
15
15
  ## No botão
16
16
 
17
- Durante uma ação, use `busy` no `Button` com um rótulo que descreva o andamento. O botão fica
17
+ Durante uma ação, use `busy` no [`Button`](/ui/button) com um rótulo que descreva o andamento. O botão fica
18
18
  desabilitado e o spinner ocupa o lugar do ícone, no glifo do tamanho do botão.
19
19
 
20
20
  ```tsx preview
@@ -24,7 +24,8 @@ desabilitado e o spinner ocupa o lugar do ícone, no glifo do tamanho do botão.
24
24
  ## Em carga de conteúdo
25
25
 
26
26
  Espera curta sem forma definida; o contêiner nomeia o estado. Para uma consulta inteira, `DataState`
27
- já monta esse contêiner. Quando o conteúdo tem forma conhecida (lista, card), prefira Skeleton.
27
+ já monta esse contêiner. Quando o conteúdo tem forma conhecida, como uma lista ou um card, prefira
28
+ [`Skeleton`](/ui/skeleton).
28
29
 
29
30
  ```tsx preview
30
31
  <div role="status" className="flex items-center gap-2 text-sm text-muted-foreground">
@@ -21,6 +21,19 @@ render(
21
21
  );
22
22
  ```
23
23
 
24
+ Escreva cada `Pane` diretamente dentro do `Split`; um fragment pode agrupá-los, como em uma
25
+ renderização condicional que devolve dois panes. O `Split` lê o tamanho de cada `Pane` antes de
26
+ renderizá-lo, então não enxerga um `Pane` devolvido por outro componente: nesse caso, escreva o
27
+ `Pane` no `Split` e passe o componente como filho dele. Qualquer outro filho interrompe a
28
+ renderização com um erro que aponta o nó recebido, em vez de sumir da tela sem aviso.
29
+
30
+ O [`opus check`](/docs/cli) antecipa esse erro com a regra `ui-structure-split`: ele aponta o filho
31
+ direto de um `Split` que o código mostra não ser `Pane`, como um elemento HTML, um texto, outro
32
+ componente do Opus ou um componente declarado no mesmo arquivo. Panes em fragment ou em condicional,
33
+ espaços e comentários são aceitos. Expressões dinâmicas, como `{children}` ou `{items.map(...)}`, e
34
+ componentes importados de outros módulos ficam fora do check; nesses casos, só o erro em runtime
35
+ revela um filho inválido.
36
+
24
37
  O `Split` responde pela relação espacial e pelo arraste. O que existe dentro de cada área — o
25
38
  cabeçalho, o corpo rolável, o rodapé, o recuo e a escolha entre regiões seccionadas e contínuas —
26
39
  pertence ao [`Pane`](/ui/pane) e está documentado na página da família.
@@ -139,6 +152,9 @@ render(<Example />);
139
152
  | `id` | `string` | | Identidade estável do grupo redimensionável. |
140
153
  | `defaultLayout` | `Record<string, number>` | | Layout percentual restaurado, indexado pelos ids dos panes. |
141
154
  | `onLayoutChanged` | `(layout, { isUserInteraction }) => void` | | Chamado ao concluir uma mudança de layout; persista onde fizer sentido. |
155
+ | `className` | `string` | | Ajusta o contêiner do grupo. |
156
+ | `data-slot` | `string` | `'split'` | Identificador semântico para testes e composição de patterns. |
157
+ | `children` | `ReactNode` | | Os `Pane`, diretos ou agrupados em fragment; outro filho gera erro. |
142
158
 
143
159
  ## Propriedades de SurfaceAssistant
144
160
 
@@ -151,3 +167,6 @@ render(<Example />);
151
167
  | `initialSize` | `PaneSize` | `28rem` | Largura inicial do painel. |
152
168
  | `minSize` | `PaneSize` | `20rem` | Largura mínima durante o redimensionamento. |
153
169
  | `maxSize` | `PaneSize` | `40rem` | Largura máxima durante o redimensionamento. |
170
+ | `className` | `string` | | Ajusta o contêiner do split. |
171
+ | `contentClassName` | `string` | | Ajusta a coluna do conteúdo principal. |
172
+ | `assistantClassName` | `string` | | Ajusta a coluna do painel de assistência. |
@@ -4,8 +4,7 @@ title: Armazenamento
4
4
 
5
5
  # Armazenamento
6
6
 
7
- > **Experimental.** Superfície mínima, sem consumidor interno ainda — cresce por
8
- > reincidência de caso real, não por especulação.
7
+ > **Experimental.** A superfície é mínima e cresce conforme casos reais a exigem.
9
8
 
10
9
  Use `StorageAdapter` para upload, download, remoção e geração de URL de arquivos. A chave é um
11
10
  caminho lógico, como `clientes/123/contrato.pdf`; cada driver decide onde o conteúdo é armazenado.
@@ -31,10 +30,11 @@ import { fsStorage } from '@softize/opus/storage/fs'
31
30
  import { s3Storage } from '@softize/opus/storage/s3'
32
31
  import { S3Client } from '@aws-sdk/client-s3'
33
32
 
34
- // Dev / single-host: arquivos sob um root local; url() exige baseUrl (o app serve o root).
33
+ // Desenvolvimento ou um único host: arquivos num diretório local; url() exige baseUrl,
34
+ // e o app serve esse diretório.
35
35
  const dev = fsStorage({ root: './var/arquivos', baseUrl: 'http://localhost:5173/arquivos' })
36
36
 
37
- // Produção: S3 e compatíveis (MinIO, R2…). O client é seu — região/endpoint/credencial.
37
+ // Produção: S3 e compatíveis (MinIO, R2…). O app cria o client com região, endpoint e credenciais.
38
38
  const prod = s3Storage({
39
39
  client: new S3Client({ region: 'us-east-1' }),
40
40
  bucket: 'meu-app',
@@ -42,8 +42,8 @@ const prod = s3Storage({
42
42
  })
43
43
  ```
44
44
 
45
- Os peers do S3 são **opcionais** (`@aws-sdk/client-s3`; `s3-request-presigner` para o
46
- `url()`): quem não usa o driver não os instala — importados sob demanda.
45
+ `@aws-sdk/client-s3` e, para `url()`, `@aws-sdk/s3-request-presigner` são dependências opcionais,
46
+ carregadas só quando o driver é usado.
47
47
 
48
48
  ## No runtime
49
49
 
@@ -62,6 +62,5 @@ handler: async (ctx, input) => {
62
62
 
63
63
  ## Limites atuais
64
64
 
65
- Bytes em memória (`Uint8Array`), sem streams — arquivo gigante ainda não é o caso;
66
- sem `list`, sem metadata rica. Cada um entra quando um projeto real cobrar — é a
67
- régua do Opus: comportamento vira peça na reincidência.
65
+ O conteúdo trafega inteiro em memória, como `Uint8Array`, sem streams; arquivos muito grandes ainda
66
+ não são suportados. Também não há `list` nem metadados além de `contentType`.
@@ -1,7 +1,8 @@
1
1
  ## Alternar uma preferência
2
2
 
3
- Use `Switch` para uma preferência booleana que entra em vigor imediatamente. Associe o controle a
4
- `Label`; `defaultChecked` define o estado inicial no modo não controlado.
3
+ Use `Switch` para uma preferência booleana que entra em vigor imediatamente. Quando a escolha só vale
4
+ ao salvar um formulário, use [`Checkbox`](/ui/checkbox). Associe o controle a um [`Label`](/ui/label);
5
+ `defaultChecked` define o estado inicial no modo não controlado.
5
6
 
6
7
  ```tsx preview
7
8
  <div className="flex items-center gap-2">
@@ -31,7 +32,8 @@ render(
31
32
 
32
33
  ## Tamanho sm
33
34
 
34
- Use `size="sm"` em linhas de lista e outras composições compactas.
35
+ Use `size="sm"` em linhas de lista e outras composições compactas. O tamanho segue o degrau do ícone
36
+ na escala dos controles, e não a altura da linha: a chave `sm` acompanha o ícone de um botão `sm`.
35
37
 
36
38
  ```tsx preview col-start
37
39
  <div className="flex items-center gap-2">
@@ -46,7 +48,8 @@ Use `size="sm"` em linhas de lista e outras composições compactas.
46
48
 
47
49
  ## Desabilitado
48
50
 
49
- disabled esmaece e bloqueia a chave — ligada ou desligada — e o rótulo em par esmaece junto (peer-disabled).
51
+ `disabled` esmaece e bloqueia a chave, ligada ou desligada. O `Label` escrito logo depois da chave,
52
+ no mesmo contêiner, esmaece junto.
50
53
 
51
54
  ```tsx preview col-start
52
55
  <div className="flex items-center gap-2">
@@ -63,8 +66,8 @@ disabled esmaece e bloqueia a chave — ligada ou desligada — e o rótulo em p
63
66
 
64
67
  | Propriedade | Tipo | Padrão | Descrição |
65
68
  |---|---|---|---|
66
- | `checked` | `boolean` | | O estado, no modo controlado — parear com onCheckedChange. |
69
+ | `checked` | `boolean` | | Estado no modo controlado. Use com `onCheckedChange`. |
67
70
  | `onCheckedChange` | `(checked: boolean) => void` | | Chamado a cada alternância, com o novo estado. |
68
71
  | `defaultChecked` | `boolean` | `false` | Estado inicial no modo não controlado. |
69
- | `size` | `'sm' \| 'default'` | `'default'` | Tamanho da chave, com os nomes da escala única — sm para densidade em linha de lista. |
70
- | `disabled` | `boolean` | `false` | Esmaece e bloqueia — o Label em par esmaece junto (peer-disabled). |
72
+ | `size` | `'sm' \| 'default'` | `'default'` | Tamanho da chave, com os nomes da escala única; `sm` para linhas de lista densas. |
73
+ | `disabled` | `boolean` | `false` | Esmaece e bloqueia a chave; o `Label` irmão posterior esmaece junto. |