@softize/opus 13.0.0 → 13.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 +47 -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/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/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/templates/app/package.json +1 -1
- 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 +2 -2
- 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/confirm.tsx +2 -2
- package/src/ui/components/patterns/content-header.tsx +7 -1
- package/src/ui/components/patterns/data-state.tsx +1 -1
- package/src/ui/components/patterns/dock.tsx +20 -3
- package/src/ui/components/patterns/form.tsx +12 -8
- package/src/ui/components/patterns/list.tsx +4 -4
- package/src/ui/components/patterns/page.tsx +19 -1
- package/src/ui/components/patterns/shell-nav.tsx +10 -3
- package/src/ui/components/patterns/sidebar.tsx +17 -6
- package/src/ui/components/patterns/trigger.tsx +14 -16
- package/src/ui/components/patterns/view.tsx +26 -17
- package/src/ui/components/primitives/alert.tsx +11 -5
- package/src/ui/components/primitives/ask.tsx +3 -3
- package/src/ui/components/primitives/badge.tsx +11 -6
- package/src/ui/components/primitives/breadcrumb.tsx +2 -2
- package/src/ui/components/primitives/button.tsx +16 -3
- 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 +12 -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 +5 -0
- 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 +1 -1
- package/src/ui/components/primitives/input-otp.tsx +1 -1
- package/src/ui/components/primitives/input.tsx +2 -2
- 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 +5 -5
- package/src/ui/components/primitives/slider.tsx +5 -1
- package/src/ui/components/primitives/sonner.tsx +3 -0
- package/src/ui/components/primitives/switch.tsx +1 -0
- package/src/ui/components/primitives/tabs.tsx +1 -0
- package/src/ui/components/primitives/textarea.tsx +1 -1
- package/src/ui/components/primitives/toggle.tsx +1 -1
- 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 +10 -3
- package/src/ui/docs/content/action-list.md +10 -1
- package/src/ui/docs/content/action-trigger.md +8 -1
- package/src/ui/docs/content/action-view.md +9 -2
- package/src/ui/docs/content/ask.md +11 -0
- 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/composer.md +15 -0
- package/src/ui/docs/content/content.md +15 -0
- package/src/ui/docs/content/copyable.md +8 -0
- package/src/ui/docs/content/detail.md +19 -1
- 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/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 +8 -0
- 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 +2 -2
- package/src/ui/docs/content/sidebar.md +14 -8
- package/src/ui/docs/content/skeleton.md +6 -0
- package/src/ui/docs/content/split.md +21 -0
- package/src/ui/docs/content/textarea.md +7 -0
- 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 +17 -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 +3 -3
- package/src/ui/react.tsx +2 -0
- package/src/ui/theme.css +10 -8
- package/src/vite/design.ts +6 -18
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import { useDicts, useFormAction, type DictLike } from '../../drivers/react.tsx'
|
|
29
29
|
import { z, type ZodTypeAny } from 'zod'
|
|
30
30
|
import { cn } from '../../lib/cn.ts'
|
|
31
|
+
import { humanizeActionError } from '../../lib/action-errors.ts'
|
|
31
32
|
import { objectSchemaShape } from '../../lib/object-schema.ts'
|
|
32
33
|
import { toast } from '../primitives/sonner.tsx'
|
|
33
34
|
import { Button } from '../primitives/button.tsx'
|
|
@@ -111,8 +112,9 @@ function inferFieldKind(schema: ZodTypeAny): FieldKind {
|
|
|
111
112
|
interface FieldSpec {
|
|
112
113
|
label?: string
|
|
113
114
|
placeholder?: string
|
|
115
|
+
/** @deprecated Use `help` (ajuda junto à label) ou `description` do campo composto. */
|
|
114
116
|
hint?: string
|
|
115
|
-
/** Ajuda na label: ícone ⓘ + tooltip no hover
|
|
117
|
+
/** Ajuda na label: ícone ⓘ + tooltip no hover. */
|
|
116
118
|
help?: string
|
|
117
119
|
/** Override explícito do tipo de campo, quando o auto-detect do Zod não basta.
|
|
118
120
|
* Honra 'textarea' (string sem max), 'code' (textarea monoespaçada, ex.: SKILL.md),
|
|
@@ -126,7 +128,7 @@ interface FieldSpec {
|
|
|
126
128
|
* No modo COMPOSIÇÃO o condicional pode (e deve) ser JSX de quem diagrama. */
|
|
127
129
|
showWhen?: (input: Record<string, unknown>) => boolean
|
|
128
130
|
/** Origem declarada das opções (static/dictionary/lookup) — ver a precedência
|
|
129
|
-
* em `ActionFormField`. `dictionary` resolve pelos dicts do
|
|
131
|
+
* em `ActionFormField`. `dictionary` resolve pelos dicts do OpusProvider. */
|
|
130
132
|
options?: OptionsSpec
|
|
131
133
|
}
|
|
132
134
|
|
|
@@ -208,7 +210,7 @@ function RequiredMark({ required }: { required: boolean }): React.ReactElement |
|
|
|
208
210
|
if (!required) return null
|
|
209
211
|
return (
|
|
210
212
|
<>
|
|
211
|
-
<span aria-hidden className="ml-0.5 text-
|
|
213
|
+
<span aria-hidden className="ml-0.5 text-context-danger-emphasis">
|
|
212
214
|
*
|
|
213
215
|
</span>
|
|
214
216
|
<span className="sr-only"> (obrigatório)</span>
|
|
@@ -530,7 +532,7 @@ export function ActionFormField({ name, options: optionsProp, className }: Actio
|
|
|
530
532
|
<Input
|
|
531
533
|
id={`${name}-${i}-text`}
|
|
532
534
|
value={item.text}
|
|
533
|
-
placeholder={spec.placeholder ?? '
|
|
535
|
+
placeholder={spec.placeholder ?? 'Descreva o critério…'}
|
|
534
536
|
onChange={(e) => setItems(items.map((it, j) => (j === i ? { ...it, text: e.target.value } : it)))}
|
|
535
537
|
aria-invalid={errorMessage !== undefined}
|
|
536
538
|
aria-describedby={describedBy}
|
|
@@ -651,7 +653,7 @@ export function ActionForm<TInput extends Record<string, unknown>, TData>({
|
|
|
651
653
|
onSuccess?.(data)
|
|
652
654
|
},
|
|
653
655
|
onError: (err) => {
|
|
654
|
-
toast.error(msgText(action.messages?.error,
|
|
656
|
+
toast.error(humanizeActionError(err, msgText(action.messages?.error, 'Não foi possível salvar.')))
|
|
655
657
|
},
|
|
656
658
|
})
|
|
657
659
|
|
|
@@ -669,13 +671,15 @@ export function ActionForm<TInput extends Record<string, unknown>, TData>({
|
|
|
669
671
|
))
|
|
670
672
|
|
|
671
673
|
// Banner de erro do servidor (chrome do próprio form, nos dois modos). O FieldGroup
|
|
672
|
-
// mantém o mesmo ritmo entre campos, blocos compostos e este estado.
|
|
674
|
+
// mantém o mesmo ritmo entre campos, blocos compostos e este estado. Só a frase de
|
|
675
|
+
// negócio chega aqui; código e mensagem técnica ficam no log do servidor.
|
|
673
676
|
const errorBanner =
|
|
674
677
|
error !== undefined && !isSuccess ? (
|
|
675
678
|
<div
|
|
676
|
-
|
|
679
|
+
role="alert"
|
|
680
|
+
className="rounded-md border border-context-danger-border bg-context-danger-subtle p-3 text-sm text-context-danger-emphasis"
|
|
677
681
|
>
|
|
678
|
-
|
|
682
|
+
{humanizeActionError(error, msgText(action.messages?.error, 'Não foi possível salvar.'))}
|
|
679
683
|
</div>
|
|
680
684
|
) : null
|
|
681
685
|
|
|
@@ -754,7 +754,7 @@ function PeriodControl({
|
|
|
754
754
|
<Button
|
|
755
755
|
key={p.value}
|
|
756
756
|
size="sm"
|
|
757
|
-
context={period === p.value ? "
|
|
757
|
+
context={period === p.value ? "primary" : "neutral"}
|
|
758
758
|
variant={period === p.value ? "solid" : "ghost"}
|
|
759
759
|
className="justify-start"
|
|
760
760
|
onClick={() => {
|
|
@@ -1428,6 +1428,7 @@ export function ActionList<TInput, TItem>({
|
|
|
1428
1428
|
<PopoverTrigger asChild>
|
|
1429
1429
|
<Button
|
|
1430
1430
|
variant="outline"
|
|
1431
|
+
size="icon"
|
|
1431
1432
|
className="shrink-0"
|
|
1432
1433
|
aria-label="Exibição"
|
|
1433
1434
|
>
|
|
@@ -1790,9 +1791,8 @@ export function ActionList<TInput, TItem>({
|
|
|
1790
1791
|
<DialogClose asChild>
|
|
1791
1792
|
<Button
|
|
1792
1793
|
size="sm"
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
}
|
|
1794
|
+
context={confirming.destructive === true ? "danger" : "primary"}
|
|
1795
|
+
variant="solid"
|
|
1796
1796
|
onClick={() => {
|
|
1797
1797
|
const a = confirming;
|
|
1798
1798
|
setConfirming(null);
|
|
@@ -14,10 +14,26 @@ import {
|
|
|
14
14
|
type ReactElement,
|
|
15
15
|
type ReactNode,
|
|
16
16
|
} from "react";
|
|
17
|
+
import { createPortal } from "react-dom";
|
|
17
18
|
import { cn } from "../../lib/cn.ts";
|
|
18
19
|
|
|
19
20
|
const PageContext = createContext(false);
|
|
20
21
|
const PageHeaderContext = createContext(false);
|
|
22
|
+
const PageActionsTargetContext = createContext<HTMLElement | null>(null);
|
|
23
|
+
|
|
24
|
+
export function PageActionsTarget({
|
|
25
|
+
target,
|
|
26
|
+
children,
|
|
27
|
+
}: {
|
|
28
|
+
target: HTMLElement | null;
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
}): ReactElement {
|
|
31
|
+
return (
|
|
32
|
+
<PageActionsTargetContext.Provider value={target}>
|
|
33
|
+
{children}
|
|
34
|
+
</PageActionsTargetContext.Provider>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
21
37
|
|
|
22
38
|
function requireParent(
|
|
23
39
|
inside: boolean,
|
|
@@ -221,7 +237,8 @@ export function PageActions({
|
|
|
221
237
|
...props
|
|
222
238
|
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
223
239
|
requireParent(useContext(PageHeaderContext), "PageActions", "PageHeader");
|
|
224
|
-
|
|
240
|
+
const target = useContext(PageActionsTargetContext);
|
|
241
|
+
const actions = (
|
|
225
242
|
<div
|
|
226
243
|
data-slot="page-actions"
|
|
227
244
|
className={cn(
|
|
@@ -231,6 +248,7 @@ export function PageActions({
|
|
|
231
248
|
{...props}
|
|
232
249
|
/>
|
|
233
250
|
);
|
|
251
|
+
return target === null ? actions : createPortal(actions, target);
|
|
234
252
|
}
|
|
235
253
|
|
|
236
254
|
export function PageBody({
|
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
|
|
15
15
|
import type { ReactNode } from 'react'
|
|
16
16
|
import { cn } from '../../lib/cn.ts'
|
|
17
|
+
import { focusRing } from '../primitives/control.ts'
|
|
17
18
|
import { useSidebarCollapsed } from './sidebar.tsx'
|
|
18
19
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../primitives/tooltip.tsx'
|
|
19
20
|
|
|
21
|
+
/** @deprecated Use `SidebarNav` (ver sidebar.md); mantido por compatibilidade. */
|
|
20
22
|
export interface ShellNavItem {
|
|
21
23
|
/** Coordenada estável — volta em `onSelect` e casa com `activeId`. Único em toda a nav. */
|
|
22
24
|
id: string
|
|
@@ -29,12 +31,14 @@ export interface ShellNavItem {
|
|
|
29
31
|
disabled?: boolean
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
/** @deprecated Use `SidebarNav` (ver sidebar.md); mantido por compatibilidade. */
|
|
32
35
|
export interface ShellNavGroup {
|
|
33
36
|
/** Cabeçalho do grupo. Ausente/vazio = itens soltos, sem rótulo. Some no recolhido. */
|
|
34
37
|
label?: string
|
|
35
38
|
items: ShellNavItem[]
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
/** @deprecated Use `SidebarNav` (ver sidebar.md); mantido por compatibilidade. */
|
|
38
42
|
export interface ShellNavProps {
|
|
39
43
|
groups: ShellNavGroup[]
|
|
40
44
|
/** `id` do item ativo. Ausente = nenhum destacado. */
|
|
@@ -74,7 +78,8 @@ function renderItem(item: ShellNavItem, activeId: string | undefined, onSelect:
|
|
|
74
78
|
className={cn(
|
|
75
79
|
'flex w-full items-center gap-2.5 rounded-md py-1.5 text-left text-sm transition-colors',
|
|
76
80
|
'disabled:pointer-events-none disabled:opacity-40',
|
|
77
|
-
'outline-none
|
|
81
|
+
'outline-none',
|
|
82
|
+
focusRing,
|
|
78
83
|
railed ? 'justify-center px-0' : 'px-2.5',
|
|
79
84
|
active ? 'bg-muted font-medium text-foreground' : 'text-foreground/80 hover:bg-muted/60',
|
|
80
85
|
)}
|
|
@@ -106,6 +111,7 @@ function renderGroup(group: ShellNavGroup, gi: number, activeId: string | undefi
|
|
|
106
111
|
)
|
|
107
112
|
}
|
|
108
113
|
|
|
114
|
+
/** @deprecated Use `SidebarNav`: mesma navegação, integrada à `Sidebar`. Mantido por compatibilidade. */
|
|
109
115
|
export function ShellNav({ groups, activeId, onSelect, heading, footer, navLabel = 'Navegação', className }: ShellNavProps): React.ReactElement {
|
|
110
116
|
const railed = useSidebarCollapsed()
|
|
111
117
|
|
|
@@ -127,6 +133,7 @@ export function ShellNav({ groups, activeId, onSelect, heading, footer, navLabel
|
|
|
127
133
|
return railed ? <TooltipProvider delayDuration={200}>{body}</TooltipProvider> : body
|
|
128
134
|
}
|
|
129
135
|
|
|
136
|
+
/** @deprecated Use `SidebarNav` (ver sidebar.md); mantido por compatibilidade. */
|
|
130
137
|
export interface ShellNavHeadingProps {
|
|
131
138
|
/** Título da seção. */
|
|
132
139
|
title: ReactNode
|
|
@@ -135,8 +142,8 @@ export interface ShellNavHeadingProps {
|
|
|
135
142
|
className?: string
|
|
136
143
|
}
|
|
137
144
|
|
|
138
|
-
/** Cabeçalho
|
|
139
|
-
*
|
|
145
|
+
/** @deprecated Use `SidebarHeader` com `SidebarNav`. Cabeçalho do ShellNav: título + espaço
|
|
146
|
+
* pra um botão. Mantido por compatibilidade. */
|
|
140
147
|
export function ShellNavHeading({ title, action, className }: ShellNavHeadingProps): React.ReactElement {
|
|
141
148
|
return (
|
|
142
149
|
<div className={cn('flex h-12 shrink-0 items-center gap-1.5 border-b border-border px-4', className)}>
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { ChevronRight } from "lucide-react";
|
|
8
8
|
import { useOverflowing } from "../../lib/overflow.ts";
|
|
9
9
|
import { cn } from "../../lib/cn.ts";
|
|
10
|
+
import { focusRing } from "../primitives/control.ts";
|
|
10
11
|
import {
|
|
11
12
|
Tooltip,
|
|
12
13
|
TooltipContent,
|
|
@@ -177,17 +178,24 @@ export function SidebarItem({
|
|
|
177
178
|
<button
|
|
178
179
|
type="button"
|
|
179
180
|
disabled={disabled}
|
|
181
|
+
aria-label={label}
|
|
180
182
|
aria-current={active ? "page" : undefined}
|
|
181
183
|
onClick={onClick}
|
|
182
184
|
className={cn(
|
|
183
185
|
"mx-auto grid size-9 place-items-center rounded-md transition-colors disabled:pointer-events-none disabled:opacity-40",
|
|
184
|
-
"outline-none
|
|
186
|
+
"outline-none",
|
|
187
|
+
focusRing,
|
|
185
188
|
active
|
|
186
189
|
? "bg-muted font-medium text-foreground"
|
|
187
190
|
: "text-foreground/80 hover:bg-muted/60",
|
|
188
191
|
)}
|
|
189
192
|
>
|
|
190
|
-
|
|
193
|
+
<span
|
|
194
|
+
data-slot="sidebar-item-icon"
|
|
195
|
+
className="grid size-4 shrink-0 place-items-center [&_svg]:size-4"
|
|
196
|
+
>
|
|
197
|
+
{icon}
|
|
198
|
+
</span>
|
|
191
199
|
</button>
|
|
192
200
|
);
|
|
193
201
|
// `className` vai no MESMO elemento nos dois modos — a linha —, senão a prop mudaria de alvo
|
|
@@ -235,8 +243,9 @@ export function SidebarItem({
|
|
|
235
243
|
{/* Sem ícone, o chevron não se esconde: ele é o único ocupante do lugar. */}
|
|
236
244
|
{icon !== undefined && (
|
|
237
245
|
<span
|
|
246
|
+
data-slot="sidebar-item-icon"
|
|
238
247
|
className={cn(
|
|
239
|
-
"absolute inset-0 flex items-center justify-center transition-opacity",
|
|
248
|
+
"absolute inset-0 flex items-center justify-center transition-opacity [&_svg]:size-4",
|
|
240
249
|
onToggle !== undefined &&
|
|
241
250
|
"group-hover/sidebar-item:opacity-0 group-has-[[data-slot=sidebar-item-toggle]:focus-visible]/sidebar-item:opacity-0",
|
|
242
251
|
)}
|
|
@@ -252,7 +261,8 @@ export function SidebarItem({
|
|
|
252
261
|
aria-label={expanded ? `Fechar ${label}` : `Abrir ${label}`}
|
|
253
262
|
aria-expanded={expanded}
|
|
254
263
|
className={cn(
|
|
255
|
-
"absolute inset-0 grid place-items-center rounded-sm text-muted-foreground transition-[color,opacity] outline-none hover:text-foreground focus-visible:pointer-events-auto focus-visible:opacity-100
|
|
264
|
+
"absolute inset-0 grid place-items-center rounded-sm text-muted-foreground transition-[color,opacity] outline-none hover:text-foreground focus-visible:pointer-events-auto focus-visible:opacity-100",
|
|
265
|
+
focusRing,
|
|
256
266
|
icon === undefined
|
|
257
267
|
? "pointer-events-auto opacity-100"
|
|
258
268
|
: "pointer-events-none opacity-0 group-hover/sidebar-item:pointer-events-auto group-hover/sidebar-item:opacity-100",
|
|
@@ -275,7 +285,8 @@ export function SidebarItem({
|
|
|
275
285
|
onClick={onClick}
|
|
276
286
|
className={cn(
|
|
277
287
|
"flex min-w-0 flex-1 items-center gap-2.5 rounded-md px-2.5 py-1.5 text-left text-sm disabled:pointer-events-none",
|
|
278
|
-
"outline-none
|
|
288
|
+
"outline-none",
|
|
289
|
+
focusRing,
|
|
279
290
|
disabled
|
|
280
291
|
? "text-muted-foreground/50"
|
|
281
292
|
: active
|
|
@@ -349,7 +360,7 @@ export function SidebarGroupLabel({
|
|
|
349
360
|
<div
|
|
350
361
|
data-slot="sidebar-group-label"
|
|
351
362
|
className={cn(
|
|
352
|
-
"px-2.5
|
|
363
|
+
"px-2.5 py-1 text-sm font-medium text-muted-foreground/70",
|
|
353
364
|
className,
|
|
354
365
|
)}
|
|
355
366
|
>
|
|
@@ -16,6 +16,7 @@ import type { SimpleContract } from '../../../core/index.ts'
|
|
|
16
16
|
import { useTriggerAction } from '../../drivers/react.tsx'
|
|
17
17
|
import { toast } from '../primitives/sonner.tsx'
|
|
18
18
|
import { cn } from '../../lib/cn.ts'
|
|
19
|
+
import { humanizeActionError } from '../../lib/action-errors.ts'
|
|
19
20
|
import { Button, type ButtonProps } from '../primitives/button.tsx'
|
|
20
21
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip.tsx'
|
|
21
22
|
import {
|
|
@@ -71,16 +72,6 @@ export interface ActionTriggerProps<TInput, TData> {
|
|
|
71
72
|
className?: string
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
/**
|
|
75
|
-
* Categorias de erro cuja frase do SERVIDOR vence o rótulo do contrato: "Este agente tem 3
|
|
76
|
-
* conversas — desabilite em vez de excluir" é acionável; "Falha ao excluir" não. É
|
|
77
|
-
* ALLOWLIST de propósito — erro inesperado carrega texto técnico cru (uma violação de FK
|
|
78
|
-
* viraria `violates foreign key constraint "user_unit_id_fkey"` no toast de quem só clicou
|
|
79
|
-
* no botão), então categoria desconhecida cai no rótulo genérico, o lado seguro de errar.
|
|
80
|
-
* `authorization` fica de fora: o runtime lança `Forbidden`, pior que a frase em pt-BR.
|
|
81
|
-
*/
|
|
82
|
-
const BUSINESS_ERRORS = ['conflict', 'validation', 'not_found']
|
|
83
|
-
|
|
84
75
|
export function ActionTrigger<TInput, TData>({
|
|
85
76
|
action,
|
|
86
77
|
input,
|
|
@@ -103,9 +94,9 @@ export function ActionTrigger<TInput, TData>({
|
|
|
103
94
|
setOpen(false)
|
|
104
95
|
},
|
|
105
96
|
onError: (err) => {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
toast.error(
|
|
97
|
+
// A frase de negócio do servidor vence o rótulo do contrato (ver lib/action-errors.ts);
|
|
98
|
+
// fora da allowlist, vale o rótulo — nunca o texto técnico cru.
|
|
99
|
+
toast.error(humanizeActionError(err, msgText(action.messages?.error, 'Não foi possível concluir.')))
|
|
109
100
|
},
|
|
110
101
|
})
|
|
111
102
|
|
|
@@ -124,12 +115,19 @@ export function ActionTrigger<TInput, TData>({
|
|
|
124
115
|
}
|
|
125
116
|
: undefined)
|
|
126
117
|
const destructive = spec?.destructive === true
|
|
127
|
-
|
|
128
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Precedência de aparência:
|
|
120
|
+
* - Gatilho: `context` explícito vence `destructive`; sem ele, destrutivo é `danger`. Com
|
|
121
|
+
* ícone o botão é chrome do item: fica ghost e o vermelho aparece no hover — uma lixeira
|
|
122
|
+
* sólida vermelha em cada linha seria um campo minado visual.
|
|
123
|
+
* - Confirmar: `destructive` vence `context` (a pergunta é o momento de avisar o risco) e o
|
|
124
|
+
* botão é sempre `solid` — não herda o `variant` do gatilho, que descreve o item, não a
|
|
125
|
+
* decisão.
|
|
126
|
+
*/
|
|
129
127
|
const triggerContext = context ?? (destructive ? 'danger' : icon !== undefined ? 'neutral' : 'primary')
|
|
130
128
|
const triggerVariant = variant ?? (icon !== undefined ? 'ghost' : 'solid')
|
|
131
129
|
const confirmContext = destructive ? 'danger' : (context ?? 'primary')
|
|
132
|
-
const confirmVariant =
|
|
130
|
+
const confirmVariant = 'solid'
|
|
133
131
|
|
|
134
132
|
const fire = () => {
|
|
135
133
|
void trigger(input)
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import type { ReactNode } from 'react'
|
|
10
10
|
import type { ViewAction } from '../../../core/index.ts'
|
|
11
11
|
import { useViewAction } from '../../drivers/react.tsx'
|
|
12
|
+
import { isBusinessError, isRetryable } from '../../lib/action-errors.ts'
|
|
12
13
|
import { Alert } from '../primitives/alert.tsx'
|
|
13
14
|
import { Skeleton } from '../primitives/skeleton.tsx'
|
|
14
15
|
import { Button } from '../primitives/button.tsx'
|
|
@@ -22,10 +23,13 @@ export interface ActionViewProps<TInput, TData> {
|
|
|
22
23
|
render?: (data: TData, refetch: () => Promise<void>) => ReactNode
|
|
23
24
|
/** Renderiza estado vazio (200 mas sem dado). Default: nada. */
|
|
24
25
|
empty?: ReactNode
|
|
25
|
-
/** Renderiza loading. Default: 3 skeletons
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
/** Renderiza loading. Default (ou `true`): 3 skeletons empilhados; `false` não renderiza
|
|
27
|
+
* nada enquanto carrega — o contêiner `data-action` permanece. */
|
|
28
|
+
loading?: ReactNode | boolean
|
|
29
|
+
/** Renderiza erro. Default: "Não foi possível carregar" + botão "Tentar de novo" (omitido em
|
|
30
|
+
* falta de permissão ou sessão); a frase do servidor entra como descrição só quando está na
|
|
31
|
+
* allowlist de lib/action-errors.ts. */
|
|
32
|
+
error?: (err: { code: string; message: string; category?: string }, retry: () => Promise<void>) => ReactNode
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
export function ActionView<TInput, TData>({
|
|
@@ -44,17 +48,16 @@ export function ActionView<TInput, TData>({
|
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
if (isLoading) {
|
|
47
|
-
|
|
48
|
-
<div
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<Skeleton className="h-4 w-2/3" />
|
|
53
|
-
<Skeleton className="h-4 w-1/2" />
|
|
54
|
-
</div>
|
|
55
|
-
)}
|
|
51
|
+
const defaultLoading = (
|
|
52
|
+
<div className="space-y-3">
|
|
53
|
+
<Skeleton className="h-6 w-1/3" />
|
|
54
|
+
<Skeleton className="h-4 w-2/3" />
|
|
55
|
+
<Skeleton className="h-4 w-1/2" />
|
|
56
56
|
</div>
|
|
57
57
|
)
|
|
58
|
+
const loadingNode =
|
|
59
|
+
typeof loading === 'boolean' ? (loading ? defaultLoading : null) : (loading ?? defaultLoading)
|
|
60
|
+
return <div data-action={action.name}>{loadingNode}</div>
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
if (isError && err !== undefined) {
|
|
@@ -63,10 +66,16 @@ export function ActionView<TInput, TData>({
|
|
|
63
66
|
}
|
|
64
67
|
return (
|
|
65
68
|
<div data-action={action.name}>
|
|
66
|
-
<Alert
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
<Alert
|
|
70
|
+
context="danger"
|
|
71
|
+
title="Não foi possível carregar"
|
|
72
|
+
description={isBusinessError(err) ? err.message.trim() : undefined}
|
|
73
|
+
>
|
|
74
|
+
{isRetryable(err) && (
|
|
75
|
+
<Button variant="outline" size="sm" onClick={() => void refetch()}>
|
|
76
|
+
Tentar de novo
|
|
77
|
+
</Button>
|
|
78
|
+
)}
|
|
70
79
|
</Alert>
|
|
71
80
|
</div>
|
|
72
81
|
)
|
|
@@ -8,8 +8,13 @@ import {
|
|
|
8
8
|
|
|
9
9
|
export type AlertContext = Exclude<UiContext, "primary">;
|
|
10
10
|
export type AlertVariant = "subtle" | "outline";
|
|
11
|
+
/**
|
|
12
|
+
* @deprecated Aliases de migração: `default` → `context="neutral"`, `destructive` →
|
|
13
|
+
* `context="danger"`; `success`, `warning` e `info` → o `context` homônimo (todos em
|
|
14
|
+
* `subtle`). Declare `context` + `variant`.
|
|
15
|
+
*/
|
|
11
16
|
export type LegacyAlertVariant =
|
|
12
|
-
"default" | "destructive" | "success" | "warning" | "info"
|
|
17
|
+
"default" | "destructive" | "success" | "warning" | "info";
|
|
13
18
|
|
|
14
19
|
interface AlertStyleOptions {
|
|
15
20
|
context?: AlertContext | null;
|
|
@@ -47,13 +52,15 @@ function resolveAlertStyle({ context, variant }: AlertStyleOptions = {}): {
|
|
|
47
52
|
context: AlertContext;
|
|
48
53
|
variant: AlertVariant;
|
|
49
54
|
} {
|
|
55
|
+
// `danger` não é alias: é um contexto que chegou pela prop errada. Segue como contexto.
|
|
56
|
+
if ((variant as string) === "danger")
|
|
57
|
+
return { context: "danger", variant: "subtle" };
|
|
50
58
|
const legacy =
|
|
51
59
|
variant === "default" ||
|
|
52
60
|
variant === "destructive" ||
|
|
53
61
|
variant === "success" ||
|
|
54
62
|
variant === "warning" ||
|
|
55
|
-
variant === "info"
|
|
56
|
-
variant === "danger";
|
|
63
|
+
variant === "info";
|
|
57
64
|
if (context != null && legacy) {
|
|
58
65
|
throw new Error(
|
|
59
66
|
"Alert não permite combinar context com uma variante semântica legada.",
|
|
@@ -65,8 +72,7 @@ function resolveAlertStyle({ context, variant }: AlertStyleOptions = {}): {
|
|
|
65
72
|
if (
|
|
66
73
|
variant === "success" ||
|
|
67
74
|
variant === "warning" ||
|
|
68
|
-
variant === "info"
|
|
69
|
-
variant === "danger"
|
|
75
|
+
variant === "info"
|
|
70
76
|
) {
|
|
71
77
|
return { context: variant, variant: "subtle" };
|
|
72
78
|
}
|
|
@@ -115,7 +115,7 @@ export function Ask({
|
|
|
115
115
|
type="button"
|
|
116
116
|
size="sm"
|
|
117
117
|
shape="pill"
|
|
118
|
-
context={selected ? '
|
|
118
|
+
context={selected ? 'primary' : 'neutral'}
|
|
119
119
|
variant={selected ? 'solid' : 'outline'}
|
|
120
120
|
role={question.multiSelect ? undefined : 'radio'}
|
|
121
121
|
aria-checked={question.multiSelect ? undefined : selected}
|
|
@@ -159,7 +159,7 @@ export function Ask({
|
|
|
159
159
|
disabled={locked}
|
|
160
160
|
/>
|
|
161
161
|
{invalid && (
|
|
162
|
-
<p id={errorId} className="text-sm text-
|
|
162
|
+
<p id={errorId} className="text-sm text-context-danger-emphasis">
|
|
163
163
|
Escolha uma opção ou escreva uma resposta.
|
|
164
164
|
</p>
|
|
165
165
|
)}
|
|
@@ -169,7 +169,7 @@ export function Ask({
|
|
|
169
169
|
</div>
|
|
170
170
|
</TooltipProvider>
|
|
171
171
|
{!validCount && (
|
|
172
|
-
<p role="alert" className="mt-3 text-sm text-
|
|
172
|
+
<p role="alert" className="mt-3 text-sm text-context-danger-emphasis">
|
|
173
173
|
Ask aceita de 1 a 4 perguntas.
|
|
174
174
|
</p>
|
|
175
175
|
)}
|
|
@@ -3,6 +3,7 @@ import { Slot } from '@radix-ui/react-slot'
|
|
|
3
3
|
|
|
4
4
|
import type { UiContext } from '../../../core/ui-context.ts'
|
|
5
5
|
import { cn } from '../../lib/cn.ts'
|
|
6
|
+
import { focusRing } from './control.ts'
|
|
6
7
|
import {
|
|
7
8
|
semanticOutlineClasses,
|
|
8
9
|
semanticSolidClasses,
|
|
@@ -11,6 +12,11 @@ import {
|
|
|
11
12
|
|
|
12
13
|
export type BadgeContext = UiContext
|
|
13
14
|
export type BadgeVariant = 'solid' | 'subtle' | 'outline'
|
|
15
|
+
/**
|
|
16
|
+
* @deprecated Aliases de migração: `default` → `context="primary"` solid, `secondary` →
|
|
17
|
+
* `context="neutral"` solid, `destructive` → `context="danger"` solid; `success`, `warning` e
|
|
18
|
+
* `info` → o `context` homônimo em `subtle`. Declare `context` + `variant`.
|
|
19
|
+
*/
|
|
14
20
|
export type LegacyBadgeVariant =
|
|
15
21
|
| 'default'
|
|
16
22
|
| 'secondary'
|
|
@@ -18,7 +24,6 @@ export type LegacyBadgeVariant =
|
|
|
18
24
|
| 'success'
|
|
19
25
|
| 'warning'
|
|
20
26
|
| 'info'
|
|
21
|
-
| 'danger'
|
|
22
27
|
|
|
23
28
|
export interface BadgeStyleOptions {
|
|
24
29
|
context?: BadgeContext | null
|
|
@@ -26,20 +31,21 @@ export interface BadgeStyleOptions {
|
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
const baseClasses =
|
|
29
|
-
'inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow]
|
|
34
|
+
'inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 [&>svg]:pointer-events-none [&>svg]:size-3'
|
|
30
35
|
|
|
31
36
|
export function resolveBadgeStyle({
|
|
32
37
|
context,
|
|
33
38
|
variant,
|
|
34
39
|
}: BadgeStyleOptions = {}): { context: BadgeContext; variant: BadgeVariant } {
|
|
40
|
+
// `danger` não é alias: é um contexto que chegou pela prop errada. Segue como contexto.
|
|
41
|
+
if ((variant as string) === 'danger') return { context: 'danger', variant: 'subtle' }
|
|
35
42
|
const legacy =
|
|
36
43
|
variant === 'default' ||
|
|
37
44
|
variant === 'secondary' ||
|
|
38
45
|
variant === 'destructive' ||
|
|
39
46
|
variant === 'success' ||
|
|
40
47
|
variant === 'warning' ||
|
|
41
|
-
variant === 'info'
|
|
42
|
-
variant === 'danger'
|
|
48
|
+
variant === 'info'
|
|
43
49
|
if (context != null && legacy) {
|
|
44
50
|
throw new Error('Badge não permite combinar context com uma variante semântica legada.')
|
|
45
51
|
}
|
|
@@ -53,7 +59,6 @@ export function resolveBadgeStyle({
|
|
|
53
59
|
case 'success':
|
|
54
60
|
case 'warning':
|
|
55
61
|
case 'info':
|
|
56
|
-
case 'danger':
|
|
57
62
|
return { context: variant, variant: 'subtle' }
|
|
58
63
|
default:
|
|
59
64
|
return {
|
|
@@ -72,7 +77,7 @@ export function badgeVariants(options: BadgeStyleOptions = {}): string {
|
|
|
72
77
|
: resolved.variant === 'outline'
|
|
73
78
|
? semanticOutlineClasses[resolved.context]
|
|
74
79
|
: semanticSubtleClasses[resolved.context]
|
|
75
|
-
return cn(baseClasses, classes)
|
|
80
|
+
return cn(baseClasses, focusRing, classes)
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
export interface BadgeProps extends React.ComponentProps<'span'>, BadgeStyleOptions {
|
|
@@ -5,7 +5,7 @@ import { Slot } from "radix-ui"
|
|
|
5
5
|
import { cn } from '../../lib/cn.ts'
|
|
6
6
|
|
|
7
7
|
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
|
8
|
-
return <nav aria-label="
|
|
8
|
+
return <nav aria-label="Navegação estrutural" data-slot="breadcrumb" {...props} />
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
|
@@ -93,7 +93,7 @@ function BreadcrumbEllipsis({
|
|
|
93
93
|
{...props}
|
|
94
94
|
>
|
|
95
95
|
<MoreHorizontal className="size-4" />
|
|
96
|
-
<span className="sr-only">
|
|
96
|
+
<span className="sr-only">Mais</span>
|
|
97
97
|
</span>
|
|
98
98
|
)
|
|
99
99
|
}
|
|
@@ -3,23 +3,30 @@ import { Slot } from '@radix-ui/react-slot'
|
|
|
3
3
|
|
|
4
4
|
import type { UiContext } from '../../../core/ui-context.ts'
|
|
5
5
|
import { cn } from '../../lib/cn.ts'
|
|
6
|
-
import type
|
|
6
|
+
import { focusRing, type ControlShape } from './control.ts'
|
|
7
7
|
import { Spinner } from './spinner.tsx'
|
|
8
8
|
|
|
9
9
|
export type ButtonContext = Extract<UiContext, 'neutral' | 'primary' | 'danger'>
|
|
10
10
|
export type ButtonVariant = 'solid' | 'subtle' | 'outline' | 'ghost' | 'link'
|
|
11
|
+
/**
|
|
12
|
+
* @deprecated Aliases de migração: `default` → `context="primary"`, `secondary` →
|
|
13
|
+
* `context="neutral"`, `destructive` → `context="danger"` (todos com `variant="solid"`).
|
|
14
|
+
* Declare `context` + `variant`.
|
|
15
|
+
*/
|
|
11
16
|
export type LegacyButtonVariant = 'default' | 'secondary' | 'destructive'
|
|
12
17
|
export type ButtonSize = 'default' | 'sm' | 'lg' | 'icon' | 'icon-sm' | 'icon-xs'
|
|
13
18
|
|
|
14
19
|
export interface ButtonStyleOptions {
|
|
20
|
+
/** Sem `context`, `solid`/`subtle` assumem `primary`; `outline`/`ghost`/`link` assumem `neutral`. */
|
|
15
21
|
context?: ButtonContext | null
|
|
22
|
+
/** Tratamento visual. Os valores de `LegacyButtonVariant` são aliases em retirada. */
|
|
16
23
|
variant?: ButtonVariant | LegacyButtonVariant | null
|
|
17
24
|
size?: ButtonSize | null
|
|
18
25
|
shape?: ControlShape | null
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
const baseClasses =
|
|
22
|
-
'inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all
|
|
29
|
+
'inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all disabled:pointer-events-none disabled:opacity-50 active:brightness-90 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4'
|
|
23
30
|
|
|
24
31
|
const sizeClasses: Record<ButtonSize, string> = {
|
|
25
32
|
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
|
@@ -71,7 +78,12 @@ export function resolveButtonStyle({ context, variant }: ButtonStyleOptions = {}
|
|
|
71
78
|
if (variant === 'default') return { context: 'primary', variant: 'solid' }
|
|
72
79
|
if (variant === 'secondary') return { context: 'neutral', variant: 'solid' }
|
|
73
80
|
if (variant === 'destructive') return { context: 'danger', variant: 'solid' }
|
|
74
|
-
|
|
81
|
+
const resolvedVariant: ButtonVariant = variant ?? 'solid'
|
|
82
|
+
// Tratamentos de superfície (solid/subtle) pedem destaque; os leves (outline/ghost/link)
|
|
83
|
+
// são o chrome discreto da casa e só ganham cor quando o contexto é declarado.
|
|
84
|
+
const defaultContext: ButtonContext =
|
|
85
|
+
resolvedVariant === 'solid' || resolvedVariant === 'subtle' ? 'primary' : 'neutral'
|
|
86
|
+
return { context: context ?? defaultContext, variant: resolvedVariant }
|
|
75
87
|
}
|
|
76
88
|
|
|
77
89
|
/**
|
|
@@ -92,6 +104,7 @@ export function buttonVariants(options: ButtonStyleOptions = {}): string {
|
|
|
92
104
|
: linkClasses[resolved.context]
|
|
93
105
|
return cn(
|
|
94
106
|
baseClasses,
|
|
107
|
+
focusRing,
|
|
95
108
|
variantClasses,
|
|
96
109
|
sizeClasses[options.size ?? 'default'],
|
|
97
110
|
options.shape === 'pill' && 'rounded-full',
|