@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
@@ -6,17 +6,27 @@ erro. O consumidor compõe somente o conteúdo disponível, aqui com [`Detail`](
6
6
  coleção, use [`ActionList`](/ui/action-list).
7
7
 
8
8
  ```tsx preview col
9
- const [id, setId] = useState('empresa-x')
9
+ const [id, setId] = useState("empresa-x");
10
10
 
11
11
  render(
12
12
  <DocBrowserActionProvider>
13
13
  <div className="w-full space-y-3">
14
14
  <ButtonGroup mode="spaced">
15
- <Button variant="outline" size="sm" onClick={() => setId('empresa-x')}>Empresa X</Button>
16
- <Button variant="outline" size="sm" onClick={() => setId('empresa-y')}>Empresa Y</Button>
17
- <Button variant="outline" size="sm" onClick={() => setId('sessao-fantasma')}>Inexistente (erro)</Button>
15
+ <Button variant="outline" size="sm" onClick={() => setId("empresa-x")}>
16
+ Empresa X
17
+ </Button>
18
+ <Button variant="outline" size="sm" onClick={() => setId("empresa-y")}>
19
+ Empresa Y
20
+ </Button>
21
+ <Button
22
+ variant="outline"
23
+ size="sm"
24
+ onClick={() => setId("sessao-fantasma")}
25
+ >
26
+ Inexistente (erro)
27
+ </Button>
18
28
  </ButtonGroup>
19
- <Card className="p-4">
29
+ <Surface framed elevation="raised" className="p-4">
20
30
  <ActionView action={docWorkspaceView} input={{ id }}>
21
31
  {(ws) => (
22
32
  <DetailGroup columns={3}>
@@ -24,15 +34,20 @@ render(
24
34
  <DetailField label="Cliente" value={ws.client} />
25
35
  <DetailField
26
36
  label="Status"
27
- value={<DictionaryValue dict={docWorkspaceStatus} value={ws.status} />}
37
+ value={
38
+ <DictionaryValue
39
+ dict={docWorkspaceStatus}
40
+ value={ws.status}
41
+ />
42
+ }
28
43
  />
29
44
  </DetailGroup>
30
45
  )}
31
46
  </ActionView>
32
- </Card>
47
+ </Surface>
33
48
  </div>
34
49
  </DocBrowserActionProvider>,
35
- )
50
+ );
36
51
  ```
37
52
 
38
53
  O handler da view deve devolver um valor. Uma consulta que resolve `undefined` é tratada como erro
@@ -47,13 +62,13 @@ mais de uma região da tela ou quando o carregamento precisa ser orquestrado por
47
62
 
48
63
  ## Propriedades de ActionView
49
64
 
50
- | Propriedade | Tipo | Padrão | Descrição |
51
- |---|---|---|---|
52
- | `action` | `ViewContract<TInput, TData>` | | A ViewAction do Opus (kind view, 1 recurso). |
53
- | `input` | `TInput` | | Geralmente { id } — mudou, recarrega. |
54
- | `children` | `(data: TData, refetch) => ReactNode` | | Obrigatório, salvo quando `render` é informado. Conteúdo apresentado quando os dados estão disponíveis. `refetch` permite recarregar por código. |
55
- | `render` | `(data: TData, refetch) => ReactNode` | | Alias de compatibilidade de `children`; `children` tem precedência. Sem nenhum dos dois, o componente lança erro. |
56
- | `loading` | `ReactNode \| boolean` | `3 skeletons` | Sobrescreve o carregamento: um nó próprio, `true` para o padrão ou `false` para não renderizar nada enquanto carrega. |
57
- | `empty` | `ReactNode` | `emptyMessage` ou nada | Estado reservado a `data` indefinido. Como o react-query converte esse resultado em erro, ele não aparece na prática; devolva um valor ou o erro `not_found`. |
58
- | `emptyMessage` | `string` | | Atalho do vazio, sujeito à mesma limitação de `empty`: a frase na mesma superfície de `DataState` (`Empty` com moldura sólida). |
59
- | `error` | `(err, retry) => ReactNode` | `"Não foi possível carregar" + Tentar de novo` | Sobrescreve o estado de erro padrão. Sem ele, a frase do servidor só aparece quando é legível pela pessoa (`conflict`, `validation`, `not_found`, `authorization`, `authentication`); código técnico nunca vira título, e o botão de tentar de novo some quando o problema é de permissão ou sessão. |
65
+ | Propriedade | Tipo | Padrão | Descrição |
66
+ | -------------- | ------------------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
67
+ | `action` | `ViewContract<TInput, TData>` | | A ViewAction do Opus (kind view, 1 recurso). |
68
+ | `input` | `TInput` | | Geralmente { id } — mudou, recarrega. |
69
+ | `children` | `(data: TData, refetch) => ReactNode` | | Obrigatório, salvo quando `render` é informado. Conteúdo apresentado quando os dados estão disponíveis. `refetch` permite recarregar por código. |
70
+ | `render` | `(data: TData, refetch) => ReactNode` | | Alias de compatibilidade de `children`; `children` tem precedência. Sem nenhum dos dois, o componente lança erro. |
71
+ | `loading` | `ReactNode \| boolean` | `3 skeletons` | Sobrescreve o carregamento: um nó próprio, `true` para o padrão ou `false` para não renderizar nada enquanto carrega. |
72
+ | `empty` | `ReactNode` | `emptyMessage` ou nada | Estado reservado a `data` indefinido. Como o react-query converte esse resultado em erro, ele não aparece na prática; devolva um valor ou o erro `not_found`. |
73
+ | `emptyMessage` | `string` | | Atalho do vazio, sujeito à mesma limitação de `empty`: a frase na mesma superfície de `DataState` (`Empty` com moldura sólida). |
74
+ | `error` | `(err, retry) => ReactNode` | `"Não foi possível carregar" + Tentar de novo` | Sobrescreve o estado de erro padrão. Sem ele, a frase do servidor só aparece quando é legível pela pessoa (`conflict`, `validation`, `not_found`, `authorization`, `authentication`); código técnico nunca vira título, e o botão de tentar de novo some quando o problema é de permissão ou sessão. |
@@ -43,7 +43,7 @@ há texto visível. O glifo dentro do controle acompanha o tamanho (0.875rem em
43
43
  Escolha o tamanho pela região, não pela importância visual: `variant` e `context` resolvem a
44
44
  hierarquia da ação. Headers de Page, a criação no cabeçalho de uma coleção e footers de
45
45
  [`Dialog`](/ui/dialog) e [`Drawer`](/ui/drawer) usam `default`; a criação de uma coleção é um botão textual, sem
46
- ícone, como mostra [`Content`](/ui/content). Operações densas de toolbar ou de seção usam `sm`;
46
+ ícone, como mostra [`Surface`](/ui/surface). Operações densas de toolbar ou de seção usam `sm`;
47
47
  ações dentro de linha ou célula usam `xs` ou `icon-xs`. Assim a mesma decisão mantém a mesma altura
48
48
  mesmo quando uma superfície troca uma ação secundária por uma primária.
49
49
 
@@ -42,7 +42,7 @@ frase. Não copie as regras da Base para cá.
42
42
  | Superfície Opus | Papel enviado à Base |
43
43
  | ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
44
44
  | `label`, botão de confirmação e filhos de `Button` | `button` |
45
- | `title`, `CardTitle`, `DialogTitle` | `title` |
45
+ | `title`, `SurfaceTitle`, `DialogTitle` | `title` |
46
46
  | `description`, `hint`, `help` | `description` ou `helper-text` |
47
47
  | `messages.success` e `messages.error` | `success` e `error` |
48
48
  | `confirm.message`, `dialog.*().body` e texto relevante no `DialogBody` | `dialog-body` |
@@ -0,0 +1,479 @@
1
+ ---
2
+ title: Composições
3
+ ---
4
+
5
+ # Composições
6
+
7
+ Use esta página para decidir como montar uma tela antes de escolher as propriedades de cada
8
+ componente. `Page`, `Dialog`, `Drawer` e `Surface` se organizam da mesma forma: um cabeçalho, um
9
+ corpo e um rodapé com a decisão da etapa. Cada família admite as ações e a navegação indicadas na
10
+ tabela abaixo. Em `Page`, `Dialog` e `Drawer`, o corpo também pode começar por uma intro que leva o
11
+ título. `Pane` fica fora dessa organização: ele só reserva, dentro de um `Split`, o espaço em que
12
+ uma `Surface` aparece.
13
+
14
+ Todos os exemplos mostram a mesma tarefa, editar uma ordem de serviço, em superfícies diferentes.
15
+ As propriedades de cada componente ficam nas páginas da família: [`Page`](/ui/page),
16
+ [`Dialog`](/ui/dialog), [`Drawer`](/ui/drawer), [`Surface`](/ui/surface) e [`Pane`](/ui/pane).
17
+
18
+ ## As regiões de cada superfície
19
+
20
+ Cada família dá um nome próprio a cada região. A tabela mostra a correspondência entre elas.
21
+
22
+ | Região | Page | Dialog | Drawer | Surface |
23
+ | -------------------- | ------------------------------------------- | ----------------------------- | ----------------------------- | ----------------------------- |
24
+ | Navegação | `PageNavigation` ou `PageBack` | `DialogNavigation` | `DrawerNavigation` | Não existe |
25
+ | Título no cabeçalho | `PageTitle` | `DialogTitle` | `DrawerTitle` | `SurfaceTitle` |
26
+ | Ações do cabeçalho | `PageActions` | `DialogActions` | `DrawerActions` | `SurfaceActions` |
27
+ | Intro | `Surface level={1}` no início do `PageBody` | `DialogIntro` | `DrawerIntro` | Não existe |
28
+ | Ações da intro | Ações da `Surface level={1}` | `DialogIntroActions` | `DrawerIntroActions` | Não existe |
29
+ | Corpo | `PageBody` | `DialogBody` | `DrawerBody` | `SurfaceBody` |
30
+ | Rodapé | `PageFooter` | `DialogFooter` | `DrawerFooter` | `SurfaceFooter` |
31
+ | Divisores | Antes do `PageFooter` | `sectioned` (padrão) | `sectioned` (padrão) | `sectioned` |
32
+ | Sem divisores | Cabeçalho e corpo | `continuous` | `continuous` | `continuous` (padrão) |
33
+
34
+ `layout` escolhe entre as duas anatomias em `DialogContent`, `DrawerContent` e `Surface`. Em
35
+ `sectioned`, uma linha separa as regiões vizinhas; em `continuous`, o espaço faz essa separação. Na
36
+ `Surface`, `sectioned` exige `framed` ou `elevation="raised"`, para que a linha tenha um limite. A
37
+ `Page` não oferece anatomia `sectioned`: cabeçalho e corpo são contínuos, enquanto o `PageFooter`
38
+ é uma faixa própria, separada por sua borda superior. O `layout` da página decide apenas a ocupação
39
+ de altura.
40
+
41
+ ## Onde cada ação fica
42
+
43
+ A posição de uma ação informa a que ela se refere. Escolha o lugar pelo alcance da ação, não pelo
44
+ espaço livre na tela.
45
+
46
+ | Lugar | Use para |
47
+ | ---------- | ---------------------------------------------------------------------------------------------------------- |
48
+ | Navegação | Sair ou voltar na estrutura: a trilha da página, a etapa anterior de um fluxo modal ou recolher o painel. |
49
+ | Cabeçalho | Ações que valem para a superfície inteira, como abrir uma conversa sobre o registro. |
50
+ | Intro | Ações ligadas ao título e ao conteúdo, como ver o histórico do registro. |
51
+ | Rodapé | A decisão da etapa: **Cancelar** em `ghost` e a ação principal sólida, alinhadas à direita. |
52
+
53
+ Em `Dialog` e `Drawer`, a navegação abre a linha do cabeçalho e fica junto do título, a `0.5rem`
54
+ dele. As ações ficam no extremo oposto, antes do botão de fechar, e mantêm a distância maior da
55
+ família até o título. Na `Page`, a navegação fica acima do título; quando o título está na intro,
56
+ ela divide a linha com as ações da página.
57
+
58
+ Em `Dialog` e `Drawer`, cabeçalho e rodapé permanecem visíveis enquanto o corpo rola; a intro rola
59
+ junto com o conteúdo.
60
+
61
+ ## Título no cabeçalho ou na intro
62
+
63
+ O título pode ficar em dois lugares, e essa escolha define a composição:
64
+
65
+ - **Com intro:** o cabeçalho guarda só a navegação, as ações da superfície e o fechamento. O título
66
+ abre o corpo junto das ações do conteúdo. Use quando esses controles da superfície precisam
67
+ continuar estáveis e separados do registro aberto.
68
+ - **Compacta:** título, ações do conteúdo e ações da superfície dividem a linha do cabeçalho, e a
69
+ intro é omitida. Use quando uma linha basta para nomear a tarefa e agir sobre ela.
70
+
71
+ Na composição compacta, coloque as ações do conteúdo antes das ações da superfície, no mesmo slot de
72
+ ações. Em `Dialog` e `Drawer`, o título no cabeçalho trunca quando não cabe na linha; na intro, ele
73
+ usa uma escala maior e quebra em mais de uma linha.
74
+
75
+ ## Dialog
76
+
77
+ Abra cada combinação para comparar. As linhas trocam o `layout` de `DialogContent`; as colunas
78
+ trocam o lugar do título. O campo e o rodapé são os mesmos nos quatro casos.
79
+
80
+ ```tsx preview
81
+ function OrderField() {
82
+ const id = useId();
83
+ return (
84
+ <div className="space-y-2">
85
+ <Label htmlFor={id}>Responsável</Label>
86
+ <Input id={id} defaultValue="Marina Alves" />
87
+ </div>
88
+ );
89
+ }
90
+
91
+ function DialogWithIntro({ layout, label }) {
92
+ return (
93
+ <Dialog>
94
+ <DialogTrigger asChild>
95
+ <Button variant="outline">{label}</Button>
96
+ </DialogTrigger>
97
+ <DialogContent layout={layout}>
98
+ <DialogHeader>
99
+ <DialogActions>
100
+ <Button variant="ghost" size="icon" aria-label="Abrir conversa">
101
+ <MessageCircle />
102
+ </Button>
103
+ </DialogActions>
104
+ </DialogHeader>
105
+ <DialogBody>
106
+ <DialogIntro>
107
+ <DialogTitle>Editar ordem de serviço</DialogTitle>
108
+ <DialogIntroActions>
109
+ <Button variant="outline" size="sm">
110
+ Ver histórico
111
+ </Button>
112
+ </DialogIntroActions>
113
+ </DialogIntro>
114
+ <OrderField />
115
+ </DialogBody>
116
+ <DialogFooter>
117
+ <ButtonGroup mode="spaced">
118
+ <DialogClose asChild>
119
+ <Button variant="ghost">Cancelar</Button>
120
+ </DialogClose>
121
+ <Button>Salvar</Button>
122
+ </ButtonGroup>
123
+ </DialogFooter>
124
+ </DialogContent>
125
+ </Dialog>
126
+ );
127
+ }
128
+
129
+ function CompactDialog({ layout, label }) {
130
+ return (
131
+ <Dialog>
132
+ <DialogTrigger asChild>
133
+ <Button variant="outline">{label}</Button>
134
+ </DialogTrigger>
135
+ <DialogContent layout={layout}>
136
+ <DialogHeader>
137
+ <DialogMedia>
138
+ <Wrench />
139
+ </DialogMedia>
140
+ <DialogTitle>Editar ordem de serviço</DialogTitle>
141
+ <DialogActions>
142
+ <Button variant="outline" size="sm">
143
+ Ver histórico
144
+ </Button>
145
+ <Button variant="ghost" size="icon" aria-label="Abrir conversa">
146
+ <MessageCircle />
147
+ </Button>
148
+ </DialogActions>
149
+ </DialogHeader>
150
+ <DialogBody>
151
+ <OrderField />
152
+ </DialogBody>
153
+ <DialogFooter>
154
+ <ButtonGroup mode="spaced">
155
+ <DialogClose asChild>
156
+ <Button variant="ghost">Cancelar</Button>
157
+ </DialogClose>
158
+ <Button>Salvar</Button>
159
+ </ButtonGroup>
160
+ </DialogFooter>
161
+ </DialogContent>
162
+ </Dialog>
163
+ );
164
+ }
165
+
166
+ render(
167
+ <div className="grid grid-cols-2 gap-3">
168
+ <DialogWithIntro layout="sectioned" label="Seccionado com intro" />
169
+ <CompactDialog layout="sectioned" label="Seccionado compacto" />
170
+ <DialogWithIntro layout="continuous" label="Contínuo com intro" />
171
+ <CompactDialog layout="continuous" label="Contínuo compacto" />
172
+ </div>,
173
+ );
174
+ ```
175
+
176
+ Prefira `continuous` para uma tarefa curta, em que a linha entre as regiões seria ruído. O `Dialog`
177
+ não recebe navegação neste exemplo: use `DialogNavigation` quando o modal trocar de etapa e a pessoa
178
+ precisar voltar à anterior.
179
+
180
+ ## Drawer
181
+
182
+ O `Drawer` segue a mesma grade. A navegação aparece aqui como o controle de recolher o painel: um
183
+ `DrawerClose` dentro de `DrawerNavigation`, com `showCloseButton={false}` para que o painel não tenha
184
+ dois controles de fechamento.
185
+
186
+ ```tsx preview
187
+ function OrderField() {
188
+ const id = useId();
189
+ return (
190
+ <div className="space-y-2">
191
+ <Label htmlFor={id}>Responsável</Label>
192
+ <Input id={id} defaultValue="Marina Alves" />
193
+ </div>
194
+ );
195
+ }
196
+
197
+ function CollapseButton() {
198
+ return (
199
+ <DrawerClose asChild>
200
+ <Button variant="ghost" size="icon" aria-label="Recolher painel">
201
+ <ChevronsRight />
202
+ </Button>
203
+ </DrawerClose>
204
+ );
205
+ }
206
+
207
+ function DrawerWithIntro({ layout, label }) {
208
+ return (
209
+ <Drawer>
210
+ <DrawerTrigger asChild>
211
+ <Button variant="outline">{label}</Button>
212
+ </DrawerTrigger>
213
+ <DrawerContent layout={layout} showCloseButton={false}>
214
+ <DrawerHeader>
215
+ <DrawerNavigation>
216
+ <CollapseButton />
217
+ </DrawerNavigation>
218
+ <DrawerActions>
219
+ <Button variant="ghost" size="icon" aria-label="Abrir conversa">
220
+ <MessageCircle />
221
+ </Button>
222
+ </DrawerActions>
223
+ </DrawerHeader>
224
+ <DrawerBody>
225
+ <DrawerIntro>
226
+ <DrawerTitle>Editar ordem de serviço</DrawerTitle>
227
+ <DrawerIntroActions>
228
+ <Button variant="outline" size="sm">
229
+ Ver histórico
230
+ </Button>
231
+ </DrawerIntroActions>
232
+ </DrawerIntro>
233
+ <OrderField />
234
+ </DrawerBody>
235
+ <DrawerFooter>
236
+ <ButtonGroup mode="spaced">
237
+ <DrawerClose asChild>
238
+ <Button variant="ghost">Cancelar</Button>
239
+ </DrawerClose>
240
+ <Button>Salvar</Button>
241
+ </ButtonGroup>
242
+ </DrawerFooter>
243
+ </DrawerContent>
244
+ </Drawer>
245
+ );
246
+ }
247
+
248
+ function CompactDrawer({ layout, label }) {
249
+ return (
250
+ <Drawer>
251
+ <DrawerTrigger asChild>
252
+ <Button variant="outline">{label}</Button>
253
+ </DrawerTrigger>
254
+ <DrawerContent layout={layout} showCloseButton={false}>
255
+ <DrawerHeader>
256
+ <DrawerNavigation>
257
+ <CollapseButton />
258
+ </DrawerNavigation>
259
+ <DrawerTitle>Editar ordem de serviço</DrawerTitle>
260
+ <DrawerActions>
261
+ <Button variant="outline" size="sm">
262
+ Ver histórico
263
+ </Button>
264
+ <Button variant="ghost" size="icon" aria-label="Abrir conversa">
265
+ <MessageCircle />
266
+ </Button>
267
+ </DrawerActions>
268
+ </DrawerHeader>
269
+ <DrawerBody>
270
+ <OrderField />
271
+ </DrawerBody>
272
+ <DrawerFooter>
273
+ <ButtonGroup mode="spaced">
274
+ <DrawerClose asChild>
275
+ <Button variant="ghost">Cancelar</Button>
276
+ </DrawerClose>
277
+ <Button>Salvar</Button>
278
+ </ButtonGroup>
279
+ </DrawerFooter>
280
+ </DrawerContent>
281
+ </Drawer>
282
+ );
283
+ }
284
+
285
+ render(
286
+ <div className="grid grid-cols-2 gap-3">
287
+ <DrawerWithIntro layout="sectioned" label="Seccionado com intro" />
288
+ <CompactDrawer layout="sectioned" label="Seccionado compacto" />
289
+ <DrawerWithIntro layout="continuous" label="Contínuo com intro" />
290
+ <CompactDrawer layout="continuous" label="Contínuo compacto" />
291
+ </div>,
292
+ );
293
+ ```
294
+
295
+ Mantenha `DrawerNavigation`, `DrawerTitle` e `DrawerActions` como filhos diretos de
296
+ `DrawerHeader`: o cabeçalho reconhece os slots pelo componente e interrompe a renderização diante de
297
+ qualquer outro nó, inclusive um componente local que devolva um slot. Por isso, o exemplo extrai
298
+ apenas o botão de recolher. Sem o botão de fechar, Esc e o clique no overlay continuam fechando o
299
+ painel.
300
+
301
+ ## Page
302
+
303
+ A `Page` não tem um slot de intro. Com intro, o `PageHeader` guarda a navegação e as ações da
304
+ página, e uma `Surface level={1}` abre o `PageBody` com o título e as ações do conteúdo. A página
305
+ precisa de exatamente um heading principal, por isso o `PageHeader` dessa composição não leva
306
+ `PageTitle`.
307
+
308
+ ```tsx preview col
309
+ render(
310
+ <div className="w-full overflow-hidden rounded-lg border border-border">
311
+ <Page>
312
+ <PageHeader>
313
+ <PageNavigation>
314
+ <Breadcrumb>
315
+ <BreadcrumbList>
316
+ <BreadcrumbItem>Oficina</BreadcrumbItem>
317
+ <BreadcrumbSeparator />
318
+ <BreadcrumbItem>Ordens de serviço</BreadcrumbItem>
319
+ <BreadcrumbSeparator />
320
+ <BreadcrumbPage>OS-1842</BreadcrumbPage>
321
+ </BreadcrumbList>
322
+ </Breadcrumb>
323
+ </PageNavigation>
324
+ <PageActions>
325
+ <Button variant="ghost" size="icon" aria-label="Abrir conversa">
326
+ <MessageCircle />
327
+ </Button>
328
+ </PageActions>
329
+ </PageHeader>
330
+ <PageBody>
331
+ <Surface
332
+ as="section"
333
+ level={1}
334
+ title="Editar ordem de serviço"
335
+ actions={
336
+ <Button variant="outline" size="sm">
337
+ Ver histórico
338
+ </Button>
339
+ }
340
+ >
341
+ <div className="space-y-2">
342
+ <Label htmlFor="page-intro-owner">Responsável</Label>
343
+ <Input id="page-intro-owner" defaultValue="Marina Alves" />
344
+ </div>
345
+ </Surface>
346
+ </PageBody>
347
+ <PageFooter>
348
+ <ButtonGroup mode="spaced">
349
+ <Button variant="ghost">Cancelar</Button>
350
+ <Button>Salvar</Button>
351
+ </ButtonGroup>
352
+ </PageFooter>
353
+ </Page>
354
+ </div>,
355
+ );
356
+ ```
357
+
358
+ Na composição compacta, `PageTitle` entra no `PageHeader` e as ações do conteúdo passam para
359
+ `PageActions`, antes das ações da página. A forma curta, `<Page title actions>`, produz esse mesmo
360
+ cabeçalho; nela, a navegação vem do [`PageShell`](/ui/page).
361
+
362
+ ```tsx preview col
363
+ render(
364
+ <div className="w-full overflow-hidden rounded-lg border border-border">
365
+ <Page>
366
+ <PageHeader>
367
+ <PageNavigation>
368
+ <Breadcrumb>
369
+ <BreadcrumbList>
370
+ <BreadcrumbItem>Oficina</BreadcrumbItem>
371
+ <BreadcrumbSeparator />
372
+ <BreadcrumbItem>Ordens de serviço</BreadcrumbItem>
373
+ <BreadcrumbSeparator />
374
+ <BreadcrumbPage>OS-1842</BreadcrumbPage>
375
+ </BreadcrumbList>
376
+ </Breadcrumb>
377
+ </PageNavigation>
378
+ <PageTitle>Editar ordem de serviço</PageTitle>
379
+ <PageActions>
380
+ <Button variant="outline" size="sm">
381
+ Ver histórico
382
+ </Button>
383
+ <Button variant="ghost" size="icon" aria-label="Abrir conversa">
384
+ <MessageCircle />
385
+ </Button>
386
+ </PageActions>
387
+ </PageHeader>
388
+ <PageBody>
389
+ <div className="space-y-2">
390
+ <Label htmlFor="page-compact-owner">Responsável</Label>
391
+ <Input id="page-compact-owner" defaultValue="Marina Alves" />
392
+ </div>
393
+ </PageBody>
394
+ <PageFooter>
395
+ <ButtonGroup mode="spaced">
396
+ <Button variant="ghost">Cancelar</Button>
397
+ <Button>Salvar</Button>
398
+ </ButtonGroup>
399
+ </PageFooter>
400
+ </Page>
401
+ </div>,
402
+ );
403
+ ```
404
+
405
+ Use `PageFooter` quando a página for uma etapa com decisão própria, como esta edição.
406
+
407
+ ## Pane com Surface
408
+
409
+ `Pane` declara uma coluna de um [`Split`](/ui/split) e não desenha nada: não tem cabeçalho, corpo,
410
+ rodapé, recuo, borda nem fundo. Quando a coluna precisa dessas regiões, componha uma `Surface`
411
+ dentro dela. A `Surface` não tem intro, então o título fica no `SurfaceHeader`, como na composição
412
+ compacta.
413
+
414
+ ```tsx preview col
415
+ render(
416
+ <div className="h-80 w-full overflow-hidden rounded-lg border border-border">
417
+ <Split>
418
+ <Pane initialSize="14rem">
419
+ <Sidebar className="w-full">
420
+ <SidebarNav
421
+ navLabel="Ordens de serviço"
422
+ activeId="os-1842"
423
+ groups={[
424
+ {
425
+ items: [
426
+ { id: "os-1842", label: "OS-1842" },
427
+ { id: "os-1839", label: "OS-1839" },
428
+ { id: "os-1835", label: "OS-1835" },
429
+ ],
430
+ },
431
+ ]}
432
+ />
433
+ </Sidebar>
434
+ </Pane>
435
+ <Pane grow>
436
+ <Surface
437
+ as="section"
438
+ framed
439
+ elevation="raised"
440
+ layout="sectioned"
441
+ className="flex h-full flex-col"
442
+ >
443
+ <SurfaceHeader>
444
+ <SurfaceMedia>
445
+ <Wrench />
446
+ </SurfaceMedia>
447
+ <SurfaceTitle>Editar ordem de serviço</SurfaceTitle>
448
+ <SurfaceActions>
449
+ <Button variant="outline" size="sm">
450
+ Ver histórico
451
+ </Button>
452
+ <Button variant="ghost" size="icon" aria-label="Abrir conversa">
453
+ <MessageCircle />
454
+ </Button>
455
+ </SurfaceActions>
456
+ </SurfaceHeader>
457
+ <SurfaceBody className="min-h-0 flex-1 overflow-y-auto">
458
+ <div className="space-y-2">
459
+ <Label htmlFor="pane-owner">Responsável</Label>
460
+ <Input id="pane-owner" defaultValue="Marina Alves" />
461
+ </div>
462
+ </SurfaceBody>
463
+ <SurfaceFooter>
464
+ <ButtonGroup mode="spaced">
465
+ <Button variant="ghost">Cancelar</Button>
466
+ <Button>Salvar</Button>
467
+ </ButtonGroup>
468
+ </SurfaceFooter>
469
+ </Surface>
470
+ </Pane>
471
+ </Split>
472
+ </div>,
473
+ );
474
+ ```
475
+
476
+ Os dois panes recebem conteúdos diferentes sem mudar de componente: o primeiro hospeda uma
477
+ [`Sidebar`](/ui/sidebar), que já tem anatomia própria, e o segundo, uma `Surface` completa. A
478
+ rolagem também pertence ao conteúdo: a `Surface` ocupa a altura do pane com `flex h-full flex-col`,
479
+ e o `SurfaceBody` declara `min-h-0 flex-1 overflow-y-auto` para rolar entre o cabeçalho e o rodapé.