@softize/opus 18.0.1 → 18.1.1
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 +105 -0
- package/PROMOTED.md +4 -5
- package/README.md +5 -4
- package/bin/cli.mjs +4 -0
- package/docs/adr/0004-page-content-state-is-composed.md +3 -0
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +6 -2
- package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +6 -1
- package/docs/adr/0014-structural-headers-do-not-carry-description.md +2 -2
- package/docs/adr/0015-action-size-follows-interaction-density.md +4 -3
- package/docs/adr/0017-ui-assumes-a-fixed-desktop-layout.md +54 -0
- package/docs/adr/{0012-modal-header-only-names-the-surface.md → 0018-modal-header-only-names-the-surface.md} +4 -1
- package/docs/adr/0019-productive-surfaces-use-compact-density.md +65 -0
- package/docs/code-style.md +2 -2
- package/docs/consumer-upgrade-propagation.md +1 -1
- package/docs/data-products.md +5 -3
- package/docs/protocol.md +6 -6
- package/docs/relative-unit-scale.md +9 -2
- package/docs/releasing.md +28 -4
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/references/evaluations.md +3 -1
- package/registry/skills/build-opus-ui/references/ui-patterns.md +20 -4
- package/src/auth/drivers/jwt.ts +2 -1
- package/src/core/runtime.ts +32 -5
- package/src/core/types.ts +16 -7
- package/src/mcp/index.ts +13 -1
- package/src/ui/components/patterns/action-list-dialog.tsx +2 -2
- package/src/ui/components/patterns/content-header.tsx +2 -2
- package/src/ui/components/patterns/form-dialog.tsx +7 -2
- package/src/ui/components/patterns/form.tsx +1 -1
- package/src/ui/components/patterns/list.tsx +239 -47
- package/src/ui/components/patterns/presentation.tsx +7 -5
- package/src/ui/components/patterns/sidebar.tsx +1 -1
- package/src/ui/components/patterns/state-surface.tsx +2 -2
- package/src/ui/components/patterns/surface-header.tsx +4 -4
- package/src/ui/components/primitives/alert.tsx +2 -2
- package/src/ui/components/primitives/breadcrumb.tsx +1 -1
- package/src/ui/components/primitives/button-group.tsx +1 -1
- package/src/ui/components/primitives/button.tsx +3 -3
- package/src/ui/components/primitives/calendar.tsx +1 -1
- package/src/ui/components/primitives/card.tsx +1 -1
- package/src/ui/components/primitives/close-button.tsx +40 -0
- package/src/ui/components/primitives/detail.tsx +68 -30
- package/src/ui/components/primitives/dialog.tsx +36 -21
- package/src/ui/components/primitives/drawer.tsx +26 -19
- package/src/ui/components/primitives/empty-value.tsx +3 -3
- package/src/ui/components/primitives/empty.tsx +1 -1
- package/src/ui/components/primitives/field.tsx +12 -12
- package/src/ui/components/primitives/icon-picker.tsx +1 -1
- package/src/ui/components/primitives/input-group.tsx +1 -1
- package/src/ui/components/primitives/input.tsx +2 -2
- package/src/ui/components/primitives/item.tsx +5 -5
- package/src/ui/components/primitives/pagination.tsx +4 -4
- package/src/ui/components/primitives/radio-group.tsx +1 -1
- package/src/ui/components/primitives/select.tsx +3 -3
- package/src/ui/components/primitives/table.tsx +26 -17
- package/src/ui/components/primitives/tabs.tsx +80 -23
- package/src/ui/components/primitives/textarea.tsx +1 -1
- package/src/ui/components/primitives/toggle-group.tsx +9 -2
- package/src/ui/docs/content/action-form-dialog.md +11 -4
- package/src/ui/docs/content/action-form.md +13 -3
- package/src/ui/docs/content/action-list-dialog.md +5 -7
- package/src/ui/docs/content/action-list.md +53 -5
- package/src/ui/docs/content/action-trigger.md +9 -5
- package/src/ui/docs/content/action-view.md +12 -8
- package/src/ui/docs/content/actions.md +36 -13
- package/src/ui/docs/content/ai.md +26 -7
- package/src/ui/docs/content/alert.md +6 -3
- package/src/ui/docs/content/aspect-ratio.md +2 -2
- package/src/ui/docs/content/auth.md +25 -10
- package/src/ui/docs/content/avatar.md +1 -1
- package/src/ui/docs/content/badge.md +2 -2
- package/src/ui/docs/content/breadcrumb.md +3 -2
- package/src/ui/docs/content/button.md +33 -8
- package/src/ui/docs/content/calendar.md +1 -1
- package/src/ui/docs/content/card.md +1 -1
- package/src/ui/docs/content/carousel.md +14 -3
- package/src/ui/docs/content/chat.md +1 -1
- package/src/ui/docs/content/cli.md +13 -7
- package/src/ui/docs/content/command.md +34 -2
- package/src/ui/docs/content/composer.md +1 -1
- package/src/ui/docs/content/content.md +5 -4
- package/src/ui/docs/content/customization.md +12 -2
- package/src/ui/docs/content/cycle.md +7 -5
- package/src/ui/docs/content/data-state.md +6 -5
- package/src/ui/docs/content/data.md +3 -3
- package/src/ui/docs/content/detail.md +12 -10
- package/src/ui/docs/content/dialog.md +14 -7
- package/src/ui/docs/content/dictionary-value.md +1 -1
- package/src/ui/docs/content/dock.md +23 -2
- package/src/ui/docs/content/dot.md +0 -2
- package/src/ui/docs/content/drawer.md +7 -4
- package/src/ui/docs/content/empty-value.md +4 -4
- package/src/ui/docs/content/empty.md +1 -4
- package/src/ui/docs/content/events.md +1 -1
- package/src/ui/docs/content/field.md +21 -12
- package/src/ui/docs/content/getting-started.md +4 -2
- package/src/ui/docs/content/icon-picker.md +2 -2
- package/src/ui/docs/content/input-otp.md +2 -0
- package/src/ui/docs/content/input.md +2 -3
- package/src/ui/docs/content/item.md +6 -3
- package/src/ui/docs/content/kbd.md +2 -1
- package/src/ui/docs/content/mcp.md +10 -4
- package/src/ui/docs/content/menu.md +27 -0
- package/src/ui/docs/content/page.md +20 -6
- package/src/ui/docs/content/pagination.md +9 -2
- package/src/ui/docs/content/popover.md +2 -2
- package/src/ui/docs/content/presentation.md +48 -47
- package/src/ui/docs/content/progress.md +2 -6
- package/src/ui/docs/content/runtime.md +8 -5
- package/src/ui/docs/content/scheduler.md +1 -1
- package/src/ui/docs/content/select.md +13 -8
- package/src/ui/docs/content/sidebar.md +3 -2
- package/src/ui/docs/content/skeleton.md +1 -1
- package/src/ui/docs/content/slider.md +4 -4
- package/src/ui/docs/content/spinner.md +3 -3
- package/src/ui/docs/content/tabs.md +22 -12
- package/src/ui/docs/content/testing.md +4 -2
- package/src/ui/docs/content/toast.md +5 -6
- package/src/ui/docs/content/toggle.md +37 -0
- package/src/ui/docs/content/tokens.md +45 -2
- package/src/ui/docs/content/tooltip.md +4 -3
- package/src/ui/docs/content/truncate.md +3 -2
- package/src/ui/docs/content/ui.md +3 -1
- package/src/ui/docs/content/upgrading.md +43 -13
- package/src/ui/docs/doc-client.tsx +1 -1
- package/src/ui/docs/registry.tsx +30 -5
- package/src/ui/meta.ts +4 -4
- package/src/ui/react.tsx +1 -0
- package/src/ui/theme.css +3 -0
|
@@ -92,7 +92,7 @@ interface SelectCustomBase extends SelectBaseProps {
|
|
|
92
92
|
clearable?: boolean
|
|
93
93
|
/** `outline`: compacto com borda; `ghost`: compacto sem borda nem fundo. */
|
|
94
94
|
variant?: 'default' | 'outline' | 'ghost'
|
|
95
|
-
/** Placeholder
|
|
95
|
+
/** Placeholder da linha de busca dentro da lista. Default: `'Buscar…'`. */
|
|
96
96
|
searchPlaceholder?: string
|
|
97
97
|
emptyText?: string
|
|
98
98
|
/** Ação custom no FIM do campo, dentro do controle (antes do chevron) — ex.: um botão que
|
|
@@ -161,7 +161,7 @@ function NativeSelect({
|
|
|
161
161
|
aria-describedby={rest['aria-describedby']}
|
|
162
162
|
onChange={(e) => onChange(e.target.value)}
|
|
163
163
|
className={cn(
|
|
164
|
-
"h-9 w-full min-w-0 appearance-none rounded-md border border-input bg-
|
|
164
|
+
"h-9 w-full min-w-0 appearance-none rounded-md border border-input bg-muted/30 px-3 py-2 pr-9 text-sm transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground disabled:pointer-events-none disabled:cursor-not-allowed data-[size=sm]:h-8 data-[size=sm]:py-1 data-[shape=pill]:rounded-full dark:bg-input/30 dark:hover:bg-input/50",
|
|
165
165
|
focusRing,
|
|
166
166
|
'aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 dark:aria-invalid:ring-context-danger/40',
|
|
167
167
|
icon ? 'pl-9' : '',
|
|
@@ -360,7 +360,7 @@ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.Rea
|
|
|
360
360
|
'flex items-center gap-1.5 rounded-md outline-none transition-[color,box-shadow] data-[shape=pill]:rounded-full',
|
|
361
361
|
ghost
|
|
362
362
|
? cn('min-h-9 px-3 py-1 text-muted-foreground hover:bg-muted hover:text-foreground data-[size=sm]:min-h-8 data-[size=sm]:px-2', focusRingWithin)
|
|
363
|
-
: cn('min-h-9 border border-input bg-
|
|
363
|
+
: cn('min-h-9 border border-input bg-muted/30 px-3 py-1 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 data-[size=sm]:min-h-8 data-[size=sm]:py-0.5 dark:bg-input/30 dark:aria-invalid:ring-context-danger/40', focusRingWithin),
|
|
364
364
|
variant === 'default' && 'flex-wrap',
|
|
365
365
|
outline && 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-input/50',
|
|
366
366
|
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
@@ -2,36 +2,45 @@ import * as React from "react"
|
|
|
2
2
|
|
|
3
3
|
import { cn } from '../../lib/cn.ts'
|
|
4
4
|
|
|
5
|
+
type TableVariant = 'plain' | 'framed'
|
|
6
|
+
const TableVariantContext = React.createContext<TableVariant>('plain')
|
|
7
|
+
|
|
5
8
|
export interface TableProps extends React.ComponentProps<"table"> {
|
|
6
9
|
/** `framed` aplica a moldura canônica de datagrid no contêiner da tabela. */
|
|
7
|
-
variant?:
|
|
10
|
+
variant?: TableVariant
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
function Table({ className, variant = 'plain', ...props }: TableProps) {
|
|
11
14
|
return (
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
'rounded-lg border border-border
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
<TableVariantContext.Provider value={variant}>
|
|
16
|
+
<div
|
|
17
|
+
data-slot="table-container"
|
|
18
|
+
data-variant={variant}
|
|
19
|
+
className={cn(
|
|
20
|
+
"relative w-full overflow-x-auto",
|
|
21
|
+
variant === 'framed' && 'rounded-lg border border-border',
|
|
22
|
+
)}
|
|
23
|
+
>
|
|
24
|
+
<table
|
|
25
|
+
data-slot="table"
|
|
26
|
+
className={cn("w-full caption-bottom text-sm", className)}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
</TableVariantContext.Provider>
|
|
27
31
|
)
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
|
35
|
+
const variant = React.useContext(TableVariantContext)
|
|
31
36
|
return (
|
|
32
37
|
<thead
|
|
33
38
|
data-slot="table-header"
|
|
34
|
-
className={cn(
|
|
39
|
+
className={cn(
|
|
40
|
+
"[&_tr]:border-b",
|
|
41
|
+
variant === 'framed' && 'bg-muted/30 dark:bg-input/30',
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
35
44
|
{...props}
|
|
36
45
|
/>
|
|
37
46
|
)
|
|
@@ -11,6 +11,9 @@ import { controlHeight, type ControlSize } from './control.ts'
|
|
|
11
11
|
// da escala de control.ts.
|
|
12
12
|
type TabsSize = Extract<ControlSize, "default" | "sm">
|
|
13
13
|
const TabsSizeContext = React.createContext<TabsSize>("default")
|
|
14
|
+
type TabsOrientation = "horizontal" | "vertical"
|
|
15
|
+
const TabsOrientationContext = React.createContext<TabsOrientation>("horizontal")
|
|
16
|
+
const TabsListVariantContext = React.createContext<"default" | "line">("default")
|
|
14
17
|
|
|
15
18
|
function Tabs({
|
|
16
19
|
className,
|
|
@@ -19,19 +22,21 @@ function Tabs({
|
|
|
19
22
|
...props
|
|
20
23
|
}: React.ComponentProps<typeof TabsPrimitive.Root> & { size?: TabsSize }) {
|
|
21
24
|
return (
|
|
22
|
-
<
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
25
|
+
<TabsOrientationContext.Provider value={orientation}>
|
|
26
|
+
<TabsSizeContext.Provider value={size}>
|
|
27
|
+
<TabsPrimitive.Root
|
|
28
|
+
data-slot="tabs"
|
|
29
|
+
data-orientation={orientation}
|
|
30
|
+
data-size={size}
|
|
31
|
+
orientation={orientation}
|
|
32
|
+
className={cn(
|
|
33
|
+
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
|
|
34
|
+
className
|
|
35
|
+
)}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
</TabsSizeContext.Provider>
|
|
39
|
+
</TabsOrientationContext.Provider>
|
|
35
40
|
)
|
|
36
41
|
}
|
|
37
42
|
|
|
@@ -44,7 +49,7 @@ const tabsListVariants = cva(
|
|
|
44
49
|
variants: {
|
|
45
50
|
variant: {
|
|
46
51
|
default: "bg-muted",
|
|
47
|
-
line: "gap-
|
|
52
|
+
line: "gap-5 bg-transparent",
|
|
48
53
|
},
|
|
49
54
|
},
|
|
50
55
|
defaultVariants: {
|
|
@@ -55,27 +60,66 @@ const tabsListVariants = cva(
|
|
|
55
60
|
|
|
56
61
|
function TabsList({
|
|
57
62
|
className,
|
|
63
|
+
children,
|
|
64
|
+
style,
|
|
58
65
|
variant = "default",
|
|
59
66
|
...props
|
|
60
67
|
}: React.ComponentProps<typeof TabsPrimitive.List> &
|
|
61
68
|
VariantProps<typeof tabsListVariants>) {
|
|
62
69
|
// O size vem do <Tabs> (contexto) — a altura mora aqui, na list.
|
|
63
70
|
const size = React.useContext(TabsSizeContext)
|
|
71
|
+
const orientation = React.useContext(TabsOrientationContext)
|
|
72
|
+
const normalizedVariant = variant ?? "default"
|
|
73
|
+
const hasHorizontalLine = normalizedVariant === "line" && orientation === "horizontal"
|
|
64
74
|
return (
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
<TabsListVariantContext.Provider value={normalizedVariant}>
|
|
76
|
+
<TabsPrimitive.List
|
|
77
|
+
data-slot="tabs-list"
|
|
78
|
+
data-variant={normalizedVariant}
|
|
79
|
+
data-size={size}
|
|
80
|
+
className={cn(tabsListVariants({ variant: normalizedVariant }), controlHeight[size], className)}
|
|
81
|
+
style={{
|
|
82
|
+
...(hasHorizontalLine
|
|
83
|
+
? { position: "relative", width: "100%", height: "auto", justifyContent: "flex-start" }
|
|
84
|
+
: {}),
|
|
85
|
+
...style,
|
|
86
|
+
}}
|
|
87
|
+
{...props}
|
|
88
|
+
>
|
|
89
|
+
{hasHorizontalLine && (
|
|
90
|
+
<span
|
|
91
|
+
aria-hidden="true"
|
|
92
|
+
data-slot="tabs-line"
|
|
93
|
+
style={{
|
|
94
|
+
position: "absolute",
|
|
95
|
+
insetInline: "calc(var(--opus-inline-gutter, 0rem) * -1)",
|
|
96
|
+
bottom: 0,
|
|
97
|
+
borderBottomWidth: "0.0625rem",
|
|
98
|
+
borderBottomStyle: "solid",
|
|
99
|
+
borderBottomColor: "var(--border)",
|
|
100
|
+
pointerEvents: "none",
|
|
101
|
+
}}
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
104
|
+
{children}
|
|
105
|
+
</TabsPrimitive.List>
|
|
106
|
+
</TabsListVariantContext.Provider>
|
|
72
107
|
)
|
|
73
108
|
}
|
|
74
109
|
|
|
75
110
|
function TabsTrigger({
|
|
76
111
|
className,
|
|
112
|
+
style,
|
|
113
|
+
icon,
|
|
114
|
+
children,
|
|
77
115
|
...props
|
|
78
|
-
}: React.ComponentProps<typeof TabsPrimitive.Trigger>
|
|
116
|
+
}: React.ComponentProps<typeof TabsPrimitive.Trigger> & {
|
|
117
|
+
/** Ícone decorativo antes do rótulo. Em uma aba sem texto, declare também `aria-label`. */
|
|
118
|
+
icon?: React.ReactNode
|
|
119
|
+
}) {
|
|
120
|
+
const variant = React.useContext(TabsListVariantContext)
|
|
121
|
+
const orientation = React.useContext(TabsOrientationContext)
|
|
122
|
+
const hasHorizontalLine = variant === "line" && orientation === "horizontal"
|
|
79
123
|
return (
|
|
80
124
|
<TabsPrimitive.Trigger
|
|
81
125
|
data-slot="tabs-trigger"
|
|
@@ -85,10 +129,23 @@ function TabsTrigger({
|
|
|
85
129
|
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
|
|
86
130
|
"data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:text-foreground",
|
|
87
131
|
"after:absolute after:bg-foreground 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",
|
|
132
|
+
hasHorizontalLine && "px-0 py-3",
|
|
88
133
|
className
|
|
89
134
|
)}
|
|
135
|
+
style={{ ...(hasHorizontalLine ? { flex: "none", height: "auto" } : {}), ...style }}
|
|
90
136
|
{...props}
|
|
91
|
-
|
|
137
|
+
>
|
|
138
|
+
{icon !== undefined && (
|
|
139
|
+
<span
|
|
140
|
+
data-slot="tabs-trigger-icon"
|
|
141
|
+
aria-hidden="true"
|
|
142
|
+
className="flex size-4 shrink-0 items-center justify-center [&_svg]:size-4"
|
|
143
|
+
>
|
|
144
|
+
{icon}
|
|
145
|
+
</span>
|
|
146
|
+
)}
|
|
147
|
+
{children}
|
|
148
|
+
</TabsPrimitive.Trigger>
|
|
92
149
|
)
|
|
93
150
|
}
|
|
94
151
|
|
|
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
|
7
7
|
<textarea
|
|
8
8
|
data-slot="textarea"
|
|
9
9
|
className={cn(
|
|
10
|
-
"flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-
|
|
10
|
+
"flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-muted/30 px-3 py-2 text-sm transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 dark:bg-input/30 dark:aria-invalid:ring-context-danger/40",
|
|
11
11
|
className
|
|
12
12
|
)}
|
|
13
13
|
{...props}
|
|
@@ -18,6 +18,12 @@ const ToggleGroupContext = React.createContext<
|
|
|
18
18
|
shape: "default",
|
|
19
19
|
})
|
|
20
20
|
|
|
21
|
+
type ToggleGroupItemProps = React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
|
22
|
+
VariantProps<typeof toggleVariants> & {
|
|
23
|
+
'data-slot'?: string
|
|
24
|
+
'data-state'?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
function ToggleGroup({
|
|
22
28
|
className,
|
|
23
29
|
variant,
|
|
@@ -58,9 +64,10 @@ function ToggleGroupItem({
|
|
|
58
64
|
variant,
|
|
59
65
|
size,
|
|
60
66
|
shape,
|
|
67
|
+
'data-slot': _composedSlot,
|
|
68
|
+
'data-state': _composedState,
|
|
61
69
|
...props
|
|
62
|
-
}:
|
|
63
|
-
VariantProps<typeof toggleVariants>) {
|
|
70
|
+
}: ToggleGroupItemProps) {
|
|
64
71
|
const context = React.useContext(ToggleGroupContext)
|
|
65
72
|
|
|
66
73
|
return (
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
Use `ActionFormDialog` quando o formulário precisar interromper o fluxo atual sem levar a pessoa
|
|
4
4
|
para outra página. O consumidor controla `open`; depois de uma execução bem-sucedida, o componente
|
|
5
5
|
fecha o modal. O cabeçalho e o rodapé permanecem visíveis enquanto os campos podem rolar.
|
|
6
|
-
|
|
7
|
-
a ação principal permanece `solid`, ambas no tamanho normal de uma decisão
|
|
6
|
+
O rodapé agrupa as decisões em um `ButtonGroup` cuja largura acompanha o conteúdo: `Cancelar`
|
|
7
|
+
aparece em `ghost` e a ação principal permanece `solid`, ambas no tamanho normal de uma decisão
|
|
8
|
+
modal. `Cancelar` está sempre presente; sem `onCancel` próprio, ele fecha o modal.
|
|
9
|
+
Com `footerDistribution="equal"`, as duas decisões dividem a faixa e `Cancelar` passa a `outline`,
|
|
10
|
+
salvo escolha explícita em `cancelVariant`.
|
|
8
11
|
|
|
9
12
|
```tsx preview
|
|
10
13
|
const [open, setOpen] = useState(false);
|
|
@@ -26,8 +29,12 @@ render(
|
|
|
26
29
|
|
|
27
30
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
28
31
|
| --------------------- | ------------------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
29
|
-
| `open / onOpenChange` | `boolean / (open: boolean) => void` | | Estado controlado do modal. `onOpenChange(false)` é chamado no sucesso e ao cancelar. |
|
|
32
|
+
| `open / onOpenChange` | `boolean / (open: boolean) => void` | | Estado controlado do modal. `onOpenChange(false)` é chamado no sucesso e, quando não há `onCancel` próprio, ao cancelar. |
|
|
30
33
|
| `title` | `string` | | Nome da tarefa exibido no cabeçalho. |
|
|
31
34
|
| `intro` | `ReactNode` | | Contexto relevante no início do corpo, como texto ou `Alert`. |
|
|
32
35
|
| `submitLabel` | `string` | `'Salvar'` | Resultado da ação principal. Preserve o padrão em criação e edição comuns; sobrescreva apenas quando a operação tiver outro efeito, como `Renomear` ou `Criar nova versão`. |
|
|
33
|
-
|
|
|
36
|
+
| `footerDistribution` | `'content' \| 'equal'` | `'content'` | Mantém a largura das ações pelo conteúdo ou divide a faixa igualmente. |
|
|
37
|
+
| `cancelVariant` | `'ghost' \| 'outline'` | `'ghost'`; `'outline'` com `equal` | Tratamento do `Cancelar`. O valor explícito vence o default derivado de `footerDistribution`. |
|
|
38
|
+
| `onCancel` | `() => void` | fecha o modal | Substitui o fechamento padrão do `Cancelar`; nesse caso, o consumidor decide quando chamar `onOpenChange(false)`. |
|
|
39
|
+
| `onSuccess` | `(data: TData) => void` | | Executado depois do sucesso, antes de o componente fechar o modal. |
|
|
40
|
+
| `…ActionFormProps` | `action, defaultValues, fieldOptions…` | | Demais propriedades repassadas ao `ActionForm` interno. `className`, `body` e `footer` não são repassadas: o wrapper as substitui para montar o corpo rolável e o footer do modal. |
|
|
@@ -34,7 +34,8 @@ invalidação continuam sob responsabilidade de `ActionForm`.
|
|
|
34
34
|
|
|
35
35
|
## Valores iniciais e rótulos
|
|
36
36
|
|
|
37
|
-
`defaultValues` preenche o formulário para edição.
|
|
37
|
+
`defaultValues` preenche o formulário para edição. Criação e edição comuns mantêm o rótulo padrão
|
|
38
|
+
`Salvar`; use `submitLabel` somente quando a operação pedir outro verbo, como `Renomear`.
|
|
38
39
|
`onCancel` devolve ao consumidor a decisão de fechar um painel ou navegar para outra página.
|
|
39
40
|
|
|
40
41
|
```tsx preview col md
|
|
@@ -42,12 +43,19 @@ invalidação continuam sob responsabilidade de `ActionForm`.
|
|
|
42
43
|
<ActionForm
|
|
43
44
|
action={docWorkspaceCreate}
|
|
44
45
|
defaultValues={{ name: "Empresa X", status: "active" }}
|
|
45
|
-
submitLabel="Atualizar"
|
|
46
46
|
onCancel={() => undefined}
|
|
47
47
|
/>
|
|
48
48
|
</DocBrowserActionProvider>
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
## Formulário modal
|
|
52
|
+
|
|
53
|
+
[`ActionFormDialog`](/ui/action-form-dialog) acrescenta a moldura, o corpo rolável e o footer ao formulário. Por padrão, as
|
|
54
|
+
ações ficam alinhadas no fim da faixa e preservam a largura do conteúdo; `Cancelar` usa `ghost`.
|
|
55
|
+
Use `footerDistribution="equal"` somente quando as duas decisões precisarem do mesmo peso visual.
|
|
56
|
+
Nesse caso, o cancelamento muda para `outline`, salvo escolha explícita em `cancelVariant`.
|
|
57
|
+
As propriedades do wrapper estão documentadas na página dele.
|
|
58
|
+
|
|
51
59
|
## Pré-requisitos
|
|
52
60
|
|
|
53
61
|
Monte os providers de consulta e execução uma vez na raiz do aplicativo:
|
|
@@ -75,7 +83,9 @@ ciclo de vida que o pattern não cobre; validação e execução continuam iguai
|
|
|
75
83
|
| `action` | `FormContract<TInput, TData>` | | O contrato da FormAction — dele saem campos (input Zod + fields), mensagens e invalidação de cache. |
|
|
76
84
|
| `defaultValues` | `Partial<TInput>` | | Valores iniciais — o modo edição de um update/patch. |
|
|
77
85
|
| `onSuccess` | `(data: TData) => void` | | Pós-sucesso (o toast e a invalidação de cache já aconteceram). |
|
|
78
|
-
| `submitLabel / cancelLabel / cancelVariant / onCancel` | `string / string / 'ghost' \| 'outline' / () => void` | `'Salvar' / 'Cancelar' / 'ghost'` | Rodapé do form — o Cancelar só aparece com onCancel.
|
|
86
|
+
| `submitLabel / cancelLabel / cancelVariant / onCancel` | `string / string / 'ghost' \| 'outline' / () => void` | `'Salvar' / 'Cancelar' / 'ghost'` | Rodapé do form — o Cancelar só aparece com onCancel. Fora de um modal, mantenha `ghost`; o `ActionFormDialog` usa `outline` somente com `footerDistribution="equal"`. |
|
|
87
|
+
| `disabled` | `boolean` | `false` | Bloqueia campos e ações sem desmontar o formulário. |
|
|
88
|
+
| `onLoadingChange` | `(loading: boolean) => void` | | Informa início e fim da execução a quem coordena ações como um grupo. |
|
|
79
89
|
| `fieldOptions` | `Record<string, SelectOption[]>` | | Opções de runtime por campo (ex.: ids de skills) — sobrepõe as inferidas do z.enum. |
|
|
80
90
|
| `className / body / footer` | `string / (node) => node / (node) => node` | | className = classes do `<form>`. body/footer = slots: recebem os campos / os botões e escolhem o invólucro — é como o ActionFormDialog injeta DialogBody/DialogFooter (scroll + faixa). |
|
|
81
91
|
| `children` | `ReactNode` | | Modo composição: diagrame com `<ActionFormField name />`. Sem children, o automático monta todos os campos na ordem do contrato. |
|
|
@@ -18,9 +18,7 @@ render(
|
|
|
18
18
|
onOpenChange={setOpen}
|
|
19
19
|
title="Workspaces"
|
|
20
20
|
actions={
|
|
21
|
-
<Button
|
|
22
|
-
<Plus /> Workspace
|
|
23
|
-
</Button>
|
|
21
|
+
<Button>Criar workspace</Button>
|
|
24
22
|
}
|
|
25
23
|
emptyMessage="Nenhum workspace."
|
|
26
24
|
>
|
|
@@ -65,14 +63,14 @@ corpo do modal.
|
|
|
65
63
|
|
|
66
64
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
67
65
|
| ------------------------------------------ | ----------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------- |
|
|
68
|
-
| `action / input` | `ListAction / TInput` |
|
|
66
|
+
| `action / input` | `ListAction / TInput` | `input`: `{}` | O contrato e o escopo base opcional da consulta — mudou o input, re-busca; `invalidates` de forms/triggers refaz sozinho. |
|
|
69
67
|
| `open / onOpenChange` | `boolean / (open) => void` | | Controle do modal — de quem orquestra. |
|
|
70
68
|
| `title` | `string` | | Nome da lista exibido no cabeçalho fixo. |
|
|
71
69
|
| `intro` | `ReactNode` | | Contexto relevante no início do corpo, como texto ou `Alert`. |
|
|
72
70
|
| `children` | `(items, refetch) => ReactNode` | | O layout dos itens (cards/linhas) — os estados já saíram daqui. |
|
|
73
|
-
| `note` | `ReactNode
|
|
71
|
+
| `note` | `ReactNode` | | Contexto curto à esquerda da toolbar. O total de itens já aparece no rodapé do `ActionList`. |
|
|
74
72
|
| `actions` | `ReactNode` | | Ação à direita da toolbar — em geral o botão de criar. |
|
|
75
73
|
| `empty` | `(items) => boolean` | `items.length === 0` | Sobrepõe o vazio derivado. |
|
|
76
74
|
| `loading` | `boolean` | | Carga extra agregada à do fetch (query irmã). |
|
|
77
|
-
| `emptyMessage / errorMessage / retryLabel` | `string` |
|
|
78
|
-
| `className` | `string` | `
|
|
75
|
+
| `emptyMessage / errorMessage / retryLabel` | `string` | `'Nenhum resultado.'` / `'Não foi possível carregar.'` / `'Tentar de novo'` | Textos dos estados, herdados do `ActionList`; `retryLabel` nomeia a ação somente com ícone e seu tooltip. |
|
|
76
|
+
| `className` | `string` | `max-w-3xl` | Largura do DialogContent. |
|
|
@@ -64,6 +64,44 @@ Seletores inline mantêm largura previsível (`w-40`; lookup e múltiplo usam `w
|
|
|
64
64
|
são truncados no controle, mas permanecem completos na lista. No modal de filtros avançados, o
|
|
65
65
|
seletor ocupa toda a largura. Busca, filtros e período aparecem somente quando declarados.
|
|
66
66
|
|
|
67
|
+
## Onde cada filtro aparece
|
|
68
|
+
|
|
69
|
+
Cada filtro declara `placement`. `inline`, o padrão, mantém o controle na barra; `advanced` o leva
|
|
70
|
+
ao modal “Filtros”; `external` mantém o filtro no estado navegável e no input, mas deixa a
|
|
71
|
+
apresentação para o consumidor, como cards de status acima da lista. No modal, filtros com a mesma
|
|
72
|
+
`section` formam um grupo, e `advancedFilters.columns` distribui os grupos em até três colunas.
|
|
73
|
+
Campos de data avançados usam calendário. A antiga `advanced: true` continua aceita durante a
|
|
74
|
+
migração e equivale a `placement: 'advanced'`.
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
// No contrato:
|
|
78
|
+
filters: {
|
|
79
|
+
status: {
|
|
80
|
+
label: 'Status',
|
|
81
|
+
type: 'select',
|
|
82
|
+
placement: 'inline',
|
|
83
|
+
options: { kind: 'dictionary', ref: 'workspace.status' },
|
|
84
|
+
},
|
|
85
|
+
client: {
|
|
86
|
+
label: 'Cliente',
|
|
87
|
+
type: 'lookup',
|
|
88
|
+
placement: 'advanced',
|
|
89
|
+
section: 'Relacionamento',
|
|
90
|
+
options: { kind: 'lookup', source: 'client.lookup' },
|
|
91
|
+
},
|
|
92
|
+
createdAt: { label: 'Criado em', type: 'date', placement: 'advanced', section: 'Datas' },
|
|
93
|
+
stage: {
|
|
94
|
+
label: 'Estágio',
|
|
95
|
+
type: 'select',
|
|
96
|
+
placement: 'external',
|
|
97
|
+
options: { kind: 'dictionary', ref: 'workspace.stage' },
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Na tela:
|
|
102
|
+
<ActionList action={workspaceList} input={{}} advancedFilters={{ columns: 2 }} />
|
|
103
|
+
```
|
|
104
|
+
|
|
67
105
|
## Colunas de dicionário
|
|
68
106
|
|
|
69
107
|
Quando o campo de saída usa `t.dict().zod()`, a coluna apresenta o valor com `DictionaryValue` e
|
|
@@ -88,6 +126,10 @@ Renderer customizado reutiliza `EmptyValue`.
|
|
|
88
126
|
|
|
89
127
|
Use `cells` somente quando uma coluna precisar de apresentação própria, como link, composição ou
|
|
90
128
|
ação. A chave corresponde à `key` da coluna. Valores de dicionário não precisam desse override.
|
|
129
|
+
O valor principal preserva o `text-sm` e o foreground da tabela; use
|
|
130
|
+
`text-xs text-muted-foreground` apenas em metadado subordinado a outro valor na mesma célula.
|
|
131
|
+
Cabeçalhos e `EmptyValue` continuam muted. O fato de uma coluna ser técnica, temporal ou menos
|
|
132
|
+
destacada não reduz nem atenua seu valor principal.
|
|
91
133
|
|
|
92
134
|
```tsx preview col
|
|
93
135
|
render(
|
|
@@ -291,6 +333,7 @@ render(
|
|
|
291
333
|
| `state` | `ActionFilterState` | | Estado atual da barra. |
|
|
292
334
|
| `onStateChange` | `(next) => void` | | Recebe o estado completo depois de cada alteração. |
|
|
293
335
|
| `filterOptions` | `Record<string, SelectOption[]>` | | Fornece opções de runtime para filtros select e lookup. |
|
|
336
|
+
| `advancedFilters` | `{ columns?: 1 \| 2 \| 3 }` | `{ columns: 1 }` | Define as colunas do diálogo de filtros avançados; o Opus deriva a largura correspondente. |
|
|
294
337
|
| `onRefresh` | `() => Promise<void> \| void` | | Exibe a ação de recarregar e executa a consulta do consumidor. |
|
|
295
338
|
| `refreshing` | `boolean` | `false` | Desabilita e anima a ação de recarregar durante a consulta. |
|
|
296
339
|
| `controls` | `ReactNode` | | Controles auxiliares agrupados com recarregar em um `ButtonGroup` espaçado. |
|
|
@@ -300,11 +343,11 @@ render(
|
|
|
300
343
|
|
|
301
344
|
| Chave | O que declara |
|
|
302
345
|
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
303
|
-
| `columns` | `{ key, label, type?, sortable?, fit?, hidden?, dateFormat?, dictionary?, empty? }` — a tabela. `hidden`
|
|
304
|
-
| `filters` | `{ [nome]: { label, type, options?, multiple?,
|
|
346
|
+
| `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. |
|
|
347
|
+
| `filters` | `{ [nome]: { label, type, options?, multiple?, 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. `depends` desabilita/cascateia; options `kind: 'lookup'` busca em uma action. Valor aplicado entra no input com o mesmo nome. |
|
|
305
348
|
| `text` | `{ fields }` — liga a busca; convenção: param `q` no input. Com período/filtros no contrato ela fica à direita; sendo a ÚNICA forma de recorte, abre a linha. |
|
|
306
349
|
| `sort` | `{ fields, default }` — ordenação inicial; header ordenável escreve `sort: 'chave:dir'`. |
|
|
307
|
-
| `periods` | `{ value, label }[]` — o controle de período (presets + Personalizado com calendário); materializa em `from`/`to` no input. |
|
|
350
|
+
| `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. |
|
|
308
351
|
|
|
309
352
|
## useListAction
|
|
310
353
|
|
|
@@ -318,16 +361,21 @@ chamar a action.
|
|
|
318
361
|
|
|
319
362
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
320
363
|
| ----------------------- | ------------------------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
321
|
-
| `action` | `ListAction<TInput, TItem>` | | A ListAction do Opus (kind list, output = item
|
|
364
|
+
| `action` | `ListAction<TInput, TItem>` | | A ListAction do Opus (kind list, output = item). A paginação envia `limit`/`page` e usa o `total` devolvido. |
|
|
322
365
|
| `input` | `TInput` | | O ESCOPO BASE (ex.: { workspaceId }) — a toolbar soma por cima, nunca sobrescreve. |
|
|
323
366
|
| `cells` | `Record<string, (item) => ReactNode>` | | Células custom por cima das colunas do contrato (chave = column.key). |
|
|
324
367
|
| `filterOptions` | `Record<string, SelectOption[]>` | | Opções de runtime para os filtros select/lookup (chave = nome do filtro). |
|
|
368
|
+
| `advancedFilters` | `{ columns?: 1 \| 2 \| 3 }` | `{ columns: 1 }` | Define as colunas dos grupos de filtros avançados; o Opus deriva a largura correspondente do diálogo. |
|
|
369
|
+
| `views` | `Record<string, ActionListView<TItem>>` | | Visualizações alternativas `{ label, icon?, render }`; o seletor alterna entre a tabela e estas. |
|
|
370
|
+
| `beforeContent` | `ReactNode` | | Conteúdo entre a toolbar e o corpo, como um resumo que reage ao recorte atual. |
|
|
325
371
|
| `columns` | `ActionListColumn<TItem>[]` | | Tabela EXPLÍCITA — sobrepõe as colunas do contrato (escape hatch). |
|
|
326
372
|
| `children` | `(items, refetch) => ReactNode` | | Modo composição: layout livre; a toolbar segue. Tem precedência sobre columns. |
|
|
327
|
-
| `batch` | `ActionListBatchAction[]` | | Ações em lote: `{ label, can?, run, confirm
|
|
373
|
+
| `batch` | `ActionListBatchAction[]` | | Ações em lote: `{ label, can?, run, confirm?: { title, message? }, destructive? }` — liga a multi-seleção. |
|
|
328
374
|
| `rowId` | `(item) => string` | `item.id` | Identidade da linha para seleção. |
|
|
329
375
|
| `pageSize` | `number` | `50` | Itens por página padrão (vira `limit`/`page` no input; handler devolve `total`). O usuário troca no popover de exibição. |
|
|
330
376
|
| `state / onStateChange` | `ActionListState / (next) => void` | interno | Estado da toolbar controlado — para quem embala sincronizar com a URL. |
|
|
377
|
+
| `empty` | `(items) => boolean` | `items.length === 0` | Sobrepõe o vazio derivado, por exemplo quando um formulário inline aberto conta como conteúdo. |
|
|
378
|
+
| `loading` | `boolean` | | Carga extra agregada à da consulta, como uma query irmã de que os `children` dependem. |
|
|
331
379
|
| `emptyMessage` | `string` | `'Nenhum resultado.'` | Frase do estado vazio (o `Empty` do `DataState`). |
|
|
332
380
|
| `errorMessage` | `string` | `'Não foi possível carregar.'` | Título do aviso de erro; a ação de tentar de novo refaz a consulta. |
|
|
333
381
|
| `retryLabel` | `string` | `'Tentar de novo'` | Nome acessível e tooltip da ação de recuperação. |
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Use `ActionTrigger` para executar uma `SimpleAction` a partir de um botão. Sem confirmação, o clique
|
|
4
4
|
inicia a operação, desabilita o controle durante a execução e apresenta as mensagens do contrato em
|
|
5
|
-
um toast. O rótulo padrão vem de `action.label
|
|
5
|
+
um toast. O rótulo padrão vem de `action.label` quando ele é texto; se o label for uma referência
|
|
6
|
+
de tradução ou estiver ausente, o botão usa `action.name`.
|
|
6
7
|
|
|
7
8
|
```tsx preview
|
|
8
9
|
<DocBrowserActionProvider>
|
|
@@ -58,7 +59,8 @@ não aciona o item clicável ao redor. `itemLabel` identifica o registro na mens
|
|
|
58
59
|
## Erro que a pessoa entende
|
|
59
60
|
|
|
60
61
|
Quando a action falha, uma mensagem do servidor substitui o texto genérico somente nos erros de
|
|
61
|
-
negócio `conflict`, `validation` e `not_found
|
|
62
|
+
negócio `conflict`, `validation` e `not_found`, e nos erros de acesso `authorization` e
|
|
63
|
+
`authentication`, cuja frase o runtime já entrega em pt-BR:
|
|
62
64
|
|
|
63
65
|
> Este agente tem 3 conversas — desabilite em vez de excluir.
|
|
64
66
|
|
|
@@ -78,10 +80,12 @@ um atalho, um arrastar, um item de menu.
|
|
|
78
80
|
|---|---|---|---|
|
|
79
81
|
| `action` | `SimpleContract<TInput, TData>` | | A SimpleAction do Opus — label, messages e confirm vêm do contrato. |
|
|
80
82
|
| `input` | `TInput` | | O que a action recebe — geralmente { id }. |
|
|
81
|
-
| `label` | `string` | `action.label` | Sobrepõe o texto do botão. |
|
|
82
|
-
| `context / variant / size` | `do Button` | gatilho: `primary`/`solid`; `danger` quando a action é `destructive`; no modo ícone, `ghost` e `neutral` (ou `danger` se destrutiva) / `icon-xs` | Visual do gatilho; `context` explícito vence. O botão de confirmar é sempre `solid`: `danger` quando a action é `destructive`, senão a prop `context` do gatilho (ou `primary`). |
|
|
83
|
-
| `confirm` | `{ title,
|
|
83
|
+
| `label` | `string` | `action.label` quando é texto; senão `action.name` | Sobrepõe o texto do botão. |
|
|
84
|
+
| `context / variant / size` | `do Button` | gatilho: `primary`/`solid`; `danger` quando a action é `destructive`; no modo ícone, `ghost` e `neutral` (ou `danger` se destrutiva) / `default`, ou `icon-xs` com `icon` | Visual do gatilho; `context` explícito vence. O botão de confirmar é sempre `solid`: `danger` quando a action é `destructive`, senão a prop `context` do gatilho (ou `primary`). |
|
|
85
|
+
| `confirm` | `{ title, body?, actionLabel?, cancelLabel? }` | | Confirmação via prop — sobrepõe o ConfirmSpec do contrato. |
|
|
86
|
+
| `disabled` | `boolean` | `false` | Desabilita o botão independentemente da execução em andamento. |
|
|
84
87
|
| `onSuccess` | `(data: TData) => void` | | Pós-sucesso (cache já invalidado pelo action.invalidates). |
|
|
88
|
+
| `onLoadingChange` | `(loading: boolean) => void` | | Informa início e fim da execução a quem coordena bloqueio entre várias actions. |
|
|
85
89
|
| `icon` | `React.ReactNode` | | Torna o botão icon-only: rótulo no tooltip e no `aria-label`, clique que não vaza para o item. |
|
|
86
90
|
| `itemLabel` | `string` | | Nome do alvo na pergunta (sai entre aspas, em destaque, antes da mensagem do contrato). |
|
|
87
91
|
| `className` | `string` | | Classes do botão. O tamanho vem de `size` (escala única); com `icon`, o default é `icon-xs`. |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## Carregar um recurso
|
|
2
2
|
|
|
3
|
-
Use `ActionView` para carregar um recurso por uma `ViewAction` e manter carregamento
|
|
3
|
+
Use `ActionView` para carregar um recurso por uma `ViewAction` e manter carregamento e erro no
|
|
4
4
|
mesmo fluxo. No exemplo, alterne entre os workspaces para observar o carregamento e a recuperação de
|
|
5
5
|
erro. O consumidor compõe somente o conteúdo disponível.
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ render(
|
|
|
12
12
|
<div className="w-full space-y-3">
|
|
13
13
|
<div className="flex flex-wrap gap-2">
|
|
14
14
|
<Button variant="outline" size="sm" onClick={() => setId('empresa-x')}>Empresa X</Button>
|
|
15
|
-
<Button variant="outline" size="sm" onClick={() => setId('
|
|
15
|
+
<Button variant="outline" size="sm" onClick={() => setId('empresa-y')}>Empresa Y</Button>
|
|
16
16
|
<Button variant="outline" size="sm" onClick={() => setId('sessao-fantasma')}>Inexistente (erro)</Button>
|
|
17
17
|
</div>
|
|
18
18
|
<div className="rounded-lg border border-border p-4">
|
|
@@ -21,7 +21,7 @@ render(
|
|
|
21
21
|
<div className="space-y-1.5">
|
|
22
22
|
<div className="flex items-center gap-2">
|
|
23
23
|
<h3 className="text-sm font-semibold">{ws.name}</h3>
|
|
24
|
-
<
|
|
24
|
+
<DictionaryValue dict={docWorkspaceStatus} value={ws.status} />
|
|
25
25
|
</div>
|
|
26
26
|
<p className="text-sm text-muted-foreground">
|
|
27
27
|
Cliente {ws.client} · {ws.agents} agentes vinculados.
|
|
@@ -35,6 +35,10 @@ render(
|
|
|
35
35
|
)
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
O handler da view deve devolver um valor. Uma consulta que resolve `undefined` é tratada como erro
|
|
39
|
+
pelo react-query, então `ActionView` apresenta o estado de erro, não um vazio. Represente um recurso
|
|
40
|
+
ausente com o erro `not_found`, cuja frase aparece para a pessoa.
|
|
41
|
+
|
|
38
42
|
## useViewAction
|
|
39
43
|
|
|
40
44
|
`ActionView` é uma composição sobre `useViewAction(action, input)`, que devolve `data`, `error`,
|
|
@@ -45,11 +49,11 @@ mais de uma região da tela ou quando o carregamento precisa ser orquestrado por
|
|
|
45
49
|
|
|
46
50
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
47
51
|
|---|---|---|---|
|
|
48
|
-
| `action` | `
|
|
52
|
+
| `action` | `ViewContract<TInput, TData>` | | A ViewAction do Opus (kind view, 1 recurso). |
|
|
49
53
|
| `input` | `TInput` | | Geralmente { id } — mudou, recarrega. |
|
|
50
|
-
| `children` | `(data: TData, refetch) => ReactNode` | | Conteúdo apresentado quando os dados estão disponíveis. `refetch` permite recarregar por código. |
|
|
51
|
-
| `render` | `(data: TData, refetch) => ReactNode` | | Alias de compatibilidade de `children`; `children` tem precedência. |
|
|
54
|
+
| `children` | `(data: TData, refetch) => ReactNode` | | Obrigatório, salvo quando `render` é informado. Conteúdo apresentado quando os dados estão disponíveis. `refetch` permite recarregar por código. |
|
|
55
|
+
| `render` | `(data: TData, refetch) => ReactNode` | | Alias de compatibilidade de `children`; `children` tem precedência. Sem nenhum dos dois, o componente lança erro. |
|
|
52
56
|
| `loading` | `ReactNode \| boolean` | `3 skeletons` | Sobrescreve o carregamento: um nó próprio, `true` para o padrão ou `false` para não renderizar nada enquanto carrega. |
|
|
53
|
-
| `empty` | `ReactNode` | `emptyMessage` ou nada |
|
|
54
|
-
| `emptyMessage` | `string` | | Atalho do vazio
|
|
57
|
+
| `empty` | `ReactNode` | `emptyMessage` ou nada | Estado reservado a `data` indefinido. Como o react-query converte esse resultado em erro, ele não aparece na prática; devolva um valor ou o erro `not_found`. |
|
|
58
|
+
| `emptyMessage` | `string` | | Atalho do vazio, sujeito à mesma limitação de `empty`: a frase na mesma superfície de `DataState` (`Empty` com moldura sólida). |
|
|
55
59
|
| `error` | `(err, retry) => ReactNode` | `"Não foi possível carregar" + Tentar de novo` | Sobrescreve o estado de erro padrão. Sem ele, a frase do servidor só aparece quando é legível pela pessoa (`conflict`, `validation`, `not_found`, `authorization`, `authentication`); código técnico nunca vira título, e o botão de tentar de novo some quando o problema é de permissão ou sessão. |
|