@softize/opus 12.9.0 → 12.11.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/bin/lib/check.mjs +1103 -310
  3. package/bin/lib/copy.mjs +74 -6
  4. package/docs/adr/0003-dictionary-presentation-is-declared.md +3 -0
  5. package/docs/adr/0004-page-content-state-is-composed.md +65 -0
  6. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +97 -0
  7. package/docs/adr/0006-semantic-context-precedes-visual-variant.md +182 -0
  8. package/docs/code-style.md +6 -2
  9. package/package.json +1 -1
  10. package/registry/instructions/opus.md +5 -0
  11. package/registry/skills/build-opus-ui/SKILL.md +27 -16
  12. package/registry/skills/build-opus-ui/references/evaluations.md +16 -5
  13. package/registry/skills/build-opus-ui/references/ui-patterns.md +38 -15
  14. package/registry/skills/model-opus-dictionary/SKILL.md +4 -2
  15. package/registry/skills/model-opus-dictionary/references/evaluations.md +4 -3
  16. package/registry/templates/app/src/App.tsx +1 -1
  17. package/src/core/dictionary.ts +52 -14
  18. package/src/core/index.ts +10 -0
  19. package/src/core/ui-context.ts +29 -0
  20. package/src/schema/drivers/zod.ts +34 -19
  21. package/src/ui/components/patterns/action-form-card.tsx +18 -12
  22. package/src/ui/components/patterns/confirm.tsx +26 -3
  23. package/src/ui/components/patterns/content-header.tsx +335 -61
  24. package/src/ui/components/patterns/data-state.tsx +23 -10
  25. package/src/ui/components/patterns/form.tsx +1 -1
  26. package/src/ui/components/patterns/list.tsx +1096 -777
  27. package/src/ui/components/patterns/page-state.tsx +115 -0
  28. package/src/ui/components/patterns/page.tsx +231 -41
  29. package/src/ui/components/patterns/sidebar.tsx +354 -80
  30. package/src/ui/components/patterns/trigger.tsx +13 -9
  31. package/src/ui/components/patterns/view.tsx +7 -11
  32. package/src/ui/components/primitives/alert-dialog.tsx +7 -5
  33. package/src/ui/components/primitives/alert.tsx +298 -80
  34. package/src/ui/components/primitives/ask.tsx +2 -1
  35. package/src/ui/components/primitives/badge.tsx +91 -30
  36. package/src/ui/components/primitives/button.tsx +99 -60
  37. package/src/ui/components/primitives/calendar.tsx +39 -39
  38. package/src/ui/components/primitives/card.tsx +96 -23
  39. package/src/ui/components/primitives/detail.tsx +2 -2
  40. package/src/ui/components/primitives/dictionary-value.tsx +9 -14
  41. package/src/ui/components/primitives/dot.tsx +74 -21
  42. package/src/ui/components/primitives/drawer.tsx +33 -20
  43. package/src/ui/components/primitives/field.tsx +4 -4
  44. package/src/ui/components/primitives/item.tsx +137 -81
  45. package/src/ui/components/primitives/menu.tsx +11 -3
  46. package/src/ui/components/primitives/metric-card.tsx +133 -0
  47. package/src/ui/components/primitives/table.tsx +2 -2
  48. package/src/ui/components/primitives/tooltip.tsx +1 -1
  49. package/src/ui/docs/DocBrowser.tsx +3 -3
  50. package/src/ui/docs/changelog.tsx +1 -1
  51. package/src/ui/docs/content/action-form-card.md +1 -1
  52. package/src/ui/docs/content/alert-dialog.md +8 -8
  53. package/src/ui/docs/content/alert.md +54 -23
  54. package/src/ui/docs/content/badge.md +18 -19
  55. package/src/ui/docs/content/button.md +12 -9
  56. package/src/ui/docs/content/card.md +5 -5
  57. package/src/ui/docs/content/content.md +44 -0
  58. package/src/ui/docs/content/customization.md +2 -2
  59. package/src/ui/docs/content/detail.md +5 -2
  60. package/src/ui/docs/content/dialog.md +2 -2
  61. package/src/ui/docs/content/dictionary-value.md +11 -10
  62. package/src/ui/docs/content/dot.md +7 -7
  63. package/src/ui/docs/content/drawer.md +6 -3
  64. package/src/ui/docs/content/field.md +1 -1
  65. package/src/ui/docs/content/input-group.md +3 -2
  66. package/src/ui/docs/content/item.md +47 -21
  67. package/src/ui/docs/content/menu.md +5 -4
  68. package/src/ui/docs/content/metric-card.md +41 -0
  69. package/src/ui/docs/content/page-state.md +45 -0
  70. package/src/ui/docs/content/page.md +48 -10
  71. package/src/ui/docs/content/semantic-context.md +63 -0
  72. package/src/ui/docs/content/sidebar.md +4 -4
  73. package/src/ui/docs/content/skeleton.md +2 -2
  74. package/src/ui/docs/content/table.md +3 -3
  75. package/src/ui/docs/content/tokens.md +28 -0
  76. package/src/ui/docs/content/tooltip.md +15 -1
  77. package/src/ui/docs/doc-client.tsx +2 -2
  78. package/src/ui/docs/registry.tsx +596 -228
  79. package/src/ui/lib/semantic-context.ts +30 -0
  80. package/src/ui/meta.ts +292 -270
  81. package/src/ui/react.tsx +378 -111
  82. package/src/ui/theme.css +66 -0
@@ -1,160 +1,434 @@
1
- import { createContext, useContext, type HTMLAttributes, type ReactNode } from 'react'
2
- import { ChevronRight } from 'lucide-react'
3
- import { useOverflowing } from '../../lib/overflow.ts'
4
- import { cn } from '../../lib/cn.ts'
5
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../primitives/tooltip.tsx'
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 { collapsed: boolean }
8
- const SidebarContext = createContext<SidebarContextValue | null>(null)
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({ collapsed = false, divider = true, className, children }: SidebarProps): React.ReactElement {
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 data-slot="sidebar" data-collapsed={String(collapsed)} className={cn('group/sidebar flex h-full min-h-0 shrink-0 flex-col transition-[width] duration-200 ease-out', divider && 'border-r border-border', collapsed ? 'w-14' : 'w-64', className)}>
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({ className, children }: { className?: string; children: ReactNode }): React.ReactElement {
36
- return <div data-slot="pane-header" className={cn('shrink-0 border-b border-border', className)}>{children}</div>
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 PaneContent({ className, children }: { className?: string; children: ReactNode }): React.ReactElement {
41
- return <div data-slot="pane-content" className={cn('min-h-0 flex-1 overflow-y-auto', className)}>{children}</div>
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({ className, children }: { className?: string; children: ReactNode }): React.ReactElement {
46
- return <div data-slot="pane-footer" className={cn('shrink-0 border-t border-border p-2', className)}>{children}</div>
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?: 'before' | 'inside' | 'after'
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({ label, icon, badge, active = false, disabled = false, actions, onToggle, expanded = false, dropPosition, dragProps, tooltipHint, className, onClick }: SidebarItemProps): React.ReactElement {
72
- const collapsed = useSidebarCollapsed()
73
- const hasActions = actions !== undefined && actions !== null
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 } = useOverflowing<HTMLSpanElement>(label)
173
+ const { ref: labelRef, overflowing: labelCut } =
174
+ useOverflowing<HTMLSpanElement>(label);
77
175
  if (collapsed) {
78
176
  const button = (
79
- <button type="button" disabled={disabled} aria-current={active ? 'page' : undefined} onClick={onClick} className={cn('mx-auto grid size-9 place-items-center rounded-md transition-colors disabled:pointer-events-none disabled:opacity-40', 'outline-none focus-visible:ring-2 focus-visible:ring-ring/50', active ? 'bg-muted font-medium text-foreground' : 'text-foreground/80 hover:bg-muted/60')}>
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 <div data-slot="sidebar-item" className={className} {...dragProps}><Tooltip><TooltipTrigger asChild>{button}</TooltipTrigger><TooltipContent side="right">{label}{tooltipHint}</TooltipContent></Tooltip></div>
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 data-slot="sidebar-item" className={cn('group/sidebar-item relative flex items-center rounded-md transition-colors', disabled ? 'text-muted-foreground/50 opacity-40' : 'hover:bg-muted/60', active && 'bg-muted', dropPosition === 'inside' && 'bg-primary/5 ring-2 ring-inset ring-primary/60', className)} {...dragProps}>
90
- {(dropPosition === 'before' || dropPosition === 'after') && <span data-slot="sidebar-item-drop-indicator" className={cn('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', dropPosition === 'before' ? '-top-px' : '-bottom-px')} />}
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 data-slot="sidebar-item-leading" className="pointer-events-none absolute left-2.5 top-1/2 z-10 grid size-4 -translate-y-1/2 place-items-center">
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 && <span className={cn('absolute inset-0 flex items-center justify-center transition-opacity', onToggle !== undefined && 'group-hover/sidebar-item:opacity-0 group-has-[[data-slot=sidebar-item-toggle]:focus-visible]/sidebar-item:opacity-0')}>{icon}</span>}
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 type="button" data-slot="sidebar-item-toggle" onClick={onToggle} aria-label={expanded ? `Fechar ${label}` : `Abrir ${label}`} aria-expanded={expanded} className={cn('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', icon === undefined ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0 group-hover/sidebar-item:pointer-events-auto group-hover/sidebar-item:opacity-100')}>
97
- <ChevronRight className={cn('size-3 transition-transform', expanded && 'rotate-90')} />
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 type="button" disabled={disabled} aria-current={active ? 'page' : undefined} onClick={onClick} className={cn('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', 'outline-none focus-visible:ring-2 focus-visible:ring-ring/50', disabled ? 'text-muted-foreground/50' : active ? 'font-medium text-foreground' : 'text-foreground/80')}>
103
- {(icon !== undefined || onToggle !== undefined) && <span data-slot="sidebar-item-leading-space" aria-hidden className="size-4 shrink-0" />}
104
- <span ref={labelRef} data-slot="sidebar-item-label" className={cn('min-w-0 flex-1 overflow-hidden whitespace-nowrap group-has-[[data-slot=dot]]/sidebar-item:mr-[1.1875rem]', labelCut && 'truncate-fade')}>{label}</span>
105
- {badge !== undefined && <span data-slot="sidebar-item-badge" className={cn('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', hasActions && 'group-hover/sidebar-item:has-[[data-slot=dot]]:opacity-0')}>{badge}</span>}
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 && <div data-slot="sidebar-item-actions" 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">{actions}</div>}
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?: SidebarNavItem[]
115
- subgroups?: SidebarNavSubgroup[]
329
+ label?: string;
330
+ items?: SidebarNavItem[];
331
+ subgroups?: SidebarNavSubgroup[];
116
332
  }
117
333
 
118
- export type SidebarNavItem = Omit<SidebarItemProps, 'active' | 'onClick'> & { id: string }
334
+ export type SidebarNavItem = Omit<SidebarItemProps, "active" | "onClick"> & {
335
+ id: string;
336
+ };
119
337
 
120
338
  export interface SidebarNavSubgroup {
121
- label?: string
122
- items: SidebarNavItem[]
339
+ label?: string;
340
+ items: SidebarNavItem[];
123
341
  }
124
342
 
125
- const titled = (label?: string): boolean => (label ?? '').trim() !== ''
126
- const subFilled = (subgroup: SidebarNavSubgroup): boolean => subgroup.items.length > 0
343
+ const titled = (label?: string): boolean => (label ?? "").trim() !== "";
344
+ const subFilled = (subgroup: SidebarNavSubgroup): boolean =>
345
+ subgroup.items.length > 0;
127
346
  const groupFilled = (group: SidebarNavGroup): boolean =>
128
- (group.items?.length ?? 0) > 0 || (group.subgroups?.some(subFilled) ?? false)
347
+ (group.items?.length ?? 0) > 0 || (group.subgroups?.some(subFilled) ?? false);
129
348
 
130
349
  /** Rótulo discreto de um grupo de navegação, compartilhado por listas e árvores. */
131
- export function SidebarGroupLabel({ className, children }: { className?: string; children: ReactNode }): React.ReactElement {
132
- return <div data-slot="sidebar-group-label" className={cn('px-2.5 pb-1 text-sm font-medium text-muted-foreground/70', className)}>{children}</div>
350
+ export function SidebarGroupLabel({
351
+ className,
352
+ children,
353
+ }: {
354
+ className?: string;
355
+ children: ReactNode;
356
+ }): React.ReactElement {
357
+ return (
358
+ <div
359
+ data-slot="sidebar-group-label"
360
+ className={cn(
361
+ "px-2.5 pb-1 text-sm font-medium text-muted-foreground/70",
362
+ className,
363
+ )}
364
+ >
365
+ {children}
366
+ </div>
367
+ );
133
368
  }
134
369
 
135
370
  /** Navegação controlada para Sidebar. Header/footer continuam slots explícitos do pai. */
136
- export function SidebarNav({ groups, activeId, onSelect, navLabel = 'Navegação', className }: { groups: SidebarNavGroup[]; activeId?: string; onSelect: (id: string) => void; navLabel?: string; className?: string }): React.ReactElement {
137
- const collapsed = useSidebarCollapsed()
138
- const renderItem = (item: SidebarNavItem, nested = false): React.ReactElement => (
371
+ export function SidebarNav({
372
+ groups,
373
+ activeId,
374
+ onSelect,
375
+ navLabel = "Navegação",
376
+ className,
377
+ }: {
378
+ groups: SidebarNavGroup[];
379
+ activeId?: string;
380
+ onSelect: (id: string) => void;
381
+ navLabel?: string;
382
+ className?: string;
383
+ }): React.ReactElement {
384
+ const collapsed = useSidebarCollapsed();
385
+ const renderItem = (
386
+ item: SidebarNavItem,
387
+ nested = false,
388
+ ): React.ReactElement => (
139
389
  <SidebarItem
140
390
  key={item.id}
141
391
  {...item}
142
- className={cn(nested && !collapsed && 'pl-6', item.className)}
392
+ className={cn(nested && !collapsed && "pl-6", item.className)}
143
393
  active={item.id === activeId}
144
394
  onClick={() => onSelect(item.id)}
145
395
  />
146
- )
396
+ );
147
397
  const body = (
148
- <nav aria-label={navLabel} data-slot="sidebar-nav" className={cn('space-y-4 p-2', className)}>
149
- {groups.map((group, gi) => !groupFilled(group) ? null : <div key={gi} className="space-y-0.5">
150
- {!collapsed && titled(group.label) && <SidebarGroupLabel>{group.label}</SidebarGroupLabel>}
151
- {group.items?.map((item) => renderItem(item))}
152
- {group.subgroups?.map((subgroup, si) => !subFilled(subgroup) ? null : <div key={si} className="space-y-0.5">
153
- {!collapsed && titled(subgroup.label) && <div className="pb-0.5 pl-4 pr-2.5 pt-1.5 text-[0.6875rem] font-medium text-muted-foreground/50">{subgroup.label}</div>}
154
- {subgroup.items.map((item) => renderItem(item, titled(subgroup.label)))}
155
- </div>)}
156
- </div>)}
398
+ <nav
399
+ aria-label={navLabel}
400
+ data-slot="sidebar-nav"
401
+ className={cn("space-y-4 p-2", className)}
402
+ >
403
+ {groups.map((group, gi) =>
404
+ !groupFilled(group) ? null : (
405
+ <div key={gi} className="space-y-0.5">
406
+ {!collapsed && titled(group.label) && (
407
+ <SidebarGroupLabel>{group.label}</SidebarGroupLabel>
408
+ )}
409
+ {group.items?.map((item) => renderItem(item))}
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
+ )}
424
+ </div>
425
+ ),
426
+ )}
157
427
  </nav>
158
- )
159
- return collapsed ? <TooltipProvider delayDuration={200}>{body}</TooltipProvider> : body
428
+ );
429
+ return collapsed ? (
430
+ <TooltipProvider delayDuration={200}>{body}</TooltipProvider>
431
+ ) : (
432
+ body
433
+ );
160
434
  }
@@ -16,7 +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 { 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
22
  AlertDialog,
@@ -43,9 +43,10 @@ export interface ActionTriggerProps<TInput, TData> {
43
43
  input: TInput
44
44
  /** Texto do botão. Default usa action.label. */
45
45
  label?: string
46
- /** Variante visual do Button. Default: 'default' (ou 'destructive' se o
47
- * action.confirm declarar destructive). */
48
- variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
46
+ /** Contexto visual. Por padrão, ações destrutivas usam `danger`. */
47
+ context?: ButtonProps['context']
48
+ /** Tratamento visual do Button. */
49
+ variant?: ButtonProps['variant']
49
50
  /** Tamanho do botão. */
50
51
  size?: 'default' | 'sm' | 'lg' | 'icon'
51
52
  /** Desabilita o botão independente de loading. */
@@ -83,6 +84,7 @@ export function ActionTrigger<TInput, TData>({
83
84
  action,
84
85
  input,
85
86
  label,
87
+ context,
86
88
  variant,
87
89
  size = 'default',
88
90
  disabled = false,
@@ -123,8 +125,10 @@ export function ActionTrigger<TInput, TData>({
123
125
  const destructive = spec?.destructive === true
124
126
  // Com ícone o botão é chrome do item: fica ghost, e o vermelho aparece no hover — uma
125
127
  // lixeira sólida vermelha em cada linha seria um campo minado visual.
126
- const triggerVariant = variant ?? (icon !== undefined ? 'ghost' : destructive ? 'destructive' : 'default')
127
- const confirmVariant = destructive ? 'destructive' : (variant ?? 'default')
128
+ const triggerContext = context ?? (destructive ? 'danger' : icon !== undefined ? 'neutral' : 'primary')
129
+ const triggerVariant = variant ?? (icon !== undefined ? 'ghost' : 'solid')
130
+ const confirmContext = destructive ? 'danger' : (context ?? 'primary')
131
+ const confirmVariant = variant ?? 'solid'
128
132
 
129
133
  const fire = () => {
130
134
  void trigger(input)
@@ -136,6 +140,7 @@ export function ActionTrigger<TInput, TData>({
136
140
  const renderButton = (onClick: () => void): React.ReactElement => {
137
141
  const button = (
138
142
  <Button
143
+ context={triggerContext}
139
144
  variant={triggerVariant}
140
145
  size={icon !== undefined ? 'icon' : size}
141
146
  busy={isLoading}
@@ -147,7 +152,6 @@ export function ActionTrigger<TInput, TData>({
147
152
  }}
148
153
  className={cn(
149
154
  icon !== undefined && 'size-7 shrink-0 text-muted-foreground/60',
150
- icon !== undefined && destructive && 'hover:bg-destructive/10 hover:text-destructive',
151
155
  className,
152
156
  )}
153
157
  data-action={action.name}
@@ -183,10 +187,10 @@ export function ActionTrigger<TInput, TData>({
183
187
  )}
184
188
  </AlertDialogHeader>
185
189
  <AlertDialogFooter>
186
- <Button variant="ghost" onClick={() => setOpen(false)} disabled={isLoading}>
190
+ <Button context="neutral" variant="ghost" onClick={() => setOpen(false)} disabled={isLoading}>
187
191
  {confirm.cancelLabel ?? 'Cancelar'}
188
192
  </Button>
189
- <Button variant={confirmVariant} busy={isLoading} onClick={fire}>
193
+ <Button context={confirmContext} variant={confirmVariant} busy={isLoading} onClick={fire}>
190
194
  {confirm.actionLabel ?? buttonLabel}
191
195
  </Button>
192
196
  </AlertDialogFooter>
@@ -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 { Alert } from '../primitives/alert.tsx'
12
13
  import { Skeleton } from '../primitives/skeleton.tsx'
13
14
  import { Button } from '../primitives/button.tsx'
14
15
 
@@ -61,17 +62,12 @@ export function ActionView<TInput, TData>({
61
62
  return <div data-action={action.name}>{error(err, refetch)}</div>
62
63
  }
63
64
  return (
64
- <div
65
- data-action={action.name}
66
- className="rounded-md border border-destructive/50 bg-destructive/10 p-4 text-sm space-y-3"
67
- >
68
- <div>
69
- <strong className="text-destructive">{err.code}</strong>
70
- <p className="text-destructive">{err.message}</p>
71
- </div>
72
- <Button variant="outline" size="sm" onClick={() => void refetch()}>
73
- Tentar de novo
74
- </Button>
65
+ <div data-action={action.name}>
66
+ <Alert context="danger" title={err.code} description={err.message}>
67
+ <Button variant="outline" size="sm" onClick={() => void refetch()}>
68
+ Tentar de novo
69
+ </Button>
70
+ </Alert>
75
71
  </div>
76
72
  )
77
73
  }
@@ -140,13 +140,14 @@ function AlertDialogMedia({
140
140
 
141
141
  function AlertDialogAction({
142
142
  className,
143
- variant = "default",
143
+ context = "primary",
144
+ variant = "solid",
144
145
  size = "default",
145
146
  ...props
146
147
  }: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
147
- Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
148
+ Pick<React.ComponentProps<typeof Button>, "context" | "variant" | "size">) {
148
149
  return (
149
- <Button variant={variant} size={size} asChild>
150
+ <Button context={context} variant={variant} size={size} asChild>
150
151
  <AlertDialogPrimitive.Action
151
152
  data-slot="alert-dialog-action"
152
153
  className={cn(className)}
@@ -158,13 +159,14 @@ function AlertDialogAction({
158
159
 
159
160
  function AlertDialogCancel({
160
161
  className,
162
+ context = "neutral",
161
163
  variant = "ghost",
162
164
  size = "default",
163
165
  ...props
164
166
  }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &
165
- Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
167
+ Pick<React.ComponentProps<typeof Button>, "context" | "variant" | "size">) {
166
168
  return (
167
- <Button variant={variant} size={size} asChild>
169
+ <Button context={context} variant={variant} size={size} asChild>
168
170
  <AlertDialogPrimitive.Cancel
169
171
  data-slot="alert-dialog-cancel"
170
172
  className={cn(className)}