@softize/opus 8.6.6
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 +1616 -0
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/bin/cli.mjs +528 -0
- package/bin/lib/check.mjs +307 -0
- package/bin/lib/components.mjs +151 -0
- package/bin/lib/create.mjs +208 -0
- package/bin/lib/db-check-runner.mjs +86 -0
- package/bin/lib/db-migrate-runner.mjs +89 -0
- package/bin/lib/db-scaffold-runner.mjs +84 -0
- package/bin/lib/db.mjs +261 -0
- package/bin/lib/docs-include.mjs +48 -0
- package/bin/lib/gen-dicts.mjs +134 -0
- package/bin/lib/gen-docs.mjs +288 -0
- package/bin/lib/gen-manifest.mjs +102 -0
- package/bin/lib/gen-openapi.mjs +195 -0
- package/bin/lib/gen-runner.mjs +472 -0
- package/bin/lib/gen-stubs.mjs +463 -0
- package/bin/lib/gen.mjs +311 -0
- package/bin/lib/init.mjs +514 -0
- package/bin/lib/introspect.mjs +107 -0
- package/bin/lib/mcp.mjs +85 -0
- package/bin/lib/postinstall.mjs +56 -0
- package/docs/chat-event-protocol.md +85 -0
- package/docs/code-style.md +16 -0
- package/docs/data-layer.md +246 -0
- package/docs/ownership-vs-shadcn-lock.md +102 -0
- package/docs/protocol.md +2053 -0
- package/docs/releasing.md +110 -0
- package/docs/shellnav.md +131 -0
- package/package.json +338 -0
- package/registry/hooks/hooks.json +26 -0
- package/registry/hooks/link-memory-on-start.mjs +46 -0
- package/registry/hooks/opus-check-on-stop.mjs +114 -0
- package/registry/skills/create-action/SKILL.md +49 -0
- package/registry/skills/create-action/scaffold.mjs +122 -0
- package/registry/templates/app/_gitignore +3 -0
- package/registry/templates/app/_npmrc +1 -0
- package/registry/templates/app/_opus/_gitignore +5 -0
- package/registry/templates/app/_prettierrc.json +6 -0
- package/registry/templates/app/index.html +13 -0
- package/registry/templates/app/opus.config.ts +16 -0
- package/registry/templates/app/package.json +43 -0
- package/registry/templates/app/pnpm-workspace.yaml +11 -0
- package/registry/templates/app/public/favicon.svg +4 -0
- package/registry/templates/app/src/App.tsx +37 -0
- package/registry/templates/app/src/domains/tasks/actions/list.test.ts +34 -0
- package/registry/templates/app/src/domains/tasks/actions/list.ts +33 -0
- package/registry/templates/app/src/domains/tasks/index.ts +13 -0
- package/registry/templates/app/src/index.css +18 -0
- package/registry/templates/app/src/main.tsx +25 -0
- package/registry/templates/app/tsconfig.json +20 -0
- package/registry/templates/app/vite.config.ts +46 -0
- package/registry/templates/monorepo/_gitignore +3 -0
- package/registry/templates/monorepo/_npmrc +1 -0
- package/registry/templates/monorepo/package.json +9 -0
- package/registry/templates/monorepo/pnpm-workspace.yaml +14 -0
- package/src/ai/ask.ts +64 -0
- package/src/ai/drivers/anthropic.ts +309 -0
- package/src/ai/index.ts +17 -0
- package/src/audit/drivers/console.ts +117 -0
- package/src/audit/drivers/pg.ts +172 -0
- package/src/audit/index.ts +51 -0
- package/src/auth/drivers/better-auth.ts +103 -0
- package/src/auth/drivers/jwt.ts +188 -0
- package/src/auth/index.ts +9 -0
- package/src/client/drivers/fetch.ts +202 -0
- package/src/client/index.ts +22 -0
- package/src/core/actions.ts +110 -0
- package/src/core/audit.ts +239 -0
- package/src/core/contracts.ts +137 -0
- package/src/core/domain.ts +310 -0
- package/src/core/errors.ts +181 -0
- package/src/core/index.ts +174 -0
- package/src/core/logical-type.ts +31 -0
- package/src/core/reactions.ts +81 -0
- package/src/core/runtime.ts +1167 -0
- package/src/core/schedules.ts +41 -0
- package/src/core/types.ts +1356 -0
- package/src/data/drivers/kysely.ts +389 -0
- package/src/data/index.ts +10 -0
- package/src/data/readonly-pool.ts +160 -0
- package/src/dsl/eval.ts +136 -0
- package/src/dsl/index.ts +29 -0
- package/src/dsl/kysely.ts +230 -0
- package/src/dsl/loads.ts +123 -0
- package/src/dsl/parser.ts +423 -0
- package/src/dsl/types.ts +113 -0
- package/src/events/drivers/mitt.ts +70 -0
- package/src/events/index.ts +9 -0
- package/src/log/drivers/pino.ts +57 -0
- package/src/log/index.ts +9 -0
- package/src/mcp/index.ts +62 -0
- package/src/queue/drivers/bullmq.ts +190 -0
- package/src/queue/index.ts +9 -0
- package/src/scheduler/drivers/node-cron.ts +93 -0
- package/src/scheduler/every.ts +45 -0
- package/src/scheduler/index.ts +9 -0
- package/src/schema/drivers/zod.ts +765 -0
- package/src/schema/entity.ts +439 -0
- package/src/schema/format/locale.ts +144 -0
- package/src/schema/index.ts +65 -0
- package/src/schema/openapi.ts +302 -0
- package/src/schema/scaffold.ts +160 -0
- package/src/server/drivers/fastify.ts +224 -0
- package/src/server/drivers/node.ts +386 -0
- package/src/server/index.ts +142 -0
- package/src/storage/drivers/fs.ts +90 -0
- package/src/storage/drivers/s3.ts +117 -0
- package/src/storage/index.ts +27 -0
- package/src/testing/fake.ts +298 -0
- package/src/testing/index.ts +324 -0
- package/src/ui/components/patterns/action-form-card.tsx +48 -0
- package/src/ui/components/patterns/action-list-dialog.tsx +93 -0
- package/src/ui/components/patterns/app-shell.tsx +227 -0
- package/src/ui/components/patterns/confirm.tsx +226 -0
- package/src/ui/components/patterns/data-state.tsx +75 -0
- package/src/ui/components/patterns/form-dialog.tsx +64 -0
- package/src/ui/components/patterns/form.tsx +584 -0
- package/src/ui/components/patterns/list.tsx +1488 -0
- package/src/ui/components/patterns/page.tsx +46 -0
- package/src/ui/components/patterns/section-shell.tsx +246 -0
- package/src/ui/components/patterns/shell-nav.tsx +150 -0
- package/src/ui/components/patterns/sidebar.tsx +89 -0
- package/src/ui/components/patterns/split.tsx +93 -0
- package/src/ui/components/patterns/trigger.tsx +196 -0
- package/src/ui/components/patterns/view.tsx +84 -0
- package/src/ui/components/primitives/accordion.tsx +64 -0
- package/src/ui/components/primitives/alert-dialog.tsx +190 -0
- package/src/ui/components/primitives/alert.tsx +116 -0
- package/src/ui/components/primitives/aspect-ratio.tsx +9 -0
- package/src/ui/components/primitives/avatar.tsx +107 -0
- package/src/ui/components/primitives/badge.tsx +37 -0
- package/src/ui/components/primitives/breadcrumb.tsx +109 -0
- package/src/ui/components/primitives/button-group.tsx +83 -0
- package/src/ui/components/primitives/button.tsx +102 -0
- package/src/ui/components/primitives/calendar.tsx +218 -0
- package/src/ui/components/primitives/card.tsx +56 -0
- package/src/ui/components/primitives/carousel.tsx +239 -0
- package/src/ui/components/primitives/chat.tsx +407 -0
- package/src/ui/components/primitives/checkbox.tsx +30 -0
- package/src/ui/components/primitives/collapsible.tsx +31 -0
- package/src/ui/components/primitives/command.tsx +182 -0
- package/src/ui/components/primitives/composer.tsx +121 -0
- package/src/ui/components/primitives/copyable.tsx +50 -0
- package/src/ui/components/primitives/dialog.tsx +147 -0
- package/src/ui/components/primitives/drawer.tsx +141 -0
- package/src/ui/components/primitives/empty.tsx +104 -0
- package/src/ui/components/primitives/field.tsx +246 -0
- package/src/ui/components/primitives/icon-picker.tsx +180 -0
- package/src/ui/components/primitives/input-group.tsx +168 -0
- package/src/ui/components/primitives/input-otp.tsx +75 -0
- package/src/ui/components/primitives/input.tsx +72 -0
- package/src/ui/components/primitives/item.tsx +193 -0
- package/src/ui/components/primitives/kbd.tsx +28 -0
- package/src/ui/components/primitives/label.tsx +22 -0
- package/src/ui/components/primitives/markdown.tsx +35 -0
- package/src/ui/components/primitives/menu.tsx +255 -0
- package/src/ui/components/primitives/pagination.tsx +127 -0
- package/src/ui/components/primitives/popover.tsx +87 -0
- package/src/ui/components/primitives/progress.tsx +29 -0
- package/src/ui/components/primitives/radio-group.tsx +43 -0
- package/src/ui/components/primitives/resizable.tsx +51 -0
- package/src/ui/components/primitives/scroll-area.tsx +56 -0
- package/src/ui/components/primitives/select.tsx +479 -0
- package/src/ui/components/primitives/separator.tsx +26 -0
- package/src/ui/components/primitives/skeleton.tsx +13 -0
- package/src/ui/components/primitives/slider.tsx +61 -0
- package/src/ui/components/primitives/sonner.tsx +46 -0
- package/src/ui/components/primitives/spinner.tsx +29 -0
- package/src/ui/components/primitives/switch.tsx +33 -0
- package/src/ui/components/primitives/table.tsx +114 -0
- package/src/ui/components/primitives/tabs.tsx +104 -0
- package/src/ui/components/primitives/textarea.tsx +18 -0
- package/src/ui/components/primitives/toggle-group.tsx +81 -0
- package/src/ui/components/primitives/toggle.tsx +45 -0
- package/src/ui/components/primitives/tooltip.tsx +55 -0
- package/src/ui/components/primitives/truncate.tsx +49 -0
- package/src/ui/docs/DocBrowser.tsx +90 -0
- package/src/ui/docs/changelog.tsx +80 -0
- package/src/ui/docs/content/accordion.md +86 -0
- package/src/ui/docs/content/action-form-card.md +24 -0
- package/src/ui/docs/content/action-form-dialog.md +30 -0
- package/src/ui/docs/content/action-form.md +125 -0
- package/src/ui/docs/content/action-list-dialog.md +68 -0
- package/src/ui/docs/content/action-list.md +194 -0
- package/src/ui/docs/content/action-trigger.md +72 -0
- package/src/ui/docs/content/action-view.md +47 -0
- package/src/ui/docs/content/actions.md +138 -0
- package/src/ui/docs/content/ai.md +112 -0
- package/src/ui/docs/content/alert-dialog.md +73 -0
- package/src/ui/docs/content/alert.md +69 -0
- package/src/ui/docs/content/app-shell.md +155 -0
- package/src/ui/docs/content/aspect-ratio.md +66 -0
- package/src/ui/docs/content/audit.md +84 -0
- package/src/ui/docs/content/auth.md +70 -0
- package/src/ui/docs/content/avatar.md +94 -0
- package/src/ui/docs/content/badge.md +48 -0
- package/src/ui/docs/content/breadcrumb.md +87 -0
- package/src/ui/docs/content/button-group.md +71 -0
- package/src/ui/docs/content/button.md +60 -0
- package/src/ui/docs/content/calendar.md +62 -0
- package/src/ui/docs/content/card.md +49 -0
- package/src/ui/docs/content/carousel.md +85 -0
- package/src/ui/docs/content/chat.md +69 -0
- package/src/ui/docs/content/checkbox.md +75 -0
- package/src/ui/docs/content/cli.md +58 -0
- package/src/ui/docs/content/collapsible.md +64 -0
- package/src/ui/docs/content/command.md +56 -0
- package/src/ui/docs/content/composer.md +50 -0
- package/src/ui/docs/content/confirm.md +120 -0
- package/src/ui/docs/content/copyable.md +30 -0
- package/src/ui/docs/content/customization.md +110 -0
- package/src/ui/docs/content/cycle.md +34 -0
- package/src/ui/docs/content/data-state.md +47 -0
- package/src/ui/docs/content/data.md +99 -0
- package/src/ui/docs/content/dialog.md +60 -0
- package/src/ui/docs/content/drawer.md +55 -0
- package/src/ui/docs/content/empty.md +66 -0
- package/src/ui/docs/content/events.md +61 -0
- package/src/ui/docs/content/field.md +58 -0
- package/src/ui/docs/content/getting-started.md +109 -0
- package/src/ui/docs/content/icon-picker.md +51 -0
- package/src/ui/docs/content/input-group.md +78 -0
- package/src/ui/docs/content/input-otp.md +72 -0
- package/src/ui/docs/content/input.md +78 -0
- package/src/ui/docs/content/item.md +84 -0
- package/src/ui/docs/content/kbd.md +62 -0
- package/src/ui/docs/content/label.md +32 -0
- package/src/ui/docs/content/log.md +55 -0
- package/src/ui/docs/content/markdown.md +41 -0
- package/src/ui/docs/content/mcp.md +44 -0
- package/src/ui/docs/content/menu.md +114 -0
- package/src/ui/docs/content/microcopy.md +83 -0
- package/src/ui/docs/content/page.md +34 -0
- package/src/ui/docs/content/pagination.md +99 -0
- package/src/ui/docs/content/popover.md +49 -0
- package/src/ui/docs/content/progress.md +69 -0
- package/src/ui/docs/content/queue.md +62 -0
- package/src/ui/docs/content/radio-group.md +77 -0
- package/src/ui/docs/content/resizable.md +86 -0
- package/src/ui/docs/content/router.md +56 -0
- package/src/ui/docs/content/runtime.md +77 -0
- package/src/ui/docs/content/scheduler.md +66 -0
- package/src/ui/docs/content/scroll-area.md +89 -0
- package/src/ui/docs/content/section-shell.md +121 -0
- package/src/ui/docs/content/select.md +342 -0
- package/src/ui/docs/content/separator.md +33 -0
- package/src/ui/docs/content/sidebar.md +38 -0
- package/src/ui/docs/content/skeleton.md +34 -0
- package/src/ui/docs/content/slider.md +64 -0
- package/src/ui/docs/content/spinner.md +37 -0
- package/src/ui/docs/content/split.md +33 -0
- package/src/ui/docs/content/storage.md +69 -0
- package/src/ui/docs/content/switch.md +69 -0
- package/src/ui/docs/content/table.md +102 -0
- package/src/ui/docs/content/tabs.md +94 -0
- package/src/ui/docs/content/testing.md +89 -0
- package/src/ui/docs/content/textarea.md +30 -0
- package/src/ui/docs/content/toast.md +67 -0
- package/src/ui/docs/content/toggle-group.md +81 -0
- package/src/ui/docs/content/toggle.md +72 -0
- package/src/ui/docs/content/tokens.md +171 -0
- package/src/ui/docs/content/tooltip.md +50 -0
- package/src/ui/docs/content/truncate.md +37 -0
- package/src/ui/docs/content/ui.md +40 -0
- package/src/ui/docs/content/upgrading.md +48 -0
- package/src/ui/docs/doc-client.tsx +214 -0
- package/src/ui/docs/doc.tsx +301 -0
- package/src/ui/docs/folder.tsx +149 -0
- package/src/ui/docs/index.ts +21 -0
- package/src/ui/docs/markdown.tsx +130 -0
- package/src/ui/docs/md-raw.d.ts +4 -0
- package/src/ui/docs/plugin.ts +104 -0
- package/src/ui/docs/registry.tsx +424 -0
- package/src/ui/docs/standalone.tsx +107 -0
- package/src/ui/drivers/react.tsx +627 -0
- package/src/ui/index.ts +92 -0
- package/src/ui/lib/cn.ts +10 -0
- package/src/ui/lib/zod-pt-br.ts +38 -0
- package/src/ui/meta.ts +412 -0
- package/src/ui/react.tsx +235 -0
- package/src/ui/router.ts +96 -0
- package/src/ui/theme.css +234 -0
- package/src/vite/design.ts +652 -0
- package/src/vite/index.ts +8 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
## Horizontal
|
|
2
|
+
|
|
3
|
+
ResizablePanelGroup envolve os painéis e o ResizableHandle entre eles. O grupo ocupa a altura do pai (h-full), então dê um tamanho ao container.
|
|
4
|
+
|
|
5
|
+
```tsx preview col
|
|
6
|
+
<div className="h-48 rounded-md border">
|
|
7
|
+
<ResizablePanelGroup orientation="horizontal">
|
|
8
|
+
<ResizablePanel defaultSize={35}>
|
|
9
|
+
<div className="flex h-full items-center justify-center p-4 text-sm">
|
|
10
|
+
Árvore do repositório
|
|
11
|
+
</div>
|
|
12
|
+
</ResizablePanel>
|
|
13
|
+
<ResizableHandle />
|
|
14
|
+
<ResizablePanel defaultSize={65}>
|
|
15
|
+
<div className="flex h-full items-center justify-center p-4 text-sm">
|
|
16
|
+
Diff da sessão
|
|
17
|
+
</div>
|
|
18
|
+
</ResizablePanel>
|
|
19
|
+
</ResizablePanelGroup>
|
|
20
|
+
</div>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Com alça visível
|
|
24
|
+
|
|
25
|
+
withHandle no ResizableHandle desenha a pega central — facilita acertar o arraste quando a divisória precisa ficar óbvia.
|
|
26
|
+
|
|
27
|
+
```tsx preview col
|
|
28
|
+
<div className="h-48 rounded-md border">
|
|
29
|
+
<ResizablePanelGroup orientation="horizontal">
|
|
30
|
+
<ResizablePanel defaultSize={40} minSize={20}>
|
|
31
|
+
<div className="flex h-full items-center justify-center p-4 text-sm">
|
|
32
|
+
Painel do agente developer
|
|
33
|
+
</div>
|
|
34
|
+
</ResizablePanel>
|
|
35
|
+
<ResizableHandle withHandle />
|
|
36
|
+
<ResizablePanel defaultSize={60} minSize={20}>
|
|
37
|
+
<div className="flex h-full items-center justify-center p-4 text-sm">
|
|
38
|
+
Preview do worktree
|
|
39
|
+
</div>
|
|
40
|
+
</ResizablePanel>
|
|
41
|
+
</ResizablePanelGroup>
|
|
42
|
+
</div>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Aninhado
|
|
46
|
+
|
|
47
|
+
Um ResizablePanelGroup dentro de um painel cruza as direções: orientation=vertical empilha os painéis e a alça vira horizontal.
|
|
48
|
+
|
|
49
|
+
```tsx preview col
|
|
50
|
+
<div className="h-64 rounded-md border">
|
|
51
|
+
<ResizablePanelGroup orientation="horizontal">
|
|
52
|
+
<ResizablePanel defaultSize={30}>
|
|
53
|
+
<div className="flex h-full items-center justify-center p-4 text-sm">
|
|
54
|
+
Workspace Empresa X
|
|
55
|
+
</div>
|
|
56
|
+
</ResizablePanel>
|
|
57
|
+
<ResizableHandle withHandle />
|
|
58
|
+
<ResizablePanel defaultSize={70}>
|
|
59
|
+
<ResizablePanelGroup orientation="vertical">
|
|
60
|
+
<ResizablePanel defaultSize={60}>
|
|
61
|
+
<div className="flex h-full items-center justify-center p-4 text-sm">
|
|
62
|
+
Chat do reviewer
|
|
63
|
+
</div>
|
|
64
|
+
</ResizablePanel>
|
|
65
|
+
<ResizableHandle withHandle />
|
|
66
|
+
<ResizablePanel defaultSize={40}>
|
|
67
|
+
<div className="flex h-full items-center justify-center p-4 text-sm">
|
|
68
|
+
Saída do opus check
|
|
69
|
+
</div>
|
|
70
|
+
</ResizablePanel>
|
|
71
|
+
</ResizablePanelGroup>
|
|
72
|
+
</ResizablePanel>
|
|
73
|
+
</ResizablePanelGroup>
|
|
74
|
+
</div>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Props
|
|
78
|
+
|
|
79
|
+
| Prop | Tipo | Default | Descrição |
|
|
80
|
+
|---|---|---|---|
|
|
81
|
+
| `orientation (ResizablePanelGroup)` | `'horizontal' \| 'vertical'` | `'horizontal'` | Direção do arraste — horizontal vira colunas, vertical empilha em linhas. |
|
|
82
|
+
| `defaultSize (ResizablePanel)` | `number \| string` | | Tamanho inicial do painel, em porcentagem do grupo. |
|
|
83
|
+
| `minSize (ResizablePanel)` | `number \| string` | | Tamanho mínimo até onde o painel encolhe no arraste. |
|
|
84
|
+
| `maxSize (ResizablePanel)` | `number \| string` | | Tamanho máximo até onde o painel cresce no arraste. |
|
|
85
|
+
| `collapsible (ResizablePanel)` | `boolean` | `false` | Deixa o painel colapsar ao ser arrastado abaixo do minSize. |
|
|
86
|
+
| `withHandle (ResizableHandle)` | `boolean` | `false` | Desenha a pega central na divisória — torna o ponto de arraste visível. |
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
## O pathname É o estado
|
|
2
|
+
|
|
3
|
+
Roteamento history-based, sem dependência: `pushState` + `popstate` + `useSyncExternalStore`. Páginas orientadas a URL — deep-link, reload e o botão voltar funcionam de graça, porque não há um segundo lugar guardando "onde estou".
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { navigate, useSegments } from '@softize/opus/ui/react'
|
|
7
|
+
|
|
8
|
+
function App() {
|
|
9
|
+
const segments = useSegments() // '/settings/users' → ['settings','users']
|
|
10
|
+
if (segments[0] === 'settings') return <Settings />
|
|
11
|
+
return <Home />
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## O escopo é pequeno de propósito
|
|
16
|
+
|
|
17
|
+
Não há tabela de rotas, `<Route>`, params tipados nem carregamento de dados. Isto é a **leitura reativa da URL + um `navigate`** — quem decide o que renderizar é o app, com `if`/`switch` sobre os segmentos.
|
|
18
|
+
|
|
19
|
+
A régua: se a sua tela precisa de casamento de padrão (`/users/:id/posts/:postId`), params tipados ou data loaders, o caso pede uma biblioteca de rotas, não isto. Um app de back-office com uma dúzia de destinos quase nunca precisa.
|
|
20
|
+
|
|
21
|
+
## As quatro peças
|
|
22
|
+
|
|
23
|
+
| | |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `usePathname()` | O pathname como estado reativo. |
|
|
26
|
+
| `useSegments()` | Os segmentos, já sem os vazios: `/settings/users` → `['settings','users']`. |
|
|
27
|
+
| `useSearchParams()` | A querystring reativa — o lar natural do estado interno de uma seção (qual relatório está aberto, o recorte de uma lista). |
|
|
28
|
+
| `navigate(path, opts?)` | `pushState` + notifica. `{ replace: true }` troca a entrada corrente. |
|
|
29
|
+
|
|
30
|
+
## Dois detalhes que custaram caro nas cópias à mão
|
|
31
|
+
|
|
32
|
+
**Destino igual é no-op.** `navigate` resolve o destino com `new URL` e compara o `href` inteiro — não faz nada se você já está exatamente lá. Sem isso, clicar duas vezes no mesmo item do menu empilha entradas idênticas e o botão "voltar" não sai do lugar.
|
|
33
|
+
|
|
34
|
+
Comparar strings cruas parece bastar e não basta, em duas frentes. O **hash**: estando em `/a#secao`, `navigate('/a')` pareceria destino repetido e a âncora nunca sairia da URL. E o **encoding**: `window.location` devolve `/relatórios` como `/relat%C3%B3rios`, então a comparação crua nunca casa e cada clique empilha — bem no caso pt-BR, e no ``navigate(`/reports?report=${arquivo}`)`` com nome de arquivo acentuado.
|
|
35
|
+
|
|
36
|
+
**A notificação é um evento, não uma lista.** `pushState` não dispara `popstate`, então `navigate` dispara — e é `window.dispatchEvent`, não um `Set` de assinantes em escopo de módulo. A diferença aparece nas bordas: código que ainda escuta `popstate` na unha continua acompanhando, e duas cópias do pacote no `node_modules` continuam se enxergando. Uma lista privada mora numa instância do bundle; o `window` é um só.
|
|
37
|
+
|
|
38
|
+
**`useSyncExternalStore`, não `useState`.** Ler `window.location` dentro de `useState`/`useEffect` sofre *tearing* no modo concurrent: dois componentes podem renderizar o mesmo commit com URLs diferentes. As cópias que este módulo substituiu faziam isso.
|
|
39
|
+
|
|
40
|
+
## `replace`: quando o passo não é história
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
navigate(`/reports?report=${file}`, { replace: true })
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Use quando a mudança não merece uma volta do botão "voltar" — um rascunho que ganha id no meio do fluxo, um filtro que a pessoa ajusta várias vezes seguidas. O default (`pushState`) é o certo para navegação de verdade.
|
|
47
|
+
|
|
48
|
+
## SSR
|
|
49
|
+
|
|
50
|
+
Os hooks têm snapshot de servidor constante (`/` e busca vazia): renderizam sem `window` e reconciliam com a URL real no primeiro render do cliente. Se a sua página precisa do path correto **no HTML servido**, isto não resolve — aí o path tem que vir do framework, por prop.
|
|
51
|
+
|
|
52
|
+
O `navigate` **não** tem essa cortesia: ele toca `window` direto e estoura no servidor. É de propósito — ler a URL sem browser tem resposta razoável, navegar não tem, e engolir a chamada em silêncio esconderia o erro de uso.
|
|
53
|
+
|
|
54
|
+
## Destino externo não é problema daqui
|
|
55
|
+
|
|
56
|
+
`navigate` espera um caminho da própria origem. Validar destino que veio de terceiro — o `?redirect=` de um pós-login, por exemplo — é responsabilidade de quem recebe esse input, e a checagem certa é resolver com `new URL(valor, origin)` e comparar `origin`, não olhar prefixo.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Runtime & adapters
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Runtime & adapters
|
|
6
|
+
|
|
7
|
+
O Opus não é framework — é **protocolo + SDK**: você compõe o runtime a partir de
|
|
8
|
+
adapters plugáveis, e cada capacidade é opcional. O litmus é a inversão de controle:
|
|
9
|
+
o runtime não toma o seu app; o seu app monta o runtime com as peças que quer.
|
|
10
|
+
|
|
11
|
+
## O modelo
|
|
12
|
+
|
|
13
|
+
> Todo adapter implementa `name`/`kind`; `init`/`dispose`/`healthCheck` são opcionais e
|
|
14
|
+
> entram no ciclo de vida do runtime (start inicializa, dispose desliga em ordem reversa,
|
|
15
|
+
> healthCheck agrega).
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createRuntime } from '@softize/opus/core'
|
|
19
|
+
|
|
20
|
+
const runtime = createRuntime({
|
|
21
|
+
server: fastifyServer({ app }), // monta as actions como endpoints
|
|
22
|
+
data: kyselyData({ db }), // fornece ctx.db
|
|
23
|
+
auth: makeAuthAdapter(), // resolve user/tenant/can
|
|
24
|
+
audit: [pgAudit({ pool })], // trilha de auditoria (N sinks)
|
|
25
|
+
storage: fsStorage({ root }), // fornece ctx.storage (experimental)
|
|
26
|
+
config: { env, i18n: { defaultLocale: 'pt-BR' } },
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
runtime.register([dealsDomain, billingDomain]) // não registrado = não existe
|
|
30
|
+
await runtime.start()
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## As capacidades
|
|
34
|
+
|
|
35
|
+
> Um subpath por driver; quem não usa, não instala (peers opcionais onde há dependência
|
|
36
|
+
> pesada). A tabela é o mapa — cada linha é um recurso do protocolo.
|
|
37
|
+
|
|
38
|
+
| Capacidade | Fornece | Driver pronto |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `server` | monta as actions (HTTP) | `fastifyServer` — `@softize/opus/server/fastify` |
|
|
41
|
+
| `data` | `ctx.db` + drift-check + CRUD | `kyselyData`, `kyselyRepo`, `crudActions` — `@softize/opus/data/kysely` |
|
|
42
|
+
| `auth` | `user`/`tenantId`/`can` do contexto | `jwtAuth` — `…/auth/jwt` · `betterAuthSession` — `…/auth/better-auth` |
|
|
43
|
+
| `audit` | trilha por execução de action | `pgAudit` — `…/audit/pg` · `consoleAudit` — `…/audit/console` |
|
|
44
|
+
| `log` | `ctx.log` estruturado | `pinoLogger` — `@softize/opus/log/pino` |
|
|
45
|
+
| `eventBus` | `ctx.emit` + **reactions** | `mittEvents` — `@softize/opus/events/mitt` |
|
|
46
|
+
| `queue` | jobs em background | `bullmqQueue` — `@softize/opus/queue/bullmq` |
|
|
47
|
+
| `scheduler` | **schedules** (cron/intervalo) | `nodeCronScheduler` — `@softize/opus/scheduler/node-cron` |
|
|
48
|
+
| `storage` | `ctx.storage` (arquivos) | `fsStorage` — `…/storage/fs` · `s3Storage` — `…/storage/s3` |
|
|
49
|
+
| `ai` | `ctx.ai` (complete/extract) | `anthropicAi` — `@softize/opus/ai/anthropic` |
|
|
50
|
+
| `client` | chamar actions de fora (stubs) | `fetchClient` — `@softize/opus/client/fetch` |
|
|
51
|
+
|
|
52
|
+
Fora do runtime, mas parte do protocolo: o harness de teste (`runAction`/`testContext`
|
|
53
|
+
— `@softize/opus/testing`) roda uma action em unidade com o mesmo pipeline do
|
|
54
|
+
`execute`. Ver **Testes de action**.
|
|
55
|
+
|
|
56
|
+
## O contexto do handler
|
|
57
|
+
|
|
58
|
+
> O `ctx` é a soma do que os adapters fornecem — sem adapter, o campo é `null`/`unknown`
|
|
59
|
+
> e o handler decide como degradar.
|
|
60
|
+
|
|
61
|
+
`user`/`tenantId`/`can` (auth) · `db` (data) · `log` (logger) · `emit` (eventBus) ·
|
|
62
|
+
`storage` (storage) · `ai` (ai) · `provenance` (quem disparou: http, schedule,
|
|
63
|
+
reaction…) · `meta`.
|
|
64
|
+
|
|
65
|
+
## Reactions e schedules
|
|
66
|
+
|
|
67
|
+
> Os dois jeitos de código rodar SEM request: evento de domínio dispara reaction;
|
|
68
|
+
> tempo dispara schedule (que executa uma action, com provenance própria).
|
|
69
|
+
|
|
70
|
+
Declarados como as actions e registrados no mesmo `runtime.register` — o manifest
|
|
71
|
+
projeta os três (o Maestro mostra o wiring em Visão geral).
|
|
72
|
+
|
|
73
|
+
## Referência profunda
|
|
74
|
+
|
|
75
|
+
O contrato completo (tipos, garantias, ordem de execução, erros) vive no
|
|
76
|
+
`docs/protocol.md` **dentro do pacote** — como o changelog, viaja com a versão
|
|
77
|
+
que você instalou.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Agendador
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Agendador
|
|
6
|
+
|
|
7
|
+
Rodar uma action no tempo — cron ou intervalo. Você declara o schedule (qual action, quando)
|
|
8
|
+
e o runtime dispara na hora, com provenance `schedule` (rastreável no audit como qualquer
|
|
9
|
+
execução). O contrato é o `SchedulerAdapter`.
|
|
10
|
+
|
|
11
|
+
## O contrato
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
interface SchedulerAdapter {
|
|
15
|
+
register(spec: ScheduleDef, fire: () => Promise<void> | void): Unsubscribe
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ScheduleDef {
|
|
19
|
+
name: string
|
|
20
|
+
action: string // action do registry, executada quando dispara
|
|
21
|
+
cron?: string // '0 9 * * *' (9h todo dia)
|
|
22
|
+
every?: string // '1h', '30m', '15s' — atalho de intervalo
|
|
23
|
+
timezone?: string // IANA, ex.: 'America/Sao_Paulo' (default UTC)
|
|
24
|
+
input?: unknown | (() => unknown | Promise<unknown>) // estático ou dinâmico
|
|
25
|
+
enabled?: boolean
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
O timing é exclusivo: `cron` **ou** `every`. A action referenciada tem que existir no
|
|
30
|
+
registry.
|
|
31
|
+
|
|
32
|
+
## Driver
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { nodeCronScheduler } from '@softize/opus/scheduler/node-cron'
|
|
36
|
+
|
|
37
|
+
const scheduler = nodeCronScheduler() // in-process
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## No runtime
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { defineSchedule } from '@softize/opus/core'
|
|
44
|
+
|
|
45
|
+
// Declarado no domínio, junto das actions:
|
|
46
|
+
const fecharDia = defineSchedule({
|
|
47
|
+
name: 'fechar-dia',
|
|
48
|
+
action: 'caixa.fechar',
|
|
49
|
+
cron: '0 23 * * *',
|
|
50
|
+
timezone: 'America/Sao_Paulo',
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const runtime = createRuntime({
|
|
54
|
+
// …server/data…
|
|
55
|
+
scheduler,
|
|
56
|
+
})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
O runtime registra os schedules do domínio no `start()` e chama a action quando o tempo bate
|
|
60
|
+
— nada de disparar no handler.
|
|
61
|
+
|
|
62
|
+
## Limites (por enquanto)
|
|
63
|
+
|
|
64
|
+
Driver hoje: `node-cron` (in-process — some se o processo cair; num cluster, cada nó
|
|
65
|
+
dispararia). Backends duráveis/distribuídos (BullMQ repeatable, Temporal…) entram por
|
|
66
|
+
reincidência.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
## Lista de altura fixa
|
|
2
|
+
|
|
3
|
+
Dê a altura ao ScrollArea (h-48) e ponha o conteúdo dentro — a barra vertical já vem por padrão, sem precisar de ScrollBar.
|
|
4
|
+
|
|
5
|
+
```tsx preview col
|
|
6
|
+
const sessions = [
|
|
7
|
+
{ id: 'empresa-x-1842', title: 'Migrar billing pro novo schema', agent: 'developer' },
|
|
8
|
+
{ id: 'empresa-x-1839', title: 'Revisar handoff do checkout', agent: 'reviewer' },
|
|
9
|
+
{ id: 'empresa-x-1835', title: 'Redesenhar o painel de rotas', agent: 'designer' },
|
|
10
|
+
{ id: 'empresa-x-1830', title: 'Corrigir flaky no teste de webhook', agent: 'developer' },
|
|
11
|
+
{ id: 'empresa-x-1827', title: 'Vincular skill clean-code ao agente', agent: 'reviewer' },
|
|
12
|
+
{ id: 'empresa-x-1821', title: 'Subir preview do worktree', agent: 'developer' },
|
|
13
|
+
{ id: 'empresa-x-1818', title: 'Ajustar copy do empty state', agent: 'designer' },
|
|
14
|
+
{ id: 'empresa-x-1814', title: 'Rodar opus check no monorepo', agent: 'reviewer' },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
render(
|
|
18
|
+
<ScrollArea className="h-48 w-full rounded-md border border-border">
|
|
19
|
+
<div className="p-3">
|
|
20
|
+
{sessions.map((s) => (
|
|
21
|
+
<div key={s.id} className="rounded-md px-2 py-1.5 text-sm hover:bg-muted">
|
|
22
|
+
<p className="font-medium">{s.title}</p>
|
|
23
|
+
<p className="text-xs text-muted-foreground">{s.id} · {s.agent}</p>
|
|
24
|
+
</div>
|
|
25
|
+
))}
|
|
26
|
+
</div>
|
|
27
|
+
</ScrollArea>,
|
|
28
|
+
)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Trilho horizontal
|
|
32
|
+
|
|
33
|
+
Pra rolar na horizontal, acrescente <ScrollBar orientation="horizontal" /> como filho e deixe o conteúdo numa linha que não quebra (flex + w-max).
|
|
34
|
+
|
|
35
|
+
```tsx preview col
|
|
36
|
+
const skills = ['clean-code', 'code-style', 'ddd', 'test', 'create-action', 'opus', 'clean-architecture']
|
|
37
|
+
|
|
38
|
+
render(
|
|
39
|
+
<ScrollArea className="w-full rounded-md border border-border whitespace-nowrap">
|
|
40
|
+
<div className="flex w-max gap-2 p-3">
|
|
41
|
+
{skills.map((skill) => (
|
|
42
|
+
<span key={skill} className="rounded-md border border-border bg-muted px-3 py-1.5 text-sm">
|
|
43
|
+
{skill}
|
|
44
|
+
</span>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
<ScrollBar orientation="horizontal" />
|
|
48
|
+
</ScrollArea>,
|
|
49
|
+
)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Painel com divisórias
|
|
53
|
+
|
|
54
|
+
O conteúdo é livre: aqui o log do agente, com Separator entre as linhas. A barra só aparece quando o conteúdo passa da altura.
|
|
55
|
+
|
|
56
|
+
```tsx preview col
|
|
57
|
+
const log = [
|
|
58
|
+
'12:04 developer clonou o worktree de empresa-x-api',
|
|
59
|
+
'12:05 developer rodou pnpm install',
|
|
60
|
+
'12:07 developer abriu a sessão empresa-x-1842',
|
|
61
|
+
'12:11 developer aplicou o diff no schema de billing',
|
|
62
|
+
'12:14 reviewer foi acionado no handoff',
|
|
63
|
+
'12:16 reviewer apontou 2 ajustes de microcopy',
|
|
64
|
+
'12:19 developer corrigiu a microcopy',
|
|
65
|
+
'12:22 opus check passou em todos os pacotes',
|
|
66
|
+
'12:24 reviewer aprovou o handoff',
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
render(
|
|
70
|
+
<ScrollArea className="h-40 w-full rounded-md border border-border">
|
|
71
|
+
<div className="p-3 text-sm">
|
|
72
|
+
{log.map((line, i) => (
|
|
73
|
+
<div key={i}>
|
|
74
|
+
{i > 0 && <Separator className="my-1.5" />}
|
|
75
|
+
<p className="text-muted-foreground">{line}</p>
|
|
76
|
+
</div>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
</ScrollArea>,
|
|
80
|
+
)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Props
|
|
84
|
+
|
|
85
|
+
| Prop | Tipo | Default | Descrição |
|
|
86
|
+
|---|---|---|---|
|
|
87
|
+
| `className (ScrollArea)` | `string` | | Onde a altura (h-48) ou a largura mora — é o que define a janela rolável. Sem dimensão, não há o que rolar. |
|
|
88
|
+
| `children (ScrollArea)` | `React.ReactNode` | | O conteúdo da janela. Inclua um <ScrollBar orientation="horizontal" /> entre os filhos pra habilitar a rolagem lateral. |
|
|
89
|
+
| `orientation (ScrollBar)` | `'vertical' \| 'horizontal'` | `'vertical'` | A direção da barra. A vertical já vem embutida; adicione a horizontal só quando precisar. |
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
## Seção com navegação própria
|
|
2
|
+
|
|
3
|
+
O nível do meio do chrome. O `AppShell` é o quadro do app e o `Page` é o esqueleto de **uma** tela; entre os dois falta a **seção**: um grupo de telas irmãs (Configurações, Relatórios, a doc) que quer a lista **dentro** do conteúdo, não pendurada na sidebar do app.
|
|
4
|
+
|
|
5
|
+
A nav é `w-56` — mais estreita que o `w-64` do `AppShell` de propósito, pra hierarquia ficar legível quando as duas aparecem lado a lado.
|
|
6
|
+
|
|
7
|
+
```tsx preview col
|
|
8
|
+
render(
|
|
9
|
+
<div className="h-96 w-full overflow-hidden rounded-lg border border-border">
|
|
10
|
+
<SectionShell
|
|
11
|
+
groups={[
|
|
12
|
+
{ label: 'Acesso', items: [{ id: 'users', label: 'Usuários' }, { id: 'roles', label: 'Perfis' }] },
|
|
13
|
+
{ label: 'Cadastros', items: [{ id: 'units', label: 'Unidades' }, { id: 'departments', label: 'Departamentos' }] },
|
|
14
|
+
{ items: [{ id: 'audit', label: 'Auditoria' }, { id: 'integrations', label: 'Integrações', disabled: true }] },
|
|
15
|
+
]}
|
|
16
|
+
activeId="users"
|
|
17
|
+
onSelect={() => {}}
|
|
18
|
+
>
|
|
19
|
+
<Page title="Usuários" description="As contas do IdP.">
|
|
20
|
+
<div className="rounded-lg border border-dashed border-border p-10 text-center text-sm text-muted-foreground">
|
|
21
|
+
A tela do item ativo (em geral um Page).
|
|
22
|
+
</div>
|
|
23
|
+
</Page>
|
|
24
|
+
</SectionShell>
|
|
25
|
+
</div>,
|
|
26
|
+
)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quando usar — sidebar, tabs ou seção
|
|
30
|
+
|
|
31
|
+
A regra da casa, pra não virar gosto pessoal:
|
|
32
|
+
|
|
33
|
+
- **Sidebar do app** (`AppShell`) — os **contextos de negócio**, o mapa que a pessoa carrega na cabeça o dia todo. Custa caro: cada item aqui compete com todos os outros.
|
|
34
|
+
- **`SectionShell`** — as telas de uma seção que a pessoa **visita raro e navega por dentro** quando visita. Configurações é o caso canônico: nenhum admin quer 6 itens de config disputando espaço com Vendas e Estoque. Também é a escolha quando a lista é **dinâmica** (relatórios, documentos) e não cabe num nav estático.
|
|
35
|
+
- **Tabs** — quando são **facetas do mesmo objeto** (a mesma entidade vista de ângulos diferentes), não telas distintas. Se cada aba tem URL própria e faz sentido dar deep-link, provavelmente é seção, não aba.
|
|
36
|
+
|
|
37
|
+
Sintoma de que uma seção devia sair da sidebar: o item de topo **não tem tela própria** e só existe pra abrir um accordion. Isso é um nó de menu, não um destino — e cobra o preço de um destino.
|
|
38
|
+
|
|
39
|
+
## Roteamento fica de fora
|
|
40
|
+
|
|
41
|
+
O componente é **controlado**: `activeId` + `onSelect`. Ele não lê nem escreve URL — quem roteia é o app, com o mecanismo que já usa (path, querystring, estado). Mesma decisão do `AppShell`: o shell é o quadro, não o conteúdo.
|
|
42
|
+
|
|
43
|
+
O padrão comum é casar `activeId` com um segmento do path e navegar no `onSelect`:
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
<SectionShell
|
|
47
|
+
groups={groups}
|
|
48
|
+
activeId={segments[1]} // /settings/users → 'users'
|
|
49
|
+
onSelect={(id) => navigate(`/settings/${id}`)}
|
|
50
|
+
>
|
|
51
|
+
{screen}
|
|
52
|
+
</SectionShell>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## O painel remonta na troca
|
|
56
|
+
|
|
57
|
+
Trocar de item **remonta** o painel — é isso que zera o scroll (sem o remount, a tela nova abre na altura em que a anterior estava). É o detalhe que toda cópia à mão esquecia.
|
|
58
|
+
|
|
59
|
+
A chave do remount é o `activeId`. `scrollResetKey` sobrescreve, e há dois usos legítimos — em direções opostas:
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
// NUNCA remontar: a seção preserva o scroll ao trocar de item (chave constante).
|
|
63
|
+
<SectionShell activeId={id} scrollResetKey="fixa" …>
|
|
64
|
+
|
|
65
|
+
// Remontar MAIS: também dentro da mesma tela, quando a sub-rota troca de registro.
|
|
66
|
+
<SectionShell activeId={id} scrollResetKey={`${id}/${recordId}`} …>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Passar `scrollResetKey={id}` com o mesmo valor do `activeId` é o default escrito à mão — não faz nada.
|
|
70
|
+
|
|
71
|
+
## Nav redimensionável
|
|
72
|
+
|
|
73
|
+
`resizable` põe uma divisa arrastável entre a nav e o painel — o mesmo mecanismo do `rail` do AppShell, agora no terceiro nível do chrome. Útil quando os rótulos da nav variam de tamanho (nome de sessão, de arquivo) e o `w-56` fixo ora aperta, ora sobra. Ligado, a largura passa a ser % (`navDefaultSize`/`navMinSize`); desligado, segue a coluna fixa de sempre.
|
|
74
|
+
|
|
75
|
+
```tsx preview
|
|
76
|
+
<div className="h-64 overflow-hidden rounded-md border">
|
|
77
|
+
<SectionShell
|
|
78
|
+
resizable
|
|
79
|
+
navDefaultSize={30}
|
|
80
|
+
groups={[{ items: [{ id: 'acoes', label: 'Ações' }, { id: 'entidades', label: 'Entidades' }] }]}
|
|
81
|
+
activeId="acoes"
|
|
82
|
+
onSelect={() => {}}
|
|
83
|
+
>
|
|
84
|
+
<div className="p-4 text-sm text-muted-foreground">Arraste a divisa à esquerda.</div>
|
|
85
|
+
</SectionShell>
|
|
86
|
+
</div>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Três níveis, quando precisar
|
|
90
|
+
|
|
91
|
+
A maioria das seções é lista simples (`items` no grupo). Quando um grupo tem famílias internas — sub-pasta na doc, por exemplo — use `subgroups`: eles saem **depois** dos `items` soltos, com rótulo mais fraco e mais indentado.
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
groups={[
|
|
95
|
+
{
|
|
96
|
+
label: 'Guias',
|
|
97
|
+
items: [{ id: 'deploy', label: 'Deploy' }], // solto na seção
|
|
98
|
+
subgroups: [{ label: 'CI', items: [{ id: 'build', label: 'Build' }] }],
|
|
99
|
+
},
|
|
100
|
+
]}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Rótulo vazio (`''`) conta como ausente — quem monta a lista a partir de dados não precisa normalizar antes.
|
|
104
|
+
|
|
105
|
+
## Props
|
|
106
|
+
|
|
107
|
+
| Prop | Tipo | O que faz |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `groups` | `SectionNavGroup[]` | A nav: grupos (`label` opcional) → `items` e/ou `subgroups`. |
|
|
110
|
+
| `activeId` | `string` | O item destacado (ganha `aria-current="page"`). Ausente = nenhum. |
|
|
111
|
+
| `onSelect` | `(id: string) => void` | Clique no item. |
|
|
112
|
+
| `navHeader` / `navFooter` | `ReactNode` | Topo e rodapé da nav (título da seção, busca, botão de criar). |
|
|
113
|
+
| `navLabel` | `string` | Rótulo da landmark `<nav>`. Default: `Navegação da seção`. |
|
|
114
|
+
| `scrollResetKey` | `string` | O que remonta o painel. Default: `activeId`. |
|
|
115
|
+
| `resizable` | `boolean` | Nav arrastável pelo usuário (mesma divisa do `rail` do AppShell). Default: `false` — a nav é a coluna fixa `w-56`. |
|
|
116
|
+
| `navDefaultSize` / `navMinSize` | `number` | Largura inicial e mínima da nav em %, quando `resizable`. Default: `18` e `12`. |
|
|
117
|
+
| `className` / `navClassName` | `string` | Classes da raiz e da COLUNA da nav (o wrapper, não a `<nav>` de dentro). Com `resizable`, largura em `navClassName` não vale — quem manda é `navDefaultSize`. |
|
|
118
|
+
|
|
119
|
+
Cada `SectionNavItem` tem `id`, `label` e, opcionais, `icon`, `badge` e `disabled` (visível mas inerte — a tela existe no mapa e ainda não abre).
|
|
120
|
+
|
|
121
|
+
O `id` precisa ser único em **toda** a nav, não só dentro do grupo: o `activeId` casa por id, então id repetido marca dois itens como atuais e o painel não remonta ao alternar entre eles.
|