@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
@@ -45,7 +45,7 @@ function MenuContent({
45
45
  data-slot="menu-content"
46
46
  sideOffset={sideOffset}
47
47
  className={cn(
48
- "z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[10rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-popover border border-border bg-popover p-1 text-popover-foreground shadow-md will-change-[transform,opacity] 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",
48
+ "z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[10rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md will-change-[transform,opacity] 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",
49
49
  className
50
50
  )}
51
51
  {...props}
@@ -233,7 +233,7 @@ function MenuSubContent({
233
233
  <MenuPrimitive.SubContent
234
234
  data-slot="menu-sub-content"
235
235
  className={cn(
236
- "z-50 min-w-[10rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-popover border border-border bg-popover p-1 text-popover-foreground shadow-md will-change-[transform,opacity] 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",
236
+ "z-50 min-w-[10rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md will-change-[transform,opacity] 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",
237
237
  className
238
238
  )}
239
239
  {...props}
@@ -34,7 +34,7 @@ function PopoverContent({
34
34
  align={align}
35
35
  sideOffset={sideOffset}
36
36
  className={cn(
37
- "z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-popover border border-border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 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",
37
+ "z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border border-border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 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",
38
38
  className
39
39
  )}
40
40
  {...props}
@@ -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
  }
@@ -1,6 +1,6 @@
1
1
  ## Box simples
2
2
 
3
- O uso mais comum: a superfície de conteúdo em repouso (`rounded-card` + `border` + `shadow-sm` ver Tokens & Tema → Escala de elevação) com o padding pelo `className`. O Card não força flex/gap/padding — você compõe o miolo (sem brigar com `space-y`/`flex` seu).
3
+ O uso mais comum: a superfície de conteúdo em repouso (`rounded-xl` + `border`), flat por padrão, com o padding pelo `className`. O Card não força flex/gap/padding — você compõe o miolo (sem brigar com `space-y`/`flex` seu). Quando a hierarquia realmente pedir elevação, declare a intensidade explicitamente, por exemplo `className="shadow-sm"`.
4
4
 
5
5
  ```tsx preview col
6
6
  <Card className="p-4">
@@ -1,6 +1,6 @@
1
1
  ## Básico
2
2
 
3
- A caixa de escrever da casa: textarea numa pílula elevada (`rounded-card` + `border` + `shadow-sm`), **Enter** envia / **Shift+Enter** quebra linha, enviar dentro. É o composer do [Chat](/components/chat) extraído — use SOZINHO quando há entrada de texto mas não um chat (ex.: criar uma sessão). Controlado: o dono do texto é você. Os callbacks opcionais `onHistoryPrevious` e `onHistoryNext` permitem que esse dono consuma **↑/↓**; sem eles, as setas mantêm o comportamento nativo da textarea.
3
+ A caixa de escrever da casa: textarea numa pílula elevada (`rounded-xl` + `border` + `shadow-sm`), **Enter** envia / **Shift+Enter** quebra linha, enviar dentro. É o composer do [Chat](/components/chat) extraído — use SOZINHO quando há entrada de texto mas não um chat (ex.: criar uma sessão). Controlado: o dono do texto é você. Os callbacks opcionais `onHistoryPrevious` e `onHistoryNext` permitem que esse dono consuma **↑/↓**; sem eles, as setas mantêm o comportamento nativo da textarea.
4
4
 
5
5
  ```tsx preview col
6
6
  const [text, setText] = React.useState('')
@@ -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
+ ```
@@ -87,7 +87,7 @@ render(
87
87
  {LEVELS.map(([level, desc]) => (
88
88
  <div key={level} className="flex flex-col items-center gap-3">
89
89
  <div
90
- className={'flex h-24 w-40 items-center justify-center rounded-card border border-border bg-card text-xs shadow-' + level}
90
+ className={'flex h-24 w-40 items-center justify-center rounded-xl border border-border bg-card text-xs text-card-foreground shadow-' + level}
91
91
  >
92
92
  Texto
93
93
  </div>
@@ -110,9 +110,9 @@ render(
110
110
  ## Radius
111
111
 
112
112
  > Escala mapeada no `@theme inline` preservando o sentido pré-v4 dos utilitários `rounded-*`
113
- > (neutraliza o rename do Tailwind v4). E a forma também tem papel: `rounded-card/popover/dialog`
114
- > são a forma das superfícies elevadas (separada da intensidade de `shadow-*`), derivados de `--radius` — a
115
- > identidade por app escala tudo junto. Controles (botões, inputs) seguem na escala sm–xl.
113
+ > (neutraliza o rename do Tailwind v4). Componentes e superfícies usam diretamente essa
114
+ > escala: a forma não cria uma segunda taxonomia por componente. A identidade por app
115
+ > sobrescreve `--radius` e escala tudo junto.
116
116
 
117
117
  ```tsx preview
118
118
  render(
@@ -122,9 +122,6 @@ render(
122
122
  ['rounded-md', 'radius − 2px'],
123
123
  ['rounded-lg', 'radius (0.625rem)'],
124
124
  ['rounded-xl', 'radius + 4px'],
125
- ['rounded-popover', 'papel popover (radius − 2px)'],
126
- ['rounded-dialog', 'papel dialog (radius + 6px)'],
127
- ['rounded-card', 'papel card (radius + 6px)'],
128
125
  ].map(([cls, calc]) => (
129
126
  <div key={cls} className="flex items-center gap-3">
130
127
  <div className={'h-12 w-12 border-2 border-foreground/30 bg-muted ' + cls} />
@@ -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
@@ -39,7 +39,7 @@ export const componentMeta = {
39
39
  name: 'card',
40
40
  ancestry: 'shadcn',
41
41
  whenToUse:
42
- 'Superfície da casa (bg-card + borda + rounded-lg, flat). Box simples: `<Card className="p-4">…</Card>`. Estruturado: Card > CardHeader(CardTitle/CardDescription) + CardContent + CardFooter — o padding mora nos slots (como o Dialog). Divergência declarada vs shadcn: rounded-lg, sem shadow, sem flex/gap forçados (o upstream brigava com card-box simples).',
42
+ 'Superfície da casa (bg-card + text-card-foreground + borda + rounded-xl, flat). Box simples: `<Card className="p-4">…</Card>`. Estruturado: Card > CardHeader(CardTitle/CardDescription) + CardContent + CardFooter — o padding mora nos slots (como o Dialog). Divergência declarada vs shadcn: sem shadow e sem flex/gap forçados (o upstream brigava com card-box simples).',
43
43
  },
44
44
  'chat': {
45
45
  name: 'chat',
@@ -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
 
package/src/ui/theme.css CHANGED
@@ -137,13 +137,6 @@
137
137
  --radius-lg: var(--radius);
138
138
  --radius-xl: calc(var(--radius) + 4px);
139
139
 
140
- /* Forma por papel (rounded-card/popover/dialog), separada da intensidade de elevação.
141
- Derivados de --radius: a identidade por app (sobrescrever --radius) escala os papéis
142
- junto. Controles ficam na escala sm–xl. */
143
- --radius-card: calc(var(--radius) + 6px);
144
- --radius-popover: calc(var(--radius) - 2px);
145
- --radius-dialog: calc(var(--radius) + 6px);
146
-
147
140
  /* Escala Tailwind, com receita direta para o compilador preservar modifiers `/opacity`.
148
141
  `light-dark()` mantém o peso adequado em cada tema sem uma variável intermediária. */
149
142
  --shadow-2xs: 0 1px 2px -1px light-dark(rgb(15 15 15 / 0.03), rgb(0 0 0 / 0.12));
package/docs/shellnav.md DELETED
@@ -1,131 +0,0 @@
1
- # Proposta: `ShellNav` — o menu como primitivo pivotável
2
-
3
- > **Status: EM DISCUSSÃO.** Desenhado com o João (2026-07-22), a partir do trabalho na
4
- > cabine do Maestro (a nav do projeto virou `SectionShell`; a sidebar ganhou Início +
5
- > Configurações). Pendente de aprovação e de uma versão do Opus. Refina o
6
- > [`ownership-vs-shadcn-lock`](ownership-vs-shadcn-lock.md): o item de menu é o próximo
7
- > primitivo a sair do "cada app copia ad-hoc".
8
-
9
- ## O problema
10
-
11
- O `AppShell` entrega a **moldura** da sidebar (o slot `sidebarNav`) e o `SectionShell`
12
- entrega a **nav de seção** (nav + painel, dentro do conteúdo). Mas o **item de menu** — a
13
- linha com ícone + label + estado ativo + badge — não é primitivo de ninguém: cada app
14
- escreve o `<button className="…">` na mão. Resultado, medido (não suposto):
15
-
16
- | Menu | Onde | Como é feito hoje |
17
- |---|---|---|
18
- | Aplicação (sidebar) | `AppShell.sidebarNav` | `<button>` à mão — sem primitivo |
19
- | Configurações | dentro do conteúdo | `SectionShell` (primitivo) ✓ |
20
- | Relatórios | dentro do conteúdo | `<aside>`/`<nav>` à mão — **não usa o SectionShell**, mesmo sendo a mesma forma |
21
-
22
- E isso se repete em **três produtos** (`softize/apps/maestro`, `softize/apps/main/web`,
23
- `empresa-x/apps/main`), cada um com a própria cópia do item — e, dentro de cada um, o
24
- markup ainda se repete por estado (ativo/inativo/recolhido) e nível (pai/filho).
25
-
26
- Dois fatos que calibram a urgência:
27
-
28
- - **Não é drift visível — é duplicação + risco.** Diff token a token: mesma versão do Opus
29
- (2.37), mesmo `--radius` (0.625rem), classes de forma **byte-a-byte iguais**
30
- (`rounded-md px-2.5 py-1.5 gap-2.5`). Só continuam iguais porque um humano copia; nada
31
- cobra. A "sensação de divergência" (item mais retangular numa app que noutra) vem de
32
- comparar **componentes diferentes** (nav de app × nav de seção × switcher), não de drift
33
- do mesmo componente.
34
- - **Já há uma inconsistência viva:** o menu de Relatórios do GB é uma nav-dentro-do-conteúdo
35
- — a MESMA forma do Configurações — e mesmo assim hand-rola em vez de usar o `SectionShell`.
36
-
37
- ## A proposta
38
-
39
- Um primitivo só: **`ShellNav`** — **pivotável e controlado** (presentacional, como o
40
- `SectionShell` já é). O app passa **dados**, não markup, e o mesmo componente serve os dois
41
- lares sem flag de "modo app" vs "modo seção":
42
-
43
- - Na **sidebar do AppShell** (`sidebarNav={<ShellNav …/>}`): lê o estado de recolhido do
44
- contexto `useAppShell` e vira ícone-só sozinho.
45
- - **Dentro do conteúdo** (o que o `SectionShell` faz): sem esse contexto, renderiza
46
- expandido. O `SectionShell` **passa a compor** `ShellNav` (a nav) + o painel — um nav,
47
- dois lares.
48
-
49
- Isto é o "tanto faz onde, mesmo comportamento": o componente **se adapta ao contexto** via
50
- `useAppShell`, sem o consumidor configurar nada.
51
-
52
- ### Heading com espaço pra botão
53
-
54
- Slot `heading` — título + ação opcional. É o "+" de novo relatório, o título da seção, a
55
- marca na sidebar. Trivial e resolve o gesto que hoje mora à mão em cada nav.
56
-
57
- ### Árvore embutida; DnD composto (a régua)
58
-
59
- - **Aninhamento/árvore → dentro do `ShellNav`.** Não é feature nova: o accordion da sidebar
60
- (Vendas → Leads/Propostas, no GB) já é uma árvore de um nível. Barato, já necessário.
61
- - **Drag-drop de reordenar → COMPOSTO por fora**, estilo `dnd-kit`: o `ShellNav` expõe a
62
- costura (um wrapper de item + um slot pro indicador de drop); um companheiro
63
- (`useNavReorder`) pluga o arrasto só onde se opta. O peso — estado de drag, indicadores,
64
- toque e principalmente a **a11y** (nav é *landmark*; reorder é *widget de manipulação* —
65
- identidades diferentes) — vive fora do primitivo.
66
-
67
- **O critério (viaja pra próximas decisões de primitivo): embute o que a maioria
68
- compartilha; compõe o que um só precisa.** Reordenar é feature de um consumidor (só o
69
- Relatórios); embuti-la faria os muitos pagarem a complexidade pelo um.
70
-
71
- ### GB é a aparência de referência
72
-
73
- Das três, a implementação do GB é a mais madura (tem rail/colapso, accordion, estados
74
- ativos, âncora inferior de Configurações). **O `ShellNav` codifica o look do GB como
75
- default** — nada regride: o GB migra pro primitivo e renderiza igual; a cabine (já alinhada
76
- às classes do GB) passa a ser *literalmente* o mesmo componente, e a "sensação de
77
- retangular" some. A **identidade por app** continua nos **tokens** (`--radius`, cores — o
78
- Opus já permite sobrescrever no `theme.css` do app), não num fork do componente: a forma é
79
- compartilhada; a pele é temável.
80
-
81
- ## API (esboço)
82
-
83
- ```tsx
84
- interface ShellNavItem {
85
- id: string
86
- label: string
87
- icon?: ReactNode
88
- badge?: ReactNode
89
- disabled?: boolean
90
- children?: ShellNavItem[] // accordion (app) / pasta (relatórios)
91
- }
92
- interface ShellNavGroup { label?: string; items: ShellNavItem[] }
93
-
94
- interface ShellNavProps {
95
- groups: ShellNavGroup[]
96
- activeId?: string
97
- onSelect: (id: string) => void
98
- heading?: ReactNode // título + espaço pra botão (o "+" do Relatórios)
99
- footer?: ReactNode // âncora inferior (Configurações, no app)
100
- collapsed?: boolean // override; default lê do useAppShell quando presente
101
- navLabel?: string
102
- reorder?: NavReorderSeam // opt-in; sem isto, ZERO DnD (o dnd-kit/useNavReorder pluga aqui)
103
- }
104
-
105
- // Conveniência do heading (padroniza título + ação):
106
- <ShellNavHeading title="Relatórios" action={<Button size="icon" icon={Plus} …/>} />
107
- ```
108
-
109
- O `SectionShell` reescreve por dentro pra `<ShellNav/>` + painel; sua API pública não muda
110
- (consumidores existentes seguem).
111
-
112
- ## Hoje × proposto
113
-
114
- | Menu | Hoje | Proposto |
115
- |---|---|---|
116
- | App (sidebar) | `<button>` à mão, N cópias por estado | `AppShell sidebarNav={<ShellNav groups footer=Configurações/>}` — colapso via contexto |
117
- | Configurações | `SectionShell` (nav+painel à mão por dentro) | `SectionShell` compõe `ShellNav` — sem mudança pro consumidor |
118
- | Relatórios | `<aside>` + tree + DnD, tudo à mão | `<ShellNav heading={título+"+"} …/>` + `useNavReorder` ao lado |
119
-
120
- ## Consequências / plano
121
-
122
- - **Escopo:** sessão dedicada no Opus (não é ajuste de app). Ciclo: desenhar a API →
123
- publicar versão do Opus → migrar os consumidores (cada `<button>` à mão vira `groups={…}`).
124
- - **Custo:** baixo em código, alto em coordenação (bump + migração dos 3 produtos + gates
125
- em cada). O `SectionShell` é o precedente vivo — foi o mesmo movimento, um nível abaixo.
126
- - **Ordem sugerida:** (1) `ShellNav` + `ShellNavHeading` no Opus com o look do GB como
127
- default; (2) `SectionShell` passa a compor; (3) migrar a cabine do Maestro (primeiro
128
- cliente, já alinhada); (4) migrar o back-office; (5) migrar o Relatórios do GB pra
129
- `ShellNav` + `useNavReorder` (fecha a inconsistência viva).
130
- - **Não-objetivo:** o `ShellNav` não vira dono de DnD, tree-editor ou roteamento — segue
131
- presentacional/controlado; o app é dono do roteamento e do estado.