@softize/opus 19.0.0 → 20.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/CHANGELOG.md +239 -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/0020-icon-tiles-are-explicitly-composed.md +8 -0
  7. package/docs/adr/0021-content-carries-structure-and-semantic-element.md +7 -0
  8. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +19 -1
  9. package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
  10. package/docs/adr/0025-content-frame-treatment-does-not-change-semantics.md +134 -0
  11. package/docs/data-products.md +15 -9
  12. package/docs/protocol.md +3 -4
  13. package/docs/releasing.md +2 -2
  14. package/package.json +2 -2
  15. package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
  16. package/registry/templates/app/package.json +1 -1
  17. package/src/core/runtime.ts +96 -8
  18. package/src/core/types.ts +5 -2
  19. package/src/ui/components/patterns/action-form-card.tsx +11 -1
  20. package/src/ui/components/patterns/content-header.tsx +109 -18
  21. package/src/ui/components/patterns/fill-region.ts +150 -0
  22. package/src/ui/components/patterns/list.tsx +91 -92
  23. package/src/ui/components/patterns/page.tsx +20 -4
  24. package/src/ui/components/patterns/presentation.tsx +52 -37
  25. package/src/ui/components/patterns/split.tsx +44 -4
  26. package/src/ui/components/patterns/surface-anatomy.ts +35 -8
  27. package/src/ui/components/patterns/surface-header.tsx +58 -25
  28. package/src/ui/components/patterns/trigger.tsx +12 -2
  29. package/src/ui/components/primitives/alert.tsx +9 -1
  30. package/src/ui/components/primitives/avatar.tsx +7 -3
  31. package/src/ui/components/primitives/card.tsx +1 -1
  32. package/src/ui/components/primitives/carousel.tsx +34 -5
  33. package/src/ui/components/primitives/checkbox.tsx +11 -3
  34. package/src/ui/components/primitives/command.tsx +2 -1
  35. package/src/ui/components/primitives/copyable.tsx +120 -10
  36. package/src/ui/components/primitives/dialog.tsx +42 -11
  37. package/src/ui/components/primitives/drawer.tsx +10 -2
  38. package/src/ui/components/primitives/empty.tsx +1 -1
  39. package/src/ui/components/primitives/icon-picker.tsx +6 -2
  40. package/src/ui/components/primitives/icon-tile.tsx +7 -0
  41. package/src/ui/components/primitives/input-group.tsx +9 -2
  42. package/src/ui/components/primitives/input-otp.tsx +1 -1
  43. package/src/ui/components/primitives/kbd.tsx +1 -1
  44. package/src/ui/components/primitives/label.tsx +3 -0
  45. package/src/ui/components/primitives/menu.tsx +5 -0
  46. package/src/ui/components/primitives/popover.tsx +1 -1
  47. package/src/ui/components/primitives/progress.tsx +9 -1
  48. package/src/ui/components/primitives/slider.tsx +62 -11
  49. package/src/ui/components/primitives/truncate.tsx +10 -1
  50. package/src/ui/docs/content/accordion.md +74 -27
  51. package/src/ui/docs/content/action-form-card.md +24 -15
  52. package/src/ui/docs/content/action-form-dialog.md +17 -7
  53. package/src/ui/docs/content/action-form.md +92 -77
  54. package/src/ui/docs/content/action-list-dialog.md +75 -38
  55. package/src/ui/docs/content/action-list.md +141 -58
  56. package/src/ui/docs/content/action-trigger.md +27 -10
  57. package/src/ui/docs/content/action-view.md +14 -14
  58. package/src/ui/docs/content/actions.md +161 -66
  59. package/src/ui/docs/content/ai.md +43 -44
  60. package/src/ui/docs/content/alert.md +83 -29
  61. package/src/ui/docs/content/ask.md +51 -26
  62. package/src/ui/docs/content/aspect-ratio.md +22 -24
  63. package/src/ui/docs/content/audit.md +23 -19
  64. package/src/ui/docs/content/auth.md +38 -19
  65. package/src/ui/docs/content/avatar.md +54 -5
  66. package/src/ui/docs/content/badge.md +29 -9
  67. package/src/ui/docs/content/breadcrumb.md +21 -8
  68. package/src/ui/docs/content/button.md +73 -43
  69. package/src/ui/docs/content/calendar.md +134 -26
  70. package/src/ui/docs/content/card.md +41 -15
  71. package/src/ui/docs/content/carousel.md +37 -6
  72. package/src/ui/docs/content/chat.md +118 -37
  73. package/src/ui/docs/content/checkbox.md +73 -7
  74. package/src/ui/docs/content/cli.md +94 -55
  75. package/src/ui/docs/content/collapsible.md +43 -19
  76. package/src/ui/docs/content/command.md +30 -8
  77. package/src/ui/docs/content/composer.md +23 -8
  78. package/src/ui/docs/content/content.md +119 -27
  79. package/src/ui/docs/content/copyable.md +39 -8
  80. package/src/ui/docs/content/customization.md +55 -46
  81. package/src/ui/docs/content/cycle.md +33 -18
  82. package/src/ui/docs/content/data-products.md +115 -0
  83. package/src/ui/docs/content/data-state.md +20 -6
  84. package/src/ui/docs/content/data.md +20 -19
  85. package/src/ui/docs/content/detail.md +12 -5
  86. package/src/ui/docs/content/dialog.md +107 -24
  87. package/src/ui/docs/content/dictionary-value.md +4 -4
  88. package/src/ui/docs/content/dock.md +54 -19
  89. package/src/ui/docs/content/dot.md +35 -5
  90. package/src/ui/docs/content/drawer.md +63 -19
  91. package/src/ui/docs/content/empty-value.md +20 -6
  92. package/src/ui/docs/content/empty.md +24 -3
  93. package/src/ui/docs/content/events.md +76 -16
  94. package/src/ui/docs/content/executions.md +9 -9
  95. package/src/ui/docs/content/field.md +21 -6
  96. package/src/ui/docs/content/getting-started.md +44 -34
  97. package/src/ui/docs/content/icon-picker.md +19 -13
  98. package/src/ui/docs/content/icon-tile.md +38 -15
  99. package/src/ui/docs/content/input-otp.md +58 -7
  100. package/src/ui/docs/content/input.md +22 -3
  101. package/src/ui/docs/content/item.md +53 -6
  102. package/src/ui/docs/content/kbd.md +5 -1
  103. package/src/ui/docs/content/label.md +13 -4
  104. package/src/ui/docs/content/log.md +5 -5
  105. package/src/ui/docs/content/markdown.md +5 -4
  106. package/src/ui/docs/content/mcp.md +49 -23
  107. package/src/ui/docs/content/menu.md +95 -10
  108. package/src/ui/docs/content/metric-card.md +12 -4
  109. package/src/ui/docs/content/observability.md +10 -9
  110. package/src/ui/docs/content/page.md +118 -12
  111. package/src/ui/docs/content/pagination.md +13 -5
  112. package/src/ui/docs/content/pane.md +55 -5
  113. package/src/ui/docs/content/popover.md +42 -5
  114. package/src/ui/docs/content/presentation.md +91 -47
  115. package/src/ui/docs/content/progress.md +27 -14
  116. package/src/ui/docs/content/queue.md +39 -21
  117. package/src/ui/docs/content/radio-group.md +26 -4
  118. package/src/ui/docs/content/router.md +29 -10
  119. package/src/ui/docs/content/runtime.md +90 -54
  120. package/src/ui/docs/content/scheduler.md +53 -21
  121. package/src/ui/docs/content/scroll-area.md +42 -35
  122. package/src/ui/docs/content/select.md +78 -33
  123. package/src/ui/docs/content/semantic-context.md +21 -2
  124. package/src/ui/docs/content/separator.md +15 -9
  125. package/src/ui/docs/content/sidebar.md +3 -2
  126. package/src/ui/docs/content/skeleton.md +6 -1
  127. package/src/ui/docs/content/slider.md +37 -9
  128. package/src/ui/docs/content/spinner.md +4 -3
  129. package/src/ui/docs/content/split.md +19 -0
  130. package/src/ui/docs/content/storage.md +8 -9
  131. package/src/ui/docs/content/switch.md +10 -7
  132. package/src/ui/docs/content/table.md +46 -8
  133. package/src/ui/docs/content/tabs.md +14 -4
  134. package/src/ui/docs/content/testing.md +76 -31
  135. package/src/ui/docs/content/textarea.md +5 -4
  136. package/src/ui/docs/content/toast.md +48 -7
  137. package/src/ui/docs/content/toggle.md +3 -3
  138. package/src/ui/docs/content/tokens.md +15 -26
  139. package/src/ui/docs/content/tooltip.md +18 -2
  140. package/src/ui/docs/content/truncate.md +36 -6
  141. package/src/ui/docs/content/ui.md +74 -22
  142. package/src/ui/docs/content/upgrading.md +45 -36
  143. package/src/ui/docs/plugin.ts +36 -2
  144. package/src/ui/docs/registry.tsx +6 -0
  145. package/src/ui/drivers/react.tsx +16 -8
  146. package/src/ui/meta.ts +32 -32
  147. package/src/ui/react.tsx +1 -0
  148. package/src/vite/design.ts +16 -4
@@ -11,6 +11,7 @@ import {
11
11
  useSurfaceLayout,
12
12
  type SurfaceLayout,
13
13
  } from "../patterns/surface-anatomy.ts";
14
+ import { FillBoundary } from "../patterns/fill-region.ts";
14
15
  import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
15
16
  import { SurfaceHeader } from "../patterns/surface-header.tsx";
16
17
  import {
@@ -45,6 +46,24 @@ const DialogContentContext = React.createContext<DialogContentContextValue>({
45
46
  closeSize: "sm",
46
47
  });
47
48
 
49
+ /**
50
+ * Compõe o handler de dispensa do consumidor com o bloqueio de `closeDisabled`: o handler dele
51
+ * roda primeiro e, com a superfície bloqueada, o evento é cancelado para que Esc, clique fora ou
52
+ * foco fora não a fechem. Sem bloqueio, devolve o handler original.
53
+ */
54
+ export function preventDismissWhen<
55
+ E extends { defaultPrevented: boolean; preventDefault(): void },
56
+ >(
57
+ blocked: boolean,
58
+ handler: ((event: E) => void) | undefined,
59
+ ): ((event: E) => void) | undefined {
60
+ if (!blocked) return handler;
61
+ return (event) => {
62
+ handler?.(event);
63
+ if (!event.defaultPrevented) event.preventDefault();
64
+ };
65
+ }
66
+
48
67
  function useDialogContext(): DialogContextValue {
49
68
  return React.useContext(DialogContext);
50
69
  }
@@ -192,11 +211,16 @@ export function DialogContent({
192
211
  closeSize = "sm",
193
212
  layout = "sectioned",
194
213
  "aria-describedby": ariaDescribedBy,
214
+ onEscapeKeyDown,
215
+ onInteractOutside,
195
216
  ...props
196
217
  }: React.ComponentProps<typeof DialogPrimitive.Content> & {
197
218
  /** Mostra a ação “Fechar” no modo padrão. O modo `alert` exige uma resposta identificável. */
198
219
  showCloseButton?: boolean;
199
- /** Mantém a ação visível, mas impede fechar enquanto a superfície está bloqueada. */
220
+ /**
221
+ * Bloqueia o fechamento enquanto a superfície está ocupada: desabilita a ação “Fechar”, que
222
+ * continua visível, e cancela Esc e clique ou foco fora depois dos handlers do consumidor.
223
+ */
200
224
  closeDisabled?: boolean;
201
225
  /** Tamanho da ação circular de fechar no cabeçalho. */
202
226
  closeSize?: CloseButtonSize;
@@ -206,7 +230,7 @@ export function DialogContent({
206
230
  const { mode } = useDialogContext();
207
231
  const showClose = showCloseButton ?? mode !== "alert";
208
232
  const contentClasses =
209
- "fixed left-[50%] top-[50%] z-50 w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] overflow-clip rounded-xl border border-transparent bg-background text-foreground shadow-xl duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 dark:border-border/70";
233
+ "fixed left-[50%] top-[50%] z-50 w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] overflow-clip rounded-xl border border-transparent bg-background text-foreground shadow-xl duration-200 outline-none [--surface-background:var(--background)] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 dark:border-border/70";
210
234
 
211
235
  if (mode === "alert") {
212
236
  return (
@@ -217,17 +241,22 @@ export function DialogContent({
217
241
  data-mode="alert"
218
242
  className={cn(contentClasses, "grid max-w-xs gap-4 p-6", className)}
219
243
  aria-describedby={ariaDescribedBy}
244
+ onEscapeKeyDown={preventDismissWhen(closeDisabled, onEscapeKeyDown)}
220
245
  {...props}
221
246
  >
222
- <DialogContentContext.Provider
223
- value={{
224
- showCloseButton: false,
225
- closeDisabled: true,
226
- closeSize,
227
- }}
228
- >
229
- {children}
230
- </DialogContentContext.Provider>
247
+ {/* Sem anatomia seccionada (exceção da ADR 0023), mas a altura continua sendo
248
+ decisão do diálogo: a cadeia `fill` da página não atravessa o portal. */}
249
+ <FillBoundary>
250
+ <DialogContentContext.Provider
251
+ value={{
252
+ showCloseButton: false,
253
+ closeDisabled: true,
254
+ closeSize,
255
+ }}
256
+ >
257
+ {children}
258
+ </DialogContentContext.Provider>
259
+ </FillBoundary>
231
260
  </AlertDialogPrimitive.Content>
232
261
  </DialogPortal>
233
262
  );
@@ -246,6 +275,8 @@ export function DialogContent({
246
275
  className,
247
276
  )}
248
277
  aria-describedby={ariaDescribedBy}
278
+ onEscapeKeyDown={preventDismissWhen(closeDisabled, onEscapeKeyDown)}
279
+ onInteractOutside={preventDismissWhen(closeDisabled, onInteractOutside)}
249
280
  {...props}
250
281
  >
251
282
  <SurfaceLayoutProvider value={layout}>
@@ -21,7 +21,7 @@ import {
21
21
  CloseButton,
22
22
  type CloseButtonSize,
23
23
  } from "./close-button.tsx";
24
- import { overlayClasses } from "./dialog.tsx";
24
+ import { overlayClasses, preventDismissWhen } from "./dialog.tsx";
25
25
 
26
26
  interface DrawerContentContextValue {
27
27
  showCloseButton: boolean;
@@ -95,10 +95,16 @@ function DrawerContent({
95
95
  closeSize = "sm",
96
96
  layout = "sectioned",
97
97
  "aria-describedby": ariaDescribedBy,
98
+ onEscapeKeyDown,
99
+ onInteractOutside,
98
100
  ...props
99
101
  }: React.ComponentProps<typeof DrawerPrimitive.Content> & {
100
102
  side?: "top" | "right" | "bottom" | "left";
101
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
+ */
102
108
  closeDisabled?: boolean;
103
109
  closeSize?: CloseButtonSize;
104
110
  /** Anatomia da superfície: `sectioned` (default) ou `continuous` — ADR 0023. */
@@ -111,7 +117,7 @@ function DrawerContent({
111
117
  data-slot="drawer-content"
112
118
  data-layout={layout}
113
119
  className={cn(
114
- "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",
120
+ "fixed z-50 flex flex-col bg-background shadow-lg transition ease-in-out [--surface-background:var(--background)] data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
115
121
  side === "right" &&
116
122
  "inset-y-0 right-0 h-full w-3/4 max-w-sm border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right",
117
123
  side === "left" &&
@@ -123,6 +129,8 @@ function DrawerContent({
123
129
  className,
124
130
  )}
125
131
  aria-describedby={ariaDescribedBy}
132
+ onEscapeKeyDown={preventDismissWhen(closeDisabled, onEscapeKeyDown)}
133
+ onInteractOutside={preventDismissWhen(closeDisabled, onInteractOutside)}
126
134
  {...props}
127
135
  >
128
136
  <SurfaceLayoutProvider value={layout}>
@@ -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>) {
@@ -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
  )
@@ -4,14 +4,40 @@ import { Slider as SliderPrimitive } from "radix-ui"
4
4
  import { cn } from '../../lib/cn.ts'
5
5
  import { focusRing } from './control.ts'
6
6
 
7
+ export interface SliderProps extends React.ComponentProps<typeof SliderPrimitive.Root> {
8
+ /**
9
+ * Nome de cada thumb, na ordem de `value`. Use em intervalos, em que cada controle precisa de
10
+ * um nome próprio (`['Custo mínimo', 'Custo máximo']`). Prevalece sobre `aria-label` e
11
+ * `aria-labelledby` no thumb correspondente.
12
+ */
13
+ thumbLabels?: readonly string[]
14
+ }
15
+
16
+ // Quem recebe foco e anuncia o valor é o thumb (`role="slider"`), não a raiz. Por isso
17
+ // `aria-label` e `aria-labelledby` vão para os thumbs, como o Select faz com o campo interno;
18
+ // na raiz, sem papel, não nomeariam nada. Em intervalo, `thumbLabels` dá um nome a cada controle.
19
+ // Sem nome declarado, o Radix usaria "Minimum"/"Maximum": o fallback fica em português. Em
20
+ // intervalo sem `thumbLabels`, o nome comum se compõe com o fallback ("Preço, mínimo"), senão os
21
+ // dois thumbs teriam o mesmo nome; com `aria-labelledby`, o fallback entra por um span oculto
22
+ // referenciado junto (`hidden` sai da tela, mas continua valendo como rótulo referenciado).
23
+ function thumbFallback(index: number, total: number): string | undefined {
24
+ if (total > 2) return `Valor ${index + 1} de ${total}`
25
+ if (total === 2) return index === 0 ? 'Mínimo' : 'Máximo'
26
+ return undefined
27
+ }
28
+
7
29
  function Slider({
8
30
  className,
9
31
  defaultValue,
10
32
  value,
11
33
  min = 0,
12
34
  max = 100,
35
+ thumbLabels,
36
+ 'aria-label': ariaLabel,
37
+ 'aria-labelledby': ariaLabelledby,
13
38
  ...props
14
- }: React.ComponentProps<typeof SliderPrimitive.Root>) {
39
+ }: SliderProps) {
40
+ const fallbackIdBase = React.useId()
15
41
  const _values = React.useMemo(
16
42
  () =>
17
43
  Array.isArray(value)
@@ -48,16 +74,41 @@ function Slider({
48
74
  )}
49
75
  />
50
76
  </SliderPrimitive.Track>
51
- {Array.from({ length: _values.length }, (_, index) => (
52
- <SliderPrimitive.Thumb
53
- data-slot="slider-thumb"
54
- key={index}
55
- className={cn(
56
- "block size-4 shrink-0 rounded-full border border-primary bg-background shadow-sm transition-[color,box-shadow] hover:ring-[0.1875rem] hover:ring-ring/50 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50",
57
- focusRing,
58
- )}
59
- />
60
- ))}
77
+ {Array.from({ length: _values.length }, (_, index) => {
78
+ const thumbLabel = thumbLabels?.[index]
79
+ const fallback = thumbFallback(index, _values.length)
80
+ const fallbackId = `${fallbackIdBase}-thumb-${index}`
81
+ const composeLabelledby = thumbLabel === undefined && ariaLabelledby !== undefined && fallback !== undefined
82
+ const label =
83
+ thumbLabel ??
84
+ (ariaLabel !== undefined && fallback !== undefined
85
+ ? `${ariaLabel}, ${fallback.toLowerCase()}`
86
+ : ariaLabel ?? fallback)
87
+ return (
88
+ <React.Fragment key={index}>
89
+ {composeLabelledby && (
90
+ <span id={fallbackId} hidden>
91
+ {fallback}
92
+ </span>
93
+ )}
94
+ <SliderPrimitive.Thumb
95
+ data-slot="slider-thumb"
96
+ aria-label={composeLabelledby ? undefined : label}
97
+ aria-labelledby={
98
+ thumbLabel !== undefined
99
+ ? undefined
100
+ : composeLabelledby
101
+ ? `${ariaLabelledby} ${fallbackId}`
102
+ : ariaLabelledby
103
+ }
104
+ className={cn(
105
+ "block size-4 shrink-0 rounded-full border border-primary bg-background shadow-sm transition-[color,box-shadow] hover:ring-[0.1875rem] hover:ring-ring/50 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50",
106
+ focusRing,
107
+ )}
108
+ />
109
+ </React.Fragment>
110
+ )
111
+ })}
61
112
  </SliderPrimitive.Root>
62
113
  )
63
114
  }
@@ -2,6 +2,7 @@ import * as React from 'react'
2
2
  import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip.tsx'
3
3
  import { useOverflowing } from '../../lib/overflow.ts'
4
4
  import { cn } from '../../lib/cn.ts'
5
+ import { focusRing } from './control.ts'
5
6
 
6
7
  export interface TruncateProps extends React.ComponentProps<'span'> {
7
8
  /** Conteúdo do tooltip quando transborda. Default: os próprios children. */
@@ -17,13 +18,21 @@ export interface TruncateProps extends React.ComponentProps<'span'> {
17
18
  * Requer `<TooltipProvider>` na raiz do app (o esqueleto do `opus create` já monta).
18
19
  * Com `fade`, a mesma medição decide quando esmaecer o fim da linha em vez de cortar
19
20
  * em reticências — sem transbordo, nada é aplicado.
21
+ * Com o texto cortado, o span entra na ordem de tabulação para que o teclado alcance a dica;
22
+ * sem corte, fica fora dela, porque não há nada escondido para revelar.
20
23
  */
21
24
  export function Truncate({ tooltip, fade = false, children, className, ...props }: TruncateProps): React.ReactElement {
22
25
  const { ref, overflowing } = useOverflowing<HTMLSpanElement>(children)
23
26
  const cut = fade ? (overflowing ? 'truncate-fade' : 'overflow-hidden whitespace-nowrap') : 'truncate'
24
27
 
25
28
  const span = (
26
- <span ref={ref} data-slot="truncate" className={cn('block', cut, className)} {...props}>
29
+ <span
30
+ ref={ref}
31
+ data-slot="truncate"
32
+ tabIndex={overflowing ? 0 : undefined}
33
+ className={cn('block', cut, overflowing && ['rounded-sm outline-none', focusRing], className)}
34
+ {...props}
35
+ >
27
36
  {children}
28
37
  </span>
29
38
  )
@@ -1,7 +1,11 @@
1
1
  ## Um painel por vez
2
2
 
3
+ Use `Accordion` para dividir um conteúdo longo em painéis que a pessoa expande sob demanda, cada um
4
+ com seu título. Para um único bloco recolhível, use [`Collapsible`](/ui/collapsible).
5
+
3
6
  Use `type="single"` quando somente um painel deve permanecer aberto. Com `collapsible`, a pessoa
4
- também pode fechar o painel atual; sem essa propriedade, um item permanece expandido.
7
+ também pode fechar o painel atual; sem essa propriedade, depois que um item é aberto, sempre haverá
8
+ um item expandido.
5
9
  `defaultValue` define o item inicialmente aberto no modo não controlado.
6
10
 
7
11
  ```tsx preview
@@ -9,41 +13,43 @@ também pode fechar o painel atual; sem essa propriedade, um item permanece expa
9
13
  <AccordionItem value="overview">
10
14
  <AccordionTrigger>Visão geral</AccordionTrigger>
11
15
  <AccordionContent>
12
- <p className="text-muted-foreground">Resumo do workspace Empresa X.</p>
16
+ <p className="text-muted-foreground">Corolla XEi 2022, placa ABC-1D23.</p>
13
17
  </AccordionContent>
14
18
  </AccordionItem>
15
- <AccordionItem value="agents">
16
- <AccordionTrigger>Agentes</AccordionTrigger>
19
+ <AccordionItem value="services">
20
+ <AccordionTrigger>Serviços</AccordionTrigger>
17
21
  <AccordionContent>
18
- <p className="text-muted-foreground">developer, reviewer e designer ativos.</p>
22
+ <p className="text-muted-foreground">Troca de óleo, filtro de ar e alinhamento.</p>
19
23
  </AccordionContent>
20
24
  </AccordionItem>
21
- <AccordionItem value="repos">
22
- <AccordionTrigger>Repositórios</AccordionTrigger>
25
+ <AccordionItem value="parts">
26
+ <AccordionTrigger>Peças</AccordionTrigger>
23
27
  <AccordionContent>
24
- <p className="text-muted-foreground">empresa-x-api e empresa-x-web vinculados.</p>
28
+ <p className="text-muted-foreground">Filtro de óleo e pastilhas de freio.</p>
25
29
  </AccordionContent>
26
30
  </AccordionItem>
27
31
  </Accordion>
28
32
  ```
29
33
 
34
+ O `AccordionTrigger` já traz o ícone que gira conforme o estado; não acrescente outro.
35
+
30
36
  ## Múltiplos abertos
31
37
 
32
38
  Use `type="multiple"` quando os painéis puderem permanecer abertos ao mesmo tempo. Nesse modo,
33
39
  `defaultValue` recebe um array com os itens inicialmente expandidos.
34
40
 
35
41
  ```tsx preview
36
- <Accordion type="multiple" defaultValue={['skills', 'prompt']} className="w-full">
37
- <AccordionItem value="prompt">
38
- <AccordionTrigger>Prompt</AccordionTrigger>
42
+ <Accordion type="multiple" defaultValue={['notes', 'parts']} className="w-full">
43
+ <AccordionItem value="notes">
44
+ <AccordionTrigger>Observações</AccordionTrigger>
39
45
  <AccordionContent>
40
- <p className="text-muted-foreground">O prompt do agente developer.</p>
46
+ <p className="text-muted-foreground">O cliente relatou ruído ao frear.</p>
41
47
  </AccordionContent>
42
48
  </AccordionItem>
43
- <AccordionItem value="skills">
44
- <AccordionTrigger>Skills</AccordionTrigger>
49
+ <AccordionItem value="parts">
50
+ <AccordionTrigger>Peças</AccordionTrigger>
45
51
  <AccordionContent>
46
- <p className="text-muted-foreground">clean-code, code-style, ddd e test vinculadas.</p>
52
+ <p className="text-muted-foreground">Pastilhas de freio substituídas.</p>
47
53
  </AccordionContent>
48
54
  </AccordionItem>
49
55
  </Accordion>
@@ -55,38 +61,79 @@ Use `value` e `onValueChange` quando outro elemento ou estado do aplicativo tamb
55
61
  controlar o painel aberto.
56
62
 
57
63
  ```tsx preview
58
- const [open, setOpen] = React.useState('developer')
64
+ const [open, setOpen] = React.useState('diagnosis')
59
65
 
60
66
  render(
61
67
  <Accordion type="single" collapsible value={open} onValueChange={setOpen} className="w-full">
62
- <AccordionItem value="developer">
63
- <AccordionTrigger>Agente developer</AccordionTrigger>
68
+ <AccordionItem value="diagnosis">
69
+ <AccordionTrigger>Diagnóstico</AccordionTrigger>
64
70
  <AccordionContent>
65
- <p className="text-muted-foreground">Implementa a task no worktree da sessão.</p>
71
+ <p className="text-muted-foreground">Identifica a causa do ruído relatado pelo cliente.</p>
66
72
  </AccordionContent>
67
73
  </AccordionItem>
68
- <AccordionItem value="reviewer">
69
- <AccordionTrigger>Agente reviewer</AccordionTrigger>
74
+ <AccordionItem value="execution">
75
+ <AccordionTrigger>Execução</AccordionTrigger>
70
76
  <AccordionContent>
71
- <p className="text-muted-foreground">Revisa o handoff e carimba o diff.</p>
77
+ <p className="text-muted-foreground">Substitui as peças aprovadas no orçamento.</p>
72
78
  </AccordionContent>
73
79
  </AccordionItem>
74
80
  </Accordion>,
75
81
  )
76
82
  ```
77
83
 
84
+ ## Item indisponível
85
+
86
+ Use `disabled` no `AccordionItem` quando um painel existe, mas ainda não pode ser aberto. O gatilho
87
+ fica esmaecido e não responde ao clique nem ao teclado.
88
+
89
+ ```tsx preview
90
+ <Accordion type="single" collapsible className="w-full">
91
+ <AccordionItem value="quote">
92
+ <AccordionTrigger>Orçamento</AccordionTrigger>
93
+ <AccordionContent>
94
+ <p className="text-muted-foreground">Aprovado pelo cliente em 12 de maio.</p>
95
+ </AccordionContent>
96
+ </AccordionItem>
97
+ <AccordionItem value="invoice" disabled>
98
+ <AccordionTrigger>Nota fiscal</AccordionTrigger>
99
+ <AccordionContent>
100
+ <p className="text-muted-foreground">Emitida depois da entrega do veículo.</p>
101
+ </AccordionContent>
102
+ </AccordionItem>
103
+ </Accordion>
104
+ ```
105
+
78
106
  ## Propriedades de Accordion
79
107
 
80
108
  | Propriedade | Tipo | Padrão | Descrição |
81
109
  |---|---|---|---|
82
- | `type` | `'single' \| 'multiple'` | | `single` abre um painel por vez; `multiple` permite manter vários abertos. |
110
+ | `type` | `'single' \| 'multiple'` | | Obrigatório. `single` abre um painel por vez; `multiple` permite manter vários abertos. |
83
111
  | `collapsible` | `boolean` | `false` | No modo `single`, permite fechar o item aberto. |
84
- | `defaultValue` | `string \| string[]` | | Item ou itens inicialmente abertos no modo não controlado. |
85
- | `value` | `string \| string[]` | | Item ou itens abertos no modo controlado. Use com `onValueChange`. |
86
- | `onValueChange` | `(value) => void` | | Chamado quando a pessoa abre ou fecha um item. |
112
+ | `defaultValue` | `string` em `single`; `string[]` em `multiple` | | Item ou itens inicialmente abertos no modo não controlado. |
113
+ | `value` | `string` em `single`; `string[]` em `multiple` | | Item ou itens abertos no modo controlado. Use com `onValueChange`. |
114
+ | `onValueChange` | `(value: string) => void` em `single`; `(value: string[]) => void` em `multiple` | | Chamado quando a pessoa abre ou fecha um item. Em `single`, recebe `''` quando o item aberto é fechado. |
115
+ | `disabled` | `boolean` | `false` | Desabilita todos os itens. |
116
+ | `className` | `string` | | Ajusta o contêiner. |
87
117
 
88
118
  ## Propriedades de AccordionItem
89
119
 
90
120
  | Propriedade | Tipo | Padrão | Descrição |
91
121
  |---|---|---|---|
92
- | `value` | `string` | | Identificador usado por `defaultValue` e `value` no `Accordion`. |
122
+ | `value` | `string` | | Obrigatório. Identificador usado por `defaultValue` e `value` no `Accordion`. |
123
+ | `disabled` | `boolean` | `false` | Impede abrir ou fechar este item. |
124
+ | `className` | `string` | | Ajusta o item, que traz um divisor inferior exceto no último. |
125
+
126
+ ## Propriedades de AccordionTrigger
127
+
128
+ | Propriedade | Tipo | Padrão | Descrição |
129
+ |---|---|---|---|
130
+ | `children` | `ReactNode` | | Rótulo do painel. O ícone de estado é acrescentado depois dele. |
131
+ | `className` | `string` | | Ajusta o botão do gatilho. |
132
+
133
+ ## Propriedades de AccordionContent
134
+
135
+ | Propriedade | Tipo | Padrão | Descrição |
136
+ |---|---|---|---|
137
+ | `children` | `ReactNode` | | Conteúdo do painel. |
138
+ | `className` | `string` | | Ajusta o recuo interno do conteúdo, aplicado a um contêiner dentro da região animada. |
139
+ | `forceMount` | `boolean` | `false` | Mantém o conteúdo montado mesmo fechado. |