@softize/opus 18.5.0 → 20.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 (151) hide show
  1. package/CHANGELOG.md +275 -0
  2. package/bin/cli.mjs +5 -0
  3. package/bin/lib/check.mjs +198 -10
  4. package/bin/lib/copy.mjs +29 -0
  5. package/bin/lib/mcp.mjs +1 -1
  6. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +298 -0
  7. package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
  8. package/docs/data-products.md +15 -9
  9. package/docs/protocol.md +3 -4
  10. package/docs/releasing.md +2 -2
  11. package/package.json +2 -2
  12. package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
  13. package/registry/templates/app/package.json +1 -1
  14. package/src/core/runtime.ts +96 -8
  15. package/src/core/types.ts +5 -2
  16. package/src/ui/components/patterns/action-form-card.tsx +11 -1
  17. package/src/ui/components/patterns/content-header.tsx +16 -12
  18. package/src/ui/components/patterns/fill-region.ts +150 -0
  19. package/src/ui/components/patterns/heading-level.ts +16 -0
  20. package/src/ui/components/patterns/list.tsx +103 -73
  21. package/src/ui/components/patterns/page.tsx +20 -4
  22. package/src/ui/components/patterns/pane.tsx +294 -0
  23. package/src/ui/components/patterns/presentation.tsx +81 -54
  24. package/src/ui/components/patterns/sidebar.tsx +13 -72
  25. package/src/ui/components/patterns/split.tsx +51 -38
  26. package/src/ui/components/patterns/surface-anatomy.ts +246 -0
  27. package/src/ui/components/patterns/surface-header.tsx +51 -6
  28. package/src/ui/components/patterns/surface-media.tsx +69 -0
  29. package/src/ui/components/patterns/trigger.tsx +12 -2
  30. package/src/ui/components/primitives/alert.tsx +9 -1
  31. package/src/ui/components/primitives/avatar.tsx +7 -3
  32. package/src/ui/components/primitives/card.tsx +91 -29
  33. package/src/ui/components/primitives/carousel.tsx +34 -5
  34. package/src/ui/components/primitives/checkbox.tsx +11 -3
  35. package/src/ui/components/primitives/command.tsx +14 -5
  36. package/src/ui/components/primitives/copyable.tsx +120 -10
  37. package/src/ui/components/primitives/detail.tsx +10 -13
  38. package/src/ui/components/primitives/dialog.tsx +179 -45
  39. package/src/ui/components/primitives/drawer.tsx +131 -24
  40. package/src/ui/components/primitives/empty.tsx +1 -1
  41. package/src/ui/components/primitives/icon-picker.tsx +6 -2
  42. package/src/ui/components/primitives/icon-tile.tsx +7 -0
  43. package/src/ui/components/primitives/input-group.tsx +9 -2
  44. package/src/ui/components/primitives/input-otp.tsx +1 -1
  45. package/src/ui/components/primitives/kbd.tsx +1 -1
  46. package/src/ui/components/primitives/label.tsx +3 -0
  47. package/src/ui/components/primitives/menu.tsx +5 -0
  48. package/src/ui/components/primitives/metric-card.tsx +98 -39
  49. package/src/ui/components/primitives/popover.tsx +1 -1
  50. package/src/ui/components/primitives/progress.tsx +9 -1
  51. package/src/ui/components/primitives/slider.tsx +62 -11
  52. package/src/ui/components/primitives/truncate.tsx +10 -1
  53. package/src/ui/docs/content/accordion.md +74 -27
  54. package/src/ui/docs/content/action-form-card.md +24 -15
  55. package/src/ui/docs/content/action-form-dialog.md +17 -7
  56. package/src/ui/docs/content/action-form.md +92 -77
  57. package/src/ui/docs/content/action-list-dialog.md +75 -38
  58. package/src/ui/docs/content/action-list.md +134 -52
  59. package/src/ui/docs/content/action-trigger.md +27 -10
  60. package/src/ui/docs/content/action-view.md +14 -14
  61. package/src/ui/docs/content/actions.md +161 -66
  62. package/src/ui/docs/content/ai.md +43 -44
  63. package/src/ui/docs/content/alert.md +83 -29
  64. package/src/ui/docs/content/ask.md +51 -26
  65. package/src/ui/docs/content/aspect-ratio.md +22 -24
  66. package/src/ui/docs/content/audit.md +23 -19
  67. package/src/ui/docs/content/auth.md +38 -19
  68. package/src/ui/docs/content/avatar.md +54 -5
  69. package/src/ui/docs/content/badge.md +29 -9
  70. package/src/ui/docs/content/breadcrumb.md +21 -8
  71. package/src/ui/docs/content/button.md +73 -43
  72. package/src/ui/docs/content/calendar.md +134 -26
  73. package/src/ui/docs/content/card.md +252 -35
  74. package/src/ui/docs/content/carousel.md +37 -6
  75. package/src/ui/docs/content/chat.md +118 -37
  76. package/src/ui/docs/content/checkbox.md +73 -7
  77. package/src/ui/docs/content/cli.md +94 -55
  78. package/src/ui/docs/content/collapsible.md +43 -19
  79. package/src/ui/docs/content/command.md +30 -8
  80. package/src/ui/docs/content/composer.md +23 -8
  81. package/src/ui/docs/content/content.md +37 -7
  82. package/src/ui/docs/content/copyable.md +39 -8
  83. package/src/ui/docs/content/customization.md +55 -46
  84. package/src/ui/docs/content/cycle.md +33 -18
  85. package/src/ui/docs/content/data-products.md +115 -0
  86. package/src/ui/docs/content/data-state.md +20 -6
  87. package/src/ui/docs/content/data.md +20 -19
  88. package/src/ui/docs/content/detail.md +12 -5
  89. package/src/ui/docs/content/dialog.md +219 -28
  90. package/src/ui/docs/content/dictionary-value.md +4 -4
  91. package/src/ui/docs/content/dock.md +54 -19
  92. package/src/ui/docs/content/dot.md +35 -5
  93. package/src/ui/docs/content/drawer.md +193 -22
  94. package/src/ui/docs/content/empty-value.md +20 -6
  95. package/src/ui/docs/content/empty.md +24 -3
  96. package/src/ui/docs/content/events.md +76 -16
  97. package/src/ui/docs/content/executions.md +9 -9
  98. package/src/ui/docs/content/field.md +21 -6
  99. package/src/ui/docs/content/getting-started.md +44 -34
  100. package/src/ui/docs/content/icon-picker.md +19 -13
  101. package/src/ui/docs/content/icon-tile.md +28 -5
  102. package/src/ui/docs/content/input-otp.md +58 -7
  103. package/src/ui/docs/content/input.md +22 -3
  104. package/src/ui/docs/content/item.md +53 -6
  105. package/src/ui/docs/content/kbd.md +5 -1
  106. package/src/ui/docs/content/label.md +13 -4
  107. package/src/ui/docs/content/log.md +5 -5
  108. package/src/ui/docs/content/markdown.md +5 -4
  109. package/src/ui/docs/content/mcp.md +49 -23
  110. package/src/ui/docs/content/menu.md +95 -10
  111. package/src/ui/docs/content/metric-card.md +68 -21
  112. package/src/ui/docs/content/observability.md +10 -9
  113. package/src/ui/docs/content/page.md +125 -11
  114. package/src/ui/docs/content/pagination.md +13 -5
  115. package/src/ui/docs/content/pane.md +300 -0
  116. package/src/ui/docs/content/popover.md +42 -5
  117. package/src/ui/docs/content/presentation.md +91 -47
  118. package/src/ui/docs/content/progress.md +27 -14
  119. package/src/ui/docs/content/queue.md +39 -21
  120. package/src/ui/docs/content/radio-group.md +26 -4
  121. package/src/ui/docs/content/router.md +29 -10
  122. package/src/ui/docs/content/runtime.md +90 -54
  123. package/src/ui/docs/content/scheduler.md +53 -21
  124. package/src/ui/docs/content/scroll-area.md +42 -35
  125. package/src/ui/docs/content/select.md +78 -33
  126. package/src/ui/docs/content/semantic-context.md +21 -2
  127. package/src/ui/docs/content/separator.md +15 -9
  128. package/src/ui/docs/content/sidebar.md +25 -45
  129. package/src/ui/docs/content/skeleton.md +13 -4
  130. package/src/ui/docs/content/slider.md +37 -9
  131. package/src/ui/docs/content/spinner.md +4 -3
  132. package/src/ui/docs/content/split.md +34 -20
  133. package/src/ui/docs/content/storage.md +8 -9
  134. package/src/ui/docs/content/switch.md +10 -7
  135. package/src/ui/docs/content/table.md +46 -8
  136. package/src/ui/docs/content/tabs.md +14 -4
  137. package/src/ui/docs/content/testing.md +76 -31
  138. package/src/ui/docs/content/textarea.md +5 -4
  139. package/src/ui/docs/content/toast.md +48 -7
  140. package/src/ui/docs/content/toggle.md +3 -3
  141. package/src/ui/docs/content/tokens.md +15 -26
  142. package/src/ui/docs/content/tooltip.md +18 -2
  143. package/src/ui/docs/content/truncate.md +36 -6
  144. package/src/ui/docs/content/ui.md +74 -22
  145. package/src/ui/docs/content/upgrading.md +45 -36
  146. package/src/ui/docs/plugin.ts +36 -2
  147. package/src/ui/docs/registry.tsx +14 -2
  148. package/src/ui/drivers/react.tsx +16 -8
  149. package/src/ui/meta.ts +38 -32
  150. package/src/ui/react.tsx +14 -0
  151. package/src/vite/design.ts +16 -4
@@ -2,11 +2,26 @@ import * as React from "react";
2
2
  import { Dialog as DrawerPrimitive } from "radix-ui";
3
3
 
4
4
  import { cn } from "../../lib/cn.ts";
5
+ import {
6
+ SurfaceLayoutProvider,
7
+ surfaceActionsClasses,
8
+ surfaceHeaderRowClasses,
9
+ surfaceRegionClasses,
10
+ surfaceTitleClasses,
11
+ useSurfaceLayout,
12
+ type SurfaceLayout,
13
+ } from "../patterns/surface-anatomy.ts";
14
+ import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
15
+ import { SurfaceHeader } from "../patterns/surface-header.tsx";
16
+ import {
17
+ SurfaceMedia,
18
+ type SurfaceMediaProps,
19
+ } from "../patterns/surface-media.tsx";
5
20
  import {
6
21
  CloseButton,
7
22
  type CloseButtonSize,
8
23
  } from "./close-button.tsx";
9
- import { overlayClasses } from "./dialog.tsx";
24
+ import { overlayClasses, preventDismissWhen } from "./dialog.tsx";
10
25
 
11
26
  interface DrawerContentContextValue {
12
27
  showCloseButton: boolean;
@@ -78,19 +93,29 @@ function DrawerContent({
78
93
  showCloseButton = true,
79
94
  closeDisabled = false,
80
95
  closeSize = "sm",
96
+ layout = "sectioned",
81
97
  "aria-describedby": ariaDescribedBy,
98
+ onEscapeKeyDown,
99
+ onInteractOutside,
82
100
  ...props
83
101
  }: React.ComponentProps<typeof DrawerPrimitive.Content> & {
84
102
  side?: "top" | "right" | "bottom" | "left";
85
103
  showCloseButton?: boolean;
104
+ /**
105
+ * Bloqueia o fechamento enquanto a superfície está ocupada: desabilita a ação “Fechar”, que
106
+ * continua visível, e cancela Esc e clique ou foco fora depois dos handlers do consumidor.
107
+ */
86
108
  closeDisabled?: boolean;
87
109
  closeSize?: CloseButtonSize;
110
+ /** Anatomia da superfície: `sectioned` (default) ou `continuous` — ADR 0023. */
111
+ layout?: SurfaceLayout;
88
112
  }) {
89
113
  return (
90
114
  <DrawerPortal>
91
115
  <DrawerOverlay />
92
116
  <DrawerPrimitive.Content
93
117
  data-slot="drawer-content"
118
+ data-layout={layout}
94
119
  className={cn(
95
120
  "fixed z-50 flex flex-col bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
96
121
  side === "right" &&
@@ -104,13 +129,17 @@ function DrawerContent({
104
129
  className,
105
130
  )}
106
131
  aria-describedby={ariaDescribedBy}
132
+ onEscapeKeyDown={preventDismissWhen(closeDisabled, onEscapeKeyDown)}
133
+ onInteractOutside={preventDismissWhen(closeDisabled, onInteractOutside)}
107
134
  {...props}
108
135
  >
109
- <DrawerContentContext.Provider
110
- value={{ showCloseButton, closeDisabled, closeSize }}
111
- >
112
- {children}
113
- </DrawerContentContext.Provider>
136
+ <SurfaceLayoutProvider value={layout}>
137
+ <DrawerContentContext.Provider
138
+ value={{ showCloseButton, closeDisabled, closeSize }}
139
+ >
140
+ {children}
141
+ </DrawerContentContext.Provider>
142
+ </SurfaceLayoutProvider>
114
143
  </DrawerPrimitive.Content>
115
144
  </DrawerPortal>
116
145
  );
@@ -121,27 +150,76 @@ function DrawerHeader({
121
150
  children,
122
151
  ...props
123
152
  }: React.ComponentProps<"div">) {
153
+ const layout = useSurfaceLayout("drawer");
124
154
  const { showCloseButton, closeDisabled, closeSize } =
125
155
  React.useContext(DrawerContentContext);
126
156
  return (
127
- <div
128
- data-slot="drawer-header"
129
- className={cn("flex shrink-0 items-center gap-4 border-b p-4", className)}
157
+ <SurfaceHeader
158
+ name="DrawerHeader"
159
+ slot="drawer"
160
+ slots={{
161
+ leading: DrawerNavigation,
162
+ media: DrawerMedia,
163
+ title: DrawerTitle,
164
+ actions: DrawerActions,
165
+ }}
166
+ leadingPlacement="inline"
167
+ titleRequired={false}
168
+ rowClassName={surfaceHeaderRowClasses("drawer")}
169
+ className={cn(
170
+ "text-left",
171
+ surfaceRegionClasses("drawer", layout, "header"),
172
+ className,
173
+ )}
174
+ trailing={
175
+ showCloseButton ? (
176
+ <DrawerCloseButton disabled={closeDisabled} size={closeSize} />
177
+ ) : undefined
178
+ }
130
179
  {...props}
131
180
  >
132
181
  {children}
133
- {showCloseButton && (
134
- <DrawerCloseButton disabled={closeDisabled} size={closeSize} />
135
- )}
136
- </div>
182
+ </SurfaceHeader>
183
+ );
184
+ }
185
+
186
+ /** Moldura de ícone ou imagem que abre a linha do header. */
187
+ function DrawerMedia(props: SurfaceMediaProps) {
188
+ return <SurfaceMedia slot="drawer-media" {...props} />;
189
+ }
190
+
191
+ /** Região introdutória do header — o voltar de um fluxo modal encadeado. */
192
+ function DrawerNavigation({ className, ...props }: React.ComponentProps<"div">) {
193
+ return (
194
+ <div
195
+ data-slot="drawer-navigation-content"
196
+ className={cn("flex min-w-0 shrink-0 items-center", className)}
197
+ {...props}
198
+ />
199
+ );
200
+ }
201
+
202
+ /** Ações da tela no header, antes do botão de fechar. */
203
+ function DrawerActions({ className, ...props }: React.ComponentProps<"div">) {
204
+ return (
205
+ <div
206
+ data-slot="drawer-actions"
207
+ className={cn(surfaceActionsClasses, className)}
208
+ {...props}
209
+ />
137
210
  );
138
211
  }
139
212
 
140
213
  function DrawerBody({ className, style, ...props }: React.ComponentProps<"div">) {
214
+ const layout = useSurfaceLayout("drawer");
141
215
  return (
142
216
  <div
143
217
  data-slot="drawer-body"
144
- className={cn("min-h-0 flex-1 overflow-y-auto p-4", className)}
218
+ className={cn(
219
+ "min-h-0 flex-1 overflow-y-auto",
220
+ surfaceRegionClasses("drawer", layout, "body"),
221
+ className,
222
+ )}
145
223
  style={{ "--opus-inline-gutter": "1rem", ...style } as React.CSSProperties}
146
224
  {...props}
147
225
  />
@@ -149,11 +227,16 @@ function DrawerBody({ className, style, ...props }: React.ComponentProps<"div">)
149
227
  }
150
228
 
151
229
  function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
230
+ const layout = useSurfaceLayout("drawer");
152
231
  return (
153
232
  <div
154
233
  data-slot="drawer-footer"
155
234
  className={cn(
156
- "mt-auto flex shrink-0 flex-row justify-end gap-2 border-t p-4",
235
+ // `mt-auto` é a divergência declarada do Drawer: o conteúdo lateral quase nunca
236
+ // preenche a altura da janela, e sem isso o rodapé subiria atrás do corpo. No
237
+ // Dialog a altura é do conteúdo, então ele não precisa (ADR 0023).
238
+ "mt-auto flex shrink-0 flex-row justify-end gap-2",
239
+ surfaceRegionClasses("drawer", layout, "footer"),
157
240
  className,
158
241
  )}
159
242
  {...props}
@@ -163,21 +246,45 @@ function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
163
246
 
164
247
  function DrawerTitle({
165
248
  className,
249
+ level,
250
+ children,
166
251
  ...props
167
- }: React.ComponentProps<typeof DrawerPrimitive.Title>) {
252
+ }: React.ComponentProps<typeof DrawerPrimitive.Title> & {
253
+ /** Hierarquia do documento. Sem ele vale o `h2` que a primitiva emite. */
254
+ level?: HeadingLevel;
255
+ }) {
256
+ const layout = useSurfaceLayout("drawer");
257
+ const classes = cn(
258
+ "min-w-0 truncate",
259
+ surfaceTitleClasses("drawer", layout),
260
+ className,
261
+ );
262
+ // O nível troca a TAG do mesmo nó, via `asChild` — a escala continua vindo do layout.
263
+ if (level === undefined) {
264
+ return (
265
+ <DrawerPrimitive.Title
266
+ data-slot="drawer-title"
267
+ className={classes}
268
+ {...props}
269
+ >
270
+ {children}
271
+ </DrawerPrimitive.Title>
272
+ );
273
+ }
274
+ const Heading = headingTags[level];
168
275
  return (
169
- <DrawerPrimitive.Title
170
- data-slot="drawer-title"
171
- className={cn(
172
- "min-w-0 flex-1 text-lg font-semibold leading-none text-foreground",
173
- className,
174
- )}
175
- {...props}
176
- />
276
+ <DrawerPrimitive.Title asChild {...props}>
277
+ <Heading data-slot="drawer-title" className={classes}>
278
+ {children}
279
+ </Heading>
280
+ </DrawerPrimitive.Title>
177
281
  );
178
282
  }
179
283
 
180
284
  export {
285
+ DrawerActions,
286
+ DrawerMedia,
287
+ DrawerNavigation,
181
288
  Drawer,
182
289
  DrawerTrigger,
183
290
  DrawerClose,
@@ -69,7 +69,7 @@ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
69
69
  )
70
70
  }
71
71
 
72
- function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
72
+ function EmptyDescription({ className, ...props }: React.ComponentProps<"div">) {
73
73
  return (
74
74
  <div
75
75
  data-slot="empty-description"
@@ -46,7 +46,7 @@ import {
46
46
  type LucideIcon,
47
47
  } from 'lucide-react'
48
48
  import { cn } from '../../lib/cn.ts'
49
- import { focusRing } from './control.ts'
49
+ import { controlHeight, focusRing } from './control.ts'
50
50
  import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from './command.tsx'
51
51
  import { Popover, PopoverContent, PopoverTrigger } from './popover.tsx'
52
52
 
@@ -142,8 +142,12 @@ export function IconPicker({
142
142
  aria-describedby={ariaDescribedBy}
143
143
  data-slot="icon-picker"
144
144
  className={cn(
145
- 'flex h-10 w-full items-center gap-2 rounded-md border border-input bg-muted/30 px-3 text-sm outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30',
145
+ // Mesma altura do Input e do Select no tamanho default: lado a lado num form, alinham.
146
+ 'flex w-full items-center gap-2 rounded-md border border-input bg-muted/30 px-3 text-sm outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30',
147
+ controlHeight.default,
146
148
  focusRing,
149
+ // As mesmas classes de inválido do Input e do Select: o erro aparece na moldura.
150
+ 'aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 dark:aria-invalid:ring-context-danger/40',
147
151
  className,
148
152
  )}
149
153
  >
@@ -41,6 +41,13 @@ export interface IconTileProps
41
41
  extends React.ComponentProps<'span'>,
42
42
  IconTileStyleOptions {}
43
43
 
44
+ /**
45
+ * Preenche os padrões de `context`, `variant` e `size` (`neutral`, `subtle`, `default`).
46
+ *
47
+ * @deprecated Não há decisão a tomar com os valores resolvidos fora do próprio IconTile. Para a
48
+ * moldura, renderize `IconTile`; para aplicá-la a outro elemento, use `iconTileVariants`, que já
49
+ * aplica os mesmos padrões. Sai do pacote numa próxima versão major.
50
+ */
44
51
  export function resolveIconTileStyle({
45
52
  context,
46
53
  variant,
@@ -37,7 +37,12 @@ function InputGroup({ className, shape = 'default', ...props }: React.ComponentP
37
37
  }
38
38
 
39
39
  const inputGroupAddonVariants = cva(
40
- "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-0.3125rem)] [&>svg:not([class*='size-'])]:size-4",
40
+ [
41
+ "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-0.3125rem)] [&>svg:not([class*='size-'])]:size-4",
42
+ // O addon acompanha o controle desabilitado sem depender do `data-disabled` manual. Só o
43
+ // controle conta: um InputGroupButton desabilitado não apaga o grupo inteiro.
44
+ "group-has-[[data-slot=input-group-control]:disabled]/input-group:opacity-50",
45
+ ],
41
46
  {
42
47
  variants: {
43
48
  align: {
@@ -72,7 +77,9 @@ function InputGroupAddon({
72
77
  if ((e.target as HTMLElement).closest("button")) {
73
78
  return
74
79
  }
75
- e.currentTarget.parentElement?.querySelector("input")?.focus()
80
+ e.currentTarget.parentElement
81
+ ?.querySelector<HTMLElement>("input, textarea")
82
+ ?.focus()
76
83
  }}
77
84
  {...props}
78
85
  />
@@ -49,7 +49,7 @@ function InputOTPSlot({
49
49
  data-slot="input-otp-slot"
50
50
  data-active={isActive}
51
51
  className={cn(
52
- "relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md aria-invalid:border-context-danger data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-[0.1875rem] data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:border-context-danger data-[active=true]:aria-invalid:ring-context-danger/20 dark:bg-input/30 dark:data-[active=true]:aria-invalid:ring-context-danger/40",
52
+ "relative flex h-9 w-9 items-center justify-center border-y border-r border-input bg-muted/30 text-sm transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md aria-invalid:border-context-danger data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-[0.1875rem] data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:border-context-danger data-[active=true]:aria-invalid:ring-context-danger/20 dark:bg-input/30 dark:data-[active=true]:aria-invalid:ring-context-danger/40",
53
53
  className
54
54
  )}
55
55
  {...props}
@@ -15,7 +15,7 @@ function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
15
15
  )
16
16
  }
17
17
 
18
- function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
18
+ function KbdGroup({ className, ...props }: React.ComponentProps<"kbd">) {
19
19
  return (
20
20
  <kbd
21
21
  data-slot="kbd-group"
@@ -12,6 +12,9 @@ function Label({
12
12
  data-slot="label"
13
13
  className={cn(
14
14
  "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
15
+ // O Field declara o grupo nomeado `group/field`: sem o sufixo, o Label não enxerga o
16
+ // `data-disabled` dele e continua aceso ao lado de um controle desabilitado.
17
+ "group-data-[disabled=true]/field:pointer-events-none group-data-[disabled=true]/field:opacity-50",
15
18
  className
16
19
  )}
17
20
  {...props}
@@ -12,6 +12,11 @@ function Menu({
12
12
  return <MenuPrimitive.Root data-slot="menu" {...props} />
13
13
  }
14
14
 
15
+ /**
16
+ * @deprecated `MenuContent` já renderiza o painel num portal próprio, então envolvê-lo com
17
+ * `MenuPortal` não muda onde o painel aparece. Use `MenuContent` diretamente. Sai do pacote numa
18
+ * próxima versão major.
19
+ */
15
20
  function MenuPortal({
16
21
  ...props
17
22
  }: React.ComponentProps<typeof MenuPrimitive.Portal>) {
@@ -42,7 +42,25 @@ interface MetricCardBaseProps extends Omit<React.ComponentProps<typeof Card>, 'c
42
42
  secondaryMetric?: MetricCardSecondaryMetric
43
43
  }
44
44
 
45
+ type MetricCardActivationProps =
46
+ | {
47
+ /** Indica se a medida controla o recorte atualmente aplicado. */
48
+ active: boolean
49
+ /** Ativa a medida como recorte ou seleção. */
50
+ onActivate: () => void
51
+ /** Nome acessível da ativação, como “Filtrar por Agendados”. */
52
+ activationLabel: string
53
+ /** Use `onActivate`; o clique genérico conflita com os controles internos do card. */
54
+ onClick?: never
55
+ }
56
+ | {
57
+ active?: never
58
+ onActivate?: never
59
+ activationLabel?: never
60
+ }
61
+
45
62
  export type MetricCardProps = MetricCardBaseProps &
63
+ MetricCardActivationProps &
46
64
  (
47
65
  | {
48
66
  /** Nome curto da medida apresentada. */
@@ -75,6 +93,9 @@ export function MetricCard({
75
93
  tone,
76
94
  action,
77
95
  secondaryMetric,
96
+ active,
97
+ onActivate,
98
+ activationLabel,
78
99
  loading = false,
79
100
  className,
80
101
  ...props
@@ -84,6 +105,7 @@ export function MetricCard({
84
105
  }
85
106
  const hasIcon = icon !== undefined && icon !== null
86
107
  const resolvedContext = context ?? (tone === 'warning' ? 'warning' : 'neutral')
108
+ const interactive = onActivate !== undefined
87
109
 
88
110
  const primaryCard = (
89
111
  <Card
@@ -92,9 +114,9 @@ export function MetricCard({
92
114
  className={cn(
93
115
  'min-w-0 p-4',
94
116
  secondaryMetric !== undefined && 'relative z-10',
95
- secondaryMetric === undefined && className,
117
+ secondaryMetric === undefined && !interactive && className,
96
118
  )}
97
- {...(secondaryMetric === undefined ? props : {})}
119
+ {...(secondaryMetric === undefined && !interactive ? props : {})}
98
120
  >
99
121
  {loading ? (
100
122
  <div className="space-y-3">
@@ -106,7 +128,10 @@ export function MetricCard({
106
128
  <>
107
129
  <div className="flex items-start justify-between gap-3">
108
130
  <div className="flex min-w-0 items-center gap-1.5">
109
- <div data-slot="metric-card-label" className="min-w-0 truncate text-sm font-medium text-muted-foreground">
131
+ <div
132
+ data-slot="metric-card-label"
133
+ className="min-w-0 truncate text-sm font-medium text-muted-foreground"
134
+ >
110
135
  {label}
111
136
  </div>
112
137
  {labelAction !== undefined && (
@@ -129,7 +154,10 @@ export function MetricCard({
129
154
  </span>
130
155
  )}
131
156
  </div>
132
- <div data-slot="metric-card-value" className="mt-3 text-2xl font-semibold tracking-tight tabular-nums">
157
+ <div
158
+ data-slot="metric-card-value"
159
+ className="mt-3 text-2xl font-semibold tracking-tight tabular-nums"
160
+ >
133
161
  {value}
134
162
  </div>
135
163
  {description !== undefined && (
@@ -147,46 +175,77 @@ export function MetricCard({
147
175
  </Card>
148
176
  )
149
177
 
150
- if (secondaryMetric === undefined) return primaryCard
151
-
152
- return (
153
- <div
154
- {...props}
155
- data-slot="metric-card-stack"
156
- aria-busy={loading || undefined}
157
- className={cn('relative isolate min-w-0', className)}
158
- >
159
- {primaryCard}
160
- <Card
161
- data-slot="metric-card-secondary"
162
- className="relative z-0 -mt-8 flex w-full min-w-0 items-center gap-1.5 bg-[color-mix(in_oklab,var(--card)_88%,black)] px-4 pt-9 pb-1"
178
+ const metric =
179
+ secondaryMetric === undefined ? (
180
+ primaryCard
181
+ ) : (
182
+ <div
183
+ {...(!interactive ? props : {})}
184
+ data-slot="metric-card-stack"
185
+ aria-busy={loading || undefined}
186
+ className={cn('relative isolate min-w-0', !interactive && className)}
163
187
  >
164
- {loading ? (
165
- <Skeleton className="h-5 w-full" />
166
- ) : (
167
- <>
168
- <div
169
- data-slot="metric-card-secondary-value"
170
- className="shrink-0 text-xs font-medium text-muted-foreground tabular-nums"
171
- >
172
- {secondaryMetric.value}
173
- </div>
174
- <div className="flex min-w-0 items-center gap-1.5">
188
+ {primaryCard}
189
+ <Card
190
+ data-slot="metric-card-secondary"
191
+ className="relative z-0 -mt-8 flex w-full min-w-0 items-center gap-1.5 bg-[color-mix(in_oklab,var(--card)_88%,black)] px-4 pt-9 pb-1"
192
+ >
193
+ {loading ? (
194
+ <Skeleton className="h-5 w-full" />
195
+ ) : (
196
+ <>
175
197
  <div
176
- data-slot="metric-card-secondary-label"
177
- className="min-w-0 truncate text-xs font-medium text-muted-foreground"
198
+ data-slot="metric-card-secondary-value"
199
+ className="shrink-0 text-xs font-medium text-muted-foreground tabular-nums"
178
200
  >
179
- {secondaryMetric.label}
201
+ {secondaryMetric.value}
180
202
  </div>
181
- {secondaryMetric.labelAction !== undefined && (
182
- <div data-slot="metric-card-secondary-label-action" className="shrink-0">
183
- {secondaryMetric.labelAction}
203
+ <div className="flex min-w-0 items-center gap-1.5">
204
+ <div
205
+ data-slot="metric-card-secondary-label"
206
+ className="min-w-0 truncate text-xs font-medium text-muted-foreground"
207
+ >
208
+ {secondaryMetric.label}
184
209
  </div>
185
- )}
186
- </div>
187
- </>
188
- )}
189
- </Card>
210
+ {secondaryMetric.labelAction !== undefined && (
211
+ <div data-slot="metric-card-secondary-label-action" className="shrink-0">
212
+ {secondaryMetric.labelAction}
213
+ </div>
214
+ )}
215
+ </div>
216
+ </>
217
+ )}
218
+ </Card>
219
+ </div>
220
+ )
221
+
222
+ if (!interactive) return metric
223
+
224
+ return (
225
+ <div
226
+ {...props}
227
+ data-slot="metric-card-activation"
228
+ data-active={active}
229
+ className={cn(
230
+ // Ativa = a borda na cor primária, e só. Anel somaria uma segunda linha de
231
+ // outra cor à borda e, pintado para fora, sumiria no primeiro ancestral com
232
+ // `overflow-hidden`.
233
+ 'relative isolate min-w-0 data-[active=true]:[&_[data-slot=metric-card]]:border-primary',
234
+ className,
235
+ )}
236
+ >
237
+ <button
238
+ type="button"
239
+ data-slot="metric-card-activation-control"
240
+ className="peer absolute inset-0 z-0 cursor-pointer rounded-xl outline-none"
241
+ aria-label={activationLabel}
242
+ aria-pressed={active}
243
+ disabled={loading}
244
+ onClick={onActivate}
245
+ />
246
+ <div className="pointer-events-none relative z-10 h-full [&>[data-slot=metric-card]]:h-full [&_[data-slot=metric-card]]:transition-colors [&_[data-slot=metric-card-action]]:pointer-events-auto [&_[data-slot=metric-card-label-action]]:pointer-events-auto [&_[data-slot=metric-card-secondary-label-action]]:pointer-events-auto peer-hover:[&_[data-slot=metric-card]]:border-foreground/40 peer-focus-visible:[&_[data-slot=metric-card]]:ring-2 peer-focus-visible:[&_[data-slot=metric-card]]:ring-inset peer-focus-visible:[&_[data-slot=metric-card]]:ring-ring">
247
+ {metric}
248
+ </div>
190
249
  </div>
191
250
  )
192
251
  }
@@ -59,7 +59,7 @@ function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
59
59
  )
60
60
  }
61
61
 
62
- function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
62
+ function PopoverTitle({ className, ...props }: React.ComponentProps<"div">) {
63
63
  return (
64
64
  <div
65
65
  data-slot="popover-title"
@@ -32,13 +32,21 @@ export interface ProgressProps extends React.ComponentProps<typeof ProgressPrimi
32
32
  function Progress({
33
33
  className,
34
34
  value,
35
+ max = 100,
35
36
  context = 'primary',
36
37
  ...props
37
38
  }: ProgressProps) {
39
+ // A largura é a fração de `max`, não de 100: `value={3} max={4}` enche três quartos. Um `max`
40
+ // não positivo cai em 100, como o Radix faz no Root.
41
+ const scale = max > 0 ? max : 100
42
+ const percent = ((value ?? 0) / scale) * 100
38
43
  return (
39
44
  <ProgressPrimitive.Root
40
45
  data-slot="progress"
41
46
  data-context={context}
47
+ // O Root precisa do `value` para anunciar o andamento (aria-valuenow, data-state).
48
+ value={value}
49
+ max={max}
42
50
  className={cn(
43
51
  "relative h-2 w-full overflow-hidden rounded-full",
44
52
  trackClasses[context],
@@ -49,7 +57,7 @@ function Progress({
49
57
  <ProgressPrimitive.Indicator
50
58
  data-slot="progress-indicator"
51
59
  className={cn("h-full w-full flex-1 transition-all", indicatorClasses[context])}
52
- style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
60
+ style={{ transform: `translateX(-${100 - percent}%)` }}
53
61
  />
54
62
  </ProgressPrimitive.Root>
55
63
  )