@softize/opus 15.2.2 → 16.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 +51 -18
- package/bin/lib/check.mjs +98 -15
- package/bin/lib/copy.mjs +811 -314
- package/bin/lib/gen-manifest.mjs +24 -23
- package/bin/lib/gen-runner.mjs +188 -148
- package/docs/adr/0010-page-header-owns-page-chrome.md +3 -4
- package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +5 -3
- package/docs/adr/0012-modal-header-only-names-the-surface.md +45 -0
- package/docs/adr/0013-presentation-is-a-portable-action-oriented-artifact.md +92 -0
- package/docs/code-style.md +24 -19
- package/package.json +5 -1
- package/registry/skills/build-opus-ui/references/ui-patterns.md +43 -26
- package/src/core/presentation.ts +512 -0
- package/src/presentation/index.ts +1 -0
- package/src/ui/components/patterns/action-list-dialog.tsx +26 -9
- package/src/ui/components/patterns/confirm.tsx +34 -29
- package/src/ui/components/patterns/form-dialog.tsx +20 -8
- package/src/ui/components/patterns/list.tsx +26 -9
- package/src/ui/components/patterns/page.tsx +99 -139
- package/src/ui/components/patterns/presentation.tsx +316 -0
- package/src/ui/components/patterns/sidebar.tsx +3 -3
- package/src/ui/components/patterns/trigger.tsx +38 -17
- package/src/ui/components/primitives/button-group.tsx +53 -43
- package/src/ui/components/primitives/command.tsx +30 -72
- package/src/ui/components/primitives/dialog.tsx +23 -89
- package/src/ui/components/primitives/drawer.tsx +8 -34
- package/src/ui/docs/content/action-form-dialog.md +12 -10
- package/src/ui/docs/content/action-list-dialog.md +22 -17
- package/src/ui/docs/content/button.md +52 -35
- package/src/ui/docs/content/communication.md +26 -26
- package/src/ui/docs/content/dialog.md +173 -154
- package/src/ui/docs/content/drawer.md +12 -11
- package/src/ui/docs/content/page.md +72 -91
- package/src/ui/docs/content/presentation.md +158 -0
- package/src/ui/docs/registry.tsx +6 -0
- package/src/ui/meta.ts +8 -2
- package/src/ui/react.tsx +10 -3
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ActionContract } from "./contracts.ts";
|
|
3
|
+
|
|
4
|
+
const identifierSchema = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/);
|
|
5
|
+
|
|
6
|
+
export type PresentationJsonValue =
|
|
7
|
+
| string
|
|
8
|
+
| number
|
|
9
|
+
| boolean
|
|
10
|
+
| null
|
|
11
|
+
| PresentationJsonValue[]
|
|
12
|
+
| { [key: string]: PresentationJsonValue };
|
|
13
|
+
|
|
14
|
+
export const presentationJsonValueSchema: z.ZodType<PresentationJsonValue> =
|
|
15
|
+
z.lazy(() =>
|
|
16
|
+
z.union([
|
|
17
|
+
z.string(),
|
|
18
|
+
z.number().finite(),
|
|
19
|
+
z.boolean(),
|
|
20
|
+
z.null(),
|
|
21
|
+
z.array(presentationJsonValueSchema),
|
|
22
|
+
z.record(presentationJsonValueSchema),
|
|
23
|
+
]),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const presentationInputSchema = z.record(presentationJsonValueSchema);
|
|
27
|
+
|
|
28
|
+
export const presentationSurfaceSchema = z.enum(["page", "dialog", "drawer"]);
|
|
29
|
+
export type PresentationSurface = z.infer<typeof presentationSurfaceSchema>;
|
|
30
|
+
|
|
31
|
+
export const presentationBindingSchema = z.discriminatedUnion("source", [
|
|
32
|
+
z.object({ source: z.literal("route"), param: identifierSchema }).strict(),
|
|
33
|
+
z.object({ source: z.literal("record"), field: z.string().min(1) }).strict(),
|
|
34
|
+
z.object({ source: z.literal("item"), field: z.string().min(1) }).strict(),
|
|
35
|
+
z.object({ source: z.literal("selection") }).strict(),
|
|
36
|
+
z.object({ source: z.literal("session"), field: z.string().min(1) }).strict(),
|
|
37
|
+
z.object({ source: z.literal("result"), field: z.string().min(1) }).strict(),
|
|
38
|
+
z
|
|
39
|
+
.object({ source: z.literal("fixed"), value: presentationJsonValueSchema })
|
|
40
|
+
.strict(),
|
|
41
|
+
z.object({ source: z.literal("now") }).strict(),
|
|
42
|
+
]);
|
|
43
|
+
export type PresentationBinding = z.infer<typeof presentationBindingSchema>;
|
|
44
|
+
|
|
45
|
+
export const presentationEffectSchema = z.discriminatedUnion("effect", [
|
|
46
|
+
z
|
|
47
|
+
.object({
|
|
48
|
+
effect: z.literal("refresh"),
|
|
49
|
+
action: z.string().min(1).optional(),
|
|
50
|
+
})
|
|
51
|
+
.strict(),
|
|
52
|
+
z.object({ effect: z.literal("close") }).strict(),
|
|
53
|
+
z.object({ effect: z.literal("back") }).strict(),
|
|
54
|
+
z
|
|
55
|
+
.object({
|
|
56
|
+
effect: z.literal("navigate"),
|
|
57
|
+
presentation: identifierSchema,
|
|
58
|
+
surface: presentationSurfaceSchema.optional(),
|
|
59
|
+
input: z.record(presentationBindingSchema).default({}),
|
|
60
|
+
mode: z.enum(["push", "replace"]).default("push"),
|
|
61
|
+
})
|
|
62
|
+
.strict(),
|
|
63
|
+
]);
|
|
64
|
+
export type PresentationEffect = z.infer<typeof presentationEffectSchema>;
|
|
65
|
+
export type PresentationEffectInput = z.input<typeof presentationEffectSchema>;
|
|
66
|
+
|
|
67
|
+
const presentationTargetSchema = z
|
|
68
|
+
.object({
|
|
69
|
+
presentation: identifierSchema,
|
|
70
|
+
surface: presentationSurfaceSchema.optional(),
|
|
71
|
+
})
|
|
72
|
+
.strict();
|
|
73
|
+
|
|
74
|
+
const presentationCommandSchema = z
|
|
75
|
+
.object({
|
|
76
|
+
action: z.string().min(1),
|
|
77
|
+
placement: z.enum(["header", "footer"]),
|
|
78
|
+
input: z.record(presentationBindingSchema).default({}),
|
|
79
|
+
target: presentationTargetSchema.optional(),
|
|
80
|
+
blocking: z.enum(["action", "group", "surface"]).default("action"),
|
|
81
|
+
onSuccess: z.array(presentationEffectSchema).default([]),
|
|
82
|
+
})
|
|
83
|
+
.strict();
|
|
84
|
+
|
|
85
|
+
const presentationFieldSchema = z
|
|
86
|
+
.object({
|
|
87
|
+
key: z.string().min(1),
|
|
88
|
+
label: z.string().min(1),
|
|
89
|
+
empty: z.string().min(1).optional(),
|
|
90
|
+
})
|
|
91
|
+
.strict();
|
|
92
|
+
|
|
93
|
+
const presentationBodySchema = z
|
|
94
|
+
.object({
|
|
95
|
+
action: z.string().min(1),
|
|
96
|
+
input: z.record(presentationBindingSchema).default({}),
|
|
97
|
+
fields: z.array(presentationFieldSchema).min(1).optional(),
|
|
98
|
+
open: z
|
|
99
|
+
.object({
|
|
100
|
+
presentation: identifierSchema,
|
|
101
|
+
surface: presentationSurfaceSchema.optional(),
|
|
102
|
+
input: z.record(presentationBindingSchema).default({}),
|
|
103
|
+
})
|
|
104
|
+
.strict()
|
|
105
|
+
.optional(),
|
|
106
|
+
})
|
|
107
|
+
.strict();
|
|
108
|
+
|
|
109
|
+
export const presentationSchema = z
|
|
110
|
+
.object({
|
|
111
|
+
schemaVersion: z.literal(1),
|
|
112
|
+
id: identifierSchema,
|
|
113
|
+
title: z.string().min(1),
|
|
114
|
+
body: presentationBodySchema,
|
|
115
|
+
actions: z.array(presentationCommandSchema).default([]),
|
|
116
|
+
})
|
|
117
|
+
.strict();
|
|
118
|
+
|
|
119
|
+
export type PresentationDefinition = z.infer<typeof presentationSchema>;
|
|
120
|
+
export type PresentationDefinitionInput = z.input<typeof presentationSchema>;
|
|
121
|
+
export type PresentationCommand = PresentationDefinition["actions"][number];
|
|
122
|
+
type DefaultedProperty<Input, Key extends PropertyKey, Output> =
|
|
123
|
+
Key extends keyof Input
|
|
124
|
+
? undefined extends Input[Key]
|
|
125
|
+
? Exclude<Input[Key], undefined> | Output
|
|
126
|
+
: Input[Key] & Output
|
|
127
|
+
: Output;
|
|
128
|
+
type NavigateEffect = Extract<PresentationEffect, { effect: "navigate" }>;
|
|
129
|
+
type DefinedPresentationEffect<Effect> = Effect extends { effect: "navigate" }
|
|
130
|
+
? Omit<Effect, "input" | "mode"> &
|
|
131
|
+
Omit<NavigateEffect, "input" | "mode"> & {
|
|
132
|
+
input: DefaultedProperty<Effect, "input", NavigateEffect["input"]>;
|
|
133
|
+
mode: DefaultedProperty<Effect, "mode", NavigateEffect["mode"]>;
|
|
134
|
+
}
|
|
135
|
+
: Effect & PresentationEffect;
|
|
136
|
+
type DefinedPresentationEffects<Input, Key extends PropertyKey> =
|
|
137
|
+
Key extends keyof Input
|
|
138
|
+
? undefined extends Input[Key]
|
|
139
|
+
? PresentationCommand["onSuccess"]
|
|
140
|
+
: Input[Key] extends readonly unknown[]
|
|
141
|
+
? {
|
|
142
|
+
-readonly [Index in keyof Input[Key]]: DefinedPresentationEffect<
|
|
143
|
+
Input[Key][Index]
|
|
144
|
+
>;
|
|
145
|
+
}
|
|
146
|
+
: PresentationCommand["onSuccess"]
|
|
147
|
+
: PresentationCommand["onSuccess"];
|
|
148
|
+
type PresentationOpen = NonNullable<PresentationDefinition["body"]["open"]>;
|
|
149
|
+
type DefinedPresentationOpen<Open> = Open extends object
|
|
150
|
+
? Omit<Open, "input"> &
|
|
151
|
+
Omit<PresentationOpen, "input"> & {
|
|
152
|
+
input: DefaultedProperty<Open, "input", PresentationOpen["input"]>;
|
|
153
|
+
}
|
|
154
|
+
: never;
|
|
155
|
+
type DefinedPresentationBody<Body extends PresentationDefinitionInput["body"]> =
|
|
156
|
+
Omit<Body, "input" | "open"> &
|
|
157
|
+
Omit<PresentationDefinition["body"], "input" | "open"> & {
|
|
158
|
+
input: DefaultedProperty<
|
|
159
|
+
Body,
|
|
160
|
+
"input",
|
|
161
|
+
PresentationDefinition["body"]["input"]
|
|
162
|
+
>;
|
|
163
|
+
} & ("open" extends keyof Body
|
|
164
|
+
? undefined extends Body["open"]
|
|
165
|
+
? { open?: DefinedPresentationOpen<Exclude<Body["open"], undefined>> }
|
|
166
|
+
: { open: DefinedPresentationOpen<Body["open"]> }
|
|
167
|
+
: { open?: PresentationDefinition["body"]["open"] });
|
|
168
|
+
type DefinedPresentationCommand<Command> = Command extends object
|
|
169
|
+
? Omit<Command, "input" | "blocking" | "onSuccess"> &
|
|
170
|
+
Omit<PresentationCommand, "input" | "blocking" | "onSuccess"> & {
|
|
171
|
+
input: DefaultedProperty<Command, "input", PresentationCommand["input"]>;
|
|
172
|
+
blocking: DefaultedProperty<
|
|
173
|
+
Command,
|
|
174
|
+
"blocking",
|
|
175
|
+
PresentationCommand["blocking"]
|
|
176
|
+
>;
|
|
177
|
+
onSuccess: DefinedPresentationEffects<Command, "onSuccess">;
|
|
178
|
+
}
|
|
179
|
+
: never;
|
|
180
|
+
type DefinedPresentationActions<T extends PresentationDefinitionInput> =
|
|
181
|
+
T extends { actions: infer Actions extends readonly unknown[] }
|
|
182
|
+
? {
|
|
183
|
+
-readonly [Index in keyof Actions]: DefinedPresentationCommand<
|
|
184
|
+
Actions[Index]
|
|
185
|
+
>;
|
|
186
|
+
}
|
|
187
|
+
: PresentationDefinition["actions"];
|
|
188
|
+
export type DefinedPresentation<T extends PresentationDefinitionInput> = Omit<
|
|
189
|
+
T,
|
|
190
|
+
"body" | "actions"
|
|
191
|
+
> &
|
|
192
|
+
Omit<PresentationDefinition, "body" | "actions"> & {
|
|
193
|
+
body: DefinedPresentationBody<T["body"]>;
|
|
194
|
+
actions: DefinedPresentationActions<T>;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export const presentationFrameSchema = z
|
|
198
|
+
.object({
|
|
199
|
+
presentationId: identifierSchema,
|
|
200
|
+
surface: presentationSurfaceSchema,
|
|
201
|
+
input: presentationInputSchema.default({}),
|
|
202
|
+
})
|
|
203
|
+
.strict();
|
|
204
|
+
|
|
205
|
+
export const presentationInvocationSchema = presentationFrameSchema
|
|
206
|
+
.extend({
|
|
207
|
+
schemaVersion: z.literal(1),
|
|
208
|
+
stack: z.array(presentationFrameSchema).default([]),
|
|
209
|
+
})
|
|
210
|
+
.strict();
|
|
211
|
+
|
|
212
|
+
export type PresentationFrame = z.infer<typeof presentationFrameSchema>;
|
|
213
|
+
export type PresentationInvocation = z.infer<
|
|
214
|
+
typeof presentationInvocationSchema
|
|
215
|
+
>;
|
|
216
|
+
|
|
217
|
+
export interface PresentationTransition {
|
|
218
|
+
invocation: PresentationInvocation | null;
|
|
219
|
+
refresh: Array<string | null>;
|
|
220
|
+
exit?: "back" | "close";
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface PresentationDiagnostic {
|
|
224
|
+
level: "info" | "warning" | "error";
|
|
225
|
+
code: string;
|
|
226
|
+
message: string;
|
|
227
|
+
path?: string;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface PresentationInspectionSnapshot {
|
|
231
|
+
definition: unknown;
|
|
232
|
+
invocation?: unknown;
|
|
233
|
+
resolved?: unknown;
|
|
234
|
+
diagnostics: PresentationDiagnostic[];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface PresentationActionRegistry {
|
|
238
|
+
readonly [name: string]: ActionContract;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface PresentationBindingContext {
|
|
242
|
+
route?: Readonly<Record<string, unknown>>;
|
|
243
|
+
record?: Readonly<Record<string, unknown>>;
|
|
244
|
+
item?: Readonly<Record<string, unknown>>;
|
|
245
|
+
selection?: readonly unknown[];
|
|
246
|
+
session?: Readonly<Record<string, unknown>>;
|
|
247
|
+
result?: Readonly<Record<string, unknown>>;
|
|
248
|
+
now?: () => Date;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function definePresentation<const T extends PresentationDefinitionInput>(
|
|
252
|
+
definition: T,
|
|
253
|
+
): DefinedPresentation<T> {
|
|
254
|
+
return presentationSchema.parse(definition) as unknown as DefinedPresentation<T>;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function definePresentationInvocation(
|
|
258
|
+
invocation: z.input<typeof presentationInvocationSchema>,
|
|
259
|
+
): PresentationInvocation {
|
|
260
|
+
return presentationInvocationSchema.parse(invocation);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function frameFromInvocation(
|
|
264
|
+
invocation: PresentationInvocation,
|
|
265
|
+
): PresentationFrame {
|
|
266
|
+
return {
|
|
267
|
+
presentationId: invocation.presentationId,
|
|
268
|
+
surface: invocation.surface,
|
|
269
|
+
input: invocation.input,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function openPresentation(
|
|
274
|
+
invocation: PresentationInvocation,
|
|
275
|
+
target: PresentationFrame,
|
|
276
|
+
mode: "push" | "replace" = "push",
|
|
277
|
+
): PresentationInvocation {
|
|
278
|
+
const parsedTarget = presentationFrameSchema.parse(target);
|
|
279
|
+
return definePresentationInvocation({
|
|
280
|
+
schemaVersion: 1,
|
|
281
|
+
...parsedTarget,
|
|
282
|
+
stack:
|
|
283
|
+
mode === "push"
|
|
284
|
+
? [...invocation.stack, frameFromInvocation(invocation)]
|
|
285
|
+
: invocation.stack,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function backPresentation(
|
|
290
|
+
invocation: PresentationInvocation,
|
|
291
|
+
): PresentationInvocation | null {
|
|
292
|
+
const parent = invocation.stack.at(-1);
|
|
293
|
+
if (parent === undefined) return null;
|
|
294
|
+
return definePresentationInvocation({
|
|
295
|
+
schemaVersion: 1,
|
|
296
|
+
...parent,
|
|
297
|
+
stack: invocation.stack.slice(0, -1),
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function closePresentation(invocation: PresentationInvocation): null {
|
|
302
|
+
if (invocation.surface === "page") {
|
|
303
|
+
throw new Error(
|
|
304
|
+
"Uma Presentation em page não pode receber o efeito close.",
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function applyPresentationEffects(
|
|
311
|
+
invocation: PresentationInvocation,
|
|
312
|
+
effects: readonly PresentationEffectInput[],
|
|
313
|
+
context: PresentationBindingContext = {},
|
|
314
|
+
): PresentationTransition {
|
|
315
|
+
let current: PresentationInvocation | null = invocation;
|
|
316
|
+
const refresh: Array<string | null> = [];
|
|
317
|
+
let exit: PresentationTransition["exit"];
|
|
318
|
+
|
|
319
|
+
for (const rawEffect of effects) {
|
|
320
|
+
const effect = presentationEffectSchema.parse(rawEffect);
|
|
321
|
+
if (effect.effect === "refresh") {
|
|
322
|
+
refresh.push(effect.action ?? null);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (current === null) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
`O efeito ${effect.effect} não possui Presentation ativa.`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
if (effect.effect === "close") {
|
|
331
|
+
current = closePresentation(current);
|
|
332
|
+
exit = "close";
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (effect.effect === "back") {
|
|
336
|
+
current = backPresentation(current);
|
|
337
|
+
if (current === null) exit = "back";
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
current = openPresentation(
|
|
341
|
+
current,
|
|
342
|
+
{
|
|
343
|
+
presentationId: effect.presentation,
|
|
344
|
+
surface: effect.surface ?? current.surface,
|
|
345
|
+
input: resolvePresentationBindings(effect.input, context) as Record<
|
|
346
|
+
string,
|
|
347
|
+
PresentationJsonValue
|
|
348
|
+
>,
|
|
349
|
+
},
|
|
350
|
+
effect.mode,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
invocation: current,
|
|
356
|
+
refresh,
|
|
357
|
+
...(exit === undefined ? {} : { exit }),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const sensitiveKeyPattern =
|
|
362
|
+
/(?:authorization|cookie|password|secret|token|api[-_]?key)/iu;
|
|
363
|
+
|
|
364
|
+
export function redactPresentationInspectionValue(
|
|
365
|
+
value: unknown,
|
|
366
|
+
key = "",
|
|
367
|
+
): unknown {
|
|
368
|
+
if (sensitiveKeyPattern.test(key)) return "[REDACTED]";
|
|
369
|
+
if (Array.isArray(value)) {
|
|
370
|
+
return value.map((item) => redactPresentationInspectionValue(item));
|
|
371
|
+
}
|
|
372
|
+
if (value !== null && typeof value === "object") {
|
|
373
|
+
return Object.fromEntries(
|
|
374
|
+
Object.entries(value).map(([entryKey, entryValue]) => [
|
|
375
|
+
entryKey,
|
|
376
|
+
redactPresentationInspectionValue(entryValue, entryKey),
|
|
377
|
+
]),
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
return value;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function createPresentationInspectionSnapshot(input: {
|
|
384
|
+
definition: unknown;
|
|
385
|
+
invocation?: unknown;
|
|
386
|
+
resolved?: unknown;
|
|
387
|
+
diagnostics?: readonly PresentationDiagnostic[];
|
|
388
|
+
}): PresentationInspectionSnapshot {
|
|
389
|
+
return redactPresentationInspectionValue({
|
|
390
|
+
definition: input.definition,
|
|
391
|
+
...(input.invocation === undefined ? {} : { invocation: input.invocation }),
|
|
392
|
+
...(input.resolved === undefined ? {} : { resolved: input.resolved }),
|
|
393
|
+
diagnostics: input.diagnostics ?? [],
|
|
394
|
+
}) as PresentationInspectionSnapshot;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function validatePresentations(
|
|
398
|
+
input: unknown,
|
|
399
|
+
actions: PresentationActionRegistry,
|
|
400
|
+
): PresentationDefinition[] {
|
|
401
|
+
const parsed = z.array(presentationSchema).parse(input);
|
|
402
|
+
const byId = new Map<string, PresentationDefinition>();
|
|
403
|
+
const problems: string[] = [];
|
|
404
|
+
|
|
405
|
+
for (const presentation of parsed) {
|
|
406
|
+
if (byId.has(presentation.id))
|
|
407
|
+
problems.push(`Presentation “${presentation.id}” duplicada.`);
|
|
408
|
+
byId.set(presentation.id, presentation);
|
|
409
|
+
|
|
410
|
+
const bodyAction = actions[presentation.body.action];
|
|
411
|
+
if (bodyAction === undefined) {
|
|
412
|
+
problems.push(
|
|
413
|
+
`A action “${presentation.body.action}” não está disponível.`,
|
|
414
|
+
);
|
|
415
|
+
} else if (bodyAction.kind === "simple") {
|
|
416
|
+
problems.push(
|
|
417
|
+
`A action simple “${bodyAction.name}” não pode ocupar o body.`,
|
|
418
|
+
);
|
|
419
|
+
} else {
|
|
420
|
+
if (
|
|
421
|
+
bodyAction.kind === "view" &&
|
|
422
|
+
presentation.body.fields === undefined
|
|
423
|
+
) {
|
|
424
|
+
problems.push(
|
|
425
|
+
`A Presentation “${presentation.id}” precisa declarar fields para a view.`,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
if (
|
|
429
|
+
bodyAction.kind !== "view" &&
|
|
430
|
+
presentation.body.fields !== undefined
|
|
431
|
+
) {
|
|
432
|
+
problems.push(`Fields só podem ser declarados para uma action view.`);
|
|
433
|
+
}
|
|
434
|
+
if (bodyAction.kind !== "list" && presentation.body.open !== undefined) {
|
|
435
|
+
problems.push(`Open só pode ser declarado para uma action list.`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
for (const command of presentation.actions) {
|
|
440
|
+
const action = actions[command.action];
|
|
441
|
+
if (action === undefined) {
|
|
442
|
+
problems.push(`A action “${command.action}” não está disponível.`);
|
|
443
|
+
} else if (action.kind === "simple" && command.target !== undefined) {
|
|
444
|
+
problems.push(
|
|
445
|
+
`A action simple “${command.action}” executa diretamente e não aceita target.`,
|
|
446
|
+
);
|
|
447
|
+
} else if (action.kind !== "simple" && command.target === undefined) {
|
|
448
|
+
problems.push(
|
|
449
|
+
`A action ${action.kind} “${command.action}” precisa abrir outra Presentation.`,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
for (const presentation of parsed) {
|
|
456
|
+
const open = presentation.body.open;
|
|
457
|
+
if (open !== undefined && !byId.has(open.presentation)) {
|
|
458
|
+
problems.push(
|
|
459
|
+
`A lista “${presentation.id}” abre a Presentation inexistente “${open.presentation}”.`,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
for (const command of presentation.actions) {
|
|
463
|
+
for (const effect of command.onSuccess) {
|
|
464
|
+
if (effect.effect === "navigate" && !byId.has(effect.presentation)) {
|
|
465
|
+
problems.push(
|
|
466
|
+
`A action “${command.action}” navega para a Presentation inexistente “${effect.presentation}”.`,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (command.target === undefined) continue;
|
|
471
|
+
const target = byId.get(command.target.presentation);
|
|
472
|
+
if (target === undefined) {
|
|
473
|
+
problems.push(
|
|
474
|
+
`A action “${command.action}” abre a Presentation inexistente “${command.target.presentation}”.`,
|
|
475
|
+
);
|
|
476
|
+
} else if (target.body.action !== command.action) {
|
|
477
|
+
problems.push(
|
|
478
|
+
`A Presentation “${target.id}” usa “${target.body.action}”, mas foi aberta por “${command.action}”.`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (problems.length > 0) throw new Error(problems.join("\n"));
|
|
485
|
+
return parsed;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export function resolvePresentationBinding(
|
|
489
|
+
binding: PresentationBinding,
|
|
490
|
+
context: PresentationBindingContext,
|
|
491
|
+
): unknown {
|
|
492
|
+
if (binding.source === "route") return context.route?.[binding.param];
|
|
493
|
+
if (binding.source === "record") return context.record?.[binding.field];
|
|
494
|
+
if (binding.source === "item") return context.item?.[binding.field];
|
|
495
|
+
if (binding.source === "selection") return context.selection;
|
|
496
|
+
if (binding.source === "session") return context.session?.[binding.field];
|
|
497
|
+
if (binding.source === "result") return context.result?.[binding.field];
|
|
498
|
+
if (binding.source === "fixed") return binding.value;
|
|
499
|
+
return (context.now ?? (() => new Date()))().toISOString();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export function resolvePresentationBindings(
|
|
503
|
+
bindings: Readonly<Record<string, PresentationBinding>>,
|
|
504
|
+
context: PresentationBindingContext,
|
|
505
|
+
): Record<string, unknown> {
|
|
506
|
+
return Object.fromEntries(
|
|
507
|
+
Object.entries(bindings).map(([key, binding]) => [
|
|
508
|
+
key,
|
|
509
|
+
resolvePresentationBinding(binding, context),
|
|
510
|
+
]),
|
|
511
|
+
);
|
|
512
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../core/presentation.ts";
|
|
@@ -11,20 +11,22 @@
|
|
|
11
11
|
* - `empty` sobrepõe o vazio derivado (ex.: form inline de criar aberto ⇒ children).
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type
|
|
14
|
+
import { useId, type ReactNode } from 'react'
|
|
15
15
|
import { cn } from '../../lib/cn.ts'
|
|
16
16
|
import {
|
|
17
17
|
Dialog,
|
|
18
18
|
DialogBody,
|
|
19
19
|
DialogContent,
|
|
20
|
-
DialogDescription,
|
|
21
20
|
DialogHeader,
|
|
22
21
|
DialogTitle,
|
|
23
22
|
} from '../primitives/dialog.tsx'
|
|
24
23
|
import { ActionList } from './list.tsx'
|
|
25
24
|
import type { ListAction } from '../../../core/index.ts'
|
|
26
25
|
|
|
27
|
-
export interface ActionListDialogProps<
|
|
26
|
+
export interface ActionListDialogProps<
|
|
27
|
+
TItem,
|
|
28
|
+
TInput extends Record<string, unknown> = Record<string, unknown>,
|
|
29
|
+
> {
|
|
28
30
|
/** A ListAction do Opus (estrutural: name + kind — o mesmo contrato da família). */
|
|
29
31
|
action: { name: string; kind: 'list' }
|
|
30
32
|
/** Input da action (o escopo base; ex.: { workspaceId }). Mudou, re-busca. */
|
|
@@ -32,7 +34,8 @@ export interface ActionListDialogProps<TItem, TInput extends Record<string, unkn
|
|
|
32
34
|
open: boolean
|
|
33
35
|
onOpenChange: (open: boolean) => void
|
|
34
36
|
title: string
|
|
35
|
-
|
|
37
|
+
/** Conteúdo relevante apresentado antes da lista, como texto ou Alert. */
|
|
38
|
+
intro?: ReactNode
|
|
36
39
|
/** Nota à esquerda da linha de ações (contexto curto). O total já mora no rodapé. */
|
|
37
40
|
note?: ReactNode
|
|
38
41
|
/** Ação à direita da linha — em geral o botão de criar. */
|
|
@@ -51,13 +54,16 @@ export interface ActionListDialogProps<TItem, TInput extends Record<string, unkn
|
|
|
51
54
|
children: (items: TItem[], refetch: () => Promise<void>) => ReactNode
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
export function ActionListDialog<
|
|
57
|
+
export function ActionListDialog<
|
|
58
|
+
TItem,
|
|
59
|
+
TInput extends Record<string, unknown> = Record<string, unknown>,
|
|
60
|
+
>({
|
|
55
61
|
action,
|
|
56
62
|
input,
|
|
57
63
|
open,
|
|
58
64
|
onOpenChange,
|
|
59
65
|
title,
|
|
60
|
-
|
|
66
|
+
intro,
|
|
61
67
|
note,
|
|
62
68
|
actions,
|
|
63
69
|
emptyMessage,
|
|
@@ -68,16 +74,27 @@ export function ActionListDialog<TItem, TInput extends Record<string, unknown> =
|
|
|
68
74
|
className,
|
|
69
75
|
children,
|
|
70
76
|
}: ActionListDialogProps<TItem, TInput>): React.ReactElement {
|
|
77
|
+
const introId = useId()
|
|
71
78
|
return (
|
|
72
79
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
73
|
-
<DialogContent
|
|
80
|
+
<DialogContent
|
|
81
|
+
className={cn('sm:max-w-3xl', className)}
|
|
82
|
+
aria-describedby={intro === undefined ? undefined : introId}
|
|
83
|
+
>
|
|
74
84
|
<DialogHeader>
|
|
75
85
|
<DialogTitle>{title}</DialogTitle>
|
|
76
|
-
{description !== undefined && <DialogDescription>{description}</DialogDescription>}
|
|
77
86
|
</DialogHeader>
|
|
78
87
|
<DialogBody>
|
|
88
|
+
{intro !== undefined && (
|
|
89
|
+
<div id={introId} className="mb-4">
|
|
90
|
+
{intro}
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
79
93
|
{(note !== undefined || actions !== undefined) && (
|
|
80
|
-
<div
|
|
94
|
+
<div
|
|
95
|
+
data-slot="action-list-dialog-toolbar"
|
|
96
|
+
className="mb-2 flex min-h-8 items-center justify-between gap-4"
|
|
97
|
+
>
|
|
81
98
|
<div className="min-w-0 text-xs text-muted-foreground/70">{note}</div>
|
|
82
99
|
{actions}
|
|
83
100
|
</div>
|