@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
|
@@ -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-
|
|
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-
|
|
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-
|
|
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 {
|
|
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:
|
|
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) => ({
|
|
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
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
78
|
+
<>
|
|
79
79
|
{rotas}
|
|
80
80
|
<Toaster />
|
|
81
81
|
<DialogHost />
|
|
82
|
-
|
|
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 `
|
|
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-
|
|
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).
|
|
114
|
-
>
|
|
115
|
-
>
|
|
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} />
|
package/src/ui/docs/registry.tsx
CHANGED
|
@@ -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-
|
|
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
|
|
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
|
|
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,
|
|
219
|
-
export type { SidebarProps, SidebarItemProps, SidebarNavGroup } from './components/patterns/sidebar.tsx'
|
|
220
|
-
|
|
221
|
-
//
|
|
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.
|