@softize/opus 29.0.0 → 31.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 (59) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/bin/lib/check.mjs +178 -94
  3. package/bin/lib/copy.mjs +2970 -1801
  4. package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +52 -0
  5. package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +27 -2
  6. package/docs/adr/0045-surface-replaces-content-and-card.md +3 -0
  7. package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +142 -0
  8. package/docs/adr/0062-listas-contidas-e-texto-flexivel.md +68 -0
  9. package/docs/adr/0063-metadados-das-superficies.md +44 -0
  10. package/docs/adr/0064-action-list-com-itens-e-fontes-externas.md +47 -0
  11. package/docs/adr/0065-presentation-composes-settings-sections.md +295 -0
  12. package/docs/adr/README.md +8 -0
  13. package/package.json +1 -1
  14. package/registry/skills/build-opus-ui/SKILL.md +21 -19
  15. package/registry/skills/build-opus-ui/references/evaluations.md +21 -10
  16. package/registry/skills/build-opus-ui/references/ui-patterns.md +33 -28
  17. package/src/core/presentation.ts +938 -32
  18. package/src/core/types.ts +2 -1
  19. package/src/ui/components/patterns/form.tsx +2 -0
  20. package/src/ui/components/patterns/grouped-list.ts +2 -2
  21. package/src/ui/components/patterns/list.tsx +588 -365
  22. package/src/ui/components/patterns/page-heading-context.tsx +12 -5
  23. package/src/ui/components/patterns/page.tsx +238 -181
  24. package/src/ui/components/patterns/presentation-fields.tsx +246 -0
  25. package/src/ui/components/patterns/presentation-sections.tsx +494 -0
  26. package/src/ui/components/patterns/presentation.tsx +1142 -229
  27. package/src/ui/components/patterns/structural-header.tsx +47 -34
  28. package/src/ui/components/patterns/surface-anatomy.ts +27 -217
  29. package/src/ui/components/patterns/surface.tsx +55 -37
  30. package/src/ui/components/patterns/trigger.tsx +10 -2
  31. package/src/ui/components/primitives/close-button.tsx +11 -3
  32. package/src/ui/components/primitives/command.tsx +2 -8
  33. package/src/ui/components/primitives/detail.tsx +1 -1
  34. package/src/ui/components/primitives/dialog.tsx +59 -89
  35. package/src/ui/components/primitives/drawer.tsx +52 -87
  36. package/src/ui/components/primitives/field.tsx +1 -1
  37. package/src/ui/components/primitives/pagination.tsx +3 -2
  38. package/src/ui/components/primitives/table.tsx +3 -3
  39. package/src/ui/components/primitives/tabs.tsx +13 -2
  40. package/src/ui/docs/content/action-form.md +2 -1
  41. package/src/ui/docs/content/action-list.md +146 -73
  42. package/src/ui/docs/content/action-trigger.md +5 -0
  43. package/src/ui/docs/content/button.md +5 -6
  44. package/src/ui/docs/content/command.md +62 -50
  45. package/src/ui/docs/content/compositions.md +129 -459
  46. package/src/ui/docs/content/dialog.md +72 -101
  47. package/src/ui/docs/content/drawer.md +69 -92
  48. package/src/ui/docs/content/field.md +2 -1
  49. package/src/ui/docs/content/page.md +207 -386
  50. package/src/ui/docs/content/presentation.md +371 -23
  51. package/src/ui/docs/content/separator.md +1 -1
  52. package/src/ui/docs/content/surface.md +94 -116
  53. package/src/ui/docs/content/table.md +7 -0
  54. package/src/ui/docs/content/tokens.md +18 -4
  55. package/src/ui/drivers/context.tsx +11 -4
  56. package/src/ui/drivers/react.tsx +27 -7
  57. package/src/ui/meta.ts +5 -5
  58. package/src/ui/react.tsx +38 -8
  59. package/src/ui/theme.css +44 -33
@@ -16,7 +16,11 @@ render(
16
16
  title="Dispositivos conectados"
17
17
  count={3}
18
18
  description="Sessões com acesso à sua conta."
19
- actions={<Button variant="outline" size="sm">Encerrar outras sessões</Button>}
19
+ actions={
20
+ <Button variant="outline" size="sm">
21
+ Encerrar outras sessões
22
+ </Button>
23
+ }
20
24
  >
21
25
  <p className="text-sm text-muted-foreground">MacBook Pro · ativo agora</p>
22
26
  </Surface>,
@@ -43,17 +47,16 @@ Quando `aria-label` é informado, ele substitui a ligação com o título. Para
43
47
 
44
48
  ## Moldura, fundo e anatomia
45
49
 
46
- Três propriedades independentes definem a aparência:
50
+ Moldura e fundo têm funções independentes:
47
51
 
48
52
  - `framed` acrescenta borda e raio.
49
53
  - `elevation="raised"` dá à superfície o fundo de um elemento elevado sobre a página.
50
- - `layout` escolhe entre o ritmo contínuo e as faixas com divisores.
51
54
 
52
55
  Uma superfície com moldura ou com fundo passa a aplicar o recuo de `1rem` nas suas regiões; você não
53
56
  escreve classes de padding no header, no corpo ou no rodapé. O header delimitado usa
54
- `1rem` em todos os lados e linha mínima de `2rem`, como Page, Dialog e Drawer.
55
- Declare `sm` para botões textuais e `icon-sm` para ícones; Pane usa a mesma geometria
56
- quando hospeda Surface. Seções sem moldura e legendas recortadas não ganham padding de barra.
57
+ `1rem` em todos os lados e linha mínima de `2,25rem`, como Page, Dialog e Drawer.
58
+ Declare `default` para botões textuais e `icon` para ícones; Pane usa a mesma geometria
59
+ quando hospeda Surface. Seções sem moldura permanecem alinhadas ao hospedeiro.
57
60
 
58
61
  ```tsx preview col
59
62
  render(
@@ -130,12 +133,7 @@ ainda ter rodapé:
130
133
 
131
134
  ```tsx preview col
132
135
  render(
133
- <Surface
134
- as="section"
135
- framed
136
- title="Plano atual"
137
- level={3}
138
- >
136
+ <Surface as="section" framed title="Plano atual" level={3}>
139
137
  <SurfaceBody>
140
138
  <p className="text-sm text-muted-foreground">
141
139
  Renova em 12 de maio. Até 10 workspaces e 50 agentes.
@@ -163,56 +161,13 @@ A `Surface` reprova as composições que deixariam a estrutura ambígua:
163
161
 
164
162
  O `opus check` aponta os mesmos problemas antes da execução.
165
163
 
166
- ## Contínuo ou seccionado
164
+ ## Divisores automáticos
167
165
 
168
- `layout` declara a anatomia da superfície inteira. No padrão, `continuous`, o espaço separa as
169
- regiões. Em `sectioned`, header e rodapé viram faixas e uma linha separa as regiões vizinhas.
170
-
171
- ```tsx preview col
172
- render(
173
- <div className="grid grid-cols-2 gap-4">
174
- <Surface as="section" framed elevation="raised" title="Contínuo" level={3}>
175
- <SurfaceBody>
176
- <p className="text-sm text-muted-foreground">
177
- Um assunto, lido de cima a baixo.
178
- </p>
179
- </SurfaceBody>
180
- <SurfaceFooter>
181
- <Button size="sm" variant="outline">
182
- Abrir
183
- </Button>
184
- </SurfaceFooter>
185
- </Surface>
186
- <Surface
187
- as="section"
188
- framed
189
- elevation="raised"
190
- layout="sectioned"
191
- title="Seccionado"
192
- level={3}
193
- >
194
- <SurfaceBody>
195
- <p className="text-sm text-muted-foreground">
196
- Regiões com pesos diferentes.
197
- </p>
198
- </SurfaceBody>
199
- <SurfaceFooter>
200
- <Button size="sm" variant="outline">
201
- Abrir
202
- </Button>
203
- </SurfaceFooter>
204
- </Surface>
205
- </div>,
206
- );
207
- ```
208
-
209
- Use `continuous` para um bloco de leitura e `sectioned` para uma superfície de operação, com lista
210
- ou tabela no corpo e ações persistentes no rodapé. `sectioned` exige `framed` ou
211
- `elevation="raised"`: os divisores precisam de um limite para alcançar.
212
-
213
- Os divisores são derivados das regiões presentes. Um header sozinho não desenha linha, e um header
214
- seguido direto de um rodapé produz uma linha só. Por isso, não escreva `border-b` nem `border-t`
215
- nas regiões, e use `layout="continuous"` para uma superfície sem divisores.
166
+ Em superfícies delimitadas, as regiões presentes determinam os divisores. Um header isolado não desenha linha; seguido
167
+ pelo rodapé, produz uma linha só. O recuo de uma superfície delimitada é `1rem` em cada região.
168
+ Sem moldura ou elevação, as regiões permanecem alinhadas ao hospedeiro e separadas por `1rem`,
169
+ sem padding nem divisor de região. Abas no Header conservam a linha de seleção própria.
170
+ Não há prop para alternar essa anatomia.
216
171
 
217
172
  ### Corpo de ponta a ponta
218
173
 
@@ -225,7 +180,6 @@ render(
225
180
  <Surface
226
181
  as="section"
227
182
  framed
228
- layout="sectioned"
229
183
  title="Veículo"
230
184
  media={
231
185
  <IconTile size="xs">
@@ -328,9 +282,8 @@ render(
328
282
  );
329
283
  ```
330
284
 
331
- Com `framed`, a legenda ocupa a borda do grupo, como em um `fieldset` nativo. Com
332
- `layout="sectioned"`, ela passa a ser a faixa do header, dentro da moldura. Sem moldura, o grupo
333
- flui alinhado ao formulário.
285
+ Em uma superfície delimitada, a legenda nativa ocupa a faixa do header. Sem moldura ou
286
+ elevação, o grupo flui alinhado ao formulário.
334
287
 
335
288
  ## Superfície clicável
336
289
 
@@ -387,33 +340,16 @@ declaram, sem recuo nem espaçamento. O conteúdo e o recuo são seus.
387
340
  documento: uma superfície dentro de uma seção que já tem `h2` usa `level={3}`. Os níveis de 2 a 6
388
341
  têm a mesma aparência.
389
342
 
390
- O nível 1 é diferente: identifica o conteúdo principal da página e usa a escala do título de
391
- [`Page`](/ui/page). É o caso da página centrada em uma coleção.
343
+ O nível 1 usa a escala do título de [`Page`](/ui/page), mas fica restrito a superfícies
344
+ fora de Page, como o acesso à aplicação. Dentro da Page, o título principal pertence ao
345
+ PageHeader e as Surfaces são seções subordinadas.
392
346
 
393
347
  ## Coleção nomeada
394
348
 
395
- Quando a região principal da página é uma coleção, a `Surface` nomeia essa coleção e a
396
- [`ActionList`](/ui/action-list) executa a consulta. Coloque criar, importar e outras ações sobre o
397
- conjunto completo nas ações da superfície. Busca, filtros, atualização e operações que dependem do
398
- recorte atual permanecem na toolbar da lista.
399
-
400
- ```tsx preview col
401
- render(
402
- <Surface
403
- as="section"
404
- level={1}
405
- title="Clientes"
406
- actions={<Button size="sm">Criar cliente</Button>}
407
- >
408
- <p className="text-sm text-muted-foreground">
409
- A lista de clientes ocupa o corpo.
410
- </p>
411
- </Surface>,
412
- );
413
- ```
414
-
415
- A criação usa um `Button` textual na variante padrão e no tamanho `sm`, sem ícone, nomeado
416
- `Criar {recurso}`; o diálogo aberto por ele repete esse título, e a edição usa `Editar {recurso}`.
349
+ Uma coleção principal usa [`Page`](/ui/page) com [`ActionList`](/ui/action-list).
350
+ Use Surface para nomear uma coleção subordinada. Criação, importação e exportação ficam em
351
+ `ActionList.toolbarActions`, junto de busca e filtros. Ações gerais da superfície permanecem
352
+ no header. O botão de criação mostra **Criar**, com nome acessível e título de formulário específicos.
417
353
 
418
354
  Em uma [`Page`](/ui/page) com `layout="fill"`, a coleção ocupa a altura da página e rola só os
419
355
  resultados. `Surface` e `SurfaceBody` repassam a altura até a `ActionList` sem classes adicionais;
@@ -432,32 +368,60 @@ faixa que cobre o conteúdo ao rolar — usa `bg-[var(--surface-background,var(-
432
368
  página, vale `--background`; dentro de uma superfície elevada, o fundo dela. Um contêiner da
433
369
  aplicação que pinte outro fundo declara a mesma variável com a cor que usa.
434
370
 
371
+ ## Metadados junto à identidade
372
+
373
+ Use `SurfaceMeta` para badges, autoria e datas relacionados ao registro. A faixa fica
374
+ abaixo da linha de título e ações, com espaço próprio; `SurfaceDescription` continua
375
+ reservado à orientação textual breve. Os itens quebram em várias linhas quando necessário.
376
+
377
+ ```tsx preview col
378
+ render(
379
+ <Surface
380
+ as="article"
381
+ title="Manual de atendimento"
382
+ meta={
383
+ <>
384
+ <Badge>Publicado</Badge>
385
+ <span>Por Ana</span>
386
+ </>
387
+ }
388
+ >
389
+ <p>Conteúdo do manual.</p>
390
+ </Surface>,
391
+ );
392
+ ```
393
+
394
+ A composição aceita um único `SurfaceMeta` e exige um `SurfaceTitle`. Na forma explícita, coloque-o em `SurfaceHeader`. Em `fieldset`, coloque os metadados no corpo; a legenda permanece restrita a título e mídia decorativa.
395
+
396
+ O total da coleção continua ao lado do título: use `count` na forma curta ou `SurfaceCount` na explícita. Se um `SurfaceMeta` antigo representava esse total, migre para `SurfaceCount`.
397
+
435
398
  ## Propriedades de Surface
436
399
 
437
- | Propriedade | Tipo | Padrão | Descrição |
438
- | ------------- | ----------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------ |
439
- | `as` | `'div' \| 'section' \| 'article' \| 'fieldset'` | `'div'` | Elemento da superfície. Não combina com `asChild`. |
440
- | `asChild` | `boolean` | `false` | Entrega a superfície ao único filho, como um `button` ou um link. |
441
- | `framed` | `boolean` | `false` | Acrescenta borda e raio, e recorta o conteúdo seccionado ou sem recuo. |
442
- | `elevation` | `'none' \| 'raised'` | `'none'` | `raised` pinta o fundo de superfície elevada e mantém a altura do próprio conteúdo. |
443
- | `layout` | `'continuous' \| 'sectioned'` | `'continuous'` | Anatomia das regiões. `sectioned` exige `framed` ou `elevation="raised"`. |
444
- | `level` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | `2` | Nível do heading do título. `1` identifica o conteúdo principal da página. Não se aplica a `fieldset`. |
445
- | `title` | `ReactNode` | | Forma curta: título da superfície. |
446
- | `media` | `ReactNode` | | Forma curta: conteúdo do slot de mídia, antes do título. Exige `title`. |
447
- | `count` | `number` | | Forma curta: total de itens ao lado do título. Exige `title`; indisponível em `fieldset`. |
448
- | `description` | `ReactNode` | | Forma curta: frase de apoio sob o título. Exige `title`; indisponível em `fieldset`. |
449
- | `actions` | `ReactNode` | | Forma curta: ações sobre a superfície inteira. Exige `title`; indisponível em `fieldset`. |
450
- | `className` | `string` | | Compõe sobre a superfície. Na caixa simples, é onde entra o recuo. |
451
- | `children` | `ReactNode` | | Filhos livres, que formam o corpo, ou os slots `SurfaceHeader`, `SurfaceBody` e `SurfaceFooter`. |
400
+ | Propriedade | Tipo | Padrão | Descrição |
401
+ | ------------- | ----------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------ |
402
+ | `as` | `'div' \| 'section' \| 'article' \| 'fieldset'` | `'div'` | Elemento da superfície. Não combina com `asChild`. |
403
+ | `asChild` | `boolean` | `false` | Entrega a superfície ao único filho, como um `button` ou um link. |
404
+ | `framed` | `boolean` | `false` | Acrescenta borda e raio, e recorta o conteúdo seccionado ou sem recuo. |
405
+ | `elevation` | `'none' \| 'raised'` | `'none'` | `raised` pinta o fundo de superfície elevada e mantém a altura do próprio conteúdo. |
406
+ | `level` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | `2` | Nível do heading do título. `1` identifica o conteúdo principal da página. Não se aplica a `fieldset`. |
407
+ | `title` | `ReactNode` | | Forma curta: título da superfície. |
408
+ | `media` | `ReactNode` | | Forma curta: conteúdo do slot de mídia, antes do título. Exige `title`. |
409
+ | `count` | `number` | | Forma curta: total de itens ao lado do título. Exige `title`; indisponível em `fieldset`. |
410
+ | `description` | `ReactNode` | | Forma curta: frase de apoio sob o título. Exige `title`; indisponível em `fieldset`. |
411
+ | `meta` | `ReactNode` | | Forma curta: metadados sob o título e a orientação. Exige `title`; indisponível em `fieldset`. |
412
+ | `actions` | `ReactNode` | | Forma curta: ações sobre a superfície inteira. Exige `title`; indisponível em `fieldset`. |
413
+ | `className` | `string` | | Compõe sobre a superfície. Na caixa simples, é onde entra o recuo. |
414
+ | `children` | `ReactNode` | | Filhos livres, que formam o corpo, ou os slots `SurfaceHeader`, `SurfaceBody` e `SurfaceFooter`. |
452
415
 
453
416
  As demais propriedades são as nativas do elemento escolhido em `as`.
454
417
 
455
418
  ## Propriedades de SurfaceHeader
456
419
 
457
- | Propriedade | Tipo | Padrão | Descrição |
458
- | ----------- | ----------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
459
- | `children` | `ReactNode` | | No máximo um de cada: `SurfaceMedia`, `SurfaceTitle`, `SurfaceMeta`, `SurfaceDescription` e `SurfaceActions`. Em `fieldset`, o `SurfaceTitle` é obrigatório e somente ele e `SurfaceMedia` são aceitos. |
460
- | `className` | `string` | | Ajusta a região do header. |
420
+ | Propriedade | Tipo | Padrão | Descrição |
421
+ | ----------- | ----------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
422
+ | `tabs` | `ReactNode` | | `TabsList` sob o título, controlado por um `Tabs` ancestral. Não disponível em fieldset. |
423
+ | `children` | `ReactNode` | | No máximo um de cada: `SurfaceMedia`, `SurfaceTitle`, `SurfaceCount`, `SurfaceMeta`, `SurfaceDescription` e `SurfaceActions`. Em `fieldset`, o `SurfaceTitle` é obrigatório e somente ele e `SurfaceMedia` são aceitos. |
424
+ | `className` | `string` | | Ajusta a região do header. |
461
425
 
462
426
  `SurfaceHeader` só existe como filho direto de `Surface`.
463
427
 
@@ -475,13 +439,6 @@ As demais propriedades são as nativas do elemento escolhido em `as`.
475
439
  | `children` | `ReactNode` | | Nome da superfície. O heading usa o `level` declarado em `Surface`. |
476
440
  | `className` | `string` | | Ajusta o título. |
477
441
 
478
- ## Propriedades de SurfaceMeta
479
-
480
- | Propriedade | Tipo | Padrão | Descrição |
481
- | ----------- | ----------- | ------ | -------------------------------------------------------- |
482
- | `children` | `ReactNode` | | Metadado curto ao lado do título, como o total de itens. |
483
- | `className` | `string` | | Ajusta o metadado. |
484
-
485
442
  ## Propriedades de SurfaceDescription
486
443
 
487
444
  | Propriedade | Tipo | Padrão | Descrição |
@@ -517,6 +474,27 @@ um wrapper filho declara relações locais mais compactas.
517
474
  ## Subheading e regras de negócio
518
475
 
519
476
  `description` e `SurfaceDescription` apresentam brevemente a finalidade da região. A orientação
520
- é opcional e não repete o título. A mesma regra vale para uma página (`level={1}`), uma seção ou
477
+ é opcional e não repete o título. A mesma regra vale para uma página, uma seção ou
521
478
  um card: condições, limites e consequências necessários à decisão ficam no corpo ou junto do
522
479
  controle correspondente. Pular o subheading não deve comprometer a tarefa.
480
+
481
+ ## Propriedades de SurfaceCount
482
+
483
+ | Propriedade | Tipo | Descrição |
484
+ | ----------- | ----------- | ---------------------------------- |
485
+ | `children` | `ReactNode` | Contador ao lado do título. |
486
+ | `className` | `string` | Ajusta a apresentação do contador. |
487
+
488
+ Aceita atributos nativos de `span`. Deve estar em `SurfaceHeader`, fora de `fieldset`.
489
+
490
+ ## Propriedades de SurfaceMeta
491
+
492
+ | Propriedade | Tipo | Descrição |
493
+ | ----------- | ----------- | ------------------------------------------------------- |
494
+ | `children` | `ReactNode` | Badges, datas, autoria ou outros metadados do registro. |
495
+ | `className` | `string` | Ajusta a faixa de metadados. |
496
+
497
+ Aceita atributos nativos de `div`. O intervalo superior é de 0,75rem; os itens têm
498
+ 0,75rem entre colunas e 0,5rem entre linhas. O componente não insere separadores nem
499
+ carrega dados. Omitir o slot não reserva espaço. Metadados não substituem orientações
500
+ ou condições necessárias à decisão e não substituem a descrição textual da região.
@@ -8,6 +8,13 @@ A variante padrão não possui borda externa; `TableBody` mantém somente as div
8
8
  Aplique `text-right` ao cabeçalho e às células quando o domínio pedir alinhamento numérico.
9
9
  `TableCaption` descreve a tabela abaixo do conteúdo.
10
10
 
11
+ As células quebram texto entre palavras. Identificadores longos sem espaços podem exigir
12
+ rolagem horizontal; aplique `wrap-anywhere` à célula quando quiser permitir quebra no meio
13
+ desses valores. Use `whitespace-nowrap` para preservar valores inteiros, como números e datas.
14
+ O contêiner limita a tabela ao espaço recebido e oferece rolagem horizontal quando o conteúdo
15
+ ou larguras mínimas explícitas não cabem. Em uma composição flex ou grid, permita que o ancestral também
16
+ encolha (`min-w-0`); a tabela não pode redefinir a largura de um ancestral.
17
+
11
18
  ```tsx preview col
12
19
  <Table>
13
20
  <TableCaption>Sessões recentes do workspace Empresa X.</TableCaption>
@@ -64,6 +64,10 @@ tom ao sair do canvas e entrar num card, porque `control` representa o resultado
64
64
  cor translúcida a ser misturada com o ancestral. Da mesma forma, hover, seleção e pressão usam
65
65
  papéis diferentes e preservam a mesma ordem comportamental nos dois temas.
66
66
 
67
+ O fundo discreto (`muted`) também se distingue do hover nos dois temas. A calibração de base
68
+ mantém essa diferença mesmo com Contraste em zero; aumentar esse ajuste amplia os degraus da
69
+ escala. Use os tokens de interação para hover, seleção e pressão, sem criar opacidades por componente.
70
+
67
71
  ```tsx preview
68
72
  const NEUTRAL_STATES = [
69
73
  ["control", "Controle em repouso."],
@@ -87,6 +91,16 @@ render(
87
91
  );
88
92
  ```
89
93
 
94
+ A navegação interna usa `bg-sidebar text-foreground`. No claro, ela é quase branca sobre
95
+ conteúdo branco; no escuro, acompanha a camada elevada (`card`). A sidebar geral pode usar
96
+ `bg-sidebar-global text-foreground` para acompanhar o canvas nos dois temas, mantendo continuidade visual sem transparência.
97
+ Os tokens são opacos, e hover, seleção e
98
+ pressão mantêm os papéis de interação compartilhados. O fundo da navegação global continua
99
+ sendo escolhido pelo hospedeiro.
100
+
101
+ No dark, a calibração aplica uma redução final de 5% à luminosidade HSL das superfícies,
102
+ linhas e estados neutros. Textos e contextos semânticos mantêm seus valores para preservar a leitura.
103
+
90
104
  ### Ajuste de aparência
91
105
 
92
106
  Os tokens neutros desta seção não são valores fixos: o tema os calcula a partir de quatro eixos,
@@ -298,8 +312,8 @@ render(
298
312
 
299
313
  1. Formato HSL nos tokens (o upstream migrou para oklch), escolhido pela legibilidade.
300
314
  2. Hierarquia opaca de superfícies e estados nos dois temas: canvas, card/popover, controle, hover,
301
- seleção e pressão têm papéis próprios. No light, o canvas quase branco sustenta superfícies
302
- brancas; no dark, cards e popovers permanecem acima do canvas. A ADR 0043 registra os valores e
315
+ seleção e pressão têm papéis próprios. No preset light, canvas, controles, cards e popovers são brancos; bordas e elevação
316
+ distinguem as regiões; no dark, cards e popovers permanecem acima do canvas. A ADR 0043 registra os valores e
303
317
  as restrições de composição. A escala neutra é derivada dos eixos de aparência (ADR 0044); no
304
318
  preset, os valores são os calibrados.
305
319
  3. Radius mapeado (`xs` a `2xl`) preservando o sentido pré-v4 dos utilitários `rounded-*`. Esse
@@ -307,8 +321,8 @@ render(
307
321
  precisam revisar a transição para esse degrau nativo ao customizar `--radius`.
308
322
  4. Escala `shadow-*` recomposta em duas camadas claras e difusas — preserva os degraus do
309
323
  Tailwind e troca somente o peso visual. Componentes e consumidores usam diretamente a escala.
310
- 5. Bordas mais leves no tema claro: `--border`/`--input` em `92.5%` (upstream `89.8%`) — par do canvas
311
- quase-branco do body; superfícies elevadas usam essa mesma borda real.
324
+ 5. Bordas mais leves no tema claro: `--border`/`--input` em `92%` (upstream `89.8%`) — par do canvas
325
+ branco do body; superfícies elevadas usam essa mesma borda real.
312
326
  6. Radius base em `0.75rem` — superfícies principais ganham cantos mais presentes sem aproximar
313
327
  controles compactos de modais. A decisão detalhada acompanha o pacote em
314
328
  `docs/radius-scale.md`, com os defaults, o comportamento de superfícies aninhadas e as
@@ -29,9 +29,12 @@ export function OpusProvider({ client, dicts, children }: OpusProviderProps): Re
29
29
  }
30
30
 
31
31
  /** Acesso interno ao adapter; não é reexportado por `@softize/opus/ui/react`. */
32
- export function useClient(hook: string): ClientAdapter {
33
- const ctx = useContext(OpusContext)
34
- if (ctx === null) {
32
+ export function useOptionalClient(): ClientAdapter | null {
33
+ return useContext(OpusContext)?.client ?? null
34
+ }
35
+
36
+ export function requireClient(client: ClientAdapter | null, hook: string): ClientAdapter {
37
+ if (client === null) {
35
38
  throw new Error(
36
39
  `${hook} foi chamado fora de um <OpusProvider> e não tem como executar a action. ` +
37
40
  'Os hooks de action e os componentes que os usam, como ActionForm, ActionList e ActionView, ' +
@@ -39,7 +42,11 @@ export function useClient(hook: string): ClientAdapter {
39
42
  'em testes e previews, envolva o componente renderizado.',
40
43
  )
41
44
  }
42
- return ctx.client
45
+ return client
46
+ }
47
+
48
+ export function useClient(hook: string): ClientAdapter {
49
+ return requireClient(useOptionalClient(), hook)
43
50
  }
44
51
 
45
52
  const NO_DICTS: Record<string, DictLike> = {}
@@ -39,7 +39,7 @@ import {
39
39
  idleAction,
40
40
  idleLookup,
41
41
  } from '../index.ts'
42
- import { useClient } from './context.tsx'
42
+ import { useClient, useOptionalClient, requireClient } from './context.tsx'
43
43
  export {
44
44
  OpusProvider,
45
45
  useDicts,
@@ -213,18 +213,37 @@ export function useListAction<TItem = unknown, TInput extends Record<string, unk
213
213
  input?: TInput,
214
214
  options?: { enabled?: boolean },
215
215
  ): UseListActionResult<TItem> {
216
- const client = useClient('useListAction')
216
+ return useListResource<TItem, TInput>(action, input, options)
217
+ }
218
+
219
+ /** Fonte já governada por outro serviço; a chave inclui o escopo de acesso. */
220
+ export interface ListDataSource<TItem> {
221
+ key: readonly string[]
222
+ load: (input: Record<string, unknown>, context: { signal: AbortSignal }) => Promise<Paginated<TItem>>
223
+ }
224
+
225
+ /** Consulta comum ao ActionList e ao hook público, sem duplicar estados de busca. */
226
+ export function useListResource<TItem = unknown, TInput extends Record<string, unknown> = Record<string, unknown>>(
227
+ resource: { name: string; kind: 'list' } | ListDataSource<TItem>,
228
+ input?: TInput,
229
+ options?: { enabled?: boolean },
230
+ ): UseListActionResult<TItem> {
231
+ const providedClient = useOptionalClient()
232
+ const external = 'load' in resource
233
+ const client = external ? null : requireClient(providedClient, 'useListAction')
217
234
 
218
235
  const query = useQuery<Paginated<TItem>, ActionError>({
219
- queryKey: [action.name, input ?? {}],
220
- queryFn: async () => {
221
- const result = await client.run<Paginated<TItem>>(action as unknown as ActionDef, input ?? {})
236
+ queryKey: external ? ['opus:list-source', ...resource.key, input ?? {}] : [resource.name, input ?? {}],
237
+ ...(external ? { gcTime: 0, retry: false } : {}),
238
+ queryFn: async (context) => {
239
+ if (external) return resource.load(input ?? {}, { signal: context.signal })
240
+ const result = await client!.run<Paginated<TItem>>(resource as unknown as ActionDef, input ?? {})
222
241
  if (!result.ok) throw result.error
223
242
  return result.data
224
243
  },
225
244
  // Input mudou (filtro/página) → mantém os itens na tela enquanto busca: o
226
245
  // ActionList esmaece em vez de piscar skeleton. isPending = só o 1º load.
227
- placeholderData: keepPreviousData,
246
+ placeholderData: external ? undefined : keepPreviousData,
228
247
  enabled: options?.enabled ?? true,
229
248
  })
230
249
 
@@ -259,7 +278,8 @@ export function useGroupedListAction<
259
278
  pages: Record<string, number>,
260
279
  pageSize: number,
261
280
  ): ListActionGroup<TItem>[] {
262
- const client = useClient('ActionList grouping')
281
+ const providedClient = useOptionalClient()
282
+ const client = field === null ? providedClient : requireClient(providedClient, 'ActionList grouping')
263
283
  return useGroupedListQueries<TItem, TInput>(
264
284
  client,
265
285
  action,
package/src/ui/meta.ts CHANGED
@@ -15,7 +15,7 @@ export const componentMeta = {
15
15
  name: "surface",
16
16
  ancestry: "opus",
17
17
  whenToUse:
18
- 'Delimite e nomeie um trecho da tela: uma região da página, um grupo de campos, um registro, um item de grade ou um bloco clicável. `as` escolhe o elemento (`div`, `section`, `article` ou `fieldset`) e `asChild` entrega a superfície a um botão ou link. `framed` acrescenta a moldura, `elevation="raised"` dá fundo próprio e `layout` escolhe entre ritmo contínuo e faixas com divisores. No caso comum, declare `title`, `description`, `count`, `actions` e `media` na própria Surface; componha SurfaceHeader, SurfaceBody e SurfaceFooter quando precisar controlar a estrutura. Numa página centrada em uma coleção, `level={1}` faz da Surface o heading principal; nas demais, ele vem de PageTitle. Para um modal use Dialog, para um painel lateral use Drawer e para uma coluna de layout use Pane.',
18
+ 'Delimite e nomeie um trecho da tela: uma região da página, um grupo de campos, um registro, um item de grade ou um bloco clicável. `as` escolhe o elemento (`div`, `section`, `article` ou `fieldset`) e `asChild` entrega a superfície a um botão ou link. `framed` acrescenta a moldura, `elevation="raised"` dá fundo próprio. Superfícies delimitadas derivam os divisores das regiões presentes; sem moldura, conservam o alinhamento e ritmo do hospedeiro. No caso comum, declare `title`, `description`, `count`, `actions` e `media` na própria Surface; componha SurfaceHeader, SurfaceBody e SurfaceFooter quando precisar controlar a estrutura. O título principal de Page vem de PageTitle; Surfaces internas usam níveis subordinados. `level={1}` fica restrito a superfícies fora de Page. Para um modal use Dialog, para um painel lateral use Drawer e para uma coluna de layout use Pane.',
19
19
  },
20
20
  ask: {
21
21
  name: "ask",
@@ -165,7 +165,7 @@ export const componentMeta = {
165
165
  name: "separator",
166
166
  ancestry: "shadcn",
167
167
  whenToUse:
168
- 'Marque a fronteira entre blocos de conteúdo que não pertencem a uma superfície com regiões. A linha horizontal fica entre blocos empilhados; com `orientation="vertical"`, ela separa itens lado a lado e herda a altura do contêiner. A linha é decorativa por padrão e fica fora da árvore de acessibilidade; use `decorative={false}` quando a divisão tiver significado. Não use Separator entre cabeçalho, corpo e rodapé de Surface, Dialog ou Drawer: nessas superfícies, os divisores são derivados das regiões com `layout="sectioned"`.',
168
+ 'Marque a fronteira entre blocos de conteúdo que não pertencem a uma superfície com regiões. A linha horizontal fica entre blocos empilhados; com `orientation="vertical"`, ela separa itens lado a lado e herda a altura do contêiner. A linha é decorativa por padrão e fica fora da árvore de acessibilidade; use `decorative={false}` quando a divisão tiver significado. Não use Separator entre cabeçalho, corpo e rodapé de Surface, Dialog ou Drawer: nessas superfícies, os divisores são derivados das regiões automaticamente.',
169
169
  },
170
170
  skeleton: {
171
171
  name: "skeleton",
@@ -261,7 +261,7 @@ export const componentMeta = {
261
261
  name: "floating-layer",
262
262
  ancestry: "opus",
263
263
  whenToUse:
264
- "Mantenha utilizável, por cima de qualquer Dialog ou Drawer aberto, uma região que acompanha a aplicação inteira, como o botão e o painel de um assistente. O modal continua aberto e intacto atrás; a camada recebe clique, foco, digitação, rolagem e Esc. Diálogos que exigem resposta, como `Dialog mode=\"alert\"` e `dialog.confirm`, continuam cobrindo a camada até a resposta. Cada filho se posiciona contra a janela, e o hospedeiro publica em `data-modal` e `data-modal-side` o Dialog ou Drawer aberto. Para ferramentas sobre uma única superfície, use Dock; para conteúdo preso a um gatilho, Popover.",
264
+ 'Mantenha utilizável, por cima de qualquer Dialog ou Drawer aberto, uma região que acompanha a aplicação inteira, como o botão e o painel de um assistente. O modal continua aberto e intacto atrás; a camada recebe clique, foco, digitação, rolagem e Esc. Diálogos que exigem resposta, como `Dialog mode="alert"` e `dialog.confirm`, continuam cobrindo a camada até a resposta. Cada filho se posiciona contra a janela, e o hospedeiro publica em `data-modal` e `data-modal-side` o Dialog ou Drawer aberto. Para ferramentas sobre uma única superfície, use Dock; para conteúdo preso a um gatilho, Popover.',
265
265
  },
266
266
  drawer: {
267
267
  name: "drawer",
@@ -388,7 +388,7 @@ export const componentMeta = {
388
388
  name: "action-list",
389
389
  ancestry: "opus",
390
390
  whenToUse:
391
- "Apresente uma coleção pesquisável a partir de uma ListAction. O contrato declara colunas, filtros, período, busca e dimensões fechadas de agrupamento; ActionList coordena visualizações, paginação, seleção em lote e sincronização com a URL. A tabela derivada não cria moldura própria. Em uma página, envolva a lista com Surface para manter título e criação junto da coleção. Para um único recurso, use ActionView.",
391
+ "Apresente uma coleção pesquisável a partir de uma ListAction. O contrato declara colunas, filtros, período, busca e dimensões fechadas de agrupamento; ActionList coordena visualizações, paginação, seleção em lote e sincronização com a URL. A tabela derivada não cria moldura própria. Use item com os campos title e description para uma lista de itens conectados; title da coleção reúne suas toolbarActions. Fontes externas já governadas por outro serviço usam source com chave de escopo e loader, preservando o transporte existente. Para um único recurso, use ActionView.",
392
392
  },
393
393
  "action-trigger": {
394
394
  name: "action-trigger",
@@ -406,7 +406,7 @@ export const componentMeta = {
406
406
  name: "page",
407
407
  ancestry: "opus",
408
408
  whenToUse:
409
- "Estruture uma página com cabeçalho, estado e conteúdo na mesma anatomia. Page renderiza o `<main>` com header e viewport em largura total e conteúdo centralizado de até 80rem; surfaceLayout escolhe sectioned (padrão) ou continuous, independente de layout flow/fill; PageHeader reúne navegação e ações da superfície; PageIntro abre PageBody com título e orientação. A composição compacta mantém o título no header, sem intro. PageShell pode fornecer navegação e ações conhecidas pela aplicação sem criar uma barra visual. Uma região interna pode fornecer o heading principal com `Surface level={1}`. PageActionsTarget fica restrito a workspaces imersivos. PageState substitui o header e o conteúdo durante carregamento, falha ou ausência, por isso informações indispensáveis entram no corpo, como Alert ou texto. Para uma falha parcial, mantenha o estado na seção com DataState; para uma região disponível para criação ou vínculo, use Empty.",
409
+ "Estruture uma página aberta com título e ações no header, metadados abaixo e conteúdo na mesma coluna. Page usa padding de 2,5rem, título de 1,875rem, intervalo de 1,5rem após o cabeçalho e de 1rem entre as demais regiões, sem divisores. width escolhe narrow, default ou wide; sem opção, ocupa a largura disponível. Ações dos headers de Page, Drawer, Dialog e Surface usam default/icon e linha mínima de 2,25rem. PageHeader.tabs recebe TabsList; TabsPanel fica no body. PageBack é o primeiro ícone das ações, com destino explícito e tooltip. PageShell fornece ações sem criar outra barra. layout flow rola a página inteira; fill mantém o header visível e entrega a altura a listas ou à rolagem do body. Drawer e Dialog mantêm suas superfícies delimitadas e recuo de 1rem. PageState substitui o header e o conteúdo durante carregamento, falha ou ausência. Use DataState para falha parcial. Título e metadados pertencem ao PageHeader.",
410
410
  },
411
411
  presentation: {
412
412
  name: "presentation",
package/src/ui/react.tsx CHANGED
@@ -25,6 +25,7 @@ export type {
25
25
  UseFormActionOptions,
26
26
  UseFormActionResult,
27
27
  UseListActionResult,
28
+ ListDataSource,
28
29
  UseLookupActionResult,
29
30
  UseTriggerActionOptions,
30
31
  UseTriggerActionResult,
@@ -175,8 +176,7 @@ export {
175
176
  DialogNavigation,
176
177
  DialogMedia,
177
178
  DialogActions,
178
- DialogIntro,
179
- DialogIntroActions,
179
+ DialogMeta,
180
180
  DialogBody,
181
181
  DialogFooter,
182
182
  DialogTitle,
@@ -194,7 +194,6 @@ export type { FloatingLayerProps } from "./components/primitives/floating-layer.
194
194
 
195
195
  // O pacote de anatomia que Surface, Dialog, Drawer e Pane declaram no root (ADR 0023). As
196
196
  // tabelas e o contexto continuam internos; o consumidor escolhe entre os dois nomes.
197
- export type { SurfaceLayout } from "./components/patterns/surface-anatomy.ts";
198
197
 
199
198
  export {
200
199
  Menu,
@@ -324,8 +323,7 @@ export {
324
323
  export {
325
324
  Drawer,
326
325
  DrawerActions,
327
- DrawerIntro,
328
- DrawerIntroActions,
326
+ DrawerMeta,
329
327
  DrawerClose,
330
328
  DrawerContent,
331
329
  DrawerFooter,
@@ -465,12 +463,15 @@ export {
465
463
  presetRange,
466
464
  listParamsToState,
467
465
  listStateToParams,
466
+ listStateToInput,
468
467
  } from "./components/patterns/list.tsx";
469
468
  export type {
470
469
  ActionFilterBarProps,
471
470
  ActionFilterState,
472
471
  AdvancedFiltersLayout,
473
472
  ActionListProps,
473
+ ActionListItemSpec,
474
+ ActionListContext,
474
475
  ActionListColumn,
475
476
  ActionListState,
476
477
  ActionListView,
@@ -515,9 +516,7 @@ export {
515
516
  PageShell,
516
517
  Page,
517
518
  PageHeader,
518
- PageIntro,
519
- PageIntroActions,
520
- PageNavigation,
519
+ PageMeta,
521
520
  PageBack,
522
521
  PageTitle,
523
522
  PageDescription,
@@ -549,6 +548,7 @@ export {
549
548
  SurfaceTitle,
550
549
  SurfaceDescription,
551
550
  SurfaceMeta,
551
+ SurfaceCount,
552
552
  SurfaceActions,
553
553
  SurfaceBody,
554
554
  SurfaceFooter,
@@ -622,3 +622,33 @@ export type {
622
622
  DotContext,
623
623
  DotVariant,
624
624
  } from "./components/primitives/dot.tsx";
625
+
626
+ export {
627
+ bindPresentationList,
628
+ bindPresentationActionList,
629
+ bindPresentationCells,
630
+ bindPresentationCollection,
631
+ bindPresentationDetail,
632
+ bindPresentationOptions,
633
+ bindPresentationField,
634
+ bindPresentationComponent,
635
+ bindPresentationContent,
636
+ type PresentationRecordSource,
637
+ type PresentationComponentProps,
638
+ type PresentationComponentAdapter,
639
+ bindPresentationListContent,
640
+ bindPresentationRowContent,
641
+ } from "./components/patterns/presentation-sections.tsx";
642
+ export type {
643
+ PresentationCompositionRuntime,
644
+ PresentationListAdapter,
645
+ PresentationCellsAdapter,
646
+ PresentationCollectionAdapter,
647
+ PresentationCollectionControls,
648
+ PresentationDetailAdapter,
649
+ PresentationOptionsAdapter,
650
+ PresentationFieldAdapter,
651
+ PresentationFieldProps,
652
+ PresentationListContentAdapter,
653
+ PresentationRowContentAdapter,
654
+ } from "./components/patterns/presentation-sections.tsx";