@softize/opus 8.9.1 → 9.0.1
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 +47 -0
- package/bin/cli.mjs +3 -3
- package/bin/lib/check.mjs +236 -2
- package/bin/lib/create.mjs +2 -2
- package/bin/lib/materialize.mjs +0 -1
- package/bin/lib/postinstall.mjs +1 -1
- package/docs/consumer-upgrade-propagation.md +56 -0
- package/docs/elevation-scale.md +55 -0
- package/docs/ownership-vs-shadcn-lock.md +4 -3
- package/package.json +1 -1
- package/registry/instructions/opus.md +2 -0
- package/src/ui/components/primitives/alert-dialog.tsx +1 -1
- package/src/ui/components/primitives/button-group.tsx +26 -1
- package/src/ui/components/primitives/button.tsx +10 -2
- package/src/ui/components/primitives/card.tsx +1 -1
- package/src/ui/components/primitives/chat.tsx +6 -2
- package/src/ui/components/primitives/composer.tsx +3 -2
- package/src/ui/components/primitives/control.ts +2 -0
- package/src/ui/components/primitives/dialog.tsx +1 -1
- package/src/ui/components/primitives/drawer.tsx +1 -1
- package/src/ui/components/primitives/input-group.tsx +4 -2
- package/src/ui/components/primitives/menu.tsx +2 -2
- package/src/ui/components/primitives/popover.tsx +1 -1
- package/src/ui/components/primitives/select.tsx +27 -9
- package/src/ui/components/primitives/tabs.tsx +6 -5
- package/src/ui/components/primitives/toggle-group.tsx +13 -2
- package/src/ui/components/primitives/toggle.tsx +8 -1
- package/src/ui/docs/content/card.md +2 -2
- package/src/ui/docs/content/composer.md +1 -1
- package/src/ui/docs/content/tokens.md +18 -18
- package/src/ui/meta.ts +5 -5
- package/src/ui/react.tsx +1 -0
- package/src/ui/theme.css +16 -27
- package/registry/github/opus.yml +0 -22
|
@@ -97,7 +97,7 @@ function UserTurn({ content }: { content: string }): React.ReactElement {
|
|
|
97
97
|
}, [content, expanded])
|
|
98
98
|
|
|
99
99
|
return (
|
|
100
|
-
<div data-slot="chat-user" className="break-words rounded-xl bg-card px-3 py-2 text-sm leading-snug
|
|
100
|
+
<div data-slot="chat-user" className="break-words rounded-xl border border-border bg-card px-3 py-2 text-sm leading-snug shadow-sm">
|
|
101
101
|
<div
|
|
102
102
|
ref={ref}
|
|
103
103
|
data-slot="chat-user-body"
|
|
@@ -155,6 +155,9 @@ export interface ChatProps {
|
|
|
155
155
|
/** Seletores discretos na barra do composer (o `actions` do `<Composer>`): agente,
|
|
156
156
|
* app, escopo… O lugar deles é a barra — `notice` é pra aviso, não pra controle. */
|
|
157
157
|
composerActions?: React.ReactNode
|
|
158
|
+
/** Ajuste do contêiner externo do composer. O Chat mantém `p-3` por padrão; shells
|
|
159
|
+
* integrados podem alinhar esse inset aos painéis vizinhos sem alcançar o DOM interno. */
|
|
160
|
+
composerClassName?: string
|
|
158
161
|
/** Texto do estado vazio (centrado, some quando a conversa começa). Não entra no
|
|
159
162
|
* transcript — é apresentação, não fala do assistente. */
|
|
160
163
|
greeting?: string
|
|
@@ -212,6 +215,7 @@ export function Chat({
|
|
|
212
215
|
activity: activityProp,
|
|
213
216
|
notice,
|
|
214
217
|
composerActions,
|
|
218
|
+
composerClassName,
|
|
215
219
|
greeting,
|
|
216
220
|
initialMessages,
|
|
217
221
|
kickoff,
|
|
@@ -391,7 +395,7 @@ export function Chat({
|
|
|
391
395
|
{/* Composer da casa (pílula elevada, enviar dentro). Extraído no <Composer> — o Chat
|
|
392
396
|
liga texto/envio e repassa `composerActions` pros seletores da conversa (agente,
|
|
393
397
|
app…); o <Composer> sozinho segue sendo o caminho de quem não tem chat. */}
|
|
394
|
-
<div className=
|
|
398
|
+
<div data-slot="chat-composer" className={cn('shrink-0 p-3', composerClassName)}>
|
|
395
399
|
{notice}
|
|
396
400
|
<Composer
|
|
397
401
|
value={input}
|
|
@@ -30,7 +30,7 @@ export interface ComposerProps {
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* <Composer /> — a caixa de escrever da casa: textarea (Enter envia / Shift+Enter quebra
|
|
33
|
-
* linha) numa pílula elevada (rounded-card +
|
|
33
|
+
* linha) numa pílula elevada (rounded-card + border + shadow-sm), com o enviar dentro.
|
|
34
34
|
* É o composer do `<Chat>` extraído pra ser reusável SOZINHO — o Maestro cria sessão com
|
|
35
35
|
* ele (sem chat nenhum), e a GB prende seletores nele. Com `actions`, ganha uma barra
|
|
36
36
|
* embaixo pros seletores; sem, é a linha única de sempre.
|
|
@@ -80,6 +80,7 @@ export function Composer({
|
|
|
80
80
|
type="submit"
|
|
81
81
|
size="icon"
|
|
82
82
|
variant="ghost"
|
|
83
|
+
shape="pill"
|
|
83
84
|
disabled={!canSubmit}
|
|
84
85
|
aria-label="Enviar"
|
|
85
86
|
className="shrink-0 text-muted-foreground hover:text-foreground"
|
|
@@ -96,7 +97,7 @@ export function Composer({
|
|
|
96
97
|
fire()
|
|
97
98
|
}}
|
|
98
99
|
className={cn(
|
|
99
|
-
'rounded-card
|
|
100
|
+
'rounded-card border border-border bg-card shadow-sm transition focus-within:ring-2 focus-within:ring-primary/15',
|
|
100
101
|
twoRow ? 'p-2' : 'relative flex items-end gap-2 p-2 pl-3.5',
|
|
101
102
|
className,
|
|
102
103
|
)}
|
|
@@ -54,7 +54,7 @@ export function DialogContent({
|
|
|
54
54
|
// Superfície PURA: só a casca (borda/bg/raio/sombra) + coluna flex com teto de
|
|
55
55
|
// altura. O padding mora nos SLOTS (Header/Body/Footer), não aqui — assim ninguém
|
|
56
56
|
// precisa negar margem pra escapar de um p-6 embutido.
|
|
57
|
-
"fixed left-[50%] top-[50%] z-50 flex max-h-[85vh] w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] flex-col overflow-hidden rounded-dialog
|
|
57
|
+
"fixed left-[50%] top-[50%] z-50 flex max-h-[85vh] w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] flex-col overflow-hidden rounded-dialog border border-border bg-card shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
|
|
58
58
|
className,
|
|
59
59
|
)}
|
|
60
60
|
{...props}
|
|
@@ -58,7 +58,7 @@ function DrawerContent({
|
|
|
58
58
|
<DrawerPrimitive.Content
|
|
59
59
|
data-slot="drawer-content"
|
|
60
60
|
className={cn(
|
|
61
|
-
"fixed z-50 flex flex-col gap-4 bg-background shadow-
|
|
61
|
+
"fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
|
|
62
62
|
side === "right" &&
|
|
63
63
|
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
|
64
64
|
side === "left" &&
|
|
@@ -2,17 +2,19 @@ import * as React from "react"
|
|
|
2
2
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
3
|
|
|
4
4
|
import { cn } from '../../lib/cn.ts'
|
|
5
|
+
import type { ControlShape } from './control.ts'
|
|
5
6
|
import { Button } from './button.tsx'
|
|
6
7
|
import { Input } from './input.tsx'
|
|
7
8
|
import { Textarea } from './textarea.tsx'
|
|
8
9
|
|
|
9
|
-
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
10
|
+
function InputGroup({ className, shape = 'default', ...props }: React.ComponentProps<"div"> & { shape?: ControlShape }) {
|
|
10
11
|
return (
|
|
11
12
|
<div
|
|
12
13
|
data-slot="input-group"
|
|
14
|
+
data-shape={shape}
|
|
13
15
|
role="group"
|
|
14
16
|
className={cn(
|
|
15
|
-
"group/input-group relative flex w-full items-center rounded-md border border-input transition-[color,box-shadow] outline-none dark:bg-input/30",
|
|
17
|
+
"group/input-group relative flex w-full items-center rounded-md border border-input transition-[color,box-shadow] outline-none data-[shape=pill]:rounded-full dark:bg-input/30",
|
|
16
18
|
"h-9 min-w-0 has-[>textarea]:h-auto",
|
|
17
19
|
|
|
18
20
|
// Variants based on alignment.
|
|
@@ -45,7 +45,7 @@ function MenuContent({
|
|
|
45
45
|
data-slot="menu-content"
|
|
46
46
|
sideOffset={sideOffset}
|
|
47
47
|
className={cn(
|
|
48
|
-
"z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[10rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-popover bg-popover p-1 text-popover-foreground
|
|
48
|
+
"z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[10rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-popover border border-border bg-popover p-1 text-popover-foreground shadow-md will-change-[transform,opacity] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
|
49
49
|
className
|
|
50
50
|
)}
|
|
51
51
|
{...props}
|
|
@@ -233,7 +233,7 @@ function MenuSubContent({
|
|
|
233
233
|
<MenuPrimitive.SubContent
|
|
234
234
|
data-slot="menu-sub-content"
|
|
235
235
|
className={cn(
|
|
236
|
-
"z-50 min-w-[10rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-popover bg-popover p-1 text-popover-foreground
|
|
236
|
+
"z-50 min-w-[10rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-popover border border-border bg-popover p-1 text-popover-foreground shadow-md will-change-[transform,opacity] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
|
237
237
|
className
|
|
238
238
|
)}
|
|
239
239
|
{...props}
|
|
@@ -34,7 +34,7 @@ function PopoverContent({
|
|
|
34
34
|
align={align}
|
|
35
35
|
sideOffset={sideOffset}
|
|
36
36
|
className={cn(
|
|
37
|
-
"z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-popover bg-popover p-4 text-popover-foreground
|
|
37
|
+
"z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-popover border border-border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
|
38
38
|
className
|
|
39
39
|
)}
|
|
40
40
|
{...props}
|
|
@@ -25,6 +25,7 @@ import { Check, ChevronDownIcon, SearchIcon, X } from 'lucide-react'
|
|
|
25
25
|
import { Command as CommandPrimitive } from 'cmdk'
|
|
26
26
|
|
|
27
27
|
import { cn } from '../../lib/cn.ts'
|
|
28
|
+
import type { ControlShape } from './control.ts'
|
|
28
29
|
import { CommandEmpty, CommandGroup, CommandItem, CommandList } from './command.tsx'
|
|
29
30
|
import { Popover, PopoverAnchor, PopoverContent } from './popover.tsx'
|
|
30
31
|
|
|
@@ -57,6 +58,8 @@ interface SelectBaseProps {
|
|
|
57
58
|
icon?: React.ReactNode
|
|
58
59
|
/** Altura: `default` (h-9, a do Input/Button) ou `sm` (h-8) pra toolbar densa. */
|
|
59
60
|
size?: 'sm' | 'default'
|
|
61
|
+
/** Geometria do controle; `pill` preserva a variante visual escolhida. */
|
|
62
|
+
shape?: ControlShape
|
|
60
63
|
'aria-label'?: string
|
|
61
64
|
}
|
|
62
65
|
|
|
@@ -84,8 +87,8 @@ interface SelectCustomBase extends SelectBaseProps {
|
|
|
84
87
|
loading?: boolean
|
|
85
88
|
/** X no campo quando há seleção — limpa num clique. Bom em filtro. */
|
|
86
89
|
clearable?: boolean
|
|
87
|
-
/** `ghost`: sem borda nem fundo
|
|
88
|
-
variant?: 'default' | 'ghost'
|
|
90
|
+
/** `outline`: compacto com borda; `ghost`: compacto sem borda nem fundo. */
|
|
91
|
+
variant?: 'default' | 'outline' | 'ghost'
|
|
89
92
|
/** Placeholder enquanto se digita a busca (cai pro `placeholder`). */
|
|
90
93
|
searchPlaceholder?: string
|
|
91
94
|
emptyText?: string
|
|
@@ -123,6 +126,7 @@ function NativeSelect({
|
|
|
123
126
|
id,
|
|
124
127
|
icon,
|
|
125
128
|
size = 'default',
|
|
129
|
+
shape = 'default',
|
|
126
130
|
value,
|
|
127
131
|
onChange,
|
|
128
132
|
...rest
|
|
@@ -142,13 +146,14 @@ function NativeSelect({
|
|
|
142
146
|
<select
|
|
143
147
|
data-slot="select"
|
|
144
148
|
data-size={size}
|
|
149
|
+
data-shape={shape}
|
|
145
150
|
id={id}
|
|
146
151
|
disabled={disabled}
|
|
147
152
|
value={value}
|
|
148
153
|
aria-label={rest['aria-label']}
|
|
149
154
|
onChange={(e) => onChange(e.target.value)}
|
|
150
155
|
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",
|
|
156
|
+
"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 data-[shape=pill]:rounded-full dark:bg-input/30 dark:hover:bg-input/50",
|
|
152
157
|
'focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50',
|
|
153
158
|
'aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40',
|
|
154
159
|
icon ? 'pl-9' : '',
|
|
@@ -199,6 +204,7 @@ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.Rea
|
|
|
199
204
|
id,
|
|
200
205
|
icon,
|
|
201
206
|
size = 'default',
|
|
207
|
+
shape = 'default',
|
|
202
208
|
searchable = false,
|
|
203
209
|
onSearch,
|
|
204
210
|
loading,
|
|
@@ -285,6 +291,8 @@ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.Rea
|
|
|
285
291
|
}
|
|
286
292
|
|
|
287
293
|
const ghost = variant === 'ghost'
|
|
294
|
+
const outline = variant === 'outline'
|
|
295
|
+
const compact = ghost || outline
|
|
288
296
|
|
|
289
297
|
/**
|
|
290
298
|
* O GATILHO nunca é caixa de busca — em nenhum modo.
|
|
@@ -326,12 +334,13 @@ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.Rea
|
|
|
326
334
|
setOpen(v)
|
|
327
335
|
}}
|
|
328
336
|
>
|
|
329
|
-
<div ref={rootRef} data-slot="select" className={cn(
|
|
337
|
+
<div ref={rootRef} data-slot="select" data-shape={shape} className={cn(compact ? 'w-fit max-w-full min-w-0' : 'min-w-40', shape === 'pill' && 'rounded-full', className)}>
|
|
330
338
|
<CommandPrimitive data-slot="select-command" shouldFilter={canSearch && onSearch === undefined} className="overflow-visible">
|
|
331
339
|
<PopoverAnchor asChild>
|
|
332
340
|
<div
|
|
333
341
|
data-slot="select-control"
|
|
334
342
|
data-size={size}
|
|
343
|
+
data-shape={shape}
|
|
335
344
|
data-variant={variant}
|
|
336
345
|
onClick={() => {
|
|
337
346
|
if (disabled !== true) {
|
|
@@ -340,10 +349,12 @@ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.Rea
|
|
|
340
349
|
}
|
|
341
350
|
}}
|
|
342
351
|
className={cn(
|
|
343
|
-
'flex items-center gap-1.5 rounded-md outline-none transition-[color,box-shadow]',
|
|
352
|
+
'flex items-center gap-1.5 rounded-md outline-none transition-[color,box-shadow] data-[shape=pill]:rounded-full',
|
|
344
353
|
ghost
|
|
345
|
-
? 'min-h-
|
|
346
|
-
: "min-h-9
|
|
354
|
+
? 'min-h-9 px-3 py-1 text-muted-foreground hover:bg-muted hover:text-foreground focus-within:ring-2 focus-within:ring-ring/50 data-[size=sm]:min-h-8 data-[size=sm]:px-2'
|
|
355
|
+
: "min-h-9 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",
|
|
356
|
+
variant === 'default' && 'flex-wrap',
|
|
357
|
+
outline && 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-input/50',
|
|
347
358
|
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
348
359
|
canSearch ? '' : 'cursor-pointer',
|
|
349
360
|
disabled === true && 'cursor-not-allowed opacity-50',
|
|
@@ -383,7 +394,14 @@ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.Rea
|
|
|
383
394
|
o gatilho ficaria em branco (o input que carregava o placeholder virou
|
|
384
395
|
invisível). Campo mudo é pior que campo vazio: não diz nem o que é. */}
|
|
385
396
|
{(!props.multiple || selectedValues.length === 0) && (
|
|
386
|
-
<span
|
|
397
|
+
<span
|
|
398
|
+
data-slot="select-trigger-label"
|
|
399
|
+
className={cn(
|
|
400
|
+
'min-w-0 truncate text-left text-sm',
|
|
401
|
+
compact ? 'flex-initial' : 'flex-1',
|
|
402
|
+
selectedOption === undefined && 'text-muted-foreground',
|
|
403
|
+
)}
|
|
404
|
+
>
|
|
387
405
|
{props.multiple ? placeholder : (selectedOption?.triggerLabel ?? selectedOption?.label ?? placeholder)}
|
|
388
406
|
</span>
|
|
389
407
|
)}
|
|
@@ -473,7 +491,7 @@ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.Rea
|
|
|
473
491
|
// título inteiro.
|
|
474
492
|
className={cn(
|
|
475
493
|
'pointer-events-auto overflow-hidden p-0',
|
|
476
|
-
|
|
494
|
+
compact ? 'w-auto min-w-56 max-w-80' : 'w-(--radix-popover-trigger-width) min-w-40',
|
|
477
495
|
)}
|
|
478
496
|
/*
|
|
479
497
|
* O foco ao abrir é SEMPRE nosso — o Radix nunca mexe.
|
|
@@ -34,9 +34,10 @@ function Tabs({
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const tabsListVariants = cva(
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
37
|
+
// A altura horizontal entra como classe simples no componente (abaixo), para que um
|
|
38
|
+
// `className="h-full"` do consumidor consiga substituí-la via tailwind-merge. Seletores
|
|
39
|
+
// condicionais por data-size têm especificidade maior e tornavam a altura inextensível.
|
|
40
|
+
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none data-[variant=line]:p-0",
|
|
40
41
|
{
|
|
41
42
|
variants: {
|
|
42
43
|
variant: {
|
|
@@ -63,7 +64,7 @@ function TabsList({
|
|
|
63
64
|
data-slot="tabs-list"
|
|
64
65
|
data-variant={variant}
|
|
65
66
|
data-size={size}
|
|
66
|
-
className={cn(tabsListVariants({ variant }), className)}
|
|
67
|
+
className={cn(tabsListVariants({ variant }), size === "sm" ? "h-8" : "h-9", className)}
|
|
67
68
|
{...props}
|
|
68
69
|
/>
|
|
69
70
|
)
|
|
@@ -80,7 +81,7 @@ function TabsTrigger({
|
|
|
80
81
|
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
81
82
|
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
|
|
82
83
|
"data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground",
|
|
83
|
-
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-
|
|
84
|
+
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-1px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
|
|
84
85
|
className
|
|
85
86
|
)}
|
|
86
87
|
{...props}
|
|
@@ -3,16 +3,19 @@ import { type VariantProps } from "class-variance-authority"
|
|
|
3
3
|
import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui"
|
|
4
4
|
|
|
5
5
|
import { cn } from '../../lib/cn.ts'
|
|
6
|
+
import type { ControlShape } from './control.ts'
|
|
6
7
|
import { toggleVariants } from './toggle.tsx'
|
|
7
8
|
|
|
8
9
|
const ToggleGroupContext = React.createContext<
|
|
9
10
|
VariantProps<typeof toggleVariants> & {
|
|
10
11
|
spacing?: number
|
|
12
|
+
shape?: ControlShape
|
|
11
13
|
}
|
|
12
14
|
>({
|
|
13
15
|
size: "default",
|
|
14
16
|
variant: "default",
|
|
15
17
|
spacing: 0,
|
|
18
|
+
shape: "default",
|
|
16
19
|
})
|
|
17
20
|
|
|
18
21
|
function ToggleGroup({
|
|
@@ -20,11 +23,13 @@ function ToggleGroup({
|
|
|
20
23
|
variant,
|
|
21
24
|
size,
|
|
22
25
|
spacing = 0,
|
|
26
|
+
shape = "default",
|
|
23
27
|
children,
|
|
24
28
|
...props
|
|
25
29
|
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
|
26
30
|
VariantProps<typeof toggleVariants> & {
|
|
27
31
|
spacing?: number
|
|
32
|
+
shape?: ControlShape
|
|
28
33
|
}) {
|
|
29
34
|
return (
|
|
30
35
|
<ToggleGroupPrimitive.Root
|
|
@@ -32,14 +37,15 @@ function ToggleGroup({
|
|
|
32
37
|
data-variant={variant}
|
|
33
38
|
data-size={size}
|
|
34
39
|
data-spacing={spacing}
|
|
40
|
+
data-shape={shape}
|
|
35
41
|
style={{ "--gap": spacing } as React.CSSProperties}
|
|
36
42
|
className={cn(
|
|
37
|
-
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md",
|
|
43
|
+
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[shape=pill]:rounded-full",
|
|
38
44
|
className
|
|
39
45
|
)}
|
|
40
46
|
{...props}
|
|
41
47
|
>
|
|
42
|
-
<ToggleGroupContext.Provider value={{ variant, size, spacing }}>
|
|
48
|
+
<ToggleGroupContext.Provider value={{ variant, size, spacing, shape }}>
|
|
43
49
|
{children}
|
|
44
50
|
</ToggleGroupContext.Provider>
|
|
45
51
|
</ToggleGroupPrimitive.Root>
|
|
@@ -51,6 +57,7 @@ function ToggleGroupItem({
|
|
|
51
57
|
children,
|
|
52
58
|
variant,
|
|
53
59
|
size,
|
|
60
|
+
shape,
|
|
54
61
|
...props
|
|
55
62
|
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
|
56
63
|
VariantProps<typeof toggleVariants>) {
|
|
@@ -62,13 +69,17 @@ function ToggleGroupItem({
|
|
|
62
69
|
data-variant={context.variant || variant}
|
|
63
70
|
data-size={context.size || size}
|
|
64
71
|
data-spacing={context.spacing}
|
|
72
|
+
data-item-shape={shape}
|
|
65
73
|
className={cn(
|
|
66
74
|
toggleVariants({
|
|
67
75
|
variant: context.variant || variant,
|
|
68
76
|
size: context.size || size,
|
|
77
|
+
shape: shape ?? context.shape,
|
|
69
78
|
}),
|
|
70
79
|
"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10",
|
|
71
80
|
"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
|
|
81
|
+
"group-data-[shape=pill]/toggle-group:data-[spacing=0]:first:rounded-l-full group-data-[shape=pill]/toggle-group:data-[spacing=0]:last:rounded-r-full",
|
|
82
|
+
"data-[item-shape=pill]:data-[spacing=0]:rounded-full",
|
|
72
83
|
className
|
|
73
84
|
)}
|
|
74
85
|
{...props}
|
|
@@ -18,10 +18,15 @@ const toggleVariants = cva(
|
|
|
18
18
|
sm: "h-8 min-w-8 px-1.5",
|
|
19
19
|
lg: "h-10 min-w-10 px-2.5",
|
|
20
20
|
},
|
|
21
|
+
shape: {
|
|
22
|
+
default: '',
|
|
23
|
+
pill: 'rounded-full',
|
|
24
|
+
},
|
|
21
25
|
},
|
|
22
26
|
defaultVariants: {
|
|
23
27
|
variant: "default",
|
|
24
28
|
size: "default",
|
|
29
|
+
shape: "default",
|
|
25
30
|
},
|
|
26
31
|
}
|
|
27
32
|
)
|
|
@@ -30,13 +35,15 @@ function Toggle({
|
|
|
30
35
|
className,
|
|
31
36
|
variant,
|
|
32
37
|
size,
|
|
38
|
+
shape,
|
|
33
39
|
...props
|
|
34
40
|
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
|
35
41
|
VariantProps<typeof toggleVariants>) {
|
|
36
42
|
return (
|
|
37
43
|
<TogglePrimitive.Root
|
|
38
44
|
data-slot="toggle"
|
|
39
|
-
|
|
45
|
+
data-shape={shape}
|
|
46
|
+
className={cn(toggleVariants({ variant, size, shape, className }))}
|
|
40
47
|
{...props}
|
|
41
48
|
/>
|
|
42
49
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## Box simples
|
|
2
2
|
|
|
3
|
-
O uso mais comum: a superfície de conteúdo em repouso (rounded-card +
|
|
3
|
+
O uso mais comum: a superfície de conteúdo em repouso (`rounded-card` + `border` + `shadow-sm` — ver Tokens & Tema → Escala de elevação) com o padding pelo `className`. O Card não força flex/gap/padding — você compõe o miolo (sem brigar com `space-y`/`flex` seu).
|
|
4
4
|
|
|
5
5
|
```tsx preview col
|
|
6
6
|
<Card className="p-4">
|
|
@@ -30,7 +30,7 @@ Pra painel com estrutura: cada slot é dono do próprio padding (como o Dialog).
|
|
|
30
30
|
|
|
31
31
|
## Com divisor
|
|
32
32
|
|
|
33
|
-
border-b no header / border-t no footer dão o divisor quando a estrutura pede separação visível. A aresta externa
|
|
33
|
+
`border-b` no header / `border-t` no footer dão o divisor quando a estrutura pede separação visível. A aresta externa e os divisores internos usam o mesmo vocabulário de `border`.
|
|
34
34
|
|
|
35
35
|
```tsx preview col
|
|
36
36
|
<Card>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## Básico
|
|
2
2
|
|
|
3
|
-
A caixa de escrever da casa: textarea numa pílula elevada (`rounded-card` + `
|
|
3
|
+
A caixa de escrever da casa: textarea numa pílula elevada (`rounded-card` + `border` + `shadow-sm`), **Enter** envia / **Shift+Enter** quebra linha, enviar dentro. É o composer do [Chat](/components/chat) extraído — use SOZINHO quando há entrada de texto mas não um chat (ex.: criar uma sessão). Controlado: o dono do texto é você.
|
|
4
4
|
|
|
5
5
|
```tsx preview col
|
|
6
6
|
const [text, setText] = React.useState('')
|
|
@@ -69,31 +69,30 @@ render(
|
|
|
69
69
|
)
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
##
|
|
72
|
+
## Escala de elevação
|
|
73
73
|
|
|
74
|
-
> A sombra
|
|
75
|
-
>
|
|
76
|
-
>
|
|
77
|
-
>
|
|
78
|
-
> `shadow-md` cru no código é sinal de review, não vocabulário da casa.
|
|
74
|
+
> A sombra usa a escala de intensidade do Tailwind (`shadow-sm` → `shadow-2xl`), mas o
|
|
75
|
+
> Opus controla sua composição para deixá-la mais clara e difusa. O componente escolhe
|
|
76
|
+
> quanto precisa subir; o nome não o prende a um papel. A aresta usa `border border-border`;
|
|
77
|
+
> o spread negativo evita que a sombra engrosse visualmente essa borda.
|
|
79
78
|
|
|
80
79
|
```tsx preview
|
|
81
|
-
const
|
|
82
|
-
['
|
|
83
|
-
['
|
|
84
|
-
['
|
|
80
|
+
const LEVELS = [
|
|
81
|
+
['sm', 'Superfície em repouso.'],
|
|
82
|
+
['md', 'Flutuante intermediário.'],
|
|
83
|
+
['lg', 'Modal ou destaque forte.'],
|
|
85
84
|
]
|
|
86
85
|
render(
|
|
87
86
|
<div className="flex flex-wrap gap-8 py-4">
|
|
88
|
-
{
|
|
89
|
-
<div key={
|
|
87
|
+
{LEVELS.map(([level, desc]) => (
|
|
88
|
+
<div key={level} className="flex flex-col items-center gap-3">
|
|
90
89
|
<div
|
|
91
|
-
className={'flex h-24 w-40 items-center justify-center
|
|
90
|
+
className={'flex h-24 w-40 items-center justify-center rounded-card border border-border bg-card text-xs shadow-' + level}
|
|
92
91
|
>
|
|
93
92
|
Texto
|
|
94
93
|
</div>
|
|
95
94
|
<div className="text-center">
|
|
96
|
-
<div className="font-mono text-xs">shadow-{
|
|
95
|
+
<div className="font-mono text-xs">shadow-{level}</div>
|
|
97
96
|
<div className="max-w-44 text-xs text-muted-foreground">{desc}</div>
|
|
98
97
|
</div>
|
|
99
98
|
</div>
|
|
@@ -112,7 +111,7 @@ render(
|
|
|
112
111
|
|
|
113
112
|
> Escala mapeada no `@theme inline` preservando o sentido pré-v4 dos utilitários `rounded-*`
|
|
114
113
|
> (neutraliza o rename do Tailwind v4). E a forma também tem papel: `rounded-card/popover/dialog`
|
|
115
|
-
> são a forma das superfícies elevadas (
|
|
114
|
+
> são a forma das superfícies elevadas (separada da intensidade de `shadow-*`), derivados de `--radius` — a
|
|
116
115
|
> identidade por app escala tudo junto. Controles (botões, inputs) seguem na escala sm–xl.
|
|
117
116
|
|
|
118
117
|
```tsx preview
|
|
@@ -150,11 +149,12 @@ render(
|
|
|
150
149
|
da casa; dialog e card "sobem" da página de verdade.
|
|
151
150
|
3. Radius mapeado (sm/md/lg/xl) preservando o sentido pré-v4 dos utilitários `rounded-*`.
|
|
152
151
|
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
4. Escala `shadow-*` recomposta em duas camadas claras e difusas — preserva os degraus do
|
|
153
|
+
Tailwind e troca somente o peso visual. Os aliases antigos por papel foram removidos;
|
|
154
|
+
componentes e consumidores usam diretamente a escala.
|
|
155
155
|
|
|
156
156
|
5. Bordas mais leves no light: `--border`/`--input` em 92.5% (upstream 89.8%) — par do canvas
|
|
157
|
-
quase-branco do body;
|
|
157
|
+
quase-branco do body; superfícies elevadas usam essa mesma borda real.
|
|
158
158
|
|
|
159
159
|
## Identidade por app
|
|
160
160
|
|
package/src/ui/meta.ts
CHANGED
|
@@ -27,7 +27,7 @@ export const componentMeta = {
|
|
|
27
27
|
name: 'button',
|
|
28
28
|
ancestry: 'shadcn',
|
|
29
29
|
whenToUse:
|
|
30
|
-
'Ação clicável. `variant`
|
|
30
|
+
'Ação clicável. `variant` define intenção (default/secondary/outline/ghost/destructive/link), `size` define altura (default/sm/lg · icon/icon-sm/icon-xs pros só-ícone, 36/32/24px) e `shape="pill"` troca somente a geometria, sem alterar cor ou hierarquia. `busy` mostra spinner e desabilita. `asChild` renderiza como outro elemento.',
|
|
31
31
|
},
|
|
32
32
|
'card': {
|
|
33
33
|
name: 'card',
|
|
@@ -110,7 +110,7 @@ export const componentMeta = {
|
|
|
110
110
|
name: 'select',
|
|
111
111
|
ancestry: 'opus',
|
|
112
112
|
whenToUse:
|
|
113
|
-
'O seletor ÚNICO da casa: toda escolha em lista. Data-driven (`options` = { value, label, hint?, content?, group? }), nunca JSX de item. Modos por prop
|
|
113
|
+
'O seletor ÚNICO da casa: toda escolha em lista. Data-driven (`options` = { value, label, hint?, content?, group? }), nunca JSX de item. Modos por prop: `searchable`, `multiple`, `onSearch` e `native`. `variant="default"` é campo de formulário; `outline` abraça o conteúdo com borda; `ghost` abraça o conteúdo sem borda. `size` segue a régua inline (default 36px/sm 32px) e `shape="pill"` troca somente a geometria. Também aceita `icon`, `trailing`, `triggerLabel` e `clearable`.',
|
|
114
114
|
},
|
|
115
115
|
'separator': {
|
|
116
116
|
name: 'separator',
|
|
@@ -137,7 +137,7 @@ export const componentMeta = {
|
|
|
137
137
|
name: 'tabs',
|
|
138
138
|
ancestry: 'shadcn',
|
|
139
139
|
whenToUse:
|
|
140
|
-
'Abas pra alternar entre painéis de conteúdo (Radix). Compõe Tabs > (TabsList > TabsTrigger + TabsContent), pareando `value` do trigger com o do content. `variant` na TabsList: `default` (pill) ou `line` (barra sublinhada). `size` no Tabs (default h-9 / sm h-8 — o par do sm de Button/Select, pra fileira densa)
|
|
140
|
+
'Abas pra alternar entre painéis de conteúdo (Radix). Compõe Tabs > (TabsList > TabsTrigger + TabsContent), pareando `value` do trigger com o do content. `variant` na TabsList: `default` (pill) ou `line` (barra sublinhada, ancorada na borda da lista). `size` no Tabs (default h-9 / sm h-8 — o par do sm de Button/Select, pra fileira densa); a altura pode ser substituída em `TabsList`. `orientation` (horizontal/vertical).',
|
|
141
141
|
},
|
|
142
142
|
'textarea': {
|
|
143
143
|
name: 'textarea',
|
|
@@ -197,7 +197,7 @@ export const componentMeta = {
|
|
|
197
197
|
name: 'button-group',
|
|
198
198
|
ancestry: 'shadcn',
|
|
199
199
|
whenToUse:
|
|
200
|
-
'Junta botões (e
|
|
200
|
+
'Junta botões (e Select) num bloco coeso — bordas internas colapsadas e cantos arredondados só nas pontas. `orientation` define o eixo e `shape="pill"` arredonda as extremidades externas sem reabrir a junção interna. ButtonGroupText adiciona rótulo/prefixo; ButtonGroupSeparator corta visualmente entre ações.',
|
|
201
201
|
},
|
|
202
202
|
'calendar': {
|
|
203
203
|
name: 'calendar',
|
|
@@ -239,7 +239,7 @@ export const componentMeta = {
|
|
|
239
239
|
name: 'input-group',
|
|
240
240
|
ancestry: 'shadcn',
|
|
241
241
|
whenToUse:
|
|
242
|
-
'Campo composto: cola ícones, texto e botões a um InputGroupInput/InputGroupTextarea numa única moldura (foco e erro propagam pro grupo todo).
|
|
242
|
+
'Campo composto: cola ícones, texto e botões a um InputGroupInput/InputGroupTextarea numa única moldura (foco e erro propagam pro grupo todo). `shape="pill"` aplica a geometria arredondada à moldura. InputGroupAddon ancora adornos; InputGroupText é rótulo inerte; InputGroupButton é o botão embutido. Pra agrupar botões soltos, use ButtonGroup.',
|
|
243
243
|
},
|
|
244
244
|
'input-otp': {
|
|
245
245
|
name: 'input-otp',
|
package/src/ui/react.tsx
CHANGED
|
@@ -13,6 +13,7 @@ export { cn } from './lib/cn.ts'
|
|
|
13
13
|
// Primitivos
|
|
14
14
|
export { Button, buttonVariants } from './components/primitives/button.tsx'
|
|
15
15
|
export type { ButtonProps } from './components/primitives/button.tsx'
|
|
16
|
+
export type { ControlShape } from './components/primitives/control.ts'
|
|
16
17
|
|
|
17
18
|
export { Input } from './components/primitives/input.tsx'
|
|
18
19
|
|