@softize/opus 19.0.0 → 20.1.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 +239 -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/0020-icon-tiles-are-explicitly-composed.md +8 -0
- package/docs/adr/0021-content-carries-structure-and-semantic-element.md +7 -0
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +19 -1
- package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
- package/docs/adr/0025-content-frame-treatment-does-not-change-semantics.md +134 -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 +109 -18
- package/src/ui/components/patterns/fill-region.ts +150 -0
- package/src/ui/components/patterns/list.tsx +91 -92
- package/src/ui/components/patterns/page.tsx +20 -4
- package/src/ui/components/patterns/presentation.tsx +52 -37
- package/src/ui/components/patterns/split.tsx +44 -4
- package/src/ui/components/patterns/surface-anatomy.ts +35 -8
- package/src/ui/components/patterns/surface-header.tsx +58 -25
- 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 +1 -1
- 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 +2 -1
- package/src/ui/components/primitives/copyable.tsx +120 -10
- package/src/ui/components/primitives/dialog.tsx +42 -11
- package/src/ui/components/primitives/drawer.tsx +10 -2
- 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/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 +141 -58
- 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 +41 -15
- 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 +119 -27
- 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 +107 -24
- 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 +63 -19
- 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 +38 -15
- 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 +12 -4
- package/src/ui/docs/content/observability.md +10 -9
- package/src/ui/docs/content/page.md +118 -12
- package/src/ui/docs/content/pagination.md +13 -5
- package/src/ui/docs/content/pane.md +55 -5
- 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 +3 -2
- package/src/ui/docs/content/skeleton.md +6 -1
- 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 +19 -0
- 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 +6 -0
- package/src/ui/drivers/react.tsx +16 -8
- package/src/ui/meta.ts +32 -32
- package/src/ui/react.tsx +1 -0
- package/src/vite/design.ts +16 -4
|
@@ -66,6 +66,7 @@ import {
|
|
|
66
66
|
type DictLike,
|
|
67
67
|
} from "../../drivers/react.tsx";
|
|
68
68
|
import { cn } from "../../lib/cn.ts";
|
|
69
|
+
import { FillBoundary, useFillLeaf } from "./fill-region.ts";
|
|
69
70
|
import { objectSchemaShape } from "../../lib/object-schema.ts";
|
|
70
71
|
import { Badge } from "../primitives/badge.tsx";
|
|
71
72
|
import { DictionaryValue } from "../primitives/dictionary-value.tsx";
|
|
@@ -343,7 +344,12 @@ export interface ActionListProps<TInput, TItem> {
|
|
|
343
344
|
action: ListActionLike;
|
|
344
345
|
/** Input BASE (escopo fixo, ex.: { workspaceId }) — a toolbar soma por cima. */
|
|
345
346
|
input: TInput;
|
|
346
|
-
/**
|
|
347
|
+
/**
|
|
348
|
+
* `fill` ocupa a altura disponível e mantém a rolagem dentro da região de resultados.
|
|
349
|
+
* Sem valor, vale `fill` quando o elemento pai repassa a altura de uma `Page` ou
|
|
350
|
+
* `Presentation` em `fill` — `PageBody`, `ContentBody` ou o corpo da Presentation — e
|
|
351
|
+
* `flow` nos demais casos (ADR 0024).
|
|
352
|
+
*/
|
|
347
353
|
layout?: "flow" | "fill";
|
|
348
354
|
/** Ações do consumidor no fim da barra de filtros, depois dos controles de exibição. */
|
|
349
355
|
toolbarActions?: ReactNode;
|
|
@@ -1212,20 +1218,8 @@ function AdvancedFiltersDialog({
|
|
|
1212
1218
|
frameVariant={
|
|
1213
1219
|
sectionVariant === "framed" ? "legend" : "default"
|
|
1214
1220
|
}
|
|
1215
|
-
title={
|
|
1216
|
-
|
|
1217
|
-
{sectionIcons?.[section] !== undefined && (
|
|
1218
|
-
<span
|
|
1219
|
-
data-slot="content-title-icon"
|
|
1220
|
-
aria-hidden="true"
|
|
1221
|
-
className="inline-flex shrink-0 items-center justify-center text-muted-foreground [&_svg]:size-4"
|
|
1222
|
-
>
|
|
1223
|
-
{sectionIcons[section]}
|
|
1224
|
-
</span>
|
|
1225
|
-
)}
|
|
1226
|
-
{section}
|
|
1227
|
-
</span>
|
|
1228
|
-
}
|
|
1221
|
+
title={section}
|
|
1222
|
+
media={sectionIcons?.[section]}
|
|
1229
1223
|
>
|
|
1230
1224
|
<FieldGroup className={cn("grid gap-4", gridColumns)}>
|
|
1231
1225
|
{entries.map(field)}
|
|
@@ -1666,7 +1660,7 @@ function RowActions({ children }: { children: ReactNode }) {
|
|
|
1666
1660
|
export function ActionList<TInput, TItem>({
|
|
1667
1661
|
action,
|
|
1668
1662
|
input,
|
|
1669
|
-
layout
|
|
1663
|
+
layout: layoutProp,
|
|
1670
1664
|
toolbarActions,
|
|
1671
1665
|
columns,
|
|
1672
1666
|
cells,
|
|
@@ -1693,6 +1687,7 @@ export function ActionList<TInput, TItem>({
|
|
|
1693
1687
|
onStateChange,
|
|
1694
1688
|
}: ActionListProps<TInput, TItem>) {
|
|
1695
1689
|
const confirmBodyId = useId();
|
|
1690
|
+
const { layout, ref: rootRef } = useFillLeaf<HTMLDivElement>(layoutProp);
|
|
1696
1691
|
// Query-backed (useListAction): busca declarativa por [name, input] E refetch
|
|
1697
1692
|
// automático quando um form/trigger invalida a action (`invalidates: ['x.list']`).
|
|
1698
1693
|
// O input efetivo é computado abaixo; o hook re-busca quando ele muda.
|
|
@@ -2367,6 +2362,7 @@ export function ActionList<TInput, TItem>({
|
|
|
2367
2362
|
return (
|
|
2368
2363
|
<TooltipProvider>
|
|
2369
2364
|
<div
|
|
2365
|
+
ref={rootRef}
|
|
2370
2366
|
data-action={action.name}
|
|
2371
2367
|
data-layout={layout}
|
|
2372
2368
|
className={cn(
|
|
@@ -2375,84 +2371,87 @@ export function ActionList<TInput, TItem>({
|
|
|
2375
2371
|
: "space-y-3",
|
|
2376
2372
|
)}
|
|
2377
2373
|
>
|
|
2378
|
-
{
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2374
|
+
{/* A lista é a folha da cadeia de altura: o que ela hospeda decide a própria. */}
|
|
2375
|
+
<FillBoundary>
|
|
2376
|
+
{toolbar}
|
|
2377
|
+
{beforeContent}
|
|
2378
|
+
{selectionBar}
|
|
2379
|
+
{/* Tabela e rodapé formam um bloco só: o `gap` da pilha separa a barra de
|
|
2380
|
+
ferramentas do conteúdo, nunca o conteúdo do seu próprio rodapé. Duas
|
|
2381
|
+
linhas: a dos resultados absorve a altura que sobra, a do rodapé se mede
|
|
2382
|
+
pelo conteúdo. Em `flow` a primeira linha também é `1fr`, mas o bloco não
|
|
2383
|
+
tem altura própria e ela vira a altura do conteúdo. */}
|
|
2384
|
+
<div className={cn("grid min-h-0 grid-rows-[1fr_auto]", layout === "fill" && "flex-1")}>
|
|
2385
|
+
{/* A rolagem mora aqui, nos dois eixos: é este elemento que o `sticky` do
|
|
2386
|
+
cabeçalho enxerga. Um contêiner de rolagem entre ele e a tabela roubaria
|
|
2387
|
+
essa referência e o cabeçalho voltaria a subir junto com as linhas.
|
|
2388
|
+
Refetch com a lista já na tela: esmaece e trava interação até chegar. */}
|
|
2389
|
+
<div
|
|
2390
|
+
aria-busy={busy}
|
|
2391
|
+
className={cn(
|
|
2392
|
+
"min-h-0",
|
|
2393
|
+
layout === "fill" && "overflow-auto",
|
|
2394
|
+
busy && items.length > 0 && "pointer-events-none opacity-60",
|
|
2395
|
+
)}
|
|
2396
|
+
>
|
|
2397
|
+
{body}
|
|
2398
|
+
</div>
|
|
2399
|
+
{footer}
|
|
2400
2400
|
</div>
|
|
2401
|
-
{
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
if (!o) setConfirming(null);
|
|
2409
|
-
}}
|
|
2410
|
-
>
|
|
2411
|
-
<DialogContent
|
|
2412
|
-
aria-describedby={
|
|
2413
|
-
confirming.confirm?.message === undefined
|
|
2414
|
-
? undefined
|
|
2415
|
-
: confirmBodyId
|
|
2416
|
-
}
|
|
2401
|
+
{confirming !== null && (
|
|
2402
|
+
<Dialog
|
|
2403
|
+
mode="alert"
|
|
2404
|
+
open
|
|
2405
|
+
onOpenChange={(o) => {
|
|
2406
|
+
if (!o) setConfirming(null);
|
|
2407
|
+
}}
|
|
2417
2408
|
>
|
|
2418
|
-
<
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
</
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
variant="
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2409
|
+
<DialogContent
|
|
2410
|
+
aria-describedby={
|
|
2411
|
+
confirming.confirm?.message === undefined
|
|
2412
|
+
? undefined
|
|
2413
|
+
: confirmBodyId
|
|
2414
|
+
}
|
|
2415
|
+
>
|
|
2416
|
+
<DialogHeader>
|
|
2417
|
+
<DialogTitle>
|
|
2418
|
+
{confirming.confirm?.title ?? confirming.label}
|
|
2419
|
+
</DialogTitle>
|
|
2420
|
+
</DialogHeader>
|
|
2421
|
+
{confirming.confirm?.message !== undefined && (
|
|
2422
|
+
<DialogBody
|
|
2423
|
+
id={confirmBodyId}
|
|
2424
|
+
className="text-sm text-muted-foreground"
|
|
2425
|
+
>
|
|
2426
|
+
{confirming.confirm.message}
|
|
2427
|
+
</DialogBody>
|
|
2428
|
+
)}
|
|
2429
|
+
<DialogFooter>
|
|
2430
|
+
<ButtonGroup mode="spaced" distribution="equal">
|
|
2431
|
+
<DialogClose initialFocus asChild>
|
|
2432
|
+
<Button variant="outline">Cancelar</Button>
|
|
2433
|
+
</DialogClose>
|
|
2434
|
+
<DialogClose asChild>
|
|
2435
|
+
<Button
|
|
2436
|
+
context={
|
|
2437
|
+
confirming.destructive === true ? "danger" : "primary"
|
|
2438
|
+
}
|
|
2439
|
+
variant="solid"
|
|
2440
|
+
onClick={() => {
|
|
2441
|
+
const a = confirming;
|
|
2442
|
+
setConfirming(null);
|
|
2443
|
+
void runBatch(a);
|
|
2444
|
+
}}
|
|
2445
|
+
>
|
|
2446
|
+
{confirming.label}
|
|
2447
|
+
</Button>
|
|
2448
|
+
</DialogClose>
|
|
2449
|
+
</ButtonGroup>
|
|
2450
|
+
</DialogFooter>
|
|
2451
|
+
</DialogContent>
|
|
2452
|
+
</Dialog>
|
|
2453
|
+
)}
|
|
2454
|
+
</FillBoundary>
|
|
2456
2455
|
</div>
|
|
2457
2456
|
</TooltipProvider>
|
|
2458
2457
|
);
|
|
@@ -30,6 +30,11 @@ import {
|
|
|
30
30
|
} from "./page-heading-context.tsx";
|
|
31
31
|
import { SurfaceHeader, surfaceHeaderClasses } from "./surface-header.tsx";
|
|
32
32
|
import { PageState, PageStatePresenceProvider } from "./page-state.tsx";
|
|
33
|
+
import {
|
|
34
|
+
FillRoot,
|
|
35
|
+
fillRegionClassName,
|
|
36
|
+
useFillRegion,
|
|
37
|
+
} from "./fill-region.ts";
|
|
33
38
|
|
|
34
39
|
interface PageContextValue {}
|
|
35
40
|
|
|
@@ -205,7 +210,10 @@ interface PageBaseProps extends Omit<
|
|
|
205
210
|
> {
|
|
206
211
|
/** Classes do container. Default: centralizado com `max-w-7xl` (80rem). */
|
|
207
212
|
className?: string;
|
|
208
|
-
/**
|
|
213
|
+
/**
|
|
214
|
+
* `fill` limita a página à altura do shell para que regiões internas assumam a rolagem.
|
|
215
|
+
* A altura chega sozinha a uma `ActionList` composta por `PageBody` e `Content` (ADR 0024).
|
|
216
|
+
*/
|
|
209
217
|
layout?: "flow" | "fill";
|
|
210
218
|
}
|
|
211
219
|
|
|
@@ -455,7 +463,7 @@ export function Page({
|
|
|
455
463
|
className,
|
|
456
464
|
)}
|
|
457
465
|
>
|
|
458
|
-
{content}
|
|
466
|
+
<FillRoot active={layout === "fill"}>{content}</FillRoot>
|
|
459
467
|
</div>
|
|
460
468
|
</main>
|
|
461
469
|
</PageHeadingPresenceProvider>
|
|
@@ -671,17 +679,25 @@ export function PageBack({
|
|
|
671
679
|
|
|
672
680
|
export function PageBody({
|
|
673
681
|
className,
|
|
682
|
+
children,
|
|
674
683
|
...props
|
|
675
684
|
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
676
685
|
const page = useContext(PageContext);
|
|
677
686
|
const integralState = useContext(PageIntegralStateContext);
|
|
687
|
+
const fill = useFillRegion();
|
|
678
688
|
requireParent(page !== null, "PageBody", "Page");
|
|
679
689
|
return (
|
|
680
690
|
<div
|
|
681
691
|
data-slot="page-body"
|
|
682
|
-
|
|
692
|
+
{...fill.attributes}
|
|
693
|
+
className={cn(
|
|
694
|
+
(integralState || fill.filling) && fillRegionClassName,
|
|
695
|
+
className,
|
|
696
|
+
)}
|
|
683
697
|
{...props}
|
|
684
|
-
|
|
698
|
+
>
|
|
699
|
+
{fill.provide(children)}
|
|
700
|
+
</div>
|
|
685
701
|
);
|
|
686
702
|
}
|
|
687
703
|
|
|
@@ -83,6 +83,7 @@ import {
|
|
|
83
83
|
ContentTitle,
|
|
84
84
|
} from "./content-header.tsx";
|
|
85
85
|
import { SurfaceAssistant } from "./surface-assistant.tsx";
|
|
86
|
+
import { fillRegionClassName, useFillRegion } from "./fill-region.ts";
|
|
86
87
|
|
|
87
88
|
interface PresentationFrameProps {
|
|
88
89
|
surface: PresentationSurface;
|
|
@@ -109,6 +110,35 @@ interface PresentationFrameProps {
|
|
|
109
110
|
const bodyClassName =
|
|
110
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";
|
|
111
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
|
+
|
|
112
142
|
function PresentationActions({
|
|
113
143
|
children,
|
|
114
144
|
equal = false,
|
|
@@ -166,17 +196,14 @@ function PresentationFrame({
|
|
|
166
196
|
</PresentationActions>
|
|
167
197
|
);
|
|
168
198
|
const body = (
|
|
169
|
-
<
|
|
170
|
-
data-slot="presentation-body"
|
|
171
|
-
className={cn(
|
|
172
|
-
bodyClassName,
|
|
173
|
-
layout === "fill" && "flex min-h-0 flex-1 flex-col",
|
|
174
|
-
bodyClassNameProp,
|
|
175
|
-
)}
|
|
176
|
-
>
|
|
199
|
+
<PresentationBody layout={layout} className={bodyClassNameProp}>
|
|
177
200
|
{typeof children === "function" ? children(footerTarget) : children}
|
|
178
|
-
</
|
|
201
|
+
</PresentationBody>
|
|
179
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;
|
|
180
207
|
const withAssistant = (content: ReactElement): ReactElement =>
|
|
181
208
|
assistantLabel === undefined || assistantPanel === undefined ? (
|
|
182
209
|
content
|
|
@@ -224,7 +251,12 @@ function PresentationFrame({
|
|
|
224
251
|
)}
|
|
225
252
|
<PageBody className="min-h-0 flex-1 overflow-hidden">
|
|
226
253
|
{withAssistant(
|
|
227
|
-
<div
|
|
254
|
+
<div
|
|
255
|
+
className={cn(
|
|
256
|
+
"h-full overflow-x-hidden overflow-y-auto",
|
|
257
|
+
viewportFillClassName,
|
|
258
|
+
)}
|
|
259
|
+
>
|
|
228
260
|
{content}
|
|
229
261
|
</div>,
|
|
230
262
|
)}
|
|
@@ -268,6 +300,7 @@ function PresentationFrame({
|
|
|
268
300
|
className={cn(
|
|
269
301
|
"flex h-full min-h-0 flex-col overflow-x-hidden",
|
|
270
302
|
layout === "fill" ? "overflow-y-hidden" : "overflow-y-auto",
|
|
303
|
+
viewportFillClassName,
|
|
271
304
|
)}
|
|
272
305
|
>
|
|
273
306
|
{body}
|
|
@@ -303,7 +336,12 @@ function PresentationFrame({
|
|
|
303
336
|
</PageHeader>
|
|
304
337
|
<PageBody className="min-h-0 flex-1 overflow-hidden">
|
|
305
338
|
{withAssistant(
|
|
306
|
-
<div
|
|
339
|
+
<div
|
|
340
|
+
className={cn(
|
|
341
|
+
"h-full overflow-x-hidden overflow-y-auto px-8 py-8",
|
|
342
|
+
viewportFillClassName,
|
|
343
|
+
)}
|
|
344
|
+
>
|
|
307
345
|
{body}
|
|
308
346
|
</div>,
|
|
309
347
|
)}
|
|
@@ -315,26 +353,15 @@ function PresentationFrame({
|
|
|
315
353
|
|
|
316
354
|
if (surface === "dialog") {
|
|
317
355
|
return (
|
|
318
|
-
<Dialog
|
|
319
|
-
open={open}
|
|
320
|
-
onOpenChange={(nextOpen) => {
|
|
321
|
-
if (!nextOpen && blocked) return;
|
|
322
|
-
onOpenChange?.(nextOpen);
|
|
323
|
-
}}
|
|
324
|
-
>
|
|
356
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
325
357
|
<DialogContent
|
|
326
358
|
className={cn(
|
|
327
359
|
assistantOpen ? "h-[min(48rem,85vh)] max-w-5xl" : undefined,
|
|
328
360
|
className,
|
|
329
361
|
)}
|
|
330
362
|
aria-describedby={undefined}
|
|
363
|
+
// `closeDisabled` bloqueia o botão de fechar, o Esc e o clique fora da superfície.
|
|
331
364
|
closeDisabled={blocked}
|
|
332
|
-
onEscapeKeyDown={(event) => {
|
|
333
|
-
if (blocked) event.preventDefault();
|
|
334
|
-
}}
|
|
335
|
-
onPointerDownOutside={(event) => {
|
|
336
|
-
if (blocked) event.preventDefault();
|
|
337
|
-
}}
|
|
338
365
|
>
|
|
339
366
|
<div className="flex h-full min-h-0 flex-col">
|
|
340
367
|
<DialogHeader>
|
|
@@ -363,13 +390,7 @@ function PresentationFrame({
|
|
|
363
390
|
}
|
|
364
391
|
|
|
365
392
|
return (
|
|
366
|
-
<Drawer
|
|
367
|
-
open={open}
|
|
368
|
-
onOpenChange={(nextOpen) => {
|
|
369
|
-
if (!nextOpen && blocked) return;
|
|
370
|
-
onOpenChange?.(nextOpen);
|
|
371
|
-
}}
|
|
372
|
-
>
|
|
393
|
+
<Drawer open={open} onOpenChange={onOpenChange}>
|
|
373
394
|
<DrawerContent
|
|
374
395
|
className={cn(
|
|
375
396
|
assistantOpen
|
|
@@ -379,12 +400,6 @@ function PresentationFrame({
|
|
|
379
400
|
)}
|
|
380
401
|
aria-describedby={undefined}
|
|
381
402
|
closeDisabled={blocked}
|
|
382
|
-
onEscapeKeyDown={(event) => {
|
|
383
|
-
if (blocked) event.preventDefault();
|
|
384
|
-
}}
|
|
385
|
-
onPointerDownOutside={(event) => {
|
|
386
|
-
if (blocked) event.preventDefault();
|
|
387
|
-
}}
|
|
388
403
|
>
|
|
389
404
|
<div className="flex h-full min-h-0 flex-col">
|
|
390
405
|
<DrawerHeader>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Children, isValidElement, type ReactElement, type ReactNode } from 'react'
|
|
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
4
|
import { Pane, paneSizeToCss, type PaneProps, type PaneSize } from './pane.tsx'
|
|
@@ -30,10 +30,50 @@ export interface SplitProps {
|
|
|
30
30
|
children: ReactNode
|
|
31
31
|
}
|
|
32
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
|
+
*/
|
|
33
66
|
function panesOf(children: ReactNode): ReactElement<PaneProps>[] {
|
|
34
|
-
return
|
|
35
|
-
(
|
|
36
|
-
|
|
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
|
+
})
|
|
37
77
|
}
|
|
38
78
|
|
|
39
79
|
// Números continuam percentuais por compatibilidade com Split/Pane. Strings preservam
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* Dois layouts, e cada um é um pacote indivisível:
|
|
10
10
|
*
|
|
11
11
|
* - `continuous` — sem divisores. O ritmo separa: o header se aproxima do corpo e a
|
|
12
|
-
* última região
|
|
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.
|
|
13
14
|
* - `sectioned` — regiões simétricas, com divisor entre as que forem adjacentes. É o
|
|
14
15
|
* modal, o drawer e o painel.
|
|
15
16
|
*
|
|
@@ -18,7 +19,14 @@
|
|
|
18
19
|
* é mais folgado que um painel de operação.
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
createContext,
|
|
24
|
+
createElement,
|
|
25
|
+
useContext,
|
|
26
|
+
type ReactElement,
|
|
27
|
+
type ReactNode,
|
|
28
|
+
} from "react";
|
|
29
|
+
import { FillBoundary } from "./fill-region.ts";
|
|
22
30
|
|
|
23
31
|
export type SurfaceFamily = "card" | "dialog" | "drawer" | "pane";
|
|
24
32
|
export type SurfaceLayout = "continuous" | "sectioned";
|
|
@@ -76,9 +84,11 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
|
|
|
76
84
|
card: {
|
|
77
85
|
regions: {
|
|
78
86
|
// O `pb-3` do header e do body encurtam a distância até a região seguinte; quando
|
|
79
|
-
//
|
|
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.
|
|
80
90
|
continuous: {
|
|
81
|
-
header: "px-5 pt-5 pb-3",
|
|
91
|
+
header: "px-5 pt-5 pb-3 [&:last-child]:pb-5",
|
|
82
92
|
body: "px-5 py-3 [&:last-child]:pb-5 [&:first-child]:pt-5",
|
|
83
93
|
footer: "px-5 pt-3 pb-5",
|
|
84
94
|
},
|
|
@@ -95,7 +105,7 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
|
|
|
95
105
|
dialog: {
|
|
96
106
|
regions: {
|
|
97
107
|
continuous: {
|
|
98
|
-
header: "p-4 pb-2",
|
|
108
|
+
header: "p-4 pb-2 [&:last-child]:pb-4",
|
|
99
109
|
body: "p-4 py-2 [&:last-child]:pb-4 [&:first-child]:pt-4",
|
|
100
110
|
footer: "p-4 pt-2",
|
|
101
111
|
},
|
|
@@ -112,7 +122,7 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
|
|
|
112
122
|
drawer: {
|
|
113
123
|
regions: {
|
|
114
124
|
continuous: {
|
|
115
|
-
header: "p-4 pb-2",
|
|
125
|
+
header: "p-4 pb-2 [&:last-child]:pb-4",
|
|
116
126
|
body: "p-4 py-2 [&:last-child]:pb-4 [&:first-child]:pt-4",
|
|
117
127
|
footer: "p-4 pt-2",
|
|
118
128
|
},
|
|
@@ -129,7 +139,7 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
|
|
|
129
139
|
pane: {
|
|
130
140
|
regions: {
|
|
131
141
|
continuous: {
|
|
132
|
-
header: "flex shrink-0 items-center px-3 pt-3 pb-2",
|
|
142
|
+
header: "flex shrink-0 items-center px-3 pt-3 pb-2 [&:last-child]:pb-3",
|
|
133
143
|
body: "p-3 [&:first-child]:pt-3",
|
|
134
144
|
footer: "px-3 pt-2 pb-3",
|
|
135
145
|
},
|
|
@@ -200,7 +210,24 @@ export const surfaceActionsClasses = "flex shrink-0 items-center gap-2";
|
|
|
200
210
|
*/
|
|
201
211
|
const SurfaceLayoutContext = createContext<SurfaceLayout | null>(null);
|
|
202
212
|
|
|
203
|
-
|
|
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
|
+
}
|
|
204
231
|
|
|
205
232
|
const familyDefaultLayout: Record<SurfaceFamily, SurfaceLayout> = {
|
|
206
233
|
card: "continuous",
|