@softize/opus 12.9.0 → 12.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/bin/lib/check.mjs +1103 -310
  3. package/bin/lib/copy.mjs +74 -6
  4. package/docs/adr/0003-dictionary-presentation-is-declared.md +3 -0
  5. package/docs/adr/0004-page-content-state-is-composed.md +65 -0
  6. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +97 -0
  7. package/docs/adr/0006-semantic-context-precedes-visual-variant.md +182 -0
  8. package/docs/code-style.md +6 -2
  9. package/package.json +1 -1
  10. package/registry/instructions/opus.md +5 -0
  11. package/registry/skills/build-opus-ui/SKILL.md +27 -16
  12. package/registry/skills/build-opus-ui/references/evaluations.md +16 -5
  13. package/registry/skills/build-opus-ui/references/ui-patterns.md +38 -15
  14. package/registry/skills/model-opus-dictionary/SKILL.md +4 -2
  15. package/registry/skills/model-opus-dictionary/references/evaluations.md +4 -3
  16. package/registry/templates/app/src/App.tsx +1 -1
  17. package/src/core/dictionary.ts +52 -14
  18. package/src/core/index.ts +10 -0
  19. package/src/core/ui-context.ts +29 -0
  20. package/src/schema/drivers/zod.ts +34 -19
  21. package/src/ui/components/patterns/action-form-card.tsx +18 -12
  22. package/src/ui/components/patterns/confirm.tsx +26 -3
  23. package/src/ui/components/patterns/content-header.tsx +335 -61
  24. package/src/ui/components/patterns/data-state.tsx +23 -10
  25. package/src/ui/components/patterns/form.tsx +1 -1
  26. package/src/ui/components/patterns/list.tsx +1096 -777
  27. package/src/ui/components/patterns/page-state.tsx +115 -0
  28. package/src/ui/components/patterns/page.tsx +231 -41
  29. package/src/ui/components/patterns/sidebar.tsx +354 -80
  30. package/src/ui/components/patterns/trigger.tsx +13 -9
  31. package/src/ui/components/patterns/view.tsx +7 -11
  32. package/src/ui/components/primitives/alert-dialog.tsx +7 -5
  33. package/src/ui/components/primitives/alert.tsx +298 -80
  34. package/src/ui/components/primitives/ask.tsx +2 -1
  35. package/src/ui/components/primitives/badge.tsx +91 -30
  36. package/src/ui/components/primitives/button.tsx +99 -60
  37. package/src/ui/components/primitives/calendar.tsx +39 -39
  38. package/src/ui/components/primitives/card.tsx +96 -23
  39. package/src/ui/components/primitives/detail.tsx +2 -2
  40. package/src/ui/components/primitives/dictionary-value.tsx +9 -14
  41. package/src/ui/components/primitives/dot.tsx +74 -21
  42. package/src/ui/components/primitives/drawer.tsx +33 -20
  43. package/src/ui/components/primitives/field.tsx +4 -4
  44. package/src/ui/components/primitives/item.tsx +137 -81
  45. package/src/ui/components/primitives/menu.tsx +11 -3
  46. package/src/ui/components/primitives/metric-card.tsx +133 -0
  47. package/src/ui/components/primitives/table.tsx +2 -2
  48. package/src/ui/components/primitives/tooltip.tsx +1 -1
  49. package/src/ui/docs/DocBrowser.tsx +3 -3
  50. package/src/ui/docs/changelog.tsx +1 -1
  51. package/src/ui/docs/content/action-form-card.md +1 -1
  52. package/src/ui/docs/content/alert-dialog.md +8 -8
  53. package/src/ui/docs/content/alert.md +54 -23
  54. package/src/ui/docs/content/badge.md +18 -19
  55. package/src/ui/docs/content/button.md +12 -9
  56. package/src/ui/docs/content/card.md +5 -5
  57. package/src/ui/docs/content/content.md +44 -0
  58. package/src/ui/docs/content/customization.md +2 -2
  59. package/src/ui/docs/content/detail.md +5 -2
  60. package/src/ui/docs/content/dialog.md +2 -2
  61. package/src/ui/docs/content/dictionary-value.md +11 -10
  62. package/src/ui/docs/content/dot.md +7 -7
  63. package/src/ui/docs/content/drawer.md +6 -3
  64. package/src/ui/docs/content/field.md +1 -1
  65. package/src/ui/docs/content/input-group.md +3 -2
  66. package/src/ui/docs/content/item.md +47 -21
  67. package/src/ui/docs/content/menu.md +5 -4
  68. package/src/ui/docs/content/metric-card.md +41 -0
  69. package/src/ui/docs/content/page-state.md +45 -0
  70. package/src/ui/docs/content/page.md +48 -10
  71. package/src/ui/docs/content/semantic-context.md +63 -0
  72. package/src/ui/docs/content/sidebar.md +4 -4
  73. package/src/ui/docs/content/skeleton.md +2 -2
  74. package/src/ui/docs/content/table.md +3 -3
  75. package/src/ui/docs/content/tokens.md +28 -0
  76. package/src/ui/docs/content/tooltip.md +15 -1
  77. package/src/ui/docs/doc-client.tsx +2 -2
  78. package/src/ui/docs/registry.tsx +596 -228
  79. package/src/ui/lib/semantic-context.ts +30 -0
  80. package/src/ui/meta.ts +292 -270
  81. package/src/ui/react.tsx +378 -111
  82. package/src/ui/theme.css +66 -0
@@ -1,38 +1,91 @@
1
1
  import * as React from 'react'
2
- import { cva, type VariantProps } from 'class-variance-authority'
2
+
3
+ import type { UiContext } from '../../../core/ui-context.ts'
3
4
  import { cn } from '../../lib/cn.ts'
4
5
 
5
- export const dotVariants = cva('inline-block size-1.5 shrink-0 rounded-full', {
6
- variants: {
7
- variant: {
8
- default: 'bg-primary',
9
- secondary: 'bg-muted-foreground/45',
10
- destructive: 'bg-destructive',
11
- outline: 'border border-border bg-transparent',
12
- success: 'bg-emerald-500',
13
- warning: 'bg-amber-500',
14
- info: 'bg-blue-500',
15
- },
16
- },
17
- defaultVariants: {
18
- variant: 'default',
19
- },
20
- })
21
-
22
- export interface DotProps extends React.ComponentProps<'span'>, VariantProps<typeof dotVariants> {
6
+ export type DotContext = UiContext
7
+ export type DotVariant = 'solid' | 'outline'
8
+ export type LegacyDotVariant =
9
+ | 'default'
10
+ | 'secondary'
11
+ | 'destructive'
12
+ | 'success'
13
+ | 'warning'
14
+ | 'info'
15
+
16
+ interface DotStyleOptions {
17
+ context?: DotContext | null
18
+ variant?: DotVariant | LegacyDotVariant | null
19
+ }
20
+
21
+ const solidClasses: Record<DotContext, string> = {
22
+ neutral: 'bg-context-neutral-emphasis',
23
+ primary: 'bg-context-primary',
24
+ info: 'bg-context-info',
25
+ success: 'bg-context-success',
26
+ warning: 'bg-context-warning',
27
+ danger: 'bg-context-danger',
28
+ }
29
+
30
+ const outlineClasses: Record<DotContext, string> = {
31
+ neutral: 'border border-context-neutral-border bg-transparent',
32
+ primary: 'border border-context-primary bg-transparent',
33
+ info: 'border border-context-info bg-transparent',
34
+ success: 'border border-context-success bg-transparent',
35
+ warning: 'border border-context-warning bg-transparent',
36
+ danger: 'border border-context-danger bg-transparent',
37
+ }
38
+
39
+ export function resolveDotStyle({ context, variant }: DotStyleOptions = {}): {
40
+ context: DotContext
41
+ variant: DotVariant
42
+ } {
43
+ const legacy =
44
+ variant === 'default' ||
45
+ variant === 'secondary' ||
46
+ variant === 'destructive' ||
47
+ variant === 'success' ||
48
+ variant === 'warning' ||
49
+ variant === 'info'
50
+ if (context != null && legacy) {
51
+ throw new Error('Dot não permite combinar context com uma variante semântica legada.')
52
+ }
53
+ if (variant === 'default') return { context: 'primary', variant: 'solid' }
54
+ if (variant === 'secondary') return { context: 'neutral', variant: 'solid' }
55
+ if (variant === 'destructive') return { context: 'danger', variant: 'solid' }
56
+ if (variant === 'success' || variant === 'warning' || variant === 'info') {
57
+ return { context: variant, variant: 'solid' }
58
+ }
59
+ return { context: context ?? 'neutral', variant: variant ?? 'solid' }
60
+ }
61
+
62
+ export function dotVariants(options: DotStyleOptions = {}): string {
63
+ const resolved = resolveDotStyle(options)
64
+ return cn(
65
+ 'inline-block size-1.5 shrink-0 rounded-full',
66
+ resolved.variant === 'outline'
67
+ ? outlineClasses[resolved.context]
68
+ : solidClasses[resolved.context],
69
+ )
70
+ }
71
+
72
+ export interface DotProps extends React.ComponentProps<'span'>, DotStyleOptions {
23
73
  /** Nome acessível quando a cor comunica estado; sem label, o ponto é decorativo. */
24
74
  label?: string
25
75
  }
26
76
 
27
77
  /** Indicador compacto de estado; use Badge quando o texto precisar permanecer visível. */
28
- export function Dot({ className, variant, label, ...props }: DotProps): React.ReactElement {
78
+ export function Dot({ className, context, variant, label, ...props }: DotProps): React.ReactElement {
79
+ const resolved = resolveDotStyle({ context, variant })
29
80
  return (
30
81
  <span
31
82
  data-slot="dot"
83
+ data-context={resolved.context}
84
+ data-variant={resolved.variant}
32
85
  role={label === undefined ? undefined : 'img'}
33
86
  aria-label={label}
34
87
  aria-hidden={label === undefined ? true : undefined}
35
- className={cn(dotVariants({ variant }), className)}
88
+ className={cn(dotVariants(resolved), className)}
36
89
  {...props}
37
90
  />
38
91
  )
@@ -1,29 +1,31 @@
1
- import * as React from "react"
2
- import { XIcon } from "lucide-react"
3
- import { Dialog as DrawerPrimitive } from "radix-ui"
1
+ import * as React from "react";
2
+ import { XIcon } from "lucide-react";
3
+ import { Dialog as DrawerPrimitive } from "radix-ui";
4
4
 
5
- import { cn } from '../../lib/cn.ts'
5
+ import { cn } from "../../lib/cn.ts";
6
6
 
7
- function Drawer({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Root>) {
8
- return <DrawerPrimitive.Root data-slot="drawer" {...props} />
7
+ function Drawer({
8
+ ...props
9
+ }: React.ComponentProps<typeof DrawerPrimitive.Root>) {
10
+ return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
9
11
  }
10
12
 
11
13
  function DrawerTrigger({
12
14
  ...props
13
15
  }: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
14
- return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
16
+ return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
15
17
  }
16
18
 
17
19
  function DrawerClose({
18
20
  ...props
19
21
  }: React.ComponentProps<typeof DrawerPrimitive.Close>) {
20
- return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
22
+ return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
21
23
  }
22
24
 
23
25
  function DrawerPortal({
24
26
  ...props
25
27
  }: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
26
- return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
28
+ return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
27
29
  }
28
30
 
29
31
  function DrawerOverlay({
@@ -35,11 +37,11 @@ function DrawerOverlay({
35
37
  data-slot="drawer-overlay"
36
38
  className={cn(
37
39
  "fixed inset-0 z-50 bg-black/30 backdrop-blur-xs data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
38
- className
40
+ className,
39
41
  )}
40
42
  {...props}
41
43
  />
42
- )
44
+ );
43
45
  }
44
46
 
45
47
  function DrawerContent({
@@ -49,8 +51,8 @@ function DrawerContent({
49
51
  showCloseButton = true,
50
52
  ...props
51
53
  }: React.ComponentProps<typeof DrawerPrimitive.Content> & {
52
- side?: "top" | "right" | "bottom" | "left"
53
- showCloseButton?: boolean
54
+ side?: "top" | "right" | "bottom" | "left";
55
+ showCloseButton?: boolean;
54
56
  }) {
55
57
  return (
56
58
  <DrawerPortal>
@@ -67,7 +69,7 @@ function DrawerContent({
67
69
  "inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
68
70
  side === "bottom" &&
69
71
  "inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
70
- className
72
+ className,
71
73
  )}
72
74
  {...props}
73
75
  >
@@ -80,7 +82,7 @@ function DrawerContent({
80
82
  )}
81
83
  </DrawerPrimitive.Content>
82
84
  </DrawerPortal>
83
- )
85
+ );
84
86
  }
85
87
 
86
88
  function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -90,7 +92,17 @@ function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
90
92
  className={cn("flex flex-col gap-1.5 p-4", className)}
91
93
  {...props}
92
94
  />
93
- )
95
+ );
96
+ }
97
+
98
+ function DrawerBody({ className, ...props }: React.ComponentProps<"div">) {
99
+ return (
100
+ <div
101
+ data-slot="drawer-body"
102
+ className={cn("min-h-0 flex-1 overflow-y-auto p-4", className)}
103
+ {...props}
104
+ />
105
+ );
94
106
  }
95
107
 
96
108
  function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -100,7 +112,7 @@ function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
100
112
  className={cn("mt-auto flex flex-col gap-2 p-4", className)}
101
113
  {...props}
102
114
  />
103
- )
115
+ );
104
116
  }
105
117
 
106
118
  function DrawerTitle({
@@ -113,7 +125,7 @@ function DrawerTitle({
113
125
  className={cn("font-semibold text-foreground", className)}
114
126
  {...props}
115
127
  />
116
- )
128
+ );
117
129
  }
118
130
 
119
131
  function DrawerDescription({
@@ -126,7 +138,7 @@ function DrawerDescription({
126
138
  className={cn("text-sm text-muted-foreground", className)}
127
139
  {...props}
128
140
  />
129
- )
141
+ );
130
142
  }
131
143
 
132
144
  export {
@@ -135,7 +147,8 @@ export {
135
147
  DrawerClose,
136
148
  DrawerContent,
137
149
  DrawerHeader,
150
+ DrawerBody,
138
151
  DrawerFooter,
139
152
  DrawerTitle,
140
153
  DrawerDescription,
141
- }
154
+ };
@@ -57,14 +57,14 @@ const fieldVariants = cva(
57
57
  {
58
58
  variants: {
59
59
  orientation: {
60
- vertical: ["flex-col gap-2 [&>*]:w-full [&>.sr-only]:w-auto"],
60
+ vertical: ["flex-col gap-1.5 [&>*]:w-full [&>.sr-only]:w-auto"],
61
61
  horizontal: [
62
62
  "flex-row items-center gap-3",
63
63
  "[&>[data-slot=field-label]]:flex-auto",
64
64
  "has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
65
65
  ],
66
66
  responsive: [
67
- "flex-col gap-2 @md/field-group:flex-row @md/field-group:items-center @md/field-group:gap-3 [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto",
67
+ "flex-col gap-1.5 @md/field-group:flex-row @md/field-group:items-center @md/field-group:gap-3 [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto",
68
68
  "@md/field-group:[&>[data-slot=field-label]]:flex-auto",
69
69
  "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
70
70
  ],
@@ -113,9 +113,9 @@ function FieldLabel({
113
113
  <Label
114
114
  data-slot="field-label"
115
115
  className={cn(
116
- "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
116
+ "group/field-label peer/field-label flex w-fit gap-2 text-xs leading-snug font-medium text-muted-foreground group-data-[invalid=true]/field:text-destructive group-data-[disabled=true]/field:opacity-50",
117
117
  "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
118
- "has-data-[state=checked]:border-primary has-data-[state=checked]:bg-primary/5 dark:has-data-[state=checked]:bg-primary/10",
118
+ "has-[>[data-slot=field]]:has-data-[state=checked]:border-primary has-[>[data-slot=field]]:has-data-[state=checked]:bg-primary/5 dark:has-[>[data-slot=field]]:has-data-[state=checked]:bg-primary/10",
119
119
  className
120
120
  )}
121
121
  {...props}
@@ -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,43 +101,41 @@ 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-stretch items-center justify-center gap-2 [&_svg]:pointer-events-none",
114
120
  {
115
121
  variants: {
116
122
  variant: {
117
123
  default: "bg-transparent",
118
- icon: "size-8 rounded-sm border bg-muted [&_svg:not([class*='size-'])]:size-4",
124
+ icon: "min-h-8 w-8 rounded-sm border bg-muted [&_svg:not([class*='size-'])]:size-4",
119
125
  image:
120
- "size-10 overflow-hidden rounded-sm [&_img]:size-full [&_img]:object-cover",
126
+ "min-h-10 w-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}