@softize/opus 15.2.1 → 16.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 (42) hide show
  1. package/CHANGELOG.md +59 -16
  2. package/bin/lib/check.mjs +98 -15
  3. package/bin/lib/copy.mjs +811 -314
  4. package/bin/lib/gen-manifest.mjs +24 -23
  5. package/bin/lib/gen-runner.mjs +198 -149
  6. package/docs/adr/0010-page-header-owns-page-chrome.md +3 -4
  7. package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +5 -3
  8. package/docs/adr/0012-data-products-are-first-class-declarations.md +4 -2
  9. package/docs/adr/0012-modal-header-only-names-the-surface.md +45 -0
  10. package/docs/adr/0013-presentation-is-a-portable-action-oriented-artifact.md +92 -0
  11. package/docs/code-style.md +24 -19
  12. package/docs/data-products.md +7 -1
  13. package/package.json +18 -15
  14. package/registry/skills/build-opus-ui/references/ui-patterns.md +43 -26
  15. package/registry/skills/maintain-opus-docs/scripts/audit-docs.mjs +0 -0
  16. package/src/core/data-product.ts +51 -7
  17. package/src/core/index.ts +3 -0
  18. package/src/core/presentation.ts +512 -0
  19. package/src/presentation/index.ts +1 -0
  20. package/src/ui/components/patterns/action-list-dialog.tsx +26 -9
  21. package/src/ui/components/patterns/confirm.tsx +34 -29
  22. package/src/ui/components/patterns/form-dialog.tsx +20 -8
  23. package/src/ui/components/patterns/list.tsx +26 -9
  24. package/src/ui/components/patterns/page.tsx +99 -139
  25. package/src/ui/components/patterns/presentation.tsx +316 -0
  26. package/src/ui/components/patterns/sidebar.tsx +3 -3
  27. package/src/ui/components/patterns/trigger.tsx +38 -17
  28. package/src/ui/components/primitives/button-group.tsx +53 -43
  29. package/src/ui/components/primitives/command.tsx +30 -72
  30. package/src/ui/components/primitives/dialog.tsx +23 -89
  31. package/src/ui/components/primitives/drawer.tsx +8 -34
  32. package/src/ui/docs/content/action-form-dialog.md +12 -10
  33. package/src/ui/docs/content/action-list-dialog.md +22 -17
  34. package/src/ui/docs/content/button.md +52 -35
  35. package/src/ui/docs/content/communication.md +26 -26
  36. package/src/ui/docs/content/dialog.md +173 -154
  37. package/src/ui/docs/content/drawer.md +12 -11
  38. package/src/ui/docs/content/page.md +72 -91
  39. package/src/ui/docs/content/presentation.md +158 -0
  40. package/src/ui/docs/registry.tsx +6 -0
  41. package/src/ui/meta.ts +8 -2
  42. package/src/ui/react.tsx +10 -3
@@ -0,0 +1,45 @@
1
+ # ADR 0012 — O cabeçalho modal apenas nomeia a superfície
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-09-11.
5
+
6
+ ## Contexto
7
+
8
+ `DialogDescription` e `DrawerDescription` tornavam natural preencher o cabeçalho com uma segunda
9
+ linha mesmo quando ela apenas repetia o título ou explicava a ação óbvia. O resultado era uma
10
+ hierarquia mais pesada, mais espaço fixo antes da tarefa e textos que raramente ajudavam a pessoa a
11
+ decidir.
12
+
13
+ Há, porém, contexto que não pode desaparecer: consequências, restrições e instruções que mudam a
14
+ decisão. Esse conteúdo pertence à tarefa, não ao nome da superfície. Também pode precisar participar
15
+ da descrição acessível do modal.
16
+
17
+ ## Decisão
18
+
19
+ - `DialogHeader` e `DrawerHeader` contêm o título e, quando necessário, mídia. Não existe API pública
20
+ `DialogDescription` ou `DrawerDescription`.
21
+ - Contexto relevante aparece no início de `DialogBody` ou `DrawerBody`, como texto, `Alert` ou uma
22
+ composição própria. Texto que apenas repete o título ou a ação é omitido.
23
+ - A API imperativa usa somente `body` para esse conteúdo. `ActionFormDialog` e `ActionListDialog`
24
+ oferecem `intro` no início do corpo; não oferecem `description`.
25
+ - `DialogContent` e `DrawerContent` não inferem uma descrição. Quando um trecho conciso do corpo deve
26
+ descrever a superfície para tecnologias assistivas, o consumidor relaciona seu `id` por
27
+ `aria-describedby`.
28
+ - `CommandDialog` mantém internamente uma instrução apenas para tecnologias assistivas, porque ela
29
+ explica o funcionamento do controle e não é copy visual do cabeçalho.
30
+
31
+ ## Consequências
32
+
33
+ - Modais comuns começam mais perto da tarefa e não pedem texto de preenchimento.
34
+ - Informações importantes continuam visíveis, mas ocupam a região rolável e podem usar o componente
35
+ semântico adequado.
36
+ - A migração troca `description` por `body` na API imperativa, por `intro` nos wrappers e move conteúdo
37
+ declarativo relevante para o começo do corpo.
38
+ - O checker reprova as APIs removidas com orientação de migração.
39
+
40
+ ## Verificação
41
+
42
+ - o barrel público não exporta `DialogDescription` nem `DrawerDescription`;
43
+ - os tipos das APIs imperativas não aceitam `description`;
44
+ - testes cobrem `body`, `intro`, a relação acessível e os diagnósticos de migração;
45
+ - a auditoria de docs, o inventário de copy, o typecheck e a suíte do pacote permanecem verdes.
@@ -0,0 +1,92 @@
1
+ # ADR 0013 — Presentation é um artefato portátil orientado a actions
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-09-11.
5
+
6
+ ## Contexto
7
+
8
+ Page, Dialog e Drawer compartilham uma anatomia estrutural, mas consumidores ainda repetem a
9
+ coordenação entre título, navegação, corpo, ações, estados e ciclo de abertura. Essa repetição
10
+ dificulta apresentar o mesmo recurso em superfícies diferentes e impede que ferramentas analisem
11
+ a interface antes de executar React.
12
+
13
+ O Opus já declara actions `simple`, `form`, `list` e `view`. Um artefato de interface não deve
14
+ reimplementar seus contratos, validação, autorização ou transporte; deve apenas compor essas
15
+ capacidade em uma apresentação verificável.
16
+
17
+ ## Decisão
18
+
19
+ `Presentation` é o artefato declarativo que descreve um recurso independentemente da superfície em
20
+ que aparece. `Surface` escolhe `page`, `dialog` ou `drawer` no momento da renderização.
21
+
22
+ A anatomia portátil é `Header + Body + Footer?`. O header organiza horizontalmente
23
+ `Navigation? + Title + Actions?`; o body contém o recurso; o footer contém ações de conclusão.
24
+ Dialog e Drawer acrescentam o fechamento ao final das ações do header. Page pode ainda usar
25
+ navegação persistente fornecida pelo shell e uma introdução opcional dentro do body, sem mover nem
26
+ duplicar o título estrutural.
27
+
28
+ Uma action assume um de dois papéis:
29
+
30
+ - conteúdo no body: actions `form`, `list` e `view` usam seus patterns canônicos;
31
+ - comando no header ou footer: `simple` executa por `ActionTrigger`; os demais kinds abrem ou
32
+ navegam para outra Presentation que os apresenta como conteúdo.
33
+
34
+ O kind declarado pela action determina o pattern. A Presentation não repete `presentation: form`
35
+ ou `presentation: trigger`. Bindings declarativos fornecem input a partir de rota, registro, item,
36
+ seleção, sessão, resultado, valor fixo ou relógio. Autorização permanece no servidor.
37
+
38
+ Efeitos posteriores e coordenação de bloqueio são dados declarativos, não callbacks persistidos. O
39
+ renderer pode coordenar os escopos `action`, `group` e `surface`, mas cada pattern continua dono de
40
+ loading, erro, vazio, validação e repetição próprios.
41
+
42
+ Presentations possuem identificador e versão de schema, são registradas no `opus.config.ts` e
43
+ projetadas no manifest. A lens lê essa projeção; não infere a interface pelo JSX. Combinações
44
+ inválidas, referências ausentes e bindings incompatíveis produzem diagnósticos antes do runtime.
45
+
46
+ `PresentationDefinition` e `PresentationInvocation` têm ciclos de vida distintos. A definição é
47
+ estática, publicável no manifest e inspecionável pela Lens. A invocação é o estado serializável de
48
+ uma execução: identifica a Presentation, escolhe a surface, carrega somente input JSON e mantém a
49
+ pilha de frames necessária para voltar. Ela não é publicada no manifest.
50
+
51
+ `back` restaura o último frame da pilha. `close` encerra apenas dialog ou drawer e é inválido para
52
+ page. `navigate` abre outra invocação por push ou substitui a atual; sincronizar esse estado com URL
53
+ é responsabilidade do adaptador da aplicação quando refresh, deep link ou histórico forem
54
+ necessários. O inspetor de desenvolvimento reúne definição, invocação, bindings resolvidos e
55
+ diagnósticos, mascarando chaves sensíveis antes de expor o JSON.
56
+
57
+ ## Consequências
58
+
59
+ - O mesmo recurso pode ser apresentado como page, dialog ou drawer sem três implementações.
60
+ - Novas interfaces convencionais concentram código na declaração e na composição específica.
61
+ - `ActionFormDialog`, `ActionListDialog` e wrappers equivalentes permanecem atalhos manuais, mas não
62
+ formam novos kinds do artefato.
63
+ - Interfaces específicas continuam possíveis por nós de extensão explícitos; uma extensão não é
64
+ considerada portátil até possuir renderer registrado para as superfícies declaradas.
65
+ - O schema evolui por versão e migração; consumidores não dependem de detalhes internos do renderer.
66
+
67
+ ## Alternativas consideradas
68
+
69
+ ### Persistir JSX ou callbacks
70
+
71
+ Preservaria liberdade irrestrita, mas não seria serializável, verificável nem seguro para autoria
72
+ por ferramentas. Foi descartada em favor de bindings e efeitos fechados.
73
+
74
+ ### Criar schemas separados para Page, Dialog e Drawer
75
+
76
+ Representaria diretamente a aparência atual, mas triplicaria a composição e faria a superfície
77
+ definir o recurso. Foi descartada porque as diferenças pertencem ao renderer.
78
+
79
+ ### Tratar toda action como ActionTrigger
80
+
81
+ Uniformizaria comandos visualmente, mas perderia os ciclos próprios de formulário, lista e view.
82
+ Foi descartada porque compatibilidade exige delegar cada kind ao pattern canônico.
83
+
84
+ ## Verificação
85
+
86
+ - testes de schema cobrem versão, referências, kinds, placements e bindings;
87
+ - testes do renderer exercitam `simple`, `form`, `list` e `view` nas superfícies válidas;
88
+ - o manifest projeta Presentations sem callbacks nem dados server-only;
89
+ - a lens lista e expõe o JSON integral de cada Presentation;
90
+ - testes de invocação cobrem push, replace, back, close e rejeição de input não JSON;
91
+ - o inspetor mascara segredo, token, cookie, autorização, senha e chave de API;
92
+ - uma aplicação consumidora usa o pacote local por symlink durante a migração página por página.
@@ -61,7 +61,12 @@ Base instalada é dona do catálogo, dos kinds aceitos e do fundamento de cada r
61
61
  "allowedUppercase": ["CPF"],
62
62
  "exclude": ["scripts/fixtures/**"],
63
63
  "transforms": [
64
- { "source": "src/account.ts", "line": 12, "role": "button", "transform": "uppercase" }
64
+ {
65
+ "source": "src/account.ts",
66
+ "line": 12,
67
+ "role": "button",
68
+ "transform": "uppercase"
69
+ }
65
70
  ],
66
71
  "exemptions": [
67
72
  {
@@ -78,24 +83,24 @@ Base instalada é dona do catálogo, dos kinds aceitos e do fundamento de cada r
78
83
  }
79
84
  ```
80
85
 
81
- | Propriedade Opus | Papel no protocolo Base |
82
- |---|---|
83
- | `label`, `confirm.*Label`, `ActionTrigger.label` | `button` |
84
- | `title`, `examples[].name` | `title` |
85
- | `summary`, `description`, `examples[].description`, `expand.*.description` | `description` |
86
- | `messages.success`, `messages.error`, `messages.confirmation` | `success`, `error`, `message` |
87
- | `confirm.message` | `dialog-body` |
88
- | `fields.*.label`, `filters.*.label` | `label` |
89
- | `placeholder`, `help` | `placeholder`, `helper-text` |
90
- | opções estáticas | `menu-item` |
91
- | `columns[].label`, `periods[].label` | `heading`, `tab` |
92
- | `Select.emptyText`, `Select.searchPlaceholder` | `empty-state`, `placeholder` |
93
- | `Select.options[].hint/triggerLabel/group` | `label`, `label`, `heading` |
94
- | `ActionTrigger.confirm.*` | papel correspondente do diálogo |
95
- | `t.dict` — `label`, `description`, `doc` das entradas e `doc` do dicionário | `label`, `description` |
86
+ | Propriedade Opus | Papel no protocolo Base |
87
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- |
88
+ | `label`, `confirm.*Label`, `ActionTrigger.label` | `button` |
89
+ | `title`, `examples[].name` | `title` |
90
+ | `summary`, `description`, `examples[].description`, `expand.*.description` | `description` |
91
+ | `messages.success`, `messages.error`, `messages.confirmation` | `success`, `error`, `message` |
92
+ | `confirm.message` | `dialog-body` |
93
+ | `fields.*.label`, `filters.*.label` | `label` |
94
+ | `placeholder`, `help` | `placeholder`, `helper-text` |
95
+ | opções estáticas | `menu-item` |
96
+ | `columns[].label`, `periods[].label` | `heading`, `tab` |
97
+ | `Select.emptyText`, `Select.searchPlaceholder` | `empty-state`, `placeholder` |
98
+ | `Select.options[].hint/triggerLabel/group` | `label`, `label`, `heading` |
99
+ | `ActionTrigger.confirm.*` | papel correspondente do diálogo |
100
+ | `t.dict` — `label`, `description`, `doc` das entradas e `doc` do dicionário | `label`, `description` |
96
101
  | `DataState`/`PageState`/`ActionList`/`ActionListDialog` — `emptyMessage`, `errorMessage`, `retryLabel`; `PageState`/`ActionListDialog` — `title`, `description`; `ActionView.emptyMessage` | `empty-state`, `error`, `button`, `title`, `description` |
97
- | `dialog.alert/confirm/prompt/choose()` — `title`, `description`, `body`, `action`, `cancel`, `placeholder`, `actions[].label` | papel correspondente do diálogo |
98
- | `TooltipContent` (children), `LabelHelp.help` | `label`, `helper-text` |
102
+ | `dialog.alert/confirm/prompt/choose()` — `title`, `description`, `body`, `action`, `cancel`, `placeholder`, `actions[].label` | papel correspondente do diálogo |
103
+ | `TooltipContent` (children), `LabelHelp.help` | `label`, `helper-text` |
99
104
 
100
105
  ## Cobertura e significado do gate verde
101
106
 
@@ -109,7 +114,7 @@ O extrator cobre três superfícies, sem heurística de nome:
109
114
  `@softize/opus/schema/zod`; metadata livre não é classificada por nome, declarações dinâmicas
110
115
  reprovam em vez de serem executadas e o dicionário mantém um snapshot imutável das entradas;
111
116
  3. texto estático em filhos e props de uma allowlist de componentes importados diretamente
112
- de `@softize/opus/ui*` — por exemplo, `Button`, `DialogTitle`, `DialogDescription`,
117
+ de `@softize/opus/ui*` — por exemplo, `Button`, `DialogTitle`, `DialogBody`,
113
118
  `FieldLabel`, `TabsTrigger`, `Page`, `Input` e `DataState`. Alias e namespace de import
114
119
  continuam rastreáveis. `uppercase`, variantes como `sm:hover:uppercase`, modificadores
115
120
  `!uppercase`/`uppercase!` e classes em descendentes são projetados na superfície que
@@ -24,7 +24,7 @@ export const salesLeads = defineDataProduct({
24
24
  sources: [{ id: 'followize', label: 'Followize' }],
25
25
  entities: ['Lead'],
26
26
  access: {
27
- contexts: ['sales'],
27
+ permissionContexts: ['sales'],
28
28
  organizationalScopes: ['unit', 'team'],
29
29
  },
30
30
  interfaces: ['sale.list', 'sales.performance'],
@@ -49,6 +49,12 @@ que uma interface deveria respeitar, por exemplo, unidade e equipe. Não é RLS
49
49
  automática. Toda interface precisa aplicar seus próprios `requires`, `authorize` e recortes no
50
50
  handler/repositório, inclusive quando for chamada por IA ou MCP.
51
51
 
52
+ `permissionContexts` nomeia especificamente chaves do vocabulário de permissão. Ele não descreve
53
+ domínio de dados, Área, departamento nem contexto de tela; projetos que não adotam essa dimensão
54
+ declaram uma lista vazia. O alias `contexts`, publicado originalmente na série 15.x, permanece
55
+ aceito na entrada e projetado no manifest apenas para compatibilidade. Novas declarações e
56
+ consumidores usam o nome qualificado; o alias só poderá ser removido numa versão major.
57
+
52
58
  ## Projeções
53
59
 
54
60
  `opus gen` publica os produtos no `.opus/manifest.json`. Actions expostas como tools carregam os
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softize/opus",
3
- "version": "15.2.1",
3
+ "version": "16.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",
@@ -153,6 +153,10 @@
153
153
  "types": "./src/seed/index.ts",
154
154
  "default": "./src/seed/index.ts"
155
155
  },
156
+ "./presentation": {
157
+ "types": "./src/presentation/index.ts",
158
+ "default": "./src/presentation/index.ts"
159
+ },
156
160
  "./server": {
157
161
  "types": "./src/server/index.ts",
158
162
  "default": "./src/server/index.ts"
@@ -212,17 +216,6 @@
212
216
  "bin": {
213
217
  "opus": "bin/cli.mjs"
214
218
  },
215
- "scripts": {
216
- "postinstall": "node ./bin/lib/postinstall.mjs",
217
- "copy:check": "node ./bin/cli.mjs copy --check",
218
- "typecheck": "tsc --noEmit",
219
- "test": "vitest run",
220
- "test:watch": "vitest",
221
- "test:cov": "vitest run --coverage",
222
- "registry:up": "npx -y verdaccio --config ~/.config/verdaccio/config.yaml",
223
- "release": "bash ./scripts/release.sh",
224
- "release:local": "bash ./scripts/release.sh --local"
225
- },
226
219
  "dependencies": {
227
220
  "@modelcontextprotocol/sdk": "^1.29.0",
228
221
  "@radix-ui/react-checkbox": "^1.1.3",
@@ -368,11 +361,21 @@
368
361
  "vitest": "^2.1.0",
369
362
  "zod": "^3.24.0"
370
363
  },
371
- "packageManager": "pnpm@9.0.0",
372
364
  "repository": {
373
365
  "type": "git",
374
366
  "url": "git+https://github.com/softize-dev/opus.git",
375
367
  "directory": "packages/opus"
376
368
  },
377
- "homepage": "https://opus.softize.com.br"
378
- }
369
+ "homepage": "https://opus.softize.com.br",
370
+ "scripts": {
371
+ "postinstall": "node ./bin/lib/postinstall.mjs",
372
+ "copy:check": "node ./bin/cli.mjs copy --check",
373
+ "typecheck": "tsc --noEmit",
374
+ "test": "vitest run",
375
+ "test:watch": "vitest",
376
+ "test:cov": "vitest run --coverage",
377
+ "registry:up": "npx -y verdaccio --config ~/.config/verdaccio/config.yaml",
378
+ "release": "bash ./scripts/release.sh",
379
+ "release:local": "bash ./scripts/release.sh --local"
380
+ }
381
+ }
@@ -6,26 +6,29 @@
6
6
  - URL representa estado que precisa sobreviver a refresh, deep link ou histórico.
7
7
  - `Page` fornece o `<main>` e o container centralizado com teto padrão de `80rem`. Sua forma
8
8
  explícita é `Page > PageHeader (PageBack? | PageNavigation?, PageTitle, PageDescription?,
9
- PageActions?) + PageBody`; `title`, `description` e `actions` no próprio `Page` são a abreviação
9
+ PageActions?) + PageBody`; `title`, `description` e `actions` no próprio `Page` são a abreviação
10
10
  para o caso direto.
11
11
  Não misturar as duas formas. Alterar `className` apenas quando a superfície tiver uma necessidade
12
12
  real de largura; não reconstruir esse container em cada rota.
13
13
  - Fora de `PageShell`, a composição explícita também pode trocar `PageHeader` por `PageIntro` quando
14
14
  o conteúdo precisar somente de título, descrição e ações, sem retorno ou breadcrumb. `PageIntro`
15
15
  dá mais presença ao título e não é uma abreviação visual de `PageHeader`.
16
- - `PageHeader` é a única região de cabeçalho da página. O default acompanha o container;
17
- `variant="bar"` apresenta a mesma anatomia como faixa compacta no topo. Em uma subpágina simples,
18
- `PageBack` recebe o destino pai explícito: aparece acima do título no default e como icon-only com
19
- tooltip na barra. Para mais de um ancestral relevante, use `Breadcrumb` dentro de
20
- `PageNavigation`. Não combine retorno e breadcrumb nem crie um chrome paralelo para uma `Page`.
21
- Ações com texto na barra usam `Button size="sm"`; ações somente com ícone usam `icon-sm`.
22
- `PageActionsTarget` fica reservado a workspaces imersivos que já possuam chrome próprio.
16
+ - `PageHeader` é a região de cabeçalho dentro de uma `Page` isolada e organiza navegação, título e
17
+ ações na mesma linha. Em uma subpágina simples, `PageBack` recebe o destino pai explícito e aparece
18
+ antes do título como controle somente com ícone. Para mais de um ancestral relevante, use
19
+ `Breadcrumb` dentro de `PageNavigation`. Não combine retorno e breadcrumb nem crie um chrome
20
+ paralelo para uma `Page`. `PageActionsTarget` fica reservado a workspaces imersivos que
21
+ possuam chrome próprio.
23
22
  - Quando shell e rota conhecem partes diferentes da mesma página, use `PageShell` ao redor da rota.
24
- O shell fornece `navigation`; a `Page` descendente continua declarando `title`, `description` e
25
- `actions`. O Opus mantém a barra de `3rem`, projeta as ações nela e apresenta título e descrição
26
- como `PageIntro` no conteúdo. Na forma explícita dentro do shell, use
27
- `Page > PageIntro (PageTitle, PageDescription?, PageActions?) + PageBody`. Não monte `PaneHeader`,
28
- portal ou seletor global para reconstruir essa composição.
23
+ O shell é o único responsável pela barra: fornece `navigation`; a `Page` descendente continua
24
+ declarando `title`, `description` e `actions`. O Opus mantém a barra de `3rem`, projeta as
25
+ ações nela e apresenta título e descrição como `PageIntro` no conteúdo. Ações com texto na barra
26
+ usam `Button size="sm"`; ações somente com ícone usam `icon-sm`. Na forma explícita dentro do
27
+ shell, use `Page > PageIntro (PageTitle, PageDescription?, PageActions?) + PageBody`. Não monte
28
+ `PaneHeader`, portal ou seletor global para reconstruir essa composição.
29
+ - Quando a rota precisa de navegação própria além da navegação persistente do shell, declare
30
+ `PageNavigation` no `PageIntro`. Esse slot permanece com o recurso ao alternar entre Page, Dialog
31
+ e Drawer; não replique nele a navegação global já fornecida pelo shell.
29
32
  - `PageState` substitui todo o conteúdo principal quando a página carrega, falha ou está vazia. Na
30
33
  forma curta ele pode ser escrito como filho direto de `Page`, que cria o `PageBody`; na forma
31
34
  explícita, fica sozinho dentro de `PageBody`. Nos três estados ativos, o cabeçalho da Page isolada
@@ -37,7 +40,7 @@
37
40
  recuperação como botão `outline` textual. Estados de seção ou coleção continuam em `DataState`,
38
41
  `ActionView`, `ActionList` ou `Alert`; não elevar uma falha parcial a estado da página.
39
42
  - `Content` delimita uma seção e segue a mesma anatomia: `Content > ContentHeader (ContentTitle,
40
- ContentDescription, ContentMeta, ContentActions) + ContentBody`. `ContentHeader` nunca fica
43
+ ContentDescription, ContentMeta, ContentActions) + ContentBody`. `ContentHeader` nunca fica
41
44
  solto. `title`, `description`, `count` e `actions` no `Content` são a abreviação para o caso
42
45
  direto e não podem ser misturados ao header explícito. `level` preserva a hierarquia semântica
43
46
  do heading.
@@ -64,6 +67,11 @@
64
67
  controles customizados pelo contexto. `ActionFormCard` e `ActionFormDialog` acrescentam a
65
68
  casca; não duplicar o form para obter card ou modal. Cancelamento em forms, confirmações e
66
69
  modais usa `ghost`, deixando o destaque visual para a ação principal.
70
+ - Cabeçalhos de `Dialog` e `Drawer` apenas nomeiam a superfície com o título. Não preencher uma
71
+ segunda linha por hábito. Consequência, restrição ou instrução que realmente mude a tarefa entra
72
+ no início de `DialogBody`/`DrawerBody`, como texto ou `Alert`; wrappers usam `intro` e a API
73
+ imperativa usa `body`. Relacione texto conciso por `aria-describedby` somente quando ele também
74
+ precisar descrever a superfície para tecnologias assistivas.
67
75
  - `ActionList` mantém fetch, toolbar, loading, erro, retry, vazio, seleção e paginação enquanto
68
76
  permite três composições de resultado: tabela por `columns`, renderer completo por `children`
69
77
  ou views nomeadas. Usar `ActionFilterBar` isoladamente só quando outra superfície assumir a
@@ -100,12 +108,12 @@
100
108
  O dicionário declara o papel; a tela só escolhe onde o valor fica. Tabela de decisão aplicada
101
109
  por `DictionaryValue` e pelas colunas de `ActionList`:
102
110
 
103
- | Papel declarado | Exemplos | Forma | Contexto | Variante | Ícone | Tooltip |
104
- |---|---|---|---|---|---|---|
105
- | `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 |
106
- | `status` | Aberto, resolvido, degradado | Badge | pela entrada; `neutral` por padrão | `subtle`; nunca `outline` | idem | idem |
107
- | `stage` | Prospect, cliente; etapa do funil | Badge | igual a `status` | `subtle` | idem | idem |
108
- | `plain` ou ausente | Fonte, formato, período | Texto | — | — | idem | idem |
111
+ | Papel declarado | Exemplos | Forma | Contexto | Variante | Ícone | Tooltip |
112
+ | ------------------ | ------------------------------------ | ----- | ---------------------------------- | -------------------------------------- | --------------------------------------- | ------------------------------------- |
113
+ | `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 |
114
+ | `status` | Aberto, resolvido, degradado | Badge | pela entrada; `neutral` por padrão | `subtle`; nunca `outline` | idem | idem |
115
+ | `stage` | Prospect, cliente; etapa do funil | Badge | igual a `status` | `subtle` | idem | idem |
116
+ | `plain` ou ausente | Fonte, formato, período | Texto | — | — | idem | idem |
109
117
 
110
118
  Regras que não dependem da tabela:
111
119
 
@@ -123,13 +131,22 @@ Exemplo correto — tipo e estágio em colunas próprias, apresentação do dici
123
131
 
124
132
  ```ts
125
133
  export const customerKindDict = t.dict(
126
- { pf: { label: 'Pessoa física', icon: 'user' }, pj: { label: 'Empresa', icon: 'building' } },
127
- { doc: 'Natureza da parte no cadastro global.', presentation: 'classification' },
128
- )
134
+ {
135
+ pf: { label: "Pessoa física", icon: "user" },
136
+ pj: { label: "Empresa", icon: "building" },
137
+ },
138
+ {
139
+ doc: "Natureza da parte no cadastro global.",
140
+ presentation: "classification",
141
+ },
142
+ );
129
143
  export const customerStageDict = t.dict(
130
- { prospect: { label: 'Prospect' }, customer: { label: 'Cliente', context: 'success' } },
131
- { doc: 'Estágio comercial atual da parte.', presentation: 'stage' },
132
- )
144
+ {
145
+ prospect: { label: "Prospect" },
146
+ customer: { label: "Cliente", context: "success" },
147
+ },
148
+ { doc: "Estágio comercial atual da parte.", presentation: "stage" },
149
+ );
133
150
  // contrato:
134
151
  // columns: [{ key: 'name' }, { key: 'kind', label: 'Tipo' }, { key: 'stage', label: 'Estágio' }]
135
152
  // output: z.object({ kind: customerKindDict.zod(), stage: customerStageDict.zod() })
@@ -14,12 +14,21 @@ export interface DataProductSource {
14
14
  }
15
15
 
16
16
  export interface DataProductAccess {
17
- /** Contextos de dados normalmente exigidos pelas interfaces do produto. Descritivo. */
17
+ /** @deprecated Use `permissionContexts`; este alias será removido numa versão major. */
18
18
  contexts: readonly string[]
19
+ /** Nome qualificado dos contextos de permissão. Sempre presente após `defineDataProduct`. */
20
+ permissionContexts?: readonly string[]
19
21
  /** Eixos organizacionais que as Actions precisam considerar, como unit e team. Descritivo. */
20
22
  organizationalScopes: readonly string[]
21
23
  }
22
24
 
25
+ export interface DataProductAccessInput {
26
+ permissionContexts?: readonly string[]
27
+ /** @deprecated Use `permissionContexts`; este alias será removido numa versão major. */
28
+ contexts?: readonly string[]
29
+ organizationalScopes: readonly string[]
30
+ }
31
+
23
32
  export interface DataProductConfig {
24
33
  /** Identidade estável e namespaced, como `sales.leads`. */
25
34
  id: string
@@ -46,6 +55,19 @@ export interface DataProductConfig {
46
55
  replacedBy?: string
47
56
  }
48
57
 
58
+ export type DataProductInput = Omit<DataProductConfig, 'access'> & { access: DataProductAccessInput }
59
+ type PermissionContextsOf<T extends DataProductAccessInput> = Extract<
60
+ | ('permissionContexts' extends keyof T ? T['permissionContexts'] : never)
61
+ | ('contexts' extends keyof T ? T['contexts'] : never),
62
+ readonly string[]
63
+ >
64
+ export type DefinedDataProduct<T extends DataProductInput = DataProductInput> = Omit<T, 'access'> & {
65
+ access: Omit<T['access'], 'contexts' | 'permissionContexts'> & DataProductAccess & {
66
+ contexts: PermissionContextsOf<T['access']>
67
+ permissionContexts: PermissionContextsOf<T['access']>
68
+ }
69
+ }
70
+
49
71
  const ID_RE = /^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)+$/
50
72
 
51
73
  function nonEmpty(value: unknown, field: string): void {
@@ -55,7 +77,7 @@ function nonEmpty(value: unknown, field: string): void {
55
77
  }
56
78
 
57
79
  /** Declara e valida um Produto de Dados sem introduzir dependência de banco ou runtime. */
58
- export function defineDataProduct<const T extends DataProductConfig>(config: T): T {
80
+ export function defineDataProduct<const T extends DataProductInput>(config: T): DefinedDataProduct<T> {
59
81
  if (!ID_RE.test(config.id)) {
60
82
  throw new TypeError(`DataProduct id "${config.id}" deve ser namespaced e casar com ${ID_RE.source}`)
61
83
  }
@@ -90,10 +112,23 @@ export function defineDataProduct<const T extends DataProductConfig>(config: T):
90
112
  throw new TypeError(`DataProduct "${config.id}" possui interface duplicada`)
91
113
  }
92
114
  for (const action of config.interfaces) nonEmpty(action, 'interfaces[]')
93
- if (!Array.isArray(config.access?.contexts) || !Array.isArray(config.access?.organizationalScopes)) {
94
- throw new TypeError('DataProduct "access" deve declarar contexts e organizationalScopes')
115
+ const hasPermissionContexts = Array.isArray(config.access?.permissionContexts)
116
+ const hasLegacyContexts = Array.isArray(config.access?.contexts)
117
+ if ((!hasPermissionContexts && !hasLegacyContexts) || !Array.isArray(config.access?.organizationalScopes)) {
118
+ throw new TypeError('DataProduct "access" deve declarar permissionContexts e organizationalScopes')
119
+ }
120
+ if (
121
+ hasPermissionContexts &&
122
+ hasLegacyContexts &&
123
+ (config.access.permissionContexts!.length !== config.access.contexts!.length ||
124
+ config.access.permissionContexts!.some((context, index) => context !== config.access.contexts![index]))
125
+ ) {
126
+ throw new TypeError('DataProduct "access" recebeu permissionContexts e contexts divergentes')
95
127
  }
96
- for (const context of config.access.contexts) nonEmpty(context, 'access.contexts[]')
128
+ const permissionContexts = (
129
+ hasPermissionContexts ? config.access.permissionContexts : config.access.contexts
130
+ ) as PermissionContextsOf<T['access']>
131
+ for (const context of permissionContexts) nonEmpty(context, 'access.permissionContexts[]')
97
132
  for (const scope of config.access.organizationalScopes) nonEmpty(scope, 'access.organizationalScopes[]')
98
133
  if (config.status !== undefined && config.status !== 'active' && config.status !== 'deprecated') {
99
134
  throw new TypeError('DataProduct "status" deve ser "active" ou "deprecated"')
@@ -107,13 +142,22 @@ export function defineDataProduct<const T extends DataProductConfig>(config: T):
107
142
  } else if (config.replacedBy !== undefined) {
108
143
  throw new TypeError('DataProduct ativo não pode declarar "replacedBy"')
109
144
  }
110
- return config
145
+ return {
146
+ ...config,
147
+ access: {
148
+ ...config.access,
149
+ permissionContexts,
150
+ contexts: permissionContexts,
151
+ },
152
+ } as unknown as DefinedDataProduct<T>
111
153
  }
112
154
 
113
155
  export function isDataProduct(value: unknown): value is DataProductConfig {
114
156
  if (typeof value !== 'object' || value === null) return false
157
+ const access = (value as { access?: { contexts?: unknown } }).access
158
+ if (!Array.isArray(access?.contexts)) return false
115
159
  try {
116
- defineDataProduct(value as DataProductConfig)
160
+ defineDataProduct(value as DataProductInput)
117
161
  return true
118
162
  } catch {
119
163
  return false
package/src/core/index.ts CHANGED
@@ -204,7 +204,10 @@ export type { DomainConfig, FlattenedDomain } from './domain.ts'
204
204
  export { defineDataProduct, isDataProduct } from './data-product.ts'
205
205
  export type {
206
206
  DataProductAccess,
207
+ DataProductAccessInput,
207
208
  DataProductConfig,
209
+ DataProductInput,
210
+ DefinedDataProduct,
208
211
  DataProductSource,
209
212
  DataProductStatus,
210
213
  } from './data-product.ts'