@softize/opus 12.2.0 → 12.3.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 +18 -0
- package/docs/elevation-scale.md +2 -0
- package/docs/radius-scale.md +1 -0
- package/docs/relative-unit-scale.md +55 -0
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +8 -4
- package/registry/skills/build-opus-ui/references/ui-patterns.md +2 -0
- package/src/ui/components/patterns/page.tsx +17 -10
- package/src/ui/components/patterns/sidebar.tsx +1 -1
- 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/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 +2 -2
- package/src/ui/components/primitives/switch.tsx +1 -1
- package/src/ui/components/primitives/table.tsx +2 -2
- 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/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/page.md +7 -2
- package/src/ui/docs/content/tabs.md +1 -1
- package/src/ui/docs/content/tokens.md +9 -9
- package/src/ui/docs/doc.tsx +1 -1
- package/src/ui/meta.ts +2 -2
- package/src/ui/theme.css +15 -24
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
|
|
|
7
7
|
`opus copy --check` · `base copy check` · `manifest:check`) — eles apontam o que a
|
|
8
8
|
mudança cobra do seu código.
|
|
9
9
|
|
|
10
|
+
## 12.3.0 — 2026-08-23
|
|
11
|
+
|
|
12
|
+
O Opus deixa de definir `font-size` em `html`. A fonte raiz volta a pertencer ao navegador e à
|
|
13
|
+
aplicação, enquanto texto, espaçamento, arredondamento, foco, elevação e limites dimensionais da
|
|
14
|
+
UI usam uma escala relativa em `rem`. Assim, preferências de acessibilidade e identidades próprias
|
|
15
|
+
conseguem ajustar a densidade do documento sem disputar com o tema da biblioteca.
|
|
16
|
+
|
|
17
|
+
A mudança é visual e não exige migração de API. Sem uma regra do consumidor, `1rem` segue a
|
|
18
|
+
configuração do navegador. Aplicações que desejarem conservar a proporção anterior podem definir
|
|
19
|
+
`html { font-size: 93.75%; }` no próprio CSS. `px` permanece apenas em hairlines e compensações
|
|
20
|
+
geométricas inventariadas por teste.
|
|
21
|
+
|
|
22
|
+
## 12.2.1 — 2026-08-23
|
|
23
|
+
|
|
24
|
+
O cabeçalho de `Page` passa a alinhar `actions` à base do bloco formado por título e descrição.
|
|
25
|
+
Assim, os botões permanecem junto ao fim do contexto quando a página apresenta uma linha
|
|
26
|
+
explicativa. Múltiplas ações conservam o espaçamento entre si. A API pública não muda.
|
|
27
|
+
|
|
10
28
|
## 12.2.0 — 2026-08-23
|
|
11
29
|
|
|
12
30
|
A escala de arredondamento ganha uma base mais presente sem transformar todo componente em uma
|
package/docs/elevation-scale.md
CHANGED
|
@@ -20,6 +20,8 @@ uma superfície que precisava apenas de mais presença acabava recebendo um nome
|
|
|
20
20
|
- O Opus declara a composição diretamente em `--shadow-*`, mantendo a progressão do
|
|
21
21
|
Tailwind com camadas mais claras, difusas e spread negativo. Sem variável intermediária,
|
|
22
22
|
modifiers nativos como `shadow-xl/30` continuam funcionando.
|
|
23
|
+
- Deslocamento, desfoque e spread usam `rem`, portanto a elevação acompanha a escala raiz
|
|
24
|
+
escolhida pelo documento.
|
|
23
25
|
- Componentes escolhem a intensidade pelo contexto: `sm` para repouso, `md` para
|
|
24
26
|
flutuantes e `lg` para modais ou destaques fortes. Essa associação é default, não uma
|
|
25
27
|
restrição semântica.
|
package/docs/radius-scale.md
CHANGED
|
@@ -18,6 +18,7 @@ voltaria a acoplar forma e tipo de componente, problema removido na versão 10.0
|
|
|
18
18
|
|
|
19
19
|
- `--radius` passa a `0.75rem`. Aplicações podem sobrescrever essa base para ajustar a identidade
|
|
20
20
|
inteira sem forkar componentes.
|
|
21
|
+
- Os offsets da escala também usam `rem`, acompanhando a fonte raiz escolhida pelo documento.
|
|
21
22
|
- `rounded-xs` até `rounded-2xl` formam uma progressão derivada da mesma base. `rounded-full`
|
|
22
23
|
continua reservado a círculos e variantes explicitamente pill.
|
|
23
24
|
- Componentes escolhem o degrau pela escala visual. Em geral, detalhes e itens internos usam
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Escala relativa e fonte raiz
|
|
2
|
+
|
|
3
|
+
> **Status: decidido (ago/2026).** O documento define a fonte raiz; o Opus organiza sua
|
|
4
|
+
> escala visual em `rem` e não altera essa escolha.
|
|
5
|
+
|
|
6
|
+
## Contexto
|
|
7
|
+
|
|
8
|
+
O tema do Opus fixava `font-size: 15px` em `html`. Isso tornava a densidade uniforme, mas
|
|
9
|
+
também fazia uma biblioteca decidir a tipografia raiz de toda a aplicação. A regra alcançava
|
|
10
|
+
conteúdo que não pertencia ao Opus, reduzia a preferência do navegador e obrigava o consumidor
|
|
11
|
+
a desfazer o tema antes de escolher sua própria escala.
|
|
12
|
+
|
|
13
|
+
Parte das medidas visuais ainda usava `px`. Radius, elevação, foco, limites de conteúdo e texto
|
|
14
|
+
compacto deixavam de acompanhar a raiz mesmo quando a aplicação assumia essa responsabilidade.
|
|
15
|
+
|
|
16
|
+
## Decisão
|
|
17
|
+
|
|
18
|
+
- O tema do Opus não declara `font-size` em `html` nem em `:root`. O navegador e a aplicação
|
|
19
|
+
definem a fonte raiz.
|
|
20
|
+
- Medidas que pertencem à identidade e à escala da interface usam `rem`, diretamente ou pela
|
|
21
|
+
escala do Tailwind, que também é relativa à raiz.
|
|
22
|
+
- Aplicações ajustam a densidade no próprio CSS. Para conservar a proporção que antes resultava
|
|
23
|
+
de uma raiz de 15 sobre a base usual de 16, podem usar `html { font-size: 93.75%; }`.
|
|
24
|
+
- `px` fica restrito a encaixes técnicos que não devem crescer com a tipografia: espessura de
|
|
25
|
+
hairline, compensação exata de uma borda, ajuste óptico preso a esse traço e o raio efetivamente
|
|
26
|
+
infinito do scrollbar nativo. Cada ocorrência integra a allowlist do teste de contrato.
|
|
27
|
+
- Valores históricos no changelog podem continuar em `px`, pois descrevem versões anteriores;
|
|
28
|
+
eles não definem o contrato vigente.
|
|
29
|
+
|
|
30
|
+
## Consequências
|
|
31
|
+
|
|
32
|
+
- Sem uma regra da aplicação, `1rem` segue a configuração do navegador. A UI pode ficar maior
|
|
33
|
+
para consumidores que recebiam os antigos 15px; essa mudança é visual e intencional.
|
|
34
|
+
- Preferências de acessibilidade e identidades próprias deixam de disputar com um override da
|
|
35
|
+
biblioteca.
|
|
36
|
+
- Alterar a raiz na aplicação escala texto, espaçamento, radius, foco, elevação e limites
|
|
37
|
+
dimensionais do Opus como um conjunto.
|
|
38
|
+
- Hairlines e compensações de borda permanecem nítidas e geometricamente estáveis.
|
|
39
|
+
|
|
40
|
+
## Alternativas descartadas
|
|
41
|
+
|
|
42
|
+
- **Manter 15px como default:** conserva a densidade anterior, mas mantém a biblioteca como dona
|
|
43
|
+
do documento inteiro.
|
|
44
|
+
- **Declarar `font-size: 100%`:** costuma reproduzir o default, mas ainda é uma opinião redundante
|
|
45
|
+
e pode sobrescrever uma decisão anterior da aplicação.
|
|
46
|
+
- **Converter também hairlines para `rem`:** torna a regra absoluta, mas permite que bordas e
|
|
47
|
+
compensações deixem de coincidir quando a raiz muda.
|
|
48
|
+
|
|
49
|
+
## Verificação
|
|
50
|
+
|
|
51
|
+
- Um teste de contrato confirma que o tema não define a fonte raiz.
|
|
52
|
+
- O mesmo teste inventaria `px` no código vivo da UI e reprova qualquer ocorrência fora da
|
|
53
|
+
allowlist técnica.
|
|
54
|
+
- Testes de radius e elevação validam as fórmulas em `rem`.
|
|
55
|
+
- A documentação de tokens e a skill `build-opus-ui` orientam a responsabilidade do consumidor.
|
package/package.json
CHANGED
|
@@ -19,18 +19,22 @@ existentes, mantendo navegação observável e componentes reutilizáveis sem la
|
|
|
19
19
|
o produto precisa de deep link, back/forward ou refresh.
|
|
20
20
|
4. Manter margem e posicionamento no consumidor; componente reutilizável controla apenas
|
|
21
21
|
seu interior.
|
|
22
|
-
5.
|
|
23
|
-
|
|
22
|
+
5. Não definir a fonte raiz em uma biblioteca ou componente. O navegador e a aplicação são
|
|
23
|
+
responsáveis por `font-size` em `html`; medidas escaláveis da UI usam `rem` ou a escala
|
|
24
|
+
relativa do Tailwind. Reservar `px` a hairlines e compensações presas à geometria da borda,
|
|
25
|
+
com justificativa e cobertura explícitas.
|
|
26
|
+
6. Evoluir um pattern compartilhado apenas quando a recorrência e o contrato estiverem claros.
|
|
27
|
+
7. Tratar tokens de superfície como pares indivisíveis no mesmo fragmento de classes:
|
|
24
28
|
`bg-card text-card-foreground` e `bg-popover text-popover-foreground`. Não depender da
|
|
25
29
|
igualdade atual com `--foreground`, porque o app pode sobrescrever cada par.
|
|
26
|
-
|
|
30
|
+
8. Usar diretamente a escala `rounded-*`; não criar radius por nome de componente quando
|
|
27
31
|
`rounded-xs` a `rounded-2xl` já expressam a forma. Escolher o degrau pela escala visual:
|
|
28
32
|
detalhes e itens internos tendem a `xs`/`sm`, controles e flutuantes compactos a `md`,
|
|
29
33
|
molduras estruturais a `lg`, e superfícies principais ou modais a `xl`. Essa associação
|
|
30
34
|
orienta o default, não cria uma restrição semântica. Em aninhamento, evitar moldura dupla e
|
|
31
35
|
reduzir o raio interno; em grupos conectados, remover os raios das arestas internas. Tamanho
|
|
32
36
|
e forma permanecem eixos separados; usar `shape="pill"` quando a pílula for intencional.
|
|
33
|
-
|
|
37
|
+
9. Testar estados de loading, vazio, erro, sucesso, permissão e interação relevante.
|
|
34
38
|
|
|
35
39
|
## Verificação
|
|
36
40
|
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
operação, não a uma tela isolada.
|
|
6
6
|
- URL representa estado que precisa sobreviver a refresh, deep link ou histórico.
|
|
7
7
|
- Componentes compartilhados não impõem margem externa; páginas e shells compõem layout.
|
|
8
|
+
- A fonte raiz pertence ao navegador e à aplicação. Medidas escaláveis usam `rem` ou a escala
|
|
9
|
+
relativa do Tailwind; `px` fica restrito a hairlines e compensações ligadas a essas bordas.
|
|
8
10
|
- Superfície semântica e foreground são um par local (`bg-card text-card-foreground`,
|
|
9
11
|
`bg-popover text-popover-foreground`); herança do foreground global não substitui o par.
|
|
10
12
|
- Forma usa a escala `rounded-*`; nome de componente não cria uma segunda escala de radius.
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Padroniza o que toda página repete: <main> + container de largura CHEIA (o caso
|
|
5
5
|
* comum do back-office; quem quiser estreito passa `className="max-w-5xl"`) + header
|
|
6
|
-
* (título, descrição e
|
|
6
|
+
* (título, descrição e ações à direita, alinhadas ao fim desse bloco — em geral o botão
|
|
7
|
+
* de criar). O conteúdo é
|
|
7
8
|
* children, sem nada imposto. Presentacional (sem contrato) de propósito: uma página
|
|
8
9
|
* agrega N fontes; o que é action-driven mora dentro (ActionList, listas query-backed).
|
|
9
10
|
*/
|
|
@@ -17,7 +18,7 @@ export interface PageProps {
|
|
|
17
18
|
count?: number
|
|
18
19
|
/** Linha de contexto sob o título. */
|
|
19
20
|
description?: ReactNode
|
|
20
|
-
/**
|
|
21
|
+
/** Ações à direita do header, alinhadas ao fim do bloco de contexto. */
|
|
21
22
|
actions?: ReactNode
|
|
22
23
|
/** Classes do container. Default: largura cheia; `max-w-5xl` estreita e centra. */
|
|
23
24
|
className?: string
|
|
@@ -28,16 +29,22 @@ export function Page({ title, count, description, actions, className, children }
|
|
|
28
29
|
return (
|
|
29
30
|
<main data-slot="page" className="min-w-0 flex-1">
|
|
30
31
|
<div className={cn('mx-auto px-8 py-8', className)}>
|
|
31
|
-
{/* As ações
|
|
32
|
+
{/* As ações acompanham o fim do contexto, mesmo quando há uma descrição sob o título. */}
|
|
32
33
|
<div data-slot="page-header" className="mb-6">
|
|
33
|
-
<div className={cn(actions !== undefined && 'flex items-
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
<div className={cn(actions !== undefined && 'flex items-end justify-between gap-4')}>
|
|
35
|
+
<div data-slot="page-heading" className="min-w-0">
|
|
36
|
+
<h1 className="flex items-baseline gap-2 text-2xl font-semibold tracking-tight">
|
|
37
|
+
{title}
|
|
38
|
+
{count !== undefined && <span className="font-mono text-base text-muted-foreground/60">{count}</span>}
|
|
39
|
+
</h1>
|
|
40
|
+
{description !== undefined && <p className="mt-1 text-sm text-muted-foreground">{description}</p>}
|
|
41
|
+
</div>
|
|
42
|
+
{actions !== undefined && (
|
|
43
|
+
<div data-slot="page-actions" className="flex shrink-0 items-center gap-4">
|
|
44
|
+
{actions}
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
39
47
|
</div>
|
|
40
|
-
{description !== undefined && <p className="mt-1 text-sm text-muted-foreground">{description}</p>}
|
|
41
48
|
</div>
|
|
42
49
|
{children}
|
|
43
50
|
</div>
|
|
@@ -104,7 +104,7 @@ export function SidebarNav({ groups, activeId, onSelect, navLabel = 'Navegação
|
|
|
104
104
|
{!collapsed && titled(group.label) && <div className="px-2.5 pb-1 text-xs font-medium text-muted-foreground/70">{group.label}</div>}
|
|
105
105
|
{group.items?.map((item) => renderItem(item))}
|
|
106
106
|
{group.subgroups?.map((subgroup, si) => !subFilled(subgroup) ? null : <div key={si} className="space-y-0.5">
|
|
107
|
-
{!collapsed && titled(subgroup.label) && <div className="pb-0.5 pl-4 pr-2.5 pt-1.5 text-[
|
|
107
|
+
{!collapsed && titled(subgroup.label) && <div className="pb-0.5 pl-4 pr-2.5 pt-1.5 text-[0.6875rem] font-medium text-muted-foreground/50">{subgroup.label}</div>}
|
|
108
108
|
{subgroup.items.map((item) => renderItem(item, titled(subgroup.label)))}
|
|
109
109
|
</div>)}
|
|
110
110
|
</div>)}
|
|
@@ -33,7 +33,7 @@ function AccordionTrigger({
|
|
|
33
33
|
<AccordionPrimitive.Trigger
|
|
34
34
|
data-slot="accordion-trigger"
|
|
35
35
|
className={cn(
|
|
36
|
-
"flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-[
|
|
36
|
+
"flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
|
37
37
|
className
|
|
38
38
|
)}
|
|
39
39
|
{...props}
|
|
@@ -31,11 +31,11 @@ export const buttonVariants = cva(
|
|
|
31
31
|
sm: 'h-8 gap-1.5 px-3 has-[>svg]:px-2.5',
|
|
32
32
|
lg: 'h-10 px-6 has-[>svg]:px-4',
|
|
33
33
|
icon: 'size-9',
|
|
34
|
-
// Ícone na escala `sm` (
|
|
35
|
-
//
|
|
36
|
-
//
|
|
34
|
+
// Ícone na escala `sm` (2rem) — o par do `sm` para fileiras densas, onde os irmãos
|
|
35
|
+
// também usam `sm`. Sem ele, um botão de ícone ao lado desses controles ficava
|
|
36
|
+
// 0.25rem mais alto ou virava override local.
|
|
37
37
|
'icon-sm': 'size-8',
|
|
38
|
-
// Ícone
|
|
38
|
+
// Ícone menor (1.5rem) — a ação dentro de um campo (clear/mostrar senha no
|
|
39
39
|
// `trailing` do Input/Select), que precisa caber folgado num h-9. É o mesmo tamanho
|
|
40
40
|
// do `icon-xs` do InputGroupButton; sem ele, o exemplo virava `className="size-6"`.
|
|
41
41
|
'icon-xs': 'size-6',
|
|
@@ -72,7 +72,7 @@ function Calendar({
|
|
|
72
72
|
defaultClassNames.dropdowns
|
|
73
73
|
),
|
|
74
74
|
dropdown_root: cn(
|
|
75
|
-
"relative rounded-md border border-input has-focus:border-ring has-focus:ring-[
|
|
75
|
+
"relative rounded-md border border-input has-focus:border-ring has-focus:ring-[0.1875rem] has-focus:ring-ring/50",
|
|
76
76
|
defaultClassNames.dropdown_root
|
|
77
77
|
),
|
|
78
78
|
dropdown: cn(
|
|
@@ -206,7 +206,7 @@ function CalendarDayButton({
|
|
|
206
206
|
data-range-end={modifiers.range_end}
|
|
207
207
|
data-range-middle={modifiers.range_middle}
|
|
208
208
|
className={cn(
|
|
209
|
-
"flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[
|
|
209
|
+
"flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[0.1875rem] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-accent-foreground [&>span]:text-xs [&>span]:opacity-70",
|
|
210
210
|
defaultClassNames.day,
|
|
211
211
|
className
|
|
212
212
|
)}
|
|
@@ -136,7 +136,7 @@ const ChatTranscript = React.memo(function ChatTranscript({
|
|
|
136
136
|
className="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-4"
|
|
137
137
|
>
|
|
138
138
|
{items.length === 0 && !indicatorVisible && greeting !== undefined && (
|
|
139
|
-
<div data-slot="chat-empty" className="m-auto max-w-[
|
|
139
|
+
<div data-slot="chat-empty" className="m-auto max-w-[17.5rem] text-center text-muted-foreground">
|
|
140
140
|
<div className="mb-2 text-4xl">✦</div>
|
|
141
141
|
<p className="text-sm leading-relaxed">{greeting}</p>
|
|
142
142
|
</div>
|
|
@@ -12,7 +12,7 @@ function Checkbox({
|
|
|
12
12
|
<CheckboxPrimitive.Root
|
|
13
13
|
data-slot="checkbox"
|
|
14
14
|
className={cn(
|
|
15
|
-
"peer size-4 shrink-0 rounded-[
|
|
15
|
+
"peer size-4 shrink-0 rounded-[0.25rem] border border-input transition-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 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-[state=checked]:bg-primary",
|
|
16
16
|
className
|
|
17
17
|
)}
|
|
18
18
|
{...props}
|
|
@@ -88,7 +88,7 @@ function CommandList({
|
|
|
88
88
|
<CommandPrimitive.List
|
|
89
89
|
data-slot="command-list"
|
|
90
90
|
className={cn(
|
|
91
|
-
"max-h-[
|
|
91
|
+
"max-h-[18.75rem] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
|
92
92
|
className
|
|
93
93
|
)}
|
|
94
94
|
{...props}
|
|
@@ -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',
|
|
@@ -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>
|
|
@@ -154,7 +154,7 @@ function NativeSelect({
|
|
|
154
154
|
onChange={(e) => onChange(e.target.value)}
|
|
155
155
|
className={cn(
|
|
156
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",
|
|
157
|
-
'focus-visible:border-ring focus-visible:ring-[
|
|
157
|
+
'focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50',
|
|
158
158
|
'aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40',
|
|
159
159
|
icon ? 'pl-9' : '',
|
|
160
160
|
className,
|
|
@@ -468,7 +468,7 @@ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.Rea
|
|
|
468
468
|
// Ação no fim do campo é SECUNDÁRIA: o tom muted mora no slot (o botão
|
|
469
469
|
// ghost herda), pra não sprinklar `text-muted-foreground` em cada call
|
|
470
470
|
// site. O hover acende via o próprio `hover:text-accent-foreground` do ghost.
|
|
471
|
-
// Botão trailing na
|
|
471
|
+
// Botão trailing na borda: aproxima 0.375rem para equilibrar o inset.
|
|
472
472
|
// direito casar com o vertical (botão quadrado no canto), como o InputGroup.
|
|
473
473
|
// Com chevron, NÃO puxa — senão cola no chevron (o `gap-1.5` do control).
|
|
474
474
|
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}
|
|
@@ -68,7 +68,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|
|
68
68
|
<th
|
|
69
69
|
data-slot="table-head"
|
|
70
70
|
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-[
|
|
71
|
+
"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
72
|
className
|
|
73
73
|
)}
|
|
74
74
|
{...props}
|
|
@@ -81,7 +81,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
|
|
81
81
|
<td
|
|
82
82
|
data-slot="table-cell"
|
|
83
83
|
className={cn(
|
|
84
|
-
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[
|
|
84
|
+
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[0.125rem]",
|
|
85
85
|
className
|
|
86
86
|
)}
|
|
87
87
|
{...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
|
)
|
|
@@ -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
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
## Esqueleto de página
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use `Page` para manter título, contexto, ações e conteúdo no mesmo ritmo visual nas telas do
|
|
4
|
+
back-office. As ações ficam à direita e acompanham a base do conjunto formado pelo título e pela
|
|
5
|
+
descrição. O componente ocupa a largura disponível; passe `className="max-w-5xl"` quando a tela
|
|
6
|
+
pedir conteúdo mais estreito e centralizado. A área abaixo do cabeçalho permanece livre para
|
|
7
|
+
tabelas, cards ou outras composições.
|
|
4
8
|
|
|
5
9
|
```tsx preview col
|
|
6
10
|
render(
|
|
@@ -8,6 +12,7 @@ render(
|
|
|
8
12
|
<Page
|
|
9
13
|
title="Workspaces"
|
|
10
14
|
count={3}
|
|
15
|
+
description="Ambientes compartilhados pela equipe."
|
|
11
16
|
actions={
|
|
12
17
|
<Button>
|
|
13
18
|
<Plus /> Novo workspace
|
|
@@ -29,6 +34,6 @@ render(
|
|
|
29
34
|
| `title` | `string` | | O h1 da página. |
|
|
30
35
|
| `count` | `number` | | Total de itens ao lado do título (mono, esmaecido). |
|
|
31
36
|
| `description` | `ReactNode` | | Linha de contexto sob o título (ex.: "N no total · X ativos"). |
|
|
32
|
-
| `actions` | `ReactNode` | |
|
|
37
|
+
| `actions` | `ReactNode` | | Ações à direita do cabeçalho, alinhadas à base do título e da descrição. |
|
|
33
38
|
| `className` | `string` | largura cheia | Classes do container (`max-w-5xl` estreita e centra). |
|
|
34
39
|
| `children` | `ReactNode` | | O conteúdo — espaçamento e diagramação são seus. |
|
|
@@ -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">
|
|
@@ -101,11 +101,11 @@ render(
|
|
|
101
101
|
)
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
##
|
|
104
|
+
## Escala relativa
|
|
105
105
|
|
|
106
|
-
>
|
|
107
|
-
>
|
|
108
|
-
>
|
|
106
|
+
> O Opus não define a fonte raiz. Cada `rem` acompanha o documento, portanto o navegador e a
|
|
107
|
+
> aplicação continuam responsáveis pela densidade. Uma aplicação pode ajustar `font-size` em
|
|
108
|
+
> `html`; texto, espaçamento, radius, foco e elevação acompanham a mesma decisão.
|
|
109
109
|
|
|
110
110
|
## Radius
|
|
111
111
|
|
|
@@ -118,12 +118,12 @@ render(
|
|
|
118
118
|
render(
|
|
119
119
|
<div className="flex flex-wrap gap-4">
|
|
120
120
|
{[
|
|
121
|
-
['rounded-xs', 'radius −
|
|
122
|
-
['rounded-sm', 'radius −
|
|
123
|
-
['rounded-md', 'radius −
|
|
121
|
+
['rounded-xs', 'radius − 0.375rem'],
|
|
122
|
+
['rounded-sm', 'radius − 0.25rem'],
|
|
123
|
+
['rounded-md', 'radius − 0.125rem'],
|
|
124
124
|
['rounded-lg', 'radius (0.75rem)'],
|
|
125
|
-
['rounded-xl', 'radius +
|
|
126
|
-
['rounded-2xl', 'radius +
|
|
125
|
+
['rounded-xl', 'radius + 0.25rem'],
|
|
126
|
+
['rounded-2xl', 'radius + 0.5rem'],
|
|
127
127
|
].map(([cls, calc]) => (
|
|
128
128
|
<div key={cls} className="flex items-center gap-3">
|
|
129
129
|
<div className={'h-12 w-12 border-2 border-foreground/30 bg-muted ' + cls} />
|
package/src/ui/docs/doc.tsx
CHANGED
|
@@ -30,7 +30,7 @@ function InlineMd({ text }: { text: string }): React.ReactElement {
|
|
|
30
30
|
{parts.map((part, i) =>
|
|
31
31
|
i % 2 === 1 ? (
|
|
32
32
|
// eslint-disable-next-line react/no-array-index-key
|
|
33
|
-
<code key={i} className="rounded-[calc(var(--radius)-
|
|
33
|
+
<code key={i} className="rounded-[calc(var(--radius)-0.25rem)] bg-muted px-1.5 py-0.5 font-mono text-[0.85em]">
|
|
34
34
|
{part}
|
|
35
35
|
</code>
|
|
36
36
|
) : (
|
package/src/ui/meta.ts
CHANGED
|
@@ -33,7 +33,7 @@ export const componentMeta = {
|
|
|
33
33
|
name: 'button',
|
|
34
34
|
ancestry: 'shadcn',
|
|
35
35
|
whenToUse:
|
|
36
|
-
'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
|
|
36
|
+
'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 para os casos só-ícone, 2.25/2/1.5rem) e `shape="pill"` troca somente a geometria, sem alterar cor ou hierarquia. `busy` mostra spinner e desabilita. `asChild` renderiza como outro elemento.',
|
|
37
37
|
},
|
|
38
38
|
'card': {
|
|
39
39
|
name: 'card',
|
|
@@ -116,7 +116,7 @@ export const componentMeta = {
|
|
|
116
116
|
name: 'select',
|
|
117
117
|
ancestry: 'opus',
|
|
118
118
|
whenToUse:
|
|
119
|
-
'
|
|
119
|
+
'Seletor para escolhas em lista. Recebe `options` no formato { value, label, hint?, content?, group? }, sem JSX por item. Os modos são definidos por `searchable`, `multiple`, `onSearch` e `native`. `variant="default"` funciona como campo de formulário; `outline` envolve o conteúdo com borda; `ghost` o envolve sem borda. `size` segue a régua inline (default 2.25rem/sm 2rem), e `shape="pill"` altera somente a geometria. Também aceita `icon`, `trailing`, `triggerLabel` e `clearable`.',
|
|
120
120
|
},
|
|
121
121
|
'separator': {
|
|
122
122
|
name: 'separator',
|
package/src/ui/theme.css
CHANGED
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
--destructive-foreground: hsl(0 0% 98%);
|
|
50
50
|
--border: hsl(0 0% 92.5%);
|
|
51
51
|
--input: hsl(0 0% 92.5%);
|
|
52
|
-
/* Ring de foco LEVE (pedido da casa): aro sutil, não um halo. Com ring-[
|
|
52
|
+
/* Ring de foco LEVE (pedido da casa): aro sutil, não um halo. Com ring-[0.1875rem] + /50,
|
|
53
53
|
~86% encosta na borda (89.8%) — um sussurro de foco. */
|
|
54
54
|
--ring: hsl(0 0% 86%);
|
|
55
55
|
--radius: 0.75rem;
|
|
@@ -134,23 +134,23 @@
|
|
|
134
134
|
--color-input: var(--input);
|
|
135
135
|
--color-ring: var(--ring);
|
|
136
136
|
|
|
137
|
-
--radius-xs: calc(var(--radius) -
|
|
138
|
-
--radius-sm: calc(var(--radius) -
|
|
139
|
-
--radius-md: calc(var(--radius) -
|
|
137
|
+
--radius-xs: calc(var(--radius) - 0.375rem);
|
|
138
|
+
--radius-sm: calc(var(--radius) - 0.25rem);
|
|
139
|
+
--radius-md: calc(var(--radius) - 0.125rem);
|
|
140
140
|
--radius-lg: var(--radius);
|
|
141
|
-
--radius-xl: calc(var(--radius) +
|
|
142
|
-
--radius-2xl: calc(var(--radius) +
|
|
141
|
+
--radius-xl: calc(var(--radius) + 0.25rem);
|
|
142
|
+
--radius-2xl: calc(var(--radius) + 0.5rem);
|
|
143
143
|
|
|
144
144
|
/* Escala Tailwind, com receita direta para o compilador preservar modifiers `/opacity`.
|
|
145
145
|
`light-dark()` mantém o peso adequado em cada tema sem uma variável intermediária. */
|
|
146
|
-
--shadow-2xs: 0
|
|
147
|
-
--shadow-xs: 0
|
|
148
|
-
--shadow-sm: 0
|
|
149
|
-
--shadow: 0
|
|
150
|
-
--shadow-md: 0
|
|
151
|
-
--shadow-lg: 0
|
|
152
|
-
--shadow-xl: 0
|
|
153
|
-
--shadow-2xl: 0
|
|
146
|
+
--shadow-2xs: 0 0.0625rem 0.125rem -0.0625rem light-dark(rgb(15 15 15 / 0.03), rgb(0 0 0 / 0.12));
|
|
147
|
+
--shadow-xs: 0 0.0625rem 0.125rem -0.0625rem light-dark(rgb(15 15 15 / 0.025), rgb(0 0 0 / 0.12)), 0 0.125rem 0.25rem -0.125rem light-dark(rgb(15 15 15 / 0.035), rgb(0 0 0 / 0.18));
|
|
148
|
+
--shadow-sm: 0 0.125rem 0.25rem -0.125rem light-dark(rgb(15 15 15 / 0.025), rgb(0 0 0 / 0.16)), 0 0.25rem 0.5rem -0.25rem light-dark(rgb(15 15 15 / 0.04), rgb(0 0 0 / 0.28));
|
|
149
|
+
--shadow: 0 0.125rem 0.375rem -0.125rem light-dark(rgb(15 15 15 / 0.03), rgb(0 0 0 / 0.16)), 0 0.375rem 0.875rem -0.375rem light-dark(rgb(15 15 15 / 0.05), rgb(0 0 0 / 0.29));
|
|
150
|
+
--shadow-md: 0 0.25rem 0.5rem -0.25rem light-dark(rgb(15 15 15 / 0.03), rgb(0 0 0 / 0.16)), 0 0.625rem 1.75rem -0.5rem light-dark(rgb(15 15 15 / 0.065), rgb(0 0 0 / 0.3));
|
|
151
|
+
--shadow-lg: 0 0.375rem 1rem -0.375rem light-dark(rgb(15 15 15 / 0.03), rgb(0 0 0 / 0.18)), 0 1.125rem 3rem -0.75rem light-dark(rgb(15 15 15 / 0.07), rgb(0 0 0 / 0.3));
|
|
152
|
+
--shadow-xl: 0 0.5rem 1.5rem -0.625rem light-dark(rgb(15 15 15 / 0.04), rgb(0 0 0 / 0.2)), 0 1.5rem 4rem -1.125rem light-dark(rgb(15 15 15 / 0.08), rgb(0 0 0 / 0.34));
|
|
153
|
+
--shadow-2xl: 0 0.75rem 2rem -0.875rem light-dark(rgb(15 15 15 / 0.05), rgb(0 0 0 / 0.22)), 0 2rem 5.5rem -1.5rem light-dark(rgb(15 15 15 / 0.1), rgb(0 0 0 / 0.38));
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
/* Base canônica do consumidor — vem DAQUI, não do entry de cada app. Tailwind v4
|
|
@@ -160,15 +160,6 @@
|
|
|
160
160
|
* {
|
|
161
161
|
border-color: var(--border);
|
|
162
162
|
}
|
|
163
|
-
/* Base 15px: o rem da casa — TODA a escala rem (texto, spacing, radius) deriva daqui,
|
|
164
|
-
então a UI densifica ~6% vs o default do browser (16px). Histórico (conferido no git):
|
|
165
|
-
até a 2.31.4 o pacote NÃO fixava rem (valia o 16px do browser); a 2.31.5 introduziu
|
|
166
|
-
14px; a 2.35.0 assenta em 15px — valor INÉDITO, não é revert. O 15px veio da prática:
|
|
167
|
-
a Empresa X escolheu 15px por conta própria em DOIS apps, e a exceção reincidente
|
|
168
|
-
virou default. Quem quiser mais denso sobrescreve o `html` no próprio entry. */
|
|
169
|
-
html {
|
|
170
|
-
font-size: 15px;
|
|
171
|
-
}
|
|
172
163
|
/* Canvas da casa: quase-branco vem do BODY — o shell é transparente e as
|
|
173
164
|
superfícies (cards, miolo flush) pintam bg-background por cima. Tinte de 25%
|
|
174
165
|
de muted: separa a superfície do canvas sem virar cinza; segue o tema. */
|
|
@@ -241,7 +232,7 @@
|
|
|
241
232
|
* regra custom vaza pra dentro do not-prose (o CodeBlock ganhava uma SEGUNDA
|
|
242
233
|
* borda no <pre>, colada na do contêiner — a "borda duplicada"). */
|
|
243
234
|
.prose :where(code):not(:where(pre code, [class~='not-prose'], [class~='not-prose'] *)) {
|
|
244
|
-
border-radius: calc(var(--radius) -
|
|
235
|
+
border-radius: calc(var(--radius) - 0.25rem);
|
|
245
236
|
background: var(--muted);
|
|
246
237
|
padding: 0.125rem 0.375rem;
|
|
247
238
|
font-weight: 400;
|