@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
@@ -81,10 +81,10 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
81
81
  )
82
82
  }
83
83
 
84
- function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
84
+ function EmptyActions({ className, ...props }: React.ComponentProps<"div">) {
85
85
  return (
86
86
  <div
87
- data-slot="empty-content"
87
+ data-slot="empty-actions"
88
88
  className={cn(
89
89
  "flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
90
90
  className
@@ -99,6 +99,6 @@ export {
99
99
  EmptyHeader,
100
100
  EmptyTitle,
101
101
  EmptyDescription,
102
- EmptyContent,
102
+ EmptyActions,
103
103
  EmptyMedia,
104
104
  }
@@ -1,24 +1,35 @@
1
- import * as React from "react"
2
- import { cva, type VariantProps } from "class-variance-authority"
3
- import { Slot } from "radix-ui"
4
-
5
- import { cn } from '../../lib/cn.ts'
6
- import { Separator } from './separator.tsx'
7
-
8
- const itemGroupVariants = cva("group/item-group @container/item-group flex flex-col", {
9
- variants: {
10
- variant: {
11
- plain: "",
12
- framed:
13
- "overflow-hidden rounded-xl border border-border bg-card text-card-foreground [&>[data-slot=item]]:rounded-none [&>[data-slot=item-container]>[data-slot=item]]:rounded-none",
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { Slot } from "radix-ui";
4
+
5
+ import { cn } from "../../lib/cn.ts";
6
+ import { Separator } from "./separator.tsx";
7
+
8
+ const itemGroupVariants = cva(
9
+ "group/item-group @container/item-group flex flex-col",
10
+ {
11
+ variants: {
12
+ variant: {
13
+ plain: "",
14
+ framed:
15
+ "overflow-hidden rounded-xl border border-border bg-card text-card-foreground [&>[data-slot=item]]:rounded-none [&>[data-slot=item-container]>[data-slot=item]]:rounded-none",
16
+ },
14
17
  },
18
+ defaultVariants: { variant: "plain" },
15
19
  },
16
- defaultVariants: {
17
- variant: "plain",
18
- },
19
- })
20
+ );
21
+
22
+ const ItemGroupContext = React.createContext(false);
23
+ const ItemContext = React.createContext(false);
24
+ const ItemHeaderContext = React.createContext(false);
20
25
 
21
- const ItemGroupContext = React.createContext(false)
26
+ function requireParent(
27
+ inside: boolean,
28
+ component: string,
29
+ parent: string,
30
+ ): void {
31
+ if (!inside) throw new Error(`${component} deve estar dentro de ${parent}.`);
32
+ }
22
33
 
23
34
  function ItemGroup({
24
35
  className,
@@ -35,7 +46,7 @@ function ItemGroup({
35
46
  {...props}
36
47
  />
37
48
  </ItemGroupContext.Provider>
38
- )
49
+ );
39
50
  }
40
51
 
41
52
  function ItemSeparator({
@@ -49,7 +60,7 @@ function ItemSeparator({
49
60
  className={cn("my-0", className)}
50
61
  {...props}
51
62
  />
52
- )
63
+ );
53
64
  }
54
65
 
55
66
  const itemVariants = cva(
@@ -66,12 +77,9 @@ const itemVariants = cva(
66
77
  sm: "gap-2.5 px-4 py-3",
67
78
  },
68
79
  },
69
- defaultVariants: {
70
- variant: "default",
71
- size: "default",
72
- },
73
- }
74
- )
80
+ defaultVariants: { variant: "default", size: "default" },
81
+ },
82
+ );
75
83
 
76
84
  function Item({
77
85
  className,
@@ -82,9 +90,8 @@ function Item({
82
90
  ...props
83
91
  }: React.ComponentProps<"div"> &
84
92
  VariantProps<typeof itemVariants> & { asChild?: boolean }) {
85
- const grouped = React.useContext(ItemGroupContext)
86
- const Comp = asChild ? Slot.Root : "div"
87
-
93
+ const grouped = React.useContext(ItemGroupContext);
94
+ const Comp = asChild ? Slot.Root : "div";
88
95
  const content = (
89
96
  <Comp
90
97
  role={role ?? (!asChild && grouped ? "listitem" : undefined)}
@@ -94,23 +101,22 @@ function Item({
94
101
  className={cn(itemVariants({ variant, size, className }))}
95
102
  {...props}
96
103
  />
97
- )
98
-
99
- if (asChild && grouped) {
100
- return (
101
- <div role="listitem" data-slot="item-container">
102
- {content}
103
- </div>
104
- )
105
- }
106
-
104
+ );
107
105
  return (
108
- content
109
- )
106
+ <ItemContext.Provider value>
107
+ {asChild && grouped ? (
108
+ <div role="listitem" data-slot="item-container">
109
+ {content}
110
+ </div>
111
+ ) : (
112
+ content
113
+ )}
114
+ </ItemContext.Provider>
115
+ );
110
116
  }
111
117
 
112
118
  const itemMediaVariants = cva(
113
- "flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:translate-y-0.5 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none",
119
+ "flex shrink-0 self-start items-center justify-center gap-2 [&_svg]:pointer-events-none",
114
120
  {
115
121
  variants: {
116
122
  variant: {
@@ -120,17 +126,16 @@ const itemMediaVariants = cva(
120
126
  "size-10 overflow-hidden rounded-sm [&_img]:size-full [&_img]:object-cover",
121
127
  },
122
128
  },
123
- defaultVariants: {
124
- variant: "default",
125
- },
126
- }
127
- )
129
+ defaultVariants: { variant: "default" },
130
+ },
131
+ );
128
132
 
129
133
  function ItemMedia({
130
134
  className,
131
135
  variant = "default",
132
136
  ...props
133
137
  }: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
138
+ requireParent(React.useContext(ItemContext), "ItemMedia", "Item");
134
139
  return (
135
140
  <div
136
141
  data-slot="item-media"
@@ -138,94 +143,145 @@ function ItemMedia({
138
143
  className={cn(itemMediaVariants({ variant, className }))}
139
144
  {...props}
140
145
  />
141
- )
146
+ );
147
+ }
148
+
149
+ function ItemHeader({
150
+ className,
151
+ children,
152
+ ...props
153
+ }: React.ComponentProps<"div">) {
154
+ requireParent(React.useContext(ItemContext), "ItemHeader", "Item");
155
+ const nodes = React.Children.toArray(children);
156
+ const titles = nodes.filter(
157
+ (node) => React.isValidElement(node) && node.type === ItemTitle,
158
+ );
159
+ const descriptions = nodes.filter(
160
+ (node) => React.isValidElement(node) && node.type === ItemDescription,
161
+ );
162
+ const canonical =
163
+ titles.length === 1 &&
164
+ descriptions.length <= 1 &&
165
+ titles.length + descriptions.length === nodes.length;
166
+ return (
167
+ <ItemHeaderContext.Provider value>
168
+ <div
169
+ data-slot="item-header"
170
+ className={cn(
171
+ canonical
172
+ ? "flex min-w-0 flex-1 flex-col justify-center gap-1"
173
+ : "flex basis-full items-center justify-between gap-2",
174
+ className,
175
+ )}
176
+ {...props}
177
+ >
178
+ {children}
179
+ </div>
180
+ </ItemHeaderContext.Provider>
181
+ );
142
182
  }
143
183
 
144
- function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
184
+ function ItemBody({ className, ...props }: React.ComponentProps<"div">) {
185
+ requireParent(React.useContext(ItemContext), "ItemBody", "Item");
145
186
  return (
146
187
  <div
147
- data-slot="item-content"
148
- className={cn(
149
- "flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
150
- className
151
- )}
188
+ data-slot="item-body"
189
+ className={cn("flex min-w-0 flex-none flex-col gap-1", className)}
152
190
  {...props}
153
191
  />
154
- )
192
+ );
193
+ }
194
+
195
+ /** @deprecated Use ItemHeader para título/descrição e ItemBody para o conteúdo restante. */
196
+ function ItemContent({
197
+ className,
198
+ children,
199
+ ...props
200
+ }: React.ComponentProps<"div">) {
201
+ requireParent(React.useContext(ItemContext), "ItemContent", "Item");
202
+ return (
203
+ <ItemHeaderContext.Provider value>
204
+ <div
205
+ data-slot="item-content"
206
+ className={cn(
207
+ "flex min-w-0 flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
208
+ className,
209
+ )}
210
+ {...props}
211
+ >
212
+ {children}
213
+ </div>
214
+ </ItemHeaderContext.Provider>
215
+ );
155
216
  }
156
217
 
157
218
  function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
219
+ requireParent(React.useContext(ItemHeaderContext), "ItemTitle", "ItemHeader");
158
220
  return (
159
221
  <div
160
222
  data-slot="item-title"
161
223
  className={cn(
162
224
  "flex w-fit items-center gap-2 text-sm leading-snug font-medium",
163
- className
225
+ className,
164
226
  )}
165
227
  {...props}
166
228
  />
167
- )
229
+ );
168
230
  }
169
231
 
170
232
  function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
233
+ requireParent(
234
+ React.useContext(ItemHeaderContext),
235
+ "ItemDescription",
236
+ "ItemHeader",
237
+ );
171
238
  return (
172
239
  <p
173
240
  data-slot="item-description"
174
241
  className={cn(
175
- "line-clamp-2 text-sm leading-normal font-normal text-balance text-muted-foreground",
176
- "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
177
- className
242
+ "line-clamp-2 text-sm leading-normal font-normal text-balance text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
243
+ className,
178
244
  )}
179
245
  {...props}
180
246
  />
181
- )
247
+ );
182
248
  }
183
249
 
184
250
  function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
251
+ requireParent(React.useContext(ItemContext), "ItemActions", "Item");
185
252
  return (
186
253
  <div
187
254
  data-slot="item-actions"
188
255
  className={cn("flex items-center gap-2", className)}
189
256
  {...props}
190
257
  />
191
- )
192
- }
193
-
194
- function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
195
- return (
196
- <div
197
- data-slot="item-header"
198
- className={cn(
199
- "flex basis-full items-center justify-between gap-2",
200
- className
201
- )}
202
- {...props}
203
- />
204
- )
258
+ );
205
259
  }
206
260
 
207
261
  function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
262
+ requireParent(React.useContext(ItemContext), "ItemFooter", "Item");
208
263
  return (
209
264
  <div
210
265
  data-slot="item-footer"
211
266
  className={cn(
212
267
  "flex basis-full items-center justify-between gap-2",
213
- className
268
+ className,
214
269
  )}
215
270
  {...props}
216
271
  />
217
- )
272
+ );
218
273
  }
219
274
 
220
275
  export {
221
276
  Item,
222
277
  ItemMedia,
278
+ ItemHeader,
279
+ ItemBody,
223
280
  ItemContent,
224
281
  ItemActions,
225
282
  ItemGroup,
226
283
  ItemSeparator,
227
284
  ItemTitle,
228
285
  ItemDescription,
229
- ItemHeader,
230
286
  ItemFooter,
231
- }
287
+ };
@@ -1,4 +1,5 @@
1
1
  import * as React from "react"
2
+ import type { UiContext } from '../../../core/ui-context.ts'
2
3
  import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
3
4
  import { DropdownMenu as MenuPrimitive } from "radix-ui"
4
5
 
@@ -65,19 +66,26 @@ function MenuGroup({
65
66
  function MenuItem({
66
67
  className,
67
68
  inset,
68
- variant = "default",
69
+ context,
70
+ variant,
69
71
  ...props
70
72
  }: React.ComponentProps<typeof MenuPrimitive.Item> & {
71
73
  inset?: boolean
74
+ context?: Extract<UiContext, 'neutral' | 'danger'>
75
+ /** @deprecated Use `context="danger"`. */
72
76
  variant?: "default" | "destructive"
73
77
  }) {
78
+ if (context !== undefined && variant !== undefined) {
79
+ throw new Error('MenuItem não permite combinar context com uma variante semântica legada.')
80
+ }
81
+ const resolvedContext = context ?? (variant === 'destructive' ? 'danger' : 'neutral')
74
82
  return (
75
83
  <MenuPrimitive.Item
76
84
  data-slot="menu-item"
77
85
  data-inset={inset}
78
- data-variant={variant}
86
+ data-context={resolvedContext}
79
87
  className={cn(
80
- "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:focus:*:[svg]:text-destructive!",
88
+ "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[context=danger]:focus:bg-context-danger-subtle data-[context=danger]:focus:text-context-danger-emphasis [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[context=danger]:focus:*:[svg]:text-context-danger-emphasis!",
81
89
  className
82
90
  )}
83
91
  {...props}
@@ -0,0 +1,133 @@
1
+ import * as React from 'react'
2
+ import type { UiContext } from '../../../core/ui-context.ts'
3
+ import { cn } from '../../lib/cn.ts'
4
+ import { Card } from './card.tsx'
5
+ import { Skeleton } from './skeleton.tsx'
6
+
7
+ /** @deprecated Use `MetricCardContext`. */
8
+ export type MetricCardTone = 'default' | 'warning'
9
+ export type MetricCardContext = Exclude<UiContext, 'primary'>
10
+
11
+ const iconContextClasses: Record<MetricCardContext, string> = {
12
+ neutral: 'bg-context-neutral-subtle text-context-neutral-emphasis',
13
+ info: 'bg-context-info-subtle text-context-info-emphasis',
14
+ success: 'bg-context-success-subtle text-context-success-emphasis',
15
+ warning: 'bg-context-warning-subtle text-context-warning-emphasis',
16
+ danger: 'bg-context-danger-subtle text-context-danger-emphasis',
17
+ }
18
+
19
+ interface MetricCardBaseProps extends Omit<React.ComponentProps<typeof Card>, 'children'> {
20
+ /** Controle complementar exibido ao lado do rótulo, como uma explicação em tooltip. */
21
+ labelAction?: React.ReactNode
22
+ /** Contexto que explica recorte, proporção ou significado do valor. */
23
+ description?: React.ReactNode
24
+ /** Ícone decorativo que identifica a natureza da medida. */
25
+ icon?: React.ReactNode
26
+ /** Contexto semântico do ícone; não altera a superfície do card. */
27
+ context?: MetricCardContext
28
+ /** @deprecated Use `context`. Compatibilidade temporária da ADR 0006. */
29
+ tone?: MetricCardTone
30
+ /** Ação relacionada diretamente à medida. */
31
+ action?: React.ReactNode
32
+ }
33
+
34
+ export type MetricCardProps = MetricCardBaseProps &
35
+ (
36
+ | {
37
+ /** Nome curto da medida apresentada. */
38
+ label: React.ReactNode
39
+ /** Valor já formatado pelo consumidor. */
40
+ value: React.ReactNode
41
+ loading?: false
42
+ }
43
+ | {
44
+ /** O rótulo pode ser omitido porque o placeholder ocupa toda a composição. */
45
+ label?: React.ReactNode
46
+ /** O valor ainda não está disponível durante o carregamento. */
47
+ value?: React.ReactNode
48
+ /** Substitui o conteúdo por placeholders preservando a geometria do card. */
49
+ loading: true
50
+ }
51
+ )
52
+
53
+ /**
54
+ * MetricCard apresenta uma medida resumida. Formatação, cálculo e carregamento dos dados
55
+ * continuam sob responsabilidade do consumidor; o componente padroniza apenas a composição.
56
+ */
57
+ export function MetricCard({
58
+ label,
59
+ labelAction,
60
+ value,
61
+ description,
62
+ icon,
63
+ context,
64
+ tone,
65
+ action,
66
+ loading = false,
67
+ className,
68
+ ...props
69
+ }: MetricCardProps): React.ReactElement {
70
+ if (context !== undefined && tone !== undefined) {
71
+ throw new Error('MetricCard não permite combinar context com tone legado.')
72
+ }
73
+ const hasIcon = icon !== undefined && icon !== null
74
+ const resolvedContext = context ?? (tone === 'warning' ? 'warning' : 'neutral')
75
+
76
+ return (
77
+ <Card
78
+ data-slot="metric-card"
79
+ aria-busy={loading || undefined}
80
+ className={cn('min-w-0 p-4', className)}
81
+ {...props}
82
+ >
83
+ {loading ? (
84
+ <div className="space-y-3">
85
+ <Skeleton className="h-4 w-28" />
86
+ <Skeleton className="h-8 w-20" />
87
+ <Skeleton className="h-3 w-full" />
88
+ </div>
89
+ ) : (
90
+ <>
91
+ <div className="flex items-start justify-between gap-3">
92
+ <div className="flex min-w-0 items-center gap-1.5">
93
+ <div data-slot="metric-card-label" className="min-w-0 truncate text-sm font-medium text-muted-foreground">
94
+ {label}
95
+ </div>
96
+ {labelAction !== undefined && (
97
+ <div data-slot="metric-card-label-action" className="shrink-0">
98
+ {labelAction}
99
+ </div>
100
+ )}
101
+ </div>
102
+ {hasIcon && (
103
+ <span
104
+ data-slot="metric-card-icon"
105
+ data-context={resolvedContext}
106
+ aria-hidden="true"
107
+ className={cn(
108
+ 'flex size-8 shrink-0 items-center justify-center rounded-md [&_svg]:size-4',
109
+ iconContextClasses[resolvedContext],
110
+ )}
111
+ >
112
+ {icon}
113
+ </span>
114
+ )}
115
+ </div>
116
+ <div data-slot="metric-card-value" className="mt-3 text-2xl font-semibold tracking-tight tabular-nums">
117
+ {value}
118
+ </div>
119
+ {description !== undefined && (
120
+ <div data-slot="metric-card-description" className="mt-1 text-xs text-muted-foreground">
121
+ {description}
122
+ </div>
123
+ )}
124
+ {action !== undefined && (
125
+ <div data-slot="metric-card-action" className="mt-3">
126
+ {action}
127
+ </div>
128
+ )}
129
+ </>
130
+ )}
131
+ </Card>
132
+ )
133
+ }