@softize/opus 23.0.0 → 24.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/bin/cli.mjs +2 -0
  3. package/bin/lib/check.mjs +241 -42
  4. package/bin/lib/copy.mjs +16 -34
  5. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
  6. package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +3 -3
  7. package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +5 -6
  8. package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
  9. package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
  10. package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
  11. package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
  12. package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
  13. package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
  14. package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
  15. package/docs/adr/0049-inline-navigation-groups-with-the-heading.md +95 -0
  16. package/package.json +5 -1
  17. package/registry/skills/build-opus-ui/SKILL.md +11 -11
  18. package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
  19. package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
  20. package/registry/templates/app/src/App.tsx +24 -12
  21. package/src/ui/appearance.ts +149 -0
  22. package/src/ui/components/patterns/action-form-card.tsx +46 -33
  23. package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
  24. package/src/ui/components/patterns/confirm.tsx +3 -1
  25. package/src/ui/components/patterns/fill-region.ts +8 -7
  26. package/src/ui/components/patterns/form-dialog.tsx +1 -3
  27. package/src/ui/components/patterns/form.tsx +8 -7
  28. package/src/ui/components/patterns/list.tsx +15 -19
  29. package/src/ui/components/patterns/page.tsx +15 -11
  30. package/src/ui/components/patterns/pane.tsx +9 -240
  31. package/src/ui/components/patterns/presentation.tsx +14 -14
  32. package/src/ui/components/patterns/sidebar.tsx +7 -17
  33. package/src/ui/components/patterns/split.tsx +111 -59
  34. package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +90 -27
  35. package/src/ui/components/patterns/structural-media.tsx +26 -0
  36. package/src/ui/components/patterns/surface-anatomy.ts +71 -61
  37. package/src/ui/components/patterns/surface-assistant.tsx +2 -4
  38. package/src/ui/components/patterns/surface.tsx +620 -0
  39. package/src/ui/components/patterns/trigger.tsx +11 -9
  40. package/src/ui/components/primitives/ask.tsx +155 -83
  41. package/src/ui/components/primitives/button.tsx +11 -9
  42. package/src/ui/components/primitives/checkbox.tsx +8 -8
  43. package/src/ui/components/primitives/command.tsx +1 -1
  44. package/src/ui/components/primitives/composer.tsx +47 -45
  45. package/src/ui/components/primitives/detail.tsx +9 -2
  46. package/src/ui/components/primitives/dialog.tsx +60 -26
  47. package/src/ui/components/primitives/drawer.tsx +58 -12
  48. package/src/ui/components/primitives/icon-picker.tsx +90 -73
  49. package/src/ui/components/primitives/input-group.tsx +35 -28
  50. package/src/ui/components/primitives/input-otp.tsx +16 -16
  51. package/src/ui/components/primitives/input.tsx +20 -10
  52. package/src/ui/components/primitives/item.tsx +9 -12
  53. package/src/ui/components/primitives/menu.tsx +44 -65
  54. package/src/ui/components/primitives/metric-card.tsx +75 -59
  55. package/src/ui/components/primitives/radio-group.tsx +9 -9
  56. package/src/ui/components/primitives/select.tsx +344 -209
  57. package/src/ui/components/primitives/switch.tsx +10 -10
  58. package/src/ui/components/primitives/table.tsx +29 -24
  59. package/src/ui/components/primitives/tabs.tsx +4 -2
  60. package/src/ui/components/primitives/textarea.tsx +6 -6
  61. package/src/ui/components/primitives/toggle-group-context.ts +24 -0
  62. package/src/ui/components/primitives/toggle-group.tsx +116 -74
  63. package/src/ui/components/primitives/toggle.tsx +82 -16
  64. package/src/ui/docs/DocBrowser.tsx +77 -69
  65. package/src/ui/docs/content/action-form-card.md +9 -9
  66. package/src/ui/docs/content/action-list.md +76 -70
  67. package/src/ui/docs/content/action-view.md +33 -18
  68. package/src/ui/docs/content/button.md +1 -1
  69. package/src/ui/docs/content/communication.md +1 -1
  70. package/src/ui/docs/content/compositions.md +479 -0
  71. package/src/ui/docs/content/customization.md +64 -11
  72. package/src/ui/docs/content/detail.md +8 -6
  73. package/src/ui/docs/content/dialog.md +105 -60
  74. package/src/ui/docs/content/drawer.md +68 -22
  75. package/src/ui/docs/content/field.md +14 -15
  76. package/src/ui/docs/content/icon-tile.md +14 -13
  77. package/src/ui/docs/content/page.md +44 -34
  78. package/src/ui/docs/content/pane.md +42 -290
  79. package/src/ui/docs/content/presentation.md +3 -3
  80. package/src/ui/docs/content/separator.md +6 -6
  81. package/src/ui/docs/content/sidebar.md +68 -266
  82. package/src/ui/docs/content/skeleton.md +20 -18
  83. package/src/ui/docs/content/split.md +14 -14
  84. package/src/ui/docs/content/surface.md +513 -0
  85. package/src/ui/docs/content/tabs.md +20 -20
  86. package/src/ui/docs/content/toggle.md +117 -65
  87. package/src/ui/docs/content/tokens.md +124 -40
  88. package/src/ui/docs/registry.tsx +10 -6
  89. package/src/ui/index.ts +30 -27
  90. package/src/ui/meta.ts +15 -27
  91. package/src/ui/react.tsx +40 -43
  92. package/src/ui/theme.css +311 -64
  93. package/src/ui/components/patterns/content-header.tsx +0 -528
  94. package/src/ui/components/patterns/surface-media.tsx +0 -69
  95. package/src/ui/components/primitives/card.tsx +0 -191
  96. package/src/ui/docs/content/card.md +0 -292
  97. package/src/ui/docs/content/content.md +0 -275
@@ -14,18 +14,14 @@ inicial no modo não controlado; o conteúdo pode ser texto ou ícone.
14
14
  Use `pressed` e `onPressedChange` quando o estado pertencer ao consumidor.
15
15
 
16
16
  ```tsx preview
17
- const [readOnly, setReadOnly] = useState(true)
17
+ const [readOnly, setReadOnly] = useState(true);
18
18
 
19
19
  render(
20
- <Toggle
21
- variant="outline"
22
- pressed={readOnly}
23
- onPressedChange={setReadOnly}
24
- >
20
+ <Toggle variant="outline" pressed={readOnly} onPressedChange={setReadOnly}>
25
21
  <Eye />
26
22
  Somente leitura
27
23
  </Toggle>,
28
- )
24
+ );
29
25
  ```
30
26
 
31
27
  ## Variantes e tamanhos
@@ -64,15 +60,15 @@ borda e usa um fundo sutil distinto dela no estado ativo. `size` ajusta a altura
64
60
 
65
61
  ## Propriedades de Toggle
66
62
 
67
- | Propriedade | Tipo | Padrão | Descrição |
68
- |---|---|---|---|
69
- | `pressed` | `boolean` | | Estado no modo controlado. Use com `onPressedChange`. |
70
- | `onPressedChange` | `(pressed: boolean) => void` | | Chamado a cada alternância, com o novo estado. |
71
- | `defaultPressed` | `boolean` | `false` | Estado inicial no modo não controlado. |
72
- | `variant` | `'default' \| 'outline'` | `'default'` | `default` não tem borda e só ganha fundo quando ativo; `outline` mantém a borda. |
73
- | `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | Altura na escala única dos controles (2 · 2.25 · 2.5rem) — sm para toolbar densa, lg para alvo mais confortável. |
74
- | `shape` | `'default' \| 'pill'` | `'default'` | Geometria do controle; `pill` arredonda as extremidades. |
75
- | `disabled` | `boolean` | `false` | Esmaece e bloqueia o clique, preservando o estado visual. |
63
+ | Propriedade | Tipo | Padrão | Descrição |
64
+ | ----------------- | ---------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------- |
65
+ | `pressed` | `boolean` | | Estado no modo controlado. Use com `onPressedChange`. |
66
+ | `onPressedChange` | `(pressed: boolean) => void` | | Chamado a cada alternância, com o novo estado. |
67
+ | `defaultPressed` | `boolean` | `false` | Estado inicial no modo não controlado. |
68
+ | `variant` | `'default' \| 'outline'` | `'default'` | `default` não tem borda e só ganha fundo quando ativo; `outline` mantém a borda. |
69
+ | `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | Altura na escala única dos controles (2 · 2.25 · 2.5rem) — sm para toolbar densa, lg para alvo mais confortável. |
70
+ | `shape` | `'default' \| 'pill'` | `'default'` | Geometria do controle; `pill` arredonda as extremidades. |
71
+ | `disabled` | `boolean` | `false` | Esmaece e bloqueia o clique, preservando o estado visual. |
76
72
 
77
73
  ## ToggleGroup
78
74
 
@@ -82,101 +78,157 @@ relacionados. `type="single"` mantém um item ativo; `type="multiple"` aceita v
82
78
  ### Escolha única
83
79
 
84
80
  ```tsx preview
85
- const [view, setView] = useState('sessions')
81
+ const [view, setView] = useState("sessions");
86
82
 
87
83
  render(
88
- <ToggleGroup type="single" value={view} onValueChange={(v) => v && setView(v)}>
89
- <ToggleGroupItem value="overview">Visão geral</ToggleGroupItem>
90
- <ToggleGroupItem value="sessions">Sessões</ToggleGroupItem>
91
- <ToggleGroupItem value="skills">Habilidades</ToggleGroupItem>
84
+ <ToggleGroup
85
+ type="single"
86
+ value={view}
87
+ onValueChange={(v) => v && setView(v)}
88
+ >
89
+ <Toggle value="overview">Visão geral</Toggle>
90
+ <Toggle value="sessions">Sessões</Toggle>
91
+ <Toggle value="skills">Habilidades</Toggle>
92
92
  </ToggleGroup>,
93
- )
93
+ );
94
94
  ```
95
95
 
96
96
  ### Escolha múltipla
97
97
 
98
98
  ```tsx preview
99
- const [marks, setMarks] = useState(['bold'])
99
+ const [marks, setMarks] = useState(["bold"]);
100
100
 
101
101
  render(
102
102
  <ToggleGroup type="multiple" value={marks} onValueChange={setMarks}>
103
- <ToggleGroupItem value="bold" aria-label="Negrito"><Bold /></ToggleGroupItem>
104
- <ToggleGroupItem value="italic" aria-label="Itálico"><Italic /></ToggleGroupItem>
105
- <ToggleGroupItem value="underline" aria-label="Sublinhado"><Underline /></ToggleGroupItem>
103
+ <Toggle value="bold" aria-label="Negrito">
104
+ <Bold />
105
+ </Toggle>
106
+ <Toggle value="italic" aria-label="Itálico">
107
+ <Italic />
108
+ </Toggle>
109
+ <Toggle value="underline" aria-label="Sublinhado">
110
+ <Underline />
111
+ </Toggle>
106
112
  </ToggleGroup>,
107
- )
113
+ );
108
114
  ```
109
115
 
110
116
  ### Itens só com ícone e tooltip
111
117
 
112
118
  Quando o item mostra somente um ícone, mantenha o `aria-label` e acrescente uma dica visual. Envolva o
113
- `ToggleGroupItem` em `TooltipTrigger asChild`: o item preserva o estado selecionado e seus atributos,
119
+ `Toggle` em `TooltipTrigger asChild`: o controle preserva o estado selecionado e seus atributos,
114
120
  e a dica aparece sem criar outro botão. O `TooltipProvider` da raiz do aplicativo continua necessário.
115
121
 
116
122
  ```tsx preview
117
- const [align, setAlign] = useState('left')
123
+ const [align, setAlign] = useState("left");
118
124
 
119
125
  render(
120
- <ToggleGroup type="single" value={align} onValueChange={(v) => v && setAlign(v)}>
126
+ <ToggleGroup
127
+ type="single"
128
+ value={align}
129
+ onValueChange={(v) => v && setAlign(v)}
130
+ >
121
131
  <Tooltip>
122
132
  <TooltipTrigger asChild>
123
- <ToggleGroupItem value="left" aria-label="Alinhar à esquerda"><AlignLeft /></ToggleGroupItem>
133
+ <Toggle value="left" aria-label="Alinhar à esquerda">
134
+ <AlignLeft />
135
+ </Toggle>
124
136
  </TooltipTrigger>
125
137
  <TooltipContent>Alinhar à esquerda</TooltipContent>
126
138
  </Tooltip>
127
139
  <Tooltip>
128
140
  <TooltipTrigger asChild>
129
- <ToggleGroupItem value="center" aria-label="Centralizar"><AlignCenter /></ToggleGroupItem>
141
+ <Toggle value="center" aria-label="Centralizar">
142
+ <AlignCenter />
143
+ </Toggle>
130
144
  </TooltipTrigger>
131
145
  <TooltipContent>Centralizar</TooltipContent>
132
146
  </Tooltip>
133
147
  <Tooltip>
134
148
  <TooltipTrigger asChild>
135
- <ToggleGroupItem value="right" aria-label="Alinhar à direita"><AlignRight /></ToggleGroupItem>
149
+ <Toggle value="right" aria-label="Alinhar à direita">
150
+ <AlignRight />
151
+ </Toggle>
136
152
  </TooltipTrigger>
137
153
  <TooltipContent>Alinhar à direita</TooltipContent>
138
154
  </Tooltip>
139
155
  </ToggleGroup>,
140
- )
156
+ );
157
+ ```
158
+
159
+ ### Relação entre os controles
160
+
161
+ `mode` segue a mesma gramática de `ButtonGroup`. `connected`, o padrão, une os controles e remove
162
+ bordas internas duplicadas; `spaced` preserva cada controle com um pequeno intervalo; `divided`
163
+ acrescenta divisores entre eles. `variant`, `size` e `shape` definidos no grupo chegam aos controles
164
+ por contexto.
165
+
166
+ ```tsx preview
167
+ <ToggleGroup type="single" variant="outline" defaultValue="call">
168
+ <Toggle value="call">
169
+ <Phone /> Telefone
170
+ </Toggle>
171
+ <Toggle value="email">
172
+ <Mail /> E-mail
173
+ </Toggle>
174
+ <Toggle value="whatsapp">
175
+ <MessageCircle /> WhatsApp
176
+ </Toggle>
177
+ </ToggleGroup>
141
178
  ```
142
179
 
143
- ### Grupo conectado com borda
180
+ Use `mode="spaced"` quando cada controle precisar preservar sua própria borda e seu próprio raio.
181
+ Use `mode="divided"` quando a separação também precisar ficar explícita por divisores.
144
182
 
145
- `variant`, `size` e `shape` definidos no grupo chegam aos itens por contexto. `spacing` separa os
146
- itens. Use `variant="outline"` com `spacing={0}` para formar um controle conectado com contorno
147
- único. A borda pertence aos itens: o grupo remove as bordas internas duplicadas, arredonda apenas
148
- as extremidades e preserva o contorno quando um item está ativo.
183
+ ### Aparência segmentada
184
+
185
+ Use `mode="segmented"` para escolhas curtas que precisam permanecer visíveis dentro de uma
186
+ superfície compartilhada. O item ativo usa o mesmo fundo de uma superfície elevada; os demais
187
+ permanecem no fundo do grupo. Segmentos inativos vizinhos recebem um divisor sutil; o divisor
188
+ desaparece dos dois lados da seleção. Essa aparência escolhe um valor no contexto atual: para
189
+ navegar entre painéis, use Tabs.
149
190
 
150
191
  ```tsx preview
151
- <ToggleGroup type="single" variant="outline" spacing={0} defaultValue="call">
152
- <ToggleGroupItem value="call"><Phone /> Telefone</ToggleGroupItem>
153
- <ToggleGroupItem value="email"><Mail /> E-mail</ToggleGroupItem>
154
- <ToggleGroupItem value="whatsapp"><MessageCircle /> WhatsApp</ToggleGroupItem>
192
+ <ToggleGroup type="single" mode="segmented" defaultValue="sessions">
193
+ <Toggle value="overview">
194
+ <LayoutGrid /> Visão geral
195
+ </Toggle>
196
+ <Toggle value="sessions">
197
+ <History /> Sessões
198
+ </Toggle>
199
+ <Toggle value="skills">
200
+ <Settings /> Habilidades
201
+ </Toggle>
155
202
  </ToggleGroup>
156
203
  ```
157
204
 
158
- Use um valor maior que zero quando cada item precisar preservar sua própria borda e seu próprio
159
- raio, sem formar um bloco contínuo.
205
+ `mode` trata a relação visual entre os controles. `variant` continua tratando cada `Toggle`; por
206
+ isso, a composição segmentada recomendada usa a variante padrão. O recuo fica dentro da altura da
207
+ escala de controles: o conjunto mantém `2rem`, `2.25rem` ou `2.5rem` conforme `size`.
208
+
209
+ ### Distribuição e moldura
210
+
211
+ `distribution="content"`, o padrão, preserva a largura dos rótulos. Use `equal` para dividir toda a
212
+ largura disponível igualmente entre os controles. `framed` acrescenta borda e recuo estrutural sem
213
+ mudar o modo de agrupamento.
160
214
 
161
215
  ### Propriedades de ToggleGroup
162
216
 
163
- | Propriedade | Tipo | Padrão | Descrição |
164
- |---|---|---|---|
165
- | `type` | `'single' \| 'multiple'` | | Define seleção única (`string`) ou múltipla (`string[]`). |
166
- | `value` | `string \| string[]` | | Seleção controlada; o tipo acompanha `type`. |
167
- | `onValueChange` | `(value: string \| string[]) => void` | | Informa a nova seleção. No modo single, uma string vazia representa nenhum item ativo. |
168
- | `defaultValue` | `string \| string[]` | | Seleção inicial no modo não controlado. |
169
- | `variant` | `'default' \| 'outline'` | `'default'` | Tratamento visual repassado aos itens. `outline` aplica borda a cada item. |
170
- | `size` | `'default' \| 'sm' \| 'lg'` | `'default'` | Tamanho repassado aos itens. |
171
- | `shape` | `'default' \| 'pill'` | `'default'` | Geometria do grupo e de suas extremidades. |
172
- | `spacing` | `number` | `0` | Distância entre os itens em unidades de spacing. Com zero, remove bordas internas duplicadas e conecta o grupo. |
173
-
174
- ### Propriedades de ToggleGroupItem
175
-
176
- | Propriedade | Tipo | Padrão | Descrição |
177
- |---|---|---|---|
178
- | `value` | `string` | | Identificador que entra no valor do grupo quando o item é ativado. |
179
- | `disabled` | `boolean` | `false` | Bloqueia somente este item e preserva seu estado visual. |
180
- | `variant` | `'default' \| 'outline'` | `'default'` | Tratamento do item quando o grupo não declara `variant`; o valor do grupo prevalece. |
181
- | `size` | `'default' \| 'sm' \| 'lg'` | `'default'` | Tamanho do item quando o grupo não declara `size`; o valor do grupo prevalece. |
182
- | `shape` | `'default' \| 'pill'` | shape do grupo | Geometria deste item; quando informada, prevalece sobre a do grupo. |
217
+ | Propriedade | Tipo | Padrão | Descrição |
218
+ | --------------- | ----------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------- |
219
+ | `type` | `'single' \| 'multiple'` | | Define seleção única (`string`) ou múltipla (`string[]`). |
220
+ | `value` | `string \| string[]` | | Seleção controlada; o tipo acompanha `type`. |
221
+ | `onValueChange` | `(value: string \| string[]) => void` | | Informa a nova seleção. No modo single, uma string vazia representa nenhum item ativo. |
222
+ | `defaultValue` | `string \| string[]` | | Seleção inicial no modo não controlado. |
223
+ | `variant` | `'default' \| 'outline'` | `'default'` | Tratamento visual repassado aos itens. `outline` aplica borda a cada item. |
224
+ | `size` | `'default' \| 'sm' \| 'lg'` | `'default'` | Tamanho repassado aos itens. |
225
+ | `shape` | `'default' \| 'pill'` | `'default'` | Geometria do grupo e de suas extremidades. |
226
+ | `mode` | `'connected' \| 'spaced' \| 'divided' \| 'segmented'` | `'connected'` | Define como os controles se relacionam; `segmented` cria uma superfície compartilhada para a seleção. |
227
+ | `distribution` | `'content' \| 'equal'` | `'content'` | Preserva a largura dos rótulos ou reparte igualmente a largura disponível. |
228
+ | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Organiza os controles e os divisores no eixo indicado. |
229
+ | `framed` | `boolean` | `false` | Acrescenta borda, raio e recuo ao grupo sem alterar o modo. |
230
+
231
+ Dentro de `ToggleGroup`, cada `Toggle` exige `value`. O grupo controla o estado selecionado; props
232
+ standalone como `pressed` e `onPressedChange` não participam dessa composição. `disabled`, `variant`,
233
+ No grupo, `variant` e `size` pertencem ao conjunto e chegam a todos os controles. `shape` no item
234
+ pode declarar uma pílula local dentro do grupo.
@@ -15,19 +15,30 @@ exemplos leem as variáveis ao vivo; troque o tema do app e a página acompanha.
15
15
 
16
16
  ```tsx preview
17
17
  const SURFACES = [
18
- ['background', 'foreground', 'O fundo da página e o texto padrão.'],
19
- ['card', 'card-foreground', 'Superfície elevada (Card, painéis).'],
20
- ['popover', 'popover-foreground', 'Superfície flutuante (menus, popovers, dialogs).'],
21
- ['muted', 'muted-foreground', 'Fundo discreto e texto secundário.'],
22
- ['accent', 'accent-foreground', 'Hover e seleção.'],
23
- ['secondary', 'secondary-foreground', 'Ação secundária.'],
24
- ['primary', 'primary-foreground', 'A ação principal — alto contraste.'],
25
- ['destructive', 'destructive-foreground', 'Ação destrutiva e erro.'],
26
- ]
18
+ ["background", "foreground", "O fundo da página e o texto padrão."],
19
+ ["card", "card-foreground", "Superfície elevada (Surface, painéis)."],
20
+ [
21
+ "popover",
22
+ "popover-foreground",
23
+ "Superfície flutuante (menus, popovers, dialogs).",
24
+ ],
25
+ ["muted", "muted-foreground", "Fundo discreto e texto secundário."],
26
+ [
27
+ "accent",
28
+ "accent-foreground",
29
+ "Alias de compatibilidade para hover neutro.",
30
+ ],
31
+ ["secondary", "secondary-foreground", "Ação secundária."],
32
+ ["primary", "primary-foreground", "A ação principal — alto contraste."],
33
+ ["destructive", "destructive-foreground", "Ação destrutiva e erro."],
34
+ ];
27
35
  render(
28
36
  <div className="w-full overflow-hidden rounded-lg border border-border">
29
37
  {SURFACES.map(([token, fg, desc]) => (
30
- <div key={token} className="flex items-center gap-4 border-b border-border px-4 py-3 last:border-b-0">
38
+ <div
39
+ key={token}
40
+ className="flex items-center gap-4 border-b border-border px-4 py-3 last:border-b-0"
41
+ >
31
42
  <div
32
43
  className="flex h-10 w-28 shrink-0 items-center justify-center rounded-md border border-border text-xs font-medium"
33
44
  style={{ backgroundColor: `var(--${token})`, color: `var(--${fg})` }}
@@ -35,15 +46,75 @@ render(
35
46
  Texto
36
47
  </div>
37
48
  <div className="min-w-0">
38
- <div className="font-mono text-xs">--{token} · --{fg}</div>
49
+ <div className="font-mono text-xs">
50
+ --{token} · --{fg}
51
+ </div>
39
52
  <div className="text-xs text-muted-foreground">{desc}</div>
40
53
  </div>
41
54
  </div>
42
55
  ))}
43
56
  </div>,
44
- )
57
+ );
45
58
  ```
46
59
 
60
+ ### Controles e estados neutros
61
+
62
+ Canvas, superfície elevada, controle e interação formam uma escala opaca. Um input não muda de
63
+ tom ao sair do canvas e entrar num card, porque `control` representa o resultado final — não uma
64
+ cor translúcida a ser misturada com o ancestral. Da mesma forma, hover, seleção e pressão usam
65
+ papéis diferentes e preservam a mesma ordem comportamental nos dois temas.
66
+
67
+ ```tsx preview
68
+ const NEUTRAL_STATES = [
69
+ ["control", "Controle em repouso."],
70
+ ["interaction-hover", "Resposta transitória ao apontador ou foco de item."],
71
+ ["interaction-selected", "Seleção ou expansão persistente."],
72
+ ["interaction-pressed", "Pressão transitória de um controle neutro."],
73
+ ];
74
+ render(
75
+ <div className="grid w-full grid-cols-2 gap-2">
76
+ {NEUTRAL_STATES.map(([token, desc]) => (
77
+ <div
78
+ key={token}
79
+ className="rounded-md border border-border p-3"
80
+ style={{ backgroundColor: `var(--${token})` }}
81
+ >
82
+ <div className="font-mono text-xs">--{token}</div>
83
+ <div className="mt-1 text-xs text-muted-foreground">{desc}</div>
84
+ </div>
85
+ ))}
86
+ </div>,
87
+ );
88
+ ```
89
+
90
+ ### Ajuste de aparência
91
+
92
+ Os tokens neutros desta seção não são valores fixos: o tema os calcula a partir de quatro eixos,
93
+ declarados separadamente para o modo claro e para o modo escuro. Com os eixos no preset, o
94
+ resultado é o tema padrão. Uma aplicação que oferece preferências de aparência altera os eixos, e
95
+ toda a escala — superfícies, estados, bordas e texto secundário — se move em conjunto, sem perder a
96
+ ordem entre os papéis.
97
+
98
+ | Eixo | Custom property (`light` ou `dark`) | O que muda | Preset claro | Preset escuro |
99
+ | ----------------------- | ---------------------------------------- | ------------------------------------------------------ | ------------ | ------------- |
100
+ | Intensidade de cor | `--appearance-<modo>-color-intensity` | Quanto do matiz `--neutral-hue` aparece nos neutros | 0 | 0 |
101
+ | Contraste | `--appearance-<modo>-contrast` | Distância de hover, seleção, pressão, `muted` e bordas | 0 | 0 |
102
+ | Brilho das superfícies | `--appearance-<modo>-surface-brightness` | Luminosidade do canvas e das superfícies | 100 | 50 |
103
+ | Contraste entre camadas | `--appearance-<modo>-layer-contrast` | Distância entre canvas, controle e superfície elevada | 0 | 0 |
104
+
105
+ Cada eixo aceita um inteiro de 0 a 100. Os limites protegem o tema padrão: nenhum valor dessa faixa
106
+ reduz a separação entre os papéis nem o contraste do texto sobre as superfícies em repouso abaixo do
107
+ que o preset oferece. Por isso contraste e contraste entre camadas partem de zero e só aumentam, e o
108
+ texto secundário acompanha as superfícies quando o brilho muda.
109
+
110
+ A intensidade de cor tinge somente os neutros. `primary`, `destructive` e os contextos `info`,
111
+ `success`, `warning` e `danger` mantêm as próprias cores. O matiz aplicado vem de `--neutral-hue`,
112
+ que a aplicação define junto com a identidade.
113
+
114
+ Os nomes, os presets e a conversão de valores em custom properties ficam em
115
+ `@softize/opus/ui/appearance`. O passo a passo para aplicar e desfazer um ajuste está em
116
+ [Customização](/ui/customization). A ADR 0044 registra a decisão e os limites.
117
+
47
118
  ## Famílias contextuais
48
119
 
49
120
  Os tokens `primary`, `secondary` e `destructive` acima permanecem fundações de superfície. A API
@@ -52,7 +123,7 @@ família oferece sólido, foreground, superfície sutil, ênfase e borda. Use as
52
123
  `variant`; classes contextuais diretas ficam reservadas à implementação dos primitives.
53
124
 
54
125
  ```tsx preview
55
- const CONTEXTS = ['neutral', 'primary', 'info', 'success', 'warning', 'danger']
126
+ const CONTEXTS = ["neutral", "primary", "info", "success", "warning", "danger"];
56
127
  render(
57
128
  <div className="grid w-full grid-cols-3 gap-2">
58
129
  {CONTEXTS.map((context) => (
@@ -69,7 +140,7 @@ render(
69
140
  </div>
70
141
  ))}
71
142
  </div>,
72
- )
143
+ );
73
144
  ```
74
145
 
75
146
  ## Linhas e foco
@@ -79,15 +150,21 @@ render(
79
150
 
80
151
  ```tsx preview
81
152
  const LINES = [
82
- ['border', 'Bordas de contêineres (cards, tabelas, separadores).'],
83
- ['input', 'Borda e fundo sutil de campos de formulário.'],
84
- ['ring', 'O anel de foco (focus-visible).'],
85
- ]
153
+ ["border", "Bordas de contêineres (cards, tabelas, separadores)."],
154
+ ["input", "Borda de campos de formulário; o fundo usa control."],
155
+ ["ring", "O anel de foco (focus-visible)."],
156
+ ];
86
157
  render(
87
158
  <div className="w-full overflow-hidden rounded-lg border border-border">
88
159
  {LINES.map(([token, desc]) => (
89
- <div key={token} className="flex items-center gap-4 border-b border-border px-4 py-3 last:border-b-0">
90
- <div className="h-10 w-28 shrink-0 rounded-md border-2" style={{ borderColor: `var(--${token})` }} />
160
+ <div
161
+ key={token}
162
+ className="flex items-center gap-4 border-b border-border px-4 py-3 last:border-b-0"
163
+ >
164
+ <div
165
+ className="h-10 w-28 shrink-0 rounded-md border-2"
166
+ style={{ borderColor: `var(--${token})` }}
167
+ />
91
168
  <div className="min-w-0">
92
169
  <div className="font-mono text-xs">--{token}</div>
93
170
  <div className="text-xs text-muted-foreground">{desc}</div>
@@ -95,7 +172,7 @@ render(
95
172
  </div>
96
173
  ))}
97
174
  </div>,
98
- )
175
+ );
99
176
  ```
100
177
 
101
178
  ## Escala de elevação
@@ -107,16 +184,19 @@ render(
107
184
 
108
185
  ```tsx preview
109
186
  const LEVELS = [
110
- ['sm', 'Superfície em repouso.'],
111
- ['md', 'Flutuante intermediário.'],
112
- ['lg', 'Modal ou destaque forte.'],
113
- ]
187
+ ["sm", "Superfície em repouso."],
188
+ ["md", "Flutuante intermediário."],
189
+ ["lg", "Modal ou destaque forte."],
190
+ ];
114
191
  render(
115
192
  <div className="flex flex-wrap gap-8 py-4">
116
193
  {LEVELS.map(([level, desc]) => (
117
194
  <div key={level} className="flex flex-col items-center gap-3">
118
195
  <div
119
- className={'flex h-24 w-40 items-center justify-center rounded-xl border border-border bg-card text-xs text-card-foreground shadow-' + level}
196
+ className={
197
+ "flex h-24 w-40 items-center justify-center rounded-xl border border-border bg-card text-xs text-card-foreground shadow-" +
198
+ level
199
+ }
120
200
  >
121
201
  Texto
122
202
  </div>
@@ -127,7 +207,7 @@ render(
127
207
  </div>
128
208
  ))}
129
209
  </div>,
130
- )
210
+ );
131
211
  ```
132
212
 
133
213
  ## Escala relativa
@@ -172,7 +252,7 @@ render(
172
252
  </p>
173
253
  </div>
174
254
  </div>,
175
- )
255
+ );
176
256
  ```
177
257
 
178
258
  Alterar `font-size` em `html` permanece válido quando a aplicação quer recalibrar **toda** a
@@ -190,15 +270,17 @@ o mecanismo para fazer `text-sm` se chamar `text-base`.
190
270
  render(
191
271
  <div className="flex flex-wrap gap-4">
192
272
  {[
193
- ['rounded-xs', 'radius − 0.375rem'],
194
- ['rounded-sm', 'radius − 0.25rem'],
195
- ['rounded-md', 'radius − 0.125rem'],
196
- ['rounded-lg', 'radius (0.75rem)'],
197
- ['rounded-xl', 'radius + 0.25rem'],
198
- ['rounded-2xl', 'radius + 0.5rem'],
273
+ ["rounded-xs", "radius − 0.375rem"],
274
+ ["rounded-sm", "radius − 0.25rem"],
275
+ ["rounded-md", "radius − 0.125rem"],
276
+ ["rounded-lg", "radius (0.75rem)"],
277
+ ["rounded-xl", "radius + 0.25rem"],
278
+ ["rounded-2xl", "radius + 0.5rem"],
199
279
  ].map(([cls, calc]) => (
200
280
  <div key={cls} className="flex items-center gap-3">
201
- <div className={'h-12 w-12 border-2 border-foreground/30 bg-muted ' + cls} />
281
+ <div
282
+ className={"h-12 w-12 border-2 border-foreground/30 bg-muted " + cls}
283
+ />
202
284
  <div>
203
285
  <div className="font-mono text-xs">{cls}</div>
204
286
  <div className="text-xs text-muted-foreground">{calc}</div>
@@ -206,7 +288,7 @@ render(
206
288
  </div>
207
289
  ))}
208
290
  </div>,
209
- )
291
+ );
210
292
  ```
211
293
 
212
294
  ## Divergências declaradas vs shadcn
@@ -214,10 +296,12 @@ render(
214
296
  > Qualquer diferença em relação ao shadcn que não esteja declarada aqui (e no `theme.css`) é uma
215
297
  > divergência a sincronizar, conforme a skill `build-opus-ui`.
216
298
 
217
- 1. Formato HSL nos tokens (o upstream migrou para oklch), escolhido pela legibilidade; os valores
218
- acompanham o upstream, só o formato difere.
219
- 2. Elevação no tema escuro: `--card` e `--popover` ficam acima de `--background` (`10%` contra `3.9%`
220
- de luminosidade), para que dialog e card se destaquem de fato da página.
299
+ 1. Formato HSL nos tokens (o upstream migrou para oklch), escolhido pela legibilidade.
300
+ 2. Hierarquia opaca de superfícies e estados nos dois temas: canvas, card/popover, controle, hover,
301
+ seleção e pressão têm papéis próprios. No light, o canvas quase branco sustenta superfícies
302
+ brancas; no dark, cards e popovers permanecem acima do canvas. A ADR 0043 registra os valores e
303
+ as restrições de composição. A escala neutra é derivada dos eixos de aparência (ADR 0044); no
304
+ preset, os valores são os calibrados.
221
305
  3. Radius mapeado (`xs` a `2xl`) preservando o sentido pré-v4 dos utilitários `rounded-*`. Esse
222
306
  trecho derivado permanece monotônico quando a base muda; aplicações que também usam `3xl`
223
307
  precisam revisar a transição para esse degrau nativo ao customizar `--radius`.
@@ -49,6 +49,7 @@ import tokensMd from "./content/tokens.md?raw";
49
49
  import semanticContextMd from "./content/semantic-context.md?raw";
50
50
  import communicationMd from "./content/communication.md?raw";
51
51
  import customizationMd from "./content/customization.md?raw";
52
+ import compositionsMd from "./content/compositions.md?raw";
52
53
 
53
54
  // — Componentes —
54
55
  import accordionMd from "./content/accordion.md?raw";
@@ -61,7 +62,6 @@ import dockMd from "./content/dock.md?raw";
61
62
  import breadcrumbMd from "./content/breadcrumb.md?raw";
62
63
  import buttonMd from "./content/button.md?raw";
63
64
  import calendarMd from "./content/calendar.md?raw";
64
- import cardMd from "./content/card.md?raw";
65
65
  import metricCardMd from "./content/metric-card.md?raw";
66
66
  import carouselMd from "./content/carousel.md?raw";
67
67
  import askMd from "./content/ask.md?raw";
@@ -125,7 +125,7 @@ import actionTriggerMd from "./content/action-trigger.md?raw";
125
125
  import actionSearchDialogMd from "./content/action-list-dialog.md?raw";
126
126
  import pageMd from "./content/page.md?raw";
127
127
  import presentationMd from "./content/presentation.md?raw";
128
- import contentMd from "./content/content.md?raw";
128
+ import surfaceMd from "./content/surface.md?raw";
129
129
  import paneMd from "./content/pane.md?raw";
130
130
  import sidebarMd from "./content/sidebar.md?raw";
131
131
  import splitMd from "./content/split.md?raw";
@@ -396,6 +396,11 @@ export const UI_SECTIONS: DocSection[] = [
396
396
  title: "Comunicação",
397
397
  render: doc(communicationMd),
398
398
  },
399
+ {
400
+ slug: "compositions",
401
+ title: "Composições",
402
+ render: doc(compositionsMd),
403
+ },
399
404
  {
400
405
  slug: "customization",
401
406
  title: "Customização",
@@ -454,9 +459,9 @@ export const UI_SECTIONS: DocSection[] = [
454
459
  ),
455
460
  },
456
461
  {
457
- slug: "content",
458
- title: "Content",
459
- render: pattern("Content", "content", contentMd),
462
+ slug: "surface",
463
+ title: "Surface",
464
+ render: pattern("Surface", "surface", surfaceMd),
460
465
  },
461
466
  {
462
467
  slug: "accordion",
@@ -468,7 +473,6 @@ export const UI_SECTIONS: DocSection[] = [
468
473
  title: "Aspect Ratio",
469
474
  render: comp("Aspect Ratio", "aspect-ratio", aspectRatioMd),
470
475
  },
471
- { slug: "card", title: "Card", render: comp("Card", "card", cardMd) },
472
476
  {
473
477
  slug: "metric-card",
474
478
  title: "Metric Card",