@softize/opus 23.0.0 → 24.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.
- package/CHANGELOG.md +81 -0
- package/bin/cli.mjs +2 -0
- package/bin/lib/check.mjs +241 -42
- package/bin/lib/copy.mjs +16 -34
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
- package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +3 -3
- package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +5 -6
- package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
- package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
- package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
- package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
- package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
- package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
- package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
- package/package.json +5 -1
- package/registry/skills/build-opus-ui/SKILL.md +11 -11
- package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
- package/registry/templates/app/src/App.tsx +24 -12
- package/src/ui/appearance.ts +149 -0
- package/src/ui/components/patterns/action-form-card.tsx +46 -33
- package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
- package/src/ui/components/patterns/confirm.tsx +3 -1
- package/src/ui/components/patterns/fill-region.ts +8 -7
- package/src/ui/components/patterns/form-dialog.tsx +1 -3
- package/src/ui/components/patterns/form.tsx +8 -7
- package/src/ui/components/patterns/list.tsx +15 -19
- package/src/ui/components/patterns/page.tsx +15 -11
- package/src/ui/components/patterns/pane.tsx +9 -240
- package/src/ui/components/patterns/presentation.tsx +14 -14
- package/src/ui/components/patterns/sidebar.tsx +7 -17
- package/src/ui/components/patterns/split.tsx +111 -59
- package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +50 -19
- package/src/ui/components/patterns/structural-media.tsx +26 -0
- package/src/ui/components/patterns/surface-anatomy.ts +71 -61
- package/src/ui/components/patterns/surface-assistant.tsx +2 -4
- package/src/ui/components/patterns/surface.tsx +620 -0
- package/src/ui/components/patterns/trigger.tsx +11 -9
- package/src/ui/components/primitives/ask.tsx +155 -83
- package/src/ui/components/primitives/button.tsx +11 -9
- package/src/ui/components/primitives/checkbox.tsx +8 -8
- package/src/ui/components/primitives/command.tsx +1 -1
- package/src/ui/components/primitives/composer.tsx +47 -45
- package/src/ui/components/primitives/detail.tsx +9 -2
- package/src/ui/components/primitives/dialog.tsx +60 -26
- package/src/ui/components/primitives/drawer.tsx +58 -12
- package/src/ui/components/primitives/icon-picker.tsx +90 -73
- package/src/ui/components/primitives/input-group.tsx +35 -28
- package/src/ui/components/primitives/input-otp.tsx +16 -16
- package/src/ui/components/primitives/input.tsx +20 -10
- package/src/ui/components/primitives/item.tsx +9 -12
- package/src/ui/components/primitives/menu.tsx +44 -65
- package/src/ui/components/primitives/metric-card.tsx +75 -59
- package/src/ui/components/primitives/radio-group.tsx +9 -9
- package/src/ui/components/primitives/select.tsx +344 -209
- package/src/ui/components/primitives/switch.tsx +10 -10
- package/src/ui/components/primitives/table.tsx +29 -24
- package/src/ui/components/primitives/tabs.tsx +4 -2
- package/src/ui/components/primitives/textarea.tsx +6 -6
- package/src/ui/components/primitives/toggle-group-context.ts +24 -0
- package/src/ui/components/primitives/toggle-group.tsx +116 -74
- package/src/ui/components/primitives/toggle.tsx +82 -16
- package/src/ui/docs/DocBrowser.tsx +77 -69
- package/src/ui/docs/content/action-form-card.md +9 -9
- package/src/ui/docs/content/action-list.md +76 -70
- package/src/ui/docs/content/action-view.md +33 -18
- package/src/ui/docs/content/button.md +1 -1
- package/src/ui/docs/content/communication.md +1 -1
- package/src/ui/docs/content/customization.md +64 -11
- package/src/ui/docs/content/detail.md +8 -6
- package/src/ui/docs/content/dialog.md +100 -58
- package/src/ui/docs/content/drawer.md +63 -19
- package/src/ui/docs/content/field.md +14 -15
- package/src/ui/docs/content/icon-tile.md +14 -13
- package/src/ui/docs/content/page.md +34 -26
- package/src/ui/docs/content/pane.md +41 -290
- package/src/ui/docs/content/presentation.md +3 -3
- package/src/ui/docs/content/separator.md +6 -6
- package/src/ui/docs/content/sidebar.md +68 -266
- package/src/ui/docs/content/skeleton.md +20 -18
- package/src/ui/docs/content/split.md +14 -14
- package/src/ui/docs/content/surface.md +513 -0
- package/src/ui/docs/content/tabs.md +20 -20
- package/src/ui/docs/content/toggle.md +117 -65
- package/src/ui/docs/content/tokens.md +124 -40
- package/src/ui/docs/registry.tsx +4 -6
- package/src/ui/index.ts +30 -27
- package/src/ui/meta.ts +15 -27
- package/src/ui/react.tsx +40 -43
- package/src/ui/theme.css +311 -64
- package/src/ui/components/patterns/content-header.tsx +0 -528
- package/src/ui/components/patterns/surface-media.tsx +0 -69
- package/src/ui/components/primitives/card.tsx +0 -191
- package/src/ui/docs/content/card.md +0 -292
- package/src/ui/docs/content/content.md +0 -275
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# ADR 0047 — Pane é somente uma unidade de layout do Split
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-10-01.
|
|
5
|
+
- **Substitui:** a anatomia visual de Pane definida pela ADR 0023.
|
|
6
|
+
- **Complementa:** ADR 0045, quanto ao uso de `Surface` para regiões visuais.
|
|
7
|
+
|
|
8
|
+
## Contexto
|
|
9
|
+
|
|
10
|
+
`Pane` nasceu como filho dimensionável de `Split`, mas passou a acumular uma segunda função:
|
|
11
|
+
oferecia header, body, footer, título, mídia, ações, divisores, inset e ritmo visual. Essa anatomia
|
|
12
|
+
duplicava `Surface` e fazia a aparência de uma coluna depender do componente que deveria apenas
|
|
13
|
+
distribuir espaço.
|
|
14
|
+
|
|
15
|
+
A duplicação ficou visível quando uma mesma região precisou funcionar em página, drawer e split.
|
|
16
|
+
Alterar o ritmo de todos os bodies incluiu `PaneBody`, embora a decisão vigente já fosse deixar a
|
|
17
|
+
superfície visual a cargo de `Surface` ou de um pattern especializado, como `Sidebar`.
|
|
18
|
+
|
|
19
|
+
## Decisão
|
|
20
|
+
|
|
21
|
+
`Pane` representa exclusivamente uma unidade espacial aceita por `Split`. Sua API pública mantém:
|
|
22
|
+
|
|
23
|
+
- `id`, para identidade e persistência do layout;
|
|
24
|
+
- `initialSize`, `minSize` e `maxSize`, para dimensionamento;
|
|
25
|
+
- `grow`, para ocupar o espaço remanescente;
|
|
26
|
+
- `className` e `children`, para o contêiner de layout.
|
|
27
|
+
|
|
28
|
+
`Pane` não aplica inset, fundo, borda, raio, divisor, orientação interna, rolagem nem anatomia de
|
|
29
|
+
conteúdo. As propriedades `inset` e `layout` são removidas.
|
|
30
|
+
|
|
31
|
+
Os slots `PaneHeader`, `PaneBody`, `PaneContent`, `PaneFooter`, `PaneMedia`, `PaneTitle`,
|
|
32
|
+
`PaneDescription` e `PaneActions` também são removidos, sem aliases de compatibilidade. O único
|
|
33
|
+
consumidor é migrado atomicamente:
|
|
34
|
+
|
|
35
|
+
- regiões visuais usam `Surface` e seus slots;
|
|
36
|
+
- barras laterais podem usar a anatomia própria de `Sidebar` ou conteúdo nativo;
|
|
37
|
+
- rolagem e organização interna pertencem ao filho inserido no pane.
|
|
38
|
+
|
|
39
|
+
## Consequências
|
|
40
|
+
|
|
41
|
+
- `Pane` e `Split` passam a ter uma única responsabilidade observável: distribuir espaço.
|
|
42
|
+
- `Surface` volta a ser a fonte única da anatomia visual compartilhada.
|
|
43
|
+
- Uma região que precise de rolagem deve declará-la no seu conteúdo ou na superfície interna.
|
|
44
|
+
- Consumidores antigos deixam de compilar até substituir os slots e remover `inset`/`layout`.
|
|
45
|
+
- O checker orienta a migração de símbolos e propriedades removidos.
|
|
46
|
+
|
|
47
|
+
## Alternativas descartadas
|
|
48
|
+
|
|
49
|
+
### Manter apenas PaneBody
|
|
50
|
+
|
|
51
|
+
Ainda deixaria `Pane` responsável por rolagem e ritmo, criando uma anatomia parcial concorrente com
|
|
52
|
+
`SurfaceBody` e com o conteúdo especializado.
|
|
53
|
+
|
|
54
|
+
### Manter os slots como aliases de Surface
|
|
55
|
+
|
|
56
|
+
Preservaria dois vocabulários para a mesma estrutura e permitiria que novas telas voltassem a
|
|
57
|
+
escolher a família errada. Como há um único consumidor migrado junto, a compatibilidade não traz
|
|
58
|
+
benefício proporcional.
|
|
59
|
+
|
|
60
|
+
### Incorporar a superfície visual ao Split
|
|
61
|
+
|
|
62
|
+
Misturaria distribuição espacial com apresentação e impediria panes deliberadamente invisíveis.
|
|
63
|
+
|
|
64
|
+
## Verificação
|
|
65
|
+
|
|
66
|
+
- a API pública exporta apenas `Pane`, `PaneProps` e `PaneSize` dessa família;
|
|
67
|
+
- `Pane` não contém tokens de padding, borda, fundo, raio, overflow ou anatomia flex interna;
|
|
68
|
+
- `Split` continua aceitando somente `Pane` como filho e preserva dimensionamento e resize;
|
|
69
|
+
- `opus check` reprova símbolos antigos e as propriedades removidas;
|
|
70
|
+
- busca nos consumidores não encontra nenhum slot `Pane*`, `inset` ou `layout` aplicado a `Pane`;
|
|
71
|
+
- testes renderizados cobrem split fixo, redimensionável e superfícies migradas.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# ADR 0048 — A intro do modal separa conteúdo e chrome
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-10-01.
|
|
5
|
+
- **Complementa:** ADR 0018, quanto à separação entre nome e contexto; ADR 0023, quanto à anatomia
|
|
6
|
+
de Dialog e Drawer; ADR 0046, quanto ao ritmo governado pelo body.
|
|
7
|
+
|
|
8
|
+
## Contexto
|
|
9
|
+
|
|
10
|
+
Dialog e Drawer podem ter uma barra persistente com navegação, ação global e fechamento. Nessa
|
|
11
|
+
composição, colocar o título do recurso na barra mistura chrome e conteúdo; colocá-lo solto no body
|
|
12
|
+
preserva o desenho, mas viola a anatomia pública e não nomeia a relação entre título e ações locais.
|
|
13
|
+
|
|
14
|
+
## Decisão
|
|
15
|
+
|
|
16
|
+
`DialogIntro` e `DrawerIntro` são o primeiro bloco opcional de seus respectivos bodies. Cada intro
|
|
17
|
+
exige o título da família e aceita `DialogIntroActions` ou `DrawerIntroActions`. O título usa a
|
|
18
|
+
escala principal da composição; o body continua responsável por inset, ritmo e rolagem.
|
|
19
|
+
|
|
20
|
+
As ações do header continuam sendo chrome da superfície. As ações da intro se relacionam ao título
|
|
21
|
+
e ao conteúdo. No formato compacto, título e ações permanecem no header e a intro é omitida.
|
|
22
|
+
|
|
23
|
+
## Consequências
|
|
24
|
+
|
|
25
|
+
- o mesmo conteúdo funciona em layout contínuo ou seccionado sem título órfão;
|
|
26
|
+
- o fechamento e a navegação podem permanecer estáveis enquanto a intro rola com o conteúdo;
|
|
27
|
+
- o checker exige intro dentro do body, título dentro de header ou intro e ações no slot correto;
|
|
28
|
+
- exemplos e consumidores deixam de reconstruir a linha com `div` e classes locais.
|
|
29
|
+
|
|
30
|
+
## Verificação
|
|
31
|
+
|
|
32
|
+
- testes renderizados cobrem os dois modais e a escala do título;
|
|
33
|
+
- `opus check` aceita as composições canônicas e reprova intro órfã ou sem título;
|
|
34
|
+
- a documentação mostra as formas com intro e compacta.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softize/opus",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "24.0.0",
|
|
4
4
|
"description": "End-to-end action protocol for TypeScript. Single package with subpath exports (core + adapters).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -205,6 +205,10 @@
|
|
|
205
205
|
"types": "./src/ui/meta.ts",
|
|
206
206
|
"default": "./src/ui/meta.ts"
|
|
207
207
|
},
|
|
208
|
+
"./ui/appearance": {
|
|
209
|
+
"types": "./src/ui/appearance.ts",
|
|
210
|
+
"default": "./src/ui/appearance.ts"
|
|
211
|
+
},
|
|
208
212
|
"./ui/docs": {
|
|
209
213
|
"types": "./src/ui/docs/index.ts",
|
|
210
214
|
"default": "./src/ui/docs/index.ts"
|
|
@@ -46,26 +46,26 @@ existentes, mantendo navegação observável e componentes reutilizáveis sem la
|
|
|
46
46
|
8. Compor superfícies pela gramática estrutural do catálogo: `PageHeader` é a única introdução
|
|
47
47
|
estrutural e reúne navegação contextual opcional, título e ações; `PageBody` contém o conteúdo.
|
|
48
48
|
Na composição explícita, declare exatamente um heading principal com `PageTitle` no header,
|
|
49
|
-
`
|
|
50
|
-
conhecidas pela aplicação, mas não cria uma barra visual. `
|
|
51
|
-
Drawer e Pane usam seus
|
|
52
|
-
|
|
53
|
-
`
|
|
49
|
+
`Surface level={1}` ou um `PageState` ativo no body. `PageShell` pode fornecer navegação e ações
|
|
50
|
+
conhecidas pela aplicação, mas não cria uma barra visual. `Surface` contém `SurfaceHeader` e
|
|
51
|
+
`SurfaceBody`; Drawer e Pane usam seus respectivos `*Body`. Para o caso direto, usar a sintaxe
|
|
52
|
+
abreviada de `Page` (`title`, `actions`) ou de `Surface` (`title`, `description`, `actions` e
|
|
53
|
+
`count`); não misturá-la com o
|
|
54
54
|
header explícito. O título da página não carrega contador. Ajustar o
|
|
55
55
|
nível do heading pela hierarquia semântica, não pelo destaque visual. O `Page` mantém seu teto
|
|
56
56
|
centralizado padrão de `80rem`.
|
|
57
|
-
Em `
|
|
58
|
-
body, como `DetailGroup` com divisores, lista ou tabela; não reconstruir
|
|
59
|
-
recorte por classes no consumidor.
|
|
60
|
-
|
|
57
|
+
Em `Surface framed`, usar `layout="sectioned"` e `SurfaceBody flush` quando o filho direto já
|
|
58
|
+
controlar o recuo do body, como `DetailGroup` com divisores, lista ou tabela; não reconstruir
|
|
59
|
+
divisor, padding ou recorte por classes no consumidor. Em grupos de formulário,
|
|
60
|
+
`as="fieldset"` transforma o header em `legend` nativa sem variante visual paralela.
|
|
61
61
|
9. Manter margem e posicionamento no consumidor; componente reutilizável controla apenas
|
|
62
62
|
seu interior.
|
|
63
63
|
10. Não definir a fonte raiz em uma biblioteca ou componente. O navegador e a aplicação são
|
|
64
64
|
responsáveis por `font-size` em `html`; medidas escaláveis da UI usam `rem` ou a escala
|
|
65
65
|
relativa do Tailwind. Reservar `px` a hairlines e compensações presas à geometria da borda,
|
|
66
66
|
com justificativa e cobertura explícitas.
|
|
67
|
-
11. Em uma página centrada em coleção, compor `
|
|
68
|
-
extremos opostos do `
|
|
67
|
+
11. Em uma página centrada em coleção, compor `Surface + ActionList`: título e criação ficam em
|
|
68
|
+
extremos opostos do `SurfaceHeader`; a criação usa botão textual `default`, sem ícone, com
|
|
69
69
|
`Criar recurso`. Busca, filtros, atualização, estados e paginação permanecem no `ActionList`.
|
|
70
70
|
Reservar `toolbarActions` a operações ligadas ao recorte atual.
|
|
71
71
|
12. Evoluir um pattern compartilhado apenas quando a recorrência e o contrato estiverem claros.
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
- Execução estrutural: montar uma página de relatório isolada com `Page > PageHeader + PageBody`,
|
|
7
7
|
outra em `PageShell` com navegação contextual acima de `Page > PageHeader + PageBody`, garantindo
|
|
8
8
|
exatamente um `PageTitle` no header, e uma coleção nomeada com
|
|
9
|
-
`
|
|
10
|
-
`
|
|
9
|
+
`Surface > SurfaceHeader + SurfaceBody > ActionList`. Provar que criação fica em
|
|
10
|
+
`SurfaceActions`, no extremo oposto ao título, como botão textual `default` sem ícone e com
|
|
11
11
|
`Criar recurso`, enquanto uma operação dependente do recorte usa `toolbarActions`; incluir
|
|
12
12
|
`ActionFilterBar` separado do renderer, `ItemGroup` para
|
|
13
13
|
uma coleção secundária e um `ActionFormDialog`; provar teto padrão de `80rem`, hierarquia por
|
|
@@ -18,13 +18,14 @@
|
|
|
18
18
|
- Execução roteável: declarar Page, criação e edição com `Presentation.route`; provar deep link,
|
|
19
19
|
hidratação do registro, fechamento para a lista e projeção integral no manifest.
|
|
20
20
|
- Execução abreviada: montar outra página com `<Page title actions>` e uma seção com
|
|
21
|
-
`<
|
|
21
|
+
`<Surface title description actions>`, provando que ambas produzem a mesma anatomia e que o lint
|
|
22
22
|
rejeita a mistura entre props abreviadas e headers explícitos.
|
|
23
|
-
- Execução de forma: compor uma tabela dentro de
|
|
23
|
+
- Execução de forma: compor uma tabela dentro de `Surface` sem moldura duplicada, manter a moldura
|
|
24
24
|
estrutural standalone em `rounded-lg` e os controles internos em `rounded-md`.
|
|
25
|
-
- Execução de
|
|
26
|
-
`DetailGroup` com divisores; provar header com divisor,
|
|
27
|
-
consumidor que reconstruam essa anatomia.
|
|
25
|
+
- Execução de Surface seccionada: compor `Surface framed layout="sectioned"` com mídia e
|
|
26
|
+
`DetailGroup` com divisores; provar header com divisor, `SurfaceBody flush` sem recuo e ausência
|
|
27
|
+
de classes no consumidor que reconstruam essa anatomia. Provar `as="fieldset"` com `legend`
|
|
28
|
+
nativa sem `frameVariant`.
|
|
28
29
|
- Reprova: criar uma casca `bg-card` que herda o texto global ou introduzir `rounded-widget`
|
|
29
30
|
quando a escala `rounded-*` atende ao papel.
|
|
30
31
|
- Reprova: trocar automaticamente o radius de um controle quando apenas seu tamanho muda ou
|
|
@@ -32,8 +33,8 @@
|
|
|
32
33
|
- Reprova: reconstruir manualmente o container de página, usar `Empty` tracejado como vazio de
|
|
33
34
|
tabela, alinhar toda coluna numérica à direita por inferência ou destacar “Cancelar” como ação
|
|
34
35
|
primária em modal.
|
|
35
|
-
- Reprova: deixar `
|
|
36
|
-
explícita, usar `
|
|
36
|
+
- Reprova: deixar `SurfaceHeader` fora de `Surface`, omitir `PageBody`/`SurfaceBody` na composição
|
|
37
|
+
explícita, usar `Card*` ou `Content*` em código novo ou tratar `DialogContent` como Body.
|
|
37
38
|
- Execução de dicionários: montar a lista de clientes com “Pessoa física/Empresa” e
|
|
38
39
|
“Prospect/Cliente”; provar que o tipo ocupa a coluna “Tipo” como classificação (badge `outline`
|
|
39
40
|
com os ícones declarados `user` e `building`), que o estágio ocupa a coluna “Estágio” como badge
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
lista; não reconstruir essa decisão com condicionais no componente da página.
|
|
10
10
|
- `Page` fornece o `<main>` e o container centralizado com teto padrão de `80rem`. Sua forma
|
|
11
11
|
explícita exige `PageBody`, exatamente um heading principal e aceita `PageHeader` e `PageFooter`.
|
|
12
|
-
Declare esse heading com `PageTitle` no header, `
|
|
12
|
+
Declare esse heading com `PageTitle` no header, `Surface level={1}` ou um `PageState` ativo no
|
|
13
13
|
body. `PageHeader` é a única introdução estrutural e reúne navegação, título e ações. `title` e `actions` no
|
|
14
14
|
próprio `Page` são a abreviação para o caso direto.
|
|
15
15
|
Não misturar as duas formas. Alterar `className` apenas quando a superfície tiver uma necessidade
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
ações somente com ícone de chrome usam `icon`. O tamanho `sm` fica para ações operacionais em
|
|
27
27
|
toolbar, seção ou coleção densa, e `xs`/`icon-xs` para ações internas de linha ou célula. Contexto
|
|
28
28
|
e variante resolvem a hierarquia visual sem alterar essa medida. Em uma coleção, a ação de
|
|
29
|
-
criar usa botão textual `default`, sem ícone, no extremo oposto do `
|
|
30
|
-
|
|
29
|
+
criar usa botão textual `default`, sem ícone, no extremo oposto do `SurfaceHeader`. Não monte
|
|
30
|
+
cabeçalho paralelo, portal ou seletor global para reconstruir essa composição.
|
|
31
31
|
- `PageState` substitui todo o conteúdo principal quando a página carrega, falha ou está vazia. Na
|
|
32
32
|
forma curta ele pode ser escrito como filho direto de `Page`, que cria o `PageBody`; na forma
|
|
33
33
|
explícita, fica sozinho dentro de `PageBody`. Nos três estados ativos, o cabeçalho da Page
|
|
@@ -37,22 +37,23 @@
|
|
|
37
37
|
`role="alert"`, usa a mesma composição central e sem moldura dos demais estados e apresenta a
|
|
38
38
|
recuperação como botão `outline` textual. Estados de seção ou coleção continuam em `DataState`,
|
|
39
39
|
`ActionView`, `ActionList` ou `Alert`; não elevar uma falha parcial a estado da página.
|
|
40
|
-
- `
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
direto e não podem ser misturados ao header explícito.
|
|
44
|
-
no extremo oposto ao título; a criação usa botão textual `default`, sem ícone, com
|
|
40
|
+
- `Surface` delimita uma região e segue a anatomia `Surface > SurfaceHeader (SurfaceMedia,
|
|
41
|
+
SurfaceTitle, SurfaceDescription, SurfaceMeta, SurfaceActions) + SurfaceBody + SurfaceFooter`.
|
|
42
|
+
Os slots nunca ficam soltos. `title`, `description`, `count`, `media` e `actions` são a abreviação
|
|
43
|
+
para o caso direto e não podem ser misturados ao header explícito. Em `level={1}`, ações da
|
|
44
|
+
coleção ficam no extremo oposto ao título; a criação usa botão textual `default`, sem ícone, com
|
|
45
|
+
`Criar recurso`.
|
|
45
46
|
O diálogo de criação usa `Criar recurso` e o de edição, `Editar recurso`. `level` preserva a
|
|
46
|
-
hierarquia semântica do heading. `
|
|
47
|
-
direto já possui recuo interno, como `DetailGroup` com
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
- Uma página centrada em coleção compõe `Content + ActionList`. O `ContentHeader` recebe título e
|
|
47
|
+
hierarquia semântica do heading. `Surface framed` aceita `layout="sectioned"`; declare
|
|
48
|
+
`SurfaceBody flush` quando o filho direto já possui recuo interno, como `DetailGroup` com
|
|
49
|
+
divisores, lista ou tabela. `as="fieldset"` cria a `legend` nativa sem variante de moldura.
|
|
50
|
+
- Uma página centrada em coleção compõe `Surface + ActionList`. O `SurfaceHeader` recebe título e
|
|
51
51
|
criação; o `ActionList` mantém busca, filtros, atualização, resultados, estados e paginação.
|
|
52
52
|
`toolbarActions` fica reservado a operações que dependem do recorte atual.
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
- `Surface` e Drawer nomeiam a região principal como `SurfaceBody` e `DrawerBody`. `Pane` é apenas
|
|
54
|
+
uma unidade dimensionável de `Split` e não possui anatomia interna. `*Content` permanece
|
|
55
|
+
reservado a raízes técnicas ou painéis cujo papel não é o corpo de uma estrutura, como
|
|
56
|
+
`DialogContent`, `PopoverContent` e `TabsPanel`.
|
|
56
57
|
- Componentes compartilhados não impõem margem externa; páginas e shells compõem layout.
|
|
57
58
|
- A UI assume layout desktop fixo e largura mínima suportada de `64rem`. Não introduzir
|
|
58
59
|
breakpoints de viewport ou container queries isoladas; uma futura responsividade precisa começar
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import type { z } from
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
Badge,
|
|
4
|
+
DataState,
|
|
5
|
+
Surface,
|
|
6
|
+
useListAction,
|
|
7
|
+
} from "@softize/opus/ui/react";
|
|
8
|
+
import { Task, taskList } from "./domains/tasks/actions/list.ts";
|
|
4
9
|
|
|
5
|
-
type TaskItem = z.infer<typeof Task
|
|
10
|
+
type TaskItem = z.infer<typeof Task>;
|
|
6
11
|
|
|
7
12
|
/** Página inicial do esqueleto: prova a fundação (tema + componentes do Opus) e o
|
|
8
13
|
* domínio-exemplo. Troque tudo — este arquivo é seu. */
|
|
@@ -10,14 +15,16 @@ export function App(): React.ReactElement {
|
|
|
10
15
|
// O front consome o CONTRATO via /api — o plugin opusDesign (vite.config.ts)
|
|
11
16
|
// serve o backend no próprio dev server: dev normal executa o handler real;
|
|
12
17
|
// `pnpm dev:design` responde com dado gerado a partir do contrato, isolado de qualquer backend.
|
|
13
|
-
const { items, isLoading, error, refetch } =
|
|
18
|
+
const { items, isLoading, error, refetch } =
|
|
19
|
+
useListAction<TaskItem>(taskList);
|
|
14
20
|
|
|
15
21
|
return (
|
|
16
22
|
<main className="flex min-h-full items-center justify-center bg-background p-6">
|
|
17
|
-
<
|
|
18
|
-
<h1 className="mb-1 text-lg font-semibold">{
|
|
23
|
+
<Surface framed elevation="raised" className="w-full max-w-md p-6">
|
|
24
|
+
<h1 className="mb-1 text-lg font-semibold">{"{{APP_NAME}}"}</h1>
|
|
19
25
|
<p className="mb-4 text-sm text-muted-foreground">
|
|
20
|
-
Esqueleto criado pelo opus create. A spec vive nas declarações do
|
|
26
|
+
Esqueleto criado pelo opus create. A spec vive nas declarações do
|
|
27
|
+
domínio.
|
|
21
28
|
</p>
|
|
22
29
|
{/* Carregando, erro e vazio saem do DataState; a lista só cuida dos itens. */}
|
|
23
30
|
<DataState
|
|
@@ -29,14 +36,19 @@ export function App(): React.ReactElement {
|
|
|
29
36
|
>
|
|
30
37
|
<ul className="space-y-2">
|
|
31
38
|
{items.map((t) => (
|
|
32
|
-
<li
|
|
39
|
+
<li
|
|
40
|
+
key={t.id}
|
|
41
|
+
className="flex items-center justify-between gap-2 text-sm"
|
|
42
|
+
>
|
|
33
43
|
<span>{t.title}</span>
|
|
34
|
-
<Badge context={t.done ?
|
|
44
|
+
<Badge context={t.done ? "success" : "neutral"}>
|
|
45
|
+
{t.done ? "Feita" : "Aberta"}
|
|
46
|
+
</Badge>
|
|
35
47
|
</li>
|
|
36
48
|
))}
|
|
37
49
|
</ul>
|
|
38
50
|
</DataState>
|
|
39
|
-
</
|
|
51
|
+
</Surface>
|
|
40
52
|
</main>
|
|
41
|
-
)
|
|
53
|
+
);
|
|
42
54
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/ui/appearance — personalização de aparência sem React (ADR 0044).
|
|
3
|
+
*
|
|
4
|
+
* O tema deriva a escala neutra de quatro eixos inteiros (0 a 100) declarados por modo em
|
|
5
|
+
* `theme.css`: `--appearance-light-*` e `--appearance-dark-*`. Este módulo publica os tipos, os
|
|
6
|
+
* presets oficiais e o helper que converte overrides parciais nas custom properties que o
|
|
7
|
+
* consumidor aplica onde quiser (em geral, inline em `<html>`).
|
|
8
|
+
*
|
|
9
|
+
* Reset não pertence ao Opus UI: um eixo sem override vira `null`, e o consumidor remove a
|
|
10
|
+
* propriedade para voltar ao cascade — ou seja, ao preset do tema ou ao CSS do próprio app.
|
|
11
|
+
* Persistência e interface de ajuste também ficam com o consumidor.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type AppearanceMode = "light" | "dark";
|
|
15
|
+
|
|
16
|
+
export const appearanceModes = [
|
|
17
|
+
"light",
|
|
18
|
+
"dark",
|
|
19
|
+
] as const satisfies readonly AppearanceMode[];
|
|
20
|
+
|
|
21
|
+
export type AppearanceAxis =
|
|
22
|
+
"colorIntensity" | "contrast" | "surfaceBrightness" | "layerContrast";
|
|
23
|
+
|
|
24
|
+
/** Ordem canônica dos eixos: a mesma usada em documentação, presets e custom properties. */
|
|
25
|
+
export const appearanceAxes = [
|
|
26
|
+
"colorIntensity",
|
|
27
|
+
"contrast",
|
|
28
|
+
"surfaceBrightness",
|
|
29
|
+
"layerContrast",
|
|
30
|
+
] as const satisfies readonly AppearanceAxis[];
|
|
31
|
+
|
|
32
|
+
/** Valor completo de um modo: todo eixo é um inteiro entre `APPEARANCE_MIN` e `APPEARANCE_MAX`. */
|
|
33
|
+
export type AppearanceSettings = Readonly<Record<AppearanceAxis, number>>;
|
|
34
|
+
|
|
35
|
+
/** Override parcial: eixo ausente (ou não finito) segue o cascade em vez de ser reescrito. */
|
|
36
|
+
export type AppearanceOverrides = Partial<
|
|
37
|
+
Record<AppearanceAxis, number | null | undefined>
|
|
38
|
+
>;
|
|
39
|
+
|
|
40
|
+
export const APPEARANCE_MIN = 0;
|
|
41
|
+
export const APPEARANCE_MAX = 100;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Presets oficiais por modo — reproduzem exatamente os valores do tema sem personalização.
|
|
45
|
+
* Intensidade, contraste e contraste entre camadas partem de zero: o preset é o piso, e o ajuste
|
|
46
|
+
* só acrescenta cor ou separação. No claro, a superfície elevada já é branca; o brilho parte do
|
|
47
|
+
* máximo e só pode diminuir. No escuro, o brilho fica no meio para permitir um escuro mais
|
|
48
|
+
* profundo ou mais suave.
|
|
49
|
+
*/
|
|
50
|
+
export const appearancePresets: Readonly<
|
|
51
|
+
Record<AppearanceMode, AppearanceSettings>
|
|
52
|
+
> = {
|
|
53
|
+
light: {
|
|
54
|
+
colorIntensity: 0,
|
|
55
|
+
contrast: 0,
|
|
56
|
+
surfaceBrightness: 100,
|
|
57
|
+
layerContrast: 0,
|
|
58
|
+
},
|
|
59
|
+
dark: {
|
|
60
|
+
colorIntensity: 0,
|
|
61
|
+
contrast: 0,
|
|
62
|
+
surfaceBrightness: 50,
|
|
63
|
+
layerContrast: 0,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const AXIS_PROPERTY: Record<AppearanceAxis, string> = {
|
|
68
|
+
colorIntensity: "color-intensity",
|
|
69
|
+
contrast: "contrast",
|
|
70
|
+
surfaceBrightness: "surface-brightness",
|
|
71
|
+
layerContrast: "layer-contrast",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type AppearanceCustomProperty =
|
|
75
|
+
`--appearance-${AppearanceMode}-${string}`;
|
|
76
|
+
|
|
77
|
+
/** Nome da custom property que `theme.css` lê para um eixo de um modo. */
|
|
78
|
+
export function appearanceCustomProperty(
|
|
79
|
+
mode: AppearanceMode,
|
|
80
|
+
axis: AppearanceAxis,
|
|
81
|
+
): AppearanceCustomProperty {
|
|
82
|
+
return `--appearance-${mode}-${AXIS_PROPERTY[axis]}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Normaliza um valor de eixo: arredonda para inteiro e limita ao intervalo. Retorna `undefined`
|
|
87
|
+
* para o que não é um número finito — o consumidor pode ter lido um valor corrompido de
|
|
88
|
+
* persistência, e a resposta correta é seguir o cascade, não travar a interface.
|
|
89
|
+
*/
|
|
90
|
+
export function normalizeAppearanceValue(value: unknown): number | undefined {
|
|
91
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
92
|
+
return Math.min(APPEARANCE_MAX, Math.max(APPEARANCE_MIN, Math.round(value)));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Preset do modo com os overrides válidos aplicados por cima. */
|
|
96
|
+
export function resolveAppearance(
|
|
97
|
+
mode: AppearanceMode,
|
|
98
|
+
overrides: AppearanceOverrides = {},
|
|
99
|
+
): AppearanceSettings {
|
|
100
|
+
const preset = appearancePresets[mode];
|
|
101
|
+
const resolved = { ...preset } as Record<AppearanceAxis, number>;
|
|
102
|
+
for (const axis of appearanceAxes) {
|
|
103
|
+
const value = normalizeAppearanceValue(overrides[axis]);
|
|
104
|
+
if (value !== undefined) resolved[axis] = value;
|
|
105
|
+
}
|
|
106
|
+
return resolved;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Converte overrides parciais nas custom properties do modo. Todo eixo aparece no resultado:
|
|
111
|
+
* com valor normalizado quando há override e `null` quando não há, para o consumidor remover a
|
|
112
|
+
* propriedade e deixar o cascade valer. Assim, o mesmo mapa serve para aplicar e para desfazer.
|
|
113
|
+
*/
|
|
114
|
+
export function appearanceCustomProperties(
|
|
115
|
+
mode: AppearanceMode,
|
|
116
|
+
overrides: AppearanceOverrides = {},
|
|
117
|
+
): Record<AppearanceCustomProperty, string | null> {
|
|
118
|
+
const properties = {} as Record<AppearanceCustomProperty, string | null>;
|
|
119
|
+
for (const axis of appearanceAxes) {
|
|
120
|
+
const value = normalizeAppearanceValue(overrides[axis]);
|
|
121
|
+
properties[appearanceCustomProperty(mode, axis)] =
|
|
122
|
+
value === undefined ? null : String(value);
|
|
123
|
+
}
|
|
124
|
+
return properties;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Alvo mínimo para aplicar propriedades: `CSSStyleDeclaration` satisfaz, e um stub de teste também. */
|
|
128
|
+
export interface AppearanceStyleTarget {
|
|
129
|
+
setProperty(name: string, value: string): void;
|
|
130
|
+
removeProperty(name: string): string | void;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Aplica os overrides de um modo num alvo de estilo. Eixos sem override são removidos, então
|
|
135
|
+
* chamar sem overrides devolve o elemento ao cascade — é o reset, feito pelo consumidor, sem
|
|
136
|
+
* estado no Opus.
|
|
137
|
+
*/
|
|
138
|
+
export function applyAppearanceCustomProperties(
|
|
139
|
+
target: AppearanceStyleTarget,
|
|
140
|
+
mode: AppearanceMode,
|
|
141
|
+
overrides: AppearanceOverrides = {},
|
|
142
|
+
): void {
|
|
143
|
+
for (const [name, value] of Object.entries(
|
|
144
|
+
appearanceCustomProperties(mode, overrides),
|
|
145
|
+
)) {
|
|
146
|
+
if (value === null) target.removeProperty(name);
|
|
147
|
+
else target.setProperty(name, value);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* <ActionFormCard /> — o ActionForm dentro de
|
|
3
|
-
* pra estruturar uma seção da página como painel. Segue o
|
|
4
|
-
* modal): sem divisor nem faixa
|
|
5
|
-
*
|
|
2
|
+
* <ActionFormCard /> — o ActionForm dentro de uma Surface elevada (header / conteúdo /
|
|
3
|
+
* rodapé), pra estruturar uma seção da página como painel. Segue o layout contínuo (não a
|
|
4
|
+
* chrome de modal): sem divisor nem faixa. Pra form em overlay, use o ActionFormDialog; pra
|
|
5
|
+
* form cru (sem chrome), o ActionForm direto.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
Card,
|
|
9
|
-
CardBody,
|
|
10
|
-
CardDescription,
|
|
11
|
-
CardFooter,
|
|
12
|
-
CardHeader,
|
|
13
|
-
CardTitle,
|
|
14
|
-
} from "../primitives/card.tsx";
|
|
15
7
|
import { ActionForm, type ActionFormProps } from "./form.tsx";
|
|
16
8
|
import type { HeadingLevel } from "./heading-level.ts";
|
|
9
|
+
import {
|
|
10
|
+
Surface,
|
|
11
|
+
SurfaceBody,
|
|
12
|
+
SurfaceDescription,
|
|
13
|
+
SurfaceFooter,
|
|
14
|
+
SurfaceHeader,
|
|
15
|
+
SurfaceTitle,
|
|
16
|
+
} from "./surface.tsx";
|
|
17
17
|
import { ButtonGroup } from "../primitives/button-group.tsx";
|
|
18
18
|
|
|
19
19
|
export interface ActionFormCardProps<
|
|
@@ -23,49 +23,62 @@ export interface ActionFormCardProps<
|
|
|
23
23
|
/** Título do header do card. Sem ele, o card começa direto no conteúdo. */
|
|
24
24
|
title?: string;
|
|
25
25
|
/**
|
|
26
|
-
* Nível semântico do título (`h1`–`h6`), repassado
|
|
27
|
-
*
|
|
28
|
-
* a do Card.
|
|
26
|
+
* Nível semântico do título (`h1`–`h6`), repassado à `Surface` como `level`. Sem ele
|
|
27
|
+
* vale `h3`: o card costuma ser uma seção dentro de uma página que já tem `h1` e `h2`.
|
|
29
28
|
*/
|
|
30
29
|
titleLevel?: HeadingLevel;
|
|
31
|
-
/** Subtítulo opcional, abaixo do título. */
|
|
30
|
+
/** Subtítulo opcional, abaixo do título. Exige `title`. */
|
|
32
31
|
description?: string;
|
|
33
|
-
/** Classes
|
|
32
|
+
/** Classes da superfície. `className` vai pro <form>. */
|
|
34
33
|
cardClassName?: string;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
export function ActionFormCard<TInput extends Record<string, unknown>, TData>({
|
|
38
37
|
title,
|
|
39
|
-
titleLevel,
|
|
38
|
+
titleLevel = 3,
|
|
40
39
|
description,
|
|
41
40
|
cardClassName,
|
|
42
41
|
...rest
|
|
43
42
|
}: ActionFormCardProps<TInput, TData>) {
|
|
43
|
+
if (description !== undefined && title === undefined) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
"ActionFormCard exige title quando description é informada.",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
44
48
|
return (
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)}
|
|
54
|
-
</CardHeader>
|
|
55
|
-
)}
|
|
49
|
+
<Surface
|
|
50
|
+
framed
|
|
51
|
+
elevation="raised"
|
|
52
|
+
level={titleLevel}
|
|
53
|
+
className={cardClassName}
|
|
54
|
+
>
|
|
55
|
+
{/* As três regiões são irmãs dentro do <form>: o ritmo e o respiro de cada uma
|
|
56
|
+
derivam da vizinhança, e os botões precisam ficar dentro do form. */}
|
|
56
57
|
<ActionForm
|
|
57
58
|
{...rest}
|
|
58
|
-
body={(fields) =>
|
|
59
|
+
body={(fields) => (
|
|
60
|
+
<>
|
|
61
|
+
{title !== undefined && (
|
|
62
|
+
<SurfaceHeader>
|
|
63
|
+
<SurfaceTitle>{title}</SurfaceTitle>
|
|
64
|
+
{description !== undefined && (
|
|
65
|
+
<SurfaceDescription>{description}</SurfaceDescription>
|
|
66
|
+
)}
|
|
67
|
+
</SurfaceHeader>
|
|
68
|
+
)}
|
|
69
|
+
<SurfaceBody>{fields}</SurfaceBody>
|
|
70
|
+
</>
|
|
71
|
+
)}
|
|
59
72
|
footer={(actions) => (
|
|
60
|
-
<
|
|
73
|
+
<SurfaceFooter>
|
|
61
74
|
{rest.onCancel === undefined ? (
|
|
62
75
|
actions
|
|
63
76
|
) : (
|
|
64
77
|
<ButtonGroup mode="spaced">{actions}</ButtonGroup>
|
|
65
78
|
)}
|
|
66
|
-
</
|
|
79
|
+
</SurfaceFooter>
|
|
67
80
|
)}
|
|
68
81
|
/>
|
|
69
|
-
</
|
|
82
|
+
</Surface>
|
|
70
83
|
);
|
|
71
84
|
}
|
|
@@ -86,14 +86,12 @@ export function ActionListDialog<
|
|
|
86
86
|
</DialogHeader>
|
|
87
87
|
<DialogBody>
|
|
88
88
|
{intro !== undefined && (
|
|
89
|
-
<div id={introId}
|
|
90
|
-
{intro}
|
|
91
|
-
</div>
|
|
89
|
+
<div id={introId}>{intro}</div>
|
|
92
90
|
)}
|
|
93
91
|
{(note !== undefined || actions !== undefined) && (
|
|
94
92
|
<div
|
|
95
93
|
data-slot="action-list-dialog-toolbar"
|
|
96
|
-
className="
|
|
94
|
+
className="flex min-h-8 items-center justify-between gap-4"
|
|
97
95
|
>
|
|
98
96
|
<div className="min-w-0 text-xs text-muted-foreground/70">{note}</div>
|
|
99
97
|
{actions}
|
|
@@ -284,7 +284,9 @@ export function DialogHost(): React.ReactElement {
|
|
|
284
284
|
<DialogTitle>{current.title}</DialogTitle>
|
|
285
285
|
</DialogHeader>
|
|
286
286
|
{current.body !== undefined && (
|
|
287
|
-
<DialogBody id={bodyId}>
|
|
287
|
+
<DialogBody id={bodyId}>
|
|
288
|
+
<div>{current.body}</div>
|
|
289
|
+
</DialogBody>
|
|
288
290
|
)}
|
|
289
291
|
{current.kind === "prompt" && (
|
|
290
292
|
<Input
|