@softize/opus 18.4.0 → 19.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 +94 -0
- package/docs/adr/0020-icon-tiles-are-explicitly-composed.md +46 -0
- package/docs/adr/0021-content-carries-structure-and-semantic-element.md +65 -0
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +293 -0
- package/package.json +1 -1
- package/src/ui/components/patterns/content-header.tsx +169 -50
- package/src/ui/components/patterns/heading-level.ts +16 -0
- package/src/ui/components/patterns/list.tsx +76 -27
- package/src/ui/components/patterns/pane.tsx +294 -0
- package/src/ui/components/patterns/presentation.tsx +29 -17
- package/src/ui/components/patterns/sidebar.tsx +13 -72
- package/src/ui/components/patterns/split.tsx +8 -35
- package/src/ui/components/patterns/surface-anatomy.ts +219 -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/primitives/card.tsx +91 -29
- package/src/ui/components/primitives/command.tsx +12 -4
- package/src/ui/components/primitives/detail.tsx +10 -13
- package/src/ui/components/primitives/dialog.tsx +138 -35
- package/src/ui/components/primitives/drawer.tsx +122 -23
- package/src/ui/components/primitives/empty.tsx +2 -1
- package/src/ui/components/primitives/field.tsx +38 -16
- package/src/ui/components/primitives/icon-tile.tsx +91 -0
- package/src/ui/components/primitives/item.tsx +2 -1
- package/src/ui/components/primitives/metric-card.tsx +98 -39
- package/src/ui/components/primitives/table.tsx +1 -1
- package/src/ui/docs/content/action-list.md +61 -52
- package/src/ui/docs/content/card.md +221 -30
- package/src/ui/docs/content/content.md +90 -8
- package/src/ui/docs/content/dialog.md +115 -7
- package/src/ui/docs/content/drawer.md +136 -9
- package/src/ui/docs/content/field.md +78 -30
- package/src/ui/docs/content/icon-tile.md +47 -0
- package/src/ui/docs/content/metric-card.md +57 -18
- package/src/ui/docs/content/page.md +8 -0
- package/src/ui/docs/content/pane.md +250 -0
- package/src/ui/docs/content/sidebar.md +22 -43
- package/src/ui/docs/content/skeleton.md +7 -3
- package/src/ui/docs/content/split.md +16 -21
- package/src/ui/docs/registry.tsx +14 -2
- package/src/ui/meta.ts +17 -5
- package/src/ui/react.tsx +31 -0
|
@@ -4,11 +4,13 @@ import {
|
|
|
4
4
|
isValidElement,
|
|
5
5
|
useContext,
|
|
6
6
|
useId,
|
|
7
|
+
type ComponentProps,
|
|
7
8
|
type HTMLAttributes,
|
|
8
9
|
type ReactElement,
|
|
9
10
|
type ReactNode,
|
|
10
11
|
} from "react";
|
|
11
12
|
import { cn } from "../../lib/cn.ts";
|
|
13
|
+
import { headingTags } from "./heading-level.ts";
|
|
12
14
|
import { usePageHeadingPresence } from "./page-heading-context.tsx";
|
|
13
15
|
import {
|
|
14
16
|
SurfaceHeader,
|
|
@@ -18,8 +20,13 @@ import {
|
|
|
18
20
|
|
|
19
21
|
export type ContentHeaderLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
20
22
|
export type ContentHeaderVariant = SurfaceHeaderVariant;
|
|
23
|
+
export type ContentElement = "section" | "fieldset";
|
|
24
|
+
export type ContentFrameVariant = "default" | "legend";
|
|
21
25
|
|
|
22
26
|
interface ContentContextValue {
|
|
27
|
+
element: ContentElement;
|
|
28
|
+
frameVariant: ContentFrameVariant;
|
|
29
|
+
framed: boolean;
|
|
23
30
|
level: ContentHeaderLevel;
|
|
24
31
|
titleId: string;
|
|
25
32
|
variant: ContentHeaderVariant;
|
|
@@ -38,17 +45,37 @@ function requiredContext<T>(
|
|
|
38
45
|
return value;
|
|
39
46
|
}
|
|
40
47
|
|
|
41
|
-
interface
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
> {
|
|
45
|
-
/** Nível semântico do heading, independente de seu destaque visual. */
|
|
46
|
-
level?: ContentHeaderLevel;
|
|
48
|
+
interface ContentSharedProps {
|
|
49
|
+
/** Aplica a moldura estrutural da região sem transferi-la ao conteúdo interno. */
|
|
50
|
+
framed?: boolean;
|
|
47
51
|
/** Hierarquia visual: `section` (default) ou `page`, a mesma de PageHeader. */
|
|
48
52
|
variant?: ContentHeaderVariant;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
interface
|
|
55
|
+
interface ContentSectionBaseProps
|
|
56
|
+
extends
|
|
57
|
+
ContentSharedProps,
|
|
58
|
+
Omit<ComponentProps<"section">, "children" | "title"> {
|
|
59
|
+
/** Elemento semântico da região. */
|
|
60
|
+
as?: "section";
|
|
61
|
+
frameVariant?: "default";
|
|
62
|
+
/** Nível semântico do heading, independente de seu destaque visual. */
|
|
63
|
+
level?: ContentHeaderLevel;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface ContentFieldsetBaseProps
|
|
67
|
+
extends
|
|
68
|
+
ContentSharedProps,
|
|
69
|
+
Omit<ComponentProps<"fieldset">, "children" | "title"> {
|
|
70
|
+
/** Usa a semântica nativa de grupo de campos; o título vira `legend`. */
|
|
71
|
+
as: "fieldset";
|
|
72
|
+
/** Tratamento visual da moldura: contínua (default) ou recortada pela legenda. */
|
|
73
|
+
frameVariant?: ContentFrameVariant;
|
|
74
|
+
level?: never;
|
|
75
|
+
variant?: "section";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface ContentSectionShorthandProps extends ContentSectionBaseProps {
|
|
52
79
|
title: ReactNode;
|
|
53
80
|
/** Total de itens ao lado do título da seção. A página não carrega contador (ADR 0009). */
|
|
54
81
|
count?: number;
|
|
@@ -57,7 +84,15 @@ interface ContentShorthandProps extends ContentBaseProps {
|
|
|
57
84
|
children: ReactNode;
|
|
58
85
|
}
|
|
59
86
|
|
|
60
|
-
interface
|
|
87
|
+
interface ContentFieldsetShorthandProps extends ContentFieldsetBaseProps {
|
|
88
|
+
title: ReactNode;
|
|
89
|
+
count?: never;
|
|
90
|
+
description?: never;
|
|
91
|
+
actions?: never;
|
|
92
|
+
children: ReactNode;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface ContentComposedProps {
|
|
61
96
|
title?: never;
|
|
62
97
|
count?: never;
|
|
63
98
|
description?: never;
|
|
@@ -65,22 +100,36 @@ interface ContentComposedProps extends ContentBaseProps {
|
|
|
65
100
|
children: ReactNode;
|
|
66
101
|
}
|
|
67
102
|
|
|
68
|
-
export type ContentProps =
|
|
103
|
+
export type ContentProps =
|
|
104
|
+
| ContentSectionShorthandProps
|
|
105
|
+
| ContentFieldsetShorthandProps
|
|
106
|
+
| ((ContentSectionBaseProps | ContentFieldsetBaseProps) &
|
|
107
|
+
ContentComposedProps);
|
|
69
108
|
|
|
70
109
|
/** Região de conteúdo nomeada. A forma curta e a explícita produzem a mesma anatomia. */
|
|
71
110
|
export function Content({
|
|
111
|
+
as = "section",
|
|
72
112
|
title,
|
|
73
113
|
count,
|
|
74
114
|
description,
|
|
75
115
|
actions,
|
|
76
116
|
level = 2,
|
|
77
117
|
variant = "section",
|
|
118
|
+
framed = false,
|
|
119
|
+
frameVariant = "default",
|
|
78
120
|
children,
|
|
79
121
|
className,
|
|
80
122
|
...props
|
|
81
123
|
}: ContentProps): ReactElement {
|
|
82
124
|
const titleId = useId();
|
|
83
|
-
const context = {
|
|
125
|
+
const context = {
|
|
126
|
+
element: as,
|
|
127
|
+
frameVariant,
|
|
128
|
+
framed,
|
|
129
|
+
level: level as ContentHeaderLevel,
|
|
130
|
+
titleId,
|
|
131
|
+
variant,
|
|
132
|
+
};
|
|
84
133
|
const shorthand = title !== undefined;
|
|
85
134
|
const nodes = Children.toArray(children);
|
|
86
135
|
|
|
@@ -110,34 +159,64 @@ export function Content({
|
|
|
110
159
|
}
|
|
111
160
|
}
|
|
112
161
|
|
|
162
|
+
const anatomy = shorthand ? (
|
|
163
|
+
<>
|
|
164
|
+
<ContentHeader>
|
|
165
|
+
<ContentTitle>{title}</ContentTitle>
|
|
166
|
+
{count !== undefined && <ContentMeta>{count}</ContentMeta>}
|
|
167
|
+
{description !== undefined && (
|
|
168
|
+
<ContentDescription>{description}</ContentDescription>
|
|
169
|
+
)}
|
|
170
|
+
{actions !== undefined && <ContentActions>{actions}</ContentActions>}
|
|
171
|
+
</ContentHeader>
|
|
172
|
+
<ContentBody>{children}</ContentBody>
|
|
173
|
+
</>
|
|
174
|
+
) : (
|
|
175
|
+
children
|
|
176
|
+
);
|
|
177
|
+
const ariaLabelledBy =
|
|
178
|
+
props["aria-label"] === undefined ? titleId : undefined;
|
|
179
|
+
|
|
113
180
|
return (
|
|
114
181
|
<ContentContext.Provider value={context}>
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
182
|
+
{as === "fieldset" ? (
|
|
183
|
+
<fieldset
|
|
184
|
+
data-slot="content"
|
|
185
|
+
data-as="fieldset"
|
|
186
|
+
data-framed={framed}
|
|
187
|
+
data-frame-variant={frameVariant}
|
|
188
|
+
aria-labelledby={ariaLabelledBy}
|
|
189
|
+
className={cn(
|
|
190
|
+
!framed && "space-y-3",
|
|
191
|
+
framed &&
|
|
192
|
+
frameVariant === "default" &&
|
|
193
|
+
"rounded-lg border border-border p-4",
|
|
194
|
+
framed &&
|
|
195
|
+
frameVariant === "legend" &&
|
|
196
|
+
"space-y-3 rounded-lg border border-border px-4 pb-4",
|
|
197
|
+
className,
|
|
198
|
+
)}
|
|
199
|
+
{...(props as ComponentProps<"fieldset">)}
|
|
200
|
+
>
|
|
201
|
+
{anatomy}
|
|
202
|
+
</fieldset>
|
|
203
|
+
) : (
|
|
204
|
+
<section
|
|
205
|
+
data-slot="content"
|
|
206
|
+
data-as="section"
|
|
207
|
+
data-framed={framed}
|
|
208
|
+
data-frame-variant={frameVariant}
|
|
209
|
+
aria-labelledby={ariaLabelledBy}
|
|
210
|
+
className={cn(
|
|
211
|
+
"space-y-3",
|
|
212
|
+
framed && "rounded-lg border border-border p-4",
|
|
213
|
+
className,
|
|
214
|
+
)}
|
|
215
|
+
{...(props as ComponentProps<"section">)}
|
|
216
|
+
>
|
|
217
|
+
{anatomy}
|
|
218
|
+
</section>
|
|
219
|
+
)}
|
|
141
220
|
</ContentContext.Provider>
|
|
142
221
|
);
|
|
143
222
|
}
|
|
@@ -158,6 +237,37 @@ export function ContentHeader({
|
|
|
158
237
|
"ContentHeader",
|
|
159
238
|
"Content",
|
|
160
239
|
);
|
|
240
|
+
if (content.element === "fieldset") {
|
|
241
|
+
const nodes = Children.toArray(children);
|
|
242
|
+
if (
|
|
243
|
+
nodes.length !== 1 ||
|
|
244
|
+
!isValidElement(nodes[0]) ||
|
|
245
|
+
nodes[0].type !== ContentTitle
|
|
246
|
+
) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
"ContentHeader de fieldset exige somente um ContentTitle; contador, descrição e ações ficam fora da legenda.",
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
return (
|
|
252
|
+
<ContentHeaderContext.Provider value={content}>
|
|
253
|
+
<legend
|
|
254
|
+
data-slot="content-header"
|
|
255
|
+
className={cn(
|
|
256
|
+
content.framed &&
|
|
257
|
+
content.frameVariant === "default" &&
|
|
258
|
+
"float-left w-full",
|
|
259
|
+
content.framed &&
|
|
260
|
+
content.frameVariant === "legend" &&
|
|
261
|
+
"-ml-2 w-auto px-2",
|
|
262
|
+
className,
|
|
263
|
+
)}
|
|
264
|
+
{...props}
|
|
265
|
+
>
|
|
266
|
+
{children}
|
|
267
|
+
</legend>
|
|
268
|
+
</ContentHeaderContext.Provider>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
161
271
|
return (
|
|
162
272
|
<ContentHeaderContext.Provider value={content}>
|
|
163
273
|
<SurfaceHeader
|
|
@@ -182,27 +292,19 @@ export function ContentHeader({
|
|
|
182
292
|
);
|
|
183
293
|
}
|
|
184
294
|
|
|
185
|
-
const headingTags = {
|
|
186
|
-
1: "h1",
|
|
187
|
-
2: "h2",
|
|
188
|
-
3: "h3",
|
|
189
|
-
4: "h4",
|
|
190
|
-
5: "h5",
|
|
191
|
-
6: "h6",
|
|
192
|
-
} as const;
|
|
193
|
-
|
|
194
295
|
export function ContentTitle({
|
|
195
296
|
className,
|
|
196
297
|
id,
|
|
197
298
|
...props
|
|
198
|
-
}: HTMLAttributes<
|
|
299
|
+
}: HTMLAttributes<HTMLElement>): ReactElement {
|
|
199
300
|
const context = requiredContext(
|
|
200
301
|
useContext(ContentHeaderContext),
|
|
201
302
|
"ContentTitle",
|
|
202
303
|
"ContentHeader",
|
|
203
304
|
);
|
|
204
|
-
const Heading =
|
|
205
|
-
|
|
305
|
+
const Heading =
|
|
306
|
+
context.element === "fieldset" ? "span" : headingTags[context.level];
|
|
307
|
+
usePageHeadingPresence(context.element !== "fieldset" && context.level === 1);
|
|
206
308
|
return (
|
|
207
309
|
<Heading
|
|
208
310
|
id={id ?? context.titleId}
|
|
@@ -279,6 +381,23 @@ export function ContentBody({
|
|
|
279
381
|
className,
|
|
280
382
|
...props
|
|
281
383
|
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
282
|
-
requiredContext(
|
|
283
|
-
|
|
384
|
+
const content = requiredContext(
|
|
385
|
+
useContext(ContentContext),
|
|
386
|
+
"ContentBody",
|
|
387
|
+
"Content",
|
|
388
|
+
);
|
|
389
|
+
return (
|
|
390
|
+
<div
|
|
391
|
+
data-slot="content-body"
|
|
392
|
+
className={cn(
|
|
393
|
+
content.element === "fieldset" && "space-y-3",
|
|
394
|
+
content.element === "fieldset" &&
|
|
395
|
+
content.framed &&
|
|
396
|
+
content.frameVariant === "default" &&
|
|
397
|
+
"clear-left pt-3",
|
|
398
|
+
className,
|
|
399
|
+
)}
|
|
400
|
+
{...props}
|
|
401
|
+
/>
|
|
402
|
+
);
|
|
284
403
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nível de heading (interno): hierarquia do documento, separada do tamanho visual.
|
|
3
|
+
*
|
|
4
|
+
* A ADR 0023 fixa a regra: `h1`–`h6` expressam hierarquia; a escala do título vem da
|
|
5
|
+
* família e do layout. As duas decisões nunca se misturam num mesmo valor.
|
|
6
|
+
*/
|
|
7
|
+
export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
8
|
+
|
|
9
|
+
export const headingTags = {
|
|
10
|
+
1: "h1",
|
|
11
|
+
2: "h2",
|
|
12
|
+
3: "h3",
|
|
13
|
+
4: "h4",
|
|
14
|
+
5: "h5",
|
|
15
|
+
6: "h6",
|
|
16
|
+
} as const satisfies Record<HeadingLevel, `h${HeadingLevel}`>;
|
|
@@ -111,8 +111,13 @@ import {
|
|
|
111
111
|
InputGroupInput,
|
|
112
112
|
} from "../primitives/input-group.tsx";
|
|
113
113
|
import { Separator } from "../primitives/separator.tsx";
|
|
114
|
-
import {
|
|
115
|
-
|
|
114
|
+
import {
|
|
115
|
+
Field,
|
|
116
|
+
FieldGroup,
|
|
117
|
+
FieldLabel,
|
|
118
|
+
type FieldSetVariant,
|
|
119
|
+
} from "../primitives/field.tsx";
|
|
120
|
+
import { Content } from "./content-header.tsx";
|
|
116
121
|
import { Search } from "lucide-react";
|
|
117
122
|
import { Select, type SelectOption } from "../primitives/select.tsx";
|
|
118
123
|
import {
|
|
@@ -307,6 +312,8 @@ export interface AdvancedFiltersLayout {
|
|
|
307
312
|
columns?: 1 | 2 | 3;
|
|
308
313
|
/** Ícone de cada seção, indexado pelo mesmo rótulo declarado nos filtros. */
|
|
309
314
|
sectionIcons?: Record<string, ReactNode>;
|
|
315
|
+
/** Tratamento visual de cada seção. `framed` integra a legenda à moldura do conjunto. */
|
|
316
|
+
sectionVariant?: FieldSetVariant;
|
|
310
317
|
}
|
|
311
318
|
|
|
312
319
|
/** Uma view alternativa da MESMA listagem (board, galeria, lista, calendário…). A
|
|
@@ -1105,6 +1112,7 @@ function AdvancedFiltersDialog({
|
|
|
1105
1112
|
filterDefaults,
|
|
1106
1113
|
filterIcons,
|
|
1107
1114
|
sectionIcons,
|
|
1115
|
+
sectionVariant = "plain",
|
|
1108
1116
|
cascade,
|
|
1109
1117
|
columns = 1,
|
|
1110
1118
|
}: {
|
|
@@ -1117,6 +1125,7 @@ function AdvancedFiltersDialog({
|
|
|
1117
1125
|
filterDefaults?: Record<string, string> | undefined;
|
|
1118
1126
|
filterIcons?: Record<string, ReactNode> | undefined;
|
|
1119
1127
|
sectionIcons?: Record<string, ReactNode> | undefined;
|
|
1128
|
+
sectionVariant?: FieldSetVariant | undefined;
|
|
1120
1129
|
columns?: 1 | 2 | 3;
|
|
1121
1130
|
/** A cascata do `depends` do pai — mudar um campo limpa os dependentes no draft. */
|
|
1122
1131
|
cascade: (
|
|
@@ -1153,8 +1162,10 @@ function AdvancedFiltersDialog({
|
|
|
1153
1162
|
}[columns];
|
|
1154
1163
|
|
|
1155
1164
|
const field = ([name, spec]: [string, FilterSpec]): React.ReactElement => (
|
|
1156
|
-
<
|
|
1157
|
-
<
|
|
1165
|
+
<Field key={name}>
|
|
1166
|
+
<FieldLabel htmlFor={`filter-${name}`}>
|
|
1167
|
+
{text(spec.label, name)}
|
|
1168
|
+
</FieldLabel>
|
|
1158
1169
|
<FilterField
|
|
1159
1170
|
name={name}
|
|
1160
1171
|
spec={spec}
|
|
@@ -1166,7 +1177,7 @@ function AdvancedFiltersDialog({
|
|
|
1166
1177
|
defaultValue={filterDefaults?.[name]}
|
|
1167
1178
|
icon={filterIcons?.[name]}
|
|
1168
1179
|
/>
|
|
1169
|
-
</
|
|
1180
|
+
</Field>
|
|
1170
1181
|
);
|
|
1171
1182
|
|
|
1172
1183
|
const clear = (): void => {
|
|
@@ -1194,14 +1205,32 @@ function AdvancedFiltersDialog({
|
|
|
1194
1205
|
<DialogBody className="space-y-4">
|
|
1195
1206
|
{sectioned ? (
|
|
1196
1207
|
sections.map(([section, entries]) => (
|
|
1197
|
-
<
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1208
|
+
<Content
|
|
1209
|
+
key={section}
|
|
1210
|
+
as="fieldset"
|
|
1211
|
+
framed={sectionVariant === "framed"}
|
|
1212
|
+
frameVariant={
|
|
1213
|
+
sectionVariant === "framed" ? "legend" : "default"
|
|
1214
|
+
}
|
|
1215
|
+
title={
|
|
1216
|
+
<span className="flex items-center gap-2">
|
|
1217
|
+
{sectionIcons?.[section] !== undefined && (
|
|
1218
|
+
<span
|
|
1219
|
+
data-slot="content-title-icon"
|
|
1220
|
+
aria-hidden="true"
|
|
1221
|
+
className="inline-flex shrink-0 items-center justify-center text-muted-foreground [&_svg]:size-4"
|
|
1222
|
+
>
|
|
1223
|
+
{sectionIcons[section]}
|
|
1224
|
+
</span>
|
|
1225
|
+
)}
|
|
1226
|
+
{section}
|
|
1227
|
+
</span>
|
|
1228
|
+
}
|
|
1229
|
+
>
|
|
1201
1230
|
<FieldGroup className={cn("grid gap-4", gridColumns)}>
|
|
1202
1231
|
{entries.map(field)}
|
|
1203
1232
|
</FieldGroup>
|
|
1204
|
-
</
|
|
1233
|
+
</Content>
|
|
1205
1234
|
))
|
|
1206
1235
|
) : (
|
|
1207
1236
|
<FieldGroup className={cn("grid gap-4", gridColumns)}>
|
|
@@ -1597,6 +1626,7 @@ export function ActionFilterBar({
|
|
|
1597
1626
|
filterIcons={filterIcons}
|
|
1598
1627
|
columns={advancedFilters?.columns}
|
|
1599
1628
|
sectionIcons={advancedFilters?.sectionIcons}
|
|
1629
|
+
sectionVariant={advancedFilters?.sectionVariant}
|
|
1600
1630
|
/>
|
|
1601
1631
|
)}
|
|
1602
1632
|
</div>
|
|
@@ -2111,18 +2141,21 @@ export function ActionList<TInput, TItem>({
|
|
|
2111
2141
|
} else {
|
|
2112
2142
|
content = (
|
|
2113
2143
|
<Table
|
|
2114
|
-
variant="
|
|
2144
|
+
variant="plain"
|
|
2115
2145
|
className={tableStateActive ? "h-full" : undefined}
|
|
2116
2146
|
containerClassName={
|
|
2147
|
+
// Quem rola é a linha de resultados, acima: aqui o contêiner abre mão da
|
|
2148
|
+
// rolagem horizontal para não virar a referência do `sticky` do cabeçalho.
|
|
2149
|
+
// `h-full` só no estado sem linhas, para a moldura vazia ocupar a altura.
|
|
2117
2150
|
tableFillsAvailableHeight
|
|
2118
|
-
? "
|
|
2151
|
+
? cn("overflow-x-visible", tableStateActive && "h-full")
|
|
2119
2152
|
: undefined
|
|
2120
2153
|
}
|
|
2121
2154
|
>
|
|
2122
2155
|
<TableHeader
|
|
2123
2156
|
className={
|
|
2124
2157
|
tableFillsAvailableHeight
|
|
2125
|
-
? "sticky top-0 z-10 bg-
|
|
2158
|
+
? "sticky top-0 z-10 bg-background shadow-[inset_0_-0.0625rem_0_var(--border)] [&_tr]:border-b-0"
|
|
2126
2159
|
: undefined
|
|
2127
2160
|
}
|
|
2128
2161
|
>
|
|
@@ -2260,7 +2293,14 @@ export function ActionList<TInput, TItem>({
|
|
|
2260
2293
|
) : null;
|
|
2261
2294
|
|
|
2262
2295
|
// — Rodapé (o estilo da casa): Página X de Y à esquerda · pager CENTRALIZADO ·
|
|
2263
|
-
// total à direita (grid de 3 colunas com laterais 1fr = centro geométrico)
|
|
2296
|
+
// total à direita (grid de 3 colunas com laterais 1fr = centro geométrico).
|
|
2297
|
+
// Encosta na última linha: a borda de cima é o fecho da tabela, não a de um
|
|
2298
|
+
// bloco solto, então o rodapé não entra no `gap` da pilha.
|
|
2299
|
+
// Altura: 2.5rem de piso (a mesma do `TableHead`) para a faixa não mudar de
|
|
2300
|
+
// tamanho quando o pager aparece — ele ocupa 1.75rem e cabe dentro do piso.
|
|
2301
|
+
// O padding fica em 0.25rem só para o dia em que o conteúdo passar disso:
|
|
2302
|
+
// com `py-2` o padding empurraria o pager para fora e a faixa voltaria a
|
|
2303
|
+
// crescer —
|
|
2264
2304
|
const pages =
|
|
2265
2305
|
total !== undefined
|
|
2266
2306
|
? Math.max(1, Math.ceil(total / (state.limit ?? pageSize)))
|
|
@@ -2270,7 +2310,7 @@ export function ActionList<TInput, TItem>({
|
|
|
2270
2310
|
total !== undefined && !isError ? (
|
|
2271
2311
|
<div
|
|
2272
2312
|
data-slot="action-list-footer"
|
|
2273
|
-
className="grid grid-cols-[1fr_auto_1fr] items-center gap-3 px-3 py-1
|
|
2313
|
+
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"
|
|
2274
2314
|
>
|
|
2275
2315
|
<span>
|
|
2276
2316
|
Página {state.page} de {pages}
|
|
@@ -2338,19 +2378,28 @@ export function ActionList<TInput, TItem>({
|
|
|
2338
2378
|
{toolbar}
|
|
2339
2379
|
{beforeContent}
|
|
2340
2380
|
{selectionBar}
|
|
2341
|
-
{/*
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2381
|
+
{/* Tabela e rodapé formam um bloco só: o `gap` da pilha separa a barra de
|
|
2382
|
+
ferramentas do conteúdo, nunca o conteúdo do seu próprio rodapé. Duas
|
|
2383
|
+
linhas: a dos resultados absorve a altura que sobra, a do rodapé se mede
|
|
2384
|
+
pelo conteúdo. Em `flow` a primeira linha também é `1fr`, mas o bloco não
|
|
2385
|
+
tem altura própria e ela vira a altura do conteúdo. */}
|
|
2386
|
+
<div className={cn("grid min-h-0 grid-rows-[1fr_auto]", layout === "fill" && "flex-1")}>
|
|
2387
|
+
{/* A rolagem mora aqui, nos dois eixos: é este elemento que o `sticky` do
|
|
2388
|
+
cabeçalho enxerga. Um contêiner de rolagem entre ele e a tabela roubaria
|
|
2389
|
+
essa referência e o cabeçalho voltaria a subir junto com as linhas.
|
|
2390
|
+
Refetch com a lista já na tela: esmaece e trava interação até chegar. */}
|
|
2391
|
+
<div
|
|
2392
|
+
aria-busy={busy}
|
|
2393
|
+
className={cn(
|
|
2394
|
+
"min-h-0",
|
|
2395
|
+
layout === "fill" && "overflow-auto",
|
|
2396
|
+
busy && items.length > 0 && "pointer-events-none opacity-60",
|
|
2397
|
+
)}
|
|
2398
|
+
>
|
|
2399
|
+
{body}
|
|
2400
|
+
</div>
|
|
2401
|
+
{footer}
|
|
2352
2402
|
</div>
|
|
2353
|
-
{footer}
|
|
2354
2403
|
{confirming !== null && (
|
|
2355
2404
|
<Dialog
|
|
2356
2405
|
mode="alert"
|