@softize/opus 21.0.1 → 22.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.
- package/CHANGELOG.md +71 -0
- package/bin/cli.mjs +9 -1
- package/bin/lib/check.mjs +37 -29
- package/docs/adr/0004-page-content-state-is-composed.md +3 -2
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +3 -0
- package/docs/adr/0006-semantic-context-precedes-visual-variant.md +23 -14
- package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +5 -0
- package/docs/adr/0013-presentation-is-a-portable-action-oriented-artifact.md +7 -4
- package/docs/adr/0016-list-collection-header-belongs-to-content.md +13 -8
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +27 -26
- package/docs/adr/0025-content-frame-treatment-does-not-change-semantics.md +1 -0
- package/docs/adr/0029-page-header-is-the-single-page-introduction.md +71 -0
- package/docs/adr/0030-framed-content-can-use-sectioned-anatomy.md +89 -0
- package/docs/adr/0031-detail-dividers-own-the-tabular-label-treatment.md +61 -0
- package/docs/adr/0032-tab-baseline-respects-the-container-inset.md +59 -0
- package/docs/adr/0033-drawer-uses-an-inset-structural-frame.md +72 -0
- package/docs/adr/0034-tabs-can-own-a-frame-without-owning-a-surface.md +60 -0
- package/docs/adr/0035-button-group-separates-frame-relationship-and-distribution.md +63 -0
- package/docs/adr/0036-tabs-declares-trigger-distribution.md +58 -0
- package/docs/adr/0037-design-data-lives-outside-the-contract.md +117 -0
- package/docs/radius-scale.md +5 -2
- package/package.json +5 -1
- package/registry/skills/build-opus-ui/SKILL.md +9 -6
- package/registry/skills/build-opus-ui/references/evaluations.md +5 -2
- package/registry/skills/build-opus-ui/references/ui-patterns.md +14 -16
- package/registry/skills/create-opus-action/references/contract-and-binding.md +5 -3
- package/registry/skills/create-opus-action/scripts/scaffold.mjs +0 -4
- package/registry/skills/test-opus-action/SKILL.md +2 -2
- package/registry/skills/test-opus-action/references/harness.md +2 -2
- package/registry/templates/app/src/App.tsx +1 -1
- package/registry/templates/app/src/domains/tasks/actions/list.test.ts +2 -19
- package/registry/templates/app/src/domains/tasks/actions/list.ts +3 -5
- package/registry/templates/app/vite.config.ts +3 -2
- package/src/core/domain.ts +2 -2
- package/src/core/types.ts +6 -20
- package/src/design/index.ts +179 -0
- package/src/testing/fake.ts +2 -2
- package/src/testing/index.ts +7 -3
- package/src/ui/components/patterns/content-header.tsx +51 -13
- package/src/ui/components/patterns/list.tsx +51 -33
- package/src/ui/components/patterns/page.tsx +139 -251
- package/src/ui/components/patterns/presentation.tsx +2 -6
- package/src/ui/components/primitives/button-group.tsx +80 -8
- package/src/ui/components/primitives/button.tsx +85 -60
- package/src/ui/components/primitives/detail.tsx +63 -57
- package/src/ui/components/primitives/dialog.tsx +8 -11
- package/src/ui/components/primitives/drawer.tsx +16 -12
- package/src/ui/components/primitives/tabs.tsx +123 -54
- package/src/ui/docs/content/actions.md +49 -43
- package/src/ui/docs/content/button.md +55 -29
- package/src/ui/docs/content/content.md +61 -36
- package/src/ui/docs/content/detail.md +28 -19
- package/src/ui/docs/content/drawer.md +53 -47
- package/src/ui/docs/content/input.md +51 -35
- package/src/ui/docs/content/page.md +84 -95
- package/src/ui/docs/content/pane.md +83 -63
- package/src/ui/docs/content/presentation.md +20 -20
- package/src/ui/docs/content/tabs.md +107 -36
- package/src/ui/docs/content/testing.md +9 -25
- package/src/ui/meta.ts +5 -5
- package/src/ui/react.tsx +0 -1
- package/src/vite/design.ts +133 -23
|
@@ -17,10 +17,7 @@ import {
|
|
|
17
17
|
SurfaceMedia,
|
|
18
18
|
type SurfaceMediaProps,
|
|
19
19
|
} from "../patterns/surface-media.tsx";
|
|
20
|
-
import {
|
|
21
|
-
CloseButton,
|
|
22
|
-
type CloseButtonSize,
|
|
23
|
-
} from "./close-button.tsx";
|
|
20
|
+
import { CloseButton, type CloseButtonSize } from "./close-button.tsx";
|
|
24
21
|
import { overlayClasses, preventDismissWhen } from "./dialog.tsx";
|
|
25
22
|
|
|
26
23
|
interface DrawerContentContextValue {
|
|
@@ -117,15 +114,15 @@ function DrawerContent({
|
|
|
117
114
|
data-slot="drawer-content"
|
|
118
115
|
data-layout={layout}
|
|
119
116
|
className={cn(
|
|
120
|
-
"fixed z-50 flex flex-col bg-background shadow-lg transition ease-in-out [--surface-background:var(--background)] data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
|
|
117
|
+
"fixed z-50 flex flex-col overflow-hidden rounded-xl border bg-background shadow-lg transition ease-in-out [--surface-background:var(--background)] data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
|
|
121
118
|
side === "right" &&
|
|
122
|
-
"inset-y-
|
|
119
|
+
"inset-y-1 right-1 h-[calc(100%-0.5rem)] w-3/4 max-w-sm data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right",
|
|
123
120
|
side === "left" &&
|
|
124
|
-
"inset-y-
|
|
121
|
+
"inset-y-1 left-1 h-[calc(100%-0.5rem)] w-3/4 max-w-sm data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left",
|
|
125
122
|
side === "top" &&
|
|
126
|
-
"inset-x-
|
|
123
|
+
"inset-x-1 top-1 h-auto data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
|
127
124
|
side === "bottom" &&
|
|
128
|
-
"inset-x-
|
|
125
|
+
"inset-x-1 bottom-1 h-auto data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
|
129
126
|
className,
|
|
130
127
|
)}
|
|
131
128
|
aria-describedby={ariaDescribedBy}
|
|
@@ -189,7 +186,10 @@ function DrawerMedia(props: SurfaceMediaProps) {
|
|
|
189
186
|
}
|
|
190
187
|
|
|
191
188
|
/** Região introdutória do header — o voltar de um fluxo modal encadeado. */
|
|
192
|
-
function DrawerNavigation({
|
|
189
|
+
function DrawerNavigation({
|
|
190
|
+
className,
|
|
191
|
+
...props
|
|
192
|
+
}: React.ComponentProps<"div">) {
|
|
193
193
|
return (
|
|
194
194
|
<div
|
|
195
195
|
data-slot="drawer-navigation-content"
|
|
@@ -210,7 +210,11 @@ function DrawerActions({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
210
210
|
);
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
function DrawerBody({
|
|
213
|
+
function DrawerBody({
|
|
214
|
+
className,
|
|
215
|
+
style,
|
|
216
|
+
...props
|
|
217
|
+
}: React.ComponentProps<"div">) {
|
|
214
218
|
const layout = useSurfaceLayout("drawer");
|
|
215
219
|
return (
|
|
216
220
|
<div
|
|
@@ -220,7 +224,7 @@ function DrawerBody({ className, style, ...props }: React.ComponentProps<"div">)
|
|
|
220
224
|
surfaceRegionClasses("drawer", layout, "body"),
|
|
221
225
|
className,
|
|
222
226
|
)}
|
|
223
|
-
style={
|
|
227
|
+
style={style}
|
|
224
228
|
{...props}
|
|
225
229
|
/>
|
|
226
230
|
);
|
|
@@ -1,43 +1,65 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
|
-
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
-
import { Tabs as TabsPrimitive } from "radix-ui"
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { Tabs as TabsPrimitive } from "radix-ui";
|
|
4
4
|
|
|
5
|
-
import { cn } from
|
|
6
|
-
import { controlHeight, type ControlSize } from
|
|
5
|
+
import { cn } from "../../lib/cn.ts";
|
|
6
|
+
import { controlHeight, type ControlSize } from "./control.ts";
|
|
7
7
|
|
|
8
8
|
// Divergência da casa (ejetado): o Tabs ganhou `size` — Button e Select têm `sm`, o Tabs
|
|
9
9
|
// não tinha, e uma fileira densa ficava com o segmento 0.25rem mais alto
|
|
10
10
|
// que os irmãos. O tamanho flui pra TabsList via contexto (a altura é dela) e a medida é a
|
|
11
11
|
// da escala de control.ts.
|
|
12
|
-
type TabsSize = Extract<ControlSize, "default" | "sm"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
12
|
+
type TabsSize = Extract<ControlSize, "default" | "sm">;
|
|
13
|
+
type TabsDistribution = "content" | "equal";
|
|
14
|
+
const TabsSizeContext = React.createContext<TabsSize>("default");
|
|
15
|
+
const TabsDistributionContext =
|
|
16
|
+
React.createContext<TabsDistribution>("content");
|
|
17
|
+
const TabsFramedContext = React.createContext(false);
|
|
18
|
+
type TabsOrientation = "horizontal" | "vertical";
|
|
19
|
+
const TabsOrientationContext =
|
|
20
|
+
React.createContext<TabsOrientation>("horizontal");
|
|
21
|
+
const TabsListVariantContext = React.createContext<"default" | "line">(
|
|
22
|
+
"default",
|
|
23
|
+
);
|
|
17
24
|
|
|
18
25
|
function Tabs({
|
|
19
26
|
className,
|
|
20
27
|
orientation = "horizontal",
|
|
21
28
|
size = "default",
|
|
29
|
+
distribution = "content",
|
|
30
|
+
framed = false,
|
|
22
31
|
...props
|
|
23
|
-
}: React.ComponentProps<typeof TabsPrimitive.Root> & {
|
|
32
|
+
}: React.ComponentProps<typeof TabsPrimitive.Root> & {
|
|
33
|
+
size?: TabsSize;
|
|
34
|
+
/** Distribui os gatilhos pela largura do conjunto ou preserva a largura do conteúdo. */
|
|
35
|
+
distribution?: TabsDistribution;
|
|
36
|
+
/** Delimita a composição sem criar uma nova superfície. */
|
|
37
|
+
framed?: boolean;
|
|
38
|
+
}) {
|
|
24
39
|
return (
|
|
25
40
|
<TabsOrientationContext.Provider value={orientation}>
|
|
26
|
-
<
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
<TabsDistributionContext.Provider value={distribution}>
|
|
42
|
+
<TabsFramedContext.Provider value={framed}>
|
|
43
|
+
<TabsSizeContext.Provider value={size}>
|
|
44
|
+
<TabsPrimitive.Root
|
|
45
|
+
data-slot="tabs"
|
|
46
|
+
data-orientation={orientation}
|
|
47
|
+
data-size={size}
|
|
48
|
+
data-distribution={distribution}
|
|
49
|
+
data-framed={framed}
|
|
50
|
+
orientation={orientation}
|
|
51
|
+
className={cn(
|
|
52
|
+
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
|
|
53
|
+
framed && "overflow-hidden rounded-xl border",
|
|
54
|
+
className,
|
|
55
|
+
)}
|
|
56
|
+
{...props}
|
|
57
|
+
/>
|
|
58
|
+
</TabsSizeContext.Provider>
|
|
59
|
+
</TabsFramedContext.Provider>
|
|
60
|
+
</TabsDistributionContext.Provider>
|
|
39
61
|
</TabsOrientationContext.Provider>
|
|
40
|
-
)
|
|
62
|
+
);
|
|
41
63
|
}
|
|
42
64
|
|
|
43
65
|
const tabsListVariants = cva(
|
|
@@ -49,14 +71,14 @@ const tabsListVariants = cva(
|
|
|
49
71
|
variants: {
|
|
50
72
|
variant: {
|
|
51
73
|
default: "bg-muted",
|
|
52
|
-
line: "gap-
|
|
74
|
+
line: "gap-1 bg-transparent",
|
|
53
75
|
},
|
|
54
76
|
},
|
|
55
77
|
defaultVariants: {
|
|
56
78
|
variant: "default",
|
|
57
79
|
},
|
|
58
|
-
}
|
|
59
|
-
)
|
|
80
|
+
},
|
|
81
|
+
);
|
|
60
82
|
|
|
61
83
|
function TabsList({
|
|
62
84
|
className,
|
|
@@ -67,20 +89,33 @@ function TabsList({
|
|
|
67
89
|
}: React.ComponentProps<typeof TabsPrimitive.List> &
|
|
68
90
|
VariantProps<typeof tabsListVariants>) {
|
|
69
91
|
// O size vem do <Tabs> (contexto) — a altura mora aqui, na list.
|
|
70
|
-
const size = React.useContext(TabsSizeContext)
|
|
71
|
-
const orientation = React.useContext(TabsOrientationContext)
|
|
72
|
-
const
|
|
73
|
-
const
|
|
92
|
+
const size = React.useContext(TabsSizeContext);
|
|
93
|
+
const orientation = React.useContext(TabsOrientationContext);
|
|
94
|
+
const distribution = React.useContext(TabsDistributionContext);
|
|
95
|
+
const normalizedVariant = variant ?? "default";
|
|
96
|
+
const hasHorizontalLine =
|
|
97
|
+
normalizedVariant === "line" && orientation === "horizontal";
|
|
74
98
|
return (
|
|
75
99
|
<TabsListVariantContext.Provider value={normalizedVariant}>
|
|
76
100
|
<TabsPrimitive.List
|
|
77
101
|
data-slot="tabs-list"
|
|
78
102
|
data-variant={normalizedVariant}
|
|
79
103
|
data-size={size}
|
|
80
|
-
|
|
104
|
+
data-distribution={distribution}
|
|
105
|
+
className={cn(
|
|
106
|
+
tabsListVariants({ variant: normalizedVariant }),
|
|
107
|
+
controlHeight[size],
|
|
108
|
+
distribution === "equal" && orientation === "horizontal" && "w-full",
|
|
109
|
+
className,
|
|
110
|
+
)}
|
|
81
111
|
style={{
|
|
82
112
|
...(hasHorizontalLine
|
|
83
|
-
? {
|
|
113
|
+
? {
|
|
114
|
+
position: "relative",
|
|
115
|
+
width: "100%",
|
|
116
|
+
height: "auto",
|
|
117
|
+
justifyContent: "flex-start",
|
|
118
|
+
}
|
|
84
119
|
: {}),
|
|
85
120
|
...style,
|
|
86
121
|
}}
|
|
@@ -92,7 +127,7 @@ function TabsList({
|
|
|
92
127
|
data-slot="tabs-line"
|
|
93
128
|
style={{
|
|
94
129
|
position: "absolute",
|
|
95
|
-
insetInline: "
|
|
130
|
+
insetInline: "0rem",
|
|
96
131
|
bottom: 0,
|
|
97
132
|
borderBottomWidth: "0.0625rem",
|
|
98
133
|
borderBottomStyle: "solid",
|
|
@@ -104,7 +139,7 @@ function TabsList({
|
|
|
104
139
|
{children}
|
|
105
140
|
</TabsPrimitive.List>
|
|
106
141
|
</TabsListVariantContext.Provider>
|
|
107
|
-
)
|
|
142
|
+
);
|
|
108
143
|
}
|
|
109
144
|
|
|
110
145
|
function TabsTrigger({
|
|
@@ -115,38 +150,72 @@ function TabsTrigger({
|
|
|
115
150
|
...props
|
|
116
151
|
}: React.ComponentProps<typeof TabsPrimitive.Trigger> & {
|
|
117
152
|
/** Ícone decorativo antes do rótulo. Em uma aba sem texto, declare também `aria-label`. */
|
|
118
|
-
icon?: React.ReactNode
|
|
153
|
+
icon?: React.ReactNode;
|
|
119
154
|
}) {
|
|
120
|
-
const variant = React.useContext(TabsListVariantContext)
|
|
121
|
-
const orientation = React.useContext(TabsOrientationContext)
|
|
122
|
-
const
|
|
155
|
+
const variant = React.useContext(TabsListVariantContext);
|
|
156
|
+
const orientation = React.useContext(TabsOrientationContext);
|
|
157
|
+
const size = React.useContext(TabsSizeContext);
|
|
158
|
+
const distribution = React.useContext(TabsDistributionContext);
|
|
159
|
+
const framed = React.useContext(TabsFramedContext);
|
|
160
|
+
const hasHorizontalLine = variant === "line" && orientation === "horizontal";
|
|
161
|
+
const content = (
|
|
162
|
+
<>
|
|
163
|
+
{icon !== undefined && (
|
|
164
|
+
<span
|
|
165
|
+
data-slot="tabs-trigger-icon"
|
|
166
|
+
aria-hidden="true"
|
|
167
|
+
className="flex size-4 shrink-0 items-center justify-center [&_svg]:size-4"
|
|
168
|
+
>
|
|
169
|
+
{icon}
|
|
170
|
+
</span>
|
|
171
|
+
)}
|
|
172
|
+
{children}
|
|
173
|
+
</>
|
|
174
|
+
);
|
|
123
175
|
return (
|
|
124
176
|
<TabsPrimitive.Trigger
|
|
125
177
|
data-slot="tabs-trigger"
|
|
126
178
|
// px: altura e indicador compensam a hairline do contêiner (um pixel físico, não escala).
|
|
127
179
|
className={cn(
|
|
128
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 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
129
|
-
"
|
|
181
|
+
"dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent",
|
|
130
182
|
"data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:text-foreground",
|
|
131
|
-
"after:absolute after:bg-
|
|
132
|
-
hasHorizontalLine &&
|
|
133
|
-
|
|
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",
|
|
184
|
+
hasHorizontalLine &&
|
|
185
|
+
cn(
|
|
186
|
+
"group/tab-trigger rounded-none px-0 py-0 hover:bg-transparent active:brightness-100 data-[state=active]:bg-transparent data-[state=active]:hover:bg-transparent",
|
|
187
|
+
framed ? "pb-2" : "pb-1.5",
|
|
188
|
+
),
|
|
189
|
+
className,
|
|
134
190
|
)}
|
|
135
|
-
style={{
|
|
191
|
+
style={{
|
|
192
|
+
...(hasHorizontalLine
|
|
193
|
+
? {
|
|
194
|
+
flex: distribution === "equal" ? "1 1 0%" : "none",
|
|
195
|
+
height: "auto",
|
|
196
|
+
minWidth: distribution === "equal" ? 0 : undefined,
|
|
197
|
+
}
|
|
198
|
+
: {}),
|
|
199
|
+
...style,
|
|
200
|
+
}}
|
|
136
201
|
{...props}
|
|
137
202
|
>
|
|
138
|
-
{
|
|
203
|
+
{hasHorizontalLine ? (
|
|
139
204
|
<span
|
|
140
|
-
data-slot="tabs-trigger-
|
|
141
|
-
|
|
142
|
-
|
|
205
|
+
data-slot="tabs-trigger-content"
|
|
206
|
+
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-trigger:text-foreground",
|
|
208
|
+
distribution === "equal" && "w-full",
|
|
209
|
+
controlHeight[size],
|
|
210
|
+
)}
|
|
143
211
|
>
|
|
144
|
-
{
|
|
212
|
+
{content}
|
|
145
213
|
</span>
|
|
214
|
+
) : (
|
|
215
|
+
content
|
|
146
216
|
)}
|
|
147
|
-
{children}
|
|
148
217
|
</TabsPrimitive.Trigger>
|
|
149
|
-
)
|
|
218
|
+
);
|
|
150
219
|
}
|
|
151
220
|
|
|
152
221
|
function TabsContent({
|
|
@@ -159,7 +228,7 @@ function TabsContent({
|
|
|
159
228
|
className={cn("flex-1 outline-none", className)}
|
|
160
229
|
{...props}
|
|
161
230
|
/>
|
|
162
|
-
)
|
|
231
|
+
);
|
|
163
232
|
}
|
|
164
233
|
|
|
165
|
-
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
|
|
234
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
|
|
@@ -152,54 +152,62 @@ A autorização é fechada por padrão: sem `public: true`, a action exige usuá
|
|
|
152
152
|
|
|
153
153
|
## Prototipar com dados simulados
|
|
154
154
|
|
|
155
|
-
Para desenhar a interface antes de o backend existir,
|
|
156
|
-
|
|
157
|
-
`
|
|
158
|
-
handler real, e o mock não precisa acessar `ctx.db`.
|
|
155
|
+
Para desenhar a interface antes de o backend existir, rode o app em modo design. Nesse modo, toda
|
|
156
|
+
action responde sem que o contrato declare nada: a resposta é gerada pelo `fake` a partir do
|
|
157
|
+
`output` do contrato (ADR 0037).
|
|
159
158
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
- A mesma requisição recebe sempre a mesma resposta.
|
|
160
|
+
- Uma `list` devolve três páginas do tamanho pedido em `limit` (20 quando a entrada não pede, até
|
|
161
|
+
100) e respeita a paginação declarada. Busca e filtros não filtram o dado gerado.
|
|
162
|
+
- Quando a entrada e a saída têm `id`, a saída repete o `id` da entrada, desde que o schema da saída
|
|
163
|
+
aceite esse valor. Assim, abrir um item da lista mostra o mesmo identificador.
|
|
164
|
+
|
|
165
|
+
O dado gerado tem o formato certo, mas não segue a regra de negócio: os valores de um enum são
|
|
166
|
+
sorteados, e uma listagem e um detalhe só coincidem no `id`. Quando o design precisa de coerência,
|
|
167
|
+
como um item em cada situação ou algo criado que aparece na lista, escreva um cenário. Ele fica fora
|
|
168
|
+
do contrato e é ligado à action pelo nome:
|
|
163
169
|
|
|
164
170
|
```ts
|
|
165
|
-
import {
|
|
166
|
-
import {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
171
|
+
import { defineDesignScenario } from '@softize/opus/design'
|
|
172
|
+
import { eventGet, eventList } from '../domains/event/contract.ts'
|
|
173
|
+
|
|
174
|
+
// Valor fixo: o caso mais comum.
|
|
175
|
+
export const eventsInEachState = defineDesignScenario(eventList, {
|
|
176
|
+
items: [
|
|
177
|
+
{ id: 'e1', title: 'Revisão agendada', status: 'scheduled' },
|
|
178
|
+
{ id: 'e2', title: 'Revisão perdida', status: 'lost' },
|
|
179
|
+
],
|
|
180
|
+
cursor: { next: null },
|
|
181
|
+
total: 2,
|
|
182
|
+
})
|
|
170
183
|
|
|
171
|
-
//
|
|
172
|
-
const
|
|
184
|
+
// Função: quando a resposta depende da entrada.
|
|
185
|
+
export const eventDetail = defineDesignScenario(eventGet, (input) => ({
|
|
186
|
+
id: input.id,
|
|
187
|
+
title: 'Revisão agendada',
|
|
188
|
+
status: 'scheduled',
|
|
189
|
+
}))
|
|
190
|
+
```
|
|
173
191
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
mockHandler: () => fake(eventRowSchema),
|
|
180
|
-
})
|
|
192
|
+
A resposta é tipada pela entrada e pela saída do contrato; em `list`, é a página inteira. Informe ao
|
|
193
|
+
plugin onde os cenários estão, com caminhos ou globs:
|
|
194
|
+
`opusDesign({ scenarios: 'design/scenarios/*.ts' })`. O cenário vence a resposta gerada. Um cenário
|
|
195
|
+
de uma action que não existe, ou dois cenários para a mesma action, impedem o runtime de design de
|
|
196
|
+
subir, e o erro lista os nomes.
|
|
181
197
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
input: z.object({}),
|
|
186
|
-
output: eventRowSchema,
|
|
187
|
-
paginate: 'cursor',
|
|
188
|
-
mockHandler: async () => ({ items: fakeMany(eventRowSchema, 20), cursor: { next: null }, total: 20 }),
|
|
189
|
-
})
|
|
198
|
+
O Opus não decide onde os cenários ficam nem se entram no repositório. Eles não fazem parte da
|
|
199
|
+
aplicação: podem ficar numa pasta ignorada pelo git e ser escritos durante a sessão de design,
|
|
200
|
+
quando forem necessários.
|
|
190
201
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
})
|
|
196
|
-
```
|
|
202
|
+
`mockHandler` está obsoleto desde a 22.1.0 e sai na próxima major. Enquanto existir, ele ainda
|
|
203
|
+
responde no modo design, abaixo do cenário e acima da resposta gerada, e o `opus check` conta
|
|
204
|
+
quantas actions ainda o declaram. Para migrar, apague o `mockHandler`; se a resposta dele importar
|
|
205
|
+
para o design, transforme-a em cenário.
|
|
197
206
|
|
|
198
|
-
O `
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
backend. A aplicação chama `/api` como sempre e recebe os dados simulados.
|
|
207
|
+
O plugin `opusDesign()`, de `@softize/opus/vite`, monta o runtime do Opus dentro do dev server e
|
|
208
|
+
responde `/api` no próprio processo. No modo design, qualquer `server.proxy` para um backend externo
|
|
209
|
+
fica desligado: um prefixo que ia para o proxy e não tem action correspondente responde 503 em
|
|
210
|
+
envelope, sem alcançar o backend. A aplicação chama `/api` como sempre e recebe os dados simulados.
|
|
203
211
|
|
|
204
212
|
```ts
|
|
205
213
|
// vite.config.ts — o plugin só carrega no modo design, por import dinâmico.
|
|
@@ -233,9 +241,7 @@ nem reiniciar o dev server. Quando uma action não é encontrada, o plugin recon
|
|
|
233
241
|
vez por ciclo de mudança e tenta de novo; se a action continuar ausente, a resposta indica o
|
|
234
242
|
`entry` a conferir.
|
|
235
243
|
|
|
236
|
-
|
|
237
|
-
chamado. Não o condicione com `import.meta.env` no contrato: essa variável não existe no Node e
|
|
238
|
-
quebraria o servidor.
|
|
244
|
+
Os cenários não entram no bundle da web: eles ficam fora do contrato, e só o plugin os carrega.
|
|
239
245
|
|
|
240
246
|
## Consumir o contrato na interface
|
|
241
247
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
## Contexto e variante
|
|
2
2
|
|
|
3
|
-
Use `Button` para iniciar uma ação. `context` declara a hierarquia ou o risco da ação;
|
|
4
|
-
escolhe o tratamento visual. Use `primary` para a ação principal, `neutral` para ações de
|
|
5
|
-
`
|
|
3
|
+
Use `Button` para iniciar uma ação. `context` declara a hierarquia, a cautela ou o risco da ação;
|
|
4
|
+
`variant` escolhe o tratamento visual. Use `primary` para a ação principal, `neutral` para ações de
|
|
5
|
+
apoio, `warning` quando a pessoa puder prosseguir mas o efeito merecer atenção e `danger` quando a
|
|
6
|
+
ação tiver uma consequência perigosa. A variante `outline` mantém o fundo
|
|
6
7
|
transparente em repouso e usa uma superfície apenas no hover, preservando o fundo da região onde o
|
|
7
8
|
botão está inserido. `subtle` preenche o botão com um tom suave do contexto, para ações que precisam
|
|
8
9
|
de presença sem competir com a principal.
|
|
@@ -13,6 +14,7 @@ de presença sem competir com a principal.
|
|
|
13
14
|
<Button variant="subtle">Convidar pessoa</Button>
|
|
14
15
|
<Button context="neutral" variant="outline">Ver prévia</Button>
|
|
15
16
|
<Button context="neutral" variant="ghost">Cancelar</Button>
|
|
17
|
+
<Button context="warning" variant="ghost">Registrar tentativa sem sucesso</Button>
|
|
16
18
|
<Button context="danger" variant="solid">Excluir</Button>
|
|
17
19
|
<Button context="primary" variant="link">Ver documentação</Button>
|
|
18
20
|
```
|
|
@@ -27,16 +29,16 @@ há texto visível. O glifo dentro do controle acompanha o tamanho (0.875rem em
|
|
|
27
29
|
`icon-xs`, `icon-sm` e `icon`; 1rem em `default`; 1.25rem em `lg` e `icon-lg`), a menos que o
|
|
28
30
|
ícone traga um `size-*` próprio. O quadrado `icon` mantém a área de 2.25rem com o glifo discreto.
|
|
29
31
|
|
|
30
|
-
| Nome | Medida | Uso
|
|
31
|
-
| --------- | ------- |
|
|
32
|
-
| `xs` | 1.5rem | Ação dentro de um campo (`InputGroupButton`).
|
|
33
|
-
| `sm` | 2rem | Operação densa em toolbar ou dentro de uma seção, ao lado de `Select` e `Tabs` `sm`.
|
|
32
|
+
| Nome | Medida | Uso |
|
|
33
|
+
| --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
34
|
+
| `xs` | 1.5rem | Ação dentro de um campo (`InputGroupButton`). |
|
|
35
|
+
| `sm` | 2rem | Operação densa em toolbar ou dentro de uma seção, ao lado de `Select` e `Tabs` `sm`. |
|
|
34
36
|
| `default` | 2.25rem | Decisão da superfície: header de Page, criação no cabeçalho de uma coleção e footer de Dialog ou Drawer; é também a linha de `Input`. |
|
|
35
|
-
| `lg` | 2.5rem | Chamada principal com mais área de toque.
|
|
36
|
-
| `icon-xs` | 1.5rem | Ação só de ícone dentro de um campo ou de uma linha densa; é o quadrado de `ActionTrigger` com `icon`.
|
|
37
|
-
| `icon-sm` | 1.75rem | Ação só de ícone em composições compactas que precisam de mais presença; também é o quadrado das setas do pager de `ActionList`.
|
|
38
|
-
| `icon` | 2.25rem | Ação só de ícone na linha padrão.
|
|
39
|
-
| `icon-lg` | 2.5rem | Ação só de ícone ao lado de um `lg`.
|
|
37
|
+
| `lg` | 2.5rem | Chamada principal com mais área de toque. |
|
|
38
|
+
| `icon-xs` | 1.5rem | Ação só de ícone dentro de um campo ou de uma linha densa; é o quadrado de `ActionTrigger` com `icon`. |
|
|
39
|
+
| `icon-sm` | 1.75rem | Ação só de ícone em composições compactas que precisam de mais presença; também é o quadrado das setas do pager de `ActionList`. |
|
|
40
|
+
| `icon` | 2.25rem | Ação só de ícone na linha padrão. |
|
|
41
|
+
| `icon-lg` | 2.5rem | Ação só de ícone ao lado de um `lg`. |
|
|
40
42
|
|
|
41
43
|
Escolha o tamanho pela região, não pela importância visual: `variant` e `context` resolvem a
|
|
42
44
|
hierarquia da ação. Headers de Page, a criação no cabeçalho de uma coleção e footers de
|
|
@@ -102,15 +104,15 @@ estilos, use `buttonVariants` para obter as classes.
|
|
|
102
104
|
|
|
103
105
|
## Propriedades de Button
|
|
104
106
|
|
|
105
|
-
| Propriedade | Tipo | Padrão
|
|
106
|
-
| ----------- | ------------------------------------------------------------------------------------ |
|
|
107
|
-
| `context` | `'neutral' \| 'primary' \| 'danger'`
|
|
108
|
-
| `variant` | `'solid' \| 'subtle' \| 'outline' \| 'ghost' \| 'link'` | `'solid'`
|
|
109
|
-
| `size` | `'xs' \| 'sm' \| 'default' \| 'lg' \| 'icon-xs' \| 'icon-sm' \| 'icon' \| 'icon-lg'` | `'default'`
|
|
110
|
-
| `shape` | `'default' \| 'pill'`
|
|
111
|
-
| `asChild` | `boolean` | `false`
|
|
112
|
-
| `busy` | `boolean` | `false`
|
|
113
|
-
| `icon` | `React.ReactNode` |
|
|
107
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
108
|
+
| ----------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
109
|
+
| `context` | `'neutral' \| 'primary' \| 'warning' \| 'danger'` | `'primary'` em `solid` e `subtle`; `'neutral'` nas demais variantes | A hierarquia, a cautela ou o risco comunicado pela ação. |
|
|
110
|
+
| `variant` | `'solid' \| 'subtle' \| 'outline' \| 'ghost' \| 'link'` | `'solid'` | O tratamento visual aplicado ao contexto. |
|
|
111
|
+
| `size` | `'xs' \| 'sm' \| 'default' \| 'lg' \| 'icon-xs' \| 'icon-sm' \| 'icon' \| 'icon-lg'` | `'default'` | A medida na escala única dos controles (tabela acima). Os `icon-*` são quadrados para botões só de ícone, com `aria-label`. |
|
|
112
|
+
| `shape` | `'default' \| 'pill'` | `'default'` | Geometria do botão; `pill` arredonda as extremidades por completo. |
|
|
113
|
+
| `asChild` | `boolean` | `false` | Renderiza como o filho (Radix Slot) em vez de `<button>` — para âncoras e afins. |
|
|
114
|
+
| `busy` | `boolean` | `false` | Ação em andamento, depois do clique: mostra o `Spinner` e desabilita o botão. Não indica carregamento de conteúdo; para isso, use `DataState` ou `Skeleton`. |
|
|
115
|
+
| `icon` | `React.ReactNode` | | Ícone à esquerda, como nó (ex.: `icon={<Plus />}`); o glifo segue o `size`. Com `busy`, dá lugar ao `Spinner`; os dois não aparecem juntos. |
|
|
114
116
|
|
|
115
117
|
## ButtonGroup
|
|
116
118
|
|
|
@@ -146,6 +148,27 @@ conectar bordas. Esse modo atende ações só com ícone em barras e linhas de l
|
|
|
146
148
|
</ButtonGroup>
|
|
147
149
|
```
|
|
148
150
|
|
|
151
|
+
### Ações divididas
|
|
152
|
+
|
|
153
|
+
Use `mode="divided"` quando cada controle precisar preservar sua própria interação, mas divisores
|
|
154
|
+
visuais ajudarem a ler o conjunto. O grupo insere os divisores entre os filhos e acompanha a
|
|
155
|
+
orientação automaticamente. Combine `framed` para delimitar o conjunto sem criar um fundo e
|
|
156
|
+
`distribution="equal"` quando as ações precisarem ocupar larguras iguais.
|
|
157
|
+
|
|
158
|
+
```tsx preview col
|
|
159
|
+
<ButtonGroup framed mode="divided" distribution="equal">
|
|
160
|
+
<Button variant="ghost" icon={<Phone />}>
|
|
161
|
+
Ligar
|
|
162
|
+
</Button>
|
|
163
|
+
<Button variant="ghost" icon={<MessageSquare />}>
|
|
164
|
+
Anotar
|
|
165
|
+
</Button>
|
|
166
|
+
<Button context="danger" variant="ghost" icon={<CircleX />}>
|
|
167
|
+
Encerrar
|
|
168
|
+
</Button>
|
|
169
|
+
</ButtonGroup>
|
|
170
|
+
```
|
|
171
|
+
|
|
149
172
|
### No rodapé de Dialog e Drawer
|
|
150
173
|
|
|
151
174
|
`DialogFooter` e `DrawerFooter` cuidam somente da faixa; envolva as decisões em um
|
|
@@ -169,13 +192,15 @@ confirmação. As ações dividem a largura disponível; use `outline` no `Cance
|
|
|
169
192
|
```tsx preview col
|
|
170
193
|
<div className="w-80">
|
|
171
194
|
<ButtonGroup mode="spaced" distribution="equal">
|
|
172
|
-
<Button context="neutral" variant="outline">
|
|
195
|
+
<Button context="neutral" variant="outline">
|
|
196
|
+
Cancelar
|
|
197
|
+
</Button>
|
|
173
198
|
<Button context="danger">Excluir</Button>
|
|
174
199
|
</ButtonGroup>
|
|
175
200
|
</div>
|
|
176
201
|
```
|
|
177
202
|
|
|
178
|
-
### Ação
|
|
203
|
+
### Ação com variações
|
|
179
204
|
|
|
180
205
|
Combine a ação principal, um separador e um botão de ícone quando o mesmo comando oferecer
|
|
181
206
|
variações.
|
|
@@ -227,12 +252,13 @@ semântica de outro elemento, como `label`.
|
|
|
227
252
|
|
|
228
253
|
### Propriedades de ButtonGroup
|
|
229
254
|
|
|
230
|
-
| Propriedade | Tipo
|
|
231
|
-
| -------------- |
|
|
232
|
-
| `orientation` | `'horizontal' \| 'vertical'`
|
|
233
|
-
| `mode` | `'connected' \| 'spaced'`
|
|
234
|
-
| `shape` | `'default' \| 'pill'`
|
|
235
|
-
| `distribution` | `'content' \| 'equal'`
|
|
255
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
256
|
+
| -------------- | -------------------------------------- | -------------- | ----------------------------------------------------------------------------------------- |
|
|
257
|
+
| `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Direção do bloco e do colapso das bordas. |
|
|
258
|
+
| `mode` | `'connected' \| 'spaced' \| 'divided'` | `'connected'` | Conecta as bordas, separa por espaço ou insere divisores entre os controles. |
|
|
259
|
+
| `shape` | `'default' \| 'pill'` | `'default'` | Geometria das extremidades externas do grupo. |
|
|
260
|
+
| `distribution` | `'content' \| 'equal'` | `'content'` | Mantém a largura do conteúdo ou divide igualmente o espaço disponível entre os controles. |
|
|
261
|
+
| `framed` | `boolean` | `false` | Aplica borda, raio e padding ao grupo sem definir uma nova superfície. |
|
|
236
262
|
|
|
237
263
|
### Propriedades de ButtonGroupSeparator
|
|
238
264
|
|