@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.
Files changed (34) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/bin/cli.mjs +3 -3
  3. package/bin/lib/check.mjs +236 -2
  4. package/bin/lib/create.mjs +2 -2
  5. package/bin/lib/materialize.mjs +0 -1
  6. package/bin/lib/postinstall.mjs +1 -1
  7. package/docs/consumer-upgrade-propagation.md +56 -0
  8. package/docs/elevation-scale.md +55 -0
  9. package/docs/ownership-vs-shadcn-lock.md +4 -3
  10. package/package.json +1 -1
  11. package/registry/instructions/opus.md +2 -0
  12. package/src/ui/components/primitives/alert-dialog.tsx +1 -1
  13. package/src/ui/components/primitives/button-group.tsx +26 -1
  14. package/src/ui/components/primitives/button.tsx +10 -2
  15. package/src/ui/components/primitives/card.tsx +1 -1
  16. package/src/ui/components/primitives/chat.tsx +6 -2
  17. package/src/ui/components/primitives/composer.tsx +3 -2
  18. package/src/ui/components/primitives/control.ts +2 -0
  19. package/src/ui/components/primitives/dialog.tsx +1 -1
  20. package/src/ui/components/primitives/drawer.tsx +1 -1
  21. package/src/ui/components/primitives/input-group.tsx +4 -2
  22. package/src/ui/components/primitives/menu.tsx +2 -2
  23. package/src/ui/components/primitives/popover.tsx +1 -1
  24. package/src/ui/components/primitives/select.tsx +27 -9
  25. package/src/ui/components/primitives/tabs.tsx +6 -5
  26. package/src/ui/components/primitives/toggle-group.tsx +13 -2
  27. package/src/ui/components/primitives/toggle.tsx +8 -1
  28. package/src/ui/docs/content/card.md +2 -2
  29. package/src/ui/docs/content/composer.md +1 -1
  30. package/src/ui/docs/content/tokens.md +18 -18
  31. package/src/ui/meta.ts +5 -5
  32. package/src/ui/react.tsx +1 -0
  33. package/src/ui/theme.css +16 -27
  34. 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 ring-1 ring-edge shadow-card">
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="shrink-0 p-3">
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 + ring-edge + shadow-card), com o enviar dentro.
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 bg-card ring-1 ring-edge shadow-card transition focus-within:ring-2 focus-within:ring-primary/15',
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
  )}
@@ -0,0 +1,2 @@
1
+ /** Geometria compartilhada pelos controles inline da casa. */
2
+ export type ControlShape = 'default' | 'pill'
@@ -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 bg-card ring-1 ring-edge shadow-dialog 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",
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-dialog transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
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 ring-1 ring-edge shadow-popover 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",
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 ring-1 ring-edge shadow-popover 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",
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 ring-1 ring-edge shadow-popover 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",
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, discreto — o seletor da barra do composer. */
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(ghost ? 'min-w-0' : 'min-w-40', className)}>
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-7 px-1.5 py-1 text-muted-foreground hover:bg-muted hover:text-foreground focus-within:ring-2 focus-within:ring-ring/50'
346
- : "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",
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 data-slot="select-trigger-label" className={cn('min-w-0 flex-1 truncate text-left text-sm', selectedOption === undefined && 'text-muted-foreground')}>
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
- ghost ? 'w-auto min-w-56 max-w-80' : 'w-(--radix-popover-trigger-width) min-w-40',
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
- // Altura: horizontal segue o size (h-9 default / h-8 sm combina `data-[size]` da PRÓPRIA
38
- // list com o `group-data-[orientation]` do ancestral); vertical é h-fit em qualquer size.
39
- "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:data-[size=default]:h-9 group-data-[orientation=horizontal]/tabs:data-[size=sm]:h-8 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none",
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-[-5px] 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
+ "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
- className={cn(toggleVariants({ variant, size, className }))}
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 + ring-edge + shadow-card — ver Tokens & Tema → Elevação por papel) 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).
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 é o anel (`ring-edge`), não uma borda divergência declarada vs shadcn (border + shadow-sm); os divisores internos seguem sendo `border`.
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` + `ring-edge` + `shadow-card`), **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ê.
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
- ## Elevação por papel
72
+ ## Escala de elevação
73
73
 
74
- > A sombra segue o mesmo desenho das cores: token por PAPEL, não por tamanho — o papel de
75
- > superfície carrega cor, elevação e forma (`bg-card` · `shadow-card` · `rounded-card`). Em superfície elevada, a aresta é o anel hairline `ring-1 ring-edge`
76
- > no lugar da borda (nunca os dois juntos); borda sólida fica onde é affordance de interação
77
- > (inputs, controles que são flat). Os defaults `shadow-*` do Tailwind seguem intactos: um
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 ROLES = [
82
- ['card', 'Conteúdo em repouso (Card, composer do Chat).'],
83
- ['popover', 'Flutuante leve (menus, selects, popovers).'],
84
- ['dialog', 'Flutuante modal (dialogs, sheets).'],
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
- {ROLES.map(([role, desc]) => (
89
- <div key={role} className="flex flex-col items-center gap-3">
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 bg-card text-xs ring-1 ring-edge rounded-' + role + ' shadow-' + role}
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-{role}</div>
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 (par do `bg-*`/`shadow-*`), derivados de `--radius` — a
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
- (A antiga nº 4 escala `shadow-*` sobrescrita foi aposentada: a elevação da casa vive em
154
- namespace próprio, `shadow-card/popover/dialog` + `ring-edge`, sem tocar os defaults do Tailwind.)
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; a aresta de superfície elevada é o `ring-edge`.
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` pra intenção (default/secondary/outline/ghost/destructive/link), `size` pro tamanho (default/sm/lg · icon/icon-sm/icon-xs pros só-ícone, 36/32/24px o icon-xs é a ação DENTRO de um campo, o trailing do Input/Select). `busy` pra ação em andamento (spinner + desabilita — é o "depois" do clique, não o "carregando" de conteúdo, que é Spinner/Skeleton num nível de página). `asChild` pra renderizar como outro elemento (ex.: âncora com cara de botão).',
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, não por componente: `searchable` (busca; a régua é ACHABILIDADE — lista longa sem busca é rolar até cansar), `multiple` (chips + Selecionar tudo), `onSearch` (busca no servidor), `native` (o <select> do sistema — lista curta e sabida, picker do SO), `variant="ghost"` (a barra do composer, com `triggerLabel` encurtando o gatilho). Também: `icon` (leading), `trailing` (ação custom no fim do campo, ex.: um botão que age sobre o valor), `size` (default/sm), `clearable`. Absorveu Combobox, NativeSelect, ComposerSelect e o Select do Radix (3.0.0). Pra ação, Menu; pra 2–3 opções sempre visíveis, RadioGroup.',
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). `orientation` (horizontal/vertical).',
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 afins) num bloco coeso — bordas internas colapsadas e cantos arredondados só nas pontas. `orientation` horizontal/vertical. ButtonGroupText pra um rótulo/prefixo colado ao grupo; ButtonGroupSeparator pra cortar visualmente entre ações. Pra um menu de opções, prefira Menu; pra campo com addon, InputGroup.',
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). InputGroupAddon ancora os adornos — `align` inline-start/inline-end nas laterais, block-start/block-end empilha acima/abaixo (exige a moldura alta do InputGroupTextarea, é o padrão do composer). InputGroupText é o rótulo inerte (prefixo/sufixo); InputGroupButton é o botão embutido (ghost por padrão, `size` xs/sm/icon-xs/icon-sm). Só um ÍCONE leading não precisa disto — é a prop `icon` do Input (uma forma só pra adorno de campo, igual ao Select). InputGroup é pros casos que a prop não cobre: botão no fim, prefixo de texto, adorno em bloco, múltiplos. Pra agrupar botões soltos, ButtonGroup.',
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