@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,479 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <Select /> — O seletor da casa: UM componente pra escolher valor(es) de uma lista.
|
|
3
|
+
*
|
|
4
|
+
* Antes eram QUATRO (NativeSelect, Select-Radix, Combobox, ComposerSelect) com TRÊS
|
|
5
|
+
* motores diferentes — e a escolha entre eles era uma decisão que ninguém acertava
|
|
6
|
+
* (o episódio real: uma tela de filtros saiu com `<select>` cru porque "qual usar?"
|
|
7
|
+
* custava mais que copiar classes). Aqui a decisão virou PROP:
|
|
8
|
+
*
|
|
9
|
+
* <Select native … /> → o <select> do sistema (picker do SO no
|
|
10
|
+
* celular, form nativo, sem portal)
|
|
11
|
+
* <Select … /> → custom (cmdk + Popover)
|
|
12
|
+
* <Select searchable … /> → com busca (o antigo Combobox)
|
|
13
|
+
* <Select multiple … /> → chips removíveis
|
|
14
|
+
* <Select variant="ghost" … /> → discreto, pra barra de composer
|
|
15
|
+
*
|
|
16
|
+
* O TIPO impede o componente de mentir: `native` é uma união discriminada, então
|
|
17
|
+
* `searchable`/`multiple`/`variant` nem compilam junto dele — o menu é do sistema
|
|
18
|
+
* operacional e não aceita busca, chip nem estilo. Erro de compilação > bug silencioso.
|
|
19
|
+
*
|
|
20
|
+
* Régua de escolha (achabilidade, não performance): lista curta e conhecida → `native`;
|
|
21
|
+
* lista longa ou que cresce → `searchable`; item que precisa de JSX → `content`.
|
|
22
|
+
*/
|
|
23
|
+
import * as React from 'react'
|
|
24
|
+
import { Check, ChevronDownIcon, X } from 'lucide-react'
|
|
25
|
+
import { Command as CommandPrimitive } from 'cmdk'
|
|
26
|
+
|
|
27
|
+
import { cn } from '../../lib/cn.ts'
|
|
28
|
+
import { CommandEmpty, CommandGroup, CommandItem, CommandList } from './command.tsx'
|
|
29
|
+
import { Popover, PopoverAnchor, PopoverContent } from './popover.tsx'
|
|
30
|
+
|
|
31
|
+
export interface SelectOption {
|
|
32
|
+
value: string
|
|
33
|
+
/** Texto da opção — é o que a BUSCA casa, o que o gatilho mostra e o que o leitor de
|
|
34
|
+
* tela anuncia. Sempre string (JSX rico vai em `content`). */
|
|
35
|
+
label: string
|
|
36
|
+
/** Render rico do item NO MENU (ícone, avatar, duas linhas). Default: o `label`.
|
|
37
|
+
* Só no modo custom — o menu nativo do SO aceita texto puro. */
|
|
38
|
+
content?: React.ReactNode
|
|
39
|
+
/** Detalhe discreto à direita do item (ex.: a chave da task). */
|
|
40
|
+
hint?: string
|
|
41
|
+
/** O que o GATILHO mostra quando esta é a escolhida (default: `label`) — pra quando o
|
|
42
|
+
* nome completo não cabe na barra e só a chave curta serve. */
|
|
43
|
+
triggerLabel?: React.ReactNode
|
|
44
|
+
/** Agrupa as opções sob um rótulo (o `<optgroup>` do nativo, o grupo do menu custom). */
|
|
45
|
+
group?: string
|
|
46
|
+
disabled?: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface SelectBaseProps {
|
|
50
|
+
options: SelectOption[]
|
|
51
|
+
/** Texto do campo vazio. */
|
|
52
|
+
placeholder?: string
|
|
53
|
+
disabled?: boolean
|
|
54
|
+
className?: string
|
|
55
|
+
id?: string
|
|
56
|
+
/** Ícone LEADING dentro do campo — identifica o filtro sem jogar o ícone ao lado. */
|
|
57
|
+
icon?: React.ReactNode
|
|
58
|
+
/** Altura: `default` (h-9, a do Input/Button) ou `sm` (h-8) pra toolbar densa. */
|
|
59
|
+
size?: 'sm' | 'default'
|
|
60
|
+
'aria-label'?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Só o que o menu do SISTEMA suporta — nada de busca, chip ou variante visual. */
|
|
64
|
+
interface SelectNativeProps extends SelectBaseProps {
|
|
65
|
+
native: true
|
|
66
|
+
value: string
|
|
67
|
+
onChange: (value: string) => void
|
|
68
|
+
searchable?: never
|
|
69
|
+
multiple?: never
|
|
70
|
+
variant?: never
|
|
71
|
+
onSearch?: never
|
|
72
|
+
loading?: never
|
|
73
|
+
clearable?: never
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface SelectCustomBase extends SelectBaseProps {
|
|
77
|
+
native?: false
|
|
78
|
+
/** Liga a busca no próprio campo (lista longa/que cresce). */
|
|
79
|
+
searchable?: boolean
|
|
80
|
+
/** Busca SERVER-SIDE (debounced): quando definida, o filtro local desliga e o pai
|
|
81
|
+
* atualiza `options`. Implica `searchable`. */
|
|
82
|
+
onSearch?: (query: string) => void
|
|
83
|
+
/** "Buscando…" enquanto o fetch corre (use com `onSearch`). */
|
|
84
|
+
loading?: boolean
|
|
85
|
+
/** X no campo quando há seleção — limpa num clique. Bom em filtro. */
|
|
86
|
+
clearable?: boolean
|
|
87
|
+
/** `ghost`: sem borda nem fundo, discreto — o seletor da barra do composer. */
|
|
88
|
+
variant?: 'default' | 'ghost'
|
|
89
|
+
/** Placeholder enquanto se digita a busca (cai pro `placeholder`). */
|
|
90
|
+
searchPlaceholder?: string
|
|
91
|
+
emptyText?: string
|
|
92
|
+
/** Ação custom no FIM do campo, dentro do controle (antes do chevron) — ex.: um botão que
|
|
93
|
+
* age sobre o valor escolhido. O clique NÃO abre a lista (o slot para a propagação). */
|
|
94
|
+
trailing?: React.ReactNode
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type SelectProps =
|
|
98
|
+
| SelectNativeProps
|
|
99
|
+
| (SelectCustomBase & { multiple?: false; value: string; onChange: (value: string) => void })
|
|
100
|
+
| (SelectCustomBase & { multiple: true; value: string[]; onChange: (value: string[]) => void })
|
|
101
|
+
|
|
102
|
+
/** Ordena preservando a ordem de aparição dos grupos (sem grupo primeiro). */
|
|
103
|
+
function byGroup(options: SelectOption[]): Array<[string | undefined, SelectOption[]]> {
|
|
104
|
+
const out: Array<[string | undefined, SelectOption[]]> = []
|
|
105
|
+
for (const o of options) {
|
|
106
|
+
const found = out.find(([g]) => g === o.group)
|
|
107
|
+
if (found) found[1].push(o)
|
|
108
|
+
else out.push([o.group, [o]])
|
|
109
|
+
}
|
|
110
|
+
return out
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function Select(props: SelectProps): React.ReactElement {
|
|
114
|
+
return props.native === true ? <NativeSelect {...props} /> : <CustomSelect {...props} />
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Nativo: o <select> do sistema ───────────────────────────────────────────────────
|
|
118
|
+
function NativeSelect({
|
|
119
|
+
options,
|
|
120
|
+
placeholder,
|
|
121
|
+
disabled,
|
|
122
|
+
className,
|
|
123
|
+
id,
|
|
124
|
+
icon,
|
|
125
|
+
size = 'default',
|
|
126
|
+
value,
|
|
127
|
+
onChange,
|
|
128
|
+
...rest
|
|
129
|
+
}: SelectNativeProps): React.ReactElement {
|
|
130
|
+
const groups = byGroup(options)
|
|
131
|
+
return (
|
|
132
|
+
<div className="group/select relative w-full min-w-0 has-[select:disabled]:opacity-50" data-slot="select-wrapper">
|
|
133
|
+
{icon ? (
|
|
134
|
+
<span
|
|
135
|
+
className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground select-none [&_svg]:size-4"
|
|
136
|
+
aria-hidden="true"
|
|
137
|
+
data-slot="select-leading"
|
|
138
|
+
>
|
|
139
|
+
{icon}
|
|
140
|
+
</span>
|
|
141
|
+
) : null}
|
|
142
|
+
<select
|
|
143
|
+
data-slot="select"
|
|
144
|
+
data-size={size}
|
|
145
|
+
id={id}
|
|
146
|
+
disabled={disabled}
|
|
147
|
+
value={value}
|
|
148
|
+
aria-label={rest['aria-label']}
|
|
149
|
+
onChange={(e) => onChange(e.target.value)}
|
|
150
|
+
className={cn(
|
|
151
|
+
"h-9 w-full min-w-0 appearance-none rounded-md border border-input bg-transparent px-3 py-2 pr-9 text-sm transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground disabled:pointer-events-none disabled:cursor-not-allowed data-[size=sm]:h-8 data-[size=sm]:py-1 dark:bg-input/30 dark:hover:bg-input/50",
|
|
152
|
+
'focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50',
|
|
153
|
+
'aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40',
|
|
154
|
+
icon ? 'pl-9' : '',
|
|
155
|
+
className,
|
|
156
|
+
)}
|
|
157
|
+
>
|
|
158
|
+
{placeholder !== undefined && (
|
|
159
|
+
<option value="" disabled>
|
|
160
|
+
{placeholder}
|
|
161
|
+
</option>
|
|
162
|
+
)}
|
|
163
|
+
{groups.map(([group, items]) =>
|
|
164
|
+
group === undefined ? (
|
|
165
|
+
items.map((o) => (
|
|
166
|
+
<option key={o.value} value={o.value} disabled={o.disabled} className="bg-[Canvas] text-[CanvasText]">
|
|
167
|
+
{o.label}
|
|
168
|
+
</option>
|
|
169
|
+
))
|
|
170
|
+
) : (
|
|
171
|
+
<optgroup key={group} label={group} className="bg-[Canvas] text-[CanvasText]">
|
|
172
|
+
{items.map((o) => (
|
|
173
|
+
<option key={o.value} value={o.value} disabled={o.disabled} className="bg-[Canvas] text-[CanvasText]">
|
|
174
|
+
{o.label}
|
|
175
|
+
</option>
|
|
176
|
+
))}
|
|
177
|
+
</optgroup>
|
|
178
|
+
),
|
|
179
|
+
)}
|
|
180
|
+
</select>
|
|
181
|
+
<ChevronDownIcon
|
|
182
|
+
className="pointer-events-none absolute top-1/2 right-3.5 size-4 -translate-y-1/2 text-muted-foreground opacity-50 select-none"
|
|
183
|
+
aria-hidden="true"
|
|
184
|
+
data-slot="select-icon"
|
|
185
|
+
/>
|
|
186
|
+
</div>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Custom: cmdk + Popover (busca, multi, item rico, ghost) ─────────────────────────
|
|
191
|
+
function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.ReactElement {
|
|
192
|
+
const {
|
|
193
|
+
options,
|
|
194
|
+
placeholder = 'Selecione…',
|
|
195
|
+
searchPlaceholder,
|
|
196
|
+
emptyText = 'Nada encontrado.',
|
|
197
|
+
disabled,
|
|
198
|
+
className,
|
|
199
|
+
id,
|
|
200
|
+
icon,
|
|
201
|
+
size = 'default',
|
|
202
|
+
searchable = false,
|
|
203
|
+
onSearch,
|
|
204
|
+
loading,
|
|
205
|
+
clearable,
|
|
206
|
+
trailing,
|
|
207
|
+
variant = 'default',
|
|
208
|
+
} = props
|
|
209
|
+
// Busca server-side implica campo buscável (o pai filtra).
|
|
210
|
+
const canSearch = searchable || onSearch !== undefined
|
|
211
|
+
const selectedValues = props.multiple ? props.value : props.value ? [props.value] : []
|
|
212
|
+
const singleValue = props.multiple ? undefined : props.value
|
|
213
|
+
|
|
214
|
+
const [open, setOpen] = React.useState(false)
|
|
215
|
+
const [query, setQuery] = React.useState('')
|
|
216
|
+
const rootRef = React.useRef<HTMLDivElement>(null)
|
|
217
|
+
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
218
|
+
|
|
219
|
+
const selectedOption = options.find((o) => o.value === singleValue)
|
|
220
|
+
// Single: fechado, o campo mostra o rótulo escolhido (sincroniza com value/options).
|
|
221
|
+
React.useEffect(() => {
|
|
222
|
+
if (singleValue === undefined || open) return
|
|
223
|
+
setQuery(selectedOption?.label ?? '')
|
|
224
|
+
}, [singleValue, selectedOption, open])
|
|
225
|
+
|
|
226
|
+
// O cmdk carimba o id DELE no input (a11y interna) e descarta o nosso — reaplicamos por
|
|
227
|
+
// ref, senão o `htmlFor` do Label não acha campo nenhum (clicar no rótulo não focaria).
|
|
228
|
+
React.useEffect(() => {
|
|
229
|
+
if (id !== undefined && inputRef.current !== null) inputRef.current.id = id
|
|
230
|
+
}, [id])
|
|
231
|
+
|
|
232
|
+
const onSearchRef = React.useRef(onSearch)
|
|
233
|
+
onSearchRef.current = onSearch
|
|
234
|
+
React.useEffect(() => {
|
|
235
|
+
if (onSearchRef.current === undefined || !open) return
|
|
236
|
+
const t = setTimeout(() => onSearchRef.current?.(query.trim()), 200)
|
|
237
|
+
return () => clearTimeout(t)
|
|
238
|
+
}, [query, open])
|
|
239
|
+
|
|
240
|
+
const onSelectOption = (o: SelectOption): void => {
|
|
241
|
+
if (o.disabled === true) return
|
|
242
|
+
if (props.multiple) {
|
|
243
|
+
props.onChange(props.value.includes(o.value) ? props.value.filter((v) => v !== o.value) : [...props.value, o.value])
|
|
244
|
+
setQuery('')
|
|
245
|
+
inputRef.current?.focus()
|
|
246
|
+
} else {
|
|
247
|
+
props.onChange(o.value)
|
|
248
|
+
setQuery(o.label)
|
|
249
|
+
setOpen(false)
|
|
250
|
+
inputRef.current?.blur()
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const removeValue = (v: string): void => {
|
|
254
|
+
if (props.multiple) props.onChange(props.value.filter((x) => x !== v))
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const allSelected = options.length > 0 && options.every((o) => selectedValues.includes(o.value))
|
|
258
|
+
const showToggleAll = props.multiple === true && options.length > 1 && onSearch === undefined && query.trim() === ''
|
|
259
|
+
const toggleAll = (): void => {
|
|
260
|
+
if (!props.multiple) return
|
|
261
|
+
props.onChange(allSelected ? [] : options.map((o) => o.value))
|
|
262
|
+
inputRef.current?.focus()
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const ghost = variant === 'ghost'
|
|
266
|
+
// Não-buscável: o campo é só gatilho — readOnly (sem caret, sem digitar), o clique abre.
|
|
267
|
+
// Um render só pros dois modos; o que muda é poder digitar.
|
|
268
|
+
const triggerText = !props.multiple && !open && selectedOption?.triggerLabel !== undefined ? undefined : query
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
272
|
+
<div ref={rootRef} data-slot="select" className={cn(ghost ? 'min-w-0' : 'min-w-40', className)}>
|
|
273
|
+
<CommandPrimitive data-slot="select-command" shouldFilter={canSearch && onSearch === undefined} className="overflow-visible">
|
|
274
|
+
<PopoverAnchor asChild>
|
|
275
|
+
<div
|
|
276
|
+
data-slot="select-control"
|
|
277
|
+
data-size={size}
|
|
278
|
+
data-variant={variant}
|
|
279
|
+
onClick={() => {
|
|
280
|
+
if (disabled !== true) {
|
|
281
|
+
setOpen(true)
|
|
282
|
+
inputRef.current?.focus()
|
|
283
|
+
}
|
|
284
|
+
}}
|
|
285
|
+
className={cn(
|
|
286
|
+
'flex items-center gap-1.5 rounded-md outline-none transition-[color,box-shadow]',
|
|
287
|
+
ghost
|
|
288
|
+
? 'min-h-7 px-1.5 py-1 text-muted-foreground hover:bg-muted hover:text-foreground focus-within:ring-2 focus-within:ring-ring/50'
|
|
289
|
+
: "min-h-9 flex-wrap border border-input bg-transparent px-3 py-1 focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[size=sm]:min-h-8 data-[size=sm]:py-0.5 dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
|
|
290
|
+
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
291
|
+
canSearch ? '' : 'cursor-pointer',
|
|
292
|
+
disabled === true && 'cursor-not-allowed opacity-50',
|
|
293
|
+
)}
|
|
294
|
+
>
|
|
295
|
+
{icon ? (
|
|
296
|
+
<span data-slot="select-leading" className="text-muted-foreground">
|
|
297
|
+
{icon}
|
|
298
|
+
</span>
|
|
299
|
+
) : null}
|
|
300
|
+
{props.multiple &&
|
|
301
|
+
selectedValues.map((v) => {
|
|
302
|
+
const o = options.find((x) => x.value === v)
|
|
303
|
+
return (
|
|
304
|
+
<span key={v} data-slot="select-chip" className="inline-flex items-center gap-1 rounded bg-secondary px-1.5 py-0.5 text-xs text-secondary-foreground">
|
|
305
|
+
{o?.label ?? v}
|
|
306
|
+
<button
|
|
307
|
+
type="button"
|
|
308
|
+
data-slot="select-chip-remove"
|
|
309
|
+
onClick={(e) => {
|
|
310
|
+
e.stopPropagation()
|
|
311
|
+
removeValue(v)
|
|
312
|
+
}}
|
|
313
|
+
className="text-muted-foreground hover:text-foreground"
|
|
314
|
+
aria-label={`Remover ${o?.label ?? v}`}
|
|
315
|
+
>
|
|
316
|
+
<X className="size-3" />
|
|
317
|
+
</button>
|
|
318
|
+
</span>
|
|
319
|
+
)
|
|
320
|
+
})}
|
|
321
|
+
{/* triggerLabel: o gatilho mostra o rótulo CURTO da opção (o input fica
|
|
322
|
+
escondido atrás dele até focar) — o caso da barra do composer. */}
|
|
323
|
+
{triggerText === undefined && (
|
|
324
|
+
<span data-slot="select-trigger-label" className="min-w-0 truncate text-sm">
|
|
325
|
+
{selectedOption?.triggerLabel}
|
|
326
|
+
</span>
|
|
327
|
+
)}
|
|
328
|
+
<CommandPrimitive.Input
|
|
329
|
+
ref={inputRef}
|
|
330
|
+
data-slot="select-input"
|
|
331
|
+
id={id}
|
|
332
|
+
disabled={disabled}
|
|
333
|
+
readOnly={!canSearch}
|
|
334
|
+
aria-label={props['aria-label']}
|
|
335
|
+
value={triggerText ?? ''}
|
|
336
|
+
onValueChange={(v) => {
|
|
337
|
+
if (!canSearch) return
|
|
338
|
+
setQuery(v)
|
|
339
|
+
setOpen(true)
|
|
340
|
+
}}
|
|
341
|
+
onFocus={() => {
|
|
342
|
+
setOpen(true)
|
|
343
|
+
if (!props.multiple && canSearch) inputRef.current?.select()
|
|
344
|
+
}}
|
|
345
|
+
onKeyDown={(e) => {
|
|
346
|
+
if (e.key === 'Escape' && open) {
|
|
347
|
+
e.stopPropagation()
|
|
348
|
+
setOpen(false)
|
|
349
|
+
}
|
|
350
|
+
}}
|
|
351
|
+
placeholder={props.multiple && selectedValues.length > 0 ? '' : (canSearch ? (searchPlaceholder ?? placeholder) : placeholder)}
|
|
352
|
+
className={cn(
|
|
353
|
+
'min-w-0 flex-1 bg-transparent text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed',
|
|
354
|
+
canSearch ? '' : 'cursor-pointer caret-transparent',
|
|
355
|
+
triggerText === undefined && 'sr-only',
|
|
356
|
+
)}
|
|
357
|
+
/>
|
|
358
|
+
{clearable === true && selectedValues.length > 0 && disabled !== true && (
|
|
359
|
+
<button
|
|
360
|
+
type="button"
|
|
361
|
+
data-slot="select-clear"
|
|
362
|
+
aria-label="Limpar seleção"
|
|
363
|
+
onPointerDown={(e) => e.preventDefault()}
|
|
364
|
+
onClick={(e) => {
|
|
365
|
+
e.stopPropagation()
|
|
366
|
+
if (props.multiple) props.onChange([])
|
|
367
|
+
else {
|
|
368
|
+
props.onChange('')
|
|
369
|
+
setQuery('')
|
|
370
|
+
}
|
|
371
|
+
}}
|
|
372
|
+
className="flex shrink-0 items-center justify-center text-muted-foreground transition-colors hover:text-foreground"
|
|
373
|
+
>
|
|
374
|
+
<X className="size-3.5" />
|
|
375
|
+
</button>
|
|
376
|
+
)}
|
|
377
|
+
{/* Ação custom (ex.: botão que age sobre o valor) — o clique NÃO abre a lista:
|
|
378
|
+
o slot para a propagação pro controle. */}
|
|
379
|
+
{trailing !== undefined && (
|
|
380
|
+
<span
|
|
381
|
+
data-slot="select-trailing"
|
|
382
|
+
// Ação no fim do campo é SECUNDÁRIA: o tom muted mora no slot (o botão
|
|
383
|
+
// ghost herda), pra não sprinklar `text-muted-foreground` em cada call
|
|
384
|
+
// site. O hover acende via o próprio `hover:text-accent-foreground` do ghost.
|
|
385
|
+
// Botão trailing na BORDA (buscável, sem chevron): puxa ~6px pra o inset
|
|
386
|
+
// direito casar com o vertical (botão quadrado no canto), como o InputGroup.
|
|
387
|
+
// Com chevron, NÃO puxa — senão cola no chevron (o `gap-1.5` do control).
|
|
388
|
+
className={cn('flex shrink-0 items-center text-muted-foreground', canSearch && 'has-[button]:-mr-1.5')}
|
|
389
|
+
onClick={(e) => e.stopPropagation()}
|
|
390
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
391
|
+
>
|
|
392
|
+
{trailing}
|
|
393
|
+
</span>
|
|
394
|
+
)}
|
|
395
|
+
{/* Chevron: a affordance de "abre uma lista". Sempre no não-buscável; no
|
|
396
|
+
buscável some (ali quem manda o sinal é o cursor de texto). */}
|
|
397
|
+
{!canSearch && (
|
|
398
|
+
<ChevronDownIcon data-slot="select-icon" className={cn('size-4 shrink-0 opacity-50', ghost ? 'h-3.5 w-3.5 opacity-60' : '')} aria-hidden="true" />
|
|
399
|
+
)}
|
|
400
|
+
</div>
|
|
401
|
+
</PopoverAnchor>
|
|
402
|
+
<PopoverContent
|
|
403
|
+
align="start"
|
|
404
|
+
// No campo de form a lista acompanha a largura do controle; no ghost, NÃO — o
|
|
405
|
+
// gatilho é do tamanho do rótulo (uma chave curta), e a lista precisa caber o
|
|
406
|
+
// título inteiro.
|
|
407
|
+
className={cn(
|
|
408
|
+
'pointer-events-auto overflow-hidden p-0',
|
|
409
|
+
ghost ? 'w-auto min-w-56 max-w-80' : 'w-(--radix-popover-trigger-width) min-w-40',
|
|
410
|
+
)}
|
|
411
|
+
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
412
|
+
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
413
|
+
onInteractOutside={(e) => {
|
|
414
|
+
const target = e.target as HTMLElement
|
|
415
|
+
if (rootRef.current?.contains(target)) return e.preventDefault()
|
|
416
|
+
const label = target.closest('label')
|
|
417
|
+
if (id !== undefined && label !== null && label.htmlFor === id) e.preventDefault()
|
|
418
|
+
}}
|
|
419
|
+
onWheel={(e) => e.stopPropagation()}
|
|
420
|
+
onTouchMove={(e) => e.stopPropagation()}
|
|
421
|
+
>
|
|
422
|
+
{showToggleAll && (
|
|
423
|
+
<div className="border-b p-1">
|
|
424
|
+
<button
|
|
425
|
+
type="button"
|
|
426
|
+
data-slot="select-toggle-all"
|
|
427
|
+
onClick={toggleAll}
|
|
428
|
+
className="flex w-full items-center rounded-sm px-2 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
|
429
|
+
>
|
|
430
|
+
{allSelected ? 'Limpar seleção' : `Selecionar tudo (${options.length})`}
|
|
431
|
+
</button>
|
|
432
|
+
</div>
|
|
433
|
+
)}
|
|
434
|
+
<CommandList data-slot="select-list" className="max-h-60 overflow-auto">
|
|
435
|
+
{loading === true ? (
|
|
436
|
+
<div data-slot="select-loading" className="py-6 text-center text-sm text-muted-foreground">
|
|
437
|
+
Buscando…
|
|
438
|
+
</div>
|
|
439
|
+
) : (
|
|
440
|
+
<>
|
|
441
|
+
<CommandEmpty>{emptyText}</CommandEmpty>
|
|
442
|
+
{byGroup(options).map(([group, items]) => (
|
|
443
|
+
<CommandGroup key={group ?? '—'} heading={group}>
|
|
444
|
+
{items.map((o) => {
|
|
445
|
+
const hasHint = o.hint !== undefined && o.hint.length > 0
|
|
446
|
+
return (
|
|
447
|
+
<CommandItem
|
|
448
|
+
key={o.value}
|
|
449
|
+
data-slot="select-item"
|
|
450
|
+
disabled={o.disabled}
|
|
451
|
+
value={`${o.label} ${o.hint ?? ''} ${o.value}`}
|
|
452
|
+
onSelect={() => onSelectOption(o)}
|
|
453
|
+
>
|
|
454
|
+
<span data-slot="select-item-label" className="min-w-0 truncate">
|
|
455
|
+
{o.content ?? o.label}
|
|
456
|
+
</span>
|
|
457
|
+
{hasHint && (
|
|
458
|
+
<span data-slot="select-item-hint" className="ml-auto shrink-0 text-xs text-muted-foreground">
|
|
459
|
+
{o.hint}
|
|
460
|
+
</span>
|
|
461
|
+
)}
|
|
462
|
+
<Check
|
|
463
|
+
data-slot="select-item-check"
|
|
464
|
+
className={cn('size-4 shrink-0', !hasHint && 'ml-auto', selectedValues.includes(o.value) ? 'opacity-100' : 'opacity-0')}
|
|
465
|
+
/>
|
|
466
|
+
</CommandItem>
|
|
467
|
+
)
|
|
468
|
+
})}
|
|
469
|
+
</CommandGroup>
|
|
470
|
+
))}
|
|
471
|
+
</>
|
|
472
|
+
)}
|
|
473
|
+
</CommandList>
|
|
474
|
+
</PopoverContent>
|
|
475
|
+
</CommandPrimitive>
|
|
476
|
+
</div>
|
|
477
|
+
</Popover>
|
|
478
|
+
)
|
|
479
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Separator as SeparatorPrimitive } from "radix-ui"
|
|
3
|
+
|
|
4
|
+
import { cn } from '../../lib/cn.ts'
|
|
5
|
+
|
|
6
|
+
function Separator({
|
|
7
|
+
className,
|
|
8
|
+
orientation = "horizontal",
|
|
9
|
+
decorative = true,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
|
12
|
+
return (
|
|
13
|
+
<SeparatorPrimitive.Root
|
|
14
|
+
data-slot="separator"
|
|
15
|
+
decorative={decorative}
|
|
16
|
+
orientation={orientation}
|
|
17
|
+
className={cn(
|
|
18
|
+
"shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { Separator }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { cn } from '../../lib/cn.ts'
|
|
2
|
+
|
|
3
|
+
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
data-slot="skeleton"
|
|
7
|
+
className={cn("animate-pulse rounded-md bg-accent", className)}
|
|
8
|
+
{...props}
|
|
9
|
+
/>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { Skeleton }
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slider as SliderPrimitive } from "radix-ui"
|
|
3
|
+
|
|
4
|
+
import { cn } from '../../lib/cn.ts'
|
|
5
|
+
|
|
6
|
+
function Slider({
|
|
7
|
+
className,
|
|
8
|
+
defaultValue,
|
|
9
|
+
value,
|
|
10
|
+
min = 0,
|
|
11
|
+
max = 100,
|
|
12
|
+
...props
|
|
13
|
+
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
|
14
|
+
const _values = React.useMemo(
|
|
15
|
+
() =>
|
|
16
|
+
Array.isArray(value)
|
|
17
|
+
? value
|
|
18
|
+
: Array.isArray(defaultValue)
|
|
19
|
+
? defaultValue
|
|
20
|
+
: [min, max],
|
|
21
|
+
[value, defaultValue, min, max]
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<SliderPrimitive.Root
|
|
26
|
+
data-slot="slider"
|
|
27
|
+
defaultValue={defaultValue}
|
|
28
|
+
value={value}
|
|
29
|
+
min={min}
|
|
30
|
+
max={max}
|
|
31
|
+
className={cn(
|
|
32
|
+
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
|
33
|
+
className
|
|
34
|
+
)}
|
|
35
|
+
{...props}
|
|
36
|
+
>
|
|
37
|
+
<SliderPrimitive.Track
|
|
38
|
+
data-slot="slider-track"
|
|
39
|
+
className={cn(
|
|
40
|
+
"relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
<SliderPrimitive.Range
|
|
44
|
+
data-slot="slider-range"
|
|
45
|
+
className={cn(
|
|
46
|
+
"absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
|
|
47
|
+
)}
|
|
48
|
+
/>
|
|
49
|
+
</SliderPrimitive.Track>
|
|
50
|
+
{Array.from({ length: _values.length }, (_, index) => (
|
|
51
|
+
<SliderPrimitive.Thumb
|
|
52
|
+
data-slot="slider-thumb"
|
|
53
|
+
key={index}
|
|
54
|
+
className="block size-4 shrink-0 rounded-full border border-primary bg-white shadow-sm ring-ring/50 transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
|
55
|
+
/>
|
|
56
|
+
))}
|
|
57
|
+
</SliderPrimitive.Root>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { Slider }
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
CircleCheckIcon,
|
|
4
|
+
InfoIcon,
|
|
5
|
+
Loader2Icon,
|
|
6
|
+
OctagonXIcon,
|
|
7
|
+
TriangleAlertIcon,
|
|
8
|
+
} from 'lucide-react'
|
|
9
|
+
import { Toaster as Sonner, toast, type ToasterProps } from 'sonner'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Toaster temático (sonner) — monte 1x no root do app. Os toasts saem estilizados
|
|
13
|
+
* com os tokens do Opus via CSS vars (popover/border/radius) + ícones lucide por
|
|
14
|
+
* tipo. Dispare com `toast.success/error(...)`.
|
|
15
|
+
*
|
|
16
|
+
* Divergência da casa: o shadcn lê o tema com `next-themes` (useTheme); aqui não
|
|
17
|
+
* usamos next-themes — passe `theme` por prop se precisar forçar (default 'system').
|
|
18
|
+
* Elevação dark preservada: `--normal-bg` aponta pro popover (não pro background).
|
|
19
|
+
*/
|
|
20
|
+
export function Toaster({ ...props }: ToasterProps): React.ReactElement {
|
|
21
|
+
return (
|
|
22
|
+
<Sonner
|
|
23
|
+
data-slot="toaster"
|
|
24
|
+
theme="system"
|
|
25
|
+
className="toaster group"
|
|
26
|
+
icons={{
|
|
27
|
+
success: <CircleCheckIcon className="size-4" />,
|
|
28
|
+
info: <InfoIcon className="size-4" />,
|
|
29
|
+
warning: <TriangleAlertIcon className="size-4" />,
|
|
30
|
+
error: <OctagonXIcon className="size-4" />,
|
|
31
|
+
loading: <Loader2Icon className="size-4 animate-spin" />,
|
|
32
|
+
}}
|
|
33
|
+
style={
|
|
34
|
+
{
|
|
35
|
+
'--normal-bg': 'var(--popover)',
|
|
36
|
+
'--normal-text': 'var(--popover-foreground)',
|
|
37
|
+
'--normal-border': 'var(--border)',
|
|
38
|
+
'--border-radius': 'var(--radius)',
|
|
39
|
+
} as React.CSSProperties
|
|
40
|
+
}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { toast }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { Loader2Icon } from 'lucide-react'
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
4
|
+
import { cn } from '../../lib/cn.ts'
|
|
5
|
+
|
|
6
|
+
/** Tamanhos do spinner — divergência da casa (o shadcn atual só tem `size-4` fixo).
|
|
7
|
+
* Mantido aditivo pra cobrir botão (sm) e estados maiores (lg). */
|
|
8
|
+
const spinnerVariants = cva('animate-spin', {
|
|
9
|
+
variants: {
|
|
10
|
+
size: { sm: 'size-3', default: 'size-4', lg: 'size-6' },
|
|
11
|
+
},
|
|
12
|
+
defaultVariants: { size: 'default' },
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export interface SpinnerProps extends React.ComponentProps<'svg'>, VariantProps<typeof spinnerVariants> {}
|
|
16
|
+
|
|
17
|
+
/** Indicador de carregamento (Loader2Icon girando) — substitui o `<Loader2Icon className="animate-spin"/>`
|
|
18
|
+
* inline repetido. Tamanhos sm/default/lg. Pra placeholder de conteúdo, use Skeleton. */
|
|
19
|
+
export function Spinner({ className, size, ...props }: SpinnerProps): React.ReactElement {
|
|
20
|
+
return (
|
|
21
|
+
<Loader2Icon
|
|
22
|
+
data-slot="spinner"
|
|
23
|
+
role="status"
|
|
24
|
+
aria-label="Carregando"
|
|
25
|
+
className={cn(spinnerVariants({ size }), className)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
)
|
|
29
|
+
}
|