@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.
- package/CHANGELOG.md +59 -16
- package/bin/lib/check.mjs +98 -15
- package/bin/lib/copy.mjs +811 -314
- package/bin/lib/gen-manifest.mjs +24 -23
- package/bin/lib/gen-runner.mjs +198 -149
- package/docs/adr/0010-page-header-owns-page-chrome.md +3 -4
- package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +5 -3
- package/docs/adr/0012-data-products-are-first-class-declarations.md +4 -2
- package/docs/adr/0012-modal-header-only-names-the-surface.md +45 -0
- package/docs/adr/0013-presentation-is-a-portable-action-oriented-artifact.md +92 -0
- package/docs/code-style.md +24 -19
- package/docs/data-products.md +7 -1
- package/package.json +18 -15
- package/registry/skills/build-opus-ui/references/ui-patterns.md +43 -26
- package/registry/skills/maintain-opus-docs/scripts/audit-docs.mjs +0 -0
- package/src/core/data-product.ts +51 -7
- package/src/core/index.ts +3 -0
- package/src/core/presentation.ts +512 -0
- package/src/presentation/index.ts +1 -0
- package/src/ui/components/patterns/action-list-dialog.tsx +26 -9
- package/src/ui/components/patterns/confirm.tsx +34 -29
- package/src/ui/components/patterns/form-dialog.tsx +20 -8
- package/src/ui/components/patterns/list.tsx +26 -9
- package/src/ui/components/patterns/page.tsx +99 -139
- package/src/ui/components/patterns/presentation.tsx +316 -0
- package/src/ui/components/patterns/sidebar.tsx +3 -3
- package/src/ui/components/patterns/trigger.tsx +38 -17
- package/src/ui/components/primitives/button-group.tsx +53 -43
- package/src/ui/components/primitives/command.tsx +30 -72
- package/src/ui/components/primitives/dialog.tsx +23 -89
- package/src/ui/components/primitives/drawer.tsx +8 -34
- package/src/ui/docs/content/action-form-dialog.md +12 -10
- package/src/ui/docs/content/action-list-dialog.md +22 -17
- package/src/ui/docs/content/button.md +52 -35
- package/src/ui/docs/content/communication.md +26 -26
- package/src/ui/docs/content/dialog.md +173 -154
- package/src/ui/docs/content/drawer.md +12 -11
- package/src/ui/docs/content/page.md +72 -91
- package/src/ui/docs/content/presentation.md +158 -0
- package/src/ui/docs/registry.tsx +6 -0
- package/src/ui/meta.ts +8 -2
- 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.
|
package/docs/code-style.md
CHANGED
|
@@ -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
|
-
{
|
|
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
|
|
82
|
-
|
|
83
|
-
| `label`, `confirm.*Label`, `ActionTrigger.label`
|
|
84
|
-
| `title`, `examples[].name`
|
|
85
|
-
| `summary`, `description`, `examples[].description`, `expand.*.description`
|
|
86
|
-
| `messages.success`, `messages.error`, `messages.confirmation`
|
|
87
|
-
| `confirm.message`
|
|
88
|
-
| `fields.*.label`, `filters.*.label`
|
|
89
|
-
| `placeholder`, `help`
|
|
90
|
-
| opções estáticas
|
|
91
|
-
| `columns[].label`, `periods[].label`
|
|
92
|
-
| `Select.emptyText`, `Select.searchPlaceholder`
|
|
93
|
-
| `Select.options[].hint/triggerLabel/group`
|
|
94
|
-
| `ActionTrigger.confirm.*`
|
|
95
|
-
| `t.dict` — `label`, `description`, `doc` das entradas e `doc` do dicionário
|
|
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`
|
|
98
|
-
| `TooltipContent` (children), `LabelHelp.help`
|
|
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`, `
|
|
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
|
package/docs/data-products.md
CHANGED
|
@@ -24,7 +24,7 @@ export const salesLeads = defineDataProduct({
|
|
|
24
24
|
sources: [{ id: 'followize', label: 'Followize' }],
|
|
25
25
|
entities: ['Lead'],
|
|
26
26
|
access: {
|
|
27
|
-
|
|
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": "
|
|
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
|
-
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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 já
|
|
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
|
|
25
|
-
`actions`. O Opus mantém a barra de `3rem`, projeta as
|
|
26
|
-
como `PageIntro` no conteúdo.
|
|
27
|
-
`
|
|
28
|
-
|
|
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
|
-
|
|
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
|
|
104
|
-
|
|
105
|
-
| `classification`
|
|
106
|
-
| `status`
|
|
107
|
-
| `stage`
|
|
108
|
-
| `plain` ou ausente | Fonte, formato, período
|
|
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
|
-
{
|
|
127
|
-
|
|
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
|
-
{
|
|
131
|
-
|
|
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() })
|
|
File without changes
|
package/src/core/data-product.ts
CHANGED
|
@@ -14,12 +14,21 @@ export interface DataProductSource {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export interface DataProductAccess {
|
|
17
|
-
/**
|
|
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
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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'
|