@softize/opus 30.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 (55) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/bin/lib/check.mjs +109 -101
  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/0060-surface-composition-separates-chrome-and-intro.md +142 -0
  7. package/docs/adr/0064-action-list-com-itens-e-fontes-externas.md +47 -0
  8. package/docs/adr/0065-presentation-composes-settings-sections.md +295 -0
  9. package/docs/adr/README.md +4 -0
  10. package/package.json +1 -1
  11. package/registry/skills/build-opus-ui/SKILL.md +21 -19
  12. package/registry/skills/build-opus-ui/references/evaluations.md +13 -11
  13. package/registry/skills/build-opus-ui/references/ui-patterns.md +24 -27
  14. package/src/core/presentation.ts +938 -32
  15. package/src/core/types.ts +1 -0
  16. package/src/ui/components/patterns/form.tsx +2 -0
  17. package/src/ui/components/patterns/grouped-list.ts +2 -2
  18. package/src/ui/components/patterns/list.tsx +572 -358
  19. package/src/ui/components/patterns/page-heading-context.tsx +12 -5
  20. package/src/ui/components/patterns/page.tsx +227 -197
  21. package/src/ui/components/patterns/presentation-fields.tsx +246 -0
  22. package/src/ui/components/patterns/presentation-sections.tsx +494 -0
  23. package/src/ui/components/patterns/presentation.tsx +1142 -229
  24. package/src/ui/components/patterns/structural-header.tsx +23 -32
  25. package/src/ui/components/patterns/surface-anatomy.ts +27 -217
  26. package/src/ui/components/patterns/surface.tsx +19 -29
  27. package/src/ui/components/patterns/trigger.tsx +10 -2
  28. package/src/ui/components/primitives/close-button.tsx +11 -3
  29. package/src/ui/components/primitives/command.tsx +2 -8
  30. package/src/ui/components/primitives/detail.tsx +1 -1
  31. package/src/ui/components/primitives/dialog.tsx +19 -81
  32. package/src/ui/components/primitives/drawer.tsx +17 -79
  33. package/src/ui/components/primitives/field.tsx +1 -1
  34. package/src/ui/components/primitives/pagination.tsx +3 -2
  35. package/src/ui/components/primitives/table.tsx +1 -1
  36. package/src/ui/components/primitives/tabs.tsx +13 -2
  37. package/src/ui/docs/content/action-form.md +2 -1
  38. package/src/ui/docs/content/action-list.md +134 -68
  39. package/src/ui/docs/content/action-trigger.md +5 -0
  40. package/src/ui/docs/content/button.md +5 -6
  41. package/src/ui/docs/content/command.md +62 -50
  42. package/src/ui/docs/content/compositions.md +129 -459
  43. package/src/ui/docs/content/dialog.md +44 -112
  44. package/src/ui/docs/content/drawer.md +44 -103
  45. package/src/ui/docs/content/field.md +2 -1
  46. package/src/ui/docs/content/page.md +204 -412
  47. package/src/ui/docs/content/presentation.md +371 -23
  48. package/src/ui/docs/content/separator.md +1 -1
  49. package/src/ui/docs/content/surface.md +65 -120
  50. package/src/ui/docs/content/tokens.md +18 -4
  51. package/src/ui/drivers/context.tsx +11 -4
  52. package/src/ui/drivers/react.tsx +27 -7
  53. package/src/ui/meta.ts +5 -5
  54. package/src/ui/react.tsx +34 -8
  55. 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
167
-
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.
164
+ ## Divisores automáticos
212
165
 
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;
@@ -440,9 +376,18 @@ reservado à orientação textual breve. Os itens quebram em várias linhas quan
440
376
 
441
377
  ```tsx preview col
442
378
  render(
443
- <Surface as="article" title="Manual de atendimento" meta={<><Badge>Publicado</Badge><span>Por Ana</span></>}>
444
- <p>Conteúdo do manual.</p>
445
- </Surface>
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>,
446
391
  );
447
392
  ```
448
393
 
@@ -452,31 +397,31 @@ O total da coleção continua ao lado do título: use `count` na forma curta ou
452
397
 
453
398
  ## Propriedades de Surface
454
399
 
455
- | Propriedade | Tipo | Padrão | Descrição |
456
- | ------------- | ----------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------ |
457
- | `as` | `'div' \| 'section' \| 'article' \| 'fieldset'` | `'div'` | Elemento da superfície. Não combina com `asChild`. |
458
- | `asChild` | `boolean` | `false` | Entrega a superfície ao único filho, como um `button` ou um link. |
459
- | `framed` | `boolean` | `false` | Acrescenta borda e raio, e recorta o conteúdo seccionado ou sem recuo. |
460
- | `elevation` | `'none' \| 'raised'` | `'none'` | `raised` pinta o fundo de superfície elevada e mantém a altura do próprio conteúdo. |
461
- | `layout` | `'continuous' \| 'sectioned'` | `'continuous'` | Anatomia das regiões. `sectioned` exige `framed` ou `elevation="raised"`. |
462
- | `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`. |
463
- | `title` | `ReactNode` | | Forma curta: título da superfície. |
464
- | `media` | `ReactNode` | | Forma curta: conteúdo do slot de mídia, antes do título. Exige `title`. |
465
- | `count` | `number` | | Forma curta: total de itens ao lado do título. Exige `title`; indisponível em `fieldset`. |
466
- | `description` | `ReactNode` | | Forma curta: frase de apoio sob o título. Exige `title`; indisponível em `fieldset`. |
467
- | `meta` | `ReactNode` | | Forma curta: metadados sob o título e a orientação. Exige `title`; indisponível em `fieldset`. |
468
- | `actions` | `ReactNode` | | Forma curta: ações sobre a superfície inteira. Exige `title`; indisponível em `fieldset`. |
469
- | `className` | `string` | | Compõe sobre a superfície. Na caixa simples, é onde entra o recuo. |
470
- | `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`. |
471
415
 
472
416
  As demais propriedades são as nativas do elemento escolhido em `as`.
473
417
 
474
418
  ## Propriedades de SurfaceHeader
475
419
 
476
- | Propriedade | Tipo | Padrão | Descrição |
477
- | ----------- | ----------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
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. |
478
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. |
479
- | `className` | `string` | | Ajusta a região do header. |
424
+ | `className` | `string` | | Ajusta a região do header. |
480
425
 
481
426
  `SurfaceHeader` só existe como filho direto de `Surface`.
482
427
 
@@ -529,25 +474,25 @@ um wrapper filho declara relações locais mais compactas.
529
474
  ## Subheading e regras de negócio
530
475
 
531
476
  `description` e `SurfaceDescription` apresentam brevemente a finalidade da região. A orientação
532
- é 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
533
478
  um card: condições, limites e consequências necessários à decisão ficam no corpo ou junto do
534
479
  controle correspondente. Pular o subheading não deve comprometer a tarefa.
535
480
 
536
481
  ## Propriedades de SurfaceCount
537
482
 
538
- | Propriedade | Tipo | Descrição |
539
- | --- | --- | --- |
540
- | `children` | `ReactNode` | Contador ao lado do título. |
541
- | `className` | `string` | Ajusta a apresentação do contador. |
483
+ | Propriedade | Tipo | Descrição |
484
+ | ----------- | ----------- | ---------------------------------- |
485
+ | `children` | `ReactNode` | Contador ao lado do título. |
486
+ | `className` | `string` | Ajusta a apresentação do contador. |
542
487
 
543
488
  Aceita atributos nativos de `span`. Deve estar em `SurfaceHeader`, fora de `fieldset`.
544
489
 
545
490
  ## Propriedades de SurfaceMeta
546
491
 
547
- | Propriedade | Tipo | Descrição |
548
- | --- | --- | --- |
549
- | `children` | `ReactNode` | Badges, datas, autoria ou outros metadados do registro. |
550
- | `className` | `string` | Ajusta a faixa de metadados. |
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. |
551
496
 
552
497
  Aceita atributos nativos de `div`. O intervalo superior é de 0,75rem; os itens têm
553
498
  0,75rem entre colunas e 0,5rem entre linhas. O componente não insere separadores nem
@@ -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,6 @@ export {
175
176
  DialogNavigation,
176
177
  DialogMedia,
177
178
  DialogActions,
178
- DialogIntro,
179
- DialogIntroActions,
180
179
  DialogMeta,
181
180
  DialogBody,
182
181
  DialogFooter,
@@ -195,7 +194,6 @@ export type { FloatingLayerProps } from "./components/primitives/floating-layer.
195
194
 
196
195
  // O pacote de anatomia que Surface, Dialog, Drawer e Pane declaram no root (ADR 0023). As
197
196
  // tabelas e o contexto continuam internos; o consumidor escolhe entre os dois nomes.
198
- export type { SurfaceLayout } from "./components/patterns/surface-anatomy.ts";
199
197
 
200
198
  export {
201
199
  Menu,
@@ -325,8 +323,6 @@ export {
325
323
  export {
326
324
  Drawer,
327
325
  DrawerActions,
328
- DrawerIntro,
329
- DrawerIntroActions,
330
326
  DrawerMeta,
331
327
  DrawerClose,
332
328
  DrawerContent,
@@ -467,12 +463,15 @@ export {
467
463
  presetRange,
468
464
  listParamsToState,
469
465
  listStateToParams,
466
+ listStateToInput,
470
467
  } from "./components/patterns/list.tsx";
471
468
  export type {
472
469
  ActionFilterBarProps,
473
470
  ActionFilterState,
474
471
  AdvancedFiltersLayout,
475
472
  ActionListProps,
473
+ ActionListItemSpec,
474
+ ActionListContext,
476
475
  ActionListColumn,
477
476
  ActionListState,
478
477
  ActionListView,
@@ -517,10 +516,7 @@ export {
517
516
  PageShell,
518
517
  Page,
519
518
  PageHeader,
520
- PageIntro,
521
- PageIntroActions,
522
519
  PageMeta,
523
- PageNavigation,
524
520
  PageBack,
525
521
  PageTitle,
526
522
  PageDescription,
@@ -626,3 +622,33 @@ export type {
626
622
  DotContext,
627
623
  DotVariant,
628
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";