@softize/opus 29.0.0 → 31.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 +137 -0
- package/bin/lib/check.mjs +178 -94
- package/bin/lib/copy.mjs +2970 -1801
- package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +52 -0
- package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +27 -2
- package/docs/adr/0045-surface-replaces-content-and-card.md +3 -0
- package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +142 -0
- package/docs/adr/0062-listas-contidas-e-texto-flexivel.md +68 -0
- package/docs/adr/0063-metadados-das-superficies.md +44 -0
- package/docs/adr/0064-action-list-com-itens-e-fontes-externas.md +47 -0
- package/docs/adr/0065-presentation-composes-settings-sections.md +295 -0
- package/docs/adr/README.md +8 -0
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +21 -19
- package/registry/skills/build-opus-ui/references/evaluations.md +21 -10
- package/registry/skills/build-opus-ui/references/ui-patterns.md +33 -28
- package/src/core/presentation.ts +938 -32
- package/src/core/types.ts +2 -1
- package/src/ui/components/patterns/form.tsx +2 -0
- package/src/ui/components/patterns/grouped-list.ts +2 -2
- package/src/ui/components/patterns/list.tsx +588 -365
- package/src/ui/components/patterns/page-heading-context.tsx +12 -5
- package/src/ui/components/patterns/page.tsx +238 -181
- package/src/ui/components/patterns/presentation-fields.tsx +246 -0
- package/src/ui/components/patterns/presentation-sections.tsx +494 -0
- package/src/ui/components/patterns/presentation.tsx +1142 -229
- package/src/ui/components/patterns/structural-header.tsx +47 -34
- package/src/ui/components/patterns/surface-anatomy.ts +27 -217
- package/src/ui/components/patterns/surface.tsx +55 -37
- package/src/ui/components/patterns/trigger.tsx +10 -2
- package/src/ui/components/primitives/close-button.tsx +11 -3
- package/src/ui/components/primitives/command.tsx +2 -8
- package/src/ui/components/primitives/detail.tsx +1 -1
- package/src/ui/components/primitives/dialog.tsx +59 -89
- package/src/ui/components/primitives/drawer.tsx +52 -87
- package/src/ui/components/primitives/field.tsx +1 -1
- package/src/ui/components/primitives/pagination.tsx +3 -2
- package/src/ui/components/primitives/table.tsx +3 -3
- package/src/ui/components/primitives/tabs.tsx +13 -2
- package/src/ui/docs/content/action-form.md +2 -1
- package/src/ui/docs/content/action-list.md +146 -73
- package/src/ui/docs/content/action-trigger.md +5 -0
- package/src/ui/docs/content/button.md +5 -6
- package/src/ui/docs/content/command.md +62 -50
- package/src/ui/docs/content/compositions.md +129 -459
- package/src/ui/docs/content/dialog.md +72 -101
- package/src/ui/docs/content/drawer.md +69 -92
- package/src/ui/docs/content/field.md +2 -1
- package/src/ui/docs/content/page.md +207 -386
- package/src/ui/docs/content/presentation.md +371 -23
- package/src/ui/docs/content/separator.md +1 -1
- package/src/ui/docs/content/surface.md +94 -116
- package/src/ui/docs/content/table.md +7 -0
- package/src/ui/docs/content/tokens.md +18 -4
- package/src/ui/drivers/context.tsx +11 -4
- package/src/ui/drivers/react.tsx +27 -7
- package/src/ui/meta.ts +5 -5
- package/src/ui/react.tsx +38 -8
- package/src/ui/theme.css +44 -33
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
import { useViewAction, useListAction } from "../../drivers/react.tsx";
|
|
2
|
+
import { DataState } from "./data-state.tsx";
|
|
3
|
+
import { dictionaryDescriptor } from "../../../core/dictionary.ts";
|
|
4
|
+
import { objectSchemaShape } from "../../lib/object-schema.ts";
|
|
5
|
+
import {
|
|
6
|
+
canAccessPresentation,
|
|
7
|
+
meetsPresentationRequirement,
|
|
8
|
+
} from "../../../core/presentation.ts";
|
|
9
|
+
import {
|
|
10
|
+
PresentationSwitch,
|
|
11
|
+
presentationTextCell,
|
|
12
|
+
PresentationFormFields,
|
|
13
|
+
PresentationViewSections,
|
|
14
|
+
} from "./presentation-fields.tsx";
|
|
15
|
+
import { PageState } from "./page-state.tsx";
|
|
16
|
+
import {
|
|
17
|
+
Tabs,
|
|
18
|
+
TabsList,
|
|
19
|
+
TabsListItem,
|
|
20
|
+
TabsPanel,
|
|
21
|
+
} from "../primitives/tabs.tsx";
|
|
22
|
+
import {
|
|
23
|
+
PresentationSections,
|
|
24
|
+
type PresentationCompositionRuntime,
|
|
25
|
+
} from "./presentation-sections.tsx";
|
|
26
|
+
import { ArrowLeft, Braces, MessageCircle } from "lucide-react";
|
|
1
27
|
import {
|
|
2
28
|
useCallback,
|
|
3
29
|
useEffect,
|
|
@@ -6,14 +32,19 @@ import {
|
|
|
6
32
|
type ReactNode,
|
|
7
33
|
} from "react";
|
|
8
34
|
import { createPortal } from "react-dom";
|
|
9
|
-
import { ArrowLeft, Braces, MessageCircle } from "lucide-react";
|
|
10
35
|
import type {
|
|
11
|
-
|
|
36
|
+
FormContract,
|
|
37
|
+
ListContract,
|
|
38
|
+
SimpleContract,
|
|
39
|
+
ViewContract,
|
|
40
|
+
} from "../../../core/contracts.ts";
|
|
41
|
+
import type {
|
|
12
42
|
PresentationActionBody,
|
|
43
|
+
PresentationActionRegistry,
|
|
13
44
|
PresentationBindingContext,
|
|
14
45
|
PresentationCommand,
|
|
15
|
-
PresentationDiagnostic,
|
|
16
46
|
PresentationDefinition,
|
|
47
|
+
PresentationDiagnostic,
|
|
17
48
|
PresentationInvocation,
|
|
18
49
|
PresentationJsonValue,
|
|
19
50
|
PresentationSurface,
|
|
@@ -22,22 +53,25 @@ import {
|
|
|
22
53
|
applyPresentationEffects,
|
|
23
54
|
createPresentationInspectionSnapshot,
|
|
24
55
|
isPresentationActionBody,
|
|
56
|
+
isPresentationComponentBody,
|
|
57
|
+
isPresentationSectionsBody,
|
|
58
|
+
isPresentationTabsBody,
|
|
59
|
+
matchesPresentationConditions,
|
|
25
60
|
openPresentation,
|
|
26
61
|
resolvePresentationBindings,
|
|
27
62
|
} from "../../../core/presentation.ts";
|
|
28
|
-
import type {
|
|
29
|
-
FormContract,
|
|
30
|
-
ListContract,
|
|
31
|
-
SimpleContract,
|
|
32
|
-
ViewContract,
|
|
33
|
-
} from "../../../core/contracts.ts";
|
|
34
63
|
import { cn } from "../../lib/cn.ts";
|
|
64
|
+
import { ButtonGroup } from "../primitives/button-group.tsx";
|
|
35
65
|
import {
|
|
36
66
|
Button,
|
|
37
67
|
buttonVariants,
|
|
38
68
|
type ButtonSize,
|
|
39
69
|
} from "../primitives/button.tsx";
|
|
40
|
-
import {
|
|
70
|
+
import {
|
|
71
|
+
Tooltip,
|
|
72
|
+
TooltipTrigger,
|
|
73
|
+
TooltipContent,
|
|
74
|
+
} from "../primitives/tooltip.tsx";
|
|
41
75
|
import { Copyable } from "../primitives/copyable.tsx";
|
|
42
76
|
import { DetailField, DetailGroup } from "../primitives/detail.tsx";
|
|
43
77
|
import {
|
|
@@ -47,9 +81,10 @@ import {
|
|
|
47
81
|
DialogContent,
|
|
48
82
|
DialogFooter,
|
|
49
83
|
DialogHeader,
|
|
50
|
-
DialogIntro,
|
|
51
84
|
DialogNavigation,
|
|
52
85
|
DialogTitle,
|
|
86
|
+
DialogMeta,
|
|
87
|
+
DialogDescription,
|
|
53
88
|
} from "../primitives/dialog.tsx";
|
|
54
89
|
import {
|
|
55
90
|
Drawer,
|
|
@@ -58,39 +93,38 @@ import {
|
|
|
58
93
|
DrawerContent,
|
|
59
94
|
DrawerFooter,
|
|
60
95
|
DrawerHeader,
|
|
61
|
-
DrawerIntro,
|
|
62
96
|
DrawerNavigation,
|
|
63
97
|
DrawerTitle,
|
|
98
|
+
DrawerMeta,
|
|
99
|
+
DrawerDescription,
|
|
64
100
|
} from "../primitives/drawer.tsx";
|
|
101
|
+
import { fillRegionClassName, useFillRegion } from "./fill-region.ts";
|
|
102
|
+
import { ActionForm } from "./form.tsx";
|
|
103
|
+
import { ActionList, type ActionListState } from "./list.tsx";
|
|
65
104
|
import {
|
|
66
105
|
Page,
|
|
106
|
+
PageBack,
|
|
67
107
|
PageActions,
|
|
68
108
|
PageBody,
|
|
69
109
|
PageFooter,
|
|
70
110
|
PageHeader,
|
|
71
|
-
|
|
72
|
-
PageNavigation,
|
|
111
|
+
PageShellContribution,
|
|
73
112
|
PageTitle,
|
|
74
|
-
|
|
113
|
+
PageMeta,
|
|
114
|
+
PageDescription,
|
|
75
115
|
} from "./page.tsx";
|
|
76
|
-
import {
|
|
77
|
-
import { ActionList, type ActionListState } from "./list.tsx";
|
|
116
|
+
import { SurfaceAssistant } from "./surface-assistant.tsx";
|
|
78
117
|
import { ActionTrigger } from "./trigger.tsx";
|
|
79
118
|
import { ActionView } from "./view.tsx";
|
|
80
|
-
import {
|
|
81
|
-
Surface,
|
|
82
|
-
SurfaceActions,
|
|
83
|
-
SurfaceBody,
|
|
84
|
-
SurfaceHeader,
|
|
85
|
-
SurfaceTitle,
|
|
86
|
-
} from "./surface.tsx";
|
|
87
|
-
import { SurfaceAssistant } from "./surface-assistant.tsx";
|
|
88
|
-
import { fillRegionClassName, useFillRegion } from "./fill-region.ts";
|
|
89
119
|
|
|
90
120
|
interface PresentationFrameProps {
|
|
121
|
+
tabs?: ReactNode;
|
|
91
122
|
surface: PresentationSurface;
|
|
92
123
|
layout?: "flow" | "fill";
|
|
124
|
+
pageWidth?: "narrow" | "default" | "wide";
|
|
93
125
|
title: ReactNode;
|
|
126
|
+
description?: ReactNode;
|
|
127
|
+
meta?: ReactNode;
|
|
94
128
|
navigation?: ReactNode;
|
|
95
129
|
headerActions?: ReactNode;
|
|
96
130
|
footerActions?: ReactNode;
|
|
@@ -101,7 +135,7 @@ interface PresentationFrameProps {
|
|
|
101
135
|
onOpenChange?: (open: boolean) => void;
|
|
102
136
|
className?: string;
|
|
103
137
|
bodyClassName?: string;
|
|
104
|
-
|
|
138
|
+
bodyFlush?: boolean;
|
|
105
139
|
contentOwnsHeading?: boolean;
|
|
106
140
|
assistantOpen?: boolean;
|
|
107
141
|
assistantLabel?: string;
|
|
@@ -118,12 +152,14 @@ const bodyClassName =
|
|
|
118
152
|
*/
|
|
119
153
|
function PresentationBody({
|
|
120
154
|
page = false,
|
|
155
|
+
ownsPage = false,
|
|
121
156
|
layout,
|
|
122
157
|
className,
|
|
123
158
|
children,
|
|
124
159
|
}: {
|
|
125
160
|
layout: "flow" | "fill";
|
|
126
161
|
page?: boolean;
|
|
162
|
+
ownsPage?: boolean;
|
|
127
163
|
className?: string;
|
|
128
164
|
children: ReactNode;
|
|
129
165
|
}): ReactElement {
|
|
@@ -134,6 +170,8 @@ function PresentationBody({
|
|
|
134
170
|
{...fill.attributes}
|
|
135
171
|
className={cn(
|
|
136
172
|
!page && bodyClassName,
|
|
173
|
+
page && (layout === "fill" || ownsPage) && "h-full",
|
|
174
|
+
ownsPage && "flex min-h-0 min-w-0 flex-1 flex-col",
|
|
137
175
|
(layout === "fill" || fill.filling) && fillRegionClassName,
|
|
138
176
|
className,
|
|
139
177
|
)}
|
|
@@ -159,9 +197,13 @@ function PresentationActions({
|
|
|
159
197
|
|
|
160
198
|
/** A mesma Presentation em Page, Dialog ou Drawer; a superfície não redefine o recurso. */
|
|
161
199
|
function PresentationFrame({
|
|
200
|
+
tabs,
|
|
162
201
|
surface,
|
|
163
202
|
layout = "flow",
|
|
203
|
+
pageWidth,
|
|
164
204
|
title,
|
|
205
|
+
description,
|
|
206
|
+
meta,
|
|
165
207
|
navigation,
|
|
166
208
|
headerActions,
|
|
167
209
|
footerActions,
|
|
@@ -172,14 +214,14 @@ function PresentationFrame({
|
|
|
172
214
|
onOpenChange,
|
|
173
215
|
className,
|
|
174
216
|
bodyClassName: bodyClassNameProp,
|
|
175
|
-
|
|
217
|
+
bodyFlush = false,
|
|
218
|
+
|
|
176
219
|
contentOwnsHeading = false,
|
|
177
220
|
assistantOpen = false,
|
|
178
221
|
assistantLabel,
|
|
179
222
|
assistantAction,
|
|
180
223
|
assistantPanel,
|
|
181
224
|
}: PresentationFrameProps): ReactElement {
|
|
182
|
-
const insidePageShell = useInsidePageShell();
|
|
183
225
|
const [footerTarget, setFooterTarget] = useState<HTMLElement | null>(null);
|
|
184
226
|
const setFooterNode = useCallback((node: HTMLSpanElement | null) => {
|
|
185
227
|
setFooterTarget(node);
|
|
@@ -202,6 +244,7 @@ function PresentationFrame({
|
|
|
202
244
|
const body = (
|
|
203
245
|
<PresentationBody
|
|
204
246
|
page={surface === "page"}
|
|
247
|
+
ownsPage={surface === "page" && contentOwnsHeading}
|
|
205
248
|
layout={layout}
|
|
206
249
|
className={bodyClassNameProp}
|
|
207
250
|
>
|
|
@@ -224,99 +267,49 @@ function PresentationFrame({
|
|
|
224
267
|
);
|
|
225
268
|
|
|
226
269
|
if (surface === "page") {
|
|
227
|
-
if (
|
|
228
|
-
const content = (
|
|
229
|
-
<Surface as="section" level={1}>
|
|
230
|
-
<SurfaceHeader>
|
|
231
|
-
<SurfaceTitle>{title}</SurfaceTitle>
|
|
232
|
-
{headerActions === undefined ? null : (
|
|
233
|
-
<SurfaceActions>
|
|
234
|
-
<PresentationActions>{headerActions}</PresentationActions>
|
|
235
|
-
</SurfaceActions>
|
|
236
|
-
)}
|
|
237
|
-
</SurfaceHeader>
|
|
238
|
-
<SurfaceBody>{body}</SurfaceBody>
|
|
239
|
-
</Surface>
|
|
240
|
-
);
|
|
241
|
-
return (
|
|
242
|
-
<Page
|
|
243
|
-
layout={layout}
|
|
244
|
-
className={cn("flex min-h-full flex-col", className)}
|
|
245
|
-
>
|
|
246
|
-
{navigation === undefined && assistantAction === undefined ? null : (
|
|
247
|
-
<PageHeader>
|
|
248
|
-
{navigation === undefined ? null : (
|
|
249
|
-
<PageNavigation>{navigation}</PageNavigation>
|
|
250
|
-
)}
|
|
251
|
-
{assistantAction === undefined ? null : (
|
|
252
|
-
<PageActions>{assistantAction}</PageActions>
|
|
253
|
-
)}
|
|
254
|
-
</PageHeader>
|
|
255
|
-
)}
|
|
256
|
-
<PageBody>{withAssistant(content)}</PageBody>
|
|
257
|
-
{hasFooter ? <PageFooter>{renderFooterActions()}</PageFooter> : null}
|
|
258
|
-
</Page>
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
if (insidePageShell) {
|
|
270
|
+
if (contentOwnsHeading)
|
|
262
271
|
return (
|
|
263
|
-
<
|
|
264
|
-
|
|
265
|
-
|
|
272
|
+
<PageShellContribution
|
|
273
|
+
width={pageWidth}
|
|
274
|
+
navigation={navigation}
|
|
275
|
+
actions={
|
|
276
|
+
<>
|
|
277
|
+
{headerActions}
|
|
278
|
+
{assistantAction}
|
|
279
|
+
</>
|
|
280
|
+
}
|
|
281
|
+
footer={hasFooter ? renderFooterActions() : undefined}
|
|
266
282
|
>
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
{navigation === undefined ? null : (
|
|
273
|
-
<PageNavigation>{navigation}</PageNavigation>
|
|
274
|
-
)}
|
|
275
|
-
{headerActions === undefined &&
|
|
276
|
-
assistantAction === undefined ? null : (
|
|
277
|
-
<PageActions>
|
|
278
|
-
<PresentationActions>
|
|
279
|
-
{headerActions}
|
|
280
|
-
{assistantAction}
|
|
281
|
-
</PresentationActions>
|
|
282
|
-
</PageActions>
|
|
283
|
-
)}
|
|
284
|
-
</PageHeader>
|
|
285
|
-
)}
|
|
286
|
-
<PageBody>
|
|
287
|
-
{contentOwnsHeading ? null : (
|
|
288
|
-
<PageIntro>
|
|
289
|
-
<PageTitle>{title}</PageTitle>
|
|
290
|
-
</PageIntro>
|
|
283
|
+
<div
|
|
284
|
+
data-slot="presentation-page"
|
|
285
|
+
className={cn(
|
|
286
|
+
"flex h-full min-h-0 min-w-0 flex-1 flex-col",
|
|
287
|
+
className,
|
|
291
288
|
)}
|
|
289
|
+
>
|
|
292
290
|
{withAssistant(body)}
|
|
293
|
-
</
|
|
294
|
-
|
|
295
|
-
</Page>
|
|
291
|
+
</div>
|
|
292
|
+
</PageShellContribution>
|
|
296
293
|
);
|
|
297
|
-
}
|
|
298
294
|
return (
|
|
299
|
-
<Page layout={layout} className={className}
|
|
300
|
-
<PageHeader>
|
|
301
|
-
{navigation
|
|
302
|
-
|
|
295
|
+
<Page layout={layout} width={pageWidth} className={className}>
|
|
296
|
+
<PageHeader tabs={tabs}>
|
|
297
|
+
{navigation}
|
|
298
|
+
<PageTitle>{title}</PageTitle>
|
|
299
|
+
{description != null && (
|
|
300
|
+
<PageDescription>{description}</PageDescription>
|
|
303
301
|
)}
|
|
304
|
-
{
|
|
302
|
+
{meta != null && <PageMeta>{meta}</PageMeta>}
|
|
303
|
+
{navigation === undefined &&
|
|
304
|
+
headerActions === undefined &&
|
|
305
305
|
assistantAction === undefined ? null : (
|
|
306
306
|
<PageActions>
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
{assistantAction}
|
|
310
|
-
</PresentationActions>
|
|
307
|
+
{headerActions}
|
|
308
|
+
{assistantAction}
|
|
311
309
|
</PageActions>
|
|
312
310
|
)}
|
|
313
311
|
</PageHeader>
|
|
314
|
-
<PageBody>
|
|
315
|
-
<PageIntro>
|
|
316
|
-
<PageTitle>{title}</PageTitle>
|
|
317
|
-
</PageIntro>
|
|
318
|
-
{withAssistant(body)}
|
|
319
|
-
</PageBody>
|
|
312
|
+
<PageBody flush={bodyFlush}>{withAssistant(body)}</PageBody>
|
|
320
313
|
{hasFooter ? <PageFooter>{renderFooterActions()}</PageFooter> : null}
|
|
321
314
|
</Page>
|
|
322
315
|
);
|
|
@@ -335,7 +328,12 @@ function PresentationFrame({
|
|
|
335
328
|
closeDisabled={blocked}
|
|
336
329
|
>
|
|
337
330
|
<div className="flex h-full min-h-0 flex-col">
|
|
338
|
-
<DialogHeader>
|
|
331
|
+
<DialogHeader tabs={tabs}>
|
|
332
|
+
<DialogTitle>{title}</DialogTitle>
|
|
333
|
+
{description != null && (
|
|
334
|
+
<DialogDescription>{description}</DialogDescription>
|
|
335
|
+
)}
|
|
336
|
+
{meta != null && <DialogMeta>{meta}</DialogMeta>}
|
|
339
337
|
{navigation === undefined ? null : (
|
|
340
338
|
<DialogNavigation>{navigation}</DialogNavigation>
|
|
341
339
|
)}
|
|
@@ -349,14 +347,7 @@ function PresentationFrame({
|
|
|
349
347
|
</DialogActions>
|
|
350
348
|
)}
|
|
351
349
|
</DialogHeader>
|
|
352
|
-
{withAssistant(
|
|
353
|
-
<DialogBody>
|
|
354
|
-
<DialogIntro>
|
|
355
|
-
<DialogTitle>{title}</DialogTitle>
|
|
356
|
-
</DialogIntro>
|
|
357
|
-
{body}
|
|
358
|
-
</DialogBody>,
|
|
359
|
-
)}
|
|
350
|
+
{withAssistant(<DialogBody>{body}</DialogBody>)}
|
|
360
351
|
{hasFooter ? (
|
|
361
352
|
<DialogFooter>{renderFooterActions()}</DialogFooter>
|
|
362
353
|
) : null}
|
|
@@ -370,16 +361,21 @@ function PresentationFrame({
|
|
|
370
361
|
<Drawer open={open} onOpenChange={onOpenChange}>
|
|
371
362
|
<DrawerContent
|
|
372
363
|
className={cn(
|
|
364
|
+
className,
|
|
373
365
|
assistantOpen
|
|
374
366
|
? "w-[min(80rem,calc(100%-2rem))] max-w-none"
|
|
375
367
|
: undefined,
|
|
376
|
-
className,
|
|
377
368
|
)}
|
|
378
369
|
aria-describedby={undefined}
|
|
379
370
|
closeDisabled={blocked}
|
|
380
371
|
>
|
|
381
372
|
<div className="flex h-full min-h-0 flex-col">
|
|
382
|
-
<DrawerHeader>
|
|
373
|
+
<DrawerHeader tabs={tabs}>
|
|
374
|
+
<DrawerTitle>{title}</DrawerTitle>
|
|
375
|
+
{description != null && (
|
|
376
|
+
<DrawerDescription>{description}</DrawerDescription>
|
|
377
|
+
)}
|
|
378
|
+
{meta != null && <DrawerMeta>{meta}</DrawerMeta>}
|
|
383
379
|
{navigation === undefined ? null : (
|
|
384
380
|
<DrawerNavigation>{navigation}</DrawerNavigation>
|
|
385
381
|
)}
|
|
@@ -393,14 +389,7 @@ function PresentationFrame({
|
|
|
393
389
|
</DrawerActions>
|
|
394
390
|
)}
|
|
395
391
|
</DrawerHeader>
|
|
396
|
-
{withAssistant(
|
|
397
|
-
<DrawerBody>
|
|
398
|
-
<DrawerIntro>
|
|
399
|
-
<DrawerTitle>{title}</DrawerTitle>
|
|
400
|
-
</DrawerIntro>
|
|
401
|
-
{body}
|
|
402
|
-
</DrawerBody>,
|
|
403
|
-
)}
|
|
392
|
+
{withAssistant(<DrawerBody>{body}</DrawerBody>)}
|
|
404
393
|
{hasFooter ? (
|
|
405
394
|
<DrawerFooter>{renderFooterActions()}</DrawerFooter>
|
|
406
395
|
) : null}
|
|
@@ -412,10 +401,20 @@ function PresentationFrame({
|
|
|
412
401
|
|
|
413
402
|
export interface PresentationAssistantRuntime {
|
|
414
403
|
icon?: ReactNode;
|
|
415
|
-
render: (controls: {
|
|
404
|
+
render: (controls: {
|
|
405
|
+
close: () => void;
|
|
406
|
+
context: PresentationBindingContext;
|
|
407
|
+
}) => ReactNode;
|
|
408
|
+
trigger?: (controls: {
|
|
409
|
+
open: boolean;
|
|
410
|
+
onOpenChange: (open: boolean) => void;
|
|
411
|
+
context: PresentationBindingContext;
|
|
412
|
+
}) => ReactNode;
|
|
416
413
|
}
|
|
417
414
|
|
|
418
415
|
export interface PresentationProps {
|
|
416
|
+
embedded?: boolean;
|
|
417
|
+
can?: (permission: string) => boolean;
|
|
419
418
|
/** Definição estática registrada e publicada no manifest. */
|
|
420
419
|
definition: PresentationDefinition;
|
|
421
420
|
/** Definições alcançáveis por open, navigate ou back. */
|
|
@@ -439,6 +438,8 @@ export interface PresentationProps {
|
|
|
439
438
|
bodyClassName?: string;
|
|
440
439
|
/** `fill` limita a página ao shell e delega a rolagem ao conteúdo React. */
|
|
441
440
|
layout?: "flow" | "fill";
|
|
441
|
+
/** Extensões nomeadas da composição e da lista; não fazem parte do JSON. */
|
|
442
|
+
compositionRuntime?: PresentationCompositionRuntime;
|
|
442
443
|
/** Conteúdo React da boundary declarada por `body.component`. */
|
|
443
444
|
children?: ReactNode;
|
|
444
445
|
/** Painel runtime; a página fecha sobre os dados do recurso que ela carregou. */
|
|
@@ -465,6 +466,7 @@ function readField(record: unknown, key: string): unknown {
|
|
|
465
466
|
|
|
466
467
|
function PresentationCommandTrigger({
|
|
467
468
|
command,
|
|
469
|
+
icon,
|
|
468
470
|
action,
|
|
469
471
|
input,
|
|
470
472
|
size,
|
|
@@ -473,6 +475,7 @@ function PresentationCommandTrigger({
|
|
|
473
475
|
onSuccess,
|
|
474
476
|
}: {
|
|
475
477
|
command: PresentationCommand;
|
|
478
|
+
icon?: ReactNode;
|
|
476
479
|
action: SimpleContract<Record<string, unknown>, unknown>;
|
|
477
480
|
input: Record<string, unknown>;
|
|
478
481
|
size: ButtonSize;
|
|
@@ -488,17 +491,200 @@ function PresentationCommandTrigger({
|
|
|
488
491
|
<ActionTrigger
|
|
489
492
|
action={action}
|
|
490
493
|
input={input}
|
|
494
|
+
label={icon ? commandAccessibleLabel(command, action) : command.label}
|
|
495
|
+
icon={icon}
|
|
496
|
+
confirm={command.confirm}
|
|
497
|
+
aria-label={commandAccessibleLabel(command, action)}
|
|
491
498
|
variant="ghost"
|
|
492
|
-
size={size}
|
|
499
|
+
size={icon ? "icon" : size}
|
|
493
500
|
disabled={disabled}
|
|
494
501
|
onLoadingChange={reportLoading}
|
|
502
|
+
successMessage={command.successMessage}
|
|
495
503
|
onSuccess={onSuccess}
|
|
496
504
|
/>
|
|
497
505
|
);
|
|
498
506
|
}
|
|
499
507
|
|
|
508
|
+
/** Mantém o recurso no nome acessível e preserva o texto visível de uma ação contextual. */
|
|
509
|
+
function commandAccessibleLabel(
|
|
510
|
+
command: PresentationCommand,
|
|
511
|
+
action: Parameters<typeof actionLabel>[0],
|
|
512
|
+
): string | undefined {
|
|
513
|
+
if (command.accessibleLabel !== undefined) return command.accessibleLabel;
|
|
514
|
+
if (command.label === undefined) return undefined;
|
|
515
|
+
const fullLabel = actionLabel(action);
|
|
516
|
+
return fullLabel.includes(command.label)
|
|
517
|
+
? fullLabel
|
|
518
|
+
: `${command.label}: ${fullLabel}`;
|
|
519
|
+
}
|
|
520
|
+
|
|
500
521
|
/** Renderiza uma definição declarativa delegando cada kind ao pattern Opus correspondente. */
|
|
501
|
-
export function Presentation({
|
|
522
|
+
export function Presentation(props: PresentationProps): ReactElement {
|
|
523
|
+
if (props.compositionRuntime?.state != null)
|
|
524
|
+
return <PresentationContent {...props} />;
|
|
525
|
+
if (
|
|
526
|
+
props.definition.externalSource &&
|
|
527
|
+
canAccessPresentation(props.definition, props.actions, props.can)
|
|
528
|
+
)
|
|
529
|
+
return <PresentationExternalSource {...props} />;
|
|
530
|
+
if (
|
|
531
|
+
props.definition.source &&
|
|
532
|
+
canAccessPresentation(props.definition, props.actions, props.can)
|
|
533
|
+
)
|
|
534
|
+
return props.definition.source.record ? (
|
|
535
|
+
<PresentationListSource {...props} />
|
|
536
|
+
) : (
|
|
537
|
+
<PresentationSource {...props} />
|
|
538
|
+
);
|
|
539
|
+
return <PresentationContent {...props} />;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function PresentationExternalSource(props: PresentationProps): ReactElement {
|
|
543
|
+
const source =
|
|
544
|
+
props.compositionRuntime?.recordSources?.[props.definition.externalSource!];
|
|
545
|
+
if (!source) throw new Error("Fonte externa da apresentação não registrada.");
|
|
546
|
+
const [attempt, retry] = useState(0);
|
|
547
|
+
const [state, setState] = useState<{
|
|
548
|
+
key: string;
|
|
549
|
+
source: typeof source;
|
|
550
|
+
data?: Record<string, unknown>;
|
|
551
|
+
error?: Error;
|
|
552
|
+
}>();
|
|
553
|
+
const input = props.bindingContext?.route ?? props.invocation.input;
|
|
554
|
+
const key = JSON.stringify([props.definition.externalSource, input, attempt]);
|
|
555
|
+
useEffect(() => {
|
|
556
|
+
const controller = new AbortController();
|
|
557
|
+
void source
|
|
558
|
+
.load(JSON.parse(JSON.stringify(input)), { signal: controller.signal })
|
|
559
|
+
.then(
|
|
560
|
+
(data) => {
|
|
561
|
+
if (!controller.signal.aborted) setState({ key, source, data });
|
|
562
|
+
},
|
|
563
|
+
() => {
|
|
564
|
+
if (!controller.signal.aborted)
|
|
565
|
+
setState({
|
|
566
|
+
key,
|
|
567
|
+
source,
|
|
568
|
+
error: new Error("Não foi possível carregar os dados."),
|
|
569
|
+
});
|
|
570
|
+
},
|
|
571
|
+
);
|
|
572
|
+
return () => controller.abort();
|
|
573
|
+
}, [source, key]);
|
|
574
|
+
const current =
|
|
575
|
+
state?.key === key && state.source === source ? state : undefined;
|
|
576
|
+
return (
|
|
577
|
+
<PresentationContent
|
|
578
|
+
{...props}
|
|
579
|
+
bindingContext={{ ...props.bindingContext, record: current?.data }}
|
|
580
|
+
compositionRuntime={{
|
|
581
|
+
...props.compositionRuntime,
|
|
582
|
+
state: current?.data ? (
|
|
583
|
+
props.compositionRuntime?.state
|
|
584
|
+
) : (
|
|
585
|
+
<DataState
|
|
586
|
+
loading={!current}
|
|
587
|
+
error={current?.error}
|
|
588
|
+
errorMessage="Não foi possível carregar os dados."
|
|
589
|
+
onRetry={() => retry((value) => value + 1)}
|
|
590
|
+
>
|
|
591
|
+
{null}
|
|
592
|
+
</DataState>
|
|
593
|
+
),
|
|
594
|
+
}}
|
|
595
|
+
/>
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function PresentationListSource(props: PresentationProps): ReactElement {
|
|
600
|
+
const source = props.definition.source!;
|
|
601
|
+
const action = props.actions[source.action];
|
|
602
|
+
if (action?.kind !== "list")
|
|
603
|
+
throw new Error(`Seleção de registro exige action list: ${source.action}.`);
|
|
604
|
+
const route = props.bindingContext?.route ?? props.invocation.input;
|
|
605
|
+
const input = resolvePresentationBindings(source.input, {
|
|
606
|
+
...props.bindingContext,
|
|
607
|
+
route,
|
|
608
|
+
});
|
|
609
|
+
const result = useListAction<Record<string, PresentationJsonValue>>(
|
|
610
|
+
action,
|
|
611
|
+
input,
|
|
612
|
+
);
|
|
613
|
+
const record = result.items.find(
|
|
614
|
+
(item) => item[source.record!.field] === route[source.record!.param],
|
|
615
|
+
);
|
|
616
|
+
return (
|
|
617
|
+
<PresentationContent
|
|
618
|
+
{...props}
|
|
619
|
+
bindingContext={{ ...props.bindingContext, record }}
|
|
620
|
+
compositionRuntime={{
|
|
621
|
+
...props.compositionRuntime,
|
|
622
|
+
state:
|
|
623
|
+
(result.isLoading && !record) || result.error || !record ? (
|
|
624
|
+
<DataState
|
|
625
|
+
loading={result.isLoading}
|
|
626
|
+
error={result.error ?? null}
|
|
627
|
+
empty={!record}
|
|
628
|
+
emptyMessage="Item não encontrado neste recorte."
|
|
629
|
+
errorMessage="Não foi possível carregar o item."
|
|
630
|
+
onRetry={result.refetch}
|
|
631
|
+
>
|
|
632
|
+
{null}
|
|
633
|
+
</DataState>
|
|
634
|
+
) : (
|
|
635
|
+
props.compositionRuntime?.state
|
|
636
|
+
),
|
|
637
|
+
}}
|
|
638
|
+
/>
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function PresentationSource(props: PresentationProps): ReactElement {
|
|
643
|
+
const source = props.definition.source!;
|
|
644
|
+
const action = props.actions[source.action];
|
|
645
|
+
if (action?.kind !== "view")
|
|
646
|
+
throw new Error(`Fonte de registro exige action view: ${source.action}.`);
|
|
647
|
+
const input = resolvePresentationBindings(source.input, {
|
|
648
|
+
...props.bindingContext,
|
|
649
|
+
route: props.bindingContext?.route ?? props.invocation.input,
|
|
650
|
+
});
|
|
651
|
+
const result = useViewAction<
|
|
652
|
+
Record<string, unknown>,
|
|
653
|
+
Record<string, PresentationJsonValue>
|
|
654
|
+
>(action, input);
|
|
655
|
+
return (
|
|
656
|
+
<PresentationContent
|
|
657
|
+
{...props}
|
|
658
|
+
bindingContext={{ ...props.bindingContext, record: result.data }}
|
|
659
|
+
compositionRuntime={{
|
|
660
|
+
...props.compositionRuntime,
|
|
661
|
+
state:
|
|
662
|
+
(result.isLoading && !result.data) ||
|
|
663
|
+
(result.error && !source.retainOnError) ||
|
|
664
|
+
!result.data ? (
|
|
665
|
+
<DataState
|
|
666
|
+
loading={result.isLoading}
|
|
667
|
+
error={result.error ?? null}
|
|
668
|
+
empty={!result.data}
|
|
669
|
+
emptyMessage="Conteúdo indisponível."
|
|
670
|
+
errorMessage={
|
|
671
|
+
source.errorMessage ?? "Não foi possível carregar o conteúdo."
|
|
672
|
+
}
|
|
673
|
+
onRetry={result.refetch}
|
|
674
|
+
>
|
|
675
|
+
{null}
|
|
676
|
+
</DataState>
|
|
677
|
+
) : (
|
|
678
|
+
props.compositionRuntime?.state
|
|
679
|
+
),
|
|
680
|
+
}}
|
|
681
|
+
/>
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function PresentationContent({
|
|
686
|
+
embedded = false,
|
|
687
|
+
can,
|
|
502
688
|
definition,
|
|
503
689
|
definitions,
|
|
504
690
|
actions,
|
|
@@ -512,10 +698,53 @@ export function Presentation({
|
|
|
512
698
|
onOpenChange,
|
|
513
699
|
className,
|
|
514
700
|
bodyClassName: bodyClassNameProp,
|
|
515
|
-
layout
|
|
701
|
+
layout: layoutProp,
|
|
702
|
+
compositionRuntime,
|
|
516
703
|
children,
|
|
517
704
|
assistant,
|
|
518
705
|
}: PresentationProps): ReactElement {
|
|
706
|
+
const layout = layoutProp ?? definition.page?.layout ?? "flow";
|
|
707
|
+
const [asideOpen, setAsideOpen] = useState(true);
|
|
708
|
+
const boundSlots = Object.fromEntries(
|
|
709
|
+
Object.entries(compositionRuntime?.components ?? {}).map(
|
|
710
|
+
([id, adapter]) => [
|
|
711
|
+
id,
|
|
712
|
+
compositionRuntime?.state != null ||
|
|
713
|
+
!canAccessPresentation(definition, actions, can) ||
|
|
714
|
+
(adapter.action !== undefined &&
|
|
715
|
+
(!actions[adapter.action] ||
|
|
716
|
+
!meetsPresentationRequirement(
|
|
717
|
+
actions[adapter.action]?.requires,
|
|
718
|
+
can,
|
|
719
|
+
)))
|
|
720
|
+
? null
|
|
721
|
+
: adapter.render({
|
|
722
|
+
input: invocation.input,
|
|
723
|
+
context: {
|
|
724
|
+
...bindingContext,
|
|
725
|
+
route: bindingContext?.route ?? invocation.input,
|
|
726
|
+
},
|
|
727
|
+
onClose: () => onInvocationChange?.(null),
|
|
728
|
+
asideOpen,
|
|
729
|
+
onAsideOpenChange: setAsideOpen,
|
|
730
|
+
}),
|
|
731
|
+
],
|
|
732
|
+
),
|
|
733
|
+
);
|
|
734
|
+
compositionRuntime = {
|
|
735
|
+
...compositionRuntime,
|
|
736
|
+
asideOpen: compositionRuntime?.asideOpen ?? asideOpen,
|
|
737
|
+
slots: { ...compositionRuntime?.slots, ...boundSlots },
|
|
738
|
+
};
|
|
739
|
+
const headerSlot = (id: string | undefined): ReactNode => {
|
|
740
|
+
if (!canAccessPresentation(definition, actions, can)) return null;
|
|
741
|
+
if (id === undefined || compositionRuntime?.state != null) return undefined;
|
|
742
|
+
if (!Object.hasOwn(compositionRuntime?.slots ?? {}, id))
|
|
743
|
+
throw new Error(
|
|
744
|
+
`Slot “${id}” do cabeçalho da Presentation não registrado.`,
|
|
745
|
+
);
|
|
746
|
+
return compositionRuntime!.slots![id];
|
|
747
|
+
};
|
|
519
748
|
if (definition.id !== invocation.presentationId) {
|
|
520
749
|
throw new Error(
|
|
521
750
|
`A invocação “${invocation.presentationId}” não corresponde à Presentation “${definition.id}”.`,
|
|
@@ -525,6 +754,52 @@ export function Presentation({
|
|
|
525
754
|
ReadonlySet<PresentationCommand>
|
|
526
755
|
>(() => new Set());
|
|
527
756
|
const [bodyLoading, setBodyLoading] = useState(false);
|
|
757
|
+
const [inspected, setInspected] = useState<{
|
|
758
|
+
presentation: string;
|
|
759
|
+
item: Record<string, unknown>;
|
|
760
|
+
}>();
|
|
761
|
+
const [localTab, setLocalTab] = useState<{
|
|
762
|
+
presentation: string;
|
|
763
|
+
value: string;
|
|
764
|
+
}>();
|
|
765
|
+
const tabsBody = isPresentationTabsBody(definition.body)
|
|
766
|
+
? definition.body
|
|
767
|
+
: null;
|
|
768
|
+
const tabValue =
|
|
769
|
+
compositionRuntime?.tabs?.value ??
|
|
770
|
+
(localTab?.presentation === definition.id &&
|
|
771
|
+
tabsBody?.tabs.some((tab) => tab.id === localTab.value)
|
|
772
|
+
? localTab.value
|
|
773
|
+
: tabsBody?.defaultTab);
|
|
774
|
+
if (tabsBody !== null) {
|
|
775
|
+
if (!tabsBody.tabs.some((tab) => tab.id === tabValue))
|
|
776
|
+
throw new Error("A aba selecionada não existe na Presentation.");
|
|
777
|
+
for (const tab of tabsBody.tabs) {
|
|
778
|
+
if (
|
|
779
|
+
tab.presentation !== undefined &&
|
|
780
|
+
!Object.hasOwn(
|
|
781
|
+
compositionRuntime?.presentations ?? {},
|
|
782
|
+
tab.presentation,
|
|
783
|
+
)
|
|
784
|
+
)
|
|
785
|
+
throw new Error(
|
|
786
|
+
`Presentation embutida “${tab.presentation}” não registrada.`,
|
|
787
|
+
);
|
|
788
|
+
if (
|
|
789
|
+
tab.content !== undefined &&
|
|
790
|
+
!Object.hasOwn(compositionRuntime?.slots ?? {}, tab.content)
|
|
791
|
+
)
|
|
792
|
+
throw new Error(
|
|
793
|
+
`Slot “${tab.content}” da Presentation não registrado.`,
|
|
794
|
+
);
|
|
795
|
+
if (
|
|
796
|
+
tab.icon !== undefined &&
|
|
797
|
+
(!Object.hasOwn(compositionRuntime?.icons ?? {}, tab.icon) ||
|
|
798
|
+
compositionRuntime?.icons?.[tab.icon] == null)
|
|
799
|
+
)
|
|
800
|
+
throw new Error(`Ícone “${tab.icon}” da Presentation não registrado.`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
528
803
|
const [assistantOpen, setAssistantOpen] = useState(false);
|
|
529
804
|
useEffect(() => {
|
|
530
805
|
setAssistantOpen(false);
|
|
@@ -535,12 +810,16 @@ export function Presentation({
|
|
|
535
810
|
};
|
|
536
811
|
|
|
537
812
|
const applyEffects = useCallback(
|
|
538
|
-
(
|
|
813
|
+
(
|
|
814
|
+
effects: PresentationActionBody["onSuccess"],
|
|
815
|
+
result?: unknown,
|
|
816
|
+
effectContext: PresentationBindingContext = context,
|
|
817
|
+
): void => {
|
|
539
818
|
const next = applyPresentationEffects(
|
|
540
819
|
invocation,
|
|
541
820
|
effects,
|
|
542
821
|
{
|
|
543
|
-
...
|
|
822
|
+
...effectContext,
|
|
544
823
|
...(result !== undefined &&
|
|
545
824
|
result !== null &&
|
|
546
825
|
typeof result === "object"
|
|
@@ -622,12 +901,21 @@ export function Presentation({
|
|
|
622
901
|
string,
|
|
623
902
|
unknown
|
|
624
903
|
>;
|
|
904
|
+
const icon = command.icon
|
|
905
|
+
? compositionRuntime?.icons?.[command.icon]
|
|
906
|
+
: undefined;
|
|
907
|
+
if (command.icon && icon == null) {
|
|
908
|
+
throw new Error(
|
|
909
|
+
`Ícone “${command.icon}” da Presentation não registrado.`,
|
|
910
|
+
);
|
|
911
|
+
}
|
|
625
912
|
// A região chamadora escolhe a escala; o footer conserva o botão padrão.
|
|
626
|
-
if (action.kind === "simple") {
|
|
913
|
+
if (action.kind === "simple" && command.target === undefined) {
|
|
627
914
|
return (
|
|
628
915
|
<PresentationCommandTrigger
|
|
629
|
-
key={`${command.placement}:${command.action}`}
|
|
916
|
+
key={`${command.placement}:${command.id ?? command.action}`}
|
|
630
917
|
command={command}
|
|
918
|
+
icon={icon}
|
|
631
919
|
action={action as SimpleContract<Record<string, unknown>, unknown>}
|
|
632
920
|
input={input}
|
|
633
921
|
size={size}
|
|
@@ -642,11 +930,21 @@ export function Presentation({
|
|
|
642
930
|
`A action “${command.action}” precisa declarar uma Presentation de destino.`,
|
|
643
931
|
);
|
|
644
932
|
}
|
|
645
|
-
const label = actionLabel(action);
|
|
646
|
-
|
|
933
|
+
const label = command.label ?? actionLabel(action);
|
|
934
|
+
const accessibleLabel = commandAccessibleLabel(command, action) ?? label;
|
|
935
|
+
const button = (
|
|
647
936
|
<Button
|
|
648
|
-
key={`${command.placement}:${command.action}`}
|
|
649
|
-
size={size}
|
|
937
|
+
key={`${command.placement}:${command.id ?? command.action}`}
|
|
938
|
+
size={command.icon ? "icon" : size}
|
|
939
|
+
icon={icon}
|
|
940
|
+
variant={
|
|
941
|
+
command.icon
|
|
942
|
+
? "ghost"
|
|
943
|
+
: command.placement === "section"
|
|
944
|
+
? "ghost"
|
|
945
|
+
: "solid"
|
|
946
|
+
}
|
|
947
|
+
aria-label={accessibleLabel}
|
|
650
948
|
disabled={isBlocked(command)}
|
|
651
949
|
onClick={() =>
|
|
652
950
|
openTarget(
|
|
@@ -656,9 +954,17 @@ export function Presentation({
|
|
|
656
954
|
)
|
|
657
955
|
}
|
|
658
956
|
>
|
|
659
|
-
{label}
|
|
957
|
+
{command.icon ? null : label}
|
|
660
958
|
</Button>
|
|
661
959
|
);
|
|
960
|
+
return icon ? (
|
|
961
|
+
<Tooltip key={`${command.placement}:${command.id ?? command.action}`}>
|
|
962
|
+
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
|
963
|
+
<TooltipContent>{accessibleLabel}</TooltipContent>
|
|
964
|
+
</Tooltip>
|
|
965
|
+
) : (
|
|
966
|
+
button
|
|
967
|
+
);
|
|
662
968
|
};
|
|
663
969
|
|
|
664
970
|
const actionBody = isPresentationActionBody(definition.body)
|
|
@@ -668,14 +974,31 @@ export function Presentation({
|
|
|
668
974
|
if (actionBody !== null && bodyAction === undefined) {
|
|
669
975
|
throw new Error(`Action “${actionBody.action}” não registrada.`);
|
|
670
976
|
}
|
|
671
|
-
|
|
977
|
+
const componentBody = isPresentationComponentBody(definition.body)
|
|
978
|
+
? definition.body
|
|
979
|
+
: null;
|
|
980
|
+
const componentAdapter = componentBody
|
|
981
|
+
? compositionRuntime?.components?.[componentBody.component]
|
|
982
|
+
: undefined;
|
|
983
|
+
if (
|
|
984
|
+
componentBody?.contract &&
|
|
985
|
+
componentAdapter?.action !== componentBody.contract
|
|
986
|
+
)
|
|
987
|
+
throw new Error(
|
|
988
|
+
`Componente “${componentBody.component}” ausente ou incompatível com o contrato.`,
|
|
989
|
+
);
|
|
990
|
+
if (
|
|
991
|
+
isPresentationComponentBody(definition.body) &&
|
|
992
|
+
children === undefined &&
|
|
993
|
+
!componentAdapter
|
|
994
|
+
) {
|
|
672
995
|
throw new Error(
|
|
673
996
|
`A Presentation “${definition.id}” declara body.component e exige children.`,
|
|
674
997
|
);
|
|
675
998
|
}
|
|
676
|
-
if (
|
|
999
|
+
if (!isPresentationComponentBody(definition.body) && children !== undefined) {
|
|
677
1000
|
throw new Error(
|
|
678
|
-
`A Presentation “${definition.id}” usa body.
|
|
1001
|
+
`A Presentation “${definition.id}” não usa body.component e não aceita children.`,
|
|
679
1002
|
);
|
|
680
1003
|
}
|
|
681
1004
|
if (definition.assistant !== undefined && assistant === undefined) {
|
|
@@ -688,56 +1011,499 @@ export function Presentation({
|
|
|
688
1011
|
`A Presentation “${definition.id}” não declara assistência e não aceita a prop assistant.`,
|
|
689
1012
|
);
|
|
690
1013
|
}
|
|
1014
|
+
const listContentAdapter =
|
|
1015
|
+
actionBody?.beforeContent === undefined
|
|
1016
|
+
? undefined
|
|
1017
|
+
: compositionRuntime?.listContent?.[actionBody.beforeContent];
|
|
1018
|
+
if (
|
|
1019
|
+
actionBody?.beforeContent !== undefined &&
|
|
1020
|
+
(bodyAction?.kind !== "list" ||
|
|
1021
|
+
!Object.hasOwn(
|
|
1022
|
+
compositionRuntime?.listContent ?? {},
|
|
1023
|
+
actionBody.beforeContent,
|
|
1024
|
+
) ||
|
|
1025
|
+
listContentAdapter?.action !== actionBody.action)
|
|
1026
|
+
)
|
|
1027
|
+
throw new Error(
|
|
1028
|
+
`Conteúdo de lista “${actionBody.beforeContent}” ausente ou incompatível com “${actionBody.action}”.`,
|
|
1029
|
+
);
|
|
1030
|
+
const collectionAdapter = actionBody?.collection
|
|
1031
|
+
? compositionRuntime?.collections?.[actionBody.collection]
|
|
1032
|
+
: undefined;
|
|
1033
|
+
const rowContentAdapter =
|
|
1034
|
+
actionBody?.rowContent === undefined
|
|
1035
|
+
? undefined
|
|
1036
|
+
: compositionRuntime?.rowContent?.[actionBody.rowContent];
|
|
1037
|
+
const toolbarContentAdapter =
|
|
1038
|
+
actionBody?.toolbarContent === undefined
|
|
1039
|
+
? undefined
|
|
1040
|
+
: compositionRuntime?.listContent?.[actionBody.toolbarContent];
|
|
1041
|
+
for (const [id, adapter] of [
|
|
1042
|
+
[actionBody?.rowContent, rowContentAdapter],
|
|
1043
|
+
[actionBody?.collection, collectionAdapter],
|
|
1044
|
+
[actionBody?.toolbarContent, toolbarContentAdapter],
|
|
1045
|
+
] as const) {
|
|
1046
|
+
if (
|
|
1047
|
+
id !== undefined &&
|
|
1048
|
+
(bodyAction?.kind !== "list" || adapter?.action !== actionBody?.action)
|
|
1049
|
+
)
|
|
1050
|
+
throw new Error(
|
|
1051
|
+
`Conteúdo de lista “${id}” ausente ou incompatível com “${actionBody?.action}”.`,
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
const cellsAdapter =
|
|
1055
|
+
actionBody?.cells === undefined
|
|
1056
|
+
? undefined
|
|
1057
|
+
: compositionRuntime?.cells?.[actionBody.cells];
|
|
1058
|
+
if (
|
|
1059
|
+
actionBody?.cells !== undefined &&
|
|
1060
|
+
(!Object.hasOwn(compositionRuntime?.cells ?? {}, actionBody.cells) ||
|
|
1061
|
+
cellsAdapter?.action !== actionBody.action)
|
|
1062
|
+
) {
|
|
1063
|
+
throw new Error(
|
|
1064
|
+
`Adaptador de células “${actionBody.cells}” ausente ou incompatível com “${actionBody.action}”.`,
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
for (const [field, id] of Object.entries(actionBody?.fieldComponents ?? {})) {
|
|
1068
|
+
const adapter = compositionRuntime?.fields?.[id];
|
|
1069
|
+
if (
|
|
1070
|
+
!Object.hasOwn(compositionRuntime?.fields ?? {}, id) ||
|
|
1071
|
+
adapter?.action !== actionBody?.action ||
|
|
1072
|
+
adapter?.field !== field
|
|
1073
|
+
)
|
|
1074
|
+
throw new Error(
|
|
1075
|
+
`Componente de campo “${id}” ausente ou incompatível com “${actionBody?.action}.${field}”.`,
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
for (const lookup of Object.values(actionBody?.lookups ?? {})) {
|
|
1079
|
+
if (
|
|
1080
|
+
lookup.format !== undefined &&
|
|
1081
|
+
(!Object.hasOwn(compositionRuntime?.options ?? {}, lookup.format) ||
|
|
1082
|
+
compositionRuntime?.options?.[lookup.format]?.action !== lookup.action)
|
|
1083
|
+
)
|
|
1084
|
+
throw new Error(
|
|
1085
|
+
`Formatador de opções “${lookup.format}” ausente ou incompatível com “${lookup.action}”.`,
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
const inspection = actionBody?.inspect;
|
|
1089
|
+
const detailAdapter =
|
|
1090
|
+
inspection === undefined
|
|
1091
|
+
? undefined
|
|
1092
|
+
: compositionRuntime?.details?.[inspection.component];
|
|
1093
|
+
if (
|
|
1094
|
+
inspection &&
|
|
1095
|
+
(!Object.hasOwn(compositionRuntime?.details ?? {}, inspection.component) ||
|
|
1096
|
+
detailAdapter?.action !== actionBody?.action)
|
|
1097
|
+
)
|
|
1098
|
+
throw new Error(
|
|
1099
|
+
`Adaptador de detalhe “${inspection.component}” ausente ou incompatível com “${actionBody?.action}”.`,
|
|
1100
|
+
);
|
|
1101
|
+
if (
|
|
1102
|
+
inspection &&
|
|
1103
|
+
(actionBody?.open || actionBody?.opens || bodyAction?.kind !== "list")
|
|
1104
|
+
)
|
|
1105
|
+
throw new Error(
|
|
1106
|
+
"Inspect exige list e não pode coexistir com open ou opens.",
|
|
1107
|
+
);
|
|
1108
|
+
const canInspect = (item: Record<string, unknown>): boolean =>
|
|
1109
|
+
inspection !== undefined &&
|
|
1110
|
+
meetsPresentationRequirement(inspection.requires, can) &&
|
|
1111
|
+
matchesPresentationConditions(inspection.when, item);
|
|
1112
|
+
const canOpenPresentation = (id: string): boolean => {
|
|
1113
|
+
const target = definitions.find((candidate) => candidate.id === id);
|
|
1114
|
+
return target !== undefined && canAccessPresentation(target, actions, can);
|
|
1115
|
+
};
|
|
1116
|
+
const canUseCommand = (command: PresentationCommand): boolean => {
|
|
1117
|
+
const action = actions[command.action];
|
|
1118
|
+
return (
|
|
1119
|
+
action !== undefined &&
|
|
1120
|
+
meetsPresentationRequirement(command.requires, can) &&
|
|
1121
|
+
meetsPresentationRequirement(action.requires, can) &&
|
|
1122
|
+
(command.target === undefined ||
|
|
1123
|
+
canOpenPresentation(command.target.presentation))
|
|
1124
|
+
);
|
|
1125
|
+
};
|
|
1126
|
+
const rowCommands = definition.actions.filter(
|
|
1127
|
+
(command) => command.placement === "row",
|
|
1128
|
+
);
|
|
1129
|
+
for (const command of rowCommands) {
|
|
1130
|
+
if (bodyAction?.kind !== "list" || command.blocking !== "action")
|
|
1131
|
+
throw new Error("Comando de linha exige lista e bloqueio por ação.");
|
|
1132
|
+
if (
|
|
1133
|
+
command.icon !== undefined &&
|
|
1134
|
+
(!Object.hasOwn(compositionRuntime?.icons ?? {}, command.icon) ||
|
|
1135
|
+
compositionRuntime?.icons?.[command.icon] == null)
|
|
1136
|
+
) {
|
|
1137
|
+
throw new Error(
|
|
1138
|
+
`Ícone “${command.icon}” da Presentation não registrado.`,
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
const renderRowCommand = (
|
|
1143
|
+
command: PresentationCommand,
|
|
1144
|
+
item: Record<string, unknown>,
|
|
1145
|
+
): ReactNode => {
|
|
1146
|
+
if (
|
|
1147
|
+
!canUseCommand(command) ||
|
|
1148
|
+
!matchesPresentationConditions(command.when, item)
|
|
1149
|
+
)
|
|
1150
|
+
return null;
|
|
1151
|
+
const action = actions[command.action];
|
|
1152
|
+
if (action === undefined)
|
|
1153
|
+
throw new Error(`Action “${command.action}” não registrada.`);
|
|
1154
|
+
const rowContext = { ...context, item };
|
|
1155
|
+
const input = resolvePresentationBindings(command.input, rowContext);
|
|
1156
|
+
const label = command.label ?? actionLabel(action);
|
|
1157
|
+
const value =
|
|
1158
|
+
command.itemLabel === undefined ? undefined : item[command.itemLabel];
|
|
1159
|
+
// O nome acessível usa o vocabulário do contrato, inclusive quando o item guarda um código.
|
|
1160
|
+
const labelSchema =
|
|
1161
|
+
command.itemLabel === undefined
|
|
1162
|
+
? undefined
|
|
1163
|
+
: objectSchemaShape(bodyAction?.output)?.[command.itemLabel];
|
|
1164
|
+
const itemLabel =
|
|
1165
|
+
typeof value === "string" || typeof value === "number"
|
|
1166
|
+
? (dictionaryDescriptor(labelSchema)?.entries[String(value)]?.label ??
|
|
1167
|
+
String(value))
|
|
1168
|
+
: undefined;
|
|
1169
|
+
const accessibleLabel =
|
|
1170
|
+
itemLabel === undefined
|
|
1171
|
+
? (commandAccessibleLabel(command, action) ?? label)
|
|
1172
|
+
: `${label} ${itemLabel}`;
|
|
1173
|
+
const icon =
|
|
1174
|
+
command.icon === undefined
|
|
1175
|
+
? undefined
|
|
1176
|
+
: compositionRuntime?.icons?.[command.icon];
|
|
1177
|
+
if (action.kind === "simple" && command.target === undefined)
|
|
1178
|
+
return (
|
|
1179
|
+
<ActionTrigger
|
|
1180
|
+
key={command.id ?? command.action}
|
|
1181
|
+
action={action as SimpleContract<Record<string, unknown>, unknown>}
|
|
1182
|
+
input={input}
|
|
1183
|
+
label={label}
|
|
1184
|
+
aria-label={accessibleLabel}
|
|
1185
|
+
icon={icon}
|
|
1186
|
+
itemLabel={itemLabel}
|
|
1187
|
+
confirm={command.confirm}
|
|
1188
|
+
disabled={surfaceBlocked}
|
|
1189
|
+
successMessage={command.successMessage}
|
|
1190
|
+
onSuccess={(result) => {
|
|
1191
|
+
applyEffects(command.onSuccess, result, rowContext);
|
|
1192
|
+
}}
|
|
1193
|
+
/>
|
|
1194
|
+
);
|
|
1195
|
+
if (command.target === undefined)
|
|
1196
|
+
throw new Error(
|
|
1197
|
+
`A action “${command.action}” precisa declarar uma Presentation de destino.`,
|
|
1198
|
+
);
|
|
1199
|
+
const button = (
|
|
1200
|
+
<Button
|
|
1201
|
+
variant="ghost"
|
|
1202
|
+
size={icon === undefined ? "default" : "icon-xs"}
|
|
1203
|
+
aria-label={accessibleLabel}
|
|
1204
|
+
disabled={surfaceBlocked}
|
|
1205
|
+
className={
|
|
1206
|
+
icon === undefined ? undefined : "shrink-0 text-muted-foreground/60"
|
|
1207
|
+
}
|
|
1208
|
+
onClick={(event) => {
|
|
1209
|
+
event.stopPropagation();
|
|
1210
|
+
openTarget(
|
|
1211
|
+
command.target!.presentation,
|
|
1212
|
+
command.target!.surface,
|
|
1213
|
+
command.input,
|
|
1214
|
+
rowContext,
|
|
1215
|
+
);
|
|
1216
|
+
}}
|
|
1217
|
+
>
|
|
1218
|
+
{icon ?? label}
|
|
1219
|
+
</Button>
|
|
1220
|
+
);
|
|
1221
|
+
return icon === undefined ? (
|
|
1222
|
+
<span
|
|
1223
|
+
key={`${command.id ?? command.action}:${command.target?.presentation ?? "execute"}`}
|
|
1224
|
+
>
|
|
1225
|
+
{button}
|
|
1226
|
+
</span>
|
|
1227
|
+
) : (
|
|
1228
|
+
<Tooltip
|
|
1229
|
+
key={`${command.id ?? command.action}:${command.target?.presentation ?? "execute"}`}
|
|
1230
|
+
>
|
|
1231
|
+
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
|
1232
|
+
<TooltipContent>{label}</TooltipContent>
|
|
1233
|
+
</Tooltip>
|
|
1234
|
+
);
|
|
1235
|
+
};
|
|
691
1236
|
const bodyInput =
|
|
692
|
-
actionBody === null
|
|
1237
|
+
actionBody === null ||
|
|
1238
|
+
compositionRuntime?.state != null ||
|
|
1239
|
+
!canAccessPresentation(definition, actions, can)
|
|
693
1240
|
? {}
|
|
694
1241
|
: (resolvePresentationBindings(actionBody.input, context) as Record<
|
|
695
1242
|
string,
|
|
696
1243
|
unknown
|
|
697
1244
|
>);
|
|
1245
|
+
const renderLink = (
|
|
1246
|
+
link: PresentationDefinition["links"][number],
|
|
1247
|
+
): ReactNode => {
|
|
1248
|
+
const target = definitions.find((item) => item.id === link.presentation);
|
|
1249
|
+
if (!target?.route || !canAccessPresentation(target, actions, can))
|
|
1250
|
+
return null;
|
|
1251
|
+
const icon = link.icon ? compositionRuntime?.icons?.[link.icon] : undefined;
|
|
1252
|
+
if (link.icon && icon == null) {
|
|
1253
|
+
throw new Error(`Ícone “${link.icon}” da Presentation não registrado.`);
|
|
1254
|
+
}
|
|
1255
|
+
const iconOnly = icon != null && link.showLabel !== true;
|
|
1256
|
+
const button = (
|
|
1257
|
+
<Button
|
|
1258
|
+
key={link.id}
|
|
1259
|
+
variant={link.placement === "section" || icon ? "ghost" : "outline"}
|
|
1260
|
+
size={iconOnly ? "icon" : "default"}
|
|
1261
|
+
icon={icon}
|
|
1262
|
+
aria-label={link.label}
|
|
1263
|
+
disabled={surfaceBlocked}
|
|
1264
|
+
onClick={() =>
|
|
1265
|
+
onInvocationChange?.(
|
|
1266
|
+
openPresentation(invocation, {
|
|
1267
|
+
presentationId: target.id,
|
|
1268
|
+
surface: target.route!.surface,
|
|
1269
|
+
input: resolvePresentationBindings(link.input, context) as Record<
|
|
1270
|
+
string,
|
|
1271
|
+
PresentationJsonValue
|
|
1272
|
+
>,
|
|
1273
|
+
}),
|
|
1274
|
+
)
|
|
1275
|
+
}
|
|
1276
|
+
>
|
|
1277
|
+
{iconOnly ? null : link.label}
|
|
1278
|
+
</Button>
|
|
1279
|
+
);
|
|
1280
|
+
return iconOnly ? (
|
|
1281
|
+
<Tooltip key={link.id}>
|
|
1282
|
+
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
|
1283
|
+
<TooltipContent>{link.label}</TooltipContent>
|
|
1284
|
+
</Tooltip>
|
|
1285
|
+
) : (
|
|
1286
|
+
button
|
|
1287
|
+
);
|
|
1288
|
+
};
|
|
698
1289
|
const renderBody = (footerTarget: HTMLElement | null): ReactNode => {
|
|
699
|
-
if (
|
|
1290
|
+
if (!canAccessPresentation(definition, actions, can))
|
|
1291
|
+
return <PageState status="empty" title="Sem acesso a este conteúdo" />;
|
|
1292
|
+
if (compositionRuntime?.state != null) return compositionRuntime.state;
|
|
1293
|
+
if (tabsBody !== null)
|
|
1294
|
+
return tabsBody.tabs.map((tab) => (
|
|
1295
|
+
<TabsPanel key={tab.id} value={tab.id} className="min-w-0">
|
|
1296
|
+
{tab.presentation === undefined
|
|
1297
|
+
? compositionRuntime!.slots![tab.content!]
|
|
1298
|
+
: compositionRuntime!.presentations![tab.presentation]}
|
|
1299
|
+
</TabsPanel>
|
|
1300
|
+
));
|
|
1301
|
+
if (isPresentationSectionsBody(definition.body)) {
|
|
1302
|
+
return (
|
|
1303
|
+
<PresentationSections
|
|
1304
|
+
body={definition.body}
|
|
1305
|
+
can={can}
|
|
1306
|
+
context={context}
|
|
1307
|
+
runtime={{
|
|
1308
|
+
...compositionRuntime,
|
|
1309
|
+
slots: {
|
|
1310
|
+
...compositionRuntime?.slots,
|
|
1311
|
+
...Object.fromEntries(
|
|
1312
|
+
definition.links
|
|
1313
|
+
.filter((link) => link.placement === "section")
|
|
1314
|
+
.map((link) => [link.id, renderLink(link)]),
|
|
1315
|
+
),
|
|
1316
|
+
...Object.fromEntries(
|
|
1317
|
+
definition.actions
|
|
1318
|
+
.filter((command) => command.placement === "section")
|
|
1319
|
+
.map((command) => [
|
|
1320
|
+
command.id ?? command.action,
|
|
1321
|
+
canUseCommand(command) &&
|
|
1322
|
+
matchesPresentationConditions(
|
|
1323
|
+
command.when,
|
|
1324
|
+
context.record ?? {},
|
|
1325
|
+
)
|
|
1326
|
+
? renderCommand(command)
|
|
1327
|
+
: null,
|
|
1328
|
+
]),
|
|
1329
|
+
),
|
|
1330
|
+
},
|
|
1331
|
+
}}
|
|
1332
|
+
/>
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
if (actionBody === null)
|
|
1336
|
+
return componentAdapter && componentBody
|
|
1337
|
+
? componentAdapter.render({
|
|
1338
|
+
input: resolvePresentationBindings(
|
|
1339
|
+
componentBody.input ?? {},
|
|
1340
|
+
context,
|
|
1341
|
+
),
|
|
1342
|
+
context,
|
|
1343
|
+
onClose: closeSurface,
|
|
1344
|
+
footerTarget,
|
|
1345
|
+
onBusyChange: setBodyLoading,
|
|
1346
|
+
})
|
|
1347
|
+
: children;
|
|
700
1348
|
if (bodyAction === null) return null;
|
|
701
1349
|
if (bodyAction.kind === "form") {
|
|
702
1350
|
return (
|
|
703
1351
|
<ActionForm
|
|
1352
|
+
key={`${definition.id}:${JSON.stringify(invocation.input)}`}
|
|
704
1353
|
action={bodyAction as FormContract<Record<string, unknown>, unknown>}
|
|
705
|
-
defaultValues={
|
|
1354
|
+
defaultValues={
|
|
1355
|
+
actionBody.defaultsFromRecord
|
|
1356
|
+
? { ...actionBody.initialValues, ...context.record, ...bodyInput }
|
|
1357
|
+
: { ...actionBody.initialValues, ...bodyInput }
|
|
1358
|
+
}
|
|
706
1359
|
submitLabel={actionBody.submitLabel}
|
|
707
1360
|
onCancel={invocation.surface === "page" ? undefined : closeSurface}
|
|
708
1361
|
onSuccess={(data) => applyEffects(actionBody.onSuccess, data)}
|
|
709
1362
|
disabled={hasSurfaceBlock}
|
|
710
1363
|
onLoadingChange={setBodyLoading}
|
|
711
|
-
footer={
|
|
712
|
-
|
|
1364
|
+
footer={
|
|
1365
|
+
embedded && invocation.surface === "page"
|
|
1366
|
+
? undefined
|
|
1367
|
+
: (actions) =>
|
|
1368
|
+
footerTarget === null
|
|
1369
|
+
? null
|
|
1370
|
+
: createPortal(actions, footerTarget)
|
|
713
1371
|
}
|
|
714
|
-
|
|
1372
|
+
>
|
|
1373
|
+
{actionBody.lookups !== undefined ||
|
|
1374
|
+
actionBody.formFields !== undefined ||
|
|
1375
|
+
actionBody.formGroups !== undefined ||
|
|
1376
|
+
actionBody.fieldComponents !== undefined ? (
|
|
1377
|
+
<PresentationFormFields
|
|
1378
|
+
body={actionBody}
|
|
1379
|
+
action={bodyAction}
|
|
1380
|
+
actions={actions}
|
|
1381
|
+
runtime={compositionRuntime}
|
|
1382
|
+
context={context}
|
|
1383
|
+
can={can}
|
|
1384
|
+
/>
|
|
1385
|
+
) : undefined}
|
|
1386
|
+
</ActionForm>
|
|
715
1387
|
);
|
|
716
1388
|
}
|
|
717
1389
|
if (bodyAction.kind === "list") {
|
|
1390
|
+
const findTarget = (item: Record<string, unknown>) =>
|
|
1391
|
+
(actionBody.opens ?? (actionBody.open ? [actionBody.open] : [])).find(
|
|
1392
|
+
(open) =>
|
|
1393
|
+
meetsPresentationRequirement(open.requires, can) &&
|
|
1394
|
+
canOpenPresentation(open.presentation) &&
|
|
1395
|
+
matchesPresentationConditions(open.when, item),
|
|
1396
|
+
);
|
|
1397
|
+
const openItem = (item: Record<string, unknown>) => {
|
|
1398
|
+
if (inspection) {
|
|
1399
|
+
if (canInspect(item))
|
|
1400
|
+
setInspected({ presentation: definition.id, item });
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
const target = findTarget(item);
|
|
1404
|
+
if (target)
|
|
1405
|
+
openTarget(target.presentation, target.surface, target.input, {
|
|
1406
|
+
...context,
|
|
1407
|
+
item,
|
|
1408
|
+
});
|
|
1409
|
+
};
|
|
1410
|
+
const renderItemActions = (item: Record<string, unknown>) => (
|
|
1411
|
+
<>
|
|
1412
|
+
{rowContentAdapter?.render(item)}
|
|
1413
|
+
{rowCommands.map((command) => renderRowCommand(command, item))}
|
|
1414
|
+
</>
|
|
1415
|
+
);
|
|
718
1416
|
return (
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
1417
|
+
<>
|
|
1418
|
+
<ActionList<Record<string, unknown>, Record<string, unknown>>
|
|
1419
|
+
action={
|
|
1420
|
+
bodyAction as ListContract<
|
|
1421
|
+
Record<string, unknown>,
|
|
1422
|
+
Record<string, unknown>
|
|
1423
|
+
>
|
|
1424
|
+
}
|
|
1425
|
+
input={bodyInput}
|
|
1426
|
+
rowId={
|
|
1427
|
+
actionBody.rowKey
|
|
1428
|
+
? (item) => String(item[actionBody.rowKey!])
|
|
1429
|
+
: undefined
|
|
1430
|
+
}
|
|
1431
|
+
beforeContent={listContentAdapter?.render}
|
|
1432
|
+
cells={
|
|
1433
|
+
actionBody.columns === undefined
|
|
1434
|
+
? cellsAdapter?.cells
|
|
1435
|
+
: {
|
|
1436
|
+
...cellsAdapter?.cells,
|
|
1437
|
+
...Object.fromEntries(
|
|
1438
|
+
Object.entries(actionBody.columns).map(
|
|
1439
|
+
([field, column]) => [
|
|
1440
|
+
field,
|
|
1441
|
+
(item: Record<string, unknown>) =>
|
|
1442
|
+
column.kind === "switch" ? (
|
|
1443
|
+
<PresentationSwitch
|
|
1444
|
+
column={column}
|
|
1445
|
+
item={item}
|
|
1446
|
+
checked={item[field] === true}
|
|
1447
|
+
actions={actions}
|
|
1448
|
+
can={can}
|
|
1449
|
+
context={context}
|
|
1450
|
+
/>
|
|
1451
|
+
) : (
|
|
1452
|
+
presentationTextCell(
|
|
1453
|
+
item[field],
|
|
1454
|
+
column,
|
|
1455
|
+
compositionRuntime?.icons,
|
|
1456
|
+
)
|
|
1457
|
+
),
|
|
1458
|
+
],
|
|
1459
|
+
),
|
|
1460
|
+
),
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
emptyMessage={actionBody.emptyMessage}
|
|
1464
|
+
filterDefaults={actionBody.filterDefaults}
|
|
1465
|
+
rowActions={
|
|
1466
|
+
rowCommands.length === 0 && rowContentAdapter === undefined
|
|
1467
|
+
? undefined
|
|
1468
|
+
: renderItemActions
|
|
1469
|
+
}
|
|
1470
|
+
state={listState}
|
|
1471
|
+
onStateChange={onListStateChange}
|
|
1472
|
+
toolbarActions={
|
|
1473
|
+
toolbarActions.length === 0 &&
|
|
1474
|
+
toolbarContentAdapter === undefined ? undefined : (
|
|
1475
|
+
<>
|
|
1476
|
+
{toolbarContentAdapter?.render(bodyInput)}
|
|
1477
|
+
{toolbarActions}
|
|
1478
|
+
</>
|
|
1479
|
+
)
|
|
1480
|
+
}
|
|
1481
|
+
onRowClick={
|
|
1482
|
+
actionBody.open || actionBody.opens || inspection
|
|
1483
|
+
? openItem
|
|
1484
|
+
: undefined
|
|
1485
|
+
}
|
|
1486
|
+
children={
|
|
1487
|
+
collectionAdapter
|
|
1488
|
+
? (items, refetch) =>
|
|
1489
|
+
collectionAdapter.render(items, {
|
|
1490
|
+
onOpen: openItem,
|
|
1491
|
+
canOpen: (item) =>
|
|
1492
|
+
inspection
|
|
1493
|
+
? canInspect(item)
|
|
1494
|
+
: findTarget(item) !== undefined,
|
|
1495
|
+
actions: renderItemActions,
|
|
1496
|
+
refetch,
|
|
1497
|
+
})
|
|
1498
|
+
: undefined
|
|
1499
|
+
}
|
|
1500
|
+
/>
|
|
1501
|
+
{inspected?.presentation === definition.id &&
|
|
1502
|
+
canInspect(inspected.item) &&
|
|
1503
|
+
detailAdapter?.render(inspected.item, () =>
|
|
1504
|
+
setInspected(undefined),
|
|
1505
|
+
)}
|
|
1506
|
+
</>
|
|
741
1507
|
);
|
|
742
1508
|
}
|
|
743
1509
|
if (bodyAction.kind === "view") {
|
|
@@ -747,18 +1513,22 @@ export function Presentation({
|
|
|
747
1513
|
action={bodyAction as ViewContract<Record<string, unknown>, unknown>}
|
|
748
1514
|
input={bodyInput}
|
|
749
1515
|
>
|
|
750
|
-
{(data) =>
|
|
751
|
-
|
|
752
|
-
{fields
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1516
|
+
{(data) =>
|
|
1517
|
+
fields.some((field) => field.format !== undefined) ? (
|
|
1518
|
+
<PresentationViewSections fields={fields} data={data} />
|
|
1519
|
+
) : (
|
|
1520
|
+
<DetailGroup>
|
|
1521
|
+
{fields.map((field) => (
|
|
1522
|
+
<DetailField
|
|
1523
|
+
key={field.key}
|
|
1524
|
+
label={field.label}
|
|
1525
|
+
value={readField(data, field.key) as ReactNode}
|
|
1526
|
+
empty={field.empty}
|
|
1527
|
+
/>
|
|
1528
|
+
))}
|
|
1529
|
+
</DetailGroup>
|
|
1530
|
+
)
|
|
1531
|
+
}
|
|
762
1532
|
</ActionView>
|
|
763
1533
|
);
|
|
764
1534
|
}
|
|
@@ -768,31 +1538,80 @@ export function Presentation({
|
|
|
768
1538
|
};
|
|
769
1539
|
|
|
770
1540
|
const headerActions = definition.actions
|
|
771
|
-
.filter(
|
|
772
|
-
|
|
1541
|
+
.filter(
|
|
1542
|
+
(command) =>
|
|
1543
|
+
command.placement === "header" &&
|
|
1544
|
+
matchesPresentationConditions(command.when, context.record ?? {}) &&
|
|
1545
|
+
compositionRuntime?.state == null &&
|
|
1546
|
+
canAccessPresentation(definition, actions, can) &&
|
|
1547
|
+
canUseCommand(command),
|
|
1548
|
+
)
|
|
1549
|
+
.map((command) => renderCommand(command, "default"));
|
|
1550
|
+
const toolbarActions = definition.actions
|
|
1551
|
+
.filter(
|
|
1552
|
+
(command) =>
|
|
1553
|
+
command.placement === "toolbar" &&
|
|
1554
|
+
matchesPresentationConditions(command.when, context.record ?? {}) &&
|
|
1555
|
+
canAccessPresentation(definition, actions, can) &&
|
|
1556
|
+
canUseCommand(command),
|
|
1557
|
+
)
|
|
1558
|
+
.map((command) => renderCommand(command));
|
|
773
1559
|
const footerActions = definition.actions
|
|
774
|
-
.filter(
|
|
1560
|
+
.filter(
|
|
1561
|
+
(command) =>
|
|
1562
|
+
command.placement === "footer" &&
|
|
1563
|
+
matchesPresentationConditions(command.when, context.record ?? {}) &&
|
|
1564
|
+
canAccessPresentation(definition, actions, can) &&
|
|
1565
|
+
canUseCommand(command),
|
|
1566
|
+
)
|
|
775
1567
|
.map((command) => renderCommand(command));
|
|
776
1568
|
const parentSurface = invocation.stack.at(-1)?.surface;
|
|
777
1569
|
const canGoBack =
|
|
778
1570
|
parentSurface !== undefined &&
|
|
779
1571
|
(invocation.surface === "page" || parentSurface !== "page");
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1572
|
+
const parentTitle = definitions.find(
|
|
1573
|
+
(candidate) => candidate.id === invocation.stack.at(-1)?.presentationId,
|
|
1574
|
+
)?.title;
|
|
1575
|
+
const backLabel = parentTitle
|
|
1576
|
+
? `Voltar para ${parentTitle}`
|
|
1577
|
+
: "Voltar para a página anterior";
|
|
1578
|
+
const navigation = !canGoBack ? undefined : invocation.surface === "page" ? (
|
|
1579
|
+
<PageBack
|
|
784
1580
|
disabled={surfaceBlocked}
|
|
785
|
-
aria-label=
|
|
786
|
-
|
|
787
|
-
onClick={() => applyEffects([{ effect: "back" }])}
|
|
1581
|
+
aria-label={backLabel}
|
|
1582
|
+
onBack={() => applyEffects([{ effect: "back" }])}
|
|
788
1583
|
>
|
|
789
|
-
|
|
790
|
-
</
|
|
1584
|
+
{parentTitle ?? "a página anterior"}
|
|
1585
|
+
</PageBack>
|
|
1586
|
+
) : (
|
|
1587
|
+
<Tooltip>
|
|
1588
|
+
<TooltipTrigger asChild>
|
|
1589
|
+
<Button
|
|
1590
|
+
size="icon"
|
|
1591
|
+
variant="ghost"
|
|
1592
|
+
disabled={surfaceBlocked}
|
|
1593
|
+
aria-label={backLabel}
|
|
1594
|
+
onClick={() => applyEffects([{ effect: "back" }])}
|
|
1595
|
+
>
|
|
1596
|
+
<ArrowLeft aria-hidden />
|
|
1597
|
+
</Button>
|
|
1598
|
+
</TooltipTrigger>
|
|
1599
|
+
<TooltipContent>{backLabel}</TooltipContent>
|
|
1600
|
+
</Tooltip>
|
|
791
1601
|
);
|
|
792
1602
|
const assistantAction =
|
|
793
|
-
definition
|
|
1603
|
+
!canAccessPresentation(definition, actions, can) ||
|
|
1604
|
+
definition.assistant === undefined ||
|
|
1605
|
+
compositionRuntime?.state != null ||
|
|
1606
|
+
assistantOpen ? undefined : assistant?.trigger ? (
|
|
1607
|
+
assistant.trigger({
|
|
1608
|
+
open: assistantOpen,
|
|
1609
|
+
onOpenChange: setAssistantOpen,
|
|
1610
|
+
context,
|
|
1611
|
+
})
|
|
1612
|
+
) : (
|
|
794
1613
|
<Button
|
|
795
|
-
size="icon
|
|
1614
|
+
size="icon"
|
|
796
1615
|
variant="ghost"
|
|
797
1616
|
aria-label={definition.assistant.triggerLabel}
|
|
798
1617
|
title={definition.assistant.triggerLabel}
|
|
@@ -802,42 +1621,136 @@ export function Presentation({
|
|
|
802
1621
|
</Button>
|
|
803
1622
|
);
|
|
804
1623
|
const assistantPanel =
|
|
805
|
-
definition
|
|
1624
|
+
!canAccessPresentation(definition, actions, can) ||
|
|
1625
|
+
definition.assistant === undefined ||
|
|
1626
|
+
assistant === undefined ||
|
|
1627
|
+
compositionRuntime?.state != null
|
|
806
1628
|
? undefined
|
|
807
|
-
: assistant.render({ close: () => setAssistantOpen(false) });
|
|
1629
|
+
: assistant.render({ close: () => setAssistantOpen(false), context });
|
|
808
1630
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1631
|
+
const renderContent = (footerTarget: HTMLElement | null) => (
|
|
1632
|
+
<>
|
|
1633
|
+
{actionBody?.intro && (
|
|
1634
|
+
<p className="mb-4 text-sm text-muted-foreground">{actionBody.intro}</p>
|
|
1635
|
+
)}
|
|
1636
|
+
{renderBody(footerTarget)}
|
|
1637
|
+
</>
|
|
1638
|
+
);
|
|
1639
|
+
const tabList =
|
|
1640
|
+
tabsBody === null ? undefined : (
|
|
1641
|
+
<TabsList aria-label={tabsBody.tabLabel}>
|
|
1642
|
+
{tabsBody.tabs.map((tab) => (
|
|
1643
|
+
<TabsListItem
|
|
1644
|
+
key={tab.id}
|
|
1645
|
+
value={tab.id}
|
|
1646
|
+
icon={
|
|
1647
|
+
tab.icon === undefined
|
|
1648
|
+
? undefined
|
|
1649
|
+
: compositionRuntime!.icons![tab.icon]
|
|
1650
|
+
}
|
|
1651
|
+
>
|
|
1652
|
+
{tab.label}
|
|
1653
|
+
</TabsListItem>
|
|
1654
|
+
))}
|
|
1655
|
+
</TabsList>
|
|
1656
|
+
);
|
|
1657
|
+
const renderedHeaderActions =
|
|
1658
|
+
definition.header?.actions === undefined &&
|
|
1659
|
+
headerActions.length === 0 &&
|
|
1660
|
+
!definition.links.some(
|
|
1661
|
+
(link) => link.placement === "header",
|
|
1662
|
+
) ? undefined : (
|
|
1663
|
+
<>
|
|
1664
|
+
{headerSlot(definition.header?.actions)}
|
|
1665
|
+
{headerActions}
|
|
1666
|
+
{definition.links
|
|
1667
|
+
.filter((link) => link.placement === "header")
|
|
1668
|
+
.map(renderLink)}
|
|
1669
|
+
</>
|
|
1670
|
+
);
|
|
1671
|
+
const frame =
|
|
1672
|
+
embedded && invocation.surface === "page" ? (
|
|
1673
|
+
<>
|
|
1674
|
+
{renderedHeaderActions !== undefined && (
|
|
1675
|
+
<ButtonGroup mode="spaced" className="justify-end">
|
|
1676
|
+
{renderedHeaderActions}
|
|
1677
|
+
</ButtonGroup>
|
|
1678
|
+
)}
|
|
1679
|
+
{tabList}
|
|
1680
|
+
{renderContent(null)}
|
|
1681
|
+
{footerActions.length > 0 && (
|
|
1682
|
+
<ButtonGroup mode="spaced" className="justify-end">
|
|
1683
|
+
{footerActions}
|
|
1684
|
+
</ButtonGroup>
|
|
1685
|
+
)}
|
|
1686
|
+
</>
|
|
1687
|
+
) : (
|
|
1688
|
+
<PresentationFrame
|
|
1689
|
+
tabs={tabList}
|
|
1690
|
+
surface={invocation.surface}
|
|
1691
|
+
layout={layout}
|
|
1692
|
+
pageWidth={definition.page?.width}
|
|
1693
|
+
title={headerSlot(definition.header?.title) ?? definition.title}
|
|
1694
|
+
meta={headerSlot(definition.header?.meta)}
|
|
1695
|
+
description={headerSlot(definition.header?.description)}
|
|
1696
|
+
navigation={navigation}
|
|
1697
|
+
headerActions={renderedHeaderActions}
|
|
1698
|
+
footerActions={footerActions.length === 0 ? undefined : footerActions}
|
|
1699
|
+
open={open}
|
|
1700
|
+
onOpenChange={(nextOpen) => {
|
|
1701
|
+
if (!nextOpen && invocation.surface !== "page") {
|
|
1702
|
+
closeSurface();
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
onOpenChange?.(nextOpen);
|
|
1706
|
+
}}
|
|
1707
|
+
className={cn(
|
|
1708
|
+
definition.width === "wide" &&
|
|
1709
|
+
(invocation.surface === "dialog"
|
|
1710
|
+
? "max-w-4xl"
|
|
1711
|
+
: invocation.surface === "drawer"
|
|
1712
|
+
? "w-[56rem] max-w-[calc(100%-2rem)]"
|
|
1713
|
+
: undefined),
|
|
1714
|
+
className,
|
|
1715
|
+
)}
|
|
1716
|
+
bodyClassName={bodyClassNameProp}
|
|
1717
|
+
bodyFlush={definition.page?.flush}
|
|
1718
|
+
hasBodyFooter={
|
|
1719
|
+
bodyAction?.kind === "form" || componentBody?.footer === true
|
|
822
1720
|
}
|
|
823
|
-
|
|
1721
|
+
blocked={surfaceBlocked}
|
|
1722
|
+
contentOwnsHeading={
|
|
1723
|
+
isPresentationComponentBody(definition.body) &&
|
|
1724
|
+
definition.body.heading === "content"
|
|
1725
|
+
}
|
|
1726
|
+
assistantOpen={assistantOpen}
|
|
1727
|
+
assistantLabel={definition.assistant?.triggerLabel}
|
|
1728
|
+
assistantAction={assistantAction}
|
|
1729
|
+
assistantPanel={assistantPanel}
|
|
1730
|
+
>
|
|
1731
|
+
{renderContent}
|
|
1732
|
+
</PresentationFrame>
|
|
1733
|
+
);
|
|
1734
|
+
return tabsBody === null ? (
|
|
1735
|
+
frame
|
|
1736
|
+
) : (
|
|
1737
|
+
<Tabs
|
|
1738
|
+
framed={tabsBody.framed}
|
|
1739
|
+
distribution={tabsBody.distribution}
|
|
1740
|
+
value={tabValue}
|
|
1741
|
+
onValueChange={(value) => {
|
|
1742
|
+
if (compositionRuntime?.tabs !== undefined)
|
|
1743
|
+
compositionRuntime.tabs.onValueChange(value);
|
|
1744
|
+
else setLocalTab({ presentation: definition.id, value });
|
|
824
1745
|
}}
|
|
825
|
-
className={
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
listBody={bodyAction?.kind === "list"}
|
|
830
|
-
contentOwnsHeading={
|
|
831
|
-
!isPresentationActionBody(definition.body) &&
|
|
832
|
-
definition.body.heading === "content"
|
|
1746
|
+
className={
|
|
1747
|
+
invocation.surface === "dialog" || invocation.surface === "drawer"
|
|
1748
|
+
? "contents"
|
|
1749
|
+
: "h-full min-h-0 min-w-0 flex-1"
|
|
833
1750
|
}
|
|
834
|
-
assistantOpen={assistantOpen}
|
|
835
|
-
assistantLabel={definition.assistant?.triggerLabel}
|
|
836
|
-
assistantAction={assistantAction}
|
|
837
|
-
assistantPanel={assistantPanel}
|
|
838
1751
|
>
|
|
839
|
-
{
|
|
840
|
-
</
|
|
1752
|
+
{frame}
|
|
1753
|
+
</Tabs>
|
|
841
1754
|
);
|
|
842
1755
|
}
|
|
843
1756
|
|