@softize/opus 22.1.0 → 23.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 +74 -1
- package/bin/cli.mjs +6 -5
- package/bin/lib/check.mjs +23 -8
- package/bin/lib/copy.mjs +1 -1
- package/bin/lib/gen-manifest.mjs +2 -0
- package/bin/lib/gen-runner.mjs +7 -0
- package/bin/lib/gen-stubs.mjs +3 -1
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +1 -1
- package/docs/adr/0032-tab-baseline-respects-the-container-inset.md +16 -12
- package/docs/adr/0034-tabs-can-own-a-frame-without-owning-a-surface.md +8 -6
- package/docs/adr/0036-tabs-declares-trigger-distribution.md +11 -8
- package/docs/adr/0037-design-data-lives-outside-the-contract.md +1 -1
- package/docs/adr/0038-content-level-defines-primary-presentation.md +62 -0
- package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +62 -0
- package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +60 -0
- package/docs/adr/0041-actions-declare-effect-and-personal-data.md +139 -0
- package/docs/code-style.md +1 -1
- package/docs/protocol.md +15 -2
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/references/ui-patterns.md +1 -1
- package/registry/skills/create-opus-action/SKILL.md +7 -2
- package/registry/skills/create-opus-action/references/contract-and-binding.md +10 -5
- package/registry/skills/create-opus-action/references/evaluations.md +3 -0
- package/registry/skills/create-opus-action/scripts/scaffold.mjs +6 -0
- package/registry/templates/app/src/domains/tasks/actions/list.ts +2 -0
- package/src/core/action-nature.ts +40 -0
- package/src/core/audit.ts +12 -4
- package/src/core/contracts.ts +1 -1
- package/src/core/index.ts +2 -0
- package/src/core/runtime.ts +57 -35
- package/src/core/types.ts +45 -23
- package/src/design/index.ts +14 -3
- package/src/mcp/index.ts +6 -0
- package/src/testing/index.ts +6 -26
- package/src/ui/components/patterns/content-header.tsx +5 -9
- package/src/ui/components/patterns/list.tsx +55 -1
- package/src/ui/components/patterns/presentation.tsx +1 -1
- package/src/ui/components/primitives/icon-picker.tsx +6 -0
- package/src/ui/components/primitives/tabs.tsx +55 -87
- package/src/ui/components/primitives/toggle.tsx +1 -1
- package/src/ui/docs/content/action-list.md +13 -4
- package/src/ui/docs/content/actions.md +36 -8
- package/src/ui/docs/content/content.md +5 -6
- package/src/ui/docs/content/mcp.md +6 -0
- package/src/ui/docs/content/semantic-context.md +0 -1
- package/src/ui/docs/content/tabs.md +68 -69
- package/src/ui/docs/content/testing.md +2 -3
- package/src/ui/docs/content/toggle.md +15 -10
- package/src/ui/drivers/react.tsx +4 -3
- package/src/ui/meta.ts +2 -2
- package/src/ui/react.tsx +2 -3
- package/src/vite/design.ts +23 -21
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
2
|
import { Tabs as TabsPrimitive } from "radix-ui";
|
|
4
3
|
|
|
5
4
|
import { cn } from "../../lib/cn.ts";
|
|
6
5
|
import { controlHeight, type ControlSize } from "./control.ts";
|
|
7
6
|
|
|
8
7
|
// Divergência da casa (ejetado): o Tabs ganhou `size` — Button e Select têm `sm`, o Tabs
|
|
9
|
-
// não tinha, e uma fileira densa ficava com
|
|
8
|
+
// não tinha, e uma fileira densa ficava com as abas 0.25rem mais altas
|
|
10
9
|
// que os irmãos. O tamanho flui pra TabsList via contexto (a altura é dela) e a medida é a
|
|
11
10
|
// da escala de control.ts.
|
|
12
11
|
type TabsSize = Extract<ControlSize, "default" | "sm">;
|
|
@@ -18,9 +17,6 @@ const TabsFramedContext = React.createContext(false);
|
|
|
18
17
|
type TabsOrientation = "horizontal" | "vertical";
|
|
19
18
|
const TabsOrientationContext =
|
|
20
19
|
React.createContext<TabsOrientation>("horizontal");
|
|
21
|
-
const TabsListVariantContext = React.createContext<"default" | "line">(
|
|
22
|
-
"default",
|
|
23
|
-
);
|
|
24
20
|
|
|
25
21
|
function Tabs({
|
|
26
22
|
className,
|
|
@@ -62,87 +58,62 @@ function Tabs({
|
|
|
62
58
|
);
|
|
63
59
|
}
|
|
64
60
|
|
|
65
|
-
const tabsListVariants = cva(
|
|
66
|
-
// A altura horizontal entra como classe simples no componente (abaixo), para que um
|
|
67
|
-
// `className="h-full"` do consumidor consiga substituí-la via tailwind-merge. Seletores
|
|
68
|
-
// condicionais por data-size têm especificidade maior e tornavam a altura inextensível.
|
|
69
|
-
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[0.1875rem] text-muted-foreground group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none data-[variant=line]:p-0",
|
|
70
|
-
{
|
|
71
|
-
variants: {
|
|
72
|
-
variant: {
|
|
73
|
-
default: "bg-muted",
|
|
74
|
-
line: "gap-1 bg-transparent",
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
defaultVariants: {
|
|
78
|
-
variant: "default",
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
);
|
|
82
|
-
|
|
83
61
|
function TabsList({
|
|
84
62
|
className,
|
|
85
63
|
children,
|
|
86
64
|
style,
|
|
87
|
-
variant = "default",
|
|
88
65
|
...props
|
|
89
|
-
}: React.ComponentProps<typeof TabsPrimitive.List>
|
|
90
|
-
VariantProps<typeof tabsListVariants>) {
|
|
66
|
+
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
|
91
67
|
// O size vem do <Tabs> (contexto) — a altura mora aqui, na list.
|
|
92
68
|
const size = React.useContext(TabsSizeContext);
|
|
93
69
|
const orientation = React.useContext(TabsOrientationContext);
|
|
94
70
|
const distribution = React.useContext(TabsDistributionContext);
|
|
95
|
-
const
|
|
96
|
-
const hasHorizontalLine =
|
|
97
|
-
normalizedVariant === "line" && orientation === "horizontal";
|
|
71
|
+
const hasHorizontalLine = orientation === "horizontal";
|
|
98
72
|
return (
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
data-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
{children}
|
|
140
|
-
</TabsPrimitive.List>
|
|
141
|
-
</TabsListVariantContext.Provider>
|
|
73
|
+
<TabsPrimitive.List
|
|
74
|
+
data-slot="tabs-list"
|
|
75
|
+
data-size={size}
|
|
76
|
+
data-distribution={distribution}
|
|
77
|
+
className={cn(
|
|
78
|
+
"group/tabs-list inline-flex w-fit items-center justify-center gap-1 rounded-none bg-transparent p-0 text-muted-foreground group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
|
|
79
|
+
controlHeight[size],
|
|
80
|
+
distribution === "equal" && orientation === "horizontal" && "w-full",
|
|
81
|
+
className,
|
|
82
|
+
)}
|
|
83
|
+
style={{
|
|
84
|
+
...(hasHorizontalLine
|
|
85
|
+
? {
|
|
86
|
+
position: "relative",
|
|
87
|
+
width: "100%",
|
|
88
|
+
height: "auto",
|
|
89
|
+
justifyContent: "flex-start",
|
|
90
|
+
}
|
|
91
|
+
: {}),
|
|
92
|
+
...style,
|
|
93
|
+
}}
|
|
94
|
+
{...props}
|
|
95
|
+
>
|
|
96
|
+
{hasHorizontalLine && (
|
|
97
|
+
<span
|
|
98
|
+
aria-hidden="true"
|
|
99
|
+
data-slot="tabs-line"
|
|
100
|
+
style={{
|
|
101
|
+
position: "absolute",
|
|
102
|
+
insetInline: "0rem",
|
|
103
|
+
bottom: 0,
|
|
104
|
+
borderBottomWidth: "0.0625rem",
|
|
105
|
+
borderBottomStyle: "solid",
|
|
106
|
+
borderBottomColor: "var(--border)",
|
|
107
|
+
pointerEvents: "none",
|
|
108
|
+
}}
|
|
109
|
+
/>
|
|
110
|
+
)}
|
|
111
|
+
{children}
|
|
112
|
+
</TabsPrimitive.List>
|
|
142
113
|
);
|
|
143
114
|
}
|
|
144
115
|
|
|
145
|
-
function
|
|
116
|
+
function TabsListItem({
|
|
146
117
|
className,
|
|
147
118
|
style,
|
|
148
119
|
icon,
|
|
@@ -152,17 +123,16 @@ function TabsTrigger({
|
|
|
152
123
|
/** Ícone decorativo antes do rótulo. Em uma aba sem texto, declare também `aria-label`. */
|
|
153
124
|
icon?: React.ReactNode;
|
|
154
125
|
}) {
|
|
155
|
-
const variant = React.useContext(TabsListVariantContext);
|
|
156
126
|
const orientation = React.useContext(TabsOrientationContext);
|
|
157
127
|
const size = React.useContext(TabsSizeContext);
|
|
158
128
|
const distribution = React.useContext(TabsDistributionContext);
|
|
159
129
|
const framed = React.useContext(TabsFramedContext);
|
|
160
|
-
const hasHorizontalLine =
|
|
130
|
+
const hasHorizontalLine = orientation === "horizontal";
|
|
161
131
|
const content = (
|
|
162
132
|
<>
|
|
163
133
|
{icon !== undefined && (
|
|
164
134
|
<span
|
|
165
|
-
data-slot="tabs-
|
|
135
|
+
data-slot="tabs-list-item-icon"
|
|
166
136
|
aria-hidden="true"
|
|
167
137
|
className="flex size-4 shrink-0 items-center justify-center [&_svg]:size-4"
|
|
168
138
|
>
|
|
@@ -174,16 +144,14 @@ function TabsTrigger({
|
|
|
174
144
|
);
|
|
175
145
|
return (
|
|
176
146
|
<TabsPrimitive.Trigger
|
|
177
|
-
data-slot="tabs-
|
|
147
|
+
data-slot="tabs-list-item"
|
|
178
148
|
// px: altura e indicador compensam a hairline do contêiner (um pixel físico, não escala).
|
|
179
149
|
className={cn(
|
|
180
|
-
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50
|
|
181
|
-
"
|
|
182
|
-
"data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:text-foreground",
|
|
183
|
-
"after:absolute after:bg-primary after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-1px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
|
|
150
|
+
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-foreground data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground dark:data-[state=active]:border-transparent dark:data-[state=active]:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
151
|
+
"after:absolute after:bg-primary after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-1px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 data-[state=active]:after:opacity-100",
|
|
184
152
|
hasHorizontalLine &&
|
|
185
153
|
cn(
|
|
186
|
-
"group/tab-
|
|
154
|
+
"group/tab-list-item rounded-none px-0 py-0 hover:bg-transparent active:brightness-100 data-[state=active]:bg-transparent data-[state=active]:hover:bg-transparent",
|
|
187
155
|
framed ? "pb-2" : "pb-1.5",
|
|
188
156
|
),
|
|
189
157
|
className,
|
|
@@ -202,9 +170,9 @@ function TabsTrigger({
|
|
|
202
170
|
>
|
|
203
171
|
{hasHorizontalLine ? (
|
|
204
172
|
<span
|
|
205
|
-
data-slot="tabs-
|
|
173
|
+
data-slot="tabs-list-item-content"
|
|
206
174
|
className={cn(
|
|
207
|
-
"inline-flex items-center justify-center gap-1.5 rounded-md px-3 transition-colors hover:bg-accent hover:text-accent-foreground active:brightness-90 group-data-[state=active]/tab-
|
|
175
|
+
"inline-flex items-center justify-center gap-1.5 rounded-md px-3 transition-colors hover:bg-accent hover:text-accent-foreground active:brightness-90 group-data-[state=active]/tab-list-item:text-foreground",
|
|
208
176
|
distribution === "equal" && "w-full",
|
|
209
177
|
controlHeight[size],
|
|
210
178
|
)}
|
|
@@ -218,17 +186,17 @@ function TabsTrigger({
|
|
|
218
186
|
);
|
|
219
187
|
}
|
|
220
188
|
|
|
221
|
-
function
|
|
189
|
+
function TabsPanel({
|
|
222
190
|
className,
|
|
223
191
|
...props
|
|
224
192
|
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
|
225
193
|
return (
|
|
226
194
|
<TabsPrimitive.Content
|
|
227
|
-
data-slot="tabs-
|
|
195
|
+
data-slot="tabs-panel"
|
|
228
196
|
className={cn("flex-1 outline-none", className)}
|
|
229
197
|
{...props}
|
|
230
198
|
/>
|
|
231
199
|
);
|
|
232
200
|
}
|
|
233
201
|
|
|
234
|
-
export { Tabs, TabsList,
|
|
202
|
+
export { Tabs, TabsList, TabsListItem, TabsPanel };
|
|
@@ -15,7 +15,7 @@ const toggleVariants = cva(
|
|
|
15
15
|
variant: {
|
|
16
16
|
default: "bg-transparent",
|
|
17
17
|
outline:
|
|
18
|
-
"border border-input bg-transparent hover:bg-
|
|
18
|
+
"border border-input bg-transparent text-muted-foreground hover:bg-muted/50 hover:text-foreground data-[state=on]:bg-muted/50 data-[state=on]:text-foreground",
|
|
19
19
|
},
|
|
20
20
|
// Alturas da escala de control.ts (`sm` = o mesmo h-8 de Button e Select).
|
|
21
21
|
size: {
|
|
@@ -42,7 +42,6 @@ extremos opostos do cabeçalho da coleção; busca, filtros e atualização perm
|
|
|
42
42
|
<Content
|
|
43
43
|
title="Workspaces"
|
|
44
44
|
level={1}
|
|
45
|
-
variant="page"
|
|
46
45
|
actions={<Button>Criar workspace</Button>}
|
|
47
46
|
>
|
|
48
47
|
<ActionList action={workspaceList} input={{}} />
|
|
@@ -238,7 +237,7 @@ render(
|
|
|
238
237
|
<div className="h-80 w-full overflow-x-auto overflow-y-hidden rounded-lg border border-border">
|
|
239
238
|
<Page layout="fill">
|
|
240
239
|
<PageBody>
|
|
241
|
-
<Content level={1}
|
|
240
|
+
<Content level={1}>
|
|
242
241
|
<ContentHeader>
|
|
243
242
|
<ContentTitle>Workspaces</ContentTitle>
|
|
244
243
|
<ContentActions>
|
|
@@ -276,7 +275,7 @@ componentes no caminho. Nos demais casos, a decisão é sua:
|
|
|
276
275
|
|
|
277
276
|
- Para a lista manter a altura dos resultados, como em uma seção secundária cuja rolagem pertence ao
|
|
278
277
|
`PageBody`, declare `layout="flow"`.
|
|
279
|
-
- Um contêiner próprio entre `ContentBody` e a lista, como um `
|
|
278
|
+
- Um contêiner próprio entre `ContentBody` e a lista, como um `TabsPanel`, não repassa a altura, e
|
|
280
279
|
a lista volta a `flow`. Declare `layout="fill"` na lista e dê ao contêiner as classes
|
|
281
280
|
`flex min-h-0 flex-1 flex-col`.
|
|
282
281
|
- Dentro de [`Card`](/ui/card), [`Dialog`](/ui/dialog), [`Drawer`](/ui/drawer) ou
|
|
@@ -364,6 +363,9 @@ render(
|
|
|
364
363
|
|
|
365
364
|
- **`depends: ['outroFiltro']`:** desabilita o filtro até que suas dependências tenham valor e o
|
|
366
365
|
limpa quando uma delas muda.
|
|
366
|
+
- **`presentation: 'toggle'`:** mantém visíveis as opções curtas de um `select` de escolha única;
|
|
367
|
+
estado, URL e input da action continuam iguais aos do controle padrão. Entradas de dicionário
|
|
368
|
+
com `icon` apresentam o ícone junto ao rótulo.
|
|
367
369
|
- **`options: { kind: 'dictionary', ref: 'sessionStatus' }`:** resolve opções e rótulos por um
|
|
368
370
|
dicionário registrado no `OpusProvider`. `filterOptions` substitui essa fonte quando informado.
|
|
369
371
|
- **`options: { kind: 'lookup', source: 'x.lookup' }`:** consulta uma action com `{ q }` e os valores
|
|
@@ -371,6 +373,13 @@ render(
|
|
|
371
373
|
|
|
372
374
|
```tsx
|
|
373
375
|
filters: {
|
|
376
|
+
channel: {
|
|
377
|
+
label: 'Canal',
|
|
378
|
+
type: 'select',
|
|
379
|
+
presentation: 'toggle',
|
|
380
|
+
allowEmpty: false,
|
|
381
|
+
options: { kind: 'static', items: [...] },
|
|
382
|
+
},
|
|
374
383
|
project: { label: 'Projeto', type: 'select', options: { kind: 'static', items: [...] } },
|
|
375
384
|
sprint: {
|
|
376
385
|
label: 'Sprint',
|
|
@@ -469,7 +478,7 @@ render(
|
|
|
469
478
|
| Chave | O que declara |
|
|
470
479
|
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
471
480
|
| `columns` | `{ key, label, type?, sortable?, fit?, hidden?, dateFormat?, dictionary?, empty? }` — a tabela. `hidden` nasce oculta e pode ser exibida pelo seletor de colunas; `dictionary` nomeia o dicionário do provider; `empty` dá o significado da ausência. |
|
|
472
|
-
| `filters` | `{ [nome]: { label, type, options?, multiple?, searchable?, allowEmpty?, placement?, section?, depends?, … } }` — a toolbar. `placement` classifica o controle como `inline`, `advanced` ou `external`; filtros avançados com `section` são agrupados no diálogo; `external` mantém estado e input, mas delega a apresentação ao consumidor. `searchable` habilita busca local em `select`; `allowEmpty: false` omite a opção vazia. `depends` desabilita/cascateia; options `kind: 'lookup'` busca em uma action. Valor aplicado entra no input com o mesmo nome. |
|
|
481
|
+
| `filters` | `{ [nome]: { label, type, options?, multiple?, presentation?, searchable?, allowEmpty?, placement?, section?, depends?, … } }` — a toolbar. `placement` classifica o controle como `inline`, `advanced` ou `external`; filtros avançados com `section` são agrupados no diálogo; `external` mantém estado e input, mas delega a apresentação ao consumidor. `presentation: 'toggle'` mostra todas as opções curtas de um `select` de escolha única em um `ToggleGroup`; sem a propriedade, o controle continua sendo `Select`. `searchable` habilita busca local em `select`; `allowEmpty: false` omite a opção vazia. `depends` desabilita/cascateia; options `kind: 'lookup'` busca em uma action. Valor aplicado entra no input com o mesmo nome. |
|
|
473
482
|
| `text` | `{ fields }` — liga a busca; convenção: param `q` no input. Ela aparece depois dos filtros inline e imediatamente antes do acesso aos filtros avançados. A busca participa da contagem de controles inline: com mais de um controle, a barra exibe as labels; quando ela é o único recorte, sua label é omitida. |
|
|
474
483
|
| `sort` | `{ fields, default }` — ordenação inicial; header ordenável escreve `sort: 'chave:dir'`. |
|
|
475
484
|
| `periods` | `{ value, label, default? }[]` — o controle de período (presets + Personalizado com calendário); `default: true` marca o preset inicial, senão vale o primeiro; materializa em `from`/`to` no input. |
|
|
@@ -120,8 +120,8 @@ nome de uma action não começa pelo caminho do domínio, como `workspace.` ou `
|
|
|
120
120
|
`subdomains` aceita outros domínios e forma esse caminho.
|
|
121
121
|
|
|
122
122
|
`actions` aceita um mapa, uma lista ou o resultado de `import * as actions`. Um domínio também pode
|
|
123
|
-
registrar só o contrato, sem handler:
|
|
124
|
-
fora dele, a execução falha por falta de handler.
|
|
123
|
+
registrar só o contrato, sem handler: o plugin do modo design liga uma resposta gerada ou um
|
|
124
|
+
cenário; fora dele, a execução falha por falta de handler.
|
|
125
125
|
|
|
126
126
|
## Ordem canônica dos campos
|
|
127
127
|
|
|
@@ -132,9 +132,9 @@ nessa estrutura. Os grupos seguem esta sequência; dentro de cada grupo, a ordem
|
|
|
132
132
|
```text
|
|
133
133
|
name, kind
|
|
134
134
|
→ label, title, summary, description, icon, color, messages, tags, examples, errors
|
|
135
|
-
→ input, output
|
|
135
|
+
→ input, output, effect, personalData
|
|
136
136
|
→ public, requires, authorize, loads
|
|
137
|
-
→ handler
|
|
137
|
+
→ handler
|
|
138
138
|
→ audit, confirm, invalidates, rateLimit, idempotency
|
|
139
139
|
→ ai, automatable, internal
|
|
140
140
|
```
|
|
@@ -146,6 +146,35 @@ Declare `input` e `output` com `z.object` e use os tipos lógicos do catálogo e
|
|
|
146
146
|
validações: `z.object({ tag: t.slug().zod(), contato: t.email().zod() })`. O `.zod()` devolve o
|
|
147
147
|
schema zod de cada tipo lógico (slug, email, phone, money…), importado de `@softize/opus/schema/zod`.
|
|
148
148
|
|
|
149
|
+
## Declarar o que a action faz com os dados
|
|
150
|
+
|
|
151
|
+
Dois fatos sobre a action valem para qualquer consumidor, da interface à IA, e ficam declarados no
|
|
152
|
+
contrato:
|
|
153
|
+
|
|
154
|
+
- **`effect`** diz se a action só lê (`'read'`) ou altera estado (`'write'`). `list` e `view` já são
|
|
155
|
+
leitura, porque são servidas por `GET`, e não precisam declarar. `simple` e `form` declaram,
|
|
156
|
+
porque o `kind` não diz isso: uma análise sob demanda costuma ser `simple` e só ler.
|
|
157
|
+
- **`personalData`** diz se a resposta contém dados que identificam pessoas, como nome, documento,
|
|
158
|
+
contato, placa ou texto livre sobre alguém. `false` também é uma declaração.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
export const salesPerformanceContract = defineContract({
|
|
162
|
+
name: 'sales.performance',
|
|
163
|
+
kind: 'simple',
|
|
164
|
+
input: z.object({ period: z.string() }),
|
|
165
|
+
output: z.object({ total: z.number() }),
|
|
166
|
+
effect: 'read',
|
|
167
|
+
personalData: false,
|
|
168
|
+
})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Com `personalData: true`, toda execução bem-sucedida fica marcada no registro de auditoria com a
|
|
172
|
+
quantidade de registros devolvidos, inclusive quando a action declara `audit: false`; nesse caso o
|
|
173
|
+
registro é mínimo, sem entrada nem saída. No modo design, a resposta simulada não é marcada. Um
|
|
174
|
+
host de IA pode usar `effect` para decidir o que oferecer ao modelo sem procedimento, e o MCP publica
|
|
175
|
+
a anotação `readOnlyHint`. O `opus check` avisa quantas actions ainda não declaram os dois fatos; uma
|
|
176
|
+
major futura passará a exigi-los.
|
|
177
|
+
|
|
149
178
|
A autorização é fechada por padrão: sem `public: true`, a action exige usuário autenticado; com
|
|
150
179
|
`authorize`, exige também a permissão declarada. `automatable: true` permite que um
|
|
151
180
|
[schedule](/docs/scheduler) execute a action como o sistema, sem abri-la a chamadores anônimos.
|
|
@@ -199,10 +228,9 @@ O Opus não decide onde os cenários ficam nem se entram no repositório. Eles n
|
|
|
199
228
|
aplicação: podem ficar numa pasta ignorada pelo git e ser escritos durante a sessão de design,
|
|
200
229
|
quando forem necessários.
|
|
201
230
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
para o design, transforme-a em cenário.
|
|
231
|
+
Dados de design não pertencem ao contrato. Quando uma resposta específica importar para a revisão
|
|
232
|
+
da interface, declare um cenário com `defineDesignScenario`; para os demais casos, preserve apenas
|
|
233
|
+
o contrato e deixe o plugin gerar a resposta pelo schema.
|
|
206
234
|
|
|
207
235
|
O plugin `opusDesign()`, de `@softize/opus/vite`, monta o runtime do Opus dentro do dev server e
|
|
208
236
|
responde `/api` no próprio processo. No modo design, qualquer `server.proxy` para um backend externo
|
|
@@ -205,15 +205,15 @@ Quando a região principal da página é uma coleção, `Content` nomeia e gover
|
|
|
205
205
|
toolbar da lista. Essa divisão aproxima cada comando do objeto que ele afeta sem criar uma família
|
|
206
206
|
paralela de componentes `List*`.
|
|
207
207
|
|
|
208
|
-
Quando a coleção é o assunto da página, use `
|
|
209
|
-
escala do cabeçalho de [`Page`](/ui/page) e
|
|
210
|
-
extremo oposto ao título
|
|
208
|
+
Quando a coleção é o assunto da página, use `level={1}`: o `Content` passa a ser o heading
|
|
209
|
+
principal, assume automaticamente a escala do cabeçalho de [`Page`](/ui/page) e mantém o mesmo
|
|
210
|
+
ritmo entre introdução e corpo. As ações ficam no extremo oposto ao título. Níveis de 2 a 6
|
|
211
|
+
continuam usando a apresentação compacta de uma seção.
|
|
211
212
|
|
|
212
213
|
```tsx preview col
|
|
213
214
|
render(
|
|
214
215
|
<Content
|
|
215
216
|
level={1}
|
|
216
|
-
variant="page"
|
|
217
217
|
title="Clientes"
|
|
218
218
|
actions={<Button>Criar cliente</Button>}
|
|
219
219
|
>
|
|
@@ -242,8 +242,7 @@ classes adicionais; um `Content` irmão, sem lista, mantém a altura do próprio
|
|
|
242
242
|
| `count` | `number` | | Forma curta: total de itens ao lado do título da região. |
|
|
243
243
|
| `description` | `ReactNode` | | Forma curta: frase de apoio sob o título. |
|
|
244
244
|
| `actions` | `ReactNode` | | Forma curta: ações sobre a região inteira, no extremo oposto ao título. |
|
|
245
|
-
| `level` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | `2` | Nível
|
|
246
|
-
| `variant` | `'page' \| 'section'` | `'section'` | Hierarquia visual: `page` reproduz o cabeçalho de `Page`; `section` é a região dentro de uma superfície. |
|
|
245
|
+
| `level` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | `2` | Nível do heading; `1` identifica o conteúdo principal da página e os demais, uma seção interna. |
|
|
247
246
|
| `as` | `'section' \| 'fieldset'` | `'section'` | Usa uma região com heading ou um grupo de campos cujo título é a legenda nativa. |
|
|
248
247
|
| `framed` | `boolean` | `false` | Envolve o header e o body com a moldura estrutural da região. |
|
|
249
248
|
| `layout` | `'continuous' \| 'sectioned'` | `'continuous'` | Mantém header e body contínuos ou os separa com divisor e body sem recuo. `sectioned` exige `framed`. |
|
|
@@ -56,6 +56,12 @@ entrada. Em `_meta`, cada tool traz os
|
|
|
56
56
|
[Produtos de Dados](/docs/data-products) relacionados: `com.softize.opus/data-products` lista os
|
|
57
57
|
identificadores e `com.softize.opus/data-product-labels`, o nome legível de cada um.
|
|
58
58
|
|
|
59
|
+
Quando a action declara o que faz com os dados, a tool traz também as anotações padrão do MCP:
|
|
60
|
+
`readOnlyHint` vem de `effect`. Numa action que escreve, `destructiveHint` vai sempre explícito, a
|
|
61
|
+
partir de `ai.destructive`, porque o MCP trata a ausência numa escrita como destrutiva.
|
|
62
|
+
Em `_meta`, `com.softize.opus/effect` repete o efeito e `com.softize.opus/personal-data` diz se a
|
|
63
|
+
resposta contém dado pessoal. Veja [Actions](/docs/actions).
|
|
64
|
+
|
|
59
65
|
`CallTool` recusa com erro qualquer nome fora dessa lista; os demais seguem por `runtime.execute`.
|
|
60
66
|
A exposição declarada em `ai` decide o que o cliente MCP pode executar, e a autorização do contexto
|
|
61
67
|
continua valendo sobre ela. Para um acesso somente de leitura, exponha apenas actions de leitura.
|
|
@@ -54,7 +54,6 @@ O par `context` + `variant` vale para componentes que comunicam significado, com
|
|
|
54
54
|
| --- | --- | --- |
|
|
55
55
|
| [`Select`](/ui/select) | `default`, `outline`, `ghost` | `default` é o campo completo; `outline` é compacto com borda, e `ghost`, compacto sem borda nem fundo. |
|
|
56
56
|
| [`Toggle`](/ui/toggle) | `default`, `outline` | `default` não tem borda nem fundo em repouso; o fundo aparece no hover e quando ativo. `outline` acrescenta a borda. |
|
|
57
|
-
| [`TabsList`](/ui/tabs) | `default`, `line` | `default` agrupa as abas em uma superfície preenchida; `line` usa uma linha-base e marca nela a aba ativa. |
|
|
58
57
|
|
|
59
58
|
O mesmo nome pode aparecer nos dois grupos. Num `Badge`, `outline` é um tratamento do significado
|
|
60
59
|
declarado em `context`; num `Select`, é apenas o formato compacto com borda.
|