@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.
- package/CHANGELOG.md +52 -3
- package/bin/lib/check.mjs +37 -1
- package/bin/lib/db-check-runner.mjs +5 -5
- package/bin/lib/db-migrate-runner.mjs +3 -3
- package/bin/lib/db-scaffold-runner.mjs +4 -4
- package/bin/lib/db.mjs +1 -1
- package/bin/lib/gen-manifest.mjs +0 -3
- package/bin/lib/gen-runner.mjs +2 -7
- package/docs/data-layer.md +2 -2
- package/docs/elevation-scale.md +2 -2
- package/docs/protocol.md +2 -2
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +8 -3
- package/registry/skills/build-opus-ui/references/evaluations.md +2 -0
- package/registry/skills/build-opus-ui/references/ui-patterns.md +3 -0
- package/src/core/domain.ts +3 -6
- package/src/schema/entity.ts +1 -1
- package/src/ui/components/patterns/shell-nav.tsx +5 -9
- package/src/ui/components/patterns/sidebar.tsx +40 -15
- package/src/ui/components/primitives/alert-dialog.tsx +1 -1
- package/src/ui/components/primitives/calendar.tsx +1 -1
- package/src/ui/components/primitives/card.tsx +3 -3
- package/src/ui/components/primitives/chat.tsx +1 -1
- package/src/ui/components/primitives/composer.tsx +2 -2
- package/src/ui/components/primitives/dialog.tsx +1 -1
- package/src/ui/components/primitives/menu.tsx +2 -2
- package/src/ui/components/primitives/popover.tsx +1 -1
- package/src/ui/docs/DocBrowser.tsx +25 -10
- package/src/ui/docs/content/card.md +1 -1
- package/src/ui/docs/content/composer.md +1 -1
- package/src/ui/docs/content/confirm.md +2 -2
- package/src/ui/docs/content/sidebar.md +21 -1
- package/src/ui/docs/content/tokens.md +4 -7
- package/src/ui/docs/registry.tsx +0 -6
- package/src/ui/meta.ts +3 -24
- package/src/ui/react.tsx +4 -19
- package/src/ui/theme.css +0 -7
- package/docs/shellnav.md +0 -131
- package/src/ui/components/patterns/app-shell.tsx +0 -227
- package/src/ui/components/patterns/section-shell.tsx +0 -246
- package/src/ui/docs/content/app-shell.md +0 -155
- package/src/ui/docs/content/resizable.md +0 -86
- 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
|
|
136
|
-
defaults; os aliases por papel e as variáveis intermediárias
|
|
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
|
|
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.
|
|
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
|
|
33
|
-
if (
|
|
34
|
-
for (const value of Object.values(
|
|
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.
|
|
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
|
|
32
|
-
if (
|
|
33
|
-
for (const value of Object.values(
|
|
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
|
|
27
|
-
if (
|
|
28
|
-
for (const value of Object.values(
|
|
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.
|
|
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.
|
|
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
|
|
package/bin/lib/gen-manifest.mjs
CHANGED
|
@@ -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,
|
package/bin/lib/gen-runner.mjs
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* SerializedDomain:
|
|
31
31
|
* {
|
|
32
32
|
* name, dicts, actions, reactions, schedules, subdomains,
|
|
33
|
-
* hasRepository, hasService
|
|
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
|
-
|
|
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) =>
|
package/docs/data-layer.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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({
|
package/docs/elevation-scale.md
CHANGED
|
@@ -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
|
-
-
|
|
36
|
-
|
|
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.
|
|
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
|
-
|
|
2063
|
+
entities: { Deal, Company, Contact },
|
|
2064
2064
|
actions: { /* deal.create, deal.search, ... */ },
|
|
2065
2065
|
})
|
|
2066
2066
|
```
|
package/package.json
CHANGED
|
@@ -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.
|
|
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
|
|
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.
|
package/src/core/domain.ts
CHANGED
|
@@ -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,
|
|
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.
|
|
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
|
-
'
|
|
152
|
+
'entities' in v ||
|
|
156
153
|
'repository' in v ||
|
|
157
154
|
'service' in v
|
|
158
155
|
)
|
package/src/schema/entity.ts
CHANGED
|
@@ -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.
|
|
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
|
-
* -
|
|
6
|
-
*
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
84
|
-
{group.items
|
|
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-
|
|
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}
|
|
@@ -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:
|
|
8
|
-
*
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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}
|