@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/bin/lib/check.mjs +1103 -310
  3. package/bin/lib/copy.mjs +74 -6
  4. package/docs/adr/0003-dictionary-presentation-is-declared.md +3 -0
  5. package/docs/adr/0004-page-content-state-is-composed.md +65 -0
  6. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +97 -0
  7. package/docs/adr/0006-semantic-context-precedes-visual-variant.md +182 -0
  8. package/docs/code-style.md +6 -2
  9. package/package.json +1 -1
  10. package/registry/instructions/opus.md +5 -0
  11. package/registry/skills/build-opus-ui/SKILL.md +27 -16
  12. package/registry/skills/build-opus-ui/references/evaluations.md +16 -5
  13. package/registry/skills/build-opus-ui/references/ui-patterns.md +38 -15
  14. package/registry/skills/model-opus-dictionary/SKILL.md +4 -2
  15. package/registry/skills/model-opus-dictionary/references/evaluations.md +4 -3
  16. package/registry/templates/app/src/App.tsx +1 -1
  17. package/src/core/dictionary.ts +52 -14
  18. package/src/core/index.ts +10 -0
  19. package/src/core/ui-context.ts +29 -0
  20. package/src/schema/drivers/zod.ts +34 -19
  21. package/src/ui/components/patterns/action-form-card.tsx +18 -12
  22. package/src/ui/components/patterns/confirm.tsx +26 -3
  23. package/src/ui/components/patterns/content-header.tsx +335 -61
  24. package/src/ui/components/patterns/data-state.tsx +23 -10
  25. package/src/ui/components/patterns/form.tsx +1 -1
  26. package/src/ui/components/patterns/list.tsx +1096 -777
  27. package/src/ui/components/patterns/page-state.tsx +115 -0
  28. package/src/ui/components/patterns/page.tsx +231 -41
  29. package/src/ui/components/patterns/sidebar.tsx +354 -80
  30. package/src/ui/components/patterns/trigger.tsx +13 -9
  31. package/src/ui/components/patterns/view.tsx +7 -11
  32. package/src/ui/components/primitives/alert-dialog.tsx +7 -5
  33. package/src/ui/components/primitives/alert.tsx +298 -80
  34. package/src/ui/components/primitives/ask.tsx +2 -1
  35. package/src/ui/components/primitives/badge.tsx +91 -30
  36. package/src/ui/components/primitives/button.tsx +99 -60
  37. package/src/ui/components/primitives/calendar.tsx +39 -39
  38. package/src/ui/components/primitives/card.tsx +96 -23
  39. package/src/ui/components/primitives/detail.tsx +2 -2
  40. package/src/ui/components/primitives/dictionary-value.tsx +9 -14
  41. package/src/ui/components/primitives/dot.tsx +74 -21
  42. package/src/ui/components/primitives/drawer.tsx +33 -20
  43. package/src/ui/components/primitives/field.tsx +4 -4
  44. package/src/ui/components/primitives/item.tsx +137 -81
  45. package/src/ui/components/primitives/menu.tsx +11 -3
  46. package/src/ui/components/primitives/metric-card.tsx +133 -0
  47. package/src/ui/components/primitives/table.tsx +2 -2
  48. package/src/ui/components/primitives/tooltip.tsx +1 -1
  49. package/src/ui/docs/DocBrowser.tsx +3 -3
  50. package/src/ui/docs/changelog.tsx +1 -1
  51. package/src/ui/docs/content/action-form-card.md +1 -1
  52. package/src/ui/docs/content/alert-dialog.md +8 -8
  53. package/src/ui/docs/content/alert.md +54 -23
  54. package/src/ui/docs/content/badge.md +18 -19
  55. package/src/ui/docs/content/button.md +12 -9
  56. package/src/ui/docs/content/card.md +5 -5
  57. package/src/ui/docs/content/content.md +44 -0
  58. package/src/ui/docs/content/customization.md +2 -2
  59. package/src/ui/docs/content/detail.md +5 -2
  60. package/src/ui/docs/content/dialog.md +2 -2
  61. package/src/ui/docs/content/dictionary-value.md +11 -10
  62. package/src/ui/docs/content/dot.md +7 -7
  63. package/src/ui/docs/content/drawer.md +6 -3
  64. package/src/ui/docs/content/field.md +1 -1
  65. package/src/ui/docs/content/input-group.md +3 -2
  66. package/src/ui/docs/content/item.md +47 -21
  67. package/src/ui/docs/content/menu.md +5 -4
  68. package/src/ui/docs/content/metric-card.md +41 -0
  69. package/src/ui/docs/content/page-state.md +45 -0
  70. package/src/ui/docs/content/page.md +48 -10
  71. package/src/ui/docs/content/semantic-context.md +63 -0
  72. package/src/ui/docs/content/sidebar.md +4 -4
  73. package/src/ui/docs/content/skeleton.md +2 -2
  74. package/src/ui/docs/content/table.md +3 -3
  75. package/src/ui/docs/content/tokens.md +28 -0
  76. package/src/ui/docs/content/tooltip.md +15 -1
  77. package/src/ui/docs/doc-client.tsx +2 -2
  78. package/src/ui/docs/registry.tsx +596 -228
  79. package/src/ui/lib/semantic-context.ts +30 -0
  80. package/src/ui/meta.ts +292 -270
  81. package/src/ui/react.tsx +378 -111
  82. 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. Apresentar valor de dicionário por `DictionaryValue` ou pela coluna de `ActionList`, que
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 tonal, `plain` em texto). Não escolher badge, tom ou ícone pelo nome do dicionário:
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
- 4. Dar a cada dimensão independente usada para comparação ou filtro um campo, coluna ou espaço
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
- 5. Deixar ausência, paginação e largura de filtro com o pattern: célula e `DetailField` já
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
- 6. Tratar página, filtro, seleção e modal importante como estado navegável por URL quando
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
- 7. Compor páginas com `Page` e seu teto centralizado padrão de `72rem`. Usar
39
- `ContentHeader` em seções que precisam da mesma estrutura de título, descrição,
40
- metadados e ações; ajustar `level` pela hierarquia semântica, não pelo destaque visual.
41
- 8. Manter margem e posicionamento no consumidor; componente reutilizável controla apenas
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
- 9. Não definir a fonte raiz em uma biblioteca ou componente. O navegador e a aplicação são
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
- 10. Evoluir um pattern compartilhado apenas quando a recorrência e o contrato estiverem claros.
48
- 11. Tratar tokens de superfície como pares indivisíveis no mesmo fragmento de classes:
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
- 12. Usar diretamente a escala `rounded-*`; não criar radius por nome de componente quando
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
- 13. Distinguir uma região disponível para criação ou vínculo, representada por `Empty` com
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
- 14. Testar estados de loading, vazio, erro, sucesso, permissão e interação relevante.
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 tom ou ícone
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`, `ContentHeader` em uma seção,
7
- `ActionFilterBar` separado do renderer, `ItemGroup` para uma coleção secundária e um
8
- `ActionFormDialog`; provar teto padrão de `72rem`, hierarquia por `level`, vazio estrutural
9
- sólido, `Empty` apenas para criação ou vínculo, números sem alinhamento inferido e cancelamento
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>`, o container centralizado com teto padrão de `72rem` e o header
8
- de página. Alterar `className` apenas quando a superfície tiver uma necessidade real de
9
- largura; não reconstruir esse container em cada rota.
10
- - `ContentHeader` compartilha a composição de título, descrição, metadados e ações entre
11
- páginas e seções. `variant` define o destaque visual; `level` preserva separadamente a
12
- hierarquia semântica do heading.
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 `tone` | se a entrada declara `icon` do catálogo | se `description` acrescenta ao rótulo |
67
- | `status` | Aberto, resolvido, degradado | Badge | tonal pelo `tone`; `neutral` sem tom; nunca `outline` | idem | idem |
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', tone: 'success' } },
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="secondary">Cliente</Badge>
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 variant="success" /></TooltipTrigger>
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 `tone` somente em status e estágio, `icon` somente com nome existente no
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 `tone` em classificação, nem `icon` fora do catálogo, nem `description` que
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 `tone` explícito, sem `description` que repita o rótulo; provar que o manifest
15
- projeta `presentation` e que `t.dict` rejeita `presentation` ou `tone` fora do vocabulário.
16
- - Reprova: declarar `tone` em uma classificação, inventar `icon` fora do catálogo, copiar `doc`
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 variant={t.done ? 'default' : 'secondary'}>{t.done ? 'Feita' : 'Aberta'}</Badge>
29
+ <Badge context={t.done ? 'success' : 'neutral'}>{t.done ? 'Feita' : 'Aberta'}</Badge>
30
30
  </li>
31
31
  ))}
32
32
  </ul>
@@ -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
- /** Tom explícito de uma entrada — honrado apenas por status e estágios. */
17
- export const DICT_TONES = ['neutral', 'info', 'success', 'warning', 'danger'] as const
18
- export type DictTone = (typeof DICT_TONES)[number]
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
- * - `tone` é o tom tonal do badge de status/estágio; classificação ignora.
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
- tone?: DictTone
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
- export function isDictTone(value: unknown): value is DictTone {
48
- return typeof value === 'string' && (DICT_TONES as readonly string[]).includes(value)
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
- /** `null` = texto; `'outline'` = classificação; tom = badge tonal de status/estágio. */
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 | outline (ignora `tone`) |
119
- * | status / stage | badge | tonal: `tone` ?? neutral |
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 { value, label: value, known: false, presentation, badge: null, icon: null, description: null }
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 badge: DictionaryValuePresentation['badge'] =
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
- ? (isDictTone(entry.tone) ? entry.tone : 'neutral')
177
+ ? 'subtle'
142
178
  : null
143
179
  return {
144
180
  value,
145
181
  label: entry.label,
146
182
  known: true,
147
183
  presentation,
148
- badge,
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), `tone` (badge tonal de status/estágio), `icon` (catálogo).
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 tonal = opts?.presentation === 'status' || opts?.presentation === 'stage'
721
+ const contextual = opts?.presentation === 'status' || opts?.presentation === 'stage'
720
722
  for (const key of keys) {
721
- const tone = (entries[key] as DictEntryMeta).tone
722
- if (tone === undefined) continue
723
- if (!isDictTone(tone)) {
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
- if (!tonal) {
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 (entries[key] as M[keyof M]).label
767
+ return (stableEntries[key] as M[keyof M]).label
753
768
  },
754
- metaFor: (key) => entries[key] as M[keyof M],
769
+ metaFor: (key) => stableEntries[key] as M[keyof M],
755
770
  options: () =>
756
771
  keys.map((k) => ({
757
- ...(entries[k] as M[keyof M]),
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(entries, key),
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
- CardContent,
9
+ CardBody,
10
10
  CardDescription,
11
11
  CardFooter,
12
12
  CardHeader,
13
13
  CardTitle,
14
- } from '../primitives/card.tsx'
15
- import { ActionForm, type ActionFormProps } from './form.tsx'
14
+ } from "../primitives/card.tsx";
15
+ import { ActionForm, type ActionFormProps } from "./form.tsx";
16
16
 
17
- export interface ActionFormCardProps<TInput extends Record<string, unknown>, TData>
18
- extends Omit<ActionFormProps<TInput, TData>, 'body' | 'footer'> {
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 && <CardDescription>{description}</CardDescription>}
40
+ {description !== undefined && (
41
+ <CardDescription>{description}</CardDescription>
42
+ )}
39
43
  </CardHeader>
40
44
  )}
41
45
  <ActionForm
42
46
  {...rest}
43
- body={(fields) => <CardContent>{fields}</CardContent>}
44
- footer={(actions) => <CardFooter className="justify-end">{actions}</CardFooter>}
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
  }