@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.
- package/CHANGELOG.md +123 -0
- package/bin/lib/check.mjs +109 -101
- package/bin/lib/copy.mjs +2973 -1801
- package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +52 -0
- package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +27 -2
- package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +142 -0
- package/docs/adr/0064-action-list-com-itens-e-fontes-externas.md +47 -0
- package/docs/adr/0065-presentation-composes-settings-sections.md +295 -0
- package/docs/adr/0066-civil-date-input-and-calendar-list.md +28 -0
- package/docs/adr/README.md +6 -0
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +21 -19
- package/registry/skills/build-opus-ui/references/evaluations.md +13 -11
- package/registry/skills/build-opus-ui/references/ui-patterns.md +24 -27
- package/src/core/presentation.ts +938 -32
- package/src/core/types.ts +2 -0
- package/src/ui/components/patterns/form.tsx +22 -1
- package/src/ui/components/patterns/grouped-list.ts +2 -2
- package/src/ui/components/patterns/list-calendar.tsx +129 -0
- package/src/ui/components/patterns/list.tsx +612 -382
- package/src/ui/components/patterns/page-heading-context.tsx +12 -5
- package/src/ui/components/patterns/page.tsx +227 -197
- package/src/ui/components/patterns/presentation-fields.tsx +246 -0
- package/src/ui/components/patterns/presentation-sections.tsx +494 -0
- package/src/ui/components/patterns/presentation.tsx +1142 -229
- package/src/ui/components/patterns/structural-header.tsx +23 -32
- package/src/ui/components/patterns/surface-anatomy.ts +27 -217
- package/src/ui/components/patterns/surface.tsx +19 -29
- package/src/ui/components/patterns/trigger.tsx +10 -2
- package/src/ui/components/primitives/calendar.tsx +39 -18
- package/src/ui/components/primitives/close-button.tsx +11 -3
- package/src/ui/components/primitives/command.tsx +2 -8
- package/src/ui/components/primitives/date-input.tsx +276 -0
- package/src/ui/components/primitives/detail.tsx +1 -1
- package/src/ui/components/primitives/dialog.tsx +19 -81
- package/src/ui/components/primitives/drawer.tsx +17 -79
- package/src/ui/components/primitives/field.tsx +1 -1
- package/src/ui/components/primitives/pagination.tsx +3 -2
- package/src/ui/components/primitives/table.tsx +1 -1
- package/src/ui/components/primitives/tabs.tsx +13 -2
- package/src/ui/docs/content/action-form.md +6 -1
- package/src/ui/docs/content/action-list.md +156 -68
- package/src/ui/docs/content/action-trigger.md +5 -0
- package/src/ui/docs/content/button.md +5 -6
- package/src/ui/docs/content/calendar-month-view.md +50 -0
- package/src/ui/docs/content/calendar.md +6 -1
- package/src/ui/docs/content/command.md +62 -50
- package/src/ui/docs/content/compositions.md +129 -459
- package/src/ui/docs/content/date-input.md +67 -0
- package/src/ui/docs/content/dialog.md +44 -112
- package/src/ui/docs/content/drawer.md +44 -103
- package/src/ui/docs/content/field.md +2 -1
- package/src/ui/docs/content/input.md +3 -0
- package/src/ui/docs/content/page.md +204 -412
- package/src/ui/docs/content/presentation.md +371 -23
- package/src/ui/docs/content/separator.md +1 -1
- package/src/ui/docs/content/surface.md +65 -120
- package/src/ui/docs/content/tokens.md +18 -4
- package/src/ui/docs/registry.tsx +38 -9
- package/src/ui/drivers/context.tsx +11 -4
- package/src/ui/drivers/react.tsx +27 -7
- package/src/ui/meta.ts +17 -6
- package/src/ui/react.tsx +41 -8
- 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={
|
|
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
|
-
|
|
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 `
|
|
55
|
-
Declare `
|
|
56
|
-
quando hospeda Surface. Seções sem moldura
|
|
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
|
-
##
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
332
|
-
|
|
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
|
|
391
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
|
444
|
-
|
|
445
|
-
|
|
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
|
|
456
|
-
| ------------- | ----------------------------------------------- |
|
|
457
|
-
| `as` | `'div' \| 'section' \| 'article' \| 'fieldset'` | `'div'`
|
|
458
|
-
| `asChild` | `boolean` | `false`
|
|
459
|
-
| `framed` | `boolean` | `false`
|
|
460
|
-
| `elevation` | `'none' \| 'raised'` | `'none'`
|
|
461
|
-
| `
|
|
462
|
-
| `
|
|
463
|
-
| `
|
|
464
|
-
| `
|
|
465
|
-
| `
|
|
466
|
-
| `
|
|
467
|
-
| `
|
|
468
|
-
| `
|
|
469
|
-
| `
|
|
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
|
|
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
|
|
539
|
-
|
|
|
540
|
-
| `children`
|
|
541
|
-
| `className` | `string`
|
|
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
|
|
548
|
-
|
|
|
549
|
-
| `children`
|
|
550
|
-
| `className` | `string`
|
|
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,
|
|
302
|
-
|
|
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
|
|
311
|
-
|
|
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
|
package/src/ui/docs/registry.tsx
CHANGED
|
@@ -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 =
|
|
156
|
-
|
|
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
|
-
{
|
|
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) =>
|
|
894
|
-
.
|
|
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(
|
|
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(
|
|
941
|
+
pages: g.pages.filter(
|
|
942
|
+
(p) => !["ui", "cycle", "upgrading"].includes(p.slug),
|
|
943
|
+
),
|
|
917
944
|
})),
|
|
918
945
|
})),
|
|
919
|
-
...UI_SECTIONS.map((section, index) =>
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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> = {}
|
package/src/ui/drivers/react.tsx
CHANGED
|
@@ -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
|
-
|
|
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: [
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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",
|