@softize/opus 21.0.0 → 22.0.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/bin/lib/check.mjs +31 -28
  3. package/docs/adr/0004-page-content-state-is-composed.md +3 -2
  4. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +3 -0
  5. package/docs/adr/0006-semantic-context-precedes-visual-variant.md +23 -14
  6. package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +5 -0
  7. package/docs/adr/0013-presentation-is-a-portable-action-oriented-artifact.md +7 -4
  8. package/docs/adr/0016-list-collection-header-belongs-to-content.md +13 -8
  9. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +27 -26
  10. package/docs/adr/0025-content-frame-treatment-does-not-change-semantics.md +1 -0
  11. package/docs/adr/0029-page-header-is-the-single-page-introduction.md +71 -0
  12. package/docs/adr/0030-framed-content-can-use-sectioned-anatomy.md +89 -0
  13. package/docs/adr/0031-detail-dividers-own-the-tabular-label-treatment.md +61 -0
  14. package/docs/adr/0032-tab-baseline-respects-the-container-inset.md +59 -0
  15. package/docs/adr/0033-drawer-uses-an-inset-structural-frame.md +72 -0
  16. package/docs/adr/0034-tabs-can-own-a-frame-without-owning-a-surface.md +60 -0
  17. package/docs/adr/0035-button-group-separates-frame-relationship-and-distribution.md +63 -0
  18. package/docs/adr/0036-tabs-declares-trigger-distribution.md +58 -0
  19. package/docs/ownership-vs-shadcn-lock.md +5 -0
  20. package/docs/radius-scale.md +5 -2
  21. package/package.json +1 -1
  22. package/registry/skills/build-opus-ui/SKILL.md +9 -6
  23. package/registry/skills/build-opus-ui/references/evaluations.md +5 -2
  24. package/registry/skills/build-opus-ui/references/ui-patterns.md +14 -16
  25. package/src/queue/drivers/bullmq.ts +15 -2
  26. package/src/ui/components/patterns/content-header.tsx +51 -13
  27. package/src/ui/components/patterns/list.tsx +51 -33
  28. package/src/ui/components/patterns/page.tsx +139 -251
  29. package/src/ui/components/patterns/presentation.tsx +2 -6
  30. package/src/ui/components/primitives/button-group.tsx +80 -8
  31. package/src/ui/components/primitives/button.tsx +85 -60
  32. package/src/ui/components/primitives/detail.tsx +63 -57
  33. package/src/ui/components/primitives/dialog.tsx +8 -11
  34. package/src/ui/components/primitives/drawer.tsx +16 -12
  35. package/src/ui/components/primitives/tabs.tsx +123 -54
  36. package/src/ui/docs/content/button.md +55 -29
  37. package/src/ui/docs/content/content.md +61 -36
  38. package/src/ui/docs/content/detail.md +28 -19
  39. package/src/ui/docs/content/drawer.md +53 -47
  40. package/src/ui/docs/content/input.md +51 -35
  41. package/src/ui/docs/content/page.md +84 -95
  42. package/src/ui/docs/content/pane.md +83 -63
  43. package/src/ui/docs/content/presentation.md +20 -20
  44. package/src/ui/docs/content/tabs.md +107 -36
  45. package/src/ui/meta.ts +5 -5
  46. package/src/ui/react.tsx +0 -1
@@ -17,10 +17,7 @@ import {
17
17
  SurfaceMedia,
18
18
  type SurfaceMediaProps,
19
19
  } from "../patterns/surface-media.tsx";
20
- import {
21
- CloseButton,
22
- type CloseButtonSize,
23
- } from "./close-button.tsx";
20
+ import { CloseButton, type CloseButtonSize } from "./close-button.tsx";
24
21
  import { overlayClasses, preventDismissWhen } from "./dialog.tsx";
25
22
 
26
23
  interface DrawerContentContextValue {
@@ -117,15 +114,15 @@ function DrawerContent({
117
114
  data-slot="drawer-content"
118
115
  data-layout={layout}
119
116
  className={cn(
120
- "fixed z-50 flex flex-col bg-background shadow-lg transition ease-in-out [--surface-background:var(--background)] data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
117
+ "fixed z-50 flex flex-col overflow-hidden rounded-xl border bg-background shadow-lg transition ease-in-out [--surface-background:var(--background)] data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
121
118
  side === "right" &&
122
- "inset-y-0 right-0 h-full w-3/4 max-w-sm border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right",
119
+ "inset-y-1 right-1 h-[calc(100%-0.5rem)] w-3/4 max-w-sm data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right",
123
120
  side === "left" &&
124
- "inset-y-0 left-0 h-full w-3/4 max-w-sm border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left",
121
+ "inset-y-1 left-1 h-[calc(100%-0.5rem)] w-3/4 max-w-sm data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left",
125
122
  side === "top" &&
126
- "inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
123
+ "inset-x-1 top-1 h-auto data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
127
124
  side === "bottom" &&
128
- "inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
125
+ "inset-x-1 bottom-1 h-auto data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
129
126
  className,
130
127
  )}
131
128
  aria-describedby={ariaDescribedBy}
@@ -189,7 +186,10 @@ function DrawerMedia(props: SurfaceMediaProps) {
189
186
  }
190
187
 
191
188
  /** Região introdutória do header — o voltar de um fluxo modal encadeado. */
192
- function DrawerNavigation({ className, ...props }: React.ComponentProps<"div">) {
189
+ function DrawerNavigation({
190
+ className,
191
+ ...props
192
+ }: React.ComponentProps<"div">) {
193
193
  return (
194
194
  <div
195
195
  data-slot="drawer-navigation-content"
@@ -210,7 +210,11 @@ function DrawerActions({ className, ...props }: React.ComponentProps<"div">) {
210
210
  );
211
211
  }
212
212
 
213
- function DrawerBody({ className, style, ...props }: React.ComponentProps<"div">) {
213
+ function DrawerBody({
214
+ className,
215
+ style,
216
+ ...props
217
+ }: React.ComponentProps<"div">) {
214
218
  const layout = useSurfaceLayout("drawer");
215
219
  return (
216
220
  <div
@@ -220,7 +224,7 @@ function DrawerBody({ className, style, ...props }: React.ComponentProps<"div">)
220
224
  surfaceRegionClasses("drawer", layout, "body"),
221
225
  className,
222
226
  )}
223
- style={{ "--opus-inline-gutter": "1rem", ...style } as React.CSSProperties}
227
+ style={style}
224
228
  {...props}
225
229
  />
226
230
  );
@@ -1,43 +1,65 @@
1
- import * as React from "react"
2
- import { cva, type VariantProps } from "class-variance-authority"
3
- import { Tabs as TabsPrimitive } from "radix-ui"
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { Tabs as TabsPrimitive } from "radix-ui";
4
4
 
5
- import { cn } from '../../lib/cn.ts'
6
- import { controlHeight, type ControlSize } from './control.ts'
5
+ import { cn } from "../../lib/cn.ts";
6
+ import { controlHeight, type ControlSize } from "./control.ts";
7
7
 
8
8
  // Divergência da casa (ejetado): o Tabs ganhou `size` — Button e Select têm `sm`, o Tabs
9
9
  // não tinha, e uma fileira densa ficava com o segmento 0.25rem mais alto
10
10
  // que os irmãos. O tamanho flui pra TabsList via contexto (a altura é dela) e a medida é a
11
11
  // da escala de control.ts.
12
- type TabsSize = Extract<ControlSize, "default" | "sm">
13
- const TabsSizeContext = React.createContext<TabsSize>("default")
14
- type TabsOrientation = "horizontal" | "vertical"
15
- const TabsOrientationContext = React.createContext<TabsOrientation>("horizontal")
16
- const TabsListVariantContext = React.createContext<"default" | "line">("default")
12
+ type TabsSize = Extract<ControlSize, "default" | "sm">;
13
+ type TabsDistribution = "content" | "equal";
14
+ const TabsSizeContext = React.createContext<TabsSize>("default");
15
+ const TabsDistributionContext =
16
+ React.createContext<TabsDistribution>("content");
17
+ const TabsFramedContext = React.createContext(false);
18
+ type TabsOrientation = "horizontal" | "vertical";
19
+ const TabsOrientationContext =
20
+ React.createContext<TabsOrientation>("horizontal");
21
+ const TabsListVariantContext = React.createContext<"default" | "line">(
22
+ "default",
23
+ );
17
24
 
18
25
  function Tabs({
19
26
  className,
20
27
  orientation = "horizontal",
21
28
  size = "default",
29
+ distribution = "content",
30
+ framed = false,
22
31
  ...props
23
- }: React.ComponentProps<typeof TabsPrimitive.Root> & { size?: TabsSize }) {
32
+ }: React.ComponentProps<typeof TabsPrimitive.Root> & {
33
+ size?: TabsSize;
34
+ /** Distribui os gatilhos pela largura do conjunto ou preserva a largura do conteúdo. */
35
+ distribution?: TabsDistribution;
36
+ /** Delimita a composição sem criar uma nova superfície. */
37
+ framed?: boolean;
38
+ }) {
24
39
  return (
25
40
  <TabsOrientationContext.Provider value={orientation}>
26
- <TabsSizeContext.Provider value={size}>
27
- <TabsPrimitive.Root
28
- data-slot="tabs"
29
- data-orientation={orientation}
30
- data-size={size}
31
- orientation={orientation}
32
- className={cn(
33
- "group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
34
- className
35
- )}
36
- {...props}
37
- />
38
- </TabsSizeContext.Provider>
41
+ <TabsDistributionContext.Provider value={distribution}>
42
+ <TabsFramedContext.Provider value={framed}>
43
+ <TabsSizeContext.Provider value={size}>
44
+ <TabsPrimitive.Root
45
+ data-slot="tabs"
46
+ data-orientation={orientation}
47
+ data-size={size}
48
+ data-distribution={distribution}
49
+ data-framed={framed}
50
+ orientation={orientation}
51
+ className={cn(
52
+ "group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
53
+ framed && "overflow-hidden rounded-xl border",
54
+ className,
55
+ )}
56
+ {...props}
57
+ />
58
+ </TabsSizeContext.Provider>
59
+ </TabsFramedContext.Provider>
60
+ </TabsDistributionContext.Provider>
39
61
  </TabsOrientationContext.Provider>
40
- )
62
+ );
41
63
  }
42
64
 
43
65
  const tabsListVariants = cva(
@@ -49,14 +71,14 @@ const tabsListVariants = cva(
49
71
  variants: {
50
72
  variant: {
51
73
  default: "bg-muted",
52
- line: "gap-5 bg-transparent",
74
+ line: "gap-1 bg-transparent",
53
75
  },
54
76
  },
55
77
  defaultVariants: {
56
78
  variant: "default",
57
79
  },
58
- }
59
- )
80
+ },
81
+ );
60
82
 
61
83
  function TabsList({
62
84
  className,
@@ -67,20 +89,33 @@ function TabsList({
67
89
  }: React.ComponentProps<typeof TabsPrimitive.List> &
68
90
  VariantProps<typeof tabsListVariants>) {
69
91
  // O size vem do <Tabs> (contexto) — a altura mora aqui, na list.
70
- const size = React.useContext(TabsSizeContext)
71
- const orientation = React.useContext(TabsOrientationContext)
72
- const normalizedVariant = variant ?? "default"
73
- const hasHorizontalLine = normalizedVariant === "line" && orientation === "horizontal"
92
+ const size = React.useContext(TabsSizeContext);
93
+ const orientation = React.useContext(TabsOrientationContext);
94
+ const distribution = React.useContext(TabsDistributionContext);
95
+ const normalizedVariant = variant ?? "default";
96
+ const hasHorizontalLine =
97
+ normalizedVariant === "line" && orientation === "horizontal";
74
98
  return (
75
99
  <TabsListVariantContext.Provider value={normalizedVariant}>
76
100
  <TabsPrimitive.List
77
101
  data-slot="tabs-list"
78
102
  data-variant={normalizedVariant}
79
103
  data-size={size}
80
- className={cn(tabsListVariants({ variant: normalizedVariant }), controlHeight[size], className)}
104
+ data-distribution={distribution}
105
+ className={cn(
106
+ tabsListVariants({ variant: normalizedVariant }),
107
+ controlHeight[size],
108
+ distribution === "equal" && orientation === "horizontal" && "w-full",
109
+ className,
110
+ )}
81
111
  style={{
82
112
  ...(hasHorizontalLine
83
- ? { position: "relative", width: "100%", height: "auto", justifyContent: "flex-start" }
113
+ ? {
114
+ position: "relative",
115
+ width: "100%",
116
+ height: "auto",
117
+ justifyContent: "flex-start",
118
+ }
84
119
  : {}),
85
120
  ...style,
86
121
  }}
@@ -92,7 +127,7 @@ function TabsList({
92
127
  data-slot="tabs-line"
93
128
  style={{
94
129
  position: "absolute",
95
- insetInline: "calc(var(--opus-inline-gutter, 0rem) * -1)",
130
+ insetInline: "0rem",
96
131
  bottom: 0,
97
132
  borderBottomWidth: "0.0625rem",
98
133
  borderBottomStyle: "solid",
@@ -104,7 +139,7 @@ function TabsList({
104
139
  {children}
105
140
  </TabsPrimitive.List>
106
141
  </TabsListVariantContext.Provider>
107
- )
142
+ );
108
143
  }
109
144
 
110
145
  function TabsTrigger({
@@ -115,38 +150,72 @@ function TabsTrigger({
115
150
  ...props
116
151
  }: React.ComponentProps<typeof TabsPrimitive.Trigger> & {
117
152
  /** Ícone decorativo antes do rótulo. Em uma aba sem texto, declare também `aria-label`. */
118
- icon?: React.ReactNode
153
+ icon?: React.ReactNode;
119
154
  }) {
120
- const variant = React.useContext(TabsListVariantContext)
121
- const orientation = React.useContext(TabsOrientationContext)
122
- const hasHorizontalLine = variant === "line" && orientation === "horizontal"
155
+ const variant = React.useContext(TabsListVariantContext);
156
+ const orientation = React.useContext(TabsOrientationContext);
157
+ const size = React.useContext(TabsSizeContext);
158
+ const distribution = React.useContext(TabsDistributionContext);
159
+ const framed = React.useContext(TabsFramedContext);
160
+ const hasHorizontalLine = variant === "line" && orientation === "horizontal";
161
+ const content = (
162
+ <>
163
+ {icon !== undefined && (
164
+ <span
165
+ data-slot="tabs-trigger-icon"
166
+ aria-hidden="true"
167
+ className="flex size-4 shrink-0 items-center justify-center [&_svg]:size-4"
168
+ >
169
+ {icon}
170
+ </span>
171
+ )}
172
+ {children}
173
+ </>
174
+ );
123
175
  return (
124
176
  <TabsPrimitive.Trigger
125
177
  data-slot="tabs-trigger"
126
178
  // px: altura e indicador compensam a hairline do contêiner (um pixel físico, não escala).
127
179
  className={cn(
128
180
  "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",
129
- "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",
181
+ "dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent",
130
182
  "data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:text-foreground",
131
- "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",
132
- hasHorizontalLine && "px-0 py-3",
133
- className
183
+ "after:absolute after:bg-primary 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",
184
+ hasHorizontalLine &&
185
+ cn(
186
+ "group/tab-trigger rounded-none px-0 py-0 hover:bg-transparent active:brightness-100 data-[state=active]:bg-transparent data-[state=active]:hover:bg-transparent",
187
+ framed ? "pb-2" : "pb-1.5",
188
+ ),
189
+ className,
134
190
  )}
135
- style={{ ...(hasHorizontalLine ? { flex: "none", height: "auto" } : {}), ...style }}
191
+ style={{
192
+ ...(hasHorizontalLine
193
+ ? {
194
+ flex: distribution === "equal" ? "1 1 0%" : "none",
195
+ height: "auto",
196
+ minWidth: distribution === "equal" ? 0 : undefined,
197
+ }
198
+ : {}),
199
+ ...style,
200
+ }}
136
201
  {...props}
137
202
  >
138
- {icon !== undefined && (
203
+ {hasHorizontalLine ? (
139
204
  <span
140
- data-slot="tabs-trigger-icon"
141
- aria-hidden="true"
142
- className="flex size-4 shrink-0 items-center justify-center [&_svg]:size-4"
205
+ data-slot="tabs-trigger-content"
206
+ className={cn(
207
+ "inline-flex items-center justify-center gap-1.5 rounded-md px-3 transition-colors hover:bg-accent hover:text-accent-foreground active:brightness-90 group-data-[state=active]/tab-trigger:text-foreground",
208
+ distribution === "equal" && "w-full",
209
+ controlHeight[size],
210
+ )}
143
211
  >
144
- {icon}
212
+ {content}
145
213
  </span>
214
+ ) : (
215
+ content
146
216
  )}
147
- {children}
148
217
  </TabsPrimitive.Trigger>
149
- )
218
+ );
150
219
  }
151
220
 
152
221
  function TabsContent({
@@ -159,7 +228,7 @@ function TabsContent({
159
228
  className={cn("flex-1 outline-none", className)}
160
229
  {...props}
161
230
  />
162
- )
231
+ );
163
232
  }
164
233
 
165
- export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
234
+ export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
@@ -1,8 +1,9 @@
1
1
  ## Contexto e variante
2
2
 
3
- Use `Button` para iniciar uma ação. `context` declara a hierarquia ou o risco da ação; `variant`
4
- escolhe o tratamento visual. Use `primary` para a ação principal, `neutral` para ações de apoio e
5
- `danger` quando a ação tiver uma consequência perigosa. A variante `outline` mantém o fundo
3
+ Use `Button` para iniciar uma ação. `context` declara a hierarquia, a cautela ou o risco da ação;
4
+ `variant` escolhe o tratamento visual. Use `primary` para a ação principal, `neutral` para ações de
5
+ apoio, `warning` quando a pessoa puder prosseguir mas o efeito merecer atenção e `danger` quando a
6
+ ação tiver uma consequência perigosa. A variante `outline` mantém o fundo
6
7
  transparente em repouso e usa uma superfície apenas no hover, preservando o fundo da região onde o
7
8
  botão está inserido. `subtle` preenche o botão com um tom suave do contexto, para ações que precisam
8
9
  de presença sem competir com a principal.
@@ -13,6 +14,7 @@ de presença sem competir com a principal.
13
14
  <Button variant="subtle">Convidar pessoa</Button>
14
15
  <Button context="neutral" variant="outline">Ver prévia</Button>
15
16
  <Button context="neutral" variant="ghost">Cancelar</Button>
17
+ <Button context="warning" variant="ghost">Registrar tentativa sem sucesso</Button>
16
18
  <Button context="danger" variant="solid">Excluir</Button>
17
19
  <Button context="primary" variant="link">Ver documentação</Button>
18
20
  ```
@@ -27,16 +29,16 @@ há texto visível. O glifo dentro do controle acompanha o tamanho (0.875rem em
27
29
  `icon-xs`, `icon-sm` e `icon`; 1rem em `default`; 1.25rem em `lg` e `icon-lg`), a menos que o
28
30
  ícone traga um `size-*` próprio. O quadrado `icon` mantém a área de 2.25rem com o glifo discreto.
29
31
 
30
- | Nome | Medida | Uso |
31
- | --------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- |
32
- | `xs` | 1.5rem | Ação dentro de um campo (`InputGroupButton`). |
33
- | `sm` | 2rem | Operação densa em toolbar ou dentro de uma seção, ao lado de `Select` e `Tabs` `sm`. |
32
+ | Nome | Medida | Uso |
33
+ | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------- |
34
+ | `xs` | 1.5rem | Ação dentro de um campo (`InputGroupButton`). |
35
+ | `sm` | 2rem | Operação densa em toolbar ou dentro de uma seção, ao lado de `Select` e `Tabs` `sm`. |
34
36
  | `default` | 2.25rem | Decisão da superfície: header de Page, criação no cabeçalho de uma coleção e footer de Dialog ou Drawer; é também a linha de `Input`. |
35
- | `lg` | 2.5rem | Chamada principal com mais área de toque. |
36
- | `icon-xs` | 1.5rem | Ação só de ícone dentro de um campo ou de uma linha densa; é o quadrado de `ActionTrigger` com `icon`. |
37
- | `icon-sm` | 1.75rem | Ação só de ícone em composições compactas que precisam de mais presença; também é o quadrado das setas do pager de `ActionList`. |
38
- | `icon` | 2.25rem | Ação só de ícone na linha padrão. |
39
- | `icon-lg` | 2.5rem | Ação só de ícone ao lado de um `lg`. |
37
+ | `lg` | 2.5rem | Chamada principal com mais área de toque. |
38
+ | `icon-xs` | 1.5rem | Ação só de ícone dentro de um campo ou de uma linha densa; é o quadrado de `ActionTrigger` com `icon`. |
39
+ | `icon-sm` | 1.75rem | Ação só de ícone em composições compactas que precisam de mais presença; também é o quadrado das setas do pager de `ActionList`. |
40
+ | `icon` | 2.25rem | Ação só de ícone na linha padrão. |
41
+ | `icon-lg` | 2.5rem | Ação só de ícone ao lado de um `lg`. |
40
42
 
41
43
  Escolha o tamanho pela região, não pela importância visual: `variant` e `context` resolvem a
42
44
  hierarquia da ação. Headers de Page, a criação no cabeçalho de uma coleção e footers de
@@ -102,15 +104,15 @@ estilos, use `buttonVariants` para obter as classes.
102
104
 
103
105
  ## Propriedades de Button
104
106
 
105
- | Propriedade | Tipo | Padrão | Descrição |
106
- | ----------- | ------------------------------------------------------------------------------------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
107
- | `context` | `'neutral' \| 'primary' \| 'danger'` | `'primary'` em `solid` e `subtle`; `'neutral'` nas demais variantes | A hierarquia ou o risco comunicado pela ação. |
108
- | `variant` | `'solid' \| 'subtle' \| 'outline' \| 'ghost' \| 'link'` | `'solid'` | O tratamento visual aplicado ao contexto. |
109
- | `size` | `'xs' \| 'sm' \| 'default' \| 'lg' \| 'icon-xs' \| 'icon-sm' \| 'icon' \| 'icon-lg'` | `'default'` | A medida na escala única dos controles (tabela acima). Os `icon-*` são quadrados para botões só de ícone, com `aria-label`. |
110
- | `shape` | `'default' \| 'pill'` | `'default'` | Geometria do botão; `pill` arredonda as extremidades por completo. |
111
- | `asChild` | `boolean` | `false` | Renderiza como o filho (Radix Slot) em vez de `<button>` — para âncoras e afins. |
112
- | `busy` | `boolean` | `false` | Ação em andamento, depois do clique: mostra o `Spinner` e desabilita o botão. Não indica carregamento de conteúdo; para isso, use `DataState` ou `Skeleton`. |
113
- | `icon` | `React.ReactNode` | | Ícone à esquerda, como nó (ex.: `icon={<Plus />}`); o glifo segue o `size`. Com `busy`, dá lugar ao `Spinner`; os dois não aparecem juntos. |
107
+ | Propriedade | Tipo | Padrão | Descrição |
108
+ | ----------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
109
+ | `context` | `'neutral' \| 'primary' \| 'warning' \| 'danger'` | `'primary'` em `solid` e `subtle`; `'neutral'` nas demais variantes | A hierarquia, a cautela ou o risco comunicado pela ação. |
110
+ | `variant` | `'solid' \| 'subtle' \| 'outline' \| 'ghost' \| 'link'` | `'solid'` | O tratamento visual aplicado ao contexto. |
111
+ | `size` | `'xs' \| 'sm' \| 'default' \| 'lg' \| 'icon-xs' \| 'icon-sm' \| 'icon' \| 'icon-lg'` | `'default'` | A medida na escala única dos controles (tabela acima). Os `icon-*` são quadrados para botões só de ícone, com `aria-label`. |
112
+ | `shape` | `'default' \| 'pill'` | `'default'` | Geometria do botão; `pill` arredonda as extremidades por completo. |
113
+ | `asChild` | `boolean` | `false` | Renderiza como o filho (Radix Slot) em vez de `<button>` — para âncoras e afins. |
114
+ | `busy` | `boolean` | `false` | Ação em andamento, depois do clique: mostra o `Spinner` e desabilita o botão. Não indica carregamento de conteúdo; para isso, use `DataState` ou `Skeleton`. |
115
+ | `icon` | `React.ReactNode` | | Ícone à esquerda, como nó (ex.: `icon={<Plus />}`); o glifo segue o `size`. Com `busy`, dá lugar ao `Spinner`; os dois não aparecem juntos. |
114
116
 
115
117
  ## ButtonGroup
116
118
 
@@ -146,6 +148,27 @@ conectar bordas. Esse modo atende ações só com ícone em barras e linhas de l
146
148
  </ButtonGroup>
147
149
  ```
148
150
 
151
+ ### Ações divididas
152
+
153
+ Use `mode="divided"` quando cada controle precisar preservar sua própria interação, mas divisores
154
+ visuais ajudarem a ler o conjunto. O grupo insere os divisores entre os filhos e acompanha a
155
+ orientação automaticamente. Combine `framed` para delimitar o conjunto sem criar um fundo e
156
+ `distribution="equal"` quando as ações precisarem ocupar larguras iguais.
157
+
158
+ ```tsx preview col
159
+ <ButtonGroup framed mode="divided" distribution="equal">
160
+ <Button variant="ghost" icon={<Phone />}>
161
+ Ligar
162
+ </Button>
163
+ <Button variant="ghost" icon={<MessageSquare />}>
164
+ Anotar
165
+ </Button>
166
+ <Button context="danger" variant="ghost" icon={<CircleX />}>
167
+ Encerrar
168
+ </Button>
169
+ </ButtonGroup>
170
+ ```
171
+
149
172
  ### No rodapé de Dialog e Drawer
150
173
 
151
174
  `DialogFooter` e `DrawerFooter` cuidam somente da faixa; envolva as decisões em um
@@ -169,13 +192,15 @@ confirmação. As ações dividem a largura disponível; use `outline` no `Cance
169
192
  ```tsx preview col
170
193
  <div className="w-80">
171
194
  <ButtonGroup mode="spaced" distribution="equal">
172
- <Button context="neutral" variant="outline">Cancelar</Button>
195
+ <Button context="neutral" variant="outline">
196
+ Cancelar
197
+ </Button>
173
198
  <Button context="danger">Excluir</Button>
174
199
  </ButtonGroup>
175
200
  </div>
176
201
  ```
177
202
 
178
- ### Ação dividida
203
+ ### Ação com variações
179
204
 
180
205
  Combine a ação principal, um separador e um botão de ícone quando o mesmo comando oferecer
181
206
  variações.
@@ -227,12 +252,13 @@ semântica de outro elemento, como `label`.
227
252
 
228
253
  ### Propriedades de ButtonGroup
229
254
 
230
- | Propriedade | Tipo | Padrão | Descrição |
231
- | -------------- | ---------------------------- | -------------- | ----------------------------------------------------------------------------------------- |
232
- | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Direção do bloco e do colapso das bordas. |
233
- | `mode` | `'connected' \| 'spaced'` | `'connected'` | Conecta as bordas dos controles ou preserva cada controle com intervalo compacto. |
234
- | `shape` | `'default' \| 'pill'` | `'default'` | Geometria das extremidades externas do grupo. |
235
- | `distribution` | `'content' \| 'equal'` | `'content'` | Mantém a largura do conteúdo ou divide igualmente o espaço disponível entre os controles. |
255
+ | Propriedade | Tipo | Padrão | Descrição |
256
+ | -------------- | -------------------------------------- | -------------- | ----------------------------------------------------------------------------------------- |
257
+ | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Direção do bloco e do colapso das bordas. |
258
+ | `mode` | `'connected' \| 'spaced' \| 'divided'` | `'connected'` | Conecta as bordas, separa por espaço ou insere divisores entre os controles. |
259
+ | `shape` | `'default' \| 'pill'` | `'default'` | Geometria das extremidades externas do grupo. |
260
+ | `distribution` | `'content' \| 'equal'` | `'content'` | Mantém a largura do conteúdo ou divide igualmente o espaço disponível entre os controles. |
261
+ | `framed` | `boolean` | `false` | Aplica borda, raio e padding ao grupo sem definir uma nova superfície. |
236
262
 
237
263
  ### Propriedades de ButtonGroupSeparator
238
264
 
@@ -4,11 +4,11 @@ Use `Content` para dar título e estrutura a uma região dentro de `PageBody`, `
4
4
  `DialogBody` ou outra superfície. Ele renderiza uma `section` ligada ao próprio título e mantém o
5
5
  espaçamento entre header e body. `ContentHeader` pertence sempre a essa estrutura; não o use solto.
6
6
 
7
- O `Content` é contínuo por definição e não expõe `layout`: ele é uma região nomeada dentro de outra
8
- superfície, e a escolha entre regiões seccionadas e contínuas pertence a quem a hospeda — o
9
- [`Card`](/ui/card), o [`Dialog`](/ui/dialog), o [`Drawer`](/ui/drawer) ou o [`Pane`](/ui/pane).
10
- Quando a própria região precisar de uma fronteira visível, use `framed`, descrito adiante. Pelo
11
- mesmo motivo o `Content` não tem rodapé: um rodapé pertence à superfície que a hospeda.
7
+ O `Content` é uma região nomeada dentro de outra superfície. Sem moldura, ele mantém a anatomia
8
+ contínua. Com `framed`, pode permanecer contínuo ou separar header e body com
9
+ `layout="sectioned"`. O layout organiza as regiões internas; não transforma `Content` em
10
+ [`Card`](/ui/card), [`Dialog`](/ui/dialog), [`Drawer`](/ui/drawer) ou [`Pane`](/ui/pane). Pelo mesmo
11
+ motivo o `Content` não tem rodapé: um rodapé pertence à superfície que o hospeda.
12
12
 
13
13
  No caso comum, prefira o shorthand. `count` mostra o total de itens ao lado do título:
14
14
 
@@ -36,11 +36,7 @@ do recuo da região.
36
36
 
37
37
  ```tsx preview col
38
38
  render(
39
- <Content
40
- framed
41
- title="Cliente"
42
- media={<UserRound aria-hidden="true" />}
43
- >
39
+ <Content framed title="Cliente" media={<UserRound aria-hidden="true" />}>
44
40
  <DetailGroup columns={2}>
45
41
  <DetailField label="Nome" value="Carla Souza" />
46
42
  <DetailField label="Telefone" value="+55 11 97333-4455" />
@@ -93,6 +89,34 @@ render(
93
89
  );
94
90
  ```
95
91
 
92
+ ## Header e body seccionados
93
+
94
+ Use `layout="sectioned"` quando o filho do body já controlar o recuo de suas células, linhas ou
95
+ itens. O header recebe `p-3` e divisor inferior; o body fica sem padding para que a estrutura interna
96
+ alcance a moldura. Esse layout exige `framed`.
97
+
98
+ ```tsx preview col
99
+ render(
100
+ <Content
101
+ framed
102
+ layout="sectioned"
103
+ title="Veículo"
104
+ media={<CarFront aria-hidden="true" />}
105
+ >
106
+ <DetailGroup dividers columns={2} orientation="horizontal">
107
+ <DetailField label="Marca" value="Toyota" />
108
+ <DetailField label="Modelo" value="Yaris XLS" />
109
+ <DetailField label="Ano/modelo" value="2021" />
110
+ <DetailField label="Placa" value="EVT0012" />
111
+ </DetailGroup>
112
+ </Content>,
113
+ );
114
+ ```
115
+
116
+ Conteúdo livre que não possui recuo próprio continua no layout `continuous`. Não combine
117
+ `layout="sectioned"` com `frameVariant="legend"`: `sectioned` coloca o header dentro da moldura;
118
+ `legend` integra o header ao traço externo.
119
+
96
120
  ## Grupo de campos
97
121
 
98
122
  Quando a região nomear um conjunto de campos relacionados, use `as="fieldset"`. O `Content`
@@ -129,12 +153,7 @@ decisão semântica, e não uma forma de obter esse desenho.
129
153
 
130
154
  ```tsx preview col
131
155
  render(
132
- <Content
133
- as="fieldset"
134
- framed
135
- frameVariant="legend"
136
- title="Dados do veículo"
137
- >
156
+ <Content as="fieldset" framed frameVariant="legend" title="Dados do veículo">
138
157
  <FieldGroup>
139
158
  <Field>
140
159
  <FieldLabel htmlFor="vehicle-plate">Placa</FieldLabel>
@@ -192,7 +211,12 @@ extremo oposto ao título, como em qualquer variante.
192
211
 
193
212
  ```tsx preview col
194
213
  render(
195
- <Content level={1} variant="page" title="Clientes" actions={<Button>Criar cliente</Button>}>
214
+ <Content
215
+ level={1}
216
+ variant="page"
217
+ title="Clientes"
218
+ actions={<Button>Criar cliente</Button>}
219
+ >
196
220
  <div className="rounded-lg border border-border p-4 text-sm text-muted-foreground">
197
221
  A lista de clientes ocupa o corpo.
198
222
  </div>
@@ -211,19 +235,20 @@ classes adicionais; um `Content` irmão, sem lista, mantém a altura do próprio
211
235
 
212
236
  ## Propriedades de Content
213
237
 
214
- | Propriedade | Tipo | Padrão | Descrição |
215
- | -------------- | ---------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------- |
216
- | `title` | `ReactNode` | | Forma curta: título da região (vira o heading ligado à `section`). |
217
- | `media` | `ReactNode` | | Forma curta: ícone ou imagem que identifica a região antes do título. |
218
- | `count` | `number` | | Forma curta: total de itens ao lado do título da região. |
219
- | `description` | `ReactNode` | | Forma curta: frase de apoio sob o título. |
220
- | `actions` | `ReactNode` | | Forma curta: ações sobre a região inteira, no extremo oposto ao título. |
221
- | `level` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | `2` | Nível semântico do heading, independente do destaque visual. |
222
- | `variant` | `'page' \| 'section'` | `'section'` | Hierarquia visual: `page` reproduz o cabeçalho de `Page`; `section` é a região dentro de uma superfície. |
223
- | `as` | `'section' \| 'fieldset'` | `'section'` | Usa uma região com heading ou um grupo de campos cujo título é a legenda nativa. |
224
- | `framed` | `boolean` | `false` | Envolve o header e o body com a moldura estrutural da região. |
225
- | `frameVariant` | `'default' \| 'legend'` | `'default'` | Mantém a borda contínua ou integra visualmente o header à moldura. Não altera o elemento nem sua semântica. |
226
- | `children` | `ReactNode` | | Corpo da região na forma curta, ou `ContentHeader` e `ContentBody` na composição explícita. |
238
+ | Propriedade | Tipo | Padrão | Descrição |
239
+ | -------------- | ----------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------- |
240
+ | `title` | `ReactNode` | | Forma curta: título da região (vira o heading ligado à `section`). |
241
+ | `media` | `ReactNode` | | Forma curta: ícone ou imagem que identifica a região antes do título. |
242
+ | `count` | `number` | | Forma curta: total de itens ao lado do título da região. |
243
+ | `description` | `ReactNode` | | Forma curta: frase de apoio sob o título. |
244
+ | `actions` | `ReactNode` | | Forma curta: ações sobre a região inteira, no extremo oposto ao título. |
245
+ | `level` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | `2` | Nível semântico do heading, independente do destaque visual. |
246
+ | `variant` | `'page' \| 'section'` | `'section'` | Hierarquia visual: `page` reproduz o cabeçalho de `Page`; `section` é a região dentro de uma superfície. |
247
+ | `as` | `'section' \| 'fieldset'` | `'section'` | Usa uma região com heading ou um grupo de campos cujo título é a legenda nativa. |
248
+ | `framed` | `boolean` | `false` | Envolve o header e o body com a moldura estrutural da região. |
249
+ | `layout` | `'continuous' \| 'sectioned'` | `'continuous'` | Mantém header e body contínuos ou os separa com divisor e body sem recuo. `sectioned` exige `framed`. |
250
+ | `frameVariant` | `'default' \| 'legend'` | `'default'` | Mantém a borda contínua ou integra visualmente o header à moldura. `legend` não combina com `layout="sectioned"`. |
251
+ | `children` | `ReactNode` | | Corpo da região na forma curta, ou `ContentHeader` e `ContentBody` na composição explícita. |
227
252
 
228
253
  As demais propriedades são as nativas de `section` ou, com `as="fieldset"`, de `fieldset`.
229
254
 
@@ -235,12 +260,12 @@ hierarquia é declarada em `Content`. `ContentHeader` não existe fora de `Conte
235
260
 
236
261
  ## Propriedades de ContentMedia
237
262
 
238
- | Propriedade | Tipo | Padrão | Descrição |
239
- | ----------- | ------------------------------------------------------------------------ | ----------- | --------------------------------------------------------------------------------------------------------------------------- |
240
- | `size` | `'xs' \| 'sm' \| 'default' \| 'lg'` | `'xs'` | Tamanho da moldura e do glifo na escala compartilhada dos controles. |
241
- | `context` | `'neutral' \| 'primary' \| 'info' \| 'success' \| 'warning' \| 'danger'` | `'neutral'` | Declara o significado da cor. |
242
- | `variant` | `'subtle' \| 'outline'` | `'subtle'` | Usa preenchimento tonal ou acrescenta borda. |
243
- | `className` | `string` | | Ajusta a moldura sem substituir a escala. |
263
+ | Propriedade | Tipo | Padrão | Descrição |
264
+ | ----------- | ------------------------------------------------------------------------ | ----------- | -------------------------------------------------------------------------------------------------------------------------- |
265
+ | `size` | `'xs' \| 'sm' \| 'default' \| 'lg'` | `'xs'` | Tamanho da moldura e do glifo na escala compartilhada dos controles. |
266
+ | `context` | `'neutral' \| 'primary' \| 'info' \| 'success' \| 'warning' \| 'danger'` | `'neutral'` | Declara o significado da cor. |
267
+ | `variant` | `'subtle' \| 'outline'` | `'subtle'` | Usa preenchimento tonal ou acrescenta borda. |
268
+ | `className` | `string` | | Ajusta a moldura sem substituir a escala. |
244
269
  | `asChild` | `boolean` | `false` | Cede o elemento para uma marca com visual próprio — um orbe, um logotipo — que ocupa o slot sem ganhar a moldura por cima. |
245
270
 
246
271
  `ContentMedia` já é a moldura. Passe só o glifo e escolha `size`, `context` e `variant` no slot. Um