@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.
Files changed (154) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/bin/lib/check.mjs +1098 -310
  3. package/bin/lib/copy.mjs +12 -5
  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 +180 -0
  7. package/docs/adr/0006-semantic-context-precedes-visual-variant.md +182 -0
  8. package/docs/adr/0007-toast-actions-form-an-ordered-collection.md +63 -0
  9. package/docs/adr/0008-hierarchical-navigation-is-composed-at-the-consumer-boundary.md +71 -0
  10. package/docs/radius-scale.md +1 -1
  11. package/package.json +1 -1
  12. package/registry/instructions/opus.md +5 -0
  13. package/registry/skills/build-opus-ui/SKILL.md +27 -16
  14. package/registry/skills/build-opus-ui/references/evaluations.md +16 -5
  15. package/registry/skills/build-opus-ui/references/ui-patterns.md +38 -15
  16. package/registry/skills/maintain-opus-docs/SKILL.md +83 -0
  17. package/registry/skills/maintain-opus-docs/agents/openai.yaml +4 -0
  18. package/registry/skills/maintain-opus-docs/references/editorial-standard.md +85 -0
  19. package/registry/skills/maintain-opus-docs/references/evaluations.md +34 -0
  20. package/registry/skills/maintain-opus-docs/scripts/audit-docs.mjs +81 -0
  21. package/registry/skills/model-opus-dictionary/SKILL.md +4 -2
  22. package/registry/skills/model-opus-dictionary/references/evaluations.md +4 -3
  23. package/registry/templates/app/src/App.tsx +1 -1
  24. package/src/core/dictionary.ts +52 -14
  25. package/src/core/index.ts +10 -0
  26. package/src/core/ui-context.ts +29 -0
  27. package/src/schema/drivers/zod.ts +17 -8
  28. package/src/ui/components/patterns/action-form-card.tsx +18 -12
  29. package/src/ui/components/patterns/confirm.tsx +163 -40
  30. package/src/ui/components/patterns/content-header.tsx +335 -61
  31. package/src/ui/components/patterns/data-state.tsx +23 -10
  32. package/src/ui/components/patterns/list.tsx +1097 -783
  33. package/src/ui/components/patterns/page-state.tsx +115 -0
  34. package/src/ui/components/patterns/page.tsx +231 -41
  35. package/src/ui/components/patterns/sidebar.tsx +357 -83
  36. package/src/ui/components/patterns/trigger.tsx +37 -30
  37. package/src/ui/components/patterns/view.tsx +7 -11
  38. package/src/ui/components/primitives/alert.tsx +298 -110
  39. package/src/ui/components/primitives/ask.tsx +2 -1
  40. package/src/ui/components/primitives/badge.tsx +91 -30
  41. package/src/ui/components/primitives/button.tsx +99 -60
  42. package/src/ui/components/primitives/calendar.tsx +39 -39
  43. package/src/ui/components/primitives/card.tsx +96 -23
  44. package/src/ui/components/primitives/detail.tsx +2 -2
  45. package/src/ui/components/primitives/dialog.tsx +196 -39
  46. package/src/ui/components/primitives/dictionary-value.tsx +9 -14
  47. package/src/ui/components/primitives/dot.tsx +74 -21
  48. package/src/ui/components/primitives/drawer.tsx +40 -24
  49. package/src/ui/components/primitives/empty.tsx +3 -3
  50. package/src/ui/components/primitives/item.tsx +135 -79
  51. package/src/ui/components/primitives/menu.tsx +11 -3
  52. package/src/ui/components/primitives/metric-card.tsx +133 -0
  53. package/src/ui/components/primitives/sonner.tsx +187 -8
  54. package/src/ui/components/primitives/table.tsx +2 -2
  55. package/src/ui/docs/DocBrowser.tsx +104 -25
  56. package/src/ui/docs/changelog.tsx +1 -1
  57. package/src/ui/docs/content/accordion.md +22 -16
  58. package/src/ui/docs/content/action-form-card.md +8 -8
  59. package/src/ui/docs/content/action-form-dialog.md +9 -9
  60. package/src/ui/docs/content/action-form.md +28 -34
  61. package/src/ui/docs/content/action-list-dialog.md +11 -6
  62. package/src/ui/docs/content/action-list.md +64 -39
  63. package/src/ui/docs/content/action-trigger.md +21 -14
  64. package/src/ui/docs/content/action-view.md +8 -8
  65. package/src/ui/docs/content/actions.md +9 -9
  66. package/src/ui/docs/content/ai.md +3 -3
  67. package/src/ui/docs/content/alert.md +54 -28
  68. package/src/ui/docs/content/aspect-ratio.md +4 -4
  69. package/src/ui/docs/content/audit.md +2 -2
  70. package/src/ui/docs/content/auth.md +3 -3
  71. package/src/ui/docs/content/avatar.md +34 -14
  72. package/src/ui/docs/content/badge.md +21 -22
  73. package/src/ui/docs/content/breadcrumb.md +13 -8
  74. package/src/ui/docs/content/button.md +93 -15
  75. package/src/ui/docs/content/calendar.md +5 -5
  76. package/src/ui/docs/content/card.md +6 -6
  77. package/src/ui/docs/content/carousel.md +16 -11
  78. package/src/ui/docs/content/chat.md +3 -3
  79. package/src/ui/docs/content/checkbox.md +7 -7
  80. package/src/ui/docs/content/cli.md +5 -5
  81. package/src/ui/docs/content/collapsible.md +8 -8
  82. package/src/ui/docs/content/command.md +16 -8
  83. package/src/ui/docs/content/composer.md +2 -2
  84. package/src/ui/docs/content/content.md +44 -0
  85. package/src/ui/docs/content/copyable.md +4 -3
  86. package/src/ui/docs/content/customization.md +7 -7
  87. package/src/ui/docs/content/cycle.md +3 -3
  88. package/src/ui/docs/content/data-state.md +11 -12
  89. package/src/ui/docs/content/data.md +26 -33
  90. package/src/ui/docs/content/detail.md +8 -5
  91. package/src/ui/docs/content/dialog.md +339 -31
  92. package/src/ui/docs/content/dictionary-value.md +19 -18
  93. package/src/ui/docs/content/dock.md +3 -3
  94. package/src/ui/docs/content/dot.md +7 -7
  95. package/src/ui/docs/content/drawer.md +32 -16
  96. package/src/ui/docs/content/empty-value.md +2 -2
  97. package/src/ui/docs/content/empty.md +19 -12
  98. package/src/ui/docs/content/events.md +4 -4
  99. package/src/ui/docs/content/field.md +34 -12
  100. package/src/ui/docs/content/getting-started.md +1 -1
  101. package/src/ui/docs/content/icon-picker.md +8 -4
  102. package/src/ui/docs/content/input-otp.md +20 -12
  103. package/src/ui/docs/content/input.md +121 -9
  104. package/src/ui/docs/content/item.md +64 -24
  105. package/src/ui/docs/content/kbd.md +19 -11
  106. package/src/ui/docs/content/label.md +5 -3
  107. package/src/ui/docs/content/log.md +4 -4
  108. package/src/ui/docs/content/markdown.md +7 -6
  109. package/src/ui/docs/content/mcp.md +13 -15
  110. package/src/ui/docs/content/menu.md +36 -17
  111. package/src/ui/docs/content/metric-card.md +41 -0
  112. package/src/ui/docs/content/observability.md +2 -2
  113. package/src/ui/docs/content/page.md +93 -10
  114. package/src/ui/docs/content/pagination.md +22 -17
  115. package/src/ui/docs/content/popover.md +16 -8
  116. package/src/ui/docs/content/progress.md +7 -5
  117. package/src/ui/docs/content/queue.md +5 -5
  118. package/src/ui/docs/content/radio-group.md +20 -12
  119. package/src/ui/docs/content/router.md +11 -6
  120. package/src/ui/docs/content/scheduler.md +4 -5
  121. package/src/ui/docs/content/scroll-area.md +12 -7
  122. package/src/ui/docs/content/select.md +42 -29
  123. package/src/ui/docs/content/semantic-context.md +63 -0
  124. package/src/ui/docs/content/separator.md +5 -5
  125. package/src/ui/docs/content/sidebar.md +325 -56
  126. package/src/ui/docs/content/skeleton.md +5 -4
  127. package/src/ui/docs/content/slider.md +8 -7
  128. package/src/ui/docs/content/spinner.md +8 -8
  129. package/src/ui/docs/content/split.md +8 -5
  130. package/src/ui/docs/content/storage.md +6 -8
  131. package/src/ui/docs/content/switch.md +8 -7
  132. package/src/ui/docs/content/table.md +16 -6
  133. package/src/ui/docs/content/tabs.md +28 -14
  134. package/src/ui/docs/content/testing.md +9 -11
  135. package/src/ui/docs/content/textarea.md +5 -4
  136. package/src/ui/docs/content/toast.md +47 -13
  137. package/src/ui/docs/content/toggle.md +75 -7
  138. package/src/ui/docs/content/tokens.md +31 -3
  139. package/src/ui/docs/content/tooltip.md +19 -11
  140. package/src/ui/docs/content/truncate.md +7 -8
  141. package/src/ui/docs/content/ui.md +10 -9
  142. package/src/ui/docs/content/upgrading.md +7 -8
  143. package/src/ui/docs/doc-client.tsx +2 -2
  144. package/src/ui/docs/registry.tsx +580 -229
  145. package/src/ui/lib/semantic-context.ts +30 -0
  146. package/src/ui/meta.ts +278 -286
  147. package/src/ui/react.tsx +377 -111
  148. package/src/ui/theme.css +116 -0
  149. package/src/ui/components/primitives/alert-dialog.tsx +0 -190
  150. package/src/ui/docs/content/alert-dialog.md +0 -73
  151. package/src/ui/docs/content/button-group.md +0 -71
  152. package/src/ui/docs/content/confirm.md +0 -120
  153. package/src/ui/docs/content/input-group.md +0 -78
  154. package/src/ui/docs/content/toggle-group.md +0 -81
@@ -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[];
116
331
  }
117
332
 
118
- export type SidebarNavItem = Omit<SidebarItemProps, 'active' | 'onClick'> & { id: string }
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
- export interface SidebarNavSubgroup {
121
- label?: string
122
- items: SidebarNavItem[]
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
- const titled = (label?: string): boolean => (label ?? '').trim() !== ''
126
- const subFilled = (subgroup: SidebarNavSubgroup): boolean => subgroup.items.length > 0
127
- const groupFilled = (group: SidebarNavGroup): boolean =>
128
- (group.items?.length ?? 0) > 0 || (group.subgroups?.some(subFilled) ?? false)
361
+ export interface SidebarTreeGroupProps {
362
+ className?: string;
363
+ children: ReactNode;
364
+ }
129
365
 
130
- /** 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>
366
+ /** Agrupa descendentes de um 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({ 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 => (
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 && 'pl-6', item.className)}
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 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>)}
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 ? <TooltipProvider delayDuration={200}>{body}</TooltipProvider> : body
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 <AlertDialog> (compacto, não fecha no clique fora) antes de disparar.
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
- AlertDialog,
23
- AlertDialogContent,
24
- AlertDialogDescription,
25
- AlertDialogFooter,
26
- AlertDialogHeader,
27
- AlertDialogTitle,
28
- } from '../primitives/alert-dialog.tsx'
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
- /** Variante visual do Button. Default: 'default' (ou 'destructive' se o
47
- * action.confirm declarar destructive). */
48
- variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
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 triggerVariant = variant ?? (icon !== undefined ? 'ghost' : destructive ? 'destructive' : 'default')
127
- const confirmVariant = destructive ? 'destructive' : (variant ?? 'default')
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
- <AlertDialog open={open} onOpenChange={setOpen}>
174
- <AlertDialogContent data-action={action.name}>
175
- <AlertDialogHeader>
176
- <AlertDialogTitle>{confirm.title}</AlertDialogTitle>
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
- <AlertDialogDescription>
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
- </AlertDialogDescription>
187
+ </DialogDescription>
183
188
  )}
184
- </AlertDialogHeader>
185
- <AlertDialogFooter>
186
- <Button variant="ghost" onClick={() => setOpen(false)} disabled={isLoading}>
187
- {confirm.cancelLabel ?? 'Cancelar'}
188
- </Button>
189
- <Button variant={confirmVariant} busy={isLoading} onClick={fire}>
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
- </AlertDialogFooter>
193
- </AlertDialogContent>
194
- </AlertDialog>
199
+ </DialogFooter>
200
+ </DialogContent>
201
+ </Dialog>
195
202
  </>
196
203
  )
197
204
  }