@softize/opus 18.5.0 → 20.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 +275 -0
- package/bin/cli.mjs +5 -0
- package/bin/lib/check.mjs +198 -10
- package/bin/lib/copy.mjs +29 -0
- package/bin/lib/mcp.mjs +1 -1
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +298 -0
- package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
- package/docs/data-products.md +15 -9
- package/docs/protocol.md +3 -4
- package/docs/releasing.md +2 -2
- package/package.json +2 -2
- package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
- package/registry/templates/app/package.json +1 -1
- package/src/core/runtime.ts +96 -8
- package/src/core/types.ts +5 -2
- package/src/ui/components/patterns/action-form-card.tsx +11 -1
- package/src/ui/components/patterns/content-header.tsx +16 -12
- package/src/ui/components/patterns/fill-region.ts +150 -0
- package/src/ui/components/patterns/heading-level.ts +16 -0
- package/src/ui/components/patterns/list.tsx +103 -73
- package/src/ui/components/patterns/page.tsx +20 -4
- package/src/ui/components/patterns/pane.tsx +294 -0
- package/src/ui/components/patterns/presentation.tsx +81 -54
- package/src/ui/components/patterns/sidebar.tsx +13 -72
- package/src/ui/components/patterns/split.tsx +51 -38
- package/src/ui/components/patterns/surface-anatomy.ts +246 -0
- package/src/ui/components/patterns/surface-header.tsx +51 -6
- package/src/ui/components/patterns/surface-media.tsx +69 -0
- package/src/ui/components/patterns/trigger.tsx +12 -2
- package/src/ui/components/primitives/alert.tsx +9 -1
- package/src/ui/components/primitives/avatar.tsx +7 -3
- package/src/ui/components/primitives/card.tsx +91 -29
- package/src/ui/components/primitives/carousel.tsx +34 -5
- package/src/ui/components/primitives/checkbox.tsx +11 -3
- package/src/ui/components/primitives/command.tsx +14 -5
- package/src/ui/components/primitives/copyable.tsx +120 -10
- package/src/ui/components/primitives/detail.tsx +10 -13
- package/src/ui/components/primitives/dialog.tsx +179 -45
- package/src/ui/components/primitives/drawer.tsx +131 -24
- package/src/ui/components/primitives/empty.tsx +1 -1
- package/src/ui/components/primitives/icon-picker.tsx +6 -2
- package/src/ui/components/primitives/icon-tile.tsx +7 -0
- package/src/ui/components/primitives/input-group.tsx +9 -2
- package/src/ui/components/primitives/input-otp.tsx +1 -1
- package/src/ui/components/primitives/kbd.tsx +1 -1
- package/src/ui/components/primitives/label.tsx +3 -0
- package/src/ui/components/primitives/menu.tsx +5 -0
- package/src/ui/components/primitives/metric-card.tsx +98 -39
- package/src/ui/components/primitives/popover.tsx +1 -1
- package/src/ui/components/primitives/progress.tsx +9 -1
- package/src/ui/components/primitives/slider.tsx +62 -11
- package/src/ui/components/primitives/truncate.tsx +10 -1
- package/src/ui/docs/content/accordion.md +74 -27
- package/src/ui/docs/content/action-form-card.md +24 -15
- package/src/ui/docs/content/action-form-dialog.md +17 -7
- package/src/ui/docs/content/action-form.md +92 -77
- package/src/ui/docs/content/action-list-dialog.md +75 -38
- package/src/ui/docs/content/action-list.md +134 -52
- package/src/ui/docs/content/action-trigger.md +27 -10
- package/src/ui/docs/content/action-view.md +14 -14
- package/src/ui/docs/content/actions.md +161 -66
- package/src/ui/docs/content/ai.md +43 -44
- package/src/ui/docs/content/alert.md +83 -29
- package/src/ui/docs/content/ask.md +51 -26
- package/src/ui/docs/content/aspect-ratio.md +22 -24
- package/src/ui/docs/content/audit.md +23 -19
- package/src/ui/docs/content/auth.md +38 -19
- package/src/ui/docs/content/avatar.md +54 -5
- package/src/ui/docs/content/badge.md +29 -9
- package/src/ui/docs/content/breadcrumb.md +21 -8
- package/src/ui/docs/content/button.md +73 -43
- package/src/ui/docs/content/calendar.md +134 -26
- package/src/ui/docs/content/card.md +252 -35
- package/src/ui/docs/content/carousel.md +37 -6
- package/src/ui/docs/content/chat.md +118 -37
- package/src/ui/docs/content/checkbox.md +73 -7
- package/src/ui/docs/content/cli.md +94 -55
- package/src/ui/docs/content/collapsible.md +43 -19
- package/src/ui/docs/content/command.md +30 -8
- package/src/ui/docs/content/composer.md +23 -8
- package/src/ui/docs/content/content.md +37 -7
- package/src/ui/docs/content/copyable.md +39 -8
- package/src/ui/docs/content/customization.md +55 -46
- package/src/ui/docs/content/cycle.md +33 -18
- package/src/ui/docs/content/data-products.md +115 -0
- package/src/ui/docs/content/data-state.md +20 -6
- package/src/ui/docs/content/data.md +20 -19
- package/src/ui/docs/content/detail.md +12 -5
- package/src/ui/docs/content/dialog.md +219 -28
- package/src/ui/docs/content/dictionary-value.md +4 -4
- package/src/ui/docs/content/dock.md +54 -19
- package/src/ui/docs/content/dot.md +35 -5
- package/src/ui/docs/content/drawer.md +193 -22
- package/src/ui/docs/content/empty-value.md +20 -6
- package/src/ui/docs/content/empty.md +24 -3
- package/src/ui/docs/content/events.md +76 -16
- package/src/ui/docs/content/executions.md +9 -9
- package/src/ui/docs/content/field.md +21 -6
- package/src/ui/docs/content/getting-started.md +44 -34
- package/src/ui/docs/content/icon-picker.md +19 -13
- package/src/ui/docs/content/icon-tile.md +28 -5
- package/src/ui/docs/content/input-otp.md +58 -7
- package/src/ui/docs/content/input.md +22 -3
- package/src/ui/docs/content/item.md +53 -6
- package/src/ui/docs/content/kbd.md +5 -1
- package/src/ui/docs/content/label.md +13 -4
- package/src/ui/docs/content/log.md +5 -5
- package/src/ui/docs/content/markdown.md +5 -4
- package/src/ui/docs/content/mcp.md +49 -23
- package/src/ui/docs/content/menu.md +95 -10
- package/src/ui/docs/content/metric-card.md +68 -21
- package/src/ui/docs/content/observability.md +10 -9
- package/src/ui/docs/content/page.md +125 -11
- package/src/ui/docs/content/pagination.md +13 -5
- package/src/ui/docs/content/pane.md +300 -0
- package/src/ui/docs/content/popover.md +42 -5
- package/src/ui/docs/content/presentation.md +91 -47
- package/src/ui/docs/content/progress.md +27 -14
- package/src/ui/docs/content/queue.md +39 -21
- package/src/ui/docs/content/radio-group.md +26 -4
- package/src/ui/docs/content/router.md +29 -10
- package/src/ui/docs/content/runtime.md +90 -54
- package/src/ui/docs/content/scheduler.md +53 -21
- package/src/ui/docs/content/scroll-area.md +42 -35
- package/src/ui/docs/content/select.md +78 -33
- package/src/ui/docs/content/semantic-context.md +21 -2
- package/src/ui/docs/content/separator.md +15 -9
- package/src/ui/docs/content/sidebar.md +25 -45
- package/src/ui/docs/content/skeleton.md +13 -4
- package/src/ui/docs/content/slider.md +37 -9
- package/src/ui/docs/content/spinner.md +4 -3
- package/src/ui/docs/content/split.md +34 -20
- package/src/ui/docs/content/storage.md +8 -9
- package/src/ui/docs/content/switch.md +10 -7
- package/src/ui/docs/content/table.md +46 -8
- package/src/ui/docs/content/tabs.md +14 -4
- package/src/ui/docs/content/testing.md +76 -31
- package/src/ui/docs/content/textarea.md +5 -4
- package/src/ui/docs/content/toast.md +48 -7
- package/src/ui/docs/content/toggle.md +3 -3
- package/src/ui/docs/content/tokens.md +15 -26
- package/src/ui/docs/content/tooltip.md +18 -2
- package/src/ui/docs/content/truncate.md +36 -6
- package/src/ui/docs/content/ui.md +74 -22
- package/src/ui/docs/content/upgrading.md +45 -36
- package/src/ui/docs/plugin.ts +36 -2
- package/src/ui/docs/registry.tsx +14 -2
- package/src/ui/drivers/react.tsx +16 -8
- package/src/ui/meta.ts +38 -32
- package/src/ui/react.tsx +14 -0
- package/src/vite/design.ts +16 -4
package/bin/lib/copy.mjs
CHANGED
|
@@ -124,6 +124,8 @@ const JSX_CHILD_ROLES = new Map([
|
|
|
124
124
|
// comando. Classificado como `button`, a política universal cobraria verbo de ação.
|
|
125
125
|
['PageBack', 'breadcrumb'],
|
|
126
126
|
['PageTitle', 'title'],
|
|
127
|
+
['PaneDescription', 'description'],
|
|
128
|
+
['PaneTitle', 'title'],
|
|
127
129
|
['TableCaption', 'description'],
|
|
128
130
|
['TableHead', 'heading'],
|
|
129
131
|
['TabsTrigger', 'tab'],
|
|
@@ -327,6 +329,10 @@ const JSX_ARRAY_PROP_ROLES = new Map([
|
|
|
327
329
|
],
|
|
328
330
|
])
|
|
329
331
|
|
|
332
|
+
// Props que recebem uma lista de textos, um por item. Cada thumb de um intervalo do Slider tem
|
|
333
|
+
// nome próprio, e é esse nome que o leitor de tela anuncia ao focar o controle.
|
|
334
|
+
const JSX_STRING_ARRAY_PROP_ROLES = new Map([['Slider', new Map([['thumbLabels', 'label']])]])
|
|
335
|
+
|
|
330
336
|
// Prop de classe que alcança cada superfície textual. A lista acompanha a implementação
|
|
331
337
|
// real dos componentes: `className` do ActionFormCard vai para o form, enquanto
|
|
332
338
|
// `cardClassName` envolve o título; o menu do Select custom vive em portal e não herda a
|
|
@@ -777,6 +783,7 @@ function knownJsxAggregateConsumer(expression, checker, mode) {
|
|
|
777
783
|
JSX_PROP_ROLES.get(component)?.has(prop) === true ||
|
|
778
784
|
JSX_OBJECT_PROP_ROLES.get(component)?.has(prop) === true ||
|
|
779
785
|
JSX_ARRAY_PROP_ROLES.get(component)?.has(prop) === true ||
|
|
786
|
+
JSX_STRING_ARRAY_PROP_ROLES.get(component)?.has(prop) === true ||
|
|
780
787
|
[...(JSX_PROP_CLASS.get(component)?.values() ?? [])].includes(prop) ||
|
|
781
788
|
[...(JSX_PROP_STYLE.get(component)?.values() ?? [])].includes(prop) ||
|
|
782
789
|
(JSX_CHILD_ROLES.has(component) && ['className', 'style'].includes(prop))
|
|
@@ -2435,6 +2442,28 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
2435
2442
|
}
|
|
2436
2443
|
}
|
|
2437
2444
|
|
|
2445
|
+
for (const [name, role] of JSX_STRING_ARRAY_PROP_ROLES.get(component) ?? []) {
|
|
2446
|
+
const item = attribute(opening.attributes, name)
|
|
2447
|
+
if (item.opaque !== undefined) {
|
|
2448
|
+
diagnostics.push(diagnostic(file, sourceFile, item.opaque, `<${component}>.${name}`))
|
|
2449
|
+
continue
|
|
2450
|
+
}
|
|
2451
|
+
if (item.candidate === undefined) continue
|
|
2452
|
+
const raw = attributeValue(item)
|
|
2453
|
+
const list = raw === null ? null : resolveBinding(raw, checker)
|
|
2454
|
+
if (list === null || !ts.isArrayLiteralExpression(list)) {
|
|
2455
|
+
diagnostics.push(
|
|
2456
|
+
diagnostic(file, sourceFile, raw ?? opening, `<${component}>.${name}`, raw === null ? 'structure' : 'content'),
|
|
2457
|
+
)
|
|
2458
|
+
continue
|
|
2459
|
+
}
|
|
2460
|
+
for (const [index, element] of list.elements.entries()) {
|
|
2461
|
+
const text = ts.isSpreadElement(element) ? null : staticText(element, checker)
|
|
2462
|
+
if (text === null) diagnostics.push(diagnostic(file, sourceFile, element, `<${component}>.${name}[${index}]`))
|
|
2463
|
+
else entries.push({ source: file, line: lineOf(sourceFile, text.node), role, text: text.text })
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2438
2467
|
const ariaLabelAttribute = attribute(opening.attributes, 'aria-label')
|
|
2439
2468
|
let hasStaticAriaLabel = false
|
|
2440
2469
|
if (ariaLabelAttribute.opaque !== undefined) {
|
package/bin/lib/mcp.mjs
CHANGED
|
@@ -42,7 +42,7 @@ export function buildServer() {
|
|
|
42
42
|
server.registerTool(
|
|
43
43
|
'opus_check',
|
|
44
44
|
{
|
|
45
|
-
description: 'Valida as convenções das actions (régua) — defineAction e o split defineContract+bindAction: naming, kind, ordem dos campos, export, requires-sem-authorize. Retorna findings.',
|
|
45
|
+
description: 'Valida as convenções das actions (régua) — defineAction e o split defineContract+bindAction: naming, kind, ordem dos campos, export, requires-sem-authorize, ignored-by-kind. Retorna findings.',
|
|
46
46
|
inputSchema: { dir: z.string().optional() },
|
|
47
47
|
},
|
|
48
48
|
async ({ dir }) => asText(await scanDir(dirOf(dir))),
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# ADR 0023 — Superfícies compartilham uma anatomia e dois layouts
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-09-23.
|
|
5
|
+
- **Complementa:** ADRs 0005, 0014, 0018, 0019 e 0021.
|
|
6
|
+
|
|
7
|
+
## Contexto
|
|
8
|
+
|
|
9
|
+
A ADR 0005 estabeleceu a gramática `Root > Header + Body + Footer` para as superfícies da casa. O
|
|
10
|
+
vocabulário foi adotado, mas a implementação não: `Page` e `Content` derivam o header de uma
|
|
11
|
+
infraestrutura interna comum, enquanto `Card`, `Dialog`, `Drawer` e `Pane` desenham o seu à mão.
|
|
12
|
+
|
|
13
|
+
O efeito acumulado é mensurável no código de hoje:
|
|
14
|
+
|
|
15
|
+
- **Três escalas de padding para o mesmo papel.** `p-3` no chrome de Page, `p-4` nos slots de Dialog
|
|
16
|
+
e Drawer, `px-5` no Card. `Card` é a única com ritmo vertical assimétrico (`pt-5 pb-3` no header,
|
|
17
|
+
`pt-3 pb-5` no footer). `Content`, `PageBody` e `PaneBody` não têm padding nenhum.
|
|
18
|
+
- **`PaneHeader` sem padding e `PaneFooter` com `p-2`** — assimetria dentro da mesma família.
|
|
19
|
+
- **`CardTitle` é um `div` sem escala tipográfica**, único título do conjunto que não é heading e
|
|
20
|
+
não declara tamanho; herda o que vier do contexto.
|
|
21
|
+
- **Divisor automático em quatro famílias e manual no Card.** A documentação pública ensina o
|
|
22
|
+
consumidor a escrever `border-b` no `CardHeader` e `border-t` no `CardFooter`.
|
|
23
|
+
- **`Dialog` e `Drawer` não têm slot de ações no header.** Quem precisa injeta nós soltos ao lado do
|
|
24
|
+
título, como `Presentation` faz hoje.
|
|
25
|
+
- **A família `Pane` está partida**: a raiz mora em `split.tsx`, os slots em `sidebar.tsx`, a
|
|
26
|
+
documentação está na página da Sidebar e nenhum teste cobre a anatomia.
|
|
27
|
+
- **`Drawer` duplica `Dialog`** com três divergências não declaradas: footer sem fundo, header sem a
|
|
28
|
+
supressão condicional do divisor e título com `text-foreground` a mais.
|
|
29
|
+
- **Nenhuma suíte transversal** compara as famílias; cada divergência só aparece quando alguém
|
|
30
|
+
compõe as duas superfícies lado a lado.
|
|
31
|
+
|
|
32
|
+
A evidência que motivou a decisão veio da tela do Evento de Pós-vendas: para aproximar dois Cards do
|
|
33
|
+
recorte já usado por Dialog e Content, a tela precisou escrever `flex-row items-center gap-4
|
|
34
|
+
border-b p-4` no header, `p-4` no body, `text-sm` no título, um `<h3>` dentro do `CardTitle` e o
|
|
35
|
+
ícone dentro do texto. São cinco decisões estruturais tomadas pelo consumidor.
|
|
36
|
+
|
|
37
|
+
As forças em tensão:
|
|
38
|
+
|
|
39
|
+
- **Uniformidade × identidade da família.** Um modal, um card e um painel não precisam ter as mesmas
|
|
40
|
+
medidas; precisam ter a mesma gramática e o mesmo comportamento estrutural.
|
|
41
|
+
- **Compatibilidade × correção.** Alterar o default de uma família muda todas as telas que já a
|
|
42
|
+
usam. Preservar o default errado perpetua o defeito que motivou a decisão.
|
|
43
|
+
- **Uma API genérica × seis APIs específicas.** Um `<Surface>` público reduziria código e apagaria a
|
|
44
|
+
semântica de cada família; seis implementações independentes preservam a semântica e produzem a
|
|
45
|
+
divergência atual.
|
|
46
|
+
|
|
47
|
+
## Decisão
|
|
48
|
+
|
|
49
|
+
### 1. Uma infraestrutura interna, seis fachadas públicas
|
|
50
|
+
|
|
51
|
+
As telas continuam usando `CardHeader`, `DialogHeader`, `DrawerHeader`, `PaneHeader`,
|
|
52
|
+
`ContentHeader` e `PageHeader`. Não existe superfície genérica pública. `SurfaceHeader` e o novo
|
|
53
|
+
módulo de anatomia permanecem internos: a aplicação não os importa.
|
|
54
|
+
|
|
55
|
+
A anatomia compartilhada passa a viver em um único módulo interno, que responde por:
|
|
56
|
+
|
|
57
|
+
- as classes de cada região (`header`, `body`, `footer`) por família e layout;
|
|
58
|
+
- a escala visual do título, separada do nível semântico;
|
|
59
|
+
- a posição e o tamanho das ações;
|
|
60
|
+
- a apresentação da mídia;
|
|
61
|
+
- os divisores entre regiões adjacentes;
|
|
62
|
+
- o alinhamento horizontal comum entre as regiões da mesma superfície.
|
|
63
|
+
|
|
64
|
+
Nenhuma família reimplementa essas regras. Quando uma família precisa divergir, a divergência é um
|
|
65
|
+
valor declarado na tabela de anatomia, não uma classe escrita no componente.
|
|
66
|
+
|
|
67
|
+
### 2. O layout é do root e chega aos slots por contexto
|
|
68
|
+
|
|
69
|
+
`layout` é propriedade do componente raiz: `Card`, `DialogContent`, `DrawerContent` e `Pane`. Os
|
|
70
|
+
slots não recebem `layout`; eles leem o contexto da superfície que os hospeda. Um slot usado fora da
|
|
71
|
+
sua família continua sendo erro, como hoje.
|
|
72
|
+
|
|
73
|
+
`Content` e `Page` não expõem `layout` de anatomia. `Content` é contínuo por definição e `Page`
|
|
74
|
+
mantém `layout="flow" | "fill"`, que descreve ocupação de altura, não anatomia. São nomes
|
|
75
|
+
diferentes para coisas diferentes e permanecem assim.
|
|
76
|
+
|
|
77
|
+
### 3. O que cada layout significa
|
|
78
|
+
|
|
79
|
+
`continuous` e `sectioned` não são a ausência e a presença de uma borda. Cada um é um pacote:
|
|
80
|
+
|
|
81
|
+
| Aspecto | `continuous` | `sectioned` |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| Divisores | nenhum | entre regiões adjacentes presentes |
|
|
84
|
+
| Ritmo vertical | header aproxima-se do corpo; o espaço separa | regiões simétricas; o divisor separa |
|
|
85
|
+
| Escala do título | a da família | a da família, um degrau mais contida quando a família tem duas |
|
|
86
|
+
| Fundo do footer | igual ao da superfície | sutil, quando a família declara |
|
|
87
|
+
| Alinhamento horizontal | idêntico entre header, body e footer | idêntico entre header, body e footer |
|
|
88
|
+
|
|
89
|
+
O alinhamento horizontal é invariante dos dois layouts: o recuo lateral de header, body e footer de
|
|
90
|
+
uma mesma superfície é sempre o mesmo valor. É o que a suíte transversal verifica.
|
|
91
|
+
|
|
92
|
+
### 4. Os divisores são derivados, não escritos
|
|
93
|
+
|
|
94
|
+
No layout `sectioned`, o divisor existe quando há duas regiões adjacentes. Um header seguido de nada
|
|
95
|
+
não desenha linha; um footer que é a única região também não. A regra é estrutural e vive no
|
|
96
|
+
componente, derivada da presença dos irmãos — não uma classe que o consumidor aplica.
|
|
97
|
+
|
|
98
|
+
A recomendação de escrever `border-b` e `border-t` no Card sai da documentação.
|
|
99
|
+
|
|
100
|
+
### 5. Defaults por família
|
|
101
|
+
|
|
102
|
+
Os defaults preservam o que cada família já entrega hoje, exceto onde o comportamento atual é o
|
|
103
|
+
defeito que esta decisão corrige:
|
|
104
|
+
|
|
105
|
+
| Família | Default | Por quê |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| `Card` | `continuous` | é o que a família entrega hoje: sem divisores, ritmo assimétrico |
|
|
108
|
+
| `Dialog` (default) | `sectioned` | já tem divisores e faixas; o default nomeia o que existe |
|
|
109
|
+
| `Dialog` (`mode="alert"`) | composição própria | a resposta obrigatória mantém sua anatomia centrada |
|
|
110
|
+
| `Drawer` | `sectioned` | idem Dialog |
|
|
111
|
+
| `Pane` | `sectioned` | header e footer já têm divisor |
|
|
112
|
+
| `Content` | contínuo, sem opção | região semântica dentro de outra superfície |
|
|
113
|
+
| `Page` | contínuo, sem opção | a separação do chrome pertence ao shell |
|
|
114
|
+
|
|
115
|
+
Nenhum default muda de valor efetivo com esta ADR. O que muda é que o valor passa a ter nome,
|
|
116
|
+
implementação única e teste.
|
|
117
|
+
|
|
118
|
+
### 5.1. O que o inventário dos consumidores mostrou
|
|
119
|
+
|
|
120
|
+
A migração foi decidida sobre 374 usos reais das seis famílias na plataforma. O que eles revelam:
|
|
121
|
+
|
|
122
|
+
- **Nenhuma tela usa `CardDescription`, `ContentDescription`, `DialogDescription` ou equivalente.**
|
|
123
|
+
Toda descrição é um `<p className="text-sm text-muted-foreground">` escrito à mão. Quando esse
|
|
124
|
+
parágrafo está dentro de um `DialogHeader`, ele contraria a ADR 0018 e precisa ir para o corpo na
|
|
125
|
+
migração — não ganhar um slot novo no header.
|
|
126
|
+
- **`Card` é usado como caixa sem slots em várias telas**, com o padding escolhido por elas (`p-3`,
|
|
127
|
+
`p-5`, `px-3 py-2`). O root do Card continua sem padding próprio: quem não usa slots continua
|
|
128
|
+
decidindo. O padding pertence aos slots, como hoje.
|
|
129
|
+
- **Os dois `PaneHeader` do produto escrevem quase a mesma linha** (`flex h-[3.75rem] items-center
|
|
130
|
+
… p-3 … border-b-0`) em contextos diferentes. É a prova de que padding e divisor pertencem à
|
|
131
|
+
primitiva.
|
|
132
|
+
- **Um comentário no código do login documenta o defeito**: o `py-3` do `CardBody` "conta com um
|
|
133
|
+
CardFooter, que o login não tem". O ritmo do layout contínuo precisa reagir à ausência do rodapé.
|
|
134
|
+
- **Três testes de consumidor contradizem a unificação** e precisam de decisão explícita, não de
|
|
135
|
+
ajuste mecânico: um proíbe `p-4` no Card, outro exige `p-0` no `DrawerBody`, outro proíbe
|
|
136
|
+
`ContentHeader` em uma seção. Os dois primeiros fixam contornos; o terceiro fixa uma escolha de
|
|
137
|
+
composição que continua válida.
|
|
138
|
+
|
|
139
|
+
### 6. Slots por família
|
|
140
|
+
|
|
141
|
+
A anatomia é comum; o conjunto de slots continua sendo decisão de cada família, porque decisões
|
|
142
|
+
anteriores já os delimitaram:
|
|
143
|
+
|
|
144
|
+
| Slot | Card | Content | Page | Dialog | Drawer | Pane |
|
|
145
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
146
|
+
| Media | sim | não | não | sim (`DialogMedia`) | sim | sim |
|
|
147
|
+
| Title | sim | sim | sim | sim | sim | sim |
|
|
148
|
+
| Description | sim | sim | não (ADR 0014) | não (ADR 0018) | não (ADR 0018) | não |
|
|
149
|
+
| Meta | não | sim (contador) | não (ADR 0009) | não | não | não |
|
|
150
|
+
| Actions | sim | sim | sim | sim | sim | sim |
|
|
151
|
+
| Body | sim | sim | sim | sim | sim | sim |
|
|
152
|
+
| Footer | sim | não | sim | sim | sim | sim |
|
|
153
|
+
|
|
154
|
+
As ADRs 0009, 0014 e 0018 não são reabertas: Page, Dialog e Drawer continuam sem descrição no
|
|
155
|
+
header, e Page continua sem contador. `Dialog`, `Drawer` e `Pane` ganham o slot de ações que hoje
|
|
156
|
+
não têm — a ausência obrigava a injetar nós soltos ao lado do título, e o `Studio` a manter três
|
|
157
|
+
anatomias paralelas para a mesma entrada declarativa. O botão de fechar continua sendo o último
|
|
158
|
+
elemento da linha; as ações da tela ficam antes dele.
|
|
159
|
+
|
|
160
|
+
`Pane` ganha também título e mídia. Hoje ele não tem nenhum dos dois, e as duas telas que precisam
|
|
161
|
+
de um cabeçalho remontam a linha com `span`, incluindo o ícone dentro do texto.
|
|
162
|
+
|
|
163
|
+
`Content` permanece sem footer: é uma região nomeada dentro de outra superfície, e um rodapé
|
|
164
|
+
pertence à superfície que a hospeda.
|
|
165
|
+
|
|
166
|
+
### 7. Título: hierarquia e tamanho são independentes
|
|
167
|
+
|
|
168
|
+
O nível semântico vem de `level` (`1`–`6`) ou de `asChild`, e nunca do tamanho visual. O tamanho vem
|
|
169
|
+
da família e do layout. `CardTitle` deixa de ser um `div` sem escala e passa a declarar as duas
|
|
170
|
+
coisas, com `h3` como default — um card é uma seção dentro de uma página que já tem `h1`.
|
|
171
|
+
|
|
172
|
+
### 8. Mídia reutiliza a moldura existente
|
|
173
|
+
|
|
174
|
+
O slot de mídia usa `IconTile` ou a mesma infraestrutura visual dele. Nenhuma família cria outra
|
|
175
|
+
escala de molduras.
|
|
176
|
+
|
|
177
|
+
Quando a marca já tem visual próprio — um orbe, um logotipo —, `asChild` deixa o slot ceder o
|
|
178
|
+
elemento para ela: a escala continua vindo da mesma fonte, e a moldura sai de cena em vez de ser
|
|
179
|
+
desenhada por baixo.
|
|
180
|
+
|
|
181
|
+
`DialogMedia` é a exceção declarada: no modo de alerta a mídia não acompanha uma linha de título,
|
|
182
|
+
ela é o elemento de reconhecimento da resposta, e mantém a moldura maior e centrada. A exceção vale
|
|
183
|
+
só nesse modo, deriva da mesma infraestrutura das demais e é coberta por teste. Fora do alerta, o
|
|
184
|
+
`DialogMedia` é a mesma moldura das outras famílias.
|
|
185
|
+
|
|
186
|
+
### 7.1. O Pane aceita conteúdo livre; as demais não
|
|
187
|
+
|
|
188
|
+
`Card`, `Dialog`, `Drawer`, `Page` e `Content` sempre nomeiam alguma coisa: seus headers aceitam só
|
|
189
|
+
os slots da família e reprovam nó solto, com uma mensagem que lista o que é aceito.
|
|
190
|
+
|
|
191
|
+
`Pane` é a superfície genérica — a coluna que hospeda navegação, marca, busca ou chrome que não é
|
|
192
|
+
título nem ação. Seu header aceita conteúdo livre ao lado dos slots, e esse conteúdo ocupa o mesmo
|
|
193
|
+
espaço do heading, de modo que as ações continuam indo para o extremo oposto sem classe no
|
|
194
|
+
consumidor. A permissividade é dele, não uma brecha geral: forçar a marca de uma barra lateral a
|
|
195
|
+
virar "título" seria uniformidade sem semântica.
|
|
196
|
+
|
|
197
|
+
O conteúdo livre é renderizado **dentro** da região de heading, depois do título e da descrição,
|
|
198
|
+
qualquer que seja a ordem no JSX. Ele herda dali a largura que o heading absorve, o que é o que
|
|
199
|
+
permite às ações continuarem no extremo oposto — mas também significa que uma busca colocada no
|
|
200
|
+
header aparece **abaixo** do título, não ao lado dele. Um header que precise de algo antes do
|
|
201
|
+
título usa o slot de mídia; um que precise de algo ao lado usa o de ações.
|
|
202
|
+
|
|
203
|
+
### 8.1. Nível semântico em todas as fachadas
|
|
204
|
+
|
|
205
|
+
Todo título de superfície aceita `level`. Nos modais, onde a primitiva de acessibilidade emite o
|
|
206
|
+
elemento, o nível troca a tag do mesmo nó por composição, nunca aninha um heading dentro de outro, e
|
|
207
|
+
não altera a escala. Sem `level`, vale o `h2` que a primitiva já emitia.
|
|
208
|
+
|
|
209
|
+
### 9. `Pane` deixa de ser uma família partida
|
|
210
|
+
|
|
211
|
+
Raiz e slots passam a viver no mesmo módulo, com validação de pertencimento, documentação própria e
|
|
212
|
+
os mesmos testes das demais famílias. `split.tsx` continua dono do redimensionamento; `sidebar.tsx`
|
|
213
|
+
continua dono da navegação. A fronteira deixa de ser acidental e passa a ser declarada.
|
|
214
|
+
|
|
215
|
+
### 10. Compatibilidade
|
|
216
|
+
|
|
217
|
+
Aliases depreciados existentes (`CardContent`, `PaneContent`) seguem seu plano de remoção; esta
|
|
218
|
+
decisão não cria novos.
|
|
219
|
+
|
|
220
|
+
Classes de recuo, ritmo e escala que o consumidor já aplica continuam funcionando: `className` é
|
|
221
|
+
mesclado por último e vence. Uma tela que ainda escreve `p-4` no `CardHeader` não quebra, apenas
|
|
222
|
+
deixa de precisar.
|
|
223
|
+
|
|
224
|
+
**Os divisores são a exceção, e é uma exceção real.** Eles nascem de variantes condicionais
|
|
225
|
+
(`:not(:last-child)`), que têm especificidade maior que uma classe simples: `border-b-0` no
|
|
226
|
+
consumidor não desliga a linha derivada. Desligar o divisor de uma região isolada não é, portanto,
|
|
227
|
+
uma decisão do consumidor — é decisão da superfície, e se expressa trocando o layout dela para
|
|
228
|
+
`continuous`. A paleta de comandos é o caso dentro do próprio SDK: ela é contínua, e não um
|
|
229
|
+
seccionado com a primeira linha apagada.
|
|
230
|
+
|
|
231
|
+
Pela mesma razão, a regra que suprime o divisor entre um header e um rodapé adjacentes precisa
|
|
232
|
+
vencer a que o cria — não basta vir depois dela na folha, porque a ordem das variantes não é nossa.
|
|
233
|
+
|
|
234
|
+
**O recuo inferior da última região no layout contínuo segue a mesma lógica** (20.0.0). Ele vem de
|
|
235
|
+
`[&:last-child]:pb-*`, e um `pb-0` do consumidor não o desliga quando a região é a última da
|
|
236
|
+
superfície: para mudar esse respiro, a superfície troca de layout. Quem precisar mesmo sobrescrever
|
|
237
|
+
escreve a mesma variante (`[&:last-child]:pb-*`), que o merge reconhece como substituta.
|
|
238
|
+
|
|
239
|
+
## Consequências
|
|
240
|
+
|
|
241
|
+
- Uma mudança de espaçamento, divisor ou escala passa a ter um lugar só; hoje tem seis.
|
|
242
|
+
- Card ganha um modo que a aplicação já construía à mão, e a documentação deixa de ensinar o
|
|
243
|
+
contorno.
|
|
244
|
+
- Dialog e Drawer ganham ações no header; `Presentation` deixa de injetar nós soltos.
|
|
245
|
+
- Pane deixa de ser exceção não testada.
|
|
246
|
+
- Consumidores que aplicavam classes de correção podem removê-las; quem não remover continua
|
|
247
|
+
funcionando, o que permite migração incremental.
|
|
248
|
+
- O custo é concentrado: quase toda a superfície visual do SDK muda de implementação ao mesmo tempo,
|
|
249
|
+
e a rede de proteção precisa existir antes da migração, não depois.
|
|
250
|
+
- Uma família que precise divergir passa a precisar declarar a divergência na tabela e cobri-la por
|
|
251
|
+
teste. Isso é deliberado: torna a diferença uma decisão, não um acidente.
|
|
252
|
+
|
|
253
|
+
## Alternativas descartadas
|
|
254
|
+
|
|
255
|
+
### Manter cada família com seu header
|
|
256
|
+
|
|
257
|
+
Preservaria compatibilidade absoluta e evitaria o custo de migração. Foi descartada porque é o
|
|
258
|
+
estado atual: produziu três escalas de padding, um título sem escala, divisores manuais em uma
|
|
259
|
+
família e a documentação pública ensinando o contorno.
|
|
260
|
+
|
|
261
|
+
### Expor um `<Surface>` genérico
|
|
262
|
+
|
|
263
|
+
Reduziria o código a uma implementação e uma API. Foi descartada porque apagaria a semântica de cada
|
|
264
|
+
família — um card, um modal e um painel deixariam de se distinguir no código da tela — e porque o
|
|
265
|
+
ganho real está na infraestrutura interna, que este ADR unifica sem expor.
|
|
266
|
+
|
|
267
|
+
### Unificar também as medidas entre famílias
|
|
268
|
+
|
|
269
|
+
Um único recuo para todas as superfícies eliminaria a tabela de anatomia. Foi descartada porque as
|
|
270
|
+
famílias têm densidades legítimas diferentes: o chrome de página é mais compacto que um card, e um
|
|
271
|
+
modal tem largura controlada. O invariante que importa é o alinhamento dentro de cada superfície,
|
|
272
|
+
não a igualdade entre superfícies diferentes.
|
|
273
|
+
|
|
274
|
+
### Tratar `layout` como propriedade de cada slot
|
|
275
|
+
|
|
276
|
+
Permitiria um header seccionado com um corpo contínuo. Foi descartada porque o layout é a relação
|
|
277
|
+
entre as regiões: um divisor depende de quem está do outro lado. Propriedade por slot permitiria
|
|
278
|
+
compor superfícies incoerentes sem erro.
|
|
279
|
+
|
|
280
|
+
### Derivar os divisores por CSS no consumidor
|
|
281
|
+
|
|
282
|
+
Manteria o componente menor. Foi descartada porque devolve ao consumidor a decisão que esta ADR
|
|
283
|
+
retira dele, e porque a regra depende de presença de irmãos, que o componente conhece e a tela
|
|
284
|
+
não.
|
|
285
|
+
|
|
286
|
+
## Verificação
|
|
287
|
+
|
|
288
|
+
- Uma suíte transversal parametrizada percorre as famílias configuráveis e verifica, para cada
|
|
289
|
+
layout: alinhamento horizontal idêntico entre header, body e footer; divisor apenas entre regiões
|
|
290
|
+
adjacentes presentes; escala do título independente do nível semântico; header com e sem mídia,
|
|
291
|
+
descrição e ações; ações no extremo oposto sem classe no consumidor; body sem header; header sem
|
|
292
|
+
footer; footer com e sem ações.
|
|
293
|
+
- A mesma suíte verifica que `Page` e `Content` não expõem `sectioned` e que nenhuma família
|
|
294
|
+
reimplementa as classes de anatomia localmente.
|
|
295
|
+
- Testes de família continuam cobrindo o que é específico de cada uma.
|
|
296
|
+
- Exemplos vivos no catálogo apresentam a matriz de layouts e slots.
|
|
297
|
+
- `opus check` continua reprovando slots fora da família.
|
|
298
|
+
- Um teste de API pública reprova a exportação de `SurfaceHeader` ou do módulo de anatomia.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# ADR 0024 — A ocupação de altura se propaga por contexto
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-09-25.
|
|
5
|
+
- **Complementa:** ADRs 0017 e 0023.
|
|
6
|
+
|
|
7
|
+
## Contexto
|
|
8
|
+
|
|
9
|
+
`Page layout="fill"` limita a página à altura do shell, e `ActionList layout="fill"` ocupa a altura
|
|
10
|
+
disponível e rola somente os resultados. As duas metades já existiam desde a 18.3.0, mas não se
|
|
11
|
+
encontravam sozinhas: a altura só chega à lista se cada elemento entre a página e ela for uma coluna
|
|
12
|
+
flexível com `min-h-0`. `PageBody`, `Content` e `ContentBody` não faziam isso.
|
|
13
|
+
|
|
14
|
+
O efeito era mensurável no código e na documentação:
|
|
15
|
+
|
|
16
|
+
- a página da `ActionList` ensinava a escrever `flex min-h-0 flex-1 flex-col` em `PageBody`,
|
|
17
|
+
`Content` e `ContentBody` para o `fill` funcionar;
|
|
18
|
+
- a `Presentation` de lista em `layout="fill"` envolvia o corpo em `Content` e `ContentBody` sem essa
|
|
19
|
+
cadeia e não repassava `layout` à lista — o `fill` declarado nunca chegava a ela;
|
|
20
|
+
- quando uma classe faltava, a lista crescia com o conteúdo e o `fill` se comportava como `flow`,
|
|
21
|
+
sem erro nem aviso.
|
|
22
|
+
|
|
23
|
+
As forças em tensão:
|
|
24
|
+
|
|
25
|
+
- **Automação × previsibilidade.** A cadeia é mecânica e o consumidor erra ao montá-la à mão; mas
|
|
26
|
+
uma página `fill` também hospeda seções irmãs, cards e diálogos que não devem virar colunas
|
|
27
|
+
flexíveis por acidente.
|
|
28
|
+
- **Contexto React × DOM.** O contexto atravessa qualquer componente, inclusive portais e
|
|
29
|
+
contêineres que não repassam altura; a altura só atravessa o DOM elemento a elemento.
|
|
30
|
+
- **Default × explícito.** Uma lista dentro da cadeia quase sempre quer `fill`; uma lista em uma
|
|
31
|
+
seção secundária da mesma página quer `flow`.
|
|
32
|
+
|
|
33
|
+
## Decisão
|
|
34
|
+
|
|
35
|
+
### 1. A raiz publica, os elos republicam, a folha se registra
|
|
36
|
+
|
|
37
|
+
- **Raiz:** `Page layout="fill"` publica a cadeia. `Page layout="flow"` a interrompe. A
|
|
38
|
+
`Presentation` em página herda o comportamento da `Page` que monta.
|
|
39
|
+
- **Elos canônicos:** `PageBody`, `Content`, `ContentBody` e o corpo da `Presentation` leem a
|
|
40
|
+
cadeia e a republicam para os filhos. Na cadeia, carregam `data-fill="false"`.
|
|
41
|
+
- **Folha:** a `ActionList`. Sem `layout`, ela assume `fill` quando o elemento pai no DOM é um elo
|
|
42
|
+
(`data-fill`) e se registra na cadeia. Com `layout="fill"` explícito, registra-se mesmo sob um
|
|
43
|
+
contêiner próprio; com `layout="flow"` explícito, nunca se registra.
|
|
44
|
+
|
|
45
|
+
### 2. Só o caminho até a folha vira coluna flexível
|
|
46
|
+
|
|
47
|
+
Um elo aplica `flex min-h-0 flex-1 flex-col` e passa a `data-fill="true"` somente quando uma folha
|
|
48
|
+
se registrou abaixo dele. Seções irmãs sem lista mantêm a altura do próprio conteúdo, e uma página
|
|
49
|
+
`fill` sem lista renderiza exatamente como antes. O registro acontece em `useLayoutEffect`, antes
|
|
50
|
+
da primeira pintura.
|
|
51
|
+
|
|
52
|
+
### 3. Superfícies com anatomia própria interrompem a cadeia
|
|
53
|
+
|
|
54
|
+
`Card`, `Dialog`, `Drawer` e `Pane` interrompem a propagação no provider de layout que já
|
|
55
|
+
compartilham (ADR 0023). A altura do corpo delas é decisão delas, inclusive quando um portal as
|
|
56
|
+
renderiza dentro de uma página `fill`. A própria `ActionList` também interrompe a cadeia para o que
|
|
57
|
+
ela hospeda. Em `Presentation`, o `fill` continua valendo apenas para a superfície `page`.
|
|
58
|
+
|
|
59
|
+
### 4. O mecanismo é interno
|
|
60
|
+
|
|
61
|
+
O contexto e os hooks vivem em um módulo interno, fora da API pública. Um contêiner próprio entre o
|
|
62
|
+
elo e a lista não é detectado como elo: quem o escreve declara `layout="fill"` na lista e dá ao
|
|
63
|
+
contêiner a coluna flexível. Novos elos ou folhas entram no SDK, com teste, e não no consumidor.
|
|
64
|
+
|
|
65
|
+
## Consequências
|
|
66
|
+
|
|
67
|
+
- `Page layout="fill"` › `PageBody` › `Content` › `ContentBody` › `ActionList` funciona sem classes
|
|
68
|
+
no consumidor, e a `Presentation` de lista em `fill` passa a entregar a rolagem interna prometida.
|
|
69
|
+
- Classes que o consumidor já escrevia para montar a cadeia continuam funcionando e podem ser
|
|
70
|
+
removidas: o `className` é mesclado por último e repete as mesmas classes.
|
|
71
|
+
- Uma `ActionList` sem `layout`, filha direta de um elo em uma página `fill`, passa a ocupar a altura.
|
|
72
|
+
Quem queria a lista crescendo com a página declara `layout="flow"`.
|
|
73
|
+
- A primeira renderização de uma cadeia com folha faz uma passada extra de layout effect. No
|
|
74
|
+
servidor, a cadeia só se forma na hidratação.
|
|
75
|
+
- `Tabs` e outros contêineres intermediários não são elos, em qualquer ponto do caminho. A lista só
|
|
76
|
+
confere o pai imediato: com um contêiner próprio entre dois elos (por exemplo, um `div` entre
|
|
77
|
+
`PageBody` e `Content`), ela ainda se registra e os elos viram coluna flexível, mas a altura não
|
|
78
|
+
atravessa o contêiner e a lista se comporta como `flow`. Quem compõe assim dá ao contêiner a
|
|
79
|
+
coluna flexível, como antes.
|
|
80
|
+
- O elo ativo vira coluna flexível com `align-items: stretch`, que a lista precisa. Os demais filhos
|
|
81
|
+
de cada elo ativo também são afetados: um botão inline se estica à largura do elo e margens
|
|
82
|
+
verticais deixam de colapsar. Para um irmão manter a largura do conteúdo, dê a ele `self-start`
|
|
83
|
+
ou envolva-o em um contêiner próprio.
|
|
84
|
+
- `Dialog` em `mode="alert"` também interrompe a cadeia, como os demais modos.
|
|
85
|
+
|
|
86
|
+
## Alternativas descartadas
|
|
87
|
+
|
|
88
|
+
### Manter a cadeia no consumidor
|
|
89
|
+
|
|
90
|
+
Não custa nada ao SDK. Foi descartada porque é o estado que produziu a documentação ensinando
|
|
91
|
+
classes de correção e a `Presentation` com `fill` que não chegava à lista.
|
|
92
|
+
|
|
93
|
+
### Aplicar a coluna flexível em todo elo dentro de `fill`
|
|
94
|
+
|
|
95
|
+
Mais simples: bastaria ler o contexto. Foi descartada porque duas seções irmãs com `flex-1` dividem
|
|
96
|
+
a altura ao meio e o conteúdo de uma transborda sobre a outra, e porque `flex-col` estica filhos
|
|
97
|
+
inline, como botões, que antes tinham a largura do conteúdo.
|
|
98
|
+
|
|
99
|
+
### Detectar a folha só por CSS (`:has`)
|
|
100
|
+
|
|
101
|
+
Evitaria o registro em React. Foi descartada porque `:has` enxerga qualquer descendente no DOM,
|
|
102
|
+
inclusive listas dentro de cards e contêineres que não repassam a altura, e porque a decisão da
|
|
103
|
+
lista — `fill` ou `flow` — ainda precisaria do contexto.
|
|
104
|
+
|
|
105
|
+
### Expor o contexto na API pública
|
|
106
|
+
|
|
107
|
+
Deixaria o consumidor criar elos próprios. Foi adiada: sem um caso real além de `ActionList`, o
|
|
108
|
+
contrato público seria especulativo.
|
|
109
|
+
|
|
110
|
+
## Verificação
|
|
111
|
+
|
|
112
|
+
- `tests/ui/fill-region.test.tsx` renderiza a cadeia `Page fill` › `PageBody` › `Content` ›
|
|
113
|
+
`ContentBody` › `ActionList` e confere atributo e classes de cada nível; cobre seções irmãs,
|
|
114
|
+
página `fill` sem lista, página `flow`, `layout="flow"` explícito, contêiner próprio com e sem
|
|
115
|
+
`fill` explícito, a interrupção por `Card` e a interrupção pela própria lista.
|
|
116
|
+
- `tests/ui/presentation.test.tsx` confere a `Presentation` de lista em `fill`, dentro e fora de
|
|
117
|
+
`PageShell`, e a lista em `flow` quando a `Presentation` é `flow`.
|
|
118
|
+
- A rolagem real é conferida em navegador: a happy-dom não calcula layout.
|
package/docs/data-products.md
CHANGED
|
@@ -10,7 +10,10 @@ Ele não é uma query, tabela ou view: essas são implementações possíveis. A
|
|
|
10
10
|
recorte e autorização continua sendo cada Action.
|
|
11
11
|
|
|
12
12
|
```ts
|
|
13
|
-
import { defineDataProduct, defineDomain } from '@softize/opus'
|
|
13
|
+
import { defineDataProduct, defineDomain } from '@softize/opus/core'
|
|
14
|
+
import { LeadEntity } from './entity.ts' // defineEntity({ name: 'lead', … })
|
|
15
|
+
import { leadList } from './actions/list.ts' // action 'lead.list'
|
|
16
|
+
import { leadFunnel } from './actions/funnel.ts' // action 'lead.funnel'
|
|
14
17
|
|
|
15
18
|
export const salesLeads = defineDataProduct({
|
|
16
19
|
id: 'sales.leads',
|
|
@@ -22,24 +25,27 @@ export const salesLeads = defineDataProduct({
|
|
|
22
25
|
classification: 'internal',
|
|
23
26
|
nature: 'real',
|
|
24
27
|
sources: [{ id: 'followize', label: 'Followize' }],
|
|
25
|
-
entities: ['
|
|
28
|
+
entities: ['lead'],
|
|
26
29
|
access: {
|
|
27
30
|
permissionContexts: ['sales'],
|
|
28
31
|
organizationalScopes: ['unit', 'team'],
|
|
29
32
|
},
|
|
30
|
-
interfaces: ['
|
|
33
|
+
interfaces: ['lead.list', 'lead.funnel'],
|
|
31
34
|
})
|
|
32
35
|
|
|
33
|
-
export const
|
|
34
|
-
name: '
|
|
35
|
-
entities: {
|
|
36
|
-
actions: {
|
|
36
|
+
export const leadDomain = defineDomain({
|
|
37
|
+
name: 'lead',
|
|
38
|
+
entities: { LeadEntity },
|
|
39
|
+
actions: { leadList, leadFunnel },
|
|
37
40
|
dataProducts: { salesLeads },
|
|
38
41
|
})
|
|
39
42
|
```
|
|
40
43
|
|
|
41
|
-
`interfaces` e `entities` usam os nomes declarados
|
|
42
|
-
|
|
44
|
+
`interfaces` e `entities` usam os nomes declarados — o `name` da action e o `name` de
|
|
45
|
+
`defineEntity` —, não a chave do objeto, o nome do arquivo ou da tabela. O registro do domínio e
|
|
46
|
+
`opus check` detectam relações inexistentes: aqui, `entities: ['LeadEntity']` ou `['Lead']` seriam
|
|
47
|
+
recusados, porque a entidade se declara como `lead`. As actions seguem o prefixo do domínio
|
|
48
|
+
(`lead.*`), como qualquer action registrada nele. `sources` identifica sistemas ou conjuntos
|
|
43
49
|
upstream; uma Fonte não precisa ser uma Entity Opus.
|
|
44
50
|
|
|
45
51
|
## Acesso e segurança
|
package/docs/protocol.md
CHANGED
|
@@ -1606,7 +1606,6 @@ defineReaction({
|
|
|
1606
1606
|
|
|
1607
1607
|
handler: async (ctx, event) => {
|
|
1608
1608
|
await syncToSalesforce(event.data.leadId)
|
|
1609
|
-
await ctx.emit('hub.lead.synced', { leadId: event.data.leadId })
|
|
1610
1609
|
},
|
|
1611
1610
|
})
|
|
1612
1611
|
|
|
@@ -1629,7 +1628,7 @@ type ReactionContext = {
|
|
|
1629
1628
|
user: User | null // ator que disparou o evento (pode ser system)
|
|
1630
1629
|
tenantId: string | null
|
|
1631
1630
|
db: unknown // mesmo data adapter
|
|
1632
|
-
emit: EmitFn //
|
|
1631
|
+
emit: EmitFn // descarta o evento sem publicar (sem encadeamento)
|
|
1633
1632
|
log: LoggerAdapter // logger pre-bound com contexto da reaction
|
|
1634
1633
|
storage: StorageAdapter | null // storage de arquivos (experimental)
|
|
1635
1634
|
provenance: Provenance // { kind: 'reaction', … }
|
|
@@ -1638,7 +1637,7 @@ type ReactionContext = {
|
|
|
1638
1637
|
}
|
|
1639
1638
|
```
|
|
1640
1639
|
|
|
1641
|
-
|
|
1640
|
+
Uma reaction não encadeia outra por evento: dentro dela, `ctx.emit` descarta o evento sem publicá-lo. Com `config.dev` ativo, cada descarte registra o aviso `runtime.reaction_emit_ignored` no log; em produção, o descarte é silencioso. Quando outro consumidor precisa desse fato, emita o evento no handler da action que o produz e declare-o em `emits`.
|
|
1642
1641
|
|
|
1643
1642
|
### Setup unificado
|
|
1644
1643
|
|
|
@@ -1690,7 +1689,7 @@ para cada reaction:
|
|
|
1690
1689
|
↓ (a) dedup check (se declarado)
|
|
1691
1690
|
↓ (b) authorize (se declarado)
|
|
1692
1691
|
↓ (c) handler executa (com timeout)
|
|
1693
|
-
↓ (d)
|
|
1692
|
+
↓ (d) ctx.emit dentro da reaction não publica (descartado, com aviso em dev)
|
|
1694
1693
|
↓ (e) audit (reaction completed/failed)
|
|
1695
1694
|
↓ (f) retry se falhou e config permite
|
|
1696
1695
|
```
|
package/docs/releasing.md
CHANGED
|
@@ -60,7 +60,7 @@ pra testar publicação seria a guarda atrapalhando quem está experimentando.
|
|
|
60
60
|
|
|
61
61
|
**Gate de qualidade**: depois do bump e da materialização, mas antes de publicar, roda
|
|
62
62
|
`pnpm typecheck` + `pnpm test` + `pnpm copy:check` + `base copy check` e **aborta a release
|
|
63
|
-
se qualquer um falhar**. O Opus declara `@softize/base ^2.
|
|
63
|
+
se qualquer um falhar**. O Opus declara `@softize/base ^2.4.0` em `dependencies`, pois usa
|
|
64
64
|
suas APIs públicas de filesystem em runtime, e materializa os artefatos Base no próprio repo;
|
|
65
65
|
a release não baixa uma política ad hoc. Como o
|
|
66
66
|
Opus **ship source** (`.ts`, sem build), essa é a última barreira antes do tarball — sem ela,
|
|
@@ -74,7 +74,7 @@ não é bloqueada por cobertura abaixo de 100%; a cobertura é acompanhada com `
|
|
|
74
74
|
**Smoke do esqueleto**: depois do bump e antes do publish, o `release.sh` gera um app com
|
|
75
75
|
`opus create`, instala o **tarball exato** que vai ser publicado e roda os gates dele
|
|
76
76
|
(typecheck · test · `opus check` · `opus copy --check` · `base copy check` · manifest ·
|
|
77
|
-
build). O template exige `@softize/base ^2.
|
|
77
|
+
build). O template exige `@softize/base ^2.4.0`; publique a Base compatível antes do Opus.
|
|
78
78
|
O `minimumReleaseAgeExclude` do template inclui os dois pacotes, e o smoke executa os
|
|
79
79
|
fragmentos de pre-push para provar que o layout pnpm instalado resolve ambos os CLIs.
|
|
80
80
|
É o que pega o que typecheck+test não
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softize/opus",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "20.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",
|
|
@@ -247,7 +247,7 @@
|
|
|
247
247
|
"@radix-ui/react-slot": "^1.1.1",
|
|
248
248
|
"@radix-ui/react-tabs": "^1.1.2",
|
|
249
249
|
"@radix-ui/react-tooltip": "^1.1.6",
|
|
250
|
-
"@softize/base": "^2.
|
|
250
|
+
"@softize/base": "^2.4.0",
|
|
251
251
|
"@tailwindcss/typography": "^0.5.20",
|
|
252
252
|
"@types/markdown-it": "^14.1.2",
|
|
253
253
|
"class-variance-authority": "^0.7.1",
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const KINDS = new Set(['simple', 'form', 'list', 'view'])
|
|
4
4
|
const cap = (value) => value.charAt(0).toUpperCase() + value.slice(1)
|
|
5
|
-
const
|
|
5
|
+
const camel = (value) => value.split('-').map((part, index) => (index === 0 ? part : cap(part))).join('')
|
|
6
|
+
const constant = (resource, verb) => camel(`${resource}-${verb}`)
|
|
6
7
|
|
|
7
8
|
function contractBody({ resource, verb, kind }) {
|
|
8
9
|
const name = `${resource}.${verb}`
|