@softize/opus 12.2.1 → 12.4.0
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 +42 -0
- package/bin/cli.mjs +2 -2
- package/bin/lib/copy.mjs +4 -0
- package/docs/elevation-scale.md +2 -0
- package/docs/radius-scale.md +1 -0
- package/docs/relative-unit-scale.md +55 -0
- package/docs/releasing.md +2 -2
- package/package.json +3 -3
- package/registry/skills/build-opus-ui/SKILL.md +8 -4
- package/registry/skills/build-opus-ui/references/ui-patterns.md +2 -0
- package/registry/templates/app/package.json +1 -1
- package/src/ui/components/patterns/dock.tsx +182 -0
- package/src/ui/components/patterns/shell-nav.tsx +1 -0
- package/src/ui/components/patterns/sidebar.tsx +56 -13
- package/src/ui/components/primitives/accordion.tsx +1 -1
- package/src/ui/components/primitives/button.tsx +4 -4
- package/src/ui/components/primitives/calendar.tsx +2 -2
- package/src/ui/components/primitives/chat.tsx +1 -1
- package/src/ui/components/primitives/checkbox.tsx +1 -1
- package/src/ui/components/primitives/command.tsx +1 -1
- package/src/ui/components/primitives/dialog.tsx +1 -1
- package/src/ui/components/primitives/dot.tsx +39 -0
- package/src/ui/components/primitives/input-group.tsx +4 -4
- package/src/ui/components/primitives/input-otp.tsx +1 -1
- package/src/ui/components/primitives/input.tsx +4 -5
- package/src/ui/components/primitives/item.tsx +1 -1
- package/src/ui/components/primitives/radio-group.tsx +1 -1
- package/src/ui/components/primitives/scroll-area.tsx +1 -1
- package/src/ui/components/primitives/select.tsx +7 -4
- package/src/ui/components/primitives/switch.tsx +1 -1
- package/src/ui/components/primitives/table.tsx +14 -4
- package/src/ui/components/primitives/tabs.tsx +3 -3
- package/src/ui/components/primitives/textarea.tsx +1 -1
- package/src/ui/components/primitives/toggle.tsx +1 -1
- package/src/ui/components/primitives/tooltip.tsx +1 -1
- package/src/ui/components/primitives/truncate.tsx +9 -20
- package/src/ui/docs/DocBrowser.tsx +1 -1
- package/src/ui/docs/content/button.md +2 -2
- package/src/ui/docs/content/customization.md +5 -0
- package/src/ui/docs/content/dock.md +69 -0
- package/src/ui/docs/content/dot.md +17 -0
- package/src/ui/docs/content/select.md +1 -0
- package/src/ui/docs/content/sidebar.md +21 -0
- package/src/ui/docs/content/table.md +25 -24
- package/src/ui/docs/content/tabs.md +1 -1
- package/src/ui/docs/content/tokens.md +9 -9
- package/src/ui/docs/content/truncate.md +26 -0
- package/src/ui/docs/doc.tsx +1 -1
- package/src/ui/docs/registry.tsx +4 -0
- package/src/ui/lib/overflow.ts +34 -0
- package/src/ui/meta.ts +16 -4
- package/src/ui/react.tsx +12 -1
- package/src/ui/theme.css +27 -24
|
@@ -81,7 +81,7 @@ export function DialogHeader({ className, ...props }: React.ComponentProps<'div'
|
|
|
81
81
|
// Fixo (shrink-0), com divisor embaixo (border-b). p-5: padding igual nos quatro
|
|
82
82
|
// lados, casando com o body. O :has tira o divisor quando o header é seguido
|
|
83
83
|
// DIRETO pelo footer (confirm sem body) — senão o border-b dele encostaria no
|
|
84
|
-
//
|
|
84
|
+
// A compensação coincide com a soma das hairlines; o divisor fica no footer.
|
|
85
85
|
className={cn(
|
|
86
86
|
'flex shrink-0 flex-col gap-2 border-b p-5 text-center sm:text-left',
|
|
87
87
|
'[&:has(+[data-slot=dialog-footer])]:border-b-0',
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
3
|
+
import { cn } from '../../lib/cn.ts'
|
|
4
|
+
|
|
5
|
+
export const dotVariants = cva('inline-block size-1.5 shrink-0 rounded-full', {
|
|
6
|
+
variants: {
|
|
7
|
+
variant: {
|
|
8
|
+
default: 'bg-primary',
|
|
9
|
+
secondary: 'bg-muted-foreground/45',
|
|
10
|
+
destructive: 'bg-destructive',
|
|
11
|
+
outline: 'border border-border bg-transparent',
|
|
12
|
+
success: 'bg-emerald-500',
|
|
13
|
+
warning: 'bg-amber-500',
|
|
14
|
+
info: 'bg-blue-500',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: {
|
|
18
|
+
variant: 'default',
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export interface DotProps extends React.ComponentProps<'span'>, VariantProps<typeof dotVariants> {
|
|
23
|
+
/** Nome acessível quando a cor comunica estado; sem label, o ponto é decorativo. */
|
|
24
|
+
label?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Indicador compacto de estado; use Badge quando o texto precisar permanecer visível. */
|
|
28
|
+
export function Dot({ className, variant, label, ...props }: DotProps): React.ReactElement {
|
|
29
|
+
return (
|
|
30
|
+
<span
|
|
31
|
+
data-slot="dot"
|
|
32
|
+
role={label === undefined ? undefined : 'img'}
|
|
33
|
+
aria-label={label}
|
|
34
|
+
aria-hidden={label === undefined ? true : undefined}
|
|
35
|
+
className={cn(dotVariants({ variant }), className)}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -24,7 +24,7 @@ function InputGroup({ className, shape = 'default', ...props }: React.ComponentP
|
|
|
24
24
|
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
|
25
25
|
|
|
26
26
|
// Focus state.
|
|
27
|
-
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[
|
|
27
|
+
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[0.1875rem] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50",
|
|
28
28
|
|
|
29
29
|
// Error state.
|
|
30
30
|
"has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
|
@@ -37,7 +37,7 @@ function InputGroup({ className, shape = 'default', ...props }: React.ComponentP
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const inputGroupAddonVariants = cva(
|
|
40
|
-
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-
|
|
40
|
+
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-0.3125rem)] [&>svg:not([class*='size-'])]:size-4",
|
|
41
41
|
{
|
|
42
42
|
variants: {
|
|
43
43
|
align: {
|
|
@@ -84,10 +84,10 @@ const inputGroupButtonVariants = cva(
|
|
|
84
84
|
{
|
|
85
85
|
variants: {
|
|
86
86
|
size: {
|
|
87
|
-
xs: "h-6 gap-1 rounded-[calc(var(--radius)-
|
|
87
|
+
xs: "h-6 gap-1 rounded-[calc(var(--radius)-0.3125rem)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
|
|
88
88
|
sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5",
|
|
89
89
|
"icon-xs":
|
|
90
|
-
"size-6 rounded-[calc(var(--radius)-
|
|
90
|
+
"size-6 rounded-[calc(var(--radius)-0.3125rem)] p-0 has-[>svg]:p-0",
|
|
91
91
|
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
|
92
92
|
},
|
|
93
93
|
},
|
|
@@ -49,7 +49,7 @@ function InputOTPSlot({
|
|
|
49
49
|
data-slot="input-otp-slot"
|
|
50
50
|
data-active={isActive}
|
|
51
51
|
className={cn(
|
|
52
|
-
"relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md aria-invalid:border-destructive data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-[
|
|
52
|
+
"relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md aria-invalid:border-destructive data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-[0.1875rem] data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:border-destructive data-[active=true]:aria-invalid:ring-destructive/20 dark:bg-input/30 dark:data-[active=true]:aria-invalid:ring-destructive/40",
|
|
53
53
|
className
|
|
54
54
|
)}
|
|
55
55
|
{...props}
|
|
@@ -14,7 +14,7 @@ export interface InputProps extends React.ComponentProps<"input"> {
|
|
|
14
14
|
|
|
15
15
|
// O <input> CRU (sem adorno): ELE é o campo — borda, foco, ref e className moram nele.
|
|
16
16
|
const bareInput =
|
|
17
|
-
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[
|
|
17
|
+
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40"
|
|
18
18
|
|
|
19
19
|
function Input({ className, type, icon, trailing, ...props }: InputProps) {
|
|
20
20
|
// Sem adorno: o <input> cru de sempre — retrocompatível (refs, forms, InputGroupInput).
|
|
@@ -24,7 +24,7 @@ function Input({ className, type, icon, trailing, ...props }: InputProps) {
|
|
|
24
24
|
// Adornado: o MESMO modelo flex do InputGroup/Select. A borda e o anel moram no WRAPPER
|
|
25
25
|
// (disparados pelo `:focus-visible` do input interno — foco de teclado); o <input> fica
|
|
26
26
|
// SEM borda e CRESCE (flex-1), e os adornos são IRMÃOS ao lado. Sem conta de pl-9/pr-9: o
|
|
27
|
-
// `px-3` + `gap` posicionam sozinhos (o trailing fica a
|
|
27
|
+
// `px-3` + `gap` posicionam sozinhos (o trailing fica a 0.75rem da borda, como no Select). O
|
|
28
28
|
// `className` estiliza o CAMPO (largura, raio, fundo, fonte), como no Select — e a fonte/
|
|
29
29
|
// cor cascateiam pro <input> (um `text-xs`/`font-mono` aqui alcança o texto digitado).
|
|
30
30
|
return (
|
|
@@ -32,7 +32,7 @@ function Input({ className, type, icon, trailing, ...props }: InputProps) {
|
|
|
32
32
|
data-slot="input-wrapper"
|
|
33
33
|
className={cn(
|
|
34
34
|
"flex h-9 w-full min-w-0 items-center gap-1.5 rounded-md border border-input bg-transparent px-3 text-base transition-[color,box-shadow] outline-none md:text-sm dark:bg-input/30",
|
|
35
|
-
"has-[input:focus-visible]:border-ring has-[input:focus-visible]:ring-[
|
|
35
|
+
"has-[input:focus-visible]:border-ring has-[input:focus-visible]:ring-[0.1875rem] has-[input:focus-visible]:ring-ring/50",
|
|
36
36
|
"has-[input[aria-invalid=true]]:border-destructive has-[input[aria-invalid=true]]:ring-destructive/20 dark:has-[input[aria-invalid=true]]:ring-destructive/40",
|
|
37
37
|
"has-[input:disabled]:pointer-events-none has-[input:disabled]:opacity-50",
|
|
38
38
|
className,
|
|
@@ -59,8 +59,7 @@ function Input({ className, type, icon, trailing, ...props }: InputProps) {
|
|
|
59
59
|
{trailing !== undefined && (
|
|
60
60
|
// Interativo (AÇÃO, não decoração): sem pointer-events-none. O tom muted (ação
|
|
61
61
|
// secundária) mora no slot — o botão ghost herda, sem sprinklar no call site. Se o
|
|
62
|
-
// trailing é
|
|
63
|
-
// quadrado no canto, não flutuando a 12px), como o InputGroup.
|
|
62
|
+
// trailing é botão: aproxima 0.375rem para casar o inset horizontal com o vertical.
|
|
64
63
|
<span data-slot="input-trailing" className="flex shrink-0 items-center text-muted-foreground has-[button]:-mr-1.5">
|
|
65
64
|
{trailing}
|
|
66
65
|
</span>
|
|
@@ -31,7 +31,7 @@ function ItemSeparator({
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const itemVariants = cva(
|
|
34
|
-
"group/item flex flex-wrap items-center rounded-md border border-transparent text-sm transition-colors duration-100 outline-none focus-visible:border-ring focus-visible:ring-[
|
|
34
|
+
"group/item flex flex-wrap items-center rounded-md border border-transparent text-sm transition-colors duration-100 outline-none focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 [a]:transition-colors [a]:hover:bg-accent/50",
|
|
35
35
|
{
|
|
36
36
|
variants: {
|
|
37
37
|
variant: {
|
|
@@ -25,7 +25,7 @@ function RadioGroupItem({
|
|
|
25
25
|
<RadioGroupPrimitive.Item
|
|
26
26
|
data-slot="radio-group-item"
|
|
27
27
|
className={cn(
|
|
28
|
-
"aspect-square size-4 shrink-0 rounded-full border border-input text-primary transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[
|
|
28
|
+
"aspect-square size-4 shrink-0 rounded-full border border-input text-primary transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
|
|
29
29
|
className
|
|
30
30
|
)}
|
|
31
31
|
{...props}
|
|
@@ -16,7 +16,7 @@ function ScrollArea({
|
|
|
16
16
|
>
|
|
17
17
|
<ScrollAreaPrimitive.Viewport
|
|
18
18
|
data-slot="scroll-area-viewport"
|
|
19
|
-
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[
|
|
19
|
+
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 focus-visible:outline-1"
|
|
20
20
|
>
|
|
21
21
|
{children}
|
|
22
22
|
</ScrollAreaPrimitive.Viewport>
|
|
@@ -52,6 +52,7 @@ interface SelectBaseProps {
|
|
|
52
52
|
/** Texto do campo vazio. */
|
|
53
53
|
placeholder?: string
|
|
54
54
|
disabled?: boolean
|
|
55
|
+
/** Classes da raiz do controle, incluindo campo, ícones e ações. */
|
|
55
56
|
className?: string
|
|
56
57
|
id?: string
|
|
57
58
|
/** Ícone LEADING dentro do campo — identifica o filtro sem jogar o ícone ao lado. */
|
|
@@ -133,7 +134,10 @@ function NativeSelect({
|
|
|
133
134
|
}: SelectNativeProps): React.ReactElement {
|
|
134
135
|
const groups = byGroup(options)
|
|
135
136
|
return (
|
|
136
|
-
<div
|
|
137
|
+
<div
|
|
138
|
+
className={cn('group/select relative w-full min-w-0 has-[select:disabled]:opacity-50', className)}
|
|
139
|
+
data-slot="select-wrapper"
|
|
140
|
+
>
|
|
137
141
|
{icon ? (
|
|
138
142
|
<span
|
|
139
143
|
className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground select-none [&_svg]:size-4"
|
|
@@ -154,10 +158,9 @@ function NativeSelect({
|
|
|
154
158
|
onChange={(e) => onChange(e.target.value)}
|
|
155
159
|
className={cn(
|
|
156
160
|
"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",
|
|
157
|
-
'focus-visible:border-ring focus-visible:ring-[
|
|
161
|
+
'focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50',
|
|
158
162
|
'aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40',
|
|
159
163
|
icon ? 'pl-9' : '',
|
|
160
|
-
className,
|
|
161
164
|
)}
|
|
162
165
|
>
|
|
163
166
|
{placeholder !== undefined && (
|
|
@@ -468,7 +471,7 @@ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.Rea
|
|
|
468
471
|
// Ação no fim do campo é SECUNDÁRIA: o tom muted mora no slot (o botão
|
|
469
472
|
// ghost herda), pra não sprinklar `text-muted-foreground` em cada call
|
|
470
473
|
// site. O hover acende via o próprio `hover:text-accent-foreground` do ghost.
|
|
471
|
-
// Botão trailing na
|
|
474
|
+
// Botão trailing na borda: aproxima 0.375rem para equilibrar o inset.
|
|
472
475
|
// direito casar com o vertical (botão quadrado no canto), como o InputGroup.
|
|
473
476
|
// Com chevron, NÃO puxa — senão cola no chevron (o `gap-1.5` do control).
|
|
474
477
|
className={cn('flex shrink-0 items-center text-muted-foreground', canSearch && 'has-[button]:-mr-1.5')}
|
|
@@ -15,7 +15,7 @@ function Switch({
|
|
|
15
15
|
data-slot="switch"
|
|
16
16
|
data-size={size}
|
|
17
17
|
className={cn(
|
|
18
|
-
"peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none focus-visible:border-ring focus-visible:ring-[
|
|
18
|
+
"peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80",
|
|
19
19
|
className
|
|
20
20
|
)}
|
|
21
21
|
{...props}
|
|
@@ -2,11 +2,21 @@ import * as React from "react"
|
|
|
2
2
|
|
|
3
3
|
import { cn } from '../../lib/cn.ts'
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
export interface TableProps extends React.ComponentProps<"table"> {
|
|
6
|
+
/** `framed` aplica a moldura canônica de datagrid no contêiner da tabela. */
|
|
7
|
+
variant?: 'plain' | 'framed'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function Table({ className, variant = 'plain', ...props }: TableProps) {
|
|
6
11
|
return (
|
|
7
12
|
<div
|
|
8
13
|
data-slot="table-container"
|
|
9
|
-
|
|
14
|
+
data-variant={variant}
|
|
15
|
+
className={cn(
|
|
16
|
+
"relative w-full overflow-x-auto",
|
|
17
|
+
variant === 'framed' &&
|
|
18
|
+
'rounded-lg border border-border [&_[data-slot=table-header]]:bg-muted/20',
|
|
19
|
+
)}
|
|
10
20
|
>
|
|
11
21
|
<table
|
|
12
22
|
data-slot="table"
|
|
@@ -68,7 +78,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|
|
68
78
|
<th
|
|
69
79
|
data-slot="table-head"
|
|
70
80
|
className={cn(
|
|
71
|
-
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[
|
|
81
|
+
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[0.125rem]",
|
|
72
82
|
className
|
|
73
83
|
)}
|
|
74
84
|
{...props}
|
|
@@ -81,7 +91,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
|
|
81
91
|
<td
|
|
82
92
|
data-slot="table-cell"
|
|
83
93
|
className={cn(
|
|
84
|
-
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[
|
|
94
|
+
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[0.125rem]",
|
|
85
95
|
className
|
|
86
96
|
)}
|
|
87
97
|
{...props}
|
|
@@ -5,7 +5,7 @@ import { Tabs as TabsPrimitive } from "radix-ui"
|
|
|
5
5
|
import { cn } from '../../lib/cn.ts'
|
|
6
6
|
|
|
7
7
|
// Divergência da casa (ejetado): o Tabs ganhou `size` — Button e Select têm `sm`, o Tabs
|
|
8
|
-
// não tinha, e uma fileira densa
|
|
8
|
+
// não tinha, e uma fileira densa ficava com o segmento 0.25rem mais alto
|
|
9
9
|
// que os irmãos. O tamanho flui pra TabsList via contexto (a altura é dela).
|
|
10
10
|
type TabsSize = "default" | "sm"
|
|
11
11
|
const TabsSizeContext = React.createContext<TabsSize>("default")
|
|
@@ -37,7 +37,7 @@ const tabsListVariants = cva(
|
|
|
37
37
|
// A altura horizontal entra como classe simples no componente (abaixo), para que um
|
|
38
38
|
// `className="h-full"` do consumidor consiga substituí-la via tailwind-merge. Seletores
|
|
39
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-[
|
|
40
|
+
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[0.1875rem] 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",
|
|
41
41
|
{
|
|
42
42
|
variants: {
|
|
43
43
|
variant: {
|
|
@@ -78,7 +78,7 @@ function TabsTrigger({
|
|
|
78
78
|
<TabsPrimitive.Trigger
|
|
79
79
|
data-slot="tabs-trigger"
|
|
80
80
|
className={cn(
|
|
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-[
|
|
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-[0.1875rem] 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",
|
|
82
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",
|
|
83
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",
|
|
84
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",
|
|
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
|
7
7
|
<textarea
|
|
8
8
|
data-slot="textarea"
|
|
9
9
|
className={cn(
|
|
10
|
-
"flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[
|
|
10
|
+
"flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
|
|
11
11
|
className
|
|
12
12
|
)}
|
|
13
13
|
{...props}
|
|
@@ -5,7 +5,7 @@ import { Toggle as TogglePrimitive } from "radix-ui"
|
|
|
5
5
|
import { cn } from '../../lib/cn.ts'
|
|
6
6
|
|
|
7
7
|
const toggleVariants = cva(
|
|
8
|
-
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[
|
|
8
|
+
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
9
9
|
{
|
|
10
10
|
variants: {
|
|
11
11
|
variant: {
|
|
@@ -46,7 +46,7 @@ function TooltipContent({
|
|
|
46
46
|
{...props}
|
|
47
47
|
>
|
|
48
48
|
{children}
|
|
49
|
-
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[
|
|
49
|
+
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[0.125rem] bg-foreground fill-foreground" />
|
|
50
50
|
</TooltipPrimitive.Content>
|
|
51
51
|
</TooltipPrimitive.Portal>
|
|
52
52
|
)
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
2
|
import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip.tsx'
|
|
3
|
+
import { useOverflowing } from '../../lib/overflow.ts'
|
|
3
4
|
import { cn } from '../../lib/cn.ts'
|
|
4
5
|
|
|
5
6
|
export interface TruncateProps extends React.ComponentProps<'span'> {
|
|
6
7
|
/** Conteúdo do tooltip quando transborda. Default: os próprios children. */
|
|
7
8
|
tooltip?: React.ReactNode
|
|
9
|
+
/** Sinaliza o corte esmaecendo o fim da linha, no lugar das reticências. */
|
|
10
|
+
fade?: boolean
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
/**
|
|
@@ -12,29 +15,15 @@ export interface TruncateProps extends React.ComponentProps<'span'> {
|
|
|
12
15
|
* `scrollWidth > clientWidth`, re-medido em resize). Substitui a composição
|
|
13
16
|
* `block truncate` + `title` sempre presente — title em texto que não corta é ruído.
|
|
14
17
|
* Requer `<TooltipProvider>` na raiz do app (o esqueleto do `opus create` já monta).
|
|
18
|
+
* Com `fade`, a mesma medição decide quando esmaecer o fim da linha em vez de cortar
|
|
19
|
+
* em reticências — sem transbordo, nada é aplicado.
|
|
15
20
|
*/
|
|
16
|
-
export function Truncate({ tooltip, children, className, ...props }: TruncateProps): React.ReactElement {
|
|
17
|
-
const ref =
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
const measure = React.useCallback(() => {
|
|
21
|
-
const el = ref.current
|
|
22
|
-
if (el === null) return
|
|
23
|
-
setOverflowing(el.scrollWidth > el.clientWidth)
|
|
24
|
-
}, [])
|
|
25
|
-
|
|
26
|
-
React.useLayoutEffect(() => {
|
|
27
|
-
measure()
|
|
28
|
-
const el = ref.current
|
|
29
|
-
if (el === null || typeof ResizeObserver === 'undefined') return
|
|
30
|
-
const ro = new ResizeObserver(measure)
|
|
31
|
-
ro.observe(el)
|
|
32
|
-
return () => ro.disconnect()
|
|
33
|
-
// children na dependência: texto novo re-mede (o RO só vê mudança de CAIXA).
|
|
34
|
-
}, [measure, children])
|
|
21
|
+
export function Truncate({ tooltip, fade = false, children, className, ...props }: TruncateProps): React.ReactElement {
|
|
22
|
+
const { ref, overflowing } = useOverflowing<HTMLSpanElement>(children)
|
|
23
|
+
const cut = fade ? (overflowing ? 'truncate-fade' : 'overflow-hidden whitespace-nowrap') : 'truncate'
|
|
35
24
|
|
|
36
25
|
const span = (
|
|
37
|
-
<span ref={ref} data-slot="truncate" className={cn('block
|
|
26
|
+
<span ref={ref} data-slot="truncate" className={cn('block', cut, className)} {...props}>
|
|
38
27
|
{children}
|
|
39
28
|
</span>
|
|
40
29
|
)
|
|
@@ -86,7 +86,7 @@ export function DocBrowser({
|
|
|
86
86
|
items: g.pages.map((p) => ({
|
|
87
87
|
id: p.slug,
|
|
88
88
|
label: p.title,
|
|
89
|
-
badge: p.badge === undefined ? undefined : <span className="rounded border border-border/60 px-1 font-mono text-[
|
|
89
|
+
badge: p.badge === undefined ? undefined : <span className="rounded border border-border/60 px-1 font-mono text-[0.625rem] leading-tight text-muted-foreground/60">{p.badge}</span>,
|
|
90
90
|
})),
|
|
91
91
|
})),
|
|
92
92
|
}))
|
|
@@ -14,7 +14,7 @@ quando destrói de verdade.
|
|
|
14
14
|
|
|
15
15
|
## Tamanhos
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
O tamanho icon exige `aria-label`, porque não há texto visível. Os botões só-ícone vêm em três tamanhos: `icon` (2.25rem), `icon-sm` (2rem, para uma fileira densa como o cabeçalho) e `icon-xs` (1.5rem, para uma ação dentro de um campo, como o `trailing` de Input ou Select).
|
|
18
18
|
|
|
19
19
|
```tsx preview
|
|
20
20
|
<Button size="sm">Pequeno</Button>
|
|
@@ -54,7 +54,7 @@ buttonVariants serve pro caso sem filho único.
|
|
|
54
54
|
| Prop | Tipo | Default | Descrição |
|
|
55
55
|
|---|---|---|---|
|
|
56
56
|
| `variant` | `'default' \| 'secondary' \| 'outline' \| 'ghost' \| 'destructive' \| 'link'` | `'default'` | A intenção da ação — define cor e peso visual. |
|
|
57
|
-
| `size` | `'default' \| 'sm' \| 'lg' \| 'icon' \| 'icon-sm' \| 'icon-xs'` | `'default'` | O tamanho. Os icon* são quadrados (
|
|
57
|
+
| `size` | `'default' \| 'sm' \| 'lg' \| 'icon' \| 'icon-sm' \| 'icon-xs'` | `'default'` | O tamanho. Os icon* são quadrados (2.25/2/1.5rem) para botões só de ícone, com `aria-label`. |
|
|
58
58
|
| `asChild` | `boolean` | `false` | Renderiza como o filho (Radix Slot) em vez de `<button>` — pra âncoras e afins. |
|
|
59
59
|
| `busy` | `boolean` | `false` | Ação em andamento (depois do clique): mostra Spinner + desabilita. Não é "carregando" de conteúdo (que é Spinner/Skeleton num nível de página). |
|
|
60
60
|
| `icon` | `React.ElementType` | | Ícone à esquerda (ex.: icon={Plus}). No busy é trocado pelo Spinner — não soma. |
|
|
@@ -28,6 +28,11 @@ divergência declarada), pra valer pra casa toda.
|
|
|
28
28
|
O Opus deriva de `rounded-xs` a `rounded-2xl` dessa base. Assim, a marca muda a presença dos
|
|
29
29
|
cantos sem substituir as utilities escolhidas pelos componentes nem criar tokens por papel.
|
|
30
30
|
|
|
31
|
+
O tema não define `font-size` em `html`: a fonte raiz pertence ao navegador e à aplicação.
|
|
32
|
+
Quando o produto precisa de outra densidade, declare essa escolha no CSS do app. Por exemplo,
|
|
33
|
+
`html { font-size: 93.75%; }` conserva a proporção que uma raiz de 15 teria sobre a base usual
|
|
34
|
+
de 16, sem transformar esse valor em uma regra da biblioteca.
|
|
35
|
+
|
|
31
36
|
## 2 · className em tudo
|
|
32
37
|
|
|
33
38
|
> Todo componente termina em `cn(base, className)` com tailwind-merge: o utilitário do consumidor
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Dock
|
|
2
|
+
|
|
3
|
+
Ferramentas ancoradas à superfície de trabalho. Um canvas ou um editor raramente comporta mais
|
|
4
|
+
uma faixa de chrome no topo: a trilha da aplicação já ocupa esse papel, e um segundo cabeçalho
|
|
5
|
+
empilha duas faixas com a mesma função. A Dock coloca as ações sobre a própria superfície,
|
|
6
|
+
agrupadas e sempre alcançáveis.
|
|
7
|
+
|
|
8
|
+
```tsx preview
|
|
9
|
+
<div className="relative h-40 w-full rounded-lg border border-border bg-muted/20">
|
|
10
|
+
<Dock label="Ferramentas do fluxo">
|
|
11
|
+
<DockGroup>
|
|
12
|
+
<DockAction icon={<MousePointer2 />} label="Selecionar e mover" pressed onClick={() => {}} />
|
|
13
|
+
</DockGroup>
|
|
14
|
+
<DockGroup>
|
|
15
|
+
<DockAction icon={<Plus />} label="Adicionar etapa" onClick={() => {}} />
|
|
16
|
+
<DockAction icon={<GitBranch />} label="Adicionar condição" onClick={() => {}} />
|
|
17
|
+
</DockGroup>
|
|
18
|
+
<DockGroup>
|
|
19
|
+
<DockAction icon={<Sparkles />} label="Revisar com IA" onClick={() => {}} />
|
|
20
|
+
</DockGroup>
|
|
21
|
+
</Dock>
|
|
22
|
+
<SurfaceStatus>Salvo</SurfaceStatus>
|
|
23
|
+
</div>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
O contêiner precisa ser `relative` — a Dock se ancora nele, não na janela. `position` escolhe a
|
|
27
|
+
aresta: `bottom` (centro), `bottom-left` e `bottom-right`. Uma superfície pode ter mais de uma
|
|
28
|
+
barra quando os papéis são distintos, como ferramentas ao centro e controles de viewport à
|
|
29
|
+
esquerda.
|
|
30
|
+
|
|
31
|
+
## Grupos e divisórias
|
|
32
|
+
|
|
33
|
+
A divisória entre grupos pertence ao componente: o consumidor declara `DockGroup` e a linha
|
|
34
|
+
aparece entre grupos consecutivos, nunca antes do primeiro. Agrupe por intenção — modo, criação,
|
|
35
|
+
IA, publicação — em vez de espalhar ícones numa fileira única.
|
|
36
|
+
|
|
37
|
+
## Estado da superfície
|
|
38
|
+
|
|
39
|
+
Estado não é ferramenta, e por isso não mora na barra: `SurfaceStatus` flutua num canto da mesma
|
|
40
|
+
superfície — `top-right` por padrão — e recebe salvamento, versão publicada, execução percorrida.
|
|
41
|
+
A região é `role="status"` com `aria-live="polite"`, então a mudança é anunciada sem roubar o foco.
|
|
42
|
+
Separar os dois preserva a barra como toolbar navegável e dá ao estado um lugar estável, que não
|
|
43
|
+
muda de posição conforme o número de ferramentas.
|
|
44
|
+
|
|
45
|
+
Mensagem de sucesso é transitória por natureza — some depois de confirmar — enquanto falha
|
|
46
|
+
permanece até o estado mudar. Essa decisão é da aplicação, que conhece o ciclo; o componente
|
|
47
|
+
apenas reserva o lugar.
|
|
48
|
+
|
|
49
|
+
`actions` recebe o que o recurso aberto permite fazer — em geral o mesmo menu que a linha dele
|
|
50
|
+
tem na navegação. Assim a superfície deixa de ser o único lugar onde renomear ou excluir não
|
|
51
|
+
alcançam o item que está na tela. Apenas o texto é região viva: anunciar o menu a cada mudança de
|
|
52
|
+
estado seria ruído para quem usa leitor de tela.
|
|
53
|
+
|
|
54
|
+
## Modo e execução
|
|
55
|
+
|
|
56
|
+
`DockAction` sem `pressed` executa uma ação. Com `pressed`, comunica um modo ligado — o botão
|
|
57
|
+
assume o preenchimento e expõe `aria-pressed`. Use `hint` quando o tooltip precisar dizer mais que
|
|
58
|
+
o rótulo, que também é o nome acessível.
|
|
59
|
+
|
|
60
|
+
## Teclado
|
|
61
|
+
|
|
62
|
+
A barra é uma `toolbar`: as setas andam entre as ações e Home/End vão às pontas. Cada ação nomeia-se
|
|
63
|
+
por tooltip, então `<TooltipProvider>` precisa existir na raiz do app — o esqueleto do `opus create`
|
|
64
|
+
já monta.
|
|
65
|
+
|
|
66
|
+
| Prop | Tipo | Default | O que faz |
|
|
67
|
+
| --- | --- | --- | --- |
|
|
68
|
+
| `position` | `'bottom' \| 'bottom-left' \| 'bottom-right'` | `'bottom'` | Aresta do contêiner onde a barra se ancora. |
|
|
69
|
+
| `label` | `string` | | Nome acessível da barra. |
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Dot
|
|
2
|
+
|
|
3
|
+
Indicador visual compacto para estados que já têm contexto. O tamanho permanece fixo; `variant`
|
|
4
|
+
seleciona somente a intenção semântica. Quando a cor carrega significado, `label` fornece o nome
|
|
5
|
+
acessível. Sem `label`, o ponto é decorativo.
|
|
6
|
+
|
|
7
|
+
```tsx preview
|
|
8
|
+
<div className="flex items-center gap-4">
|
|
9
|
+
<Dot variant="success" label="Ativo" />
|
|
10
|
+
<Dot variant="warning" label="Atenção" />
|
|
11
|
+
<Dot variant="destructive" label="Falhou" />
|
|
12
|
+
<Dot variant="info" label="Publicado" />
|
|
13
|
+
<Dot variant="secondary" aria-hidden />
|
|
14
|
+
</div>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Use `Badge` quando o estado precisar permanecer legível sem depender do contexto ao redor.
|
|
@@ -340,3 +340,4 @@ render(
|
|
|
340
340
|
| `size` | `'default' \| 'sm'` | `'default'` | Altura: default (h-9, a do Input e do Button) ou sm (h-8) pra toolbar densa. |
|
|
341
341
|
| `disabled` | `boolean` | `false` | Esmaece e trava o controle. |
|
|
342
342
|
| `id` | `string` | | Vai pro campo — pra parear com o `htmlFor` do Label. |
|
|
343
|
+
| `className` | `string` | | Classes da raiz do controle, incluindo campo, ícones e ações, em todos os modos. |
|
|
@@ -29,6 +29,27 @@ render(
|
|
|
29
29
|
|
|
30
30
|
`collapsed` pertence à própria `Sidebar`; `SidebarItem`, `SidebarNav` e `ShellNav` adaptam-se automaticamente para botões `size-9` centralizados, ícones e tooltips. `PaneHeader`, `PaneContent` e `PaneFooter` são os slots do pane: a aplicação mantém a identidade e ações que lhe pertencem sem atribuí-las artificialmente à sidebar.
|
|
31
31
|
|
|
32
|
+
`SidebarItem` também é a linha de árvores de navegação. `actions` e o controle formado por
|
|
33
|
+
`onToggle`/`expanded` são irmãos do botão principal, portanto menus e chevrons não criam
|
|
34
|
+
controles interativos aninhados. No modo recolhido, a linha conserva somente o destino com
|
|
35
|
+
ícone e tooltip. No modo aberto, o chevron ocupa o lugar do ícone da pasta durante o hover da própria linha ou
|
|
36
|
+
foco, e as ações ficam sobrepostas à extremidade direita: controles invisíveis não reduzem o
|
|
37
|
+
espaço disponível para o rótulo. O foco comum no destino não mantém o chevron aberto;
|
|
38
|
+
somente o foco visível no próprio controle o revela para navegação por teclado.
|
|
39
|
+
As ações aparecem no hover da própria linha, no foco visível do próprio controle ou enquanto o menu está
|
|
40
|
+
aberto; focar o destino da linha não revela as reticências.
|
|
41
|
+
O rótulo que não cabe na linha desaparece num gradiente até a borda, em vez de terminar em
|
|
42
|
+
reticências; a linha mede o próprio transbordo, então o rótulo que cabe inteiro fica intacto.
|
|
43
|
+
O foco visível usa o mesmo anel do `Button` (`ring-2 ring-ring/50`), na linha aberta, no botão do
|
|
44
|
+
rail recolhido e no chevron — sem ele a navegação por teclado cairia no anel padrão do navegador,
|
|
45
|
+
que destoa do tema.
|
|
46
|
+
Durante drag-and-drop, `dropPosition="before"` e `"after"` desenham uma linha sobreposta ao
|
|
47
|
+
limite do item; `"inside"` realça a superfície da pasta. O indicador nunca reserva espaço.
|
|
48
|
+
|
|
49
|
+
Árvores montadas por composição usam `SidebarGroupLabel` para os mesmos rótulos discretos
|
|
50
|
+
que o `SidebarNav` desenha automaticamente. O heading permanece `text-sm`; hierarquia vem
|
|
51
|
+
do peso médio e da cor atenuada, não de reduzir legibilidade.
|
|
52
|
+
|
|
32
53
|
```tsx
|
|
33
54
|
<Sidebar collapsed={collapsed}>
|
|
34
55
|
<PaneHeader><MySidebarHeader onToggle={() => setCollapsed(!collapsed)} /></PaneHeader>
|
|
@@ -38,32 +38,33 @@ A Table vem SEM borda externa — só as divisórias de linha (a última o Table
|
|
|
38
38
|
|
|
39
39
|
## Moldura (o datagrid da casa)
|
|
40
40
|
|
|
41
|
-
A
|
|
41
|
+
A variante `framed` aplica no próprio contêiner a borda externa, os cantos arredondados, o
|
|
42
|
+
scroll horizontal contido e o fundo discreto do cabeçalho. A última linha já vem sem divisória,
|
|
43
|
+
então o quadro fecha limpo. O ActionList continua sendo o pattern indicado para coleções
|
|
44
|
+
pesquisáveis derivadas de actions `kind: 'list'`.
|
|
42
45
|
|
|
43
46
|
```tsx preview col
|
|
44
|
-
<
|
|
45
|
-
<
|
|
46
|
-
<
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
</Table>
|
|
66
|
-
</div>
|
|
47
|
+
<Table variant="framed">
|
|
48
|
+
<TableHeader>
|
|
49
|
+
<TableRow>
|
|
50
|
+
<TableHead>Sessão</TableHead>
|
|
51
|
+
<TableHead>Agente</TableHead>
|
|
52
|
+
<TableHead className="text-right">Duração</TableHead>
|
|
53
|
+
</TableRow>
|
|
54
|
+
</TableHeader>
|
|
55
|
+
<TableBody>
|
|
56
|
+
<TableRow>
|
|
57
|
+
<TableCell className="font-medium">Importar pedidos da transportadora</TableCell>
|
|
58
|
+
<TableCell>developer</TableCell>
|
|
59
|
+
<TableCell className="text-right">42 min</TableCell>
|
|
60
|
+
</TableRow>
|
|
61
|
+
<TableRow>
|
|
62
|
+
<TableCell className="font-medium">Revisar contrato de rastreio</TableCell>
|
|
63
|
+
<TableCell>reviewer</TableCell>
|
|
64
|
+
<TableCell className="text-right">18 min</TableCell>
|
|
65
|
+
</TableRow>
|
|
66
|
+
</TableBody>
|
|
67
|
+
</Table>
|
|
67
68
|
```
|
|
68
69
|
|
|
69
70
|
## Com rodapé (TableFooter)
|
|
@@ -69,7 +69,7 @@ orientation=vertical no Tabs: a lista vira coluna e o traço da variante line mi
|
|
|
69
69
|
|
|
70
70
|
## Densidade (size)
|
|
71
71
|
|
|
72
|
-
`size="sm"` no `Tabs`
|
|
72
|
+
`size="sm"` no `Tabs` reduz a lista de `h-9` (2.25rem) para `h-8` (2rem). É o par do `sm` de Button e Select para uma fileira densa, como uma toolbar ou um cabeçalho, em que o segmento não deve ficar mais alto que os elementos vizinhos.
|
|
73
73
|
|
|
74
74
|
```tsx preview col
|
|
75
75
|
<Tabs defaultValue="preview" size="sm">
|