@softize/opus 19.0.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 +193 -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 +5 -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 +15 -3
- package/src/ui/components/patterns/fill-region.ts +150 -0
- package/src/ui/components/patterns/list.tsx +89 -78
- 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/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/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 +41 -10
- package/src/ui/components/primitives/drawer.tsx +9 -1
- 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 +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 +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 +31 -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 +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 +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 +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 +31 -31
- package/src/vite/design.ts +16 -4
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Propagação da ocupação de altura (interno) — ADR 0024.
|
|
3
|
+
*
|
|
4
|
+
* `Page layout="fill"` limita a página à altura do shell; a `Presentation` em página herda
|
|
5
|
+
* esse limite pela `Page` que ela monta. Para uma região interna ocupar a folga e rolar
|
|
6
|
+
* sozinha, cada elemento entre a raiz limitada e essa região precisa repassar a altura como
|
|
7
|
+
* coluna flexível. Este módulo faz essa cadeia se formar sem classes no consumidor:
|
|
8
|
+
*
|
|
9
|
+
* - a raiz (`FillRoot`) publica o contexto;
|
|
10
|
+
* - os elos canônicos (`PageBody`, `Content`, `ContentBody`, o corpo da `Presentation`)
|
|
11
|
+
* leem o contexto com `useFillRegion` e o republicam para os filhos;
|
|
12
|
+
* - a folha (`ActionList`) usa `useFillLeaf`: assume `fill` por padrão quando o elemento
|
|
13
|
+
* pai é um elo e se registra na cadeia;
|
|
14
|
+
* - um elo só vira coluna flexível quando uma folha se registrou abaixo dele. Irmãos sem
|
|
15
|
+
* folha mantêm a altura do próprio conteúdo, e uma página `fill` sem lista não muda;
|
|
16
|
+
* - superfícies com anatomia própria (Card, Dialog, Drawer, Pane) cortam a cadeia com
|
|
17
|
+
* `FillBoundary`: a altura delas é decisão delas, inclusive quando um portal as renderiza
|
|
18
|
+
* dentro de uma página `fill`.
|
|
19
|
+
*
|
|
20
|
+
* O registro acontece em `useLayoutEffect`, então as classes chegam antes da primeira pintura.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
createContext,
|
|
25
|
+
createElement,
|
|
26
|
+
useContext,
|
|
27
|
+
useLayoutEffect,
|
|
28
|
+
useMemo,
|
|
29
|
+
useRef,
|
|
30
|
+
useState,
|
|
31
|
+
type ReactElement,
|
|
32
|
+
type ReactNode,
|
|
33
|
+
type RefObject,
|
|
34
|
+
} from "react";
|
|
35
|
+
|
|
36
|
+
export type FillLayout = "flow" | "fill";
|
|
37
|
+
|
|
38
|
+
interface FillChain {
|
|
39
|
+
/** Anuncia uma folha abaixo deste elo; devolve a retirada do anúncio. */
|
|
40
|
+
register(): () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const FillChainContext = createContext<FillChain | null>(null);
|
|
44
|
+
|
|
45
|
+
/** A raiz já tem altura limitada: não há o que propagar acima dela. */
|
|
46
|
+
const rootChain: FillChain = { register: () => () => {} };
|
|
47
|
+
|
|
48
|
+
/** Classes de um elo que repassa a altura à folha registrada abaixo dele. */
|
|
49
|
+
export const fillRegionClassName = "flex min-h-0 flex-1 flex-col";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Atributo que marca um elo da cadeia no DOM: `"true"` quando repassa a altura, `"false"`
|
|
53
|
+
* quando está na cadeia sem folha abaixo. A folha só assume `fill` implícito quando o
|
|
54
|
+
* elemento pai o carrega — um contêiner próprio no meio não repassa a altura.
|
|
55
|
+
*/
|
|
56
|
+
export const fillRegionAttribute = "data-fill";
|
|
57
|
+
|
|
58
|
+
/** Publica a cadeia quando `active`; caso contrário a interrompe. */
|
|
59
|
+
export function FillRoot({
|
|
60
|
+
active,
|
|
61
|
+
children,
|
|
62
|
+
}: {
|
|
63
|
+
active: boolean;
|
|
64
|
+
children?: ReactNode;
|
|
65
|
+
}): ReactElement {
|
|
66
|
+
return createElement(
|
|
67
|
+
FillChainContext.Provider,
|
|
68
|
+
{ value: active ? rootChain : null },
|
|
69
|
+
children,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Interrompe a cadeia: o conteúdo abaixo decide a própria altura. */
|
|
74
|
+
export function FillBoundary({
|
|
75
|
+
children,
|
|
76
|
+
}: {
|
|
77
|
+
children?: ReactNode;
|
|
78
|
+
}): ReactElement {
|
|
79
|
+
return createElement(FillChainContext.Provider, { value: null }, children);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface FillRegion {
|
|
83
|
+
/** O elo está em uma cadeia `fill`, com ou sem folha abaixo. */
|
|
84
|
+
inChain: boolean;
|
|
85
|
+
/** Uma folha se registrou abaixo: o elo deve repassar a altura. */
|
|
86
|
+
filling: boolean;
|
|
87
|
+
/** Atributos do elemento do elo. */
|
|
88
|
+
attributes: { "data-fill"?: "true" | "false" };
|
|
89
|
+
/** Republica a cadeia para os filhos do elo. */
|
|
90
|
+
provide(children: ReactNode): ReactElement;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Elo da cadeia: lê o contexto, conta as folhas abaixo e o republica. */
|
|
94
|
+
export function useFillRegion(): FillRegion {
|
|
95
|
+
const parent = useContext(FillChainContext);
|
|
96
|
+
const [leaves, setLeaves] = useState(0);
|
|
97
|
+
const chain = useMemo<FillChain | null>(
|
|
98
|
+
() =>
|
|
99
|
+
parent === null
|
|
100
|
+
? null
|
|
101
|
+
: {
|
|
102
|
+
register() {
|
|
103
|
+
setLeaves((count) => count + 1);
|
|
104
|
+
const releaseParent = parent.register();
|
|
105
|
+
let registered = true;
|
|
106
|
+
return () => {
|
|
107
|
+
if (!registered) return;
|
|
108
|
+
registered = false;
|
|
109
|
+
setLeaves((count) => Math.max(0, count - 1));
|
|
110
|
+
releaseParent();
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
[parent],
|
|
115
|
+
);
|
|
116
|
+
const inChain = parent !== null;
|
|
117
|
+
const filling = inChain && leaves > 0;
|
|
118
|
+
return {
|
|
119
|
+
inChain,
|
|
120
|
+
filling,
|
|
121
|
+
attributes: inChain ? { "data-fill": filling ? "true" : "false" } : {},
|
|
122
|
+
provide: (children) =>
|
|
123
|
+
createElement(FillChainContext.Provider, { value: chain }, children),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Folha da cadeia. Sem `layout` explícito, assume `fill` quando o elemento pai é um elo;
|
|
129
|
+
* com `layout="fill"` explícito, registra-se mesmo sob um contêiner próprio, que passa a
|
|
130
|
+
* ser responsabilidade de quem o escreveu. `layout="flow"` nunca se registra.
|
|
131
|
+
*/
|
|
132
|
+
export function useFillLeaf<TElement extends HTMLElement>(
|
|
133
|
+
layout: FillLayout | undefined,
|
|
134
|
+
): { layout: FillLayout; ref: RefObject<TElement | null> } {
|
|
135
|
+
const chain = useContext(FillChainContext);
|
|
136
|
+
const ref = useRef<TElement | null>(null);
|
|
137
|
+
const [linked, setLinked] = useState(false);
|
|
138
|
+
useLayoutEffect(() => {
|
|
139
|
+
setLinked(
|
|
140
|
+
chain !== null &&
|
|
141
|
+
ref.current?.parentElement?.hasAttribute(fillRegionAttribute) === true,
|
|
142
|
+
);
|
|
143
|
+
}, [chain]);
|
|
144
|
+
const resolved = layout ?? (linked ? "fill" : "flow");
|
|
145
|
+
useLayoutEffect(() => {
|
|
146
|
+
if (chain === null || resolved !== "fill") return undefined;
|
|
147
|
+
return chain.register();
|
|
148
|
+
}, [chain, resolved]);
|
|
149
|
+
return { layout: resolved, ref };
|
|
150
|
+
}
|
|
@@ -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;
|
|
@@ -1666,7 +1672,7 @@ function RowActions({ children }: { children: ReactNode }) {
|
|
|
1666
1672
|
export function ActionList<TInput, TItem>({
|
|
1667
1673
|
action,
|
|
1668
1674
|
input,
|
|
1669
|
-
layout
|
|
1675
|
+
layout: layoutProp,
|
|
1670
1676
|
toolbarActions,
|
|
1671
1677
|
columns,
|
|
1672
1678
|
cells,
|
|
@@ -1693,6 +1699,7 @@ export function ActionList<TInput, TItem>({
|
|
|
1693
1699
|
onStateChange,
|
|
1694
1700
|
}: ActionListProps<TInput, TItem>) {
|
|
1695
1701
|
const confirmBodyId = useId();
|
|
1702
|
+
const { layout, ref: rootRef } = useFillLeaf<HTMLDivElement>(layoutProp);
|
|
1696
1703
|
// Query-backed (useListAction): busca declarativa por [name, input] E refetch
|
|
1697
1704
|
// automático quando um form/trigger invalida a action (`invalidates: ['x.list']`).
|
|
1698
1705
|
// O input efetivo é computado abaixo; o hook re-busca quando ele muda.
|
|
@@ -2367,6 +2374,7 @@ export function ActionList<TInput, TItem>({
|
|
|
2367
2374
|
return (
|
|
2368
2375
|
<TooltipProvider>
|
|
2369
2376
|
<div
|
|
2377
|
+
ref={rootRef}
|
|
2370
2378
|
data-action={action.name}
|
|
2371
2379
|
data-layout={layout}
|
|
2372
2380
|
className={cn(
|
|
@@ -2375,84 +2383,87 @@ export function ActionList<TInput, TItem>({
|
|
|
2375
2383
|
: "space-y-3",
|
|
2376
2384
|
)}
|
|
2377
2385
|
>
|
|
2378
|
-
{
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2386
|
+
{/* A lista é a folha da cadeia de altura: o que ela hospeda decide a própria. */}
|
|
2387
|
+
<FillBoundary>
|
|
2388
|
+
{toolbar}
|
|
2389
|
+
{beforeContent}
|
|
2390
|
+
{selectionBar}
|
|
2391
|
+
{/* Tabela e rodapé formam um bloco só: o `gap` da pilha separa a barra de
|
|
2392
|
+
ferramentas do conteúdo, nunca o conteúdo do seu próprio rodapé. Duas
|
|
2393
|
+
linhas: a dos resultados absorve a altura que sobra, a do rodapé se mede
|
|
2394
|
+
pelo conteúdo. Em `flow` a primeira linha também é `1fr`, mas o bloco não
|
|
2395
|
+
tem altura própria e ela vira a altura do conteúdo. */}
|
|
2396
|
+
<div className={cn("grid min-h-0 grid-rows-[1fr_auto]", layout === "fill" && "flex-1")}>
|
|
2397
|
+
{/* A rolagem mora aqui, nos dois eixos: é este elemento que o `sticky` do
|
|
2398
|
+
cabeçalho enxerga. Um contêiner de rolagem entre ele e a tabela roubaria
|
|
2399
|
+
essa referência e o cabeçalho voltaria a subir junto com as linhas.
|
|
2400
|
+
Refetch com a lista já na tela: esmaece e trava interação até chegar. */}
|
|
2401
|
+
<div
|
|
2402
|
+
aria-busy={busy}
|
|
2403
|
+
className={cn(
|
|
2404
|
+
"min-h-0",
|
|
2405
|
+
layout === "fill" && "overflow-auto",
|
|
2406
|
+
busy && items.length > 0 && "pointer-events-none opacity-60",
|
|
2407
|
+
)}
|
|
2408
|
+
>
|
|
2409
|
+
{body}
|
|
2410
|
+
</div>
|
|
2411
|
+
{footer}
|
|
2400
2412
|
</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
|
-
}
|
|
2413
|
+
{confirming !== null && (
|
|
2414
|
+
<Dialog
|
|
2415
|
+
mode="alert"
|
|
2416
|
+
open
|
|
2417
|
+
onOpenChange={(o) => {
|
|
2418
|
+
if (!o) setConfirming(null);
|
|
2419
|
+
}}
|
|
2417
2420
|
>
|
|
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
|
-
|
|
2421
|
+
<DialogContent
|
|
2422
|
+
aria-describedby={
|
|
2423
|
+
confirming.confirm?.message === undefined
|
|
2424
|
+
? undefined
|
|
2425
|
+
: confirmBodyId
|
|
2426
|
+
}
|
|
2427
|
+
>
|
|
2428
|
+
<DialogHeader>
|
|
2429
|
+
<DialogTitle>
|
|
2430
|
+
{confirming.confirm?.title ?? confirming.label}
|
|
2431
|
+
</DialogTitle>
|
|
2432
|
+
</DialogHeader>
|
|
2433
|
+
{confirming.confirm?.message !== undefined && (
|
|
2434
|
+
<DialogBody
|
|
2435
|
+
id={confirmBodyId}
|
|
2436
|
+
className="text-sm text-muted-foreground"
|
|
2437
|
+
>
|
|
2438
|
+
{confirming.confirm.message}
|
|
2439
|
+
</DialogBody>
|
|
2440
|
+
)}
|
|
2441
|
+
<DialogFooter>
|
|
2442
|
+
<ButtonGroup mode="spaced" distribution="equal">
|
|
2443
|
+
<DialogClose initialFocus asChild>
|
|
2444
|
+
<Button variant="outline">Cancelar</Button>
|
|
2445
|
+
</DialogClose>
|
|
2446
|
+
<DialogClose asChild>
|
|
2447
|
+
<Button
|
|
2448
|
+
context={
|
|
2449
|
+
confirming.destructive === true ? "danger" : "primary"
|
|
2450
|
+
}
|
|
2451
|
+
variant="solid"
|
|
2452
|
+
onClick={() => {
|
|
2453
|
+
const a = confirming;
|
|
2454
|
+
setConfirming(null);
|
|
2455
|
+
void runBatch(a);
|
|
2456
|
+
}}
|
|
2457
|
+
>
|
|
2458
|
+
{confirming.label}
|
|
2459
|
+
</Button>
|
|
2460
|
+
</DialogClose>
|
|
2461
|
+
</ButtonGroup>
|
|
2462
|
+
</DialogFooter>
|
|
2463
|
+
</DialogContent>
|
|
2464
|
+
</Dialog>
|
|
2465
|
+
)}
|
|
2466
|
+
</FillBoundary>
|
|
2456
2467
|
</div>
|
|
2457
2468
|
</TooltipProvider>
|
|
2458
2469
|
);
|
|
@@ -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
|