@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,342 @@
|
|
|
1
|
+
## Básico
|
|
2
|
+
|
|
3
|
+
Um componente para toda escolha em lista. As opções são DADO (`options`), não JSX: `{ value, label }` — o `label` é o que o campo mostra, o que a busca casa e o que o leitor de tela lê. O `id` pareia com o `htmlFor` do Label.
|
|
4
|
+
|
|
5
|
+
```tsx preview col md
|
|
6
|
+
const [role, setRole] = useState('')
|
|
7
|
+
|
|
8
|
+
render(
|
|
9
|
+
<div className="grid gap-2">
|
|
10
|
+
<Label htmlFor="agent-role">Papel do agente</Label>
|
|
11
|
+
<Select
|
|
12
|
+
id="agent-role"
|
|
13
|
+
value={role}
|
|
14
|
+
onChange={setRole}
|
|
15
|
+
placeholder="Selecione o papel"
|
|
16
|
+
options={[
|
|
17
|
+
{ value: 'designer', label: 'Designer' },
|
|
18
|
+
{ value: 'developer', label: 'Developer' },
|
|
19
|
+
{ value: 'reviewer', label: 'Revisor' },
|
|
20
|
+
]}
|
|
21
|
+
/>
|
|
22
|
+
</div>,
|
|
23
|
+
)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Com busca (searchable)
|
|
27
|
+
|
|
28
|
+
`searchable` transforma o próprio campo na busca: digitou, a lista filtra embaixo (fuzzy do cmdk, casando label, hint e value). É o que resolve a **achabilidade** — a régua é essa, não performance: 200 itens sem busca é embaçado, achar é rolar até cansar.
|
|
29
|
+
|
|
30
|
+
```tsx preview col md
|
|
31
|
+
const [issue, setIssue] = useState('')
|
|
32
|
+
|
|
33
|
+
render(
|
|
34
|
+
<div className="grid gap-2">
|
|
35
|
+
<Label htmlFor="issue">Issue da sessão</Label>
|
|
36
|
+
<Select
|
|
37
|
+
searchable
|
|
38
|
+
id="issue"
|
|
39
|
+
value={issue}
|
|
40
|
+
onChange={setIssue}
|
|
41
|
+
placeholder="Buscar issue…"
|
|
42
|
+
options={[
|
|
43
|
+
{ value: '412', label: 'Ajustar microcopy do handoff', hint: 'SOF-412' },
|
|
44
|
+
{ value: '418', label: 'Preview da sessão cai após deploy', hint: 'SOF-418' },
|
|
45
|
+
{ value: '425', label: 'Sincronizar skills com o registry', hint: 'SOF-425' },
|
|
46
|
+
]}
|
|
47
|
+
/>
|
|
48
|
+
</div>,
|
|
49
|
+
)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Múltiplo (chips)
|
|
53
|
+
|
|
54
|
+
`multiple` muda o contrato (`value` e `onChange` viram `string[]`): os escolhidos viram chips removíveis e a lista fica aberta pra seleção em sequência. Com a busca vazia, o topo do dropdown oferece Selecionar tudo / Limpar seleção (só no modo client — com filtro ativo ou `onSearch`, "tudo" seria ambíguo).
|
|
55
|
+
|
|
56
|
+
```tsx preview col md
|
|
57
|
+
const [skills, setSkills] = useState<string[]>(['clean-code'])
|
|
58
|
+
|
|
59
|
+
render(
|
|
60
|
+
<div className="grid gap-2">
|
|
61
|
+
<Label htmlFor="agent-skills">Skills do agente</Label>
|
|
62
|
+
<Select
|
|
63
|
+
multiple
|
|
64
|
+
searchable
|
|
65
|
+
id="agent-skills"
|
|
66
|
+
value={skills}
|
|
67
|
+
onChange={setSkills}
|
|
68
|
+
placeholder="Adicionar skill…"
|
|
69
|
+
options={[
|
|
70
|
+
{ value: 'clean-code', label: 'clean-code' },
|
|
71
|
+
{ value: 'code-style', label: 'code-style' },
|
|
72
|
+
{ value: 'ddd', label: 'ddd' },
|
|
73
|
+
{ value: 'test', label: 'test' },
|
|
74
|
+
]}
|
|
75
|
+
/>
|
|
76
|
+
</div>,
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Nativo (native)
|
|
81
|
+
|
|
82
|
+
`native` renderiza o `<select>` do sistema (só com a seta restilizada — a nativa cola na borda e ignora o tema). Ganha o picker do próprio SO: roda de scroll no iOS, teclado do Android, zero JS. Use em **lista curta e sabida** (2–8 itens que a pessoa já conhece: Sim/Não, prioridade, ambiente). Item rico, busca ou multi não existem aqui — pra isso, o modo default.
|
|
83
|
+
|
|
84
|
+
```tsx preview col md
|
|
85
|
+
const [env, setEnv] = useState('preview')
|
|
86
|
+
|
|
87
|
+
render(
|
|
88
|
+
<div className="grid gap-2">
|
|
89
|
+
<Label htmlFor="env">Ambiente</Label>
|
|
90
|
+
<Select
|
|
91
|
+
native
|
|
92
|
+
id="env"
|
|
93
|
+
value={env}
|
|
94
|
+
onChange={setEnv}
|
|
95
|
+
options={[
|
|
96
|
+
{ value: 'dev', label: 'Dev local' },
|
|
97
|
+
{ value: 'preview', label: 'Preview da sessão' },
|
|
98
|
+
{ value: 'prod', label: 'Produção' },
|
|
99
|
+
]}
|
|
100
|
+
/>
|
|
101
|
+
</div>,
|
|
102
|
+
)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Grupos
|
|
106
|
+
|
|
107
|
+
`group` na opção agrupa a lista — bloco nomeado nos dois modos (`<optgroup>` no nativo, heading no custom). Pra listas com origens distintas (membros × agentes).
|
|
108
|
+
|
|
109
|
+
```tsx preview col md
|
|
110
|
+
const [reviewer, setReviewer] = useState('')
|
|
111
|
+
|
|
112
|
+
render(
|
|
113
|
+
<Select
|
|
114
|
+
value={reviewer}
|
|
115
|
+
onChange={setReviewer}
|
|
116
|
+
placeholder="Escolha o revisor"
|
|
117
|
+
options={[
|
|
118
|
+
{ value: 'joao', label: 'João Campos', group: 'Membros' },
|
|
119
|
+
{ value: 'rafa', label: 'Rafa', group: 'Membros' },
|
|
120
|
+
{ value: 'developer', label: 'Developer', group: 'Agentes' },
|
|
121
|
+
{ value: 'reviewer', label: 'Revisor', group: 'Agentes' },
|
|
122
|
+
]}
|
|
123
|
+
/>,
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Busca server-side
|
|
128
|
+
|
|
129
|
+
Com `onSearch` o filtro do cmdk desliga: o pai busca (debounced, ao abrir e ao digitar) e devolve `options`; `loading` mostra Buscando… enquanto o fetch corre. Implica campo buscável (não precisa repetir `searchable`).
|
|
130
|
+
|
|
131
|
+
```tsx preview col md
|
|
132
|
+
const ASSIGNEES = [
|
|
133
|
+
{ value: 'joao', label: 'João Campos', hint: 'humano' },
|
|
134
|
+
{ value: 'designer', label: 'Designer', hint: 'IA' },
|
|
135
|
+
{ value: 'developer', label: 'Developer', hint: 'IA' },
|
|
136
|
+
{ value: 'reviewer', label: 'Revisor', hint: 'IA' },
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
const [assignee, setAssignee] = useState('')
|
|
140
|
+
const [results, setResults] = useState(ASSIGNEES)
|
|
141
|
+
const [loading, setLoading] = useState(false)
|
|
142
|
+
|
|
143
|
+
// Simula a busca no servidor — no app real, aqui vai o fetch.
|
|
144
|
+
const searchAssignees = (query: string): void => {
|
|
145
|
+
setLoading(true)
|
|
146
|
+
setTimeout(() => {
|
|
147
|
+
setResults(ASSIGNEES.filter((a) => a.label.toLowerCase().includes(query.toLowerCase())))
|
|
148
|
+
setLoading(false)
|
|
149
|
+
}, 400)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
render(
|
|
153
|
+
<Select
|
|
154
|
+
value={assignee}
|
|
155
|
+
onChange={setAssignee}
|
|
156
|
+
options={results}
|
|
157
|
+
onSearch={searchAssignees}
|
|
158
|
+
loading={loading}
|
|
159
|
+
searchPlaceholder="Buscar responsável…"
|
|
160
|
+
/>,
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Ícone dentro do campo
|
|
165
|
+
|
|
166
|
+
`icon` põe o ícone DENTRO do controle, antes do texto (e dos chips, no multi) — identifica o campo numa barra de filtros. Ícone AO LADO do campo é o antipadrão: desalinha na quebra de linha e some no responsivo. Vale nos dois modos.
|
|
167
|
+
|
|
168
|
+
```tsx preview col-start
|
|
169
|
+
const [task, setTask] = useState('')
|
|
170
|
+
|
|
171
|
+
render(
|
|
172
|
+
<Select
|
|
173
|
+
icon={<Ticket />}
|
|
174
|
+
value={task}
|
|
175
|
+
onChange={setTask}
|
|
176
|
+
placeholder="Vincular task"
|
|
177
|
+
options={[
|
|
178
|
+
{ value: 'gra-2', label: 'Nova tela anotações', hint: 'GRA-2' },
|
|
179
|
+
{ value: 'gra-7', label: 'Filtro por filial', hint: 'GRA-7' },
|
|
180
|
+
]}
|
|
181
|
+
/>,
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Densidade (size)
|
|
186
|
+
|
|
187
|
+
O controle segue a altura do design system: default é h-9 (a mesma do Input e do Button) e `size="sm"` dá h-8 pra barra de filtros/toolbar densa.
|
|
188
|
+
|
|
189
|
+
```tsx preview col-start
|
|
190
|
+
const [a, setA] = useState('gra-2')
|
|
191
|
+
|
|
192
|
+
render(
|
|
193
|
+
<div className="flex items-center gap-2">
|
|
194
|
+
<Select
|
|
195
|
+
size="sm"
|
|
196
|
+
icon={<Ticket />}
|
|
197
|
+
value={a}
|
|
198
|
+
onChange={setA}
|
|
199
|
+
options={[{ value: 'gra-2', label: 'Nova tela anotações', hint: 'GRA-2' }]}
|
|
200
|
+
/>
|
|
201
|
+
<Button size="sm" variant="outline">Filtrar</Button>
|
|
202
|
+
</div>,
|
|
203
|
+
)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Limpar (clearable)
|
|
207
|
+
|
|
208
|
+
`clearable` põe o X no controle quando há seleção — limpa num clique (single volta a vazio, multi a `[]`), sem abrir a lista. Bom pra filtro, onde "sem valor" é um estado de uso frequente; os filtros do ActionList já vêm assim.
|
|
209
|
+
|
|
210
|
+
```tsx preview col md
|
|
211
|
+
function Demo() {
|
|
212
|
+
const [values, setValues] = useState(['developer', 'reviewer'])
|
|
213
|
+
return (
|
|
214
|
+
<Select
|
|
215
|
+
multiple
|
|
216
|
+
clearable
|
|
217
|
+
value={values}
|
|
218
|
+
onChange={setValues}
|
|
219
|
+
options={[
|
|
220
|
+
{ value: 'developer', label: 'Developer' },
|
|
221
|
+
{ value: 'reviewer', label: 'Reviewer' },
|
|
222
|
+
{ value: 'designer', label: 'Designer' },
|
|
223
|
+
]}
|
|
224
|
+
placeholder="Papéis…"
|
|
225
|
+
className="w-72"
|
|
226
|
+
/>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
render(<Demo />)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Item rico (content)
|
|
233
|
+
|
|
234
|
+
`label` é sempre string (é o que o campo mostra e a busca casa); `content` é o render RICO do item na lista — ícone, avatar, duas linhas. Assim o item pode ser elaborado sem estragar busca nem a11y.
|
|
235
|
+
|
|
236
|
+
```tsx preview col md
|
|
237
|
+
const [agent, setAgent] = useState('developer')
|
|
238
|
+
|
|
239
|
+
render(
|
|
240
|
+
<Select
|
|
241
|
+
value={agent}
|
|
242
|
+
onChange={setAgent}
|
|
243
|
+
className="w-72"
|
|
244
|
+
options={[
|
|
245
|
+
{
|
|
246
|
+
value: 'developer',
|
|
247
|
+
label: 'Developer',
|
|
248
|
+
content: (
|
|
249
|
+
<span className="flex items-center gap-2">
|
|
250
|
+
<Bot className="size-4 text-muted-foreground" />
|
|
251
|
+
<span>
|
|
252
|
+
Developer <span className="text-muted-foreground">· implementa</span>
|
|
253
|
+
</span>
|
|
254
|
+
</span>
|
|
255
|
+
),
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
value: 'reviewer',
|
|
259
|
+
label: 'Revisor',
|
|
260
|
+
content: (
|
|
261
|
+
<span className="flex items-center gap-2">
|
|
262
|
+
<ShieldCheck className="size-4 text-muted-foreground" />
|
|
263
|
+
<span>
|
|
264
|
+
Revisor <span className="text-muted-foreground">· contesta</span>
|
|
265
|
+
</span>
|
|
266
|
+
</span>
|
|
267
|
+
),
|
|
268
|
+
},
|
|
269
|
+
]}
|
|
270
|
+
/>,
|
|
271
|
+
)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Barra do composer (variant ghost)
|
|
275
|
+
|
|
276
|
+
`variant="ghost"` tira moldura e padding: o seletor discreto que vive na barra de ações do Composer/Chat (app, task, agente), à esquerda do enviar. `triggerLabel` na opção encurta o gatilho quando o rótulo da lista é longo (o título inteiro fica só na lista).
|
|
277
|
+
|
|
278
|
+
```tsx preview col-start
|
|
279
|
+
const [task, setTask] = useState('GB-42')
|
|
280
|
+
|
|
281
|
+
render(
|
|
282
|
+
<Select
|
|
283
|
+
variant="ghost"
|
|
284
|
+
value={task}
|
|
285
|
+
onChange={setTask}
|
|
286
|
+
options={[
|
|
287
|
+
{ value: '', label: 'Sem task' },
|
|
288
|
+
{ value: 'GB-42', label: 'Trocar o FAB por botão no header', hint: 'GB-42', triggerLabel: 'GB-42' },
|
|
289
|
+
{ value: 'GB-51', label: 'Anexo no composer de criação', hint: 'GB-51', triggerLabel: 'GB-51' },
|
|
290
|
+
]}
|
|
291
|
+
/>,
|
|
292
|
+
)
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Ação no fim do campo (trailing)
|
|
296
|
+
|
|
297
|
+
`trailing` põe uma ação custom DENTRO do controle, no fim (antes do chevron) — um botão que age sobre o valor escolhido, sem virar um irmão solto ao lado do campo. O clique no trailing **não** abre a lista (o slot para a propagação).
|
|
298
|
+
|
|
299
|
+
```tsx preview col-start
|
|
300
|
+
const [task, setTask] = useState('gra-2')
|
|
301
|
+
|
|
302
|
+
render(
|
|
303
|
+
<Select
|
|
304
|
+
icon={<Ticket />}
|
|
305
|
+
value={task}
|
|
306
|
+
onChange={setTask}
|
|
307
|
+
className="w-72"
|
|
308
|
+
options={[
|
|
309
|
+
{ value: 'gra-2', label: 'Nova tela anotações', hint: 'GRA-2' },
|
|
310
|
+
{ value: 'gra-7', label: 'Filtro por filial', hint: 'GRA-7' },
|
|
311
|
+
]}
|
|
312
|
+
trailing={
|
|
313
|
+
<Button variant="ghost" size="icon-xs">
|
|
314
|
+
<FileText />
|
|
315
|
+
</Button>
|
|
316
|
+
}
|
|
317
|
+
/>,
|
|
318
|
+
)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Props
|
|
322
|
+
|
|
323
|
+
| Prop | Tipo | Default | Descrição |
|
|
324
|
+
|---|---|---|---|
|
|
325
|
+
| `options` | `SelectOption[]` | | As opções: `{ value, label, hint?, content?, triggerLabel?, group?, disabled? }`. |
|
|
326
|
+
| `value` | `string \| string[]` | | O selecionado: string no single, string[] no multiple. |
|
|
327
|
+
| `onChange` | `(value: string) => void \| (value: string[]) => void` | | Chamado ao escolher (e ao remover chip, no multiple) — a assinatura segue o modo. |
|
|
328
|
+
| `native` | `boolean` | `false` | Renderiza o `<select>` do sistema. Exclui busca, multi e ghost (o browser é quem desenha a lista). |
|
|
329
|
+
| `searchable` | `boolean` | `false` | O campo vira busca: filtra a lista enquanto digita. |
|
|
330
|
+
| `multiple` | `boolean` | `false` | Chips removíveis, lista que permanece aberta e Selecionar tudo. |
|
|
331
|
+
| `variant` | `'default' \| 'ghost'` | `'default'` | `ghost` = sem moldura, pra barra do composer. |
|
|
332
|
+
| `placeholder` | `string` | `'Selecione…'` | Texto do campo vazio. |
|
|
333
|
+
| `searchPlaceholder` | `string` | | Placeholder enquanto busca; cai pro `placeholder` se ausente. |
|
|
334
|
+
| `emptyText` | `string` | `'Nada encontrado.'` | Mensagem quando a busca não acha nada. |
|
|
335
|
+
| `onSearch` | `(query: string) => void` | | Busca server-side (debounced, ao abrir e ao digitar): desliga o filtro do cmdk — o pai atualiza `options`. |
|
|
336
|
+
| `loading` | `boolean` | | Mostra Buscando… enquanto o fetch corre (use com `onSearch`). |
|
|
337
|
+
| `clearable` | `boolean` | `false` | X no controle quando há seleção — limpa num clique. |
|
|
338
|
+
| `icon` | `React.ReactNode` | | Ícone leading DENTRO do controle (decorativo) — herda `size-4` e o tom muted. |
|
|
339
|
+
| `trailing` | `React.ReactNode` | | Ação custom no FIM do controle (antes do chevron) — o clique não abre a lista. |
|
|
340
|
+
| `size` | `'default' \| 'sm'` | `'default'` | Altura: default (h-9, a do Input e do Button) ou sm (h-8) pra toolbar densa. |
|
|
341
|
+
| `disabled` | `boolean` | `false` | Esmaece e trava o controle. |
|
|
342
|
+
| `id` | `string` | | Vai pro campo — pra parear com o `htmlFor` do Label. |
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
## Horizontal
|
|
2
|
+
|
|
3
|
+
Divide blocos empilhados — ocupa a largura toda do contêiner.
|
|
4
|
+
|
|
5
|
+
```tsx preview col
|
|
6
|
+
<div>
|
|
7
|
+
<h4 className="text-sm font-medium">Empresa X</h4>
|
|
8
|
+
<p className="text-sm text-muted-foreground">Workspace do cliente Empresa X.</p>
|
|
9
|
+
</div>
|
|
10
|
+
<Separator />
|
|
11
|
+
<p className="text-sm text-muted-foreground">Última sessão há 2 horas.</p>
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Vertical
|
|
15
|
+
|
|
16
|
+
Entre itens de uma linha. O vertical herda a altura do contêiner — dê altura à linha (ex.: h-4 no flex).
|
|
17
|
+
|
|
18
|
+
```tsx preview
|
|
19
|
+
<div className="flex h-4 items-center gap-3 text-sm">
|
|
20
|
+
<span>2 repositórios</span>
|
|
21
|
+
<Separator orientation="vertical" />
|
|
22
|
+
<span>3 agentes</span>
|
|
23
|
+
<Separator orientation="vertical" />
|
|
24
|
+
<span>6 skills</span>
|
|
25
|
+
</div>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Props
|
|
29
|
+
|
|
30
|
+
| Prop | Tipo | Default | Descrição |
|
|
31
|
+
|---|---|---|---|
|
|
32
|
+
| `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Direção da linha. Vertical precisa de altura vinda do contêiner. |
|
|
33
|
+
| `decorative` | `boolean` | `true` | Decorativa some da árvore de acessibilidade. Use false quando a divisão é semântica. |
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
## Sidebar
|
|
2
|
+
|
|
3
|
+
`Sidebar` é uma coluna de chrome e navegação. Ela não escolhe a posição: entra em qualquer `Pane` de um `Split`. A mesma forma serve para a sidebar global do app e a navegação interna de Configurações.
|
|
4
|
+
|
|
5
|
+
```tsx preview
|
|
6
|
+
render(
|
|
7
|
+
<div className="h-72 overflow-hidden rounded-lg border border-border">
|
|
8
|
+
<Split resizable>
|
|
9
|
+
<Pane initialSize={32} minSize={20} inset="none">
|
|
10
|
+
<Sidebar divider={false} className="w-full">
|
|
11
|
+
<PaneHeader className="px-4 py-3 text-sm font-semibold">Projeto</PaneHeader>
|
|
12
|
+
<PaneContent>
|
|
13
|
+
<SidebarNav
|
|
14
|
+
groups={[{ label: 'Design', items: [{ id: 'preview', label: 'Preview' }, { id: 'code', label: 'Código' }] }]}
|
|
15
|
+
activeId="preview"
|
|
16
|
+
onSelect={() => {}}
|
|
17
|
+
/>
|
|
18
|
+
</PaneContent>
|
|
19
|
+
<PaneFooter><span className="text-xs text-muted-foreground">Configurações</span></PaneFooter>
|
|
20
|
+
</Sidebar>
|
|
21
|
+
</Pane>
|
|
22
|
+
<Pane grow inset="lg"><p className="text-sm text-muted-foreground">Conteúdo da tela.</p></Pane>
|
|
23
|
+
</Split>
|
|
24
|
+
</div>,
|
|
25
|
+
)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Colapso
|
|
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.
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
<Sidebar collapsed={collapsed}>
|
|
34
|
+
<PaneHeader><MySidebarHeader onToggle={() => setCollapsed(!collapsed)} /></PaneHeader>
|
|
35
|
+
<PaneContent><SidebarNav groups={groups} activeId={active} onSelect={go} /></PaneContent>
|
|
36
|
+
<PaneFooter><UserMenu /></PaneFooter>
|
|
37
|
+
</Sidebar>
|
|
38
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
## Linha de lista com avatar
|
|
2
|
+
|
|
3
|
+
O skeleton imita a forma do conteúdo: círculo pro avatar (rounded-full), barras com a largura aproximada do texto.
|
|
4
|
+
|
|
5
|
+
```tsx preview col
|
|
6
|
+
<div className="flex items-center gap-3">
|
|
7
|
+
<Skeleton className="size-10 rounded-full" />
|
|
8
|
+
<div className="space-y-2">
|
|
9
|
+
<Skeleton className="h-4 w-48" />
|
|
10
|
+
<Skeleton className="h-3 w-32" />
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Card em carregamento
|
|
16
|
+
|
|
17
|
+
O esqueleto reproduz o layout final do card — título, descrição, conteúdo e ações — pra tela não pular quando os dados chegarem.
|
|
18
|
+
|
|
19
|
+
```tsx preview col
|
|
20
|
+
<Card>
|
|
21
|
+
<CardHeader>
|
|
22
|
+
<Skeleton className="h-5 w-40" />
|
|
23
|
+
<Skeleton className="h-4 w-56" />
|
|
24
|
+
</CardHeader>
|
|
25
|
+
<CardContent className="space-y-2">
|
|
26
|
+
<Skeleton className="h-4 w-full" />
|
|
27
|
+
<Skeleton className="h-4 w-3/4" />
|
|
28
|
+
</CardContent>
|
|
29
|
+
<CardFooter className="gap-2">
|
|
30
|
+
<Skeleton className="h-8 w-28" />
|
|
31
|
+
<Skeleton className="h-8 w-24" />
|
|
32
|
+
</CardFooter>
|
|
33
|
+
</Card>
|
|
34
|
+
```
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
## Básico
|
|
2
|
+
|
|
3
|
+
defaultValue é um array — [n] pra um thumb. min, max e step delimitam a faixa; aqui de 0 a 100 em passos de 5.
|
|
4
|
+
|
|
5
|
+
```tsx preview col
|
|
6
|
+
<Slider defaultValue={[40]} min={0} max={100} step={5} />
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Controlado
|
|
10
|
+
|
|
11
|
+
onValueChange recebe o array atualizado — leia value[0] pra um thumb. Bom pra mostrar o valor escolhido ao lado do rótulo.
|
|
12
|
+
|
|
13
|
+
```tsx preview col
|
|
14
|
+
const [parallelism, setParallelism] = useState([4])
|
|
15
|
+
|
|
16
|
+
render(
|
|
17
|
+
<div className="grid w-full gap-3">
|
|
18
|
+
<div className="flex items-center justify-between">
|
|
19
|
+
<Label>Sessões em paralelo</Label>
|
|
20
|
+
<span className="text-sm text-muted-foreground">{parallelism[0]}</span>
|
|
21
|
+
</div>
|
|
22
|
+
<Slider value={parallelism} onValueChange={setParallelism} min={1} max={8} step={1} />
|
|
23
|
+
</div>,
|
|
24
|
+
)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Intervalo
|
|
28
|
+
|
|
29
|
+
value com dois números rende dois thumbs e a faixa fica entre eles — o jeito de filtrar por um intervalo, como o custo estimado de uma task.
|
|
30
|
+
|
|
31
|
+
```tsx preview col
|
|
32
|
+
const [budget, setBudget] = useState([20, 60])
|
|
33
|
+
|
|
34
|
+
render(
|
|
35
|
+
<div className="grid w-full gap-3">
|
|
36
|
+
<div className="flex items-center justify-between">
|
|
37
|
+
<Label>Custo estimado (US$)</Label>
|
|
38
|
+
<span className="text-sm text-muted-foreground">{budget[0]} – {budget[1]}</span>
|
|
39
|
+
</div>
|
|
40
|
+
<Slider value={budget} onValueChange={setBudget} min={0} max={100} step={5} />
|
|
41
|
+
</div>,
|
|
42
|
+
)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Desabilitado
|
|
46
|
+
|
|
47
|
+
disabled esmaece o trilho e o thumb e bloqueia o arraste — o valor fica congelado no defaultValue.
|
|
48
|
+
|
|
49
|
+
```tsx preview col
|
|
50
|
+
<Slider defaultValue={[70]} min={0} max={100} disabled />
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Props
|
|
54
|
+
|
|
55
|
+
| Prop | Tipo | Default | Descrição |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| `value` | `number[]` | | O valor no modo controlado — [n] pra um thumb, [a, b] pra um intervalo. Pareie com onValueChange. |
|
|
58
|
+
| `onValueChange` | `(value: number[]) => void` | | Chamado a cada arraste, com o array atualizado. |
|
|
59
|
+
| `defaultValue` | `number[]` | | Valor inicial no modo não controlado. |
|
|
60
|
+
| `min` | `number` | `0` | Limite inferior da faixa. |
|
|
61
|
+
| `max` | `number` | `100` | Limite superior da faixa. |
|
|
62
|
+
| `step` | `number` | `1` | Incremento entre valores possíveis. |
|
|
63
|
+
| `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Direção do trilho — vertical exige altura no container. |
|
|
64
|
+
| `disabled` | `boolean` | `false` | Esmaece e bloqueia o arraste. |
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
## Tamanhos
|
|
2
|
+
|
|
3
|
+
Divergência da casa: o shadcn atual só tem size-4 fixo; mantivemos sm/default/lg pra cobrir botão
|
|
4
|
+
(sm) e estados maiores (lg). O aria-label Carregando já vem embutido.
|
|
5
|
+
|
|
6
|
+
```tsx preview
|
|
7
|
+
<Spinner size="sm" />
|
|
8
|
+
<Spinner />
|
|
9
|
+
<Spinner size="lg" />
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## No botão
|
|
13
|
+
|
|
14
|
+
Ação em andamento: disabled + Spinner sm + verbo no gerúndio — substitui o Loader2Icon inline
|
|
15
|
+
repetido.
|
|
16
|
+
|
|
17
|
+
```tsx preview
|
|
18
|
+
<Button disabled><Spinner size="sm" /> Publicando…</Button>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Em carga de conteúdo
|
|
22
|
+
|
|
23
|
+
Espera curta sem forma definida. Quando o conteúdo tem forma conhecida (lista, card), prefira
|
|
24
|
+
Skeleton.
|
|
25
|
+
|
|
26
|
+
```tsx preview
|
|
27
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
28
|
+
<Spinner />
|
|
29
|
+
<span>Carregando sessões…</span>
|
|
30
|
+
</div>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Props
|
|
34
|
+
|
|
35
|
+
| Prop | Tipo | Default | Descrição |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | O tamanho — sm pra dentro de botão, lg pra estados de página. |
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
## Split & Pane
|
|
2
|
+
|
|
3
|
+
`Split` é o primitive espacial da UI. A ordem dos `Pane` define esquerda→direita (ou cima→baixo); o pane não sabe onde está. Sem `resizable`, é flex simples. Com ele, a fronteira vira arrastável sem trocar o modelo do componente.
|
|
4
|
+
|
|
5
|
+
```tsx preview
|
|
6
|
+
render(
|
|
7
|
+
<div className="h-72 overflow-hidden rounded-lg border border-border">
|
|
8
|
+
<Split resizable>
|
|
9
|
+
<Pane initialSize={30} minSize={20} inset="none" className="bg-muted/30">
|
|
10
|
+
<div className="p-4 text-sm">Navegação</div>
|
|
11
|
+
</Pane>
|
|
12
|
+
<Pane grow inset="lg">
|
|
13
|
+
<p className="text-sm text-muted-foreground">Conteúdo. Arraste a divisória.</p>
|
|
14
|
+
</Pane>
|
|
15
|
+
</Split>
|
|
16
|
+
</div>,
|
|
17
|
+
)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Inset
|
|
21
|
+
|
|
22
|
+
O `Pane` traz `inset="md"` por default. Use `none` para chrome, navegação e conteúdo que já possui padding próprio; `sm` e `lg` atendem densidades comuns. `className` sempre pode ajustar o caso específico.
|
|
23
|
+
|
|
24
|
+
## Rail
|
|
25
|
+
|
|
26
|
+
Um rail não é uma prop especial: é só o último pane.
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
<Split resizable>
|
|
30
|
+
<Pane grow inset="none"><Main /></Pane>
|
|
31
|
+
<Pane initialSize={28} minSize={20} inset="none"><Inspector /></Pane>
|
|
32
|
+
</Split>
|
|
33
|
+
```
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Armazenamento
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Armazenamento
|
|
6
|
+
|
|
7
|
+
> **Experimental.** Superfície mínima, sem consumidor interno ainda — cresce por
|
|
8
|
+
> reincidência de caso real, não por especulação.
|
|
9
|
+
|
|
10
|
+
Upload, download, remoção e URL de acesso — o que backoffice precisa de arquivos
|
|
11
|
+
(anexo de nota, foto de produto, contrato). O contrato é um adapter do core
|
|
12
|
+
(`StorageAdapter`): a key é um **caminho lógico** (`clientes/123/contrato.pdf`) e o
|
|
13
|
+
driver resolve onde ela mora.
|
|
14
|
+
|
|
15
|
+
## O contrato
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
interface StorageAdapter {
|
|
19
|
+
put(key, data: Uint8Array, opts?: { contentType?: string }): Promise<void>
|
|
20
|
+
get(key): Promise<{ data: Uint8Array; contentType?: string } | null>
|
|
21
|
+
delete(key): Promise<void> // idempotente
|
|
22
|
+
url(key, opts?: { expiresInSeconds?: number }): Promise<string>
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Keys passam pela mesma régua em todo driver (`normalizeKey`): relativas, sem `..`,
|
|
27
|
+
sem `\` — traversal é barrado no contrato, não em cada driver.
|
|
28
|
+
|
|
29
|
+
## Drivers
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { fsStorage } from '@softize/opus/storage/fs'
|
|
33
|
+
import { s3Storage } from '@softize/opus/storage/s3'
|
|
34
|
+
import { S3Client } from '@aws-sdk/client-s3'
|
|
35
|
+
|
|
36
|
+
// Dev / single-host: arquivos sob um root local; url() exige baseUrl (o app serve o root).
|
|
37
|
+
const dev = fsStorage({ root: './var/arquivos', baseUrl: 'http://localhost:5173/arquivos' })
|
|
38
|
+
|
|
39
|
+
// Produção: S3 e compatíveis (MinIO, R2…). O client é seu — região/endpoint/credencial.
|
|
40
|
+
const prod = s3Storage({
|
|
41
|
+
client: new S3Client({ region: 'us-east-1' }),
|
|
42
|
+
bucket: 'meu-app',
|
|
43
|
+
prefix: 'uploads',
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Os peers do S3 são **opcionais** (`@aws-sdk/client-s3`; `s3-request-presigner` pro
|
|
48
|
+
`url()`): quem não usa o driver não os instala — importados sob demanda.
|
|
49
|
+
|
|
50
|
+
## No runtime
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const runtime = createRuntime({
|
|
54
|
+
// …server/data/auth…
|
|
55
|
+
storage: prod,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// No handler: ctx.storage (null quando não configurado).
|
|
59
|
+
handler: async (ctx, input) => {
|
|
60
|
+
await ctx.storage!.put(`notas/${input.id}.pdf`, bytes, { contentType: 'application/pdf' })
|
|
61
|
+
return { url: await ctx.storage!.url(`notas/${input.id}.pdf`, { expiresInSeconds: 300 }) }
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Limites (por enquanto)
|
|
66
|
+
|
|
67
|
+
Bytes em memória (`Uint8Array`), sem streams — arquivo gigante ainda não é o caso;
|
|
68
|
+
sem `list`, sem metadata rica. Cada um entra quando um projeto real cobrar — é a
|
|
69
|
+
régua do Opus: comportamento vira peça na reincidência.
|