@softize/opus 12.11.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 +76 -0
- package/PROMOTED.md +46 -0
- package/README.md +28 -19
- package/bin/cli.mjs +87 -216
- package/bin/lib/check.mjs +2 -7
- package/bin/lib/cli-shared.mjs +131 -0
- package/bin/lib/copy.mjs +1 -5
- 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/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +93 -10
- package/docs/adr/0007-toast-actions-form-an-ordered-collection.md +63 -0
- package/docs/adr/0008-hierarchical-navigation-is-composed-at-the-consumer-boundary.md +71 -0
- package/docs/ownership-vs-shadcn-lock.md +2 -3
- package/docs/protocol.md +7 -7
- package/docs/radius-scale.md +1 -1
- package/docs/releasing.md +8 -2
- package/package.json +7 -3
- package/registry/skills/maintain-opus-docs/SKILL.md +83 -0
- package/registry/skills/maintain-opus-docs/agents/openai.yaml +4 -0
- package/registry/skills/maintain-opus-docs/references/editorial-standard.md +85 -0
- package/registry/skills/maintain-opus-docs/references/evaluations.md +34 -0
- package/registry/skills/maintain-opus-docs/scripts/audit-docs.mjs +81 -0
- 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 +142 -42
- 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 +36 -41
- package/src/ui/components/patterns/page-state.tsx +2 -2
- 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 +43 -32
- package/src/ui/components/patterns/trigger.tsx +39 -38
- package/src/ui/components/patterns/view.tsx +26 -17
- package/src/ui/components/primitives/alert.tsx +14 -8
- 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 +202 -40
- package/src/ui/components/primitives/dot.tsx +5 -0
- package/src/ui/components/primitives/drawer.tsx +18 -8
- package/src/ui/components/primitives/empty.tsx +3 -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/item.tsx +3 -3
- 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 +190 -8
- 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/DocBrowser.tsx +102 -23
- package/src/ui/docs/changelog.tsx +1 -1
- package/src/ui/docs/content/accordion.md +22 -16
- package/src/ui/docs/content/action-form-card.md +8 -8
- package/src/ui/docs/content/action-form-dialog.md +9 -9
- package/src/ui/docs/content/action-form.md +37 -36
- package/src/ui/docs/content/action-list-dialog.md +11 -6
- package/src/ui/docs/content/action-list.md +73 -39
- package/src/ui/docs/content/action-trigger.md +29 -15
- package/src/ui/docs/content/action-view.md +17 -10
- package/src/ui/docs/content/actions.md +9 -9
- package/src/ui/docs/content/ai.md +3 -3
- package/src/ui/docs/content/alert.md +14 -12
- package/src/ui/docs/content/ask.md +11 -0
- package/src/ui/docs/content/aspect-ratio.md +4 -4
- package/src/ui/docs/content/audit.md +2 -2
- package/src/ui/docs/content/auth.md +3 -3
- package/src/ui/docs/content/avatar.md +34 -14
- package/src/ui/docs/content/badge.md +3 -3
- package/src/ui/docs/content/breadcrumb.md +13 -8
- package/src/ui/docs/content/button.md +81 -6
- package/src/ui/docs/content/calendar.md +18 -5
- package/src/ui/docs/content/card.md +27 -1
- package/src/ui/docs/content/carousel.md +16 -11
- package/src/ui/docs/content/chat.md +23 -3
- package/src/ui/docs/content/checkbox.md +7 -7
- package/src/ui/docs/content/cli.md +74 -22
- package/src/ui/docs/content/collapsible.md +8 -8
- package/src/ui/docs/content/command.md +16 -8
- package/src/ui/docs/content/composer.md +17 -2
- package/src/ui/docs/content/content.md +17 -2
- package/src/ui/docs/content/copyable.md +12 -3
- package/src/ui/docs/content/customization.md +5 -5
- package/src/ui/docs/content/cycle.md +3 -3
- package/src/ui/docs/content/data-state.md +11 -12
- package/src/ui/docs/content/data.md +26 -33
- package/src/ui/docs/content/detail.md +22 -4
- package/src/ui/docs/content/dialog.md +339 -31
- package/src/ui/docs/content/dictionary-value.md +17 -10
- package/src/ui/docs/content/dock.md +11 -3
- package/src/ui/docs/content/dot.md +8 -0
- package/src/ui/docs/content/drawer.md +27 -14
- package/src/ui/docs/content/empty-value.md +2 -2
- package/src/ui/docs/content/empty.md +19 -12
- package/src/ui/docs/content/events.md +4 -4
- package/src/ui/docs/content/field.md +34 -12
- package/src/ui/docs/content/getting-started.md +3 -3
- package/src/ui/docs/content/icon-picker.md +19 -4
- package/src/ui/docs/content/input-otp.md +20 -12
- package/src/ui/docs/content/input.md +121 -9
- package/src/ui/docs/content/item.md +27 -13
- package/src/ui/docs/content/kbd.md +19 -11
- package/src/ui/docs/content/label.md +12 -3
- package/src/ui/docs/content/log.md +4 -4
- package/src/ui/docs/content/markdown.md +7 -6
- package/src/ui/docs/content/mcp.md +13 -15
- package/src/ui/docs/content/menu.md +40 -16
- package/src/ui/docs/content/metric-card.md +13 -0
- package/src/ui/docs/content/observability.md +2 -2
- package/src/ui/docs/content/page.md +59 -6
- package/src/ui/docs/content/pagination.md +22 -17
- package/src/ui/docs/content/popover.md +22 -8
- package/src/ui/docs/content/progress.md +15 -16
- package/src/ui/docs/content/queue.md +5 -5
- package/src/ui/docs/content/radio-group.md +20 -12
- package/src/ui/docs/content/router.md +11 -6
- package/src/ui/docs/content/scheduler.md +4 -5
- package/src/ui/docs/content/scroll-area.md +12 -7
- package/src/ui/docs/content/select.md +47 -34
- package/src/ui/docs/content/semantic-context.md +2 -2
- package/src/ui/docs/content/separator.md +5 -5
- package/src/ui/docs/content/sidebar.md +329 -54
- package/src/ui/docs/content/skeleton.md +9 -2
- package/src/ui/docs/content/slider.md +8 -7
- package/src/ui/docs/content/spinner.md +8 -8
- package/src/ui/docs/content/split.md +29 -5
- package/src/ui/docs/content/storage.md +6 -8
- package/src/ui/docs/content/switch.md +8 -7
- package/src/ui/docs/content/table.md +13 -3
- package/src/ui/docs/content/tabs.md +28 -14
- package/src/ui/docs/content/testing.md +9 -11
- package/src/ui/docs/content/textarea.md +12 -4
- package/src/ui/docs/content/toast.md +47 -13
- package/src/ui/docs/content/toggle.md +75 -7
- package/src/ui/docs/content/tokens.md +7 -7
- package/src/ui/docs/content/tooltip.md +19 -11
- package/src/ui/docs/content/truncate.md +15 -8
- package/src/ui/docs/content/ui.md +24 -9
- package/src/ui/docs/content/upgrading.md +7 -8
- package/src/ui/docs/doc-client.tsx +5 -5
- package/src/ui/docs/doc.tsx +26 -14
- package/src/ui/docs/registry.tsx +25 -42
- 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 +65 -95
- package/src/ui/react.tsx +17 -16
- package/src/ui/theme.css +60 -8
- package/src/vite/design.ts +6 -18
- package/src/ui/components/primitives/alert-dialog.tsx +0 -192
- package/src/ui/docs/content/alert-dialog.md +0 -73
- package/src/ui/docs/content/button-group.md +0 -71
- package/src/ui/docs/content/confirm.md +0 -120
- package/src/ui/docs/content/input-group.md +0 -79
- package/src/ui/docs/content/page-state.md +0 -45
- package/src/ui/docs/content/toggle-group.md +0 -81
|
@@ -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
|
|
@@ -327,24 +338,15 @@ export function SidebarItem({
|
|
|
327
338
|
|
|
328
339
|
export interface SidebarNavGroup {
|
|
329
340
|
label?: string;
|
|
330
|
-
items
|
|
331
|
-
subgroups?: SidebarNavSubgroup[];
|
|
341
|
+
items: SidebarNavItem[];
|
|
332
342
|
}
|
|
333
343
|
|
|
334
344
|
export type SidebarNavItem = Omit<SidebarItemProps, "active" | "onClick"> & {
|
|
335
345
|
id: string;
|
|
336
346
|
};
|
|
337
347
|
|
|
338
|
-
export interface SidebarNavSubgroup {
|
|
339
|
-
label?: string;
|
|
340
|
-
items: SidebarNavItem[];
|
|
341
|
-
}
|
|
342
|
-
|
|
343
348
|
const titled = (label?: string): boolean => (label ?? "").trim() !== "";
|
|
344
|
-
const
|
|
345
|
-
subgroup.items.length > 0;
|
|
346
|
-
const groupFilled = (group: SidebarNavGroup): boolean =>
|
|
347
|
-
(group.items?.length ?? 0) > 0 || (group.subgroups?.some(subFilled) ?? false);
|
|
349
|
+
const groupFilled = (group: SidebarNavGroup): boolean => group.items.length > 0;
|
|
348
350
|
|
|
349
351
|
/** Rótulo discreto de um grupo de navegação, compartilhado por listas e árvores. */
|
|
350
352
|
export function SidebarGroupLabel({
|
|
@@ -358,7 +360,30 @@ export function SidebarGroupLabel({
|
|
|
358
360
|
<div
|
|
359
361
|
data-slot="sidebar-group-label"
|
|
360
362
|
className={cn(
|
|
361
|
-
"px-2.5
|
|
363
|
+
"px-2.5 py-1 text-sm font-medium text-muted-foreground/70",
|
|
364
|
+
className,
|
|
365
|
+
)}
|
|
366
|
+
>
|
|
367
|
+
{children}
|
|
368
|
+
</div>
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export interface SidebarTreeGroupProps {
|
|
373
|
+
className?: string;
|
|
374
|
+
children: ReactNode;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Agrupa descendentes de um nó e desenha a guia visual da hierarquia. */
|
|
378
|
+
export function SidebarTreeGroup({
|
|
379
|
+
className,
|
|
380
|
+
children,
|
|
381
|
+
}: SidebarTreeGroupProps): React.ReactElement {
|
|
382
|
+
return (
|
|
383
|
+
<div
|
|
384
|
+
data-slot="sidebar-tree-group"
|
|
385
|
+
className={cn(
|
|
386
|
+
"ml-[1.0625rem] space-y-0.5 border-l border-border pl-1.5",
|
|
362
387
|
className,
|
|
363
388
|
)}
|
|
364
389
|
>
|
|
@@ -406,21 +431,7 @@ export function SidebarNav({
|
|
|
406
431
|
{!collapsed && titled(group.label) && (
|
|
407
432
|
<SidebarGroupLabel>{group.label}</SidebarGroupLabel>
|
|
408
433
|
)}
|
|
409
|
-
{group.items
|
|
410
|
-
{group.subgroups?.map((subgroup, si) =>
|
|
411
|
-
!subFilled(subgroup) ? null : (
|
|
412
|
-
<div key={si} className="space-y-0.5">
|
|
413
|
-
{!collapsed && titled(subgroup.label) && (
|
|
414
|
-
<div className="pb-0.5 pl-4 pr-2.5 pt-1.5 text-[0.6875rem] font-medium text-muted-foreground/50">
|
|
415
|
-
{subgroup.label}
|
|
416
|
-
</div>
|
|
417
|
-
)}
|
|
418
|
-
{subgroup.items.map((item) =>
|
|
419
|
-
renderItem(item, titled(subgroup.label)),
|
|
420
|
-
)}
|
|
421
|
-
</div>
|
|
422
|
-
),
|
|
423
|
-
)}
|
|
434
|
+
{group.items.map((item) => renderItem(item))}
|
|
424
435
|
</div>
|
|
425
436
|
),
|
|
426
437
|
)}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Use pra mutações sem form: assign, escalate, close, archive, excluir.
|
|
5
5
|
* - Loading state automático (disabled + "..." enquanto roda).
|
|
6
6
|
* - Toast em sucesso/erro; cache invalidation via action.invalidates.
|
|
7
|
-
* - Confirmação opcional via <
|
|
7
|
+
* - Confirmação opcional via <Dialog> (compacto, não fecha no clique fora) antes de disparar.
|
|
8
8
|
* - `icon` faz o botão virar icon-only com tooltip: é a ação que mora NO item (linha,
|
|
9
9
|
* card). Absorveu o antigo DeleteButton, que era este componente com uma lixeira.
|
|
10
10
|
* Emite `data-action="<action.name>"` na raiz (selector E2E).
|
|
@@ -16,16 +16,18 @@ 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 {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
Dialog,
|
|
24
|
+
DialogClose,
|
|
25
|
+
DialogContent,
|
|
26
|
+
DialogDescription,
|
|
27
|
+
DialogFooter,
|
|
28
|
+
DialogHeader,
|
|
29
|
+
DialogTitle,
|
|
30
|
+
} from '../primitives/dialog.tsx'
|
|
29
31
|
|
|
30
32
|
/** action.messages.* pode ser string ou I18nRef ({ key, default }). Resolve pra texto. */
|
|
31
33
|
function msgText(m: unknown, fallback: string): string {
|
|
@@ -70,16 +72,6 @@ export interface ActionTriggerProps<TInput, TData> {
|
|
|
70
72
|
className?: string
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
/**
|
|
74
|
-
* Categorias de erro cuja frase do SERVIDOR vence o rótulo do contrato: "Este agente tem 3
|
|
75
|
-
* conversas — desabilite em vez de excluir" é acionável; "Falha ao excluir" não. É
|
|
76
|
-
* ALLOWLIST de propósito — erro inesperado carrega texto técnico cru (uma violação de FK
|
|
77
|
-
* viraria `violates foreign key constraint "user_unit_id_fkey"` no toast de quem só clicou
|
|
78
|
-
* no botão), então categoria desconhecida cai no rótulo genérico, o lado seguro de errar.
|
|
79
|
-
* `authorization` fica de fora: o runtime lança `Forbidden`, pior que a frase em pt-BR.
|
|
80
|
-
*/
|
|
81
|
-
const BUSINESS_ERRORS = ['conflict', 'validation', 'not_found']
|
|
82
|
-
|
|
83
75
|
export function ActionTrigger<TInput, TData>({
|
|
84
76
|
action,
|
|
85
77
|
input,
|
|
@@ -102,9 +94,9 @@ export function ActionTrigger<TInput, TData>({
|
|
|
102
94
|
setOpen(false)
|
|
103
95
|
},
|
|
104
96
|
onError: (err) => {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
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.')))
|
|
108
100
|
},
|
|
109
101
|
})
|
|
110
102
|
|
|
@@ -123,12 +115,19 @@ export function ActionTrigger<TInput, TData>({
|
|
|
123
115
|
}
|
|
124
116
|
: undefined)
|
|
125
117
|
const destructive = spec?.destructive === true
|
|
126
|
-
|
|
127
|
-
|
|
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
|
+
*/
|
|
128
127
|
const triggerContext = context ?? (destructive ? 'danger' : icon !== undefined ? 'neutral' : 'primary')
|
|
129
128
|
const triggerVariant = variant ?? (icon !== undefined ? 'ghost' : 'solid')
|
|
130
129
|
const confirmContext = destructive ? 'danger' : (context ?? 'primary')
|
|
131
|
-
const confirmVariant =
|
|
130
|
+
const confirmVariant = 'solid'
|
|
132
131
|
|
|
133
132
|
const fire = () => {
|
|
134
133
|
void trigger(input)
|
|
@@ -174,28 +173,30 @@ export function ActionTrigger<TInput, TData>({
|
|
|
174
173
|
<>
|
|
175
174
|
{renderButton(() => setOpen(true))}
|
|
176
175
|
|
|
177
|
-
<
|
|
178
|
-
<
|
|
179
|
-
<
|
|
180
|
-
<
|
|
176
|
+
<Dialog mode="alert" open={open} onOpenChange={setOpen}>
|
|
177
|
+
<DialogContent data-action={action.name}>
|
|
178
|
+
<DialogHeader>
|
|
179
|
+
<DialogTitle>{confirm.title}</DialogTitle>
|
|
181
180
|
{(itemLabel !== undefined || confirm.description !== undefined) && (
|
|
182
|
-
<
|
|
181
|
+
<DialogDescription>
|
|
183
182
|
{itemLabel !== undefined && <span className="font-medium text-foreground">“{itemLabel}”</span>}
|
|
184
183
|
{itemLabel !== undefined && confirm.description !== undefined ? ' — ' : ''}
|
|
185
184
|
{confirm.description}
|
|
186
|
-
</
|
|
185
|
+
</DialogDescription>
|
|
187
186
|
)}
|
|
188
|
-
</
|
|
189
|
-
<
|
|
190
|
-
<
|
|
191
|
-
|
|
192
|
-
|
|
187
|
+
</DialogHeader>
|
|
188
|
+
<DialogFooter>
|
|
189
|
+
<DialogClose initialFocus asChild>
|
|
190
|
+
<Button context="neutral" variant="ghost" disabled={isLoading}>
|
|
191
|
+
{confirm.cancelLabel ?? 'Cancelar'}
|
|
192
|
+
</Button>
|
|
193
|
+
</DialogClose>
|
|
193
194
|
<Button context={confirmContext} variant={confirmVariant} busy={isLoading} onClick={fire}>
|
|
194
195
|
{confirm.actionLabel ?? buttonLabel}
|
|
195
196
|
</Button>
|
|
196
|
-
</
|
|
197
|
-
</
|
|
198
|
-
</
|
|
197
|
+
</DialogFooter>
|
|
198
|
+
</DialogContent>
|
|
199
|
+
</Dialog>
|
|
199
200
|
</>
|
|
200
201
|
)
|
|
201
202
|
}
|
|
@@ -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
|
}
|
|
@@ -76,7 +82,7 @@ function resolveAlertStyle({ context, variant }: AlertStyleOptions = {}): {
|
|
|
76
82
|
export function alertVariants(options: AlertStyleOptions = {}): string {
|
|
77
83
|
const resolved = resolveAlertStyle(options);
|
|
78
84
|
return cn(
|
|
79
|
-
"relative grid w-full grid-cols-[0_minmax(0,1fr)] items-stretch gap-x-0 gap-y-1 rounded-lg border
|
|
85
|
+
"relative grid w-full grid-cols-[0_minmax(0,1fr)_auto] items-stretch gap-x-0 gap-y-1 rounded-lg border p-4 text-sm has-[>[data-slot=alert-media]]:grid-cols-[3rem_minmax(0,1fr)_auto]",
|
|
80
86
|
resolved.variant === "outline"
|
|
81
87
|
? semanticOutlineClasses[resolved.context]
|
|
82
88
|
: semanticSubtleClasses[resolved.context],
|
|
@@ -92,7 +98,7 @@ function alertMediaClasses(context: AlertContext): string {
|
|
|
92
98
|
danger: "text-context-danger-emphasis",
|
|
93
99
|
};
|
|
94
100
|
return cn(
|
|
95
|
-
"col-start-1 row-start-1 flex
|
|
101
|
+
"col-start-1 row-start-1 flex size-9 self-start items-center justify-center rounded-md bg-current/10 [&_svg]:size-5",
|
|
96
102
|
textClasses[context],
|
|
97
103
|
);
|
|
98
104
|
}
|
|
@@ -325,7 +331,7 @@ export function AlertActions({
|
|
|
325
331
|
<div
|
|
326
332
|
data-slot="alert-actions"
|
|
327
333
|
className={cn(
|
|
328
|
-
"col-start-
|
|
334
|
+
"col-start-3 row-start-1 ms-3 flex self-center items-center gap-2",
|
|
329
335
|
className,
|
|
330
336
|
)}
|
|
331
337
|
{...props}
|
|
@@ -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',
|