@softize/opus 23.0.0 → 24.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 +81 -0
- package/bin/cli.mjs +2 -0
- package/bin/lib/check.mjs +241 -42
- package/bin/lib/copy.mjs +16 -34
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
- package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +3 -3
- package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +5 -6
- package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
- package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
- package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
- package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
- package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
- package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
- package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
- package/package.json +5 -1
- package/registry/skills/build-opus-ui/SKILL.md +11 -11
- package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
- package/registry/templates/app/src/App.tsx +24 -12
- package/src/ui/appearance.ts +149 -0
- package/src/ui/components/patterns/action-form-card.tsx +46 -33
- package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
- package/src/ui/components/patterns/confirm.tsx +3 -1
- package/src/ui/components/patterns/fill-region.ts +8 -7
- package/src/ui/components/patterns/form-dialog.tsx +1 -3
- package/src/ui/components/patterns/form.tsx +8 -7
- package/src/ui/components/patterns/list.tsx +15 -19
- package/src/ui/components/patterns/page.tsx +15 -11
- package/src/ui/components/patterns/pane.tsx +9 -240
- package/src/ui/components/patterns/presentation.tsx +14 -14
- package/src/ui/components/patterns/sidebar.tsx +7 -17
- package/src/ui/components/patterns/split.tsx +111 -59
- package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +50 -19
- package/src/ui/components/patterns/structural-media.tsx +26 -0
- package/src/ui/components/patterns/surface-anatomy.ts +71 -61
- package/src/ui/components/patterns/surface-assistant.tsx +2 -4
- package/src/ui/components/patterns/surface.tsx +620 -0
- package/src/ui/components/patterns/trigger.tsx +11 -9
- package/src/ui/components/primitives/ask.tsx +155 -83
- package/src/ui/components/primitives/button.tsx +11 -9
- package/src/ui/components/primitives/checkbox.tsx +8 -8
- package/src/ui/components/primitives/command.tsx +1 -1
- package/src/ui/components/primitives/composer.tsx +47 -45
- package/src/ui/components/primitives/detail.tsx +9 -2
- package/src/ui/components/primitives/dialog.tsx +60 -26
- package/src/ui/components/primitives/drawer.tsx +58 -12
- package/src/ui/components/primitives/icon-picker.tsx +90 -73
- package/src/ui/components/primitives/input-group.tsx +35 -28
- package/src/ui/components/primitives/input-otp.tsx +16 -16
- package/src/ui/components/primitives/input.tsx +20 -10
- package/src/ui/components/primitives/item.tsx +9 -12
- package/src/ui/components/primitives/menu.tsx +44 -65
- package/src/ui/components/primitives/metric-card.tsx +75 -59
- package/src/ui/components/primitives/radio-group.tsx +9 -9
- package/src/ui/components/primitives/select.tsx +344 -209
- package/src/ui/components/primitives/switch.tsx +10 -10
- package/src/ui/components/primitives/table.tsx +29 -24
- package/src/ui/components/primitives/tabs.tsx +4 -2
- package/src/ui/components/primitives/textarea.tsx +6 -6
- package/src/ui/components/primitives/toggle-group-context.ts +24 -0
- package/src/ui/components/primitives/toggle-group.tsx +116 -74
- package/src/ui/components/primitives/toggle.tsx +82 -16
- package/src/ui/docs/DocBrowser.tsx +77 -69
- package/src/ui/docs/content/action-form-card.md +9 -9
- package/src/ui/docs/content/action-list.md +76 -70
- package/src/ui/docs/content/action-view.md +33 -18
- package/src/ui/docs/content/button.md +1 -1
- package/src/ui/docs/content/communication.md +1 -1
- package/src/ui/docs/content/customization.md +64 -11
- package/src/ui/docs/content/detail.md +8 -6
- package/src/ui/docs/content/dialog.md +100 -58
- package/src/ui/docs/content/drawer.md +63 -19
- package/src/ui/docs/content/field.md +14 -15
- package/src/ui/docs/content/icon-tile.md +14 -13
- package/src/ui/docs/content/page.md +34 -26
- package/src/ui/docs/content/pane.md +41 -290
- package/src/ui/docs/content/presentation.md +3 -3
- package/src/ui/docs/content/separator.md +6 -6
- package/src/ui/docs/content/sidebar.md +68 -266
- package/src/ui/docs/content/skeleton.md +20 -18
- package/src/ui/docs/content/split.md +14 -14
- package/src/ui/docs/content/surface.md +513 -0
- package/src/ui/docs/content/tabs.md +20 -20
- package/src/ui/docs/content/toggle.md +117 -65
- package/src/ui/docs/content/tokens.md +124 -40
- package/src/ui/docs/registry.tsx +4 -6
- package/src/ui/index.ts +30 -27
- package/src/ui/meta.ts +15 -27
- package/src/ui/react.tsx +40 -43
- package/src/ui/theme.css +311 -64
- package/src/ui/components/patterns/content-header.tsx +0 -528
- package/src/ui/components/patterns/surface-media.tsx +0 -69
- package/src/ui/components/primitives/card.tsx +0 -191
- package/src/ui/docs/content/card.md +0 -292
- package/src/ui/docs/content/content.md +0 -275
|
@@ -7,15 +7,16 @@
|
|
|
7
7
|
* coluna flexível. Este módulo faz essa cadeia se formar sem classes no consumidor:
|
|
8
8
|
*
|
|
9
9
|
* - a raiz (`FillRoot`) publica o contexto;
|
|
10
|
-
* - os elos canônicos (`PageBody`, `
|
|
11
|
-
* leem o contexto com `useFillRegion` e o republicam para os filhos;
|
|
10
|
+
* - os elos canônicos (`PageBody`, `Surface` sem elevação, `SurfaceBody`, o corpo da
|
|
11
|
+
* `Presentation`) leem o contexto com `useFillRegion` e o republicam para os filhos;
|
|
12
12
|
* - a folha (`ActionList`) usa `useFillLeaf`: assume `fill` por padrão quando o elemento
|
|
13
13
|
* pai é um elo e se registra na cadeia;
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
14
|
+
* - os bodies estruturais já são colunas para governar o ritmo; quando uma folha se registra,
|
|
15
|
+
* os elos também passam a ocupar a folga disponível. Irmãos sem folha mantêm a altura do
|
|
16
|
+
* próprio conteúdo, e uma página `fill` sem lista não muda;
|
|
17
|
+
* - superfícies com altura própria (`Surface elevation="raised"`, Dialog, Drawer, Pane)
|
|
18
|
+
* cortam a cadeia com `FillBoundary`: a altura delas é decisão delas, inclusive quando um
|
|
19
|
+
* portal as renderiza dentro de uma página `fill`.
|
|
19
20
|
*
|
|
20
21
|
* O registro acontece em `useLayoutEffect`, então as classes chegam antes da primeira pintura.
|
|
21
22
|
*/
|
|
@@ -49,7 +49,8 @@ import {
|
|
|
49
49
|
} from "../primitives/tooltip.tsx";
|
|
50
50
|
import { Info } from "lucide-react";
|
|
51
51
|
import { Select, type SelectOption } from "../primitives/select.tsx";
|
|
52
|
-
import {
|
|
52
|
+
import { Toggle } from "../primitives/toggle.tsx";
|
|
53
|
+
import { ToggleGroup } from "../primitives/toggle-group.tsx";
|
|
53
54
|
import {
|
|
54
55
|
Field,
|
|
55
56
|
FieldError,
|
|
@@ -464,14 +465,14 @@ export function ActionFormField({
|
|
|
464
465
|
value={(formValues[name] as string[] | undefined) ?? []}
|
|
465
466
|
onValueChange={(value) => setValue(value)}
|
|
466
467
|
variant="outline"
|
|
467
|
-
|
|
468
|
+
mode="spaced"
|
|
468
469
|
aria-labelledby={`${name}-label`}
|
|
469
470
|
aria-invalid={errorMessage !== undefined}
|
|
470
471
|
aria-describedby={describedBy}
|
|
471
472
|
className="grid w-full grid-cols-[repeat(auto-fit,minmax(min(100%,12rem),1fr))] items-stretch gap-2"
|
|
472
473
|
>
|
|
473
474
|
{options.map((option) => (
|
|
474
|
-
<
|
|
475
|
+
<Toggle
|
|
475
476
|
key={option.value}
|
|
476
477
|
value={option.value}
|
|
477
478
|
disabled={option.disabled}
|
|
@@ -488,7 +489,7 @@ export function ActionFormField({
|
|
|
488
489
|
</span>
|
|
489
490
|
)}
|
|
490
491
|
</span>
|
|
491
|
-
</
|
|
492
|
+
</Toggle>
|
|
492
493
|
))}
|
|
493
494
|
</ToggleGroup>
|
|
494
495
|
) : (
|
|
@@ -502,7 +503,7 @@ export function ActionFormField({
|
|
|
502
503
|
}
|
|
503
504
|
}}
|
|
504
505
|
variant="outline"
|
|
505
|
-
|
|
506
|
+
mode="spaced"
|
|
506
507
|
aria-labelledby={`${name}-label`}
|
|
507
508
|
aria-required={effectiveKind.required}
|
|
508
509
|
aria-invalid={errorMessage !== undefined}
|
|
@@ -510,7 +511,7 @@ export function ActionFormField({
|
|
|
510
511
|
className="grid w-full grid-cols-[repeat(auto-fit,minmax(min(100%,12rem),1fr))] items-stretch gap-2"
|
|
511
512
|
>
|
|
512
513
|
{options.map((option) => (
|
|
513
|
-
<
|
|
514
|
+
<Toggle
|
|
514
515
|
key={option.value}
|
|
515
516
|
value={option.value}
|
|
516
517
|
disabled={option.disabled}
|
|
@@ -527,7 +528,7 @@ export function ActionFormField({
|
|
|
527
528
|
</span>
|
|
528
529
|
)}
|
|
529
530
|
</span>
|
|
530
|
-
</
|
|
531
|
+
</Toggle>
|
|
531
532
|
))}
|
|
532
533
|
</ToggleGroup>
|
|
533
534
|
)
|
|
@@ -119,7 +119,7 @@ import {
|
|
|
119
119
|
FieldLabel,
|
|
120
120
|
type FieldSetVariant,
|
|
121
121
|
} from "../primitives/field.tsx";
|
|
122
|
-
import {
|
|
122
|
+
import { Surface } from "./surface.tsx";
|
|
123
123
|
import { Search } from "lucide-react";
|
|
124
124
|
import { Select, type SelectOption } from "../primitives/select.tsx";
|
|
125
125
|
import {
|
|
@@ -130,7 +130,8 @@ import {
|
|
|
130
130
|
TableHeader,
|
|
131
131
|
TableRow,
|
|
132
132
|
} from "../primitives/table.tsx";
|
|
133
|
-
import {
|
|
133
|
+
import { Toggle } from "../primitives/toggle.tsx";
|
|
134
|
+
import { ToggleGroup } from "../primitives/toggle-group.tsx";
|
|
134
135
|
import {
|
|
135
136
|
Tooltip,
|
|
136
137
|
TooltipContent,
|
|
@@ -348,7 +349,7 @@ export interface ActionListProps<TInput, TItem> {
|
|
|
348
349
|
/**
|
|
349
350
|
* `fill` ocupa a altura disponível e mantém a rolagem dentro da região de resultados.
|
|
350
351
|
* Sem valor, vale `fill` quando o elemento pai repassa a altura de uma `Page` ou
|
|
351
|
-
* `Presentation` em `fill` — `PageBody`, `
|
|
352
|
+
* `Presentation` em `fill` — `PageBody`, `SurfaceBody` ou o corpo da Presentation — e
|
|
352
353
|
* `flow` nos demais casos (ADR 0024).
|
|
353
354
|
*/
|
|
354
355
|
layout?: "flow" | "fill";
|
|
@@ -598,7 +599,7 @@ function FilterField({
|
|
|
598
599
|
return (
|
|
599
600
|
<ToggleGroup
|
|
600
601
|
type="single"
|
|
601
|
-
|
|
602
|
+
mode="segmented"
|
|
602
603
|
id={`filter-${name}`}
|
|
603
604
|
value={toggleValue}
|
|
604
605
|
onValueChange={(next) => {
|
|
@@ -612,14 +613,12 @@ function FilterField({
|
|
|
612
613
|
className={className ?? "max-w-full"}
|
|
613
614
|
>
|
|
614
615
|
{acceptsEmptyValue && (
|
|
615
|
-
<
|
|
616
|
-
{text(spec.placeholder, "Todos")}
|
|
617
|
-
</ToggleGroupItem>
|
|
616
|
+
<Toggle value={ALL}>{text(spec.placeholder, "Todos")}</Toggle>
|
|
618
617
|
)}
|
|
619
618
|
{base.map((option) => (
|
|
620
|
-
<
|
|
619
|
+
<Toggle key={option.value} value={option.value}>
|
|
621
620
|
{option.content ?? option.label}
|
|
622
|
-
</
|
|
621
|
+
</Toggle>
|
|
623
622
|
))}
|
|
624
623
|
</ToggleGroup>
|
|
625
624
|
);
|
|
@@ -1047,7 +1046,7 @@ function PeriodControl({
|
|
|
1047
1046
|
variant={embedded ? "ghost" : "outline"}
|
|
1048
1047
|
data-slot="action-list-period"
|
|
1049
1048
|
className={cn(
|
|
1050
|
-
"relative max-w-full gap-1.5 overflow-hidden bg-
|
|
1049
|
+
"relative max-w-full gap-1.5 overflow-hidden bg-control px-3 hover:bg-interaction-hover hover:text-foreground",
|
|
1051
1050
|
// O X e o chevron ocupam posições explícitas na ponta do controle. O
|
|
1052
1051
|
// padding reserva os dois sem depender da largura do texto selecionado.
|
|
1053
1052
|
clearable && "pr-16",
|
|
@@ -1271,23 +1270,20 @@ function AdvancedFiltersDialog({
|
|
|
1271
1270
|
<DialogHeader>
|
|
1272
1271
|
<DialogTitle>Filtros</DialogTitle>
|
|
1273
1272
|
</DialogHeader>
|
|
1274
|
-
<DialogBody
|
|
1273
|
+
<DialogBody>
|
|
1275
1274
|
{sectioned ? (
|
|
1276
1275
|
sections.map(([section, entries]) => (
|
|
1277
|
-
<
|
|
1276
|
+
<Surface
|
|
1278
1277
|
key={section}
|
|
1279
1278
|
as="fieldset"
|
|
1280
1279
|
framed={sectionVariant === "framed"}
|
|
1281
|
-
frameVariant={
|
|
1282
|
-
sectionVariant === "framed" ? "legend" : "default"
|
|
1283
|
-
}
|
|
1284
1280
|
title={section}
|
|
1285
1281
|
media={sectionIcons?.[section]}
|
|
1286
1282
|
>
|
|
1287
1283
|
<FieldGroup className={cn("grid gap-4", gridColumns)}>
|
|
1288
1284
|
{entries.map(field)}
|
|
1289
1285
|
</FieldGroup>
|
|
1290
|
-
</
|
|
1286
|
+
</Surface>
|
|
1291
1287
|
))
|
|
1292
1288
|
) : (
|
|
1293
1289
|
<FieldGroup className={cn("grid gap-4", gridColumns)}>
|
|
@@ -1485,7 +1481,7 @@ export function ActionFilterBar({
|
|
|
1485
1481
|
<div
|
|
1486
1482
|
data-slot="action-filter-period-group"
|
|
1487
1483
|
className={cn(
|
|
1488
|
-
"flex min-h-9 w-fit min-w-0 max-w-full items-center rounded-md border border-input bg-
|
|
1484
|
+
"flex min-h-9 w-fit min-w-0 max-w-full items-center rounded-md border border-input bg-control text-foreground transition-[color,box-shadow] focus-within:border-ring focus-within:ring-[0.1875rem] focus-within:ring-ring/50 has-[[data-state=open]]:border-ring has-[[data-state=open]]:ring-[0.1875rem] has-[[data-state=open]]:ring-ring/50",
|
|
1489
1485
|
!periodGroupClearable && "pr-3",
|
|
1490
1486
|
)}
|
|
1491
1487
|
>
|
|
@@ -2035,9 +2031,9 @@ export function ActionList<TInput, TItem>({
|
|
|
2035
2031
|
{viewEntries.map(([key, label, icon]) => (
|
|
2036
2032
|
<Tooltip key={key}>
|
|
2037
2033
|
<TooltipTrigger asChild>
|
|
2038
|
-
<
|
|
2034
|
+
<Toggle value={key} aria-label={label}>
|
|
2039
2035
|
{icon ?? label}
|
|
2040
|
-
</
|
|
2036
|
+
</Toggle>
|
|
2041
2037
|
</TooltipTrigger>
|
|
2042
2038
|
<TooltipContent>{label}</TooltipContent>
|
|
2043
2039
|
</Tooltip>
|
|
@@ -24,12 +24,15 @@ import { createPortal } from "react-dom";
|
|
|
24
24
|
import { ArrowLeft } from "lucide-react";
|
|
25
25
|
import { cn } from "../../lib/cn.ts";
|
|
26
26
|
import { Button } from "../primitives/button.tsx";
|
|
27
|
-
import {
|
|
27
|
+
import { Surface } from "./surface.tsx";
|
|
28
28
|
import {
|
|
29
29
|
PageHeadingPresenceProvider,
|
|
30
30
|
usePageHeadingPresence,
|
|
31
31
|
} from "./page-heading-context.tsx";
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
StructuralHeader,
|
|
34
|
+
structuralHeaderClasses,
|
|
35
|
+
} from "./structural-header.tsx";
|
|
33
36
|
import { PageState, PageStatePresenceProvider } from "./page-state.tsx";
|
|
34
37
|
import { FillRoot, fillRegionClassName, useFillRegion } from "./fill-region.ts";
|
|
35
38
|
|
|
@@ -137,7 +140,7 @@ interface PageBaseProps extends Omit<
|
|
|
137
140
|
className?: string;
|
|
138
141
|
/**
|
|
139
142
|
* `fill` limita a página à altura do shell para que regiões internas assumam a rolagem.
|
|
140
|
-
* A altura chega sozinha a uma `ActionList` composta por `PageBody` e `
|
|
143
|
+
* A altura chega sozinha a uma `ActionList` composta por `PageBody` e `Surface` (ADR 0024).
|
|
141
144
|
*/
|
|
142
145
|
layout?: "flow" | "fill";
|
|
143
146
|
}
|
|
@@ -198,7 +201,7 @@ function countPageBodyHeadings(node: ReactNode): {
|
|
|
198
201
|
return {
|
|
199
202
|
content:
|
|
200
203
|
total.content +
|
|
201
|
-
(child.type ===
|
|
204
|
+
(child.type === Surface && props.level === 1 ? 1 : 0) +
|
|
202
205
|
nested.content,
|
|
203
206
|
state: total.state + nested.state,
|
|
204
207
|
};
|
|
@@ -211,7 +214,7 @@ function hasOpaquePageBodyContent(node: ReactNode): boolean {
|
|
|
211
214
|
return Children.toArray(node).some((child) => {
|
|
212
215
|
if (!isValidElement(child)) return false;
|
|
213
216
|
if (typeof child.type !== "string") {
|
|
214
|
-
return child.type !==
|
|
217
|
+
return child.type !== Surface && child.type !== PageState;
|
|
215
218
|
}
|
|
216
219
|
return hasOpaquePageBodyContent(
|
|
217
220
|
(child.props as { children?: ReactNode }).children,
|
|
@@ -323,7 +326,7 @@ export function Page({
|
|
|
323
326
|
);
|
|
324
327
|
if (headingSources > 1 || (headingSources === 0 && !hasOpaqueBodyContent)) {
|
|
325
328
|
throw new Error(
|
|
326
|
-
"Page explícito exige exatamente um heading principal em PageHeader,
|
|
329
|
+
"Page explícito exige exatamente um heading principal em PageHeader, Surface level={1} ou PageState ativo dentro de PageBody.",
|
|
327
330
|
);
|
|
328
331
|
}
|
|
329
332
|
}
|
|
@@ -332,7 +335,7 @@ export function Page({
|
|
|
332
335
|
if (integralState !== hasActiveIntegralState) return;
|
|
333
336
|
if (headingCount.current !== 1) {
|
|
334
337
|
throw new Error(
|
|
335
|
-
"Page explícito exige exatamente um heading principal em PageHeader,
|
|
338
|
+
"Page explícito exige exatamente um heading principal em PageHeader, Surface level={1} ou PageState ativo dentro de PageBody.",
|
|
336
339
|
);
|
|
337
340
|
}
|
|
338
341
|
});
|
|
@@ -452,7 +455,7 @@ export function PageHeader({
|
|
|
452
455
|
}
|
|
453
456
|
return (
|
|
454
457
|
<PageHeaderContext.Provider value>
|
|
455
|
-
<
|
|
458
|
+
<StructuralHeader
|
|
456
459
|
name="PageHeader"
|
|
457
460
|
slot="page"
|
|
458
461
|
slots={{
|
|
@@ -472,7 +475,7 @@ export function PageHeader({
|
|
|
472
475
|
)}
|
|
473
476
|
{structuralChildren}
|
|
474
477
|
{mergedActions}
|
|
475
|
-
</
|
|
478
|
+
</StructuralHeader>
|
|
476
479
|
</PageHeaderContext.Provider>
|
|
477
480
|
);
|
|
478
481
|
}
|
|
@@ -487,7 +490,7 @@ export function PageTitle({
|
|
|
487
490
|
return (
|
|
488
491
|
<h1
|
|
489
492
|
data-slot="page-title"
|
|
490
|
-
className={cn(
|
|
493
|
+
className={cn(structuralHeaderClasses.page.title, className)}
|
|
491
494
|
{...props}
|
|
492
495
|
/>
|
|
493
496
|
);
|
|
@@ -503,7 +506,7 @@ export function PageActions({
|
|
|
503
506
|
const actions = (
|
|
504
507
|
<div
|
|
505
508
|
data-slot="page-actions"
|
|
506
|
-
className={cn(
|
|
509
|
+
className={cn(structuralHeaderClasses.page.actions, className)}
|
|
507
510
|
{...props}
|
|
508
511
|
/>
|
|
509
512
|
);
|
|
@@ -579,6 +582,7 @@ export function PageBody({
|
|
|
579
582
|
data-slot="page-body"
|
|
580
583
|
{...fill.attributes}
|
|
581
584
|
className={cn(
|
|
585
|
+
"flex flex-col gap-6 *:shrink-0",
|
|
582
586
|
(integralState || fill.filling) && fillRegionClassName,
|
|
583
587
|
className,
|
|
584
588
|
)}
|
|
@@ -1,45 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pane —
|
|
3
|
-
* lista ou conteúdo, com regiões fixas no topo e no rodapé e um corpo que rola.
|
|
2
|
+
* Pane — unidade dimensionável de um Split.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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.
|
|
4
|
+
* Pane não é uma superfície visual: não aplica inset, borda, fundo, rolagem nem anatomia
|
|
5
|
+
* interna. O filho decide sua própria apresentação, normalmente com Surface ou Sidebar.
|
|
19
6
|
*/
|
|
20
|
-
import {
|
|
21
|
-
Children,
|
|
22
|
-
Fragment,
|
|
23
|
-
isValidElement,
|
|
24
|
-
type CSSProperties,
|
|
25
|
-
type ReactElement,
|
|
26
|
-
type ReactNode,
|
|
27
|
-
} from "react";
|
|
7
|
+
import type { CSSProperties, ReactElement, ReactNode } from "react";
|
|
28
8
|
|
|
29
9
|
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
10
|
|
|
44
11
|
export type PaneSize =
|
|
45
12
|
| number
|
|
@@ -67,48 +34,15 @@ export interface PaneProps {
|
|
|
67
34
|
maxSize?: PaneSize;
|
|
68
35
|
/** Ocupa o espaço remanescente no layout simples. */
|
|
69
36
|
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
37
|
className?: string;
|
|
80
38
|
children: ReactNode;
|
|
81
39
|
}
|
|
82
40
|
|
|
83
|
-
|
|
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>. */
|
|
41
|
+
/** Um conteúdo encaixável. Sua posição é dada pela ordem dentro de Split. */
|
|
106
42
|
export function Pane({
|
|
107
43
|
id,
|
|
108
44
|
initialSize,
|
|
109
45
|
grow = false,
|
|
110
|
-
inset,
|
|
111
|
-
layout = "sectioned",
|
|
112
46
|
className,
|
|
113
47
|
children,
|
|
114
48
|
}: PaneProps): ReactElement {
|
|
@@ -116,177 +50,12 @@ export function Pane({
|
|
|
116
50
|
initialSize === undefined
|
|
117
51
|
? undefined
|
|
118
52
|
: { 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
53
|
return (
|
|
282
54
|
<div
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
className,
|
|
288
|
-
)}
|
|
289
|
-
{...props}
|
|
55
|
+
id={id === undefined ? undefined : String(id)}
|
|
56
|
+
data-slot="pane"
|
|
57
|
+
className={cn("min-h-0 min-w-0 shrink-0", grow && "flex-1", className)}
|
|
58
|
+
style={style}
|
|
290
59
|
>
|
|
291
60
|
{children}
|
|
292
61
|
</div>
|
|
@@ -75,12 +75,12 @@ import { ActionList, type ActionListState } from "./list.tsx";
|
|
|
75
75
|
import { ActionTrigger } from "./trigger.tsx";
|
|
76
76
|
import { ActionView } from "./view.tsx";
|
|
77
77
|
import {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
} from "./
|
|
78
|
+
Surface,
|
|
79
|
+
SurfaceActions,
|
|
80
|
+
SurfaceBody,
|
|
81
|
+
SurfaceHeader,
|
|
82
|
+
SurfaceTitle,
|
|
83
|
+
} from "./surface.tsx";
|
|
84
84
|
import { SurfaceAssistant } from "./surface-assistant.tsx";
|
|
85
85
|
import { fillRegionClassName, useFillRegion } from "./fill-region.ts";
|
|
86
86
|
|
|
@@ -221,17 +221,17 @@ function PresentationFrame({
|
|
|
221
221
|
if (surface === "page") {
|
|
222
222
|
if (listBody) {
|
|
223
223
|
const content = (
|
|
224
|
-
<
|
|
225
|
-
<
|
|
226
|
-
<
|
|
224
|
+
<Surface as="section" level={1}>
|
|
225
|
+
<SurfaceHeader>
|
|
226
|
+
<SurfaceTitle>{title}</SurfaceTitle>
|
|
227
227
|
{headerActions === undefined ? null : (
|
|
228
|
-
<
|
|
228
|
+
<SurfaceActions>
|
|
229
229
|
<PresentationActions>{headerActions}</PresentationActions>
|
|
230
|
-
</
|
|
230
|
+
</SurfaceActions>
|
|
231
231
|
)}
|
|
232
|
-
</
|
|
233
|
-
<
|
|
234
|
-
</
|
|
232
|
+
</SurfaceHeader>
|
|
233
|
+
<SurfaceBody>{body}</SurfaceBody>
|
|
234
|
+
</Surface>
|
|
235
235
|
);
|
|
236
236
|
return (
|
|
237
237
|
<Page
|