@softize/opus 23.0.0 → 24.0.1
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 +99 -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/docs/adr/0049-inline-navigation-groups-with-the-heading.md +95 -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} +90 -27
- 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/compositions.md +479 -0
- 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 +105 -60
- package/src/ui/docs/content/drawer.md +68 -22
- 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 +44 -34
- package/src/ui/docs/content/pane.md +42 -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 +10 -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.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# ADR 0049 — Navegação inline forma um grupo com o título
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-10-01.
|
|
5
|
+
- **Complementa:** ADR 0023, quanto à linha do cabeçalho de Dialog e Drawer; ADR 0048, quanto ao
|
|
6
|
+
formato compacto, com título no cabeçalho.
|
|
7
|
+
|
|
8
|
+
## Contexto
|
|
9
|
+
|
|
10
|
+
`StructuralHeader` monta a linha do cabeçalho de Dialog e Drawer com `leadingPlacement="inline"`:
|
|
11
|
+
a navegação abre a linha, a mídia e o título vêm em seguida, as ações ficam no extremo oposto e o
|
|
12
|
+
botão de fechar entra por último. Até a 24.0.0, todos esses nós eram irmãos da mesma linha flex e
|
|
13
|
+
recebiam o intervalo da família, que em Dialog e Drawer é `gap-4` (`1rem`).
|
|
14
|
+
|
|
15
|
+
Com um único intervalo, o voltar ou o recolher ficava tão longe do título quanto o título ficava das
|
|
16
|
+
ações. A proximidade não distinguia duas relações diferentes:
|
|
17
|
+
|
|
18
|
+
- a navegação diz de onde a pessoa veio ou como sair da etapa, e só faz sentido lida junto com o
|
|
19
|
+
nome da tela;
|
|
20
|
+
- as ações operam sobre a superfície inteira e formam um conjunto próprio, com ritmo interno denso
|
|
21
|
+
(`gap-2`, de `surfaceActionsClasses`).
|
|
22
|
+
|
|
23
|
+
A ADR 0048 fixou a escolha entre a intro e o formato compacto, em que o título permanece no
|
|
24
|
+
cabeçalho. A página **Composições** passou a mostrar as duas formas lado a lado, e a distância
|
|
25
|
+
uniforme ficou evidente no Drawer compacto, em que o recolher do painel precede o título.
|
|
26
|
+
|
|
27
|
+
## Decisão
|
|
28
|
+
|
|
29
|
+
Quando um `StructuralHeader` com `leadingPlacement="inline"` recebe navegação e heading, os dois
|
|
30
|
+
formam um grupo contextual:
|
|
31
|
+
|
|
32
|
+
1. Um wrapper `data-slot="<slot>-header-context"` (`flex min-w-0 flex-1 items-center gap-2`)
|
|
33
|
+
envolve a navegação e o heading. O intervalo entre eles passa a ser `gap-2` (`0.5rem`).
|
|
34
|
+
2. As ações e o botão de fechar ficam fora do grupo, como irmãos dele na linha
|
|
35
|
+
`<slot>-header-row`, e continuam separados pelo intervalo da família (`gap-4` em Dialog e
|
|
36
|
+
Drawer). O ritmo interno das ações não muda.
|
|
37
|
+
3. O grupo ocupa a folga (`flex-1`) e aceita compressão (`min-w-0`): as ações seguem no extremo
|
|
38
|
+
oposto e o título continua truncando quando não cabe.
|
|
39
|
+
4. Quando há mídia na linha, mídia e heading ficam num wrapper `<slot>-header-identity` que
|
|
40
|
+
reaplica as classes da linha da família. A aproximação vale só entre a navegação e o que vem
|
|
41
|
+
depois dela; a distância da mídia até o título é a mesma que existiria sem navegação.
|
|
42
|
+
5. A navegação mantém `data-slot="<slot>-navigation"` dentro do grupo, para que seletores de teste
|
|
43
|
+
e de CSS continuem encontrando a região nas duas apresentações.
|
|
44
|
+
|
|
45
|
+
Fora dessa combinação nada muda: navegação sem título, título sem navegação,
|
|
46
|
+
`leadingPlacement="above"`, usado pela `Page`, e o `Dialog mode="alert"`, cujo cabeçalho não usa
|
|
47
|
+
`StructuralHeader`. `SurfaceHeader` não tem slot de navegação, e as intros (`DialogIntro` e
|
|
48
|
+
`DrawerIntro`) também não aceitam navegação.
|
|
49
|
+
|
|
50
|
+
A regra mora uma vez em `structural-header.tsx`. Dialog e Drawer a herdam porque ambos declaram
|
|
51
|
+
`leadingPlacement="inline"` e passam `surfaceHeaderRowClasses(family)` como `rowClassName`; nenhuma
|
|
52
|
+
família reescreve o intervalo.
|
|
53
|
+
|
|
54
|
+
## Consequências
|
|
55
|
+
|
|
56
|
+
- Em Dialog e Drawer com navegação e título, o voltar ou o recolher passa de `1rem` para `0.5rem`
|
|
57
|
+
do título, ou da mídia que o antecede. Ações e fechamento mantêm a mesma posição e o mesmo ritmo.
|
|
58
|
+
- A API pública não muda, por isso a correção sai como patch (24.0.1).
|
|
59
|
+
- O DOM ganha um nível nessa combinação, e dois quando há mídia: a navegação deixa de ser filha
|
|
60
|
+
direta de `<slot>-header-row` e passa a ser filha de `<slot>-header-context`. Seletores que
|
|
61
|
+
dependiam do filho direto, como `[data-slot=drawer-header-row] > [data-slot=drawer-navigation]`,
|
|
62
|
+
precisam usar o descendente; seletores pelo `data-slot` da navegação continuam válidos.
|
|
63
|
+
- Uma família nova que adote a linha inline com navegação herda o agrupamento sem configuração. Se
|
|
64
|
+
ela precisar de outra relação entre navegação e título, a mudança é desta ADR, não de um
|
|
65
|
+
`className` local.
|
|
66
|
+
|
|
67
|
+
## Alternativas consideradas
|
|
68
|
+
|
|
69
|
+
- **Manter o intervalo único.** Não exige mudança, mas iguala duas relações diferentes.
|
|
70
|
+
Descartada.
|
|
71
|
+
- **Reduzir o intervalo da linha inteira para `gap-2`.** Aproximaria navegação e título, mas também
|
|
72
|
+
aproximaria as ações do título, que é a separação que a família precisa preservar. Descartada.
|
|
73
|
+
- **Ajustar a margem no slot de navegação de cada família**, por exemplo com `-mr-2` em
|
|
74
|
+
`DrawerNavigation`. Resolveria o caso visual, mas espalharia uma decisão da anatomia
|
|
75
|
+
compartilhada por cada família e dependeria de compensar um intervalo que continua existindo.
|
|
76
|
+
Descartada.
|
|
77
|
+
- **Levar a navegação para dentro do wrapper do heading.** Misturaria uma região de controle com o
|
|
78
|
+
bloco que nomeia a superfície e mudaria o elemento que absorve a compressão do título. Descartada.
|
|
79
|
+
- **Aplicar `gap-2` também entre mídia e título quando houver navegação.** Seria mais simples, mas
|
|
80
|
+
faria a distância da mídia até o título depender da presença da navegação. Descartada.
|
|
81
|
+
- **Expor o intervalo como propriedade pública.** Abriria uma variação por consumidor para uma
|
|
82
|
+
relação que deve ser igual em todas as superfícies. Descartada.
|
|
83
|
+
|
|
84
|
+
## Verificação
|
|
85
|
+
|
|
86
|
+
- `tests/ui/drawer.test.tsx` fixa a regra no Drawer: com navegação, título e ações, a linha mantém
|
|
87
|
+
`gap-4`, o grupo `drawer-header-context` usa `gap-2` e contém o título, `drawer-actions` mantém
|
|
88
|
+
`gap-2` e não há wrapper de identidade sem mídia. Com mídia, `drawer-header-identity` fica dentro
|
|
89
|
+
do grupo, mantém `gap-4` e reúne mídia e título, sem a navegação.
|
|
90
|
+
- `tests/ui/dialog.test.tsx` cobre a mesma regra no Dialog com navegação, mídia, título e ações,
|
|
91
|
+
inclusive as ações como filhas diretas da linha, fora do grupo.
|
|
92
|
+
- Os testes existentes cobrem as combinações que não mudam: navegação e ações sem título continuam
|
|
93
|
+
na mesma linha, e ações isoladas continuam no extremo final.
|
|
94
|
+
- Na página **UI > Design > Composições**, o Drawer compacto exibe o recolher e o título no
|
|
95
|
+
cabeçalho e serve de verificação visual da regra.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softize/opus",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "24.0.1",
|
|
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
|
+
}
|