@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
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import type { ActionContract } from "../../../core/contracts.ts";
|
|
2
|
+
import { EmptyValue } from "../primitives/empty-value.tsx";
|
|
3
|
+
import {
|
|
4
|
+
matchesPresentationConditions,
|
|
5
|
+
meetsPresentationRequirement,
|
|
6
|
+
} from "../../../core/presentation.ts";
|
|
7
|
+
import type { SelectOption } from "../primitives/select.tsx";
|
|
8
|
+
import type { FormContract, ListContract } from "../../../core/contracts.ts";
|
|
9
|
+
import { Fragment, type ReactNode } from "react";
|
|
10
|
+
import type {
|
|
11
|
+
PresentationBindingContext,
|
|
12
|
+
PresentationListSection,
|
|
13
|
+
PresentationSectionsBody,
|
|
14
|
+
} from "../../../core/presentation.ts";
|
|
15
|
+
import type { ListDataSource } from "../../drivers/react.tsx";
|
|
16
|
+
import {
|
|
17
|
+
Item,
|
|
18
|
+
ItemActions,
|
|
19
|
+
ItemBody,
|
|
20
|
+
ItemDescription,
|
|
21
|
+
ItemGroup,
|
|
22
|
+
ItemHeader,
|
|
23
|
+
ItemSeparator,
|
|
24
|
+
ItemTitle,
|
|
25
|
+
} from "../primitives/item.tsx";
|
|
26
|
+
import { Surface, SurfaceBody } from "./surface.tsx";
|
|
27
|
+
import { FillBoundary, useFillLeaf } from "./fill-region.ts";
|
|
28
|
+
import {
|
|
29
|
+
ActionList,
|
|
30
|
+
type ActionListProps,
|
|
31
|
+
type ListActionLike,
|
|
32
|
+
} from "./list.tsx";
|
|
33
|
+
|
|
34
|
+
export interface PresentationListAdapter {
|
|
35
|
+
requires?: ActionContract<unknown, unknown>["requires"];
|
|
36
|
+
render: (section: PresentationListSection) => ReactNode;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PresentationCellsAdapter {
|
|
40
|
+
action: string;
|
|
41
|
+
cells: Record<string, (item: Record<string, unknown>) => ReactNode>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Vincula células ao contrato que fornece os itens, sem serializar funções. */
|
|
45
|
+
export function bindPresentationCells<TInput, TItem>(
|
|
46
|
+
action: ListContract<TInput, TItem>,
|
|
47
|
+
cells: NonNullable<ActionListProps<TInput, TItem>["cells"]>,
|
|
48
|
+
): PresentationCellsAdapter {
|
|
49
|
+
return {
|
|
50
|
+
action: action.name,
|
|
51
|
+
cells: Object.fromEntries(
|
|
52
|
+
Object.entries(cells).map(([key, render]) => [
|
|
53
|
+
key,
|
|
54
|
+
(item: Record<string, unknown>) => render(item as TItem),
|
|
55
|
+
]),
|
|
56
|
+
),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface PresentationRowContentAdapter {
|
|
61
|
+
action: string;
|
|
62
|
+
render: (item: Record<string, unknown>) => ReactNode;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Conteúdo específico de uma linha; comandos comuns continuam na declaração. */
|
|
66
|
+
export function bindPresentationRowContent<TInput, TItem>(
|
|
67
|
+
action: ListContract<TInput, TItem>,
|
|
68
|
+
render: (item: TItem) => ReactNode,
|
|
69
|
+
): PresentationRowContentAdapter {
|
|
70
|
+
return { action: action.name, render: (item) => render(item as TItem) };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface PresentationCollectionControls<TItem> {
|
|
74
|
+
onOpen: (item: TItem) => void;
|
|
75
|
+
canOpen: (item: TItem) => boolean;
|
|
76
|
+
actions: (item: TItem) => ReactNode;
|
|
77
|
+
refetch: () => Promise<void>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface PresentationCollectionAdapter {
|
|
81
|
+
action: string;
|
|
82
|
+
render: (
|
|
83
|
+
items: Record<string, unknown>[],
|
|
84
|
+
controls: PresentationCollectionControls<Record<string, unknown>>,
|
|
85
|
+
) => ReactNode;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Apresenta os resultados sem substituir consulta, comandos ou navegação da lista. */
|
|
89
|
+
export function bindPresentationCollection<TInput, TItem>(
|
|
90
|
+
action: ListContract<TInput, TItem>,
|
|
91
|
+
render: (
|
|
92
|
+
items: TItem[],
|
|
93
|
+
controls: PresentationCollectionControls<TItem>,
|
|
94
|
+
) => ReactNode,
|
|
95
|
+
): PresentationCollectionAdapter {
|
|
96
|
+
return {
|
|
97
|
+
action: action.name,
|
|
98
|
+
render: (items, controls) =>
|
|
99
|
+
render(
|
|
100
|
+
items as TItem[],
|
|
101
|
+
controls as unknown as PresentationCollectionControls<TItem>,
|
|
102
|
+
),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface PresentationDetailAdapter {
|
|
107
|
+
action: string;
|
|
108
|
+
render: (item: Record<string, unknown>, onClose: () => void) => ReactNode;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Vincula um detalhe especializado aos itens da lista; a seleção pertence ao renderer. */
|
|
112
|
+
export function bindPresentationDetail<TInput, TItem>(
|
|
113
|
+
action: ListContract<TInput, TItem>,
|
|
114
|
+
render: (item: TItem, onClose: () => void) => ReactNode,
|
|
115
|
+
): PresentationDetailAdapter {
|
|
116
|
+
return {
|
|
117
|
+
action: action.name,
|
|
118
|
+
render: (item, onClose) => render(item as TItem, onClose),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface PresentationOptionsAdapter {
|
|
123
|
+
action: string;
|
|
124
|
+
format: (item: Record<string, unknown>) => SelectOption;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Formata uma opção com dados do contrato, sem alterar a consulta nem o formulário. */
|
|
128
|
+
export function bindPresentationOptions<TInput, TItem>(
|
|
129
|
+
action: ListContract<TInput, TItem>,
|
|
130
|
+
format: (item: TItem) => SelectOption,
|
|
131
|
+
): PresentationOptionsAdapter {
|
|
132
|
+
return { action: action.name, format: (item) => format(item as TItem) };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface PresentationFieldProps {
|
|
136
|
+
name: string;
|
|
137
|
+
context: PresentationBindingContext;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface PresentationFieldAdapter {
|
|
141
|
+
action: string;
|
|
142
|
+
field: string;
|
|
143
|
+
render: (props: PresentationFieldProps) => ReactNode;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Vincula um controle ao campo do contrato; ActionForm preserva validação e envio. */
|
|
147
|
+
export function bindPresentationField<
|
|
148
|
+
TInput extends Record<string, unknown>,
|
|
149
|
+
TOutput,
|
|
150
|
+
>(
|
|
151
|
+
action: FormContract<TInput, TOutput>,
|
|
152
|
+
field: keyof TInput & string,
|
|
153
|
+
render: (props: PresentationFieldProps) => ReactNode,
|
|
154
|
+
): PresentationFieldAdapter {
|
|
155
|
+
return { action: action.name, field, render };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface PresentationListContentAdapter {
|
|
159
|
+
action: string;
|
|
160
|
+
render: (input: Record<string, unknown>) => ReactNode;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Conteúdo especializado recebe o mesmo recorte materializado pela lista. */
|
|
164
|
+
export function bindPresentationListContent<TInput, TItem>(
|
|
165
|
+
action: ListContract<TInput, TItem>,
|
|
166
|
+
render: (input: Partial<TInput>) => ReactNode,
|
|
167
|
+
): PresentationListContentAdapter {
|
|
168
|
+
return {
|
|
169
|
+
action: action.name,
|
|
170
|
+
render: (input) => render(input as Partial<TInput>),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface PresentationComponentProps<TInput = Record<string, unknown>> {
|
|
175
|
+
input: TInput;
|
|
176
|
+
context: PresentationBindingContext;
|
|
177
|
+
onClose: () => void;
|
|
178
|
+
footerTarget?: HTMLElement | null;
|
|
179
|
+
onBusyChange?: (busy: boolean) => void;
|
|
180
|
+
asideOpen?: boolean;
|
|
181
|
+
onAsideOpenChange?: (open: boolean) => void;
|
|
182
|
+
}
|
|
183
|
+
export interface PresentationComponentAdapter {
|
|
184
|
+
action?: string;
|
|
185
|
+
render: (props: PresentationComponentProps) => ReactNode;
|
|
186
|
+
}
|
|
187
|
+
/** Conteúdo específico vinculado ao contrato; estrutura e navegação pertencem à declaração. */
|
|
188
|
+
export function bindPresentationComponent<TInput, TOutput>(
|
|
189
|
+
action: ActionContract<TInput, TOutput>,
|
|
190
|
+
render: (props: PresentationComponentProps<TInput>) => ReactNode,
|
|
191
|
+
): PresentationComponentAdapter {
|
|
192
|
+
return {
|
|
193
|
+
action: action.name,
|
|
194
|
+
render: (props) => render({ ...props, input: props.input as TInput }),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Integração registrada pelo host, sem simular um contrato Opus para serviços externos. */
|
|
199
|
+
export function bindPresentationContent(
|
|
200
|
+
render: (props: PresentationComponentProps) => ReactNode,
|
|
201
|
+
): PresentationComponentAdapter {
|
|
202
|
+
return { render };
|
|
203
|
+
}
|
|
204
|
+
export interface PresentationRecordSource {
|
|
205
|
+
load: (
|
|
206
|
+
input: Record<string, unknown>,
|
|
207
|
+
options: { signal: AbortSignal },
|
|
208
|
+
) => Promise<Record<string, unknown>>;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface PresentationCompositionRuntime {
|
|
212
|
+
recordSources?: Readonly<Record<string, PresentationRecordSource>>;
|
|
213
|
+
components?: Readonly<Record<string, PresentationComponentAdapter>>;
|
|
214
|
+
/** Estado integral temporário; mantém a identidade e a geometria declaradas. */
|
|
215
|
+
state?: ReactNode;
|
|
216
|
+
asideOpen?: boolean;
|
|
217
|
+
presentations?: Readonly<Record<string, ReactNode>>;
|
|
218
|
+
tabs?: { value: string; onValueChange: (value: string) => void };
|
|
219
|
+
cells?: Readonly<Record<string, PresentationCellsAdapter>>;
|
|
220
|
+
details?: Readonly<Record<string, PresentationDetailAdapter>>;
|
|
221
|
+
listContent?: Readonly<Record<string, PresentationListContentAdapter>>;
|
|
222
|
+
rowContent?: Readonly<Record<string, PresentationRowContentAdapter>>;
|
|
223
|
+
collections?: Readonly<Record<string, PresentationCollectionAdapter>>;
|
|
224
|
+
fields?: Readonly<Record<string, PresentationFieldAdapter>>;
|
|
225
|
+
options?: Readonly<Record<string, PresentationOptionsAdapter>>;
|
|
226
|
+
icons?: Readonly<Record<string, ReactNode>>;
|
|
227
|
+
slots?: Readonly<Record<string, ReactNode>>;
|
|
228
|
+
lists?: Readonly<Record<string, PresentationListAdapter>>;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Mantém a tipagem dos itens no adaptador; a definição só transporta nomes de campos. */
|
|
232
|
+
export function bindPresentationList<TItem>(
|
|
233
|
+
source: ListDataSource<TItem>,
|
|
234
|
+
actions: Pick<
|
|
235
|
+
ActionListProps<Record<string, never>, TItem>,
|
|
236
|
+
"toolbarActions" | "rowActions"
|
|
237
|
+
> = {},
|
|
238
|
+
): PresentationListAdapter {
|
|
239
|
+
return {
|
|
240
|
+
render: (section) => (
|
|
241
|
+
<ActionList<Record<string, never>, TItem>
|
|
242
|
+
source={source}
|
|
243
|
+
input={{}}
|
|
244
|
+
layout="flow"
|
|
245
|
+
title={section.title}
|
|
246
|
+
item={section.item}
|
|
247
|
+
emptyMessage={section.emptyMessage}
|
|
248
|
+
errorMessage={section.errorMessage}
|
|
249
|
+
toolbarActions={actions.toolbarActions}
|
|
250
|
+
rowActions={actions.rowActions}
|
|
251
|
+
/>
|
|
252
|
+
),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Liga uma coleção de action à seção, preservando tipos e comportamento do contrato. */
|
|
257
|
+
export function bindPresentationActionList<TInput, TItem>(
|
|
258
|
+
action: ListActionLike,
|
|
259
|
+
props: Omit<ActionListProps<TInput, TItem>, "action" | "source">,
|
|
260
|
+
): PresentationListAdapter {
|
|
261
|
+
return {
|
|
262
|
+
requires: (
|
|
263
|
+
action as ListActionLike &
|
|
264
|
+
Pick<ActionContract<unknown, unknown>, "requires">
|
|
265
|
+
).requires,
|
|
266
|
+
render: (section) => (
|
|
267
|
+
<ActionList<TInput, TItem>
|
|
268
|
+
{...props}
|
|
269
|
+
action={action}
|
|
270
|
+
title={section.title ?? props.title}
|
|
271
|
+
item={section.item ?? props.item}
|
|
272
|
+
emptyMessage={section.emptyMessage ?? props.emptyMessage}
|
|
273
|
+
errorMessage={section.errorMessage ?? props.errorMessage}
|
|
274
|
+
/>
|
|
275
|
+
),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function PresentationContentRegion({ children }: { children: ReactNode }) {
|
|
280
|
+
const fill = useFillLeaf<HTMLDivElement>("fill");
|
|
281
|
+
return (
|
|
282
|
+
<div
|
|
283
|
+
ref={fill.ref}
|
|
284
|
+
data-slot="presentation-content-region"
|
|
285
|
+
className="flex min-h-0 flex-1 flex-col"
|
|
286
|
+
>
|
|
287
|
+
<FillBoundary>{children}</FillBoundary>
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function PresentationSections({
|
|
293
|
+
body,
|
|
294
|
+
runtime,
|
|
295
|
+
context,
|
|
296
|
+
can,
|
|
297
|
+
}: {
|
|
298
|
+
can?: (permission: string) => boolean;
|
|
299
|
+
context?: PresentationBindingContext;
|
|
300
|
+
body: PresentationSectionsBody;
|
|
301
|
+
runtime?: PresentationCompositionRuntime;
|
|
302
|
+
}): ReactNode {
|
|
303
|
+
const fill = useFillLeaf<HTMLDivElement>(body.aside ? "fill" : "flow");
|
|
304
|
+
function slot(id: string | undefined): ReactNode {
|
|
305
|
+
if (id === undefined) return null;
|
|
306
|
+
if (!Object.hasOwn(runtime?.slots ?? {}, id)) {
|
|
307
|
+
throw new Error(`Slot “${id}” da Presentation não registrado.`);
|
|
308
|
+
}
|
|
309
|
+
return runtime!.slots![id];
|
|
310
|
+
}
|
|
311
|
+
// Conferir todas as referências antes de iniciar consultas de qualquer seção.
|
|
312
|
+
for (const section of body.sections) {
|
|
313
|
+
slot(section.feedback);
|
|
314
|
+
if (section.kind === "content") {
|
|
315
|
+
slot(section.actions);
|
|
316
|
+
slot(section.content);
|
|
317
|
+
} else if (section.kind === "items") {
|
|
318
|
+
slot(section.intro);
|
|
319
|
+
slot(section.actions);
|
|
320
|
+
for (const item of section.items) slot(item.content);
|
|
321
|
+
} else if (
|
|
322
|
+
!Object.hasOwn(runtime?.lists ?? {}, section.source) ||
|
|
323
|
+
!runtime?.lists?.[section.source]
|
|
324
|
+
) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
`Fonte “${section.source}” da Presentation não registrada.`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
for (const id of body.overlays ?? []) slot(id);
|
|
331
|
+
slot(body.aside?.content);
|
|
332
|
+
|
|
333
|
+
const notices = (section: PresentationSectionsBody["sections"][number]) =>
|
|
334
|
+
section.notices
|
|
335
|
+
?.filter((notice) =>
|
|
336
|
+
matchesPresentationConditions(notice.when, context?.record ?? {}),
|
|
337
|
+
)
|
|
338
|
+
.map((notice, index) => (
|
|
339
|
+
<p key={index} className="text-sm">
|
|
340
|
+
{notice.text}
|
|
341
|
+
</p>
|
|
342
|
+
));
|
|
343
|
+
const itemContent = (
|
|
344
|
+
item: Extract<
|
|
345
|
+
PresentationSectionsBody["sections"][number],
|
|
346
|
+
{ kind: "items" }
|
|
347
|
+
>["items"][number],
|
|
348
|
+
) => {
|
|
349
|
+
if (!item.value) return slot(item.content);
|
|
350
|
+
const value = context?.record?.[item.value.field];
|
|
351
|
+
return (
|
|
352
|
+
<p className="whitespace-pre-wrap break-words text-sm">
|
|
353
|
+
{value == null || value === ""
|
|
354
|
+
? (item.value.empty ?? <EmptyValue />)
|
|
355
|
+
: `${String(value)}${item.value.suffix ?? ""}`}
|
|
356
|
+
</p>
|
|
357
|
+
);
|
|
358
|
+
};
|
|
359
|
+
const main = (
|
|
360
|
+
<Surface>
|
|
361
|
+
<SurfaceBody>
|
|
362
|
+
{body.sections.map((section) =>
|
|
363
|
+
section.kind === "content" ? (
|
|
364
|
+
section.title === undefined && section.actions === undefined ? (
|
|
365
|
+
<Fragment key={section.id}>
|
|
366
|
+
{section.layout === "fill" ? (
|
|
367
|
+
<PresentationContentRegion>
|
|
368
|
+
{slot(section.content)}
|
|
369
|
+
</PresentationContentRegion>
|
|
370
|
+
) : (
|
|
371
|
+
slot(section.content)
|
|
372
|
+
)}
|
|
373
|
+
{slot(section.feedback)}
|
|
374
|
+
{notices(section)}
|
|
375
|
+
</Fragment>
|
|
376
|
+
) : (
|
|
377
|
+
<Surface
|
|
378
|
+
key={section.id}
|
|
379
|
+
as="section"
|
|
380
|
+
title={section.title}
|
|
381
|
+
level={2}
|
|
382
|
+
actions={
|
|
383
|
+
section.actions === undefined
|
|
384
|
+
? undefined
|
|
385
|
+
: slot(section.actions)
|
|
386
|
+
}
|
|
387
|
+
>
|
|
388
|
+
{section.layout === "fill" ? (
|
|
389
|
+
<PresentationContentRegion>
|
|
390
|
+
{slot(section.content)}
|
|
391
|
+
</PresentationContentRegion>
|
|
392
|
+
) : (
|
|
393
|
+
slot(section.content)
|
|
394
|
+
)}
|
|
395
|
+
{slot(section.feedback)}
|
|
396
|
+
{notices(section)}
|
|
397
|
+
</Surface>
|
|
398
|
+
)
|
|
399
|
+
) : section.kind === "items" ? (
|
|
400
|
+
<Surface
|
|
401
|
+
key={section.id}
|
|
402
|
+
as="section"
|
|
403
|
+
title={section.title}
|
|
404
|
+
level={2}
|
|
405
|
+
actions={
|
|
406
|
+
section.actions === undefined
|
|
407
|
+
? undefined
|
|
408
|
+
: slot(section.actions)
|
|
409
|
+
}
|
|
410
|
+
>
|
|
411
|
+
{slot(section.intro)}
|
|
412
|
+
<ItemGroup variant="framed">
|
|
413
|
+
{section.items.map((item, index) => (
|
|
414
|
+
<Fragment key={item.id}>
|
|
415
|
+
{index > 0 && <ItemSeparator />}
|
|
416
|
+
<Item>
|
|
417
|
+
<ItemHeader>
|
|
418
|
+
<ItemTitle id={item.labelId}>{item.title}</ItemTitle>
|
|
419
|
+
{item.description && (
|
|
420
|
+
<ItemDescription>{item.description}</ItemDescription>
|
|
421
|
+
)}
|
|
422
|
+
</ItemHeader>
|
|
423
|
+
{(item.content !== undefined ||
|
|
424
|
+
item.value !== undefined) &&
|
|
425
|
+
(item.placement === "actions" ? (
|
|
426
|
+
<ItemActions>{itemContent(item)}</ItemActions>
|
|
427
|
+
) : (
|
|
428
|
+
<ItemBody
|
|
429
|
+
className={
|
|
430
|
+
item.placement === "below" ? "w-full" : "w-2/3"
|
|
431
|
+
}
|
|
432
|
+
>
|
|
433
|
+
{itemContent(item)}
|
|
434
|
+
</ItemBody>
|
|
435
|
+
))}
|
|
436
|
+
</Item>
|
|
437
|
+
</Fragment>
|
|
438
|
+
))}
|
|
439
|
+
</ItemGroup>
|
|
440
|
+
{slot(section.feedback)}
|
|
441
|
+
{notices(section)}
|
|
442
|
+
</Surface>
|
|
443
|
+
) : !meetsPresentationRequirement(
|
|
444
|
+
runtime!.lists![section.source]!.requires,
|
|
445
|
+
can,
|
|
446
|
+
) ? null : (
|
|
447
|
+
<Fragment key={section.id}>
|
|
448
|
+
{runtime!.lists![section.source]!.render(section)}
|
|
449
|
+
{slot(section.feedback)}
|
|
450
|
+
{notices(section)}
|
|
451
|
+
</Fragment>
|
|
452
|
+
),
|
|
453
|
+
)}
|
|
454
|
+
</SurfaceBody>
|
|
455
|
+
</Surface>
|
|
456
|
+
);
|
|
457
|
+
return (
|
|
458
|
+
<>
|
|
459
|
+
{body.aside ? (
|
|
460
|
+
<div
|
|
461
|
+
ref={fill.ref}
|
|
462
|
+
data-slot="presentation-columns"
|
|
463
|
+
className="grid min-h-0 flex-1 gap-4"
|
|
464
|
+
style={{
|
|
465
|
+
gridTemplateColumns:
|
|
466
|
+
runtime?.asideOpen === false
|
|
467
|
+
? "minmax(0, 1fr)"
|
|
468
|
+
: `minmax(0, 1fr) ${{ narrow: "20rem", default: "24rem", wide: "28rem" }[body.aside.width ?? "default"]}`,
|
|
469
|
+
}}
|
|
470
|
+
>
|
|
471
|
+
<div
|
|
472
|
+
data-slot="presentation-primary"
|
|
473
|
+
className="min-h-0 min-w-0 overflow-y-auto overscroll-contain"
|
|
474
|
+
>
|
|
475
|
+
<FillBoundary>{main}</FillBoundary>
|
|
476
|
+
</div>
|
|
477
|
+
<aside
|
|
478
|
+
aria-label={body.aside.label}
|
|
479
|
+
hidden={runtime?.asideOpen === false}
|
|
480
|
+
data-slot="presentation-aside"
|
|
481
|
+
className="min-h-0 min-w-0 overflow-hidden"
|
|
482
|
+
>
|
|
483
|
+
{slot(body.aside.content)}
|
|
484
|
+
</aside>
|
|
485
|
+
</div>
|
|
486
|
+
) : (
|
|
487
|
+
main
|
|
488
|
+
)}
|
|
489
|
+
{(body.overlays ?? []).map((id) => (
|
|
490
|
+
<Fragment key={id}>{slot(id)}</Fragment>
|
|
491
|
+
))}
|
|
492
|
+
</>
|
|
493
|
+
);
|
|
494
|
+
}
|