@softize/opus 13.0.0 → 14.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 +81 -0
- package/PROMOTED.md +46 -0
- package/README.md +28 -19
- package/bin/cli.mjs +87 -216
- package/bin/lib/cli-shared.mjs +131 -0
- package/bin/lib/copy.mjs +276 -6
- package/bin/lib/db.mjs +16 -74
- package/bin/lib/gen-openapi.mjs +3 -3
- package/bin/lib/gen-runner.mjs +1 -1
- package/bin/lib/gen.mjs +14 -69
- package/bin/lib/mcp.mjs +3 -1
- package/bin/lib/seed.mjs +5 -62
- package/docs/code-style.md +4 -1
- package/docs/ownership-vs-shadcn-lock.md +2 -3
- package/docs/protocol.md +7 -7
- package/docs/releasing.md +8 -2
- package/package.json +7 -3
- package/registry/instructions/opus.md +3 -3
- package/registry/templates/app/package.json +1 -1
- package/registry/templates/app/src/App.tsx +11 -6
- package/registry/templates/app/src/main.tsx +4 -4
- package/src/audit/drivers/console.ts +1 -0
- package/src/auth/drivers/better-auth.ts +1 -0
- package/src/auth/drivers/jwt.ts +1 -0
- package/src/cache/drivers/memory.ts +1 -0
- package/src/client/drivers/fetch.ts +2 -1
- package/src/core/actions.ts +6 -1
- package/src/core/audit.ts +9 -3
- package/src/core/contracts.ts +7 -0
- package/src/core/domain.ts +1 -1
- package/src/core/errors.ts +18 -15
- package/src/core/index.ts +4 -2
- package/src/core/package-version.ts +26 -0
- package/src/core/reactions.ts +1 -1
- package/src/core/runtime.ts +33 -23
- package/src/core/schedules.ts +1 -1
- package/src/core/types.ts +5 -6
- package/src/dsl/eval.ts +2 -2
- package/src/dsl/kysely.ts +2 -2
- package/src/dsl/loads.ts +1 -1
- package/src/dsl/parser.ts +5 -5
- package/src/events/drivers/mitt.ts +1 -0
- package/src/mcp/index.ts +2 -1
- package/src/observability/drivers/opentelemetry.ts +1 -0
- package/src/queue/drivers/bullmq.ts +3 -3
- package/src/scheduler/drivers/node-cron.ts +3 -2
- package/src/scheduler/every.ts +7 -7
- package/src/schema/openapi.ts +3 -3
- package/src/seed/index.ts +29 -0
- package/src/server/drivers/fastify.ts +5 -2
- package/src/server/drivers/node.ts +9 -6
- package/src/server/index.ts +3 -1
- package/src/storage/drivers/fs.ts +1 -0
- package/src/testing/index.ts +3 -3
- package/src/ui/components/patterns/action-list-dialog.tsx +10 -3
- package/src/ui/components/patterns/confirm.tsx +2 -31
- package/src/ui/components/patterns/content-header.tsx +44 -137
- package/src/ui/components/patterns/data-state.tsx +42 -68
- package/src/ui/components/patterns/dock.tsx +20 -3
- package/src/ui/components/patterns/form.tsx +26 -22
- package/src/ui/components/patterns/list.tsx +18 -12
- package/src/ui/components/patterns/page-state.tsx +39 -51
- package/src/ui/components/patterns/page.tsx +37 -54
- package/src/ui/components/patterns/sidebar.tsx +17 -6
- package/src/ui/components/patterns/state-surface.tsx +148 -0
- package/src/ui/components/patterns/surface-header.tsx +119 -0
- package/src/ui/components/patterns/trigger.tsx +21 -25
- package/src/ui/components/patterns/view.tsx +29 -22
- package/src/ui/components/primitives/alert.tsx +1 -27
- package/src/ui/components/primitives/ask.tsx +3 -3
- package/src/ui/components/primitives/avatar.tsx +15 -5
- package/src/ui/components/primitives/badge.tsx +5 -41
- package/src/ui/components/primitives/breadcrumb.tsx +2 -2
- package/src/ui/components/primitives/button.tsx +39 -30
- package/src/ui/components/primitives/calendar.tsx +28 -2
- package/src/ui/components/primitives/carousel.tsx +3 -3
- package/src/ui/components/primitives/chat.tsx +1 -1
- package/src/ui/components/primitives/checkbox.tsx +1 -1
- package/src/ui/components/primitives/command.tsx +2 -2
- package/src/ui/components/primitives/control.ts +72 -0
- package/src/ui/components/primitives/copyable.tsx +1 -1
- package/src/ui/components/primitives/dialog.tsx +12 -7
- package/src/ui/components/primitives/dot.tsx +1 -25
- package/src/ui/components/primitives/drawer.tsx +10 -3
- package/src/ui/components/primitives/field.tsx +3 -3
- package/src/ui/components/primitives/icon-picker.tsx +3 -1
- package/src/ui/components/primitives/input-group.tsx +12 -9
- package/src/ui/components/primitives/input-otp.tsx +1 -1
- package/src/ui/components/primitives/input.tsx +2 -2
- package/src/ui/components/primitives/item.tsx +3 -1
- package/src/ui/components/primitives/menu.tsx +1 -7
- package/src/ui/components/primitives/pagination.tsx +16 -8
- package/src/ui/components/primitives/progress.tsx +32 -3
- package/src/ui/components/primitives/radio-group.tsx +1 -1
- package/src/ui/components/primitives/resizable.tsx +3 -1
- package/src/ui/components/primitives/select.tsx +6 -6
- package/src/ui/components/primitives/slider.tsx +5 -1
- package/src/ui/components/primitives/sonner.tsx +3 -0
- package/src/ui/components/primitives/spinner.tsx +13 -16
- package/src/ui/components/primitives/switch.tsx +5 -1
- package/src/ui/components/primitives/tabs.tsx +6 -3
- package/src/ui/components/primitives/textarea.tsx +1 -1
- package/src/ui/components/primitives/toggle.tsx +9 -4
- package/src/ui/components/primitives/tooltip.tsx +1 -0
- package/src/ui/docs/changelog.tsx +1 -1
- package/src/ui/docs/content/action-form.md +36 -3
- package/src/ui/docs/content/action-list-dialog.md +2 -2
- package/src/ui/docs/content/action-list.md +13 -2
- package/src/ui/docs/content/action-trigger.md +12 -5
- package/src/ui/docs/content/action-view.md +11 -3
- package/src/ui/docs/content/ask.md +11 -0
- package/src/ui/docs/content/avatar.md +7 -3
- package/src/ui/docs/content/button.md +30 -14
- package/src/ui/docs/content/calendar.md +13 -0
- package/src/ui/docs/content/card.md +26 -0
- package/src/ui/docs/content/chat.md +20 -0
- package/src/ui/docs/content/cli.md +71 -19
- package/src/ui/docs/content/communication.md +36 -0
- package/src/ui/docs/content/composer.md +15 -0
- package/src/ui/docs/content/content.md +17 -1
- package/src/ui/docs/content/copyable.md +8 -0
- package/src/ui/docs/content/data-state.md +17 -13
- package/src/ui/docs/content/detail.md +19 -1
- package/src/ui/docs/content/dialog.md +1 -4
- package/src/ui/docs/content/dictionary-value.md +9 -2
- package/src/ui/docs/content/dock.md +8 -0
- package/src/ui/docs/content/dot.md +8 -0
- package/src/ui/docs/content/empty.md +2 -2
- package/src/ui/docs/content/getting-started.md +2 -2
- package/src/ui/docs/content/icon-picker.md +11 -0
- package/src/ui/docs/content/input.md +1 -1
- package/src/ui/docs/content/item.md +1 -1
- package/src/ui/docs/content/label.md +7 -0
- package/src/ui/docs/content/menu.md +6 -0
- package/src/ui/docs/content/metric-card.md +13 -0
- package/src/ui/docs/content/page.md +20 -4
- package/src/ui/docs/content/pagination.md +11 -9
- package/src/ui/docs/content/popover.md +6 -0
- package/src/ui/docs/content/progress.md +8 -11
- package/src/ui/docs/content/select.md +5 -5
- package/src/ui/docs/content/semantic-context.md +5 -4
- package/src/ui/docs/content/sidebar.md +13 -47
- package/src/ui/docs/content/skeleton.md +6 -0
- package/src/ui/docs/content/spinner.md +9 -6
- package/src/ui/docs/content/split.md +21 -0
- package/src/ui/docs/content/switch.md +1 -1
- package/src/ui/docs/content/tabs.md +1 -1
- package/src/ui/docs/content/textarea.md +7 -0
- package/src/ui/docs/content/toggle.md +1 -1
- package/src/ui/docs/content/tokens.md +4 -4
- package/src/ui/docs/content/truncate.md +8 -0
- package/src/ui/docs/content/ui.md +14 -0
- package/src/ui/docs/doc-client.tsx +5 -5
- package/src/ui/docs/doc.tsx +26 -14
- package/src/ui/docs/registry.tsx +5 -5
- package/src/ui/docs/standalone.tsx +2 -2
- package/src/ui/drivers/react.tsx +12 -12
- package/src/ui/lib/action-errors.ts +45 -0
- package/src/ui/lib/zod-pt-br.ts +31 -4
- package/src/ui/meta.ts +8 -8
- package/src/ui/react.tsx +10 -16
- package/src/ui/theme.css +10 -8
- package/src/vite/design.ts +6 -18
- package/src/ui/components/patterns/shell-nav.tsx +0 -147
|
@@ -67,18 +67,12 @@ function MenuItem({
|
|
|
67
67
|
className,
|
|
68
68
|
inset,
|
|
69
69
|
context,
|
|
70
|
-
variant,
|
|
71
70
|
...props
|
|
72
71
|
}: React.ComponentProps<typeof MenuPrimitive.Item> & {
|
|
73
72
|
inset?: boolean
|
|
74
73
|
context?: Extract<UiContext, 'neutral' | 'danger'>
|
|
75
|
-
/** @deprecated Use `context="danger"`. */
|
|
76
|
-
variant?: "default" | "destructive"
|
|
77
74
|
}) {
|
|
78
|
-
|
|
79
|
-
throw new Error('MenuItem não permite combinar context com uma variante semântica legada.')
|
|
80
|
-
}
|
|
81
|
-
const resolvedContext = context ?? (variant === 'destructive' ? 'danger' : 'neutral')
|
|
75
|
+
const resolvedContext = context ?? 'neutral'
|
|
82
76
|
return (
|
|
83
77
|
<MenuPrimitive.Item
|
|
84
78
|
data-slot="menu-item"
|
|
@@ -3,6 +3,7 @@ import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from 'lucide-re
|
|
|
3
3
|
|
|
4
4
|
import { cn } from '../../lib/cn.ts'
|
|
5
5
|
import { buttonVariants, type ButtonProps } from './button.tsx'
|
|
6
|
+
import { controlSquare, type ControlIconSize } from './control.ts'
|
|
6
7
|
|
|
7
8
|
function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
|
|
8
9
|
return (
|
|
@@ -33,8 +34,8 @@ function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
|
|
|
33
34
|
export interface PaginationLinkProps extends Omit<React.ComponentProps<'a'>, 'children'> {
|
|
34
35
|
/** Página atual: vira `outline` e ganha `aria-current="page"`. */
|
|
35
36
|
isActive?: boolean
|
|
36
|
-
/**
|
|
37
|
-
* com os dígitos) ou
|
|
37
|
+
/** A escala de control.ts, via Button. `default` (números: altura fixa, largura mínima
|
|
38
|
+
* quadrada que cresce com os dígitos) ou um `icon*` (setas quadradas, sem padding). */
|
|
38
39
|
size?: ButtonProps['size']
|
|
39
40
|
/** Número da página: dá o nome acessível (“Página N”) e o conteúdo, quando `children` falta. */
|
|
40
41
|
page?: number
|
|
@@ -83,10 +84,11 @@ function PaginationLink({ className, isActive, page, disabled, href, size = 'def
|
|
|
83
84
|
export interface PaginationControlProps extends Omit<PaginationLinkProps, 'page' | 'isActive'> {
|
|
84
85
|
/** Nome acessível e texto visível. Default: “Página anterior” / “Próxima página”. */
|
|
85
86
|
label?: string
|
|
86
|
-
/** Só a seta, quadrada (
|
|
87
|
+
/** Só a seta, quadrada (`icon`, 2.25rem; `size="icon-sm"` para o rodapé denso); o `label`
|
|
88
|
+
* continua como nome acessível. */
|
|
87
89
|
iconOnly?: boolean
|
|
88
|
-
/** Classe do ícone
|
|
89
|
-
* fallback `[&_svg:not([class*=
|
|
90
|
+
/** Classe do ícone, quando o glifo da escala não servir. Vai no próprio svg, porque o
|
|
91
|
+
* fallback `[&_svg:not([class*=size-])]` do Button vence qualquer seletor de fora. */
|
|
90
92
|
iconClassName?: string
|
|
91
93
|
}
|
|
92
94
|
|
|
@@ -132,16 +134,22 @@ function PaginationNext({
|
|
|
132
134
|
)
|
|
133
135
|
}
|
|
134
136
|
|
|
137
|
+
export interface PaginationEllipsisProps extends React.ComponentProps<'span'> {
|
|
138
|
+
/** O quadrado da escala de control.ts — o mesmo das setas ao lado. */
|
|
139
|
+
size?: ControlIconSize
|
|
140
|
+
}
|
|
141
|
+
|
|
135
142
|
/** Decorativa: `aria-hidden`; os números ao redor já dizem que há páginas no vão. */
|
|
136
|
-
function PaginationEllipsis({ className, ...props }:
|
|
143
|
+
function PaginationEllipsis({ className, size = 'icon', ...props }: PaginationEllipsisProps) {
|
|
137
144
|
return (
|
|
138
145
|
<span
|
|
139
146
|
aria-hidden
|
|
140
147
|
data-slot="pagination-ellipsis"
|
|
141
|
-
|
|
148
|
+
data-size={size}
|
|
149
|
+
className={cn('flex items-center justify-center', controlSquare[size], className)}
|
|
142
150
|
{...props}
|
|
143
151
|
>
|
|
144
|
-
<MoreHorizontalIcon className=
|
|
152
|
+
<MoreHorizontalIcon className={size === 'icon-xs' || size === 'icon-sm' ? 'size-3.5' : 'size-4'} />
|
|
145
153
|
</span>
|
|
146
154
|
)
|
|
147
155
|
}
|
|
@@ -1,25 +1,54 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
2
|
import { Progress as ProgressPrimitive } from "radix-ui"
|
|
3
3
|
|
|
4
|
+
import type { UiContext } from '../../../core/ui-context.ts'
|
|
4
5
|
import { cn } from '../../lib/cn.ts'
|
|
5
6
|
|
|
7
|
+
export type ProgressContext = UiContext
|
|
8
|
+
|
|
9
|
+
const trackClasses: Record<ProgressContext, string> = {
|
|
10
|
+
neutral: 'bg-context-neutral-subtle',
|
|
11
|
+
primary: 'bg-context-primary/20',
|
|
12
|
+
info: 'bg-context-info/20',
|
|
13
|
+
success: 'bg-context-success/20',
|
|
14
|
+
warning: 'bg-context-warning/20',
|
|
15
|
+
danger: 'bg-context-danger/20',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const indicatorClasses: Record<ProgressContext, string> = {
|
|
19
|
+
neutral: 'bg-context-neutral-emphasis',
|
|
20
|
+
primary: 'bg-context-primary',
|
|
21
|
+
info: 'bg-context-info',
|
|
22
|
+
success: 'bg-context-success',
|
|
23
|
+
warning: 'bg-context-warning',
|
|
24
|
+
danger: 'bg-context-danger',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ProgressProps extends React.ComponentProps<typeof ProgressPrimitive.Root> {
|
|
28
|
+
/** O que o andamento comunica: `success` para meta atingida, `warning`/`danger` para atenção. */
|
|
29
|
+
context?: ProgressContext
|
|
30
|
+
}
|
|
31
|
+
|
|
6
32
|
function Progress({
|
|
7
33
|
className,
|
|
8
34
|
value,
|
|
35
|
+
context = 'primary',
|
|
9
36
|
...props
|
|
10
|
-
}:
|
|
37
|
+
}: ProgressProps) {
|
|
11
38
|
return (
|
|
12
39
|
<ProgressPrimitive.Root
|
|
13
40
|
data-slot="progress"
|
|
41
|
+
data-context={context}
|
|
14
42
|
className={cn(
|
|
15
|
-
"relative h-2 w-full overflow-hidden rounded-full
|
|
43
|
+
"relative h-2 w-full overflow-hidden rounded-full",
|
|
44
|
+
trackClasses[context],
|
|
16
45
|
className
|
|
17
46
|
)}
|
|
18
47
|
{...props}
|
|
19
48
|
>
|
|
20
49
|
<ProgressPrimitive.Indicator
|
|
21
50
|
data-slot="progress-indicator"
|
|
22
|
-
className="h-full w-full flex-1
|
|
51
|
+
className={cn("h-full w-full flex-1 transition-all", indicatorClasses[context])}
|
|
23
52
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
|
24
53
|
/>
|
|
25
54
|
</ProgressPrimitive.Root>
|
|
@@ -25,7 +25,7 @@ function RadioGroupItem({
|
|
|
25
25
|
<RadioGroupPrimitive.Item
|
|
26
26
|
data-slot="radio-group-item"
|
|
27
27
|
className={cn(
|
|
28
|
-
"aspect-square size-4 shrink-0 rounded-full border border-input text-primary transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-
|
|
28
|
+
"aspect-square size-4 shrink-0 rounded-full border border-input text-primary transition-[color,box-shadow] outline-none 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",
|
|
29
29
|
className
|
|
30
30
|
)}
|
|
31
31
|
{...props}
|
|
@@ -2,6 +2,7 @@ import { GripVerticalIcon } from "lucide-react"
|
|
|
2
2
|
import * as ResizablePrimitive from "react-resizable-panels"
|
|
3
3
|
|
|
4
4
|
import { cn } from '../../lib/cn.ts'
|
|
5
|
+
import { focusRing } from './control.ts'
|
|
5
6
|
|
|
6
7
|
function ResizablePanelGroup({
|
|
7
8
|
className,
|
|
@@ -34,7 +35,8 @@ function ResizableHandle({
|
|
|
34
35
|
<ResizablePrimitive.Separator
|
|
35
36
|
data-slot="resizable-handle"
|
|
36
37
|
className={cn(
|
|
37
|
-
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:
|
|
38
|
+
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90",
|
|
39
|
+
focusRing,
|
|
38
40
|
className
|
|
39
41
|
)}
|
|
40
42
|
{...props}
|
|
@@ -25,7 +25,7 @@ import { Check, ChevronDownIcon, SearchIcon, X } from 'lucide-react'
|
|
|
25
25
|
import { Command as CommandPrimitive } from 'cmdk'
|
|
26
26
|
|
|
27
27
|
import { cn } from '../../lib/cn.ts'
|
|
28
|
-
import type
|
|
28
|
+
import { focusRing, focusRingWithin, type ControlShape, type ControlSize } from './control.ts'
|
|
29
29
|
import { CommandEmpty, CommandGroup, CommandItem, CommandList } from './command.tsx'
|
|
30
30
|
import { Popover, PopoverAnchor, PopoverContent } from './popover.tsx'
|
|
31
31
|
|
|
@@ -60,7 +60,7 @@ interface SelectBaseProps {
|
|
|
60
60
|
'aria-invalid'?: boolean
|
|
61
61
|
'aria-describedby'?: string
|
|
62
62
|
/** Altura: `default` (h-9, a do Input/Button) ou `sm` (h-8) pra toolbar densa. */
|
|
63
|
-
size?: 'sm' | 'default'
|
|
63
|
+
size?: Extract<ControlSize, 'sm' | 'default'>
|
|
64
64
|
/** Geometria do controle; `pill` preserva a variante visual escolhida. */
|
|
65
65
|
shape?: ControlShape
|
|
66
66
|
'aria-label'?: string
|
|
@@ -162,8 +162,8 @@ function NativeSelect({
|
|
|
162
162
|
onChange={(e) => onChange(e.target.value)}
|
|
163
163
|
className={cn(
|
|
164
164
|
"h-9 w-full min-w-0 appearance-none rounded-md border border-input bg-transparent 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
|
-
|
|
166
|
-
'aria-invalid:border-
|
|
165
|
+
focusRing,
|
|
166
|
+
'aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 dark:aria-invalid:ring-context-danger/40',
|
|
167
167
|
icon ? 'pl-9' : '',
|
|
168
168
|
)}
|
|
169
169
|
>
|
|
@@ -359,8 +359,8 @@ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.Rea
|
|
|
359
359
|
className={cn(
|
|
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
|
-
? 'min-h-9 px-3 py-1 text-muted-foreground hover:bg-muted hover:text-foreground
|
|
363
|
-
:
|
|
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-transparent 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,6 +2,7 @@ import * as React from "react"
|
|
|
2
2
|
import { Slider as SliderPrimitive } from "radix-ui"
|
|
3
3
|
|
|
4
4
|
import { cn } from '../../lib/cn.ts'
|
|
5
|
+
import { focusRing } from './control.ts'
|
|
5
6
|
|
|
6
7
|
function Slider({
|
|
7
8
|
className,
|
|
@@ -51,7 +52,10 @@ function Slider({
|
|
|
51
52
|
<SliderPrimitive.Thumb
|
|
52
53
|
data-slot="slider-thumb"
|
|
53
54
|
key={index}
|
|
54
|
-
className=
|
|
55
|
+
className={cn(
|
|
56
|
+
"block size-4 shrink-0 rounded-full border border-primary bg-background shadow-sm transition-[color,box-shadow] hover:ring-[0.1875rem] hover:ring-ring/50 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50",
|
|
57
|
+
focusRing,
|
|
58
|
+
)}
|
|
55
59
|
/>
|
|
56
60
|
))}
|
|
57
61
|
</SliderPrimitive.Root>
|
|
@@ -170,6 +170,9 @@ export function Toaster({
|
|
|
170
170
|
<Sonner
|
|
171
171
|
data-slot="toaster"
|
|
172
172
|
theme="system"
|
|
173
|
+
// O sonner 1.x só expõe o rótulo da região; o do botão de fechar ("Close toast") vem
|
|
174
|
+
// com o 2.x (`closeButtonAriaLabel`).
|
|
175
|
+
containerAriaLabel="Notificações"
|
|
173
176
|
className={cn('toaster group', className)}
|
|
174
177
|
icons={{
|
|
175
178
|
success: (
|
|
@@ -1,28 +1,25 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
2
|
import { Loader2Icon } from 'lucide-react'
|
|
3
|
-
import { cva, type VariantProps } from 'class-variance-authority'
|
|
4
3
|
import { cn } from '../../lib/cn.ts'
|
|
4
|
+
import { controlGlyph, type ControlSize } from './control.ts'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
size: { sm: 'size-3', default: 'size-4', lg: 'size-6' },
|
|
11
|
-
},
|
|
12
|
-
defaultVariants: { size: 'default' },
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
export interface SpinnerProps extends React.ComponentProps<'svg'>, VariantProps<typeof spinnerVariants> {}
|
|
6
|
+
export interface SpinnerProps extends React.ComponentProps<'svg'> {
|
|
7
|
+
/** O glifo do controle daquele tamanho na escala de control.ts (`sm` cabe num Button sm). */
|
|
8
|
+
size?: ControlSize | null
|
|
9
|
+
}
|
|
16
10
|
|
|
17
|
-
/**
|
|
18
|
-
*
|
|
11
|
+
/**
|
|
12
|
+
* Indicador de carregamento (Loader2Icon girando) — substitui o `<Loader2Icon
|
|
13
|
+
* className="animate-spin"/>` inline repetido. É decorativo: quem nomeia a espera é o contêiner
|
|
14
|
+
* (`role="status"` em DataState, PageState ou no seu) ou o texto ao lado; assim a superfície
|
|
15
|
+
* carrega um status só, sem role duplicado. Pra placeholder de conteúdo, use Skeleton.
|
|
16
|
+
*/
|
|
19
17
|
export function Spinner({ className, size, ...props }: SpinnerProps): React.ReactElement {
|
|
20
18
|
return (
|
|
21
19
|
<Loader2Icon
|
|
22
20
|
data-slot="spinner"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
className={cn(spinnerVariants({ size }), className)}
|
|
21
|
+
aria-hidden="true"
|
|
22
|
+
className={cn('animate-spin', controlGlyph[size ?? 'default'], className)}
|
|
26
23
|
{...props}
|
|
27
24
|
/>
|
|
28
25
|
)
|
|
@@ -2,13 +2,16 @@ import * as React from "react"
|
|
|
2
2
|
import { Switch as SwitchPrimitive } from "radix-ui"
|
|
3
3
|
|
|
4
4
|
import { cn } from '../../lib/cn.ts'
|
|
5
|
+
import type { ControlSize } from './control.ts'
|
|
5
6
|
|
|
7
|
+
/** `size` usa os nomes da escala de control.ts; a chave é um trilho (não uma linha), então a
|
|
8
|
+
* medida é a do glifo daquele tamanho: `sm` acompanha o svg de um controle sm (0.875rem). */
|
|
6
9
|
function Switch({
|
|
7
10
|
className,
|
|
8
11
|
size = "default",
|
|
9
12
|
...props
|
|
10
13
|
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
|
|
11
|
-
size?: "sm" | "default"
|
|
14
|
+
size?: Extract<ControlSize, "sm" | "default">
|
|
12
15
|
}) {
|
|
13
16
|
return (
|
|
14
17
|
<SwitchPrimitive.Root
|
|
@@ -22,6 +25,7 @@ function Switch({
|
|
|
22
25
|
>
|
|
23
26
|
<SwitchPrimitive.Thumb
|
|
24
27
|
data-slot="switch-thumb"
|
|
28
|
+
// px: o percurso do thumb desconta a borda (um pixel de cada lado) no calc do translate.
|
|
25
29
|
className={cn(
|
|
26
30
|
"pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground"
|
|
27
31
|
)}
|
|
@@ -3,11 +3,13 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|
|
3
3
|
import { Tabs as TabsPrimitive } from "radix-ui"
|
|
4
4
|
|
|
5
5
|
import { cn } from '../../lib/cn.ts'
|
|
6
|
+
import { controlHeight, type ControlSize } from './control.ts'
|
|
6
7
|
|
|
7
8
|
// Divergência da casa (ejetado): o Tabs ganhou `size` — Button e Select têm `sm`, o Tabs
|
|
8
9
|
// não tinha, e uma fileira densa ficava com o segmento 0.25rem mais alto
|
|
9
|
-
// que os irmãos. O tamanho flui pra TabsList via contexto (a altura é dela)
|
|
10
|
-
|
|
10
|
+
// que os irmãos. O tamanho flui pra TabsList via contexto (a altura é dela) e a medida é a
|
|
11
|
+
// da escala de control.ts.
|
|
12
|
+
type TabsSize = Extract<ControlSize, "default" | "sm">
|
|
11
13
|
const TabsSizeContext = React.createContext<TabsSize>("default")
|
|
12
14
|
|
|
13
15
|
function Tabs({
|
|
@@ -64,7 +66,7 @@ function TabsList({
|
|
|
64
66
|
data-slot="tabs-list"
|
|
65
67
|
data-variant={variant}
|
|
66
68
|
data-size={size}
|
|
67
|
-
className={cn(tabsListVariants({ variant }), size
|
|
69
|
+
className={cn(tabsListVariants({ variant }), controlHeight[size], className)}
|
|
68
70
|
{...props}
|
|
69
71
|
/>
|
|
70
72
|
)
|
|
@@ -77,6 +79,7 @@ function TabsTrigger({
|
|
|
77
79
|
return (
|
|
78
80
|
<TabsPrimitive.Trigger
|
|
79
81
|
data-slot="tabs-trigger"
|
|
82
|
+
// px: altura e indicador compensam a hairline do contêiner (um pixel físico, não escala).
|
|
80
83
|
className={cn(
|
|
81
84
|
"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",
|
|
82
85
|
"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",
|
|
@@ -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-transparent px-3 py-2 text-base 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-
|
|
10
|
+
"flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base 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 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-context-danger/40",
|
|
11
11
|
className
|
|
12
12
|
)}
|
|
13
13
|
{...props}
|
|
@@ -3,9 +3,13 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|
|
3
3
|
import { Toggle as TogglePrimitive } from "radix-ui"
|
|
4
4
|
|
|
5
5
|
import { cn } from '../../lib/cn.ts'
|
|
6
|
+
import { controlHeight, focusRing } from './control.ts'
|
|
6
7
|
|
|
7
8
|
const toggleVariants = cva(
|
|
8
|
-
|
|
9
|
+
[
|
|
10
|
+
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground dark:aria-invalid:ring-context-danger/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
11
|
+
focusRing,
|
|
12
|
+
],
|
|
9
13
|
{
|
|
10
14
|
variants: {
|
|
11
15
|
variant: {
|
|
@@ -13,10 +17,11 @@ const toggleVariants = cva(
|
|
|
13
17
|
outline:
|
|
14
18
|
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
|
15
19
|
},
|
|
20
|
+
// Alturas da escala de control.ts (`sm` = o mesmo h-8 de Button e Select).
|
|
16
21
|
size: {
|
|
17
|
-
default:
|
|
18
|
-
sm:
|
|
19
|
-
lg:
|
|
22
|
+
default: `${controlHeight.default} min-w-9 px-2`,
|
|
23
|
+
sm: `${controlHeight.sm} min-w-8 px-1.5`,
|
|
24
|
+
lg: `${controlHeight.lg} min-w-10 px-2.5`,
|
|
20
25
|
},
|
|
21
26
|
shape: {
|
|
22
27
|
default: '',
|
|
@@ -46,6 +46,7 @@ function TooltipContent({
|
|
|
46
46
|
{...props}
|
|
47
47
|
>
|
|
48
48
|
{children}
|
|
49
|
+
{/* px: a seta sobe dois pixels para encaixar na borda do conteúdo, independente da fonte. */}
|
|
49
50
|
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[0.125rem] bg-foreground fill-foreground" />
|
|
50
51
|
</TooltipPrimitive.Content>
|
|
51
52
|
</TooltipPrimitive.Portal>
|
|
@@ -54,7 +54,7 @@ export function ChangelogView({ source }: { source: string }): React.ReactElemen
|
|
|
54
54
|
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
|
55
55
|
<h2 className="text-xl font-semibold tracking-tight">{v.version}</h2>
|
|
56
56
|
{v.date !== undefined && <span className="text-sm text-muted-foreground">{v.date}</span>}
|
|
57
|
-
{v.breaking !== undefined && <Badge context="danger" variant="solid">
|
|
57
|
+
{v.breaking !== undefined && <Badge context="danger" variant="solid">Incompatível</Badge>}
|
|
58
58
|
</div>
|
|
59
59
|
{v.body.length > 0 && (
|
|
60
60
|
<div className="mt-3">
|
|
@@ -55,12 +55,19 @@ Monte os providers de consulta e execução uma vez na raiz do aplicativo:
|
|
|
55
55
|
```tsx
|
|
56
56
|
/* main.tsx do app. */
|
|
57
57
|
<QueryClientProvider client={queryClient}>
|
|
58
|
-
<
|
|
58
|
+
<OpusProvider client={apiClient}>
|
|
59
59
|
<App />
|
|
60
|
-
</
|
|
60
|
+
</OpusProvider>
|
|
61
61
|
</QueryClientProvider>
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
## useFormAction
|
|
65
|
+
|
|
66
|
+
`ActionForm` é uma composição sobre `useFormAction(action, { defaultValues, onSuccess, onError })`,
|
|
67
|
+
que devolve o `form` do react-hook-form já com o resolver Zod em pt-BR, `submit`, `isLoading`,
|
|
68
|
+
`error`, `isSuccess` e `reset`. Use o hook direto quando o formulário precisar de um layout ou de um
|
|
69
|
+
ciclo de vida que o pattern não cobre; validação e execução continuam iguais.
|
|
70
|
+
|
|
64
71
|
## Propriedades de ActionForm
|
|
65
72
|
|
|
66
73
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
@@ -81,11 +88,37 @@ Monte os providers de consulta e execução uma vez na raiz do aplicativo:
|
|
|
81
88
|
| `options` | `SelectOption[]` | Opções por id de runtime — sobrepõe o fieldOptions do form e o z.enum. |
|
|
82
89
|
| `className` | `string` | Classes do invólucro (ex.: `col-span-2` em uma grid). |
|
|
83
90
|
|
|
91
|
+
## Ajuda na label
|
|
92
|
+
|
|
93
|
+
`FieldSpec.help` aparece como um ícone junto à label, com o texto num tooltip. Escreva ali o
|
|
94
|
+
critério, o efeito ou a limitação que a label não diz; ajuda que só repete a label deve ser omitida.
|
|
95
|
+
Não existe texto auxiliar sob o campo: quem precisa de uma frase permanente abaixo do controle usa
|
|
96
|
+
`FieldDescription` na composição própria.
|
|
97
|
+
|
|
98
|
+
`LabelHelp` é esse mesmo ícone, exportado para um formulário sem contrato mostrar a ajuda no mesmo
|
|
99
|
+
lugar:
|
|
100
|
+
|
|
101
|
+
```tsx preview col md
|
|
102
|
+
<Field>
|
|
103
|
+
<FieldLabel htmlFor="staff" className="items-center gap-1.5">
|
|
104
|
+
<span>Staff</span>
|
|
105
|
+
<LabelHelp help="Tem acesso ao back-office e a todos os workspaces." />
|
|
106
|
+
</FieldLabel>
|
|
107
|
+
<Input id="staff" placeholder="Empresa X" />
|
|
108
|
+
</Field>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Propriedades de LabelHelp
|
|
112
|
+
|
|
113
|
+
| Propriedade | Tipo | Descrição |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `help` | `string` | O texto do tooltip. Vazio ou ausente, o ícone não renderiza. |
|
|
116
|
+
|
|
84
117
|
## Origem das opções de seleção
|
|
85
118
|
|
|
86
119
|
As opções seguem esta precedência: `options` no campo, `fieldOptions` no formulário, `options` no
|
|
87
120
|
`FieldSpec`, metadata de `t.dict` no schema e, por último, as chaves de `z.enum`. Uma fonte mais
|
|
88
|
-
específica substitui as seguintes. Dicionários registrados no `
|
|
121
|
+
específica substitui as seguintes. Dicionários registrados no `OpusProvider` resolvem rótulos por
|
|
89
122
|
referência; itens estáticos permanecem como declarados no contrato.
|
|
90
123
|
|
|
91
124
|
## Widgets declarativos
|
|
@@ -23,7 +23,7 @@ render(
|
|
|
23
23
|
<Plus /> Workspace
|
|
24
24
|
</Button>
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
emptyMessage="Nenhum workspace."
|
|
27
27
|
>
|
|
28
28
|
{(workspaces) => (
|
|
29
29
|
<div className="space-y-2">
|
|
@@ -69,5 +69,5 @@ corpo do modal.
|
|
|
69
69
|
| `actions` | `ReactNode` | | Ação à direita da toolbar — em geral o botão de criar. |
|
|
70
70
|
| `empty` | `(items) => boolean` | `items.length === 0` | Sobrepõe o vazio derivado. |
|
|
71
71
|
| `loading` | `boolean` | | Carga extra agregada à do fetch (query irmã). |
|
|
72
|
-
| `
|
|
72
|
+
| `emptyMessage / errorMessage / retryLabel` | `string` | | Textos dos estados, repassados ao `ActionList` (e dele ao `DataState`). |
|
|
73
73
|
| `className` | `string` | `sm:max-w-3xl` | Largura do DialogContent. |
|
|
@@ -97,6 +97,7 @@ periods: [
|
|
|
97
97
|
{ value: 'thisMonth', label: 'Este mês' },
|
|
98
98
|
]
|
|
99
99
|
// Presets computáveis: today · yesterday · last7 · last30 · thisMonth · lastMonth.
|
|
100
|
+
// presetRange('last7') → { from: 'YYYY-MM-DD', to: 'YYYY-MM-DD' } — o mesmo cálculo do pattern.
|
|
100
101
|
```
|
|
101
102
|
|
|
102
103
|
## Paginação e carregamento
|
|
@@ -171,7 +172,7 @@ render(
|
|
|
171
172
|
- **`depends: ['outroFiltro']`:** desabilita o filtro até que suas dependências tenham valor e o
|
|
172
173
|
limpa quando uma delas muda.
|
|
173
174
|
- **`options: { kind: 'dictionary', ref: 'sessionStatus' }`:** resolve opções e rótulos por um
|
|
174
|
-
dicionário registrado no `
|
|
175
|
+
dicionário registrado no `OpusProvider`. `filterOptions` substitui essa fonte quando informado.
|
|
175
176
|
- **`options: { kind: 'lookup', source: 'x.lookup' }`:** consulta uma action com `{ q }` e os valores
|
|
176
177
|
das dependências. A resposta segue o formato `{ value, label }`.
|
|
177
178
|
|
|
@@ -244,6 +245,14 @@ render(
|
|
|
244
245
|
| `sort` | `{ fields, default }` — ordenação inicial; header ordenável escreve `sort: 'chave:dir'`. |
|
|
245
246
|
| `periods` | `{ value, label }[]` — o controle de período (presets + Personalizado com calendário); materializa em `from`/`to` no input. |
|
|
246
247
|
|
|
248
|
+
## useListAction
|
|
249
|
+
|
|
250
|
+
Quando o layout não cabe no `ActionList`, `useListAction(action, input)` entrega o mesmo fetch
|
|
251
|
+
declarativo (`items`, `total`, `error`, `isLoading`, `isFetching`, `refetch`) sob o
|
|
252
|
+
`QueryClientProvider`, e `presetRange(preset)` materializa os presets de período (`today`, `last7`,
|
|
253
|
+
`thisMonth`…) em `{ from, to }` no formato `YYYY-MM-DD` — o mesmo cálculo que o pattern faz antes de
|
|
254
|
+
chamar a action.
|
|
255
|
+
|
|
247
256
|
## Propriedades de ActionList
|
|
248
257
|
|
|
249
258
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
@@ -258,6 +267,8 @@ render(
|
|
|
258
267
|
| `rowId` | `(item) => string` | `item.id` | Identidade da linha para seleção. |
|
|
259
268
|
| `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. |
|
|
260
269
|
| `state / onStateChange` | `ActionListState / (next) => void` | interno | Estado da toolbar controlado — para quem embala sincronizar com a URL. |
|
|
261
|
-
| `emptyMessage` | `string` | `'Nenhum resultado.'` |
|
|
270
|
+
| `emptyMessage` | `string` | `'Nenhum resultado.'` | Frase do estado vazio (o `Empty` do `DataState`). |
|
|
271
|
+
| `errorMessage` | `string` | `'Não foi possível carregar.'` | Título do aviso de erro; o botão de tentar de novo refaz a consulta. |
|
|
272
|
+
| `retryLabel` | `string` | `'Tentar de novo'` | Rótulo do botão de recuperação. |
|
|
262
273
|
| `onRowClick` | `(item: TItem) => void` | | Clique na linha (ex.: navegar para o detalhe) — só na tabela. |
|
|
263
274
|
| `rowActions` | `(item: TItem) => ReactNode` | | Ações por linha (coluna final, à direita) — apresentação de quem chama, como `cells`; cliques ali não disparam o `onRowClick`. |
|
|
@@ -37,9 +37,9 @@ confirm: {
|
|
|
37
37
|
|
|
38
38
|
## Ação somente com ícone
|
|
39
39
|
|
|
40
|
-
Com `icon`, o botão exibe somente o ícone
|
|
41
|
-
|
|
42
|
-
mensagem de confirmação.
|
|
40
|
+
Com `icon`, o botão exibe somente o ícone no quadrado `icon-sm` da escala (1.75rem, a ação que mora
|
|
41
|
+
dentro de uma linha ou card) e usa `label`, ou `action.label`, no tooltip e no nome acessível. O clique
|
|
42
|
+
não aciona o item clicável ao redor. `itemLabel` identifica o registro na mensagem de confirmação.
|
|
43
43
|
|
|
44
44
|
```tsx
|
|
45
45
|
<ActionTrigger
|
|
@@ -64,6 +64,13 @@ negócio `conflict`, `validation` e `not_found`:
|
|
|
64
64
|
Erros inesperados mantêm a mensagem genérica do contrato, evitando expor detalhes de banco ou
|
|
65
65
|
infraestrutura. O servidor registra o detalhe técnico para diagnóstico.
|
|
66
66
|
|
|
67
|
+
## useTriggerAction
|
|
68
|
+
|
|
69
|
+
`ActionTrigger` é uma composição sobre `useTriggerAction(action, { onSuccess, onError })`, que
|
|
70
|
+
devolve `trigger(input)`, `isLoading`, `error` e `reset`, e invalida o cache declarado em
|
|
71
|
+
`action.invalidates`. Use o hook direto quando a execução parte de um gesto que não é um botão —
|
|
72
|
+
um atalho, um arrastar, um item de menu.
|
|
73
|
+
|
|
67
74
|
## Propriedades de ActionTrigger
|
|
68
75
|
|
|
69
76
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
@@ -71,9 +78,9 @@ infraestrutura. O servidor registra o detalhe técnico para diagnóstico.
|
|
|
71
78
|
| `action` | `SimpleContract<TInput, TData>` | | A SimpleAction do Opus — label, messages e confirm vêm do contrato. |
|
|
72
79
|
| `input` | `TInput` | | O que a action recebe — geralmente { id }. |
|
|
73
80
|
| `label` | `string` | `action.label` | Sobrepõe o texto do botão. |
|
|
74
|
-
| `variant / size` | `
|
|
81
|
+
| `context / variant / size` | `do Button` | gatilho: `primary`/`solid`; `danger` quando a action é `destructive`; no modo ícone, `ghost` e `neutral` (ou `danger` se destrutiva) / `default` | 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`). |
|
|
75
82
|
| `confirm` | `{ title, description?, actionLabel?, cancelLabel? }` | | Confirmação via prop — sobrepõe o ConfirmSpec do contrato. |
|
|
76
83
|
| `onSuccess` | `(data: TData) => void` | | Pós-sucesso (cache já invalidado pelo action.invalidates). |
|
|
77
84
|
| `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. |
|
|
78
85
|
| `itemLabel` | `string` | | Nome do alvo na pergunta (sai entre aspas, em destaque, antes da mensagem do contrato). |
|
|
79
|
-
| `className` | `string` | | Classes do botão (
|
|
86
|
+
| `className` | `string` | | Classes do botão. O tamanho vem de `size` (escala única); com `icon` é sempre `icon-sm`. |
|
|
@@ -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
|
-
<Badge
|
|
24
|
+
<Badge context={ws.status === 'active' ? 'success' : 'warning'}>{ws.status}</Badge>
|
|
25
25
|
</div>
|
|
26
26
|
<p className="text-sm text-muted-foreground">
|
|
27
27
|
Cliente {ws.client} · {ws.agents} agentes vinculados.
|
|
@@ -35,6 +35,12 @@ render(
|
|
|
35
35
|
)
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
## useViewAction
|
|
39
|
+
|
|
40
|
+
`ActionView` é uma composição sobre `useViewAction(action, input)`, que devolve `data`, `error`,
|
|
41
|
+
`isLoading` e `refetch` sob o `QueryClientProvider`. Use o hook direto quando o recurso alimenta
|
|
42
|
+
mais de uma região da tela ou quando o carregamento precisa ser orquestrado por outro componente.
|
|
43
|
+
|
|
38
44
|
## Propriedades de ActionView
|
|
39
45
|
|
|
40
46
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
@@ -43,5 +49,7 @@ render(
|
|
|
43
49
|
| `input` | `TInput` | | Geralmente { id } — mudou, recarrega. |
|
|
44
50
|
| `children` | `(data: TData, refetch) => ReactNode` | | Conteúdo apresentado quando os dados estão disponíveis. `refetch` permite recarregar por código. |
|
|
45
51
|
| `render` | `(data: TData, refetch) => ReactNode` | | Alias de compatibilidade de `children`; `children` tem precedência. |
|
|
46
|
-
| `loading
|
|
47
|
-
| `
|
|
52
|
+
| `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 | Sobrescreve o estado vazio (200 sem dado). |
|
|
54
|
+
| `emptyMessage` | `string` | | Atalho do vazio: a frase na mesma superfície de `DataState` (`Empty` com moldura sólida). |
|
|
55
|
+
| `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. |
|
|
@@ -29,3 +29,14 @@ render(
|
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
Cada pergunta exige pelo menos uma opção válida selecionada **ou** texto livre não vazio. `busy` mostra o progresso no botão e trava os controles; `disabled` apenas trava a interação. A ordem de `answers` acompanha a ordem de `questions`, e o `header` de cada resposta é derivado de `question.header` (ou do próprio enunciado quando o header estiver vazio).
|
|
32
|
+
|
|
33
|
+
## Propriedades de Ask
|
|
34
|
+
|
|
35
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| `questions` | `AskQuestion[]` | | Uma a quatro perguntas do contrato de elicitação; fora dessa faixa o envio é bloqueado. |
|
|
38
|
+
| `answers` | `AskAnswer[]` | | Respostas controladas, na mesma ordem de `questions`; entradas ausentes contam como vazias. |
|
|
39
|
+
| `onChange` | `(answers: AskAnswer[]) => void` | | Recebe a lista completa a cada alteração. |
|
|
40
|
+
| `onSubmit` | `(answers: AskAnswer[]) => void` | | Dispara somente quando toda pergunta tem uma opção ou texto livre. |
|
|
41
|
+
| `disabled` | `boolean` | `false` | Trava as opções e o envio. |
|
|
42
|
+
| `busy` | `boolean` | `false` | Envio em andamento: o botão mostra o spinner e desabilita. |
|