@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
|
@@ -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.
|
|
@@ -2144,8 +2151,11 @@ export function ActionList<TInput, TItem>({
|
|
|
2144
2151
|
variant="plain"
|
|
2145
2152
|
className={tableStateActive ? "h-full" : undefined}
|
|
2146
2153
|
containerClassName={
|
|
2154
|
+
// Quem rola é a linha de resultados, acima: aqui o contêiner abre mão da
|
|
2155
|
+
// rolagem horizontal para não virar a referência do `sticky` do cabeçalho.
|
|
2156
|
+
// `h-full` só no estado sem linhas, para a moldura vazia ocupar a altura.
|
|
2147
2157
|
tableFillsAvailableHeight
|
|
2148
|
-
? "
|
|
2158
|
+
? cn("overflow-x-visible", tableStateActive && "h-full")
|
|
2149
2159
|
: undefined
|
|
2150
2160
|
}
|
|
2151
2161
|
>
|
|
@@ -2290,7 +2300,14 @@ export function ActionList<TInput, TItem>({
|
|
|
2290
2300
|
) : null;
|
|
2291
2301
|
|
|
2292
2302
|
// — Rodapé (o estilo da casa): Página X de Y à esquerda · pager CENTRALIZADO ·
|
|
2293
|
-
// total à direita (grid de 3 colunas com laterais 1fr = centro geométrico)
|
|
2303
|
+
// total à direita (grid de 3 colunas com laterais 1fr = centro geométrico).
|
|
2304
|
+
// Encosta na última linha: a borda de cima é o fecho da tabela, não a de um
|
|
2305
|
+
// bloco solto, então o rodapé não entra no `gap` da pilha.
|
|
2306
|
+
// Altura: 2.5rem de piso (a mesma do `TableHead`) para a faixa não mudar de
|
|
2307
|
+
// tamanho quando o pager aparece — ele ocupa 1.75rem e cabe dentro do piso.
|
|
2308
|
+
// O padding fica em 0.25rem só para o dia em que o conteúdo passar disso:
|
|
2309
|
+
// com `py-2` o padding empurraria o pager para fora e a faixa voltaria a
|
|
2310
|
+
// crescer —
|
|
2294
2311
|
const pages =
|
|
2295
2312
|
total !== undefined
|
|
2296
2313
|
? Math.max(1, Math.ceil(total / (state.limit ?? pageSize)))
|
|
@@ -2300,7 +2317,7 @@ export function ActionList<TInput, TItem>({
|
|
|
2300
2317
|
total !== undefined && !isError ? (
|
|
2301
2318
|
<div
|
|
2302
2319
|
data-slot="action-list-footer"
|
|
2303
|
-
className="grid grid-cols-[1fr_auto_1fr] items-center gap-3 border-t border-border px-3 py-1
|
|
2320
|
+
className="grid min-h-10 grid-cols-[1fr_auto_1fr] items-center gap-3 border-t border-border px-3 py-1 text-xs text-muted-foreground"
|
|
2304
2321
|
>
|
|
2305
2322
|
<span>
|
|
2306
2323
|
Página {state.page} de {pages}
|
|
@@ -2357,6 +2374,7 @@ export function ActionList<TInput, TItem>({
|
|
|
2357
2374
|
return (
|
|
2358
2375
|
<TooltipProvider>
|
|
2359
2376
|
<div
|
|
2377
|
+
ref={rootRef}
|
|
2360
2378
|
data-action={action.name}
|
|
2361
2379
|
data-layout={layout}
|
|
2362
2380
|
className={cn(
|
|
@@ -2365,75 +2383,87 @@ export function ActionList<TInput, TItem>({
|
|
|
2365
2383
|
: "space-y-3",
|
|
2366
2384
|
)}
|
|
2367
2385
|
>
|
|
2368
|
-
{
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
if (!o) setConfirming(null);
|
|
2390
|
-
}}
|
|
2391
|
-
>
|
|
2392
|
-
<DialogContent
|
|
2393
|
-
aria-describedby={
|
|
2394
|
-
confirming.confirm?.message === undefined
|
|
2395
|
-
? undefined
|
|
2396
|
-
: confirmBodyId
|
|
2397
|
-
}
|
|
2398
|
-
>
|
|
2399
|
-
<DialogHeader>
|
|
2400
|
-
<DialogTitle>
|
|
2401
|
-
{confirming.confirm?.title ?? confirming.label}
|
|
2402
|
-
</DialogTitle>
|
|
2403
|
-
</DialogHeader>
|
|
2404
|
-
{confirming.confirm?.message !== undefined && (
|
|
2405
|
-
<DialogBody
|
|
2406
|
-
id={confirmBodyId}
|
|
2407
|
-
className="text-sm text-muted-foreground"
|
|
2408
|
-
>
|
|
2409
|
-
{confirming.confirm.message}
|
|
2410
|
-
</DialogBody>
|
|
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",
|
|
2411
2407
|
)}
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2408
|
+
>
|
|
2409
|
+
{body}
|
|
2410
|
+
</div>
|
|
2411
|
+
{footer}
|
|
2412
|
+
</div>
|
|
2413
|
+
{confirming !== null && (
|
|
2414
|
+
<Dialog
|
|
2415
|
+
mode="alert"
|
|
2416
|
+
open
|
|
2417
|
+
onOpenChange={(o) => {
|
|
2418
|
+
if (!o) setConfirming(null);
|
|
2419
|
+
}}
|
|
2420
|
+
>
|
|
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>
|
|
2437
2467
|
</div>
|
|
2438
2468
|
</TooltipProvider>
|
|
2439
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
|
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pane — a superfície encaixável de um Split: a coluna ou faixa que hospeda navegação,
|
|
3
|
+
* lista ou conteúdo, com regiões fixas no topo e no rodapé e um corpo que rola.
|
|
4
|
+
*
|
|
5
|
+
* Até a ADR 0023 esta família vivia partida: a raiz em `split.tsx`, os slots em
|
|
6
|
+
* `sidebar.tsx`, sem validação entre eles e com padding assimétrico (header sem recuo,
|
|
7
|
+
* footer com `p-2`). Agora raiz e slots moram aqui e derivam a anatomia da mesma tabela
|
|
8
|
+
* das demais famílias.
|
|
9
|
+
*
|
|
10
|
+
* <Pane>
|
|
11
|
+
* <PaneHeader><PaneMedia>…</PaneMedia><PaneTitle>Conversas</PaneTitle><PaneActions>…</PaneActions></PaneHeader>
|
|
12
|
+
* <PaneBody>…</PaneBody>
|
|
13
|
+
* <PaneFooter>…</PaneFooter>
|
|
14
|
+
* </Pane>
|
|
15
|
+
*
|
|
16
|
+
* `inset` continua existindo para o pane que NÃO usa slots — a caixa simples que recebe
|
|
17
|
+
* conteúdo direto. Quando há slots, o recuo é de cada região, e o inset do pane vira
|
|
18
|
+
* `none` sozinho: o divisor precisa alcançar a borda.
|
|
19
|
+
*/
|
|
20
|
+
import {
|
|
21
|
+
Children,
|
|
22
|
+
Fragment,
|
|
23
|
+
isValidElement,
|
|
24
|
+
type CSSProperties,
|
|
25
|
+
type ReactElement,
|
|
26
|
+
type ReactNode,
|
|
27
|
+
} from "react";
|
|
28
|
+
|
|
29
|
+
import { cn } from "../../lib/cn.ts";
|
|
30
|
+
import { headingTags, type HeadingLevel } from "./heading-level.ts";
|
|
31
|
+
import {
|
|
32
|
+
SurfaceLayoutProvider,
|
|
33
|
+
surfaceActionsClasses,
|
|
34
|
+
surfaceDescriptionClasses,
|
|
35
|
+
surfaceHeaderRowClasses,
|
|
36
|
+
surfaceRegionClasses,
|
|
37
|
+
surfaceTitleClasses,
|
|
38
|
+
useSurfaceLayout,
|
|
39
|
+
type SurfaceLayout,
|
|
40
|
+
} from "./surface-anatomy.ts";
|
|
41
|
+
import { SurfaceHeader } from "./surface-header.tsx";
|
|
42
|
+
import { SurfaceMedia, type SurfaceMediaProps } from "./surface-media.tsx";
|
|
43
|
+
|
|
44
|
+
export type PaneSize =
|
|
45
|
+
| number
|
|
46
|
+
| `${number}%`
|
|
47
|
+
| `${number}rem`
|
|
48
|
+
| `${number}em`
|
|
49
|
+
| `${number}vh`
|
|
50
|
+
| `${number}vw`
|
|
51
|
+
| `${number}px`;
|
|
52
|
+
|
|
53
|
+
// Números continuam percentuais por compatibilidade com Split/Pane. Strings preservam
|
|
54
|
+
// a unidade explícita para layouts que precisam de um limite físico, como `28rem`.
|
|
55
|
+
export function paneSizeToCss(value: PaneSize): string {
|
|
56
|
+
return typeof value === "number" ? `${value}%` : value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface PaneProps {
|
|
60
|
+
/** Identidade estável do painel dentro do grupo. */
|
|
61
|
+
id?: string;
|
|
62
|
+
/** Tamanho inicial. Números representam porcentagens; strings aceitam %, rem, em, vh, vw e px. */
|
|
63
|
+
initialSize?: PaneSize;
|
|
64
|
+
/** Tamanho mínimo quando o split é redimensionável. */
|
|
65
|
+
minSize?: PaneSize;
|
|
66
|
+
/** Tamanho máximo quando o split é redimensionável. */
|
|
67
|
+
maxSize?: PaneSize;
|
|
68
|
+
/** Ocupa o espaço remanescente no layout simples. */
|
|
69
|
+
grow?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Respiro interno do pane sem slots. Com header, body ou footer como filhos DIRETOS o
|
|
72
|
+
* default passa a `none`, porque aí o recuo é de cada região. Quando os slots vêm
|
|
73
|
+
* dentro de outro componente — a `Sidebar`, por exemplo —, o pane não os enxerga:
|
|
74
|
+
* declare `inset="none"` você mesmo.
|
|
75
|
+
*/
|
|
76
|
+
inset?: "none" | "sm" | "md" | "lg";
|
|
77
|
+
/** Anatomia das regiões: `sectioned` (default) ou `continuous` — ADR 0023. */
|
|
78
|
+
layout?: SurfaceLayout;
|
|
79
|
+
className?: string;
|
|
80
|
+
children: ReactNode;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const insets = { none: "", sm: "p-2", md: "p-4", lg: "p-6" } as const;
|
|
84
|
+
|
|
85
|
+
/** Expande fragments de primeiro nível antes de procurar os slots. */
|
|
86
|
+
function flatten(children: ReactNode): ReactNode[] {
|
|
87
|
+
return Children.toArray(children).flatMap((node) =>
|
|
88
|
+
isValidElement(node) && node.type === Fragment
|
|
89
|
+
? Children.toArray((node.props as { children?: ReactNode }).children)
|
|
90
|
+
: [node],
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function hasStructuralSlots(children: ReactNode): boolean {
|
|
95
|
+
return flatten(children).some(
|
|
96
|
+
(node) =>
|
|
97
|
+
isValidElement(node) &&
|
|
98
|
+
(node.type === PaneHeader ||
|
|
99
|
+
node.type === PaneBody ||
|
|
100
|
+
node.type === PaneContent ||
|
|
101
|
+
node.type === PaneFooter),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Um conteúdo encaixável. Sua posição é dada pela ordem dentro de <Split>. */
|
|
106
|
+
export function Pane({
|
|
107
|
+
id,
|
|
108
|
+
initialSize,
|
|
109
|
+
grow = false,
|
|
110
|
+
inset,
|
|
111
|
+
layout = "sectioned",
|
|
112
|
+
className,
|
|
113
|
+
children,
|
|
114
|
+
}: PaneProps): ReactElement {
|
|
115
|
+
const style: CSSProperties | undefined =
|
|
116
|
+
initialSize === undefined
|
|
117
|
+
? undefined
|
|
118
|
+
: { flexBasis: paneSizeToCss(initialSize) };
|
|
119
|
+
const structural = hasStructuralSlots(children);
|
|
120
|
+
const resolvedInset = inset ?? (structural ? "none" : "md");
|
|
121
|
+
return (
|
|
122
|
+
<SurfaceLayoutProvider value={layout}>
|
|
123
|
+
<div
|
|
124
|
+
id={id === undefined ? undefined : String(id)}
|
|
125
|
+
data-slot="pane"
|
|
126
|
+
data-inset={resolvedInset}
|
|
127
|
+
data-layout={layout}
|
|
128
|
+
className={cn(
|
|
129
|
+
"min-h-0 min-w-0 shrink-0",
|
|
130
|
+
grow && "flex-1",
|
|
131
|
+
// Com slots o pane é uma coluna: header fixo, corpo que rola, footer fixo.
|
|
132
|
+
structural && "flex flex-col",
|
|
133
|
+
insets[resolvedInset],
|
|
134
|
+
className,
|
|
135
|
+
)}
|
|
136
|
+
style={style}
|
|
137
|
+
>
|
|
138
|
+
{children}
|
|
139
|
+
</div>
|
|
140
|
+
</SurfaceLayoutProvider>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Faixa fixa no topo de um pane — marca, contexto ou ações locais. */
|
|
145
|
+
export function PaneHeader({
|
|
146
|
+
className,
|
|
147
|
+
children,
|
|
148
|
+
...props
|
|
149
|
+
}: React.ComponentProps<"div">): ReactElement {
|
|
150
|
+
const layout = useSurfaceLayout("pane");
|
|
151
|
+
return (
|
|
152
|
+
<SurfaceHeader
|
|
153
|
+
name="PaneHeader"
|
|
154
|
+
slot="pane"
|
|
155
|
+
slots={{
|
|
156
|
+
media: PaneMedia,
|
|
157
|
+
title: PaneTitle,
|
|
158
|
+
description: PaneDescription,
|
|
159
|
+
actions: PaneActions,
|
|
160
|
+
}}
|
|
161
|
+
leadingPlacement="inline"
|
|
162
|
+
titleRequired={false}
|
|
163
|
+
// O pane é a superfície genérica: além dos slots, o header hospeda marca, busca ou
|
|
164
|
+
// chrome que não são título nem ação (ADR 0023).
|
|
165
|
+
allowFreeContent
|
|
166
|
+
rowClassName={surfaceHeaderRowClasses("pane")}
|
|
167
|
+
className={cn(surfaceRegionClasses("pane", layout, "header"), className)}
|
|
168
|
+
{...props}
|
|
169
|
+
>
|
|
170
|
+
{children}
|
|
171
|
+
</SurfaceHeader>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Moldura de ícone ou imagem que abre a linha do header. */
|
|
176
|
+
export function PaneMedia(props: SurfaceMediaProps): ReactElement {
|
|
177
|
+
return <SurfaceMedia slot="pane-media" size="xs" {...props} />;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function PaneTitle({
|
|
181
|
+
className,
|
|
182
|
+
level = 2,
|
|
183
|
+
...props
|
|
184
|
+
}: React.ComponentProps<"h2"> & {
|
|
185
|
+
/** Hierarquia do documento (`h1`–`h6`). O tamanho vem do layout, nunca do nível. */
|
|
186
|
+
level?: HeadingLevel;
|
|
187
|
+
}): ReactElement {
|
|
188
|
+
const layout = useSurfaceLayout("pane");
|
|
189
|
+
const Comp = headingTags[level];
|
|
190
|
+
return (
|
|
191
|
+
<Comp
|
|
192
|
+
data-slot="pane-title"
|
|
193
|
+
className={cn(
|
|
194
|
+
"min-w-0 truncate",
|
|
195
|
+
surfaceTitleClasses("pane", layout),
|
|
196
|
+
className,
|
|
197
|
+
)}
|
|
198
|
+
{...props}
|
|
199
|
+
/>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function PaneDescription({
|
|
204
|
+
className,
|
|
205
|
+
...props
|
|
206
|
+
}: React.ComponentProps<"p">): ReactElement {
|
|
207
|
+
return (
|
|
208
|
+
<p
|
|
209
|
+
data-slot="pane-description"
|
|
210
|
+
className={cn("truncate", surfaceDescriptionClasses("pane"), className)}
|
|
211
|
+
{...props}
|
|
212
|
+
/>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Ações do header — a família posiciona no extremo oposto ao título. */
|
|
217
|
+
export function PaneActions({
|
|
218
|
+
className,
|
|
219
|
+
...props
|
|
220
|
+
}: React.ComponentProps<"div">): ReactElement {
|
|
221
|
+
return (
|
|
222
|
+
<div
|
|
223
|
+
data-slot="pane-actions"
|
|
224
|
+
className={cn(surfaceActionsClasses, className)}
|
|
225
|
+
{...props}
|
|
226
|
+
/>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Região rolável que ocupa o espaço restante de um pane. */
|
|
231
|
+
export function PaneBody({
|
|
232
|
+
className,
|
|
233
|
+
children,
|
|
234
|
+
...props
|
|
235
|
+
}: React.ComponentProps<"div">): ReactElement {
|
|
236
|
+
const layout = useSurfaceLayout("pane");
|
|
237
|
+
return (
|
|
238
|
+
<div
|
|
239
|
+
data-slot="pane-body"
|
|
240
|
+
className={cn(
|
|
241
|
+
"min-h-0 flex-1 overflow-y-auto",
|
|
242
|
+
surfaceRegionClasses("pane", layout, "body"),
|
|
243
|
+
className,
|
|
244
|
+
)}
|
|
245
|
+
{...props}
|
|
246
|
+
>
|
|
247
|
+
{children}
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** @deprecated Use PaneBody. Alias mantido para migração; sai numa próxima versão major. */
|
|
253
|
+
export function PaneContent({
|
|
254
|
+
className,
|
|
255
|
+
children,
|
|
256
|
+
...props
|
|
257
|
+
}: React.ComponentProps<"div">): ReactElement {
|
|
258
|
+
const layout = useSurfaceLayout("pane");
|
|
259
|
+
return (
|
|
260
|
+
<div
|
|
261
|
+
data-slot="pane-content"
|
|
262
|
+
className={cn(
|
|
263
|
+
"min-h-0 flex-1 overflow-y-auto",
|
|
264
|
+
surfaceRegionClasses("pane", layout, "body"),
|
|
265
|
+
className,
|
|
266
|
+
)}
|
|
267
|
+
{...props}
|
|
268
|
+
>
|
|
269
|
+
{children}
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Faixa fixa no rodapé de um pane para ações persistentes. */
|
|
275
|
+
export function PaneFooter({
|
|
276
|
+
className,
|
|
277
|
+
children,
|
|
278
|
+
...props
|
|
279
|
+
}: React.ComponentProps<"div">): ReactElement {
|
|
280
|
+
const layout = useSurfaceLayout("pane");
|
|
281
|
+
return (
|
|
282
|
+
<div
|
|
283
|
+
data-slot="pane-footer"
|
|
284
|
+
className={cn(
|
|
285
|
+
"shrink-0",
|
|
286
|
+
surfaceRegionClasses("pane", layout, "footer"),
|
|
287
|
+
className,
|
|
288
|
+
)}
|
|
289
|
+
{...props}
|
|
290
|
+
>
|
|
291
|
+
{children}
|
|
292
|
+
</div>
|
|
293
|
+
);
|
|
294
|
+
}
|