@softize/opus 10.0.0 → 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 CHANGED
@@ -11,6 +11,33 @@ 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
+
14
41
  ## 10.0.0 — 2026-08-20
15
42
 
16
43
  **Breaking — forma usa a escala do Tailwind e superfícies sempre carregam seu foreground.**
@@ -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({
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": "10.0.0",
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",
@@ -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
  )
@@ -14,7 +14,8 @@
14
14
  */
15
15
  import { useEffect } from 'react'
16
16
  import { DOC_SECTIONS, type DocSection, type DocEntry } from './registry'
17
- import { SectionShell, type SectionNavGroup } from '../components/patterns/section-shell.tsx'
17
+ import { Pane, Split } from '../components/patterns/split.tsx'
18
+ import { PaneContent, Sidebar, SidebarNav, type SidebarNavGroup } from '../components/patterns/sidebar.tsx'
18
19
  import { navigate, usePathname } from '../router.ts'
19
20
 
20
21
  function findPage(sections: DocSection[], slug: string): DocEntry | undefined {
@@ -70,21 +71,35 @@ export function DocBrowser({
70
71
  // Seção → grupo, e os grupos da seção → subgrupos. O tier do meio NÃO pode ser
71
72
  // achatado: `docSectionsFromFolder` o preenche a partir de sub-pasta (ou do
72
73
  // frontmatter `group:`), que é o caminho do `opusDocs({ source })`.
73
- const navGroups: SectionNavGroup[] = sections.map((section) => ({
74
+ const navGroups: SidebarNavGroup[] = sections.map((section) => ({
74
75
  label: section.label,
75
76
  subgroups: section.groups.map((g) => ({
76
77
  label: g.label,
77
- items: g.pages.map((p) => ({ id: p.slug, label: p.title, badge: p.badge })),
78
+ items: g.pages.map((p) => ({
79
+ id: p.slug,
80
+ label: p.title,
81
+ badge: p.badge === undefined ? undefined : <span className="rounded border border-border/60 px-1 font-mono text-[10px] leading-tight text-muted-foreground/60">{p.badge}</span>,
82
+ })),
78
83
  })),
79
84
  }))
80
85
 
81
86
  return (
82
- <SectionShell
83
- groups={navGroups}
84
- activeId={page?.slug}
85
- onSelect={(slug) => go(`${basePath}/${slug}`)}
86
- >
87
- <div className="mx-auto max-w-3xl px-8 py-8">{page?.render()}</div>
88
- </SectionShell>
87
+ <Split className="h-full">
88
+ <Pane inset="none" className="w-56">
89
+ <Sidebar className="w-full">
90
+ <PaneContent>
91
+ <SidebarNav
92
+ groups={navGroups}
93
+ activeId={page?.slug}
94
+ navLabel="Navegação da documentação"
95
+ onSelect={(slug) => go(`${basePath}/${slug}`)}
96
+ />
97
+ </PaneContent>
98
+ </Sidebar>
99
+ </Pane>
100
+ <Pane key={page?.slug} grow inset="none" className="overflow-y-auto">
101
+ <div className="mx-auto max-w-3xl px-8 py-8">{page?.render()}</div>
102
+ </Pane>
103
+ </Split>
89
104
  )
90
105
  }
@@ -75,11 +75,11 @@ render(<Demo />)
75
75
  Monte **um** `<DialogHost />` no shell do app, ao lado do `<Toaster />`:
76
76
 
77
77
  ```tsx
78
- <AppShell sidebar={…}>
78
+ <>
79
79
  {rotas}
80
80
  <Toaster />
81
81
  <DialogHost />
82
- </AppShell>
82
+ </>
83
83
  ```
84
84
 
85
85
  Sem ele, os três **lançam** — em vez de devolver uma promise que nunca resolve. Promise pendurada viraria clique sem efeito, o pior desfecho pra uma interrupção que exige resposta: a pessoa acha que respondeu, ou clica de novo. (`<ConfirmHost />` segue valendo como alias de `<DialogHost />`.)
@@ -27,7 +27,7 @@ render(
27
27
 
28
28
  ## Colapso
29
29
 
30
- `collapsed` pertence à própria `Sidebar`; `SidebarItem` e `SidebarNav` adaptam-se automaticamente para botões `size-9` centralizados, ícones e tooltips. `PaneHeader`, `PaneContent` e `PaneFooter` são slots do pane: a aplicação mantém a identidade e ações que lhe pertencem sem atribuí-las artificialmente à sidebar. `SidebarHeader`, `SidebarContent` e `SidebarFooter` seguem como aliases deprecated durante a migração.
30
+ `collapsed` pertence à própria `Sidebar`; `SidebarItem`, `SidebarNav` e `ShellNav` adaptam-se automaticamente para botões `size-9` centralizados, ícones e tooltips. `PaneHeader`, `PaneContent` e `PaneFooter` são os slots do pane: a aplicação mantém a identidade e ações que lhe pertencem sem atribuí-las artificialmente à sidebar.
31
31
 
32
32
  ```tsx
33
33
  <Sidebar collapsed={collapsed}>
@@ -36,3 +36,23 @@ render(
36
36
  <PaneFooter><UserMenu /></PaneFooter>
37
37
  </Sidebar>
38
38
  ```
39
+
40
+ ## Navegação contextual
41
+
42
+ `SidebarNav` aceita grupos e subgrupos. Isso cobre tanto a navegação global quanto seções internas, como Configurações ou documentação, sem outro shell especializado.
43
+
44
+ ```tsx
45
+ <SidebarNav
46
+ groups={[
47
+ {
48
+ label: 'Configurações',
49
+ subgroups: [
50
+ { label: 'Acesso', items: [{ id: 'users', label: 'Usuários' }] },
51
+ { label: 'Dados', items: [{ id: 'imports', label: 'Importações' }] },
52
+ ],
53
+ },
54
+ ]}
55
+ activeId={active}
56
+ onSelect={go}
57
+ />
58
+ ```
@@ -80,7 +80,6 @@ import paginationMd from './content/pagination.md?raw'
80
80
  import popoverMd from './content/popover.md?raw'
81
81
  import progressMd from './content/progress.md?raw'
82
82
  import radioGroupMd from './content/radio-group.md?raw'
83
- import resizableMd from './content/resizable.md?raw'
84
83
  import scrollAreaMd from './content/scroll-area.md?raw'
85
84
  import selectMd from './content/select.md?raw'
86
85
  import separatorMd from './content/separator.md?raw'
@@ -114,8 +113,6 @@ import actionViewMd from './content/action-view.md?raw'
114
113
  import actionTriggerMd from './content/action-trigger.md?raw'
115
114
  import actionSearchDialogMd from './content/action-list-dialog.md?raw'
116
115
  import pageMd from './content/page.md?raw'
117
- import appShellMd from './content/app-shell.md?raw'
118
- import sectionShellMd from './content/section-shell.md?raw'
119
116
  import sidebarMd from './content/sidebar.md?raw'
120
117
  import splitMd from './content/split.md?raw'
121
118
  import routerMd from './content/router.md?raw'
@@ -295,11 +292,8 @@ export const UI_SECTIONS: DocSection[] = [
295
292
  { slug: 'aspect-ratio', title: 'Aspect Ratio', render: comp('Aspect Ratio', 'aspect-ratio', aspectRatioMd) },
296
293
  { slug: 'card', title: 'Card', render: comp('Card', 'card', cardMd) },
297
294
  { slug: 'collapsible', title: 'Collapsible', render: comp('Collapsible', 'collapsible', collapsibleMd) },
298
- { slug: 'resizable', title: 'Resizable', badge: componentMeta.resizable.deprecated ? 'deprecated' : undefined, render: comp('Resizable', 'resizable', resizableMd) },
299
295
  { slug: 'scroll-area', title: 'Scroll Area', render: comp('Scroll Area', 'scroll-area', scrollAreaMd) },
300
296
  { slug: 'separator', title: 'Separator', render: comp('Separator', 'separator', separatorMd) },
301
- { slug: 'app-shell', title: 'App Shell', badge: componentMeta['app-shell'].deprecated ? 'deprecated' : undefined, render: pattern('App Shell', 'app-shell', appShellMd) },
302
- { slug: 'section-shell', title: 'Section Shell', badge: componentMeta['section-shell'].deprecated ? 'deprecated' : undefined, render: pattern('Section Shell', 'section-shell', sectionShellMd) },
303
297
  ],
304
298
  },
305
299
  {
package/src/ui/meta.ts CHANGED
@@ -283,24 +283,17 @@ export const componentMeta = {
283
283
  whenToUse:
284
284
  'Escolha única entre opções mutuamente exclusivas, todas visíveis ao mesmo tempo (Radix). Cada RadioGroupItem tem um `value`; o item escolhido vira o `value` do RadioGroup, controlado por `value`/`onValueChange` (ou `defaultValue` no modo não controlado). Pareie cada item com um Label. Pra poucas opções que cabem na tela; com muitas, prefira Select; pra ligar/desligar um único item, Checkbox ou Switch.',
285
285
  },
286
- 'resizable': {
287
- name: 'resizable',
288
- ancestry: 'shadcn',
289
- whenToUse:
290
- 'Painéis redimensionáveis por arraste (react-resizable-panels). Compõe ResizablePanelGroup > ResizablePanel + ResizableHandle entre eles; `orientation` (horizontal/vertical) define a direção do arraste, `defaultSize`/`minSize`/`maxSize` (em %) limitam cada painel e `withHandle` desenha a pega na divisória. O grupo ocupa a altura do pai (h-full), então dê tamanho ao container. Pra alternar entre painéis sem dividir a vista, use Tabs.',
291
- deprecated: { alternative: '`Split resizable` + `Pane`', since: 'próxima versão' },
292
- },
293
286
  'split': {
294
287
  name: 'split',
295
288
  ancestry: 'opus',
296
289
  whenToUse:
297
- 'Divide uma área em panes em sequência horizontal ou vertical. Use `resizable` quando a pessoa deve ajustar a fronteira; o mesmo `<Split>` vira flex simples sem ele. Cada `<Pane>` declara tamanho inicial/mínimo e inset. É o mecanismo espacial para sidebar, conteúdo e rail — não use AppShell/SectionShell novos.',
290
+ 'Divide uma área em panes em sequência horizontal ou vertical. Use `resizable` quando a pessoa deve ajustar a fronteira; o mesmo `<Split>` vira flex simples sem ele. Cada `<Pane>` declara tamanho inicial/mínimo e inset. É o mecanismo espacial para sidebar, conteúdo e rail.',
298
291
  },
299
292
  'sidebar': {
300
293
  name: 'sidebar',
301
294
  ancestry: 'opus',
302
295
  whenToUse:
303
- 'Chrome e navegação de uma coluna lateral, encaixada onde um Split decidir. `Sidebar` possui o colapso; `PaneHeader`, `PaneContent` e `PaneFooter` estruturam qualquer pane, e `SidebarNav`/`SidebarItem` apresentam a navegação. Serve tanto a barra global quanto uma nav contextual; em Split redimensionável passe `divider={false}` para não duplicar a divisória. `SidebarHeader`, `SidebarContent` e `SidebarFooter` são aliases deprecated.',
296
+ 'Chrome e navegação de uma coluna lateral, encaixada onde um Split decidir. `Sidebar` possui o colapso; `PaneHeader`, `PaneContent` e `PaneFooter` estruturam qualquer pane, e `SidebarNav`/`SidebarItem` apresentam navegação com grupos e subgrupos. Serve tanto a barra global quanto uma nav contextual; em Split redimensionável passe `divider={false}` para não duplicar a divisória.',
304
297
  },
305
298
  'scroll-area': {
306
299
  name: 'scroll-area',
@@ -381,20 +374,6 @@ export const componentMeta = {
381
374
  whenToUse:
382
375
  'O esqueleto de página do back-office: <main> + container de largura cheia (className="max-w-5xl" estreita e centra) + header com título, descrição e ação à direita (em geral o criar). Presentacional (a página agrega N fontes); o action-driven mora dentro. Pra listagem em modal, ActionListDialog.',
383
376
  },
384
- 'app-shell': {
385
- name: 'app-shell',
386
- ancestry: 'opus',
387
- whenToUse:
388
- 'O chrome da aplicação: sidebar de navegação (header + nav rolável + rodapé ancorado) + conteúdo, com rail opcional à direita (chat de agente, inspetor) em painéis redimensionáveis. É o quadro — o que vai em cada slot é do app. Pro esqueleto de UMA página (título, descrição, ação), Page. Chrome `flush` opcional (full-bleed, filete à esquerda) com <AppShellBar> — a faixa h-12 border-b usada na sidebar (marca) e no topo do conteúdo pra linha do header atravessar a tela. Sidebar recolhível com `collapsible` (opt-in): o shell controla a largura e publica `data-collapsed` no aside; o rótulo some por CSS (`group-data-[collapsed=true]/sidebar:hidden`) porque o nav é do app. O botão é o <AppShellTrigger />, posicionado pelo consumidor; `useAppShell()` dá o estado em JS.',
389
- deprecated: { alternative: '`Split` + `Pane` + `Sidebar`', since: 'próxima versão' },
390
- },
391
- 'section-shell': {
392
- name: 'section-shell',
393
- ancestry: 'opus',
394
- whenToUse:
395
- 'Uma SEÇÃO com navegação própria — o nível entre AppShell (o app) e Page (uma tela): nav w-56 com filete + painel, pras telas irmãs de Configurações, Relatórios ou doc. Use quando a seção é visitada raro e navegada por dentro quando visitada (não vale queimar item na sidebar do app), ou quando a lista é dinâmica demais pra um nav estático. Pra facetas do MESMO objeto, Tabs. Controlado (activeId + onSelect): o roteamento é do app. Grupos aceitam `items` e/ou `subgroups` (o 3º nível, ex.: sub-pasta na doc). O painel remonta quando o `activeId` muda — é o que zera o scroll; `scrollResetKey` sobrescreve a chave nos dois sentidos: constante = nunca remonta (preserva o scroll), mais fina que o activeId = remonta também dentro da mesma tela.',
396
- deprecated: { alternative: '`Split` + `Pane` + `Sidebar`', since: 'próxima versão' },
397
- },
398
377
  'router': {
399
378
  name: 'router',
400
379
  ancestry: 'opus',
package/src/ui/react.tsx CHANGED
@@ -164,7 +164,6 @@ export { Kbd, KbdGroup } from './components/primitives/kbd.tsx'
164
164
  export { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from './components/primitives/pagination.tsx'
165
165
  export { Progress } from './components/primitives/progress.tsx'
166
166
  export { RadioGroup, RadioGroupItem } from './components/primitives/radio-group.tsx'
167
- export { ResizableHandle, ResizablePanel, ResizablePanelGroup } from './components/primitives/resizable.tsx'
168
167
  export { ScrollArea, ScrollBar } from './components/primitives/scroll-area.tsx'
169
168
  export { Slider } from './components/primitives/slider.tsx'
170
169
  export { Switch } from './components/primitives/switch.tsx'
@@ -206,29 +205,15 @@ export type { DataStateProps } from './components/patterns/data-state.tsx'
206
205
  export { Page } from './components/patterns/page.tsx'
207
206
  export type { PageProps } from './components/patterns/page.tsx'
208
207
 
209
- // Chrome da aplicação (sidebar + conteúdo + rail opcional redimensionável).
210
- export { AppShell, AppShellBar, AppShellTrigger, useAppShell, useSidebarSlot } from './components/patterns/app-shell.tsx'
211
- export type { AppShellProps } from './components/patterns/app-shell.tsx'
212
-
213
208
  // Layout composicional: Split decide a relação espacial; Pane carrega conteúdo com inset.
214
209
  export { Split, Pane } from './components/patterns/split.tsx'
215
210
  export type { SplitProps, PaneProps } from './components/patterns/split.tsx'
216
211
 
217
212
  // Barra lateral composicional: header/conteúdo/footer e navegação, sem possuir o layout.
218
- export { PaneHeader, PaneContent, PaneFooter, Sidebar, SidebarHeader, SidebarContent, SidebarFooter, SidebarItem, SidebarNav } from './components/patterns/sidebar.tsx'
219
- export type { SidebarProps, SidebarItemProps, SidebarNavGroup } from './components/patterns/sidebar.tsx'
220
-
221
- // Seção com navegação própria (nav w-56 + painel) o nível entre o AppShell e a Page.
222
- export { SectionShell } from './components/patterns/section-shell.tsx'
223
- export type {
224
- SectionShellProps,
225
- SectionNavGroup,
226
- SectionNavSubgroup,
227
- SectionNavItem,
228
- } from './components/patterns/section-shell.tsx'
229
-
230
- // O menu como primitivo PIVOTÁVEL: mesmo nav na sidebar do AppShell ou dentro do conteúdo
231
- // (colapso por CSS via o grupo `sidebar`). Ver docs/shellnav.md.
213
+ export { PaneHeader, PaneContent, PaneFooter, Sidebar, SidebarItem, SidebarNav } from './components/patterns/sidebar.tsx'
214
+ export type { SidebarProps, SidebarItemProps, SidebarNavGroup, SidebarNavSubgroup, SidebarNavItem } from './components/patterns/sidebar.tsx'
215
+
216
+ // O menu como primitivo pivotável: recolhe quando composto dentro de Sidebar.
232
217
  export { ShellNav, ShellNavHeading } from './components/patterns/shell-nav.tsx'
233
218
  export type { ShellNavProps, ShellNavGroup, ShellNavItem, ShellNavHeadingProps } from './components/patterns/shell-nav.tsx'
234
219