@ptengine/design-components 0.3.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 (197) hide show
  1. package/README.md +309 -0
  2. package/components.json +21 -0
  3. package/dist/components/ui/accordion.d.ts +8 -0
  4. package/dist/components/ui/accordion.d.ts.map +1 -0
  5. package/dist/components/ui/alert-dialog.d.ts +21 -0
  6. package/dist/components/ui/alert-dialog.d.ts.map +1 -0
  7. package/dist/components/ui/alert.d.ts +9 -0
  8. package/dist/components/ui/alert.d.ts.map +1 -0
  9. package/dist/components/ui/aspect-ratio.d.ts +4 -0
  10. package/dist/components/ui/aspect-ratio.d.ts.map +1 -0
  11. package/dist/components/ui/attachment.d.ts +26 -0
  12. package/dist/components/ui/attachment.d.ts.map +1 -0
  13. package/dist/components/ui/avatar.d.ts +10 -0
  14. package/dist/components/ui/avatar.d.ts.map +1 -0
  15. package/dist/components/ui/badge.d.ts +10 -0
  16. package/dist/components/ui/badge.d.ts.map +1 -0
  17. package/dist/components/ui/breadcrumb.d.ts +22 -0
  18. package/dist/components/ui/breadcrumb.d.ts.map +1 -0
  19. package/dist/components/ui/bubble.d.ts +19 -0
  20. package/dist/components/ui/bubble.d.ts.map +1 -0
  21. package/dist/components/ui/button-group.d.ts +13 -0
  22. package/dist/components/ui/button-group.d.ts.map +1 -0
  23. package/dist/components/ui/button.d.ts +13 -0
  24. package/dist/components/ui/button.d.ts.map +1 -0
  25. package/dist/components/ui/calendar.d.ts +9 -0
  26. package/dist/components/ui/calendar.d.ts.map +1 -0
  27. package/dist/components/ui/card.d.ts +9 -0
  28. package/dist/components/ui/card.d.ts.map +1 -0
  29. package/dist/components/ui/carousel.d.ts +23 -0
  30. package/dist/components/ui/carousel.d.ts.map +1 -0
  31. package/dist/components/ui/chart.d.ts +63 -0
  32. package/dist/components/ui/chart.d.ts.map +1 -0
  33. package/dist/components/ui/checkbox.d.ts +5 -0
  34. package/dist/components/ui/checkbox.d.ts.map +1 -0
  35. package/dist/components/ui/collapsible.d.ts +6 -0
  36. package/dist/components/ui/collapsible.d.ts.map +1 -0
  37. package/dist/components/ui/command.d.ts +81 -0
  38. package/dist/components/ui/command.d.ts.map +1 -0
  39. package/dist/components/ui/context-menu.d.ts +28 -0
  40. package/dist/components/ui/context-menu.d.ts.map +1 -0
  41. package/dist/components/ui/dialog.d.ts +22 -0
  42. package/dist/components/ui/dialog.d.ts.map +1 -0
  43. package/dist/components/ui/direction.d.ts +8 -0
  44. package/dist/components/ui/direction.d.ts.map +1 -0
  45. package/dist/components/ui/drawer.d.ts +23 -0
  46. package/dist/components/ui/drawer.d.ts.map +1 -0
  47. package/dist/components/ui/dropdown-menu.d.ts +28 -0
  48. package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
  49. package/dist/components/ui/empty.d.ts +13 -0
  50. package/dist/components/ui/empty.d.ts.map +1 -0
  51. package/dist/components/ui/field.d.ts +25 -0
  52. package/dist/components/ui/field.d.ts.map +1 -0
  53. package/dist/components/ui/hover-card.d.ts +7 -0
  54. package/dist/components/ui/hover-card.d.ts.map +1 -0
  55. package/dist/components/ui/input-group.d.ts +13 -0
  56. package/dist/components/ui/input-group.d.ts.map +1 -0
  57. package/dist/components/ui/input-otp.d.ts +35 -0
  58. package/dist/components/ui/input-otp.d.ts.map +1 -0
  59. package/dist/components/ui/input.d.ts +5 -0
  60. package/dist/components/ui/input.d.ts.map +1 -0
  61. package/dist/components/ui/item.d.ts +24 -0
  62. package/dist/components/ui/item.d.ts.map +1 -0
  63. package/dist/components/ui/kbd.d.ts +5 -0
  64. package/dist/components/ui/kbd.d.ts.map +1 -0
  65. package/dist/components/ui/label.d.ts +6 -0
  66. package/dist/components/ui/label.d.ts.map +1 -0
  67. package/dist/components/ui/marker.d.ts +13 -0
  68. package/dist/components/ui/marker.d.ts.map +1 -0
  69. package/dist/components/ui/menubar.d.ts +29 -0
  70. package/dist/components/ui/menubar.d.ts.map +1 -0
  71. package/dist/components/ui/message-scroller.d.ts +26 -0
  72. package/dist/components/ui/message-scroller.d.ts.map +1 -0
  73. package/dist/components/ui/message.d.ts +11 -0
  74. package/dist/components/ui/message.d.ts.map +1 -0
  75. package/dist/components/ui/native-select.d.ts +4 -0
  76. package/dist/components/ui/native-select.d.ts.map +1 -0
  77. package/dist/components/ui/navigation-menu.d.ts +13 -0
  78. package/dist/components/ui/navigation-menu.d.ts.map +1 -0
  79. package/dist/components/ui/pagination.d.ts +35 -0
  80. package/dist/components/ui/pagination.d.ts.map +1 -0
  81. package/dist/components/ui/popover.d.ts +8 -0
  82. package/dist/components/ui/popover.d.ts.map +1 -0
  83. package/dist/components/ui/progress.d.ts +5 -0
  84. package/dist/components/ui/progress.d.ts.map +1 -0
  85. package/dist/components/ui/radio-group.d.ts +6 -0
  86. package/dist/components/ui/radio-group.d.ts.map +1 -0
  87. package/dist/components/ui/resizable.d.ts +24 -0
  88. package/dist/components/ui/resizable.d.ts.map +1 -0
  89. package/dist/components/ui/scroll-area.d.ts +6 -0
  90. package/dist/components/ui/scroll-area.d.ts.map +1 -0
  91. package/dist/components/ui/search-input.d.ts +23 -0
  92. package/dist/components/ui/search-input.d.ts.map +1 -0
  93. package/dist/components/ui/select.d.ts +14 -0
  94. package/dist/components/ui/select.d.ts.map +1 -0
  95. package/dist/components/ui/separator.d.ts +5 -0
  96. package/dist/components/ui/separator.d.ts.map +1 -0
  97. package/dist/components/ui/sheet.d.ts +28 -0
  98. package/dist/components/ui/sheet.d.ts.map +1 -0
  99. package/dist/components/ui/sidebar.d.ts +70 -0
  100. package/dist/components/ui/sidebar.d.ts.map +1 -0
  101. package/dist/components/ui/skeleton.d.ts +4 -0
  102. package/dist/components/ui/skeleton.d.ts.map +1 -0
  103. package/dist/components/ui/slider.d.ts +5 -0
  104. package/dist/components/ui/slider.d.ts.map +1 -0
  105. package/dist/components/ui/sonner.d.ts +6 -0
  106. package/dist/components/ui/sonner.d.ts.map +1 -0
  107. package/dist/components/ui/spinner.d.ts +7 -0
  108. package/dist/components/ui/spinner.d.ts.map +1 -0
  109. package/dist/components/ui/switch.d.ts +5 -0
  110. package/dist/components/ui/switch.d.ts.map +1 -0
  111. package/dist/components/ui/table.d.ts +11 -0
  112. package/dist/components/ui/table.d.ts.map +1 -0
  113. package/dist/components/ui/tabs.d.ts +13 -0
  114. package/dist/components/ui/tabs.d.ts.map +1 -0
  115. package/dist/components/ui/textarea.d.ts +5 -0
  116. package/dist/components/ui/textarea.d.ts.map +1 -0
  117. package/dist/components/ui/toggle-group.d.ts +13 -0
  118. package/dist/components/ui/toggle-group.d.ts.map +1 -0
  119. package/dist/components/ui/toggle.d.ts +13 -0
  120. package/dist/components/ui/toggle.d.ts.map +1 -0
  121. package/dist/components/ui/tooltip.d.ts +8 -0
  122. package/dist/components/ui/tooltip.d.ts.map +1 -0
  123. package/dist/hooks/use-mobile.d.ts +2 -0
  124. package/dist/hooks/use-mobile.d.ts.map +1 -0
  125. package/dist/index.cjs +82 -0
  126. package/dist/index.d.ts +71 -0
  127. package/dist/index.d.ts.map +1 -0
  128. package/dist/index.js +20969 -0
  129. package/dist/lib/utils.d.ts +4 -0
  130. package/dist/lib/utils.d.ts.map +1 -0
  131. package/llms.txt +173 -0
  132. package/package.json +125 -0
  133. package/src/components/ui/accordion.tsx +51 -0
  134. package/src/components/ui/alert-dialog.tsx +115 -0
  135. package/src/components/ui/alert.tsx +48 -0
  136. package/src/components/ui/aspect-ratio.tsx +5 -0
  137. package/src/components/ui/attachment.tsx +186 -0
  138. package/src/components/ui/avatar.tsx +57 -0
  139. package/src/components/ui/badge.tsx +40 -0
  140. package/src/components/ui/breadcrumb.tsx +101 -0
  141. package/src/components/ui/bubble.tsx +123 -0
  142. package/src/components/ui/button-group.tsx +74 -0
  143. package/src/components/ui/button.tsx +64 -0
  144. package/src/components/ui/calendar.tsx +63 -0
  145. package/src/components/ui/card.tsx +55 -0
  146. package/src/components/ui/carousel.tsx +252 -0
  147. package/src/components/ui/chart.tsx +323 -0
  148. package/src/components/ui/checkbox.tsx +26 -0
  149. package/src/components/ui/collapsible.tsx +9 -0
  150. package/src/components/ui/command.tsx +136 -0
  151. package/src/components/ui/context-menu.tsx +187 -0
  152. package/src/components/ui/dialog.tsx +103 -0
  153. package/src/components/ui/direction.tsx +15 -0
  154. package/src/components/ui/drawer.tsx +98 -0
  155. package/src/components/ui/dropdown-menu.tsx +186 -0
  156. package/src/components/ui/empty.tsx +75 -0
  157. package/src/components/ui/field.tsx +175 -0
  158. package/src/components/ui/hover-card.tsx +27 -0
  159. package/src/components/ui/input-group.tsx +80 -0
  160. package/src/components/ui/input-otp.tsx +67 -0
  161. package/src/components/ui/input.tsx +24 -0
  162. package/src/components/ui/item.tsx +118 -0
  163. package/src/components/ui/kbd.tsx +21 -0
  164. package/src/components/ui/label.tsx +19 -0
  165. package/src/components/ui/marker.tsx +67 -0
  166. package/src/components/ui/menubar.tsx +218 -0
  167. package/src/components/ui/message-scroller.tsx +206 -0
  168. package/src/components/ui/message.tsx +85 -0
  169. package/src/components/ui/native-select.tsx +29 -0
  170. package/src/components/ui/navigation-menu.tsx +120 -0
  171. package/src/components/ui/pagination.tsx +108 -0
  172. package/src/components/ui/popover.tsx +31 -0
  173. package/src/components/ui/progress.tsx +23 -0
  174. package/src/components/ui/radio-group.tsx +36 -0
  175. package/src/components/ui/resizable.tsx +40 -0
  176. package/src/components/ui/scroll-area.tsx +44 -0
  177. package/src/components/ui/search-input.tsx +97 -0
  178. package/src/components/ui/select.tsx +151 -0
  179. package/src/components/ui/separator.tsx +24 -0
  180. package/src/components/ui/sheet.tsx +124 -0
  181. package/src/components/ui/sidebar.tsx +704 -0
  182. package/src/components/ui/skeleton.tsx +9 -0
  183. package/src/components/ui/slider.tsx +41 -0
  184. package/src/components/ui/sonner.tsx +45 -0
  185. package/src/components/ui/spinner.tsx +21 -0
  186. package/src/components/ui/switch.tsx +27 -0
  187. package/src/components/ui/table.tsx +95 -0
  188. package/src/components/ui/tabs.tsx +119 -0
  189. package/src/components/ui/textarea.tsx +23 -0
  190. package/src/components/ui/toggle-group.tsx +55 -0
  191. package/src/components/ui/toggle.tsx +40 -0
  192. package/src/components/ui/tooltip.tsx +30 -0
  193. package/src/hooks/use-mobile.ts +19 -0
  194. package/src/index.ts +414 -0
  195. package/src/lib/utils.ts +7 -0
  196. package/src/styles/tokens.css +360 -0
  197. package/tailwind.preset.js +235 -0
@@ -0,0 +1,118 @@
1
+ import * as React from 'react';
2
+ import { Slot } from '@radix-ui/react-slot';
3
+ import { cva, type VariantProps } from 'class-variance-authority';
4
+
5
+ import { cn } from '../../lib/utils';
6
+ import { Separator } from './separator';
7
+
8
+ function ItemGroup({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
9
+ return <div role="list" className={cn('group/item-group flex flex-col', className)} {...props} />;
10
+ }
11
+
12
+ function ItemSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {
13
+ return <Separator orientation="horizontal" className={cn('my-0', className)} {...props} />;
14
+ }
15
+
16
+ const itemVariants = cva(
17
+ 'group/item flex flex-wrap items-center rounded-[var(--pt-radius-md)] border text-[14px] leading-[20px] text-foreground outline-none transition-colors',
18
+ {
19
+ variants: {
20
+ variant: {
21
+ default: 'border-transparent',
22
+ outline: 'border-border',
23
+ muted: 'border-transparent bg-muted'
24
+ },
25
+ size: {
26
+ default: 'gap-[16px] p-[16px]',
27
+ sm: 'gap-[10px] px-[16px] py-[12px]'
28
+ }
29
+ },
30
+ defaultVariants: {
31
+ variant: 'default',
32
+ size: 'default'
33
+ }
34
+ }
35
+ );
36
+
37
+ function Item({
38
+ className,
39
+ variant,
40
+ size,
41
+ asChild = false,
42
+ ...props
43
+ }: React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof itemVariants> & { asChild?: boolean }) {
44
+ const Comp = asChild ? Slot : 'div';
45
+ return <Comp className={cn(itemVariants({ variant, size }), className)} {...props} />;
46
+ }
47
+
48
+ const itemMediaVariants = cva('flex shrink-0 items-center justify-center gap-[8px] [&_svg]:pointer-events-none', {
49
+ variants: {
50
+ variant: {
51
+ default: 'bg-transparent [&_svg]:size-[20px] [&_svg]:text-muted-foreground',
52
+ icon: 'size-[32px] rounded-[var(--pt-radius-sm)] border bg-secondary [&_svg]:size-[16px] [&_svg]:text-foreground',
53
+ image: 'size-[40px] overflow-hidden rounded-[var(--pt-radius-sm)] [&_img]:size-full [&_img]:object-cover'
54
+ }
55
+ },
56
+ defaultVariants: {
57
+ variant: 'default'
58
+ }
59
+ });
60
+
61
+ function ItemMedia({
62
+ className,
63
+ variant,
64
+ ...props
65
+ }: React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof itemMediaVariants>) {
66
+ return <div className={cn(itemMediaVariants({ variant }), className)} {...props} />;
67
+ }
68
+
69
+ function ItemContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
70
+ return <div className={cn('flex flex-1 flex-col gap-[4px]', className)} {...props} />;
71
+ }
72
+
73
+ function ItemTitle({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
74
+ return (
75
+ <div
76
+ className={cn('flex w-fit items-center gap-[8px] text-[14px] leading-[20px] font-medium leading-snug', className)}
77
+ {...props}
78
+ />
79
+ );
80
+ }
81
+
82
+ function ItemDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
83
+ return (
84
+ <p
85
+ className={cn('line-clamp-2 text-[14px] leading-[20px] leading-normal text-muted-foreground', className)}
86
+ {...props}
87
+ />
88
+ );
89
+ }
90
+
91
+ function ItemActions({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
92
+ return <div className={cn('flex items-center gap-[8px]', className)} {...props} />;
93
+ }
94
+
95
+ function ItemHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
96
+ return (
97
+ <div className={cn('flex basis-full items-center justify-between gap-[8px]', className)} {...props} />
98
+ );
99
+ }
100
+
101
+ function ItemFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
102
+ return (
103
+ <div className={cn('flex basis-full items-center justify-between gap-[8px]', className)} {...props} />
104
+ );
105
+ }
106
+
107
+ export {
108
+ Item,
109
+ ItemActions,
110
+ ItemContent,
111
+ ItemDescription,
112
+ ItemFooter,
113
+ ItemGroup,
114
+ ItemHeader,
115
+ ItemMedia,
116
+ ItemSeparator,
117
+ ItemTitle
118
+ };
@@ -0,0 +1,21 @@
1
+ import * as React from 'react';
2
+
3
+ import { cn } from '../../lib/utils';
4
+
5
+ function Kbd({ className, ...props }: React.HTMLAttributes<HTMLElement>) {
6
+ return (
7
+ <kbd
8
+ className={cn(
9
+ 'pointer-events-none inline-flex h-[20px] w-fit min-w-[20px] select-none items-center justify-center gap-[4px] rounded-[var(--pt-radius-sm)] bg-muted px-[4px] font-sans text-[12px] leading-[16px] font-medium text-muted-foreground [&_svg]:size-[12px]',
10
+ className
11
+ )}
12
+ {...props}
13
+ />
14
+ );
15
+ }
16
+
17
+ function KbdGroup({ className, ...props }: React.HTMLAttributes<HTMLElement>) {
18
+ return <span className={cn('inline-flex items-center gap-[4px]', className)} {...props} />;
19
+ }
20
+
21
+ export { Kbd, KbdGroup };
@@ -0,0 +1,19 @@
1
+ import * as React from 'react';
2
+ import * as LabelPrimitive from '@radix-ui/react-label';
3
+ import { cva, type VariantProps } from 'class-variance-authority';
4
+
5
+ import { cn } from '../../lib/utils';
6
+
7
+ const labelVariants = cva(
8
+ 'text-[14px] leading-[20px] font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50'
9
+ );
10
+
11
+ const Label = React.forwardRef<
12
+ React.ElementRef<typeof LabelPrimitive.Root>,
13
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
14
+ >(({ className, ...props }, ref) => (
15
+ <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
16
+ ));
17
+ Label.displayName = LabelPrimitive.Root.displayName;
18
+
19
+ export { Label };
@@ -0,0 +1,67 @@
1
+ import * as React from 'react';
2
+ import { Slot } from '@radix-ui/react-slot';
3
+ import { cva, type VariantProps } from 'class-variance-authority';
4
+
5
+ import { cn } from '../../lib/utils';
6
+
7
+ const markerVariants = cva(
8
+ "group/marker relative flex min-h-[16px] w-full items-center gap-[8px] text-left text-[14px] leading-[20px] text-muted-foreground [&_svg:not([class*='size-'])]:size-[16px] [&:is(a)]:underline [&:is(a)]:underline-offset-4 [&:is(a):hover]:text-foreground",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: '',
13
+ separator:
14
+ 'before:mr-[4px] before:h-px before:min-w-0 before:flex-1 before:bg-border after:ml-[4px] after:h-px after:min-w-0 after:flex-1 after:bg-border',
15
+ border: 'border-b border-border pb-[8px]'
16
+ }
17
+ }
18
+ }
19
+ );
20
+
21
+ function Marker({
22
+ className,
23
+ variant = 'default',
24
+ asChild = false,
25
+ ...props
26
+ }: React.HTMLAttributes<HTMLDivElement> &
27
+ VariantProps<typeof markerVariants> & {
28
+ /** Render the child element as the root (e.g. an <a>), keeping Marker styles. */
29
+ asChild?: boolean;
30
+ }) {
31
+ const Comp = asChild ? Slot : 'div';
32
+
33
+ return (
34
+ <Comp
35
+ data-slot="marker"
36
+ data-variant={variant}
37
+ className={cn(markerVariants({ variant, className }))}
38
+ {...props}
39
+ />
40
+ );
41
+ }
42
+
43
+ function MarkerIcon({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) {
44
+ return (
45
+ <span
46
+ data-slot="marker-icon"
47
+ aria-hidden="true"
48
+ className={cn("size-[16px] shrink-0 [&_svg:not([class*='size-'])]:size-[16px]", className)}
49
+ {...props}
50
+ />
51
+ );
52
+ }
53
+
54
+ function MarkerContent({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) {
55
+ return (
56
+ <span
57
+ data-slot="marker-content"
58
+ className={cn(
59
+ 'min-w-0 break-words group-data-[variant=separator]/marker:flex-none group-data-[variant=separator]/marker:text-center [&_a]:underline [&_a]:underline-offset-4 [&_a:hover]:text-foreground',
60
+ className
61
+ )}
62
+ {...props}
63
+ />
64
+ );
65
+ }
66
+
67
+ export { Marker, MarkerIcon, MarkerContent, markerVariants };
@@ -0,0 +1,218 @@
1
+ import * as React from 'react';
2
+ import * as MenubarPrimitive from '@radix-ui/react-menubar';
3
+ import { Check, ChevronRight, Circle } from 'lucide-react';
4
+
5
+ import { cn } from '../../lib/utils';
6
+
7
+ function MenubarMenu(props: React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Menu>) {
8
+ return <MenubarPrimitive.Menu {...props} />;
9
+ }
10
+
11
+ const MenubarGroup = MenubarPrimitive.Group;
12
+
13
+ const MenubarPortal = MenubarPrimitive.Portal;
14
+
15
+ const MenubarSub = MenubarPrimitive.Sub;
16
+
17
+ const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
18
+
19
+ const Menubar = React.forwardRef<
20
+ React.ElementRef<typeof MenubarPrimitive.Root>,
21
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
22
+ >(({ className, ...props }, ref) => (
23
+ <MenubarPrimitive.Root
24
+ ref={ref}
25
+ className={cn('flex h-[32px] items-center gap-[4px] rounded-[var(--pt-radius-md)] border bg-background p-[4px]', className)}
26
+ {...props}
27
+ />
28
+ ));
29
+ Menubar.displayName = MenubarPrimitive.Root.displayName;
30
+
31
+ const MenubarTrigger = React.forwardRef<
32
+ React.ElementRef<typeof MenubarPrimitive.Trigger>,
33
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
34
+ >(({ className, ...props }, ref) => (
35
+ <MenubarPrimitive.Trigger
36
+ ref={ref}
37
+ className={cn(
38
+ 'flex cursor-default select-none items-center rounded-[var(--pt-radius-sm)] px-[12px] py-[4px] text-[14px] leading-[20px] font-medium text-foreground outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
39
+ className
40
+ )}
41
+ {...props}
42
+ />
43
+ ));
44
+ MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
45
+
46
+ const MenubarSubTrigger = React.forwardRef<
47
+ React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
48
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
49
+ inset?: boolean;
50
+ }
51
+ >(({ className, inset, children, ...props }, ref) => (
52
+ <MenubarPrimitive.SubTrigger
53
+ ref={ref}
54
+ className={cn(
55
+ 'flex cursor-default select-none items-center rounded-[var(--pt-radius-sm)] px-[8px] py-[6px] text-[14px] leading-[20px] outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
56
+ inset && 'pl-[32px]',
57
+ className
58
+ )}
59
+ {...props}
60
+ >
61
+ {children}
62
+ <ChevronRight className="ml-auto h-[16px] w-[16px]" />
63
+ </MenubarPrimitive.SubTrigger>
64
+ ));
65
+ MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
66
+
67
+ const MenubarSubContent = React.forwardRef<
68
+ React.ElementRef<typeof MenubarPrimitive.SubContent>,
69
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
70
+ >(({ className, ...props }, ref) => (
71
+ <MenubarPrimitive.SubContent
72
+ ref={ref}
73
+ className={cn(
74
+ 'z-[var(--pt-z-popup,50)] min-w-[8rem] overflow-hidden rounded-[var(--pt-radius-md)] border bg-popover p-[4px] text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
75
+ className
76
+ )}
77
+ {...props}
78
+ />
79
+ ));
80
+ MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
81
+
82
+ const MenubarContent = React.forwardRef<
83
+ React.ElementRef<typeof MenubarPrimitive.Content>,
84
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
85
+ >(({ className, align = 'start', alignOffset = -4, sideOffset = 8, ...props }, ref) => (
86
+ <MenubarPrimitive.Portal>
87
+ <MenubarPrimitive.Content
88
+ ref={ref}
89
+ align={align}
90
+ alignOffset={alignOffset}
91
+ sideOffset={sideOffset}
92
+ className={cn(
93
+ 'z-[var(--pt-z-popup,50)] min-w-[12rem] overflow-hidden rounded-[var(--pt-radius-md)] border bg-popover p-[4px] text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
94
+ className
95
+ )}
96
+ {...props}
97
+ />
98
+ </MenubarPrimitive.Portal>
99
+ ));
100
+ MenubarContent.displayName = MenubarPrimitive.Content.displayName;
101
+
102
+ const MenubarItem = React.forwardRef<
103
+ React.ElementRef<typeof MenubarPrimitive.Item>,
104
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
105
+ inset?: boolean;
106
+ }
107
+ >(({ className, inset, ...props }, ref) => (
108
+ <MenubarPrimitive.Item
109
+ ref={ref}
110
+ className={cn(
111
+ 'relative flex cursor-default select-none items-center rounded-[var(--pt-radius-sm)] px-[8px] py-[6px] text-[14px] leading-[20px] outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
112
+ inset && 'pl-[32px]',
113
+ className
114
+ )}
115
+ {...props}
116
+ />
117
+ ));
118
+ MenubarItem.displayName = MenubarPrimitive.Item.displayName;
119
+
120
+ const MenubarCheckboxItem = React.forwardRef<
121
+ React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
122
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
123
+ >(({ className, children, checked, ...props }, ref) => (
124
+ <MenubarPrimitive.CheckboxItem
125
+ ref={ref}
126
+ className={cn(
127
+ 'relative flex cursor-default select-none items-center rounded-[var(--pt-radius-sm)] py-[6px] pl-[32px] pr-[8px] text-[14px] leading-[20px] outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
128
+ className
129
+ )}
130
+ checked={checked}
131
+ {...props}
132
+ >
133
+ <span className="absolute left-[8px] flex h-[14px] w-[14px] items-center justify-center">
134
+ <MenubarPrimitive.ItemIndicator>
135
+ <Check className="h-[16px] w-[16px]" />
136
+ </MenubarPrimitive.ItemIndicator>
137
+ </span>
138
+ {children}
139
+ </MenubarPrimitive.CheckboxItem>
140
+ ));
141
+ MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
142
+
143
+ const MenubarRadioItem = React.forwardRef<
144
+ React.ElementRef<typeof MenubarPrimitive.RadioItem>,
145
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
146
+ >(({ className, children, ...props }, ref) => (
147
+ <MenubarPrimitive.RadioItem
148
+ ref={ref}
149
+ className={cn(
150
+ 'relative flex cursor-default select-none items-center rounded-[var(--pt-radius-sm)] py-[6px] pl-[32px] pr-[8px] text-[14px] leading-[20px] outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
151
+ className
152
+ )}
153
+ {...props}
154
+ >
155
+ <span className="absolute left-[8px] flex h-[14px] w-[14px] items-center justify-center">
156
+ <MenubarPrimitive.ItemIndicator>
157
+ <Circle className="h-[8px] w-[8px] fill-current" />
158
+ </MenubarPrimitive.ItemIndicator>
159
+ </span>
160
+ {children}
161
+ </MenubarPrimitive.RadioItem>
162
+ ));
163
+ MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
164
+
165
+ const MenubarLabel = React.forwardRef<
166
+ React.ElementRef<typeof MenubarPrimitive.Label>,
167
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
168
+ inset?: boolean;
169
+ }
170
+ >(({ className, inset, ...props }, ref) => (
171
+ <MenubarPrimitive.Label
172
+ ref={ref}
173
+ className={cn('px-[8px] py-[6px] text-[14px] leading-[20px] font-semibold text-foreground', inset && 'pl-[32px]', className)}
174
+ {...props}
175
+ />
176
+ ));
177
+ MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
178
+
179
+ const MenubarSeparator = React.forwardRef<
180
+ React.ElementRef<typeof MenubarPrimitive.Separator>,
181
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
182
+ >(({ className, ...props }, ref) => (
183
+ <MenubarPrimitive.Separator
184
+ ref={ref}
185
+ className={cn('-mx-[4px] my-[4px] h-px bg-muted', className)}
186
+ {...props}
187
+ />
188
+ ));
189
+ MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
190
+
191
+ const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
192
+ return (
193
+ <span
194
+ className={cn('ml-auto text-[13px] leading-[20px] tracking-widest text-muted-foreground', className)}
195
+ {...props}
196
+ />
197
+ );
198
+ };
199
+ MenubarShortcut.displayName = 'MenubarShortcut';
200
+
201
+ export {
202
+ Menubar,
203
+ MenubarMenu,
204
+ MenubarTrigger,
205
+ MenubarContent,
206
+ MenubarItem,
207
+ MenubarSeparator,
208
+ MenubarLabel,
209
+ MenubarCheckboxItem,
210
+ MenubarRadioGroup,
211
+ MenubarRadioItem,
212
+ MenubarPortal,
213
+ MenubarSubContent,
214
+ MenubarSubTrigger,
215
+ MenubarGroup,
216
+ MenubarSub,
217
+ MenubarShortcut
218
+ };
@@ -0,0 +1,206 @@
1
+ import * as React from 'react';
2
+ import { ArrowDown } from 'lucide-react';
3
+
4
+ import { cn } from '../../lib/utils';
5
+ import { Button } from './button';
6
+
7
+ const EDGE_THRESHOLD = 32;
8
+
9
+ type MessageScrollerContextValue = {
10
+ viewportRef: React.RefObject<HTMLDivElement | null>;
11
+ atStart: boolean;
12
+ atEnd: boolean;
13
+ scrollToEdge: (edge: 'start' | 'end', behavior?: ScrollBehavior) => void;
14
+ measure: () => void;
15
+ stickToEndRef: React.MutableRefObject<boolean>;
16
+ };
17
+
18
+ const MessageScrollerContext = React.createContext<MessageScrollerContextValue | null>(null);
19
+
20
+ function useMessageScroller() {
21
+ const ctx = React.useContext(MessageScrollerContext);
22
+ if (!ctx) {
23
+ throw new Error(
24
+ 'useMessageScroller must be used within <MessageScrollerProvider /> or <MessageScroller />'
25
+ );
26
+ }
27
+ return ctx;
28
+ }
29
+
30
+ function useMessageScrollerState(): MessageScrollerContextValue {
31
+ const viewportRef = React.useRef<HTMLDivElement | null>(null);
32
+ const stickToEndRef = React.useRef(true);
33
+ const [atStart, setAtStart] = React.useState(true);
34
+ const [atEnd, setAtEnd] = React.useState(true);
35
+
36
+ const measure = React.useCallback(() => {
37
+ const el = viewportRef.current;
38
+ if (!el) return;
39
+ const end = el.scrollHeight - el.scrollTop - el.clientHeight <= EDGE_THRESHOLD;
40
+ stickToEndRef.current = end;
41
+ setAtEnd(end);
42
+ setAtStart(el.scrollTop <= EDGE_THRESHOLD);
43
+ }, []);
44
+
45
+ const scrollToEdge = React.useCallback(
46
+ (edge: 'start' | 'end', behavior: ScrollBehavior = 'smooth') => {
47
+ const el = viewportRef.current;
48
+ if (!el) return;
49
+ el.scrollTo({ top: edge === 'end' ? el.scrollHeight : 0, behavior });
50
+ },
51
+ []
52
+ );
53
+
54
+ return React.useMemo(
55
+ () => ({ viewportRef, atStart, atEnd, scrollToEdge, measure, stickToEndRef }),
56
+ [atStart, atEnd, scrollToEdge, measure]
57
+ );
58
+ }
59
+
60
+ /** 可选:把滚动状态提升到 MessageScroller 之外(如让按钮/输入框读取 atEnd)。 */
61
+ function MessageScrollerProvider({ children }: { children: React.ReactNode }) {
62
+ const value = useMessageScrollerState();
63
+ return (
64
+ <MessageScrollerContext.Provider value={value}>{children}</MessageScrollerContext.Provider>
65
+ );
66
+ }
67
+
68
+ function MessageScroller({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
69
+ const outer = React.useContext(MessageScrollerContext);
70
+ const own = useMessageScrollerState();
71
+ return (
72
+ <MessageScrollerContext.Provider value={outer ?? own}>
73
+ <div
74
+ data-slot="message-scroller"
75
+ className={cn(
76
+ 'group/message-scroller relative flex size-full min-h-0 flex-col overflow-hidden',
77
+ className
78
+ )}
79
+ {...props}
80
+ />
81
+ </MessageScrollerContext.Provider>
82
+ );
83
+ }
84
+
85
+ function MessageScrollerViewport({
86
+ className,
87
+ onScroll,
88
+ children,
89
+ ...props
90
+ }: React.HTMLAttributes<HTMLDivElement>) {
91
+ const { viewportRef, measure, stickToEndRef, scrollToEdge } = useMessageScroller();
92
+
93
+ React.useLayoutEffect(() => {
94
+ scrollToEdge('end', 'auto');
95
+ measure();
96
+ }, [scrollToEdge, measure]);
97
+
98
+ React.useEffect(() => {
99
+ const el = viewportRef.current;
100
+ const content = el?.firstElementChild;
101
+ if (!el || !content) return;
102
+ const ro = new ResizeObserver(() => {
103
+ if (stickToEndRef.current) el.scrollTop = el.scrollHeight;
104
+ measure();
105
+ });
106
+ ro.observe(content);
107
+ return () => ro.disconnect();
108
+ }, [viewportRef, stickToEndRef, measure]);
109
+
110
+ return (
111
+ <div
112
+ ref={viewportRef}
113
+ data-slot="message-scroller-viewport"
114
+ className={cn(
115
+ 'size-full min-h-0 min-w-0 overflow-y-auto overscroll-contain [contain:content]',
116
+ className
117
+ )}
118
+ onScroll={e => {
119
+ measure();
120
+ onScroll?.(e);
121
+ }}
122
+ {...props}
123
+ >
124
+ {children}
125
+ </div>
126
+ );
127
+ }
128
+
129
+ function MessageScrollerContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
130
+ return (
131
+ <div
132
+ data-slot="message-scroller-content"
133
+ className={cn('flex h-max min-h-full flex-col gap-[32px]', className)}
134
+ {...props}
135
+ />
136
+ );
137
+ }
138
+
139
+ function MessageScrollerItem({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
140
+ return (
141
+ <div
142
+ data-slot="message-scroller-item"
143
+ className={cn(
144
+ 'min-w-0 shrink-0 [contain-intrinsic-size:auto_10rem] [content-visibility:auto]',
145
+ className
146
+ )}
147
+ {...props}
148
+ />
149
+ );
150
+ }
151
+
152
+ function MessageScrollerButton({
153
+ direction = 'end',
154
+ className,
155
+ children,
156
+ label,
157
+ variant = 'secondary',
158
+ size = 'icon',
159
+ onClick,
160
+ ...props
161
+ }: React.ComponentProps<typeof Button> & {
162
+ direction?: 'start' | 'end';
163
+ /** 读屏文案,库不做 i18n,消费方传译文 */
164
+ label?: string;
165
+ }) {
166
+ const { atStart, atEnd, scrollToEdge } = useMessageScroller();
167
+ const active = direction === 'end' ? !atEnd : !atStart;
168
+
169
+ return (
170
+ <Button
171
+ data-slot="message-scroller-button"
172
+ data-direction={direction}
173
+ data-active={active}
174
+ variant={variant}
175
+ size={size}
176
+ className={cn(
177
+ 'absolute left-1/2 z-10 h-[32px] w-[32px] -translate-x-1/2 rounded-full transition-[opacity,transform] duration-200 data-[direction=end]:bottom-[16px] data-[direction=start]:top-[16px] data-[active=false]:pointer-events-none data-[active=false]:opacity-0 data-[active=true]:opacity-100 data-[direction=start]:[&_svg]:rotate-180',
178
+ className
179
+ )}
180
+ onClick={e => {
181
+ onClick?.(e);
182
+ if (!e.defaultPrevented) scrollToEdge(direction);
183
+ }}
184
+ {...props}
185
+ >
186
+ {children ?? (
187
+ <>
188
+ <ArrowDown />
189
+ <span className="sr-only">
190
+ {label ?? (direction === 'end' ? 'Scroll to end' : 'Scroll to start')}
191
+ </span>
192
+ </>
193
+ )}
194
+ </Button>
195
+ );
196
+ }
197
+
198
+ export {
199
+ MessageScrollerProvider,
200
+ MessageScroller,
201
+ MessageScrollerViewport,
202
+ MessageScrollerContent,
203
+ MessageScrollerItem,
204
+ MessageScrollerButton,
205
+ useMessageScroller
206
+ };