@softize/opus 30.0.0 → 31.1.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 (64) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/bin/lib/check.mjs +109 -101
  3. package/bin/lib/copy.mjs +2973 -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/0066-civil-date-input-and-calendar-list.md +28 -0
  10. package/docs/adr/README.md +6 -0
  11. package/package.json +1 -1
  12. package/registry/skills/build-opus-ui/SKILL.md +21 -19
  13. package/registry/skills/build-opus-ui/references/evaluations.md +13 -11
  14. package/registry/skills/build-opus-ui/references/ui-patterns.md +24 -27
  15. package/src/core/presentation.ts +938 -32
  16. package/src/core/types.ts +2 -0
  17. package/src/ui/components/patterns/form.tsx +22 -1
  18. package/src/ui/components/patterns/grouped-list.ts +2 -2
  19. package/src/ui/components/patterns/list-calendar.tsx +129 -0
  20. package/src/ui/components/patterns/list.tsx +612 -382
  21. package/src/ui/components/patterns/page-heading-context.tsx +12 -5
  22. package/src/ui/components/patterns/page.tsx +227 -197
  23. package/src/ui/components/patterns/presentation-fields.tsx +246 -0
  24. package/src/ui/components/patterns/presentation-sections.tsx +494 -0
  25. package/src/ui/components/patterns/presentation.tsx +1142 -229
  26. package/src/ui/components/patterns/structural-header.tsx +23 -32
  27. package/src/ui/components/patterns/surface-anatomy.ts +27 -217
  28. package/src/ui/components/patterns/surface.tsx +19 -29
  29. package/src/ui/components/patterns/trigger.tsx +10 -2
  30. package/src/ui/components/primitives/calendar.tsx +39 -18
  31. package/src/ui/components/primitives/close-button.tsx +11 -3
  32. package/src/ui/components/primitives/command.tsx +2 -8
  33. package/src/ui/components/primitives/date-input.tsx +276 -0
  34. package/src/ui/components/primitives/detail.tsx +1 -1
  35. package/src/ui/components/primitives/dialog.tsx +19 -81
  36. package/src/ui/components/primitives/drawer.tsx +17 -79
  37. package/src/ui/components/primitives/field.tsx +1 -1
  38. package/src/ui/components/primitives/pagination.tsx +3 -2
  39. package/src/ui/components/primitives/table.tsx +1 -1
  40. package/src/ui/components/primitives/tabs.tsx +13 -2
  41. package/src/ui/docs/content/action-form.md +6 -1
  42. package/src/ui/docs/content/action-list.md +156 -68
  43. package/src/ui/docs/content/action-trigger.md +5 -0
  44. package/src/ui/docs/content/button.md +5 -6
  45. package/src/ui/docs/content/calendar-month-view.md +50 -0
  46. package/src/ui/docs/content/calendar.md +6 -1
  47. package/src/ui/docs/content/command.md +62 -50
  48. package/src/ui/docs/content/compositions.md +129 -459
  49. package/src/ui/docs/content/date-input.md +67 -0
  50. package/src/ui/docs/content/dialog.md +44 -112
  51. package/src/ui/docs/content/drawer.md +44 -103
  52. package/src/ui/docs/content/field.md +2 -1
  53. package/src/ui/docs/content/input.md +3 -0
  54. package/src/ui/docs/content/page.md +204 -412
  55. package/src/ui/docs/content/presentation.md +371 -23
  56. package/src/ui/docs/content/separator.md +1 -1
  57. package/src/ui/docs/content/surface.md +65 -120
  58. package/src/ui/docs/content/tokens.md +18 -4
  59. package/src/ui/docs/registry.tsx +38 -9
  60. package/src/ui/drivers/context.tsx +11 -4
  61. package/src/ui/drivers/react.tsx +27 -7
  62. package/src/ui/meta.ts +17 -6
  63. package/src/ui/react.tsx +41 -8
  64. 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
+ }