@softize/opus 30.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 +116 -0
- package/bin/lib/check.mjs +109 -101
- 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/0060-surface-composition-separates-chrome-and-intro.md +142 -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 +4 -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 +13 -11
- package/registry/skills/build-opus-ui/references/ui-patterns.md +24 -27
- package/src/core/presentation.ts +938 -32
- package/src/core/types.ts +1 -0
- 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 +572 -358
- package/src/ui/components/patterns/page-heading-context.tsx +12 -5
- package/src/ui/components/patterns/page.tsx +227 -197
- 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 +23 -32
- package/src/ui/components/patterns/surface-anatomy.ts +27 -217
- package/src/ui/components/patterns/surface.tsx +19 -29
- 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 +19 -81
- package/src/ui/components/primitives/drawer.tsx +17 -79
- 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 +1 -1
- 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 +134 -68
- 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 +44 -112
- package/src/ui/docs/content/drawer.md +44 -103
- package/src/ui/docs/content/field.md +2 -1
- package/src/ui/docs/content/page.md +204 -412
- 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 +65 -120
- 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 +34 -8
- package/src/ui/theme.css +44 -33
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import type { PresentationCompositionRuntime } from "./presentation-sections.tsx";
|
|
2
|
+
import { meetsPresentationRequirement } from "../../../core/presentation.ts";
|
|
3
|
+
import { Fragment, useRef, type ReactNode } from "react";
|
|
4
|
+
import type { ActionContract } from "../../../core/contracts.ts";
|
|
5
|
+
import type {
|
|
6
|
+
PresentationActionBody,
|
|
7
|
+
PresentationActionRegistry,
|
|
8
|
+
PresentationBindingContext,
|
|
9
|
+
PresentationColumn,
|
|
10
|
+
} from "../../../core/presentation.ts";
|
|
11
|
+
import { resolvePresentationBindings } from "../../../core/presentation.ts";
|
|
12
|
+
import { useListAction, useTriggerAction } from "../../drivers/react.tsx";
|
|
13
|
+
import { ActionFormField } from "./form.tsx";
|
|
14
|
+
import { DataState } from "./data-state.tsx";
|
|
15
|
+
import { Surface } from "./surface.tsx";
|
|
16
|
+
import { EmptyValue } from "../primitives/empty-value.tsx";
|
|
17
|
+
import { Switch } from "../primitives/switch.tsx";
|
|
18
|
+
import { toast } from "../primitives/sonner.tsx";
|
|
19
|
+
|
|
20
|
+
export function PresentationSwitch({
|
|
21
|
+
column,
|
|
22
|
+
item,
|
|
23
|
+
checked,
|
|
24
|
+
actions,
|
|
25
|
+
can,
|
|
26
|
+
context,
|
|
27
|
+
}: {
|
|
28
|
+
column: Extract<PresentationColumn, { kind: "switch" }>;
|
|
29
|
+
item: Record<string, unknown>;
|
|
30
|
+
checked: boolean;
|
|
31
|
+
actions: PresentationActionRegistry;
|
|
32
|
+
can?: (permission: string) => boolean;
|
|
33
|
+
context: PresentationBindingContext;
|
|
34
|
+
}) {
|
|
35
|
+
const action = actions[column.action];
|
|
36
|
+
if (!action) throw new Error(`Action “${column.action}” não registrada.`);
|
|
37
|
+
const update = useTriggerAction(action);
|
|
38
|
+
const pending = useRef(false);
|
|
39
|
+
const disabled =
|
|
40
|
+
!meetsPresentationRequirement(column.requires, can) ||
|
|
41
|
+
!meetsPresentationRequirement(action.requires, can);
|
|
42
|
+
return (
|
|
43
|
+
<Switch
|
|
44
|
+
checked={checked}
|
|
45
|
+
disabled={disabled || update.isLoading}
|
|
46
|
+
aria-label={`${column.label}: ${String(item[column.itemLabel] ?? "")}`}
|
|
47
|
+
onClick={(event) => event.stopPropagation()}
|
|
48
|
+
onCheckedChange={async (value) => {
|
|
49
|
+
if (disabled || pending.current) return;
|
|
50
|
+
pending.current = true;
|
|
51
|
+
try {
|
|
52
|
+
const result = await update.trigger({
|
|
53
|
+
...resolvePresentationBindings(column.input, { ...context, item }),
|
|
54
|
+
[column.valueField]: value,
|
|
55
|
+
});
|
|
56
|
+
if (!result.isSuccess)
|
|
57
|
+
toast.error(
|
|
58
|
+
result.error?.message ?? "Não foi possível salvar a alteração.",
|
|
59
|
+
);
|
|
60
|
+
} catch {
|
|
61
|
+
update.reset();
|
|
62
|
+
toast.error("Não foi possível salvar a alteração.");
|
|
63
|
+
} finally {
|
|
64
|
+
pending.current = false;
|
|
65
|
+
}
|
|
66
|
+
}}
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function presentationTextCell(
|
|
72
|
+
value: unknown,
|
|
73
|
+
column: Extract<PresentationColumn, { kind: "text" }>,
|
|
74
|
+
icons?: Readonly<Record<string, ReactNode>>,
|
|
75
|
+
): ReactNode {
|
|
76
|
+
if (value === null || value === undefined || value === "")
|
|
77
|
+
return column.empty ?? <EmptyValue compact />;
|
|
78
|
+
const text =
|
|
79
|
+
column.labels === undefined
|
|
80
|
+
? `${column.prefix ?? ""}${String(value)}`
|
|
81
|
+
: Object.hasOwn(column.labels, String(value))
|
|
82
|
+
? column.labels[String(value)]
|
|
83
|
+
: (column.fallback ?? String(value));
|
|
84
|
+
if (column.icon === undefined) return text;
|
|
85
|
+
if (!Object.hasOwn(icons ?? {}, column.icon))
|
|
86
|
+
throw new Error(`Ícone “${column.icon}” não registrado.`);
|
|
87
|
+
return (
|
|
88
|
+
<span className="flex items-center gap-2">
|
|
89
|
+
<span aria-hidden className="size-4 shrink-0 [&>svg]:size-4">
|
|
90
|
+
{icons![column.icon]}
|
|
91
|
+
</span>
|
|
92
|
+
{text}
|
|
93
|
+
</span>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function PresentationLookupField({
|
|
98
|
+
name,
|
|
99
|
+
lookup,
|
|
100
|
+
actions,
|
|
101
|
+
runtime,
|
|
102
|
+
can,
|
|
103
|
+
}: {
|
|
104
|
+
name: string;
|
|
105
|
+
lookup: NonNullable<PresentationActionBody["lookups"]>[string];
|
|
106
|
+
actions: PresentationActionRegistry;
|
|
107
|
+
runtime?: PresentationCompositionRuntime;
|
|
108
|
+
can?: (permission: string) => boolean;
|
|
109
|
+
}) {
|
|
110
|
+
const action = actions[lookup.action];
|
|
111
|
+
if (action?.kind !== "list")
|
|
112
|
+
throw new Error(`Lookup “${lookup.action}” não é uma list action.`);
|
|
113
|
+
if (action.paginate !== false)
|
|
114
|
+
throw new Error(`Lookup “${lookup.action}” exige paginate false.`);
|
|
115
|
+
const allowed = meetsPresentationRequirement(action.requires, can);
|
|
116
|
+
const result = useListAction<Record<string, unknown>>(
|
|
117
|
+
action,
|
|
118
|
+
{},
|
|
119
|
+
{ enabled: allowed },
|
|
120
|
+
);
|
|
121
|
+
if (!allowed) return <p role="alert">Sem acesso às opções deste campo.</p>;
|
|
122
|
+
const adapter =
|
|
123
|
+
lookup.format === undefined ? undefined : runtime?.options?.[lookup.format];
|
|
124
|
+
return (
|
|
125
|
+
<DataState
|
|
126
|
+
loading={result.isLoading}
|
|
127
|
+
error={result.isError ? (result.error ?? {}) : null}
|
|
128
|
+
onRetry={result.refetch}
|
|
129
|
+
>
|
|
130
|
+
<ActionFormField
|
|
131
|
+
name={name}
|
|
132
|
+
options={[
|
|
133
|
+
...(lookup.prepend ?? []),
|
|
134
|
+
...result.items.map(
|
|
135
|
+
(item) =>
|
|
136
|
+
adapter?.format(item) ?? {
|
|
137
|
+
value: String(item[lookup.value]),
|
|
138
|
+
label: String(item[lookup.label]),
|
|
139
|
+
},
|
|
140
|
+
),
|
|
141
|
+
]}
|
|
142
|
+
/>
|
|
143
|
+
</DataState>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function PresentationFormFields({
|
|
148
|
+
body,
|
|
149
|
+
action,
|
|
150
|
+
actions,
|
|
151
|
+
runtime,
|
|
152
|
+
can,
|
|
153
|
+
context,
|
|
154
|
+
}: {
|
|
155
|
+
body: PresentationActionBody;
|
|
156
|
+
action: ActionContract;
|
|
157
|
+
actions: PresentationActionRegistry;
|
|
158
|
+
runtime?: PresentationCompositionRuntime;
|
|
159
|
+
can?: (permission: string) => boolean;
|
|
160
|
+
context: PresentationBindingContext;
|
|
161
|
+
}) {
|
|
162
|
+
if (body.formGroups)
|
|
163
|
+
return (
|
|
164
|
+
<>
|
|
165
|
+
{body.formGroups.map((group, index) => (
|
|
166
|
+
<Surface key={index} as="fieldset" title={group.title}>
|
|
167
|
+
<PresentationFormFields
|
|
168
|
+
{...{ action, actions, runtime, can, context }}
|
|
169
|
+
body={{
|
|
170
|
+
...body,
|
|
171
|
+
formGroups: undefined,
|
|
172
|
+
formFields: group.fields,
|
|
173
|
+
}}
|
|
174
|
+
/>
|
|
175
|
+
</Surface>
|
|
176
|
+
))}
|
|
177
|
+
</>
|
|
178
|
+
);
|
|
179
|
+
const fields = body.formFields ?? [
|
|
180
|
+
...new Set([
|
|
181
|
+
...Object.keys(action.kind === "form" ? (action.fields ?? {}) : {}),
|
|
182
|
+
...Object.keys(body.fieldComponents ?? {}),
|
|
183
|
+
]),
|
|
184
|
+
];
|
|
185
|
+
return (
|
|
186
|
+
<>
|
|
187
|
+
{fields.map((name) =>
|
|
188
|
+
body.fieldComponents?.[name] !== undefined ? (
|
|
189
|
+
<Fragment key={name}>
|
|
190
|
+
{runtime!.fields![body.fieldComponents[name]!]!.render({
|
|
191
|
+
name,
|
|
192
|
+
context,
|
|
193
|
+
})}
|
|
194
|
+
</Fragment>
|
|
195
|
+
) : body.lookups?.[name] === undefined ? (
|
|
196
|
+
<ActionFormField key={name} name={name} />
|
|
197
|
+
) : (
|
|
198
|
+
<PresentationLookupField
|
|
199
|
+
key={name}
|
|
200
|
+
name={name}
|
|
201
|
+
lookup={body.lookups[name]!}
|
|
202
|
+
actions={actions}
|
|
203
|
+
runtime={runtime}
|
|
204
|
+
can={can}
|
|
205
|
+
/>
|
|
206
|
+
),
|
|
207
|
+
)}
|
|
208
|
+
</>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function PresentationViewSections({
|
|
213
|
+
fields,
|
|
214
|
+
data,
|
|
215
|
+
}: {
|
|
216
|
+
fields: NonNullable<PresentationActionBody["fields"]>;
|
|
217
|
+
data: unknown;
|
|
218
|
+
}) {
|
|
219
|
+
const record = data as Record<string, unknown>;
|
|
220
|
+
return (
|
|
221
|
+
<div className="space-y-4">
|
|
222
|
+
{fields.map((field) => {
|
|
223
|
+
const value = record[field.key];
|
|
224
|
+
return (
|
|
225
|
+
<Surface key={field.key} as="section" level={3} title={field.label}>
|
|
226
|
+
{field.format === "html" ? (
|
|
227
|
+
<iframe
|
|
228
|
+
title={field.label}
|
|
229
|
+
sandbox=""
|
|
230
|
+
referrerPolicy="no-referrer"
|
|
231
|
+
srcDoc={`<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'">${String(value ?? "")}`}
|
|
232
|
+
className="h-64 w-full rounded-md border bg-white"
|
|
233
|
+
/>
|
|
234
|
+
) : (
|
|
235
|
+
<pre className="whitespace-pre-wrap rounded-md border bg-muted/30 p-3 font-sans text-sm">
|
|
236
|
+
{value == null
|
|
237
|
+
? (field.empty ?? <EmptyValue compact />)
|
|
238
|
+
: String(value)}
|
|
239
|
+
</pre>
|
|
240
|
+
)}
|
|
241
|
+
</Surface>
|
|
242
|
+
);
|
|
243
|
+
})}
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|