@softize/opus 18.5.0 → 20.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 +275 -0
- package/bin/cli.mjs +5 -0
- package/bin/lib/check.mjs +198 -10
- package/bin/lib/copy.mjs +29 -0
- package/bin/lib/mcp.mjs +1 -1
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +298 -0
- package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
- package/docs/data-products.md +15 -9
- package/docs/protocol.md +3 -4
- package/docs/releasing.md +2 -2
- package/package.json +2 -2
- package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
- package/registry/templates/app/package.json +1 -1
- package/src/core/runtime.ts +96 -8
- package/src/core/types.ts +5 -2
- package/src/ui/components/patterns/action-form-card.tsx +11 -1
- package/src/ui/components/patterns/content-header.tsx +16 -12
- package/src/ui/components/patterns/fill-region.ts +150 -0
- package/src/ui/components/patterns/heading-level.ts +16 -0
- package/src/ui/components/patterns/list.tsx +103 -73
- package/src/ui/components/patterns/page.tsx +20 -4
- package/src/ui/components/patterns/pane.tsx +294 -0
- package/src/ui/components/patterns/presentation.tsx +81 -54
- package/src/ui/components/patterns/sidebar.tsx +13 -72
- package/src/ui/components/patterns/split.tsx +51 -38
- package/src/ui/components/patterns/surface-anatomy.ts +246 -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/patterns/trigger.tsx +12 -2
- package/src/ui/components/primitives/alert.tsx +9 -1
- package/src/ui/components/primitives/avatar.tsx +7 -3
- package/src/ui/components/primitives/card.tsx +91 -29
- package/src/ui/components/primitives/carousel.tsx +34 -5
- package/src/ui/components/primitives/checkbox.tsx +11 -3
- package/src/ui/components/primitives/command.tsx +14 -5
- package/src/ui/components/primitives/copyable.tsx +120 -10
- package/src/ui/components/primitives/detail.tsx +10 -13
- package/src/ui/components/primitives/dialog.tsx +179 -45
- package/src/ui/components/primitives/drawer.tsx +131 -24
- package/src/ui/components/primitives/empty.tsx +1 -1
- package/src/ui/components/primitives/icon-picker.tsx +6 -2
- package/src/ui/components/primitives/icon-tile.tsx +7 -0
- package/src/ui/components/primitives/input-group.tsx +9 -2
- package/src/ui/components/primitives/input-otp.tsx +1 -1
- package/src/ui/components/primitives/kbd.tsx +1 -1
- package/src/ui/components/primitives/label.tsx +3 -0
- package/src/ui/components/primitives/menu.tsx +5 -0
- package/src/ui/components/primitives/metric-card.tsx +98 -39
- package/src/ui/components/primitives/popover.tsx +1 -1
- package/src/ui/components/primitives/progress.tsx +9 -1
- package/src/ui/components/primitives/slider.tsx +62 -11
- package/src/ui/components/primitives/truncate.tsx +10 -1
- package/src/ui/docs/content/accordion.md +74 -27
- package/src/ui/docs/content/action-form-card.md +24 -15
- package/src/ui/docs/content/action-form-dialog.md +17 -7
- package/src/ui/docs/content/action-form.md +92 -77
- package/src/ui/docs/content/action-list-dialog.md +75 -38
- package/src/ui/docs/content/action-list.md +134 -52
- package/src/ui/docs/content/action-trigger.md +27 -10
- package/src/ui/docs/content/action-view.md +14 -14
- package/src/ui/docs/content/actions.md +161 -66
- package/src/ui/docs/content/ai.md +43 -44
- package/src/ui/docs/content/alert.md +83 -29
- package/src/ui/docs/content/ask.md +51 -26
- package/src/ui/docs/content/aspect-ratio.md +22 -24
- package/src/ui/docs/content/audit.md +23 -19
- package/src/ui/docs/content/auth.md +38 -19
- package/src/ui/docs/content/avatar.md +54 -5
- package/src/ui/docs/content/badge.md +29 -9
- package/src/ui/docs/content/breadcrumb.md +21 -8
- package/src/ui/docs/content/button.md +73 -43
- package/src/ui/docs/content/calendar.md +134 -26
- package/src/ui/docs/content/card.md +252 -35
- package/src/ui/docs/content/carousel.md +37 -6
- package/src/ui/docs/content/chat.md +118 -37
- package/src/ui/docs/content/checkbox.md +73 -7
- package/src/ui/docs/content/cli.md +94 -55
- package/src/ui/docs/content/collapsible.md +43 -19
- package/src/ui/docs/content/command.md +30 -8
- package/src/ui/docs/content/composer.md +23 -8
- package/src/ui/docs/content/content.md +37 -7
- package/src/ui/docs/content/copyable.md +39 -8
- package/src/ui/docs/content/customization.md +55 -46
- package/src/ui/docs/content/cycle.md +33 -18
- package/src/ui/docs/content/data-products.md +115 -0
- package/src/ui/docs/content/data-state.md +20 -6
- package/src/ui/docs/content/data.md +20 -19
- package/src/ui/docs/content/detail.md +12 -5
- package/src/ui/docs/content/dialog.md +219 -28
- package/src/ui/docs/content/dictionary-value.md +4 -4
- package/src/ui/docs/content/dock.md +54 -19
- package/src/ui/docs/content/dot.md +35 -5
- package/src/ui/docs/content/drawer.md +193 -22
- package/src/ui/docs/content/empty-value.md +20 -6
- package/src/ui/docs/content/empty.md +24 -3
- package/src/ui/docs/content/events.md +76 -16
- package/src/ui/docs/content/executions.md +9 -9
- package/src/ui/docs/content/field.md +21 -6
- package/src/ui/docs/content/getting-started.md +44 -34
- package/src/ui/docs/content/icon-picker.md +19 -13
- package/src/ui/docs/content/icon-tile.md +28 -5
- package/src/ui/docs/content/input-otp.md +58 -7
- package/src/ui/docs/content/input.md +22 -3
- package/src/ui/docs/content/item.md +53 -6
- package/src/ui/docs/content/kbd.md +5 -1
- package/src/ui/docs/content/label.md +13 -4
- package/src/ui/docs/content/log.md +5 -5
- package/src/ui/docs/content/markdown.md +5 -4
- package/src/ui/docs/content/mcp.md +49 -23
- package/src/ui/docs/content/menu.md +95 -10
- package/src/ui/docs/content/metric-card.md +68 -21
- package/src/ui/docs/content/observability.md +10 -9
- package/src/ui/docs/content/page.md +125 -11
- package/src/ui/docs/content/pagination.md +13 -5
- package/src/ui/docs/content/pane.md +300 -0
- package/src/ui/docs/content/popover.md +42 -5
- package/src/ui/docs/content/presentation.md +91 -47
- package/src/ui/docs/content/progress.md +27 -14
- package/src/ui/docs/content/queue.md +39 -21
- package/src/ui/docs/content/radio-group.md +26 -4
- package/src/ui/docs/content/router.md +29 -10
- package/src/ui/docs/content/runtime.md +90 -54
- package/src/ui/docs/content/scheduler.md +53 -21
- package/src/ui/docs/content/scroll-area.md +42 -35
- package/src/ui/docs/content/select.md +78 -33
- package/src/ui/docs/content/semantic-context.md +21 -2
- package/src/ui/docs/content/separator.md +15 -9
- package/src/ui/docs/content/sidebar.md +25 -45
- package/src/ui/docs/content/skeleton.md +13 -4
- package/src/ui/docs/content/slider.md +37 -9
- package/src/ui/docs/content/spinner.md +4 -3
- package/src/ui/docs/content/split.md +34 -20
- package/src/ui/docs/content/storage.md +8 -9
- package/src/ui/docs/content/switch.md +10 -7
- package/src/ui/docs/content/table.md +46 -8
- package/src/ui/docs/content/tabs.md +14 -4
- package/src/ui/docs/content/testing.md +76 -31
- package/src/ui/docs/content/textarea.md +5 -4
- package/src/ui/docs/content/toast.md +48 -7
- package/src/ui/docs/content/toggle.md +3 -3
- package/src/ui/docs/content/tokens.md +15 -26
- package/src/ui/docs/content/tooltip.md +18 -2
- package/src/ui/docs/content/truncate.md +36 -6
- package/src/ui/docs/content/ui.md +74 -22
- package/src/ui/docs/content/upgrading.md +45 -36
- package/src/ui/docs/plugin.ts +36 -2
- package/src/ui/docs/registry.tsx +14 -2
- package/src/ui/drivers/react.tsx +16 -8
- package/src/ui/meta.ts +38 -32
- package/src/ui/react.tsx +14 -0
- package/src/vite/design.ts +16 -4
|
@@ -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 {
|
|
@@ -79,6 +83,7 @@ import {
|
|
|
79
83
|
ContentTitle,
|
|
80
84
|
} from "./content-header.tsx";
|
|
81
85
|
import { SurfaceAssistant } from "./surface-assistant.tsx";
|
|
86
|
+
import { fillRegionClassName, useFillRegion } from "./fill-region.ts";
|
|
82
87
|
|
|
83
88
|
interface PresentationFrameProps {
|
|
84
89
|
surface: PresentationSurface;
|
|
@@ -105,6 +110,35 @@ interface PresentationFrameProps {
|
|
|
105
110
|
const bodyClassName =
|
|
106
111
|
"h-full min-h-full [&>[data-slot=data-state]]:flex [&>[data-slot=data-state]]:min-h-full [&>[data-slot=data-state]]:items-center [&>[data-slot=data-state]]:justify-center";
|
|
107
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Corpo da Presentation. Em página `fill`, é o último elo da cadeia de altura (ADR 0024):
|
|
115
|
+
* a lista do body é filha direta dele e assume `fill` sem classe no consumidor.
|
|
116
|
+
*/
|
|
117
|
+
function PresentationBody({
|
|
118
|
+
layout,
|
|
119
|
+
className,
|
|
120
|
+
children,
|
|
121
|
+
}: {
|
|
122
|
+
layout: "flow" | "fill";
|
|
123
|
+
className?: string;
|
|
124
|
+
children: ReactNode;
|
|
125
|
+
}): ReactElement {
|
|
126
|
+
const fill = useFillRegion();
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
data-slot="presentation-body"
|
|
130
|
+
{...fill.attributes}
|
|
131
|
+
className={cn(
|
|
132
|
+
bodyClassName,
|
|
133
|
+
(layout === "fill" || fill.filling) && fillRegionClassName,
|
|
134
|
+
className,
|
|
135
|
+
)}
|
|
136
|
+
>
|
|
137
|
+
{fill.provide(children)}
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
108
142
|
function PresentationActions({
|
|
109
143
|
children,
|
|
110
144
|
equal = false,
|
|
@@ -162,17 +196,14 @@ function PresentationFrame({
|
|
|
162
196
|
</PresentationActions>
|
|
163
197
|
);
|
|
164
198
|
const body = (
|
|
165
|
-
<
|
|
166
|
-
data-slot="presentation-body"
|
|
167
|
-
className={cn(
|
|
168
|
-
bodyClassName,
|
|
169
|
-
layout === "fill" && "flex min-h-0 flex-1 flex-col",
|
|
170
|
-
bodyClassNameProp,
|
|
171
|
-
)}
|
|
172
|
-
>
|
|
199
|
+
<PresentationBody layout={layout} className={bodyClassNameProp}>
|
|
173
200
|
{typeof children === "function" ? children(footerTarget) : children}
|
|
174
|
-
</
|
|
201
|
+
</PresentationBody>
|
|
175
202
|
);
|
|
203
|
+
// Viewport entre o PageBody e o conteúdo. Em `fill`, é coluna flexível para a altura
|
|
204
|
+
// chegar ao conteúdo; a rolagem continua disponível como recurso de última instância.
|
|
205
|
+
const viewportFillClassName =
|
|
206
|
+
layout === "fill" ? fillRegionClassName : undefined;
|
|
176
207
|
const withAssistant = (content: ReactElement): ReactElement =>
|
|
177
208
|
assistantLabel === undefined || assistantPanel === undefined ? (
|
|
178
209
|
content
|
|
@@ -220,7 +251,12 @@ function PresentationFrame({
|
|
|
220
251
|
)}
|
|
221
252
|
<PageBody className="min-h-0 flex-1 overflow-hidden">
|
|
222
253
|
{withAssistant(
|
|
223
|
-
<div
|
|
254
|
+
<div
|
|
255
|
+
className={cn(
|
|
256
|
+
"h-full overflow-x-hidden overflow-y-auto",
|
|
257
|
+
viewportFillClassName,
|
|
258
|
+
)}
|
|
259
|
+
>
|
|
224
260
|
{content}
|
|
225
261
|
</div>,
|
|
226
262
|
)}
|
|
@@ -253,19 +289,20 @@ function PresentationFrame({
|
|
|
253
289
|
)}
|
|
254
290
|
</PageHeader>
|
|
255
291
|
)}
|
|
292
|
+
{contentOwnsHeading ? null : (
|
|
293
|
+
<PageIntro>
|
|
294
|
+
<PageTitle>{title}</PageTitle>
|
|
295
|
+
</PageIntro>
|
|
296
|
+
)}
|
|
256
297
|
<PageBody className="min-h-0 flex-1 overflow-hidden">
|
|
257
298
|
{withAssistant(
|
|
258
299
|
<div
|
|
259
300
|
className={cn(
|
|
260
301
|
"flex h-full min-h-0 flex-col overflow-x-hidden",
|
|
261
302
|
layout === "fill" ? "overflow-y-hidden" : "overflow-y-auto",
|
|
303
|
+
viewportFillClassName,
|
|
262
304
|
)}
|
|
263
305
|
>
|
|
264
|
-
{contentOwnsHeading ? null : (
|
|
265
|
-
<PageIntro>
|
|
266
|
-
<PageTitle>{title}</PageTitle>
|
|
267
|
-
</PageIntro>
|
|
268
|
-
)}
|
|
269
306
|
{body}
|
|
270
307
|
</div>,
|
|
271
308
|
)}
|
|
@@ -299,7 +336,12 @@ function PresentationFrame({
|
|
|
299
336
|
</PageHeader>
|
|
300
337
|
<PageBody className="min-h-0 flex-1 overflow-hidden">
|
|
301
338
|
{withAssistant(
|
|
302
|
-
<div
|
|
339
|
+
<div
|
|
340
|
+
className={cn(
|
|
341
|
+
"h-full overflow-x-hidden overflow-y-auto px-8 py-8",
|
|
342
|
+
viewportFillClassName,
|
|
343
|
+
)}
|
|
344
|
+
>
|
|
303
345
|
{body}
|
|
304
346
|
</div>,
|
|
305
347
|
)}
|
|
@@ -311,37 +353,30 @@ function PresentationFrame({
|
|
|
311
353
|
|
|
312
354
|
if (surface === "dialog") {
|
|
313
355
|
return (
|
|
314
|
-
<Dialog
|
|
315
|
-
open={open}
|
|
316
|
-
onOpenChange={(nextOpen) => {
|
|
317
|
-
if (!nextOpen && blocked) return;
|
|
318
|
-
onOpenChange?.(nextOpen);
|
|
319
|
-
}}
|
|
320
|
-
>
|
|
356
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
321
357
|
<DialogContent
|
|
322
358
|
className={cn(
|
|
323
359
|
assistantOpen ? "h-[min(48rem,85vh)] max-w-5xl" : undefined,
|
|
324
360
|
className,
|
|
325
361
|
)}
|
|
326
362
|
aria-describedby={undefined}
|
|
363
|
+
// `closeDisabled` bloqueia o botão de fechar, o Esc e o clique fora da superfície.
|
|
327
364
|
closeDisabled={blocked}
|
|
328
|
-
onEscapeKeyDown={(event) => {
|
|
329
|
-
if (blocked) event.preventDefault();
|
|
330
|
-
}}
|
|
331
|
-
onPointerDownOutside={(event) => {
|
|
332
|
-
if (blocked) event.preventDefault();
|
|
333
|
-
}}
|
|
334
365
|
>
|
|
335
366
|
<div className="flex h-full min-h-0 flex-col">
|
|
336
367
|
<DialogHeader>
|
|
337
|
-
{navigation
|
|
338
|
-
|
|
368
|
+
{navigation === undefined ? null : (
|
|
369
|
+
<DialogNavigation>{navigation}</DialogNavigation>
|
|
370
|
+
)}
|
|
371
|
+
<DialogTitle>{title}</DialogTitle>
|
|
339
372
|
{headerActions === undefined &&
|
|
340
373
|
assistantAction === undefined ? null : (
|
|
341
|
-
<
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
374
|
+
<DialogActions>
|
|
375
|
+
<PresentationActions>
|
|
376
|
+
{headerActions}
|
|
377
|
+
{assistantAction}
|
|
378
|
+
</PresentationActions>
|
|
379
|
+
</DialogActions>
|
|
345
380
|
)}
|
|
346
381
|
</DialogHeader>
|
|
347
382
|
{withAssistant(<DialogBody>{body}</DialogBody>)}
|
|
@@ -355,13 +390,7 @@ function PresentationFrame({
|
|
|
355
390
|
}
|
|
356
391
|
|
|
357
392
|
return (
|
|
358
|
-
<Drawer
|
|
359
|
-
open={open}
|
|
360
|
-
onOpenChange={(nextOpen) => {
|
|
361
|
-
if (!nextOpen && blocked) return;
|
|
362
|
-
onOpenChange?.(nextOpen);
|
|
363
|
-
}}
|
|
364
|
-
>
|
|
393
|
+
<Drawer open={open} onOpenChange={onOpenChange}>
|
|
365
394
|
<DrawerContent
|
|
366
395
|
className={cn(
|
|
367
396
|
assistantOpen
|
|
@@ -371,23 +400,21 @@ function PresentationFrame({
|
|
|
371
400
|
)}
|
|
372
401
|
aria-describedby={undefined}
|
|
373
402
|
closeDisabled={blocked}
|
|
374
|
-
onEscapeKeyDown={(event) => {
|
|
375
|
-
if (blocked) event.preventDefault();
|
|
376
|
-
}}
|
|
377
|
-
onPointerDownOutside={(event) => {
|
|
378
|
-
if (blocked) event.preventDefault();
|
|
379
|
-
}}
|
|
380
403
|
>
|
|
381
404
|
<div className="flex h-full min-h-0 flex-col">
|
|
382
405
|
<DrawerHeader>
|
|
383
|
-
{navigation
|
|
384
|
-
|
|
406
|
+
{navigation === undefined ? null : (
|
|
407
|
+
<DrawerNavigation>{navigation}</DrawerNavigation>
|
|
408
|
+
)}
|
|
409
|
+
<DrawerTitle>{title}</DrawerTitle>
|
|
385
410
|
{headerActions === undefined &&
|
|
386
411
|
assistantAction === undefined ? null : (
|
|
387
|
-
<
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
412
|
+
<DrawerActions>
|
|
413
|
+
<PresentationActions>
|
|
414
|
+
{headerActions}
|
|
415
|
+
{assistantAction}
|
|
416
|
+
</PresentationActions>
|
|
417
|
+
</DrawerActions>
|
|
391
418
|
)}
|
|
392
419
|
</DrawerHeader>
|
|
393
420
|
{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,
|
|
1
|
+
import { Children, cloneElement, Fragment, 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
|
|
@@ -53,20 +30,56 @@ export interface SplitProps {
|
|
|
53
30
|
children: ReactNode
|
|
54
31
|
}
|
|
55
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Expande fragments em qualquer profundidade, como quem escreve `<>{a}{b}</>` espera.
|
|
35
|
+
* `Children.toArray` numera as chaves por nível, então o filho de um fragment herda a
|
|
36
|
+
* chave do fragment como prefixo — sem isso, `.0` de dentro colidiria com `.0` de fora.
|
|
37
|
+
*/
|
|
38
|
+
function flattenFragments(children: ReactNode, prefix = ''): ReactNode[] {
|
|
39
|
+
return Children.toArray(children).flatMap((node): ReactNode[] => {
|
|
40
|
+
if (isValidElement(node) && node.type === Fragment)
|
|
41
|
+
return flattenFragments((node.props as { children?: ReactNode }).children, `${prefix}${String(node.key)}/`)
|
|
42
|
+
if (prefix !== '' && isValidElement(node)) return [cloneElement(node, { key: `${prefix}${String(node.key)}` })]
|
|
43
|
+
return [node]
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Nome legível do filho recusado, para a mensagem apontar o nó certo. */
|
|
48
|
+
function describeChild(node: ReactNode): string {
|
|
49
|
+
if (typeof node === 'string' || typeof node === 'number' || typeof node === 'bigint') {
|
|
50
|
+
const text = String(node).trim()
|
|
51
|
+
return `o texto "${text.length > 40 ? `${text.slice(0, 40)}…` : text}"`
|
|
52
|
+
}
|
|
53
|
+
if (isValidElement(node)) {
|
|
54
|
+
const type = node.type as string | { displayName?: string; name?: string }
|
|
55
|
+
const name = typeof type === 'string' ? type : (type.displayName ?? type.name ?? '')
|
|
56
|
+
return name === '' ? 'um componente sem nome' : `<${name}>`
|
|
57
|
+
}
|
|
58
|
+
return 'um nó que não é elemento React'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* O Split lê `initialSize`, `minSize`, `grow` e `id` de cada Pane ANTES de renderizá-lo,
|
|
63
|
+
* então só enxerga um `<Pane>` escrito diretamente (ou dentro de fragment). Qualquer outro
|
|
64
|
+
* filho seria descartado sem aviso — a tela perderia uma região e ninguém saberia por quê.
|
|
65
|
+
*/
|
|
56
66
|
function panesOf(children: ReactNode): ReactElement<PaneProps>[] {
|
|
57
|
-
return
|
|
58
|
-
(
|
|
59
|
-
|
|
67
|
+
return flattenFragments(children).flatMap((child) => {
|
|
68
|
+
if (isValidElement<PaneProps>(child) && child.type === Pane) return [child]
|
|
69
|
+
// Espaço em branco (um `{' '}` da formatação) não é conteúdo: nunca ocupou lugar.
|
|
70
|
+
if (typeof child === 'string' && child.trim() === '') return []
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Split recebeu ${describeChild(child)} como filho, mas só distribui <Pane>: esse conteúdo ficaria fora do layout. ` +
|
|
73
|
+
'Envolva-o em um <Pane>. Se ele vem de um componente que devolve um <Pane>, escreva o <Pane> dentro do Split e ' +
|
|
74
|
+
'passe o componente como filho dele: o Split lê o tamanho de cada pane antes de renderizá-lo.',
|
|
75
|
+
)
|
|
76
|
+
})
|
|
60
77
|
}
|
|
61
78
|
|
|
62
79
|
// Números continuam percentuais por compatibilidade com Split/Pane. Strings preservam
|
|
63
80
|
// 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
81
|
function resizableSize(value: PaneSize | undefined): string | undefined {
|
|
69
|
-
return value === undefined ? undefined :
|
|
82
|
+
return value === undefined ? undefined : paneSizeToCss(value)
|
|
70
83
|
}
|
|
71
84
|
|
|
72
85
|
function percentage(value: PaneSize | undefined): number | undefined {
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anatomia de superfície (interno): as medidas que Card, Dialog, Drawer e Pane
|
|
3
|
+
* compartilham — ADR 0023.
|
|
4
|
+
*
|
|
5
|
+
* Aqui mora o que antes era escrito seis vezes: recuo de cada região, ritmo vertical,
|
|
6
|
+
* escala do título, posição das ações e a regra dos divisores. As famílias públicas
|
|
7
|
+
* consomem estas tabelas; nenhuma reescreve as classes.
|
|
8
|
+
*
|
|
9
|
+
* Dois layouts, e cada um é um pacote indivisível:
|
|
10
|
+
*
|
|
11
|
+
* - `continuous` — sem divisores. O ritmo separa: o header se aproxima do corpo e a
|
|
12
|
+
* última região — seja ela o header, o body ou o footer — fecha o espaço de baixo com
|
|
13
|
+
* o mesmo respiro do topo. É o card de leitura.
|
|
14
|
+
* - `sectioned` — regiões simétricas, com divisor entre as que forem adjacentes. É o
|
|
15
|
+
* modal, o drawer e o painel.
|
|
16
|
+
*
|
|
17
|
+
* O recuo lateral é o mesmo nas três regiões da mesma superfície; é isso que a suíte
|
|
18
|
+
* transversal verifica. Entre famílias diferentes ele pode divergir: o card de leitura
|
|
19
|
+
* é mais folgado que um painel de operação.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
createContext,
|
|
24
|
+
createElement,
|
|
25
|
+
useContext,
|
|
26
|
+
type ReactElement,
|
|
27
|
+
type ReactNode,
|
|
28
|
+
} from "react";
|
|
29
|
+
import { FillBoundary } from "./fill-region.ts";
|
|
30
|
+
|
|
31
|
+
export type SurfaceFamily = "card" | "dialog" | "drawer" | "pane";
|
|
32
|
+
export type SurfaceLayout = "continuous" | "sectioned";
|
|
33
|
+
export type SurfaceRegion = "header" | "body" | "footer";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Divisores: a região desenha a própria linha quando existe uma vizinha do outro lado.
|
|
37
|
+
* Um header sozinho não fecha nada; um footer que é a única região também não.
|
|
38
|
+
*
|
|
39
|
+
* O header suprime a própria linha quando o vizinho imediato é um footer — os dois
|
|
40
|
+
* juntos desenhariam duas bordas encostadas, uma sobre a outra.
|
|
41
|
+
*/
|
|
42
|
+
const headerDivider = [
|
|
43
|
+
"[&:not(:last-child)]:border-b",
|
|
44
|
+
"[&:not(:last-child)]:border-border",
|
|
45
|
+
// A supressão precisa VENCER a regra acima na cascata, e não só vir depois dela: as
|
|
46
|
+
// duas nascem com a mesma especificidade e a ordem na folha é do Tailwind, não nossa.
|
|
47
|
+
// Repetir o `:not(:last-child)` aqui acrescenta uma pseudoclasse e desempata.
|
|
48
|
+
"[&:not(:last-child):has(+[data-slot$=-footer])]:border-b-0",
|
|
49
|
+
].join(" ");
|
|
50
|
+
const footerDivider =
|
|
51
|
+
"[&:not(:first-child)]:border-t [&:not(:first-child)]:border-border";
|
|
52
|
+
|
|
53
|
+
interface RegionAnatomy {
|
|
54
|
+
header: string;
|
|
55
|
+
body: string;
|
|
56
|
+
footer: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface FamilyAnatomy {
|
|
60
|
+
/** Recuo e ritmo por layout. O recuo lateral é comum às três regiões. */
|
|
61
|
+
regions: Record<SurfaceLayout, RegionAnatomy>;
|
|
62
|
+
/** Escala visual do título. O nível semântico do heading é escolhido à parte. */
|
|
63
|
+
title: Record<SurfaceLayout, string>;
|
|
64
|
+
/** Descrição no header, quando a família a oferece (ADRs 0014 e 0018 excluem modais). */
|
|
65
|
+
description: string;
|
|
66
|
+
/** Espaço entre mídia, título e ações na linha do header. */
|
|
67
|
+
headerGap: string;
|
|
68
|
+
/** Faixa do rodapé no layout seccionado, quando a família declara uma. */
|
|
69
|
+
footerBand: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* O recuo seccionado de Card, Dialog e Drawer: 1rem, a densidade das superfícies de
|
|
74
|
+
* operação, e o que permite um card seccionado conviver com um drawer sem degrau
|
|
75
|
+
* visível. O pane tem a sua (`px-3`, a do chrome), declarada na tabela abaixo.
|
|
76
|
+
*/
|
|
77
|
+
const sectionedRegions: RegionAnatomy = {
|
|
78
|
+
header: "p-4",
|
|
79
|
+
body: "p-4",
|
|
80
|
+
footer: "p-4",
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
|
|
84
|
+
card: {
|
|
85
|
+
regions: {
|
|
86
|
+
// O `pb-3` do header e do body encurtam a distância até a região seguinte; quando
|
|
87
|
+
// a região é a última, ela reassume o respiro de baixo (`[&:last-child]:pb-5`) —
|
|
88
|
+
// inclusive o header sozinho, que sem isso fecharia o card mais apertado embaixo
|
|
89
|
+
// do que em cima.
|
|
90
|
+
continuous: {
|
|
91
|
+
header: "px-5 pt-5 pb-3 [&:last-child]:pb-5",
|
|
92
|
+
body: "px-5 py-3 [&:last-child]:pb-5 [&:first-child]:pt-5",
|
|
93
|
+
footer: "px-5 pt-3 pb-5",
|
|
94
|
+
},
|
|
95
|
+
sectioned: sectionedRegions,
|
|
96
|
+
},
|
|
97
|
+
title: {
|
|
98
|
+
continuous: "text-base font-semibold leading-none",
|
|
99
|
+
sectioned: "text-sm font-semibold leading-none",
|
|
100
|
+
},
|
|
101
|
+
description: "text-sm text-muted-foreground",
|
|
102
|
+
headerGap: "gap-3",
|
|
103
|
+
footerBand: "",
|
|
104
|
+
},
|
|
105
|
+
dialog: {
|
|
106
|
+
regions: {
|
|
107
|
+
continuous: {
|
|
108
|
+
header: "p-4 pb-2 [&:last-child]:pb-4",
|
|
109
|
+
body: "p-4 py-2 [&:last-child]:pb-4 [&:first-child]:pt-4",
|
|
110
|
+
footer: "p-4 pt-2",
|
|
111
|
+
},
|
|
112
|
+
sectioned: sectionedRegions,
|
|
113
|
+
},
|
|
114
|
+
title: {
|
|
115
|
+
continuous: "text-lg font-semibold leading-none",
|
|
116
|
+
sectioned: "text-lg font-semibold leading-none",
|
|
117
|
+
},
|
|
118
|
+
description: "text-sm text-muted-foreground",
|
|
119
|
+
headerGap: "gap-4",
|
|
120
|
+
footerBand: "bg-muted/20 dark:bg-input/20",
|
|
121
|
+
},
|
|
122
|
+
drawer: {
|
|
123
|
+
regions: {
|
|
124
|
+
continuous: {
|
|
125
|
+
header: "p-4 pb-2 [&:last-child]:pb-4",
|
|
126
|
+
body: "p-4 py-2 [&:last-child]:pb-4 [&:first-child]:pt-4",
|
|
127
|
+
footer: "p-4 pt-2",
|
|
128
|
+
},
|
|
129
|
+
sectioned: sectionedRegions,
|
|
130
|
+
},
|
|
131
|
+
title: {
|
|
132
|
+
continuous: "text-lg font-semibold leading-none",
|
|
133
|
+
sectioned: "text-lg font-semibold leading-none",
|
|
134
|
+
},
|
|
135
|
+
description: "text-sm text-muted-foreground",
|
|
136
|
+
headerGap: "gap-4",
|
|
137
|
+
footerBand: "bg-muted/20 dark:bg-input/20",
|
|
138
|
+
},
|
|
139
|
+
pane: {
|
|
140
|
+
regions: {
|
|
141
|
+
continuous: {
|
|
142
|
+
header: "flex shrink-0 items-center px-3 pt-3 pb-2 [&:last-child]:pb-3",
|
|
143
|
+
body: "p-3 [&:first-child]:pt-3",
|
|
144
|
+
footer: "px-3 pt-2 pb-3",
|
|
145
|
+
},
|
|
146
|
+
sectioned: {
|
|
147
|
+
// A faixa tem a altura da barra do shell; `flex items-center` centraliza a linha
|
|
148
|
+
// dentro dela, e `shrink-0` impede que um corpo longo comprima a faixa.
|
|
149
|
+
header: "flex h-[3.75rem] shrink-0 items-center px-3",
|
|
150
|
+
body: "p-3",
|
|
151
|
+
footer: "px-3 py-2",
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
title: {
|
|
155
|
+
continuous: "text-sm font-semibold leading-none",
|
|
156
|
+
sectioned: "text-sm font-semibold leading-none",
|
|
157
|
+
},
|
|
158
|
+
description: "text-xs text-muted-foreground",
|
|
159
|
+
headerGap: "gap-2",
|
|
160
|
+
footerBand: "",
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/** Classes da região, já com o divisor que o layout deriva da vizinhança. */
|
|
165
|
+
export function surfaceRegionClasses(
|
|
166
|
+
family: SurfaceFamily,
|
|
167
|
+
layout: SurfaceLayout,
|
|
168
|
+
region: SurfaceRegion,
|
|
169
|
+
): string {
|
|
170
|
+
const anatomy = surfaceAnatomy[family];
|
|
171
|
+
const base = anatomy.regions[layout][region];
|
|
172
|
+
if (layout !== "sectioned") return base;
|
|
173
|
+
if (region === "header") return `${base} ${headerDivider}`;
|
|
174
|
+
if (region === "footer")
|
|
175
|
+
return [base, footerDivider, anatomy.footerBand].filter(Boolean).join(" ");
|
|
176
|
+
return base;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Escala visual do título — o nível do heading vem da composição, nunca daqui. */
|
|
180
|
+
export function surfaceTitleClasses(
|
|
181
|
+
family: SurfaceFamily,
|
|
182
|
+
layout: SurfaceLayout,
|
|
183
|
+
): string {
|
|
184
|
+
return surfaceAnatomy[family].title[layout];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function surfaceDescriptionClasses(family: SurfaceFamily): string {
|
|
188
|
+
return surfaceAnatomy[family].description;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A linha do header: mídia, heading que absorve a folga, ações no extremo oposto.
|
|
193
|
+
*
|
|
194
|
+
* `flex-1` não é decoração: quando a própria região é flex — o pane, que tem altura de
|
|
195
|
+
* barra —, a linha vira um item que se mede pelo conteúdo e as ações param no meio.
|
|
196
|
+
*/
|
|
197
|
+
export function surfaceHeaderRowClasses(family: SurfaceFamily): string {
|
|
198
|
+
return `flex min-w-0 flex-1 items-center ${surfaceAnatomy[family].headerGap}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Ações do header e do footer: fora do fluxo de compressão, na escala densa. */
|
|
202
|
+
export const surfaceActionsClasses = "flex shrink-0 items-center gap-2";
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* O layout é do root e chega aos slots por contexto — ADR 0023. Slot não recebe
|
|
206
|
+
* `layout`: o divisor depende de quem está do outro lado, então a decisão precisa valer
|
|
207
|
+
* para a superfície inteira.
|
|
208
|
+
*
|
|
209
|
+
* O default por família preserva o que ela já entregava antes desta ADR.
|
|
210
|
+
*/
|
|
211
|
+
const SurfaceLayoutContext = createContext<SurfaceLayout | null>(null);
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Publica o layout da superfície para os slots. A superfície também interrompe a
|
|
215
|
+
* propagação de `layout="fill"` da página (ADR 0024): a altura do seu corpo é decisão dela,
|
|
216
|
+
* inclusive quando um portal a renderiza dentro de uma página `fill`.
|
|
217
|
+
*/
|
|
218
|
+
export function SurfaceLayoutProvider({
|
|
219
|
+
value,
|
|
220
|
+
children,
|
|
221
|
+
}: {
|
|
222
|
+
value: SurfaceLayout;
|
|
223
|
+
children?: ReactNode;
|
|
224
|
+
}): ReactElement {
|
|
225
|
+
return createElement(
|
|
226
|
+
SurfaceLayoutContext.Provider,
|
|
227
|
+
{ value },
|
|
228
|
+
createElement(FillBoundary, null, children),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const familyDefaultLayout: Record<SurfaceFamily, SurfaceLayout> = {
|
|
233
|
+
card: "continuous",
|
|
234
|
+
dialog: "sectioned",
|
|
235
|
+
drawer: "sectioned",
|
|
236
|
+
pane: "sectioned",
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/** O layout vigente: o declarado pelo root da superfície, ou o default da família. */
|
|
240
|
+
export function useSurfaceLayout(family: SurfaceFamily): SurfaceLayout {
|
|
241
|
+
return useContext(SurfaceLayoutContext) ?? familyDefaultLayout[family];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function defaultSurfaceLayout(family: SurfaceFamily): SurfaceLayout {
|
|
245
|
+
return familyDefaultLayout[family];
|
|
246
|
+
}
|