@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/bin/lib/check.mjs +109 -101
  3. package/bin/lib/copy.mjs +2970 -1801
  4. package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +52 -0
  5. package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +27 -2
  6. package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +142 -0
  7. package/docs/adr/0064-action-list-com-itens-e-fontes-externas.md +47 -0
  8. package/docs/adr/0065-presentation-composes-settings-sections.md +295 -0
  9. package/docs/adr/README.md +4 -0
  10. package/package.json +1 -1
  11. package/registry/skills/build-opus-ui/SKILL.md +21 -19
  12. package/registry/skills/build-opus-ui/references/evaluations.md +13 -11
  13. package/registry/skills/build-opus-ui/references/ui-patterns.md +24 -27
  14. package/src/core/presentation.ts +938 -32
  15. package/src/core/types.ts +1 -0
  16. package/src/ui/components/patterns/form.tsx +2 -0
  17. package/src/ui/components/patterns/grouped-list.ts +2 -2
  18. package/src/ui/components/patterns/list.tsx +572 -358
  19. package/src/ui/components/patterns/page-heading-context.tsx +12 -5
  20. package/src/ui/components/patterns/page.tsx +227 -197
  21. package/src/ui/components/patterns/presentation-fields.tsx +246 -0
  22. package/src/ui/components/patterns/presentation-sections.tsx +494 -0
  23. package/src/ui/components/patterns/presentation.tsx +1142 -229
  24. package/src/ui/components/patterns/structural-header.tsx +23 -32
  25. package/src/ui/components/patterns/surface-anatomy.ts +27 -217
  26. package/src/ui/components/patterns/surface.tsx +19 -29
  27. package/src/ui/components/patterns/trigger.tsx +10 -2
  28. package/src/ui/components/primitives/close-button.tsx +11 -3
  29. package/src/ui/components/primitives/command.tsx +2 -8
  30. package/src/ui/components/primitives/detail.tsx +1 -1
  31. package/src/ui/components/primitives/dialog.tsx +19 -81
  32. package/src/ui/components/primitives/drawer.tsx +17 -79
  33. package/src/ui/components/primitives/field.tsx +1 -1
  34. package/src/ui/components/primitives/pagination.tsx +3 -2
  35. package/src/ui/components/primitives/table.tsx +1 -1
  36. package/src/ui/components/primitives/tabs.tsx +13 -2
  37. package/src/ui/docs/content/action-form.md +2 -1
  38. package/src/ui/docs/content/action-list.md +134 -68
  39. package/src/ui/docs/content/action-trigger.md +5 -0
  40. package/src/ui/docs/content/button.md +5 -6
  41. package/src/ui/docs/content/command.md +62 -50
  42. package/src/ui/docs/content/compositions.md +129 -459
  43. package/src/ui/docs/content/dialog.md +44 -112
  44. package/src/ui/docs/content/drawer.md +44 -103
  45. package/src/ui/docs/content/field.md +2 -1
  46. package/src/ui/docs/content/page.md +204 -412
  47. package/src/ui/docs/content/presentation.md +371 -23
  48. package/src/ui/docs/content/separator.md +1 -1
  49. package/src/ui/docs/content/surface.md +65 -120
  50. package/src/ui/docs/content/tokens.md +18 -4
  51. package/src/ui/drivers/context.tsx +11 -4
  52. package/src/ui/drivers/react.tsx +27 -7
  53. package/src/ui/meta.ts +5 -5
  54. package/src/ui/react.tsx +34 -8
  55. 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
+ }