@softize/opus 18.1.1 → 18.2.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 +17 -0
- package/package.json +1 -1
- package/src/core/presentation.ts +143 -58
- package/src/ui/components/patterns/presentation.tsx +199 -60
- package/src/ui/components/patterns/split.tsx +5 -2
- package/src/ui/components/patterns/surface-assistant.tsx +74 -0
- package/src/ui/docs/content/presentation.md +40 -14
- package/src/ui/docs/content/split.md +80 -19
- package/src/ui/meta.ts +1 -1
- package/src/ui/react.tsx +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
|
|
|
7
7
|
`opus copy --check` · `base copy check` · `manifest:check`) — eles apontam o que a
|
|
8
8
|
mudança cobra do seu código.
|
|
9
9
|
|
|
10
|
+
## 18.2.0 — 2026-09-16
|
|
11
|
+
|
|
12
|
+
`Presentation` passa a aceitar um body serializável baseado em componente. O contrato continua
|
|
13
|
+
definindo rota, superfície, navegação e ações, enquanto a aplicação fornece o conteúdo React em
|
|
14
|
+
tempo de execução. Assim, uma página pode adotar a casca dirigida por spec sem precisar migrar de
|
|
15
|
+
uma vez toda a implementação interna para actions declarativas.
|
|
16
|
+
|
|
17
|
+
Recursos podem declarar um gatilho de assistência contextual em `assistant`. A aplicação fornece o
|
|
18
|
+
painel e os dados vivos do recurso por `assistant.render`; identidade, autorização e demais dados
|
|
19
|
+
não entram no manifest. O novo `SurfaceAssistant`, também disponível pela API pública, divide
|
|
20
|
+
somente o body de Page, Dialog ou Drawer. Cabeçalho e rodapé permanecem em toda a largura, e o
|
|
21
|
+
gatilho sai do cabeçalho enquanto o painel aberto assume a identidade visual da assistência.
|
|
22
|
+
|
|
23
|
+
O split contextual usa um único separador redimensionável, sem sobrepor uma segunda borda entre o
|
|
24
|
+
conteúdo e o painel. O conteúdo principal permanece montado durante a conversa e volta a ocupar
|
|
25
|
+
toda a largura quando ela é fechada.
|
|
26
|
+
|
|
10
27
|
## 18.1.1 — 2026-09-16
|
|
11
28
|
|
|
12
29
|
**Correção de segurança.** Quem executa tools de IA só roda o que `aiTools()` anunciou. Até aqui, o
|
package/package.json
CHANGED
package/src/core/presentation.ts
CHANGED
|
@@ -90,7 +90,7 @@ const presentationFieldSchema = z
|
|
|
90
90
|
})
|
|
91
91
|
.strict();
|
|
92
92
|
|
|
93
|
-
const
|
|
93
|
+
const presentationActionBodySchema = z
|
|
94
94
|
.object({
|
|
95
95
|
action: z.string().min(1),
|
|
96
96
|
input: z.record(presentationBindingSchema).default({}),
|
|
@@ -108,6 +108,27 @@ const presentationBodySchema = z
|
|
|
108
108
|
})
|
|
109
109
|
.strict();
|
|
110
110
|
|
|
111
|
+
const presentationComponentBodySchema = z
|
|
112
|
+
.object({
|
|
113
|
+
/** Boundary estático do conteúdo React fornecido pelo consumidor. */
|
|
114
|
+
component: identifierSchema,
|
|
115
|
+
/** Define se o heading principal nasce na casca ou no conteúdo React. */
|
|
116
|
+
heading: z.enum(["shell", "content"]).default("shell"),
|
|
117
|
+
})
|
|
118
|
+
.strict();
|
|
119
|
+
|
|
120
|
+
const presentationBodySchema = z.union([
|
|
121
|
+
presentationActionBodySchema,
|
|
122
|
+
presentationComponentBodySchema,
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
const presentationAssistantSchema = z
|
|
126
|
+
.object({
|
|
127
|
+
/** Nome acessível do botão que abre a assistência contextual. */
|
|
128
|
+
triggerLabel: z.string().min(1),
|
|
129
|
+
})
|
|
130
|
+
.strict();
|
|
131
|
+
|
|
111
132
|
const presentationRouteRecordSchema = z
|
|
112
133
|
.object({
|
|
113
134
|
presentation: identifierSchema,
|
|
@@ -136,6 +157,7 @@ export const presentationSchema = z
|
|
|
136
157
|
title: z.string().min(1),
|
|
137
158
|
route: presentationRouteSchema.optional(),
|
|
138
159
|
body: presentationBodySchema,
|
|
160
|
+
assistant: presentationAssistantSchema.optional(),
|
|
139
161
|
actions: z.array(presentationCommandSchema).default([]),
|
|
140
162
|
})
|
|
141
163
|
.strict();
|
|
@@ -143,6 +165,12 @@ export const presentationSchema = z
|
|
|
143
165
|
export type PresentationDefinition = z.infer<typeof presentationSchema>;
|
|
144
166
|
export type PresentationDefinitionInput = z.input<typeof presentationSchema>;
|
|
145
167
|
export type PresentationCommand = PresentationDefinition["actions"][number];
|
|
168
|
+
export type PresentationActionBody = z.infer<
|
|
169
|
+
typeof presentationActionBodySchema
|
|
170
|
+
>;
|
|
171
|
+
export type PresentationComponentBody = z.infer<
|
|
172
|
+
typeof presentationComponentBodySchema
|
|
173
|
+
>;
|
|
146
174
|
type DefaultedProperty<
|
|
147
175
|
Input,
|
|
148
176
|
Key extends PropertyKey,
|
|
@@ -174,27 +202,48 @@ type DefinedPresentationEffects<
|
|
|
174
202
|
}
|
|
175
203
|
: PresentationCommand["onSuccess"]
|
|
176
204
|
: PresentationCommand["onSuccess"];
|
|
177
|
-
type PresentationOpen = NonNullable<
|
|
205
|
+
type PresentationOpen = NonNullable<PresentationActionBody["open"]>;
|
|
178
206
|
type DefinedPresentationOpen<Open> = Open extends object
|
|
179
207
|
? Omit<Open, "input"> &
|
|
180
208
|
Omit<PresentationOpen, "input"> & {
|
|
181
209
|
input: DefaultedProperty<Open, "input", PresentationOpen["input"]>;
|
|
182
210
|
}
|
|
183
211
|
: never;
|
|
212
|
+
type DefinedPresentationActionBody<Body> =
|
|
213
|
+
Body extends z.input<typeof presentationActionBodySchema>
|
|
214
|
+
? Omit<Body, "input" | "open" | "onSuccess"> &
|
|
215
|
+
Omit<PresentationActionBody, "input" | "open" | "onSuccess"> & {
|
|
216
|
+
input: DefaultedProperty<
|
|
217
|
+
Body,
|
|
218
|
+
"input",
|
|
219
|
+
PresentationActionBody["input"]
|
|
220
|
+
>;
|
|
221
|
+
onSuccess: DefinedPresentationEffects<Body, "onSuccess">;
|
|
222
|
+
} & ("open" extends keyof Body
|
|
223
|
+
? undefined extends Body["open"]
|
|
224
|
+
? {
|
|
225
|
+
open?: DefinedPresentationOpen<
|
|
226
|
+
Exclude<Body["open"], undefined>
|
|
227
|
+
>;
|
|
228
|
+
}
|
|
229
|
+
: { open: DefinedPresentationOpen<Body["open"]> }
|
|
230
|
+
: { open?: PresentationActionBody["open"] })
|
|
231
|
+
: never;
|
|
232
|
+
type DefinedPresentationComponentBody<Body> =
|
|
233
|
+
Body extends z.input<typeof presentationComponentBodySchema>
|
|
234
|
+
? Omit<Body, "heading"> &
|
|
235
|
+
Omit<PresentationComponentBody, "heading"> & {
|
|
236
|
+
heading: DefaultedProperty<
|
|
237
|
+
Body,
|
|
238
|
+
"heading",
|
|
239
|
+
PresentationComponentBody["heading"]
|
|
240
|
+
>;
|
|
241
|
+
}
|
|
242
|
+
: never;
|
|
184
243
|
type DefinedPresentationBody<Body extends PresentationDefinitionInput["body"]> =
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
Body,
|
|
189
|
-
"input",
|
|
190
|
-
PresentationDefinition["body"]["input"]
|
|
191
|
-
>;
|
|
192
|
-
onSuccess: DefinedPresentationEffects<Body, "onSuccess">;
|
|
193
|
-
} & ("open" extends keyof Body
|
|
194
|
-
? undefined extends Body["open"]
|
|
195
|
-
? { open?: DefinedPresentationOpen<Exclude<Body["open"], undefined>> }
|
|
196
|
-
: { open: DefinedPresentationOpen<Body["open"]> }
|
|
197
|
-
: { open?: PresentationDefinition["body"]["open"] });
|
|
244
|
+
Body extends { component: string }
|
|
245
|
+
? DefinedPresentationComponentBody<Body>
|
|
246
|
+
: DefinedPresentationActionBody<Body>;
|
|
198
247
|
type DefinedPresentationCommand<Command> = Command extends object
|
|
199
248
|
? Omit<Command, "input" | "blocking" | "onSuccess"> &
|
|
200
249
|
Omit<PresentationCommand, "input" | "blocking" | "onSuccess"> & {
|
|
@@ -272,6 +321,18 @@ export interface PresentationActionRegistry {
|
|
|
272
321
|
readonly [name: string]: ActionContract;
|
|
273
322
|
}
|
|
274
323
|
|
|
324
|
+
export function isPresentationActionBody(
|
|
325
|
+
body: PresentationDefinition["body"],
|
|
326
|
+
): body is PresentationActionBody {
|
|
327
|
+
return "action" in body;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function isPresentationComponentBody(
|
|
331
|
+
body: PresentationDefinition["body"],
|
|
332
|
+
): body is PresentationComponentBody {
|
|
333
|
+
return "component" in body;
|
|
334
|
+
}
|
|
335
|
+
|
|
275
336
|
export interface PresentationRouteMatch {
|
|
276
337
|
definition: PresentationDefinition;
|
|
277
338
|
input: Record<string, string>;
|
|
@@ -312,8 +373,7 @@ export function matchPresentationRoute(
|
|
|
312
373
|
const decoded = decodeRouteSegment(received);
|
|
313
374
|
if (decoded === null) return [];
|
|
314
375
|
input[expected.slice(1)] = decoded;
|
|
315
|
-
}
|
|
316
|
-
else if (expected !== received) return [];
|
|
376
|
+
} else if (expected !== received) return [];
|
|
317
377
|
}
|
|
318
378
|
return [{ definition, input }];
|
|
319
379
|
});
|
|
@@ -527,46 +587,53 @@ export function validatePresentations(
|
|
|
527
587
|
problems.push(`Presentation “${presentation.id}” duplicada.`);
|
|
528
588
|
byId.set(presentation.id, presentation);
|
|
529
589
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
problems.push(
|
|
533
|
-
`A action “${presentation.body.action}” não está disponível.`,
|
|
534
|
-
);
|
|
535
|
-
} else if (bodyAction.kind === "simple") {
|
|
536
|
-
problems.push(
|
|
537
|
-
`A action simple “${bodyAction.name}” não pode ocupar o body.`,
|
|
538
|
-
);
|
|
590
|
+
if (isPresentationComponentBody(presentation.body)) {
|
|
591
|
+
// O conteúdo interno permanece uma fronteira React opaca para a spec da casca.
|
|
539
592
|
} else {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
bodyAction,
|
|
543
|
-
`${presentation.id}.body.input`,
|
|
544
|
-
problems,
|
|
545
|
-
);
|
|
546
|
-
if (
|
|
547
|
-
bodyAction.kind === "view" &&
|
|
548
|
-
presentation.body.fields === undefined
|
|
549
|
-
) {
|
|
593
|
+
const bodyAction = actions[presentation.body.action];
|
|
594
|
+
if (bodyAction === undefined) {
|
|
550
595
|
problems.push(
|
|
551
|
-
`A
|
|
596
|
+
`A action “${presentation.body.action}” não está disponível.`,
|
|
552
597
|
);
|
|
553
|
-
}
|
|
554
|
-
if (
|
|
555
|
-
bodyAction.kind !== "view" &&
|
|
556
|
-
presentation.body.fields !== undefined
|
|
557
|
-
) {
|
|
558
|
-
problems.push(`Fields só podem ser declarados para uma action view.`);
|
|
559
|
-
}
|
|
560
|
-
if (bodyAction.kind !== "list" && presentation.body.open !== undefined) {
|
|
561
|
-
problems.push(`Open só pode ser declarado para uma action list.`);
|
|
562
|
-
}
|
|
563
|
-
if (
|
|
564
|
-
bodyAction.kind !== "form" &&
|
|
565
|
-
presentation.body.submitLabel !== undefined
|
|
566
|
-
) {
|
|
598
|
+
} else if (bodyAction.kind === "simple") {
|
|
567
599
|
problems.push(
|
|
568
|
-
`
|
|
600
|
+
`A action simple “${bodyAction.name}” não pode ocupar o body.`,
|
|
601
|
+
);
|
|
602
|
+
} else {
|
|
603
|
+
validateActionBindings(
|
|
604
|
+
presentation.body.input,
|
|
605
|
+
bodyAction,
|
|
606
|
+
`${presentation.id}.body.input`,
|
|
607
|
+
problems,
|
|
569
608
|
);
|
|
609
|
+
if (
|
|
610
|
+
bodyAction.kind === "view" &&
|
|
611
|
+
presentation.body.fields === undefined
|
|
612
|
+
) {
|
|
613
|
+
problems.push(
|
|
614
|
+
`A Presentation “${presentation.id}” precisa declarar fields para a view.`,
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
if (
|
|
618
|
+
bodyAction.kind !== "view" &&
|
|
619
|
+
presentation.body.fields !== undefined
|
|
620
|
+
) {
|
|
621
|
+
problems.push(`Fields só podem ser declarados para uma action view.`);
|
|
622
|
+
}
|
|
623
|
+
if (
|
|
624
|
+
bodyAction.kind !== "list" &&
|
|
625
|
+
presentation.body.open !== undefined
|
|
626
|
+
) {
|
|
627
|
+
problems.push(`Open só pode ser declarado para uma action list.`);
|
|
628
|
+
}
|
|
629
|
+
if (
|
|
630
|
+
bodyAction.kind !== "form" &&
|
|
631
|
+
presentation.body.submitLabel !== undefined
|
|
632
|
+
) {
|
|
633
|
+
problems.push(
|
|
634
|
+
`SubmitLabel só pode ser declarado para uma action form.`,
|
|
635
|
+
);
|
|
636
|
+
}
|
|
570
637
|
}
|
|
571
638
|
}
|
|
572
639
|
|
|
@@ -615,7 +682,10 @@ export function validatePresentations(
|
|
|
615
682
|
problems.push(
|
|
616
683
|
`A route de “${presentation.id}” consulta a Presentation inexistente “${record.presentation}”.`,
|
|
617
684
|
);
|
|
618
|
-
} else if (
|
|
685
|
+
} else if (
|
|
686
|
+
!isPresentationActionBody(source.body) ||
|
|
687
|
+
actions[source.body.action]?.kind !== "list"
|
|
688
|
+
) {
|
|
619
689
|
problems.push(
|
|
620
690
|
`A route de “${presentation.id}” exige que “${record.presentation}” tenha uma list action no body.`,
|
|
621
691
|
);
|
|
@@ -633,7 +703,9 @@ export function validatePresentations(
|
|
|
633
703
|
}
|
|
634
704
|
}
|
|
635
705
|
}
|
|
636
|
-
const open = presentation.body
|
|
706
|
+
const open = isPresentationActionBody(presentation.body)
|
|
707
|
+
? presentation.body.open
|
|
708
|
+
: undefined;
|
|
637
709
|
if (open !== undefined && !byId.has(open.presentation)) {
|
|
638
710
|
problems.push(
|
|
639
711
|
`A lista “${presentation.id}” abre a Presentation inexistente “${open.presentation}”.`,
|
|
@@ -650,7 +722,9 @@ export function validatePresentations(
|
|
|
650
722
|
);
|
|
651
723
|
}
|
|
652
724
|
const action =
|
|
653
|
-
target
|
|
725
|
+
target !== undefined && isPresentationActionBody(target.body)
|
|
726
|
+
? actions[target.body.action]
|
|
727
|
+
: undefined;
|
|
654
728
|
if (action !== undefined) {
|
|
655
729
|
validateActionBindings(
|
|
656
730
|
open.input,
|
|
@@ -660,7 +734,10 @@ export function validatePresentations(
|
|
|
660
734
|
);
|
|
661
735
|
}
|
|
662
736
|
}
|
|
663
|
-
|
|
737
|
+
const bodyEffects = isPresentationActionBody(presentation.body)
|
|
738
|
+
? presentation.body.onSuccess
|
|
739
|
+
: [];
|
|
740
|
+
for (const effect of bodyEffects) {
|
|
664
741
|
if (effect.effect !== "navigate") continue;
|
|
665
742
|
const target = byId.get(effect.presentation);
|
|
666
743
|
if (target === undefined) {
|
|
@@ -678,7 +755,9 @@ export function validatePresentations(
|
|
|
678
755
|
`O body de “${presentation.id}” navega para “${target.id}” como ${effect.surface}, mas sua route declara ${target.route.surface}.`,
|
|
679
756
|
);
|
|
680
757
|
}
|
|
681
|
-
const action =
|
|
758
|
+
const action = isPresentationActionBody(target.body)
|
|
759
|
+
? actions[target.body.action]
|
|
760
|
+
: undefined;
|
|
682
761
|
if (action !== undefined) {
|
|
683
762
|
validateActionBindings(
|
|
684
763
|
effect.input,
|
|
@@ -706,7 +785,9 @@ export function validatePresentations(
|
|
|
706
785
|
`A action “${command.action}” navega para “${target.id}” como ${effect.surface}, mas sua route declara ${target.route.surface}.`,
|
|
707
786
|
);
|
|
708
787
|
}
|
|
709
|
-
const action =
|
|
788
|
+
const action = isPresentationActionBody(target.body)
|
|
789
|
+
? actions[target.body.action]
|
|
790
|
+
: undefined;
|
|
710
791
|
if (action !== undefined) {
|
|
711
792
|
validateActionBindings(
|
|
712
793
|
effect.input,
|
|
@@ -724,6 +805,10 @@ export function validatePresentations(
|
|
|
724
805
|
problems.push(
|
|
725
806
|
`A action “${command.action}” abre a Presentation inexistente “${command.target.presentation}”.`,
|
|
726
807
|
);
|
|
808
|
+
} else if (!isPresentationActionBody(target.body)) {
|
|
809
|
+
problems.push(
|
|
810
|
+
`A Presentation “${target.id}” usa conteúdo de componente e não pode ser aberta pela action “${command.action}”.`,
|
|
811
|
+
);
|
|
727
812
|
} else if (target.body.action !== command.action) {
|
|
728
813
|
problems.push(
|
|
729
814
|
`A Presentation “${target.id}” usa “${target.body.action}”, mas foi aberta por “${command.action}”.`,
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useCallback,
|
|
3
|
+
useEffect,
|
|
3
4
|
useState,
|
|
4
5
|
type ReactElement,
|
|
5
6
|
type ReactNode,
|
|
6
7
|
} from "react";
|
|
7
8
|
import { createPortal } from "react-dom";
|
|
8
|
-
import { ArrowLeft, Braces } from "lucide-react";
|
|
9
|
+
import { ArrowLeft, Braces, MessageCircle } from "lucide-react";
|
|
9
10
|
import type {
|
|
10
11
|
PresentationActionRegistry,
|
|
12
|
+
PresentationActionBody,
|
|
11
13
|
PresentationBindingContext,
|
|
12
14
|
PresentationCommand,
|
|
13
15
|
PresentationDiagnostic,
|
|
@@ -19,6 +21,7 @@ import type {
|
|
|
19
21
|
import {
|
|
20
22
|
applyPresentationEffects,
|
|
21
23
|
createPresentationInspectionSnapshot,
|
|
24
|
+
isPresentationActionBody,
|
|
22
25
|
openPresentation,
|
|
23
26
|
resolvePresentationBindings,
|
|
24
27
|
} from "../../../core/presentation.ts";
|
|
@@ -75,6 +78,7 @@ import {
|
|
|
75
78
|
ContentHeader,
|
|
76
79
|
ContentTitle,
|
|
77
80
|
} from "./content-header.tsx";
|
|
81
|
+
import { SurfaceAssistant } from "./surface-assistant.tsx";
|
|
78
82
|
|
|
79
83
|
interface PresentationFrameProps {
|
|
80
84
|
surface: PresentationSurface;
|
|
@@ -90,6 +94,11 @@ interface PresentationFrameProps {
|
|
|
90
94
|
className?: string;
|
|
91
95
|
bodyClassName?: string;
|
|
92
96
|
listBody?: boolean;
|
|
97
|
+
contentOwnsHeading?: boolean;
|
|
98
|
+
assistantOpen?: boolean;
|
|
99
|
+
assistantLabel?: string;
|
|
100
|
+
assistantAction?: ReactNode;
|
|
101
|
+
assistantPanel?: ReactNode;
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
const bodyClassName =
|
|
@@ -124,6 +133,11 @@ function PresentationFrame({
|
|
|
124
133
|
className,
|
|
125
134
|
bodyClassName: bodyClassNameProp,
|
|
126
135
|
listBody = false,
|
|
136
|
+
contentOwnsHeading = false,
|
|
137
|
+
assistantOpen = false,
|
|
138
|
+
assistantLabel,
|
|
139
|
+
assistantAction,
|
|
140
|
+
assistantPanel,
|
|
127
141
|
}: PresentationFrameProps): ReactElement {
|
|
128
142
|
const insidePageShell = useInsidePageShell();
|
|
129
143
|
const [footerTarget, setFooterTarget] = useState<HTMLElement | null>(null);
|
|
@@ -153,6 +167,20 @@ function PresentationFrame({
|
|
|
153
167
|
{typeof children === "function" ? children(footerTarget) : children}
|
|
154
168
|
</div>
|
|
155
169
|
);
|
|
170
|
+
const withAssistant = (content: ReactElement): ReactElement =>
|
|
171
|
+
assistantLabel === undefined || assistantPanel === undefined ? (
|
|
172
|
+
content
|
|
173
|
+
) : (
|
|
174
|
+
<SurfaceAssistant
|
|
175
|
+
open={assistantOpen}
|
|
176
|
+
assistantLabel={assistantLabel}
|
|
177
|
+
assistant={assistantPanel}
|
|
178
|
+
className="min-h-0 flex-1"
|
|
179
|
+
contentClassName="flex min-h-0 flex-col"
|
|
180
|
+
>
|
|
181
|
+
{content}
|
|
182
|
+
</SurfaceAssistant>
|
|
183
|
+
);
|
|
156
184
|
|
|
157
185
|
if (surface === "page") {
|
|
158
186
|
if (listBody) {
|
|
@@ -171,13 +199,20 @@ function PresentationFrame({
|
|
|
171
199
|
);
|
|
172
200
|
return (
|
|
173
201
|
<Page className={cn("flex min-h-full flex-col", className)}>
|
|
174
|
-
{navigation === undefined ? null : (
|
|
202
|
+
{navigation === undefined && assistantAction === undefined ? null : (
|
|
175
203
|
<PageHeader>
|
|
176
|
-
|
|
204
|
+
{navigation === undefined ? null : (
|
|
205
|
+
<PageNavigation>{navigation}</PageNavigation>
|
|
206
|
+
)}
|
|
207
|
+
{assistantAction === undefined ? null : (
|
|
208
|
+
<PageActions>{assistantAction}</PageActions>
|
|
209
|
+
)}
|
|
177
210
|
</PageHeader>
|
|
178
211
|
)}
|
|
179
|
-
<PageBody className="min-h-0 flex-1 overflow-
|
|
180
|
-
{
|
|
212
|
+
<PageBody className="min-h-0 flex-1 overflow-hidden">
|
|
213
|
+
{withAssistant(
|
|
214
|
+
<div className="h-full overflow-y-auto">{content}</div>,
|
|
215
|
+
)}
|
|
181
216
|
</PageBody>
|
|
182
217
|
{hasFooter ? <PageFooter>{renderFooterActions()}</PageFooter> : null}
|
|
183
218
|
</Page>
|
|
@@ -186,22 +221,36 @@ function PresentationFrame({
|
|
|
186
221
|
if (insidePageShell) {
|
|
187
222
|
return (
|
|
188
223
|
<Page className={cn("flex min-h-full flex-col", className)}>
|
|
189
|
-
{navigation === undefined &&
|
|
224
|
+
{navigation === undefined &&
|
|
225
|
+
headerActions === undefined &&
|
|
226
|
+
assistantAction === undefined ? null : (
|
|
190
227
|
<PageHeader>
|
|
191
228
|
{navigation === undefined ? null : (
|
|
192
229
|
<PageNavigation>{navigation}</PageNavigation>
|
|
193
230
|
)}
|
|
194
|
-
{headerActions === undefined
|
|
231
|
+
{headerActions === undefined &&
|
|
232
|
+
assistantAction === undefined ? null : (
|
|
195
233
|
<PageActions>
|
|
196
|
-
<PresentationActions>
|
|
234
|
+
<PresentationActions>
|
|
235
|
+
{headerActions}
|
|
236
|
+
{assistantAction}
|
|
237
|
+
</PresentationActions>
|
|
197
238
|
</PageActions>
|
|
198
239
|
)}
|
|
199
240
|
</PageHeader>
|
|
200
241
|
)}
|
|
201
|
-
<
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
242
|
+
<PageBody className="min-h-0 flex-1 overflow-hidden">
|
|
243
|
+
{withAssistant(
|
|
244
|
+
<div className="flex h-full min-h-0 flex-col overflow-y-auto">
|
|
245
|
+
{contentOwnsHeading ? null : (
|
|
246
|
+
<PageIntro>
|
|
247
|
+
<PageTitle>{title}</PageTitle>
|
|
248
|
+
</PageIntro>
|
|
249
|
+
)}
|
|
250
|
+
{body}
|
|
251
|
+
</div>,
|
|
252
|
+
)}
|
|
253
|
+
</PageBody>
|
|
205
254
|
{hasFooter ? <PageFooter>{renderFooterActions()}</PageFooter> : null}
|
|
206
255
|
</Page>
|
|
207
256
|
);
|
|
@@ -218,14 +267,20 @@ function PresentationFrame({
|
|
|
218
267
|
<PageNavigation>{navigation}</PageNavigation>
|
|
219
268
|
)}
|
|
220
269
|
<PageTitle className="text-lg leading-none">{title}</PageTitle>
|
|
221
|
-
{headerActions === undefined
|
|
270
|
+
{headerActions === undefined &&
|
|
271
|
+
assistantAction === undefined ? null : (
|
|
222
272
|
<PageActions>
|
|
223
|
-
<PresentationActions>
|
|
273
|
+
<PresentationActions>
|
|
274
|
+
{headerActions}
|
|
275
|
+
{assistantAction}
|
|
276
|
+
</PresentationActions>
|
|
224
277
|
</PageActions>
|
|
225
278
|
)}
|
|
226
279
|
</PageHeader>
|
|
227
|
-
<PageBody className="min-h-0 flex-1 overflow-
|
|
228
|
-
{
|
|
280
|
+
<PageBody className="min-h-0 flex-1 overflow-hidden">
|
|
281
|
+
{withAssistant(
|
|
282
|
+
<div className="h-full overflow-y-auto px-8 py-8">{body}</div>,
|
|
283
|
+
)}
|
|
229
284
|
</PageBody>
|
|
230
285
|
{hasFooter ? <PageFooter>{renderFooterActions()}</PageFooter> : null}
|
|
231
286
|
</Page>
|
|
@@ -242,7 +297,10 @@ function PresentationFrame({
|
|
|
242
297
|
}}
|
|
243
298
|
>
|
|
244
299
|
<DialogContent
|
|
245
|
-
className={
|
|
300
|
+
className={cn(
|
|
301
|
+
assistantOpen ? "h-[min(48rem,85vh)] max-w-5xl" : undefined,
|
|
302
|
+
className,
|
|
303
|
+
)}
|
|
246
304
|
aria-describedby={undefined}
|
|
247
305
|
closeDisabled={blocked}
|
|
248
306
|
onEscapeKeyDown={(event) => {
|
|
@@ -252,17 +310,23 @@ function PresentationFrame({
|
|
|
252
310
|
if (blocked) event.preventDefault();
|
|
253
311
|
}}
|
|
254
312
|
>
|
|
255
|
-
<
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
313
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
314
|
+
<DialogHeader>
|
|
315
|
+
{navigation}
|
|
316
|
+
<DialogTitle className="truncate">{title}</DialogTitle>
|
|
317
|
+
{headerActions === undefined &&
|
|
318
|
+
assistantAction === undefined ? null : (
|
|
319
|
+
<PresentationActions>
|
|
320
|
+
{headerActions}
|
|
321
|
+
{assistantAction}
|
|
322
|
+
</PresentationActions>
|
|
323
|
+
)}
|
|
324
|
+
</DialogHeader>
|
|
325
|
+
{withAssistant(<DialogBody>{body}</DialogBody>)}
|
|
326
|
+
{hasFooter ? (
|
|
327
|
+
<DialogFooter>{renderFooterActions()}</DialogFooter>
|
|
328
|
+
) : null}
|
|
329
|
+
</div>
|
|
266
330
|
</DialogContent>
|
|
267
331
|
</Dialog>
|
|
268
332
|
);
|
|
@@ -277,7 +341,12 @@ function PresentationFrame({
|
|
|
277
341
|
}}
|
|
278
342
|
>
|
|
279
343
|
<DrawerContent
|
|
280
|
-
className={
|
|
344
|
+
className={cn(
|
|
345
|
+
assistantOpen
|
|
346
|
+
? "w-[min(80rem,calc(100%-2rem))] max-w-none"
|
|
347
|
+
: undefined,
|
|
348
|
+
className,
|
|
349
|
+
)}
|
|
281
350
|
aria-describedby={undefined}
|
|
282
351
|
closeDisabled={blocked}
|
|
283
352
|
onEscapeKeyDown={(event) => {
|
|
@@ -287,22 +356,33 @@ function PresentationFrame({
|
|
|
287
356
|
if (blocked) event.preventDefault();
|
|
288
357
|
}}
|
|
289
358
|
>
|
|
290
|
-
<
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
359
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
360
|
+
<DrawerHeader>
|
|
361
|
+
{navigation}
|
|
362
|
+
<DrawerTitle className="truncate">{title}</DrawerTitle>
|
|
363
|
+
{headerActions === undefined &&
|
|
364
|
+
assistantAction === undefined ? null : (
|
|
365
|
+
<PresentationActions>
|
|
366
|
+
{headerActions}
|
|
367
|
+
{assistantAction}
|
|
368
|
+
</PresentationActions>
|
|
369
|
+
)}
|
|
370
|
+
</DrawerHeader>
|
|
371
|
+
{withAssistant(<DrawerBody>{body}</DrawerBody>)}
|
|
372
|
+
{hasFooter ? (
|
|
373
|
+
<DrawerFooter>{renderFooterActions()}</DrawerFooter>
|
|
374
|
+
) : null}
|
|
375
|
+
</div>
|
|
301
376
|
</DrawerContent>
|
|
302
377
|
</Drawer>
|
|
303
378
|
);
|
|
304
379
|
}
|
|
305
380
|
|
|
381
|
+
export interface PresentationAssistantRuntime {
|
|
382
|
+
icon?: ReactNode;
|
|
383
|
+
render: (controls: { close: () => void }) => ReactNode;
|
|
384
|
+
}
|
|
385
|
+
|
|
306
386
|
export interface PresentationProps {
|
|
307
387
|
/** Definição estática registrada e publicada no manifest. */
|
|
308
388
|
definition: PresentationDefinition;
|
|
@@ -325,6 +405,10 @@ export interface PresentationProps {
|
|
|
325
405
|
onOpenChange?: (open: boolean) => void;
|
|
326
406
|
className?: string;
|
|
327
407
|
bodyClassName?: string;
|
|
408
|
+
/** Conteúdo React da boundary declarada por `body.component`. */
|
|
409
|
+
children?: ReactNode;
|
|
410
|
+
/** Painel runtime; a página fecha sobre os dados do recurso que ela carregou. */
|
|
411
|
+
assistant?: PresentationAssistantRuntime;
|
|
328
412
|
}
|
|
329
413
|
|
|
330
414
|
function actionLabel(action: { name: string; label?: unknown }): string {
|
|
@@ -394,6 +478,8 @@ export function Presentation({
|
|
|
394
478
|
onOpenChange,
|
|
395
479
|
className,
|
|
396
480
|
bodyClassName: bodyClassNameProp,
|
|
481
|
+
children,
|
|
482
|
+
assistant,
|
|
397
483
|
}: PresentationProps): ReactElement {
|
|
398
484
|
if (definition.id !== invocation.presentationId) {
|
|
399
485
|
throw new Error(
|
|
@@ -404,16 +490,17 @@ export function Presentation({
|
|
|
404
490
|
ReadonlySet<PresentationCommand>
|
|
405
491
|
>(() => new Set());
|
|
406
492
|
const [bodyLoading, setBodyLoading] = useState(false);
|
|
493
|
+
const [assistantOpen, setAssistantOpen] = useState(false);
|
|
494
|
+
useEffect(() => {
|
|
495
|
+
setAssistantOpen(false);
|
|
496
|
+
}, [definition.id, invocation.surface]);
|
|
407
497
|
const context: PresentationBindingContext = {
|
|
408
498
|
...bindingContext,
|
|
409
499
|
route: bindingContext?.route ?? invocation.input,
|
|
410
500
|
};
|
|
411
501
|
|
|
412
502
|
const applyEffects = useCallback(
|
|
413
|
-
(
|
|
414
|
-
effects: PresentationDefinition["body"]["onSuccess"],
|
|
415
|
-
result?: unknown,
|
|
416
|
-
): void => {
|
|
503
|
+
(effects: PresentationActionBody["onSuccess"], result?: unknown): void => {
|
|
417
504
|
const next = applyPresentationEffects(
|
|
418
505
|
invocation,
|
|
419
506
|
effects,
|
|
@@ -541,23 +628,51 @@ export function Presentation({
|
|
|
541
628
|
);
|
|
542
629
|
};
|
|
543
630
|
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
631
|
+
const actionBody = isPresentationActionBody(definition.body)
|
|
632
|
+
? definition.body
|
|
633
|
+
: null;
|
|
634
|
+
const bodyAction = actionBody === null ? null : actions[actionBody.action];
|
|
635
|
+
if (actionBody !== null && bodyAction === undefined) {
|
|
636
|
+
throw new Error(`Action “${actionBody.action}” não registrada.`);
|
|
547
637
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
638
|
+
if (actionBody === null && children === undefined) {
|
|
639
|
+
throw new Error(
|
|
640
|
+
`A Presentation “${definition.id}” declara body.component e exige children.`,
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
if (actionBody !== null && children !== undefined) {
|
|
644
|
+
throw new Error(
|
|
645
|
+
`A Presentation “${definition.id}” usa body.action e não aceita children.`,
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
if (definition.assistant !== undefined && assistant === undefined) {
|
|
649
|
+
throw new Error(
|
|
650
|
+
`A Presentation “${definition.id}” declara assistência e exige a prop assistant.`,
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
if (definition.assistant === undefined && assistant !== undefined) {
|
|
654
|
+
throw new Error(
|
|
655
|
+
`A Presentation “${definition.id}” não declara assistência e não aceita a prop assistant.`,
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
const bodyInput =
|
|
659
|
+
actionBody === null
|
|
660
|
+
? {}
|
|
661
|
+
: (resolvePresentationBindings(actionBody.input, context) as Record<
|
|
662
|
+
string,
|
|
663
|
+
unknown
|
|
664
|
+
>);
|
|
552
665
|
const renderBody = (footerTarget: HTMLElement | null): ReactNode => {
|
|
666
|
+
if (actionBody === null) return children;
|
|
667
|
+
if (bodyAction === null) return null;
|
|
553
668
|
if (bodyAction.kind === "form") {
|
|
554
669
|
return (
|
|
555
670
|
<ActionForm
|
|
556
671
|
action={bodyAction as FormContract<Record<string, unknown>, unknown>}
|
|
557
672
|
defaultValues={bodyInput}
|
|
558
|
-
submitLabel={
|
|
673
|
+
submitLabel={actionBody.submitLabel}
|
|
559
674
|
onCancel={invocation.surface === "page" ? undefined : closeSurface}
|
|
560
|
-
onSuccess={(data) => applyEffects(
|
|
675
|
+
onSuccess={(data) => applyEffects(actionBody.onSuccess, data)}
|
|
561
676
|
disabled={hasSurfaceBlock}
|
|
562
677
|
onLoadingChange={setBodyLoading}
|
|
563
678
|
footer={(actions) =>
|
|
@@ -578,14 +693,14 @@ export function Presentation({
|
|
|
578
693
|
input={bodyInput}
|
|
579
694
|
state={listState}
|
|
580
695
|
onStateChange={onListStateChange}
|
|
581
|
-
{...(
|
|
696
|
+
{...(actionBody.open === undefined
|
|
582
697
|
? {}
|
|
583
698
|
: {
|
|
584
699
|
onRowClick: (item: Record<string, unknown>) =>
|
|
585
700
|
openTarget(
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
701
|
+
actionBody.open!.presentation,
|
|
702
|
+
actionBody.open!.surface,
|
|
703
|
+
actionBody.open!.input,
|
|
589
704
|
{ ...context, item },
|
|
590
705
|
),
|
|
591
706
|
})}
|
|
@@ -593,7 +708,7 @@ export function Presentation({
|
|
|
593
708
|
);
|
|
594
709
|
}
|
|
595
710
|
if (bodyAction.kind === "view") {
|
|
596
|
-
const fields =
|
|
711
|
+
const fields = actionBody.fields ?? [];
|
|
597
712
|
return (
|
|
598
713
|
<ActionView
|
|
599
714
|
action={bodyAction as ViewContract<Record<string, unknown>, unknown>}
|
|
@@ -641,6 +756,22 @@ export function Presentation({
|
|
|
641
756
|
<ArrowLeft aria-hidden />
|
|
642
757
|
</Button>
|
|
643
758
|
);
|
|
759
|
+
const assistantAction =
|
|
760
|
+
definition.assistant === undefined || assistantOpen ? undefined : (
|
|
761
|
+
<Button
|
|
762
|
+
size="icon"
|
|
763
|
+
variant="ghost"
|
|
764
|
+
aria-label={definition.assistant.triggerLabel}
|
|
765
|
+
title={definition.assistant.triggerLabel}
|
|
766
|
+
onClick={() => setAssistantOpen(true)}
|
|
767
|
+
>
|
|
768
|
+
{assistant?.icon ?? <MessageCircle aria-hidden />}
|
|
769
|
+
</Button>
|
|
770
|
+
);
|
|
771
|
+
const assistantPanel =
|
|
772
|
+
definition.assistant === undefined || assistant === undefined
|
|
773
|
+
? undefined
|
|
774
|
+
: assistant.render({ close: () => setAssistantOpen(false) });
|
|
644
775
|
|
|
645
776
|
return (
|
|
646
777
|
<PresentationFrame
|
|
@@ -659,9 +790,17 @@ export function Presentation({
|
|
|
659
790
|
}}
|
|
660
791
|
className={className}
|
|
661
792
|
bodyClassName={bodyClassNameProp}
|
|
662
|
-
hasBodyFooter={bodyAction
|
|
793
|
+
hasBodyFooter={bodyAction?.kind === "form"}
|
|
663
794
|
blocked={surfaceBlocked}
|
|
664
|
-
listBody={bodyAction
|
|
795
|
+
listBody={bodyAction?.kind === "list"}
|
|
796
|
+
contentOwnsHeading={
|
|
797
|
+
!isPresentationActionBody(definition.body) &&
|
|
798
|
+
definition.body.heading === "content"
|
|
799
|
+
}
|
|
800
|
+
assistantOpen={assistantOpen}
|
|
801
|
+
assistantLabel={definition.assistant?.triggerLabel}
|
|
802
|
+
assistantAction={assistantAction}
|
|
803
|
+
assistantPanel={assistantPanel}
|
|
665
804
|
>
|
|
666
805
|
{renderBody}
|
|
667
806
|
</PresentationFrame>
|
|
@@ -48,6 +48,8 @@ export interface SplitProps {
|
|
|
48
48
|
/** Chamado ao concluir uma mudança de layout; pode persistir o resultado em localStorage. */
|
|
49
49
|
onLayoutChanged?: (layout: SplitLayout, meta: SplitLayoutChange) => void
|
|
50
50
|
className?: string
|
|
51
|
+
/** Identificador semântico opcional para testes e composição de patterns. */
|
|
52
|
+
'data-slot'?: string
|
|
51
53
|
children: ReactNode
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -86,6 +88,7 @@ export function Split({
|
|
|
86
88
|
defaultLayout,
|
|
87
89
|
onLayoutChanged,
|
|
88
90
|
className,
|
|
91
|
+
'data-slot': dataSlot = 'split',
|
|
89
92
|
children,
|
|
90
93
|
}: SplitProps): React.ReactElement {
|
|
91
94
|
const panes = panesOf(children)
|
|
@@ -93,7 +96,7 @@ export function Split({
|
|
|
93
96
|
|
|
94
97
|
if (!resizable) {
|
|
95
98
|
return (
|
|
96
|
-
<div id={id === undefined ? undefined : String(id)} data-slot=
|
|
99
|
+
<div id={id === undefined ? undefined : String(id)} data-slot={dataSlot} data-direction={direction} className={cn('flex min-h-0 min-w-0 flex-1', vertical && 'flex-col', className)}>
|
|
97
100
|
{panes}
|
|
98
101
|
</div>
|
|
99
102
|
)
|
|
@@ -111,7 +114,7 @@ export function Split({
|
|
|
111
114
|
orientation={direction}
|
|
112
115
|
defaultLayout={defaultLayout}
|
|
113
116
|
onLayoutChanged={onLayoutChanged}
|
|
114
|
-
data-slot=
|
|
117
|
+
data-slot={dataSlot}
|
|
115
118
|
className={cn('min-h-0 min-w-0 flex-1', className)}
|
|
116
119
|
>
|
|
117
120
|
{panes.flatMap((pane, index) => {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn.ts";
|
|
3
|
+
import { Pane, type PaneSize, Split } from "./split.tsx";
|
|
4
|
+
|
|
5
|
+
export interface SurfaceAssistantProps {
|
|
6
|
+
/** Controla somente a presença visual do painel; o conteúdo pertence ao consumidor. */
|
|
7
|
+
open: boolean;
|
|
8
|
+
/** Nome acessível da região complementar. */
|
|
9
|
+
assistantLabel: string;
|
|
10
|
+
/** Conteúdo principal que permanece visível à esquerda. */
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
/** Painel fornecido pela superfície que conhece o contexto do recurso. */
|
|
13
|
+
assistant: ReactNode;
|
|
14
|
+
initialSize?: PaneSize;
|
|
15
|
+
minSize?: PaneSize;
|
|
16
|
+
maxSize?: PaneSize;
|
|
17
|
+
className?: string;
|
|
18
|
+
contentClassName?: string;
|
|
19
|
+
assistantClassName?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Mantém conteúdo e assistência contextual na mesma superfície. O host decide abertura,
|
|
24
|
+
* trigger e dados; o pattern preserva a relação espacial e o redimensionamento.
|
|
25
|
+
*/
|
|
26
|
+
export function SurfaceAssistant({
|
|
27
|
+
open,
|
|
28
|
+
assistantLabel,
|
|
29
|
+
children,
|
|
30
|
+
assistant,
|
|
31
|
+
initialSize = "28rem",
|
|
32
|
+
minSize = "20rem",
|
|
33
|
+
maxSize = "40rem",
|
|
34
|
+
className,
|
|
35
|
+
contentClassName,
|
|
36
|
+
assistantClassName,
|
|
37
|
+
}: SurfaceAssistantProps): React.ReactElement {
|
|
38
|
+
return (
|
|
39
|
+
<Split
|
|
40
|
+
resizable
|
|
41
|
+
data-slot="surface-assistant"
|
|
42
|
+
className={cn("h-full", className)}
|
|
43
|
+
>
|
|
44
|
+
<Pane
|
|
45
|
+
key="surface-content"
|
|
46
|
+
id="surface-content"
|
|
47
|
+
grow
|
|
48
|
+
inset="none"
|
|
49
|
+
className={cn("h-full", contentClassName)}
|
|
50
|
+
>
|
|
51
|
+
{children}
|
|
52
|
+
</Pane>
|
|
53
|
+
{open ? (
|
|
54
|
+
<Pane
|
|
55
|
+
key="surface-assistant"
|
|
56
|
+
id="surface-assistant"
|
|
57
|
+
initialSize={initialSize}
|
|
58
|
+
minSize={minSize}
|
|
59
|
+
maxSize={maxSize}
|
|
60
|
+
inset="none"
|
|
61
|
+
className={cn("h-full bg-background", assistantClassName)}
|
|
62
|
+
>
|
|
63
|
+
<aside
|
|
64
|
+
data-slot="surface-assistant-panel"
|
|
65
|
+
aria-label={assistantLabel}
|
|
66
|
+
className="h-full min-w-0 overflow-hidden"
|
|
67
|
+
>
|
|
68
|
+
{assistant}
|
|
69
|
+
</aside>
|
|
70
|
+
</Pane>
|
|
71
|
+
) : null}
|
|
72
|
+
</Split>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -32,11 +32,20 @@ shell que efetivamente hospeda a rota.
|
|
|
32
32
|
O exemplo declara a Presentation sobre um contrato de formulário e alterna a superfície. O
|
|
33
33
|
`DocBrowserActionProvider` simula o cliente de actions; na aplicação, esse papel é do provider real.
|
|
34
34
|
|
|
35
|
+
Quando o recurso aceita assistência contextual, declare somente o gatilho serializável em
|
|
36
|
+
`assistant`. O renderer coloca esse gatilho no cabeçalho e abre o painel à direita do body da
|
|
37
|
+
própria Page, Dialog ou Drawer. Cabeçalho e rodapé continuam ocupando toda a largura da superfície;
|
|
38
|
+
enquanto o painel está aberto, o gatilho sai do cabeçalho e o próprio painel assume sua identidade
|
|
39
|
+
visual. O dado vivo do recurso não entra na definição: a página que o carregou fornece
|
|
40
|
+
`assistant.render`, captura identidade, nome e demais dados autorizados e monta o chat. Assim,
|
|
41
|
+
contexto de conversa não vira autorização nem dado persistido no manifest.
|
|
42
|
+
|
|
35
43
|
```tsx live
|
|
36
44
|
const workspacePresentation = definePresentation({
|
|
37
45
|
schemaVersion: 1,
|
|
38
46
|
id: "workspace.create",
|
|
39
47
|
title: "Criar workspace",
|
|
48
|
+
assistant: { triggerLabel: "Conversar sobre este workspace" },
|
|
40
49
|
body: { action: docWorkspaceCreate.name },
|
|
41
50
|
});
|
|
42
51
|
|
|
@@ -81,6 +90,22 @@ function Example() {
|
|
|
81
90
|
if (next) setInvocation(next);
|
|
82
91
|
else setOpen(false);
|
|
83
92
|
}}
|
|
93
|
+
assistant={{
|
|
94
|
+
render: ({ close }) => (
|
|
95
|
+
<div className="flex h-full flex-col p-3">
|
|
96
|
+
<p className="text-sm">
|
|
97
|
+
Conversa vinculada ao workspace carregado pela página.
|
|
98
|
+
</p>
|
|
99
|
+
<Button
|
|
100
|
+
className="mt-3 self-start"
|
|
101
|
+
variant="ghost"
|
|
102
|
+
onClick={close}
|
|
103
|
+
>
|
|
104
|
+
Fechar
|
|
105
|
+
</Button>
|
|
106
|
+
</div>
|
|
107
|
+
),
|
|
108
|
+
}}
|
|
84
109
|
/>
|
|
85
110
|
</div>
|
|
86
111
|
</DocBrowserActionProvider>
|
|
@@ -164,21 +189,22 @@ no manifest e concentre a inspeção na Lens em vez de criar um launcher flutuan
|
|
|
164
189
|
|
|
165
190
|
## Propriedades de Presentation
|
|
166
191
|
|
|
167
|
-
| Propriedade | Tipo | Padrão | Descrição
|
|
168
|
-
| -------------------- | ------------------------------------------------ | ------ |
|
|
169
|
-
| `definition` | `PresentationDefinition` | | Artefato estático que descreve o recurso.
|
|
170
|
-
| `definitions` | `PresentationDefinition[]` | | Registry alcançável por navegação, abertura e retorno.
|
|
171
|
-
| `actions` | `PresentationActionRegistry` | | Contratos compartilháveis referenciados pelo artefato.
|
|
172
|
-
| `invocation` | `PresentationInvocation` | | Surface, input e pilha da exibição atual.
|
|
173
|
-
| `bindingContext` | `PresentationBindingContext` | | Rota, item, seleção, sessão e resultado disponíveis.
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
192
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
193
|
+
| -------------------- | ------------------------------------------------ | ------ | --------------------------------------------------------------------------------------------------- |
|
|
194
|
+
| `definition` | `PresentationDefinition` | | Artefato estático que descreve o recurso. |
|
|
195
|
+
| `definitions` | `PresentationDefinition[]` | | Registry alcançável por navegação, abertura e retorno. |
|
|
196
|
+
| `actions` | `PresentationActionRegistry` | | Contratos compartilháveis referenciados pelo artefato. |
|
|
197
|
+
| `invocation` | `PresentationInvocation` | | Surface, input e pilha da exibição atual. |
|
|
198
|
+
| `bindingContext` | `PresentationBindingContext` | | Rota, item, seleção, sessão e resultado disponíveis. |
|
|
199
|
+
| `assistant` | `PresentationAssistantRuntime` | | Painel contextual fornecido pela superfície hospedeira. |
|
|
200
|
+
| `onInvocationChange` | `(next: PresentationInvocation \| null) => void` | | Recebe navegação, retorno e fechamento. |
|
|
201
|
+
| `onRefresh` | `(action: string \| null) => void` | | Recebe invalidações declaradas após sucesso. |
|
|
202
|
+
| `listState` | `ActionListState` | | Recorte controlado da action `list` do body, quando a aplicação o sincroniza com a URL. |
|
|
177
203
|
| `onListStateChange` | `(state: ActionListState) => void` | | Recebe cada mudança de recorte ou exibição da lista do body, como busca, filtros, período e página. |
|
|
178
|
-
| `open` | `boolean` | `true` | Estado controlado de Dialog ou Drawer.
|
|
179
|
-
| `onOpenChange` | `(open: boolean) => void` | | Notifica abertura e fechamento da superfície modal.
|
|
180
|
-
| `className` | `string` | | Classes adicionais da superfície.
|
|
181
|
-
| `bodyClassName` | `string` | | Classes adicionais do body, aplicadas uma única vez.
|
|
204
|
+
| `open` | `boolean` | `true` | Estado controlado de Dialog ou Drawer. |
|
|
205
|
+
| `onOpenChange` | `(open: boolean) => void` | | Notifica abertura e fechamento da superfície modal. |
|
|
206
|
+
| `className` | `string` | | Classes adicionais da superfície. |
|
|
207
|
+
| `bodyClassName` | `string` | | Classes adicionais do body, aplicadas uma única vez. |
|
|
182
208
|
|
|
183
209
|
## Propriedades de PresentationInspector
|
|
184
210
|
|
|
@@ -12,11 +12,13 @@ render(
|
|
|
12
12
|
<div className="p-4 text-sm">Navegação</div>
|
|
13
13
|
</Pane>
|
|
14
14
|
<Pane grow inset="lg">
|
|
15
|
-
<p className="text-sm text-muted-foreground">
|
|
15
|
+
<p className="text-sm text-muted-foreground">
|
|
16
|
+
Conteúdo. Arraste a divisória.
|
|
17
|
+
</p>
|
|
16
18
|
</Pane>
|
|
17
19
|
</Split>
|
|
18
20
|
</div>,
|
|
19
|
-
)
|
|
21
|
+
);
|
|
20
22
|
```
|
|
21
23
|
|
|
22
24
|
## Espaçamento interno
|
|
@@ -30,8 +32,12 @@ Quando uma área precisa continuar legível em telas largas, use uma unidade abs
|
|
|
30
32
|
|
|
31
33
|
```tsx
|
|
32
34
|
<Split resizable>
|
|
33
|
-
<Pane grow inset="none"
|
|
34
|
-
|
|
35
|
+
<Pane grow inset="none">
|
|
36
|
+
<Main />
|
|
37
|
+
</Pane>
|
|
38
|
+
<Pane initialSize="28rem" minSize="20rem" inset="none">
|
|
39
|
+
<Inspector />
|
|
40
|
+
</Pane>
|
|
35
41
|
</Split>
|
|
36
42
|
```
|
|
37
43
|
|
|
@@ -75,23 +81,78 @@ const defaultLayout = readLayout('workspace-layout')
|
|
|
75
81
|
|
|
76
82
|
Em aplicações renderizadas no servidor, leia o armazenamento somente no cliente. `onLayoutChanged` também permite usar `sessionStorage` ou uma camada própria quando o layout precisa acompanhar outro escopo.
|
|
77
83
|
|
|
84
|
+
## Assistência dentro da superfície
|
|
85
|
+
|
|
86
|
+
`SurfaceAssistant` especializa o split usado por uma Page, Dialog ou Drawer que abre assistência
|
|
87
|
+
contextual. O conteúdo principal permanece montado e o painel aparece à direita com largura
|
|
88
|
+
redimensionável. A superfície hospedeira controla o gatilho, o estado aberto e os dados passados ao
|
|
89
|
+
painel; o pattern cuida apenas da relação espacial. Envolva somente o body da superfície com
|
|
90
|
+
`SurfaceAssistant`: cabeçalho e rodapé devem permanecer fora do split e ocupar toda a largura.
|
|
91
|
+
|
|
92
|
+
```tsx preview
|
|
93
|
+
function Example() {
|
|
94
|
+
const [open, setOpen] = React.useState(true);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="h-72 overflow-hidden rounded-lg border border-border">
|
|
98
|
+
<SurfaceAssistant
|
|
99
|
+
open={open}
|
|
100
|
+
assistantLabel="Assistente do pedido"
|
|
101
|
+
assistant={
|
|
102
|
+
<div className="h-full p-3">
|
|
103
|
+
<p className="text-sm">Conversa vinculada ao pedido carregado.</p>
|
|
104
|
+
<Button
|
|
105
|
+
className="mt-3"
|
|
106
|
+
variant="ghost"
|
|
107
|
+
onClick={() => setOpen(false)}
|
|
108
|
+
>
|
|
109
|
+
Fechar
|
|
110
|
+
</Button>
|
|
111
|
+
</div>
|
|
112
|
+
}
|
|
113
|
+
>
|
|
114
|
+
<div className="h-full p-3">
|
|
115
|
+
<Button variant="outline" onClick={() => setOpen(true)}>
|
|
116
|
+
Abrir assistente
|
|
117
|
+
</Button>
|
|
118
|
+
</div>
|
|
119
|
+
</SurfaceAssistant>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
render(<Example />);
|
|
125
|
+
```
|
|
126
|
+
|
|
78
127
|
## Propriedades de Split
|
|
79
128
|
|
|
80
|
-
| Propriedade
|
|
81
|
-
|
|
82
|
-
| `direction`
|
|
83
|
-
| `resizable`
|
|
84
|
-
| `handle`
|
|
85
|
-
| `id`
|
|
86
|
-
| `defaultLayout`
|
|
87
|
-
| `onLayoutChanged` | `(layout, { isUserInteraction }) => void` |
|
|
129
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
130
|
+
| ----------------- | ----------------------------------------- | -------------- | ----------------------------------------------------------------------- |
|
|
131
|
+
| `direction` | `'horizontal' \| 'vertical'` | `'horizontal'` | Sentido em que os panes se alinham. |
|
|
132
|
+
| `resizable` | `boolean` | `false` | Cada fronteira ganha um separador acessível e arrastável. |
|
|
133
|
+
| `handle` | `boolean` | `false` | Mostra a alça visual no separador. |
|
|
134
|
+
| `id` | `string` | | Identidade estável do grupo redimensionável. |
|
|
135
|
+
| `defaultLayout` | `Record<string, number>` | | Layout percentual restaurado, indexado pelos ids dos panes. |
|
|
136
|
+
| `onLayoutChanged` | `(layout, { isUserInteraction }) => void` | | Chamado ao concluir uma mudança de layout; persista onde fizer sentido. |
|
|
88
137
|
|
|
89
138
|
## Propriedades de Pane
|
|
90
139
|
|
|
91
|
-
| Propriedade
|
|
92
|
-
|
|
93
|
-
| `id`
|
|
94
|
-
| `initialSize`
|
|
95
|
-
| `minSize` / `maxSize` | `number \| string`
|
|
96
|
-
| `grow`
|
|
97
|
-
| `inset`
|
|
140
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
141
|
+
| --------------------- | -------------------------------- | ------- | ----------------------------------------------------------------------------------------- |
|
|
142
|
+
| `id` | `string` | | Identidade estável usada pelo layout redimensionável e persistido. |
|
|
143
|
+
| `initialSize` | `number \| string` | | Tamanho inicial; número é porcentagem, string aceita `%`, `rem`, `em`, `vh`, `vw` e `px`. |
|
|
144
|
+
| `minSize` / `maxSize` | `number \| string` | | Limites quando o split é redimensionável. |
|
|
145
|
+
| `grow` | `boolean` | `false` | Ocupa o espaço remanescente no layout simples. |
|
|
146
|
+
| `inset` | `'none' \| 'sm' \| 'md' \| 'lg'` | `'md'` | Respiro interno; `none` para chrome, navegação ou conteúdo com inset próprio. |
|
|
147
|
+
|
|
148
|
+
## Propriedades de SurfaceAssistant
|
|
149
|
+
|
|
150
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
151
|
+
| ---------------- | ----------- | ------- | ------------------------------------------------------- |
|
|
152
|
+
| `open` | `boolean` | | Exibe o painel complementar à direita. |
|
|
153
|
+
| `assistantLabel` | `string` | | Nome acessível da região complementar. |
|
|
154
|
+
| `children` | `ReactNode` | | Conteúdo principal que permanece montado. |
|
|
155
|
+
| `assistant` | `ReactNode` | | Painel fornecido pela superfície que conhece o recurso. |
|
|
156
|
+
| `initialSize` | `PaneSize` | `28rem` | Largura inicial do painel. |
|
|
157
|
+
| `minSize` | `PaneSize` | `20rem` | Largura mínima durante o redimensionamento. |
|
|
158
|
+
| `maxSize` | `PaneSize` | `40rem` | Largura máxima durante o redimensionamento. |
|
package/src/ui/meta.ts
CHANGED
|
@@ -321,7 +321,7 @@ export const componentMeta = {
|
|
|
321
321
|
name: "split",
|
|
322
322
|
ancestry: "opus",
|
|
323
323
|
whenToUse:
|
|
324
|
-
"Divide uma área em panes em sequência horizontal ou vertical. Use `resizable` quando a pessoa deve ajustar a fronteira; o mesmo `<Split>` vira flex simples sem ele. Cada `<Pane>` declara tamanho inicial/mínimo e inset.
|
|
324
|
+
"Divide uma área em panes em sequência horizontal ou vertical. Use `resizable` quando a pessoa deve ajustar a fronteira; o mesmo `<Split>` vira flex simples sem ele. Cada `<Pane>` declara tamanho inicial/mínimo e inset. SurfaceAssistant especializa esse layout para assistência contextual dentro de Page, Dialog ou Drawer.",
|
|
325
325
|
},
|
|
326
326
|
sidebar: {
|
|
327
327
|
name: "sidebar",
|
package/src/ui/react.tsx
CHANGED
|
@@ -424,6 +424,7 @@ export {
|
|
|
424
424
|
PresentationInspector,
|
|
425
425
|
} from "./components/patterns/presentation.tsx";
|
|
426
426
|
export type {
|
|
427
|
+
PresentationAssistantRuntime,
|
|
427
428
|
PresentationProps,
|
|
428
429
|
PresentationInspectorProps,
|
|
429
430
|
} from "./components/patterns/presentation.tsx";
|
|
@@ -483,6 +484,10 @@ export type {
|
|
|
483
484
|
SplitLayoutChange,
|
|
484
485
|
} from "./components/patterns/split.tsx";
|
|
485
486
|
|
|
487
|
+
// Assistência contextual: mantém o recurso visível e abre um painel redimensionável na superfície.
|
|
488
|
+
export { SurfaceAssistant } from "./components/patterns/surface-assistant.tsx";
|
|
489
|
+
export type { SurfaceAssistantProps } from "./components/patterns/surface-assistant.tsx";
|
|
490
|
+
|
|
486
491
|
// A barra de ferramentas que flutua sobre uma superfície de trabalho (canvas, editor, preview).
|
|
487
492
|
export {
|
|
488
493
|
Dock,
|