@softize/opus 18.4.0 → 19.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.
- package/CHANGELOG.md +94 -0
- package/docs/adr/0020-icon-tiles-are-explicitly-composed.md +46 -0
- package/docs/adr/0021-content-carries-structure-and-semantic-element.md +65 -0
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +293 -0
- package/package.json +1 -1
- package/src/ui/components/patterns/content-header.tsx +169 -50
- package/src/ui/components/patterns/heading-level.ts +16 -0
- package/src/ui/components/patterns/list.tsx +76 -27
- package/src/ui/components/patterns/pane.tsx +294 -0
- package/src/ui/components/patterns/presentation.tsx +29 -17
- package/src/ui/components/patterns/sidebar.tsx +13 -72
- package/src/ui/components/patterns/split.tsx +8 -35
- package/src/ui/components/patterns/surface-anatomy.ts +219 -0
- package/src/ui/components/patterns/surface-header.tsx +51 -6
- package/src/ui/components/patterns/surface-media.tsx +69 -0
- package/src/ui/components/primitives/card.tsx +91 -29
- package/src/ui/components/primitives/command.tsx +12 -4
- package/src/ui/components/primitives/detail.tsx +10 -13
- package/src/ui/components/primitives/dialog.tsx +138 -35
- package/src/ui/components/primitives/drawer.tsx +122 -23
- package/src/ui/components/primitives/empty.tsx +2 -1
- package/src/ui/components/primitives/field.tsx +38 -16
- package/src/ui/components/primitives/icon-tile.tsx +91 -0
- package/src/ui/components/primitives/item.tsx +2 -1
- package/src/ui/components/primitives/metric-card.tsx +98 -39
- package/src/ui/components/primitives/table.tsx +1 -1
- package/src/ui/docs/content/action-list.md +61 -52
- package/src/ui/docs/content/card.md +221 -30
- package/src/ui/docs/content/content.md +90 -8
- package/src/ui/docs/content/dialog.md +115 -7
- package/src/ui/docs/content/drawer.md +136 -9
- package/src/ui/docs/content/field.md +78 -30
- package/src/ui/docs/content/icon-tile.md +47 -0
- package/src/ui/docs/content/metric-card.md +57 -18
- package/src/ui/docs/content/page.md +8 -0
- package/src/ui/docs/content/pane.md +250 -0
- package/src/ui/docs/content/sidebar.md +22 -43
- package/src/ui/docs/content/skeleton.md +7 -3
- package/src/ui/docs/content/split.md +16 -21
- package/src/ui/docs/registry.tsx +14 -2
- package/src/ui/meta.ts +17 -5
- package/src/ui/react.tsx +31 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pane — a superfície encaixável de um Split: a coluna ou faixa que hospeda navegação,
|
|
3
|
+
* lista ou conteúdo, com regiões fixas no topo e no rodapé e um corpo que rola.
|
|
4
|
+
*
|
|
5
|
+
* Até a ADR 0023 esta família vivia partida: a raiz em `split.tsx`, os slots em
|
|
6
|
+
* `sidebar.tsx`, sem validação entre eles e com padding assimétrico (header sem recuo,
|
|
7
|
+
* footer com `p-2`). Agora raiz e slots moram aqui e derivam a anatomia da mesma tabela
|
|
8
|
+
* das demais famílias.
|
|
9
|
+
*
|
|
10
|
+
* <Pane>
|
|
11
|
+
* <PaneHeader><PaneMedia>…</PaneMedia><PaneTitle>Conversas</PaneTitle><PaneActions>…</PaneActions></PaneHeader>
|
|
12
|
+
* <PaneBody>…</PaneBody>
|
|
13
|
+
* <PaneFooter>…</PaneFooter>
|
|
14
|
+
* </Pane>
|
|
15
|
+
*
|
|
16
|
+
* `inset` continua existindo para o pane que NÃO usa slots — a caixa simples que recebe
|
|
17
|
+
* conteúdo direto. Quando há slots, o recuo é de cada região, e o inset do pane vira
|
|
18
|
+
* `none` sozinho: o divisor precisa alcançar a borda.
|
|
19
|
+
*/
|
|
20
|
+
import {
|
|
21
|
+
Children,
|
|
22
|
+
Fragment,
|
|
23
|
+
isValidElement,
|
|
24
|
+
type CSSProperties,
|
|
25
|
+
type ReactElement,
|
|
26
|
+
type ReactNode,
|
|
27
|
+
} from "react";
|
|
28
|
+
|
|
29
|
+
import { cn } from "../../lib/cn.ts";
|
|
30
|
+
import { headingTags, type HeadingLevel } from "./heading-level.ts";
|
|
31
|
+
import {
|
|
32
|
+
SurfaceLayoutProvider,
|
|
33
|
+
surfaceActionsClasses,
|
|
34
|
+
surfaceDescriptionClasses,
|
|
35
|
+
surfaceHeaderRowClasses,
|
|
36
|
+
surfaceRegionClasses,
|
|
37
|
+
surfaceTitleClasses,
|
|
38
|
+
useSurfaceLayout,
|
|
39
|
+
type SurfaceLayout,
|
|
40
|
+
} from "./surface-anatomy.ts";
|
|
41
|
+
import { SurfaceHeader } from "./surface-header.tsx";
|
|
42
|
+
import { SurfaceMedia, type SurfaceMediaProps } from "./surface-media.tsx";
|
|
43
|
+
|
|
44
|
+
export type PaneSize =
|
|
45
|
+
| number
|
|
46
|
+
| `${number}%`
|
|
47
|
+
| `${number}rem`
|
|
48
|
+
| `${number}em`
|
|
49
|
+
| `${number}vh`
|
|
50
|
+
| `${number}vw`
|
|
51
|
+
| `${number}px`;
|
|
52
|
+
|
|
53
|
+
// Números continuam percentuais por compatibilidade com Split/Pane. Strings preservam
|
|
54
|
+
// a unidade explícita para layouts que precisam de um limite físico, como `28rem`.
|
|
55
|
+
export function paneSizeToCss(value: PaneSize): string {
|
|
56
|
+
return typeof value === "number" ? `${value}%` : value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface PaneProps {
|
|
60
|
+
/** Identidade estável do painel dentro do grupo. */
|
|
61
|
+
id?: string;
|
|
62
|
+
/** Tamanho inicial. Números representam porcentagens; strings aceitam %, rem, em, vh, vw e px. */
|
|
63
|
+
initialSize?: PaneSize;
|
|
64
|
+
/** Tamanho mínimo quando o split é redimensionável. */
|
|
65
|
+
minSize?: PaneSize;
|
|
66
|
+
/** Tamanho máximo quando o split é redimensionável. */
|
|
67
|
+
maxSize?: PaneSize;
|
|
68
|
+
/** Ocupa o espaço remanescente no layout simples. */
|
|
69
|
+
grow?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Respiro interno do pane sem slots. Com header, body ou footer como filhos DIRETOS o
|
|
72
|
+
* default passa a `none`, porque aí o recuo é de cada região. Quando os slots vêm
|
|
73
|
+
* dentro de outro componente — a `Sidebar`, por exemplo —, o pane não os enxerga:
|
|
74
|
+
* declare `inset="none"` você mesmo.
|
|
75
|
+
*/
|
|
76
|
+
inset?: "none" | "sm" | "md" | "lg";
|
|
77
|
+
/** Anatomia das regiões: `sectioned` (default) ou `continuous` — ADR 0023. */
|
|
78
|
+
layout?: SurfaceLayout;
|
|
79
|
+
className?: string;
|
|
80
|
+
children: ReactNode;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const insets = { none: "", sm: "p-2", md: "p-4", lg: "p-6" } as const;
|
|
84
|
+
|
|
85
|
+
/** Expande fragments de primeiro nível antes de procurar os slots. */
|
|
86
|
+
function flatten(children: ReactNode): ReactNode[] {
|
|
87
|
+
return Children.toArray(children).flatMap((node) =>
|
|
88
|
+
isValidElement(node) && node.type === Fragment
|
|
89
|
+
? Children.toArray((node.props as { children?: ReactNode }).children)
|
|
90
|
+
: [node],
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function hasStructuralSlots(children: ReactNode): boolean {
|
|
95
|
+
return flatten(children).some(
|
|
96
|
+
(node) =>
|
|
97
|
+
isValidElement(node) &&
|
|
98
|
+
(node.type === PaneHeader ||
|
|
99
|
+
node.type === PaneBody ||
|
|
100
|
+
node.type === PaneContent ||
|
|
101
|
+
node.type === PaneFooter),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Um conteúdo encaixável. Sua posição é dada pela ordem dentro de <Split>. */
|
|
106
|
+
export function Pane({
|
|
107
|
+
id,
|
|
108
|
+
initialSize,
|
|
109
|
+
grow = false,
|
|
110
|
+
inset,
|
|
111
|
+
layout = "sectioned",
|
|
112
|
+
className,
|
|
113
|
+
children,
|
|
114
|
+
}: PaneProps): ReactElement {
|
|
115
|
+
const style: CSSProperties | undefined =
|
|
116
|
+
initialSize === undefined
|
|
117
|
+
? undefined
|
|
118
|
+
: { flexBasis: paneSizeToCss(initialSize) };
|
|
119
|
+
const structural = hasStructuralSlots(children);
|
|
120
|
+
const resolvedInset = inset ?? (structural ? "none" : "md");
|
|
121
|
+
return (
|
|
122
|
+
<SurfaceLayoutProvider value={layout}>
|
|
123
|
+
<div
|
|
124
|
+
id={id === undefined ? undefined : String(id)}
|
|
125
|
+
data-slot="pane"
|
|
126
|
+
data-inset={resolvedInset}
|
|
127
|
+
data-layout={layout}
|
|
128
|
+
className={cn(
|
|
129
|
+
"min-h-0 min-w-0 shrink-0",
|
|
130
|
+
grow && "flex-1",
|
|
131
|
+
// Com slots o pane é uma coluna: header fixo, corpo que rola, footer fixo.
|
|
132
|
+
structural && "flex flex-col",
|
|
133
|
+
insets[resolvedInset],
|
|
134
|
+
className,
|
|
135
|
+
)}
|
|
136
|
+
style={style}
|
|
137
|
+
>
|
|
138
|
+
{children}
|
|
139
|
+
</div>
|
|
140
|
+
</SurfaceLayoutProvider>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Faixa fixa no topo de um pane — marca, contexto ou ações locais. */
|
|
145
|
+
export function PaneHeader({
|
|
146
|
+
className,
|
|
147
|
+
children,
|
|
148
|
+
...props
|
|
149
|
+
}: React.ComponentProps<"div">): ReactElement {
|
|
150
|
+
const layout = useSurfaceLayout("pane");
|
|
151
|
+
return (
|
|
152
|
+
<SurfaceHeader
|
|
153
|
+
name="PaneHeader"
|
|
154
|
+
slot="pane"
|
|
155
|
+
slots={{
|
|
156
|
+
media: PaneMedia,
|
|
157
|
+
title: PaneTitle,
|
|
158
|
+
description: PaneDescription,
|
|
159
|
+
actions: PaneActions,
|
|
160
|
+
}}
|
|
161
|
+
leadingPlacement="inline"
|
|
162
|
+
titleRequired={false}
|
|
163
|
+
// O pane é a superfície genérica: além dos slots, o header hospeda marca, busca ou
|
|
164
|
+
// chrome que não são título nem ação (ADR 0023).
|
|
165
|
+
allowFreeContent
|
|
166
|
+
rowClassName={surfaceHeaderRowClasses("pane")}
|
|
167
|
+
className={cn(surfaceRegionClasses("pane", layout, "header"), className)}
|
|
168
|
+
{...props}
|
|
169
|
+
>
|
|
170
|
+
{children}
|
|
171
|
+
</SurfaceHeader>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Moldura de ícone ou imagem que abre a linha do header. */
|
|
176
|
+
export function PaneMedia(props: SurfaceMediaProps): ReactElement {
|
|
177
|
+
return <SurfaceMedia slot="pane-media" size="xs" {...props} />;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function PaneTitle({
|
|
181
|
+
className,
|
|
182
|
+
level = 2,
|
|
183
|
+
...props
|
|
184
|
+
}: React.ComponentProps<"h2"> & {
|
|
185
|
+
/** Hierarquia do documento (`h1`–`h6`). O tamanho vem do layout, nunca do nível. */
|
|
186
|
+
level?: HeadingLevel;
|
|
187
|
+
}): ReactElement {
|
|
188
|
+
const layout = useSurfaceLayout("pane");
|
|
189
|
+
const Comp = headingTags[level];
|
|
190
|
+
return (
|
|
191
|
+
<Comp
|
|
192
|
+
data-slot="pane-title"
|
|
193
|
+
className={cn(
|
|
194
|
+
"min-w-0 truncate",
|
|
195
|
+
surfaceTitleClasses("pane", layout),
|
|
196
|
+
className,
|
|
197
|
+
)}
|
|
198
|
+
{...props}
|
|
199
|
+
/>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function PaneDescription({
|
|
204
|
+
className,
|
|
205
|
+
...props
|
|
206
|
+
}: React.ComponentProps<"p">): ReactElement {
|
|
207
|
+
return (
|
|
208
|
+
<p
|
|
209
|
+
data-slot="pane-description"
|
|
210
|
+
className={cn("truncate", surfaceDescriptionClasses("pane"), className)}
|
|
211
|
+
{...props}
|
|
212
|
+
/>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Ações do header — a família posiciona no extremo oposto ao título. */
|
|
217
|
+
export function PaneActions({
|
|
218
|
+
className,
|
|
219
|
+
...props
|
|
220
|
+
}: React.ComponentProps<"div">): ReactElement {
|
|
221
|
+
return (
|
|
222
|
+
<div
|
|
223
|
+
data-slot="pane-actions"
|
|
224
|
+
className={cn(surfaceActionsClasses, className)}
|
|
225
|
+
{...props}
|
|
226
|
+
/>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Região rolável que ocupa o espaço restante de um pane. */
|
|
231
|
+
export function PaneBody({
|
|
232
|
+
className,
|
|
233
|
+
children,
|
|
234
|
+
...props
|
|
235
|
+
}: React.ComponentProps<"div">): ReactElement {
|
|
236
|
+
const layout = useSurfaceLayout("pane");
|
|
237
|
+
return (
|
|
238
|
+
<div
|
|
239
|
+
data-slot="pane-body"
|
|
240
|
+
className={cn(
|
|
241
|
+
"min-h-0 flex-1 overflow-y-auto",
|
|
242
|
+
surfaceRegionClasses("pane", layout, "body"),
|
|
243
|
+
className,
|
|
244
|
+
)}
|
|
245
|
+
{...props}
|
|
246
|
+
>
|
|
247
|
+
{children}
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** @deprecated Use PaneBody. Alias mantido para migração; sai numa próxima versão major. */
|
|
253
|
+
export function PaneContent({
|
|
254
|
+
className,
|
|
255
|
+
children,
|
|
256
|
+
...props
|
|
257
|
+
}: React.ComponentProps<"div">): ReactElement {
|
|
258
|
+
const layout = useSurfaceLayout("pane");
|
|
259
|
+
return (
|
|
260
|
+
<div
|
|
261
|
+
data-slot="pane-content"
|
|
262
|
+
className={cn(
|
|
263
|
+
"min-h-0 flex-1 overflow-y-auto",
|
|
264
|
+
surfaceRegionClasses("pane", layout, "body"),
|
|
265
|
+
className,
|
|
266
|
+
)}
|
|
267
|
+
{...props}
|
|
268
|
+
>
|
|
269
|
+
{children}
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Faixa fixa no rodapé de um pane para ações persistentes. */
|
|
275
|
+
export function PaneFooter({
|
|
276
|
+
className,
|
|
277
|
+
children,
|
|
278
|
+
...props
|
|
279
|
+
}: React.ComponentProps<"div">): ReactElement {
|
|
280
|
+
const layout = useSurfaceLayout("pane");
|
|
281
|
+
return (
|
|
282
|
+
<div
|
|
283
|
+
data-slot="pane-footer"
|
|
284
|
+
className={cn(
|
|
285
|
+
"shrink-0",
|
|
286
|
+
surfaceRegionClasses("pane", layout, "footer"),
|
|
287
|
+
className,
|
|
288
|
+
)}
|
|
289
|
+
{...props}
|
|
290
|
+
>
|
|
291
|
+
{children}
|
|
292
|
+
</div>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
@@ -42,18 +42,22 @@ import { Copyable } from "../primitives/copyable.tsx";
|
|
|
42
42
|
import { DetailField, DetailGroup } from "../primitives/detail.tsx";
|
|
43
43
|
import {
|
|
44
44
|
Dialog,
|
|
45
|
+
DialogActions,
|
|
45
46
|
DialogBody,
|
|
46
47
|
DialogContent,
|
|
47
48
|
DialogFooter,
|
|
48
49
|
DialogHeader,
|
|
50
|
+
DialogNavigation,
|
|
49
51
|
DialogTitle,
|
|
50
52
|
} from "../primitives/dialog.tsx";
|
|
51
53
|
import {
|
|
52
54
|
Drawer,
|
|
55
|
+
DrawerActions,
|
|
53
56
|
DrawerBody,
|
|
54
57
|
DrawerContent,
|
|
55
58
|
DrawerFooter,
|
|
56
59
|
DrawerHeader,
|
|
60
|
+
DrawerNavigation,
|
|
57
61
|
DrawerTitle,
|
|
58
62
|
} from "../primitives/drawer.tsx";
|
|
59
63
|
import {
|
|
@@ -253,6 +257,11 @@ function PresentationFrame({
|
|
|
253
257
|
)}
|
|
254
258
|
</PageHeader>
|
|
255
259
|
)}
|
|
260
|
+
{contentOwnsHeading ? null : (
|
|
261
|
+
<PageIntro>
|
|
262
|
+
<PageTitle>{title}</PageTitle>
|
|
263
|
+
</PageIntro>
|
|
264
|
+
)}
|
|
256
265
|
<PageBody className="min-h-0 flex-1 overflow-hidden">
|
|
257
266
|
{withAssistant(
|
|
258
267
|
<div
|
|
@@ -261,11 +270,6 @@ function PresentationFrame({
|
|
|
261
270
|
layout === "fill" ? "overflow-y-hidden" : "overflow-y-auto",
|
|
262
271
|
)}
|
|
263
272
|
>
|
|
264
|
-
{contentOwnsHeading ? null : (
|
|
265
|
-
<PageIntro>
|
|
266
|
-
<PageTitle>{title}</PageTitle>
|
|
267
|
-
</PageIntro>
|
|
268
|
-
)}
|
|
269
273
|
{body}
|
|
270
274
|
</div>,
|
|
271
275
|
)}
|
|
@@ -334,14 +338,18 @@ function PresentationFrame({
|
|
|
334
338
|
>
|
|
335
339
|
<div className="flex h-full min-h-0 flex-col">
|
|
336
340
|
<DialogHeader>
|
|
337
|
-
{navigation
|
|
338
|
-
|
|
341
|
+
{navigation === undefined ? null : (
|
|
342
|
+
<DialogNavigation>{navigation}</DialogNavigation>
|
|
343
|
+
)}
|
|
344
|
+
<DialogTitle>{title}</DialogTitle>
|
|
339
345
|
{headerActions === undefined &&
|
|
340
346
|
assistantAction === undefined ? null : (
|
|
341
|
-
<
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
347
|
+
<DialogActions>
|
|
348
|
+
<PresentationActions>
|
|
349
|
+
{headerActions}
|
|
350
|
+
{assistantAction}
|
|
351
|
+
</PresentationActions>
|
|
352
|
+
</DialogActions>
|
|
345
353
|
)}
|
|
346
354
|
</DialogHeader>
|
|
347
355
|
{withAssistant(<DialogBody>{body}</DialogBody>)}
|
|
@@ -380,14 +388,18 @@ function PresentationFrame({
|
|
|
380
388
|
>
|
|
381
389
|
<div className="flex h-full min-h-0 flex-col">
|
|
382
390
|
<DrawerHeader>
|
|
383
|
-
{navigation
|
|
384
|
-
|
|
391
|
+
{navigation === undefined ? null : (
|
|
392
|
+
<DrawerNavigation>{navigation}</DrawerNavigation>
|
|
393
|
+
)}
|
|
394
|
+
<DrawerTitle>{title}</DrawerTitle>
|
|
385
395
|
{headerActions === undefined &&
|
|
386
396
|
assistantAction === undefined ? null : (
|
|
387
|
-
<
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
397
|
+
<DrawerActions>
|
|
398
|
+
<PresentationActions>
|
|
399
|
+
{headerActions}
|
|
400
|
+
{assistantAction}
|
|
401
|
+
</PresentationActions>
|
|
402
|
+
</DrawerActions>
|
|
391
403
|
)}
|
|
392
404
|
</DrawerHeader>
|
|
393
405
|
{withAssistant(<DrawerBody>{body}</DrawerBody>)}
|
|
@@ -7,6 +7,19 @@ import {
|
|
|
7
7
|
import { ChevronRight } from "lucide-react";
|
|
8
8
|
import { useOverflowing } from "../../lib/overflow.ts";
|
|
9
9
|
import { cn } from "../../lib/cn.ts";
|
|
10
|
+
|
|
11
|
+
// Os slots de Pane moram em `pane.tsx` desde a ADR 0023. A Sidebar continua sendo uma
|
|
12
|
+
// das superfícies que os compõe, então reexporta o que antes definia.
|
|
13
|
+
export {
|
|
14
|
+
PaneActions,
|
|
15
|
+
PaneBody,
|
|
16
|
+
PaneContent,
|
|
17
|
+
PaneDescription,
|
|
18
|
+
PaneFooter,
|
|
19
|
+
PaneHeader,
|
|
20
|
+
PaneMedia,
|
|
21
|
+
PaneTitle,
|
|
22
|
+
} from "./pane.tsx";
|
|
10
23
|
import { focusRing } from "../primitives/control.ts";
|
|
11
24
|
import {
|
|
12
25
|
Tooltip,
|
|
@@ -58,78 +71,6 @@ export function Sidebar({
|
|
|
58
71
|
);
|
|
59
72
|
}
|
|
60
73
|
|
|
61
|
-
/** Faixa fixa no topo de um pane — marca, contexto ou ações locais. */
|
|
62
|
-
export function PaneHeader({
|
|
63
|
-
className,
|
|
64
|
-
children,
|
|
65
|
-
}: {
|
|
66
|
-
className?: string;
|
|
67
|
-
children: ReactNode;
|
|
68
|
-
}): React.ReactElement {
|
|
69
|
-
return (
|
|
70
|
-
<div
|
|
71
|
-
data-slot="pane-header"
|
|
72
|
-
className={cn("shrink-0 border-b border-border", className)}
|
|
73
|
-
>
|
|
74
|
-
{children}
|
|
75
|
-
</div>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Região rolável que ocupa o espaço restante de um pane. */
|
|
80
|
-
export function PaneBody({
|
|
81
|
-
className,
|
|
82
|
-
children,
|
|
83
|
-
}: {
|
|
84
|
-
className?: string;
|
|
85
|
-
children: ReactNode;
|
|
86
|
-
}): React.ReactElement {
|
|
87
|
-
return (
|
|
88
|
-
<div
|
|
89
|
-
data-slot="pane-body"
|
|
90
|
-
className={cn("min-h-0 flex-1 overflow-y-auto", className)}
|
|
91
|
-
>
|
|
92
|
-
{children}
|
|
93
|
-
</div>
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** @deprecated Use PaneBody. Alias mantido para migração; sai numa próxima versão major. */
|
|
98
|
-
export function PaneContent({
|
|
99
|
-
className,
|
|
100
|
-
children,
|
|
101
|
-
}: {
|
|
102
|
-
className?: string;
|
|
103
|
-
children: ReactNode;
|
|
104
|
-
}): React.ReactElement {
|
|
105
|
-
return (
|
|
106
|
-
<div
|
|
107
|
-
data-slot="pane-content"
|
|
108
|
-
className={cn("min-h-0 flex-1 overflow-y-auto", className)}
|
|
109
|
-
>
|
|
110
|
-
{children}
|
|
111
|
-
</div>
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Faixa fixa no rodapé de um pane para ações persistentes. */
|
|
116
|
-
export function PaneFooter({
|
|
117
|
-
className,
|
|
118
|
-
children,
|
|
119
|
-
}: {
|
|
120
|
-
className?: string;
|
|
121
|
-
children: ReactNode;
|
|
122
|
-
}): React.ReactElement {
|
|
123
|
-
return (
|
|
124
|
-
<div
|
|
125
|
-
data-slot="pane-footer"
|
|
126
|
-
className={cn("shrink-0 border-t border-border p-2", className)}
|
|
127
|
-
>
|
|
128
|
-
{children}
|
|
129
|
-
</div>
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
74
|
export interface SidebarItemProps {
|
|
134
75
|
label: string;
|
|
135
76
|
icon?: ReactNode;
|
|
@@ -1,41 +1,18 @@
|
|
|
1
|
-
import { Children, isValidElement, type
|
|
1
|
+
import { Children, isValidElement, type ReactElement, type ReactNode } from 'react'
|
|
2
2
|
import { cn } from '../../lib/cn.ts'
|
|
3
3
|
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../primitives/resizable.tsx'
|
|
4
|
+
import { Pane, paneSizeToCss, type PaneProps, type PaneSize } from './pane.tsx'
|
|
5
|
+
|
|
6
|
+
// A família Pane mora em `pane.tsx` desde a ADR 0023; o Split continua sendo sua casa
|
|
7
|
+
// pública, então reexporta o que já era importado daqui.
|
|
8
|
+
export { Pane, PaneActions, PaneBody, PaneContent, PaneDescription, PaneFooter, PaneHeader, PaneMedia, PaneTitle } from './pane.tsx'
|
|
9
|
+
export type { PaneProps, PaneSize }
|
|
4
10
|
|
|
5
|
-
export type PaneSize = number | `${number}%` | `${number}rem` | `${number}em` | `${number}vh` | `${number}vw` | `${number}px`
|
|
6
11
|
export type SplitLayout = Record<string, number>
|
|
7
12
|
export interface SplitLayoutChange {
|
|
8
13
|
isUserInteraction: boolean
|
|
9
14
|
}
|
|
10
15
|
|
|
11
|
-
export interface PaneProps {
|
|
12
|
-
/** Identidade estável usada pelo layout redimensionável e por layouts persistidos. */
|
|
13
|
-
id?: string
|
|
14
|
-
/** Tamanho inicial. Números representam porcentagens; strings aceitam %, rem, em, vh, vw e px. */
|
|
15
|
-
initialSize?: PaneSize
|
|
16
|
-
/** Tamanho mínimo quando o split é redimensionável. */
|
|
17
|
-
minSize?: PaneSize
|
|
18
|
-
/** Tamanho máximo quando o split é redimensionável. */
|
|
19
|
-
maxSize?: PaneSize
|
|
20
|
-
/** Ocupa o espaço remanescente no layout simples. */
|
|
21
|
-
grow?: boolean
|
|
22
|
-
/** Respiro interno padrão. Use `none` para chrome, navegação ou conteúdo com inset próprio. */
|
|
23
|
-
inset?: 'none' | 'sm' | 'md' | 'lg'
|
|
24
|
-
className?: string
|
|
25
|
-
children: ReactNode
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Um conteúdo encaixável. Sua posição é dada pela ordem dentro de <Split>. */
|
|
29
|
-
export function Pane({ id, initialSize, grow = false, inset = 'md', className, children }: PaneProps): React.ReactElement {
|
|
30
|
-
const style: CSSProperties | undefined = initialSize === undefined ? undefined : { flexBasis: sizeToCss(initialSize) }
|
|
31
|
-
const insets = { none: '', sm: 'p-2', md: 'p-4', lg: 'p-6' } as const
|
|
32
|
-
return (
|
|
33
|
-
<div id={id === undefined ? undefined : String(id)} data-slot="pane" data-inset={inset} className={cn('min-h-0 min-w-0 shrink-0', grow && 'flex-1', insets[inset], className)} style={style}>
|
|
34
|
-
{children}
|
|
35
|
-
</div>
|
|
36
|
-
)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
16
|
export interface SplitProps {
|
|
40
17
|
/** Identidade estável do grupo redimensionável. */
|
|
41
18
|
id?: string
|
|
@@ -61,12 +38,8 @@ function panesOf(children: ReactNode): ReactElement<PaneProps>[] {
|
|
|
61
38
|
|
|
62
39
|
// Números continuam percentuais por compatibilidade com Split/Pane. Strings preservam
|
|
63
40
|
// a unidade explícita para layouts que precisam de um limite físico, como `28rem`.
|
|
64
|
-
function sizeToCss(value: PaneSize): string {
|
|
65
|
-
return typeof value === 'number' ? `${value}%` : value
|
|
66
|
-
}
|
|
67
|
-
|
|
68
41
|
function resizableSize(value: PaneSize | undefined): string | undefined {
|
|
69
|
-
return value === undefined ? undefined :
|
|
42
|
+
return value === undefined ? undefined : paneSizeToCss(value)
|
|
70
43
|
}
|
|
71
44
|
|
|
72
45
|
function percentage(value: PaneSize | undefined): number | undefined {
|