@softize/opus 9.1.1 → 11.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 (43) hide show
  1. package/CHANGELOG.md +52 -3
  2. package/bin/lib/check.mjs +37 -1
  3. package/bin/lib/db-check-runner.mjs +5 -5
  4. package/bin/lib/db-migrate-runner.mjs +3 -3
  5. package/bin/lib/db-scaffold-runner.mjs +4 -4
  6. package/bin/lib/db.mjs +1 -1
  7. package/bin/lib/gen-manifest.mjs +0 -3
  8. package/bin/lib/gen-runner.mjs +2 -7
  9. package/docs/data-layer.md +2 -2
  10. package/docs/elevation-scale.md +2 -2
  11. package/docs/protocol.md +2 -2
  12. package/package.json +1 -1
  13. package/registry/skills/build-opus-ui/SKILL.md +8 -3
  14. package/registry/skills/build-opus-ui/references/evaluations.md +2 -0
  15. package/registry/skills/build-opus-ui/references/ui-patterns.md +3 -0
  16. package/src/core/domain.ts +3 -6
  17. package/src/schema/entity.ts +1 -1
  18. package/src/ui/components/patterns/shell-nav.tsx +5 -9
  19. package/src/ui/components/patterns/sidebar.tsx +40 -15
  20. package/src/ui/components/primitives/alert-dialog.tsx +1 -1
  21. package/src/ui/components/primitives/calendar.tsx +1 -1
  22. package/src/ui/components/primitives/card.tsx +3 -3
  23. package/src/ui/components/primitives/chat.tsx +1 -1
  24. package/src/ui/components/primitives/composer.tsx +2 -2
  25. package/src/ui/components/primitives/dialog.tsx +1 -1
  26. package/src/ui/components/primitives/menu.tsx +2 -2
  27. package/src/ui/components/primitives/popover.tsx +1 -1
  28. package/src/ui/docs/DocBrowser.tsx +25 -10
  29. package/src/ui/docs/content/card.md +1 -1
  30. package/src/ui/docs/content/composer.md +1 -1
  31. package/src/ui/docs/content/confirm.md +2 -2
  32. package/src/ui/docs/content/sidebar.md +21 -1
  33. package/src/ui/docs/content/tokens.md +4 -7
  34. package/src/ui/docs/registry.tsx +0 -6
  35. package/src/ui/meta.ts +3 -24
  36. package/src/ui/react.tsx +4 -19
  37. package/src/ui/theme.css +0 -7
  38. package/docs/shellnav.md +0 -131
  39. package/src/ui/components/patterns/app-shell.tsx +0 -227
  40. package/src/ui/components/patterns/section-shell.tsx +0 -246
  41. package/src/ui/docs/content/app-shell.md +0 -155
  42. package/src/ui/docs/content/resizable.md +0 -86
  43. package/src/ui/docs/content/section-shell.md +0 -121
package/CHANGELOG.md CHANGED
@@ -11,6 +11,55 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
11
11
  > tinha ficado sem registro nenhum, o que é exatamente o caso que este arquivo existe
12
12
  > pra cobrir.
13
13
 
14
+ ## 11.0.0 — 2026-08-21
15
+
16
+ **Breaking — deprecateds removidos na fronteira pública.** O major anterior manteve por
17
+ engano APIs cuja própria documentação prometia remoção na próxima versão; esta versão
18
+ fecha a migração em vez de carregar duas taxonomias indefinidamente.
19
+
20
+ - `AppShell`, `AppShellBar`, `AppShellTrigger`, `useAppShell`, `useSidebarSlot` e
21
+ `SectionShell` (incluindo seus tipos `Section*`) saem. Componha chrome e seções com
22
+ `Split`, `Pane`, `Sidebar`, `PaneHeader`, `PaneContent`, `PaneFooter` e `SidebarNav`.
23
+ - `ResizablePanelGroup`, `ResizablePanel` e `ResizableHandle` deixam de ser exports
24
+ públicos. Use `<Split resizable>` e declare tamanho/limites nos `<Pane>`.
25
+ - `SidebarHeader`, `SidebarContent` e `SidebarFooter` saem; eram aliases de
26
+ `PaneHeader`, `PaneContent` e `PaneFooter`.
27
+ - `DomainConfig.models` sai. Declare entidades em `DomainConfig.entities`; os comandos
28
+ `opus db` e o gerador agora leem somente esse campo. O manifest deixa de emitir o array
29
+ redundante `models`; os nomes continuam disponíveis em `entities[].name`.
30
+
31
+ `SidebarNav` passa a aceitar `subgroups`, cobrindo navegação contextual e a árvore de três
32
+ níveis da documentação sem um shell especializado. `ShellNav` agora deriva o estado
33
+ recolhido diretamente da `Sidebar`. O próprio `DocBrowser` foi migrado para a composição
34
+ canônica e serve como consumidor de referência.
35
+
36
+ **Migração:** substitua shells prontos pela composição `Split > Pane > Sidebar`; troque os
37
+ aliases `Sidebar*` pelos slots `Pane*`; envolva painéis ajustáveis em `<Split resizable>`;
38
+ renomeie `domain.models` para `domain.entities` e, ao consumir manifest, derive nomes de
39
+ `domain.entities.map(entity => entity.name)`.
40
+
41
+ ## 10.0.0 — 2026-08-20
42
+
43
+ **Breaking — forma usa a escala do Tailwind e superfícies sempre carregam seu foreground.**
44
+
45
+ - `Card` volta a ser flat por padrão, alinhando a implementação ao contrato já documentado
46
+ no componente e no catálogo. Consumidores que precisam de elevação devem declarar a
47
+ intensidade explicitamente (`shadow-sm`, `shadow-md`, etc.).
48
+ - Forma deixa de criar uma taxonomia paralela por componente: `rounded-card` e
49
+ `rounded-dialog` migram para `rounded-xl`; `rounded-popover`, para `rounded-md`. Os tokens
50
+ `--radius-card/popover/dialog` saem, e `opus check` acusa classes e overrides antigos.
51
+ - Superfícies `card` e `popover` passam a exigir seu foreground no mesmo fragmento de
52
+ classes. Dialog, Composer, Chat e o fundo nativo do Calendar foram corrigidos; `opus check`
53
+ agora reprova `bg-card` sem `text-card-foreground` e `bg-popover` sem
54
+ `text-popover-foreground`, evitando que a igualdade acidental com o foreground global
55
+ esconda temas quebrados.
56
+
57
+ **Migração:** troque `rounded-card` e `rounded-dialog` por `rounded-xl`, e
58
+ `rounded-popover` por `rounded-md`. Remova overrides de `--radius-card`,
59
+ `--radius-dialog` e `--radius-popover`; ajuste `--radius` ou a escala `--radius-*` quando
60
+ necessário. Sempre declare `bg-card text-card-foreground` e
61
+ `bg-popover text-popover-foreground` juntos. `opus check` aponta todos esses casos.
62
+
14
63
  ## 9.1.1 — 2026-08-20
15
64
 
16
65
  **`Ask` leva elicitação estruturada para `@softize/opus/ui/react`.** O componente
@@ -89,7 +138,6 @@ todas as mensagens que apenas têm comportamento `sticky` disponível.
89
138
  fronteira memoizada separada do estado do composer, e cada bloco Markdown reaproveita o
90
139
  resultado enquanto seu conteúdo não muda. Conversas longas mantêm o custo de digitação
91
140
  constante; durante streaming, somente a mensagem alterada volta a passar pelo parser.
92
-
93
141
  ## 9.0.3 — 2026-08-12
94
142
 
95
143
  **O pre-push ignora worktrees internos do Maestro.** Projetos sob `.maestro/` pertencem
@@ -132,8 +180,9 @@ depender de coincidências do call site:
132
180
  removidos, deixando `ring-*` para foco e estados transitórios.
133
181
  - a elevação volta ao vocabulário de intensidade do Tailwind (`shadow-sm`, `shadow-md`,
134
182
  `shadow-lg` etc.), com a composição clara e difusa controlada pelo tema do Opus.
135
- `Card`/`Composer`, menus/popovers e modais usam respectivamente `sm`, `md` e `lg` como
136
- defaults; os aliases por papel e as variáveis intermediárias `--elevation-*` saem.
183
+ Na 9.0.0, `Card`/`Composer`, menus/popovers e modais passaram a usar respectivamente
184
+ `sm`, `md` e `lg` como defaults; os aliases por papel e as variáveis intermediárias
185
+ `--elevation-*` saíram. O default do `Card` foi posteriormente removido na 10.0.0.
137
186
  - `shadow-lg` ganha spread negativo e menos opacidade: mantém alcance, mas deixa de
138
187
  engrossar visualmente a borda no ponto de contato com a superfície.
139
188
  - toda a escala passa a declarar a receita diretamente em `--shadow-*`, preservando a
package/bin/lib/check.mjs CHANGED
@@ -53,11 +53,25 @@ const REMOVED_UI_TOKENS = new Map([
53
53
  ['shadow-card', 'use `shadow-sm`'],
54
54
  ['shadow-popover', 'use `shadow-md`'],
55
55
  ['shadow-dialog', 'use `shadow-lg`'],
56
+ ['rounded-card', 'use `rounded-xl`'],
57
+ ['rounded-popover', 'use `rounded-md`'],
58
+ ['rounded-dialog', 'use `rounded-xl`'],
56
59
  ['--edge-value', 'remova o override; a aresta agora usa `--border`'],
57
60
  ['--color-edge', 'remova o token; a aresta agora usa `--border`'],
58
61
  ['--elevation-card', 'use/ajuste `--shadow-sm`'],
59
62
  ['--elevation-popover', 'use/ajuste `--shadow-md`'],
60
63
  ['--elevation-dialog', 'use/ajuste `--shadow-lg`'],
64
+ ['--radius-card', 'use/ajuste `--radius-xl`'],
65
+ ['--radius-popover', 'use/ajuste `--radius-md`'],
66
+ ['--radius-dialog', 'use/ajuste `--radius-xl`'],
67
+ ])
68
+
69
+ // Superfícies com foreground próprio formam um par indivisível. Exigir as duas
70
+ // utilities no mesmo fragmento de classes torna a regra local, revisável e evita que a
71
+ // igualdade acidental com `--foreground` esconda um tema quebrado.
72
+ const UI_SURFACE_PAIRS = new Map([
73
+ ['bg-card', 'text-card-foreground'],
74
+ ['bg-popover', 'text-popover-foreground'],
61
75
  ])
62
76
 
63
77
  /** Os três nomes que denotam action/contrato — o arquivo sem nenhum deles é pulado. */
@@ -399,7 +413,7 @@ export function checkUiMigrations(file, text) {
399
413
  action: token,
400
414
  line,
401
415
  file,
402
- message: `token removido na 9.0.0 — ${replacement}.`,
416
+ message: `token removido — ${replacement}.`,
403
417
  })
404
418
  }
405
419
  for (const [index, raw] of source.split(/\r?\n/).entries()) {
@@ -422,6 +436,28 @@ export function checkUiMigrations(file, text) {
422
436
  const objectProperty = /\b(?!aria(?:Label|Description)\b|label\b|title\b|description\b|text\b|message\b|note\b|content\b)[A-Za-z_$][\w$-]*\s*:\s*$/.test(beforeValue)
423
437
  const classListCall = /\bclassList\.(?:add|remove|toggle|replace|contains)\s*\([^)]*$/.test(beforeValue)
424
438
  const stylePropertyCall = /\b(?:style\.)?setProperty\s*\([^)]*$/.test(beforeValue)
439
+ const utilityContext = classAttribute || classExpression || utilityHelper || classBinding || objectProperty || classListCall
440
+ const utilities = value.trim().split(/\s+/).filter(Boolean)
441
+ for (const [background, foreground] of UI_SURFACE_PAIRS) {
442
+ const pairKey = `surface:${background}`
443
+ const hasUtility = (expected) => utilities.some((utility) => {
444
+ const normalized = utility.replace(/^!/, '').replace(/!$/, '')
445
+ return normalized === expected || normalized.startsWith(`${expected}/`)
446
+ })
447
+ const hasBackground = hasUtility(background)
448
+ const hasForeground = hasUtility(foreground)
449
+ if (!seen.has(pairKey) && utilityContext && hasBackground && !hasForeground) {
450
+ seen.add(pairKey)
451
+ findings.push({
452
+ rule: 'unpaired-ui-surface',
453
+ level: 'error',
454
+ action: background,
455
+ line: startLine,
456
+ file,
457
+ message: `superfície sem o foreground correspondente — use \`${background} ${foreground}\` no mesmo fragmento de classes.`,
458
+ })
459
+ }
460
+ }
425
461
  for (const [token, replacement] of REMOVED_UI_TOKENS) {
426
462
  const usedAsUtility = matchesToken(value, token) && (
427
463
  classAttribute || classExpression || utilityHelper || classBinding || objectProperty ||
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * Contrato do `opus.config.ts` pros comandos `db`:
10
10
  * - `entities?: EntityConfig[]` — entidades explícitas (opcional)
11
- * - `domains?: DomainConfig[]` — entidades coletadas de `domain.models`
11
+ * - `domains?: DomainConfig[]` — entidades coletadas de `domain.entities`
12
12
  * - `database: () => Kysely | Promise<Kysely>` — factory LAZY do banco.
13
13
  * É factory (não instância) de propósito: o `gen` importa o mesmo config
14
14
  * sem nunca chamar `database()`, então não abre conexão.
@@ -29,9 +29,9 @@ function emit(obj) {
29
29
 
30
30
  function collectEntities(domain, out) {
31
31
  if (domain === null || typeof domain !== 'object') return
32
- const models = domain.models
33
- if (models !== undefined && models !== null && typeof models === 'object') {
34
- for (const value of Object.values(models)) {
32
+ const entities = domain.entities
33
+ if (entities !== undefined && entities !== null && typeof entities === 'object') {
34
+ for (const value of Object.values(entities)) {
35
35
  if (isEntityConfig(value)) out.push(value)
36
36
  }
37
37
  }
@@ -59,7 +59,7 @@ async function main() {
59
59
  }
60
60
 
61
61
  if (entities.length === 0) {
62
- emit({ ok: false, error: 'nenhuma entidade encontrada (config.entities ou domain.models)' })
62
+ emit({ ok: false, error: 'nenhuma entidade encontrada (config.entities ou domain.entities)' })
63
63
  return
64
64
  }
65
65
 
@@ -28,9 +28,9 @@ function emit(obj) {
28
28
 
29
29
  function collectEntities(domain, out) {
30
30
  if (domain === null || typeof domain !== 'object') return
31
- const models = domain.models
32
- if (models !== undefined && models !== null && typeof models === 'object') {
33
- for (const value of Object.values(models)) if (isEntityConfig(value)) out.push(value)
31
+ const entities = domain.entities
32
+ if (entities !== undefined && entities !== null && typeof entities === 'object') {
33
+ for (const value of Object.values(entities)) if (isEntityConfig(value)) out.push(value)
34
34
  }
35
35
  if (Array.isArray(domain.subdomains)) for (const sub of domain.subdomains) collectEntities(sub, out)
36
36
  }
@@ -23,9 +23,9 @@ function emit(obj) {
23
23
 
24
24
  function collectEntities(domain, out) {
25
25
  if (domain === null || typeof domain !== 'object') return
26
- const models = domain.models
27
- if (models !== undefined && models !== null && typeof models === 'object') {
28
- for (const value of Object.values(models)) if (isEntityConfig(value)) out.push(value)
26
+ const entities = domain.entities
27
+ if (entities !== undefined && entities !== null && typeof entities === 'object') {
28
+ for (const value of Object.values(entities)) if (isEntityConfig(value)) out.push(value)
29
29
  }
30
30
  if (Array.isArray(domain.subdomains)) for (const sub of domain.subdomains) collectEntities(sub, out)
31
31
  }
@@ -48,7 +48,7 @@ async function main() {
48
48
  if (Array.isArray(config.entities)) for (const e of config.entities) if (isEntityConfig(e)) entities.push(e)
49
49
  if (Array.isArray(config.domains)) for (const d of config.domains) collectEntities(d, entities)
50
50
  if (entities.length === 0) {
51
- emit({ ok: false, error: 'nenhuma entidade encontrada (config.entities ou domain.models)' })
51
+ emit({ ok: false, error: 'nenhuma entidade encontrada (config.entities ou domain.entities)' })
52
52
  return
53
53
  }
54
54
  if (typeof config.database !== 'function') {
package/bin/lib/db.mjs CHANGED
@@ -249,7 +249,7 @@ Flags:
249
249
 
250
250
  O opus.config.ts precisa expor, pros comandos db:
251
251
  database: () => Kysely factory LAZY do banco (gen não a chama)
252
- entities | domains entidades (explícitas ou via domain.models)
252
+ entities | domains entidades (explícitas ou via domain.entities)
253
253
  schema?: string script SQL idempotente (default: ./db/schema.sql)
254
254
  migrations?: string pasta dos rascunhos do scaffold (default: ./migrations)
255
255
 
@@ -25,10 +25,7 @@ function shapeDomain(d) {
25
25
  dicts: d.dicts,
26
26
  repository: d.hasRepository,
27
27
  service: d.hasService,
28
- // `entities` = a fonte estrutural+negócio (campos + docs). `models` = nomes
29
- // (legado, removido no PR de rename). Ambos por ora pra não quebrar a lente.
30
28
  entities: d.entities ?? [],
31
- models: d.modelNames,
32
29
  actions: d.actions.map(shapeAction),
33
30
  reactions: d.reactions,
34
31
  schedules: d.schedules,
@@ -30,7 +30,7 @@
30
30
  * SerializedDomain:
31
31
  * {
32
32
  * name, dicts, actions, reactions, schedules, subdomains,
33
- * hasRepository, hasService, hasModels
33
+ * hasRepository, hasService
34
34
  * }
35
35
  *
36
36
  * SerializedAction (todos os ActionDef preservam suas strings):
@@ -143,17 +143,12 @@ async function main() {
143
143
  // =============================================================================
144
144
 
145
145
  function serializeDomain(domain, ctx) {
146
- // `entities` é o nome preferido; `models` é o legado (alias). Lê os dois.
147
- const entitySrc = domain.entities ?? domain.models
148
- const entityList = serializeEntities(entitySrc)
146
+ const entityList = serializeEntities(domain.entities)
149
147
  return {
150
148
  name: domain.name,
151
149
  description: nullable(domain.description),
152
150
  hasRepository: domain.repository !== undefined,
153
151
  hasService: domain.service !== undefined,
154
- // Compat: nomes ainda expostos; `entities` traz a estrutura+doc completa.
155
- hasModels: entityList.length > 0,
156
- modelNames: entityList.map((e) => e.name),
157
152
  entities: entityList,
158
153
  dicts: serializeDicts(domain.dicts),
159
154
  actions: Array.from(iterateActions(domain.actions), (a) =>
@@ -90,7 +90,7 @@ infra do `gen`, não a do `check`). Contrato do config **pros comandos `db`**:
90
90
 
91
91
  ```ts
92
92
  export default {
93
- domains: [...], // entidades coletadas de domain.models
93
+ domains: [...], // entidades coletadas de domain.entities
94
94
  entities: [Deal, Company], // ou explícitas (opcional)
95
95
  schema: './src/db/schema.sql', // o script idempotente (default: ./db/schema.sql)
96
96
  database: () => new Kysely({ ... }), // factory LAZY do banco
@@ -139,7 +139,7 @@ import { Kysely, PostgresDialect, CamelCasePlugin } from 'kysely'
139
139
  import { Deal } from './src/domains/deals/deal.entity.ts'
140
140
 
141
141
  export default {
142
- entities: [Deal], // ou domains: [...] (entidades via domain.models)
142
+ entities: [Deal], // ou domains: [...] (entidades via domain.entities)
143
143
  naming: 'snake', // colunas snake no banco
144
144
  migrations: './migrations',
145
145
  database: () => new Kysely({
@@ -32,8 +32,8 @@ uma superfície que precisava apenas de mais presença acabava recebendo um nome
32
32
  - Qualquer componente pode subir ou descer um degrau sem adotar o nome de outro papel.
33
33
  - A escala global afeta todo uso legítimo de `shadow-*`, inclusive consumidores externos;
34
34
  isso é intencional e fica declarado no tema.
35
- - Forma e cor podem continuar semânticas (`rounded-card`, `bg-popover`), pois não
36
- representam uma intensidade intercambiável.
35
+ - Cor pode continuar semântica (`bg-popover`); forma usa diretamente a escala
36
+ (`rounded-md`, `rounded-xl`). Nenhuma delas representa intensidade intercambiável.
37
37
 
38
38
  ## Alternativas descartadas
39
39
 
package/docs/protocol.md CHANGED
@@ -2053,14 +2053,14 @@ defineAction({
2053
2053
 
2054
2054
  ### Entidades dentro de um domínio
2055
2055
 
2056
- `DomainConfig.models` recebe as entidades do recorte (substitui o `unknown` placeholder atual). O domínio agrupa; a entidade declara.
2056
+ `DomainConfig.entities` recebe as entidades do recorte. O domínio agrupa; a entidade declara.
2057
2057
 
2058
2058
  ```ts
2059
2059
  import { defineDomain } from '@softize/opus'
2060
2060
 
2061
2061
  export const crm = defineDomain({
2062
2062
  name: 'crm',
2063
- models: { Deal, Company, Contact },
2063
+ entities: { Deal, Company, Contact },
2064
2064
  actions: { /* deal.create, deal.search, ... */ },
2065
2065
  })
2066
2066
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softize/opus",
3
- "version": "9.1.1",
3
+ "version": "11.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",
@@ -20,12 +20,17 @@ existentes, mantendo navegação observável e componentes reutilizáveis sem la
20
20
  4. Manter margem e posicionamento no consumidor; componente reutilizável controla apenas
21
21
  seu interior.
22
22
  5. Evoluir um pattern compartilhado apenas quando a recorrência e o contrato estiverem claros.
23
- 6. Testar estados de loading, vazio, erro, sucesso, permissão e interação relevante.
23
+ 6. Tratar tokens de superfície como pares indivisíveis no mesmo fragmento de classes:
24
+ `bg-card text-card-foreground` e `bg-popover text-popover-foreground`. Não depender da
25
+ igualdade atual com `--foreground`, porque o app pode sobrescrever cada par.
26
+ 7. Usar diretamente a escala `rounded-*`; não criar radius por nome de componente quando
27
+ `rounded-sm` a `rounded-xl` já expressam a forma.
28
+ 8. Testar estados de loading, vazio, erro, sucesso, permissão e interação relevante.
24
29
 
25
30
  ## Verificação
26
31
 
27
- Rodar testes focados, typecheck e build da superfície. Inspecionar visualmente a rota real
28
- e validar navegação por URL quando aplicável.
32
+ Rodar `opus check`, testes focados, typecheck e build da superfície. Inspecionar visualmente
33
+ a rota real e validar navegação por URL quando aplicável.
29
34
 
30
35
  ## Limites
31
36
 
@@ -3,3 +3,5 @@
3
3
  - Dispara: “Monte a tela de edição usando a form action do Opus.”
4
4
  - Não dispara: “Ajuste o CSS de um e-mail estático.”
5
5
  - Execução: implementar uma lista com modal roteável e provar loading, erro, vazio e back.
6
+ - Reprova: criar uma casca `bg-card` que herda o texto global ou introduzir `rounded-widget`
7
+ quando a escala `rounded-*` atende ao papel.
@@ -5,4 +5,7 @@
5
5
  operação, não a uma tela isolada.
6
6
  - URL representa estado que precisa sobreviver a refresh, deep link ou histórico.
7
7
  - Componentes compartilhados não impõem margem externa; páginas e shells compõem layout.
8
+ - Superfície semântica e foreground são um par local (`bg-card text-card-foreground`,
9
+ `bg-popover text-popover-foreground`); herança do foreground global não substitui o par.
10
+ - Forma usa a escala `rounded-*`; nome de componente não cria uma segunda escala de radius.
8
11
  - Catálogo e API efetivos vêm dos exports da versão instalada, não de memória ou exemplo antigo.
@@ -2,7 +2,7 @@
2
2
  * tbdlib — `defineDomain` factory + flatten helper.
3
3
  *
4
4
  * Domain agrupa as peças que pertencem a um mesmo recorte funcional
5
- * (dicts, models, repository, service, actions, reactions, schedules,
5
+ * (dicts, entities, repository, service, actions, reactions, schedules,
6
6
  * subdomains). É puramente declarativo: nenhum efeito colateral acontece
7
7
  * em `defineDomain`. O runtime consome o objeto via `flattenDomain` quando
8
8
  * recebe um `DomainConfig` em `register(...)`.
@@ -55,12 +55,9 @@ export interface DomainConfig {
55
55
  dicts?: Record<string, unknown>
56
56
 
57
57
  /** Entidades do domínio (`defineEntity`). Map de nome → EntityConfig. É a fonte
58
- * estrutural+negócio que vai pro manifest. `models` é o nome legado (alias). */
58
+ * estrutural+negócio que vai pro manifest. */
59
59
  entities?: Record<string, unknown>
60
60
 
61
- /** @deprecated Use `entities`. Mantido por compat — o runtime/gen lê os dois. */
62
- models?: Record<string, unknown>
63
-
64
61
  /** Repository class do domínio. */
65
62
  repository?: unknown
66
63
 
@@ -152,7 +149,7 @@ export function isDomainConfig(value: unknown): value is DomainConfig {
152
149
  'schedules' in v ||
153
150
  'subdomains' in v ||
154
151
  'dicts' in v ||
155
- 'models' in v ||
152
+ 'entities' in v ||
156
153
  'repository' in v ||
157
154
  'service' in v
158
155
  )
@@ -310,7 +310,7 @@ export function entityTable(config: EntityConfig): string {
310
310
 
311
311
  /**
312
312
  * `true` se `value` parece um `EntityConfig`. Usado pra coletar entidades de
313
- * `domain.models` (que é `unknown`) sem confundir com action/reaction/model.
313
+ * `domain.entities` (que é `unknown`) sem confundir com action/reaction/model.
314
314
  * Heurística: tem `name` string + `fields` objeto e **não** tem `kind`
315
315
  * (descarta `ActionDef`).
316
316
  */
@@ -2,11 +2,8 @@
2
2
  * <ShellNav /> — o menu como primitivo PIVOTÁVEL. Um nav (grupos → itens, com heading e
3
3
  * âncora inferior) que serve os DOIS lares do chrome sem flag:
4
4
  *
5
- * - na sidebar do <AppShell> → recolhe pra ícone-só (com tooltip) quando a sidebar
6
- * recolhe, sozinho. Sabe que está NA sidebar (e se ela recolheu) pelo contexto de
7
- * slot do AppShell (`useSidebarSlot`) — o app não passa isso em duas mãos.
8
- * - dentro do conteúdo (o que o <SectionShell> faz) → sempre expandido (fora da sidebar,
9
- * `useSidebarSlot` é null).
5
+ * - dentro de <Sidebar> → recolhe pra ícone-só (com tooltip) junto com a coluna;
6
+ * - fora de <Sidebar> permanece expandido.
10
7
  *
11
8
  * "Tanto faz onde": o componente se adapta ao lugar, sem "modo" configurado.
12
9
  *
@@ -17,7 +14,7 @@
17
14
 
18
15
  import type { ReactNode } from 'react'
19
16
  import { cn } from '../../lib/cn.ts'
20
- import { useSidebarSlot } from './app-shell.tsx'
17
+ import { useSidebarCollapsed } from './sidebar.tsx'
21
18
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../primitives/tooltip.tsx'
22
19
 
23
20
  export interface ShellNavItem {
@@ -49,7 +46,7 @@ export interface ShellNavProps {
49
46
  footer?: ShellNavGroup[]
50
47
  /** Rótulo da landmark `<nav>`. */
51
48
  navLabel?: string
52
- /** Classes da raiz (a coluna). A LARGURA vem do lar (sidebar do AppShell / SectionShell). */
49
+ /** Classes da raiz (a coluna). A largura vem do pane/sidebar que a contém. */
53
50
  className?: string
54
51
  }
55
52
 
@@ -109,8 +106,7 @@ function renderGroup(group: ShellNavGroup, gi: number, activeId: string | undefi
109
106
  }
110
107
 
111
108
  export function ShellNav({ groups, activeId, onSelect, heading, footer, navLabel = 'Navegação', className }: ShellNavProps): React.ReactElement {
112
- // Recolhe na sidebar recolhida (o contexto de slot é null no conteúdo → sempre expandido).
113
- const railed = useSidebarSlot()?.collapsed ?? false
109
+ const railed = useSidebarCollapsed()
114
110
 
115
111
  const body = (
116
112
  <div data-slot="shell-nav" className={cn('flex h-full min-h-0 flex-col', className)}>
@@ -5,6 +5,11 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../pri
5
5
  interface SidebarContextValue { collapsed: boolean }
6
6
  const SidebarContext = createContext<SidebarContextValue | null>(null)
7
7
 
8
+ /** Estado da Sidebar mais próxima. Uso interno por navegações que se adaptam ao colapso. */
9
+ export function useSidebarCollapsed(): boolean {
10
+ return useContext(SidebarContext)?.collapsed ?? false
11
+ }
12
+
8
13
  export interface SidebarProps {
9
14
  collapsed?: boolean
10
15
  /** Desliga a borda quando o Split já desenha a divisória. */
@@ -39,27 +44,21 @@ export function PaneFooter({ className, children }: { className?: string; childr
39
44
  return <div data-slot="pane-footer" className={cn('shrink-0 border-t border-border p-2', className)}>{children}</div>
40
45
  }
41
46
 
42
- /** @deprecated Use PaneHeader. */
43
- export const SidebarHeader = PaneHeader
44
- /** @deprecated Use PaneContent. */
45
- export const SidebarContent = PaneContent
46
- /** @deprecated Use PaneFooter. */
47
- export const SidebarFooter = PaneFooter
48
-
49
47
  export interface SidebarItemProps {
50
48
  label: string
51
49
  icon?: ReactNode
52
50
  badge?: ReactNode
53
51
  active?: boolean
54
52
  disabled?: boolean
53
+ className?: string
55
54
  onClick: () => void
56
55
  }
57
56
 
58
57
  /** Item de navegação que se adapta ao colapso da Sidebar em que está. */
59
- export function SidebarItem({ label, icon, badge, active = false, disabled = false, onClick }: SidebarItemProps): React.ReactElement {
60
- const collapsed = useContext(SidebarContext)?.collapsed ?? false
58
+ export function SidebarItem({ label, icon, badge, active = false, disabled = false, className, onClick }: SidebarItemProps): React.ReactElement {
59
+ const collapsed = useSidebarCollapsed()
61
60
  const button = (
62
- <button type="button" disabled={disabled} aria-current={active ? 'page' : undefined} onClick={onClick} className={cn('flex items-center gap-2.5 rounded-md transition-colors', 'disabled:pointer-events-none disabled:opacity-40', collapsed ? 'mx-auto size-9 justify-center p-0' : 'w-full px-2.5 py-1.5 text-left text-sm', active ? 'bg-muted font-medium text-foreground' : 'text-foreground/80 hover:bg-muted/60')}>
61
+ <button type="button" disabled={disabled} aria-current={active ? 'page' : undefined} onClick={onClick} className={cn('flex items-center gap-2.5 rounded-md transition-colors', 'disabled:pointer-events-none disabled:opacity-40', collapsed ? 'mx-auto size-9 justify-center p-0' : 'w-full px-2.5 py-1.5 text-left text-sm', active ? 'bg-muted font-medium text-foreground' : 'text-foreground/80 hover:bg-muted/60', className)}>
63
62
  {icon !== undefined && <span className="flex shrink-0">{icon}</span>}
64
63
  {!collapsed && <span className="min-w-0 flex-1 truncate">{label}</span>}
65
64
  {!collapsed && badge !== undefined && <span className="shrink-0">{badge}</span>}
@@ -71,17 +70,43 @@ export function SidebarItem({ label, icon, badge, active = false, disabled = fal
71
70
 
72
71
  export interface SidebarNavGroup {
73
72
  label?: string
74
- items: Array<Omit<SidebarItemProps, 'active' | 'onClick'> & { id: string }>
73
+ items?: SidebarNavItem[]
74
+ subgroups?: SidebarNavSubgroup[]
75
+ }
76
+
77
+ export type SidebarNavItem = Omit<SidebarItemProps, 'active' | 'onClick'> & { id: string }
78
+
79
+ export interface SidebarNavSubgroup {
80
+ label?: string
81
+ items: SidebarNavItem[]
75
82
  }
76
83
 
84
+ const titled = (label?: string): boolean => (label ?? '').trim() !== ''
85
+ const subFilled = (subgroup: SidebarNavSubgroup): boolean => subgroup.items.length > 0
86
+ const groupFilled = (group: SidebarNavGroup): boolean =>
87
+ (group.items?.length ?? 0) > 0 || (group.subgroups?.some(subFilled) ?? false)
88
+
77
89
  /** Navegação controlada para Sidebar. Header/footer continuam slots explícitos do pai. */
78
90
  export function SidebarNav({ groups, activeId, onSelect, navLabel = 'Navegação', className }: { groups: SidebarNavGroup[]; activeId?: string; onSelect: (id: string) => void; navLabel?: string; className?: string }): React.ReactElement {
79
- const collapsed = useContext(SidebarContext)?.collapsed ?? false
91
+ const collapsed = useSidebarCollapsed()
92
+ const renderItem = (item: SidebarNavItem, nested = false): React.ReactElement => (
93
+ <SidebarItem
94
+ key={item.id}
95
+ {...item}
96
+ className={cn(nested && !collapsed && 'pl-6', item.className)}
97
+ active={item.id === activeId}
98
+ onClick={() => onSelect(item.id)}
99
+ />
100
+ )
80
101
  const body = (
81
102
  <nav aria-label={navLabel} data-slot="sidebar-nav" className={cn('space-y-4 p-2', className)}>
82
- {groups.map((group, gi) => <div key={gi} className="space-y-0.5">
83
- {!collapsed && group.label?.trim() && <div className="px-2.5 pb-1 text-xs font-medium text-muted-foreground/70">{group.label}</div>}
84
- {group.items.map(({ id, ...item }) => <SidebarItem key={id} {...item} active={id === activeId} onClick={() => onSelect(id)} />)}
103
+ {groups.map((group, gi) => !groupFilled(group) ? null : <div key={gi} className="space-y-0.5">
104
+ {!collapsed && titled(group.label) && <div className="px-2.5 pb-1 text-xs font-medium text-muted-foreground/70">{group.label}</div>}
105
+ {group.items?.map((item) => renderItem(item))}
106
+ {group.subgroups?.map((subgroup, si) => !subFilled(subgroup) ? null : <div key={si} className="space-y-0.5">
107
+ {!collapsed && titled(subgroup.label) && <div className="pb-0.5 pl-4 pr-2.5 pt-1.5 text-[11px] font-medium text-muted-foreground/50">{subgroup.label}</div>}
108
+ {subgroup.items.map((item) => renderItem(item, titled(subgroup.label)))}
109
+ </div>)}
85
110
  </div>)}
86
111
  </nav>
87
112
  )
@@ -52,7 +52,7 @@ function AlertDialogContent({
52
52
  <AlertDialogPrimitive.Content
53
53
  data-slot="alert-dialog-content"
54
54
  className={cn(
55
- "group/alert-dialog-content fixed top-[50%] left-[50%] z-50 grid w-full max-w-xs translate-x-[-50%] translate-y-[-50%] gap-4 rounded-dialog border border-border bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
55
+ "group/alert-dialog-content fixed top-[50%] left-[50%] z-50 grid w-full max-w-xs translate-x-[-50%] translate-y-[-50%] gap-4 rounded-xl border border-border bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
56
56
  className
57
57
  )}
58
58
  {...props}
@@ -76,7 +76,7 @@ function Calendar({
76
76
  defaultClassNames.dropdown_root
77
77
  ),
78
78
  dropdown: cn(
79
- "absolute inset-0 bg-popover opacity-0",
79
+ "absolute inset-0 bg-popover text-popover-foreground opacity-0",
80
80
  defaultClassNames.dropdown
81
81
  ),
82
82
  caption_label: cn(
@@ -4,8 +4,8 @@ import { cn } from '../../lib/cn.ts'
4
4
 
5
5
  /**
6
6
  * Card — a superfície da casa: SÓ a casca (borda + raio + bg). Divergência DECLARADA vs
7
- * shadcn: `rounded-lg` (não `rounded-xl`), FLAT (sem `shadow-sm`) e SEM `flex/gap/py`
8
- * forçados. O upstream embute `py-6 gap-6` + slots `px-6`, o que briga com card-box
7
+ * shadcn: FLAT (sem `shadow-sm`) e SEM `flex/gap/py` forçados. O upstream embute
8
+ * `py-6 gap-6` + slots `px-6`, o que briga com card-box
9
9
  * simples (o `gap-6` do Card + o `space-y` do consumidor = espaçamento dobrado) — por isso
10
10
  * a casa hand-rollava. Aqui o padding mora no SLOT (Header/Content/Footer) ou no `className`
11
11
  * do consumidor (box simples) — mesmo modelo do Dialog.
@@ -24,7 +24,7 @@ export function Card({
24
24
  }): React.ReactElement {
25
25
  const Comp = asChild ? Slot : 'div'
26
26
  return (
27
- <Comp data-slot="card" className={cn('rounded-card border border-border bg-card text-card-foreground shadow-sm', className)} {...props} />
27
+ <Comp data-slot="card" className={cn('rounded-xl border border-border bg-card text-card-foreground', className)} {...props} />
28
28
  )
29
29
  }
30
30
 
@@ -163,7 +163,7 @@ const ChatTranscript = React.memo(function ChatTranscript({
163
163
  href={item.artifact.ref}
164
164
  target="_blank"
165
165
  rel="noreferrer"
166
- className="rounded-xl border bg-card px-3.5 py-2 text-sm underline-offset-2 hover:underline"
166
+ className="rounded-xl border bg-card px-3.5 py-2 text-sm text-card-foreground underline-offset-2 hover:underline"
167
167
  >
168
168
  {item.artifact.title ?? item.artifact.ref}
169
169
  </a>
@@ -34,7 +34,7 @@ export interface ComposerProps {
34
34
 
35
35
  /**
36
36
  * <Composer /> — a caixa de escrever da casa: textarea (Enter envia / Shift+Enter quebra
37
- * linha) numa pílula elevada (rounded-card + border + shadow-sm), com o enviar dentro.
37
+ * linha) numa pílula elevada (rounded-xl + border + shadow-sm), com o enviar dentro.
38
38
  * É o composer do `<Chat>` extraído pra ser reusável SOZINHO — o Maestro cria sessão com
39
39
  * ele (sem chat nenhum), e a GB prende seletores nele. Com `actions`, ganha uma barra
40
40
  * embaixo pros seletores; sem, é a linha única de sempre.
@@ -111,7 +111,7 @@ export function Composer({
111
111
  fire()
112
112
  }}
113
113
  className={cn(
114
- 'rounded-card border border-border bg-card shadow-sm transition focus-within:ring-2 focus-within:ring-primary/15',
114
+ 'rounded-xl border border-border bg-card text-card-foreground shadow-sm transition',
115
115
  twoRow ? 'p-2' : 'relative flex items-end gap-2 p-2 pl-3.5',
116
116
  className,
117
117
  )}
@@ -54,7 +54,7 @@ export function DialogContent({
54
54
  // Superfície PURA: só a casca (borda/bg/raio/sombra) + coluna flex com teto de
55
55
  // altura. O padding mora nos SLOTS (Header/Body/Footer), não aqui — assim ninguém
56
56
  // precisa negar margem pra escapar de um p-6 embutido.
57
- "fixed left-[50%] top-[50%] z-50 flex max-h-[85vh] w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] flex-col overflow-hidden rounded-dialog border border-border bg-card shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
57
+ "fixed left-[50%] top-[50%] z-50 flex max-h-[85vh] w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] flex-col overflow-hidden rounded-xl border border-border bg-card text-card-foreground shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
58
58
  className,
59
59
  )}
60
60
  {...props}