@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.
Files changed (151) hide show
  1. package/CHANGELOG.md +275 -0
  2. package/bin/cli.mjs +5 -0
  3. package/bin/lib/check.mjs +198 -10
  4. package/bin/lib/copy.mjs +29 -0
  5. package/bin/lib/mcp.mjs +1 -1
  6. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +298 -0
  7. package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
  8. package/docs/data-products.md +15 -9
  9. package/docs/protocol.md +3 -4
  10. package/docs/releasing.md +2 -2
  11. package/package.json +2 -2
  12. package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
  13. package/registry/templates/app/package.json +1 -1
  14. package/src/core/runtime.ts +96 -8
  15. package/src/core/types.ts +5 -2
  16. package/src/ui/components/patterns/action-form-card.tsx +11 -1
  17. package/src/ui/components/patterns/content-header.tsx +16 -12
  18. package/src/ui/components/patterns/fill-region.ts +150 -0
  19. package/src/ui/components/patterns/heading-level.ts +16 -0
  20. package/src/ui/components/patterns/list.tsx +103 -73
  21. package/src/ui/components/patterns/page.tsx +20 -4
  22. package/src/ui/components/patterns/pane.tsx +294 -0
  23. package/src/ui/components/patterns/presentation.tsx +81 -54
  24. package/src/ui/components/patterns/sidebar.tsx +13 -72
  25. package/src/ui/components/patterns/split.tsx +51 -38
  26. package/src/ui/components/patterns/surface-anatomy.ts +246 -0
  27. package/src/ui/components/patterns/surface-header.tsx +51 -6
  28. package/src/ui/components/patterns/surface-media.tsx +69 -0
  29. package/src/ui/components/patterns/trigger.tsx +12 -2
  30. package/src/ui/components/primitives/alert.tsx +9 -1
  31. package/src/ui/components/primitives/avatar.tsx +7 -3
  32. package/src/ui/components/primitives/card.tsx +91 -29
  33. package/src/ui/components/primitives/carousel.tsx +34 -5
  34. package/src/ui/components/primitives/checkbox.tsx +11 -3
  35. package/src/ui/components/primitives/command.tsx +14 -5
  36. package/src/ui/components/primitives/copyable.tsx +120 -10
  37. package/src/ui/components/primitives/detail.tsx +10 -13
  38. package/src/ui/components/primitives/dialog.tsx +179 -45
  39. package/src/ui/components/primitives/drawer.tsx +131 -24
  40. package/src/ui/components/primitives/empty.tsx +1 -1
  41. package/src/ui/components/primitives/icon-picker.tsx +6 -2
  42. package/src/ui/components/primitives/icon-tile.tsx +7 -0
  43. package/src/ui/components/primitives/input-group.tsx +9 -2
  44. package/src/ui/components/primitives/input-otp.tsx +1 -1
  45. package/src/ui/components/primitives/kbd.tsx +1 -1
  46. package/src/ui/components/primitives/label.tsx +3 -0
  47. package/src/ui/components/primitives/menu.tsx +5 -0
  48. package/src/ui/components/primitives/metric-card.tsx +98 -39
  49. package/src/ui/components/primitives/popover.tsx +1 -1
  50. package/src/ui/components/primitives/progress.tsx +9 -1
  51. package/src/ui/components/primitives/slider.tsx +62 -11
  52. package/src/ui/components/primitives/truncate.tsx +10 -1
  53. package/src/ui/docs/content/accordion.md +74 -27
  54. package/src/ui/docs/content/action-form-card.md +24 -15
  55. package/src/ui/docs/content/action-form-dialog.md +17 -7
  56. package/src/ui/docs/content/action-form.md +92 -77
  57. package/src/ui/docs/content/action-list-dialog.md +75 -38
  58. package/src/ui/docs/content/action-list.md +134 -52
  59. package/src/ui/docs/content/action-trigger.md +27 -10
  60. package/src/ui/docs/content/action-view.md +14 -14
  61. package/src/ui/docs/content/actions.md +161 -66
  62. package/src/ui/docs/content/ai.md +43 -44
  63. package/src/ui/docs/content/alert.md +83 -29
  64. package/src/ui/docs/content/ask.md +51 -26
  65. package/src/ui/docs/content/aspect-ratio.md +22 -24
  66. package/src/ui/docs/content/audit.md +23 -19
  67. package/src/ui/docs/content/auth.md +38 -19
  68. package/src/ui/docs/content/avatar.md +54 -5
  69. package/src/ui/docs/content/badge.md +29 -9
  70. package/src/ui/docs/content/breadcrumb.md +21 -8
  71. package/src/ui/docs/content/button.md +73 -43
  72. package/src/ui/docs/content/calendar.md +134 -26
  73. package/src/ui/docs/content/card.md +252 -35
  74. package/src/ui/docs/content/carousel.md +37 -6
  75. package/src/ui/docs/content/chat.md +118 -37
  76. package/src/ui/docs/content/checkbox.md +73 -7
  77. package/src/ui/docs/content/cli.md +94 -55
  78. package/src/ui/docs/content/collapsible.md +43 -19
  79. package/src/ui/docs/content/command.md +30 -8
  80. package/src/ui/docs/content/composer.md +23 -8
  81. package/src/ui/docs/content/content.md +37 -7
  82. package/src/ui/docs/content/copyable.md +39 -8
  83. package/src/ui/docs/content/customization.md +55 -46
  84. package/src/ui/docs/content/cycle.md +33 -18
  85. package/src/ui/docs/content/data-products.md +115 -0
  86. package/src/ui/docs/content/data-state.md +20 -6
  87. package/src/ui/docs/content/data.md +20 -19
  88. package/src/ui/docs/content/detail.md +12 -5
  89. package/src/ui/docs/content/dialog.md +219 -28
  90. package/src/ui/docs/content/dictionary-value.md +4 -4
  91. package/src/ui/docs/content/dock.md +54 -19
  92. package/src/ui/docs/content/dot.md +35 -5
  93. package/src/ui/docs/content/drawer.md +193 -22
  94. package/src/ui/docs/content/empty-value.md +20 -6
  95. package/src/ui/docs/content/empty.md +24 -3
  96. package/src/ui/docs/content/events.md +76 -16
  97. package/src/ui/docs/content/executions.md +9 -9
  98. package/src/ui/docs/content/field.md +21 -6
  99. package/src/ui/docs/content/getting-started.md +44 -34
  100. package/src/ui/docs/content/icon-picker.md +19 -13
  101. package/src/ui/docs/content/icon-tile.md +28 -5
  102. package/src/ui/docs/content/input-otp.md +58 -7
  103. package/src/ui/docs/content/input.md +22 -3
  104. package/src/ui/docs/content/item.md +53 -6
  105. package/src/ui/docs/content/kbd.md +5 -1
  106. package/src/ui/docs/content/label.md +13 -4
  107. package/src/ui/docs/content/log.md +5 -5
  108. package/src/ui/docs/content/markdown.md +5 -4
  109. package/src/ui/docs/content/mcp.md +49 -23
  110. package/src/ui/docs/content/menu.md +95 -10
  111. package/src/ui/docs/content/metric-card.md +68 -21
  112. package/src/ui/docs/content/observability.md +10 -9
  113. package/src/ui/docs/content/page.md +125 -11
  114. package/src/ui/docs/content/pagination.md +13 -5
  115. package/src/ui/docs/content/pane.md +300 -0
  116. package/src/ui/docs/content/popover.md +42 -5
  117. package/src/ui/docs/content/presentation.md +91 -47
  118. package/src/ui/docs/content/progress.md +27 -14
  119. package/src/ui/docs/content/queue.md +39 -21
  120. package/src/ui/docs/content/radio-group.md +26 -4
  121. package/src/ui/docs/content/router.md +29 -10
  122. package/src/ui/docs/content/runtime.md +90 -54
  123. package/src/ui/docs/content/scheduler.md +53 -21
  124. package/src/ui/docs/content/scroll-area.md +42 -35
  125. package/src/ui/docs/content/select.md +78 -33
  126. package/src/ui/docs/content/semantic-context.md +21 -2
  127. package/src/ui/docs/content/separator.md +15 -9
  128. package/src/ui/docs/content/sidebar.md +25 -45
  129. package/src/ui/docs/content/skeleton.md +13 -4
  130. package/src/ui/docs/content/slider.md +37 -9
  131. package/src/ui/docs/content/spinner.md +4 -3
  132. package/src/ui/docs/content/split.md +34 -20
  133. package/src/ui/docs/content/storage.md +8 -9
  134. package/src/ui/docs/content/switch.md +10 -7
  135. package/src/ui/docs/content/table.md +46 -8
  136. package/src/ui/docs/content/tabs.md +14 -4
  137. package/src/ui/docs/content/testing.md +76 -31
  138. package/src/ui/docs/content/textarea.md +5 -4
  139. package/src/ui/docs/content/toast.md +48 -7
  140. package/src/ui/docs/content/toggle.md +3 -3
  141. package/src/ui/docs/content/tokens.md +15 -26
  142. package/src/ui/docs/content/tooltip.md +18 -2
  143. package/src/ui/docs/content/truncate.md +36 -6
  144. package/src/ui/docs/content/ui.md +74 -22
  145. package/src/ui/docs/content/upgrading.md +45 -36
  146. package/src/ui/docs/plugin.ts +36 -2
  147. package/src/ui/docs/registry.tsx +14 -2
  148. package/src/ui/drivers/react.tsx +16 -8
  149. package/src/ui/meta.ts +38 -32
  150. package/src/ui/react.tsx +14 -0
  151. 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.
@@ -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: ['Lead'],
28
+ entities: ['lead'],
26
29
  access: {
27
30
  permissionContexts: ['sales'],
28
31
  organizationalScopes: ['unit', 'team'],
29
32
  },
30
- interfaces: ['sale.list', 'sales.performance'],
33
+ interfaces: ['lead.list', 'lead.funnel'],
31
34
  })
32
35
 
33
- export const salesDomain = defineDomain({
34
- name: 'sales',
35
- entities: { Lead },
36
- actions: { saleList, salesPerformance },
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, não nomes de arquivo ou tabela. O registro do
42
- domínio e `opus check` detectam relações inexistentes. `sources` identifica sistemas ou conjuntos
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 // reaction pode emitir novos eventos (chain)
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
- Reaction pode emitir eventos novos via `ctx.emit` — habilita workflows declarativos em cadeia (`lead.created` → reaction → `lead.onboarded` → outra reaction → ...).
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) handler emite eventos (opcional, via ctx.emit)
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.3.0` em `dependencies`, pois usa
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.3.0`; publique a Base compatível antes do Opus.
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": "18.5.0",
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.3.0",
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 constant = (resource, verb) => resource + verb.split('-').map(cap).join('')
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}`
@@ -30,7 +30,7 @@
30
30
  "zod": "^3.24.0"
31
31
  },
32
32
  "devDependencies": {
33
- "@softize/base": "^2.3.0",
33
+ "@softize/base": "^2.4.0",
34
34
  "@tailwindcss/vite": "^4.1.0",
35
35
  "@types/node": "^22.0.0",
36
36
  "@types/react": "^19.0.0",