@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
package/bin/lib/copy.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Inventário semântico de copy dos contratos Opus.
2
+ * Inventário semântico de copy dos contratos e dicionários Opus.
3
3
  *
4
- * O Opus conhece o papel dos textos declarados em defineAction/defineContract;
4
+ * O Opus conhece o papel dos textos declarados em defineAction/defineContract e t.dict;
5
5
  * a @softize/base continua dona da política e da validação editorial. Este módulo
6
6
  * apenas projeta o protocolo JSON v2 sem executar código do consumidor.
7
7
  */
@@ -61,6 +61,7 @@ const COPY_ROLES = new Set([
61
61
  const AUDIT_REFERENCE = /^(?:https?:\/\/\S+|[a-z][a-z0-9-]*:\S+|(?:[A-Za-z0-9._-]+\/)+[A-Za-z0-9._#/-]+|[A-Za-z0-9._-]+\.(?:md|json|ya?ml)(?:#[^\s]+)?)$/u
62
62
  const CONTRACT_MODULES = new Set(['@softize/opus', '@softize/opus/core'])
63
63
  const CONTRACT_FACTORIES = new Set(['defineAction', 'defineContract'])
64
+ const SCHEMA_ZOD_MODULES = new Set(['@softize/opus/schema/zod'])
64
65
  const ACTION_BINDING_KEYS = new Set(['authorize', 'background', 'emits', 'handler', 'idempotency', 'loads'])
65
66
 
66
67
  // Somente exports Opus cujo papel textual é estável por contrato do componente.
@@ -83,6 +84,8 @@ const JSX_CHILD_ROLES = new Map([
83
84
  ['CardTitle', 'title'],
84
85
  ['CommandEmpty', 'empty-state'],
85
86
  ['CommandItem', 'menu-item'],
87
+ ['ContentDescription', 'description'],
88
+ ['ContentTitle', 'title'],
86
89
  ['DialogDescription', 'dialog-body'],
87
90
  ['DialogTitle', 'title'],
88
91
  ['DrawerDescription', 'dialog-body'],
@@ -103,6 +106,8 @@ const JSX_CHILD_ROLES = new Map([
103
106
  ['MenuRadioItem', 'menu-item'],
104
107
  ['PopoverDescription', 'description'],
105
108
  ['PopoverTitle', 'title'],
109
+ ['PageDescription', 'description'],
110
+ ['PageTitle', 'title'],
106
111
  ['TableCaption', 'description'],
107
112
  ['TableHead', 'heading'],
108
113
  ['TabsTrigger', 'tab'],
@@ -129,6 +134,8 @@ const JSX_PROP_ROLES = new Map([
129
134
  ['ActionTrigger', new Map([['label', 'button']])],
130
135
  ['Alert', new Map([['title', 'title'], ['description', 'message']])],
131
136
  ['CommandInput', new Map([['placeholder', 'placeholder']])],
137
+ ['Content', new Map([['title', 'title'], ['description', 'description']])],
138
+ ['ContentHeader', new Map([['title', 'title'], ['description', 'description']])],
132
139
  ['DataState', new Map([['emptyText', 'empty-state'], ['errorText', 'error']])],
133
140
  // A Dock nomeia a barra e cada ação por prop. Sem estas linhas, a copy sairia do inventário
134
141
  // exatamente quando uma superfície migra de <Button aria-label> para <DockAction label>.
@@ -137,6 +144,7 @@ const JSX_PROP_ROLES = new Map([
137
144
  ['Input', new Map([['placeholder', 'placeholder']])],
138
145
  ['InputGroupInput', new Map([['placeholder', 'placeholder']])],
139
146
  ['InputGroupTextarea', new Map([['placeholder', 'placeholder']])],
147
+ ['MetricCard', new Map([['label', 'label'], ['description', 'description']])],
140
148
  ['Page', new Map([['title', 'title'], ['description', 'description']])],
141
149
  ['Select', new Map([
142
150
  ['placeholder', 'placeholder'],
@@ -182,9 +190,12 @@ const JSX_PROP_CLASS = new Map([
182
190
  ['ActionTrigger', new Map([['label', 'className']])],
183
191
  ['Alert', new Map([['title', 'className'], ['description', 'className']])],
184
192
  ['CommandInput', new Map([['placeholder', 'className']])],
193
+ ['Content', new Map([['title', 'className'], ['description', 'className']])],
194
+ ['ContentHeader', new Map([['title', 'className'], ['description', 'className']])],
185
195
  ['Input', new Map([['placeholder', 'className']])],
186
196
  ['InputGroupInput', new Map([['placeholder', 'className']])],
187
197
  ['InputGroupTextarea', new Map([['placeholder', 'className']])],
198
+ ['MetricCard', new Map([['label', 'className'], ['description', 'className']])],
188
199
  ['Page', new Map([['title', 'className'], ['description', 'className']])],
189
200
  ['Select', new Map([['placeholder', 'className']])],
190
201
  ['Textarea', new Map([['placeholder', 'className']])],
@@ -199,6 +210,7 @@ const JSX_PROP_STYLE = new Map([
199
210
  ['Input', new Map([['placeholder', 'style']])],
200
211
  ['InputGroupInput', new Map([['placeholder', 'style']])],
201
212
  ['InputGroupTextarea', new Map([['placeholder', 'style']])],
213
+ ['MetricCard', new Map([['label', 'style'], ['description', 'style']])],
202
214
  ['Textarea', new Map([['placeholder', 'style']])],
203
215
  ])
204
216
 
@@ -397,6 +409,18 @@ function knownContractFactory(call, checker) {
397
409
  return name !== null && CONTRACT_FACTORIES.has(name)
398
410
  }
399
411
 
412
+ function knownDictionaryFactory(call, checker) {
413
+ const expression = resolveBinding(call.expression, checker)
414
+ if (!ts.isPropertyAccessExpression(expression) && !ts.isElementAccessExpression(expression)) return false
415
+ const member = ts.isPropertyAccessExpression(expression)
416
+ ? expression.name.text
417
+ : expression.argumentExpression === undefined
418
+ ? null
419
+ : staticText(expression.argumentExpression, checker)?.text ?? null
420
+ return member === 'dict' &&
421
+ importedMemberName(expression.expression, checker, (module) => SCHEMA_ZOD_MODULES.has(module)) === 't'
422
+ }
423
+
400
424
  function knownBindAction(call, checker) {
401
425
  return importedMemberName(call.expression, checker, (module) => CONTRACT_MODULES.has(module)) === 'bindAction'
402
426
  }
@@ -512,7 +536,10 @@ function stableExpressionUse(node, checker, mode, stack) {
512
536
  ts.isSpreadAssignment(parent) || ts.isSpreadElement(parent) || ts.isArrayLiteralExpression(parent)
513
537
  ) return consumerMode === 'aggregate' && stableComposition(carrier, checker, stack)
514
538
  if (ts.isCallExpression(parent) && parent.arguments.some((argument) => argument === carrier)) {
515
- if (consumerMode === 'aggregate') return knownContractFactory(parent, checker) && stableFactoryResult(parent, checker, stack)
539
+ if (consumerMode === 'aggregate') {
540
+ if (knownDictionaryFactory(parent, checker)) return true
541
+ return knownContractFactory(parent, checker) && stableFactoryResult(parent, checker, stack)
542
+ }
516
543
  if (
517
544
  consumerMode === 'factory-result' && parent.arguments[0] === carrier &&
518
545
  knownBindAction(parent, checker) && safeBindActionBinding(parent, checker)
@@ -865,14 +892,15 @@ function diagnostic(file, sourceFile, node, field, category = 'content') {
865
892
  /**
866
893
  * Extrai as superfícies humanas de um source isolado.
867
894
  *
868
- * O retorno distingue arquivo sem contrato de contrato sem copy. Essa diferença é
869
- * necessária para hashear todo contrato e detectar a adição posterior de um texto.
895
+ * O retorno distingue arquivo sem superfície Opus de contrato sem copy. Essa diferença é
896
+ * necessária para hashear todo contrato ou dicionário e detectar a adição posterior de texto.
870
897
  */
871
898
  export function extractCopyFromSource(file, sourceText) {
872
899
  const { sourceFile, checker } = parseSource(file, sourceText)
873
900
  const entries = []
874
901
  const diagnostics = []
875
902
  let hasContract = false
903
+ let hasDictionary = false
876
904
  const hasUiImport = sourceFile.statements.some(
877
905
  (statement) =>
878
906
  ts.isImportDeclaration(statement) &&
@@ -999,6 +1027,42 @@ export function extractCopyFromSource(file, sourceText) {
999
1027
  nestedObject(object, 'errors', (error, prefix) => addProperty(error, 'description', 'error', prefix))
1000
1028
  }
1001
1029
 
1030
+ function extractDictionary(call) {
1031
+ const entriesNode = call.arguments[0]
1032
+ const entries = resolveBinding(entriesNode, checker)
1033
+ if (!ts.isObjectLiteralExpression(entries)) {
1034
+ diagnostics.push(diagnostic(file, sourceFile, entriesNode, 't.dict entries', 'structure'))
1035
+ return
1036
+ }
1037
+
1038
+ const listed = properties(entries, sourceFile, checker)
1039
+ for (const opaque of listed.opaque) {
1040
+ diagnostics.push(diagnostic(file, sourceFile, opaque, 't.dict entries', 'structure'))
1041
+ }
1042
+ for (const [key, entry] of listed.items) {
1043
+ const valueNode = propertyValue(entry)
1044
+ const value = resolveBinding(valueNode, checker)
1045
+ if (!ts.isObjectLiteralExpression(value)) {
1046
+ diagnostics.push(diagnostic(file, sourceFile, valueNode, `t.dict.${key}`, 'structure'))
1047
+ continue
1048
+ }
1049
+ addProperty(value, 'label', 'label', `t.dict.${key}.`)
1050
+ addProperty(value, 'description', 'description', `t.dict.${key}.`)
1051
+ addProperty(value, 'doc', 'description', `t.dict.${key}.`)
1052
+ }
1053
+
1054
+ if (call.arguments.length < 2) return
1055
+ const optionsNode = call.arguments[1]
1056
+ const options = resolveBinding(optionsNode, checker)
1057
+ if (ts.isIdentifier(options) && options.text === 'undefined') {
1058
+ const symbol = checker.getSymbolAtLocation(options)
1059
+ const shadowed = symbol?.declarations?.some((declaration) => declaration.getSourceFile() === sourceFile) ?? false
1060
+ if (!shadowed) return
1061
+ }
1062
+ if (ts.isObjectLiteralExpression(options)) addProperty(options, 'doc', 'description', 't.dict.options.')
1063
+ else diagnostics.push(diagnostic(file, sourceFile, optionsNode, 't.dict options', 'structure'))
1064
+ }
1065
+
1002
1066
  function importedMember(node, modulePredicate) {
1003
1067
  return importedMemberName(node, checker, modulePredicate)
1004
1068
  }
@@ -1790,12 +1854,16 @@ export function extractCopyFromSource(file, sourceText) {
1790
1854
  else diagnostics.push(diagnostic(file, sourceFile, node.arguments[0], `${factory}(...)`, 'structure'))
1791
1855
  }
1792
1856
  }
1857
+ if (ts.isCallExpression(node) && knownDictionaryFactory(node, checker) && node.arguments.length > 0) {
1858
+ hasDictionary = true
1859
+ extractDictionary(node)
1860
+ }
1793
1861
  if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) extractJsx(node)
1794
1862
  ts.forEachChild(node, visit)
1795
1863
  }
1796
1864
  visit(sourceFile)
1797
1865
 
1798
- return { hasContract, hasCopySurface: hasContract || hasUiImport, entries, diagnostics }
1866
+ return { hasContract, hasCopySurface: hasContract || hasDictionary || hasUiImport, entries, diagnostics }
1799
1867
  }
1800
1868
 
1801
1869
  function globRegex(pattern) {
@@ -3,6 +3,9 @@
3
3
  - Status: aceita
4
4
  - Data: 2026-09-02
5
5
 
6
+ > A ADR 0006 substitui o nome `tone` por `context` para a família semântica e define sua migração
7
+ > compatível. O princípio desta ADR — apresentação declarada e não inferida — permanece vigente.
8
+
6
9
  ## Contexto e forças
7
10
 
8
11
  `t.dict` declara vocabulário fechado uma única vez: código estável, rótulo, documentação de
@@ -0,0 +1,65 @@
1
+ # ADR 0004 — O estado integral do conteúdo é composto dentro de Page
2
+
3
+ ## Contexto
4
+
5
+ `Page` padroniza o `<main>`, o cabeçalho e o container de uma página. Hoje, consumidores que ainda
6
+ não têm conteúdo para mostrar repetem spinners, alertas e vazios diretamente em `children`. Essas
7
+ composições divergem visualmente e nem sempre distinguem carregamento, ausência e falha ou oferecem
8
+ uma ação de recuperação.
9
+
10
+ Ao mesmo tempo, uma página pode agregar várias fontes independentes. Fazer `Page` receber flags de
11
+ carregamento ou erro faria o esqueleto decidir quando toda a página deve desaparecer por causa de
12
+ uma única fonte.
13
+
14
+ ## Decisão
15
+
16
+ `Page` continua presentacional e sem conhecimento de dados. O Opus fornece `PageState` como pattern
17
+ composto para o estado integral da área de conteúdo. Na forma curta ele pode ser escrito como filho
18
+ direto de `Page`, que materializa `PageBody`; na composição explícita, `PageState` fica dentro de
19
+ `PageBody`. Ele é usado quando o conteúdo principal inteiro está carregando, falhou ou está vazio.
20
+
21
+ `PageState`:
22
+
23
+ - recebe um estado discriminado entre `loading`, `error`, `empty` e `ready`;
24
+ - preserva o cabeçalho da página em todos os estados;
25
+ - compõe `Spinner`, `Alert` e `Empty` em vez de recriar suas superfícies;
26
+ - aceita título, descrição, ícone e ação contextual sem exibir erro técnico;
27
+ - expõe `data-slot="page-state"` e `data-status` para testes e análise estrutural;
28
+ - renderiza o conteúdo sem moldura adicional em `ready`.
29
+
30
+ Estados parciais continuam pertencendo a `DataState`, `ActionView`, `ActionList` ou `Alert`, conforme
31
+ a fronteira afetada. Uma coleção vazia continua dentro da estrutura da coleção; `Empty` representa
32
+ uma região disponível, uma escolha pendente ou uma próxima ação.
33
+
34
+ ## Alternativas consideradas
35
+
36
+ ### Flags diretamente em Page
37
+
38
+ Rejeitada porque mistura layout com aquisição de dados e torna ambígua a precedência entre várias
39
+ fontes da mesma página.
40
+
41
+ ### Usar somente DataState
42
+
43
+ Rejeitada como contrato único porque o modo bloco de `DataState` também atende seções menores e não
44
+ expressa que o conteúdo principal inteiro está indisponível. `PageState` pode compartilhar as mesmas
45
+ primitivas sem confundir as duas escalas.
46
+
47
+ ### Manter composições locais
48
+
49
+ Rejeitada porque mantém diferenças de altura, borda, copy, recuperação e semântica acessível entre
50
+ telas equivalentes.
51
+
52
+ ## Consequências
53
+
54
+ - Consumidores ganham uma composição uniforme sem acoplar `Page` a hooks ou actions.
55
+ - A copy específica do fluxo permanece no consumidor; defaults seguros cobrem usos simples.
56
+ - Migrações precisam distinguir estado integral de estado parcial antes de substituir a composição.
57
+ - O gate estrutural do consumidor deve impedir novos estados integrais montados manualmente.
58
+
59
+ ## Verificação
60
+
61
+ - Testes do Opus cobrem os quatro estados, slots, precedência do conteúdo e ação contextual.
62
+ - A documentação do catálogo demonstra o uso na forma curta e sua posição dentro de `PageBody` na
63
+ composição explícita.
64
+ - Consumidores verificam `data-slot="page-state"` nos estados integrais e mantêm testes próprios para
65
+ recuperação e distinção entre erro e vazio.
@@ -0,0 +1,97 @@
1
+ # ADR 0005 — Superfícies estruturais compartilham uma anatomia explícita
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-09-04.
5
+
6
+ ## Contexto
7
+
8
+ Os componentes estruturais da UI descrevem regiões equivalentes com APIs diferentes. `Dialog`
9
+ expõe header, título, descrição, body e footer; `Card` chama o corpo de `Content`; e `Page`
10
+ recebe título, descrição e ações somente por propriedades, sem expor seus elementos estruturais.
11
+ `ContentHeader`, por sua vez, pode aparecer solto, embora seus nomes sugiram uma família `Content`.
12
+
13
+ Essa variação obriga quem consome a biblioteca a reaprender a composição em cada superfície e
14
+ impede que análise estática verifique relações como “um título pertence ao header da sua família”.
15
+ Ao mesmo tempo, a forma curta de `Page` e `ContentHeader` atende bem ao caso comum e não precisa ser
16
+ perdida para obter uma estrutura explícita.
17
+
18
+ ## Decisão
19
+
20
+ As superfícies mantidas pela casa adotam a gramática `Root > Header + Body + Footer`, com
21
+ `Title`, `Description`, `Meta` e `Actions` pertencendo ao `Header` da mesma família.
22
+
23
+ - `Page` oferece `PageHeader`, `PageTitle`, `PageDescription`, `PageMeta`, `PageActions` e
24
+ `PageBody`.
25
+ - `Content` representa uma região de conteúdo semanticamente nomeada e oferece `ContentHeader`,
26
+ `ContentTitle`, `ContentDescription`, `ContentMeta`, `ContentActions` e `ContentBody`.
27
+ - `CardContent` passa a ter `CardBody` como nome canônico.
28
+ - `Drawer` recebe `DrawerBody`.
29
+ - `PaneContent` passa a ter `PaneBody` como nome canônico.
30
+ - `Dialog` permanece como referência porque já possui `DialogHeader` e `DialogBody`.
31
+ - `Alert` oferece `AlertMedia`, `AlertHeader`, `AlertTitle`, `AlertDescription` e
32
+ `AlertActions`; a forma curta por propriedades materializa esses mesmos slots.
33
+ - `Item` oferece `ItemMedia`, `ItemHeader`, `ItemTitle`, `ItemDescription`, `ItemBody`,
34
+ `ItemActions` e `ItemFooter`. `ItemContent` permanece temporariamente como alias legado de
35
+ corpo, mas deixa de envolver título e descrição no código novo.
36
+
37
+ `Page` e `Content` aceitam também uma forma curta com `title`, `description`, `meta` e `actions`.
38
+ Essa forma é açúcar sintático: produz a mesma árvore semântica, os mesmos estilos e os mesmos
39
+ `data-slot` da composição explícita. Um consumidor não pode misturar as duas formas na mesma raiz.
40
+
41
+ `ContentHeader` deixa de ser uma região solta e passa a pertencer a `Content`. A forma histórica
42
+ por propriedades continua disponível temporariamente dentro de `Content`, mas a composição
43
+ explícita usa os slots da família.
44
+
45
+ `Content` técnico mantém seu nome quando representa o contêiner montado por uma primitiva ou um
46
+ painel controlado, como `DialogContent`, `PopoverContent`, `TabsContent` e `AccordionContent`.
47
+ `EmptyContent` e `CarouselContent` também permanecem porque não representam o body de uma
48
+ superfície estrutural.
49
+
50
+ Nas superfícies horizontais, `Media` e `Header` formam a mesma linha estrutural. A região visual
51
+ de `Media` tem largura estável e se estende pela altura útil do header, mantendo seu conteúdo
52
+ centralizado. Assim, título e descrição de uma linha não deixam uma sobra inferior ao lado da
53
+ moldura; conteúdo textual maior continua determinando naturalmente a altura da linha.
54
+
55
+ ## Consequências
56
+
57
+ - A API comum fica previsível sem obrigar o caso simples a escrever todos os slots.
58
+ - A forma explícita permite composição e extensão sem reconstruir o layout da biblioteca.
59
+ - Em `Page` e `Content`, tipos separam as props das duas formas, contexto em runtime protege os
60
+ slots e `opus check` verifica a anatomia JSX completa. Nas famílias históricas, o lint reconhece
61
+ wrappers transparentes e render props: reprova uma família visivelmente errada sem proibir que
62
+ um slot seja encapsulado por um componente reutilizável.
63
+ - Aliases históricos permanecem durante a versão 12 e podem ser removidos numa versão major.
64
+ - Consumidores precisam migrar `ContentHeader` solto para `Content` e podem migrar as demais formas
65
+ gradualmente enquanto os aliases existirem.
66
+ - Alertas e itens simples ganham uma hierarquia igual sem perder suas semânticas distintas:
67
+ `Alert` comunica estado e `Item` representa uma entidade ou opção numa coleção.
68
+
69
+ ## Alternativas consideradas
70
+
71
+ ### Manter APIs diferentes por componente
72
+
73
+ Preservaria compatibilidade total, mas manteria a carga cognitiva e impediria uma regra estrutural
74
+ comum. Foi descartada porque as diferenças não representam comportamentos distintos.
75
+
76
+ ### Exigir somente composição explícita
77
+
78
+ Produziria uma API uniforme, mas tornaria páginas e regiões simples desnecessariamente verbosas.
79
+ Foi descartada porque shorthand e slots podem convergir para uma única implementação.
80
+
81
+ ### Renomear `ContentHeader` sem criar `Content`
82
+
83
+ Nomes como `SectionHeader` pressupõem outro pai; nomes como `GenericHeader` descrevem ausência de
84
+ semântica; e `HeadingBlock` abandona a gramática das demais famílias. Foi descartada em favor de
85
+ dar a `ContentHeader` um pai estrutural real.
86
+
87
+ ## Verificação
88
+
89
+ - Testes de UI comparam DOM, acessibilidade e `data-slot` das formas curta e explícita.
90
+ - Testes de runtime proíbem a mistura das formas e os slots de `Page`/`Content` usados fora da
91
+ família correspondente; essa relação entre elementos JSX não é representável somente pelo tipo
92
+ de `children` do React.
93
+ - `opus check` reprova relações JSX estruturais inválidas nos consumidores.
94
+ - Testes de layout verificam que `AlertMedia` e `ItemMedia` se estendem pela linha do header e
95
+ centralizam o ícone, sem fixar a altura do conteúdo textual.
96
+ - Documentação e metadados apresentam a forma curta como caminho comum e a composição explícita
97
+ como caminho de extensão.
@@ -0,0 +1,182 @@
1
+ # ADR 0006 — Contexto semântico precede variante visual
2
+
3
+ - Status: aceita
4
+ - Data: 2026-09-04
5
+
6
+ ## Contexto e forças
7
+
8
+ Os componentes do Opus usam `variant` para eixos diferentes. Em Button, Badge, Alert e Dot, a
9
+ prop mistura hierarquia (`default`, `secondary`), contexto semântico (`success`, `warning`,
10
+ `destructive`) e tratamento visual (`outline`, `ghost`, `link`). Em Table, Detail, Tabs e Item,
11
+ `variant` descreve apenas uma alternativa estrutural ou visual local (`plain`, `framed`, `line`).
12
+
13
+ Ao mesmo tempo, entradas de `t.dict` usam `tone` para selecionar a família semântica de status e
14
+ estágios. Essa família não coincide com os componentes: o estado vermelho é `danger` no dicionário,
15
+ `destructive` em alguns primitives, e `MetricCard tone="warning"` atualmente usa tokens vermelhos.
16
+ Um agente ou consumidor precisa conhecer cada exceção para obter a mesma linguagem visual.
17
+
18
+ As forças em tensão são:
19
+
20
+ - o significado precisa permanecer independente da forma concreta com que cada componente o
21
+ apresenta;
22
+ - a API deve ser previsível entre primitives sem transformar todas as combinações em opções
23
+ válidas para todos os componentes;
24
+ - `destructive` precisa continuar descrevendo o risco comportamental de uma ação, sem se tornar o
25
+ nome da família visual vermelha;
26
+ - dicionários e componentes existentes precisam de uma migração explícita e verificável;
27
+ - agentes precisam aprender a regra por contratos, documentação e avaliações, não por memória ou
28
+ inferência a partir de exemplos isolados;
29
+ - light mode e dark mode são temas, não significados de produto.
30
+
31
+ ## Alternativas consideradas
32
+
33
+ ### Manter `tone` nos dicionários e `variant` nos componentes
34
+
35
+ Preserva compatibilidade imediata, mas mantém dois nomes para o mesmo eixo e deixa `variant`
36
+ misturar significado com apresentação. Cada novo componente precisaria repetir mapas locais.
37
+ Rejeitada.
38
+
39
+ ### Copiar literalmente as variantes contextuais do Bootstrap
40
+
41
+ O vocabulário `primary`, `secondary`, `success`, `danger`, `warning`, `info`, `light` e `dark` é
42
+ conhecido e cobre grande parte dos casos. Porém, o Bootstrap chama de variante tanto o contexto
43
+ quanto sua materialização (`btn-danger`, `btn-outline-danger`), mistura hierarquia com semântica e
44
+ inclui `light`/`dark`, que pertencem ao tema. Copiar a API manteria a ambiguidade que queremos
45
+ remover. Rejeitada como contrato literal e aceita como referência de vocabulário.
46
+
47
+ ### Declarar contexto e variante como eixos independentes
48
+
49
+ `context` escolhe a família de tokens e responde por que existe o realce. `variant` escolhe como a
50
+ família aparece naquele componente. Cada primitive aceita somente o subconjunto coerente com seu
51
+ papel e fornece defaults. É uma mudança maior, mas torna combinações e exceções explícitas e permite
52
+ que domínio, UI e agentes compartilhem o mesmo modelo. Aceita.
53
+
54
+ ## Decisão
55
+
56
+ ### Vocabulário contextual
57
+
58
+ O Opus define o vocabulário canônico:
59
+
60
+ ```ts
61
+ type UiContext =
62
+ | 'neutral'
63
+ | 'primary'
64
+ | 'info'
65
+ | 'success'
66
+ | 'warning'
67
+ | 'danger'
68
+ ```
69
+
70
+ - `neutral`: estado normal, inativo ou sem julgamento positivo/negativo;
71
+ - `primary`: ação ou elemento de maior destaque no contexto atual;
72
+ - `info`: informação ou processo em andamento sem alerta;
73
+ - `success`: resultado positivo ou estado saudável;
74
+ - `warning`: condição que pede atenção, mas não representa falha;
75
+ - `danger`: falha, impedimento ou consequência perigosa.
76
+
77
+ `secondary` não é contexto universal: ação secundária é hierarquia, enquanto estado neutro é
78
+ semântica. `light` e `dark` permanecem modos de cor. Nenhum dos três entra em `UiContext`.
79
+
80
+ Entradas de dicionário aceitam `DictContext`, o subconjunto sem `primary`, porque um estado de
81
+ domínio não se torna a ação principal da interface. A metadata canônica passa de `tone` para
82
+ `context`.
83
+
84
+ ### Variante visual
85
+
86
+ Para componentes semânticos, `variant` descreve somente o tratamento visual:
87
+
88
+ ```ts
89
+ type SemanticVariant = 'solid' | 'subtle' | 'outline' | 'ghost' | 'link'
90
+ ```
91
+
92
+ Cada componente aceita apenas as variantes que consegue materializar com coerência. Os defaults
93
+ iniciais são:
94
+
95
+ | Componente | Contexto padrão | Variante padrão | Contextos permitidos |
96
+ | --- | --- | --- | --- |
97
+ | Button | `primary` | `solid` | `neutral`, `primary`, `danger` |
98
+ | Badge | `neutral` | `subtle` | todos |
99
+ | Alert | `neutral` | `subtle` | `neutral`, `info`, `success`, `warning`, `danger` |
100
+ | Dot | `neutral` | `solid` | `neutral`, `primary`, `info`, `success`, `warning`, `danger` |
101
+ | MetricCard | `neutral` | `subtle` no ícone | `neutral`, `info`, `success`, `warning`, `danger` |
102
+
103
+ `solid`, `subtle` e `outline` podem compartilhar um contexto sem compartilhar classes. `ghost` e
104
+ `link` ficam restritos a controles interativos. Cor e ícone continuam reforços: texto ou nome
105
+ acessível comunica o significado.
106
+
107
+ Componentes cuja `variant` é exclusivamente estrutural ou local, como `plain | framed` e
108
+ `default | line`, podem mantê-la. Ao criar API nova, preferir uma prop específica quando o nome do
109
+ eixo for mais claro (`frame`, `layout`, `appearance`); não renomear primitives existentes sem ganho
110
+ observável.
111
+
112
+ ### Ações destrutivas
113
+
114
+ `destructive` permanece em contratos de ação e confirmação para declarar risco, confirmação e
115
+ comportamento. A projeção visual desse risco usa `context="danger"`. Portanto, `destructive` não é
116
+ um `UiContext` nem uma variante visual canônica.
117
+
118
+ ### Compatibilidade
119
+
120
+ A migração ocorre em uma janela explícita:
121
+
122
+ 1. componentes e `t.dict` passam a aceitar a API canônica e os nomes antigos como aliases
123
+ depreciados;
124
+ 2. informar os dois eixos antigo e novo ao mesmo tempo é inválido quando houver ambiguidade;
125
+ 3. renderers normalizam aliases antes de escolher tokens;
126
+ 4. manifest, documentação e exemplos gerados projetam somente `context` como forma canônica;
127
+ 5. consumidores são migrados e um gate impede novos usos semânticos de `tone` e de variantes como
128
+ `success`, `warning`, `danger` ou `destructive`;
129
+ 6. os aliases são removidos somente em uma versão major posterior, depois de o inventário chegar a
130
+ zero.
131
+
132
+ Usos locais de `tone` que não representam contexto semântico não são convertidos automaticamente.
133
+ Por exemplo, relações de diagrama com valores `optional`, `same` e `return` devem receber um nome
134
+ de domínio como `kind` ou `relation`, não `UiContext`.
135
+
136
+ ## Orientação para agentes
137
+
138
+ A regra precisa alcançar uma IA por fontes complementares e verificáveis:
139
+
140
+ 1. **Contrato compilável:** `UiContext`, `DictContext` e os tipos de props limitam as combinações
141
+ possíveis e são a fonte primária.
142
+ 2. **Catálogo do Opus:** documentação e metadata de cada componente explicam contexto, variante,
143
+ defaults e exceções com exemplos canônicos.
144
+ 3. **Skills:** `build-opus-ui` ensina a matriz `context × variant`; `model-opus-dictionary` exige
145
+ `context` em status e estágios e proíbe inferência pela chave ou pelo nome do dicionário.
146
+ 4. **Avaliações:** casos positivos, negativos e de execução reprovam `variant="success"`,
147
+ `tone="warning"` sem compatibilidade justificada, `destructive` como cor e comunicação somente
148
+ por cor.
149
+ 5. **Instrução permanente:** a instrução materializada do Opus resume a regra e encaminha às
150
+ skills; não replica toda a tabela.
151
+ 6. **Gate estático:** o check do Opus detecta novas ocorrências legadas fora de arquivos de
152
+ compatibilidade, testes de migração e exemplos negativos explicitamente marcados.
153
+ 7. **Materialização:** mudanças no registry são propagadas por `opus setup`; o projeto consumidor
154
+ valida que `.agents` e os adapters suportados não divergiram da versão declarada.
155
+
156
+ Essa redundância é intencional: tipos impedem combinações inválidas no código, documentação apoia
157
+ decisões humanas, skills orientam o fluxo e o gate detecta regressão mesmo quando uma instrução não
158
+ for consultada.
159
+
160
+ ## Consequências
161
+
162
+ - Primitives semânticos passam a compartilhar um vocabulário e tokens contextuais.
163
+ - Defaults podem variar por componente, mas a mesma palavra nunca muda de significado.
164
+ - A API fica mais explícita em chamadas que precisam dos dois eixos.
165
+ - A transição aumenta temporariamente tipos, testes e normalização por causa dos aliases.
166
+ - Temas precisam oferecer tokens de superfície, borda, texto de ênfase e sólido para cada contexto,
167
+ em light e dark mode.
168
+ - Consumidores com classes Tailwind semânticas literais não são automaticamente corretos; a
169
+ migração precisa classificar se representam contexto, visualização de dados ou linguagem própria
170
+ de um diagrama.
171
+
172
+ ## Verificação
173
+
174
+ - testes puros cobrem o vocabulário, aliases e combinações inválidas;
175
+ - testes de cada primitive cobrem a matriz aceita e seus defaults;
176
+ - testes de `t.dict`, descriptor e manifest comprovam `context` e a compatibilidade de leitura;
177
+ - testes de renderização provam que `DictionaryValue` usa `context` sem a tela escolher classes;
178
+ - avaliações das skills cobrem seleção, execução correta e exemplos que devem ser recusados;
179
+ - o validador de skills passa no registry e nas cópias materializadas;
180
+ - o gate estático reprova novas variantes semânticas e novos `tone` públicos;
181
+ - `opus check`, testes, typecheck, build, `opus copy --check` e gates do consumidor permanecem
182
+ verdes.
@@ -92,16 +92,20 @@ Base instalada é dona do catálogo, dos kinds aceitos e do fundamento de cada r
92
92
  | `Select.emptyText`, `Select.searchPlaceholder` | `empty-state`, `placeholder` |
93
93
  | `Select.options[].hint/triggerLabel/group` | `label`, `label`, `heading` |
94
94
  | `ActionTrigger.confirm.*` | papel correspondente do diálogo |
95
+ | `t.dict` — `label`, `description`, `doc` das entradas e `doc` do dicionário | `label`, `description` |
95
96
 
96
97
  ## Cobertura e significado do gate verde
97
98
 
98
- O extrator cobre duas superfícies, sem heurística de nome:
99
+ O extrator cobre três superfícies, sem heurística de nome:
99
100
 
100
101
  1. propriedades estáticas de `defineAction` e `defineContract` descritas acima, somente
101
102
  quando a factory resolve por símbolo a um import de `@softize/opus` ou
102
103
  `@softize/opus/core`; alias, namespace e destructuring `const` de namespace funcionam,
103
104
  homônimo local não é contrato;
104
- 2. texto estático em filhos e props de uma allowlist de componentes importados diretamente
105
+ 2. `label`, `description` e `doc` estáticos de `t.dict` importado de
106
+ `@softize/opus/schema/zod`; metadata livre não é classificada por nome, declarações dinâmicas
107
+ reprovam em vez de serem executadas e o dicionário mantém um snapshot imutável das entradas;
108
+ 3. texto estático em filhos e props de uma allowlist de componentes importados diretamente
105
109
  de `@softize/opus/ui*` — por exemplo, `Button`, `DialogTitle`, `DialogDescription`,
106
110
  `FieldLabel`, `TabsTrigger`, `Page`, `Input` e `DataState`. Alias e namespace de import
107
111
  continuam rastreáveis. `uppercase`, variantes como `sm:hover:uppercase`, modificadores
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softize/opus",
3
- "version": "12.9.0",
3
+ "version": "12.11.0",
4
4
  "description": "End-to-end action protocol for TypeScript. Single package with subpath exports (core + adapters).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -15,6 +15,11 @@ artefatos de agentes fica em `base.json`; cada app Opus mantém seu marcador `op
15
15
  - Manter contrato compartilhável separado de banco, segredo e driver server-only; usar
16
16
  `defineContract` com `bindAction` quando cliente e servidor consomem a mesma action.
17
17
  - Não duplicar schemas, tipos de transporte, validação ou fetch que o contrato já fornece.
18
+ - Em UI semântica, declarar primeiro `context` (`neutral`, `primary`, `info`, `success`, `warning`
19
+ ou `danger`) e usar `variant` somente para o tratamento visual (`solid`, `subtle`, `outline`,
20
+ `ghost` ou `link`). Dicionários de status e estágio declaram `context`; `tone` e variantes
21
+ semânticas antigas são apenas compatibilidade de migração. `destructive` permanece uma
22
+ propriedade comportamental de actions e se projeta visualmente como `danger`.
18
23
  - Declarar datasets persistentes com `defineSeed` + `bindSeed`, registrá-los em `opus.config.ts`
19
24
  e operá-los por `opus seed`; não criar comandos de seed paralelos nem reset implícito.
20
25
  - Regenerar o inventário com `opus copy` quando mudar copy em contrato ou componente Opus