@softize/opus 30.0.0 → 31.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 (64) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/bin/lib/check.mjs +109 -101
  3. package/bin/lib/copy.mjs +2973 -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/0066-civil-date-input-and-calendar-list.md +28 -0
  10. package/docs/adr/README.md +6 -0
  11. package/package.json +1 -1
  12. package/registry/skills/build-opus-ui/SKILL.md +21 -19
  13. package/registry/skills/build-opus-ui/references/evaluations.md +13 -11
  14. package/registry/skills/build-opus-ui/references/ui-patterns.md +24 -27
  15. package/src/core/presentation.ts +938 -32
  16. package/src/core/types.ts +2 -0
  17. package/src/ui/components/patterns/form.tsx +22 -1
  18. package/src/ui/components/patterns/grouped-list.ts +2 -2
  19. package/src/ui/components/patterns/list-calendar.tsx +129 -0
  20. package/src/ui/components/patterns/list.tsx +612 -382
  21. package/src/ui/components/patterns/page-heading-context.tsx +12 -5
  22. package/src/ui/components/patterns/page.tsx +227 -197
  23. package/src/ui/components/patterns/presentation-fields.tsx +246 -0
  24. package/src/ui/components/patterns/presentation-sections.tsx +494 -0
  25. package/src/ui/components/patterns/presentation.tsx +1142 -229
  26. package/src/ui/components/patterns/structural-header.tsx +23 -32
  27. package/src/ui/components/patterns/surface-anatomy.ts +27 -217
  28. package/src/ui/components/patterns/surface.tsx +19 -29
  29. package/src/ui/components/patterns/trigger.tsx +10 -2
  30. package/src/ui/components/primitives/calendar.tsx +39 -18
  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/date-input.tsx +276 -0
  34. package/src/ui/components/primitives/detail.tsx +1 -1
  35. package/src/ui/components/primitives/dialog.tsx +19 -81
  36. package/src/ui/components/primitives/drawer.tsx +17 -79
  37. package/src/ui/components/primitives/field.tsx +1 -1
  38. package/src/ui/components/primitives/pagination.tsx +3 -2
  39. package/src/ui/components/primitives/table.tsx +1 -1
  40. package/src/ui/components/primitives/tabs.tsx +13 -2
  41. package/src/ui/docs/content/action-form.md +6 -1
  42. package/src/ui/docs/content/action-list.md +156 -68
  43. package/src/ui/docs/content/action-trigger.md +5 -0
  44. package/src/ui/docs/content/button.md +5 -6
  45. package/src/ui/docs/content/calendar-month-view.md +50 -0
  46. package/src/ui/docs/content/calendar.md +6 -1
  47. package/src/ui/docs/content/command.md +62 -50
  48. package/src/ui/docs/content/compositions.md +129 -459
  49. package/src/ui/docs/content/date-input.md +67 -0
  50. package/src/ui/docs/content/dialog.md +44 -112
  51. package/src/ui/docs/content/drawer.md +44 -103
  52. package/src/ui/docs/content/field.md +2 -1
  53. package/src/ui/docs/content/input.md +3 -0
  54. package/src/ui/docs/content/page.md +204 -412
  55. package/src/ui/docs/content/presentation.md +371 -23
  56. package/src/ui/docs/content/separator.md +1 -1
  57. package/src/ui/docs/content/surface.md +65 -120
  58. package/src/ui/docs/content/tokens.md +18 -4
  59. package/src/ui/docs/registry.tsx +38 -9
  60. package/src/ui/drivers/context.tsx +11 -4
  61. package/src/ui/drivers/react.tsx +27 -7
  62. package/src/ui/meta.ts +17 -6
  63. package/src/ui/react.tsx +41 -8
  64. 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
@@ -67,6 +67,8 @@ import dockMd from "./content/dock.md?raw";
67
67
  import breadcrumbMd from "./content/breadcrumb.md?raw";
68
68
  import buttonMd from "./content/button.md?raw";
69
69
  import calendarMd from "./content/calendar.md?raw";
70
+ import dateInputMd from "./content/date-input.md?raw";
71
+ import calendarMonthViewMd from "./content/calendar-month-view.md?raw";
70
72
  import metricCardMd from "./content/metric-card.md?raw";
71
73
  import carouselMd from "./content/carousel.md?raw";
72
74
  import askMd from "./content/ask.md?raw";
@@ -152,9 +154,15 @@ export const iconScope: Record<string, unknown> = Object.fromEntries(
152
154
  );
153
155
 
154
156
  /** Página conceitual (Começar/Framework/Design/Intro): markdown direto (o H1/lead moram no md). */
155
- const doc = (content: string, codeCollapsible = true) => (): React.ReactElement => (
156
- <DocMarkdown content={content} scope={iconScope} codeCollapsible={codeCollapsible} />
157
- );
157
+ const doc =
158
+ (content: string, codeCollapsible = true) =>
159
+ (): React.ReactElement => (
160
+ <DocMarkdown
161
+ content={content}
162
+ scope={iconScope}
163
+ codeCollapsible={codeCollapsible}
164
+ />
165
+ );
158
166
 
159
167
  /**
160
168
  * Página de componente: o cabeçalho (título + badge de origem + whenToUse) vem do `meta` central
@@ -305,7 +313,11 @@ export const PROTOCOL_SECTIONS: DocSection[] = [
305
313
  searchText: mcpMd,
306
314
  render: doc(mcpMd),
307
315
  },
308
- { slug: "ui", title: "Interface com Opus ↗", render: () => <UiRedirect /> },
316
+ {
317
+ slug: "ui",
318
+ title: "Interface com Opus ↗",
319
+ render: () => <UiRedirect />,
320
+ },
309
321
  {
310
322
  slug: "documentation-portal",
311
323
  title: "Publicação de documentação",
@@ -585,6 +597,11 @@ export const UI_SECTIONS: DocSection[] = [
585
597
  actionFormDialogMd,
586
598
  ),
587
599
  },
600
+ {
601
+ slug: "calendar-month-view",
602
+ title: "CalendarMonthView",
603
+ render: comp("CalendarMonthView", "calendar-month-view", calendarMonthViewMd),
604
+ },
588
605
  {
589
606
  slug: "action-list",
590
607
  title: "Action List",
@@ -637,6 +654,11 @@ export const UI_SECTIONS: DocSection[] = [
637
654
  title: "Calendar",
638
655
  render: comp("Calendar", "calendar", calendarMd),
639
656
  },
657
+ {
658
+ slug: "date-input",
659
+ title: "DateInput",
660
+ render: comp("DateInput", "date-input", dateInputMd),
661
+ },
640
662
  {
641
663
  slug: "checkbox",
642
664
  title: "Checkbox",
@@ -890,8 +912,9 @@ export const DOC_SECTIONS: DocSection[] = [
890
912
  {
891
913
  pages: [
892
914
  ...["getting-started", "cycle", "upgrading"].map((slug) =>
893
- PROTOCOL_SECTIONS.flatMap((section) => section.groups.flatMap((group) => group.pages))
894
- .find((page) => page.slug === slug)!,
915
+ PROTOCOL_SECTIONS.flatMap((section) =>
916
+ section.groups.flatMap((group) => group.pages),
917
+ ).find((page) => page.slug === slug)!,
895
918
  ),
896
919
  {
897
920
  slug: "changelog",
@@ -902,7 +925,9 @@ export const DOC_SECTIONS: DocSection[] = [
902
925
  },
903
926
  {
904
927
  label: "Introdução ao SDK",
905
- pages: PROTOCOL_SECTIONS[0]!.groups[0]!.pages.filter((page) => page.slug !== "getting-started"),
928
+ pages: PROTOCOL_SECTIONS[0]!.groups[0]!.pages.filter(
929
+ (page) => page.slug !== "getting-started",
930
+ ),
906
931
  },
907
932
  ],
908
933
  },
@@ -913,8 +938,12 @@ export const DOC_SECTIONS: DocSection[] = [
913
938
  label: s.label === "Testes e evolução" ? "Testes" : s.label,
914
939
  groups: s.groups.map((g) => ({
915
940
  ...g,
916
- pages: g.pages.filter((p) => !["ui", "cycle", "upgrading"].includes(p.slug)),
941
+ pages: g.pages.filter(
942
+ (p) => !["ui", "cycle", "upgrading"].includes(p.slug),
943
+ ),
917
944
  })),
918
945
  })),
919
- ...UI_SECTIONS.map((section, index) => index === 0 ? { ...section, label: "Introdução à UI" } : section),
946
+ ...UI_SECTIONS.map((section, index) =>
947
+ index === 0 ? { ...section, label: "Introdução à UI" } : section,
948
+ ),
920
949
  ];
@@ -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",
@@ -131,6 +131,12 @@ export const componentMeta = {
131
131
  whenToUse:
132
132
  "Receba texto em uma única linha com os atributos nativos de `input`. Use `icon` para identidade e `trailing` para uma ação no fim do campo; associe uma Label. Quando precisar combinar múltiplos adornos ou controles na mesma moldura, use InputGroup.",
133
133
  },
134
+ "date-input": {
135
+ name: "date-input",
136
+ ancestry: "opus",
137
+ whenToUse:
138
+ "Receba uma data por digitação em DD/MM/AAAA ou escolha no calendário. O valor válido permanece uma string civil AAAA-MM-DD, sem conversão de fuso horário. Associe uma Label; no ActionForm, declare widget: 'date'.",
139
+ },
134
140
  label: {
135
141
  name: "label",
136
142
  ancestry: "shadcn",
@@ -165,7 +171,7 @@ export const componentMeta = {
165
171
  name: "separator",
166
172
  ancestry: "shadcn",
167
173
  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"`.',
174
+ '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
175
  },
170
176
  skeleton: {
171
177
  name: "skeleton",
@@ -239,11 +245,16 @@ export const componentMeta = {
239
245
  whenToUse:
240
246
  "Mostre onde a pessoa está quando a página fica vários níveis abaixo da entrada da seção e permita voltar a um nível intermediário. Use BreadcrumbLink nos níveis navegáveis, BreadcrumbPage no nível atual, que não é clicável, BreadcrumbSeparator entre eles e BreadcrumbEllipsis para colapsar trilhas longas. Quando basta voltar ao nível imediatamente acima, use PageBack; para alternar painéis no mesmo nível, Tabs.",
241
247
  },
248
+ "calendar-month-view": {
249
+ name: "calendar-month-view",
250
+ ancestry: "opus",
251
+ whenToUse: "Consulte registros distribuídos pelos dias de um mês, como expediente e agenda. Use como visualização de ActionList para preservar consulta, filtros e estados da coleção.",
252
+ },
242
253
  calendar: {
243
254
  name: "calendar",
244
255
  ancestry: "shadcn",
245
256
  whenToUse:
246
- 'Permita escolher datas vendo o mês inteiro, como a data de uma entrega ou o período de um relatório. `mode` define a seleção — `single`, `multiple` ou `range` — e o formato de `selected` e `onSelect` (Date, Date[] ou { from, to }). `captionLayout="dropdown"` troca o título do mês por seletores de mês e ano, útil para chegar a um período distante; `numberOfMonths` mostra meses lado a lado e `disabled` impede a escolha de datas específicas. Os textos usam pt-BR por padrão. Para abrir o calendário a partir de um campo, ancore-o em um Popover.',
257
+ 'Permita escolher datas vendo o mês inteiro, como a data de uma entrega ou o período de um relatório. `mode` define a seleção — `single`, `multiple` ou `range` — e o formato de `selected` e `onSelect` (Date, Date[] ou { from, to }). `captionLayout="dropdown"` troca o título do mês por seletores de mês e ano, útil para chegar a um período distante; `numberOfMonths` mostra meses lado a lado e `disabled` impede a escolha de datas específicas. Os textos usam pt-BR por padrão. Para preencher uma data em um campo com digitação e calendário, use DateInput. Use Popover para composições próprias.',
247
258
  },
248
259
  carousel: {
249
260
  name: "carousel",
@@ -261,7 +272,7 @@ export const componentMeta = {
261
272
  name: "floating-layer",
262
273
  ancestry: "opus",
263
274
  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.",
275
+ '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
276
  },
266
277
  drawer: {
267
278
  name: "drawer",
@@ -388,7 +399,7 @@ export const componentMeta = {
388
399
  name: "action-list",
389
400
  ancestry: "opus",
390
401
  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.",
402
+ "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
403
  },
393
404
  "action-trigger": {
394
405
  name: "action-trigger",
@@ -406,7 +417,7 @@ export const componentMeta = {
406
417
  name: "page",
407
418
  ancestry: "opus",
408
419
  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.",
420
+ "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
421
  },
411
422
  presentation: {
412
423
  name: "presentation",