@softize/opus 12.9.0 → 12.11.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 +44 -0
- package/bin/lib/check.mjs +1103 -310
- package/bin/lib/copy.mjs +74 -6
- package/docs/adr/0003-dictionary-presentation-is-declared.md +3 -0
- package/docs/adr/0004-page-content-state-is-composed.md +65 -0
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +97 -0
- package/docs/adr/0006-semantic-context-precedes-visual-variant.md +182 -0
- package/docs/code-style.md +6 -2
- package/package.json +1 -1
- package/registry/instructions/opus.md +5 -0
- package/registry/skills/build-opus-ui/SKILL.md +27 -16
- package/registry/skills/build-opus-ui/references/evaluations.md +16 -5
- package/registry/skills/build-opus-ui/references/ui-patterns.md +38 -15
- package/registry/skills/model-opus-dictionary/SKILL.md +4 -2
- package/registry/skills/model-opus-dictionary/references/evaluations.md +4 -3
- package/registry/templates/app/src/App.tsx +1 -1
- package/src/core/dictionary.ts +52 -14
- package/src/core/index.ts +10 -0
- package/src/core/ui-context.ts +29 -0
- package/src/schema/drivers/zod.ts +34 -19
- package/src/ui/components/patterns/action-form-card.tsx +18 -12
- package/src/ui/components/patterns/confirm.tsx +26 -3
- package/src/ui/components/patterns/content-header.tsx +335 -61
- package/src/ui/components/patterns/data-state.tsx +23 -10
- package/src/ui/components/patterns/form.tsx +1 -1
- package/src/ui/components/patterns/list.tsx +1096 -777
- package/src/ui/components/patterns/page-state.tsx +115 -0
- package/src/ui/components/patterns/page.tsx +231 -41
- package/src/ui/components/patterns/sidebar.tsx +354 -80
- package/src/ui/components/patterns/trigger.tsx +13 -9
- package/src/ui/components/patterns/view.tsx +7 -11
- package/src/ui/components/primitives/alert-dialog.tsx +7 -5
- package/src/ui/components/primitives/alert.tsx +298 -80
- package/src/ui/components/primitives/ask.tsx +2 -1
- package/src/ui/components/primitives/badge.tsx +91 -30
- package/src/ui/components/primitives/button.tsx +99 -60
- package/src/ui/components/primitives/calendar.tsx +39 -39
- package/src/ui/components/primitives/card.tsx +96 -23
- package/src/ui/components/primitives/detail.tsx +2 -2
- package/src/ui/components/primitives/dictionary-value.tsx +9 -14
- package/src/ui/components/primitives/dot.tsx +74 -21
- package/src/ui/components/primitives/drawer.tsx +33 -20
- package/src/ui/components/primitives/field.tsx +4 -4
- package/src/ui/components/primitives/item.tsx +137 -81
- package/src/ui/components/primitives/menu.tsx +11 -3
- package/src/ui/components/primitives/metric-card.tsx +133 -0
- package/src/ui/components/primitives/table.tsx +2 -2
- package/src/ui/components/primitives/tooltip.tsx +1 -1
- package/src/ui/docs/DocBrowser.tsx +3 -3
- package/src/ui/docs/changelog.tsx +1 -1
- package/src/ui/docs/content/action-form-card.md +1 -1
- package/src/ui/docs/content/alert-dialog.md +8 -8
- package/src/ui/docs/content/alert.md +54 -23
- package/src/ui/docs/content/badge.md +18 -19
- package/src/ui/docs/content/button.md +12 -9
- package/src/ui/docs/content/card.md +5 -5
- package/src/ui/docs/content/content.md +44 -0
- package/src/ui/docs/content/customization.md +2 -2
- package/src/ui/docs/content/detail.md +5 -2
- package/src/ui/docs/content/dialog.md +2 -2
- package/src/ui/docs/content/dictionary-value.md +11 -10
- package/src/ui/docs/content/dot.md +7 -7
- package/src/ui/docs/content/drawer.md +6 -3
- package/src/ui/docs/content/field.md +1 -1
- package/src/ui/docs/content/input-group.md +3 -2
- package/src/ui/docs/content/item.md +47 -21
- package/src/ui/docs/content/menu.md +5 -4
- package/src/ui/docs/content/metric-card.md +41 -0
- package/src/ui/docs/content/page-state.md +45 -0
- package/src/ui/docs/content/page.md +48 -10
- package/src/ui/docs/content/semantic-context.md +63 -0
- package/src/ui/docs/content/sidebar.md +4 -4
- package/src/ui/docs/content/skeleton.md +2 -2
- package/src/ui/docs/content/table.md +3 -3
- package/src/ui/docs/content/tokens.md +28 -0
- package/src/ui/docs/content/tooltip.md +15 -1
- package/src/ui/docs/doc-client.tsx +2 -2
- package/src/ui/docs/registry.tsx +596 -228
- package/src/ui/lib/semantic-context.ts +30 -0
- package/src/ui/meta.ts +292 -270
- package/src/ui/react.tsx +378 -111
- package/src/ui/theme.css +66 -0
|
@@ -19,46 +19,54 @@ existentes, mantendo navegação observável e componentes reutilizáveis sem la
|
|
|
19
19
|
mensagem de vocabulário fechado vêm do dicionário do domínio (`labelFor`/`metaFor`); ao
|
|
20
20
|
encontrar catálogo repetido à mão ou vocabulário ainda sem dicionário, carregar
|
|
21
21
|
`$model-opus-dictionary`.
|
|
22
|
-
3.
|
|
22
|
+
3. Em componentes semânticos, declarar `context` antes de escolher `variant`: `context` comunica
|
|
23
|
+
`neutral`, `primary`, `info`, `success`, `warning` ou `danger`; `variant` descreve somente o
|
|
24
|
+
tratamento `solid`, `subtle`, `outline`, `ghost` ou `link`, conforme o subconjunto aceito pelo
|
|
25
|
+
componente. Não usar `variant="success"`, `variant="destructive"` nem `tone` em código novo.
|
|
26
|
+
`destructive` permanece metadata comportamental de action e se projeta como `context="danger"`.
|
|
27
|
+
4. Apresentar valor de dicionário por `DictionaryValue` ou pela coluna de `ActionList`, que
|
|
23
28
|
aplicam o papel declarado em `presentation` (classificação em badge `outline`, status e estágio
|
|
24
|
-
em badge
|
|
29
|
+
em badge `context + subtle`, `plain` em texto). Não escolher badge, contexto ou ícone pelo nome do dicionário:
|
|
25
30
|
dicionário sem papel declarado renderiza texto e pede classificação por
|
|
26
31
|
`$model-opus-dictionary` antes de qualquer destaque visual. Sobrepor os defaults só com motivo
|
|
27
32
|
explícito nas props do renderer.
|
|
28
|
-
|
|
33
|
+
5. Dar a cada dimensão independente usada para comparação ou filtro um campo, coluna ou espaço
|
|
29
34
|
identificável próprio, com rótulo. Hierarquia tipográfica (texto secundário sob um nome) não
|
|
30
35
|
pode fazer uma dimensão parecer explicação de outra. Cor e ícone reforçam; o texto do valor
|
|
31
36
|
permanece sempre presente.
|
|
32
|
-
|
|
37
|
+
6. Deixar ausência, paginação e largura de filtro com o pattern: célula e `DetailField` já
|
|
33
38
|
representam valor ausente (`EmptyValue`, `empty` para o significado do domínio); listas
|
|
34
39
|
paginam pela primitiva `Pagination`; selects inline de filtro têm largura fixa. Não reescrever
|
|
35
40
|
esses defaults na tela.
|
|
36
|
-
|
|
41
|
+
7. Tratar página, filtro, seleção e modal importante como estado navegável por URL quando
|
|
37
42
|
o produto precisa de deep link, back/forward ou refresh.
|
|
38
|
-
|
|
39
|
-
`
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
8. Compor superfícies pela gramática estrutural do catálogo: `Page` contém `PageHeader` e
|
|
44
|
+
`PageBody`; `Content` contém `ContentHeader` e `ContentBody`; Card, Drawer e Pane usam seus
|
|
45
|
+
respectivos `*Body`. Para o caso direto, usar a sintaxe abreviada de `Page` ou `Content` com
|
|
46
|
+
`title`, `description`, `meta` e `actions`; não misturá-la com o header explícito. Ajustar o
|
|
47
|
+
nível do heading pela hierarquia semântica, não pelo destaque visual. O `Page` mantém seu teto
|
|
48
|
+
centralizado padrão de `80rem`.
|
|
49
|
+
9. Manter margem e posicionamento no consumidor; componente reutilizável controla apenas
|
|
42
50
|
seu interior.
|
|
43
|
-
|
|
51
|
+
10. Não definir a fonte raiz em uma biblioteca ou componente. O navegador e a aplicação são
|
|
44
52
|
responsáveis por `font-size` em `html`; medidas escaláveis da UI usam `rem` ou a escala
|
|
45
53
|
relativa do Tailwind. Reservar `px` a hairlines e compensações presas à geometria da borda,
|
|
46
54
|
com justificativa e cobertura explícitas.
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
11. Evoluir um pattern compartilhado apenas quando a recorrência e o contrato estiverem claros.
|
|
56
|
+
12. Tratar tokens de superfície como pares indivisíveis no mesmo fragmento de classes:
|
|
49
57
|
`bg-card text-card-foreground` e `bg-popover text-popover-foreground`. Não depender da
|
|
50
58
|
igualdade atual com `--foreground`, porque o app pode sobrescrever cada par.
|
|
51
|
-
|
|
59
|
+
13. Usar diretamente a escala `rounded-*`; não criar radius por nome de componente quando
|
|
52
60
|
`rounded-xs` a `rounded-2xl` já expressam a forma. Escolher o degrau pela escala visual:
|
|
53
61
|
detalhes e itens internos tendem a `xs`/`sm`, controles e flutuantes compactos a `md`,
|
|
54
62
|
molduras estruturais a `lg`, e superfícies principais ou modais a `xl`. Essa associação
|
|
55
63
|
orienta o default, não cria uma restrição semântica. Em aninhamento, evitar moldura dupla e
|
|
56
64
|
reduzir o raio interno; em grupos conectados, remover os raios das arestas internas. Tamanho
|
|
57
65
|
e forma permanecem eixos separados; usar `shape="pill"` quando a pílula for intencional.
|
|
58
|
-
|
|
66
|
+
14. Distinguir uma região disponível para criação ou vínculo, representada por `Empty` com
|
|
59
67
|
moldura tracejada, de um resultado vazio dentro de uma estrutura existente, que preserva
|
|
60
68
|
a moldura sólida dessa estrutura.
|
|
61
|
-
|
|
69
|
+
15. Testar estados de loading, vazio, erro, sucesso, permissão e interação relevante.
|
|
62
70
|
|
|
63
71
|
## Verificação
|
|
64
72
|
|
|
@@ -76,8 +84,11 @@ Inspecionar visualmente a rota real e validar navegação por URL quando aplicá
|
|
|
76
84
|
- Não criar fetch, schema ou tipo paralelo ao contrato.
|
|
77
85
|
- Não copiar componente da lib para customizar sem antes verificar extensão/composição.
|
|
78
86
|
- Não forçar modal roteável quando o estado é efêmero e sem valor de navegação.
|
|
79
|
-
- Não inferir apresentação de dicionário: nem badge para todo valor, nem
|
|
87
|
+
- Não inferir apresentação de dicionário: nem badge para todo valor, nem contexto ou ícone
|
|
80
88
|
inventados, nem tooltip que repete o rótulo.
|
|
89
|
+
- Não usar nomes de compatibilidade (`tone`, `variant="success"`, `variant="destructive"`) em
|
|
90
|
+
código novo; consultar a página “Contexto & Variante” do catálogo quando a combinação não estiver
|
|
91
|
+
clara.
|
|
81
92
|
- Não repetir fallback de ausência, paginador ou largura de filtro que o pattern já resolve.
|
|
82
93
|
|
|
83
94
|
## Recursos
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
- Dispara: “Monte a tela de edição usando a form action do Opus.”
|
|
4
4
|
- Não dispara: “Ajuste o CSS de um e-mail estático.”
|
|
5
5
|
- Execução: implementar uma lista com modal roteável e provar loading, erro, vazio e back.
|
|
6
|
-
- Execução estrutural: montar uma página de relatório com `Page
|
|
7
|
-
`ActionFilterBar` separado do renderer, `ItemGroup` para
|
|
8
|
-
`ActionFormDialog`; provar teto padrão de `
|
|
9
|
-
sólido, `Empty` apenas para criação ou vínculo, números sem alinhamento
|
|
10
|
-
`ghost` no modal.
|
|
6
|
+
- Execução estrutural: montar uma página de relatório com `Page > PageHeader + PageBody`, uma seção
|
|
7
|
+
`Content > ContentHeader + ContentBody`, `ActionFilterBar` separado do renderer, `ItemGroup` para
|
|
8
|
+
uma coleção secundária e um `ActionFormDialog`; provar teto padrão de `80rem`, hierarquia por
|
|
9
|
+
`level`, vazio estrutural sólido, `Empty` apenas para criação ou vínculo, números sem alinhamento
|
|
10
|
+
inferido e cancelamento `ghost` no modal.
|
|
11
|
+
- Execução abreviada: montar outra página com `<Page title description actions>` e uma seção com
|
|
12
|
+
`<Content title description actions>`, provando que ambas produzem a mesma anatomia e que o lint
|
|
13
|
+
rejeita a mistura entre props abreviadas e headers explícitos.
|
|
11
14
|
- Execução de forma: compor uma tabela dentro de Card sem moldura duplicada, manter a moldura
|
|
12
15
|
estrutural standalone em `rounded-lg` e os controles internos em `rounded-md`.
|
|
13
16
|
- Reprova: criar uma casca `bg-card` que herda o texto global ou introduzir `rounded-widget`
|
|
@@ -17,11 +20,19 @@
|
|
|
17
20
|
- Reprova: reconstruir manualmente o container de página, usar `Empty` tracejado como vazio de
|
|
18
21
|
tabela, alinhar toda coluna numérica à direita por inferência ou destacar “Cancelar” como ação
|
|
19
22
|
primária em modal.
|
|
23
|
+
- Reprova: deixar `ContentHeader` fora de `Content`, omitir `PageBody`/`ContentBody` na composição
|
|
24
|
+
explícita, usar `CardContent`/`PaneContent` em código novo ou tratar `DialogContent` como Body.
|
|
20
25
|
- Execução de dicionários: montar a lista de clientes com “Pessoa física/Empresa” e
|
|
21
26
|
“Prospect/Cliente”; provar que o tipo ocupa a coluna “Tipo” como classificação (badge `outline`
|
|
22
27
|
com os ícones declarados `user` e `building`), que o estágio ocupa a coluna “Estágio” como badge
|
|
23
28
|
tonal sem `outline`, que nenhuma das duas usa `cells`, que o texto do valor está presente e que
|
|
24
29
|
não há tooltip em “Pessoa física/Empresa” quando a descrição não acrescenta ao rótulo.
|
|
30
|
+
- Execução de contexto: montar ações, badges, alertas e indicadores com o mesmo estado `warning`;
|
|
31
|
+
exigir `context="warning"`, variantes visuais coerentes por componente e tokens da mesma família.
|
|
32
|
+
Uma exclusão continua `destructive` no contrato, mas usa `context="danger"` na projeção visual.
|
|
33
|
+
- Reprova: usar `tone` em componente novo, `variant="success"`, `variant="warning"` ou
|
|
34
|
+
`variant="destructive"`; usar `light`/`dark` como contexto; ou tratar `secondary` como sinônimo
|
|
35
|
+
de estado neutro.
|
|
25
36
|
- Reprova: envolver todo valor de dicionário em badge sem papel declarado, ou escolher a variante
|
|
26
37
|
pelo nome do dicionário.
|
|
27
38
|
- Reprova: colocar duas dimensões independentes na mesma célula sem identificação, como o tipo em
|
|
@@ -4,12 +4,24 @@
|
|
|
4
4
|
- Campos, labels, mensagens e invalidações pertencem ao contrato quando são parte da
|
|
5
5
|
operação, não a uma tela isolada.
|
|
6
6
|
- URL representa estado que precisa sobreviver a refresh, deep link ou histórico.
|
|
7
|
-
- `Page` fornece o `<main
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
- `Page` fornece o `<main>` e o container centralizado com teto padrão de `80rem`. Sua forma
|
|
8
|
+
explícita é `Page > PageHeader (PageTitle, PageDescription, PageMeta, PageActions) + PageBody`;
|
|
9
|
+
`title`, `description`, `meta` e `actions` no próprio `Page` são a abreviação para o caso direto.
|
|
10
|
+
Não misturar as duas formas. Alterar `className` apenas quando a superfície tiver uma necessidade
|
|
11
|
+
real de largura; não reconstruir esse container em cada rota.
|
|
12
|
+
- `PageState` substitui todo o conteúdo principal quando a página carrega, falha ou está vazia. Na
|
|
13
|
+
forma curta ele pode ser escrito como filho direto de `Page`, que cria o `PageBody`; na forma
|
|
14
|
+
explícita, fica dentro de `PageBody`. O cabeçalho permanece visível. Estados de seção ou coleção
|
|
15
|
+
continuam em `DataState`, `ActionView`, `ActionList` ou `Alert`; não elevar uma falha parcial a
|
|
16
|
+
estado da página.
|
|
17
|
+
- `Content` delimita uma seção e segue a mesma anatomia: `Content > ContentHeader (ContentTitle,
|
|
18
|
+
ContentDescription, ContentMeta, ContentActions) + ContentBody`. `ContentHeader` nunca fica
|
|
19
|
+
solto. `title`, `description`, `meta` e `actions` no `Content` são a abreviação para o caso
|
|
20
|
+
direto e não podem ser misturados ao header explícito. `level` preserva a hierarquia semântica
|
|
21
|
+
do heading.
|
|
22
|
+
- Card, Drawer e Pane nomeiam a região principal como `CardBody`, `DrawerBody` e `PaneBody`.
|
|
23
|
+
`*Content` permanece reservado a raízes técnicas ou painéis cujo papel não é o corpo de uma
|
|
24
|
+
estrutura, como `DialogContent`, `PopoverContent` e `TabsContent`.
|
|
13
25
|
- Componentes compartilhados não impõem margem externa; páginas e shells compõem layout.
|
|
14
26
|
- A fonte raiz pertence ao navegador e à aplicação. Medidas escaláveis usam `rem` ou a escala
|
|
15
27
|
relativa do Tailwind; `px` fica restrito a hairlines e compensações ligadas a essas bordas.
|
|
@@ -55,18 +67,23 @@
|
|
|
55
67
|
valor trunca no trigger e a opção inteira fica na lista. Não dimensionar filtro pelo conteúdo
|
|
56
68
|
nem pela label; o modal usa `w-full`.
|
|
57
69
|
- Catálogo e API efetivos vêm dos exports da versão instalada, não de memória ou exemplo antigo.
|
|
70
|
+
- Em componentes semânticos, `context` responde por que há destaque e `variant` responde como ele
|
|
71
|
+
aparece. O vocabulário canônico é `neutral | primary | info | success | warning | danger`; as
|
|
72
|
+
variantes visuais são `solid | subtle | outline | ghost | link`, limitadas por componente.
|
|
73
|
+
`light`/`dark` são temas, `secondary` não substitui estado neutro e `destructive` é comportamento
|
|
74
|
+
de action projetado visualmente como `danger`.
|
|
58
75
|
|
|
59
76
|
## Dicionários e dimensões
|
|
60
77
|
|
|
61
78
|
O dicionário declara o papel; a tela só escolhe onde o valor fica. Tabela de decisão aplicada
|
|
62
79
|
por `DictionaryValue` e pelas colunas de `ActionList`:
|
|
63
80
|
|
|
64
|
-
| Papel declarado | Exemplos | Forma | Variante | Ícone | Tooltip |
|
|
65
|
-
|
|
66
|
-
| `classification` | Tipo de cliente, categoria, natureza | Badge | `outline`; ignora `
|
|
67
|
-
| `status` | Aberto, resolvido, degradado | Badge |
|
|
68
|
-
| `stage` | Prospect, cliente; etapa do funil | Badge | igual a `status` | idem | idem |
|
|
69
|
-
| `plain` ou ausente | Fonte, formato, período | Texto | — | idem | idem |
|
|
81
|
+
| Papel declarado | Exemplos | Forma | Contexto | Variante | Ícone | Tooltip |
|
|
82
|
+
|---|---|---|---|---|---|---|
|
|
83
|
+
| `classification` | Tipo de cliente, categoria, natureza | Badge | `neutral` | `outline`; ignora `context` da entrada | se a entrada declara `icon` do catálogo | se `description` acrescenta ao rótulo |
|
|
84
|
+
| `status` | Aberto, resolvido, degradado | Badge | pela entrada; `neutral` por padrão | `subtle`; nunca `outline` | idem | idem |
|
|
85
|
+
| `stage` | Prospect, cliente; etapa do funil | Badge | igual a `status` | `subtle` | idem | idem |
|
|
86
|
+
| `plain` ou ausente | Fonte, formato, período | Texto | — | — | idem | idem |
|
|
70
87
|
|
|
71
88
|
Regras que não dependem da tabela:
|
|
72
89
|
|
|
@@ -88,7 +105,7 @@ export const customerKindDict = t.dict(
|
|
|
88
105
|
{ doc: 'Natureza da parte no cadastro global.', presentation: 'classification' },
|
|
89
106
|
)
|
|
90
107
|
export const customerStageDict = t.dict(
|
|
91
|
-
{ prospect: { label: 'Prospect' }, customer: { label: 'Cliente',
|
|
108
|
+
{ prospect: { label: 'Prospect' }, customer: { label: 'Cliente', context: 'success' } },
|
|
92
109
|
{ doc: 'Estágio comercial atual da parte.', presentation: 'stage' },
|
|
93
110
|
)
|
|
94
111
|
// contrato:
|
|
@@ -112,7 +129,7 @@ cells={{
|
|
|
112
129
|
),
|
|
113
130
|
stage: (item) =>
|
|
114
131
|
item.stage === 'customer'
|
|
115
|
-
? <Badge variant="
|
|
132
|
+
? <Badge context="neutral" variant="solid">Cliente</Badge>
|
|
116
133
|
: <Badge variant="outline">Prospect</Badge>,
|
|
117
134
|
}}
|
|
118
135
|
|
|
@@ -124,7 +141,13 @@ cells={{
|
|
|
124
141
|
|
|
125
142
|
// Tooltip que repete o rótulo; cor como único sinal.
|
|
126
143
|
<Tooltip>
|
|
127
|
-
<TooltipTrigger><Dot
|
|
144
|
+
<TooltipTrigger><Dot context="success" /></TooltipTrigger>
|
|
128
145
|
<TooltipContent>Cliente</TooltipContent>
|
|
129
146
|
</Tooltip>
|
|
147
|
+
|
|
148
|
+
// Contexto semântico usado como variante: a IA perdeu um eixo da API.
|
|
149
|
+
<Badge variant="success">Concluído</Badge>
|
|
150
|
+
|
|
151
|
+
// Risco comportamental usado como nome visual.
|
|
152
|
+
<Button variant="destructive">Excluir</Button>
|
|
130
153
|
```
|
|
@@ -35,7 +35,8 @@ da mesma instância, sem catálogo repetido em enum, opção estática ou format
|
|
|
35
35
|
- **status** — situação operacional que muda com o tempo;
|
|
36
36
|
- **stage** — etapa de um ciclo ou funil;
|
|
37
37
|
- **plain** — valor que só precisa ser legível (o mesmo efeito de omitir).
|
|
38
|
-
Por entrada, declarar `
|
|
38
|
+
Por entrada, declarar `context` somente em status e estágio, usando `neutral`, `info`, `success`,
|
|
39
|
+
`warning` ou `danger`; `icon` somente com nome existente no
|
|
39
40
|
catálogo `iconPickerIcons`, e `description` somente quando acrescentar algo ao rótulo. `doc`
|
|
40
41
|
continua sendo o entendimento de negócio para manifest e Lens; não é tooltip.
|
|
41
42
|
4. Derivar o schema por `.zod()` e usá-lo em todo contrato que valide o código; não redeclarar a
|
|
@@ -68,7 +69,8 @@ contrato ou componente mapeado, regenerar com `opus copy` e executar os checks d
|
|
|
68
69
|
- Não tratar metadata de dicionário como autorização ou invariante de integridade.
|
|
69
70
|
- Não omitir `presentation` esperando que a tela deduza o papel pelo nome do dicionário; sem
|
|
70
71
|
papel declarado, o valor é texto.
|
|
71
|
-
- Não declarar `
|
|
72
|
+
- Não declarar `context` em classificação, nem usar o alias legado `tone` em código novo, nem
|
|
73
|
+
`icon` fora do catálogo, nem `description` que
|
|
72
74
|
repita o rótulo.
|
|
73
75
|
|
|
74
76
|
## Recursos
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
interface lê rótulos por `labelFor`/`metaFor`.
|
|
12
12
|
- Execução de apresentação: dado um cadastro com “Pessoa física/Empresa” e “Prospect/Cliente”,
|
|
13
13
|
declarar o primeiro como `classification` com ícones `user` e `building` do catálogo e o segundo
|
|
14
|
-
como `stage` com `
|
|
15
|
-
projeta `presentation` e que `t.dict` rejeita
|
|
16
|
-
- Reprova: declarar `
|
|
14
|
+
como `stage` com `context` explícito, sem `description` que repita o rótulo; provar que o manifest
|
|
15
|
+
projeta `presentation` e `context` e que `t.dict` rejeita valores fora do vocabulário.
|
|
16
|
+
- Reprova: declarar `context` em uma classificação, usar o alias legado `tone` em código novo,
|
|
17
|
+
inventar `icon` fora do catálogo, copiar `doc`
|
|
17
18
|
para `description` ou deixar `presentation` ausente em um status esperando que a tela infira o
|
|
18
19
|
badge.
|
|
@@ -26,7 +26,7 @@ export function App(): React.ReactElement {
|
|
|
26
26
|
{items.map((t) => (
|
|
27
27
|
<li key={t.id} className="flex items-center justify-between gap-2 text-sm">
|
|
28
28
|
<span>{t.title}</span>
|
|
29
|
-
<Badge
|
|
29
|
+
<Badge context={t.done ? 'success' : 'neutral'}>{t.done ? 'Feita' : 'Aberta'}</Badge>
|
|
30
30
|
</li>
|
|
31
31
|
))}
|
|
32
32
|
</ul>
|
package/src/core/dictionary.ts
CHANGED
|
@@ -8,14 +8,20 @@
|
|
|
8
8
|
|
|
9
9
|
import { getLogicalType } from './logical-type.ts'
|
|
10
10
|
import type { LogicalTypeMeta } from './types.ts'
|
|
11
|
+
import {
|
|
12
|
+
DICT_CONTEXTS,
|
|
13
|
+
isDictContext,
|
|
14
|
+
type DictContext,
|
|
15
|
+
} from './ui-context.ts'
|
|
11
16
|
|
|
12
17
|
/** Papel de apresentação declarado no nível do dicionário. */
|
|
13
18
|
export const DICT_PRESENTATIONS = ['classification', 'status', 'stage', 'plain'] as const
|
|
14
19
|
export type DictPresentation = (typeof DICT_PRESENTATIONS)[number]
|
|
15
20
|
|
|
16
|
-
/**
|
|
17
|
-
export const DICT_TONES =
|
|
18
|
-
|
|
21
|
+
/** @deprecated Use `DICT_CONTEXTS` (ADR 0006). */
|
|
22
|
+
export const DICT_TONES = DICT_CONTEXTS
|
|
23
|
+
/** @deprecated Use `DictContext` (ADR 0006). */
|
|
24
|
+
export type DictTone = DictContext
|
|
19
25
|
|
|
20
26
|
/**
|
|
21
27
|
* Meta de cada chave de um dict. `label` obrigatório; resto livre.
|
|
@@ -24,7 +30,7 @@ export type DictTone = (typeof DICT_TONES)[number]
|
|
|
24
30
|
* entra/sai) — a fonte rica de consulta rápida; flui pro manifest/Lens e NÃO vira tooltip.
|
|
25
31
|
* - `description` é o texto curto voltado à pessoa: aparece no tooltip do `DictionaryValue`
|
|
26
32
|
* e como apoio de opção. Não repetir o rótulo — descrição igual ao rótulo não renderiza.
|
|
27
|
-
* - `
|
|
33
|
+
* - `context` é o contexto semântico do status/estágio; classificação ignora.
|
|
28
34
|
* - `icon` é o identificador estável do catálogo de ícones do Opus (`iconPickerIcons`);
|
|
29
35
|
* nome fora do catálogo não renderiza ícone.
|
|
30
36
|
* - `color` permanece metadata livre (lida pela Lens); o renderer não a interpreta.
|
|
@@ -33,7 +39,9 @@ export interface DictEntryMeta {
|
|
|
33
39
|
label: string
|
|
34
40
|
doc?: string
|
|
35
41
|
description?: string
|
|
36
|
-
|
|
42
|
+
context?: DictContext
|
|
43
|
+
/** @deprecated Use `context`. Compatibilidade temporária da ADR 0006. */
|
|
44
|
+
tone?: DictContext
|
|
37
45
|
icon?: string
|
|
38
46
|
color?: string
|
|
39
47
|
order?: number
|
|
@@ -44,8 +52,16 @@ export function isDictPresentation(value: unknown): value is DictPresentation {
|
|
|
44
52
|
return typeof value === 'string' && (DICT_PRESENTATIONS as readonly string[]).includes(value)
|
|
45
53
|
}
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
/** @deprecated Use `isDictContext` (ADR 0006). */
|
|
56
|
+
export const isDictTone = isDictContext
|
|
57
|
+
|
|
58
|
+
/** Normaliza a metadata canônica e seu alias temporário. */
|
|
59
|
+
export function dictionaryEntryContext(entry: DictEntryMeta): DictContext | undefined {
|
|
60
|
+
return isDictContext(entry.context)
|
|
61
|
+
? entry.context
|
|
62
|
+
: isDictContext(entry.tone)
|
|
63
|
+
? entry.tone
|
|
64
|
+
: undefined
|
|
49
65
|
}
|
|
50
66
|
|
|
51
67
|
/** Forma normalizada de um dicionário, independente de onde a meta foi lida. */
|
|
@@ -92,7 +108,11 @@ export interface DictionaryValuePresentation {
|
|
|
92
108
|
/** False quando o valor não está no dicionário. */
|
|
93
109
|
known: boolean
|
|
94
110
|
presentation: DictPresentation
|
|
95
|
-
/**
|
|
111
|
+
/** Contexto semântico; `null` quando a apresentação é texto. */
|
|
112
|
+
context: DictContext | null
|
|
113
|
+
/** Tratamento visual; `null` quando a apresentação é texto. */
|
|
114
|
+
variant: 'subtle' | 'outline' | null
|
|
115
|
+
/** @deprecated Use `context` e `variant`. */
|
|
96
116
|
badge: 'outline' | DictTone | null
|
|
97
117
|
/** Identificador do catálogo, só quando declarado na entrada. */
|
|
98
118
|
icon: string | null
|
|
@@ -115,8 +135,8 @@ export function dictionaryDescription(label: string, description: unknown): stri
|
|
|
115
135
|
* Aplica a tabela de decisão da ADR 0003:
|
|
116
136
|
*
|
|
117
137
|
* | papel | forma | variante |
|
|
118
|
-
* | classification | badge |
|
|
119
|
-
* | status / stage | badge |
|
|
138
|
+
* | classification | badge | neutral + outline |
|
|
139
|
+
* | status / stage | badge | `context` + subtle |
|
|
120
140
|
* | plain / ausente | texto | — |
|
|
121
141
|
*
|
|
122
142
|
* Ícone só quando declarado; tooltip só quando `description` acrescenta. Valor fora do
|
|
@@ -132,20 +152,38 @@ export function presentDictionaryValue(
|
|
|
132
152
|
? descriptor.entries[value]
|
|
133
153
|
: undefined
|
|
134
154
|
if (entry === undefined || typeof entry.label !== 'string') {
|
|
135
|
-
return {
|
|
155
|
+
return {
|
|
156
|
+
value,
|
|
157
|
+
label: value,
|
|
158
|
+
known: false,
|
|
159
|
+
presentation,
|
|
160
|
+
context: null,
|
|
161
|
+
variant: null,
|
|
162
|
+
badge: null,
|
|
163
|
+
icon: null,
|
|
164
|
+
description: null,
|
|
165
|
+
}
|
|
136
166
|
}
|
|
137
|
-
const
|
|
167
|
+
const context: DictionaryValuePresentation['context'] =
|
|
168
|
+
presentation === 'classification'
|
|
169
|
+
? 'neutral'
|
|
170
|
+
: presentation === 'status' || presentation === 'stage'
|
|
171
|
+
? (dictionaryEntryContext(entry) ?? 'neutral')
|
|
172
|
+
: null
|
|
173
|
+
const variant: DictionaryValuePresentation['variant'] =
|
|
138
174
|
presentation === 'classification'
|
|
139
175
|
? 'outline'
|
|
140
176
|
: presentation === 'status' || presentation === 'stage'
|
|
141
|
-
?
|
|
177
|
+
? 'subtle'
|
|
142
178
|
: null
|
|
143
179
|
return {
|
|
144
180
|
value,
|
|
145
181
|
label: entry.label,
|
|
146
182
|
known: true,
|
|
147
183
|
presentation,
|
|
148
|
-
|
|
184
|
+
context,
|
|
185
|
+
variant,
|
|
186
|
+
badge: variant === 'outline' ? 'outline' : context,
|
|
149
187
|
icon: typeof entry.icon === 'string' && entry.icon.length > 0 ? entry.icon : null,
|
|
150
188
|
description: dictionaryDescription(entry.label, entry.description),
|
|
151
189
|
}
|
package/src/core/index.ts
CHANGED
|
@@ -138,12 +138,22 @@ export { normalizeTraceContext } from './trace.ts'
|
|
|
138
138
|
// `@softize/opus/schema` re-exporta os dois pra manter a API de sempre.
|
|
139
139
|
export { attachLogicalType, getLogicalType } from './logical-type.ts'
|
|
140
140
|
|
|
141
|
+
// — Contextos semânticos de UI (ADR 0006) ————————————————————————————————————
|
|
142
|
+
export {
|
|
143
|
+
UI_CONTEXTS,
|
|
144
|
+
DICT_CONTEXTS,
|
|
145
|
+
isUiContext,
|
|
146
|
+
isDictContext,
|
|
147
|
+
} from './ui-context.ts'
|
|
148
|
+
export type { UiContext, DictContext } from './ui-context.ts'
|
|
149
|
+
|
|
141
150
|
// — Dicionários (apresentação declarada — ADR 0003) ————————————————————————————
|
|
142
151
|
export {
|
|
143
152
|
DICT_PRESENTATIONS,
|
|
144
153
|
DICT_TONES,
|
|
145
154
|
isDictPresentation,
|
|
146
155
|
isDictTone,
|
|
156
|
+
dictionaryEntryContext,
|
|
147
157
|
dictionaryDescriptor,
|
|
148
158
|
dictionaryDescription,
|
|
149
159
|
presentDictionaryValue,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Famílias semânticas compartilhadas entre contratos e UI (ADR 0006).
|
|
3
|
+
*
|
|
4
|
+
* Contexto responde por que um realce existe; cada componente decide como materializá-lo.
|
|
5
|
+
* Tema (light/dark), geometria e variante visual são eixos independentes.
|
|
6
|
+
*/
|
|
7
|
+
export const UI_CONTEXTS = [
|
|
8
|
+
'neutral',
|
|
9
|
+
'primary',
|
|
10
|
+
'info',
|
|
11
|
+
'success',
|
|
12
|
+
'warning',
|
|
13
|
+
'danger',
|
|
14
|
+
] as const
|
|
15
|
+
|
|
16
|
+
export type UiContext = (typeof UI_CONTEXTS)[number]
|
|
17
|
+
|
|
18
|
+
/** Contextos válidos para estados e estágios de domínio. */
|
|
19
|
+
export const DICT_CONTEXTS = ['neutral', 'info', 'success', 'warning', 'danger'] as const
|
|
20
|
+
|
|
21
|
+
export type DictContext = (typeof DICT_CONTEXTS)[number]
|
|
22
|
+
|
|
23
|
+
export function isUiContext(value: unknown): value is UiContext {
|
|
24
|
+
return typeof value === 'string' && (UI_CONTEXTS as readonly string[]).includes(value)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isDictContext(value: unknown): value is DictContext {
|
|
28
|
+
return typeof value === 'string' && (DICT_CONTEXTS as readonly string[]).includes(value)
|
|
29
|
+
}
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
type DictEntryMeta,
|
|
37
37
|
type DictPresentation,
|
|
38
38
|
} from '../../core/dictionary.ts'
|
|
39
|
+
import { DICT_CONTEXTS, isDictContext } from '../../core/ui-context.ts'
|
|
39
40
|
import {
|
|
40
41
|
formatDatetime,
|
|
41
42
|
formatDate,
|
|
@@ -656,7 +657,8 @@ function isLogicalType(value: unknown): value is LogicalType<unknown> {
|
|
|
656
657
|
/**
|
|
657
658
|
* Meta de cada chave de um dict — o tipo mora no core (`@softize/opus/core`, ADR 0003) e é
|
|
658
659
|
* reexportado aqui pra manter a API: `label` obrigatório, `doc` (negócio, manifest/Lens),
|
|
659
|
-
* `description` (tooltip), `
|
|
660
|
+
* `description` (tooltip), `context` (contexto semântico de status/estágio), `icon` (catálogo).
|
|
661
|
+
* `tone` permanece como alias temporário de compatibilidade.
|
|
660
662
|
*/
|
|
661
663
|
export type { DictEntryMeta }
|
|
662
664
|
|
|
@@ -680,7 +682,7 @@ export interface DictType<K extends string, M extends DictEntryMeta> {
|
|
|
680
682
|
*/
|
|
681
683
|
labelFor(key: K, locale?: string): string
|
|
682
684
|
/** Meta completa da chave (label + extras). */
|
|
683
|
-
metaFor(key: K): M
|
|
685
|
+
metaFor(key: K): Readonly<M>
|
|
684
686
|
/** Lista pronta pra `<Select options={...} />`: `{ value, ...meta }`. */
|
|
685
687
|
options(): DictOption<K, M>[]
|
|
686
688
|
/** True se `key` está no dict. */
|
|
@@ -707,7 +709,7 @@ const dict = <const M extends Record<string, DictEntryMeta>>(
|
|
|
707
709
|
opts?: DictOpts,
|
|
708
710
|
): DictType<Extract<keyof M, string>, M[keyof M]> => {
|
|
709
711
|
type K = Extract<keyof M, string>
|
|
710
|
-
const keys = Object.keys(entries) as K[]
|
|
712
|
+
const keys = Object.freeze(Object.keys(entries)) as readonly K[]
|
|
711
713
|
if (keys.length === 0) {
|
|
712
714
|
throw new Error('t.dict precisa de pelo menos uma entrada')
|
|
713
715
|
}
|
|
@@ -716,29 +718,42 @@ const dict = <const M extends Record<string, DictEntryMeta>>(
|
|
|
716
718
|
`t.dict: presentation "${String(opts.presentation)}" inválida; use ${DICT_PRESENTATIONS.join(' | ')}`,
|
|
717
719
|
)
|
|
718
720
|
}
|
|
719
|
-
const
|
|
721
|
+
const contextual = opts?.presentation === 'status' || opts?.presentation === 'stage'
|
|
720
722
|
for (const key of keys) {
|
|
721
|
-
const
|
|
722
|
-
if (tone
|
|
723
|
-
|
|
724
|
-
throw new Error(`t.dict: tone "${String(tone)}" inválido em "${key}"; use ${DICT_TONES.join(' | ')}`)
|
|
723
|
+
const entry = entries[key] as DictEntryMeta
|
|
724
|
+
if (entry.context !== undefined && entry.tone !== undefined) {
|
|
725
|
+
throw new Error(`t.dict: use context ou tone em "${key}", nunca os dois`)
|
|
725
726
|
}
|
|
726
|
-
|
|
727
|
+
const context = entry.context ?? entry.tone
|
|
728
|
+
if (context === undefined) continue
|
|
729
|
+
const legacy = entry.context === undefined
|
|
730
|
+
if (!(legacy ? isDictTone(context) : isDictContext(context))) {
|
|
731
|
+
const values = legacy ? DICT_TONES : DICT_CONTEXTS
|
|
732
|
+
const field = legacy ? 'tone' : 'context'
|
|
733
|
+
throw new Error(`t.dict: ${field} "${String(context)}" inválido em "${key}"; use ${values.join(' | ')}`)
|
|
734
|
+
}
|
|
735
|
+
if (!contextual) {
|
|
727
736
|
throw new Error(
|
|
728
|
-
`t.dict: tone em "${key}" exige presentation "status" ou "stage" (classificação usa outline; plain é texto)`,
|
|
737
|
+
`t.dict: ${legacy ? 'tone' : 'context'} em "${key}" exige presentation "status" ou "stage" (classificação usa outline; plain é texto)`,
|
|
729
738
|
)
|
|
730
739
|
}
|
|
731
740
|
}
|
|
741
|
+
// O dicionário é declarativo: mantém um snapshot próprio e imutável para que `metaFor`,
|
|
742
|
+
// a metadata do schema e mutações posteriores no objeto de entrada não alterem a copy
|
|
743
|
+
// ou a apresentação que foram inventariadas no código-fonte.
|
|
744
|
+
const stableEntries = Object.freeze(
|
|
745
|
+
Object.fromEntries(keys.map((key) => [key, Object.freeze({ ...entries[key] })])),
|
|
746
|
+
) as unknown as Readonly<M>
|
|
732
747
|
const zodSchema = z.enum(keys as [K, ...K[]])
|
|
733
|
-
const meta: LogicalTypeMeta = {
|
|
748
|
+
const meta: LogicalTypeMeta = Object.freeze({
|
|
734
749
|
logicalType: 'dict',
|
|
735
|
-
params: {
|
|
750
|
+
params: Object.freeze({
|
|
736
751
|
keys,
|
|
737
|
-
entries,
|
|
752
|
+
entries: stableEntries,
|
|
738
753
|
...(opts?.doc !== undefined ? { doc: opts.doc } : {}),
|
|
739
754
|
...(opts?.presentation !== undefined ? { presentation: opts.presentation } : {}),
|
|
740
|
-
},
|
|
741
|
-
}
|
|
755
|
+
}),
|
|
756
|
+
})
|
|
742
757
|
attachLogicalType(zodSchema as unknown as object, meta)
|
|
743
758
|
|
|
744
759
|
return {
|
|
@@ -749,16 +764,16 @@ const dict = <const M extends Record<string, DictEntryMeta>>(
|
|
|
749
764
|
keys: () => [...keys],
|
|
750
765
|
labelFor: (key, _locale) => {
|
|
751
766
|
// _locale reservado pra i18n futuro; no v1 sempre retorna o label cru.
|
|
752
|
-
return (
|
|
767
|
+
return (stableEntries[key] as M[keyof M]).label
|
|
753
768
|
},
|
|
754
|
-
metaFor: (key) =>
|
|
769
|
+
metaFor: (key) => stableEntries[key] as M[keyof M],
|
|
755
770
|
options: () =>
|
|
756
771
|
keys.map((k) => ({
|
|
757
|
-
...(
|
|
772
|
+
...(stableEntries[k] as M[keyof M]),
|
|
758
773
|
value: k,
|
|
759
774
|
})) as DictOption<K, M[keyof M]>[],
|
|
760
775
|
has: (key: string): key is K =>
|
|
761
|
-
Object.prototype.hasOwnProperty.call(
|
|
776
|
+
Object.prototype.hasOwnProperty.call(stableEntries, key),
|
|
762
777
|
}
|
|
763
778
|
}
|
|
764
779
|
|
|
@@ -6,22 +6,24 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import {
|
|
8
8
|
Card,
|
|
9
|
-
|
|
9
|
+
CardBody,
|
|
10
10
|
CardDescription,
|
|
11
11
|
CardFooter,
|
|
12
12
|
CardHeader,
|
|
13
13
|
CardTitle,
|
|
14
|
-
} from
|
|
15
|
-
import { ActionForm, type ActionFormProps } from
|
|
14
|
+
} from "../primitives/card.tsx";
|
|
15
|
+
import { ActionForm, type ActionFormProps } from "./form.tsx";
|
|
16
16
|
|
|
17
|
-
export interface ActionFormCardProps<
|
|
18
|
-
extends
|
|
17
|
+
export interface ActionFormCardProps<
|
|
18
|
+
TInput extends Record<string, unknown>,
|
|
19
|
+
TData,
|
|
20
|
+
> extends Omit<ActionFormProps<TInput, TData>, "body" | "footer"> {
|
|
19
21
|
/** Título do header do card. Sem ele, o card começa direto no conteúdo. */
|
|
20
|
-
title?: string
|
|
22
|
+
title?: string;
|
|
21
23
|
/** Subtítulo opcional, abaixo do título. */
|
|
22
|
-
description?: string
|
|
24
|
+
description?: string;
|
|
23
25
|
/** Classes do Card (a superfície). `className` vai pro <form>. */
|
|
24
|
-
cardClassName?: string
|
|
26
|
+
cardClassName?: string;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export function ActionFormCard<TInput extends Record<string, unknown>, TData>({
|
|
@@ -35,14 +37,18 @@ export function ActionFormCard<TInput extends Record<string, unknown>, TData>({
|
|
|
35
37
|
{(title !== undefined || description !== undefined) && (
|
|
36
38
|
<CardHeader>
|
|
37
39
|
{title !== undefined && <CardTitle>{title}</CardTitle>}
|
|
38
|
-
{description !== undefined &&
|
|
40
|
+
{description !== undefined && (
|
|
41
|
+
<CardDescription>{description}</CardDescription>
|
|
42
|
+
)}
|
|
39
43
|
</CardHeader>
|
|
40
44
|
)}
|
|
41
45
|
<ActionForm
|
|
42
46
|
{...rest}
|
|
43
|
-
body={(fields) => <
|
|
44
|
-
footer={(actions) =>
|
|
47
|
+
body={(fields) => <CardBody>{fields}</CardBody>}
|
|
48
|
+
footer={(actions) => (
|
|
49
|
+
<CardFooter className="justify-end">{actions}</CardFooter>
|
|
50
|
+
)}
|
|
45
51
|
/>
|
|
46
52
|
</Card>
|
|
47
|
-
)
|
|
53
|
+
);
|
|
48
54
|
}
|