@softize/opus 22.2.0 → 23.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/bin/cli.mjs +1 -9
  3. package/bin/lib/check.mjs +1 -7
  4. package/bin/lib/copy.mjs +1 -1
  5. package/bin/lib/gen-runner.mjs +2 -0
  6. package/bin/lib/gen-stubs.mjs +3 -1
  7. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +1 -1
  8. package/docs/adr/0032-tab-baseline-respects-the-container-inset.md +16 -12
  9. package/docs/adr/0034-tabs-can-own-a-frame-without-owning-a-surface.md +8 -6
  10. package/docs/adr/0036-tabs-declares-trigger-distribution.md +11 -8
  11. package/docs/adr/0037-design-data-lives-outside-the-contract.md +1 -1
  12. package/docs/adr/0038-content-level-defines-primary-presentation.md +62 -0
  13. package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +62 -0
  14. package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +60 -0
  15. package/docs/code-style.md +1 -1
  16. package/package.json +1 -1
  17. package/registry/skills/build-opus-ui/references/ui-patterns.md +1 -1
  18. package/registry/skills/create-opus-action/references/contract-and-binding.md +3 -3
  19. package/src/core/contracts.ts +1 -1
  20. package/src/core/runtime.ts +4 -37
  21. package/src/core/types.ts +5 -23
  22. package/src/design/index.ts +14 -3
  23. package/src/testing/index.ts +6 -26
  24. package/src/ui/components/patterns/content-header.tsx +5 -9
  25. package/src/ui/components/patterns/list.tsx +55 -1
  26. package/src/ui/components/patterns/presentation.tsx +1 -1
  27. package/src/ui/components/primitives/icon-picker.tsx +6 -0
  28. package/src/ui/components/primitives/tabs.tsx +55 -87
  29. package/src/ui/components/primitives/toggle.tsx +1 -1
  30. package/src/ui/docs/content/action-list.md +13 -4
  31. package/src/ui/docs/content/actions.md +6 -7
  32. package/src/ui/docs/content/content.md +5 -6
  33. package/src/ui/docs/content/semantic-context.md +0 -1
  34. package/src/ui/docs/content/tabs.md +68 -69
  35. package/src/ui/docs/content/testing.md +2 -3
  36. package/src/ui/docs/content/toggle.md +15 -10
  37. package/src/ui/drivers/react.tsx +4 -3
  38. package/src/ui/meta.ts +2 -2
  39. package/src/ui/react.tsx +2 -3
  40. package/src/vite/design.ts +23 -21
package/CHANGELOG.md CHANGED
@@ -7,6 +7,44 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
7
7
  `opus copy --check` · `base copy check` · `manifest:check`) — eles apontam o que a
8
8
  mudança cobra do seu código.
9
9
 
10
+ ## 23.0.0 — 2026-09-29
11
+
12
+ Esta major conclui a separação entre contrato e dados de design, simplifica a hierarquia de
13
+ `Content` e dá a Tabs uma anatomia pública única para navegação. A seção **Breaking** reúne as
14
+ migrações obrigatórias.
15
+
16
+ ### Breaking
17
+
18
+ `mockHandler` foi removido de `ActionBase`, contratos, actions de lista, runtime e `runAction`,
19
+ concluindo a ADR 0037. O core não lê mais `OPUS_MODE`: o plugin `opusDesign()` liga cenários ou
20
+ respostas geradas como handlers antes de registrar as actions no runtime. Migração: remova cada
21
+ `mockHandler`; quando a resposta específica importar para o design, transforme-a em cenário com
22
+ `defineDesignScenario`. Testes devem passar a action produzida por `bindAction` ao `runAction`.
23
+
24
+ Filtros `select` de escolha única aceitam `presentation: "toggle"` para manter opções curtas e
25
+ mutuamente exclusivas visíveis em um grupo conectado. A apresentação padrão continua sendo
26
+ `Select`; estado, URL, defaults e input da action não mudam. Opções vindas de dicionário também
27
+ apresentam o ícone declarado na entrada, sempre acompanhado pelo rótulo.
28
+
29
+ `TabsList` passa a usar somente a linha de navegação e deixa de aceitar `variant`. Migração: remova
30
+ `variant="line"`; usos da antiga superfície preenchida devem representar escolha de valor com
31
+ `ToggleGroup`, usando `variant="outline"` quando precisarem de borda.
32
+
33
+ A anatomia pública de Tabs passa a nomear estrutura, não o mecanismo da primitiva:
34
+ `TabsTrigger` vira `TabsListItem` e `TabsContent` vira `TabsPanel`. A mudança não mantém aliases;
35
+ consumidores devem atualizar imports e JSX no mesmo upgrade.
36
+
37
+ `Content` deixa de aceitar `variant="page" | "section"`. `level={1}` identifica o conteúdo
38
+ principal, aplica automaticamente a escala de `PageHeader` e usa o mesmo intervalo de `1.5rem`
39
+ entre header e body; níveis de 2 a 6 preservam a apresentação compacta de seção. Migração: remova
40
+ `variant="page"` e mantenha `level={1}`. Não existe substituição para `variant="section"`, que era
41
+ o comportamento dos níveis internos.
42
+
43
+ ### Filtros e controles
44
+
45
+ `Toggle` e `ToggleGroupItem` em `variant="outline"` preservam a borda no estado ativo e usam um
46
+ fundo sutil distinto dela, inclusive no tema escuro.
47
+
10
48
  ## 22.2.0 — 2026-09-29
11
49
 
12
50
  As actions passam a declarar o que fazem com os dados (ADR 0041). Dois campos novos no contrato,
@@ -381,7 +419,7 @@ página (ADR 0024). A `Presentation` de lista em `layout="fill"` passa a entrega
381
419
  lista ocupa a altura da página e só os resultados rolam.
382
420
 
383
421
  Quem encadeava `flex min-h-0 flex-1 flex-col` à mão pode remover as classes; mantidas, continuam
384
- funcionando. Com um contêiner próprio entre `ContentBody` e a lista, como um `TabsContent`, a lista
422
+ funcionando. Com um contêiner próprio entre `ContentBody` e a lista, como um `TabsPanel`, a lista
385
423
  continua em `flow`: declare `layout="fill"` nela e dê ao contêiner `flex min-h-0 flex-1 flex-col`.
386
424
 
387
425
  ### Superfícies e patterns
package/bin/cli.mjs CHANGED
@@ -40,16 +40,8 @@ async function cmdCheck(dir) {
40
40
  for (const error of materialization.errors) log('error', `✗ opus check: ${error}`)
41
41
  process.exit(1)
42
42
  }
43
- const { files, actions, contracts, deprecatedMockHandlers, missingEffect, missingPersonalData, findings, hasMarker } =
43
+ const { files, actions, contracts, missingEffect, missingPersonalData, findings, hasMarker } =
44
44
  await scanDir(root)
45
- // Aviso de migração, não violação: uma linha só, para não poluir o gate a cada execução.
46
- if (deprecatedMockHandlers > 0) {
47
- log(
48
- 'warn',
49
- `⚠ opus check: ${deprecatedMockHandlers} action(s) ainda declaram \`mockHandler\`, que sai na próxima major. ` +
50
- 'No modo design, a action sem mock já responde com dado gerado; use `defineDesignScenario` quando o design precisar de um cenário (ADR 0037).',
51
- )
52
- }
53
45
 
54
46
  if (missingEffect > 0 || missingPersonalData > 0) {
55
47
  log(
package/bin/lib/check.mjs CHANGED
@@ -58,7 +58,6 @@ const FIELD_GROUP = {
58
58
  authorize: 3,
59
59
  loads: 3,
60
60
  handler: 4,
61
- mockHandler: 4,
62
61
  audit: 5,
63
62
  confirm: 5,
64
63
  invalidates: 5,
@@ -651,8 +650,6 @@ export function checkProject(sources) {
651
650
  const productDeclarations = [];
652
651
  const entityNames = new Set();
653
652
  const actionNames = new Set();
654
- // `mockHandler` obsoleto (ADR 0037): contado, não reprovado — a minor ainda o aceita.
655
- let deprecatedMockHandlers = 0;
656
653
  // Natureza da action (ADR 0041): declarações ausentes são contadas, não reprovadas, na minor.
657
654
  let missingEffect = 0;
658
655
  let missingPersonalData = 0;
@@ -680,7 +677,6 @@ export function checkProject(sources) {
680
677
  if (item.name !== null) actionNames.add(item.name);
681
678
  }
682
679
  for (const f of lintAction(item)) findings.push({ ...f, file });
683
- if (item.keys.includes("mockHandler")) deprecatedMockHandlers += 1;
684
680
  if (item.form !== "bindAction") {
685
681
  const writesByGet = item.kind === "list" || item.kind === "view";
686
682
  if (!writesByGet && !item.keys.includes("effect")) missingEffect += 1;
@@ -791,7 +787,6 @@ export function checkProject(sources) {
791
787
  actions,
792
788
  contracts,
793
789
  dataProducts: productDeclarations.length,
794
- deprecatedMockHandlers,
795
790
  missingEffect,
796
791
  missingPersonalData,
797
792
  findings,
@@ -1944,7 +1939,7 @@ async function walkUiFiles(dir, acc = []) {
1944
1939
  return acc;
1945
1940
  }
1946
1941
 
1947
- /** Escaneia um diretório → { files, actions, contracts, deprecatedMockHandlers, findings, hasMarker }.
1942
+ /** Escaneia um diretório → { files, actions, contracts, findings, hasMarker }.
1948
1943
  * `hasMarker` = o diretório é um projeto opus (tem opus.json na raiz). */
1949
1944
  export async function scanDir(rootDir) {
1950
1945
  rootDir = canonicalProjectDirectory(rootDir);
@@ -1974,7 +1969,6 @@ export async function scanDir(rootDir) {
1974
1969
  actions: checked.actions,
1975
1970
  contracts: checked.contracts,
1976
1971
  dataProducts: checked.dataProducts,
1977
- deprecatedMockHandlers: checked.deprecatedMockHandlers,
1978
1972
  missingEffect: checked.missingEffect,
1979
1973
  missingPersonalData: checked.missingPersonalData,
1980
1974
  findings: [...checked.findings, ...uiFindings],
package/bin/lib/copy.mjs CHANGED
@@ -128,7 +128,7 @@ const JSX_CHILD_ROLES = new Map([
128
128
  ['PaneTitle', 'title'],
129
129
  ['TableCaption', 'description'],
130
130
  ['TableHead', 'heading'],
131
- ['TabsTrigger', 'tab'],
131
+ ['TabsListItem', 'tab'],
132
132
  // A Base não tem papel `tooltip`. O tooltip nomeia um controle icon-only, logo é um
133
133
  // fragmento (sem ponto final), não uma frase; `label` é o papel que reflete isso.
134
134
  ['TooltipContent', 'label'],
@@ -466,6 +466,8 @@ function serializeFilters(filters) {
466
466
  placeholder: spec.placeholder ?? null,
467
467
  type: spec.type ?? null,
468
468
  multiple: spec.multiple === true,
469
+ presentation: spec.presentation ?? null,
470
+ allowEmpty: spec.allowEmpty ?? null,
469
471
  mode: spec.mode ?? null,
470
472
  operators: Array.isArray(spec.operators) ? spec.operators : null,
471
473
  path: spec.path ?? null,
@@ -182,7 +182,9 @@ function stripNullsFromFields(record) {
182
182
  for (const [k, v] of Object.entries(spec)) {
183
183
  if (v === null || v === undefined) continue
184
184
  if (k === 'hasShowWhen' || k === 'hasRequireWhen') continue
185
- if (typeof v === 'boolean' && v === false) continue
185
+ // `false` costuma ser o default implícito e não precisa viajar. `allowEmpty` é a
186
+ // exceção: seu default é aceitar vazio, então `false` altera o comportamento.
187
+ if (typeof v === 'boolean' && v === false && k !== 'allowEmpty') continue
186
188
  clean[k] = v
187
189
  }
188
190
  out[name] = clean
@@ -62,7 +62,7 @@ por propriedades continua disponível temporariamente dentro de `Content`, mas a
62
62
  explícita usa os slots da família.
63
63
 
64
64
  `Content` técnico mantém seu nome quando representa o contêiner montado por uma primitiva ou um
65
- painel controlado, como `DialogContent`, `PopoverContent`, `TabsContent` e `AccordionContent`.
65
+ painel controlado, como `DialogContent`, `PopoverContent`, `TabsPanel` e `AccordionContent`.
66
66
  `CarouselContent` também permanece porque representa o trilho técnico da primitiva, não o body de
67
67
  uma superfície estrutural. No `Empty`, a antiga região `EmptyContent` não é um body: como contém
68
68
  somente comandos, seu nome canônico passa a ser `EmptyActions`.
@@ -2,7 +2,8 @@
2
2
 
3
3
  - **Status:** aceita.
4
4
  - **Data:** 2026-09-28.
5
- - **Relacionada:** ADR 0023, quanto ao alinhamento horizontal dentro de superfícies.
5
+ - **Relacionadas:** ADR 0023, quanto ao alinhamento horizontal dentro de superfícies; ADR 0040,
6
+ que torna a linha a única apresentação de `TabsList`.
6
7
 
7
8
  ## Contexto
8
9
 
@@ -23,14 +24,16 @@ continua pertencendo à anatomia da superfície, e Tabs apenas ocupa a largura q
23
24
 
24
25
  ### Emenda de 2026-09-28 — a aba combina uma área de botão e o indicador
25
26
 
26
- Na variante horizontal `line`, o gatilho mantém duas camadas: uma área interna com os estados
27
- interativos e a mesma altura de um Button do tamanho correspondente, e o indicador ancorado na
28
- linha-base da lista. A área interna permanece transparente e recebe fundo apenas no hover; a aba
29
- ativa se distingue pelo indicador primário. Um respiro de `0.375rem` separa as duas camadas no uso
30
- comum; dentro de `Tabs framed`, ele aumenta para `0.5rem` e acompanha o padding superior da moldura.
27
+ Na apresentação horizontal que então se chamava `line`, o gatilho mantém duas camadas: uma área
28
+ interna com os estados interativos e a mesma altura de um Button do tamanho correspondente, e o
29
+ indicador ancorado na linha-base da lista. A área interna permanece transparente e recebe fundo
30
+ apenas no hover; a aba ativa se distingue pelo indicador primário. Um respiro de `0.375rem` separa
31
+ as duas camadas no uso comum; dentro de `Tabs framed`, ele aumenta para `0.5rem` e acompanha o
32
+ padding superior da moldura.
31
33
 
32
- Essa apresentação pertence ao Opus: consumidores escolhem `variant="line"` e não repetem classes
33
- de fundo, raio, padding ou interação para obter o estado selecionado.
34
+ Essa apresentação pertence ao Opus: consumidores não repetem classes de fundo, raio, padding ou
35
+ interação para obter o estado selecionado. Desde a ADR 0040, a linha é a única apresentação de
36
+ `TabsList` e `variant` não faz mais parte da API.
34
37
 
35
38
  ## Consequências
36
39
 
@@ -38,7 +41,7 @@ de fundo, raio, padding ou interação para obter o estado selecionado.
38
41
  - A seleção permanece indicada pela linha inferior, sem manter um segundo fundo persistente.
39
42
  - Drawer e Dialog não mantêm uma integração implícita com Tabs.
40
43
  - Um consumidor que realmente precise de uma linha até as bordas deve compor essa apresentação de
41
- forma explícita, pois ela não é mais o comportamento padrão da variante `line`.
44
+ forma explícita, pois ela não faz parte do comportamento canônico de `TabsList`.
42
45
 
43
46
  ## Alternativas descartadas
44
47
 
@@ -54,6 +57,7 @@ sem consumidor conhecido. A composição explícita é suficiente caso essa nece
54
57
 
55
58
  ## Verificação
56
59
 
57
- O teste da variante `line` verifica que a linha usa `inset-inline: 0`, inclusive quando Tabs está
58
- dentro de `DrawerBody`. O mesmo teste verifica a separação estrutural entre a superfície interna e
59
- o indicador da aba ativa. A documentação viva demonstra a variante com os limites do contêiner.
60
+ O teste da apresentação horizontal verifica que a linha usa `inset-inline: 0`, inclusive quando
61
+ Tabs está dentro de `DrawerBody`. O mesmo teste verifica a separação estrutural entre a superfície
62
+ interna e o indicador da aba ativa. A documentação viva demonstra a linha com os limites do
63
+ contêiner.
@@ -2,7 +2,8 @@
2
2
 
3
3
  - **Status:** aceita.
4
4
  - **Data:** 2026-09-28.
5
- - **Relacionadas:** ADR 0023, quanto à anatomia das superfícies; ADR 0032, quanto à linha-base das abas.
5
+ - **Relacionadas:** ADR 0023, quanto à anatomia das superfícies; ADR 0032, quanto à linha-base das
6
+ abas; ADR 0040, que substitui a escolha de variante em `TabsList`.
6
7
 
7
8
  ## Contexto
8
9
 
@@ -23,9 +24,10 @@ Quando ativa, `framed` aplica ao root a moldura estrutural canônica para esse b
23
24
  `rounded-xl` e recorte do conteúdo. Ela não aplica token de fundo nem de foreground. O conjunto
24
25
  herda a superfície do contêiner em que foi composto.
25
26
 
26
- Essa propriedade não altera a apresentação de `TabsList`, a seleção, o tamanho nem a orientação. O
27
- consumidor continua escolhendo esses comportamentos nas APIs existentes, inclusive
28
- `TabsList variant="line"`.
27
+ Essa propriedade não altera a apresentação de `TabsList`, a seleção, o tamanho nem a orientação. À
28
+ época desta decisão, o consumidor ainda escolhia a apresentação com `TabsList variant="line"`.
29
+ Essa parte foi substituída pela ADR 0040, que tornou a linha a única apresentação; a independência
30
+ entre moldura e apresentação permanece válida.
29
31
 
30
32
  ## Consequências
31
33
 
@@ -33,8 +35,8 @@ consumidor continua escolhendo esses comportamentos nas APIs existentes, inclusi
33
35
  - Em Drawer, Dialog ou Pane, a moldura acompanha o fundo da superfície externa nos temas claro e
34
36
  escuro.
35
37
  - `Card` continua correto quando as abas realmente precisarem de uma superfície semântica própria.
36
- - `framed` permanece independente de `TabsList variant`, que descreve apenas a apresentação da
37
- lista.
38
+ - `framed` permanece independente da apresentação de `TabsList`; desde a ADR 0040, a lista usa
39
+ somente a linha de navegação e não aceita `variant`.
38
40
 
39
41
  ## Alternativas descartadas
40
42
 
@@ -2,7 +2,9 @@
2
2
 
3
3
  - **Status:** aceita.
4
4
  - **Data:** 2026-09-28.
5
- - **Relacionadas:** ADR 0032, quanto à composição da variante `line`; ADR 0034, quanto à moldura de Tabs; ADR 0035, quanto ao eixo de distribuição em grupos de controles.
5
+ - **Relacionadas:** ADR 0032, quanto à composição da linha; ADR 0034, quanto à moldura de Tabs; ADR
6
+ 0035, quanto ao eixo de distribuição em grupos de controles; ADR 0040, que substitui a escolha de
7
+ variante em `TabsList`.
6
8
 
7
9
  ## Contexto
8
10
 
@@ -11,10 +13,11 @@ entre os gatilhos. A view de Evento usa quatro abas no cabeçalho de um bloco em
11
13
  derivadas do texto deixam esse cabeçalho visualmente desequilibrado, embora todas as seções tenham o
12
14
  mesmo nível.
13
15
 
14
- Hoje, a variante horizontal `line` força cada gatilho a usar a largura do próprio conteúdo. Um
15
- consumidor que precise de larguras iguais teria de sobrescrever o `flex` interno ou reconstruir a
16
- lista com grid, duplicando uma decisão que já existe em `ButtonGroup` como
17
- `distribution="content" | "equal"`.
16
+ Na API vigente quando esta decisão foi registrada, a variante horizontal `line` forçava cada
17
+ gatilho a usar a largura do próprio conteúdo. Um consumidor que precisasse de larguras iguais teria
18
+ de sobrescrever o `flex` interno ou reconstruir a lista com grid, duplicando uma decisão que já
19
+ existia em `ButtonGroup` como `distribution="content" | "equal"`. A ADR 0040 depois tornou a linha
20
+ a única apresentação e removeu `variant`, sem alterar o problema de distribuição resolvido aqui.
18
21
 
19
22
  ## Decisão
20
23
 
@@ -30,9 +33,9 @@ largura nem o layout dos painéis de conteúdo.
30
33
  ## Consequências
31
34
 
32
35
  - Consumidores escolhem a mesma linguagem de distribuição usada por `ButtonGroup`.
33
- - `TabsList variant="line"` continua responsável pela linha e pelos estados visuais; não recebe
34
- classes de layout no consumidor.
35
- - A view de Evento declara `distribution="equal"` no `Tabs` sem alterar cada `TabsTrigger`.
36
+ - `TabsList` continua responsável pela linha e pelos estados visuais; desde a ADR 0040, não aceita
37
+ `variant` nem recebe classes de layout no consumidor.
38
+ - A view de Evento declara `distribution="equal"` no `Tabs` sem alterar cada `TabsListItem`.
36
39
  - Usos existentes preservam a distribuição pelo conteúdo.
37
40
 
38
41
  ## Alternativas descartadas
@@ -1,6 +1,6 @@
1
1
  # ADR 0037 — Dados do modo design ficam fora do contrato
2
2
 
3
- - **Status:** aceita; a primeira etapa saiu na 22.1.0, e a remoção fica para a 23.0.0.
3
+ - **Status:** aceita; transição concluída na 23.0.0.
4
4
  - **Data:** 2026-09-28.
5
5
  - **Relacionadas:** ADR 0002 (seeds estruturados).
6
6
 
@@ -0,0 +1,62 @@
1
+ # ADR 0038 — O nível do Content define sua apresentação principal
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-09-29.
5
+ - **Complementa:** ADRs 0016, 0021, 0023 e 0029.
6
+
7
+ ## Contexto
8
+
9
+ `Content` aceitava duas declarações independentes para a mesma hierarquia: `level={1}` fazia da
10
+ região o heading principal da `Page`, enquanto `variant="page"` escolhia a escala visual desse
11
+ mesmo papel. Todos os consumidores válidos precisavam repetir as duas propriedades, e o contrato
12
+ também permitia combinações contraditórias como um heading principal com aparência de seção.
13
+
14
+ A duplicação ainda produzia ritmos diferentes entre páginas equivalentes. Quando `PageHeader`
15
+ fornecia o heading, `Page` separava introdução e corpo por `1.5rem`; quando `Content` fornecia o
16
+ heading principal, a raiz mantinha os `0.75rem` próprios de uma seção interna.
17
+
18
+ ## Decisão
19
+
20
+ `Content` deixa de expor `variant`. O nível semântico passa a selecionar a única apresentação
21
+ válida para esse componente:
22
+
23
+ - `level={1}` identifica o conteúdo principal, usa a escala de `PageHeader` e separa header e body
24
+ por `1.5rem` no layout contínuo;
25
+ - níveis de 2 a 6 identificam seções internas e preservam a escala e o intervalo de `0.75rem`;
26
+ - `fieldset` permanece uma seção interna: sua legenda não participa da hierarquia de headings.
27
+
28
+ `Page` continua sendo a superfície estrutural, e `heading: "content"` continua declarando na
29
+ `Presentation` quem possui o heading principal. A decisão não transforma `Content` em `Page`; ela
30
+ remove uma escolha visual que não tinha significado independente dentro de `Content`.
31
+
32
+ ## Consequências
33
+
34
+ - combinações contraditórias deixam de ser representáveis;
35
+ - consumidores removem `variant="page"` e preservam `level={1}`;
36
+ - o ritmo do conteúdo principal fica igual ao de páginas cujo heading pertence a `PageHeader`;
37
+ - a remoção da propriedade é incompatível e exige migração coordenada dos consumidores;
38
+ - a escala interna continua centralizada em `SurfaceHeader`, sem duplicação no consumidor.
39
+
40
+ ## Alternativas descartadas
41
+
42
+ ### Manter variant e corrigir somente o espaço
43
+
44
+ Corrigiria o sintoma visual, mas preservaria a duplicação e os estados inválidos no contrato.
45
+
46
+ ### Renomear variant para role
47
+
48
+ Tornaria a intenção mais explícita, porém continuaria exigindo duas propriedades para declarar o
49
+ mesmo papel e permitiria que elas divergissem.
50
+
51
+ ### Derivar a apresentação da presença em Page
52
+
53
+ Acoplaria `Content` ao local de renderização. O heading principal já é declarado diretamente por
54
+ `level={1}` e pode ser validado por `Page` sem outro contexto implícito.
55
+
56
+ ## Verificação
57
+
58
+ - testes de `Content` confirmam escala e intervalo de página no nível 1 e de seção nos demais;
59
+ - tipos e exemplos públicos não aceitam mais `variant` em `Content`;
60
+ - testes de `Page`, `Presentation` e preenchimento preservam `Content level={1}` como heading
61
+ principal;
62
+ - consumidores compilam sem `variant="page"`.
@@ -0,0 +1,62 @@
1
+ # ADR 0039 — A apresentação do filtro é independente de seu tipo
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-09-29.
5
+ - **Complementa:** ADR 0019.
6
+
7
+ ## Contexto
8
+
9
+ `FilterSpec` declara o significado e o transporte de cada filtro, mas a interface apresentava todo
10
+ filtro `select` com o mesmo controle. Escolhas únicas, curtas e mutuamente exclusivas — como um
11
+ canal com três opções — continuavam escondidas em um menu mesmo quando a comparação direta entre
12
+ as alternativas era mais útil.
13
+
14
+ Transformar `toggle` em um novo `type` misturaria apresentação com valor: tanto o menu quanto o
15
+ grupo produzem a mesma string, ocupam o mesmo campo no estado navegável e chegam sem mudança à
16
+ action.
17
+
18
+ ## Decisão
19
+
20
+ `FilterSpec` passa a aceitar `presentation: "select" | "toggle"`. A ausência da propriedade
21
+ preserva `select` como apresentação padrão.
22
+
23
+ `toggle` aplica-se a filtros `type: "select"` de escolha única e usa as mesmas opções, valor de
24
+ repouso, regra de vazio, dependências e atualização de estado do controle padrão. A apresentação
25
+ mostra todas as opções em um `ToggleGroup` conectado e não permite desmarcar a opção ativa quando o
26
+ filtro não aceita vazio. Quando a opção vem de um dicionário e sua entrada declara um ícone do
27
+ catálogo, o grupo apresenta ícone e rótulo juntos.
28
+
29
+ O tipo do filtro continua descrevendo o valor e sua operação. A apresentação escolhe somente o
30
+ controle usado para editá-lo.
31
+
32
+ ## Consequências
33
+
34
+ - contratos podem expor escolhas pequenas e fechadas sem reconstruir a barra no consumidor;
35
+ - URL, input da action, defaults e handlers permanecem inalterados;
36
+ - filtros existentes continuam usando `Select` sem migração;
37
+ - `toggle` não se aplica a seleção múltipla, lookup, texto, número ou data;
38
+ - conjuntos extensos ou pesquisáveis continuam usando `Select`.
39
+
40
+ ## Alternativas descartadas
41
+
42
+ ### Criar `type: "toggle"`
43
+
44
+ Duplicaria a semântica de `select` e exigiria que serialização, validação e adaptadores conhecessem
45
+ um tipo cujo valor não mudou.
46
+
47
+ ### Compor o ToggleGroup em cada página
48
+
49
+ Preservaria o contrato incompleto e duplicaria a sincronização entre controle, URL, default e input
50
+ efetivo da action.
51
+
52
+ ### Trocar automaticamente selects com poucas opções
53
+
54
+ Quantidade de opções não determina a melhor apresentação. O contrato precisa declarar a intenção,
55
+ sem uma heurística visual invisível.
56
+
57
+ ## Verificação
58
+
59
+ - teste do `ActionFilterBar` confirma que `presentation: "toggle"` renderiza todas as opções e
60
+ atualiza o mesmo campo de filtro;
61
+ - teste de regressão confirma que um `select` sem apresentação continua usando `Select`;
62
+ - typecheck impede valores de apresentação fora do vocabulário público.
@@ -0,0 +1,60 @@
1
+ # ADR 0040 — Tabs usa uma única apresentação de navegação
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-09-29.
5
+ - **Substitui parcialmente:** ADR 0032, ADR 0034 e ADR 0036 quanto à escolha de variante em `TabsList`.
6
+
7
+ ## Contexto
8
+
9
+ `TabsList` oferecia uma superfície preenchida e uma variante com linha. A superfície preenchida
10
+ ficava visualmente próxima de um controle segmentado, embora `Tabs` represente navegação entre
11
+ painéis e um `ToggleGroup` represente escolha de valores. A mesma aparência para comportamentos
12
+ diferentes dificultava escolher o componente correto e levou filtros a serem descritos como tabs.
13
+
14
+ ## Decisão
15
+
16
+ `TabsList` usa somente a linha de navegação e deixa de aceitar `variant`. Cada `TabsListItem`
17
+ continua associado a um `TabsPanel` pelo mesmo valor. Orientação, tamanho, distribuição e moldura
18
+ permanecem propriedades independentes do conjunto.
19
+
20
+ A anatomia pública nomeia a posição de cada parte: `TabsListItem` pertence a `TabsList`, enquanto
21
+ `TabsPanel` identifica o painel associado. Os nomes anteriores, `TabsTrigger` e `TabsContent`, não
22
+ são mantidos como aliases porque há um único consumidor e a migração é atômica.
23
+
24
+ Escolhas únicas ou múltiplas que alteram um valor no contexto atual usam `ToggleGroup`. Quando o
25
+ grupo precisa de um contorno conectado, o consumidor declara `variant="outline"` com `spacing={0}`;
26
+ a borda pertence aos itens e as arestas internas duplicadas são removidas pela primitiva.
27
+
28
+ ## Consequências
29
+
30
+ - Tabs mantém uma identidade visual única e vinculada à navegação entre painéis.
31
+ - Filtros e escolhas de formulário não usam semântica de tabs para obter uma aparência compacta.
32
+ - Consumidores removem `variant="line"`; não há substituição para a antiga superfície preenchida.
33
+ - Consumidores renomeiam `TabsTrigger` para `TabsListItem` e `TabsContent` para `TabsPanel`.
34
+ - A documentação de ToggleGroup explicita como formar o contorno conectado.
35
+
36
+ ## Alternativas descartadas
37
+
38
+ ### Manter duas variantes em TabsList
39
+
40
+ Preservaria flexibilidade visual, mas manteria a ambiguidade entre navegação e escolha de valor.
41
+
42
+ ### Compartilhar uma apresentação segmentada entre Tabs e ToggleGroup
43
+
44
+ Permitiria a mesma aparência com semânticas distintas, mas não resolveria a dificuldade de leitura
45
+ observada no produto. A linha torna a navegação reconhecível sem depender do contexto ao redor.
46
+
47
+ ### Preservar Trigger e Content como nomes públicos
48
+
49
+ Manteria proximidade com a primitiva subjacente, mas nomearia mecanismo e conteúdo genérico em vez
50
+ da anatomia exposta ao consumidor. `ListItem` e `Panel` deixam a composição legível no JSX.
51
+
52
+ ## Verificação
53
+
54
+ - o tipo público de `TabsList` não aceita `variant`;
55
+ - a exportação pública oferece `TabsListItem` e `TabsPanel`, sem aliases para os nomes anteriores;
56
+ - testes da primitiva confirmam a linha-base, o indicador ativo e a distribuição;
57
+ - testes do `ActionFilterBar` confirmam que `presentation: "toggle"` usa seleção exclusiva sem
58
+ alterar o estado transportado para a action;
59
+ - a documentação de Tabs não oferece apresentação preenchida e a de ToggleGroup demonstra o grupo
60
+ conectado com borda.
@@ -115,7 +115,7 @@ O extrator cobre três superfícies, sem heurística de nome:
115
115
  reprovam em vez de serem executadas e o dicionário mantém um snapshot imutável das entradas;
116
116
  3. texto estático em filhos e props de uma allowlist de componentes importados diretamente
117
117
  de `@softize/opus/ui*` — por exemplo, `Button`, `DialogTitle`, `DialogBody`,
118
- `FieldLabel`, `TabsTrigger`, `Page`, `Input` e `DataState`. Alias e namespace de import
118
+ `FieldLabel`, `TabsListItem`, `Page`, `Input` e `DataState`. Alias e namespace de import
119
119
  continuam rastreáveis. `uppercase`, variantes como `sm:hover:uppercase`, modificadores
120
120
  `!uppercase`/`uppercase!` e classes em descendentes são projetados na superfície que
121
121
  realmente herda o CSS. Props específicas também respeitam a implementação: por exemplo,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softize/opus",
3
- "version": "22.2.0",
3
+ "version": "23.0.0",
4
4
  "description": "End-to-end action protocol for TypeScript. Single package with subpath exports (core + adapters).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -52,7 +52,7 @@ ContentDescription, ContentMeta, ContentActions) + ContentBody`. `ContentHeader`
52
52
  `toolbarActions` fica reservado a operações que dependem do recorte atual.
53
53
  - Card, Drawer e Pane nomeiam a região principal como `CardBody`, `DrawerBody` e `PaneBody`.
54
54
  `*Content` permanece reservado a raízes técnicas ou painéis cujo papel não é o corpo de uma
55
- estrutura, como `DialogContent`, `PopoverContent` e `TabsContent`.
55
+ estrutura, como `DialogContent`, `PopoverContent` e `TabsPanel`.
56
56
  - Componentes compartilhados não impõem margem externa; páginas e shells compõem layout.
57
57
  - A UI assume layout desktop fixo e largura mínima suportada de `64rem`. Não introduzir
58
58
  breakpoints de viewport ou container queries isoladas; uma futura responsividade precisa começar
@@ -1,9 +1,9 @@
1
1
  # Contrato e binding
2
2
 
3
3
  O contrato contém `name`, `kind`, documentação, schemas, metadata de UI e autorização
4
- declarativa compartilhável. Ele não declara dados de design: no modo design, a action sem mock já
5
- responde com dado gerado a partir do `output`, e um cenário específico, quando necessário, fica
6
- fora do contrato em `defineDesignScenario` (ADR 0037). `mockHandler` está obsoleto.
4
+ declarativa compartilhável. Ele não declara dados de design: no modo design, a action responde
5
+ com dado gerado a partir do `output`, e um cenário específico, quando necessário, fica fora do
6
+ contrato em `defineDesignScenario` (ADR 0037).
7
7
 
8
8
  O binding contém `handler`, `loads`, autorização dependente de dados carregados, execução
9
9
  em background, eventos e idempotência. `bindAction(contract, binding)` produz o `ActionDef`
@@ -42,7 +42,7 @@ type BindingKeys = 'handler' | 'loads' | 'background' | 'emits' | 'idempotency'
42
42
  * `ParsedIn` (o lado PARSEADO do schema de input — defaults aplicados) defaulta
43
43
  * a `any` nos contratos, de propósito: quem anota contrato à mão é o CLIENTE
44
44
  * (`FormContract<MeuInput, MeuOutput>` em hooks/props), que só conhece o fio —
45
- * e `authorize`/`mockHandler` são contravariantes em `ParsedIn`, então um
45
+ * e `authorize` é contravariante em `ParsedIn`, então um
46
46
  * default estreito rejeitaria contrato real cujo schema tem `.default()`.
47
47
  * Quem precisa do parseado com precisão (`bindAction`) o infere do schema.
48
48
  */
@@ -833,16 +833,7 @@ export class Runtime {
833
833
  }
834
834
 
835
835
  // — 5. Handler ————————————————————————————————————————————————————————
836
- // Modo design (`OPUS_MODE=design`): tenta mockHandler primeiro, cai
837
- // no handler real se não houver. Em qualquer outro modo, handler real
838
- // sempre manda. Leitura de env mora aqui, no boundary do execute — o
839
- // resto do core continua sem tocar `process.env`.
840
- const isDesignMode = isOpusDesignMode()
841
- const handlerToRun =
842
- isDesignMode && action.mockHandler !== undefined
843
- ? action.mockHandler
844
- : action.handler
845
- const output = await handlerToRun(
836
+ const output = await action.handler(
846
837
  ctx,
847
838
  validatedInput,
848
839
  loaded,
@@ -850,8 +841,6 @@ export class Runtime {
850
841
  ? progress
851
842
  : undefined,
852
843
  )
853
- const usedMock = handlerToRun === action.mockHandler
854
-
855
844
  // — 6. Validate output (dev only) —————————————————————————————————————
856
845
  // Search retorna Paginated<Item> mas `output` schema descreve só Item;
857
846
  // skipamos pra evitar falso negativo. v1 pode validar items[].
@@ -874,7 +863,6 @@ export class Runtime {
874
863
  ctx,
875
864
  input: validatedInput,
876
865
  output: validatedOutput,
877
- mode: usedMock ? 'design' : undefined,
878
866
  }),
879
867
  action.audit,
880
868
  )
@@ -1271,8 +1259,6 @@ export class Runtime {
1271
1259
  input: unknown
1272
1260
  output?: unknown
1273
1261
  error?: ReturnType<typeof error>
1274
- /** Quando `'design'`, sinaliza que o mockHandler foi executado. */
1275
- mode?: 'design' | undefined
1276
1262
  }): AuditRecord {
1277
1263
  const record: AuditRecord = {
1278
1264
  id: args.actionId,
@@ -1293,13 +1279,12 @@ export class Runtime {
1293
1279
  if (args.nature !== undefined) {
1294
1280
  const effect = resolveActionEffect(args.nature)
1295
1281
  if (effect !== undefined) record.effect = effect
1296
- // A exposição só existe quando a action devolveu algo: erro não expõe o dado declarado, e a
1297
- // resposta simulada do modo design (mock, cenário ou gerada) não é de ninguém.
1298
- if (args.nature.personalData === true && args.outcome === 'success' && args.mode !== 'design') {
1282
+ // A exposição só existe quando a action devolveu algo. O plugin de design remove o marcador
1283
+ // das respostas simuladas antes de registrar a action no runtime.
1284
+ if (args.nature.personalData === true && args.outcome === 'success') {
1299
1285
  record.personalData = { records: countReturnedRecords(args.nature.kind, args.output) }
1300
1286
  }
1301
1287
  }
1302
- if (args.mode !== undefined) record.meta = { mode: args.mode }
1303
1288
  return record
1304
1289
  }
1305
1290
 
@@ -2095,21 +2080,3 @@ function generateId(): string {
2095
2080
  // fallback determinístico-suficiente quando crypto não disponível
2096
2081
  return `id_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`
2097
2082
  }
2098
-
2099
- /**
2100
- * Único ponto do core que lê `process.env`. Usado pelo `execute()` pra
2101
- * decidir entre `handler` real e `mockHandler`. Isolado em função pra
2102
- * testabilidade — testes podem `vi.stubEnv('OPUS_MODE', ...)`.
2103
- *
2104
- * Mantém a invariante "core nunca lê env" intacta no resto do código:
2105
- * adapters/config seguem recebendo valores literais.
2106
- *
2107
- * Exportado (fora do index público) pro harness de `testing` aplicar a MESMA
2108
- * régua no `runAction` — a semântica de design mora num lugar só.
2109
- */
2110
- export function isOpusDesignMode(): boolean {
2111
- // Acesso via globalThis: defensivo pra runtimes sem `process` (browser, edge) E
2112
- // não exige @types/node no consumidor que compila este source (apps web).
2113
- const g = globalThis as { process?: { env?: Record<string, string | undefined> } }
2114
- return g.process?.env?.['OPUS_MODE'] === 'design'
2115
- }