@softize/opus 12.10.0 → 13.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 +56 -0
- package/bin/lib/check.mjs +1098 -310
- package/bin/lib/copy.mjs +12 -5
- package/docs/adr/0003-dictionary-presentation-is-declared.md +3 -0
- package/docs/adr/0004-page-content-state-is-composed.md +65 -0
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +180 -0
- package/docs/adr/0006-semantic-context-precedes-visual-variant.md +182 -0
- 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/radius-scale.md +1 -1
- package/package.json +1 -1
- package/registry/instructions/opus.md +5 -0
- package/registry/skills/build-opus-ui/SKILL.md +27 -16
- package/registry/skills/build-opus-ui/references/evaluations.md +16 -5
- package/registry/skills/build-opus-ui/references/ui-patterns.md +38 -15
- 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/skills/model-opus-dictionary/SKILL.md +4 -2
- package/registry/skills/model-opus-dictionary/references/evaluations.md +4 -3
- package/registry/templates/app/src/App.tsx +1 -1
- package/src/core/dictionary.ts +52 -14
- package/src/core/index.ts +10 -0
- package/src/core/ui-context.ts +29 -0
- package/src/schema/drivers/zod.ts +17 -8
- package/src/ui/components/patterns/action-form-card.tsx +18 -12
- package/src/ui/components/patterns/confirm.tsx +163 -40
- package/src/ui/components/patterns/content-header.tsx +335 -61
- package/src/ui/components/patterns/data-state.tsx +23 -10
- package/src/ui/components/patterns/list.tsx +1097 -783
- package/src/ui/components/patterns/page-state.tsx +115 -0
- package/src/ui/components/patterns/page.tsx +231 -41
- package/src/ui/components/patterns/sidebar.tsx +357 -83
- package/src/ui/components/patterns/trigger.tsx +37 -30
- package/src/ui/components/patterns/view.tsx +7 -11
- package/src/ui/components/primitives/alert.tsx +298 -110
- package/src/ui/components/primitives/ask.tsx +2 -1
- package/src/ui/components/primitives/badge.tsx +91 -30
- package/src/ui/components/primitives/button.tsx +99 -60
- package/src/ui/components/primitives/calendar.tsx +39 -39
- package/src/ui/components/primitives/card.tsx +96 -23
- package/src/ui/components/primitives/detail.tsx +2 -2
- package/src/ui/components/primitives/dialog.tsx +196 -39
- package/src/ui/components/primitives/dictionary-value.tsx +9 -14
- package/src/ui/components/primitives/dot.tsx +74 -21
- package/src/ui/components/primitives/drawer.tsx +40 -24
- package/src/ui/components/primitives/empty.tsx +3 -3
- package/src/ui/components/primitives/item.tsx +135 -79
- package/src/ui/components/primitives/menu.tsx +11 -3
- package/src/ui/components/primitives/metric-card.tsx +133 -0
- package/src/ui/components/primitives/sonner.tsx +187 -8
- package/src/ui/components/primitives/table.tsx +2 -2
- package/src/ui/docs/DocBrowser.tsx +104 -25
- 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 +28 -34
- package/src/ui/docs/content/action-list-dialog.md +11 -6
- package/src/ui/docs/content/action-list.md +64 -39
- package/src/ui/docs/content/action-trigger.md +21 -14
- package/src/ui/docs/content/action-view.md +8 -8
- 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 +54 -28
- 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 +21 -22
- package/src/ui/docs/content/breadcrumb.md +13 -8
- package/src/ui/docs/content/button.md +93 -15
- package/src/ui/docs/content/calendar.md +5 -5
- package/src/ui/docs/content/card.md +6 -6
- package/src/ui/docs/content/carousel.md +16 -11
- package/src/ui/docs/content/chat.md +3 -3
- package/src/ui/docs/content/checkbox.md +7 -7
- package/src/ui/docs/content/cli.md +5 -5
- 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 +2 -2
- package/src/ui/docs/content/content.md +44 -0
- package/src/ui/docs/content/copyable.md +4 -3
- package/src/ui/docs/content/customization.md +7 -7
- 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 +8 -5
- package/src/ui/docs/content/dialog.md +339 -31
- package/src/ui/docs/content/dictionary-value.md +19 -18
- package/src/ui/docs/content/dock.md +3 -3
- package/src/ui/docs/content/dot.md +7 -7
- package/src/ui/docs/content/drawer.md +32 -16
- 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 +1 -1
- package/src/ui/docs/content/icon-picker.md +8 -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 +64 -24
- package/src/ui/docs/content/kbd.md +19 -11
- package/src/ui/docs/content/label.md +5 -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 +36 -17
- package/src/ui/docs/content/metric-card.md +41 -0
- package/src/ui/docs/content/observability.md +2 -2
- package/src/ui/docs/content/page.md +93 -10
- package/src/ui/docs/content/pagination.md +22 -17
- package/src/ui/docs/content/popover.md +16 -8
- package/src/ui/docs/content/progress.md +7 -5
- 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 +42 -29
- package/src/ui/docs/content/semantic-context.md +63 -0
- package/src/ui/docs/content/separator.md +5 -5
- package/src/ui/docs/content/sidebar.md +325 -56
- package/src/ui/docs/content/skeleton.md +5 -4
- 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 +8 -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 +16 -6
- 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 +5 -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 +31 -3
- package/src/ui/docs/content/tooltip.md +19 -11
- package/src/ui/docs/content/truncate.md +7 -8
- package/src/ui/docs/content/ui.md +10 -9
- package/src/ui/docs/content/upgrading.md +7 -8
- package/src/ui/docs/doc-client.tsx +2 -2
- package/src/ui/docs/registry.tsx +580 -229
- package/src/ui/lib/semantic-context.ts +30 -0
- package/src/ui/meta.ts +278 -286
- package/src/ui/react.tsx +377 -111
- package/src/ui/theme.css +116 -0
- package/src/ui/components/primitives/alert-dialog.tsx +0 -190
- 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 -78
- package/src/ui/docs/content/toggle-group.md +0 -81
|
@@ -1,160 +1,434 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
type HTMLAttributes,
|
|
5
|
+
type ReactNode,
|
|
6
|
+
} from "react";
|
|
7
|
+
import { ChevronRight } from "lucide-react";
|
|
8
|
+
import { useOverflowing } from "../../lib/overflow.ts";
|
|
9
|
+
import { cn } from "../../lib/cn.ts";
|
|
10
|
+
import {
|
|
11
|
+
Tooltip,
|
|
12
|
+
TooltipContent,
|
|
13
|
+
TooltipProvider,
|
|
14
|
+
TooltipTrigger,
|
|
15
|
+
} from "../primitives/tooltip.tsx";
|
|
6
16
|
|
|
7
|
-
interface SidebarContextValue {
|
|
8
|
-
|
|
17
|
+
interface SidebarContextValue {
|
|
18
|
+
collapsed: boolean;
|
|
19
|
+
}
|
|
20
|
+
const SidebarContext = createContext<SidebarContextValue | null>(null);
|
|
9
21
|
|
|
10
22
|
/** Estado da Sidebar mais próxima. Uso interno por navegações que se adaptam ao colapso. */
|
|
11
23
|
export function useSidebarCollapsed(): boolean {
|
|
12
|
-
return useContext(SidebarContext)?.collapsed ?? false
|
|
24
|
+
return useContext(SidebarContext)?.collapsed ?? false;
|
|
13
25
|
}
|
|
14
26
|
|
|
15
27
|
export interface SidebarProps {
|
|
16
|
-
collapsed?: boolean
|
|
28
|
+
collapsed?: boolean;
|
|
17
29
|
/** Desliga a borda quando o Split já desenha a divisória. */
|
|
18
|
-
divider?: boolean
|
|
19
|
-
className?: string
|
|
20
|
-
children: ReactNode
|
|
30
|
+
divider?: boolean;
|
|
31
|
+
className?: string;
|
|
32
|
+
children: ReactNode;
|
|
21
33
|
}
|
|
22
34
|
|
|
23
35
|
/** Moldura composicional de uma barra lateral; o Split decide a posição, Sidebar o chrome. */
|
|
24
|
-
export function Sidebar({
|
|
36
|
+
export function Sidebar({
|
|
37
|
+
collapsed = false,
|
|
38
|
+
divider = true,
|
|
39
|
+
className,
|
|
40
|
+
children,
|
|
41
|
+
}: SidebarProps): React.ReactElement {
|
|
25
42
|
return (
|
|
26
43
|
<SidebarContext.Provider value={{ collapsed }}>
|
|
27
|
-
<aside
|
|
44
|
+
<aside
|
|
45
|
+
data-slot="sidebar"
|
|
46
|
+
data-collapsed={String(collapsed)}
|
|
47
|
+
className={cn(
|
|
48
|
+
"group/sidebar flex h-full min-h-0 shrink-0 flex-col transition-[width] duration-200 ease-out",
|
|
49
|
+
divider && "border-r border-border",
|
|
50
|
+
collapsed ? "w-14" : "w-64",
|
|
51
|
+
className,
|
|
52
|
+
)}
|
|
53
|
+
>
|
|
28
54
|
{children}
|
|
29
55
|
</aside>
|
|
30
56
|
</SidebarContext.Provider>
|
|
31
|
-
)
|
|
57
|
+
);
|
|
32
58
|
}
|
|
33
59
|
|
|
34
60
|
/** Faixa fixa no topo de um pane — marca, contexto ou ações locais. */
|
|
35
|
-
export function PaneHeader({
|
|
36
|
-
|
|
61
|
+
export function PaneHeader({
|
|
62
|
+
className,
|
|
63
|
+
children,
|
|
64
|
+
}: {
|
|
65
|
+
className?: string;
|
|
66
|
+
children: ReactNode;
|
|
67
|
+
}): React.ReactElement {
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
data-slot="pane-header"
|
|
71
|
+
className={cn("shrink-0 border-b border-border", className)}
|
|
72
|
+
>
|
|
73
|
+
{children}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
37
76
|
}
|
|
38
77
|
|
|
39
78
|
/** Região rolável que ocupa o espaço restante de um pane. */
|
|
40
|
-
export function
|
|
41
|
-
|
|
79
|
+
export function PaneBody({
|
|
80
|
+
className,
|
|
81
|
+
children,
|
|
82
|
+
}: {
|
|
83
|
+
className?: string;
|
|
84
|
+
children: ReactNode;
|
|
85
|
+
}): React.ReactElement {
|
|
86
|
+
return (
|
|
87
|
+
<div
|
|
88
|
+
data-slot="pane-body"
|
|
89
|
+
className={cn("min-h-0 flex-1 overflow-y-auto", className)}
|
|
90
|
+
>
|
|
91
|
+
{children}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** @deprecated Use PaneBody. Mantido durante a versão 12 para migração gradual. */
|
|
97
|
+
export function PaneContent({
|
|
98
|
+
className,
|
|
99
|
+
children,
|
|
100
|
+
}: {
|
|
101
|
+
className?: string;
|
|
102
|
+
children: ReactNode;
|
|
103
|
+
}): React.ReactElement {
|
|
104
|
+
return (
|
|
105
|
+
<div
|
|
106
|
+
data-slot="pane-content"
|
|
107
|
+
className={cn("min-h-0 flex-1 overflow-y-auto", className)}
|
|
108
|
+
>
|
|
109
|
+
{children}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
42
112
|
}
|
|
43
113
|
|
|
44
114
|
/** Faixa fixa no rodapé de um pane para ações persistentes. */
|
|
45
|
-
export function PaneFooter({
|
|
46
|
-
|
|
115
|
+
export function PaneFooter({
|
|
116
|
+
className,
|
|
117
|
+
children,
|
|
118
|
+
}: {
|
|
119
|
+
className?: string;
|
|
120
|
+
children: ReactNode;
|
|
121
|
+
}): React.ReactElement {
|
|
122
|
+
return (
|
|
123
|
+
<div
|
|
124
|
+
data-slot="pane-footer"
|
|
125
|
+
className={cn("shrink-0 border-t border-border p-2", className)}
|
|
126
|
+
>
|
|
127
|
+
{children}
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
47
130
|
}
|
|
48
131
|
|
|
49
132
|
export interface SidebarItemProps {
|
|
50
|
-
label: string
|
|
51
|
-
icon?: ReactNode
|
|
52
|
-
badge?: ReactNode
|
|
53
|
-
active?: boolean
|
|
54
|
-
disabled?: boolean
|
|
133
|
+
label: string;
|
|
134
|
+
icon?: ReactNode;
|
|
135
|
+
badge?: ReactNode;
|
|
136
|
+
active?: boolean;
|
|
137
|
+
disabled?: boolean;
|
|
55
138
|
/** Controles irmãos do alvo de navegação (ex.: menu de ações). */
|
|
56
|
-
actions?: ReactNode
|
|
139
|
+
actions?: ReactNode;
|
|
57
140
|
/** Presente quando a linha desdobra filhos; o controle fica fora do botão principal. */
|
|
58
|
-
onToggle?: () => void
|
|
59
|
-
expanded?: boolean
|
|
141
|
+
onToggle?: () => void;
|
|
142
|
+
expanded?: boolean;
|
|
60
143
|
/** Posição indicada durante drag-and-drop; bordas inserem, centro recebe dentro. */
|
|
61
|
-
dropPosition?:
|
|
144
|
+
dropPosition?: "before" | "inside" | "after";
|
|
62
145
|
/** Props de drag-and-drop aplicadas à linha, sem acoplar a um driver. */
|
|
63
|
-
dragProps?: HTMLAttributes<HTMLDivElement
|
|
146
|
+
dragProps?: HTMLAttributes<HTMLDivElement>;
|
|
64
147
|
/** Complemento do tooltip no rail recolhido. */
|
|
65
|
-
tooltipHint?: ReactNode
|
|
66
|
-
className?: string
|
|
67
|
-
onClick: () => void
|
|
148
|
+
tooltipHint?: ReactNode;
|
|
149
|
+
className?: string;
|
|
150
|
+
onClick: () => void;
|
|
68
151
|
}
|
|
69
152
|
|
|
70
153
|
/** Linha única de navegação: o alvo principal e seus controles são irmãos, nunca botões aninhados. */
|
|
71
|
-
export function SidebarItem({
|
|
72
|
-
|
|
73
|
-
|
|
154
|
+
export function SidebarItem({
|
|
155
|
+
label,
|
|
156
|
+
icon,
|
|
157
|
+
badge,
|
|
158
|
+
active = false,
|
|
159
|
+
disabled = false,
|
|
160
|
+
actions,
|
|
161
|
+
onToggle,
|
|
162
|
+
expanded = false,
|
|
163
|
+
dropPosition,
|
|
164
|
+
dragProps,
|
|
165
|
+
tooltipHint,
|
|
166
|
+
className,
|
|
167
|
+
onClick,
|
|
168
|
+
}: SidebarItemProps): React.ReactElement {
|
|
169
|
+
const collapsed = useSidebarCollapsed();
|
|
170
|
+
const hasActions = actions !== undefined && actions !== null;
|
|
74
171
|
// O rótulo cortado esmaece em vez de terminar em reticências; a medição do transbordo
|
|
75
172
|
// mantém a máscara fora do rótulo que cabe inteiro na linha.
|
|
76
|
-
const { ref: labelRef, overflowing: labelCut } =
|
|
173
|
+
const { ref: labelRef, overflowing: labelCut } =
|
|
174
|
+
useOverflowing<HTMLSpanElement>(label);
|
|
77
175
|
if (collapsed) {
|
|
78
176
|
const button = (
|
|
79
|
-
<button
|
|
177
|
+
<button
|
|
178
|
+
type="button"
|
|
179
|
+
disabled={disabled}
|
|
180
|
+
aria-current={active ? "page" : undefined}
|
|
181
|
+
onClick={onClick}
|
|
182
|
+
className={cn(
|
|
183
|
+
"mx-auto grid size-9 place-items-center rounded-md transition-colors disabled:pointer-events-none disabled:opacity-40",
|
|
184
|
+
"outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
|
|
185
|
+
active
|
|
186
|
+
? "bg-muted font-medium text-foreground"
|
|
187
|
+
: "text-foreground/80 hover:bg-muted/60",
|
|
188
|
+
)}
|
|
189
|
+
>
|
|
80
190
|
{icon}
|
|
81
191
|
</button>
|
|
82
|
-
)
|
|
192
|
+
);
|
|
83
193
|
// `className` vai no MESMO elemento nos dois modos — a linha —, senão a prop mudaria de alvo
|
|
84
194
|
// conforme o rail estivesse recolhido, e o consumidor não teria como saber o que estilizou.
|
|
85
|
-
return
|
|
195
|
+
return (
|
|
196
|
+
<div data-slot="sidebar-item" className={className} {...dragProps}>
|
|
197
|
+
<Tooltip>
|
|
198
|
+
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
|
199
|
+
<TooltipContent side="right">
|
|
200
|
+
{label}
|
|
201
|
+
{tooltipHint}
|
|
202
|
+
</TooltipContent>
|
|
203
|
+
</Tooltip>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
86
206
|
}
|
|
87
207
|
|
|
88
208
|
return (
|
|
89
|
-
<div
|
|
90
|
-
|
|
209
|
+
<div
|
|
210
|
+
data-slot="sidebar-item"
|
|
211
|
+
className={cn(
|
|
212
|
+
"group/sidebar-item relative flex items-center rounded-md transition-colors",
|
|
213
|
+
disabled ? "text-muted-foreground/50 opacity-40" : "hover:bg-muted/60",
|
|
214
|
+
active && "bg-muted",
|
|
215
|
+
dropPosition === "inside" &&
|
|
216
|
+
"bg-primary/5 ring-2 ring-inset ring-primary/60",
|
|
217
|
+
className,
|
|
218
|
+
)}
|
|
219
|
+
{...dragProps}
|
|
220
|
+
>
|
|
221
|
+
{(dropPosition === "before" || dropPosition === "after") && (
|
|
222
|
+
<span
|
|
223
|
+
data-slot="sidebar-item-drop-indicator"
|
|
224
|
+
className={cn(
|
|
225
|
+
"pointer-events-none absolute inset-x-1 z-20 h-0.5 rounded-full bg-primary before:absolute before:-left-1 before:top-1/2 before:size-2 before:-translate-y-1/2 before:rounded-full before:border-2 before:border-primary before:bg-background",
|
|
226
|
+
dropPosition === "before" ? "-top-px" : "-bottom-px",
|
|
227
|
+
)}
|
|
228
|
+
/>
|
|
229
|
+
)}
|
|
91
230
|
{(icon !== undefined || onToggle !== undefined) && (
|
|
92
|
-
<span
|
|
231
|
+
<span
|
|
232
|
+
data-slot="sidebar-item-leading"
|
|
233
|
+
className="pointer-events-none absolute left-2.5 top-1/2 z-10 grid size-4 -translate-y-1/2 place-items-center"
|
|
234
|
+
>
|
|
93
235
|
{/* Sem ícone, o chevron não se esconde: ele é o único ocupante do lugar. */}
|
|
94
|
-
{icon !== undefined &&
|
|
236
|
+
{icon !== undefined && (
|
|
237
|
+
<span
|
|
238
|
+
className={cn(
|
|
239
|
+
"absolute inset-0 flex items-center justify-center transition-opacity",
|
|
240
|
+
onToggle !== undefined &&
|
|
241
|
+
"group-hover/sidebar-item:opacity-0 group-has-[[data-slot=sidebar-item-toggle]:focus-visible]/sidebar-item:opacity-0",
|
|
242
|
+
)}
|
|
243
|
+
>
|
|
244
|
+
{icon}
|
|
245
|
+
</span>
|
|
246
|
+
)}
|
|
95
247
|
{onToggle !== undefined && (
|
|
96
|
-
<button
|
|
97
|
-
|
|
248
|
+
<button
|
|
249
|
+
type="button"
|
|
250
|
+
data-slot="sidebar-item-toggle"
|
|
251
|
+
onClick={onToggle}
|
|
252
|
+
aria-label={expanded ? `Fechar ${label}` : `Abrir ${label}`}
|
|
253
|
+
aria-expanded={expanded}
|
|
254
|
+
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 focus-visible:ring-2 focus-visible:ring-ring/50",
|
|
256
|
+
icon === undefined
|
|
257
|
+
? "pointer-events-auto opacity-100"
|
|
258
|
+
: "pointer-events-none opacity-0 group-hover/sidebar-item:pointer-events-auto group-hover/sidebar-item:opacity-100",
|
|
259
|
+
)}
|
|
260
|
+
>
|
|
261
|
+
<ChevronRight
|
|
262
|
+
className={cn(
|
|
263
|
+
"size-3 transition-transform",
|
|
264
|
+
expanded && "rotate-90",
|
|
265
|
+
)}
|
|
266
|
+
/>
|
|
98
267
|
</button>
|
|
99
268
|
)}
|
|
100
269
|
</span>
|
|
101
270
|
)}
|
|
102
|
-
<button
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
{
|
|
271
|
+
<button
|
|
272
|
+
type="button"
|
|
273
|
+
disabled={disabled}
|
|
274
|
+
aria-current={active ? "page" : undefined}
|
|
275
|
+
onClick={onClick}
|
|
276
|
+
className={cn(
|
|
277
|
+
"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 focus-visible:ring-2 focus-visible:ring-ring/50",
|
|
279
|
+
disabled
|
|
280
|
+
? "text-muted-foreground/50"
|
|
281
|
+
: active
|
|
282
|
+
? "font-medium text-foreground"
|
|
283
|
+
: "text-foreground/80",
|
|
284
|
+
)}
|
|
285
|
+
>
|
|
286
|
+
{(icon !== undefined || onToggle !== undefined) && (
|
|
287
|
+
<span
|
|
288
|
+
data-slot="sidebar-item-leading-space"
|
|
289
|
+
aria-hidden
|
|
290
|
+
className="size-4 shrink-0"
|
|
291
|
+
/>
|
|
292
|
+
)}
|
|
293
|
+
<span
|
|
294
|
+
ref={labelRef}
|
|
295
|
+
data-slot="sidebar-item-label"
|
|
296
|
+
className={cn(
|
|
297
|
+
"min-w-0 flex-1 overflow-hidden whitespace-nowrap group-has-[[data-slot=dot]]/sidebar-item:mr-[1.1875rem]",
|
|
298
|
+
labelCut && "truncate-fade",
|
|
299
|
+
)}
|
|
300
|
+
>
|
|
301
|
+
{label}
|
|
302
|
+
</span>
|
|
303
|
+
{badge !== undefined && (
|
|
304
|
+
<span
|
|
305
|
+
data-slot="sidebar-item-badge"
|
|
306
|
+
className={cn(
|
|
307
|
+
"shrink-0 transition-opacity has-[[data-slot=dot]]:absolute has-[[data-slot=dot]]:right-1 has-[[data-slot=dot]]:top-1/2 has-[[data-slot=dot]]:z-10 has-[[data-slot=dot]]:grid has-[[data-slot=dot]]:size-6 has-[[data-slot=dot]]:-translate-y-1/2 has-[[data-slot=dot]]:place-items-center",
|
|
308
|
+
hasActions &&
|
|
309
|
+
"group-hover/sidebar-item:has-[[data-slot=dot]]:opacity-0",
|
|
310
|
+
)}
|
|
311
|
+
>
|
|
312
|
+
{badge}
|
|
313
|
+
</span>
|
|
314
|
+
)}
|
|
106
315
|
</button>
|
|
107
|
-
{hasActions &&
|
|
316
|
+
{hasActions && (
|
|
317
|
+
<div
|
|
318
|
+
data-slot="sidebar-item-actions"
|
|
319
|
+
className="pointer-events-none absolute right-1 top-1/2 z-10 flex -translate-y-1/2 items-center opacity-0 transition-opacity group-hover/sidebar-item:pointer-events-auto group-hover/sidebar-item:opacity-100 has-[:focus-visible]:pointer-events-auto has-[:focus-visible]:opacity-100 has-[[data-state=open]]:pointer-events-auto has-[[data-state=open]]:opacity-100 [&_button]:grid [&_button]:size-6 [&_button]:place-items-center [&_button]:p-0 [&_svg]:size-3.5"
|
|
320
|
+
>
|
|
321
|
+
{actions}
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
108
324
|
</div>
|
|
109
|
-
)
|
|
325
|
+
);
|
|
110
326
|
}
|
|
111
327
|
|
|
112
328
|
export interface SidebarNavGroup {
|
|
113
|
-
label?: string
|
|
114
|
-
items
|
|
115
|
-
subgroups?: SidebarNavSubgroup[]
|
|
329
|
+
label?: string;
|
|
330
|
+
items: SidebarNavItem[];
|
|
116
331
|
}
|
|
117
332
|
|
|
118
|
-
export type SidebarNavItem = Omit<SidebarItemProps,
|
|
333
|
+
export type SidebarNavItem = Omit<SidebarItemProps, "active" | "onClick"> & {
|
|
334
|
+
id: string;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const titled = (label?: string): boolean => (label ?? "").trim() !== "";
|
|
338
|
+
const groupFilled = (group: SidebarNavGroup): boolean => group.items.length > 0;
|
|
119
339
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
340
|
+
/** Rótulo discreto de um grupo de navegação, compartilhado por listas e árvores. */
|
|
341
|
+
export function SidebarGroupLabel({
|
|
342
|
+
className,
|
|
343
|
+
children,
|
|
344
|
+
}: {
|
|
345
|
+
className?: string;
|
|
346
|
+
children: ReactNode;
|
|
347
|
+
}): React.ReactElement {
|
|
348
|
+
return (
|
|
349
|
+
<div
|
|
350
|
+
data-slot="sidebar-group-label"
|
|
351
|
+
className={cn(
|
|
352
|
+
"px-2.5 pb-1 text-sm font-medium text-muted-foreground/70",
|
|
353
|
+
className,
|
|
354
|
+
)}
|
|
355
|
+
>
|
|
356
|
+
{children}
|
|
357
|
+
</div>
|
|
358
|
+
);
|
|
123
359
|
}
|
|
124
360
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
361
|
+
export interface SidebarTreeGroupProps {
|
|
362
|
+
className?: string;
|
|
363
|
+
children: ReactNode;
|
|
364
|
+
}
|
|
129
365
|
|
|
130
|
-
/**
|
|
131
|
-
export function
|
|
132
|
-
|
|
366
|
+
/** Agrupa descendentes de um nó e desenha a guia visual da hierarquia. */
|
|
367
|
+
export function SidebarTreeGroup({
|
|
368
|
+
className,
|
|
369
|
+
children,
|
|
370
|
+
}: SidebarTreeGroupProps): React.ReactElement {
|
|
371
|
+
return (
|
|
372
|
+
<div
|
|
373
|
+
data-slot="sidebar-tree-group"
|
|
374
|
+
className={cn(
|
|
375
|
+
"ml-[1.0625rem] space-y-0.5 border-l border-border pl-1.5",
|
|
376
|
+
className,
|
|
377
|
+
)}
|
|
378
|
+
>
|
|
379
|
+
{children}
|
|
380
|
+
</div>
|
|
381
|
+
);
|
|
133
382
|
}
|
|
134
383
|
|
|
135
384
|
/** Navegação controlada para Sidebar. Header/footer continuam slots explícitos do pai. */
|
|
136
|
-
export function SidebarNav({
|
|
137
|
-
|
|
138
|
-
|
|
385
|
+
export function SidebarNav({
|
|
386
|
+
groups,
|
|
387
|
+
activeId,
|
|
388
|
+
onSelect,
|
|
389
|
+
navLabel = "Navegação",
|
|
390
|
+
className,
|
|
391
|
+
}: {
|
|
392
|
+
groups: SidebarNavGroup[];
|
|
393
|
+
activeId?: string;
|
|
394
|
+
onSelect: (id: string) => void;
|
|
395
|
+
navLabel?: string;
|
|
396
|
+
className?: string;
|
|
397
|
+
}): React.ReactElement {
|
|
398
|
+
const collapsed = useSidebarCollapsed();
|
|
399
|
+
const renderItem = (
|
|
400
|
+
item: SidebarNavItem,
|
|
401
|
+
nested = false,
|
|
402
|
+
): React.ReactElement => (
|
|
139
403
|
<SidebarItem
|
|
140
404
|
key={item.id}
|
|
141
405
|
{...item}
|
|
142
|
-
className={cn(nested && !collapsed &&
|
|
406
|
+
className={cn(nested && !collapsed && "pl-6", item.className)}
|
|
143
407
|
active={item.id === activeId}
|
|
144
408
|
onClick={() => onSelect(item.id)}
|
|
145
409
|
/>
|
|
146
|
-
)
|
|
410
|
+
);
|
|
147
411
|
const body = (
|
|
148
|
-
<nav
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
412
|
+
<nav
|
|
413
|
+
aria-label={navLabel}
|
|
414
|
+
data-slot="sidebar-nav"
|
|
415
|
+
className={cn("space-y-4 p-2", className)}
|
|
416
|
+
>
|
|
417
|
+
{groups.map((group, gi) =>
|
|
418
|
+
!groupFilled(group) ? null : (
|
|
419
|
+
<div key={gi} className="space-y-0.5">
|
|
420
|
+
{!collapsed && titled(group.label) && (
|
|
421
|
+
<SidebarGroupLabel>{group.label}</SidebarGroupLabel>
|
|
422
|
+
)}
|
|
423
|
+
{group.items.map((item) => renderItem(item))}
|
|
424
|
+
</div>
|
|
425
|
+
),
|
|
426
|
+
)}
|
|
157
427
|
</nav>
|
|
158
|
-
)
|
|
159
|
-
return collapsed ?
|
|
428
|
+
);
|
|
429
|
+
return collapsed ? (
|
|
430
|
+
<TooltipProvider delayDuration={200}>{body}</TooltipProvider>
|
|
431
|
+
) : (
|
|
432
|
+
body
|
|
433
|
+
);
|
|
160
434
|
}
|
|
@@ -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,17 @@ 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 { Button } from '../primitives/button.tsx'
|
|
19
|
+
import { Button, type ButtonProps } from '../primitives/button.tsx'
|
|
20
20
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip.tsx'
|
|
21
21
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
Dialog,
|
|
23
|
+
DialogClose,
|
|
24
|
+
DialogContent,
|
|
25
|
+
DialogDescription,
|
|
26
|
+
DialogFooter,
|
|
27
|
+
DialogHeader,
|
|
28
|
+
DialogTitle,
|
|
29
|
+
} from '../primitives/dialog.tsx'
|
|
29
30
|
|
|
30
31
|
/** action.messages.* pode ser string ou I18nRef ({ key, default }). Resolve pra texto. */
|
|
31
32
|
function msgText(m: unknown, fallback: string): string {
|
|
@@ -43,9 +44,10 @@ export interface ActionTriggerProps<TInput, TData> {
|
|
|
43
44
|
input: TInput
|
|
44
45
|
/** Texto do botão. Default usa action.label. */
|
|
45
46
|
label?: string
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
/** Contexto visual. Por padrão, ações destrutivas usam `danger`. */
|
|
48
|
+
context?: ButtonProps['context']
|
|
49
|
+
/** Tratamento visual do Button. */
|
|
50
|
+
variant?: ButtonProps['variant']
|
|
49
51
|
/** Tamanho do botão. */
|
|
50
52
|
size?: 'default' | 'sm' | 'lg' | 'icon'
|
|
51
53
|
/** Desabilita o botão independente de loading. */
|
|
@@ -83,6 +85,7 @@ export function ActionTrigger<TInput, TData>({
|
|
|
83
85
|
action,
|
|
84
86
|
input,
|
|
85
87
|
label,
|
|
88
|
+
context,
|
|
86
89
|
variant,
|
|
87
90
|
size = 'default',
|
|
88
91
|
disabled = false,
|
|
@@ -123,8 +126,10 @@ export function ActionTrigger<TInput, TData>({
|
|
|
123
126
|
const destructive = spec?.destructive === true
|
|
124
127
|
// Com ícone o botão é chrome do item: fica ghost, e o vermelho aparece no hover — uma
|
|
125
128
|
// lixeira sólida vermelha em cada linha seria um campo minado visual.
|
|
126
|
-
const
|
|
127
|
-
const
|
|
129
|
+
const triggerContext = context ?? (destructive ? 'danger' : icon !== undefined ? 'neutral' : 'primary')
|
|
130
|
+
const triggerVariant = variant ?? (icon !== undefined ? 'ghost' : 'solid')
|
|
131
|
+
const confirmContext = destructive ? 'danger' : (context ?? 'primary')
|
|
132
|
+
const confirmVariant = variant ?? 'solid'
|
|
128
133
|
|
|
129
134
|
const fire = () => {
|
|
130
135
|
void trigger(input)
|
|
@@ -136,6 +141,7 @@ export function ActionTrigger<TInput, TData>({
|
|
|
136
141
|
const renderButton = (onClick: () => void): React.ReactElement => {
|
|
137
142
|
const button = (
|
|
138
143
|
<Button
|
|
144
|
+
context={triggerContext}
|
|
139
145
|
variant={triggerVariant}
|
|
140
146
|
size={icon !== undefined ? 'icon' : size}
|
|
141
147
|
busy={isLoading}
|
|
@@ -147,7 +153,6 @@ export function ActionTrigger<TInput, TData>({
|
|
|
147
153
|
}}
|
|
148
154
|
className={cn(
|
|
149
155
|
icon !== undefined && 'size-7 shrink-0 text-muted-foreground/60',
|
|
150
|
-
icon !== undefined && destructive && 'hover:bg-destructive/10 hover:text-destructive',
|
|
151
156
|
className,
|
|
152
157
|
)}
|
|
153
158
|
data-action={action.name}
|
|
@@ -170,28 +175,30 @@ export function ActionTrigger<TInput, TData>({
|
|
|
170
175
|
<>
|
|
171
176
|
{renderButton(() => setOpen(true))}
|
|
172
177
|
|
|
173
|
-
<
|
|
174
|
-
<
|
|
175
|
-
<
|
|
176
|
-
<
|
|
178
|
+
<Dialog mode="alert" open={open} onOpenChange={setOpen}>
|
|
179
|
+
<DialogContent data-action={action.name}>
|
|
180
|
+
<DialogHeader>
|
|
181
|
+
<DialogTitle>{confirm.title}</DialogTitle>
|
|
177
182
|
{(itemLabel !== undefined || confirm.description !== undefined) && (
|
|
178
|
-
<
|
|
183
|
+
<DialogDescription>
|
|
179
184
|
{itemLabel !== undefined && <span className="font-medium text-foreground">“{itemLabel}”</span>}
|
|
180
185
|
{itemLabel !== undefined && confirm.description !== undefined ? ' — ' : ''}
|
|
181
186
|
{confirm.description}
|
|
182
|
-
</
|
|
187
|
+
</DialogDescription>
|
|
183
188
|
)}
|
|
184
|
-
</
|
|
185
|
-
<
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
</DialogHeader>
|
|
190
|
+
<DialogFooter>
|
|
191
|
+
<DialogClose initialFocus asChild>
|
|
192
|
+
<Button context="neutral" variant="ghost" disabled={isLoading}>
|
|
193
|
+
{confirm.cancelLabel ?? 'Cancelar'}
|
|
194
|
+
</Button>
|
|
195
|
+
</DialogClose>
|
|
196
|
+
<Button context={confirmContext} variant={confirmVariant} busy={isLoading} onClick={fire}>
|
|
190
197
|
{confirm.actionLabel ?? buttonLabel}
|
|
191
198
|
</Button>
|
|
192
|
-
</
|
|
193
|
-
</
|
|
194
|
-
</
|
|
199
|
+
</DialogFooter>
|
|
200
|
+
</DialogContent>
|
|
201
|
+
</Dialog>
|
|
195
202
|
</>
|
|
196
203
|
)
|
|
197
204
|
}
|